From ecd9a6be2e2b665139c83e6c3f1ae769cb794713 Mon Sep 17 00:00:00 2001 From: David Eads Date: Thu, 19 Apr 2018 10:41:17 -0400 Subject: [PATCH] final record flag cleanup --- hack/make-rules/test-cmd-util.sh | 5 +- pkg/kubectl/cmd/expose.go | 51 +++++++++--- pkg/kubectl/cmd/label.go | 62 +++++++++----- pkg/kubectl/cmd/label_test.go | 24 +++--- pkg/kubectl/cmd/patch.go | 83 ++++++++++++------- pkg/kubectl/cmd/replace.go | 62 +++++++------- pkg/kubectl/cmd/scale.go | 62 ++++++++++---- pkg/kubectl/cmd/set/set_env.go | 7 +- pkg/kubectl/cmd/set/set_image.go | 4 - pkg/kubectl/cmd/set/set_resources.go | 6 +- pkg/kubectl/cmd/set/set_selector.go | 11 +-- pkg/kubectl/cmd/set/set_serviceaccount.go | 6 -- pkg/kubectl/cmd/util/BUILD | 3 - pkg/kubectl/cmd/util/helpers.go | 79 ------------------ pkg/kubectl/cmd/util/helpers_test.go | 14 ---- pkg/kubectl/genericclioptions/BUILD | 2 + pkg/kubectl/genericclioptions/record_flags.go | 28 +++++++ 17 files changed, 265 insertions(+), 244 deletions(-) diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 80cf0d7c762..efce34f3306 100755 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -592,9 +592,10 @@ run_pod_tests() { # Post-condition: valid-pod's record annotation still contains command with --record=true kube::test::get_object_assert 'pod valid-pod' "{{range$annotations_field}}{{.}}:{{end}}" ".*--record=true.*" - ### Record label change with unspecified flag and previous change already recorded + ### Record label change with specified flag and previous change already recorded + ### we are no longer tricked by data from another user into revealing more information about our client # Command - kubectl label pods valid-pod new-record-change=true "${kube_flags[@]}" + kubectl label pods valid-pod new-record-change=true --record=true "${kube_flags[@]}" # Post-condition: valid-pod's record annotation contains new change kube::test::get_object_assert 'pod valid-pod' "{{range$annotations_field}}{{.}}:{{end}}" ".*new-record-change=true.*" diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index c511215ad39..96269dca51c 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -23,11 +23,13 @@ import ( "github.com/spf13/cobra" + "github.com/golang/glog" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/util/i18n" ) @@ -72,8 +74,23 @@ var ( kubectl expose deployment nginx --port=80 --target-port=8000`)) ) +type ExposeServiceOptions struct { + FilenameOptions resource.FilenameOptions + RecordFlags *genericclioptions.RecordFlags + + Recorder genericclioptions.Recorder +} + +func NewExposeServiceOptions() *ExposeServiceOptions { + return &ExposeServiceOptions{ + RecordFlags: genericclioptions.NewRecordFlags(), + + Recorder: genericclioptions.NoopRecorder{}, + } +} + func NewCmdExposeService(f cmdutil.Factory, out io.Writer) *cobra.Command { - options := &resource.FilenameOptions{} + o := NewExposeServiceOptions() validArgs := []string{} resources := regexp.MustCompile(`\s*,`).Split(exposeResources, -1) @@ -88,12 +105,15 @@ func NewCmdExposeService(f cmdutil.Factory, out io.Writer) *cobra.Command { Long: exposeLong, Example: exposeExample, Run: func(cmd *cobra.Command, args []string) { - err := RunExpose(f, out, cmd, args, options) - cmdutil.CheckErr(err) + cmdutil.CheckErr(o.Complete(f, cmd)) + cmdutil.CheckErr(o.RunExpose(f, out, cmd, args)) }, ValidArgs: validArgs, ArgAliases: kubectl.ResourceAliases(validArgs), } + + o.RecordFlags.AddFlags(cmd) + cmdutil.AddPrinterFlags(cmd) cmd.Flags().String("generator", "service/v2", i18n.T("The name of the API generator to use. There are 2 generators: 'service/v1' and 'service/v2'. The only difference between them is that service port in v1 is named 'default', while it is left unnamed in v2. Default is 'service/v2'.")) cmd.Flags().String("protocol", "", i18n.T("The network protocol for the service to be created. Default is 'TCP'.")) @@ -112,14 +132,25 @@ func NewCmdExposeService(f cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().String("cluster-ip", "", i18n.T("ClusterIP to be assigned to the service. Leave empty to auto-allocate, or set to 'None' to create a headless service.")) usage := "identifying the resource to expose a service" - cmdutil.AddFilenameOptionFlags(cmd, options, usage) + cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddDryRunFlag(cmd) cmdutil.AddApplyAnnotationFlags(cmd) - cmdutil.AddRecordFlag(cmd) return cmd } -func RunExpose(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, options *resource.FilenameOptions) error { +func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { + var err error + + o.RecordFlags.Complete(f.Command(cmd, false)) + o.Recorder, err = o.RecordFlags.ToRecorder() + if err != nil { + return err + } + + return err +} + +func (o *ExposeServiceOptions) RunExpose(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error { namespace, enforceNamespace, err := f.DefaultNamespace() if err != nil { return err @@ -130,7 +161,7 @@ func RunExpose(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri Internal(). ContinueOnError(). NamespaceParam(namespace).DefaultNamespace(). - FilenameParam(enforceNamespace, options). + FilenameParam(enforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(false, args...). Flatten(). Do() @@ -247,10 +278,8 @@ func RunExpose(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri if err != nil { return err } - if cmdutil.ShouldRecord(cmd, info) { - if err := cmdutil.RecordChangeCause(object, f.Command(cmd, false)); err != nil { - return err - } + if err := o.Recorder.Record(object); err != nil { + glog.V(4).Infof("error recording current command: %v", err) } info.Refresh(object, true) if cmdutil.GetDryRunFlag(cmd) { diff --git a/pkg/kubectl/cmd/label.go b/pkg/kubectl/cmd/label.go index 3d2de88d490..14d3f25e77e 100644 --- a/pkg/kubectl/cmd/label.go +++ b/pkg/kubectl/cmd/label.go @@ -37,6 +37,7 @@ import ( "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/util/i18n" ) @@ -45,6 +46,7 @@ import ( type LabelOptions struct { // Filename options resource.FilenameOptions + RecordFlags *genericclioptions.RecordFlags // Common user flags overwrite bool @@ -61,6 +63,8 @@ type LabelOptions struct { newLabels map[string]string removeLabels []string + Recorder genericclioptions.Recorder + // Common shared fields out io.Writer errout io.Writer @@ -96,10 +100,19 @@ var ( kubectl label pods foo bar-`)) ) -func NewCmdLabel(f cmdutil.Factory, out, errout io.Writer) *cobra.Command { - options := &LabelOptions{ - errout: errout, +func NewLabelOptions(out, errOut io.Writer) *LabelOptions { + return &LabelOptions{ + RecordFlags: genericclioptions.NewRecordFlags(), + + Recorder: genericclioptions.NoopRecorder{}, + + out: out, + errout: errOut, } +} + +func NewCmdLabel(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { + o := NewLabelOptions(out, errOut) validArgs := cmdutil.ValidArgList(f) @@ -110,36 +123,45 @@ func NewCmdLabel(f cmdutil.Factory, out, errout io.Writer) *cobra.Command { Long: fmt.Sprintf(labelLong, validation.LabelValueMaxLength), Example: labelExample, Run: func(cmd *cobra.Command, args []string) { - if err := options.Complete(out, cmd, args); err != nil { + if err := o.Complete(f, cmd, args); err != nil { cmdutil.CheckErr(cmdutil.UsageErrorf(cmd, err.Error())) } - if err := options.Validate(); err != nil { + if err := o.Validate(); err != nil { cmdutil.CheckErr(cmdutil.UsageErrorf(cmd, err.Error())) } - cmdutil.CheckErr(options.RunLabel(f, cmd)) + cmdutil.CheckErr(o.RunLabel(f, cmd)) }, ValidArgs: validArgs, ArgAliases: kubectl.ResourceAliases(validArgs), } + + o.RecordFlags.AddFlags(cmd) + cmdutil.AddPrinterFlags(cmd) - cmd.Flags().BoolVar(&options.overwrite, "overwrite", options.overwrite, "If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels.") - cmd.Flags().BoolVar(&options.list, "list", options.list, "If true, display the labels for a given resource.") - cmd.Flags().BoolVar(&options.local, "local", options.local, "If true, label will NOT contact api-server but run locally.") - cmd.Flags().StringVarP(&options.selector, "selector", "l", options.selector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2).") - cmd.Flags().BoolVar(&options.all, "all", options.all, "Select all resources, including uninitialized ones, in the namespace of the specified resource types") - cmd.Flags().StringVar(&options.resourceVersion, "resource-version", options.resourceVersion, i18n.T("If non-empty, the labels update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.")) + cmd.Flags().BoolVar(&o.overwrite, "overwrite", o.overwrite, "If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels.") + cmd.Flags().BoolVar(&o.list, "list", o.list, "If true, display the labels for a given resource.") + cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, label will NOT contact api-server but run locally.") + cmd.Flags().StringVarP(&o.selector, "selector", "l", o.selector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2).") + cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all resources, including uninitialized ones, in the namespace of the specified resource types") + cmd.Flags().StringVar(&o.resourceVersion, "resource-version", o.resourceVersion, i18n.T("If non-empty, the labels 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 labels" - cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) + cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddDryRunFlag(cmd) - cmdutil.AddRecordFlag(cmd) cmdutil.AddIncludeUninitializedFlag(cmd) return cmd } // Complete adapts from the command line args and factory to the data required. -func (o *LabelOptions) Complete(out io.Writer, cmd *cobra.Command, args []string) (err error) { - o.out = out +func (o *LabelOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error + + o.RecordFlags.Complete(f.Command(cmd, false)) + o.Recorder, err = o.RecordFlags.ToRecorder() + if err != nil { + return err + } + o.outputFormat = cmdutil.GetFlagString(cmd, "output") o.dryrun = cmdutil.GetDryRunFlag(cmd) @@ -178,8 +200,6 @@ func (o *LabelOptions) RunLabel(f cmdutil.Factory, cmd *cobra.Command) error { return err } - changeCause := f.Command(cmd, false) - includeUninitialized := cmdutil.ShouldIncludeUninitialized(cmd, false) b := f.NewBuilder(). Unstructured(). @@ -242,10 +262,8 @@ func (o *LabelOptions) RunLabel(f cmdutil.Factory, cmd *cobra.Command) error { if err := labelFunc(obj, o.overwrite, o.resourceVersion, o.newLabels, o.removeLabels); err != nil { return err } - if cmdutil.ShouldRecord(cmd, info) { - if err := cmdutil.RecordChangeCause(obj, changeCause); err != nil { - return err - } + if err := o.Recorder.Record(obj); err != nil { + glog.V(4).Infof("error recording current command: %v", err) } newData, err := json.Marshal(obj) if err != nil { diff --git a/pkg/kubectl/cmd/label_test.go b/pkg/kubectl/cmd/label_test.go index 8377514943c..4237b901be3 100644 --- a/pkg/kubectl/cmd/label_test.go +++ b/pkg/kubectl/cmd/label_test.go @@ -29,7 +29,6 @@ import ( "k8s.io/client-go/rest/fake" "k8s.io/kubernetes/pkg/api/legacyscheme" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" - "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/scheme" ) @@ -335,8 +334,8 @@ func TestLabelErrors(t *testing.T) { for k, v := range testCase.flags { cmd.Flags().Set(k, v) } - opts := LabelOptions{} - err := opts.Complete(buf, cmd, testCase.args) + opts := NewLabelOptions(buf, buf) + err := opts.Complete(tf, cmd, testCase.args) if err == nil { err = opts.Validate() } @@ -392,9 +391,9 @@ func TestLabelForResourceFromFile(t *testing.T) { buf := bytes.NewBuffer([]byte{}) cmd := NewCmdLabel(tf, buf, buf) - opts := LabelOptions{FilenameOptions: resource.FilenameOptions{ - Filenames: []string{"../../../test/e2e/testing-manifests/statefulset/cassandra/controller.yaml"}}} - err := opts.Complete(buf, cmd, []string{"a=b"}) + opts := NewLabelOptions(buf, buf) + opts.Filenames = []string{"../../../test/e2e/testing-manifests/statefulset/cassandra/controller.yaml"} + err := opts.Complete(tf, cmd, []string{"a=b"}) if err == nil { err = opts.Validate() } @@ -425,10 +424,10 @@ func TestLabelLocal(t *testing.T) { buf := bytes.NewBuffer([]byte{}) cmd := NewCmdLabel(tf, buf, buf) - opts := LabelOptions{FilenameOptions: resource.FilenameOptions{ - Filenames: []string{"../../../test/e2e/testing-manifests/statefulset/cassandra/controller.yaml"}}, - local: true} - err := opts.Complete(buf, cmd, []string{"a=b"}) + opts := NewLabelOptions(buf, buf) + opts.Filenames = []string{"../../../test/e2e/testing-manifests/statefulset/cassandra/controller.yaml"} + opts.local = true + err := opts.Complete(tf, cmd, []string{"a=b"}) if err == nil { err = opts.Validate() } @@ -482,9 +481,10 @@ func TestLabelMultipleObjects(t *testing.T) { tf.ClientConfigVal = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) - opts := LabelOptions{all: true} + opts := NewLabelOptions(buf, buf) + opts.all = true cmd := NewCmdLabel(tf, buf, buf) - err := opts.Complete(buf, cmd, []string{"pods", "a=b"}) + err := opts.Complete(tf, cmd, []string{"pods", "a=b"}) if err == nil { err = opts.Validate() } diff --git a/pkg/kubectl/cmd/patch.go b/pkg/kubectl/cmd/patch.go index 89c59824e5e..96627603c77 100644 --- a/pkg/kubectl/cmd/patch.go +++ b/pkg/kubectl/cmd/patch.go @@ -36,6 +36,7 @@ import ( "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/scheme" "k8s.io/kubernetes/pkg/kubectl/util/i18n" @@ -47,10 +48,13 @@ var patchTypes = map[string]types.PatchType{"json": types.JSONPatchType, "merge" // referencing the cmd.Flags() type PatchOptions struct { resource.FilenameOptions + RecordFlags *genericclioptions.RecordFlags Local bool DryRun bool + Recorder genericclioptions.Recorder + OutputFormat string } @@ -79,8 +83,16 @@ var ( kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"new image"}]'`)) ) +func NewPatchOptions() *PatchOptions { + return &PatchOptions{ + RecordFlags: genericclioptions.NewRecordFlags(), + + Recorder: genericclioptions.NoopRecorder{}, + } +} + func NewCmdPatch(f cmdutil.Factory, out io.Writer) *cobra.Command { - options := &PatchOptions{} + o := NewPatchOptions() validArgs := cmdutil.ValidArgList(f) cmd := &cobra.Command{ @@ -90,32 +102,47 @@ func NewCmdPatch(f cmdutil.Factory, out io.Writer) *cobra.Command { Long: patchLong, Example: patchExample, Run: func(cmd *cobra.Command, args []string) { - options.OutputFormat = cmdutil.GetFlagString(cmd, "output") - options.DryRun = cmdutil.GetFlagBool(cmd, "dry-run") - err := RunPatch(f, out, cmd, args, options) - cmdutil.CheckErr(err) + cmdutil.CheckErr(o.Complete(f, cmd)) + cmdutil.CheckErr(o.RunPatch(f, out, cmd, args)) }, ValidArgs: validArgs, ArgAliases: kubectl.ResourceAliases(validArgs), } + + o.RecordFlags.AddFlags(cmd) + cmd.Flags().StringP("patch", "p", "", "The patch to be applied to the resource JSON file.") cmd.MarkFlagRequired("patch") cmd.Flags().String("type", "strategic", fmt.Sprintf("The type of patch being provided; one of %v", sets.StringKeySet(patchTypes).List())) cmdutil.AddPrinterFlags(cmd) - cmdutil.AddRecordFlag(cmd) cmdutil.AddDryRunFlag(cmd) usage := "identifying the resource to update" - cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) + cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) - cmd.Flags().BoolVar(&options.Local, "local", options.Local, "If true, patch will operate on the content of the file, not the server-side resource.") + cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, patch will operate on the content of the file, not the server-side resource.") return cmd } -func RunPatch(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, options *PatchOptions) error { +func (o *PatchOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { + var err error + + o.RecordFlags.Complete(f.Command(cmd, false)) + o.Recorder, err = o.RecordFlags.ToRecorder() + if err != nil { + return err + } + + o.OutputFormat = cmdutil.GetFlagString(cmd, "output") + o.DryRun = cmdutil.GetFlagBool(cmd, "dry-run") + + return err +} + +func (o *PatchOptions) RunPatch(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error { switch { - case options.Local && len(args) != 0: + case o.Local && len(args) != 0: return fmt.Errorf("cannot specify --local and server resources") } @@ -148,7 +175,7 @@ func RunPatch(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin Unstructured(). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). - FilenameParam(enforceNamespace, &options.FilenameOptions). + FilenameParam(enforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(false, args...). Flatten(). Do() @@ -169,38 +196,36 @@ func RunPatch(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin return err } - if !options.Local && !options.DryRun { + if !o.Local && !o.DryRun { helper := resource.NewHelper(client, mapping) patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes) if err != nil { return err } - // Record the change as a second patch to avoid trying to merge with a user's patch data - if cmdutil.ShouldRecord(cmd, info) { - // Copy the resource info and update with the result of applying the user's patch - infoCopy := *info - infoCopy.Object = patchedObj - if patch, patchType, err := cmdutil.ChangeResourcePatch(&infoCopy, f.Command(cmd, true)); err == nil { - if recordedObj, err := helper.Patch(info.Namespace, info.Name, patchType, patch); err != nil { - glog.V(4).Infof("error recording reason: %v", err) - } else { - patchedObj = recordedObj - } + + didPatch := !reflect.DeepEqual(info.Object, patchedObj) + + // if the recorder makes a change, compute and create another patch + if mergePatch, err := o.Recorder.MakeRecordMergePatch(patchedObj); err != nil { + glog.V(4).Infof("error recording current command: %v", err) + } else if len(mergePatch) > 0 { + if recordedObj, err := helper.Patch(info.Namespace, info.Name, types.MergePatchType, mergePatch); err != nil { + glog.V(4).Infof("error recording reason: %v", err) + } else { + patchedObj = recordedObj } } count++ - didPatch := !reflect.DeepEqual(info.Object, patchedObj) - // After computing whether we changed data, refresh the resource info with the resulting object if err := info.Refresh(patchedObj, true); err != nil { return err } - if len(options.OutputFormat) > 0 && options.OutputFormat != "name" { + if len(o.OutputFormat) > 0 && o.OutputFormat != "name" { return cmdutil.PrintObject(cmd, info.Object, out) } - cmdutil.PrintSuccess(options.OutputFormat == "name", out, info.Object, false, patchOperation(didPatch)) + cmdutil.PrintSuccess(o.OutputFormat == "name", out, info.Object, false, patchOperation(didPatch)) // if object was not successfully patched, exit with error code 1 if !didPatch { @@ -239,11 +264,11 @@ func RunPatch(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin } } - if len(options.OutputFormat) > 0 && options.OutputFormat != "name" { + if len(o.OutputFormat) > 0 && o.OutputFormat != "name" { return cmdutil.PrintObject(cmd, info.Object, out) } - cmdutil.PrintSuccess(options.OutputFormat == "name", out, info.Object, options.DryRun, patchOperation(didPatch)) + cmdutil.PrintSuccess(o.OutputFormat == "name", out, info.Object, o.DryRun, patchOperation(didPatch)) return nil }) if err != nil { diff --git a/pkg/kubectl/cmd/replace.go b/pkg/kubectl/cmd/replace.go index 2aec4ec007a..b257ed1d7ce 100644 --- a/pkg/kubectl/cmd/replace.go +++ b/pkg/kubectl/cmd/replace.go @@ -33,6 +33,7 @@ import ( "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/util/i18n" "k8s.io/kubernetes/pkg/kubectl/validation" @@ -64,39 +65,43 @@ var ( kubectl replace --force -f ./pod.json`)) ) -type ReplaceOpts struct { +type ReplaceOptions struct { PrintFlags *printers.PrintFlags DeleteFlags *DeleteFlags + RecordFlags *genericclioptions.RecordFlags DeleteOptions *DeleteOptions PrintObj func(obj runtime.Object) error createAnnotation bool - changeCause string validate bool Schema validation.Schema Builder func() *resource.Builder BuilderArgs []string - ShouldRecord func(info *resource.Info) bool - Namespace string EnforceNamespace bool + Recorder genericclioptions.Recorder + Out io.Writer ErrOut io.Writer } -func NewCmdReplace(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { - options := &ReplaceOpts{ +func NewReplaceOptions(out, errOut io.Writer) *ReplaceOptions { + return &ReplaceOptions{ PrintFlags: printers.NewPrintFlags("replaced"), DeleteFlags: NewDeleteFlags("to use to replace the resource."), Out: out, ErrOut: errOut, } +} + +func NewCmdReplace(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { + o := NewReplaceOptions(out, errOut) cmd := &cobra.Command{ Use: "replace -f FILENAME", @@ -106,32 +111,35 @@ func NewCmdReplace(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { Example: replaceExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd)) - cmdutil.CheckErr(options.Complete(f, cmd, args)) - cmdutil.CheckErr(options.Validate(cmd)) - cmdutil.CheckErr(options.Run()) + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate(cmd)) + cmdutil.CheckErr(o.Run()) }, } - options.PrintFlags.AddFlags(cmd) - options.DeleteFlags.AddFlags(cmd) + o.PrintFlags.AddFlags(cmd) + o.DeleteFlags.AddFlags(cmd) + o.RecordFlags.AddFlags(cmd) cmd.MarkFlagRequired("filename") cmdutil.AddValidateFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) - cmdutil.AddRecordFlag(cmd) return cmd } -func (o *ReplaceOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - o.validate = cmdutil.GetFlagBool(cmd, "validate") - o.changeCause = f.Command(cmd, false) - o.createAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) +func (o *ReplaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error - o.ShouldRecord = func(info *resource.Info) bool { - return cmdutil.ShouldRecord(cmd, info) + o.RecordFlags.Complete(f.Command(cmd, false)) + o.Recorder, err = o.RecordFlags.ToRecorder() + if err != nil { + return err } + o.validate = cmdutil.GetFlagBool(cmd, "validate") + o.createAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) + printer, err := o.PrintFlags.ToPrinter() if err != nil { return err @@ -173,7 +181,7 @@ func (o *ReplaceOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str return nil } -func (o *ReplaceOpts) Validate(cmd *cobra.Command) error { +func (o *ReplaceOptions) Validate(cmd *cobra.Command) error { if o.DeleteOptions.GracePeriod >= 0 && !o.DeleteOptions.ForceDeletion { return fmt.Errorf("--grace-period must have --force specified") } @@ -189,7 +197,7 @@ func (o *ReplaceOpts) Validate(cmd *cobra.Command) error { return nil } -func (o *ReplaceOpts) Run() error { +func (o *ReplaceOptions) Run() error { if o.DeleteOptions.ForceDeletion { return o.forceReplace() } @@ -215,10 +223,8 @@ func (o *ReplaceOpts) Run() error { return cmdutil.AddSourceToErr("replacing", info.Source, err) } - if o.ShouldRecord(info) { - if err := cmdutil.RecordChangeCause(info.Object, o.changeCause); err != nil { - return cmdutil.AddSourceToErr("replacing", info.Source, err) - } + if err := o.Recorder.Record(info.Object); err != nil { + glog.V(4).Infof("error recording current command: %v", err) } // Serialize the object with the annotation applied. @@ -232,7 +238,7 @@ func (o *ReplaceOpts) Run() error { }) } -func (o *ReplaceOpts) forceReplace() error { +func (o *ReplaceOptions) forceReplace() error { for i, filename := range o.DeleteOptions.FilenameOptions.Filenames { if filename == "-" { tempDir, err := ioutil.TempDir("", "kubectl_replace_") @@ -313,10 +319,8 @@ func (o *ReplaceOpts) forceReplace() error { return err } - if o.ShouldRecord(info) { - if err := cmdutil.RecordChangeCause(info.Object, o.changeCause); err != nil { - return cmdutil.AddSourceToErr("replacing", info.Source, err) - } + if err := o.Recorder.Record(info.Object); err != nil { + glog.V(4).Infof("error recording current command: %v", err) } obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) diff --git a/pkg/kubectl/cmd/scale.go b/pkg/kubectl/cmd/scale.go index 43e049e2a33..428bbafc170 100644 --- a/pkg/kubectl/cmd/scale.go +++ b/pkg/kubectl/cmd/scale.go @@ -22,11 +22,14 @@ import ( "github.com/spf13/cobra" + "github.com/golang/glog" + "k8s.io/apimachinery/pkg/types" batchclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/batch/internalversion" "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/scalejob" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/util/i18n" ) @@ -58,9 +61,24 @@ var ( kubectl scale --replicas=3 statefulset/web`)) ) +type ScaleOptions struct { + FilenameOptions resource.FilenameOptions + RecordFlags *genericclioptions.RecordFlags + + Recorder genericclioptions.Recorder +} + +func NewScaleOptions() *ScaleOptions { + return &ScaleOptions{ + RecordFlags: genericclioptions.NewRecordFlags(), + + Recorder: genericclioptions.NoopRecorder{}, + } +} + // NewCmdScale returns a cobra command with the appropriate configuration and flags to run scale func NewCmdScale(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { - options := &resource.FilenameOptions{} + o := NewScaleOptions() validArgs := []string{"deployment", "replicaset", "replicationcontroller", "statefulset"} argAliases := kubectl.ResourceAliases(validArgs) @@ -72,14 +90,17 @@ func NewCmdScale(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { Long: scaleLong, Example: scaleExample, Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, cmd)) cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd)) shortOutput := cmdutil.GetFlagString(cmd, "output") == "name" - err := RunScale(f, out, errOut, cmd, args, shortOutput, options) - cmdutil.CheckErr(err) + cmdutil.CheckErr(o.RunScale(f, out, errOut, cmd, args, shortOutput)) }, ValidArgs: validArgs, ArgAliases: argAliases, } + + o.RecordFlags.AddFlags(cmd) + cmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") cmd.Flags().Bool("all", false, "Select all resources in the namespace of the specified resource types") cmd.Flags().String("resource-version", "", i18n.T("Precondition for resource version. Requires that the current resource version match this value in order to scale.")) @@ -88,15 +109,26 @@ func NewCmdScale(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmd.MarkFlagRequired("replicas") cmd.Flags().Duration("timeout", 0, "The length of time to wait before giving up on a scale operation, zero means don't wait. Any other values should contain a corresponding time unit (e.g. 1s, 2m, 3h).") cmdutil.AddOutputFlagsForMutation(cmd) - cmdutil.AddRecordFlag(cmd) usage := "identifying the resource to set a new size" - cmdutil.AddFilenameOptionFlags(cmd, options, usage) + cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) return cmd } +func (o *ScaleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { + var err error + + o.RecordFlags.Complete(f.Command(cmd, false)) + o.Recorder, err = o.RecordFlags.ToRecorder() + if err != nil { + return err + } + + return err +} + // RunScale executes the scaling -func RunScale(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, shortOutput bool, options *resource.FilenameOptions) error { +func (o *ScaleOptions) RunScale(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, shortOutput bool) error { cmdNamespace, enforceNamespace, err := f.DefaultNamespace() if err != nil { return err @@ -114,7 +146,7 @@ func RunScale(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args Unstructured(). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). - FilenameParam(enforceNamespace, options). + FilenameParam(enforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(all, args...). Flatten(). LabelSelectorParam(selector). @@ -180,22 +212,20 @@ func RunScale(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args } } - if cmdutil.ShouldRecord(cmd, info) { - patchBytes, patchType, err := cmdutil.ChangeResourcePatch(info, f.Command(cmd, true)) - if err != nil { - return err - } - mapping := info.ResourceMapping() + // if the recorder makes a change, compute and create another patch + if mergePatch, err := o.Recorder.MakeRecordMergePatch(info.Object); err != nil { + glog.V(4).Infof("error recording current command: %v", err) + } else if len(mergePatch) > 0 { client, err := f.UnstructuredClientForMapping(mapping) if err != nil { return err } helper := resource.NewHelper(client, mapping) - _, err = helper.Patch(info.Namespace, info.Name, patchType, patchBytes) - if err != nil { - return err + if _, err := helper.Patch(info.Namespace, info.Name, types.MergePatchType, mergePatch); err != nil { + glog.V(4).Infof("error recording reason: %v", err) } } + counter++ cmdutil.PrintSuccess(shortOutput, out, info.Object, false, "scaled") return nil diff --git a/pkg/kubectl/cmd/set/set_env.go b/pkg/kubectl/cmd/set/set_env.go index 8f1b756e12c..b4e0e507104 100644 --- a/pkg/kubectl/cmd/set/set_env.go +++ b/pkg/kubectl/cmd/set/set_env.go @@ -122,8 +122,6 @@ type EnvOptions struct { Builder *resource.Builder Infos []*resource.Info - Cmd *cobra.Command - UpdatePodSpecForObject func(obj runtime.Object, fn func(*v1.PodSpec) error) (bool, error) } @@ -193,7 +191,7 @@ func (o *EnvOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri } resources, envArgs, ok := envutil.SplitEnvironmentFromResources(args) if !ok { - return cmdutil.UsageErrorf(o.Cmd, "all resources must be specified before environment changes: %s", strings.Join(args, " ")) + return cmdutil.UsageErrorf(cmd, "all resources must be specified before environment changes: %s", strings.Join(args, " ")) } if len(o.Filenames) == 0 && len(resources) < 1 { return cmdutil.UsageErrorf(cmd, "one or more resources must be specified as or /") @@ -213,7 +211,6 @@ func (o *EnvOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri o.EnvArgs = envArgs o.Resources = resources - o.Cmd = cmd if o.DryRun { // TODO(juanvallejo): This can be cleaned up even further by creating @@ -229,7 +226,7 @@ func (o *EnvOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri o.PrintObj = printer.PrintObj if o.List && len(o.Output) > 0 { - return cmdutil.UsageErrorf(o.Cmd, "--list and --output may not be specified together") + return cmdutil.UsageErrorf(cmd, "--list and --output may not be specified together") } return nil diff --git a/pkg/kubectl/cmd/set/set_image.go b/pkg/kubectl/cmd/set/set_image.go index 6b621146146..70e578e331c 100644 --- a/pkg/kubectl/cmd/set/set_image.go +++ b/pkg/kubectl/cmd/set/set_image.go @@ -50,9 +50,7 @@ type SetImageOptions struct { DryRun bool All bool Output string - ChangeCause string Local bool - Cmd *cobra.Command ResolveImage func(in string) (string, error) PrintObj printers.ResourcePrinterFunc @@ -138,11 +136,9 @@ func (o *SetImageOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [ } o.UpdatePodSpecForObject = f.UpdatePodSpecForObject - o.ChangeCause = f.Command(cmd, false) o.DryRun = cmdutil.GetDryRunFlag(cmd) o.Output = cmdutil.GetFlagString(cmd, "output") o.ResolveImage = f.ResolveImage - o.Cmd = cmd if o.DryRun { o.PrintFlags.Complete("%s (dry run)") diff --git a/pkg/kubectl/cmd/set/set_resources.go b/pkg/kubectl/cmd/set/set_resources.go index eb20a887b57..3fbb18f00a7 100644 --- a/pkg/kubectl/cmd/set/set_resources.go +++ b/pkg/kubectl/cmd/set/set_resources.go @@ -75,9 +75,7 @@ type SetResourcesOptions struct { ContainerSelector string Output string All bool - ChangeCause string Local bool - Cmd *cobra.Command DryRun bool @@ -157,9 +155,7 @@ func (o *SetResourcesOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, ar o.UpdatePodSpecForObject = f.UpdatePodSpecForObject o.Output = cmdutil.GetFlagString(cmd, "output") - o.ChangeCause = f.Command(cmd, false) - o.Cmd = cmd - o.DryRun = cmdutil.GetDryRunFlag(o.Cmd) + o.DryRun = cmdutil.GetDryRunFlag(cmd) if o.DryRun { o.PrintFlags.Complete("%s (dry run)") diff --git a/pkg/kubectl/cmd/set/set_selector.go b/pkg/kubectl/cmd/set/set_selector.go index c052223c50b..1fb92ac3080 100644 --- a/pkg/kubectl/cmd/set/set_selector.go +++ b/pkg/kubectl/cmd/set/set_selector.go @@ -45,12 +45,10 @@ type SetSelectorOptions struct { PrintFlags *printers.PrintFlags RecordFlags *genericclioptions.RecordFlags - local bool - dryrun bool - all bool - record bool - changeCause string - output string + local bool + dryrun bool + all bool + output string resources []string selector *metav1.LabelSelector @@ -141,7 +139,6 @@ func (o *SetSelectorOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, arg return err } - o.changeCause = f.Command(cmd, false) mapper, _ := f.Object() o.mapper = mapper diff --git a/pkg/kubectl/cmd/set/set_serviceaccount.go b/pkg/kubectl/cmd/set/set_serviceaccount.go index 989b9e31507..5723c943a32 100644 --- a/pkg/kubectl/cmd/set/set_serviceaccount.go +++ b/pkg/kubectl/cmd/set/set_serviceaccount.go @@ -65,12 +65,9 @@ type SetServiceAccountOptions struct { out io.Writer err io.Writer dryRun bool - cmd *cobra.Command shortOutput bool all bool - record bool output string - changeCause string local bool updatePodSpecForObject func(runtime.Object, func(*v1.PodSpec) error) (bool, error) infos []*resource.Info @@ -132,12 +129,9 @@ func (o *SetServiceAccountOptions) Complete(f cmdutil.Factory, cmd *cobra.Comman } o.shortOutput = cmdutil.GetFlagString(cmd, "output") == "name" - o.record = cmdutil.GetRecordFlag(cmd) - o.changeCause = f.Command(cmd, false) o.dryRun = cmdutil.GetDryRunFlag(cmd) o.output = cmdutil.GetFlagString(cmd, "output") o.updatePodSpecForObject = f.UpdatePodSpecForObject - o.cmd = cmd if o.dryRun { o.PrintFlags.Complete("%s (dry run)") diff --git a/pkg/kubectl/cmd/util/BUILD b/pkg/kubectl/cmd/util/BUILD index 71189b0781b..02bbeceab1b 100644 --- a/pkg/kubectl/cmd/util/BUILD +++ b/pkg/kubectl/cmd/util/BUILD @@ -58,11 +58,8 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library", "//vendor/k8s.io/apimachinery/pkg/version:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index fae6ad16031..a256ab1997d 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -34,13 +34,9 @@ import ( kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubernetes/pkg/kubectl" @@ -517,64 +513,10 @@ func UpdateObject(info *resource.Info, codec runtime.Codec, updateFn func(runtim return info.Object, nil } -func AddRecordFlag(cmd *cobra.Command) { - cmd.Flags().Bool("record", false, "Record current kubectl command in the resource annotation. If set to false, do not record the command. If set to true, record the command. If not set, default to updating the existing annotation value only if one already exists.") -} - -func GetRecordFlag(cmd *cobra.Command) bool { - return GetFlagBool(cmd, "record") -} - func GetDryRunFlag(cmd *cobra.Command) bool { return GetFlagBool(cmd, "dry-run") } -// RecordChangeCause annotate change-cause to input runtime object. -func RecordChangeCause(obj runtime.Object, changeCause string) error { - accessor, err := meta.Accessor(obj) - if err != nil { - return err - } - annotations := accessor.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[kubectl.ChangeCauseAnnotation] = changeCause - accessor.SetAnnotations(annotations) - return nil -} - -// ChangeResourcePatch creates a patch between the origin input resource info -// and the annotated with change-cause input resource info. -func ChangeResourcePatch(info *resource.Info, changeCause string) ([]byte, types.PatchType, error) { - // Get a versioned object - obj, err := info.Mapping.ConvertToVersion(info.Object, info.Mapping.GroupVersionKind.GroupVersion()) - if err != nil { - return nil, types.StrategicMergePatchType, err - } - - oldData, err := json.Marshal(obj) - if err != nil { - return nil, types.StrategicMergePatchType, err - } - if err := RecordChangeCause(obj, changeCause); err != nil { - return nil, types.StrategicMergePatchType, err - } - newData, err := json.Marshal(obj) - if err != nil { - return nil, types.StrategicMergePatchType, err - } - - switch obj := obj.(type) { - case *unstructured.Unstructured: - patch, err := jsonpatch.CreateMergePatch(oldData, newData) - return patch, types.MergePatchType, err - default: - patch, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj) - return patch, types.StrategicMergePatchType, err - } -} - // ContainsChangeCause checks if input resource info contains change-cause annotation. func ContainsChangeCause(info *resource.Info) bool { annotations, err := info.Mapping.MetadataAccessor.Annotations(info.Object) @@ -584,11 +526,6 @@ func ContainsChangeCause(info *resource.Info) bool { return len(annotations[kubectl.ChangeCauseAnnotation]) > 0 } -// ShouldRecord checks if we should record current change cause -func ShouldRecord(cmd *cobra.Command, info *resource.Info) bool { - return GetRecordFlag(cmd) || (ContainsChangeCause(info) && !cmd.Flags().Changed("record")) -} - // GetResourcesAndPairs retrieves resources and "KEY=VALUE or KEY-" pair args from given args func GetResourcesAndPairs(args []string, pairType string) (resources []string, pairArgs []string, err error) { foundPair := false @@ -703,22 +640,6 @@ func RequireNoArguments(c *cobra.Command, args []string) { } } -// OutputsRawFormat determines if a command's output format is machine parsable -// or returns false if it is human readable (name, wide, etc.) -func OutputsRawFormat(cmd *cobra.Command) bool { - output := GetFlagString(cmd, "output") - if output == "json" || - output == "yaml" || - output == "go-template" || - output == "go-template-file" || - output == "jsonpath" || - output == "jsonpath-file" { - return true - } - - return false -} - // StripComments will transform a YAML file into JSON, thus dropping any comments // in it. Note that if the given file has a syntax error, the transformation will // fail and we will manually drop all comments from the file. diff --git a/pkg/kubectl/cmd/util/helpers_test.go b/pkg/kubectl/cmd/util/helpers_test.go index db3d09ae392..875b2472e19 100644 --- a/pkg/kubectl/cmd/util/helpers_test.go +++ b/pkg/kubectl/cmd/util/helpers_test.go @@ -19,7 +19,6 @@ package util import ( "fmt" "io/ioutil" - "net/http" "os" "strings" "syscall" @@ -195,19 +194,6 @@ func TestMerge(t *testing.T) { } } -type fileHandler struct { - data []byte -} - -func (f *fileHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { - if req.URL.Path == "/error" { - res.WriteHeader(http.StatusNotFound) - return - } - res.WriteHeader(http.StatusOK) - res.Write(f.data) -} - type checkErrTestCase struct { err error expectedErr string diff --git a/pkg/kubectl/genericclioptions/BUILD b/pkg/kubectl/genericclioptions/BUILD index 080abfe414a..f2af80dd683 100644 --- a/pkg/kubectl/genericclioptions/BUILD +++ b/pkg/kubectl/genericclioptions/BUILD @@ -9,9 +9,11 @@ go_library( importpath = "k8s.io/kubernetes/pkg/kubectl/genericclioptions", visibility = ["//visibility:public"], deps = [ + "//vendor/github.com/evanphx/json-patch:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", ], ) diff --git a/pkg/kubectl/genericclioptions/record_flags.go b/pkg/kubectl/genericclioptions/record_flags.go index 8a26c6a9bac..1a32d8b05ce 100644 --- a/pkg/kubectl/genericclioptions/record_flags.go +++ b/pkg/kubectl/genericclioptions/record_flags.go @@ -17,10 +17,12 @@ limitations under the License. package genericclioptions import ( + "github.com/evanphx/json-patch" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/json" ) // ChangeCauseAnnotation is the annotation indicating a guess at "why" something was changed @@ -92,6 +94,7 @@ func NewRecordFlags() *RecordFlags { type Recorder interface { // Record records why a runtime.Object was changed in an annotation. Record(runtime.Object) error + MakeRecordMergePatch(runtime.Object) ([]byte, error) } // NoopRecorder does nothing. It is a "do nothing" that can be returned so code doesn't switch on it. @@ -102,6 +105,11 @@ func (r NoopRecorder) Record(obj runtime.Object) error { return nil } +// MakeRecordMergePatch implements Recorder +func (r NoopRecorder) MakeRecordMergePatch(obj runtime.Object) ([]byte, error) { + return nil, nil +} + // ChangeCauseRecorder annotates a "change-cause" to an input runtime object type ChangeCauseRecorder struct { changeCause string @@ -122,3 +130,23 @@ func (r *ChangeCauseRecorder) Record(obj runtime.Object) error { accessor.SetAnnotations(annotations) return nil } + +// MakeRecordMergePatch produces a merge patch for updating the recording annotation. +func (r *ChangeCauseRecorder) MakeRecordMergePatch(obj runtime.Object) ([]byte, error) { + // copy so we don't mess with the original + objCopy := obj.DeepCopyObject() + if err := r.Record(objCopy); err != nil { + return nil, err + } + + oldData, err := json.Marshal(obj) + if err != nil { + return nil, err + } + newData, err := json.Marshal(objCopy) + if err != nil { + return nil, err + } + + return jsonpatch.CreateMergePatch(oldData, newData) +}