1
16
17 package editor
18
19 import (
20 "bufio"
21 "bytes"
22 "encoding/json"
23 "errors"
24 "fmt"
25 "io"
26 "os"
27 "path/filepath"
28 "reflect"
29 goruntime "runtime"
30 "strings"
31
32 jsonpatch "github.com/evanphx/json-patch"
33 "github.com/spf13/cobra"
34 "k8s.io/klog/v2"
35
36 corev1 "k8s.io/api/core/v1"
37 apierrors "k8s.io/apimachinery/pkg/api/errors"
38 "k8s.io/apimachinery/pkg/api/meta"
39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
40 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
41 "k8s.io/apimachinery/pkg/runtime"
42 "k8s.io/apimachinery/pkg/types"
43 "k8s.io/apimachinery/pkg/util/mergepatch"
44 "k8s.io/apimachinery/pkg/util/strategicpatch"
45 "k8s.io/apimachinery/pkg/util/validation/field"
46 "k8s.io/apimachinery/pkg/util/yaml"
47 "k8s.io/cli-runtime/pkg/genericclioptions"
48 "k8s.io/cli-runtime/pkg/genericiooptions"
49 "k8s.io/cli-runtime/pkg/printers"
50 "k8s.io/cli-runtime/pkg/resource"
51 cmdutil "k8s.io/kubectl/pkg/cmd/util"
52 "k8s.io/kubectl/pkg/cmd/util/editor/crlf"
53 "k8s.io/kubectl/pkg/scheme"
54 "k8s.io/kubectl/pkg/util"
55 "k8s.io/kubectl/pkg/util/slice"
56 )
57
58 var SupportedSubresources = []string{"status"}
59
60
61 type EditOptions struct {
62 resource.FilenameOptions
63 RecordFlags *genericclioptions.RecordFlags
64
65 PrintFlags *genericclioptions.PrintFlags
66 ToPrinter func(string) (printers.ResourcePrinter, error)
67
68 OutputPatch bool
69 WindowsLineEndings bool
70
71 cmdutil.ValidateOptions
72 ValidationDirective string
73
74 OriginalResult *resource.Result
75
76 EditMode EditMode
77
78 CmdNamespace string
79 ApplyAnnotation bool
80 ChangeCause string
81
82 managedFields map[types.UID][]metav1.ManagedFieldsEntry
83
84 genericiooptions.IOStreams
85
86 Recorder genericclioptions.Recorder
87 f cmdutil.Factory
88 editPrinterOptions *editPrinterOptions
89 updatedResultGetter func(data []byte) *resource.Result
90
91 FieldManager string
92
93 Subresource string
94 }
95
96
97 func NewEditOptions(editMode EditMode, ioStreams genericiooptions.IOStreams) *EditOptions {
98 return &EditOptions{
99 RecordFlags: genericclioptions.NewRecordFlags(),
100
101 EditMode: editMode,
102
103 PrintFlags: genericclioptions.NewPrintFlags("edited").WithTypeSetter(scheme.Scheme),
104
105 editPrinterOptions: &editPrinterOptions{
106
107
108 printFlags: (&genericclioptions.PrintFlags{
109 JSONYamlPrintFlags: genericclioptions.NewJSONYamlPrintFlags(),
110 }).WithDefaultOutput("yaml"),
111 ext: ".yaml",
112 addHeader: true,
113 },
114
115 WindowsLineEndings: goruntime.GOOS == "windows",
116
117 Recorder: genericclioptions.NoopRecorder{},
118
119 IOStreams: ioStreams,
120 }
121 }
122
123 type editPrinterOptions struct {
124 printFlags *genericclioptions.PrintFlags
125 ext string
126 addHeader bool
127 }
128
129 func (e *editPrinterOptions) Complete(fromPrintFlags *genericclioptions.PrintFlags) error {
130 if e.printFlags == nil {
131 return fmt.Errorf("missing PrintFlags in editor printer options")
132 }
133
134
135 if fromPrintFlags != nil && len(*fromPrintFlags.OutputFormat) > 0 {
136 e.printFlags.OutputFormat = fromPrintFlags.OutputFormat
137 }
138
139
140
141 if *e.printFlags.OutputFormat == "json" {
142 e.addHeader = false
143 e.ext = ".json"
144 return nil
145 }
146
147
148 e.addHeader = true
149 e.ext = ".yaml"
150 return nil
151 }
152
153 func (e *editPrinterOptions) PrintObj(obj runtime.Object, out io.Writer) error {
154 p, err := e.printFlags.ToPrinter()
155 if err != nil {
156 return err
157 }
158
159 return p.PrintObj(obj, out)
160 }
161
162
163 func (o *EditOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) error {
164 var err error
165
166 o.RecordFlags.Complete(cmd)
167 o.Recorder, err = o.RecordFlags.ToRecorder()
168 if err != nil {
169 return err
170 }
171
172 if o.EditMode != NormalEditMode && o.EditMode != EditBeforeCreateMode && o.EditMode != ApplyEditMode {
173 return fmt.Errorf("unsupported edit mode %q", o.EditMode)
174 }
175
176 o.editPrinterOptions.Complete(o.PrintFlags)
177
178 if o.OutputPatch && o.EditMode != NormalEditMode {
179 return fmt.Errorf("the edit mode doesn't support output the patch")
180 }
181
182 cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
183 if err != nil {
184 return err
185 }
186 b := f.NewBuilder().
187 Unstructured()
188 if o.EditMode == NormalEditMode || o.EditMode == ApplyEditMode {
189
190 b = b.ResourceTypeOrNameArgs(true, args...).Latest()
191 }
192 r := b.NamespaceParam(cmdNamespace).DefaultNamespace().
193 FilenameParam(enforceNamespace, &o.FilenameOptions).
194 Subresource(o.Subresource).
195 ContinueOnError().
196 Flatten().
197 Do()
198 err = r.Err()
199 if err != nil {
200 return err
201 }
202 o.OriginalResult = r
203
204 o.updatedResultGetter = func(data []byte) *resource.Result {
205
206 return f.NewBuilder().
207 Unstructured().
208 Stream(bytes.NewReader(data), "edited-file").
209 Subresource(o.Subresource).
210 ContinueOnError().
211 Flatten().
212 Do()
213 }
214
215 o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
216 o.PrintFlags.NamePrintFlags.Operation = operation
217 return o.PrintFlags.ToPrinter()
218 }
219
220 o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
221 if err != nil {
222 return err
223 }
224
225 o.CmdNamespace = cmdNamespace
226 o.f = f
227
228 return nil
229 }
230
231
232 func (o *EditOptions) Validate() error {
233 if len(o.Subresource) > 0 && !slice.ContainsString(SupportedSubresources, o.Subresource, nil) {
234 return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, SupportedSubresources)
235 }
236 return nil
237 }
238
239
240 func (o *EditOptions) Run() error {
241 edit := NewDefaultEditor(editorEnvs())
242
243 editFn := func(infos []*resource.Info) error {
244 var (
245 results = editResults{}
246 original = []byte{}
247 edited = []byte{}
248 file string
249 err error
250 )
251
252 containsError := false
253
254 for {
255
256 var originalObj runtime.Object
257 switch len(infos) {
258 case 1:
259 originalObj = infos[0].Object
260 default:
261 l := &unstructured.UnstructuredList{
262 Object: map[string]interface{}{
263 "kind": "List",
264 "apiVersion": "v1",
265 "metadata": map[string]interface{}{},
266 },
267 }
268 for _, info := range infos {
269 l.Items = append(l.Items, *info.Object.(*unstructured.Unstructured))
270 }
271 originalObj = l
272 }
273
274
275 buf := &bytes.Buffer{}
276 var w io.Writer = buf
277 if o.WindowsLineEndings {
278 w = crlf.NewCRLFWriter(w)
279 }
280
281 if o.editPrinterOptions.addHeader {
282 results.header.writeTo(w, o.EditMode)
283 }
284
285 if !containsError {
286 if err := o.extractManagedFields(originalObj); err != nil {
287 return preservedFile(err, results.file, o.ErrOut)
288 }
289
290 if err := o.editPrinterOptions.PrintObj(originalObj, w); err != nil {
291 return preservedFile(err, results.file, o.ErrOut)
292 }
293 original = buf.Bytes()
294 } else {
295
296
297
298 buf.Write(cmdutil.ManualStrip(edited))
299 }
300
301
302 editedDiff := edited
303 edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), o.editPrinterOptions.ext, buf)
304 if err != nil {
305 return preservedFile(err, results.file, o.ErrOut)
306 }
307
308
309 if containsError && bytes.Equal(cmdutil.StripComments(editedDiff), cmdutil.StripComments(edited)) {
310 return preservedFile(fmt.Errorf("%s", "Edit cancelled, no valid changes were saved."), file, o.ErrOut)
311 }
312
313 if len(results.file) > 0 {
314 os.Remove(results.file)
315 }
316 klog.V(4).Infof("User edited:\n%s", string(edited))
317
318
319 schema, err := o.f.Validator(o.ValidationDirective)
320 if err != nil {
321 return preservedFile(err, file, o.ErrOut)
322 }
323 err = schema.ValidateBytes(cmdutil.StripComments(edited))
324 if err != nil {
325 results = editResults{
326 file: file,
327 }
328 containsError = true
329 fmt.Fprintln(o.ErrOut, results.addError(apierrors.NewInvalid(corev1.SchemeGroupVersion.WithKind("").GroupKind(),
330 "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), infos[0]))
331 continue
332 }
333
334
335 if bytes.Equal(cmdutil.StripComments(original), cmdutil.StripComments(edited)) {
336 os.Remove(file)
337 fmt.Fprintln(o.ErrOut, "Edit cancelled, no changes made.")
338 return nil
339 }
340
341 lines, err := hasLines(bytes.NewBuffer(edited))
342 if err != nil {
343 return preservedFile(err, file, o.ErrOut)
344 }
345 if !lines {
346 os.Remove(file)
347 fmt.Fprintln(o.ErrOut, "Edit cancelled, saved file was empty.")
348 return nil
349 }
350
351 results = editResults{
352 file: file,
353 }
354
355
356 updatedInfos, err := o.updatedResultGetter(edited).Infos()
357 if err != nil {
358
359 containsError = true
360 results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)})
361 continue
362 }
363
364
365 containsError = false
366 updatedVisitor := resource.InfoListVisitor(updatedInfos)
367
368
369 if err := o.restoreManagedFields(updatedInfos); err != nil {
370 return preservedFile(err, file, o.ErrOut)
371 }
372 if err := o.restoreManagedFields(infos); err != nil {
373 return preservedFile(err, file, o.ErrOut)
374 }
375
376
377 if err := updatedVisitor.Visit(resource.RequireNamespace(o.CmdNamespace)); err != nil {
378 return preservedFile(err, file, o.ErrOut)
379 }
380
381
382 if err := o.visitAnnotation(updatedVisitor); err != nil {
383 return preservedFile(err, file, o.ErrOut)
384 }
385
386 switch o.EditMode {
387 case NormalEditMode:
388 err = o.visitToPatch(infos, updatedVisitor, &results)
389 case ApplyEditMode:
390 err = o.visitToApplyEditPatch(infos, updatedVisitor)
391 case EditBeforeCreateMode:
392 err = o.visitToCreate(updatedVisitor)
393 default:
394 err = fmt.Errorf("unsupported edit mode %q", o.EditMode)
395 }
396 if err != nil {
397 return preservedFile(err, results.file, o.ErrOut)
398 }
399
400
401
402
403
404
405 if results.retryable > 0 {
406 fmt.Fprintf(o.ErrOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file)
407 return cmdutil.ErrExit
408 }
409 if results.notfound > 0 {
410 fmt.Fprintf(o.ErrOut, "The edits you made on deleted resources have been saved to %q\n", file)
411 return cmdutil.ErrExit
412 }
413
414 if len(results.edit) == 0 {
415 if results.notfound == 0 {
416 os.Remove(file)
417 } else {
418 fmt.Fprintf(o.Out, "The edits you made on deleted resources have been saved to %q\n", file)
419 }
420 return nil
421 }
422
423 if len(results.header.reasons) > 0 {
424 containsError = true
425 }
426 }
427 }
428
429 switch o.EditMode {
430
431 case NormalEditMode:
432 infos, err := o.OriginalResult.Infos()
433 if err != nil {
434 return err
435 }
436 if len(infos) == 0 {
437 return errors.New("edit cancelled, no objects found")
438 }
439 return editFn(infos)
440 case ApplyEditMode:
441 infos, err := o.OriginalResult.Infos()
442 if err != nil {
443 return err
444 }
445 var annotationInfos []*resource.Info
446 for i := range infos {
447 data, err := util.GetOriginalConfiguration(infos[i].Object)
448 if err != nil {
449 return err
450 }
451 if data == nil {
452 continue
453 }
454
455 tempInfos, err := o.updatedResultGetter(data).Infos()
456 if err != nil {
457 return err
458 }
459 annotationInfos = append(annotationInfos, tempInfos[0])
460 }
461 if len(annotationInfos) == 0 {
462 return errors.New("no last-applied-configuration annotation found on resources, to create the annotation, use command `kubectl apply set-last-applied --create-annotation`")
463 }
464 return editFn(annotationInfos)
465
466 case EditBeforeCreateMode:
467 return o.OriginalResult.Visit(func(info *resource.Info, err error) error {
468 return editFn([]*resource.Info{info})
469 })
470 default:
471 return fmt.Errorf("unsupported edit mode %q", o.EditMode)
472 }
473 }
474
475 func (o *EditOptions) extractManagedFields(obj runtime.Object) error {
476 o.managedFields = make(map[types.UID][]metav1.ManagedFieldsEntry)
477 if meta.IsListType(obj) {
478 err := meta.EachListItem(obj, func(obj runtime.Object) error {
479 uid, mf, err := clearManagedFields(obj)
480 if err != nil {
481 return err
482 }
483 o.managedFields[uid] = mf
484 return nil
485 })
486 return err
487 }
488 uid, mf, err := clearManagedFields(obj)
489 if err != nil {
490 return err
491 }
492 o.managedFields[uid] = mf
493 return nil
494 }
495
496 func clearManagedFields(obj runtime.Object) (types.UID, []metav1.ManagedFieldsEntry, error) {
497 metaObjs, err := meta.Accessor(obj)
498 if err != nil {
499 return "", nil, err
500 }
501 mf := metaObjs.GetManagedFields()
502 metaObjs.SetManagedFields(nil)
503 return metaObjs.GetUID(), mf, nil
504 }
505
506 func (o *EditOptions) restoreManagedFields(infos []*resource.Info) error {
507 for _, info := range infos {
508 metaObjs, err := meta.Accessor(info.Object)
509 if err != nil {
510 return err
511 }
512 mf := o.managedFields[metaObjs.GetUID()]
513 metaObjs.SetManagedFields(mf)
514 }
515 return nil
516 }
517
518 func (o *EditOptions) visitToApplyEditPatch(originalInfos []*resource.Info, patchVisitor resource.Visitor) error {
519 err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error {
520 editObjUID, err := meta.NewAccessor().UID(info.Object)
521 if err != nil {
522 return err
523 }
524
525 var originalInfo *resource.Info
526 for _, i := range originalInfos {
527 originalObjUID, err := meta.NewAccessor().UID(i.Object)
528 if err != nil {
529 return err
530 }
531 if editObjUID == originalObjUID {
532 originalInfo = i
533 break
534 }
535 }
536 if originalInfo == nil {
537 return fmt.Errorf("no original object found for %#v", info.Object)
538 }
539
540 originalJS, err := encodeToJSON(originalInfo.Object.(runtime.Unstructured))
541 if err != nil {
542 return err
543 }
544
545 editedJS, err := encodeToJSON(info.Object.(runtime.Unstructured))
546 if err != nil {
547 return err
548 }
549
550 if reflect.DeepEqual(originalJS, editedJS) {
551 printer, err := o.ToPrinter("skipped")
552 if err != nil {
553 return err
554 }
555 return printer.PrintObj(info.Object, o.Out)
556 }
557 err = o.annotationPatch(info)
558 if err != nil {
559 return err
560 }
561
562 printer, err := o.ToPrinter("edited")
563 if err != nil {
564 return err
565 }
566 return printer.PrintObj(info.Object, o.Out)
567 })
568 return err
569 }
570
571 func (o *EditOptions) annotationPatch(update *resource.Info) error {
572 patch, _, patchType, err := GetApplyPatch(update.Object.(runtime.Unstructured))
573 if err != nil {
574 return err
575 }
576 mapping := update.ResourceMapping()
577 client, err := o.f.UnstructuredClientForMapping(mapping)
578 if err != nil {
579 return err
580 }
581 helper := resource.NewHelper(client, mapping).
582 WithFieldManager(o.FieldManager).
583 WithFieldValidation(o.ValidationDirective).
584 WithSubresource(o.Subresource)
585 _, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch, nil)
586 return err
587 }
588
589
590 func GetApplyPatch(obj runtime.Unstructured) ([]byte, []byte, types.PatchType, error) {
591 beforeJSON, err := encodeToJSON(obj)
592 if err != nil {
593 return nil, []byte(""), types.MergePatchType, err
594 }
595 objCopy := obj.DeepCopyObject()
596 accessor := meta.NewAccessor()
597 annotations, err := accessor.Annotations(objCopy)
598 if err != nil {
599 return nil, beforeJSON, types.MergePatchType, err
600 }
601 if annotations == nil {
602 annotations = map[string]string{}
603 }
604 annotations[corev1.LastAppliedConfigAnnotation] = string(beforeJSON)
605 accessor.SetAnnotations(objCopy, annotations)
606 afterJSON, err := encodeToJSON(objCopy.(runtime.Unstructured))
607 if err != nil {
608 return nil, beforeJSON, types.MergePatchType, err
609 }
610 patch, err := jsonpatch.CreateMergePatch(beforeJSON, afterJSON)
611 return patch, beforeJSON, types.MergePatchType, err
612 }
613
614 func encodeToJSON(obj runtime.Unstructured) ([]byte, error) {
615 serialization, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
616 if err != nil {
617 return nil, err
618 }
619 js, err := yaml.ToJSON(serialization)
620 if err != nil {
621 return nil, err
622 }
623 return js, nil
624 }
625
626 func (o *EditOptions) visitToPatch(originalInfos []*resource.Info, patchVisitor resource.Visitor, results *editResults) error {
627 err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error {
628 editObjUID, err := meta.NewAccessor().UID(info.Object)
629 if err != nil {
630 return err
631 }
632
633 var originalInfo *resource.Info
634 for _, i := range originalInfos {
635 originalObjUID, err := meta.NewAccessor().UID(i.Object)
636 if err != nil {
637 return err
638 }
639 if editObjUID == originalObjUID {
640 originalInfo = i
641 break
642 }
643 }
644 if originalInfo == nil {
645 return fmt.Errorf("no original object found for %#v", info.Object)
646 }
647
648 originalJS, err := encodeToJSON(originalInfo.Object.(runtime.Unstructured))
649 if err != nil {
650 return err
651 }
652
653 editedJS, err := encodeToJSON(info.Object.(runtime.Unstructured))
654 if err != nil {
655 return err
656 }
657
658 if reflect.DeepEqual(originalJS, editedJS) {
659
660 printer, err := o.ToPrinter("skipped")
661 if err != nil {
662 return err
663 }
664 return printer.PrintObj(info.Object, o.Out)
665 }
666
667 preconditions := []mergepatch.PreconditionFunc{
668 mergepatch.RequireKeyUnchanged("apiVersion"),
669 mergepatch.RequireKeyUnchanged("kind"),
670 mergepatch.RequireMetadataKeyUnchanged("name"),
671 mergepatch.RequireKeyUnchanged("managedFields"),
672 }
673
674
675
676 versionedObject, err := scheme.Scheme.New(info.Mapping.GroupVersionKind)
677 var patchType types.PatchType
678 var patch []byte
679 switch {
680 case runtime.IsNotRegisteredError(err):
681
682 patchType = types.MergePatchType
683 patch, err = jsonpatch.CreateMergePatch(originalJS, editedJS)
684 if err != nil {
685 klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
686 return err
687 }
688 var patchMap map[string]interface{}
689 err = json.Unmarshal(patch, &patchMap)
690 if err != nil {
691 klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
692 return err
693 }
694 for _, precondition := range preconditions {
695 if !precondition(patchMap) {
696 klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
697 return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
698 }
699 }
700 case err != nil:
701 return err
702 default:
703 patchType = types.StrategicMergePatchType
704 patch, err = strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, versionedObject, preconditions...)
705 if err != nil {
706 klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
707 if mergepatch.IsPreconditionFailed(err) {
708 return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
709 }
710 return err
711 }
712 }
713
714 if o.OutputPatch {
715 fmt.Fprintf(o.Out, "Patch: %s\n", string(patch))
716 }
717
718 patched, err := resource.NewHelper(info.Client, info.Mapping).
719 WithFieldManager(o.FieldManager).
720 WithFieldValidation(o.ValidationDirective).
721 WithSubresource(o.Subresource).
722 Patch(info.Namespace, info.Name, patchType, patch, nil)
723 if err != nil {
724 fmt.Fprintln(o.ErrOut, results.addError(err, info))
725 return nil
726 }
727 info.Refresh(patched, true)
728 printer, err := o.ToPrinter("edited")
729 if err != nil {
730 return err
731 }
732 return printer.PrintObj(info.Object, o.Out)
733 })
734 return err
735 }
736
737 func (o *EditOptions) visitToCreate(createVisitor resource.Visitor) error {
738 err := createVisitor.Visit(func(info *resource.Info, incomingErr error) error {
739 obj, err := resource.NewHelper(info.Client, info.Mapping).
740 WithFieldManager(o.FieldManager).
741 WithFieldValidation(o.ValidationDirective).
742 Create(info.Namespace, true, info.Object)
743 if err != nil {
744 return err
745 }
746 info.Refresh(obj, true)
747 printer, err := o.ToPrinter("created")
748 if err != nil {
749 return err
750 }
751 return printer.PrintObj(info.Object, o.Out)
752 })
753 return err
754 }
755
756 func (o *EditOptions) visitAnnotation(annotationVisitor resource.Visitor) error {
757
758 err := annotationVisitor.Visit(func(info *resource.Info, incomingErr error) error {
759
760 if o.ApplyAnnotation {
761 if err := util.CreateOrUpdateAnnotation(true, info.Object, scheme.DefaultJSONEncoder()); err != nil {
762 return err
763 }
764 }
765 if err := o.Recorder.Record(info.Object); err != nil {
766 klog.V(4).Infof("error recording current command: %v", err)
767 }
768
769 return nil
770
771 })
772 return err
773 }
774
775
776 type EditMode string
777
778 const (
779
780 NormalEditMode EditMode = "normal_mode"
781
782
783 EditBeforeCreateMode EditMode = "edit_before_create_mode"
784
785
786 ApplyEditMode EditMode = "edit_last_applied_mode"
787 )
788
789
790 type editReason struct {
791 head string
792 other []string
793 }
794
795
796 type editHeader struct {
797 reasons []editReason
798 }
799
800
801 func (h *editHeader) writeTo(w io.Writer, editMode EditMode) error {
802 if editMode == ApplyEditMode {
803 fmt.Fprint(w, `# Please edit the 'last-applied-configuration' annotations below.
804 # Lines beginning with a '#' will be ignored, and an empty file will abort the edit.
805 #
806 `)
807 } else {
808 fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored,
809 # and an empty file will abort the edit. If an error occurs while saving this file will be
810 # reopened with the relevant failures.
811 #
812 `)
813 }
814
815 for _, r := range h.reasons {
816 if len(r.other) > 0 {
817 fmt.Fprintf(w, "# %s:\n", hashOnLineBreak(r.head))
818 } else {
819 fmt.Fprintf(w, "# %s\n", hashOnLineBreak(r.head))
820 }
821 for _, o := range r.other {
822 fmt.Fprintf(w, "# * %s\n", hashOnLineBreak(o))
823 }
824 fmt.Fprintln(w, "#")
825 }
826 return nil
827 }
828
829
830 type editResults struct {
831 header editHeader
832 retryable int
833 notfound int
834 edit []*resource.Info
835 file string
836 }
837
838 func (r *editResults) addError(err error, info *resource.Info) string {
839 resourceString := info.Mapping.Resource.Resource
840 if len(info.Mapping.Resource.Group) > 0 {
841 resourceString = resourceString + "." + info.Mapping.Resource.Group
842 }
843
844 switch {
845 case apierrors.IsInvalid(err):
846 r.edit = append(r.edit, info)
847 reason := editReason{
848 head: fmt.Sprintf("%s %q was not valid", resourceString, info.Name),
849 }
850 if err, ok := err.(apierrors.APIStatus); ok {
851 if details := err.Status().Details; details != nil {
852 for _, cause := range details.Causes {
853 reason.other = append(reason.other, fmt.Sprintf("%s: %s", cause.Field, cause.Message))
854 }
855 }
856 }
857 r.header.reasons = append(r.header.reasons, reason)
858 return fmt.Sprintf("error: %s %q is invalid", resourceString, info.Name)
859 case apierrors.IsNotFound(err):
860 r.notfound++
861 return fmt.Sprintf("error: %s %q could not be found on the server", resourceString, info.Name)
862 default:
863 r.retryable++
864 return fmt.Sprintf("error: %s %q could not be patched: %v", resourceString, info.Name, err)
865 }
866 }
867
868
869
870
871 func preservedFile(err error, path string, out io.Writer) error {
872 if len(path) > 0 {
873 if _, err := os.Stat(path); !os.IsNotExist(err) {
874 fmt.Fprintf(out, "A copy of your changes has been stored to %q\n", path)
875 }
876 }
877 return err
878 }
879
880
881
882
883 func hasLines(r io.Reader) (bool, error) {
884
885
886 s := bufio.NewScanner(r)
887 for s.Scan() {
888 if line := strings.TrimSpace(s.Text()); len(line) > 0 && line[0] != '#' {
889 return true, nil
890 }
891 }
892 if err := s.Err(); err != nil && err != io.EOF {
893 return false, err
894 }
895 return false, nil
896 }
897
898
899
900 func hashOnLineBreak(s string) string {
901 r := ""
902 for i, ch := range s {
903 j := i + 1
904 if j < len(s) && ch == '\n' && s[j] != '#' {
905 r += "\n# "
906 } else {
907 r += string(ch)
908 }
909 }
910 return r
911 }
912
913
914 func editorEnvs() []string {
915 return []string{
916 "KUBE_EDITOR",
917 "EDITOR",
918 }
919 }
920
View as plain text