editoptions.go 24 KB

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