diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index 70e3c9a4da..cbd1af708e 100755 --- a/hack/make-rules/test-cmd.sh +++ b/hack/make-rules/test-cmd.sh @@ -865,6 +865,7 @@ __EOF__ [ "$(EDITOR=cat kubectl edit pod/valid-pod | grep 'name: valid-pod')" ] [ "$(EDITOR=cat kubectl edit --windows-line-endings pod/valid-pod | file - | grep CRLF)" ] [ ! "$(EDITOR=cat kubectl edit --windows-line-endings=false pod/valid-pod | file - | grep CRLF)" ] + [ "$(EDITOR=cat kubectl edit ns | grep 'kind: List')" ] ### Label POD YAML file locally without effecting the live pod. # Pre-condition: name is valid-pod @@ -1377,8 +1378,10 @@ __EOF__ echo -e '#!/bin/bash\nsed -i "s/image: busybox/image: prom\/busybox/g" $1' > /tmp/tmp-editor.sh chmod +x /tmp/tmp-editor.sh output_message=$(! EDITOR=/tmp/tmp-editor.sh kubectl edit -f hack/testdata/recursive/pod --recursive 2>&1 "${kube_flags[@]}") - # Post-condition: busybox0 & busybox1 PODs are edited, and since busybox2 is malformed, it should error - kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'prom/busybox:prom/busybox:' + # Post-condition: busybox0 & busybox1 PODs are not edited, and since busybox2 is malformed, it should error + # The reason why busybox0 & busybox1 PODs are not edited is because the editor tries to load all objects in + # a list but since it contains invalid objects, it will never open. + kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'busybox:busybox:' kube::test::if_has_string "${output_message}" "Object 'Kind' is missing" # cleaning rm /tmp/tmp-editor.sh diff --git a/pkg/kubectl/cmd/edit.go b/pkg/kubectl/cmd/edit.go index 4321a3fc58..22479dc58c 100644 --- a/pkg/kubectl/cmd/edit.go +++ b/pkg/kubectl/cmd/edit.go @@ -143,181 +143,181 @@ func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args return err } + infos, err := r.Infos() + if err != nil { + return err + } + var ( windowsLineEndings = cmdutil.GetFlagBool(cmd, "windows-line-endings") edit = editor.NewDefaultEditor(f.EditorEnvs()) ) - err = r.Visit(func(info *resource.Info, err error) error { - var ( - results = editResults{} - original = []byte{} - edited = []byte{} - file string - ) + var ( + results = editResults{} + original = []byte{} + edited = []byte{} + file string + ) - containsError := false + containsError := false + for { + originalObj, err := resource.AsVersionedObject(infos, false, defaultVersion, encoder) + if err != nil { + return err + } - for { - infos := []*resource.Info{info} - originalObj, err := resource.AsVersionedObject(infos, false, defaultVersion, encoder) - if err != nil { - return err - } + objToEdit := originalObj - objToEdit := originalObj + // generate the file to edit + buf := &bytes.Buffer{} + var w io.Writer = buf + if windowsLineEndings { + w = crlf.NewCRLFWriter(w) + } - // generate the file to edit - buf := &bytes.Buffer{} - var w io.Writer = buf - if windowsLineEndings { - w = crlf.NewCRLFWriter(w) - } + if o.addHeader { + results.header.writeTo(w) + } - if o.addHeader { - results.header.writeTo(w) - } - - if !containsError { - if err := o.printer.PrintObj(objToEdit, w); err != nil { - return preservedFile(err, results.file, errOut) - } - original = buf.Bytes() - } else { - // In case of an error, preserve the edited file. - // Remove the comments (header) from it since we already - // have included the latest header in the buffer above. - buf.Write(manualStrip(edited)) - } - - // launch the editor - editedDiff := edited - edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), o.ext, buf) - if err != nil { + if !containsError { + if err := o.printer.PrintObj(objToEdit, w); err != nil { return preservedFile(err, results.file, errOut) } - 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) - } - } + original = buf.Bytes() + } else { + // In case of an error, preserve the edited file. + // Remove the comments (header) from it since we already + // have included the latest header in the buffer above. + buf.Write(manualStrip(edited)) + } - // cleanup any file from the previous pass - if len(results.file) > 0 { - os.Remove(results.file) + // launch the editor + editedDiff := edited + edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), o.ext, buf) + if err != nil { + return preservedFile(err, results.file, errOut) + } + 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) } - glog.V(4).Infof("User edited:\n%s", string(edited)) + } - // Apply validation - schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagString(cmd, "schema-cache-dir")) - if err != nil { - return preservedFile(err, file, errOut) - } - err = schema.ValidateBytes(stripComments(edited)) - if err != nil { - results = editResults{ - file: file, - } - containsError = true - fmt.Fprintln(out, results.addError(errors.NewInvalid(api.Kind(""), "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), info)) - continue - } - - // Compare content without comments - if bytes.Equal(stripComments(original), stripComments(edited)) { - os.Remove(file) - fmt.Fprintln(errOut, "Edit cancelled, no changes made.") - return nil - } - - lines, err := hasLines(bytes.NewBuffer(edited)) - if err != nil { - return preservedFile(err, file, errOut) - } - if !lines { - os.Remove(file) - fmt.Fprintln(errOut, "Edit cancelled, saved file was empty.") - return nil - } + // cleanup any file from the previous pass + if len(results.file) > 0 { + os.Remove(results.file) + } + glog.V(4).Infof("User edited:\n%s", string(edited)) + // Apply validation + schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagString(cmd, "schema-cache-dir")) + if err != nil { + return preservedFile(err, file, errOut) + } + err = schema.ValidateBytes(stripComments(edited)) + if err != nil { results = editResults{ file: file, } - - // parse the edited file - updates, err := resourceMapper.InfoForData(edited, "edited-file") - 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 - } - // not a syntax error as it turns out... - containsError = false - - namespaceVisitor := resource.NewFlattenListVisitor(updates, resourceMapper) - // need to make sure the original namespace wasn't changed while editing - if err = namespaceVisitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil { - return preservedFile(err, file, errOut) - } - - // iterate through all items to apply annotations - mutatedObjects, err := visitAnnotation(cmd, f, updates, resourceMapper, encoder) - if err != nil { - return preservedFile(err, file, errOut) - } - - // if we mutated a list in the visitor, persist the changes on the overall object - if meta.IsListType(updates.Object) { - meta.SetList(updates.Object, mutatedObjects) - } - - 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) - } - - // Handle all possible errors - // - // 1. retryable: propose kubectl replace -f - // 2. notfound: indicate the location of the saved configuration of the deleted resource - // 3. invalid: retry those on the spot by looping ie. reloading the editor - if results.retryable > 0 { - fmt.Fprintf(errOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file) - return cmdutil.ErrExit - } - if results.notfound > 0 { - fmt.Fprintf(errOut, "The edits you made on deleted resources have been saved to %q\n", file) - return cmdutil.ErrExit - } - - if len(results.edit) == 0 { - if results.notfound == 0 { - os.Remove(file) - } else { - fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file) - } - return nil - } - - if len(results.header.reasons) > 0 { - containsError = true - } + containsError = true + fmt.Fprintln(out, results.addError(errors.NewInvalid(api.Kind(""), "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), infos[0])) + continue } - }) - return err + + // Compare content without comments + if bytes.Equal(stripComments(original), stripComments(edited)) { + os.Remove(file) + fmt.Fprintln(errOut, "Edit cancelled, no changes made.") + return nil + } + + lines, err := hasLines(bytes.NewBuffer(edited)) + if err != nil { + return preservedFile(err, file, errOut) + } + if !lines { + os.Remove(file) + fmt.Fprintln(errOut, "Edit cancelled, saved file was empty.") + return nil + } + + results = editResults{ + file: file, + } + + // parse the edited file + updates, err := resourceMapper.InfoForData(edited, "edited-file") + 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 + } + // not a syntax error as it turns out... + containsError = false + + namespaceVisitor := resource.NewFlattenListVisitor(updates, resourceMapper) + // need to make sure the original namespace wasn't changed while editing + if err = namespaceVisitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil { + return preservedFile(err, file, errOut) + } + + // iterate through all items to apply annotations + mutatedObjects, err := visitAnnotation(cmd, f, updates, resourceMapper, encoder) + if err != nil { + return preservedFile(err, file, errOut) + } + + // if we mutated a list in the visitor, persist the changes on the overall object + if meta.IsListType(updates.Object) { + meta.SetList(updates.Object, mutatedObjects) + } + + 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) + } + + // Handle all possible errors + // + // 1. retryable: propose kubectl replace -f + // 2. notfound: indicate the location of the saved configuration of the deleted resource + // 3. invalid: retry those on the spot by looping ie. reloading the editor + if results.retryable > 0 { + fmt.Fprintf(errOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file) + return cmdutil.ErrExit + } + if results.notfound > 0 { + fmt.Fprintf(errOut, "The edits you made on deleted resources have been saved to %q\n", file) + return cmdutil.ErrExit + } + + if len(results.edit) == 0 { + if results.notfound == 0 { + os.Remove(file) + } else { + fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file) + } + return nil + } + + if len(results.header.reasons) > 0 { + containsError = true + } + } } func getPrinter(cmd *cobra.Command) (*editPrinterOptions, error) {