...

Source file src/k8s.io/kubectl/pkg/cmd/util/editor/editoptions.go

Documentation: k8s.io/kubectl/pkg/cmd/util/editor

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    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  // EditOptions contains all the options for running edit cli command.
    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  // NewEditOptions returns an initialized EditOptions instance
    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  			// create new editor-specific PrintFlags, with all
   107  			// output flags disabled, except json / yaml
   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  	// bind output format from existing printflags
   135  	if fromPrintFlags != nil && len(*fromPrintFlags.OutputFormat) > 0 {
   136  		e.printFlags.OutputFormat = fromPrintFlags.OutputFormat
   137  	}
   138  
   139  	// prevent a commented header at the top of the user's
   140  	// default editor if presenting contents as json.
   141  	if *e.printFlags.OutputFormat == "json" {
   142  		e.addHeader = false
   143  		e.ext = ".json"
   144  		return nil
   145  	}
   146  
   147  	// we default to yaml if check above is false, as only json or yaml are supported
   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  // Complete completes all the required options
   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  		// when do normal edit or apply edit we need to always retrieve the latest resource from server
   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  		// resource builder to read objects from edited data
   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  // Validate checks the EditOptions to see if there is sufficient information to run the command.
   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  // Run performs the execution
   240  func (o *EditOptions) Run() error {
   241  	edit := NewDefaultEditor(editorEnvs())
   242  	// 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)
   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  		// loop until we succeed or cancel editing
   254  		for {
   255  			// get the object we're going to serialize as input to the editor
   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  			// generate the file to edit
   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  				// In case of an error, preserve the edited file.
   296  				// Remove the comments (header) from it since we already
   297  				// have included the latest header in the buffer above.
   298  				buf.Write(cmdutil.ManualStrip(edited))
   299  			}
   300  
   301  			// launch the editor
   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  			// If we're retrying the loop because of an error, and no change was made in the file, short-circuit
   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  			// cleanup any file from the previous pass
   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  			// Apply validation
   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  			// Compare content without comments
   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  			// parse the edited file
   356  			updatedInfos, err := o.updatedResultGetter(edited).Infos()
   357  			if err != nil {
   358  				// syntax error
   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  			// not a syntax error as it turns out...
   365  			containsError = false
   366  			updatedVisitor := resource.InfoListVisitor(updatedInfos)
   367  
   368  			// we need to add back managedFields to both updated and original object
   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  			// need to make sure the original namespace wasn't changed while editing
   377  			if err := updatedVisitor.Visit(resource.RequireNamespace(o.CmdNamespace)); err != nil {
   378  				return preservedFile(err, file, o.ErrOut)
   379  			}
   380  
   381  			// iterate through all items to apply annotations
   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  			// Handle all possible errors
   401  			//
   402  			// 1. retryable: propose kubectl replace -f
   403  			// 2. notfound: indicate the location of the saved configuration of the deleted resource
   404  			// 3. invalid: retry those on the spot by looping ie. reloading the editor
   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  	// If doing normal edit we cannot use Visit because we need to edit a list for convenience. Ref: #20519
   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  	// If doing an edit before created, we don't want a list and instead want the normal behavior as kubectl create.
   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  // GetApplyPatch is used to get and apply patches
   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  			// no edit, so just skip it.
   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  		// Create the versioned struct from the type defined in the mapping
   675  		// (which is the API version we'll be submitting the patch to)
   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  			// fall back to generic JSON merge patch
   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  	// iterate through all items to apply annotations
   758  	err := annotationVisitor.Visit(func(info *resource.Info, incomingErr error) error {
   759  		// put configuration annotation in "updates"
   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  // EditMode can be either NormalEditMode, EditBeforeCreateMode or ApplyEditMode
   776  type EditMode string
   777  
   778  const (
   779  	// NormalEditMode is an edit mode
   780  	NormalEditMode EditMode = "normal_mode"
   781  
   782  	// EditBeforeCreateMode is an edit mode
   783  	EditBeforeCreateMode EditMode = "edit_before_create_mode"
   784  
   785  	// ApplyEditMode is an edit mode
   786  	ApplyEditMode EditMode = "edit_last_applied_mode"
   787  )
   788  
   789  // editReason preserves a message about the reason this file must be edited again
   790  type editReason struct {
   791  	head  string
   792  	other []string
   793  }
   794  
   795  // editHeader includes a list of reasons the edit must be retried
   796  type editHeader struct {
   797  	reasons []editReason
   798  }
   799  
   800  // writeTo outputs the current header information into a stream
   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  // editResults capture the result of an update
   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  // preservedFile writes out a message about the provided file if it exists to the
   869  // provided output stream when an error happens. Used to notify the user where
   870  // their updates were preserved.
   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  // hasLines returns true if any line in the provided stream is non empty - has non-whitespace
   881  // characters, or the first non-whitespace character is a '#' indicating a comment. Returns
   882  // any errors encountered reading the stream.
   883  func hasLines(r io.Reader) (bool, error) {
   884  	// TODO: if any files we read have > 64KB lines, we'll need to switch to bytes.ReadLine
   885  	// TODO: probably going to be secrets
   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  // hashOnLineBreak returns a string built from the provided string by inserting any necessary '#'
   899  // characters after '\n' characters, indicating a comment.
   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  // editorEnvs returns an ordered list of env vars to check for editor preferences.
   914  func editorEnvs() []string {
   915  	return []string{
   916  		"KUBE_EDITOR",
   917  		"EDITOR",
   918  	}
   919  }
   920  

View as plain text