From ac64404d86190065ed71d9c864d1294c720fcad1 Mon Sep 17 00:00:00 2001 From: deads2k Date: Thu, 2 Jun 2016 13:50:52 -0400 Subject: [PATCH] let patch use local file content to mutate --- hack/test-cmd.sh | 3 ++ pkg/kubectl/cmd/patch.go | 106 ++++++++++++++++++++++++++++++++------- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 6cb0bf8fce..1c4d748c50 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -627,6 +627,9 @@ runTests() { # Post-condition: valid-pod POD is created kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' + ## Patch can modify a local object + kubectl patch --local -f pkg/api/validation/testdata/v1/validPod.yaml --patch='{"spec": {"restartPolicy":"Never"}}' -o jsonpath='{.spec.restartPolicy}' | grep -q "Never" + ## Patch pod can change image # Command kubectl patch "${kube_flags[@]}" pod valid-pod --record -p='{"spec":{"containers":[{"name": "kubernetes-serve-hostname", "image": "nginx"}]}}' diff --git a/pkg/kubectl/cmd/patch.go b/pkg/kubectl/cmd/patch.go index 8959f32fdc..88b15b5baf 100644 --- a/pkg/kubectl/cmd/patch.go +++ b/pkg/kubectl/cmd/patch.go @@ -21,13 +21,16 @@ import ( "io" "strings" + "github.com/evanphx/json-patch" "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/sets" + "k8s.io/kubernetes/pkg/util/strategicpatch" "k8s.io/kubernetes/pkg/util/yaml" ) @@ -38,6 +41,9 @@ var patchTypes = map[string]api.PatchType{"json": api.JSONPatchType, "merge": ap type PatchOptions struct { Filenames []string Recursive bool + Local bool + + OutputFormat string } const ( @@ -78,9 +84,8 @@ func NewCmdPatch(f *cmdutil.Factory, out io.Writer) *cobra.Command { Long: patch_long, Example: patch_example, Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd)) - shortOutput := cmdutil.GetFlagString(cmd, "output") == "name" - err := RunPatch(f, out, cmd, args, shortOutput, options) + options.OutputFormat = cmdutil.GetFlagString(cmd, "output") + err := RunPatch(f, out, cmd, args, options) cmdutil.CheckErr(err) }, ValidArgs: validArgs, @@ -89,17 +94,25 @@ func NewCmdPatch(f *cmdutil.Factory, out io.Writer) *cobra.Command { 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.AddOutputFlagsForMutation(cmd) + cmdutil.AddPrinterFlags(cmd) cmdutil.AddRecordFlag(cmd) cmdutil.AddInclude3rdPartyFlags(cmd) usage := "Filename, directory, or URL to a file identifying the resource to update" kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) cmdutil.AddRecursiveFlag(cmd, &options.Recursive) + + cmd.Flags().BoolVar(&options.Local, "local", false, "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, shortOutput bool, options *PatchOptions) error { +func RunPatch(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, options *PatchOptions) error { + switch { + case options.Local && len(args) != 0: + return fmt.Errorf("cannot specify --local and server resources") + } + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() if err != nil { return err @@ -149,22 +162,60 @@ func RunPatch(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri return err } - helper := resource.NewHelper(client, mapping) - patchedObject, err := helper.Patch(namespace, name, patchType, patchBytes) + if !options.Local { + helper := resource.NewHelper(client, mapping) + patchedObject, err := helper.Patch(namespace, name, patchType, patchBytes) + if err != nil { + return err + } + if cmdutil.ShouldRecord(cmd, info) { + if err := cmdutil.RecordChangeCause(patchedObject, f.Command()); err == nil { + // don't return an error on failure. The patch itself succeeded, its only the hint for that change that failed + // don't bother checking for failures of this replace, because a failure to indicate the hint doesn't fail the command + // also, don't force the replacement. If the replacement fails on a resourceVersion conflict, then it means this + // record hint is likely to be invalid anyway, so avoid the bad hint + resource.NewHelper(client, mapping).Replace(namespace, name, false, patchedObject) + } + } + count++ + + if options.OutputFormat == "name" || len(options.OutputFormat) == 0 { + cmdutil.PrintSuccess(mapper, options.OutputFormat == "name", out, "", name, "patched") + } + return nil + } + + count++ + + patchedObj, err := api.Scheme.DeepCopy(info.VersionedObject) if err != nil { return err } - if cmdutil.ShouldRecord(cmd, info) { - if err := cmdutil.RecordChangeCause(patchedObject, f.Command()); err == nil { - // don't return an error on failure. The patch itself succeeded, its only the hint for that change that failed - // don't bother checking for failures of this replace, because a failure to indicate the hint doesn't fail the command - // also, don't force the replacement. If the replacement fails on a resourceVersion conflict, then it means this - // record hint is likely to be invalid anyway, so avoid the bad hint - resource.NewHelper(client, mapping).Replace(namespace, name, false, patchedObject) - } + originalObjJS, err := runtime.Encode(api.Codecs.LegacyCodec(), info.VersionedObject.(runtime.Object)) + if err != nil { + return err } - count++ - cmdutil.PrintSuccess(mapper, shortOutput, out, "", name, "patched") + originalPatchedObjJS, err := getPatchedJSON(patchType, originalObjJS, patchBytes, patchedObj.(runtime.Object)) + if err != nil { + return err + } + targetObj, err := runtime.Decode(api.Codecs.UniversalDecoder(), originalPatchedObjJS) + if err != nil { + return err + } + // TODO: if we ever want to go generic, this allows a clean -o yaml without trying to print columns or anything + // rawExtension := &runtime.Unknown{ + // Raw: originalPatchedObjJS, + // } + + printer, err := f.PrinterForMapping(cmd, mapping, false) + if err != nil { + return err + } + if err := printer.PrintObj(targetObj, out); err != nil { + return err + } + return nil }) if err != nil { @@ -175,3 +226,24 @@ func RunPatch(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri } return nil } + +func getPatchedJSON(patchType api.PatchType, originalJS, patchJS []byte, obj runtime.Object) ([]byte, error) { + switch patchType { + case api.JSONPatchType: + patchObj, err := jsonpatch.DecodePatch(patchJS) + if err != nil { + return nil, err + } + return patchObj.Apply(originalJS) + + case api.MergePatchType: + return jsonpatch.MergePatch(originalJS, patchJS) + + case api.StrategicMergePatchType: + return strategicpatch.StrategicMergePatchData(originalJS, patchJS, obj) + + default: + // only here as a safety net - go-restful filters content-type + return nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType) + } +}