diff --git a/pkg/kubectl/resource/builder.go b/pkg/kubectl/resource/builder.go index 35544929571..835fd2225a1 100644 --- a/pkg/kubectl/resource/builder.go +++ b/pkg/kubectl/resource/builder.go @@ -184,6 +184,28 @@ func (b *Builder) ResourceTypes(types ...string) *Builder { return b } +// ResourceNames accepts a default type and one or more names, and creates tuples of +// resources +func (b *Builder) ResourceNames(resource string, names ...string) *Builder { + for _, name := range names { + // See if this input string is of type/name format + tuple, ok, err := splitResourceTypeName(name) + if err != nil { + b.errs = append(b.errs, err) + return b + } + + if ok { + b.resourceTuples = append(b.resourceTuples, tuple) + continue + } + + // Use the given default type to create a resource tuple + b.resourceTuples = append(b.resourceTuples, resourceTuple{Resource: resource, Name: name}) + } + return b +} + // SelectorParam defines a selector that should be applied to the object types to load. // This will not affect files loaded from disk or URL. If the parameter is empty it is // a no-op - to select all resources invoke `b.Selector(labels.Everything)`. @@ -279,17 +301,14 @@ func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string 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")) + tuple, ok, err := splitResourceTypeName(s) + if err != nil { + b.errs = append(b.errs, err) 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 + if ok { + b.resourceTuples = append(b.resourceTuples, tuple) } - b.resourceTuples = append(b.resourceTuples, resourceTuple{Resource: resource, Name: name}) } return b } @@ -346,6 +365,23 @@ func hasCombinedTypeArgs(args []string) (bool, error) { } } +// splitResourceTypeName handles type/name resource formats and returns a resource tuple +// (empty or not), whether it successfully found one, and an error +func splitResourceTypeName(s string) (resourceTuple, bool, error) { + if !strings.Contains(s, "/") { + return resourceTuple{}, false, nil + } + seg := strings.Split(s, "/") + if len(seg) != 2 { + return resourceTuple{}, false, fmt.Errorf("arguments in resource/name form may not have more than one slash") + } + resource, name := seg[0], seg[1] + if len(resource) == 0 || len(name) == 0 || len(SplitResourceArgument(resource)) != 1 { + return resourceTuple{}, false, fmt.Errorf("arguments in resource/name form must have a single resource and name") + } + return resourceTuple{Resource: resource, Name: name}, true, nil +} + // Flatten will convert any objects with a field named "Items" that is an array of runtime.Object // compatible types into individual entries and give them their own items. The original object // is not passed to any visitors. diff --git a/pkg/kubectl/resource/builder_test.go b/pkg/kubectl/resource/builder_test.go index d6caa670385..d2921d63547 100644 --- a/pkg/kubectl/resource/builder_test.go +++ b/pkg/kubectl/resource/builder_test.go @@ -114,6 +114,10 @@ func testData() (*api.PodList, *api.ServiceList) { Items: []api.Service{ { ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, + Spec: api.ServiceSpec{ + Type: "ClusterIP", + SessionAffinity: "None", + }, }, }, } @@ -374,7 +378,7 @@ func TestResourceByName(t *testing.T) { t.Fatalf("unexpected response: %v %t %#v", err, singular, test.Infos) } if !reflect.DeepEqual(&pods.Items[0], test.Objects()[0]) { - t.Errorf("unexpected object: %#v", test.Objects()) + t.Errorf("unexpected object: %#v", test.Objects()[0]) } mapping, err := b.Do().ResourceMapping() @@ -418,6 +422,34 @@ func TestMultipleResourceByTheSameName(t *testing.T) { } } +func TestResourceNames(t *testing.T) { + pods, svc := 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/services/baz": runtime.EncodeOrDie(latest.Codec, &svc.Items[0]), + })). + NamespaceParam("test") + + test := &testVisitor{} + + if b.Do().Err() == nil { + t.Errorf("unexpected non-error") + } + + b.ResourceNames("pods", "foo", "services/baz") + + err := b.Do().Visit(test.Handle) + if err != nil || len(test.Infos) != 2 { + t.Fatalf("unexpected response: %v %#v", err, test.Infos) + } + if !reflect.DeepEqual(&pods.Items[0], test.Objects()[0]) { + t.Errorf("unexpected object: \n%#v, expected: \n%#v", test.Objects()[0], &pods.Items[0]) + } + if !reflect.DeepEqual(&svc.Items[0], test.Objects()[1]) { + t.Errorf("unexpected object: \n%#v, expected: \n%#v", test.Objects()[1], &svc.Items[0]) + } +} + func TestResourceByNameWithoutRequireObject(t *testing.T) { b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith("", t, map[string]string{})). NamespaceParam("test")