From 4597658cb9bf03640816b561c0ee036c4c1edcd5 Mon Sep 17 00:00:00 2001 From: Shiyang Wang Date: Sun, 26 Feb 2017 20:36:20 +0800 Subject: [PATCH] add `kubectl apply edit-last-applied` subcommand --- docs/.generated_docs | 2 + .../man1/kubectl-apply-edit-last-applied.1 | 3 + .../kubectl_apply_edit-last-applied.md | 3 + pkg/kubectl/cmd/BUILD | 1 + pkg/kubectl/cmd/apply.go | 1 + pkg/kubectl/cmd/apply_edit_last_applied.go | 103 ++++++++++ pkg/kubectl/cmd/apply_set_last_applied.go | 24 ++- pkg/kubectl/cmd/edit_test.go | 2 + .../0.request | 0 .../0.response | 21 +++ .../1.request | 0 .../1.response | 35 ++++ .../2.edited | 38 ++++ .../2.original | 36 ++++ .../3.request | 7 + .../3.response | 21 +++ .../4.request | 7 + .../4.response | 35 ++++ .../test.yaml | 40 ++++ .../0.request | 0 .../0.response | 35 ++++ .../1.edited | 21 +++ .../1.original | 21 +++ .../2.edited | 24 +++ .../2.original | 23 +++ .../3.request | 7 + .../3.response | 35 ++++ .../test.yaml | 28 +++ .../0.request | 0 .../0.response | 38 ++++ .../testcase-apply-edit-last-applied/1.edited | 26 +++ .../1.original | 26 +++ .../2.request | 7 + .../2.response | 38 ++++ .../test.yaml | 27 +++ pkg/kubectl/cmd/util/editor/editoptions.go | 178 +++++++++++++++--- pkg/kubectl/cmd/util/helpers.go | 2 +- 37 files changed, 881 insertions(+), 34 deletions(-) create mode 100644 docs/man/man1/kubectl-apply-edit-last-applied.1 create mode 100644 docs/user-guide/kubectl/kubectl_apply_edit-last-applied.md create mode 100644 pkg/kubectl/cmd/apply_edit_last_applied.go create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/0.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/0.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/1.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/1.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.edited create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.original create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/test.yaml create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/0.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/0.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.edited create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.original create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.edited create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.original create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/test.yaml create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/0.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/0.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.edited create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.original create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/test.yaml diff --git a/docs/.generated_docs b/docs/.generated_docs index a288f3c4082..07c73be4975 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -13,6 +13,7 @@ docs/man/man1/kube-scheduler.1 docs/man/man1/kubectl-annotate.1 docs/man/man1/kubectl-api-versions.1 docs/man/man1/kubectl-apiversions.1 +docs/man/man1/kubectl-apply-edit-last-applied.1 docs/man/man1/kubectl-apply-set-last-applied.1 docs/man/man1/kubectl-apply-view-last-applied.1 docs/man/man1/kubectl-apply.1 @@ -111,6 +112,7 @@ docs/user-guide/kubectl/kubectl.md docs/user-guide/kubectl/kubectl_annotate.md docs/user-guide/kubectl/kubectl_api-versions.md docs/user-guide/kubectl/kubectl_apply.md +docs/user-guide/kubectl/kubectl_apply_edit-last-applied.md docs/user-guide/kubectl/kubectl_apply_set-last-applied.md docs/user-guide/kubectl/kubectl_apply_view-last-applied.md docs/user-guide/kubectl/kubectl_attach.md diff --git a/docs/man/man1/kubectl-apply-edit-last-applied.1 b/docs/man/man1/kubectl-apply-edit-last-applied.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-apply-edit-last-applied.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_apply_edit-last-applied.md b/docs/user-guide/kubectl/kubectl_apply_edit-last-applied.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_apply_edit-last-applied.md @@ -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/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index f832fd844c4..06dbf0061ba 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -12,6 +12,7 @@ go_library( "annotate.go", "apiversions.go", "apply.go", + "apply_edit_last_applied.go", "apply_set_last_applied.go", "apply_view_last_applied.go", "attach.go", diff --git a/pkg/kubectl/cmd/apply.go b/pkg/kubectl/cmd/apply.go index 7ec30a461fd..7b8ccb76a96 100644 --- a/pkg/kubectl/cmd/apply.go +++ b/pkg/kubectl/cmd/apply.go @@ -131,6 +131,7 @@ func NewCmdApply(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { // apply subcommands cmd.AddCommand(NewCmdApplyViewLastApplied(f, out, errOut)) cmd.AddCommand(NewCmdApplySetLastApplied(f, out, errOut)) + cmd.AddCommand(NewCmdApplyEditLastApplied(f, out, errOut)) return cmd } diff --git a/pkg/kubectl/cmd/apply_edit_last_applied.go b/pkg/kubectl/cmd/apply_edit_last_applied.go new file mode 100644 index 00000000000..fcf4201d005 --- /dev/null +++ b/pkg/kubectl/cmd/apply_edit_last_applied.go @@ -0,0 +1,103 @@ +/* +Copyright 2017 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 cmd + +import ( + "io" + gruntime "runtime" + + "github.com/spf13/cobra" + + "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/cmd/util/editor" + "k8s.io/kubernetes/pkg/printers" +) + +var ( + applyEditLastAppliedLong = templates.LongDesc(` + Edit the latest last-applied-configuration annotations of resources from the default editor. + + The edit-last-applied command allows you to directly edit any API resource you can retrieve via the + command line tools. It will open the editor defined by your KUBE_EDITOR, or EDITOR + environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows. + You can edit multiple objects, although changes are applied one at a time. The command + accepts filenames as well as command line arguments, although the files you point to must + be previously saved versions of resources. + + The default format is YAML. To edit in JSON, specify "-o json". + + The flag --windows-line-endings can be used to force Windows line endings, + otherwise the default for your operating system will be used. + + In the event an error occurs while updating, a temporary file will be created on disk + that contains your unapplied changes. The most common error when updating a resource + is another editor changing the resource on the server. When this occurs, you will have + to apply your changes to the newer version of the resource, or update your temporary + saved copy to include the latest resource version.`) + + applyEditLastAppliedExample = templates.Examples(` + # Edit the last-applied-configuration annotations by type/name in YAML. + kubectl apply edit-last-applied deployment/nginx + + # Edit the last-applied-configuration annotations by file in JSON. + kubectl apply edit-last-applied -f deploy.yaml -o json`) +) + +func NewCmdApplyEditLastApplied(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { + options := &editor.EditOptions{ + EditMode: editor.ApplyEditMode, + } + + // retrieve a list of handled resources from printer as valid args + validArgs, argAliases := []string{}, []string{} + p, err := f.Printer(nil, printers.PrintOptions{ + ColumnLabels: []string{}, + }) + cmdutil.CheckErr(err) + if p != nil { + validArgs = p.HandledResources() + argAliases = kubectl.ResourceAliases(validArgs) + } + + cmd := &cobra.Command{ + Use: "edit-last-applied (RESOURCE/NAME | -f FILENAME)", + Short: "Edit latest last-applied-configuration annotations of a resource/object", + Long: applyEditLastAppliedLong, + Example: applyEditLastAppliedExample, + Run: func(cmd *cobra.Command, args []string) { + options.ChangeCause = f.Command(cmd, false) + if err := options.Complete(f, out, errOut, args); err != nil { + cmdutil.CheckErr(err) + } + if err := options.Run(); err != nil { + cmdutil.CheckErr(err) + } + }, + ValidArgs: validArgs, + ArgAliases: argAliases, + } + + usage := "to use to edit the resource" + cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) + cmd.Flags().StringVarP(&options.Output, "output", "o", "yaml", "Output format. One of: yaml|json.") + cmd.Flags().BoolVar(&options.WindowsLineEndings, "windows-line-endings", gruntime.GOOS == "windows", "Use Windows line-endings (default Unix line-endings)") + cmdutil.AddRecordVarFlag(cmd, &options.Record) + + return cmd +} diff --git a/pkg/kubectl/cmd/apply_set_last_applied.go b/pkg/kubectl/cmd/apply_set_last_applied.go index 09b8b9e6e86..6f7ccbf4a84 100644 --- a/pkg/kubectl/cmd/apply_set_last_applied.go +++ b/pkg/kubectl/cmd/apply_set_last_applied.go @@ -35,6 +35,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/cmd/util/editor" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/util/i18n" ) @@ -52,12 +53,17 @@ type SetLastAppliedOptions struct { CreateAnnotation bool Output string Codec runtime.Encoder - PatchBufferList [][]byte + PatchBufferList []PatchBuffer Factory cmdutil.Factory Out io.Writer ErrOut io.Writer } +type PatchBuffer struct { + Patch []byte + PatchType types.PatchType +} + var ( applySetLastAppliedLong = templates.LongDesc(i18n.T(` Set the latest last-applied-configuration annotations by setting it to match the contents of a file. @@ -137,8 +143,7 @@ func (o *SetLastAppliedOptions) Validate(f cmdutil.Factory, cmd *cobra.Command) return err } - var diffBuf, patchBuf []byte - patchBuf, diffBuf, err = o.getPatch(info) + patchBuf, diffBuf, patchType, err := editor.GetApplyPatch(info.VersionedObject, o.Codec) if err != nil { return err } @@ -161,7 +166,8 @@ func (o *SetLastAppliedOptions) Validate(f cmdutil.Factory, cmd *cobra.Command) //only add to PatchBufferList when changed if !bytes.Equal(cmdutil.StripComments(oringalBuf), cmdutil.StripComments(diffBuf)) { - o.PatchBufferList = append(o.PatchBufferList, patchBuf) + p := PatchBuffer{Patch: patchBuf, PatchType: patchType} + o.PatchBufferList = append(o.PatchBufferList, p) o.InfoList = append(o.InfoList, info) } else { fmt.Fprintf(o.Out, "set-last-applied %s: no changes required.\n", info.Name) @@ -185,7 +191,7 @@ func (o *SetLastAppliedOptions) RunSetLastApplied(f cmdutil.Factory, cmd *cobra. return err } helper := resource.NewHelper(client, mapping) - patchedObj, err := helper.Patch(o.Namespace, info.Name, types.MergePatchType, patch) + patchedObj, err := helper.Patch(o.Namespace, info.Name, patch.PatchType, patch.Patch) if err != nil { return err } @@ -197,7 +203,7 @@ func (o *SetLastAppliedOptions) RunSetLastApplied(f cmdutil.Factory, cmd *cobra. cmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, o.DryRun, "configured") } else { - err := o.formatPrinter(o.Output, patch) + err := o.formatPrinter(o.Output, patch.Patch, o.Out) if err != nil { return err } @@ -207,7 +213,7 @@ func (o *SetLastAppliedOptions) RunSetLastApplied(f cmdutil.Factory, cmd *cobra. return nil } -func (o *SetLastAppliedOptions) formatPrinter(output string, buf []byte) error { +func (o *SetLastAppliedOptions) formatPrinter(output string, buf []byte, w io.Writer) error { yamlOutput, err := yaml.JSONToYAML(buf) if err != nil { return err @@ -219,9 +225,9 @@ func (o *SetLastAppliedOptions) formatPrinter(output string, buf []byte) error { if err != nil { return err } - fmt.Fprintf(o.Out, string(jsonBuffer.Bytes())) + fmt.Fprintf(w, string(jsonBuffer.Bytes())) case "yaml": - fmt.Fprintf(o.Out, string(yamlOutput)) + fmt.Fprintf(w, string(yamlOutput)) } return nil } diff --git a/pkg/kubectl/cmd/edit_test.go b/pkg/kubectl/cmd/edit_test.go index dd5c7b217b0..55516bb0beb 100644 --- a/pkg/kubectl/cmd/edit_test.go +++ b/pkg/kubectl/cmd/edit_test.go @@ -238,6 +238,8 @@ func TestEdit(t *testing.T) { case "create": cmd = NewCmdCreate(f, buf, errBuf) cmd.Flags().Set("edit", "true") + case "edit-last-applied": + cmd = NewCmdApplyEditLastApplied(f, buf, errBuf) default: t.Errorf("%s: unexpected mode %s", name, testcase.Mode) continue diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/0.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/0.request new file mode 100755 index 00000000000..e69de29bb2d diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/0.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/0.response new file mode 100755 index 00000000000..75d223aadcc --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/0.response @@ -0,0 +1,21 @@ +{ + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "cm1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/configmaps/cm1", + "uid": "cc08a131-3d6f-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3518", + "creationTimestamp": "2017-05-20T15:20:03Z", + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" + } + }, + "data": { + "baz": "qux", + "foo": "changed-value", + "new-data": "new-value", + "new-data2": "new-value" + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/1.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/1.request new file mode 100755 index 00000000000..e69de29bb2d diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/1.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/1.response new file mode 100755 index 00000000000..9703a466ab8 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/1.response @@ -0,0 +1,35 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "svc1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/services/svc1", + "uid": "d8b96f0b-3d6f-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3525", + "creationTimestamp": "2017-05-20T15:20:24Z", + "labels": { + "app": "svc1", + "new-label": "foo" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + }, + "spec": { + "ports": [ + { + "name": "80", + "protocol": "TCP", + "port": 81, + "targetPort": 81 + } + ], + "clusterIP": "172.30.32.183", + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.edited b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.edited new file mode 100755 index 00000000000..1769ec8faed --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.edited @@ -0,0 +1,38 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +apiVersion: v1 +items: +- apiVersion: v1 + data: + baz: qux + foo: changed-value + new-data: new-value + new-data2: new-value + new-data3: newivalue + kind: ConfigMap + metadata: + annotations: {} + name: cm1 + namespace: myproject +- kind: Service + metadata: + annotations: {} + labels: + app: svc1 + new-label: foo + new-label2: foo2 + name: svc1 + namespace: myproject + spec: + ports: + - name: "80" + port: 82 + protocol: TCP + targetPort: 81 + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +kind: List +metadata: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.original b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.original new file mode 100755 index 00000000000..82770327ca1 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/2.original @@ -0,0 +1,36 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +apiVersion: v1 +items: +- apiVersion: v1 + data: + baz: qux + foo: changed-value + new-data: new-value + new-data2: new-value + kind: ConfigMap + metadata: + annotations: {} + name: cm1 + namespace: myproject +- kind: Service + metadata: + annotations: {} + labels: + app: svc1 + new-label: foo + name: svc1 + namespace: myproject + spec: + ports: + - name: "80" + port: 81 + protocol: TCP + targetPort: 81 + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +kind: List +metadata: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.request new file mode 100755 index 00000000000..4998c9e8686 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.request @@ -0,0 +1,7 @@ +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io~1last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\",\"new-data3\":\"newivalue\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" + } + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.response new file mode 100755 index 00000000000..e2115785efb --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/3.response @@ -0,0 +1,21 @@ +{ + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "cm1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/configmaps/cm1", + "uid": "cc08a131-3d6f-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3554", + "creationTimestamp": "2017-05-20T15:20:03Z", + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\",\"new-data3\":\"newivalue\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" + } + }, + "data": { + "baz": "qux", + "foo": "changed-value", + "new-data": "new-value", + "new-data2": "new-value" + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.request new file mode 100755 index 00000000000..a9f7bce400e --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.request @@ -0,0 +1,7 @@ +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io~1last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label2\":\"foo2\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":82,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.response new file mode 100755 index 00000000000..dcf030403c1 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/4.response @@ -0,0 +1,35 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "svc1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/services/svc1", + "uid": "d8b96f0b-3d6f-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3555", + "creationTimestamp": "2017-05-20T15:20:24Z", + "labels": { + "app": "svc1", + "new-label": "foo" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label2\":\"foo2\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":82,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + }, + "spec": { + "ports": [ + { + "name": "80", + "protocol": "TCP", + "port": 81, + "targetPort": 81 + } + ], + "clusterIP": "172.30.32.183", + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/test.yaml b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/test.yaml new file mode 100755 index 00000000000..ef9f5588de1 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-list/test.yaml @@ -0,0 +1,40 @@ +description: add a testcase description +mode: edit-last-applied +args: +- configmaps/cm1 +- service/svc1 +namespace: "myproject" +expectedStdout: +- configmap "cm1" edited +- service "svc1" edited +expectedExitCode: 0 +steps: +- type: request + expectedMethod: GET + expectedPath: /api/v1/namespaces/myproject/configmaps/cm1 + expectedInput: 0.request + resultingStatusCode: 200 + resultingOutput: 0.response +- type: request + expectedMethod: GET + expectedPath: /api/v1/namespaces/myproject/services/svc1 + expectedInput: 1.request + resultingStatusCode: 200 + resultingOutput: 1.response +- type: edit + expectedInput: 2.original + resultingOutput: 2.edited +- type: request + expectedMethod: PATCH + expectedPath: /api/v1/namespaces/myproject/configmaps/cm1 + expectedContentType: application/merge-patch+json + expectedInput: 3.request + resultingStatusCode: 200 + resultingOutput: 3.response +- type: request + expectedMethod: PATCH + expectedPath: /api/v1/namespaces/myproject/services/svc1 + expectedContentType: application/merge-patch+json + expectedInput: 4.request + resultingStatusCode: 200 + resultingOutput: 4.response diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/0.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/0.request new file mode 100755 index 00000000000..e69de29bb2d diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/0.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/0.response new file mode 100755 index 00000000000..23ee361bca8 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/0.response @@ -0,0 +1,35 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "svc1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/services/svc1", + "uid": "1e16d988-3d72-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3731", + "creationTimestamp": "2017-05-20T15:36:39Z", + "labels": { + "app": "svc1", + "new-label": "foo" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + }, + "spec": { + "ports": [ + { + "name": "80", + "protocol": "TCP", + "port": 81, + "targetPort": 81 + } + ], + "clusterIP": "172.30.105.209", + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.edited b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.edited new file mode 100755 index 00000000000..4f5e571f468 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.edited @@ -0,0 +1,21 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +kind: Service +metadata: + annotations: {} + labels: + app: svc1 + new-label: foo + name: svc1 + namespace: myproject +spec + ports: + name: "80" + port: 81 + protocol: TCP + targetPort: 81 + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: { diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.original b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.original new file mode 100755 index 00000000000..8a290b4b12d --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/1.original @@ -0,0 +1,21 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +kind: Service +metadata: + annotations: {} + labels: + app: svc1 + new-label: foo + name: svc1 + namespace: myproject +spec: + ports: + - name: "80" + port: 81 + protocol: TCP + targetPort: 81 + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.edited b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.edited new file mode 100755 index 00000000000..e7759c9c224 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.edited @@ -0,0 +1,24 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +# The edited file had a syntax error: error converting YAML to JSON: yaml: line 12: could not find expected ':' +# +kind: Service +metadata: + annotations: {} + labels: + app: svc1 + new-label: foo + new-label1: foo1 + name: svc1 + namespace: myproject +spec: + ports: + - name: "80" + port: 81 + protocol: TCP + targetPort: 81 + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.original b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.original new file mode 100755 index 00000000000..d17734396ff --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/2.original @@ -0,0 +1,23 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +# The edited file had a syntax error: error converting YAML to JSON: yaml: line 12: could not find expected ':' +# +kind: Service +metadata: + annotations: {} + labels: + app: svc1 + new-label: foo + name: svc1 + namespace: myproject +spec + ports: + name: "80" + port: 81 + protocol: TCP + targetPort: 81 + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: { diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.request new file mode 100755 index 00000000000..c511d3965f8 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.request @@ -0,0 +1,7 @@ +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io~1last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label1\":\"foo1\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.response new file mode 100755 index 00000000000..ca945db3ec1 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/3.response @@ -0,0 +1,35 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "svc1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/services/svc1", + "uid": "1e16d988-3d72-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3857", + "creationTimestamp": "2017-05-20T15:36:39Z", + "labels": { + "app": "svc1", + "new-label": "foo" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label1\":\"foo1\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + }, + "spec": { + "ports": [ + { + "name": "80", + "protocol": "TCP", + "port": 81, + "targetPort": 81 + } + ], + "clusterIP": "172.30.105.209", + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/test.yaml b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/test.yaml new file mode 100755 index 00000000000..f500dc65edd --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied-syntax-error/test.yaml @@ -0,0 +1,28 @@ +description: edit with a syntax error, then re-edit and save +mode: edit-last-applied +args: +- service/svc1 +namespace: myproject +expectedStdout: +- "service \"svc1\" edited" +expectedExitCode: 0 +steps: +- type: request + expectedMethod: GET + expectedPath: /api/v1/namespaces/myproject/services/svc1 + expectedInput: 0.request + resultingStatusCode: 200 + resultingOutput: 0.response +- type: edit + expectedInput: 1.original + resultingOutput: 1.edited +- type: edit + expectedInput: 2.original + resultingOutput: 2.edited +- type: request + expectedMethod: PATCH + expectedPath: /api/v1/namespaces/myproject/services/svc1 + expectedContentType: application/merge-patch+json + expectedInput: 3.request + resultingStatusCode: 200 + resultingOutput: 3.response diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/0.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/0.request new file mode 100755 index 00000000000..e69de29bb2d diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/0.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/0.response new file mode 100755 index 00000000000..7bb8832afbc --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/0.response @@ -0,0 +1,38 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "svc1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/services/svc1", + "uid": "bc66b442-3d6a-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3036", + "creationTimestamp": "2017-05-20T14:43:49Z", + "labels": { + "app": "svc1", + "new-label": "new-value" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-01T21:14:09Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"myproject\",\"resourceVersion\":\"20820\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + }, + "spec": { + "ports": [ + { + "name": "80", + "protocol": "TCP", + "port": 81, + "targetPort": 80 + } + ], + "selector": { + "app": "svc1" + }, + "clusterIP": "172.30.136.24", + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.edited b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.edited new file mode 100755 index 00000000000..22e33fb814b --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.edited @@ -0,0 +1,26 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +apiVersion: v1 +kind: Service +metadata: + annotations: {} + creationTimestamp: 2017-02-01T21:14:09Z + labels: + app: svc1 + new-label: new-value + name: svc1 + namespace: myproject + resourceVersion: "20820" +spec: + ports: + - name: "80" + port: 81 + protocol: TCP + targetPort: 92 + selector: + app: svc1 + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.original b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.original new file mode 100755 index 00000000000..e863ac7098c --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/1.original @@ -0,0 +1,26 @@ +# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +apiVersion: v1 +kind: Service +metadata: + annotations: {} + creationTimestamp: 2017-02-01T21:14:09Z + labels: + app: svc1 + new-label: new-value + name: svc1 + namespace: myproject + resourceVersion: "20820" +spec: + ports: + - name: "80" + port: 81 + protocol: TCP + targetPort: 80 + selector: + app: svc1 + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.request b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.request new file mode 100755 index 00000000000..448ebbba2b3 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.request @@ -0,0 +1,7 @@ +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io~1last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-01T21:14:09Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"myproject\",\"resourceVersion\":\"20820\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":92}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.response b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.response new file mode 100755 index 00000000000..d5b2b9b8988 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/2.response @@ -0,0 +1,38 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "svc1", + "namespace": "myproject", + "selfLink": "/api/v1/namespaces/myproject/services/svc1", + "uid": "bc66b442-3d6a-11e7-8ef0-c85b76034b7b", + "resourceVersion": "3093", + "creationTimestamp": "2017-05-20T14:43:49Z", + "labels": { + "app": "svc1", + "new-label": "new-value" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-01T21:14:09Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"myproject\",\"resourceVersion\":\"20820\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":92}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" + } + }, + "spec": { + "ports": [ + { + "name": "80", + "protocol": "TCP", + "port": 81, + "targetPort": 80 + } + ], + "selector": { + "app": "svc1" + }, + "clusterIP": "172.30.136.24", + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/test.yaml b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/test.yaml new file mode 100755 index 00000000000..a284da093f2 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-apply-edit-last-applied/test.yaml @@ -0,0 +1,27 @@ +description: add a testcase description +mode: edit-last-applied +args: +- service +- svc1 +outputFormat: yaml +namespace: myproject +expectedStdout: +- service "svc1" edited +expectedExitCode: 0 +steps: +- type: request + expectedMethod: GET + expectedPath: /api/v1/namespaces/myproject/services/svc1 + expectedInput: 0.request + resultingStatusCode: 200 + resultingOutput: 0.response +- type: edit + expectedInput: 1.original + resultingOutput: 1.edited +- type: request + expectedMethod: PATCH + expectedPath: /api/v1/namespaces/myproject/services/svc1 + expectedContentType: application/merge-patch+json + expectedInput: 2.request + resultingStatusCode: 200 + resultingOutput: 2.response diff --git a/pkg/kubectl/cmd/util/editor/editoptions.go b/pkg/kubectl/cmd/util/editor/editoptions.go index 3a5e649bcde..96a0bca9dfd 100644 --- a/pkg/kubectl/cmd/util/editor/editoptions.go +++ b/pkg/kubectl/cmd/util/editor/editoptions.go @@ -19,6 +19,7 @@ package editor import ( "bufio" "bytes" + "errors" "fmt" "io" "os" @@ -29,7 +30,7 @@ import ( "github.com/evanphx/json-patch" "github.com/golang/glog" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -85,7 +86,7 @@ type editPrinterOptions struct { // Complete completes all the required options func (o *EditOptions) Complete(f cmdutil.Factory, out, errOut io.Writer, args []string) error { - if o.EditMode != NormalEditMode && o.EditMode != EditBeforeCreateMode { + if o.EditMode != NormalEditMode && o.EditMode != EditBeforeCreateMode && o.EditMode != ApplyEditMode { return fmt.Errorf("unsupported edit mode %q", o.EditMode) } if o.Output != "" { @@ -104,8 +105,8 @@ func (o *EditOptions) Complete(f cmdutil.Factory, out, errOut io.Writer, args [] return err } b := resource.NewBuilder(mapper, f.CategoryExpander(), typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), unstructured.UnstructuredJSONScheme) - if o.EditMode == NormalEditMode { - // when do normal edit we need to always retrieve the latest resource from server + if o.EditMode == NormalEditMode || o.EditMode == ApplyEditMode { + // when do normal edit or apply edit we need to always retrieve the latest resource from server b = b.ResourceTypeOrNameArgs(true, args...).Latest() } r := b.NamespaceParam(cmdNamespace).DefaultNamespace(). @@ -158,7 +159,6 @@ func (o *EditOptions) Run() error { ) containsError := false - // loop until we succeed or cancel editing for { // get the object we're going to serialize as input to the editor @@ -188,7 +188,7 @@ func (o *EditOptions) Run() error { } if o.editPrinterOptions.addHeader { - results.header.writeTo(w) + results.header.writeTo(w, o.EditMode) } if !containsError { @@ -230,7 +230,7 @@ func (o *EditOptions) Run() error { file: file, } containsError = true - fmt.Fprintln(o.ErrOut, results.addError(errors.NewInvalid(api.Kind(""), "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), infos[0])) + fmt.Fprintln(o.ErrOut, results.addError(apierrors.NewInvalid(api.Kind(""), "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), infos[0])) continue } @@ -280,6 +280,8 @@ func (o *EditOptions) Run() error { switch o.EditMode { case NormalEditMode: err = o.visitToPatch(infos, updatedVisitor, &results) + case ApplyEditMode: + err = o.visitToApplyEditPatch(infos, updatedVisitor) case EditBeforeCreateMode: err = o.visitToCreate(updatedVisitor) default: @@ -326,6 +328,31 @@ func (o *EditOptions) Run() error { return err } return editFn(infos) + case ApplyEditMode: + infos, err := o.OriginalResult.Infos() + if err != nil { + return err + } + var annotationInfos []*resource.Info + for i := range infos { + data, err := kubectl.GetOriginalConfiguration(infos[i].Mapping, infos[i].Object) + if err != nil { + return err + } + if data == nil { + continue + } + + tempInfos, err := o.updatedResultGetter(data).Infos() + if err != nil { + return err + } + annotationInfos = append(annotationInfos, tempInfos[0]) + } + if len(annotationInfos) == 0 { + return errors.New("no last-applied-configuration annotation found on resources, to create the annotation, use command `kubectl apply set-last-applied --create-annotation`") + } + return editFn(annotationInfos) // If doing an edit before created, we don't want a list and instead want the normal behavior as kubectl create. case EditBeforeCreateMode: return o.OriginalResult.Visit(func(info *resource.Info, err error) error { @@ -336,6 +363,110 @@ func (o *EditOptions) Run() error { } } +func (o *EditOptions) visitToApplyEditPatch(originalInfos []*resource.Info, patchVisitor resource.Visitor) error { + err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error { + editObjUID, err := meta.NewAccessor().UID(info.Object) + if err != nil { + return err + } + + var originalInfo *resource.Info + for _, i := range originalInfos { + originalObjUID, err := meta.NewAccessor().UID(i.Object) + if err != nil { + return err + } + if editObjUID == originalObjUID { + originalInfo = i + break + } + } + if originalInfo == nil { + return fmt.Errorf("no original object found for %#v", info.Object) + } + + originalJS, err := encodeToJson(o.Encoder, originalInfo.Object) + if err != nil { + return err + } + + editedJS, err := encodeToJson(o.Encoder, info.Object) + if err != nil { + return err + } + + if reflect.DeepEqual(originalJS, editedJS) { + cmdutil.PrintSuccess(o.Mapper, false, o.Out, info.Mapping.Resource, info.Name, false, "skipped") + return nil + } else { + err := o.annotationPatch(info) + if err != nil { + return err + } + cmdutil.PrintSuccess(o.Mapper, false, o.Out, info.Mapping.Resource, info.Name, false, "edited") + return nil + } + }) + return err +} + +func (o *EditOptions) annotationPatch(update *resource.Info) error { + patch, _, patchType, err := GetApplyPatch(update.Object, o.Encoder) + if err != nil { + return err + } + mapping := update.ResourceMapping() + client, err := o.f.UnstructuredClientForMapping(mapping) + if err != nil { + return err + } + helper := resource.NewHelper(client, mapping) + _, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch) + if err != nil { + return err + } + return nil +} + +func GetApplyPatch(obj runtime.Object, codec runtime.Encoder) ([]byte, []byte, types.PatchType, error) { + beforeJSON, err := encodeToJson(codec, obj) + if err != nil { + return nil, []byte(""), types.MergePatchType, err + } + objCopy, err := api.Scheme.Copy(obj) + if err != nil { + return nil, beforeJSON, types.MergePatchType, err + } + accessor := meta.NewAccessor() + annotations, err := accessor.Annotations(objCopy) + if err != nil { + return nil, beforeJSON, types.MergePatchType, err + } + if annotations == nil { + annotations = map[string]string{} + } + annotations[api.LastAppliedConfigAnnotation] = string(beforeJSON) + accessor.SetAnnotations(objCopy, annotations) + afterJSON, err := encodeToJson(codec, objCopy) + if err != nil { + return nil, beforeJSON, types.MergePatchType, err + } + patch, err := jsonpatch.CreateMergePatch(beforeJSON, afterJSON) + return patch, beforeJSON, types.MergePatchType, err +} + +func encodeToJson(codec runtime.Encoder, obj runtime.Object) ([]byte, error) { + serialization, err := runtime.Encode(codec, obj) + if err != nil { + return nil, err + } + js, err := yaml.ToJSON(serialization) + if err != nil { + return nil, err + } + return js, nil +} + func getPrinter(format string) *editPrinterOptions { switch format { case "json": @@ -386,22 +517,12 @@ func (o *EditOptions) visitToPatch( return fmt.Errorf("no original object found for %#v", info.Object) } - originalSerialization, err := runtime.Encode(o.Encoder, originalInfo.Object) - if err != nil { - return err - } - editedSerialization, err := runtime.Encode(o.Encoder, info.Object) + originalJS, err := encodeToJson(o.Encoder, originalInfo.Object) if err != nil { return err } - // compute the patch on a per-item basis - // use strategic merge to create a patch - originalJS, err := yaml.ToJSON(originalSerialization) - if err != nil { - return err - } - editedJS, err := yaml.ToJSON(editedSerialization) + editedJS, err := encodeToJson(o.Encoder, info.Object) if err != nil { return err } @@ -501,6 +622,7 @@ type EditMode string const ( NormalEditMode EditMode = "normal_mode" EditBeforeCreateMode EditMode = "edit_before_create_mode" + ApplyEditMode EditMode = "edit_last_applied_mode" ) // editReason preserves a message about the reason this file must be edited again @@ -515,12 +637,20 @@ type editHeader struct { } // writeTo outputs the current header information into a stream -func (h *editHeader) writeTo(w io.Writer) error { - fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored, +func (h *editHeader) writeTo(w io.Writer, editMode EditMode) error { + if editMode == ApplyEditMode { + fmt.Fprint(w, `# Please edit the 'last-applied-configuration' annotations below. +# Lines beginning with a '#' will be ignored, and an empty file will abort the edit. +# +`) + } else { + fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # `) + } + for _, r := range h.reasons { if len(r.other) > 0 { fmt.Fprintf(w, "# %s:\n", r.head) @@ -552,12 +682,12 @@ type editResults struct { func (r *editResults) addError(err error, info *resource.Info) string { switch { - case errors.IsInvalid(err): + case apierrors.IsInvalid(err): r.edit = append(r.edit, info) reason := editReason{ head: fmt.Sprintf("%s %q was not valid", info.Mapping.Resource, info.Name), } - if err, ok := err.(errors.APIStatus); ok { + if err, ok := err.(apierrors.APIStatus); ok { if details := err.Status().Details; details != nil { for _, cause := range details.Causes { reason.other = append(reason.other, fmt.Sprintf("%s: %s", cause.Field, cause.Message)) @@ -566,7 +696,7 @@ func (r *editResults) addError(err error, info *resource.Info) string { } r.header.reasons = append(r.header.reasons, reason) return fmt.Sprintf("error: %s %q is invalid", info.Mapping.Resource, info.Name) - case errors.IsNotFound(err): + case apierrors.IsNotFound(err): r.notfound++ return fmt.Sprintf("error: %s %q could not be found on the server", info.Mapping.Resource, info.Name) default: diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index fc7e2b5f1e9..867d5c55cea 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -436,7 +436,7 @@ func AddApplyAnnotationFlags(cmd *cobra.Command) { } func AddApplyAnnotationVarFlags(cmd *cobra.Command, applyAnnotation *bool) { - cmd.Flags().BoolVar(applyAnnotation, ApplyAnnotationsFlag, false, "If true, the configuration of current object will be saved in its annotation. This is useful when you want to perform kubectl apply on this object in the future.") + cmd.Flags().BoolVar(applyAnnotation, ApplyAnnotationsFlag, false, "If true, the configuration of current object will be saved in its annotation. Otherwise, the annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.") } // AddGeneratorFlags adds flags common to resource generation commands