From 8fd20d5d2ab0f7b5342bd61d34ac0b492efb2279 Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Thu, 20 Aug 2020 15:46:11 +0300 Subject: [PATCH 01/39] working command, without interactive flag --- go.mod | 3 +- go.sum | 16 +------ internals/secrethub/app.go | 1 + internals/secrethub/import.go | 26 +++++++++++ internals/secrethub/import_dot_env.go | 63 +++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 internals/secrethub/import.go create mode 100644 internals/secrethub/import_dot_env.go diff --git a/go.mod b/go.mod index 227fefe6..47dfa588 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,9 @@ 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/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..0ae88f85 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= 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_dot_env.go b/internals/secrethub/import_dot_env.go new file mode 100644 index 00000000..70b25a6b --- /dev/null +++ b/internals/secrethub/import_dot_env.go @@ -0,0 +1,63 @@ +package secrethub + +import ( + "fmt" + "github.com/joho/godotenv" + "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/secrethub/command" + "github.com/secrethub/secrethub-go/internals/api" +) + +// ImportDotEnvCommand handles the migration of secrets from .env files to SecretHub. +type ImportDotEnvCommand struct { + io ui.IO + path api.DirPath + interactive bool + dotenv 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("dot-env", "Import secrets from .env files.") + clause.Arg("dir-path", "The path to where to write the new secrets").PlaceHolder(dirPathPlaceHolder).SetValue(&cmd.path) + clause.Flag("interactive", "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.dotenv) + + command.BindAction(clause, cmd.Run) +} + +func (cmd *ImportDotEnvCommand) Run() error { + var envVar map[string]string + envVar, err := godotenv.Read() + if err != nil { + return err + } + + client, err := cmd.newClient() + if err != nil { + return err + } + + for key, value := range envVar { + _, err := client.Secrets().Write(cmd.path.Value()+"/"+key, []byte(value)) + 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 + } + + return nil +} From 76b736d37c5d8a019d65d8b764a1858600d536c1 Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Thu, 20 Aug 2020 17:06:41 +0300 Subject: [PATCH 02/39] working interactive flag --- internals/secrethub/import_dot_env.go | 33 ++++++++++-- internals/secrethub/interactive_helper.go | 63 +++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 internals/secrethub/interactive_helper.go diff --git a/internals/secrethub/import_dot_env.go b/internals/secrethub/import_dot_env.go index 70b25a6b..13549234 100644 --- a/internals/secrethub/import_dot_env.go +++ b/internals/secrethub/import_dot_env.go @@ -2,6 +2,7 @@ package secrethub import ( "fmt" + "github.com/joho/godotenv" "github.com/secrethub/secrethub-cli/internals/cli/ui" "github.com/secrethub/secrethub-cli/internals/secrethub/command" @@ -14,6 +15,7 @@ type ImportDotEnvCommand struct { path api.DirPath interactive bool dotenv string + editor string newClient newClientFunc } @@ -29,9 +31,9 @@ func NewImportDotEnvCommand(io ui.IO, newClient newClientFunc) *ImportDotEnvComm func (cmd *ImportDotEnvCommand) Register(r command.Registerer) { clause := r.Command("dot-env", "Import secrets from .env files.") clause.Arg("dir-path", "The path to where to write the new secrets").PlaceHolder(dirPathPlaceHolder).SetValue(&cmd.path) - clause.Flag("interactive", "Edit the paths to where the secrets should be written.").Short('i').BoolVar(&cmd.interactive) + 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.dotenv) - + clause.Flag("editor", "The editor where you will define your secret paths. Only available in the interactive mode.").Default("nano").HintOptions("vim", "nano").StringVar(&cmd.editor) command.BindAction(clause, cmd.Run) } @@ -47,13 +49,26 @@ func (cmd *ImportDotEnvCommand) Run() error { return err } - for key, value := range envVar { - _, err := client.Secrets().Write(cmd.path.Value()+"/"+key, []byte(value)) + if cmd.interactive { + locationsMap, err := openEditor(cmd.editor, cmd.path.Value(), getMapKeys(envVar)) if err != nil { return err } - } + for key, value := range envVar { + _, err := client.Secrets().Write(locationsMap[key], []byte(value)) + if err != nil { + return err + } + } + } else { + for key, value := range envVar { + _, err := client.Secrets().Write(cmd.path.Value()+"/"+key, []byte(value)) + 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 @@ -61,3 +76,11 @@ func (cmd *ImportDotEnvCommand) Run() error { return nil } + +func getMapKeys(stringMap map[string]string) []string { + keys := make([]string, 0, len(stringMap)) + for k := range stringMap { + keys = append(keys, k) + } + return keys +} diff --git a/internals/secrethub/interactive_helper.go b/internals/secrethub/interactive_helper.go new file mode 100644 index 00000000..e7146029 --- /dev/null +++ b/internals/secrethub/interactive_helper.go @@ -0,0 +1,63 @@ +package secrethub + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" +) + +func openEditor(editor, path string, secretPaths []string) (map[string]string, error) { + fpath := os.TempDir() + "secretPaths.txt" + f, err := os.Create(fpath) + if err != nil { + return nil, err + } + + _, err = f.WriteString(buildFile(path, secretPaths)) + if err != nil { + return nil, err + } + + cmd := exec.Command(editor, fpath) + + 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 + } + + reading, err := ioutil.ReadFile(fpath) + return buildMap(string(reading)), nil +} + +func buildFile(path string, secretPaths []string) string { + output := "Choose the paths to where your secrets will be written:\n" + for _, secretPath := range secretPaths { + output += fmt.Sprintf("%s%s%s%s%s\n", secretPath, " => ", + path, "/", strings.ToLower(secretPath)) + } + return output +} + +func buildMap(input string) map[string]string { + scanner := bufio.NewScanner(strings.NewReader(input)) + scanner.Scan() + locationsMap := make(map[string]string) + for scanner.Scan() { + line := scanner.Text() + split := strings.Split(line, "=>") + locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) + } + + return locationsMap +} From 2af5963bd9869e881e79a0f2d6d5495cdc7552c1 Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Thu, 20 Aug 2020 17:09:50 +0300 Subject: [PATCH 03/39] included error check --- internals/secrethub/interactive_helper.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internals/secrethub/interactive_helper.go b/internals/secrethub/interactive_helper.go index e7146029..cb066722 100644 --- a/internals/secrethub/interactive_helper.go +++ b/internals/secrethub/interactive_helper.go @@ -37,6 +37,10 @@ func openEditor(editor, path string, secretPaths []string) (map[string]string, e } reading, err := ioutil.ReadFile(fpath) + if err != nil { + return nil, err + } + return buildMap(string(reading)), nil } From d7e573f1072788d219429d8c155081bb73dfeac3 Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Thu, 20 Aug 2020 17:57:09 +0300 Subject: [PATCH 04/39] added the secrethub.env output and the overwrite checks --- internals/secrethub/import_dot_env.go | 64 +++++++++++++++++------ internals/secrethub/interactive_helper.go | 13 +++++ 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/internals/secrethub/import_dot_env.go b/internals/secrethub/import_dot_env.go index 13549234..24244d7c 100644 --- a/internals/secrethub/import_dot_env.go +++ b/internals/secrethub/import_dot_env.go @@ -2,6 +2,8 @@ package secrethub import ( "fmt" + "os" + "strings" "github.com/joho/godotenv" "github.com/secrethub/secrethub-cli/internals/cli/ui" @@ -29,16 +31,18 @@ func NewImportDotEnvCommand(io ui.IO, newClient newClientFunc) *ImportDotEnvComm // Register registers the command and its sub-commands on the provided Registerer. func (cmd *ImportDotEnvCommand) Register(r command.Registerer) { - clause := r.Command("dot-env", "Import secrets from .env files.") + clause := r.Command("dot-env", "Import secrets from `.env` files. Outputs a `secrethub.env` file, containing references to your secrets in SecretHub.") clause.Arg("dir-path", "The path to where to write the new secrets").PlaceHolder(dirPathPlaceHolder).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.dotenv) - clause.Flag("editor", "The editor where you will define your secret paths. Only available in the interactive mode.").Default("nano").HintOptions("vim", "nano").StringVar(&cmd.editor) + clause.Flag("editor", "The editor where you will define your secret paths. Only has effect in interactive mode.").Default("nano").HintOptions("vim", "nano").StringVar(&cmd.editor) command.BindAction(clause, cmd.Run) } func (cmd *ImportDotEnvCommand) Run() error { var envVar map[string]string + locationsMap := make(map[string]string) + envVar, err := godotenv.Read() if err != nil { return err @@ -50,37 +54,65 @@ func (cmd *ImportDotEnvCommand) Run() error { } if cmd.interactive { - locationsMap, err := openEditor(cmd.editor, cmd.path.Value(), getMapKeys(envVar)) + locationsMap, err = openEditor(cmd.editor, cmd.path.Value(), getMapKeys(envVar)) if err != nil { return err } + } else { + for key := range envVar { + locationsMap[key] = cmd.path.Value() + "/" + strings.ToLower(key) + } + } - for key, value := range envVar { - _, err := client.Secrets().Write(locationsMap[key], []byte(value)) - if err != nil { - return err - } + for key, value := range envVar { + exists, err := client.Secrets().Exists(locationsMap[key]) + if err != nil { + return err } - } else { - for key, value := range envVar { - _, err := client.Secrets().Write(cmd.path.Value()+"/"+key, []byte(value)) + if exists { + confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ + "This import process will overwrite this secret. Do you wish to contine?", locationsMap[key]), ui.DefaultNo) + if err != nil { return err } + + if !confirmed { + fmt.Fprintln(cmd.io.Output(), "Aborting.") + return nil + } + } + _, err = client.Secrets().Write(locationsMap[key], []byte(value)) + 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 } + if err = generateSecretHubEnv(locationsMap); err != nil { + return err + } + return nil } -func getMapKeys(stringMap map[string]string) []string { - keys := make([]string, 0, len(stringMap)) - for k := range stringMap { - keys = append(keys, k) +func generateSecretHubEnv(locationsMap map[string]string) error { + output := "# Now .env secrets exist in the following SecretHub locations:\n" + for key, value := range locationsMap { + output += fmt.Sprintf("%s=secrethub://%s\n", key, value) } - return keys + fpath := "secrethub.env" + f, err := os.Create(fpath) + if err != nil { + return err + } + _, err = f.WriteString(output) + if err != nil { + return err + } + return nil } diff --git a/internals/secrethub/interactive_helper.go b/internals/secrethub/interactive_helper.go index cb066722..ab925616 100644 --- a/internals/secrethub/interactive_helper.go +++ b/internals/secrethub/interactive_helper.go @@ -41,6 +41,11 @@ func openEditor(editor, path string, secretPaths []string) (map[string]string, e return nil, err } + err = os.Remove(fpath) + if err != nil { + return nil, err + } + return buildMap(string(reading)), nil } @@ -65,3 +70,11 @@ func buildMap(input string) map[string]string { return locationsMap } + +func getMapKeys(stringMap map[string]string) []string { + keys := make([]string, 0, len(stringMap)) + for k := range stringMap { + keys = append(keys, k) + } + return keys +} From 48a45d166e39a7bb7d762ae7df46edb2964ad61a Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Thu, 20 Aug 2020 18:00:16 +0300 Subject: [PATCH 05/39] fix typo --- internals/secrethub/import_dot_env.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/secrethub/import_dot_env.go b/internals/secrethub/import_dot_env.go index 24244d7c..0bce1663 100644 --- a/internals/secrethub/import_dot_env.go +++ b/internals/secrethub/import_dot_env.go @@ -71,7 +71,7 @@ func (cmd *ImportDotEnvCommand) Run() error { } if exists { confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ - "This import process will overwrite this secret. Do you wish to contine?", locationsMap[key]), ui.DefaultNo) + "This import process will overwrite this secret. Do you wish to continue?", locationsMap[key]), ui.DefaultNo) if err != nil { return err From 6cda0938efc96d080d7e373946987c9ccfb00ced Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Thu, 20 Aug 2020 20:43:14 +0300 Subject: [PATCH 06/39] some formatting and making the import operation transactional --- internals/secrethub/import_dot_env.go | 5 ++++- internals/secrethub/interactive_helper.go | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/internals/secrethub/import_dot_env.go b/internals/secrethub/import_dot_env.go index 0bce1663..5c359c66 100644 --- a/internals/secrethub/import_dot_env.go +++ b/internals/secrethub/import_dot_env.go @@ -64,7 +64,7 @@ func (cmd *ImportDotEnvCommand) Run() error { } } - for key, value := range envVar { + for key := range envVar { exists, err := client.Secrets().Exists(locationsMap[key]) if err != nil { return err @@ -82,6 +82,9 @@ func (cmd *ImportDotEnvCommand) Run() error { return nil } } + } + + for key, value := range envVar { _, err = client.Secrets().Write(locationsMap[key], []byte(value)) if err != nil { return err diff --git a/internals/secrethub/interactive_helper.go b/internals/secrethub/interactive_helper.go index ab925616..9565cedd 100644 --- a/internals/secrethub/interactive_helper.go +++ b/internals/secrethub/interactive_helper.go @@ -51,9 +51,10 @@ func openEditor(editor, path string, secretPaths []string) (map[string]string, e func buildFile(path string, secretPaths []string) string { output := "Choose the paths to where your secrets will be written:\n" + for _, secretPath := range secretPaths { - output += fmt.Sprintf("%s%s%s%s%s\n", secretPath, " => ", - path, "/", strings.ToLower(secretPath)) + output += fmt.Sprintf("%s => %s/%s\n", secretPath, + path, strings.ToLower(secretPath)) } return output } @@ -62,17 +63,18 @@ func buildMap(input string) map[string]string { scanner := bufio.NewScanner(strings.NewReader(input)) scanner.Scan() locationsMap := make(map[string]string) + for scanner.Scan() { line := scanner.Text() split := strings.Split(line, "=>") locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) } - return locationsMap } func getMapKeys(stringMap map[string]string) []string { keys := make([]string, 0, len(stringMap)) + for k := range stringMap { keys = append(keys, k) } From 860a5c9676f3d9a86c3ab7c8a81a074311346295 Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Fri, 21 Aug 2020 11:42:51 +0300 Subject: [PATCH 07/39] added the secrethub.env check and a force flag --- internals/secrethub/import_dot_env.go | 65 ++++++++++++++++++++------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/internals/secrethub/import_dot_env.go b/internals/secrethub/import_dot_env.go index 5c359c66..a9ed0c11 100644 --- a/internals/secrethub/import_dot_env.go +++ b/internals/secrethub/import_dot_env.go @@ -16,6 +16,7 @@ type ImportDotEnvCommand struct { io ui.IO path api.DirPath interactive bool + force bool dotenv string editor string newClient newClientFunc @@ -36,6 +37,7 @@ func (cmd *ImportDotEnvCommand) Register(r command.Registerer) { 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.dotenv) clause.Flag("editor", "The editor where you will define your secret paths. Only has effect in interactive mode.").Default("nano").HintOptions("vim", "nano").StringVar(&cmd.editor) + registerForceFlag(clause).BoolVar(&cmd.force) command.BindAction(clause, cmd.Run) } @@ -64,22 +66,24 @@ func (cmd *ImportDotEnvCommand) Run() error { } } - for key := range envVar { - exists, err := client.Secrets().Exists(locationsMap[key]) - if err != nil { - return err - } - if exists { - confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ - "This import process will overwrite this secret. Do you wish to continue?", locationsMap[key]), ui.DefaultNo) - + if !cmd.force { + for key := range envVar { + exists, err := client.Secrets().Exists(locationsMap[key]) if err != nil { return err } - - if !confirmed { - fmt.Fprintln(cmd.io.Output(), "Aborting.") - return nil + if exists { + confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ + "This import process will overwrite this secret. Do you wish to continue?", locationsMap[key]), ui.DefaultNo) + + if err != nil { + return err + } + + if !confirmed { + fmt.Fprintln(cmd.io.Output(), "Aborting.") + return nil + } } } } @@ -96,19 +100,36 @@ func (cmd *ImportDotEnvCommand) Run() error { return err } - if err = generateSecretHubEnv(locationsMap); err != nil { + if err = generateSecretHubEnv(locationsMap, cmd.force, cmd.io); err != nil { return err } return nil } -func generateSecretHubEnv(locationsMap map[string]string) error { +func generateSecretHubEnv(locationsMap map[string]string, force bool, io ui.IO) error { output := "# Now .env secrets exist in the following SecretHub locations:\n" for key, value := range locationsMap { output += fmt.Sprintf("%s=secrethub://%s\n", key, value) } fpath := "secrethub.env" + + fileExists := fileExists(fpath) + + if fileExists && !force { + confirmed, err := ui.AskYesNo(io, "A `secrethub.env` file already exists "+ + "at this location. This process will overwrite it. Do you wish to continue?", ui.DefaultNo) + + if err != nil { + return err + } + + if !confirmed { + fmt.Fprintln(io.Output(), "Aborting.") + return nil + } + } + f, err := os.Create(fpath) if err != nil { return err @@ -117,5 +138,19 @@ func generateSecretHubEnv(locationsMap map[string]string) error { if err != nil { return err } + + _, err = fmt.Fprintf(io.Output(), "The `secrethub.env` file has been successfully created.") + if err != nil { + return err + } + return nil } + +func fileExists(file string) bool { + info, err := os.Stat(file) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} From cccfeb30b11f6160918f52c13ee6423001cf5ecd Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Fri, 21 Aug 2020 11:44:45 +0300 Subject: [PATCH 08/39] silenced the warnings --- internals/secrethub/import_dot_env.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internals/secrethub/import_dot_env.go b/internals/secrethub/import_dot_env.go index a9ed0c11..42018851 100644 --- a/internals/secrethub/import_dot_env.go +++ b/internals/secrethub/import_dot_env.go @@ -81,7 +81,10 @@ func (cmd *ImportDotEnvCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Output(), "Aborting.") + _, err = fmt.Fprintln(cmd.io.Output(), "Aborting.") + if err != nil { + return err + } return nil } } @@ -125,7 +128,10 @@ func generateSecretHubEnv(locationsMap map[string]string, force bool, io ui.IO) } if !confirmed { - fmt.Fprintln(io.Output(), "Aborting.") + _, err = fmt.Fprintln(io.Output(), "Aborting.") + if err != nil { + return err + } return nil } } From 8d1545c9f1c1f26066da0ca22aed43f29994e1bd Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 11:56:53 +0100 Subject: [PATCH 09/39] Rename import dot-env to import dotenv This is also how the dotenv project is called, a.o. on npm: https://www.npmjs.com/package/dotenv --- internals/secrethub/{import_dot_env.go => import_dotenv.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename internals/secrethub/{import_dot_env.go => import_dotenv.go} (96%) diff --git a/internals/secrethub/import_dot_env.go b/internals/secrethub/import_dotenv.go similarity index 96% rename from internals/secrethub/import_dot_env.go rename to internals/secrethub/import_dotenv.go index 42018851..8f7fa5e1 100644 --- a/internals/secrethub/import_dot_env.go +++ b/internals/secrethub/import_dotenv.go @@ -32,7 +32,7 @@ func NewImportDotEnvCommand(io ui.IO, newClient newClientFunc) *ImportDotEnvComm // Register registers the command and its sub-commands on the provided Registerer. func (cmd *ImportDotEnvCommand) Register(r command.Registerer) { - clause := r.Command("dot-env", "Import secrets from `.env` files. Outputs a `secrethub.env` file, containing references to your secrets in SecretHub.") + 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", "The path to where to write the new secrets").PlaceHolder(dirPathPlaceHolder).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.dotenv) From 07cf5f17b12c33888fca47d7489e3b806ce784ae Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 12:01:16 +0100 Subject: [PATCH 10/39] Rephrase dir-path argument - Ommitting "the" shortens the description. A more concise help-text is easier to browse and read. - For the user, the effect of the command is more important than the implementation. Therefore, I've replaced "write" with "store". - I've changed "path" to "path to a directory on SecretHub" to be more specific and make sure there's no confusion with the local filesystem path or the secret paths themselves. --- internals/secrethub/import_dotenv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 8f7fa5e1..cef2588f 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -33,7 +33,7 @@ func NewImportDotEnvCommand(io ui.IO, newClient newClientFunc) *ImportDotEnvComm // 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", "The path to where to write the new secrets").PlaceHolder(dirPathPlaceHolder).SetValue(&cmd.path) + clause.Arg("dir-path", "path to a directory on SecretHub in which to store the imported secrets").PlaceHolder(dirPathPlaceHolder).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.dotenv) clause.Flag("editor", "The editor where you will define your secret paths. Only has effect in interactive mode.").Default("nano").HintOptions("vim", "nano").StringVar(&cmd.editor) From 69dc06534ea8b0873318adb41ac70692ec606727 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 12:07:48 +0100 Subject: [PATCH 11/39] Actually use dotenv file specified with the --env-file flag --- internals/secrethub/import_dotenv.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index cef2588f..ccab2201 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -17,7 +17,7 @@ type ImportDotEnvCommand struct { path api.DirPath interactive bool force bool - dotenv string + dotenvFile string editor string newClient newClientFunc } @@ -35,7 +35,7 @@ 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(dirPathPlaceHolder).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.dotenv) + clause.Flag("env-file", "The location of the .env file. Defaults to `.env`.").Default(".env").ExistingFileVar(&cmd.dotenvFile) clause.Flag("editor", "The editor where you will define your secret paths. Only has effect in interactive mode.").Default("nano").HintOptions("vim", "nano").StringVar(&cmd.editor) registerForceFlag(clause).BoolVar(&cmd.force) command.BindAction(clause, cmd.Run) @@ -45,7 +45,7 @@ func (cmd *ImportDotEnvCommand) Run() error { var envVar map[string]string locationsMap := make(map[string]string) - envVar, err := godotenv.Read() + envVar, err := godotenv.Read(cmd.dotenvFile) if err != nil { return err } From 53eee1ecfa4302af73701e744df526ec3e487a2b Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 12:42:54 +0100 Subject: [PATCH 12/39] Use system default editor for import interactive mode For those who want more control, they can use the default $EDITOR environment variable. --- internals/secrethub/import_dotenv.go | 4 +--- internals/secrethub/interactive_helper.go | 7 ++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index ccab2201..51acc2f2 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -18,7 +18,6 @@ type ImportDotEnvCommand struct { interactive bool force bool dotenvFile string - editor string newClient newClientFunc } @@ -36,7 +35,6 @@ func (cmd *ImportDotEnvCommand) Register(r command.Registerer) { clause.Arg("dir-path", "path to a directory on SecretHub in which to store the imported secrets").PlaceHolder(dirPathPlaceHolder).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) - clause.Flag("editor", "The editor where you will define your secret paths. Only has effect in interactive mode.").Default("nano").HintOptions("vim", "nano").StringVar(&cmd.editor) registerForceFlag(clause).BoolVar(&cmd.force) command.BindAction(clause, cmd.Run) } @@ -56,7 +54,7 @@ func (cmd *ImportDotEnvCommand) Run() error { } if cmd.interactive { - locationsMap, err = openEditor(cmd.editor, cmd.path.Value(), getMapKeys(envVar)) + locationsMap, err = openEditor(cmd.path.Value(), getMapKeys(envVar)) if err != nil { return err } diff --git a/internals/secrethub/interactive_helper.go b/internals/secrethub/interactive_helper.go index 9565cedd..ec623a4b 100644 --- a/internals/secrethub/interactive_helper.go +++ b/internals/secrethub/interactive_helper.go @@ -9,7 +9,7 @@ import ( "strings" ) -func openEditor(editor, path string, secretPaths []string) (map[string]string, error) { +func openEditor(path string, secretPaths []string) (map[string]string, error) { fpath := os.TempDir() + "secretPaths.txt" f, err := os.Create(fpath) if err != nil { @@ -21,6 +21,11 @@ func openEditor(editor, path string, secretPaths []string) (map[string]string, e return nil, err } + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "editor" + } + cmd := exec.Command(editor, fpath) cmd.Stdin = os.Stdin From 571b105b30b6fccc8e66ece87df2451cfe2e9bf8 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 12:49:29 +0100 Subject: [PATCH 13/39] Don't create a secrethub.env file on dotenv import - When no edits are made to the paths on SecretHub on which the secrets are imported, --secrets-dir can provision the secrets and no secrethub.env file is necessary. - The current generated secrethub.env file is incorrect (it uses secrethub://, while a secrethub.env file uses {{ path/to/secret }} - Sometimes, it may be preferred to use secret reference syntax in environment variables of the orchestrator instead. Therefore, we leave provisioning as a separate next step to the import for now. We may be adding further support for a full migration to this command later. --- internals/secrethub/import_dotenv.go | 56 ---------------------------- 1 file changed, 56 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 51acc2f2..d0196777 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -2,7 +2,6 @@ package secrethub import ( "fmt" - "os" "strings" "github.com/joho/godotenv" @@ -101,60 +100,5 @@ func (cmd *ImportDotEnvCommand) Run() error { return err } - if err = generateSecretHubEnv(locationsMap, cmd.force, cmd.io); err != nil { - return err - } - - return nil -} - -func generateSecretHubEnv(locationsMap map[string]string, force bool, io ui.IO) error { - output := "# Now .env secrets exist in the following SecretHub locations:\n" - for key, value := range locationsMap { - output += fmt.Sprintf("%s=secrethub://%s\n", key, value) - } - fpath := "secrethub.env" - - fileExists := fileExists(fpath) - - if fileExists && !force { - confirmed, err := ui.AskYesNo(io, "A `secrethub.env` file already exists "+ - "at this location. This process will overwrite it. Do you wish to continue?", ui.DefaultNo) - - if err != nil { - return err - } - - if !confirmed { - _, err = fmt.Fprintln(io.Output(), "Aborting.") - if err != nil { - return err - } - return nil - } - } - - f, err := os.Create(fpath) - if err != nil { - return err - } - _, err = f.WriteString(output) - if err != nil { - return err - } - - _, err = fmt.Fprintf(io.Output(), "The `secrethub.env` file has been successfully created.") - if err != nil { - return err - } - return nil } - -func fileExists(file string) bool { - info, err := os.Stat(file) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} From d47355ca9138b8fab1967b72874683c22bed8d0d Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 12:54:05 +0100 Subject: [PATCH 14/39] Move import helper functions to import command file --- internals/secrethub/import_dotenv.go | 81 +++++++++++++++++++++ internals/secrethub/interactive_helper.go | 87 ----------------------- 2 files changed, 81 insertions(+), 87 deletions(-) delete mode 100644 internals/secrethub/interactive_helper.go diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index d0196777..cd7258af 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -1,7 +1,11 @@ package secrethub import ( + "bufio" "fmt" + "io/ioutil" + "os" + "os/exec" "strings" "github.com/joho/godotenv" @@ -102,3 +106,80 @@ func (cmd *ImportDotEnvCommand) Run() error { return nil } + +func openEditor(path string, secretPaths []string) (map[string]string, error) { + fpath := os.TempDir() + "secretPaths.txt" + f, err := os.Create(fpath) + if err != nil { + return nil, err + } + + _, err = f.WriteString(buildFile(path, secretPaths)) + if err != nil { + return nil, err + } + + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "editor" + } + + cmd := exec.Command(editor, fpath) + + 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 + } + + reading, err := ioutil.ReadFile(fpath) + if err != nil { + return nil, err + } + + err = os.Remove(fpath) + if err != nil { + return nil, err + } + + return buildMap(string(reading)), nil +} + +func buildFile(path string, secretPaths []string) string { + output := "Choose the paths to where your secrets will be written:\n" + + for _, secretPath := range secretPaths { + output += fmt.Sprintf("%s => %s/%s\n", secretPath, + path, strings.ToLower(secretPath)) + } + return output +} + +func buildMap(input string) map[string]string { + scanner := bufio.NewScanner(strings.NewReader(input)) + scanner.Scan() + locationsMap := make(map[string]string) + + for scanner.Scan() { + line := scanner.Text() + split := strings.Split(line, "=>") + locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) + } + return locationsMap +} + +func getMapKeys(stringMap map[string]string) []string { + keys := make([]string, 0, len(stringMap)) + + for k := range stringMap { + keys = append(keys, k) + } + return keys +} diff --git a/internals/secrethub/interactive_helper.go b/internals/secrethub/interactive_helper.go deleted file mode 100644 index ec623a4b..00000000 --- a/internals/secrethub/interactive_helper.go +++ /dev/null @@ -1,87 +0,0 @@ -package secrethub - -import ( - "bufio" - "fmt" - "io/ioutil" - "os" - "os/exec" - "strings" -) - -func openEditor(path string, secretPaths []string) (map[string]string, error) { - fpath := os.TempDir() + "secretPaths.txt" - f, err := os.Create(fpath) - if err != nil { - return nil, err - } - - _, err = f.WriteString(buildFile(path, secretPaths)) - if err != nil { - return nil, err - } - - editor := os.Getenv("EDITOR") - if editor == "" { - editor = "editor" - } - - cmd := exec.Command(editor, fpath) - - 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 - } - - reading, err := ioutil.ReadFile(fpath) - if err != nil { - return nil, err - } - - err = os.Remove(fpath) - if err != nil { - return nil, err - } - - return buildMap(string(reading)), nil -} - -func buildFile(path string, secretPaths []string) string { - output := "Choose the paths to where your secrets will be written:\n" - - for _, secretPath := range secretPaths { - output += fmt.Sprintf("%s => %s/%s\n", secretPath, - path, strings.ToLower(secretPath)) - } - return output -} - -func buildMap(input string) map[string]string { - scanner := bufio.NewScanner(strings.NewReader(input)) - scanner.Scan() - locationsMap := make(map[string]string) - - for scanner.Scan() { - line := scanner.Text() - split := strings.Split(line, "=>") - locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) - } - return locationsMap -} - -func getMapKeys(stringMap map[string]string) []string { - keys := make([]string, 0, len(stringMap)) - - for k := range stringMap { - keys = append(keys, k) - } - return keys -} From feb297512d04b69d04f6063063bb858bceffda16 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 17:24:55 +0100 Subject: [PATCH 15/39] Extract mapping logic from openEditor function The openEditor function is now solely focussed on editing a given string value by the user. All logic concerning the mapping of environment variables to secret paths is extracted. --- internals/secrethub/import_dotenv.go | 29 +++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index cd7258af..4338bb87 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -43,9 +43,6 @@ func (cmd *ImportDotEnvCommand) Register(r command.Registerer) { } func (cmd *ImportDotEnvCommand) Run() error { - var envVar map[string]string - locationsMap := make(map[string]string) - envVar, err := godotenv.Read(cmd.dotenvFile) if err != nil { return err @@ -56,11 +53,13 @@ func (cmd *ImportDotEnvCommand) Run() error { return err } + locationsMap := make(map[string]string) if cmd.interactive { - locationsMap, err = openEditor(cmd.path.Value(), getMapKeys(envVar)) + mappingString, err := openEditor(buildFile(cmd.path.Value(), getMapKeys(envVar))) if err != nil { return err } + locationsMap = buildMap(mappingString) } else { for key := range envVar { locationsMap[key] = cmd.path.Value() + "/" + strings.ToLower(key) @@ -107,16 +106,20 @@ func (cmd *ImportDotEnvCommand) Run() error { return nil } -func openEditor(path string, secretPaths []string) (map[string]string, error) { +// openEditor opens an editor with the provided input as contents, +// lets the user edit those contents with the editor and returns +// the edited contents. +// Note that this functions is blocking for user input. +func openEditor(input string) (string, error) { fpath := os.TempDir() + "secretPaths.txt" f, err := os.Create(fpath) if err != nil { - return nil, err + return "", err } - _, err = f.WriteString(buildFile(path, secretPaths)) + _, err = f.WriteString(input) if err != nil { - return nil, err + return "", err } editor := os.Getenv("EDITOR") @@ -132,24 +135,24 @@ func openEditor(path string, secretPaths []string) (map[string]string, error) { err = cmd.Start() if err != nil { - return nil, err + return "", err } err = cmd.Wait() if err != nil { - return nil, err + return "", err } reading, err := ioutil.ReadFile(fpath) if err != nil { - return nil, err + return "", err } err = os.Remove(fpath) if err != nil { - return nil, err + return "", err } - return buildMap(string(reading)), nil + return string(reading), nil } func buildFile(path string, secretPaths []string) string { From ba52c26c4bf9dd46dd87e4bc4987817043a16f91 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 17:47:46 +0100 Subject: [PATCH 16/39] Fix missing / in openEditor function --- internals/secrethub/import_dotenv.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 4338bb87..49599c3f 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "strings" "github.com/joho/godotenv" @@ -111,7 +112,7 @@ func (cmd *ImportDotEnvCommand) Run() error { // the edited contents. // Note that this functions is blocking for user input. func openEditor(input string) (string, error) { - fpath := os.TempDir() + "secretPaths.txt" + fpath := filepath.Join(os.TempDir(), "secretPaths.txt") f, err := os.Create(fpath) if err != nil { return "", err From 85fb8d77a6a55ef208cf357dedf95cbb1032ea39 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 17:54:59 +0100 Subject: [PATCH 17/39] Use TempFile function to create temporary file This adds a random suffix to the file --- internals/secrethub/import_dotenv.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 49599c3f..0cabdb80 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "os" "os/exec" - "path/filepath" "strings" "github.com/joho/godotenv" @@ -112,13 +111,12 @@ func (cmd *ImportDotEnvCommand) Run() error { // the edited contents. // Note that this functions is blocking for user input. func openEditor(input string) (string, error) { - fpath := filepath.Join(os.TempDir(), "secretPaths.txt") - f, err := os.Create(fpath) + tmpFile, err := ioutil.TempFile(os.TempDir(), "secrethub-") if err != nil { return "", err } - _, err = f.WriteString(input) + _, err = tmpFile.WriteString(input) if err != nil { return "", err } @@ -128,7 +126,7 @@ func openEditor(input string) (string, error) { editor = "editor" } - cmd := exec.Command(editor, fpath) + cmd := exec.Command(editor, tmpFile.Name()) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -143,17 +141,17 @@ func openEditor(input string) (string, error) { return "", err } - reading, err := ioutil.ReadFile(fpath) + out, err := ioutil.ReadFile(tmpFile.Name()) if err != nil { return "", err } - err = os.Remove(fpath) + err = os.Remove(tmpFile.Name()) if err != nil { return "", err } - return string(reading), nil + return string(out), nil } func buildFile(path string, secretPaths []string) string { From 1e5d3658c35f94bd15bf6811dbdb1d1db4412321 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 17:58:00 +0100 Subject: [PATCH 18/39] Defer temporary file removal in openEditor function By defering the removal, it's also called when any error in the function causes the function to return before the old removal call was reached. --- internals/secrethub/import_dotenv.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 0cabdb80..fe0faa9f 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -115,6 +115,9 @@ func openEditor(input string) (string, error) { if err != nil { return "", err } + defer func() { + _ = os.Remove(tmpFile.Name()) + }() _, err = tmpFile.WriteString(input) if err != nil { @@ -146,11 +149,6 @@ func openEditor(input string) (string, error) { return "", err } - err = os.Remove(tmpFile.Name()) - if err != nil { - return "", err - } - return string(out), nil } From 1050df2adb924967e77c78f8040479803957cf65 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 19:02:36 +0100 Subject: [PATCH 19/39] De-duplicate default envvar to path mapping Previously, the mapping between envvar keys and paths to where the values of these envvars should be stored on SecretHub was defined twice: once for interactive mode and once for non-interactive mode. This commit refactors the code to use a single definition for both modes. This is intended to improve maintainability of the code and to ensure the default mapping of the interactive mode is the same as the mapping used in non-interactive mode. --- internals/secrethub/import_dotenv.go | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index fe0faa9f..22fca35f 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -54,16 +54,15 @@ func (cmd *ImportDotEnvCommand) Run() error { } locationsMap := make(map[string]string) + for key := range envVar { + locationsMap[key] = cmd.path.Value() + "/" + strings.ToLower(key) + } if cmd.interactive { - mappingString, err := openEditor(buildFile(cmd.path.Value(), getMapKeys(envVar))) + mappingString, err := openEditor(buildFile(locationsMap)) if err != nil { return err } locationsMap = buildMap(mappingString) - } else { - for key := range envVar { - locationsMap[key] = cmd.path.Value() + "/" + strings.ToLower(key) - } } if !cmd.force { @@ -152,12 +151,11 @@ func openEditor(input string) (string, error) { return string(out), nil } -func buildFile(path string, secretPaths []string) string { +func buildFile(locationsMap map[string]string) string { output := "Choose the paths to where your secrets will be written:\n" - for _, secretPath := range secretPaths { - output += fmt.Sprintf("%s => %s/%s\n", secretPath, - path, strings.ToLower(secretPath)) + for envVarKey, secretPath := range locationsMap { + output += fmt.Sprintf("%s => %s\n", envVarKey, secretPath) } return output } @@ -174,12 +172,3 @@ func buildMap(input string) map[string]string { } return locationsMap } - -func getMapKeys(stringMap map[string]string) []string { - keys := make([]string, 0, len(stringMap)) - - for k := range stringMap { - keys = append(keys, k) - } - return keys -} From 004c21d6f23cc5c8b715d87560bccf2a1dbc0466 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 19:08:01 +0100 Subject: [PATCH 20/39] Use secretpath.Join to join two paths on SecretHub --- internals/secrethub/import_dotenv.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 22fca35f..f46022a6 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -12,6 +12,7 @@ import ( "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" ) // ImportDotEnvCommand handles the migration of secrets from .env files to SecretHub. @@ -55,7 +56,7 @@ func (cmd *ImportDotEnvCommand) Run() error { locationsMap := make(map[string]string) for key := range envVar { - locationsMap[key] = cmd.path.Value() + "/" + strings.ToLower(key) + locationsMap[key] = secretpath.Join(cmd.path.Value(), strings.ToLower(key)) } if cmd.interactive { mappingString, err := openEditor(buildFile(locationsMap)) From bc24a12235014ceb01834dc56232d1094f86c2f5 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 19:08:27 +0100 Subject: [PATCH 21/39] Separate codeblock processing interactive mode It's now separate from the locations mapping --- internals/secrethub/import_dotenv.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index f46022a6..c90a3dc1 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -58,6 +58,7 @@ func (cmd *ImportDotEnvCommand) Run() error { for key := range envVar { locationsMap[key] = secretpath.Join(cmd.path.Value(), strings.ToLower(key)) } + if cmd.interactive { mappingString, err := openEditor(buildFile(locationsMap)) if err != nil { From 6206b64a86be92ea804ae5815cbc9a8c6b4400d0 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 19:13:04 +0100 Subject: [PATCH 22/39] Create parent directories of imported secrets if they dont exist already --- internals/secrethub/import_dotenv.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index c90a3dc1..9732c2df 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -93,7 +93,14 @@ func (cmd *ImportDotEnvCommand) Run() error { } for key, value := range envVar { - _, err = client.Secrets().Write(locationsMap[key], []byte(value)) + secretPath := locationsMap[key] + + 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(value)) if err != nil { return err } From b65f0083522d869d8e19a33855ea6090d194ba11 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 19:16:17 +0100 Subject: [PATCH 23/39] Account for omitted envvars Previously, when envvar keys would be removed, renamed or added in the interactive mode, the command would panic. Now, removed envvars are skipped and for unknown (renamed or added) envvars an error is returned. --- internals/secrethub/import_dotenv.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 9732c2df..dadb025b 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -68,14 +68,14 @@ func (cmd *ImportDotEnvCommand) Run() error { } if !cmd.force { - for key := range envVar { - exists, err := client.Secrets().Exists(locationsMap[key]) + for _, path := range locationsMap { + exists, err := client.Secrets().Exists(path) if err != nil { return err } if exists { confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ - "This import process will overwrite this secret. Do you wish to continue?", locationsMap[key]), ui.DefaultNo) + "This import process will overwrite this secret. Do you wish to continue?", path), ui.DefaultNo) if err != nil { return err @@ -92,15 +92,18 @@ func (cmd *ImportDotEnvCommand) Run() error { } } - for key, value := range envVar { - secretPath := locationsMap[key] + for envVarKey, secretPath := range locationsMap { + 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(value)) + _, err = client.Secrets().Write(secretPath, []byte(envVarValue)) if err != nil { return err } From e45423e5d5d7dbba8e0e5fcb386018678ae17068 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 20:31:00 +0100 Subject: [PATCH 24/39] Make storing imported secrets concurrent --- go.mod | 1 + go.sum | 1 + internals/secrethub/import_dotenv.go | 42 +++++++++++++++++++--------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 47dfa588..9df07dd8 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ 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/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 diff --git a/go.sum b/go.sum index 0ae88f85..d9e02ebc 100644 --- a/go.sum +++ b/go.sum @@ -255,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/import_dotenv.go b/internals/secrethub/import_dotenv.go index dadb025b..f329f1d1 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -2,17 +2,22 @@ package secrethub import ( "bufio" + "context" "fmt" "io/ioutil" "os" "os/exec" "strings" - "github.com/joho/godotenv" + "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. @@ -92,21 +97,32 @@ func (cmd *ImportDotEnvCommand) Run() error { } } + errGroup, _ := errgroup.WithContext(context.Background()) for envVarKey, secretPath := range locationsMap { - envVarValue, ok := envVar[envVarKey] - if !ok { - return fmt.Errorf("key not found in .env file: %s", envVarKey) - } + 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.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 - } + _, 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()) From 940bea8bff4b8dde55ab4ddc551fcfb49c3d31a7 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Thu, 19 Nov 2020 20:40:44 +0100 Subject: [PATCH 25/39] Make checking secret existence concurrent --- internals/secrethub/import_dotenv.go | 43 ++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index f329f1d1..37b934ab 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "strings" + "sync" "golang.org/x/sync/errgroup" @@ -73,26 +74,44 @@ func (cmd *ImportDotEnvCommand) Run() error { } if !cmd.force { + alreadyExist := make(map[string]struct{}) + var m sync.Mutex + errGroup, _ := errgroup.WithContext(context.Background()) for _, path := range locationsMap { - exists, err := client.Secrets().Exists(path) + 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 + } + + for path := range alreadyExist { + confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ + "This import process will overwrite this secret. Do you wish to continue?", path), ui.DefaultNo) + if err != nil { return err } - if exists { - confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ - "This import process will overwrite this secret. Do you wish to continue?", path), ui.DefaultNo) + if !confirmed { + _, err = fmt.Fprintln(cmd.io.Output(), "Aborting.") if err != nil { return err } - - if !confirmed { - _, err = fmt.Fprintln(cmd.io.Output(), "Aborting.") - if err != nil { - return err - } - return nil - } + return nil } } } From d6bdb1f0d5d5c27e81f46345f65660a8fa01703c Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Wed, 25 Nov 2020 15:49:47 +0100 Subject: [PATCH 26/39] Detect directory possiblities in import dotenv import dotenv now detects shared prefixes of environment variables and suggests to group these variables in a directory on SecretHub. --- internals/secrethub/import_dotenv.go | 74 +++++++++++++++++- internals/secrethub/import_dotenv_test.go | 93 +++++++++++++++++++++++ 2 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 internals/secrethub/import_dotenv_test.go diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 37b934ab..dbac2b29 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -60,9 +60,14 @@ func (cmd *ImportDotEnvCommand) Run() error { return err } - locationsMap := make(map[string]string) - for key := range envVar { - locationsMap[key] = secretpath.Join(cmd.path.Value(), strings.ToLower(key)) + 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 { @@ -219,3 +224,66 @@ func buildMap(input string) map[string]string { } return locationsMap } + +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) + }) + } +} From 86c4ed27e490fedd5aad5875ac6743f50d5fff7d Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Wed, 25 Nov 2020 15:55:50 +0100 Subject: [PATCH 27/39] Skip commented lines in dotenv import Before, one scanner.Scan() was hardcoded to skip the first line which explained the prompt. Now, the parser instead recognizes comment lines (starting with #) and skips those. This has the effect that the comment line can be safely removed by the user without affecting the result. And also the user can add more comment lines or comment out lines to ignore them. One more internal benefit is that we don't have a hardcoded dependency between the buildFile and buildMap functions sharing a dependency on how many lines the comment line is. --- internals/secrethub/import_dotenv.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index dbac2b29..9dd8529c 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -204,7 +204,7 @@ func openEditor(input string) (string, error) { } func buildFile(locationsMap map[string]string) string { - output := "Choose the paths to where your secrets will be written:\n" + output := "# Choose the paths to where your secrets will be written:\n" for envVarKey, secretPath := range locationsMap { output += fmt.Sprintf("%s => %s\n", envVarKey, secretPath) @@ -214,13 +214,14 @@ func buildFile(locationsMap map[string]string) string { func buildMap(input string) map[string]string { scanner := bufio.NewScanner(strings.NewReader(input)) - scanner.Scan() locationsMap := make(map[string]string) for scanner.Scan() { line := scanner.Text() - split := strings.Split(line, "=>") - locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) + if !strings.HasPrefix(strings.TrimSpace(line), "#") { + split := strings.Split(line, "=>") + locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) + } } return locationsMap } From 58d1d35e872dcef31d40795a5a648d412c943eca Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Wed, 25 Nov 2020 16:00:41 +0100 Subject: [PATCH 28/39] Include all after "=>" in dotenv import prompt as the secret path Before, the user could add an extra "=>" in the prompt and everything following that would be ignored. Now, all after the first "=>" is used as the path. The secret read will validate the secret path and throw an error in case the path contains "=>", as that's not allowed in a secret path. So, this commit changes an ignore and continue into an error case. --- internals/secrethub/import_dotenv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 9dd8529c..e5295836 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -219,7 +219,7 @@ func buildMap(input string) map[string]string { for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(strings.TrimSpace(line), "#") { - split := strings.Split(line, "=>") + split := strings.SplitN(line, "=>",2) locationsMap[strings.TrimSpace(split[0])] = strings.TrimSpace(split[1]) } } From b4073f6beb5bac97594ba17f1e7a0cf9eeb7aa8f Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Wed, 25 Nov 2020 16:09:10 +0100 Subject: [PATCH 29/39] Handle lines not containing "=>" and lines not containing a secret path --- internals/secrethub/import_dotenv.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index e5295836..aa19927b 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -75,7 +75,10 @@ func (cmd *ImportDotEnvCommand) Run() error { if err != nil { return err } - locationsMap = buildMap(mappingString) + locationsMap, err = buildMap(mappingString) + if err != nil { + return err + } } if !cmd.force { @@ -212,18 +215,26 @@ func buildFile(locationsMap map[string]string) string { return output } -func buildMap(input string) map[string]string { +func buildMap(input string) (map[string]string, error) { scanner := bufio.NewScanner(strings.NewReader(input)) locationsMap := make(map[string]string) + i := 0 for scanner.Scan() { + i++ line := scanner.Text() if !strings.HasPrefix(strings.TrimSpace(line), "#") { - split := strings.SplitN(line, "=>",2) + 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 + return locationsMap, nil } type envKeyToPath struct { From be48a95e364035a425e4cdbea74e81a6b203bb2a Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Wed, 25 Nov 2020 16:30:08 +0100 Subject: [PATCH 30/39] Use io.Reader and io.Writer in dotenv import helper functions This prevents loading all contents in memory at the same time. It removes an unnecessary switch from io.Reader to string and back to io.Reader. It gives the helper functions more control, for example, the buildFile function can now use a tabwriter instead (to be added in a following commit). --- internals/secrethub/import_dotenv.go | 67 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index aa19927b..955b4250 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -71,11 +72,17 @@ func (cmd *ImportDotEnvCommand) Run() error { } if cmd.interactive { - mappingString, err := openEditor(buildFile(locationsMap)) + editor, err := newEditor() if err != nil { return err } - locationsMap, err = buildMap(mappingString) + buildFile(locationsMap, editor) + edited, err := editor.openAndWait() + if err != nil { + return err + } + + locationsMap, err = buildMap(edited) if err != nil { return err } @@ -160,63 +167,65 @@ func (cmd *ImportDotEnvCommand) Run() error { return nil } -// openEditor opens an editor with the provided input as contents, -// lets the user edit those contents with the editor and returns -// the edited contents. -// Note that this functions is blocking for user input. -func openEditor(input string) (string, error) { +type editor struct { + file *os.File +} + +func newEditor() (editor, error) { tmpFile, err := ioutil.TempFile(os.TempDir(), "secrethub-") if err != nil { - return "", err + 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(tmpFile.Name()) + _ = os.Remove(e.file.Name()) }() - _, err = tmpFile.WriteString(input) - if err != nil { - return "", err - } - editor := os.Getenv("EDITOR") if editor == "" { editor = "editor" } - cmd := exec.Command(editor, tmpFile.Name()) + cmd := exec.Command(editor, e.file.Name()) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err = cmd.Start() + err := cmd.Start() if err != nil { - return "", err + return nil, err } err = cmd.Wait() if err != nil { - return "", err + return nil, err } - out, err := ioutil.ReadFile(tmpFile.Name()) - if err != nil { - return "", err - } + return os.Open(e.file.Name()) +} - return string(out), nil +func (e editor) Write(in []byte) (int, error) { + return e.file.Write(in) } -func buildFile(locationsMap map[string]string) string { - output := "# Choose the paths to where your secrets will be written:\n" +func buildFile(locationsMap map[string]string, w io.Writer) { + _, _ = fmt.Fprintln(w, "# Choose the paths to where your secrets will be written:") for envVarKey, secretPath := range locationsMap { - output += fmt.Sprintf("%s => %s\n", envVarKey, secretPath) + _, _ = fmt.Fprintf(w, "%s => %s\n", envVarKey, secretPath) } - return output } -func buildMap(input string) (map[string]string, error) { - scanner := bufio.NewScanner(strings.NewReader(input)) +func buildMap(input io.Reader) (map[string]string, error) { + scanner := bufio.NewScanner(input) locationsMap := make(map[string]string) i := 0 From b88b4b6932bc5bc9d6c7e25b0b0d5339d5c1a48c Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Wed, 25 Nov 2020 18:16:04 +0100 Subject: [PATCH 31/39] Align mappings in import dotenv prompt Now all "=>" and SecretHub paths are aligned, improving readability --- internals/secrethub/import_dotenv.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 955b4250..b9bafd67 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -10,6 +10,7 @@ import ( "os/exec" "strings" "sync" + "text/tabwriter" "golang.org/x/sync/errgroup" @@ -219,9 +220,12 @@ func (e editor) Write(in []byte) (int, error) { func buildFile(locationsMap map[string]string, w io.Writer) { _, _ = fmt.Fprintln(w, "# Choose the paths to where your secrets will be written:") + tabWriter := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0) + for envVarKey, secretPath := range locationsMap { - _, _ = fmt.Fprintf(w, "%s => %s\n", envVarKey, secretPath) + _, _ = fmt.Fprintf(tabWriter, "%s\t=>\t%s\n", envVarKey, secretPath) } + _ = tabWriter.Flush() } func buildMap(input io.Reader) (map[string]string, error) { From f647193daf16f5ee50d77e8b8a1c166780fd00d3 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Wed, 25 Nov 2020 18:27:33 +0100 Subject: [PATCH 32/39] Combine all secret overwrite prompts in a single prompt in dotenv import Instead of prompting the user to confirm to overwrite a single secret at once, instead list all secrets which will be overwritten immediately, and just ask once whether to overwrite all these secrets. --- internals/secrethub/import_dotenv.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index b9bafd67..6d1c3a32 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -114,9 +114,23 @@ func (cmd *ImportDotEnvCommand) Run() error { return err } - for path := range alreadyExist { - confirmed, err := ui.AskYesNo(cmd.io, fmt.Sprintf("A secret at location %s already exists. "+ - "This import process will overwrite this secret. Do you wish to continue?", path), ui.DefaultNo) + 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 From 768c8ab548d0bd2cc89a3586c8e248b72f4c3457 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Fri, 27 Nov 2020 12:32:30 +0100 Subject: [PATCH 33/39] Skip empty lines in dotenv import prompt --- internals/secrethub/import_dotenv.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 6d1c3a32..afc51ee7 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -250,7 +250,8 @@ func buildMap(input io.Reader) (map[string]string, error) { for scanner.Scan() { i++ line := scanner.Text() - if !strings.HasPrefix(strings.TrimSpace(line), "#") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "#") && line != "" { split := strings.SplitN(line, "=>", 2) if len(split) != 2 { if strings.Contains(line, "=>") { From 400e7927746b8c5473de86a1f195224c175bbdcf Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Fri, 27 Nov 2020 12:33:10 +0100 Subject: [PATCH 34/39] Add longer explanation comment to dotenv import prompt --- internals/secrethub/import_dotenv.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index afc51ee7..2d552781 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -232,14 +232,20 @@ func (e editor) Write(in []byte) (int, error) { } func buildFile(locationsMap map[string]string, w io.Writer) { - _, _ = fmt.Fprintln(w, "# Choose the paths to where your secrets will be written:") - 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 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) { From efe1d8b94ee8aa33dbd07555b39f93e6a991467c Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Fri, 27 Nov 2020 12:35:31 +0100 Subject: [PATCH 35/39] Update dotenv import comment to indicate '=>' --- internals/secrethub/import_dotenv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 2d552781..942a8427 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -240,7 +240,7 @@ func buildFile(locationsMap map[string]string, w io.Writer) { _ = tabWriter.Flush() _, _ = fmt.Fprintln(w) - _, _ = fmt.Fprintln(w, "# Environment variables on the left will be stored in SecretHub at the given path on the right.") + _, _ = 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.") From 48dcfb886afb0736adc927fd5ce112e3053348cf Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Fri, 27 Nov 2020 12:59:59 +0100 Subject: [PATCH 36/39] Print tree at the end of dotenv import --- internals/secrethub/import_dotenv.go | 14 +++++++++++- internals/secrethub/tree.go | 33 +++++++++++++++------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 942a8427..3d3228c4 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -174,11 +174,23 @@ func (cmd *ImportDotEnvCommand) Run() error { return err } - _, err = fmt.Fprintf(cmd.io.Output(), "Transfer complete! The secrets have been written to %s.\n", cmd.path.String()) + _, 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 } diff --git a/internals/secrethub/tree.go b/internals/secrethub/tree.go index e026b27f..7cdbc42b 100644 --- a/internals/secrethub/tree.go +++ b/internals/secrethub/tree.go @@ -60,22 +60,25 @@ 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()) + if fullPaths { + printDirContentsRecursively(t.RootDir, "", w, path, fullPaths, indentation) } else { - cmd.printDirContentsRecursively(t.RootDir, "", w, "") + printDirContentsRecursively(t.RootDir, "", w, "", fullPaths, indentation) } - if !cmd.noReport { + if showReport { fmt.Fprintf(w, "\n%s, %s\n", pluralize("directory", "directories", t.DirCount()), @@ -86,14 +89,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 +106,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) From 22c03f787a0ade30a56172a81df7d97455e09ffa Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Fri, 27 Nov 2020 13:02:29 +0100 Subject: [PATCH 37/39] De-duplicate call to printDirContentsRecursively --- internals/secrethub/tree.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internals/secrethub/tree.go b/internals/secrethub/tree.go index 7cdbc42b..a0c76805 100644 --- a/internals/secrethub/tree.go +++ b/internals/secrethub/tree.go @@ -73,11 +73,12 @@ func printTree(t *api.Tree, w io.Writer, fullPaths bool, path string, showReport name := colorizeByStatus(t.RootDir.Status, rootDirName) fmt.Fprintf(w, "%s\n", name) + prevPath := "" if fullPaths { - printDirContentsRecursively(t.RootDir, "", w, path, fullPaths, indentation) - } else { - printDirContentsRecursively(t.RootDir, "", w, "", fullPaths, indentation) + prevPath = path } + printDirContentsRecursively(t.RootDir, "", w, prevPath, fullPaths, indentation) + if showReport { fmt.Fprintf(w, "\n%s, %s\n", From f6371fcba9d44a764932684c5f1423169b56d605 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Fri, 27 Nov 2020 13:11:57 +0100 Subject: [PATCH 38/39] Make dotenv import dir-path argument required --- internals/secrethub/import_dotenv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 3d3228c4..744ca551 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -44,7 +44,7 @@ func NewImportDotEnvCommand(io ui.IO, newClient newClientFunc) *ImportDotEnvComm // 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(dirPathPlaceHolder).SetValue(&cmd.path) + clause.Arg("dir-path", "path to a directory on SecretHub in which to store the imported secrets").PlaceHolder(dirPathPlaceHolder).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) From 485780c623cad83f723ad82a90d56e05aae17098 Mon Sep 17 00:00:00 2001 From: Simon Barendse Date: Fri, 27 Nov 2020 13:12:23 +0100 Subject: [PATCH 39/39] Change dotenv import dir-path placeholder to indicate repopath is allowed A path to the root of a repository is also an acceptable argument for the dir-path in dotenv import. --- internals/secrethub/import_dotenv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/secrethub/import_dotenv.go b/internals/secrethub/import_dotenv.go index 744ca551..a7026a05 100644 --- a/internals/secrethub/import_dotenv.go +++ b/internals/secrethub/import_dotenv.go @@ -44,7 +44,7 @@ func NewImportDotEnvCommand(io ui.IO, newClient newClientFunc) *ImportDotEnvComm // 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(dirPathPlaceHolder).Required().SetValue(&cmd.path) + 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)