diff --git a/docs/kubectl-get.md b/docs/kubectl-get.md index 87d247cf38c..8be36b251d1 100644 --- a/docs/kubectl-get.md +++ b/docs/kubectl-get.md @@ -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 diff --git a/docs/man/man1/kubectl-get.1 b/docs/man/man1/kubectl-get.1 index 1c9cd2f0591..c0727177590 100644 --- a/docs/man/man1/kubectl-get.1 +++ b/docs/man/man1/kubectl-get.1 @@ -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 diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 0126a4f91ca..7759a8ed87d 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -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 diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 2b8dcc744b4..99737374a88 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -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 { diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go index 05a5e662315..10cac279bf0 100644 --- a/pkg/kubectl/cmd/create_test.go +++ b/pkg/kubectl/cmd/create_test.go @@ -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()) } } diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index c3bbe58d58f..7af851e265d 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -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 { diff --git a/pkg/kubectl/cmd/delete_test.go b/pkg/kubectl/cmd/delete_test.go index 28309050e60..c8310c3f8c4 100644 --- a/pkg/kubectl/cmd/delete_test.go +++ b/pkg/kubectl/cmd/delete_test.go @@ -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()) } } diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 7aeb52f5a0d..bf2a62f89bc 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -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 { diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index d3bb8925930..5ef1abd899e 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -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{ { diff --git a/pkg/kubectl/cmd/stop.go b/pkg/kubectl/cmd/stop.go index f5f5df93b37..7d38fdb6552 100644 --- a/pkg/kubectl/cmd/stop.go +++ b/pkg/kubectl/cmd/stop.go @@ -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 }) }, diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index baaa8b71134..0bbbb7b59db 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -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 }) diff --git a/pkg/kubectl/cmd/update_test.go b/pkg/kubectl/cmd/update_test.go index 0986b6b057b..eeed5284797 100644 --- a/pkg/kubectl/cmd/update_test.go +++ b/pkg/kubectl/cmd/update_test.go @@ -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()) } } diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index fffaee5fd0e..641a22356fa 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -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())) } } diff --git a/pkg/kubectl/resource/builder.go b/pkg/kubectl/resource/builder.go index 940639d1b13..7e7aceaa23f 100644 --- a/pkg/kubectl/resource/builder.go +++ b/pkg/kubectl/resource/builder.go @@ -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 } diff --git a/pkg/kubectl/resource/builder_test.go b/pkg/kubectl/resource/builder_test.go index e3f59479144..474bae98018 100644 --- a/pkg/kubectl/resource/builder_test.go +++ b/pkg/kubectl/resource/builder_test.go @@ -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()) diff --git a/pkg/kubectl/resource/visitor.go b/pkg/kubectl/resource/visitor.go index 2c6bcd78bf2..a364bae5ed8 100644 --- a/pkg/kubectl/resource/visitor.go +++ b/pkg/kubectl/resource/visitor.go @@ -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 +}