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