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