github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/cmd/orbctl/edit.go (about) 1 // Inspired by https://samrapdev.com/capturing-sensitive-input-with-editor-in-golang-from-the-cli/ 2 3 package main 4 5 import ( 6 "bytes" 7 "errors" 8 "io" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "strings" 13 14 "github.com/caos/orbos/pkg/kubernetes/cli" 15 16 "github.com/caos/orbos/mntr" 17 18 "github.com/spf13/cobra" 19 "k8s.io/kubectl/pkg/util/term" 20 21 "github.com/caos/orbos/pkg/git" 22 ) 23 24 func EditCommand(getRv GetRootValues) *cobra.Command { 25 return &cobra.Command{ 26 Use: "edit <path>", 27 Short: "Edit the file in your favorite text editor", 28 Args: cobra.ExactArgs(1), 29 Example: `orbctl file edit desired.yml`, 30 RunE: func(cmd *cobra.Command, args []string) (err error) { 31 32 rv := getRv("edit", "", map[string]interface{}{"file": args[0]}) 33 defer rv.ErrFunc(err) 34 35 if !rv.Gitops { 36 return mntr.ToUserError(errors.New("edit command is only supported with the --gitops flag")) 37 } 38 39 if _, err := cli.Init(rv.Monitor, rv.OrbConfig, rv.GitClient, rv.Kubeconfig, rv.Gitops, true, true); err != nil { 40 return err 41 } 42 43 edited, err := captureInputFromEditor(GetPreferredEditorFromEnvironment, bytes.NewReader(rv.GitClient.Read(args[0]))) 44 if err != nil { 45 panic(err) 46 } 47 48 return rv.GitClient.UpdateRemote("File written by orbctl", func() []git.File { 49 return []git.File{{ 50 Path: args[0], 51 Content: edited, 52 }} 53 }) 54 }, 55 } 56 } 57 58 // DefaultEditor is vim because we're adults ;) 59 const DefaultEditor = "vim" 60 61 // PreferredEditorResolver is a function that returns an editor that the user 62 // prefers to use, such as the configured `$EDITOR` environment variable. 63 type PreferredEditorResolver func() string 64 65 // GetPreferredEditorFromEnvironment returns the user's editor as defined by the 66 // `$EDITOR` environment variable, or the `DefaultEditor` if it is not set. 67 func GetPreferredEditorFromEnvironment() string { 68 editor := os.Getenv("EDITOR") 69 70 if editor == "" { 71 return DefaultEditor 72 } 73 74 return editor 75 } 76 77 func resolveEditorArguments(executable string, filename string) []string { 78 args := []string{filename} 79 80 if strings.Contains(executable, "Visual Studio Code.app") { 81 args = append([]string{"--wait"}, args...) 82 } 83 84 if strings.Contains(executable, "vim") { 85 args = append([]string{"--not-a-term", "-c", "set nowrap"}, args...) 86 } 87 88 // Other common editors 89 90 return args 91 } 92 93 // openFileInEditor opens filename in a text editor. 94 func openFileInEditor(filename string, resolveEditor PreferredEditorResolver) error { 95 // Get the full executable path for the editor. 96 executable, err := exec.LookPath(resolveEditor()) 97 if err != nil { 98 return mntr.ToUserError(err) 99 } 100 101 cmd := exec.Command(executable, resolveEditorArguments(executable, filename)...) 102 cmd.Stdin = os.Stdin 103 cmd.Stdout = os.Stdout 104 cmd.Stderr = os.Stderr 105 106 return (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run) 107 } 108 109 // captureInputFromEditor opens a temporary file in a text editor and returns 110 // the written bytes on success or an error on failure. It handles deletion 111 // of the temporary file behind the scenes. 112 func captureInputFromEditor(resolveEditor PreferredEditorResolver, content io.Reader) ([]byte, error) { 113 file, err := ioutil.TempFile(os.TempDir(), "*") 114 if err != nil { 115 return []byte{}, err 116 } 117 118 filename := file.Name() 119 120 // Defer removal of the temporary file in case any of the next steps fail. 121 defer os.Remove(filename) 122 123 if _, err := io.Copy(file, content); err != nil { 124 return []byte{}, err 125 } 126 127 if err = file.Close(); err != nil { 128 return []byte{}, err 129 } 130 131 if err = openFileInEditor(filename, resolveEditor); err != nil { 132 return []byte{}, err 133 } 134 135 return ioutil.ReadFile(filename) 136 }