From ec271f5c09fdc8fe01529a6033f1ca280b1f5eb6 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Sun, 12 Feb 2017 19:31:06 -0500 Subject: [PATCH] Switch 'kubectl edit' to use unstructured objects, handle schemaless objects mark --output-version as deprecated, add example for fully-qualifying version to edit Add 'kubectl edit' testcase for editing schemaed and schemaless data together Add 'kubectl edit' testcase for editing unknown version of known group/kind --- pkg/kubectl/cmd/edit.go | 249 +++++++++--------- pkg/kubectl/cmd/edit_test.go | 16 +- .../edit/testcase-schemaless-list/0.request | 0 .../edit/testcase-schemaless-list/0.response | 32 +++ .../edit/testcase-schemaless-list/1.request | 0 .../edit/testcase-schemaless-list/1.response | 16 ++ .../edit/testcase-schemaless-list/2.request | 0 .../edit/testcase-schemaless-list/2.response | 21 ++ .../edit/testcase-schemaless-list/3.edited | 62 +++++ .../edit/testcase-schemaless-list/3.original | 59 +++++ .../edit/testcase-schemaless-list/4.request | 7 + .../edit/testcase-schemaless-list/4.response | 33 +++ .../edit/testcase-schemaless-list/5.request | 3 + .../edit/testcase-schemaless-list/5.response | 17 ++ .../edit/testcase-schemaless-list/6.request | 6 + .../edit/testcase-schemaless-list/6.response | 22 ++ .../edit/testcase-schemaless-list/test.yaml | 55 ++++ .../0.request | 0 .../0.response | 22 ++ .../1.edited | 22 ++ .../1.original | 20 ++ .../2.request | 11 + .../2.response | 24 ++ .../test.yaml | 25 ++ pkg/kubectl/cmd/testing/fake.go | 33 +++ 25 files changed, 622 insertions(+), 133 deletions(-) create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/0.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/0.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/1.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/1.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/2.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/2.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.edited create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.original create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/test.yaml create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/0.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/0.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.edited create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.original create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.request create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.response create mode 100755 pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/test.yaml diff --git a/pkg/kubectl/cmd/edit.go b/pkg/kubectl/cmd/edit.go index 48ae40f518..b6cc9c4a3e 100644 --- a/pkg/kubectl/cmd/edit.go +++ b/pkg/kubectl/cmd/edit.go @@ -46,6 +46,7 @@ import ( "k8s.io/kubernetes/pkg/util/crlf" "k8s.io/kubernetes/pkg/util/i18n" + jsonpatch "github.com/evanphx/json-patch" "github.com/golang/glog" "github.com/spf13/cobra" ) @@ -61,9 +62,12 @@ var ( accepts filenames as well as command line arguments, although the files you point to must be previously saved versions of resources. - The files to edit will be output in the default API version, or a version specified - by --output-version. The default format is YAML - if you would like to edit in JSON - pass -o json. The flag --windows-line-endings can be used to force Windows line endings, + Editing is done with the API version used to fetch the resource. + To edit using a specific API version, fully-qualify the resource, version, and group. + + 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 @@ -79,8 +83,8 @@ var ( # Use an alternative editor KUBE_EDITOR="nano" kubectl edit svc/docker-registry - # Edit the service 'docker-registry' in JSON using the v1 API format: - kubectl edit svc/docker-registry --output-version=v1 -o json`) + # Edit the job 'myjob' in JSON using the v1 API format: + kubectl edit job.v1.batch/myjob -o json`) ) func NewCmdEdit(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { @@ -113,7 +117,10 @@ func NewCmdEdit(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmdutil.AddFilenameOptionFlags(cmd, options, usage) cmdutil.AddValidateFlags(cmd) cmd.Flags().StringP("output", "o", "yaml", "Output format. One of: yaml|json.") - cmd.Flags().String("output-version", "", "Output the formatted object with the given group version (for ex: 'extensions/v1beta1').") + cmd.Flags().String("output-version", "", "DEPRECATED: To edit using a specific API version, fully-qualify the resource, version, and group (for example: 'jobs.v1.batch/myjob').") + cmd.Flags().MarkDeprecated("output-version", "editing is now done using the resource exactly as fetched from the API. To edit using a specific API version, fully-qualify the resource, version, and group (for example: 'jobs.v1.batch/myjob').") + cmd.Flags().MarkHidden("output-version") + cmd.Flags().Bool("windows-line-endings", gruntime.GOOS == "windows", "Use Windows line-endings (default Unix line-endings)") cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddRecordFlag(cmd) @@ -125,6 +132,10 @@ func RunEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args return runEdit(f, out, errOut, cmd, args, options, NormalEditMode) } +// runEdit performs an interactive edit on the resources specified by filename or resource builder args. +// in NormalEditMode, all resources are edited as a single list. +// in CreateEditMode, resources are edited one-by-one. +// TODO: refactor runEdit and editFn into smaller simpler chunks func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, options *resource.FilenameOptions, editMode EditMode) error { o, err := getPrinter(cmd) if err != nil { @@ -136,22 +147,14 @@ func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args return err } - clientConfig, err := f.ClientConfig() - if err != nil { - return err - } - encoder := f.JSONEncoder() - defaultVersion, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion) - if err != nil { - return err - } var ( windowsLineEndings = cmdutil.GetFlagBool(cmd, "windows-line-endings") edit = editor.NewDefaultEditor(f.EditorEnvs()) ) + // editFn is invoked for each edit session (once with a list for normal edit, once for each individual resource in a edit-on-create invocation) editFn := func(infos []*resource.Info) error { var ( results = editResults{} @@ -161,13 +164,27 @@ func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args ) containsError := false - for { - originalObj, err := resource.AsVersionedObject(infos, false, defaultVersion, encoder) - if err != nil { - return err - } - objToEdit := originalObj + // loop until we succeed or cancel editing + for { + // get the object we're going to serialize as input to the editor + var originalObj runtime.Object + switch len(infos) { + case 1: + originalObj = infos[0].Object + default: + l := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "kind": "List", + "apiVersion": "v1", + "metadata": map[string]interface{}{}, + }, + } + for _, info := range infos { + l.Items = append(l.Items, info.Object.(*unstructured.Unstructured)) + } + originalObj = l + } // generate the file to edit buf := &bytes.Buffer{} @@ -181,7 +198,7 @@ func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args } if !containsError { - if err := o.printer.PrintObj(objToEdit, w); err != nil { + if err := o.printer.PrintObj(originalObj, w); err != nil { return preservedFile(err, results.file, errOut) } original = buf.Bytes() @@ -245,39 +262,34 @@ func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args } // parse the edited file - var updatedVisitor resource.Visitor - if updatedInfos, err := updatedResultsGetter(edited).Infos(); err != nil { + updatedInfos, err := updatedResultsGetter(edited).Infos() + if err != nil { // syntax error containsError = true results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)}) continue - } else { - updatedVisitor = resource.InfoListVisitor(updatedInfos) } // not a syntax error as it turns out... containsError = false + updatedVisitor := resource.InfoListVisitor(updatedInfos) - namespaceVisitor := updatedVisitor // need to make sure the original namespace wasn't changed while editing - if err = namespaceVisitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil { + if err := updatedVisitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil { return preservedFile(err, file, errOut) } // iterate through all items to apply annotations - annotationVisitor := updatedVisitor - if _, err := visitAnnotation(cmd, f, annotationVisitor, encoder); err != nil { + if err := visitAnnotation(cmd, f, updatedVisitor, encoder); err != nil { return preservedFile(err, file, errOut) } switch editMode { case NormalEditMode: - patchVisitor := updatedVisitor - err = visitToPatch(originalObj, patchVisitor, mapper, encoder, out, errOut, defaultVersion, &results, file) + err = visitToPatch(infos, updatedVisitor, mapper, encoder, out, errOut, &results, file) case EditBeforeCreateMode: - createVisitor := updatedVisitor - err = visitToCreate(createVisitor, mapper, out, errOut, defaultVersion, &results, file) + err = visitToCreate(updatedVisitor, mapper, out, errOut, &results, file) default: - err = fmt.Errorf("Not supported edit mode %q", editMode) + err = fmt.Errorf("Unsupported edit mode %q", editMode) } if err != nil { return preservedFile(err, results.file, errOut) @@ -326,7 +338,7 @@ func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args return editFn([]*resource.Info{info}) }) default: - return fmt.Errorf("Not supported edit mode %q", editMode) + return fmt.Errorf("Unsupported edit mode %q", editMode) } } @@ -352,36 +364,30 @@ func getPrinter(cmd *cobra.Command) (*editPrinterOptions, error) { type resultGetter func([]byte) *resource.Result +// getMapperAndResult obtains the initial set of resources to edit, and returns: +// * mapper: restmapper used for printing objects +// * result: initial set of resources to edit. contains latest versions from the server when in normal editing mode +// * resultGetter: function that returns a set of resources parsed from user input. used to get resources from edited file. +// * cmdNamespace: namespace the edit was invoked with. used to verify namespaces don't change during editing. +// * error: any error that occurs fetching initial resources or building results. func getMapperAndResult(f cmdutil.Factory, args []string, options *resource.FilenameOptions, editMode EditMode) (meta.RESTMapper, *resource.Result, resultGetter, string, error) { + if editMode != NormalEditMode && editMode != EditBeforeCreateMode { + return nil, nil, nil, "", fmt.Errorf("Unsupported edit mode %q", editMode) + } + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() if err != nil { return nil, nil, nil, "", err } - var mapper meta.RESTMapper - var typer runtime.ObjectTyper - switch editMode { - case NormalEditMode: - mapper, typer = f.Object() - case EditBeforeCreateMode: - mapper, typer, err = f.UnstructuredObject() - default: - return nil, nil, nil, "", fmt.Errorf("Not supported edit mode %q", editMode) - } + mapper, typer, err := f.UnstructuredObject() if err != nil { return nil, nil, nil, "", err } - // resource builder to read objects from the original file or arg invocation - var b *resource.Builder - switch editMode { - case NormalEditMode: - b = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)). - ResourceTypeOrNameArgs(true, args...). - Latest() - case EditBeforeCreateMode: - b = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), unstructured.UnstructuredJSONScheme) - default: - return nil, nil, nil, "", fmt.Errorf("Not supported edit mode %q", editMode) + b := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), unstructured.UnstructuredJSONScheme) + if editMode == NormalEditMode { + // if in normal mode, also read from args, and fetch latest from the server + b = b.ResourceTypeOrNameArgs(true, args...).Latest() } originalResult := b.NamespaceParam(cmdNamespace).DefaultNamespace(). @@ -395,64 +401,48 @@ func getMapperAndResult(f cmdutil.Factory, args []string, options *resource.File } updatedResultGetter := func(data []byte) *resource.Result { - // resource builder to read objects from the original file or arg invocation - var b *resource.Builder - switch editMode { - case NormalEditMode: - b = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)) - case EditBeforeCreateMode: - b = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), unstructured.UnstructuredJSONScheme) - } - return b.Stream(bytes.NewReader(data), "edited-file").ContinueOnError().Flatten().Do() + // resource builder to read objects from edited data + return resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), unstructured.UnstructuredJSONScheme). + Stream(bytes.NewReader(data), "edited-file"). + ContinueOnError(). + Flatten(). + Do() } return mapper, originalResult, updatedResultGetter, cmdNamespace, err } func visitToPatch( - originalObj runtime.Object, + originalInfos []*resource.Info, patchVisitor resource.Visitor, mapper meta.RESTMapper, encoder runtime.Encoder, out, errOut io.Writer, - defaultVersion schema.GroupVersion, results *editResults, file string, ) error { err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error { - currOriginalObj := originalObj - - // if we're editing a list, then navigate the list to find the item that we're currently trying to edit - if meta.IsListType(originalObj) { - currOriginalObj = nil - editObjUID, err := meta.NewAccessor().UID(info.Object) - if err != nil { - return err - } - - listItems, err := meta.ExtractList(originalObj) - if err != nil { - return err - } - - // iterate through the list to find the item with the matching UID - for i := range listItems { - originalObjUID, err := meta.NewAccessor().UID(listItems[i]) - if err != nil { - return err - } - if editObjUID == originalObjUID { - currOriginalObj = listItems[i] - break - } - } - if currOriginalObj == nil { - return fmt.Errorf("no original object found for %#v", info.Object) - } - + editObjUID, err := meta.NewAccessor().UID(info.Object) + if err != nil { + return err } - originalSerialization, err := runtime.Encode(encoder, currOriginalObj) + 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) + } + + originalSerialization, err := runtime.Encode(encoder, originalInfo.Object) if err != nil { return err } @@ -478,19 +468,47 @@ func visitToPatch( return nil } - preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"), - mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")} - patch, err := strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, currOriginalObj, preconditions...) - if err != nil { - glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) - if mergepatch.IsPreconditionFailed(err) { - return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") - } - return err + preconditions := []mergepatch.PreconditionFunc{ + mergepatch.RequireKeyUnchanged("apiVersion"), + mergepatch.RequireKeyUnchanged("kind"), + mergepatch.RequireMetadataKeyUnchanged("name"), } - results.version = defaultVersion - patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch) + // Create the versioned struct from the type defined in the mapping + // (which is the API version we'll be submitting the patch to) + versionedObject, err := api.Scheme.New(info.Mapping.GroupVersionKind) + var patchType types.PatchType + var patch []byte + switch { + case runtime.IsNotRegisteredError(err): + // fall back to generic JSON merge patch + patchType = types.MergePatchType + patch, err = jsonpatch.CreateMergePatch(originalJS, editedJS) + if err != nil { + glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) + return err + } + for _, precondition := range preconditions { + if !precondition(patch) { + glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) + return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") + } + } + case err != nil: + return err + default: + patchType = types.StrategicMergePatchType + patch, err = strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, versionedObject, preconditions...) + if err != nil { + glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) + if mergepatch.IsPreconditionFailed(err) { + return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") + } + return err + } + } + + patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, patchType, patch) if err != nil { fmt.Fprintln(errOut, results.addError(err, info)) return nil @@ -502,9 +520,8 @@ func visitToPatch( return err } -func visitToCreate(createVisitor resource.Visitor, mapper meta.RESTMapper, out, errOut io.Writer, defaultVersion schema.GroupVersion, results *editResults, file string) error { +func visitToCreate(createVisitor resource.Visitor, mapper meta.RESTMapper, out, errOut io.Writer, results *editResults, file string) error { err := createVisitor.Visit(func(info *resource.Info, incomingErr error) error { - results.version = defaultVersion if err := createAndRefresh(info); err != nil { return err } @@ -514,8 +531,7 @@ func visitToCreate(createVisitor resource.Visitor, mapper meta.RESTMapper, out, return err } -func visitAnnotation(cmd *cobra.Command, f cmdutil.Factory, annotationVisitor resource.Visitor, encoder runtime.Encoder) ([]runtime.Object, error) { - mutatedObjects := []runtime.Object{} +func visitAnnotation(cmd *cobra.Command, f cmdutil.Factory, annotationVisitor resource.Visitor, encoder runtime.Encoder) error { // iterate through all items to apply annotations err := annotationVisitor.Visit(func(info *resource.Info, incomingErr error) error { // put configuration annotation in "updates" @@ -527,12 +543,9 @@ func visitAnnotation(cmd *cobra.Command, f cmdutil.Factory, annotationVisitor re return err } } - mutatedObjects = append(mutatedObjects, info.Object) - return nil - }) - return mutatedObjects, err + return err } type EditMode string diff --git a/pkg/kubectl/cmd/edit_test.go b/pkg/kubectl/cmd/edit_test.go index 6826bf19f2..91d3da7cbc 100644 --- a/pkg/kubectl/cmd/edit_test.go +++ b/pkg/kubectl/cmd/edit_test.go @@ -205,22 +205,8 @@ func TestEdit(t *testing.T) { t.Fatalf("%s: %v", name, err) } - f, tf, _, ns := cmdtesting.NewAPIFactory() + f, tf, _, _ := cmdtesting.NewAPIFactory() tf.Printer = &testPrinter{} - tf.ClientForMappingFunc = func(mapping *meta.RESTMapping) (resource.RESTClient, error) { - versionedAPIPath := "" - if mapping.GroupVersionKind.Group == "" { - versionedAPIPath = "/api/" + mapping.GroupVersionKind.Version - } else { - versionedAPIPath = "/apis/" + mapping.GroupVersionKind.Group + "/" + mapping.GroupVersionKind.Version - } - return &fake.RESTClient{ - APIRegistry: api.Registry, - VersionedAPIPath: versionedAPIPath, - NegotiatedSerializer: ns, //unstructuredSerializer, - Client: fake.CreateHTTPClient(reqResp), - }, nil - } tf.UnstructuredClientForMappingFunc = func(mapping *meta.RESTMapping) (resource.RESTClient, error) { versionedAPIPath := "" if mapping.GroupVersionKind.Group == "" { diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/0.request b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/0.request new file mode 100755 index 0000000000..e69de29bb2 diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/0.response b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/0.response new file mode 100755 index 0000000000..1bc0144821 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/0.response @@ -0,0 +1,32 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "kubernetes", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/services/kubernetes", + "uid": "6a8e8829-f15f-11e6-b041-acbc32c1ca87", + "resourceVersion": "16953", + "creationTimestamp": "2017-02-12T20:11:19Z", + "labels": { + "component": "apiserver", + "provider": "kubernetes" + } + }, + "spec": { + "ports": [ + { + "name": "https", + "protocol": "TCP", + "port": 443, + "targetPort": 443 + } + ], + "clusterIP": "10.0.0.1", + "type": "ClusterIP", + "sessionAffinity": "ClientIP" + }, + "status": { + "loadBalancer": {} + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/1.request b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/1.request new file mode 100755 index 0000000000..e69de29bb2 diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/1.response b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/1.response new file mode 100755 index 0000000000..b2519124b7 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/1.response @@ -0,0 +1,16 @@ +{ + "apiVersion": "company.com/v1", + "kind": "Bar", + "metadata": { + "name": "test", + "namespace": "default", + "selfLink": "/apis/company.com/v1/namespaces/default/bars/test", + "uid": "fd16c23d-f185-11e6-b041-acbc32c1ca87", + "resourceVersion": "16954", + "creationTimestamp": "2017-02-13T00:47:26Z" + }, + "some-field": "field1", + "third-field": { + "sub-field": "bar2" + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/2.request b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/2.request new file mode 100755 index 0000000000..e69de29bb2 diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/2.response b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/2.response new file mode 100755 index 0000000000..c3a509b81e --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/2.response @@ -0,0 +1,21 @@ +{ + "apiVersion": "company.com/v1", + "field1": "value1", + "field2": true, + "field3": [ + 1 + ], + "field4": { + "a": true, + "b": false + }, + "kind": "Bar", + "metadata": { + "name": "test2", + "namespace": "default", + "selfLink": "/apis/company.com/v1/namespaces/default/bars/test2", + "uid": "5ef5b446-f186-11e6-b041-acbc32c1ca87", + "resourceVersion": "16955", + "creationTimestamp": "2017-02-13T00:50:10Z" + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.edited b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.edited new file mode 100755 index 0000000000..2fc0a1aa25 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.edited @@ -0,0 +1,62 @@ +# 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. +# +apiVersion: v1 +items: +- apiVersion: v1 + kind: Service + metadata: + creationTimestamp: 2017-02-12T20:11:19Z + labels: + component: apiserver + provider: kubernetes + new-label: new-value + name: kubernetes + namespace: default + resourceVersion: "16953" + selfLink: /api/v1/namespaces/default/services/kubernetes + uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 + spec: + clusterIP: 10.0.0.1 + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 443 + sessionAffinity: ClientIP + type: ClusterIP + status: + loadBalancer: {} +- apiVersion: company.com/v1 + kind: Bar + metadata: + creationTimestamp: 2017-02-13T00:47:26Z + name: test + namespace: default + resourceVersion: "16954" + selfLink: /apis/company.com/v1/namespaces/default/bars/test + uid: fd16c23d-f185-11e6-b041-acbc32c1ca87 + some-field: field1 + other-field: other-value + third-field: + sub-field: bar2 +- apiVersion: company.com/v1 + field1: value1 + field2: true + field3: + - 1 + - 2 + field4: + a: true + b: false + kind: Bar + metadata: + creationTimestamp: 2017-02-13T00:50:10Z + name: test2 + namespace: default + resourceVersion: "16955" + selfLink: /apis/company.com/v1/namespaces/default/bars/test2 + uid: 5ef5b446-f186-11e6-b041-acbc32c1ca87 +kind: List +metadata: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.original b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.original new file mode 100755 index 0000000000..e9cb478b6b --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/3.original @@ -0,0 +1,59 @@ +# 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. +# +apiVersion: v1 +items: +- apiVersion: v1 + kind: Service + metadata: + creationTimestamp: 2017-02-12T20:11:19Z + labels: + component: apiserver + provider: kubernetes + name: kubernetes + namespace: default + resourceVersion: "16953" + selfLink: /api/v1/namespaces/default/services/kubernetes + uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 + spec: + clusterIP: 10.0.0.1 + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 443 + sessionAffinity: ClientIP + type: ClusterIP + status: + loadBalancer: {} +- apiVersion: company.com/v1 + kind: Bar + metadata: + creationTimestamp: 2017-02-13T00:47:26Z + name: test + namespace: default + resourceVersion: "16954" + selfLink: /apis/company.com/v1/namespaces/default/bars/test + uid: fd16c23d-f185-11e6-b041-acbc32c1ca87 + some-field: field1 + third-field: + sub-field: bar2 +- apiVersion: company.com/v1 + field1: value1 + field2: true + field3: + - 1 + field4: + a: true + b: false + kind: Bar + metadata: + creationTimestamp: 2017-02-13T00:50:10Z + name: test2 + namespace: default + resourceVersion: "16955" + selfLink: /apis/company.com/v1/namespaces/default/bars/test2 + uid: 5ef5b446-f186-11e6-b041-acbc32c1ca87 +kind: List +metadata: {} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.request b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.request new file mode 100755 index 0000000000..bd858120dd --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.request @@ -0,0 +1,7 @@ +{ + "metadata": { + "labels": { + "new-label": "new-value" + } + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.response b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.response new file mode 100755 index 0000000000..16d3eb5e9c --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/4.response @@ -0,0 +1,33 @@ +{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "kubernetes", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/services/kubernetes", + "uid": "6a8e8829-f15f-11e6-b041-acbc32c1ca87", + "resourceVersion": "17087", + "creationTimestamp": "2017-02-12T20:11:19Z", + "labels": { + "component": "apiserver", + "new-label": "new-value", + "provider": "kubernetes" + } + }, + "spec": { + "ports": [ + { + "name": "https", + "protocol": "TCP", + "port": 443, + "targetPort": 443 + } + ], + "clusterIP": "10.0.0.1", + "type": "ClusterIP", + "sessionAffinity": "ClientIP" + }, + "status": { + "loadBalancer": {} + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.request b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.request new file mode 100755 index 0000000000..5a74e33dda --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.request @@ -0,0 +1,3 @@ +{ + "other-field": "other-value" +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.response b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.response new file mode 100755 index 0000000000..5ebd7c6dc0 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/5.response @@ -0,0 +1,17 @@ +{ + "apiVersion": "company.com/v1", + "kind": "Bar", + "metadata": { + "name": "test", + "namespace": "default", + "selfLink": "/apis/company.com/v1/namespaces/default/bars/test", + "uid": "fd16c23d-f185-11e6-b041-acbc32c1ca87", + "resourceVersion": "17088", + "creationTimestamp": "2017-02-13T00:47:26Z" + }, + "other-field": "other-value", + "some-field": "field1", + "third-field": { + "sub-field": "bar2" + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.request b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.request new file mode 100755 index 0000000000..3c98e4fcf9 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.request @@ -0,0 +1,6 @@ +{ + "field3": [ + 1, + 2 + ] +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.response b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.response new file mode 100755 index 0000000000..d9b94f9e62 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/6.response @@ -0,0 +1,22 @@ +{ + "apiVersion": "company.com/v1", + "field1": "value1", + "field2": true, + "field3": [ + 1, + 2 + ], + "field4": { + "a": true, + "b": false + }, + "kind": "Bar", + "metadata": { + "name": "test2", + "namespace": "default", + "selfLink": "/apis/company.com/v1/namespaces/default/bars/test2", + "uid": "5ef5b446-f186-11e6-b041-acbc32c1ca87", + "resourceVersion": "17089", + "creationTimestamp": "2017-02-13T00:50:10Z" + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/test.yaml b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/test.yaml new file mode 100755 index 0000000000..1e2d9da2f3 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-schemaless-list/test.yaml @@ -0,0 +1,55 @@ +description: edit a mix of schema and schemaless data +mode: edit +args: +- service/kubernetes +- bars/test +- bars/test2 +namespace: default +expectedStdout: +- "service \"kubernetes\" edited" +- "bar \"test\" edited" +- "bar \"test2\" edited" +expectedExitCode: 0 +steps: +- type: request + expectedMethod: GET + expectedPath: /api/v1/namespaces/default/services/kubernetes + expectedInput: 0.request + resultingStatusCode: 200 + resultingOutput: 0.response +- type: request + expectedMethod: GET + expectedPath: /apis/company.com/v1/namespaces/default/bars/test + expectedInput: 1.request + resultingStatusCode: 200 + resultingOutput: 1.response +- type: request + expectedMethod: GET + expectedPath: /apis/company.com/v1/namespaces/default/bars/test2 + expectedInput: 2.request + resultingStatusCode: 200 + resultingOutput: 2.response +- type: edit + expectedInput: 3.original + resultingOutput: 3.edited +- type: request + expectedMethod: PATCH + expectedPath: /api/v1/namespaces/default/services/kubernetes + expectedContentType: application/strategic-merge-patch+json + expectedInput: 4.request + resultingStatusCode: 200 + resultingOutput: 4.response +- type: request + expectedMethod: PATCH + expectedPath: /apis/company.com/v1/namespaces/default/bars/test + expectedContentType: application/merge-patch+json + expectedInput: 5.request + resultingStatusCode: 200 + resultingOutput: 5.response +- type: request + expectedMethod: PATCH + expectedPath: /apis/company.com/v1/namespaces/default/bars/test2 + expectedContentType: application/merge-patch+json + expectedInput: 6.request + resultingStatusCode: 200 + resultingOutput: 6.response diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/0.request b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/0.request new file mode 100755 index 0000000000..e69de29bb2 diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/0.response b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/0.response new file mode 100755 index 0000000000..54ea661e8b --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/0.response @@ -0,0 +1,22 @@ +{ + "kind": "StorageClass", + "apiVersion": "storage.k8s.io/v0", + "metadata": { + "name": "foo", + "selfLink": "/apis/storage.k8s.io/v0/storageclassesfoo", + "uid": "b2287558-f190-11e6-b041-acbc32c1ca87", + "resourceVersion": "21388", + "creationTimestamp": "2017-02-13T02:04:04Z", + "labels": { + "label1": "value1" + } + }, + "provisioner": "foo", + "parameters": { + "baz": "qux", + "foo": "bar" + }, + "extraField": { + "otherData": true + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.edited b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.edited new file mode 100755 index 0000000000..7cbf1c4ae7 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.edited @@ -0,0 +1,22 @@ +# 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. +# +apiVersion: storage.k8s.io/v0 +extraField: + otherData: true + addedData: "foo" +kind: StorageClass +metadata: + creationTimestamp: 2017-02-13T02:04:04Z + labels: + label1: value1 + label2: value2 + name: foo + resourceVersion: "21388" + selfLink: /apis/storage.k8s.io/v0/storageclassesfoo + uid: b2287558-f190-11e6-b041-acbc32c1ca87 +parameters: + baz: qux + foo: bar +provisioner: foo diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.original b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.original new file mode 100755 index 0000000000..b720c022f7 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/1.original @@ -0,0 +1,20 @@ +# 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. +# +apiVersion: storage.k8s.io/v0 +extraField: + otherData: true +kind: StorageClass +metadata: + creationTimestamp: 2017-02-13T02:04:04Z + labels: + label1: value1 + name: foo + resourceVersion: "21388" + selfLink: /apis/storage.k8s.io/v0/storageclassesfoo + uid: b2287558-f190-11e6-b041-acbc32c1ca87 +parameters: + baz: qux + foo: bar +provisioner: foo diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.request b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.request new file mode 100755 index 0000000000..654d91e4e6 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.request @@ -0,0 +1,11 @@ +{ + "extraField": { + "addedData": "foo" + }, + "metadata": { + "labels": { + "label2": "value2" + }, + "namespace": "" + } +} \ No newline at end of file diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.response b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.response new file mode 100755 index 0000000000..143c7fe2f9 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/2.response @@ -0,0 +1,24 @@ +{ + "kind": "StorageClass", + "apiVersion": "storage.k8s.io/v0", + "metadata": { + "name": "foo", + "selfLink": "/apis/storage.k8s.io/v0/storageclassesfoo", + "uid": "b2287558-f190-11e6-b041-acbc32c1ca87", + "resourceVersion": "21431", + "creationTimestamp": "2017-02-13T02:04:04Z", + "labels": { + "label1": "value1", + "label2": "value2" + } + }, + "provisioner": "foo", + "parameters": { + "baz": "qux", + "foo": "bar" + }, + "extraField": { + "otherData": true, + "addedData": true + } +} diff --git a/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/test.yaml b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/test.yaml new file mode 100755 index 0000000000..0a705622b2 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/edit/testcase-unknown-version-known-group-kind/test.yaml @@ -0,0 +1,25 @@ +description: edit an unknown version of a known group/kind +mode: edit +args: +- storageclasses.v0.storage.k8s.io/foo +namespace: default +expectedStdout: +- "storageclass \"foo\" edited" +expectedExitCode: 0 +steps: +- type: request + expectedMethod: GET + expectedPath: /apis/storage.k8s.io/v0/storageclasses/foo + expectedInput: 0.request + resultingStatusCode: 200 + resultingOutput: 0.response +- type: edit + expectedInput: 1.original + resultingOutput: 1.edited +- type: request + expectedMethod: PATCH + expectedPath: /apis/storage.k8s.io/v0/storageclasses/foo + expectedContentType: application/merge-patch+json + expectedInput: 2.request + resultingStatusCode: 200 + resultingOutput: 2.response diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index 7192e65eba..d2ea1532a7 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -719,5 +719,38 @@ func testDynamicResources() []*discovery.APIGroupResources { }, }, }, + { + Group: metav1.APIGroup{ + Name: "storage.k8s.io", + Versions: []metav1.GroupVersionForDiscovery{ + {Version: "v1beta1"}, + {Version: "v0"}, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1beta1"}, + }, + VersionedResources: map[string][]metav1.APIResource{ + "v1beta1": { + {Name: "storageclasses", Namespaced: false, Kind: "StorageClass"}, + }, + // bogus version of a known group/version/resource to make sure kubectl falls back to generic object mode + "v0": { + {Name: "storageclasses", Namespaced: false, Kind: "StorageClass"}, + }, + }, + }, + { + Group: metav1.APIGroup{ + Name: "company.com", + Versions: []metav1.GroupVersionForDiscovery{ + {Version: "v1"}, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, + }, + VersionedResources: map[string][]metav1.APIResource{ + "v1": { + {Name: "bars", Namespaced: true, Kind: "Bar"}, + }, + }, + }, } }