diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index 53c57f7540..6f3eba7f4f 100755 --- a/hack/make-rules/test-cmd.sh +++ b/hack/make-rules/test-cmd.sh @@ -677,6 +677,25 @@ runTests() { # Cleanup kubectl delete pod pod-with-precision "${kube_flags[@]}" + ### Annotate POD YAML file locally without effecting the live pod. + kubectl create -f hack/testdata/pod.yaml "${kube_flags[@]}" + # Command + kubectl annotate -f hack/testdata/pod.yaml annotatekey=annotatevalue "${kube_flags[@]}" + + # Pre-condition: annotationkey is annotationvalue + kube::test::get_object_assert 'pod test-pod' "{{${annotations_field}.annotatekey}}" 'annotatevalue' + + # Command + output_message=$(kubectl annotate --local -f hack/testdata/pod.yaml annotatekey=localvalue -o yaml "${kube_flags[@]}") + echo $output_message + + # Post-condition: annotationkey is still annotationvalue in the live pod, but command output is the new value + kube::test::get_object_assert 'pod test-pod' "{{${annotations_field}.annotatekey}}" 'annotatevalue' + kube::test::if_has_string "${output_message}" "localvalue" + + # Cleanup + kubectl delete -f hack/testdata/pod.yaml "${kube_flags[@]}" + ### Create valid-pod POD # Pre-condition: no POD exists create_and_use_new_namespace diff --git a/pkg/kubectl/cmd/annotate.go b/pkg/kubectl/cmd/annotate.go index 0fbc92406f..3d7ba0c4ca 100644 --- a/pkg/kubectl/cmd/annotate.go +++ b/pkg/kubectl/cmd/annotate.go @@ -45,6 +45,7 @@ type AnnotateOptions struct { selector string overwrite bool + local bool all bool resourceVersion string @@ -125,6 +126,7 @@ func NewCmdAnnotate(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmdutil.AddInclude3rdPartyFlags(cmd) cmd.Flags().StringVarP(&options.selector, "selector", "l", "", "Selector (label query) to filter on") cmd.Flags().BoolVar(&options.overwrite, "overwrite", false, "If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.") + cmd.Flags().BoolVar(&options.local, "local", false, "If true, annotation will NOT contact api-server but run locally.") cmd.Flags().BoolVar(&options.all, "all", false, "select all resources in the namespace of the specified resource types") cmd.Flags().StringVar(&options.resourceVersion, "resource-version", "", "If non-empty, the annotation update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.") usage := "identifying the resource to update the annotation" @@ -167,10 +169,12 @@ func (o *AnnotateOptions) Complete(f *cmdutil.Factory, out io.Writer, cmd *cobra ContinueOnError(). NamespaceParam(namespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). - SelectorParam(o.selector). - ResourceTypeOrNameArgs(o.all, o.resources...). - Flatten(). - Latest() + Flatten() + if !o.local { + o.builder = o.builder.SelectorParam(o.selector). + ResourceTypeOrNameArgs(o.all, o.resources...). + Latest() + } o.f = f o.out = out @@ -207,49 +211,56 @@ func (o AnnotateOptions) RunAnnotate() error { return err } + var outputObj runtime.Object obj, err := cmdutil.MaybeConvertObject(info.Object, info.Mapping.GroupVersionKind.GroupVersion(), info.Mapping) if err != nil { return err } - name, namespace := info.Name, info.Namespace - oldData, err := json.Marshal(obj) - if err != nil { - return err - } - // If we should record change-cause, add it to new annotations - if cmdutil.ContainsChangeCause(info) || o.recordChangeCause { - o.newAnnotations[kubectl.ChangeCauseAnnotation] = o.changeCause - } - if err := o.updateAnnotations(obj); err != nil { - return err - } - newData, err := json.Marshal(obj) - if err != nil { - return err - } - patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj) - createdPatch := err == nil - if err != nil { - glog.V(2).Infof("couldn't compute patch: %v", err) - } - - mapping := info.ResourceMapping() - client, err := o.f.ClientForMapping(mapping) - if err != nil { - return err - } - helper := resource.NewHelper(client, mapping) - - var outputObj runtime.Object - if createdPatch { - outputObj, err = helper.Patch(namespace, name, api.StrategicMergePatchType, patchBytes) + if o.local { + if err := o.updateAnnotations(obj); err != nil { + return err + } + outputObj = obj } else { - outputObj, err = helper.Replace(namespace, name, false, obj) - } - if err != nil { - return err - } + name, namespace := info.Name, info.Namespace + oldData, err := json.Marshal(obj) + if err != nil { + return err + } + // If we should record change-cause, add it to new annotations + if cmdutil.ContainsChangeCause(info) || o.recordChangeCause { + o.newAnnotations[kubectl.ChangeCauseAnnotation] = o.changeCause + } + if err := o.updateAnnotations(obj); err != nil { + return err + } + newData, err := json.Marshal(obj) + if err != nil { + return err + } + patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj) + createdPatch := err == nil + if err != nil { + glog.V(2).Infof("couldn't compute patch: %v", err) + } + mapping := info.ResourceMapping() + client, err := o.f.ClientForMapping(mapping) + if err != nil { + return err + } + helper := resource.NewHelper(client, mapping) + + if createdPatch { + outputObj, err = helper.Patch(namespace, name, api.StrategicMergePatchType, patchBytes) + } else { + outputObj, err = helper.Replace(namespace, name, false, obj) + } + if err != nil { + return err + } + + } mapper, _ := o.f.Object() outputFormat := cmdutil.GetFlagString(o.cmd, "output") if outputFormat != "" { diff --git a/pkg/kubectl/cmd/annotate_test.go b/pkg/kubectl/cmd/annotate_test.go index 52473fe017..ee0d70941b 100644 --- a/pkg/kubectl/cmd/annotate_test.go +++ b/pkg/kubectl/cmd/annotate_test.go @@ -517,6 +517,34 @@ func TestAnnotateObjectFromFile(t *testing.T) { } } +func TestAnnotateLocal(t *testing.T) { + f, tf, _, ns := NewAPIFactory() + tf.Client = &fake.RESTClient{ + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) + return nil, nil + }), + } + tf.Namespace = "test" + tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}} + + buf := bytes.NewBuffer([]byte{}) + cmd := NewCmdAnnotate(f, buf) + options := &AnnotateOptions{local: true} + options.Filenames = []string{"../../../examples/storage/cassandra/cassandra-controller.yaml"} + args := []string{"a=b"} + if err := options.Complete(f, buf, cmd, args); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := options.Validate(args); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := options.RunAnnotate(); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + func TestAnnotateMultipleObjects(t *testing.T) { pods, _, _ := testData()