diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 8da6bd86a3b..50e873dd752 100755 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -430,6 +430,13 @@ run_pod_tests() { kubectl create -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml "${kube_flags[@]}" # Post-condition: valid-pod POD is created kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' + # Command + output_message=$(kubectl get pods --field-selector metadata.name=valid-pod "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" "valid-pod" + # Command + phase=$(kubectl get "${kube_flags[@]}" pod valid-pod -o go-template='{{ .status.phase }}') + output_message=$(kubectl get pods --field-selector status.phase="${phase}" "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" "valid-pod" ### Delete PODs with no parameter mustn't kill everything # Pre-condition: valid-pod POD exists diff --git a/pkg/kubectl/cmd/annotate.go b/pkg/kubectl/cmd/annotate.go index f014134bbcd..92ec1a12527 100644 --- a/pkg/kubectl/cmd/annotate.go +++ b/pkg/kubectl/cmd/annotate.go @@ -205,7 +205,7 @@ func (o AnnotateOptions) RunAnnotate(f cmdutil.Factory, cmd *cobra.Command) erro return err } - b = b.SelectorParam(o.selector). + b = b.LabelSelectorParam(o.selector). Unstructured(f.UnstructuredClientForMapping, mapper, typer). ResourceTypeOrNameArgs(o.all, o.resources...). Latest() diff --git a/pkg/kubectl/cmd/apply.go b/pkg/kubectl/cmd/apply.go index ec969eab7ee..df3b2190079 100644 --- a/pkg/kubectl/cmd/apply.go +++ b/pkg/kubectl/cmd/apply.go @@ -219,7 +219,7 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, opti ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &options.FilenameOptions). - SelectorParam(options.Selector). + LabelSelectorParam(options.Selector). IncludeUninitialized(includeUninitialized). Flatten(). Do() @@ -367,8 +367,8 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, opti clientFunc: f.UnstructuredClientForMapping, clientsetFunc: f.ClientSet, - selector: options.Selector, - visitedUids: visitedUids, + labelSelector: options.Selector, + visitedUids: visitedUids, cascade: options.Cascade, dryRun: dryRun, @@ -453,8 +453,9 @@ type pruner struct { clientFunc resource.ClientMapperFunc clientsetFunc func() (internalclientset.Interface, error) - visitedUids sets.String - selector string + visitedUids sets.String + labelSelector string + fieldSelector string cascade bool dryRun bool @@ -474,7 +475,8 @@ func (p *pruner) prune(namespace string, mapping *meta.RESTMapping, shortOutput, mapping.GroupVersionKind.Version, false, &metav1.ListOptions{ - LabelSelector: p.selector, + LabelSelector: p.labelSelector, + FieldSelector: p.fieldSelector, IncludeUninitialized: includeUninitialized, }, ) diff --git a/pkg/kubectl/cmd/apply_view_last_applied.go b/pkg/kubectl/cmd/apply_view_last_applied.go index 4c7cec8214d..8692cffde1f 100644 --- a/pkg/kubectl/cmd/apply_view_last_applied.go +++ b/pkg/kubectl/cmd/apply_view_last_applied.go @@ -98,7 +98,7 @@ func (o *ViewLastAppliedOptions) Complete(f cmdutil.Factory, args []string) erro FilenameParam(enforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(enforceNamespace, args...). SelectAllParam(o.All). - SelectorParam(o.Selector). + LabelSelectorParam(o.Selector). Latest(). Flatten(). Do() diff --git a/pkg/kubectl/cmd/clusterinfo.go b/pkg/kubectl/cmd/clusterinfo.go index 142b7bab0ab..2c63d49a82e 100644 --- a/pkg/kubectl/cmd/clusterinfo.go +++ b/pkg/kubectl/cmd/clusterinfo.go @@ -74,7 +74,7 @@ func RunClusterInfo(f cmdutil.Factory, out io.Writer, cmd *cobra.Command) error // TODO use generalized labels once they are implemented (#341) b := f.NewBuilder(). NamespaceParam(cmdNamespace).DefaultNamespace(). - SelectorParam("kubernetes.io/cluster-service=true"). + LabelSelectorParam("kubernetes.io/cluster-service=true"). ResourceTypeOrNameArgs(false, []string{"services"}...). Latest() err = b.Do().Visit(func(r *resource.Info, err error) error { diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 8caa921870d..6ec61c93acd 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -194,7 +194,7 @@ func RunCreate(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, opt ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &options.FilenameOptions). - SelectorParam(options.Selector). + LabelSelectorParam(options.Selector). Flatten(). Do() err = r.Err() diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 50f32f4ca0e..bf37e301367 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -182,7 +182,7 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, out, errOut io.Writer, args ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). - SelectorParam(o.Selector). + LabelSelectorParam(o.Selector). IncludeUninitialized(includeUninitialized). SelectAllParam(o.DeleteAll). ResourceTypeOrNameArgs(false, args...).RequireObject(false). diff --git a/pkg/kubectl/cmd/describe.go b/pkg/kubectl/cmd/describe.go index 91d8306378f..0f69c73164a 100644 --- a/pkg/kubectl/cmd/describe.go +++ b/pkg/kubectl/cmd/describe.go @@ -129,7 +129,7 @@ func RunDescribe(f cmdutil.Factory, out, cmdErr io.Writer, cmd *cobra.Command, a ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces). FilenameParam(enforceNamespace, options). - SelectorParam(selector). + LabelSelectorParam(selector). IncludeUninitialized(includeUninitialized). ResourceTypeOrNameArgs(true, args...). Flatten(). diff --git a/pkg/kubectl/cmd/drain.go b/pkg/kubectl/cmd/drain.go index 19d49c3e6e5..396f5e2eac6 100644 --- a/pkg/kubectl/cmd/drain.go +++ b/pkg/kubectl/cmd/drain.go @@ -243,7 +243,7 @@ func (o *DrainOptions) SetupDrain(cmd *cobra.Command, args []string) error { Flatten() if len(o.Selector) > 0 { - builder = builder.SelectorParam(o.Selector). + builder = builder.LabelSelectorParam(o.Selector). ResourceTypes("nodes") } diff --git a/pkg/kubectl/cmd/label.go b/pkg/kubectl/cmd/label.go index 01166db620d..e53e4b720d2 100644 --- a/pkg/kubectl/cmd/label.go +++ b/pkg/kubectl/cmd/label.go @@ -206,7 +206,7 @@ func (o *LabelOptions) RunLabel(f cmdutil.Factory, cmd *cobra.Command) error { return err } - b = b.SelectorParam(o.selector). + b = b.LabelSelectorParam(o.selector). Unstructured(f.UnstructuredClientForMapping, mapper, typer). ResourceTypeOrNameArgs(o.all, o.resources...). Latest() diff --git a/pkg/kubectl/cmd/logs.go b/pkg/kubectl/cmd/logs.go index 8392c95babc..7cfc93d3df6 100644 --- a/pkg/kubectl/cmd/logs.go +++ b/pkg/kubectl/cmd/logs.go @@ -199,7 +199,7 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, out io.Writer, cmd *cobra.Comm builder.ResourceNames("pods", o.ResourceArg) } if selector != "" { - builder.ResourceTypes("pods").SelectorParam(selector) + builder.ResourceTypes("pods").LabelSelectorParam(selector) } infos, err := builder.Do().Infos() if err != nil { diff --git a/pkg/kubectl/cmd/resource/get.go b/pkg/kubectl/cmd/resource/get.go index c632f259dff..96931118393 100644 --- a/pkg/kubectl/cmd/resource/get.go +++ b/pkg/kubectl/cmd/resource/get.go @@ -56,6 +56,7 @@ type GetOptions struct { ChunkSize int64 LabelSelector string + FieldSelector string AllNamespaces bool Namespace string ExplicitNamespace bool @@ -158,6 +159,7 @@ func NewCmdGet(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Comman cmd.Flags().Int64Var(&options.ChunkSize, "chunk-size", 500, "Return large lists in chunks rather than all at once. Pass 0 to disable. This flag is beta and may change in the future.") cmd.Flags().BoolVar(&options.IgnoreNotFound, "ignore-not-found", options.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.") cmd.Flags().StringVarP(&options.LabelSelector, "selector", "l", options.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") + cmd.Flags().StringVar(&options.FieldSelector, "field-selector", options.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") cmd.Flags().BoolVar(&options.AllNamespaces, "all-namespaces", options.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmdutil.AddIncludeUninitializedFlag(cmd) cmdutil.AddPrinterFlags(cmd) @@ -233,7 +235,8 @@ func (options *GetOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []str Unstructured(f.UnstructuredClientForMapping, mapper, typer). NamespaceParam(options.Namespace).DefaultNamespace().AllNamespaces(options.AllNamespaces). FilenameParam(options.ExplicitNamespace, &options.FilenameOptions). - SelectorParam(options.LabelSelector). + LabelSelectorParam(options.LabelSelector). + FieldSelectorParam(options.FieldSelector). ExportParam(options.Export). RequestChunksOf(options.ChunkSize). IncludeUninitialized(cmdutil.ShouldIncludeUninitialized(cmd, false)). // TODO: this needs to be better factored @@ -450,7 +453,8 @@ func (options *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []s Unstructured(f.UnstructuredClientForMapping, mapper, typer). NamespaceParam(options.Namespace).DefaultNamespace().AllNamespaces(options.AllNamespaces). FilenameParam(options.ExplicitNamespace, &options.FilenameOptions). - SelectorParam(options.LabelSelector). + LabelSelectorParam(options.LabelSelector). + FieldSelectorParam(options.FieldSelector). ExportParam(options.Export). RequestChunksOf(options.ChunkSize). IncludeUninitialized(includeUninitialized). diff --git a/pkg/kubectl/cmd/resource/get_test.go b/pkg/kubectl/cmd/resource/get_test.go index 1054004fbe8..7ecfb2c4160 100644 --- a/pkg/kubectl/cmd/resource/get_test.go +++ b/pkg/kubectl/cmd/resource/get_test.go @@ -825,7 +825,7 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) { } } -func TestGetMultipleTypeObjectsWithSelector(t *testing.T) { +func TestGetMultipleTypeObjectsWithLabelSelector(t *testing.T) { pods, svc, _ := testData() f, tf, codec, _ := cmdtesting.NewAPIFactory() @@ -868,6 +868,49 @@ func TestGetMultipleTypeObjectsWithSelector(t *testing.T) { } } +func TestGetMultipleTypeObjectsWithFieldSelector(t *testing.T) { + pods, svc, _ := testData() + + f, tf, codec, _ := cmdtesting.NewAPIFactory() + tf.Printer = &testPrinter{} + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + } + switch req.URL.Path { + case "/namespaces/test/pods": + return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, pods)}, nil + case "/namespaces/test/services": + return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, svc)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdGet(f, buf, errBuf) + cmd.SetOutput(buf) + + cmd.Flags().Set("field-selector", "a=b") + cmd.Run(cmd, []string{"pods,services"}) + + expected, err := extractResourceList([]runtime.Object{pods, svc}) + if err != nil { + t.Fatal(err) + } + verifyObjects(t, expected, tf.Printer.(*testPrinter).Objects) + + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + func TestGetMultipleTypeObjectsWithDirectReference(t *testing.T) { _, svc, _ := testData() node := &api.Node{ @@ -1006,7 +1049,7 @@ func watchTestData() ([]api.Pod, []watch.Event) { return pods, events } -func TestWatchSelector(t *testing.T) { +func TestWatchLabelSelector(t *testing.T) { pods, events := watchTestData() f, tf, codec, _ := cmdtesting.NewAPIFactory() @@ -1054,6 +1097,54 @@ func TestWatchSelector(t *testing.T) { } } +func TestWatchFieldSelector(t *testing.T) { + pods, events := watchTestData() + + f, tf, codec, _ := cmdtesting.NewAPIFactory() + tf.Printer = &testPrinter{} + podList := &api.PodList{ + Items: pods, + ListMeta: metav1.ListMeta{ + ResourceVersion: "10", + }, + } + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + } + switch req.URL.Path { + case "/namespaces/test/pods": + if req.URL.Query().Get("watch") == "true" { + return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: watchBody(codec, events[2:])}, nil + } + return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, podList)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdGet(f, buf, errBuf) + cmd.SetOutput(buf) + + cmd.Flags().Set("watch", "true") + cmd.Flags().Set("field-selector", "a=b") + cmd.Run(cmd, []string{"pods"}) + + expected := []runtime.Object{&pods[0], &pods[1], events[2].Object, events[3].Object} + verifyObjects(t, expected, tf.Printer.(*testPrinter).Objects) + + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + func TestWatchResource(t *testing.T) { pods, events := watchTestData() diff --git a/pkg/kubectl/cmd/scale.go b/pkg/kubectl/cmd/scale.go index de3fef08ffd..f94e445af1f 100644 --- a/pkg/kubectl/cmd/scale.go +++ b/pkg/kubectl/cmd/scale.go @@ -110,7 +110,7 @@ func RunScale(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin FilenameParam(enforceNamespace, options). ResourceTypeOrNameArgs(all, args...). Flatten(). - SelectorParam(selector). + LabelSelectorParam(selector). Do() err = r.Err() if resource.IsUsageError(err) { diff --git a/pkg/kubectl/cmd/set/set_env.go b/pkg/kubectl/cmd/set/set_env.go index 5bb1998cde9..e6a2679715f 100644 --- a/pkg/kubectl/cmd/set/set_env.go +++ b/pkg/kubectl/cmd/set/set_env.go @@ -240,7 +240,7 @@ func (o *EnvOptions) RunEnv(f cmdutil.Factory) error { if !o.Local { b = b. - SelectorParam(o.Selector). + LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, o.From). Latest() } else { @@ -304,7 +304,7 @@ func (o *EnvOptions) RunEnv(f cmdutil.Factory) error { if !o.Local { b = b. - SelectorParam(o.Selector). + LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, o.Resources...). Latest() } else { diff --git a/pkg/kubectl/cmd/set/set_image.go b/pkg/kubectl/cmd/set/set_image.go index f933100df8f..9cac8b68cae 100644 --- a/pkg/kubectl/cmd/set/set_image.go +++ b/pkg/kubectl/cmd/set/set_image.go @@ -149,7 +149,7 @@ func (o *ImageOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st if !o.Local { builder = builder. - SelectorParam(o.Selector). + LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, o.Resources...). Latest() } else { diff --git a/pkg/kubectl/cmd/set/set_resources.go b/pkg/kubectl/cmd/set/set_resources.go index 02c3904f365..3278c6a1733 100644 --- a/pkg/kubectl/cmd/set/set_resources.go +++ b/pkg/kubectl/cmd/set/set_resources.go @@ -153,7 +153,7 @@ func (o *ResourcesOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args if !o.Local { builder = builder. - SelectorParam(o.Selector). + LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, args...). Latest() } else { diff --git a/pkg/kubectl/cmd/set/set_subject.go b/pkg/kubectl/cmd/set/set_subject.go index 0e7684b530a..9ab633310b2 100644 --- a/pkg/kubectl/cmd/set/set_subject.go +++ b/pkg/kubectl/cmd/set/set_subject.go @@ -135,7 +135,7 @@ func (o *SubjectOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [] if !o.Local { builder = builder. - SelectorParam(o.Selector). + LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, args...). Latest() } else { diff --git a/pkg/kubectl/cmd/taint.go b/pkg/kubectl/cmd/taint.go index 04578bbee95..3aae8aeb11d 100644 --- a/pkg/kubectl/cmd/taint.go +++ b/pkg/kubectl/cmd/taint.go @@ -152,7 +152,7 @@ func (o *TaintOptions) Complete(f cmdutil.Factory, out io.Writer, cmd *cobra.Com ContinueOnError(). NamespaceParam(namespace).DefaultNamespace() if o.selector != "" { - o.builder = o.builder.SelectorParam(o.selector).ResourceTypes("node") + o.builder = o.builder.LabelSelectorParam(o.selector).ResourceTypes("node") } if o.all { o.builder = o.builder.SelectAllParam(o.all).ResourceTypes("node").Flatten().Latest() @@ -160,7 +160,7 @@ func (o *TaintOptions) Complete(f cmdutil.Factory, out io.Writer, cmd *cobra.Com if !o.all && o.selector == "" && len(o.resources) >= 2 { o.builder = o.builder.ResourceNames("node", o.resources[1:]...) } - o.builder = o.builder.SelectorParam(o.selector). + o.builder = o.builder.LabelSelectorParam(o.selector). Flatten(). Latest() o.f = f diff --git a/pkg/kubectl/resource/builder.go b/pkg/kubectl/resource/builder.go index ab1757baab2..08b63ad7723 100644 --- a/pkg/kubectl/resource/builder.go +++ b/pkg/kubectl/resource/builder.go @@ -53,7 +53,8 @@ type Builder struct { stream bool dir bool - selector *string + labelSelector *string + fieldSelector *string selectAll bool includeUninitialized bool limitChunks int64 @@ -274,24 +275,41 @@ func (b *Builder) ResourceNames(resource string, names ...string) *Builder { return b } -// SelectorParam defines a selector that should be applied to the object types to load. +// LabelSelectorParam 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.String)`. -func (b *Builder) SelectorParam(s string) *Builder { +// a no-op - to select all resources invoke `b.LabelSelector(labels.Everything.String)`. +func (b *Builder) LabelSelectorParam(s string) *Builder { selector := strings.TrimSpace(s) if len(selector) == 0 { return b } if b.selectAll { - b.errs = append(b.errs, fmt.Errorf("found non empty selector %q with previously set 'all' parameter. ", s)) + b.errs = append(b.errs, fmt.Errorf("found non-empty label selector %q with previously set 'all' parameter. ", s)) return b } - return b.Selector(selector) + return b.LabelSelector(selector) } -// Selector accepts a selector directly, and if non nil will trigger a list action. -func (b *Builder) Selector(selector string) *Builder { - b.selector = &selector +// LabelSelector accepts a selector directly and will filter the resulting list by that object. +// Use LabelSelectorParam instead for user input. +func (b *Builder) LabelSelector(selector string) *Builder { + b.labelSelector = &selector + return b +} + +// FieldSelectorParam 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. +func (b *Builder) FieldSelectorParam(s string) *Builder { + s = strings.TrimSpace(s) + if len(s) == 0 { + return b + } + if b.selectAll { + b.errs = append(b.errs, fmt.Errorf("found non-empty field selector %q with previously set 'all' parameter. ", s)) + return b + } + b.fieldSelector = &s return b } @@ -349,7 +367,7 @@ func (b *Builder) RequestChunksOf(chunkSize int64) *Builder { // SelectEverythingParam func (b *Builder) SelectAllParam(selectAll bool) *Builder { - if selectAll && b.selector != nil { + if selectAll && (b.labelSelector != nil || b.fieldSelector != nil) { b.errs = append(b.errs, fmt.Errorf("setting 'all' parameter but found a non empty selector. ")) return b } @@ -394,9 +412,9 @@ func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string b.ResourceTypes(SplitResourceArgument(args[0])...) case len(args) == 1: b.ResourceTypes(SplitResourceArgument(args[0])...) - if b.selector == nil && allowEmptySelector { + if b.labelSelector == nil && allowEmptySelector { selector := labels.Everything().String() - b.selector = &selector + b.labelSelector = &selector } case len(args) == 0: default: @@ -585,7 +603,7 @@ func (b *Builder) visitorResult() *Result { if b.selectAll { selector := labels.Everything().String() - b.selector = &selector + b.labelSelector = &selector } // visit items specified by paths @@ -594,7 +612,7 @@ func (b *Builder) visitorResult() *Result { } // visit selectors - if b.selector != nil { + if b.labelSelector != nil || b.fieldSelector != nil { return b.visitBySelector() } @@ -634,6 +652,14 @@ func (b *Builder) visitBySelector() *Result { return result } + var labelSelector, fieldSelector string + if b.labelSelector != nil { + labelSelector = *b.labelSelector + } + if b.fieldSelector != nil { + fieldSelector = *b.fieldSelector + } + visitors := []Visitor{} for _, mapping := range mappings { client, err := b.mapper.ClientForMapping(mapping) @@ -645,7 +671,7 @@ func (b *Builder) visitBySelector() *Result { if mapping.Scope.Name() != meta.RESTScopeNameNamespace { selectorNamespace = "" } - visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, *b.selector, b.export, b.includeUninitialized, b.limitChunks)) + visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, labelSelector, fieldSelector, b.export, b.includeUninitialized, b.limitChunks)) } if b.continueOnError { result.visitor = EagerVisitorList(visitors) @@ -820,12 +846,12 @@ func (b *Builder) visitByPaths() *Result { } visitors = NewDecoratedVisitor(visitors, RetrieveLatest) } - if b.selector != nil { - selector, err := labels.Parse(*b.selector) + if b.labelSelector != nil { + selector, err := labels.Parse(*b.labelSelector) if err != nil { - return result.withError(fmt.Errorf("the provided selector %q is not valid: %v", b.selector, err)) + return result.withError(fmt.Errorf("the provided selector %q is not valid: %v", b.labelSelector, err)) } - visitors = NewFilteredVisitor(visitors, FilterBySelector(selector)) + visitors = NewFilteredVisitor(visitors, FilterByLabelSelector(selector)) } result.visitor = visitors result.sources = b.paths diff --git a/pkg/kubectl/resource/builder_test.go b/pkg/kubectl/resource/builder_test.go index 31664f29891..ec21bf67f1b 100644 --- a/pkg/kubectl/resource/builder_test.go +++ b/pkg/kubectl/resource/builder_test.go @@ -697,7 +697,7 @@ func TestResourceByNameAndEmptySelector(t *testing.T) { "/namespaces/test/pods/foo": runtime.EncodeOrDie(corev1Codec, &pods.Items[0]), }), corev1Codec). NamespaceParam("test"). - SelectorParam(""). + LabelSelectorParam(""). ResourceTypeOrNameArgs(true, "pods", "foo") singleItemImplied := false @@ -718,14 +718,14 @@ func TestResourceByNameAndEmptySelector(t *testing.T) { } } -func TestSelector(t *testing.T) { +func TestLabelSelector(t *testing.T) { pods, svc := testData() labelKey := metav1.LabelSelectorQueryParam(corev1GV.String()) b := NewBuilder(restmapper, LegacyCategoryExpander, scheme.Scheme, fakeClientWith("", t, map[string]string{ "/namespaces/test/pods?" + labelKey + "=a%3Db": runtime.EncodeOrDie(corev1Codec, pods), "/namespaces/test/services?" + labelKey + "=a%3Db": runtime.EncodeOrDie(corev1Codec, svc), }), corev1Codec). - SelectorParam("a=b"). + LabelSelectorParam("a=b"). NamespaceParam("test"). Flatten() @@ -751,9 +751,53 @@ func TestSelector(t *testing.T) { } } -func TestSelectorRequiresKnownTypes(t *testing.T) { +func TestLabelSelectorRequiresKnownTypes(t *testing.T) { b := NewBuilder(restmapper, LegacyCategoryExpander, scheme.Scheme, fakeClient(), corev1Codec). - SelectorParam("a=b"). + LabelSelectorParam("a=b"). + NamespaceParam("test"). + ResourceTypes("unknown") + + if b.Do().Err() == nil { + t.Errorf("unexpected non-error") + } +} + +func TestFieldSelector(t *testing.T) { + pods, svc := testData() + fieldKey := metav1.FieldSelectorQueryParam(corev1GV.String()) + b := NewBuilder(restmapper, LegacyCategoryExpander, scheme.Scheme, fakeClientWith("", t, map[string]string{ + "/namespaces/test/pods?" + fieldKey + "=a%3Db": runtime.EncodeOrDie(corev1Codec, pods), + "/namespaces/test/services?" + fieldKey + "=a%3Db": runtime.EncodeOrDie(corev1Codec, svc), + }), corev1Codec). + FieldSelectorParam("a=b"). + NamespaceParam("test"). + Flatten() + + test := &testVisitor{} + singleItemImplied := false + + if b.Do().Err() == nil { + t.Errorf("unexpected non-error") + } + + b.ResourceTypeOrNameArgs(true, "pods,service") + + err := b.Do().IntoSingleItemImplied(&singleItemImplied).Visit(test.Handle) + if err != nil || singleItemImplied || len(test.Infos) != 3 { + t.Fatalf("unexpected response: %v %t %#v", err, singleItemImplied, test.Infos) + } + if !apiequality.Semantic.DeepDerivative([]runtime.Object{&pods.Items[0], &pods.Items[1], &svc.Items[0]}, test.Objects()) { + t.Errorf("unexpected visited objects: %#v", test.Objects()) + } + + if _, err := b.Do().ResourceMapping(); err == nil { + t.Errorf("unexpected non-error") + } +} + +func TestFieldSelectorRequiresKnownTypes(t *testing.T) { + b := NewBuilder(restmapper, LegacyCategoryExpander, scheme.Scheme, fakeClient(), corev1Codec). + FieldSelectorParam("a=b"). NamespaceParam("test"). ResourceTypes("unknown") @@ -764,7 +808,7 @@ func TestSelectorRequiresKnownTypes(t *testing.T) { func TestSingleResourceType(t *testing.T) { b := NewBuilder(restmapper, LegacyCategoryExpander, scheme.Scheme, fakeClient(), corev1Codec). - SelectorParam("a=b"). + LabelSelectorParam("a=b"). SingleResourceType(). ResourceTypeOrNameArgs(true, "pods,services") @@ -1019,7 +1063,7 @@ func TestListObject(t *testing.T) { b := NewBuilder(restmapper, LegacyCategoryExpander, scheme.Scheme, fakeClientWith("", t, map[string]string{ "/namespaces/test/pods?" + labelKey + "=a%3Db": runtime.EncodeOrDie(corev1Codec, pods), }), corev1Codec). - SelectorParam("a=b"). + LabelSelectorParam("a=b"). NamespaceParam("test"). ResourceTypeOrNameArgs(true, "pods"). Flatten() @@ -1053,7 +1097,7 @@ func TestListObjectWithDifferentVersions(t *testing.T) { "/namespaces/test/pods?" + labelKey + "=a%3Db": runtime.EncodeOrDie(corev1Codec, pods), "/namespaces/test/services?" + labelKey + "=a%3Db": runtime.EncodeOrDie(corev1Codec, svc), }), corev1Codec). - SelectorParam("a=b"). + LabelSelectorParam("a=b"). NamespaceParam("test"). ResourceTypeOrNameArgs(true, "pods,services"). Flatten(). diff --git a/pkg/kubectl/resource/helper.go b/pkg/kubectl/resource/helper.go index ddece216a5b..e5ca36221ed 100644 --- a/pkg/kubectl/resource/helper.go +++ b/pkg/kubectl/resource/helper.go @@ -75,15 +75,12 @@ func (m *Helper) List(namespace, apiVersion string, export bool, options *metav1 return req.Do().Get() } -func (m *Helper) Watch(namespace, resourceVersion, apiVersion string, labelSelector string) (watch.Interface, error) { +func (m *Helper) Watch(namespace, apiVersion string, options *metav1.ListOptions) (watch.Interface, error) { + options.Watch = true return m.RESTClient.Get(). NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). - VersionedParams(&metav1.ListOptions{ - ResourceVersion: resourceVersion, - Watch: true, - LabelSelector: labelSelector, - }, metav1.ParameterCodec). + VersionedParams(options, metav1.ParameterCodec). Watch() } diff --git a/pkg/kubectl/resource/helper_test.go b/pkg/kubectl/resource/helper_test.go index 55be32a766c..96a2d7789ca 100644 --- a/pkg/kubectl/resource/helper_test.go +++ b/pkg/kubectl/resource/helper_test.go @@ -385,6 +385,72 @@ func TestHelperList(t *testing.T) { } } +func TestHelperListSelectorCombination(t *testing.T) { + tests := []struct { + Name string + Err bool + ErrMsg string + FieldSelector string + LabelSelector string + }{ + { + Name: "No selector", + Err: false, + }, + { + Name: "Only Label Selector", + Err: false, + LabelSelector: "foo=baz", + }, + { + Name: "Only Field Selector", + Err: false, + FieldSelector: "xyz=zyx", + }, + { + Name: "Both Label and Field Selector", + Err: false, + LabelSelector: "foo=baz", + FieldSelector: "xyz=zyx", + }, + } + + resp := &http.Response{ + StatusCode: http.StatusOK, + Header: header(), + Body: objBody(&corev1.PodList{ + Items: []corev1.Pod{{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + }, + }, + }), + } + client := &fake.RESTClient{ + NegotiatedSerializer: scheme.Codecs, + Resp: resp, + Err: nil, + } + modifier := &Helper{ + RESTClient: client, + NamespaceScoped: true, + } + + for _, test := range tests { + _, err := modifier.List("bar", + corev1GV.String(), + false, + &metav1.ListOptions{LabelSelector: test.LabelSelector, FieldSelector: test.FieldSelector}) + if test.Err { + if err == nil { + t.Errorf("%q expected error: %q", test.Name, test.ErrMsg) + } + if err != nil && err.Error() != test.ErrMsg { + t.Errorf("%q expected error: %q", test.Name, test.ErrMsg) + } + } + } +} + func TestHelperReplace(t *testing.T) { expectPut := func(path string, req *http.Request) bool { if req.Method != "PUT" { diff --git a/pkg/kubectl/resource/selector.go b/pkg/kubectl/resource/selector.go index 71a9d5886a1..c46d39ac8ac 100644 --- a/pkg/kubectl/resource/selector.go +++ b/pkg/kubectl/resource/selector.go @@ -30,19 +30,21 @@ type Selector struct { Client RESTClient Mapping *meta.RESTMapping Namespace string - Selector string + LabelSelector string + FieldSelector string Export bool IncludeUninitialized bool LimitChunks int64 } // NewSelector creates a resource selector which hides details of getting items by their label selector. -func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace string, selector string, export, includeUninitialized bool, limitChunks int64) *Selector { +func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace, labelSelector, fieldSelector string, export, includeUninitialized bool, limitChunks int64) *Selector { return &Selector{ Client: client, Mapping: mapping, Namespace: namespace, - Selector: selector, + LabelSelector: labelSelector, + FieldSelector: fieldSelector, Export: export, IncludeUninitialized: includeUninitialized, LimitChunks: limitChunks, @@ -58,7 +60,8 @@ func (r *Selector) Visit(fn VisitorFunc) error { r.ResourceMapping().GroupVersionKind.GroupVersion().String(), r.Export, &metav1.ListOptions{ - LabelSelector: r.Selector, + LabelSelector: r.LabelSelector, + FieldSelector: r.FieldSelector, IncludeUninitialized: r.IncludeUninitialized, Limit: r.LimitChunks, Continue: continueToken, @@ -71,17 +74,17 @@ func (r *Selector) Visit(fn VisitorFunc) error { if errors.IsBadRequest(err) || errors.IsNotFound(err) { if se, ok := err.(*errors.StatusError); ok { // modify the message without hiding this is an API error - if len(r.Selector) == 0 { + if len(r.LabelSelector) == 0 && len(r.FieldSelector) == 0 { se.ErrStatus.Message = fmt.Sprintf("Unable to list %q: %v", r.Mapping.Resource, se.ErrStatus.Message) } else { - se.ErrStatus.Message = fmt.Sprintf("Unable to find %q that match the selector %q: %v", r.Mapping.Resource, r.Selector, se.ErrStatus.Message) + se.ErrStatus.Message = fmt.Sprintf("Unable to find %q that match label selector %q, field selector %q: %v", r.Mapping.Resource, r.LabelSelector, r.FieldSelector, se.ErrStatus.Message) } return se } - if len(r.Selector) == 0 { + if len(r.LabelSelector) == 0 && len(r.FieldSelector) == 0 { return fmt.Errorf("Unable to list %q: %v", r.Mapping.Resource, err) } - return fmt.Errorf("Unable to find %q that match the selector %q: %v", r.Mapping.Resource, r.Selector, err) + return fmt.Errorf("Unable to find %q that match label selector %q, field selector %q: %v", r.Mapping.Resource, r.LabelSelector, r.FieldSelector, err) } return err } @@ -107,7 +110,8 @@ func (r *Selector) Visit(fn VisitorFunc) error { } func (r *Selector) Watch(resourceVersion string) (watch.Interface, error) { - return NewHelper(r.Client, r.Mapping).Watch(r.Namespace, resourceVersion, r.ResourceMapping().GroupVersionKind.GroupVersion().String(), r.Selector) + return NewHelper(r.Client, r.Mapping).Watch(r.Namespace, r.ResourceMapping().GroupVersionKind.GroupVersion().String(), + &metav1.ListOptions{ResourceVersion: resourceVersion, LabelSelector: r.LabelSelector, FieldSelector: r.FieldSelector}) } // ResourceMapping returns the mapping for this resource and implements ResourceMapping diff --git a/pkg/kubectl/resource/visitor.go b/pkg/kubectl/resource/visitor.go index 5e5a73a3160..824f0132b2a 100644 --- a/pkg/kubectl/resource/visitor.go +++ b/pkg/kubectl/resource/visitor.go @@ -691,7 +691,7 @@ func (v FilteredVisitor) Visit(fn VisitorFunc) error { }) } -func FilterBySelector(s labels.Selector) FilterFunc { +func FilterByLabelSelector(s labels.Selector) FilterFunc { return func(info *Info, err error) (bool, error) { if err != nil { return false, err