diff --git a/go.mod b/go.mod index 227fefe6..9df07dd8 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/aws/aws-sdk-go v1.25.49 github.com/docker/go-units v0.3.3 github.com/fatih/color v1.7.0 + github.com/joho/godotenv v1.3.0 github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943 github.com/mattn/go-colorable v0.1.1 github.com/mattn/go-isatty v0.0.7 @@ -20,11 +21,10 @@ require ( github.com/secrethub/secrethub-go v0.30.0 github.com/zalando/go-keyring v0.0.0-20190208082241-fbe81aec3a07 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a golang.org/x/sys v0.0.0-20200501052902-10377860bb8e golang.org/x/text v0.3.2 google.golang.org/api v0.26.0 - google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84 gopkg.in/yaml.v2 v2.2.2 gotest.tools v2.2.0+incompatible ) diff --git a/go.sum b/go.sum index 4a4de5e0..d9e02ebc 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -162,20 +164,6 @@ github.com/secrethub/demo-app v0.1.0 h1:HwPPxuiSvx4TBE7Qppzu3A9eHqmsBrIz4Ko8u8pq github.com/secrethub/demo-app v0.1.0/go.mod h1:ymjm8+WXTSDTFqsGVBNVmHSnwtZMYi7KptHvpo/fLH4= github.com/secrethub/secrethub-cli v0.30.0/go.mod h1:dC0wd40v+iQdV83/0rUrOa01LYq+8Yj2AtJB1vzh2ao= github.com/secrethub/secrethub-go v0.21.0/go.mod h1:rc2IfKKBJ4L0wGec0u4XnF5/pe0FFPE4Q1MWfrFso7s= -github.com/secrethub/secrethub-go v0.29.1-0.20200626075900-f7c68f70dc36 h1:kRVdL7PRfR80xjpOxFy1O0JROVpILWc2FZWE7Ni2Z2M= -github.com/secrethub/secrethub-go v0.29.1-0.20200626075900-f7c68f70dc36/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= -github.com/secrethub/secrethub-go v0.29.1-0.20200630121846-9adfc0eb3add h1:+DzHsSjht15ycb7GFmyfmQ39gy8ZtA7FjWfJbWUPIYk= -github.com/secrethub/secrethub-go v0.29.1-0.20200630121846-9adfc0eb3add/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= -github.com/secrethub/secrethub-go v0.29.1-0.20200702094400-d465926a4a6a h1:rtFQLsSWGkdqd6LQFbgHsG/be60Cpqv8tc1w4XoKgKM= -github.com/secrethub/secrethub-go v0.29.1-0.20200702094400-d465926a4a6a/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= -github.com/secrethub/secrethub-go v0.29.1-0.20200702114848-1a3657310d91 h1:10KZJ3o7hodrTO1xAP1uNhDWSlLV9Bh9RqRFtiNCYJ4= -github.com/secrethub/secrethub-go v0.29.1-0.20200702114848-1a3657310d91/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= -github.com/secrethub/secrethub-go v0.29.1-0.20200703092019-9f5d3de9b0e4 h1:TszZ+u/DRpPjaAGwEFSQNHkWhG4QR3KBxQJ66NfTAMk= -github.com/secrethub/secrethub-go v0.29.1-0.20200703092019-9f5d3de9b0e4/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= -github.com/secrethub/secrethub-go v0.29.1-0.20200703150346-411544a71e9d h1:tADItWP+YXaGLD1ZMFocxDaKKVcu8wXgEulbcUmX4Ec= -github.com/secrethub/secrethub-go v0.29.1-0.20200703150346-411544a71e9d/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= -github.com/secrethub/secrethub-go v0.29.1-0.20200707154958-5e5602145597 h1:uC9ODMKaqBo1k8fxmFSWGkLr05TgEd3t4mHqJ8Jo9Gc= -github.com/secrethub/secrethub-go v0.29.1-0.20200707154958-5e5602145597/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= github.com/secrethub/secrethub-go v0.30.0 h1:Nh1twPDwPbYQj/cYc1NG+j7sv76LZiXLPovyV83tZj0= github.com/secrethub/secrethub-go v0.30.0/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= @@ -267,6 +255,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= diff --git a/internals/secrethub/app.go b/internals/secrethub/app.go index e95a9c8b..8f6f8460 100644 --- a/internals/secrethub/app.go +++ b/internals/secrethub/app.go @@ -186,6 +186,7 @@ func (app *App) registerCommands() { NewAuditCommand(app.io, app.clientFactory.NewClient).Register(app.cli) NewInjectCommand(app.io, app.clientFactory.NewClient).Register(app.cli) NewRunCommand(app.io, app.clientFactory.NewClient).Register(app.cli) + NewImportCommand(app.io, app.clientFactory.NewClient).Register(app.cli) NewPrintEnvCommand(app.cli, app.io).Register(app.cli) // Hidden commands diff --git a/internals/secrethub/import.go b/internals/secrethub/import.go new file mode 100644 index 00000000..446a49d9 --- /dev/null +++ b/internals/secrethub/import.go @@ -0,0 +1,26 @@ +package secrethub + +import ( + "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/secrethub/command" +) + +// ImportCommand handles the migration of secrets from outside SecretHub to SecretHub. +type ImportCommand struct { + io ui.IO + newClient newClientFunc +} + +// NewImportCommand creates a new ImportCommand. +func NewImportCommand(io ui.IO, newClient newClientFunc) *ImportCommand { + return &ImportCommand{ + io: io, + newClient: newClient, + } +} + +// Register registers the command and its sub-commands on the provided Registerer. +func (cmd *ImportCommand) Register(r command.Registerer) { + clause := r.Command("import", "Import secrets from outside of SecretHub.") + NewImportDotEnvCommand(cmd.io, cmd.newClient).Register(clause) +} diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go new file mode 100644 index 00000000..a7026a05 --- /dev/null +++ b/internals/secrethub/import_dotenv.go @@ -0,0 +1,347 @@ +package secrethub + +import ( + "bufio" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "strings" + "sync" + "text/tabwriter" + + "golang.org/x/sync/errgroup" + + "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/secrethub/command" + + "github.com/secrethub/secrethub-go/internals/api" + "github.com/secrethub/secrethub-go/pkg/secretpath" + + "github.com/joho/godotenv" +) + +// ImportDotEnvCommand handles the migration of secrets from .env files to SecretHub. +type ImportDotEnvCommand struct { + io ui.IO + path api.DirPath + interactive bool + force bool + dotenvFile string + newClient newClientFunc +} + +// NewImportDotEnvCommand creates a new ImportDotEnvCommand. +func NewImportDotEnvCommand(io ui.IO, newClient newClientFunc) *ImportDotEnvCommand { + return &ImportDotEnvCommand{ + io: io, + newClient: newClient, + } +} + +// Register registers the command and its sub-commands on the provided Registerer. +func (cmd *ImportDotEnvCommand) Register(r command.Registerer) { + clause := r.Command("dotenv", "Import secrets from `.env` files. Outputs a `secrethub.env` file, containing references to your secrets in SecretHub.") + clause.Arg("dir-path", "path to a directory on SecretHub in which to store the imported secrets").PlaceHolder(optionalDirPathPlaceHolder).Required().SetValue(&cmd.path) + clause.Flag("interactive", "Interactive mode. Edit the paths to where the secrets should be written.").Short('i').BoolVar(&cmd.interactive) + clause.Flag("env-file", "The location of the .env file. Defaults to `.env`.").Default(".env").ExistingFileVar(&cmd.dotenvFile) + registerForceFlag(clause).BoolVar(&cmd.force) + command.BindAction(clause, cmd.Run) +} + +func (cmd *ImportDotEnvCommand) Run() error { + envVar, err := godotenv.Read(cmd.dotenvFile) + if err != nil { + return err + } + + client, err := cmd.newClient() + if err != nil { + return err + } + + keys := make([]string, 0, len(envVar)) + for k := range envVar { + keys = append(keys, k) + } + unPrefixedLocationsMap := envkeysToPaths(keys) + locationsMap := make(map[string]string, len(unPrefixedLocationsMap)) + for key, path := range unPrefixedLocationsMap { + locationsMap[key] = secretpath.Join(cmd.path.Value(), path) + } + + if cmd.interactive { + editor, err := newEditor() + if err != nil { + return err + } + buildFile(locationsMap, editor) + edited, err := editor.openAndWait() + if err != nil { + return err + } + + locationsMap, err = buildMap(edited) + if err != nil { + return err + } + } + + if !cmd.force { + alreadyExist := make(map[string]struct{}) + var m sync.Mutex + errGroup, _ := errgroup.WithContext(context.Background()) + for _, path := range locationsMap { + errGroup.Go(func(path string) func() error { + return func() error { + exists, err := client.Secrets().Exists(path) + if err != nil { + return err + } + if exists { + m.Lock() + alreadyExist[path] = struct{}{} + m.Unlock() + } + return nil + } + }(path)) + } + err = errGroup.Wait() + if err != nil { + return err + } + + if len(alreadyExist) > 0 { + _, promptOut, err := cmd.io.Prompts() + if err != nil { + errMessage := "secrets already exist at the following locations: " + for location := range alreadyExist { + errMessage += location + ", " + } + errMessage = errMessage[:len(errMessage)-2] + return fmt.Errorf(errMessage) + } + + fmt.Fprintln(promptOut, "secrets already exist at the following locations:") + for location := range alreadyExist { + fmt.Fprintln(promptOut, location) + } + + confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("This import process will overwrite these secrets. Do you wish to continue?"), ui.DefaultNo) + + if err != nil { + return err + } + + if !confirmed { + _, err = fmt.Fprintln(cmd.io.Output(), "Aborting.") + if err != nil { + return err + } + return nil + } + } + } + + errGroup, _ := errgroup.WithContext(context.Background()) + for envVarKey, secretPath := range locationsMap { + errGroup.Go(func(envVarKey, secretPath string) func() error { + return func() error { + envVarValue, ok := envVar[envVarKey] + if !ok { + return fmt.Errorf("key not found in .env file: %s", envVarKey) + } + + err = client.Dirs().CreateAll(secretpath.Parent(secretPath)) + if err != nil { + return fmt.Errorf("creating parent directories for %s: %s", secretPath, err) + } + + _, err = client.Secrets().Write(secretPath, []byte(envVarValue)) + if err != nil { + return err + } + + return nil + } + }(envVarKey, secretPath)) + } + err = errGroup.Wait() + if err != nil { + return err + } + + _, err = fmt.Fprintf(cmd.io.Output(), "Transfer complete! The secrets have been written to %s:\n", cmd.path.String()) + if err != nil { + return err + } + + _, err = fmt.Fprintln(cmd.io.Output()) + if err != nil { + return err + } + + tree, err := client.Dirs().GetTree(cmd.path.Value(), -1, false) + if err != nil { + return err + } + + printTree(tree, cmd.io.Output(), false, cmd.path.Value(), false, true) + + return nil +} + +type editor struct { + file *os.File +} + +func newEditor() (editor, error) { + tmpFile, err := ioutil.TempFile(os.TempDir(), "secrethub-") + if err != nil { + return editor{}, nil + } + return editor{ + file: tmpFile, + }, nil +} + +// openAndWait opens the editors file in an editor and waits +// for the user to exit the editor. +// It returns a reader to read the edited contents of the file. +func (e editor) openAndWait() (io.Reader, error) { + defer func() { + _ = os.Remove(e.file.Name()) + }() + + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "editor" + } + + cmd := exec.Command(editor, e.file.Name()) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Start() + if err != nil { + return nil, err + } + err = cmd.Wait() + if err != nil { + return nil, err + } + + return os.Open(e.file.Name()) +} + +func (e editor) Write(in []byte) (int, error) { + return e.file.Write(in) +} + +func buildFile(locationsMap map[string]string, w io.Writer) { + tabWriter := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0) + + for envVarKey, secretPath := range locationsMap { + _, _ = fmt.Fprintf(tabWriter, "%s\t=>\t%s\n", envVarKey, secretPath) + } + _ = tabWriter.Flush() + + _, _ = fmt.Fprintln(w) + _, _ = fmt.Fprintln(w, "# Environment variables on the left of '=>' will be stored in SecretHub at the given path on the right.") + _, _ = fmt.Fprintln(w, "# You can remove or comment out lines for environment variables you do not want to import.") + _, _ = fmt.Fprintln(w, "# You can change the path where the secrets are stored for the variables you want to keep.") + _, _ = fmt.Fprintln(w, "# For example, you can group variables in a directory.") + _, _ = fmt.Fprintln(w) + _, _ = fmt.Fprintln(w, "# When everything is to your liking, you can save the file and exit the editor to continue.") +} + +func buildMap(input io.Reader) (map[string]string, error) { + scanner := bufio.NewScanner(input) + locationsMap := make(map[string]string) + + i := 0 + for scanner.Scan() { + i++ + line := scanner.Text() + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "#") && line != "" { + split := strings.SplitN(line, "=>", 2) + if len(split) != 2 { + if strings.Contains(line, "=>") { + return nil, fmt.Errorf("could not parse prompt at line %d: '=>' should be followed by a secret path", i) + } + return nil, fmt.Errorf("could not parse prompt at line %d: missing '=>'", i) + } + locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) + } + } + return locationsMap, nil +} + +type envKeyToPath struct { + key string + tail []string +} + +// envkeysToPaths maps environment variable keys to paths on SecretHub in +// which to store the secrets the corresponding environment variables +// contain. +// See Test_envkeysToPaths for examples on how envkeysToPaths maps the keys +// to paths. +func envkeysToPaths(envkeys []string) map[string]string { + keys := make([]envKeyToPath, len(envkeys)) + for i, envkey := range envkeys { + keys[i] = envKeyToPath{ + key: envkey, + tail: strings.Split(strings.ToLower(envkey), "_"), + } + } + res, _ := splittedEnvKeysToPaths(keys) + return res +} + +func splittedEnvKeysToPaths(keys []envKeyToPath) (map[string]string, bool) { + byHeads := make(map[string][]envKeyToPath) + for _, key := range keys { + if len(key.tail) == 0 { + // If there's no tail, that means one key is completely equal to part of another key. + // e.g. STRIPE_API, STRIPE_API_KEY + // In this edge-case we create secrets "api" and "api-key" and we don't create a directory called "api". + res := make(map[string]string, len(keys)) + for _, key = range keys { + res[key.key] = strings.Join(key.tail, "-") + } + return res, true + } + byHeads[key.tail[0]] = append(byHeads[key.tail[0]], envKeyToPath{key: key.key, tail: key.tail[1:]}) + } + + res := make(map[string]string) + for head, keys := range byHeads { + if len(keys) > 1 { + paths, oneDir := splittedEnvKeysToPaths(keys) + for key, path := range paths { + if oneDir { + res[key] = head + if path != "" { + // If all secrets starting with this prefix are already in a single directory, + // we don't want to put that directory into another directory, but instead use + // a longer name for that directory. For example, we don't want MY_APP prefix + // to convert to my/app/ directories, but to one single my-app directory. + res[key] += "-" + path + } + } else { + res[key] = secretpath.Join(head, path) + } + } + } else { + res[keys[0].key] = strings.Join(append([]string{head}, keys[0].tail...), "-") + } + } + return res, len(byHeads) == 1 +} diff --git a/internals/secrethub/import_dotenv_test.go b/internals/secrethub/import_dotenv_test.go new file mode 100644 index 00000000..8b54a21e --- /dev/null +++ b/internals/secrethub/import_dotenv_test.go @@ -0,0 +1,93 @@ +package secrethub + +import ( + "testing" + + "github.com/secrethub/secrethub-go/internals/assert" +) + +func Test_envkeysToPaths(t *testing.T) { + cases := map[string]struct { + envkeys []string + expected map[string]string + }{ + "single key": { + envkeys: []string{ + "STRIPE_API_KEY", + }, + expected: map[string]string{ + "STRIPE_API_KEY": "stripe-api-key", + }, + }, + "multiple different keys": { + envkeys: []string{ + "STRIPE_API_KEY", + "DB_USER", + }, + expected: map[string]string{ + "STRIPE_API_KEY": "stripe-api-key", + "DB_USER": "db-user", + }, + }, + "keys with common prefix": { + envkeys: []string{ + "STRIPE_API_KEY", + "DB_USER", + "DB_PASSWORD", + }, + expected: map[string]string{ + "STRIPE_API_KEY": "stripe-api-key", + "DB_USER": "db/user", + "DB_PASSWORD": "db/password", + }, + }, + "prefix with multiple underscores": { + envkeys: []string{ + "MY_APP_STRIPE_API_KEY", + "MY_APP_DB_PASSWORD", + }, + expected: map[string]string{ + "MY_APP_STRIPE_API_KEY": "my-app/stripe-api-key", + "MY_APP_DB_PASSWORD": "my-app/db-password", + }, + }, + "two levels of directories": { + envkeys: []string{ + "MY_APP_STRIPE_API_KEY", + "MY_APP_DB_USER", + "MY_APP_DB_PASSWORD", + }, + expected: map[string]string{ + "MY_APP_STRIPE_API_KEY": "my-app/stripe-api-key", + "MY_APP_DB_USER": "my-app/db/user", + "MY_APP_DB_PASSWORD": "my-app/db/password", + }, + }, + "key without underscores": { + envkeys: []string{ + "ENVIRONMENT", + }, + expected: map[string]string{ + "ENVIRONMENT": "environment", + }, + }, + "one key equal to the start of another": { + envkeys: []string{ + "STRIPE_API_KEY", + "STRIPE_API", + }, + expected: map[string]string{ + "STRIPE_API_KEY": "stripe-api-key", + "STRIPE_API": "stripe-api", + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + actual := envkeysToPaths(tc.envkeys) + + assert.Equal(t, actual, tc.expected) + }) + } +} diff --git a/internals/secrethub/tree.go b/internals/secrethub/tree.go index e026b27f..a0c76805 100644 --- a/internals/secrethub/tree.go +++ b/internals/secrethub/tree.go @@ -60,22 +60,26 @@ func (cmd *TreeCommand) Register(r command.Registerer) { // printTree recursively prints the tree's contents in a tree-like structure. func (cmd *TreeCommand) printTree(t *api.Tree, w io.Writer) { + printTree(t, w, cmd.fullPaths, cmd.path.Value(), !cmd.noReport, !cmd.noIndentation) +} +func printTree(t *api.Tree, w io.Writer, fullPaths bool, path string, showReport bool, indentation bool) { rootDirName := func() string { - if cmd.fullPaths { - return cmd.path.Value() + "/" + if fullPaths { + return path + "/" } return t.RootDir.Name + "/" }() name := colorizeByStatus(t.RootDir.Status, rootDirName) fmt.Fprintf(w, "%s\n", name) - if cmd.fullPaths { - cmd.printDirContentsRecursively(t.RootDir, "", w, cmd.path.Value()) - } else { - cmd.printDirContentsRecursively(t.RootDir, "", w, "") + prevPath := "" + if fullPaths { + prevPath = path } - if !cmd.noReport { + printDirContentsRecursively(t.RootDir, "", w, prevPath, fullPaths, indentation) + + if showReport { fmt.Fprintf(w, "\n%s, %s\n", pluralize("directory", "directories", t.DirCount()), @@ -86,14 +90,14 @@ func (cmd *TreeCommand) printTree(t *api.Tree, w io.Writer) { // printDirContentsRecursively is a recursive function that prints the directory's contents // in a tree-like structure, subdirs first followed by secrets. -func (cmd *TreeCommand) printDirContentsRecursively(dir *api.Dir, prefix string, w io.Writer, prevPath string) { +func printDirContentsRecursively(dir *api.Dir, prefix string, w io.Writer, prevPath string, fullPaths bool, indentation bool) { sort.Sort(api.SortDirByName(dir.SubDirs)) sort.Sort(api.SortSecretByName(dir.Secrets)) total := len(dir.SubDirs) + len(dir.Secrets) - if cmd.fullPaths { + if fullPaths { prevPath += "/" } else { prevPath = "" @@ -103,32 +107,32 @@ func (cmd *TreeCommand) printDirContentsRecursively(dir *api.Dir, prefix string, for _, sub := range dir.SubDirs { name := sub.Name - if cmd.fullPaths { + if fullPaths { name = prevPath + name } colorName := colorizeByStatus(sub.Status, name+"/") - if cmd.noIndentation { + if !indentation { fmt.Fprintf(w, "%s\n", colorName) - cmd.printDirContentsRecursively(sub, prefix, w, name) + printDirContentsRecursively(sub, prefix, w, name, fullPaths, indentation) } else if i == total-1 { fmt.Fprintf(w, "%s└── %s\n", prefix, colorName) - cmd.printDirContentsRecursively(sub, prefix+" ", w, name) + printDirContentsRecursively(sub, prefix+" ", w, name, fullPaths, indentation) } else { fmt.Fprintf(w, "%s├── %s\n", prefix, colorName) - cmd.printDirContentsRecursively(sub, prefix+"│ ", w, name) + printDirContentsRecursively(sub, prefix+"│ ", w, name, fullPaths, indentation) } i++ } for _, secret := range dir.Secrets { name := secret.Name - if cmd.fullPaths { + if fullPaths { name = prevPath + name } colorName := colorizeByStatus(secret.Status, name) - if cmd.noIndentation { + if !indentation { fmt.Fprintf(w, "%s\n", colorName) } else if i == total-1 { fmt.Fprintf(w, "%s└── %s\n", prefix, colorName)