edit.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. /*
  2. Copyright 2015 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 cmd
  14. import (
  15. "bufio"
  16. "bytes"
  17. "fmt"
  18. "io"
  19. "os"
  20. "path/filepath"
  21. "reflect"
  22. gruntime "runtime"
  23. "strings"
  24. "github.com/renstrom/dedent"
  25. "k8s.io/kubernetes/pkg/api"
  26. "k8s.io/kubernetes/pkg/api/errors"
  27. "k8s.io/kubernetes/pkg/api/meta"
  28. "k8s.io/kubernetes/pkg/api/unversioned"
  29. "k8s.io/kubernetes/pkg/kubectl"
  30. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  31. "k8s.io/kubernetes/pkg/kubectl/cmd/util/editor"
  32. "k8s.io/kubernetes/pkg/kubectl/cmd/util/jsonmerge"
  33. "k8s.io/kubernetes/pkg/kubectl/resource"
  34. "k8s.io/kubernetes/pkg/runtime"
  35. "k8s.io/kubernetes/pkg/util/crlf"
  36. "k8s.io/kubernetes/pkg/util/strategicpatch"
  37. "k8s.io/kubernetes/pkg/util/yaml"
  38. "github.com/golang/glog"
  39. "github.com/spf13/cobra"
  40. )
  41. var (
  42. editLong = dedent.Dedent(`
  43. Edit a resource from the default editor.
  44. The edit command allows you to directly edit any API resource you can retrieve via the
  45. command line tools. It will open the editor defined by your KUBE_EDITOR, or EDITOR
  46. environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows.
  47. You can edit multiple objects, although changes are applied one at a time. The command
  48. accepts filenames as well as command line arguments, although the files you point to must
  49. be previously saved versions of resources.
  50. The files to edit will be output in the default API version, or a version specified
  51. by --output-version. The default format is YAML - if you would like to edit in JSON
  52. pass -o json. The flag --windows-line-endings can be used to force Windows line endings,
  53. otherwise the default for your operating system will be used.
  54. In the event an error occurs while updating, a temporary file will be created on disk
  55. that contains your unapplied changes. The most common error when updating a resource
  56. is another editor changing the resource on the server. When this occurs, you will have
  57. to apply your changes to the newer version of the resource, or update your temporary
  58. saved copy to include the latest resource version.`)
  59. editExample = dedent.Dedent(`
  60. # Edit the service named 'docker-registry':
  61. kubectl edit svc/docker-registry
  62. # Use an alternative editor
  63. KUBE_EDITOR="nano" kubectl edit svc/docker-registry
  64. # Edit the service 'docker-registry' in JSON using the v1 API format:
  65. kubectl edit svc/docker-registry --output-version=v1 -o json`)
  66. )
  67. // EditOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
  68. // referencing the cmd.Flags()
  69. type EditOptions struct {
  70. Filenames []string
  71. Recursive bool
  72. }
  73. var errExit = fmt.Errorf("exit directly")
  74. func NewCmdEdit(f *cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
  75. options := &EditOptions{}
  76. // retrieve a list of handled resources from printer as valid args
  77. validArgs, argAliases := []string{}, []string{}
  78. p, err := f.Printer(nil, kubectl.PrintOptions{
  79. ColumnLabels: []string{},
  80. })
  81. cmdutil.CheckErr(err)
  82. if p != nil {
  83. validArgs = p.HandledResources()
  84. argAliases = kubectl.ResourceAliases(validArgs)
  85. }
  86. cmd := &cobra.Command{
  87. Use: "edit (RESOURCE/NAME | -f FILENAME)",
  88. Short: "Edit a resource on the server",
  89. Long: editLong,
  90. Example: fmt.Sprintf(editExample),
  91. Run: func(cmd *cobra.Command, args []string) {
  92. err := RunEdit(f, out, errOut, cmd, args, options)
  93. if err == errExit {
  94. os.Exit(1)
  95. }
  96. cmdutil.CheckErr(err)
  97. },
  98. ValidArgs: validArgs,
  99. ArgAliases: argAliases,
  100. }
  101. usage := "Filename, directory, or URL to file to use to edit the resource"
  102. kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage)
  103. cmdutil.AddRecursiveFlag(cmd, &options.Recursive)
  104. cmdutil.AddValidateFlags(cmd)
  105. cmd.Flags().StringP("output", "o", "yaml", "Output format. One of: yaml|json.")
  106. cmd.Flags().String("output-version", "", "Output the formatted object with the given group version (for ex: 'extensions/v1beta1').")
  107. cmd.Flags().Bool("windows-line-endings", gruntime.GOOS == "windows", "Use Windows line-endings (default Unix line-endings)")
  108. cmdutil.AddApplyAnnotationFlags(cmd)
  109. cmdutil.AddRecordFlag(cmd)
  110. cmdutil.AddInclude3rdPartyFlags(cmd)
  111. return cmd
  112. }
  113. func RunEdit(f *cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, options *EditOptions) error {
  114. var printer kubectl.ResourcePrinter
  115. var ext string
  116. switch format := cmdutil.GetFlagString(cmd, "output"); format {
  117. case "json":
  118. printer = &kubectl.JSONPrinter{}
  119. ext = ".json"
  120. case "yaml":
  121. printer = &kubectl.YAMLPrinter{}
  122. ext = ".yaml"
  123. default:
  124. return cmdutil.UsageError(cmd, "The flag 'output' must be one of yaml|json")
  125. }
  126. cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
  127. if err != nil {
  128. return err
  129. }
  130. mapper, typer := f.Object(cmdutil.GetIncludeThirdPartyAPIs(cmd))
  131. resourceMapper := &resource.Mapper{
  132. ObjectTyper: typer,
  133. RESTMapper: mapper,
  134. ClientMapper: resource.ClientMapperFunc(f.ClientForMapping),
  135. // NB: we use `f.Decoder(false)` to get a plain deserializer for
  136. // the resourceMapper, since it's used to read in edits and
  137. // we don't want to convert into the internal version when
  138. // reading in edits (this would cause us to potentially try to
  139. // compare two different GroupVersions).
  140. Decoder: f.Decoder(false),
  141. }
  142. r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
  143. NamespaceParam(cmdNamespace).DefaultNamespace().
  144. FilenameParam(enforceNamespace, options.Recursive, options.Filenames...).
  145. ResourceTypeOrNameArgs(true, args...).
  146. ContinueOnError().
  147. Flatten().
  148. Latest().
  149. Do()
  150. err = r.Err()
  151. if err != nil {
  152. return err
  153. }
  154. clientConfig, err := f.ClientConfig()
  155. if err != nil {
  156. return err
  157. }
  158. encoder := f.JSONEncoder()
  159. defaultVersion, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
  160. if err != nil {
  161. return err
  162. }
  163. var (
  164. windowsLineEndings = cmdutil.GetFlagBool(cmd, "windows-line-endings")
  165. edit = editor.NewDefaultEditor(f.EditorEnvs())
  166. )
  167. err = r.Visit(func(info *resource.Info, err error) error {
  168. var (
  169. results = editResults{}
  170. original = []byte{}
  171. edited = []byte{}
  172. file string
  173. )
  174. containsError := false
  175. for {
  176. infos := []*resource.Info{info}
  177. originalObj, err := resource.AsVersionedObject(infos, false, defaultVersion, encoder)
  178. if err != nil {
  179. return err
  180. }
  181. objToEdit := originalObj
  182. // generate the file to edit
  183. buf := &bytes.Buffer{}
  184. var w io.Writer = buf
  185. if windowsLineEndings {
  186. w = crlf.NewCRLFWriter(w)
  187. }
  188. results.header.writeTo(w)
  189. if !containsError {
  190. if err := printer.PrintObj(objToEdit, w); err != nil {
  191. return preservedFile(err, results.file, errOut)
  192. }
  193. original = buf.Bytes()
  194. } else {
  195. // In case of an error, preserve the edited file.
  196. // Remove the comments (header) from it since we already
  197. // have included the latest header in the buffer above.
  198. buf.Write(manualStrip(edited))
  199. }
  200. // launch the editor
  201. editedDiff := edited
  202. edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), ext, buf)
  203. if err != nil {
  204. return preservedFile(err, results.file, errOut)
  205. }
  206. if bytes.Equal(stripComments(editedDiff), stripComments(edited)) {
  207. // Ugly hack right here. We will hit this either (1) when we try to
  208. // save the same changes we tried to save in the previous iteration
  209. // which means our changes are invalid or (2) when we exit the second
  210. // time. The second case is more usual so we can probably live with it.
  211. // TODO: A less hacky fix would be welcome :)
  212. fmt.Fprintln(errOut, "Edit cancelled, no valid changes were saved.")
  213. return nil
  214. }
  215. // cleanup any file from the previous pass
  216. if len(results.file) > 0 {
  217. os.Remove(results.file)
  218. }
  219. glog.V(4).Infof("User edited:\n%s", string(edited))
  220. // Apply validation
  221. schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagString(cmd, "schema-cache-dir"))
  222. if err != nil {
  223. return preservedFile(err, file, errOut)
  224. }
  225. err = schema.ValidateBytes(stripComments(edited))
  226. if err != nil {
  227. return preservedFile(err, file, errOut)
  228. }
  229. // Compare content without comments
  230. if bytes.Equal(stripComments(original), stripComments(edited)) {
  231. os.Remove(file)
  232. fmt.Fprintln(errOut, "Edit cancelled, no changes made.")
  233. return nil
  234. }
  235. lines, err := hasLines(bytes.NewBuffer(edited))
  236. if err != nil {
  237. return preservedFile(err, file, errOut)
  238. }
  239. if !lines {
  240. os.Remove(file)
  241. fmt.Fprintln(errOut, "Edit cancelled, saved file was empty.")
  242. return nil
  243. }
  244. results = editResults{
  245. file: file,
  246. }
  247. // parse the edited file
  248. updates, err := resourceMapper.InfoForData(edited, "edited-file")
  249. if err != nil {
  250. // syntax error
  251. containsError = true
  252. results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)})
  253. continue
  254. }
  255. // not a syntax error as it turns out...
  256. containsError = false
  257. namespaceVisitor := resource.NewFlattenListVisitor(updates, resourceMapper)
  258. // need to make sure the original namespace wasn't changed while editing
  259. if err = namespaceVisitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil {
  260. return preservedFile(err, file, errOut)
  261. }
  262. mutatedObjects := []runtime.Object{}
  263. annotationVisitor := resource.NewFlattenListVisitor(updates, resourceMapper)
  264. // iterate through all items to apply annotations
  265. if err = annotationVisitor.Visit(func(info *resource.Info, incomingErr error) error {
  266. // put configuration annotation in "updates"
  267. if err := kubectl.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info, encoder); err != nil {
  268. return err
  269. }
  270. if cmdutil.ShouldRecord(cmd, info) {
  271. if err := cmdutil.RecordChangeCause(info.Object, f.Command()); err != nil {
  272. return err
  273. }
  274. }
  275. mutatedObjects = append(mutatedObjects, info.Object)
  276. return nil
  277. }); err != nil {
  278. return preservedFile(err, file, errOut)
  279. }
  280. // if we mutated a list in the visitor, persist the changes on the overall object
  281. if meta.IsListType(updates.Object) {
  282. meta.SetList(updates.Object, mutatedObjects)
  283. }
  284. patchVisitor := resource.NewFlattenListVisitor(updates, resourceMapper)
  285. err = patchVisitor.Visit(func(info *resource.Info, incomingErr error) error {
  286. currOriginalObj := originalObj
  287. // if we're editing a list, then navigate the list to find the item that we're currently trying to edit
  288. if meta.IsListType(originalObj) {
  289. currOriginalObj = nil
  290. editObjUID, err := meta.NewAccessor().UID(info.Object)
  291. if err != nil {
  292. return err
  293. }
  294. listItems, err := meta.ExtractList(originalObj)
  295. if err != nil {
  296. return err
  297. }
  298. // iterate through the list to find the item with the matching UID
  299. for i := range listItems {
  300. originalObjUID, err := meta.NewAccessor().UID(listItems[i])
  301. if err != nil {
  302. return err
  303. }
  304. if editObjUID == originalObjUID {
  305. currOriginalObj = listItems[i]
  306. break
  307. }
  308. }
  309. if currOriginalObj == nil {
  310. return fmt.Errorf("no original object found for %#v", info.Object)
  311. }
  312. }
  313. originalSerialization, err := runtime.Encode(encoder, currOriginalObj)
  314. if err != nil {
  315. return err
  316. }
  317. editedSerialization, err := runtime.Encode(encoder, info.Object)
  318. if err != nil {
  319. return err
  320. }
  321. // compute the patch on a per-item basis
  322. // use strategic merge to create a patch
  323. originalJS, err := yaml.ToJSON(originalSerialization)
  324. if err != nil {
  325. return err
  326. }
  327. editedJS, err := yaml.ToJSON(editedSerialization)
  328. if err != nil {
  329. return err
  330. }
  331. if reflect.DeepEqual(originalJS, editedJS) {
  332. // no edit, so just skip it.
  333. cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "skipped")
  334. return nil
  335. }
  336. patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, currOriginalObj)
  337. // TODO: change all jsonmerge to strategicpatch
  338. // for checking preconditions
  339. preconditions := []jsonmerge.PreconditionFunc{}
  340. if err != nil {
  341. glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
  342. return err
  343. } else {
  344. preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("apiVersion"))
  345. preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("kind"))
  346. preconditions = append(preconditions, jsonmerge.RequireMetadataKeyUnchanged("name"))
  347. results.version = defaultVersion
  348. }
  349. if hold, msg := jsonmerge.TestPreconditionsHold(patch, preconditions); !hold {
  350. fmt.Fprintf(errOut, "error: %s", msg)
  351. return preservedFile(nil, file, errOut)
  352. }
  353. patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch)
  354. if err != nil {
  355. fmt.Fprintln(out, results.addError(err, info))
  356. return nil
  357. }
  358. info.Refresh(patched, true)
  359. cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "edited")
  360. return nil
  361. })
  362. if err != nil {
  363. return preservedFile(err, results.file, errOut)
  364. }
  365. // Handle all possible errors
  366. //
  367. // 1. retryable: propose kubectl replace -f
  368. // 2. notfound: indicate the location of the saved configuration of the deleted resource
  369. // 3. invalid: retry those on the spot by looping ie. reloading the editor
  370. if results.retryable > 0 {
  371. fmt.Fprintf(errOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file)
  372. return errExit
  373. }
  374. if results.notfound > 0 {
  375. fmt.Fprintf(errOut, "The edits you made on deleted resources have been saved to %q\n", file)
  376. return errExit
  377. }
  378. if len(results.edit) == 0 {
  379. if results.notfound == 0 {
  380. os.Remove(file)
  381. } else {
  382. fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file)
  383. }
  384. return nil
  385. }
  386. // loop again and edit the remaining items
  387. infos = results.edit
  388. }
  389. })
  390. return err
  391. }
  392. // editReason preserves a message about the reason this file must be edited again
  393. type editReason struct {
  394. head string
  395. other []string
  396. }
  397. // editHeader includes a list of reasons the edit must be retried
  398. type editHeader struct {
  399. reasons []editReason
  400. }
  401. // writeTo outputs the current header information into a stream
  402. func (h *editHeader) writeTo(w io.Writer) error {
  403. fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored,
  404. # and an empty file will abort the edit. If an error occurs while saving this file will be
  405. # reopened with the relevant failures.
  406. #
  407. `)
  408. for _, r := range h.reasons {
  409. if len(r.other) > 0 {
  410. fmt.Fprintf(w, "# %s:\n", r.head)
  411. } else {
  412. fmt.Fprintf(w, "# %s\n", r.head)
  413. }
  414. for _, o := range r.other {
  415. fmt.Fprintf(w, "# * %s\n", o)
  416. }
  417. fmt.Fprintln(w, "#")
  418. }
  419. return nil
  420. }
  421. func (h *editHeader) flush() {
  422. h.reasons = []editReason{}
  423. }
  424. // editResults capture the result of an update
  425. type editResults struct {
  426. header editHeader
  427. retryable int
  428. notfound int
  429. edit []*resource.Info
  430. file string
  431. version unversioned.GroupVersion
  432. }
  433. func (r *editResults) addError(err error, info *resource.Info) string {
  434. switch {
  435. case errors.IsInvalid(err):
  436. r.edit = append(r.edit, info)
  437. reason := editReason{
  438. head: fmt.Sprintf("%s %q was not valid", info.Mapping.Resource, info.Name),
  439. }
  440. if err, ok := err.(errors.APIStatus); ok {
  441. if details := err.Status().Details; details != nil {
  442. for _, cause := range details.Causes {
  443. reason.other = append(reason.other, fmt.Sprintf("%s: %s", cause.Field, cause.Message))
  444. }
  445. }
  446. }
  447. r.header.reasons = append(r.header.reasons, reason)
  448. return fmt.Sprintf("error: %s %q is invalid", info.Mapping.Resource, info.Name)
  449. case errors.IsNotFound(err):
  450. r.notfound++
  451. return fmt.Sprintf("error: %s %q could not be found on the server", info.Mapping.Resource, info.Name)
  452. default:
  453. r.retryable++
  454. return fmt.Sprintf("error: %s %q could not be patched: %v", info.Mapping.Resource, info.Name, err)
  455. }
  456. }
  457. // preservedFile writes out a message about the provided file if it exists to the
  458. // provided output stream when an error happens. Used to notify the user where
  459. // their updates were preserved.
  460. func preservedFile(err error, path string, out io.Writer) error {
  461. if len(path) > 0 {
  462. if _, err := os.Stat(path); !os.IsNotExist(err) {
  463. fmt.Fprintf(out, "A copy of your changes has been stored to %q\n", path)
  464. }
  465. }
  466. return err
  467. }
  468. // hasLines returns true if any line in the provided stream is non empty - has non-whitespace
  469. // characters, or the first non-whitespace character is a '#' indicating a comment. Returns
  470. // any errors encountered reading the stream.
  471. func hasLines(r io.Reader) (bool, error) {
  472. // TODO: if any files we read have > 64KB lines, we'll need to switch to bytes.ReadLine
  473. // TODO: probably going to be secrets
  474. s := bufio.NewScanner(r)
  475. for s.Scan() {
  476. if line := strings.TrimSpace(s.Text()); len(line) > 0 && line[0] != '#' {
  477. return true, nil
  478. }
  479. }
  480. if err := s.Err(); err != nil && err != io.EOF {
  481. return false, err
  482. }
  483. return false, nil
  484. }
  485. // stripComments will transform a YAML file into JSON, thus dropping any comments
  486. // in it. Note that if the given file has a syntax error, the transformation will
  487. // fail and we will manually drop all comments from the file.
  488. func stripComments(file []byte) []byte {
  489. stripped := file
  490. stripped, err := yaml.ToJSON(stripped)
  491. if err != nil {
  492. stripped = manualStrip(file)
  493. }
  494. return stripped
  495. }
  496. // manualStrip is used for dropping comments from a YAML file
  497. func manualStrip(file []byte) []byte {
  498. stripped := []byte{}
  499. lines := bytes.Split(file, []byte("\n"))
  500. for i, line := range lines {
  501. if bytes.HasPrefix(bytes.TrimSpace(line), []byte("#")) {
  502. continue
  503. }
  504. stripped = append(stripped, line...)
  505. if i < len(lines)-1 {
  506. stripped = append(stripped, '\n')
  507. }
  508. }
  509. return stripped
  510. }