mirror of https://github.com/k3s-io/k3s
Merge pull request #5903 from smarterclayton/support_resources_by_type_and_name
Allow resource.Builder commands to take arguments by type/namepull/6/head
commit
1e496696ca
|
@ -8,13 +8,13 @@ Display one or many resources
|
|||
Display one or many resources.
|
||||
|
||||
Possible resources include pods (po), replication controllers (rc), services
|
||||
(se), minions (mi), or events (ev).
|
||||
(svc), minions (mi), or events (ev).
|
||||
|
||||
By specifying the output as 'template' and providing a Go template as the value
|
||||
of the --template flag, you can filter the attributes of the fetched resource(s).
|
||||
|
||||
```
|
||||
kubectl get [(-o|--output=)json|yaml|template|...] RESOURCE [ID]
|
||||
kubectl get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
@ -23,17 +23,20 @@ kubectl get [(-o|--output=)json|yaml|template|...] RESOURCE [ID]
|
|||
// List all pods in ps output format.
|
||||
$ kubectl get pods
|
||||
|
||||
// List a single replication controller with specified ID in ps output format.
|
||||
$ kubectl get replicationController 1234-56-7890-234234-456456
|
||||
// List a single replication controller with specified NAME in ps output format.
|
||||
$ kubectl get replicationController web
|
||||
|
||||
// List a single pod in JSON output format.
|
||||
$ kubectl get -o json pod 1234-56-7890-234234-456456
|
||||
$ kubectl get -o json pod web-pod-13je7
|
||||
|
||||
// Return only the status value of the specified pod.
|
||||
$ kubectl get -o template pod 1234-56-7890-234234-456456 --template={{.currentState.status}}
|
||||
$ kubectl get -o template web-pod-13je7 --template={{.currentState.status}}
|
||||
|
||||
// List all replication controllers and services together in ps output format.
|
||||
$ kubectl get rc,services
|
||||
|
||||
// List one or more resources by their type and names
|
||||
$ kubectl get rc/web service/frontend pods/web-pod-13je7
|
||||
```
|
||||
|
||||
### Options
|
||||
|
|
|
@ -17,7 +17,7 @@ Display one or many resources.
|
|||
|
||||
.PP
|
||||
Possible resources include pods (po), replication controllers (rc), services
|
||||
(se), minions (mi), or events (ev).
|
||||
(svc), minions (mi), or events (ev).
|
||||
|
||||
.PP
|
||||
By specifying the output as 'template' and providing a Go template as the value
|
||||
|
@ -169,18 +169,21 @@ of the \-\-template flag, you can filter the attributes of the fetched resource(
|
|||
// List all pods in ps output format.
|
||||
$ kubectl get pods
|
||||
|
||||
// List a single replication controller with specified ID in ps output format.
|
||||
$ kubectl get replicationController 1234\-56\-7890\-234234\-456456
|
||||
// List a single replication controller with specified NAME in ps output format.
|
||||
$ kubectl get replicationController web
|
||||
|
||||
// List a single pod in JSON output format.
|
||||
$ kubectl get \-o json pod 1234\-56\-7890\-234234\-456456
|
||||
$ kubectl get \-o json pod web\-pod\-13je7
|
||||
|
||||
// Return only the status value of the specified pod.
|
||||
$ kubectl get \-o template pod 1234\-56\-7890\-234234\-456456 \-\-template=\{\{.currentState.status\}\}
|
||||
$ kubectl get \-o template web\-pod\-13je7 \-\-template=\{\{.currentState.status\}\}
|
||||
|
||||
// List all replication controllers and services together in ps output format.
|
||||
$ kubectl get rc,services
|
||||
|
||||
// List one or more resources by their type and names
|
||||
$ kubectl get rc/web service/frontend pods/web\-pod\-13je7
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
|
|
|
@ -155,6 +155,8 @@ for version in "${kube_api_versions[@]}"; do
|
|||
# Post-condition: valid-pod POD is running
|
||||
kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:'
|
||||
kube::test::get_object_assert 'pod valid-pod' "{{.$id_field}}" 'valid-pod'
|
||||
kube::test::get_object_assert 'pod/valid-pod' "{{.$id_field}}" 'valid-pod'
|
||||
kube::test::get_object_assert 'pods/valid-pod' "{{.$id_field}}" 'valid-pod'
|
||||
# Describe command should print detailed information
|
||||
kube::test::describe_object_assert pods 'valid-pod' "Name:" "Image(s):" "Host:" "Labels:" "Status:" "Replication Controllers"
|
||||
|
||||
|
@ -524,6 +526,13 @@ __EOF__
|
|||
kube::test::describe_object_assert minions "127.0.0.1" "Name:" "Conditions:" "Addresses:" "Capacity:" "Pods:"
|
||||
fi
|
||||
|
||||
#####################
|
||||
# Retrieve multiple #
|
||||
#####################
|
||||
|
||||
kube::log::status "Testing kubectl(${version}:multiget)"
|
||||
kube::test::get_object_assert 'nodes/127.0.0.1 service/kubernetes' "{{range.items}}{{.$id_field}}:{{end}}" '127.0.0.1:kubernetes:'
|
||||
|
||||
kube::test::clear_all
|
||||
done
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ func RunCreate(f *Factory, out io.Writer, cmd *cobra.Command, filenames util.Str
|
|||
}
|
||||
count++
|
||||
info.Refresh(obj, true)
|
||||
fmt.Fprintf(out, "%s\n", info.Name)
|
||||
fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -61,7 +61,7 @@ func TestCreateObject(t *testing.T) {
|
|||
cmd.Run(cmd, []string{})
|
||||
|
||||
// uses the name from the file, not the response
|
||||
if buf.String() != "redis-master-controller\n" {
|
||||
if buf.String() != "replicationControllers/redis-master-controller\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ func TestCreateMultipleObject(t *testing.T) {
|
|||
cmd.Run(cmd, []string{})
|
||||
|
||||
// Names should come from the REST response, NOT the files
|
||||
if buf.String() != "rc1\nbaz\n" {
|
||||
if buf.String() != "replicationControllers/rc1\nservices/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ func TestCreateDirectory(t *testing.T) {
|
|||
cmd.Flags().Set("filename", "../../../examples/guestbook")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "name\nbaz\nname\nbaz\nname\nbaz\n" {
|
||||
if buf.String() != "replicationControllers/name\nservices/baz\nreplicationControllers/name\nservices/baz\nreplicationControllers/name\nservices/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ func RunDelete(f *Factory, out io.Writer, cmd *cobra.Command, args []string, fil
|
|||
if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(out, "%s\n", r.Name)
|
||||
fmt.Fprintf(out, "%s/%s\n", r.Mapping.Resource, r.Name)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -50,7 +50,7 @@ func TestDeleteObject(t *testing.T) {
|
|||
cmd.Run(cmd, []string{})
|
||||
|
||||
// uses the name from the file, not the response
|
||||
if buf.String() != "redis-master-controller\n" {
|
||||
if buf.String() != "replicationControllers/redis-master-controller\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ func TestDeleteMultipleObject(t *testing.T) {
|
|||
cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "redis-master-controller\nfrontend\n" {
|
||||
if buf.String() != "replicationControllers/redis-master-controller\nservices/frontend\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ func TestDeleteMultipleObjectIgnoreMissing(t *testing.T) {
|
|||
cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "frontend\n" {
|
||||
if buf.String() != "services/frontend\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ func TestDeleteDirectory(t *testing.T) {
|
|||
cmd.Flags().Set("filename", "../../../examples/guestbook")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "frontend-controller\nfrontend\nredis-master-controller\nredis-master\nredis-slave-controller\nredis-slave\n" {
|
||||
if buf.String() != "replicationControllers/frontend-controller\nservices/frontend\nreplicationControllers/redis-master-controller\nservices/redis-master\nreplicationControllers/redis-slave-controller\nservices/redis-slave\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -213,7 +213,7 @@ func TestDeleteMultipleSelector(t *testing.T) {
|
|||
cmd.Flags().Set("selector", "a=b")
|
||||
cmd.Run(cmd, []string{"pods,services"})
|
||||
|
||||
if buf.String() != "foo\nbar\nbaz\n" {
|
||||
if buf.String() != "pods/foo\npods/bar\nservices/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,31 +34,34 @@ const (
|
|||
get_long = `Display one or many resources.
|
||||
|
||||
Possible resources include pods (po), replication controllers (rc), services
|
||||
(se), minions (mi), or events (ev).
|
||||
(svc), minions (mi), or events (ev).
|
||||
|
||||
By specifying the output as 'template' and providing a Go template as the value
|
||||
of the --template flag, you can filter the attributes of the fetched resource(s).`
|
||||
get_example = `// List all pods in ps output format.
|
||||
$ kubectl get pods
|
||||
|
||||
// List a single replication controller with specified ID in ps output format.
|
||||
$ kubectl get replicationController 1234-56-7890-234234-456456
|
||||
// List a single replication controller with specified NAME in ps output format.
|
||||
$ kubectl get replicationController web
|
||||
|
||||
// List a single pod in JSON output format.
|
||||
$ kubectl get -o json pod 1234-56-7890-234234-456456
|
||||
$ kubectl get -o json pod web-pod-13je7
|
||||
|
||||
// Return only the status value of the specified pod.
|
||||
$ kubectl get -o template pod 1234-56-7890-234234-456456 --template={{.currentState.status}}
|
||||
$ kubectl get -o template web-pod-13je7 --template={{.currentState.status}}
|
||||
|
||||
// List all replication controllers and services together in ps output format.
|
||||
$ kubectl get rc,services`
|
||||
$ kubectl get rc,services
|
||||
|
||||
// List one or more resources by their type and names
|
||||
$ kubectl get rc/web service/frontend pods/web-pod-13je7`
|
||||
)
|
||||
|
||||
// NewCmdGet creates a command object for the generic "get" action, which
|
||||
// retrieves one or more resources from a server.
|
||||
func (f *Factory) NewCmdGet(out io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "get [(-o|--output=)json|yaml|template|...] RESOURCE [ID]",
|
||||
Use: "get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)",
|
||||
Short: "Display one or many resources",
|
||||
Long: get_long,
|
||||
Example: get_example,
|
||||
|
@ -141,6 +144,7 @@ func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) error
|
|||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||
SelectorParam(selector).
|
||||
ResourceTypeOrNameArgs(true, args...).
|
||||
ContinueOnError().
|
||||
Latest()
|
||||
printer, generic, err := util.PrinterForCommand(cmd)
|
||||
if err != nil {
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
|
||||
)
|
||||
|
@ -313,6 +314,47 @@ func TestGetMultipleTypeObjectsWithSelector(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetMultipleTypeObjectsWithDirectReference(t *testing.T) {
|
||||
_, svc, _ := testData()
|
||||
node := &api.Node{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
}
|
||||
|
||||
f, tf, codec := NewAPIFactory()
|
||||
tf.Printer = &testPrinter{}
|
||||
tf.Client = &client.FakeRESTClient{
|
||||
Codec: codec,
|
||||
Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
|
||||
switch req.URL.Path {
|
||||
case "/nodes/foo":
|
||||
return &http.Response{StatusCode: 200, Body: objBody(codec, node)}, nil
|
||||
case "/namespaces/test/services/bar":
|
||||
return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
tf.Namespace = "test"
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
|
||||
cmd := f.NewCmdGet(buf)
|
||||
cmd.SetOutput(buf)
|
||||
|
||||
cmd.Run(cmd, []string{"services/bar", "node/foo"})
|
||||
|
||||
expected := []runtime.Object{&svc.Items[0], node}
|
||||
actual := tf.Printer.(*testPrinter).Objects
|
||||
if !api.Semantic.DeepEqual(expected, actual) {
|
||||
t.Errorf("unexpected object: %s", util.ObjectDiff(expected, actual))
|
||||
}
|
||||
if len(buf.String()) == 0 {
|
||||
t.Errorf("unexpected empty output")
|
||||
}
|
||||
}
|
||||
func watchTestData() ([]api.Pod, []watch.Event) {
|
||||
pods := []api.Pod{
|
||||
{
|
||||
|
|
|
@ -71,11 +71,10 @@ func (f *Factory) NewCmdStop(out io.Writer) *cobra.Command {
|
|||
r.Visit(func(info *resource.Info) error {
|
||||
reaper, err := f.Reaper(info.Mapping)
|
||||
cmdutil.CheckErr(err)
|
||||
s, err := reaper.Stop(info.Namespace, info.Name)
|
||||
if err != nil {
|
||||
if _, err := reaper.Stop(info.Namespace, info.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(out, "%s\n", s)
|
||||
fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
|
@ -111,7 +111,7 @@ func RunUpdate(f *Factory, out io.Writer, cmd *cobra.Command, args []string, fil
|
|||
return err
|
||||
}
|
||||
info.Refresh(obj, true)
|
||||
fmt.Fprintf(out, "%s\n", info.Name)
|
||||
fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ func TestUpdateObject(t *testing.T) {
|
|||
cmd.Run(cmd, []string{})
|
||||
|
||||
// uses the name from the file, not the response
|
||||
if buf.String() != "rc1\n" {
|
||||
if buf.String() != "replicationControllers/rc1\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ func TestUpdateMultipleObject(t *testing.T) {
|
|||
cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "rc1\nbaz\n" {
|
||||
if buf.String() != "replicationControllers/rc1\nservices/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ func TestUpdateDirectory(t *testing.T) {
|
|||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "rc1\nbaz\nrc1\nbaz\nrc1\nbaz\n" {
|
||||
if buf.String() != "replicationControllers/rc1\nservices/baz\nreplicationControllers/rc1\nservices/baz\nreplicationControllers/rc1\nservices/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func CheckErr(err error) {
|
|||
if client.IsUnexpectedStatusError(err) {
|
||||
glog.FatalDepth(1, fmt.Sprintf("Unexpected status received from server: %s", err.Error()))
|
||||
}
|
||||
glog.FatalDepth(1, fmt.Sprintf("Client error processing command: %s", err.Error()))
|
||||
glog.FatalDepth(1, fmt.Sprintf("Error: %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,8 @@ type Builder struct {
|
|||
namespace string
|
||||
names []string
|
||||
|
||||
resourceTuples []resourceTuple
|
||||
|
||||
defaultNamespace bool
|
||||
requireNamespace bool
|
||||
|
||||
|
@ -60,6 +62,11 @@ type Builder struct {
|
|||
continueOnError bool
|
||||
}
|
||||
|
||||
type resourceTuple struct {
|
||||
Resource string
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewBuilder creates a builder that operates on generic objects.
|
||||
func NewBuilder(mapper meta.RESTMapper, typer runtime.ObjectTyper, clientMapper ClientMapper) *Builder {
|
||||
return &Builder{
|
||||
|
@ -223,6 +230,26 @@ func (b *Builder) SelectAllParam(selectAll bool) *Builder {
|
|||
// When two or more arguments are received, they must be a single type and resource name(s).
|
||||
// The allowEmptySelector permits to select all the resources (via Everything func).
|
||||
func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string) *Builder {
|
||||
if ok, err := hasCombinedTypeArgs(args); ok {
|
||||
if err != nil {
|
||||
b.errs = append(b.errs, err)
|
||||
return b
|
||||
}
|
||||
for _, s := range args {
|
||||
seg := strings.Split(s, "/")
|
||||
if len(seg) != 2 {
|
||||
b.errs = append(b.errs, fmt.Errorf("arguments in resource/name form may not have more than one slash"))
|
||||
return b
|
||||
}
|
||||
resource, name := seg[0], seg[1]
|
||||
if len(resource) == 0 || len(name) == 0 || len(SplitResourceArgument(resource)) != 1 {
|
||||
b.errs = append(b.errs, fmt.Errorf("arguments in resource/name form must have a single resource and name"))
|
||||
return b
|
||||
}
|
||||
b.resourceTuples = append(b.resourceTuples, resourceTuple{Resource: resource, Name: name})
|
||||
}
|
||||
return b
|
||||
}
|
||||
switch {
|
||||
case len(args) > 2:
|
||||
b.names = append(b.names, args[1:]...)
|
||||
|
@ -242,6 +269,23 @@ func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string
|
|||
return b
|
||||
}
|
||||
|
||||
func hasCombinedTypeArgs(args []string) (bool, error) {
|
||||
hasSlash := 0
|
||||
for _, s := range args {
|
||||
if strings.Contains(s, "/") {
|
||||
hasSlash++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case hasSlash > 0 && hasSlash == len(args):
|
||||
return true, nil
|
||||
case hasSlash > 0 && hasSlash != len(args):
|
||||
return true, fmt.Errorf("when passing arguments in resource/name form, all arguments must include the resource")
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceTypeAndNameArgs expects two arguments, a resource type, and a resource name. The resource
|
||||
// matching that type and and name will be retrieved from the server.
|
||||
func (b *Builder) ResourceTypeAndNameArgs(args ...string) *Builder {
|
||||
|
@ -304,6 +348,31 @@ func (b *Builder) resourceMappings() ([]*meta.RESTMapping, error) {
|
|||
return mappings, nil
|
||||
}
|
||||
|
||||
func (b *Builder) resourceTupleMappings() (map[string]*meta.RESTMapping, error) {
|
||||
mappings := make(map[string]*meta.RESTMapping)
|
||||
canonical := make(map[string]struct{})
|
||||
for _, r := range b.resourceTuples {
|
||||
if _, ok := mappings[r.Resource]; ok {
|
||||
continue
|
||||
}
|
||||
version, kind, err := b.mapper.VersionAndKindForResource(r.Resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := b.mapper.RESTMapping(kind, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mappings[mapping.Resource] = mapping
|
||||
mappings[r.Resource] = mapping
|
||||
canonical[mapping.Resource] = struct{}{}
|
||||
}
|
||||
if len(canonical) > 1 && b.singleResourceType {
|
||||
return nil, fmt.Errorf("you may only specify a single resource type")
|
||||
}
|
||||
return mappings, nil
|
||||
}
|
||||
|
||||
func (b *Builder) visitorResult() *Result {
|
||||
if len(b.errs) > 0 {
|
||||
return &Result{err: errors.NewAggregate(b.errs)}
|
||||
|
@ -318,6 +387,9 @@ func (b *Builder) visitorResult() *Result {
|
|||
if len(b.names) != 0 {
|
||||
return &Result{err: fmt.Errorf("name cannot be provided when a selector is specified")}
|
||||
}
|
||||
if len(b.resourceTuples) != 0 {
|
||||
return &Result{err: fmt.Errorf("selectors and the all flag cannot be used when passing resource/name arguments")}
|
||||
}
|
||||
if len(b.resources) == 0 {
|
||||
return &Result{err: fmt.Errorf("at least one resource must be specified to use a selector")}
|
||||
}
|
||||
|
@ -352,6 +424,69 @@ func (b *Builder) visitorResult() *Result {
|
|||
return &Result{visitor: VisitorList(visitors), sources: visitors}
|
||||
}
|
||||
|
||||
// visit items specified by resource and name
|
||||
if len(b.resourceTuples) != 0 {
|
||||
isSingular := len(b.resourceTuples) == 1
|
||||
|
||||
if len(b.paths) != 0 {
|
||||
return &Result{singular: isSingular, err: fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify a resource by arguments as well")}
|
||||
}
|
||||
if len(b.resources) != 0 {
|
||||
return &Result{singular: isSingular, err: fmt.Errorf("you may not specify individual resources and bulk resources in the same call")}
|
||||
}
|
||||
|
||||
// retrieve one client for each resource
|
||||
mappings, err := b.resourceTupleMappings()
|
||||
if err != nil {
|
||||
return &Result{singular: isSingular, err: err}
|
||||
}
|
||||
clients := make(map[string]RESTClient)
|
||||
for _, mapping := range mappings {
|
||||
s := fmt.Sprintf("%s/%s", mapping.APIVersion, mapping.Resource)
|
||||
if _, ok := clients[s]; ok {
|
||||
continue
|
||||
}
|
||||
client, err := b.mapper.ClientForMapping(mapping)
|
||||
if err != nil {
|
||||
return &Result{err: err}
|
||||
}
|
||||
clients[s] = client
|
||||
}
|
||||
|
||||
items := []Visitor{}
|
||||
for _, tuple := range b.resourceTuples {
|
||||
mapping, ok := mappings[tuple.Resource]
|
||||
if !ok {
|
||||
return &Result{singular: isSingular, err: fmt.Errorf("resource %q is not recognized: %v", tuple.Resource, mappings)}
|
||||
}
|
||||
s := fmt.Sprintf("%s/%s", mapping.APIVersion, mapping.Resource)
|
||||
client, ok := clients[s]
|
||||
if !ok {
|
||||
return &Result{singular: isSingular, err: fmt.Errorf("could not find a client for resource %q", tuple.Resource)}
|
||||
}
|
||||
|
||||
selectorNamespace := b.namespace
|
||||
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
|
||||
selectorNamespace = ""
|
||||
} else {
|
||||
if len(b.namespace) == 0 {
|
||||
return &Result{singular: isSingular, err: fmt.Errorf("namespace may not be empty when retrieving a resource by name")}
|
||||
}
|
||||
}
|
||||
|
||||
info := NewInfo(client, mapping, selectorNamespace, tuple.Name)
|
||||
items = append(items, info)
|
||||
}
|
||||
|
||||
var visitors Visitor
|
||||
if b.continueOnError {
|
||||
visitors = EagerVisitorList(items)
|
||||
} else {
|
||||
visitors = VisitorList(items)
|
||||
}
|
||||
return &Result{singular: isSingular, visitor: visitors, sources: items}
|
||||
}
|
||||
|
||||
// visit items specified by name
|
||||
if len(b.names) != 0 {
|
||||
isSingular := len(b.names) == 1
|
||||
|
@ -444,7 +579,10 @@ func (b *Builder) Do() *Result {
|
|||
if b.requireNamespace {
|
||||
helpers = append(helpers, RequireNamespace(b.namespace))
|
||||
}
|
||||
helpers = append(helpers, FilterNamespace())
|
||||
helpers = append(helpers, FilterNamespace)
|
||||
if b.latest {
|
||||
helpers = append(helpers, RetrieveLazy)
|
||||
}
|
||||
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -416,6 +416,88 @@ func TestSingleResourceType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResourceTuple(t *testing.T) {
|
||||
expectNoErr := func(err error) bool { return err == nil }
|
||||
expectErr := func(err error) bool { return err != nil }
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
errFn func(error) bool
|
||||
}{
|
||||
"valid": {
|
||||
args: []string{"pods/foo"},
|
||||
errFn: expectNoErr,
|
||||
},
|
||||
"valid multiple with name indirection": {
|
||||
args: []string{"pods/foo", "pod/bar"},
|
||||
errFn: expectNoErr,
|
||||
},
|
||||
"valid multiple with namespaced and non-namespaced types": {
|
||||
args: []string{"minions/foo", "pod/bar"},
|
||||
errFn: expectNoErr,
|
||||
},
|
||||
"mixed arg types": {
|
||||
args: []string{"pods/foo", "bar"},
|
||||
errFn: expectErr,
|
||||
},
|
||||
/*"missing resource": {
|
||||
args: []string{"pods/foo2"},
|
||||
errFn: expectNoErr, // not an error because resources are lazily visited
|
||||
},*/
|
||||
"comma in resource": {
|
||||
args: []string{",pods/foo"},
|
||||
errFn: expectErr,
|
||||
},
|
||||
"multiple types in resource": {
|
||||
args: []string{"pods,services/foo"},
|
||||
errFn: expectErr,
|
||||
},
|
||||
"unknown resource type": {
|
||||
args: []string{"unknown/foo"},
|
||||
errFn: expectErr,
|
||||
},
|
||||
"leading slash": {
|
||||
args: []string{"/bar"},
|
||||
errFn: expectErr,
|
||||
},
|
||||
"trailing slash": {
|
||||
args: []string{"bar/"},
|
||||
errFn: expectErr,
|
||||
},
|
||||
}
|
||||
for k, testCase := range testCases {
|
||||
pods, _ := testData()
|
||||
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||
"/namespaces/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]),
|
||||
"/namespaces/test/pods/bar": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]),
|
||||
"/nodes/foo": runtime.EncodeOrDie(latest.Codec, &api.Node{ObjectMeta: api.ObjectMeta{Name: "foo"}}),
|
||||
})).
|
||||
NamespaceParam("test").DefaultNamespace().
|
||||
ResourceTypeOrNameArgs(true, testCase.args...)
|
||||
|
||||
r := b.Do()
|
||||
|
||||
if !testCase.errFn(r.Err()) {
|
||||
t.Errorf("%s: unexpected error: %v", k, r.Err())
|
||||
}
|
||||
if r.Err() != nil {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case (r.singular && len(testCase.args) != 1),
|
||||
(!r.singular && len(testCase.args) == 1):
|
||||
t.Errorf("%s: result had unexpected singular value", k)
|
||||
}
|
||||
info, err := r.Infos()
|
||||
if err != nil {
|
||||
// test error
|
||||
continue
|
||||
}
|
||||
if len(info) != len(testCase.args) {
|
||||
t.Errorf("%s: unexpected number of infos returned: %#v", info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream(t *testing.T) {
|
||||
r, pods, rc := streamTestData()
|
||||
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||
|
@ -619,7 +701,7 @@ func TestLatest(t *testing.T) {
|
|||
|
||||
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||
if err != nil || singular || len(test.Infos) != 3 {
|
||||
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||
t.Fatalf("unexpected response: %v %t %#v", err, singular, test.Infos)
|
||||
}
|
||||
if !api.Semantic.DeepDerivative([]runtime.Object{newPod, newPod2, newSvc}, test.Objects()) {
|
||||
t.Errorf("unexpected visited objects: %#v", test.Objects())
|
||||
|
|
|
@ -130,6 +130,11 @@ func (i *Info) Refresh(obj runtime.Object, ignoreError bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Namespaced returns true if the object belongs to a namespace
|
||||
func (i *Info) Namespaced() bool {
|
||||
return i.Mapping != nil && i.Mapping.Scope.Name() == meta.RESTScopeNameNamespace
|
||||
}
|
||||
|
||||
// Watch returns server changes to this object after it was retrieved.
|
||||
func (i *Info) Watch(resourceVersion string) (watch.Interface, error) {
|
||||
return NewHelper(i.Client, i.Mapping).WatchSingle(i.Namespace, i.Name, resourceVersion)
|
||||
|
@ -418,14 +423,12 @@ func UpdateObjectNamespace(info *Info) error {
|
|||
}
|
||||
|
||||
// FilterNamespace omits the namespace if the object is not namespace scoped
|
||||
func FilterNamespace() VisitorFunc {
|
||||
return func(info *Info) error {
|
||||
if info.Mapping.Scope.Name() != meta.RESTScopeNameNamespace {
|
||||
info.Namespace = ""
|
||||
UpdateObjectNamespace(info)
|
||||
}
|
||||
return nil
|
||||
func FilterNamespace(info *Info) error {
|
||||
if !info.Namespaced() {
|
||||
info.Namespace = ""
|
||||
UpdateObjectNamespace(info)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNamespace ensures that every Info object visited will have a namespace
|
||||
|
@ -446,6 +449,9 @@ func SetNamespace(namespace string) VisitorFunc {
|
|||
// accidentally operating on resources outside their namespace.
|
||||
func RequireNamespace(namespace string) VisitorFunc {
|
||||
return func(info *Info) error {
|
||||
if !info.Namespaced() {
|
||||
return nil
|
||||
}
|
||||
if len(info.Namespace) == 0 {
|
||||
info.Namespace = namespace
|
||||
UpdateObjectNamespace(info)
|
||||
|
@ -461,9 +467,12 @@ func RequireNamespace(namespace string) VisitorFunc {
|
|||
// RetrieveLatest updates the Object on each Info by invoking a standard client
|
||||
// Get.
|
||||
func RetrieveLatest(info *Info) error {
|
||||
if len(info.Name) == 0 || len(info.Namespace) == 0 {
|
||||
if len(info.Name) == 0 {
|
||||
return nil
|
||||
}
|
||||
if info.Namespaced() && len(info.Namespace) == 0 {
|
||||
return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name)
|
||||
}
|
||||
obj, err := NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -472,3 +481,11 @@ func RetrieveLatest(info *Info) error {
|
|||
info.ResourceVersion, _ = info.Mapping.MetadataAccessor.ResourceVersion(obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetrieveLazy updates the object if it has not been loaded yet.
|
||||
func RetrieveLazy(info *Info) error {
|
||||
if info.Object == nil {
|
||||
return info.Get()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue