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  }