diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index dab33477e7e..ef4e3995c04 100755 --- a/hack/make-rules/test-cmd.sh +++ b/hack/make-rules/test-cmd.sh @@ -720,6 +720,25 @@ runTests() { # Pre-condition: no POD exists create_and_use_new_namespace kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' + ## kubectl create --edit can update the image field of a POD. tmp-editor.sh is a fake editor + TEMP=$(mktemp /tmp/tmp-editor-XXXXXXXX.sh) + echo -e "#!/bin/bash\n$SED -i \"s/gcr.io\/google_containers\/serve_hostname/nginx/g\" \$1" > ${TEMP} + chmod +x ${TEMP} + # Command + EDITOR=${TEMP} kubectl create --edit -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml "${kube_flags[@]}" + # Post-condition: valid-pod POD is created and has image gcr.io/google_containers/serve_hostname + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'nginx:' + # Clean up + rm ${TEMP} + kubectl delete pods/valid-pod "${kube_flags[@]}" + + ## kubectl create --edit won't create anything if user makes no changes + [ "$(EDITOR=cat kubectl create --edit -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml -o json 2>&1 | grep 'Edit cancelled')" ] + + ## Create valid-pod POD + # Pre-condition: no POD exists + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml "${kube_flags[@]}" # Post-condition: valid-pod POD is created diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index d1babebef5c..53fea2ec284 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -221,7 +221,7 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob { Message: "Basic Commands (Beginner):", Commands: []*cobra.Command{ - NewCmdCreate(f, out), + NewCmdCreate(f, out, err), NewCmdExposeService(f, out), NewCmdRun(f, in, out, err), set.NewCmdSet(f, out, err), diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index c82e9b3d4f9..a8d0bc8e838 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -19,6 +19,7 @@ package cmd import ( "fmt" "io" + gruntime "runtime" "github.com/spf13/cobra" @@ -41,10 +42,13 @@ var ( kubectl create -f ./pod.json # Create a pod based on the JSON passed into stdin. - cat pod.json | kubectl create -f -`) + cat pod.json | kubectl create -f - + + # Edit the data in docker-registry.yaml in JSON using the v1 API format then create the resource using the edited data. + kubectl create -f docker-registry.yaml --edit --output-version=v1 -o json`) ) -func NewCmdCreate(f cmdutil.Factory, out io.Writer) *cobra.Command { +func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { options := &resource.FilenameOptions{} cmd := &cobra.Command{ @@ -58,8 +62,7 @@ func NewCmdCreate(f cmdutil.Factory, out io.Writer) *cobra.Command { return } cmdutil.CheckErr(ValidateArgs(cmd, args)) - cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd)) - cmdutil.CheckErr(RunCreate(f, cmd, out, options)) + cmdutil.CheckErr(RunCreate(f, cmd, out, errOut, options)) }, } @@ -68,6 +71,8 @@ func NewCmdCreate(f cmdutil.Factory, out io.Writer) *cobra.Command { cmd.MarkFlagRequired("filename") cmdutil.AddValidateFlags(cmd) cmdutil.AddPrinterFlags(cmd) + cmd.Flags().Bool("edit", false, "Edit the API resource before creating") + cmd.Flags().Bool("windows-line-endings", gruntime.GOOS == "windows", "Only relevant if --edit=true. Use Windows line-endings (default Unix line-endings)") cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddRecordFlag(cmd) cmdutil.AddDryRunFlag(cmd) @@ -91,7 +96,10 @@ func ValidateArgs(cmd *cobra.Command, args []string) error { return nil } -func RunCreate(f cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *resource.FilenameOptions) error { +func RunCreate(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, options *resource.FilenameOptions) error { + if cmdutil.GetFlagBool(cmd, "edit") { + return RunEditOnCreate(f, out, errOut, cmd, options) + } schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagString(cmd, "schema-cache-dir")) if err != nil { return err @@ -159,6 +167,10 @@ func RunCreate(f cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *re return nil } +func RunEditOnCreate(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, options *resource.FilenameOptions) error { + return runEdit(f, out, errOut, cmd, []string{}, options, EditBeforeCreateMode) +} + // createAndRefresh creates an object from input info and refreshes info with that object func createAndRefresh(info *resource.Info) error { obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go index 469dc6c6ec5..7818048e432 100644 --- a/pkg/kubectl/cmd/create_test.go +++ b/pkg/kubectl/cmd/create_test.go @@ -29,9 +29,10 @@ import ( func TestExtraArgsFail(t *testing.T) { initTestErrorHandler(t) buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) f, _, _, _ := cmdtesting.NewAPIFactory() - c := NewCmdCreate(f, buf) + c := NewCmdCreate(f, buf, errBuf) if ValidateArgs(c, []string{"rc"}) == nil { t.Errorf("unexpected non-error") } @@ -59,8 +60,9 @@ func TestCreateObject(t *testing.T) { } tf.Namespace = "test" buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) - cmd := NewCmdCreate(f, buf) + cmd := NewCmdCreate(f, buf, errBuf) cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) @@ -94,8 +96,9 @@ func TestCreateMultipleObject(t *testing.T) { } tf.Namespace = "test" buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) - cmd := NewCmdCreate(f, buf) + cmd := NewCmdCreate(f, buf, errBuf) cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.yaml") cmd.Flags().Set("output", "name") @@ -129,8 +132,9 @@ func TestCreateDirectory(t *testing.T) { } tf.Namespace = "test" buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) - cmd := NewCmdCreate(f, buf) + cmd := NewCmdCreate(f, buf, errBuf) cmd.Flags().Set("filename", "../../../examples/guestbook/legacy") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) diff --git a/pkg/kubectl/cmd/edit.go b/pkg/kubectl/cmd/edit.go index 68090f6702a..4321a3fc58d 100644 --- a/pkg/kubectl/cmd/edit.go +++ b/pkg/kubectl/cmd/edit.go @@ -118,12 +118,16 @@ func NewCmdEdit(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { } func RunEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, options *resource.FilenameOptions) error { + return runEdit(f, out, errOut, cmd, args, options, NormalEditMode) +} + +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 { return err } - mapper, resourceMapper, r, cmdNamespace, err := getMapperAndResult(f, args, options) + mapper, resourceMapper, r, cmdNamespace, err := getMapperAndResult(f, args, options, editMode) if err != nil { return err } @@ -192,14 +196,15 @@ func RunEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args if err != nil { return preservedFile(err, results.file, errOut) } - if bytes.Equal(stripComments(editedDiff), stripComments(edited)) { - // Ugly hack right here. We will hit this either (1) when we try to - // save the same changes we tried to save in the previous iteration - // which means our changes are invalid or (2) when we exit the second - // time. The second case is more usual so we can probably live with it. - // TODO: A less hacky fix would be welcome :) - fmt.Fprintln(errOut, "Edit cancelled, no valid changes were saved.") - return nil + if editMode == NormalEditMode || containsError { + if bytes.Equal(stripComments(editedDiff), stripComments(edited)) { + // Ugly hack right here. We will hit this either (1) when we try to + // save the same changes we tried to save in the previous iteration + // which means our changes are invalid or (2) when we exit the second + // time. The second case is more usual so we can probably live with it. + // TODO: A less hacky fix would be welcome :) + return preservedFile(fmt.Errorf("%s", "Edit cancelled, no valid changes were saved."), file, errOut) + } } // cleanup any file from the previous pass @@ -229,6 +234,7 @@ func RunEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args fmt.Fprintln(errOut, "Edit cancelled, no changes made.") return nil } + lines, err := hasLines(bytes.NewBuffer(edited)) if err != nil { return preservedFile(err, file, errOut) @@ -271,7 +277,14 @@ func RunEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args meta.SetList(updates.Object, mutatedObjects) } - err = visitToPatch(originalObj, updates, mapper, resourceMapper, encoder, out, errOut, defaultVersion, &results, file) + switch editMode { + case NormalEditMode: + err = visitToPatch(originalObj, updates, mapper, resourceMapper, encoder, out, errOut, defaultVersion, &results, file) + case EditBeforeCreateMode: + err = visitToCreate(updates, mapper, resourceMapper, out, errOut, defaultVersion, &results, file) + default: + err = fmt.Errorf("Not supported edit mode %q", editMode) + } if err != nil { return preservedFile(err, results.file, errOut) } @@ -315,7 +328,8 @@ func getPrinter(cmd *cobra.Command) (*editPrinterOptions, error) { ext: ".json", addHeader: false, }, nil - case "yaml": + // If flag -o is not specified, use yaml as default + case "yaml", "": return &editPrinterOptions{ printer: &kubectl.YAMLPrinter{}, ext: ".yaml", @@ -326,13 +340,24 @@ func getPrinter(cmd *cobra.Command) (*editPrinterOptions, error) { } } -func getMapperAndResult(f cmdutil.Factory, args []string, options *resource.FilenameOptions) (meta.RESTMapper, *resource.Mapper, *resource.Result, string, error) { +func getMapperAndResult(f cmdutil.Factory, args []string, options *resource.FilenameOptions, editMode EditMode) (meta.RESTMapper, *resource.Mapper, *resource.Result, string, error) { cmdNamespace, enforceNamespace, err := f.DefaultNamespace() if err != nil { return nil, nil, nil, "", err } - - mapper, typer := f.Object() + 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) + } + if err != nil { + return nil, nil, nil, "", err + } resourceMapper := &resource.Mapper{ ObjectTyper: typer, RESTMapper: mapper, @@ -345,14 +370,21 @@ func getMapperAndResult(f cmdutil.Factory, args []string, options *resource.File // compare two different GroupVersions). Decoder: f.Decoder(false), } - - r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)). - NamespaceParam(cmdNamespace).DefaultNamespace(). + 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), runtime.UnstructuredJSONScheme) + default: + return nil, nil, nil, "", fmt.Errorf("Not supported edit mode %q", editMode) + } + r := b.NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, options). - ResourceTypeOrNameArgs(true, args...). ContinueOnError(). Flatten(). - Latest(). Do() err = r.Err() if err != nil { @@ -428,7 +460,7 @@ func visitToPatch(originalObj runtime.Object, updates *resource.Info, mapper met if err != nil { glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) if strategicpatch.IsPreconditionFailed(err) { - return preservedFile(nil, file, errOut) + return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") } return err } @@ -446,6 +478,19 @@ func visitToPatch(originalObj runtime.Object, updates *resource.Info, mapper met return err } +func visitToCreate(updates *resource.Info, mapper meta.RESTMapper, resourceMapper *resource.Mapper, out, errOut io.Writer, defaultVersion unversioned.GroupVersion, results *editResults, file string) error { + createVisitor := resource.NewFlattenListVisitor(updates, resourceMapper) + err := createVisitor.Visit(func(info *resource.Info, incomingErr error) error { + results.version = defaultVersion + if err := createAndRefresh(info); err != nil { + return err + } + cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, false, "created") + return nil + }) + return err +} + func visitAnnotation(cmd *cobra.Command, f cmdutil.Factory, updates *resource.Info, resourceMapper *resource.Mapper, encoder runtime.Encoder) ([]runtime.Object, error) { mutatedObjects := []runtime.Object{} annotationVisitor := resource.NewFlattenListVisitor(updates, resourceMapper) @@ -468,6 +513,13 @@ func visitAnnotation(cmd *cobra.Command, f cmdutil.Factory, updates *resource.In return mutatedObjects, err } +type EditMode string + +const ( + NormalEditMode EditMode = "normal_mode" + EditBeforeCreateMode EditMode = "edit_before_create_mode" +) + // editReason preserves a message about the reason this file must be edited again type editReason struct { head string