diff --git a/.generated_docs b/.generated_docs index ea5fb67eaa6..a5895bc7816 100644 --- a/.generated_docs +++ b/.generated_docs @@ -80,6 +80,7 @@ docs/man/man1/kubectl-run.1 docs/man/man1/kubectl-scale.1 docs/man/man1/kubectl-set-image.1 docs/man/man1/kubectl-set-resources.1 +docs/man/man1/kubectl-set-selector.1 docs/man/man1/kubectl-set.1 docs/man/man1/kubectl-stop.1 docs/man/man1/kubectl-taint.1 @@ -162,6 +163,7 @@ docs/user-guide/kubectl/kubectl_scale.md docs/user-guide/kubectl/kubectl_set.md docs/user-guide/kubectl/kubectl_set_image.md docs/user-guide/kubectl/kubectl_set_resources.md +docs/user-guide/kubectl/kubectl_set_selector.md docs/user-guide/kubectl/kubectl_taint.md docs/user-guide/kubectl/kubectl_top.md docs/user-guide/kubectl/kubectl_top_node.md diff --git a/docs/man/man1/kubectl-set-selector.1 b/docs/man/man1/kubectl-set-selector.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-set-selector.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/user-guide/kubectl/kubectl_set_selector.md b/docs/user-guide/kubectl/kubectl_set_selector.md new file mode 100644 index 00000000000..68fc283dc99 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_set_selector.md @@ -0,0 +1,7 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_set_selector.md?pixel)]() + diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index 07e35906a57..aceadb4c6a4 100755 --- a/hack/make-rules/test-cmd.sh +++ b/hack/make-rules/test-cmd.sh @@ -1933,6 +1933,19 @@ __EOF__ # Describe command should print events information when show-events=true kube::test::describe_resource_events_assert services true + ### set selector + # prove role=master + kube::test::get_object_assert 'services redis-master' "{{range$service_selector_field}}{{.}}:{{end}}" "redis:master:backend:" + + # Set command to change the selector. + kubectl set selector -f examples/guestbook/redis-master-service.yaml role=padawan + # prove role=padawan + kube::test::get_object_assert 'services redis-master' "{{range$service_selector_field}}{{.}}:{{end}}" "padawan:" + # Set command to reset the selector back to the original one. + kubectl set selector -f examples/guestbook/redis-master-service.yaml app=redis,role=master,tier=backend + # prove role=master + kube::test::get_object_assert 'services redis-master' "{{range$service_selector_field}}{{.}}:{{end}}" "redis:master:backend:" + ### Dump current redis-master service output_service=$(kubectl get service redis-master -o json --output-version=v1 "${kube_flags[@]}") diff --git a/pkg/kubectl/cmd/rollout/rollout_pause.go b/pkg/kubectl/cmd/rollout/rollout_pause.go index 4e02d384eaf..94c25579778 100644 --- a/pkg/kubectl/cmd/rollout/rollout_pause.go +++ b/pkg/kubectl/cmd/rollout/rollout_pause.go @@ -38,7 +38,7 @@ import ( type PauseConfig struct { resource.FilenameOptions - Pauser func(info *resource.Info) (bool, error) + Pauser func(info *resource.Info) ([]byte, error) Mapper meta.RESTMapper Typer runtime.ObjectTyper Encoder runtime.Encoder diff --git a/pkg/kubectl/cmd/rollout/rollout_resume.go b/pkg/kubectl/cmd/rollout/rollout_resume.go index 56623934083..702cd2ac78a 100644 --- a/pkg/kubectl/cmd/rollout/rollout_resume.go +++ b/pkg/kubectl/cmd/rollout/rollout_resume.go @@ -38,7 +38,7 @@ import ( type ResumeConfig struct { resource.FilenameOptions - Resumer func(object *resource.Info) (bool, error) + Resumer func(object *resource.Info) ([]byte, error) Mapper meta.RESTMapper Typer runtime.ObjectTyper Encoder runtime.Encoder diff --git a/pkg/kubectl/cmd/set/BUILD b/pkg/kubectl/cmd/set/BUILD index ad0de7530a4..2caf3bb236e 100644 --- a/pkg/kubectl/cmd/set/BUILD +++ b/pkg/kubectl/cmd/set/BUILD @@ -15,12 +15,14 @@ go_library( "set.go", "set_image.go", "set_resources.go", + "set_selector.go", ], tags = ["automanaged"], deps = [ "//pkg/api:go_default_library", "//pkg/api/errors:go_default_library", "//pkg/api/meta:go_default_library", + "//pkg/apis/meta/v1:go_default_library", "//pkg/kubectl:go_default_library", "//pkg/kubectl/cmd/templates:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", @@ -36,6 +38,7 @@ go_test( name = "go_default_test", srcs = [ "set_image_test.go", + "set_selector_test.go", "set_test.go", ], data = [ @@ -46,11 +49,16 @@ go_test( deps = [ "//pkg/api:go_default_library", "//pkg/apimachinery/registered:go_default_library", + "//pkg/apis/batch:go_default_library", + "//pkg/apis/extensions:go_default_library", + "//pkg/apis/meta/v1:go_default_library", "//pkg/client/restclient:go_default_library", "//pkg/client/restclient/fake:go_default_library", "//pkg/kubectl/cmd/testing:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", "//pkg/kubectl/resource:go_default_library", + "//pkg/runtime:go_default_library", "//vendor:github.com/spf13/cobra", + "//vendor:github.com/stretchr/testify/assert", ], ) diff --git a/pkg/kubectl/cmd/set/helper.go b/pkg/kubectl/cmd/set/helper.go index 7b4a380a5c5..4d986db457a 100644 --- a/pkg/kubectl/cmd/set/helper.go +++ b/pkg/kubectl/cmd/set/helper.go @@ -117,46 +117,44 @@ type Patch struct { Patch []byte } -// CalculatePatches calls the mutation function on each provided info object, and generates a strategic merge patch for -// the changes in the object. Encoder must be able to encode the info into the appropriate destination type. If mutateFn -// returns false, the object is not included in the final list of patches. -func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn func(*resource.Info) (bool, error)) []*Patch { +// patchFn is a function type that accepts an info object and returns a byte slice. +// Implementations of patchFn should update the object and return it encoded. +type patchFn func(*resource.Info) ([]byte, error) + +// CalculatePatch calls the mutation function on the provided info object, and generates a strategic merge patch for +// the changes in the object. Encoder must be able to encode the info into the appropriate destination type. +// This function returns whether the mutation function made any change in the original object. +func CalculatePatch(patch *Patch, encoder runtime.Encoder, mutateFn patchFn) bool { + patch.Before, patch.Err = runtime.Encode(encoder, patch.Info.Object) + + patch.After, patch.Err = mutateFn(patch.Info) + if patch.Err != nil { + return true + } + if patch.After == nil { + return false + } + + // TODO: should be via New + versioned, err := patch.Info.Mapping.ConvertToVersion(patch.Info.Object, patch.Info.Mapping.GroupVersionKind.GroupVersion()) + if err != nil { + patch.Err = err + return true + } + + patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, versioned) + return true +} + +// CalculatePatches calculates patches on each provided info object. If the provided mutateFn +// makes no change in an object, the object is not included in the final list of patches. +func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn patchFn) []*Patch { var patches []*Patch for _, info := range infos { patch := &Patch{Info: info} - patch.Before, patch.Err = runtime.Encode(encoder, info.Object) - if patch.Err != nil { + if CalculatePatch(patch, encoder, mutateFn) { patches = append(patches, patch) - continue } - - ok, err := mutateFn(info) - if err != nil { - patch.Err = err - patches = append(patches, patch) - continue - } - if !ok { - continue - } - patches = append(patches, patch) - if patch.Err != nil { - continue - } - - patch.After, patch.Err = runtime.Encode(encoder, info.Object) - if patch.Err != nil { - continue - } - - // TODO: should be via New - versioned, err := info.Mapping.ConvertToVersion(info.Object, info.Mapping.GroupVersionKind.GroupVersion()) - if err != nil { - patch.Err = err - continue - } - - patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, versioned) } return patches } diff --git a/pkg/kubectl/cmd/set/set.go b/pkg/kubectl/cmd/set/set.go index 64bcb6fa139..bb2fb28ddeb 100644 --- a/pkg/kubectl/cmd/set/set.go +++ b/pkg/kubectl/cmd/set/set.go @@ -42,6 +42,7 @@ func NewCmdSet(f cmdutil.Factory, out, err io.Writer) *cobra.Command { // add subcommands cmd.AddCommand(NewCmdImage(f, out, err)) cmd.AddCommand(NewCmdResources(f, out, err)) + cmd.AddCommand(NewCmdSelector(f, out)) return cmd } diff --git a/pkg/kubectl/cmd/set/set_image.go b/pkg/kubectl/cmd/set/set_image.go index 9fe5b8a2a38..33d10c31aec 100644 --- a/pkg/kubectl/cmd/set/set_image.go +++ b/pkg/kubectl/cmd/set/set_image.go @@ -169,7 +169,7 @@ func (o *ImageOptions) Validate() error { func (o *ImageOptions) Run() error { allErrs := []error{} - patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) (bool, error) { + patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) ([]byte, error) { transformed := false _, err := o.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error { for name, image := range o.ContainerImages { @@ -205,7 +205,10 @@ func (o *ImageOptions) Run() error { } return nil }) - return transformed, err + if transformed && err == nil { + return runtime.Encode(o.Encoder, info.Object) + } + return nil, err }) for _, patch := range patches { diff --git a/pkg/kubectl/cmd/set/set_resources.go b/pkg/kubectl/cmd/set/set_resources.go index 8885a6580a9..adaf5f85812 100644 --- a/pkg/kubectl/cmd/set/set_resources.go +++ b/pkg/kubectl/cmd/set/set_resources.go @@ -174,7 +174,7 @@ func (o *ResourcesOptions) Validate() error { func (o *ResourcesOptions) Run() error { allErrs := []error{} - patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) (bool, error) { + patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) ([]byte, error) { transformed := false _, err := o.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error { containers, _ := selectContainers(spec.Containers, o.ContainerSelector) @@ -200,7 +200,10 @@ func (o *ResourcesOptions) Run() error { } return nil }) - return transformed, err + if transformed && err == nil { + return runtime.Encode(o.Encoder, info.Object) + } + return nil, err }) for _, patch := range patches { diff --git a/pkg/kubectl/cmd/set/set_selector.go b/pkg/kubectl/cmd/set/set_selector.go new file mode 100644 index 00000000000..e1bac951280 --- /dev/null +++ b/pkg/kubectl/cmd/set/set_selector.go @@ -0,0 +1,221 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/meta" + metav1 "k8s.io/kubernetes/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" +) + +// SelectorOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of +// referencing the cmd.Flags() +type SelectorOptions struct { + fileOptions resource.FilenameOptions + + local bool + dryrun bool + all bool + record bool + changeCause string + + resources []string + selector *metav1.LabelSelector + + out io.Writer + PrintObject func(obj runtime.Object) error + ClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) + + builder *resource.Builder + mapper meta.RESTMapper + encoder runtime.Encoder +} + +var ( + selectorLong = templates.LongDesc(` + Set the selector on a resource. Note that the new selector will overwrite the old selector if the resource had one prior to the invocation + of 'set selector'. + + A selector must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters. + If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used. + Note: currently selectors can only be set on Service objects.`) + selectorExample = templates.Examples(` + # set the labels and selector before creating a deployment/service pair. + kubectl create service clusterip my-svc -o yaml --dry-run | kubectl set selector --local -f - 'environment=qa' -o yaml | kubectl create -f - + kubectl create deployment my-dep -o yaml --dry-run | kubectl label --local -f - environment=qa -o yaml | kubectl create -f -`) +) + +// NewCmdSelector is the "set selector" command. +func NewCmdSelector(f cmdutil.Factory, out io.Writer) *cobra.Command { + options := &SelectorOptions{ + out: out, + } + + cmd := &cobra.Command{ + Use: "selector (-f FILENAME | TYPE NAME) EXPRESSIONS [--resource-version=version]", + Short: "Set the selector on a resource", + Long: fmt.Sprintf(selectorLong), + Example: selectorExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(options.Complete(f, cmd, args, out)) + cmdutil.CheckErr(options.Validate()) + cmdutil.CheckErr(options.RunSelector()) + }, + } + cmdutil.AddPrinterFlags(cmd) + cmd.Flags().Bool("all", false, "Select all resources in the namespace of the specified resource types") + cmd.Flags().Bool("local", false, "If true, set selector will NOT contact api-server but run locally.") + cmd.Flags().String("resource-version", "", "If non-empty, the selectors update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.") + usage := "the resource to update the selectors" + cmdutil.AddFilenameOptionFlags(cmd, &options.fileOptions, usage) + cmdutil.AddDryRunFlag(cmd) + cmdutil.AddRecordFlag(cmd) + + return cmd +} + +// Complete assigns the SelectorOptions from args. +func (o *SelectorOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string, out io.Writer) error { + o.local = cmdutil.GetFlagBool(cmd, "local") + o.all = cmdutil.GetFlagBool(cmd, "all") + o.record = cmdutil.GetRecordFlag(cmd) + o.dryrun = cmdutil.GetDryRunFlag(cmd) + + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + o.changeCause = f.Command() + mapper, _ := f.Object() + o.mapper = mapper + o.encoder = f.JSONEncoder() + + o.builder = f.NewBuilder(). + ContinueOnError(). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, &o.fileOptions). + Flatten() + + o.PrintObject = func(obj runtime.Object) error { + return f.PrintObject(cmd, mapper, obj, o.out) + } + o.ClientForMapping = func(mapping *meta.RESTMapping) (resource.RESTClient, error) { + return f.ClientForMapping(mapping) + } + + o.resources, o.selector, err = getResourcesAndSelector(args) + return err +} + +// Validate basic inputs +func (o *SelectorOptions) Validate() error { + if len(o.resources) < 1 && cmdutil.IsFilenameEmpty(o.fileOptions.Filenames) { + return fmt.Errorf("one or more resources must be specified as or /") + } + return nil +} + +// RunSelector executes the command. +func (o *SelectorOptions) RunSelector() error { + if !o.local { + o.builder = o.builder.ResourceTypeOrNameArgs(o.all, o.resources...). + Latest() + } + r := o.builder.Do() + err := r.Err() + if err != nil { + return err + } + + return r.Visit(func(info *resource.Info, err error) error { + patch := &Patch{Info: info} + CalculatePatch(patch, o.encoder, func(info *resource.Info) ([]byte, error) { + selectErr := updateSelectorForObject(info.Object, *o.selector) + + if selectErr == nil { + return runtime.Encode(o.encoder, info.Object) + } + return nil, selectErr + }) + + if patch.Err != nil { + return patch.Err + } + if o.local || o.dryrun { + fmt.Fprintln(o.out, "running in local/dry-run mode...") + o.PrintObject(info.Object) + return nil + } + + patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch.Patch) + if err != nil { + return err + } + + if o.record || cmdutil.ContainsChangeCause(info) { + if err := cmdutil.RecordChangeCause(patched, o.changeCause); err == nil { + if patched, err = resource.NewHelper(info.Client, info.Mapping).Replace(info.Namespace, info.Name, false, patched); err != nil { + return fmt.Errorf("changes to %s/%s can't be recorded: %v\n", info.Mapping.Resource, info.Name, err) + } + } + } + + info.Refresh(patched, true) + cmdutil.PrintSuccess(o.mapper, false, o.out, info.Mapping.Resource, info.Name, o.dryrun, "selector updated") + return nil + }) +} + +func updateSelectorForObject(obj runtime.Object, selector metav1.LabelSelector) error { + copyOldSelector := func() (map[string]string, error) { + if len(selector.MatchExpressions) > 0 { + return nil, fmt.Errorf("match expression %v not supported on this object", selector.MatchExpressions) + } + dst := make(map[string]string) + for label, value := range selector.MatchLabels { + dst[label] = value + } + return dst, nil + } + var err error + switch t := obj.(type) { + case *api.Service: + t.Spec.Selector, err = copyOldSelector() + default: + err = fmt.Errorf("setting a selector is only supported for Services") + } + return err +} + +// getResourcesAndSelector retrieves resources and the selector expression from the given args (assuming selectors the last arg) +func getResourcesAndSelector(args []string) (resources []string, selector *metav1.LabelSelector, err error) { + if len(args) > 1 { + resources = args[:len(args)-1] + } + selector, err = metav1.ParseToLabelSelector(args[len(args)-1]) + return resources, selector, err +} diff --git a/pkg/kubectl/cmd/set/set_selector_test.go b/pkg/kubectl/cmd/set/set_selector_test.go new file mode 100644 index 00000000000..39018817396 --- /dev/null +++ b/pkg/kubectl/cmd/set/set_selector_test.go @@ -0,0 +1,302 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/batch" + "k8s.io/kubernetes/pkg/apis/extensions" + metav1 "k8s.io/kubernetes/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/runtime" +) + +func TestUpdateSelectorForObjectTypes(t *testing.T) { + before := metav1.LabelSelector{MatchLabels: map[string]string{"fee": "true"}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"on", "yes"}, + }, + }} + + rc := api.ReplicationController{} + ser := api.Service{} + dep := extensions.Deployment{Spec: extensions.DeploymentSpec{Selector: &before}} + ds := extensions.DaemonSet{Spec: extensions.DaemonSetSpec{Selector: &before}} + rs := extensions.ReplicaSet{Spec: extensions.ReplicaSetSpec{Selector: &before}} + job := batch.Job{Spec: batch.JobSpec{Selector: &before}} + pvc := api.PersistentVolumeClaim{Spec: api.PersistentVolumeClaimSpec{Selector: &before}} + sa := api.ServiceAccount{} + type args struct { + obj runtime.Object + selector metav1.LabelSelector + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "rc", + args: args{ + obj: &rc, + selector: metav1.LabelSelector{}, + }, + wantErr: true, + }, + {name: "ser", + args: args{ + obj: &ser, + selector: metav1.LabelSelector{}, + }, + wantErr: false, + }, + {name: "dep", + args: args{ + obj: &dep, + selector: metav1.LabelSelector{}, + }, + wantErr: true, + }, + {name: "ds", + args: args{ + obj: &ds, + selector: metav1.LabelSelector{}, + }, + wantErr: true, + }, + {name: "rs", + args: args{ + obj: &rs, + selector: metav1.LabelSelector{}, + }, + wantErr: true, + }, + {name: "job", + args: args{ + obj: &job, + selector: metav1.LabelSelector{}, + }, + wantErr: true, + }, + {name: "pvc - no updates", + args: args{ + obj: &pvc, + selector: metav1.LabelSelector{}, + }, + wantErr: true, + }, + {name: "sa - no selector", + args: args{ + obj: &sa, + selector: metav1.LabelSelector{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr { + t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr) + } + } +} + +func TestUpdateNewSelectorValuesForObject(t *testing.T) { + ser := api.Service{} + type args struct { + obj runtime.Object + selector metav1.LabelSelector + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "empty", + args: args{ + obj: &ser, + selector: metav1.LabelSelector{ + MatchLabels: map[string]string{}, + MatchExpressions: []metav1.LabelSelectorRequirement{}, + }, + }, + wantErr: false, + }, + {name: "label-only", + args: args{ + obj: &ser, + selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"b": "u"}, + MatchExpressions: []metav1.LabelSelectorRequirement{}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr { + t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr) + } + + assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name) + + } +} + +func TestUpdateOldSelectorValuesForObject(t *testing.T) { + ser := api.Service{Spec: api.ServiceSpec{Selector: map[string]string{"fee": "true"}}} + type args struct { + obj runtime.Object + selector metav1.LabelSelector + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "empty", + args: args{ + obj: &ser, + selector: metav1.LabelSelector{ + MatchLabels: map[string]string{}, + MatchExpressions: []metav1.LabelSelectorRequirement{}, + }, + }, + wantErr: false, + }, + {name: "label-only", + args: args{ + obj: &ser, + selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"fee": "false", "x": "y"}, + MatchExpressions: []metav1.LabelSelectorRequirement{}, + }, + }, + wantErr: false, + }, + {name: "expr-only - err", + args: args{ + obj: &ser, + selector: metav1.LabelSelector{ + MatchLabels: map[string]string{}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "a", + Operator: "In", + Values: []string{"x", "y"}, + }, + }, + }, + }, + wantErr: true, + }, + {name: "both - err", + args: args{ + obj: &ser, + selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"b": "u"}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "a", + Operator: "In", + Values: []string{"x", "y"}, + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + err := updateSelectorForObject(tt.args.obj, tt.args.selector) + if (err != nil) != tt.wantErr { + t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr) + } else if !tt.wantErr { + assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name) + } + } +} + +func TestGetResourcesAndSelector(t *testing.T) { + type args struct { + args []string + } + tests := []struct { + name string + args args + wantResources []string + wantSelector *metav1.LabelSelector + wantErr bool + }{ + { + name: "basic match", + args: args{args: []string{"rc/foo", "healthy=true"}}, + wantResources: []string{"rc/foo"}, + wantErr: false, + wantSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"healthy": "true"}, + MatchExpressions: []metav1.LabelSelectorRequirement{}, + }, + }, + { + name: "basic expression", + args: args{args: []string{"rc/foo", "buildType notin (debug, test)"}}, + wantResources: []string{"rc/foo"}, + wantErr: false, + wantSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "buildType", + Operator: "NotIn", + Values: []string{"debug", "test"}, + }, + }, + }, + }, + { + name: "selector error", + args: args{args: []string{"rc/foo", "buildType notthis (debug, test)"}}, + wantResources: []string{"rc/foo"}, + wantErr: true, + wantSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{}, + MatchExpressions: []metav1.LabelSelectorRequirement{}, + }, + }, + } + for _, tt := range tests { + gotResources, gotSelector, err := getResourcesAndSelector(tt.args.args) + if err != nil { + if !tt.wantErr { + t.Errorf("%q. getResourcesAndSelector() error = %v, wantErr %v", tt.name, err, tt.wantErr) + } + continue + } + if !reflect.DeepEqual(gotResources, tt.wantResources) { + t.Errorf("%q. getResourcesAndSelector() gotResources = %v, want %v", tt.name, gotResources, tt.wantResources) + } + if !reflect.DeepEqual(gotSelector, tt.wantSelector) { + t.Errorf("%q. getResourcesAndSelector() gotSelector = %v, want %v", tt.name, gotSelector, tt.wantSelector) + } + } +} diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index b3c602040ae..1173d46af5a 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -303,12 +303,12 @@ func (f *FakeFactory) LogsForObject(object, options runtime.Object) (*restclient return nil, nil } -func (f *FakeFactory) Pauser(info *resource.Info) (bool, error) { - return false, nil +func (f *FakeFactory) Pauser(info *resource.Info) ([]byte, error) { + return nil, nil } -func (f *FakeFactory) Resumer(info *resource.Info) (bool, error) { - return false, nil +func (f *FakeFactory) Resumer(info *resource.Info) ([]byte, error) { + return nil, nil } func (f *FakeFactory) ResolveImage(name string) (string, error) { diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 0b46d69e786..67522c5271d 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -146,10 +146,14 @@ type ClientAccessFactory interface { // Returns a Printer for formatting objects of the given type or an error. Printer(mapping *meta.RESTMapping, options kubectl.PrintOptions) (kubectl.ResourcePrinter, error) - // Pauser marks the object in the info as paused ie. it will not be reconciled by its controller. - Pauser(info *resource.Info) (bool, error) - // Resumer resumes a paused object inside the info ie. it will be reconciled by its controller. - Resumer(info *resource.Info) (bool, error) + // Pauser marks the object in the info as paused. Currently supported only for Deployments. + // Returns the patched object in bytes and any error that occured during the encoding or + // in case the object is already paused. + Pauser(info *resource.Info) ([]byte, error) + // Resumer resumes a paused object inside the info. Currently supported only for Deployments. + // Returns the patched object in bytes and any error that occured during the encoding or + // in case the object is already resumed. + Resumer(info *resource.Info) ([]byte, error) // ResolveImage resolves the image names. For kubernetes this function is just // passthrough but it allows to perform more sophisticated image name resolving for diff --git a/pkg/kubectl/cmd/util/factory_client_access.go b/pkg/kubectl/cmd/util/factory_client_access.go index ee47731c1c7..31dc8d19316 100644 --- a/pkg/kubectl/cmd/util/factory_client_access.go +++ b/pkg/kubectl/cmd/util/factory_client_access.go @@ -398,16 +398,16 @@ func (f *ring0Factory) Printer(mapping *meta.RESTMapping, options kubectl.PrintO return kubectl.NewHumanReadablePrinter(options), nil } -func (f *ring0Factory) Pauser(info *resource.Info) (bool, error) { +func (f *ring0Factory) Pauser(info *resource.Info) ([]byte, error) { switch obj := info.Object.(type) { case *extensions.Deployment: if obj.Spec.Paused { - return true, errors.New("is already paused") + return nil, errors.New("is already paused") } obj.Spec.Paused = true - return true, nil + return runtime.Encode(f.JSONEncoder(), info.Object) default: - return false, fmt.Errorf("pausing is not supported") + return nil, fmt.Errorf("pausing is not supported") } } @@ -415,16 +415,16 @@ func (f *ring0Factory) ResolveImage(name string) (string, error) { return name, nil } -func (f *ring0Factory) Resumer(info *resource.Info) (bool, error) { +func (f *ring0Factory) Resumer(info *resource.Info) ([]byte, error) { switch obj := info.Object.(type) { case *extensions.Deployment: if !obj.Spec.Paused { - return true, errors.New("is not paused") + return nil, errors.New("is not paused") } obj.Spec.Paused = false - return true, nil + return runtime.Encode(f.JSONEncoder(), info.Object) default: - return false, fmt.Errorf("resuming is not supported") + return nil, fmt.Errorf("resuming is not supported") } }