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
pull/6/head
Jordan Liggitt 2017-02-12 19:31:06 -05:00
parent 5b805bc18a
commit ec271f5c09
No known key found for this signature in database
GPG Key ID: 24E7ADF9A3B42012
25 changed files with 622 additions and 133 deletions

View File

@ -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])
var originalInfo *resource.Info
for _, i := range originalInfos {
originalObjUID, err := meta.NewAccessor().UID(i.Object)
if err != nil {
return err
}
if editObjUID == originalObjUID {
currOriginalObj = listItems[i]
originalInfo = i
break
}
}
if currOriginalObj == nil {
if originalInfo == nil {
return fmt.Errorf("no original object found for %#v", info.Object)
}
}
originalSerialization, err := runtime.Encode(encoder, currOriginalObj)
originalSerialization, err := runtime.Encode(encoder, originalInfo.Object)
if err != nil {
return err
}
@ -478,9 +468,37 @@ func visitToPatch(
return nil
}
preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
patch, err := strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, currOriginalObj, preconditions...)
preconditions := []mergepatch.PreconditionFunc{
mergepatch.RequireKeyUnchanged("apiVersion"),
mergepatch.RequireKeyUnchanged("kind"),
mergepatch.RequireMetadataKeyUnchanged("name"),
}
// 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) {
@ -488,9 +506,9 @@ func visitToPatch(
}
return err
}
}
results.version = defaultVersion
patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch)
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

View File

@ -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 == "" {

View File

@ -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": {}
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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: {}

View File

@ -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: {}

View File

@ -0,0 +1,7 @@
{
"metadata": {
"labels": {
"new-label": "new-value"
}
}
}

View File

@ -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": {}
}
}

View File

@ -0,0 +1,3 @@
{
"other-field": "other-value"
}

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
{
"field3": [
1,
2
]
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,11 @@
{
"extraField": {
"addedData": "foo"
},
"metadata": {
"labels": {
"label2": "value2"
},
"namespace": ""
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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"},
},
},
},
}
}