diff --git a/README.md b/README.md
index bba33652..03311024 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
@@ -21,10 +21,6 @@ The SecretHub CLI provides the command-line interface to interact with the Secre
> [SecretHub][secrethub] is a secrets management tool that works for every engineer. Securely provision passwords and keys throughout your entire stack with just a few lines of code.
-## Get started
-
-Follow the [Getting Started Guide][getting-started] to quickly get up and running with SecretHub :rocket:
-
## Usage
Below you can find a selection of some of the most-used SecretHub commands. Run `secrethub --help` or the [CLI reference docs][cli-reference-docs] for a complete list of all commands.
diff --git a/go.mod b/go.mod
index 227fefe6..2bcb5bd4 100644
--- a/go.mod
+++ b/go.mod
@@ -17,14 +17,12 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pkg/errors v0.9.1 // indirect
github.com/secrethub/demo-app v0.1.0
- github.com/secrethub/secrethub-go v0.30.0
+ github.com/secrethub/secrethub-go v0.31.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..3c09732b 100644
--- a/go.sum
+++ b/go.sum
@@ -178,6 +178,8 @@ github.com/secrethub/secrethub-go v0.29.1-0.20200707154958-5e5602145597 h1:uC9OD
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/secrethub/secrethub-go v0.31.0 h1:0KoG0KHBOa5knkvf3K0f6sKuPSQ5VGPXLD4ttC9Eul8=
+github.com/secrethub/secrethub-go v0.31.0/go.mod h1:ZIco8Y0G0Pi0Vb7pQROjvEKgSreZiRMLhAbzWUneUSQ=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
diff --git a/internals/cli/filemode/filemode.go b/internals/cli/filemode/filemode.go
index c293405d..656767e7 100644
--- a/internals/cli/filemode/filemode.go
+++ b/internals/cli/filemode/filemode.go
@@ -2,6 +2,7 @@
package filemode
import (
+ "fmt"
"os"
"strconv"
@@ -47,7 +48,7 @@ func (m *FileMode) Set(value string) error {
// String implements the flag.Value interface.
func (m FileMode) String() string {
- return string(m)
+ return fmt.Sprintf("%#o", m.FileMode().Perm())
}
// FileMode returns the file mode as an os.FileMode.
diff --git a/internals/cli/masker/stream.go b/internals/cli/masker/stream.go
index ce28f79a..d3c85de1 100644
--- a/internals/cli/masker/stream.go
+++ b/internals/cli/masker/stream.go
@@ -3,6 +3,7 @@ package masker
import (
"bytes"
"io"
+ "io/ioutil"
"sync"
"time"
)
@@ -64,16 +65,14 @@ func (s *stream) flush(n int) error {
if exists {
// Get any unprocessed bytes before this match to the destination.
- beforeMatch := s.buf.upToIndex(i)
-
- _, err := s.dest.Write(beforeMatch)
+ bytesBeforeMatch, err := s.buf.writeUpToIndex(s.dest, i)
if err != nil {
return err
}
// Only write the redaction text if there were bytes between this match and the previous match
// or this is the first flush for the buffer.
- if len(beforeMatch) > 0 || s.buf.currentIndex == 0 {
+ if bytesBeforeMatch > 0 || s.buf.currentIndex == 0 {
_, err = s.dest.Write([]byte(""))
if err != nil {
return err
@@ -81,14 +80,17 @@ func (s *stream) flush(n int) error {
}
// Drop all bytes until the end of the mask.
- _ = s.buf.upToIndex(i + int64(length))
+ _, err = s.buf.writeUpToIndex(ioutil.Discard, i+int64(length))
+ if err != nil {
+ return err
+ }
delete(s.matches, i)
}
}
// Write all bytes after the last match.
- _, err := s.dest.Write(s.buf.upToIndex(endIndex))
+ _, err := s.buf.writeUpToIndex(s.dest, endIndex)
if err != nil {
return err
}
@@ -109,16 +111,17 @@ func (b *indexedBuffer) write(p []byte) (n int, err error) {
return b.buffer.Write(p)
}
-// upToIndex pops and returns all bytes in the buffer up to the given index.
-// If all bytes up to this given index have already been returned previously, an empty slice is returned.
-func (b *indexedBuffer) upToIndex(index int64) []byte {
+// writeUpToIndex pops all bytes in the buffer up to the given index and writes them to the given writer.
+// The number of bytes written and any errors encountered are returned
+func (b *indexedBuffer) writeUpToIndex(w io.Writer, index int64) (int, error) {
b.mutex.Lock()
defer b.mutex.Unlock()
if index < b.currentIndex {
- return []byte{}
+ return 0, nil
}
n := int(index - b.currentIndex)
b.currentIndex = index
- return b.buffer.Next(n)
+ bufferSlice := b.buffer.Next(n)
+ return w.Write(bufferSlice)
}
diff --git a/internals/demo/init.go b/internals/demo/init.go
index 4f9e6c68..2efd5d02 100644
--- a/internals/demo/init.go
+++ b/internals/demo/init.go
@@ -64,8 +64,15 @@ func (cmd *InitCommand) Run() error {
}
_, err = client.Repos().Create(repoPath)
- if err == api.ErrRepoAlreadyExists && cmd.repo == "" {
- return fmt.Errorf("demo repo %s already exists, use --repo to specify another repo to use", repoPath)
+ if err == api.ErrRepoAlreadyExists {
+ demoRepo, err := cmd.isDemoRepo(client, repoPath)
+ if err != nil {
+ return err
+ }
+ if demoRepo {
+ return nil
+ }
+ return fmt.Errorf("repo %s already exists and is not a demo repo, use --repo to specify another repo to use", repoPath)
} else if err != nil {
return err
}
@@ -89,3 +96,34 @@ func (cmd *InitCommand) Run() error {
return nil
}
+
+// isDemoRepo checks whether the repo on the given path is a demo repository.
+// It returns true iff the repository contains exactly two secrets named username and password.
+func (cmd *InitCommand) isDemoRepo(client secrethub.ClientInterface, repoPath string) (bool, error) {
+ repo, err := client.Repos().Get(repoPath)
+ if err != nil {
+ return false, err
+ }
+ if repo.SecretCount != 2 {
+ return false, nil
+ }
+
+ usernamePath := secretpath.Join(repoPath, "username")
+ exists, err := client.Secrets().Exists(usernamePath)
+ if err != nil {
+ return false, err
+ }
+ if !exists {
+ return false, nil
+ }
+
+ passwordPath := secretpath.Join(repoPath, "password")
+ exists, err = client.Secrets().Exists(passwordPath)
+ if err != nil {
+ return false, err
+ }
+ if !exists {
+ return false, nil
+ }
+ return true, nil
+}
diff --git a/internals/secrethub/app.go b/internals/secrethub/app.go
index e95a9c8b..7409eb71 100644
--- a/internals/secrethub/app.go
+++ b/internals/secrethub/app.go
@@ -79,6 +79,8 @@ func NewApp() *App {
io := ui.NewUserIO()
store := NewCredentialConfig(io)
help := "The SecretHub command-line interface is a unified tool to manage your infrastructure secrets with SecretHub.\n\n" +
+ "If you do not yet have a SecretHub account, go here to create one:\n\n" +
+ " https://signup.secrethub.io/\n\n" +
"For a step-by-step introduction, check out:\n\n" +
" https://secrethub.io/docs/getting-started/\n\n" +
"To get help, see:\n\n" +
diff --git a/internals/secrethub/credential_store.go b/internals/secrethub/credential_store.go
index 09f2ccf2..d6ca2d3a 100644
--- a/internals/secrethub/credential_store.go
+++ b/internals/secrethub/credential_store.go
@@ -11,7 +11,7 @@ import (
// Errors
var (
- ErrCredentialNotExist = errMain.Code("credential_not_exist").Error("could not find credential file. Run `secrethub signup` to create an account.")
+ ErrCredentialNotExist = errMain.Code("credential_not_exist").Error("could not find credential file. Go to https://signup.secrethub.io/ to create an account or run `secrethub init` to use an already existing account on this machine.")
)
// CredentialConfig handles the configuration necessary for local credentials.
diff --git a/internals/secrethub/env.go b/internals/secrethub/env.go
index 845d50f3..5e653311 100644
--- a/internals/secrethub/env.go
+++ b/internals/secrethub/env.go
@@ -24,5 +24,5 @@ func (cmd *EnvCommand) Register(r command.Registerer) {
clause := r.Command("env", "[BETA] Manage environment variables.").Hidden()
clause.HelpLong("This command is hidden because it is still in beta. Future versions may break.")
NewEnvReadCommand(cmd.io, cmd.newClient).Register(clause)
- NewEnvListCommand(cmd.io).Register(clause)
+ NewEnvListCommand(cmd.io, cmd.newClient).Register(clause)
}
diff --git a/internals/secrethub/env_ls.go b/internals/secrethub/env_ls.go
index e4835701..c1cac212 100644
--- a/internals/secrethub/env_ls.go
+++ b/internals/secrethub/env_ls.go
@@ -14,10 +14,10 @@ type EnvListCommand struct {
}
// NewEnvListCommand creates a new EnvListCommand.
-func NewEnvListCommand(io ui.IO) *EnvListCommand {
+func NewEnvListCommand(io ui.IO, newClient newClientFunc) *EnvListCommand {
return &EnvListCommand{
io: io,
- environment: newEnvironment(io),
+ environment: newEnvironment(io, newClient),
}
}
diff --git a/internals/secrethub/env_read.go b/internals/secrethub/env_read.go
index 028b2f69..af216e25 100644
--- a/internals/secrethub/env_read.go
+++ b/internals/secrethub/env_read.go
@@ -20,7 +20,7 @@ func NewEnvReadCommand(io ui.IO, newClient newClientFunc) *EnvReadCommand {
return &EnvReadCommand{
io: io,
newClient: newClient,
- environment: newEnvironment(io),
+ environment: newEnvironment(io, newClient),
}
}
diff --git a/internals/secrethub/env_source.go b/internals/secrethub/env_source.go
index 607d3e6f..242d4f59 100644
--- a/internals/secrethub/env_source.go
+++ b/internals/secrethub/env_source.go
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"errors"
+ "fmt"
"io"
"io/ioutil"
"os"
@@ -22,8 +23,19 @@ import (
"gopkg.in/yaml.v2"
)
+type errNameCollision struct {
+ name string
+ firstPath string
+ secondPath string
+}
+
+func (e errNameCollision) Error() string {
+ return fmt.Sprintf("secrets at path %s and %s map to the same environment variable: %s. Rename one of the secrets or source them in a different way", e.firstPath, e.secondPath, e.name)
+}
+
type environment struct {
io ui.IO
+ newClient newClientFunc
osEnv []string
readFile func(filename string) ([]byte, error)
osStat func(filename string) (os.FileInfo, error)
@@ -32,12 +44,14 @@ type environment struct {
templateVars map[string]string
templateVersion string
dontPromptMissingTemplateVar bool
+ secretsDir string
secretsEnvDir string
}
-func newEnvironment(io ui.IO) *environment {
+func newEnvironment(io ui.IO, newClient newClientFunc) *environment {
return &environment{
io: io,
+ newClient: newClient,
osEnv: os.Environ(),
readFile: ioutil.ReadFile,
osStat: os.Stat,
@@ -53,6 +67,7 @@ func (env *environment) register(clause *cli.CommandClause) {
clause.Flag("var", "Define the value for a template variable with `VAR=VALUE`, e.g. --var env=prod").Short('v').StringMapVar(&env.templateVars)
clause.Flag("template-version", "The template syntax version to be used. The options are v1, v2, latest or auto to automatically detect the version.").Default("auto").StringVar(&env.templateVersion)
clause.Flag("no-prompt", "Do not prompt when a template variable is missing and return an error instead.").BoolVar(&env.dontPromptMissingTemplateVar)
+ clause.Flag("secrets-dir", "Recursively include all secrets from a directory. Environment variable names are derived from the path of the secret: `/` are replaced with `_` and the name is uppercased.").StringVar(&env.secretsDir)
clause.Flag("env", "The name of the environment prepared by the set command (default is `default`)").Default("default").Hidden().StringVar(&env.secretsEnvDir)
}
@@ -75,6 +90,12 @@ func (env *environment) env() (map[string]value, error) {
sources = append(sources, dirSource)
}
+ // --secrets-dir flag
+ if env.secretsDir != "" {
+ secretsDirEnv := newSecretsDirEnv(env.newClient, env.secretsDir)
+ sources = append(sources, secretsDirEnv)
+ }
+
//secrethub.env file
if env.envFile == "" {
_, err := env.osStat(defaultEnvFile)
@@ -173,6 +194,69 @@ func newSecretValue(path string) value {
return &secretValue{path: path}
}
+// secretsDirEnv sources environment variables from the directory specified with the --secrets-dir flag.
+type secretsDirEnv struct {
+ newClient newClientFunc
+ dirPath string
+}
+
+// env returns a map of environment variables containing all secrets from the specified path.
+// The variable names are the relative paths of their corresponding secrets in uppercase snake case.
+// An error is returned if two secret paths map to the same variable name.
+func (s *secretsDirEnv) env() (map[string]value, error) {
+ client, err := s.newClient()
+ if err != nil {
+ return nil, err
+ }
+
+ tree, err := client.Dirs().GetTree(s.dirPath, -1, false)
+ if err != nil {
+ return nil, err
+ }
+
+ paths := make(map[string]string, tree.SecretCount())
+ for id := range tree.Secrets {
+ secretPath, err := tree.AbsSecretPath(id)
+ if err != nil {
+ return nil, err
+ }
+ path := secretPath.String()
+
+ envVarName := s.envVarName(path)
+ if prevPath, found := paths[envVarName]; found {
+ return nil, errNameCollision{
+ name: envVarName,
+ firstPath: prevPath,
+ secondPath: path,
+ }
+ }
+ paths[envVarName] = path
+ }
+
+ result := make(map[string]value, len(paths))
+ for name, path := range paths {
+ result[name] = newSecretValue(path)
+ }
+ return result, nil
+}
+
+// envVarName returns the environment variable name corresponding to the secret on the specified path
+// by converting the relative path to uppercase snake case.
+func (s *secretsDirEnv) envVarName(path string) string {
+ envVarName := strings.TrimPrefix(path, s.dirPath)
+ envVarName = strings.TrimPrefix(envVarName, "/")
+ envVarName = strings.ReplaceAll(envVarName, "/", "_")
+ envVarName = strings.ToUpper(envVarName)
+ return envVarName
+}
+
+func newSecretsDirEnv(newClient newClientFunc, dirPath string) *secretsDirEnv {
+ return &secretsDirEnv{
+ newClient: newClient,
+ dirPath: dirPath,
+ }
+}
+
// EnvFlags defines environment variables sourced from command-line flags.
type EnvFlags map[string]string
diff --git a/internals/secrethub/env_source_test.go b/internals/secrethub/env_source_test.go
new file mode 100644
index 00000000..a6ecad93
--- /dev/null
+++ b/internals/secrethub/env_source_test.go
@@ -0,0 +1,142 @@
+package secrethub
+
+import (
+ "testing"
+
+ "github.com/secrethub/secrethub-go/internals/api"
+ "github.com/secrethub/secrethub-go/internals/api/uuid"
+ "github.com/secrethub/secrethub-go/internals/assert"
+ "github.com/secrethub/secrethub-go/pkg/secrethub"
+ "github.com/secrethub/secrethub-go/pkg/secrethub/fakeclient"
+)
+
+func TestSecretsDirEnv(t *testing.T) {
+ const dirPath = "namespace/repo"
+ rootDirUUID := uuid.New()
+ subDirUUID := uuid.New()
+ secretUUID1 := uuid.New()
+ secretUUID2 := uuid.New()
+
+ cases := map[string]struct {
+ newClient newClientFunc
+ expectedValues []string
+ err error
+ }{
+ "success": {
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ DirService: &fakeclient.DirService{
+ GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
+ return &api.Tree{
+ ParentPath: "namespace",
+ RootDir: &api.Dir{
+ DirID: rootDirUUID,
+ Name: "repo",
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ secretUUID1: {
+ SecretID: secretUUID1,
+ DirID: rootDirUUID,
+ Name: "foo",
+ },
+ },
+ }, nil
+ },
+ },
+ }, nil
+ },
+ expectedValues: []string{"FOO"},
+ },
+ "success secret in dir": {
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ DirService: &fakeclient.DirService{
+ GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
+ return &api.Tree{
+ ParentPath: "namespace",
+ RootDir: &api.Dir{
+ DirID: rootDirUUID,
+ Name: "repo",
+ },
+ Dirs: map[uuid.UUID]*api.Dir{
+ subDirUUID: {
+ DirID: subDirUUID,
+ ParentID: &rootDirUUID,
+ Name: "foo",
+ },
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ secretUUID1: {
+ SecretID: secretUUID1,
+ DirID: subDirUUID,
+ Name: "bar",
+ },
+ },
+ }, nil
+ },
+ },
+ }, nil
+ },
+ expectedValues: []string{"FOO_BAR"},
+ },
+ "name collision": {
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ DirService: &fakeclient.DirService{
+ GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
+ return &api.Tree{
+ ParentPath: "namespace",
+ RootDir: &api.Dir{
+ DirID: rootDirUUID,
+ Name: "repo",
+ },
+ Dirs: map[uuid.UUID]*api.Dir{
+ subDirUUID: {
+ DirID: subDirUUID,
+ ParentID: &rootDirUUID,
+ Name: "foo",
+ },
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ secretUUID1: {
+ SecretID: secretUUID1,
+ DirID: subDirUUID,
+ Name: "bar",
+ },
+ secretUUID2: {
+ SecretID: secretUUID2,
+ DirID: rootDirUUID,
+ Name: "foo_bar",
+ },
+ },
+ }, nil
+ },
+ },
+ }, nil
+ },
+ err: errNameCollision{
+ name: "FOO_BAR",
+ firstPath: "namespace/repo/foo/bar",
+ secondPath: "namespace/repo/foo_bar",
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ source := newSecretsDirEnv(tc.newClient, dirPath)
+ secrets, err := source.env()
+ if tc.err != nil {
+ assert.Equal(t, err, tc.err)
+ } else {
+ assert.OK(t, err)
+ assert.Equal(t, len(secrets), len(tc.expectedValues))
+ for _, name := range tc.expectedValues {
+ if _, ok := secrets[name]; !ok {
+ t.Errorf("expected but not found env var with name: %s", name)
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/internals/secrethub/init.go b/internals/secrethub/init.go
index dce0c9ac..82b9eb31 100644
--- a/internals/secrethub/init.go
+++ b/internals/secrethub/init.go
@@ -16,23 +16,24 @@ import (
// InitCommand configures the user's SecretHub account for use on this machine.
type InitCommand struct {
- backupCode string
- force bool
- io ui.IO
- newClient newClientFunc
- newClientWithoutCredentials func(credentials.Provider) (secrethub.ClientInterface, error)
- credentialStore CredentialConfig
- progressPrinter progress.Printer
+ backupCode string
+ setupCode string
+ force bool
+ io ui.IO
+ newUnauthenticatedClient newClientFunc
+ newClientWithCredentials func(credentials.Provider) (secrethub.ClientInterface, error)
+ credentialStore CredentialConfig
+ progressPrinter progress.Printer
}
// NewInitCommand creates a new InitCommand.
-func NewInitCommand(io ui.IO, newClient newClientFunc, newClientWithoutCredentials func(credentials.Provider) (secrethub.ClientInterface, error), credentialStore CredentialConfig) *InitCommand {
+func NewInitCommand(io ui.IO, newUnauthenticatedClient newClientFunc, newClientWithCredentials func(credentials.Provider) (secrethub.ClientInterface, error), credentialStore CredentialConfig) *InitCommand {
return &InitCommand{
- io: io,
- newClient: newClient,
- newClientWithoutCredentials: newClientWithoutCredentials,
- credentialStore: credentialStore,
- progressPrinter: progress.NewPrinter(io.Output(), 500*time.Millisecond),
+ io: io,
+ newUnauthenticatedClient: newUnauthenticatedClient,
+ newClientWithCredentials: newClientWithCredentials,
+ credentialStore: credentialStore,
+ progressPrinter: progress.NewPrinter(io.Output(), 500*time.Millisecond),
}
}
@@ -40,6 +41,7 @@ func NewInitCommand(io ui.IO, newClient newClientFunc, newClientWithoutCredentia
func (cmd *InitCommand) Register(r command.Registerer) {
clause := r.Command("init", "Initialize the SecretHub client for first use on this device.")
clause.Flag("backup-code", "The backup code used to restore an existing account to this device.").StringVar(&cmd.backupCode)
+ clause.Flag("setup-code", "The setup code used to configure the CLI to use an account created on the website.").StringVar(&cmd.setupCode)
registerForceFlag(clause).BoolVar(&cmd.force)
command.BindAction(clause, cmd.Run)
@@ -50,11 +52,16 @@ type InitMode int
const (
InitModeSignup InitMode = iota + 1
InitModeBackupCode
+ InitModeSetupCode
)
// Run configures the user's SecretHub account for use on this machine.
// If an account was already configured, the user is prompted for confirmation to overwrite it.
func (cmd *InitCommand) Run() error {
+ if cmd.setupCode != "" && cmd.backupCode != "" {
+ return ErrFlagsConflict("--backup-code and --setup-code")
+ }
+
credentialPath := cmd.credentialStore.ConfigDir().Credential().Path()
if cmd.credentialStore.ConfigDir().Credential().Exists() && !cmd.force {
@@ -76,7 +83,9 @@ func (cmd *InitCommand) Run() error {
}
var mode InitMode
- if cmd.backupCode != "" {
+ if cmd.setupCode != "" {
+ mode = InitModeSetupCode
+ } else if cmd.backupCode != "" {
mode = InitModeBackupCode
}
@@ -86,16 +95,18 @@ func (cmd *InitCommand) Run() error {
}
option, err := ui.Choose(cmd.io, "How do you want to initialize your SecretHub account on this device?",
[]string{
- "Signup for a new account",
+ "Sign up for a new account",
"Use a backup code to recover an existing account",
}, 3)
if err != nil {
return err
}
+ fmt.Fprintln(cmd.io.Output())
switch option {
case 0:
- mode = InitModeSignup
+ fmt.Fprintln(cmd.io.Output(), "Go to https://signup.secrethub.io/ and follow the steps to create an account and get it set up on this machine.")
+ return nil
case 1:
mode = InitModeBackupCode
}
@@ -105,12 +116,83 @@ func (cmd *InitCommand) Run() error {
case InitModeSignup:
signupCommand := SignUpCommand{
io: cmd.io,
- newClient: cmd.newClient,
+ newClient: cmd.newUnauthenticatedClient,
credentialStore: cmd.credentialStore,
progressPrinter: cmd.progressPrinter,
force: cmd.force,
}
return signupCommand.Run()
+ case InitModeSetupCode:
+ setupCode := cmd.setupCode
+
+ fmt.Fprintf(cmd.io.Output(), credentialCreationMessage, credentialPath)
+
+ // Only prompt for a passphrase when the user hasn't used --force.
+ // Otherwise, we assume the passphrase was intentionally not
+ // configured to output a plaintext credential.
+ var passphrase string
+ if !cmd.credentialStore.IsPassphraseSet() && !cmd.force {
+ var err error
+ passphrase, err = askCredentialPassphrase(cmd.io)
+ if err != nil {
+ return err
+ }
+ }
+
+ deviceName, err := promptForDeviceName(cmd.io)
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprint(cmd.io.Output(), "Setting up your account...")
+ cmd.progressPrinter.Start()
+
+ client, err := cmd.newClientWithCredentials(credentials.NewSetupCode(setupCode))
+ if err != nil {
+ cmd.progressPrinter.Stop()
+ return err
+ }
+
+ credential := credentials.CreateKey()
+ _, err = client.Credentials().Create(credential, deviceName)
+ if err != nil {
+ cmd.progressPrinter.Stop()
+ return err
+ }
+
+ err = writeNewCredential(credential, passphrase, cmd.credentialStore.ConfigDir().Credential())
+ if err != nil {
+ cmd.progressPrinter.Stop()
+ return err
+ }
+
+ client, err = cmd.newClientWithCredentials(credential)
+ if err != nil {
+ cmd.progressPrinter.Stop()
+ return err
+ }
+
+ me, err := client.Me().GetUser()
+ if err != nil {
+ cmd.progressPrinter.Stop()
+ return err
+ }
+
+ secretPath, err := createStartRepo(client, me.Username, me.FullName)
+ if err != nil {
+ cmd.progressPrinter.Stop()
+ return err
+ }
+ cmd.progressPrinter.Stop()
+ fmt.Fprint(cmd.io.Output(), "Created your account.\n\n")
+
+ err = createWorkspace(client, cmd.io, "", "", cmd.progressPrinter)
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(cmd.io.Output(), "Setup complete. To read your first secret, run:\n\n secrethub read %s\n\n", secretPath)
+ return nil
case InitModeBackupCode:
backupCode := cmd.backupCode
@@ -122,7 +204,7 @@ func (cmd *InitCommand) Run() error {
}
}
- client, err := cmd.newClientWithoutCredentials(credentials.UseBackupCode(backupCode))
+ client, err := cmd.newClientWithCredentials(credentials.UseBackupCode(backupCode))
if err != nil {
return err
}
@@ -146,19 +228,9 @@ func (cmd *InitCommand) Run() error {
return nil
}
- deviceName := ""
- question := "What is the name of this device?"
- hostName, err := os.Hostname()
- if err == nil {
- deviceName, err = ui.AskWithDefault(cmd.io, question, hostName)
- if err != nil {
- return err
- }
- } else {
- deviceName, err = ui.Ask(cmd.io, question)
- if err != nil {
- return err
- }
+ deviceName, err := promptForDeviceName(cmd.io)
+ if err != nil {
+ return err
}
// Only prompt for a passphrase when the user hasn't used --force.
@@ -167,7 +239,7 @@ func (cmd *InitCommand) Run() error {
var passphrase string
if !cmd.credentialStore.IsPassphraseSet() && !cmd.force {
var err error
- passphrase, err = ui.AskPassphrase(cmd.io, "Please enter a passphrase to protect your local credential (leave empty for no passphrase): ", "Enter the same passphrase again: ", 3)
+ passphrase, err = askCredentialPassphrase(cmd.io)
if err != nil {
return err
}
@@ -197,3 +269,21 @@ func (cmd *InitCommand) Run() error {
return errors.New("invalid option")
}
}
+
+func promptForDeviceName(io ui.IO) (string, error) {
+ deviceName := ""
+ question := "What is the name of this device?"
+ hostName, err := os.Hostname()
+ if err == nil {
+ deviceName, err = ui.AskWithDefault(io, question, hostName)
+ if err != nil {
+ return "", err
+ }
+ } else {
+ deviceName, err = ui.Ask(io, question)
+ if err != nil {
+ return "", err
+ }
+ }
+ return deviceName, nil
+}
diff --git a/internals/secrethub/run.go b/internals/secrethub/run.go
index 939726f4..db770e20 100644
--- a/internals/secrethub/run.go
+++ b/internals/secrethub/run.go
@@ -63,7 +63,7 @@ func NewRunCommand(io ui.IO, newClient newClientFunc) *RunCommand {
return &RunCommand{
io: io,
osEnv: os.Environ(),
- environment: newEnvironment(io),
+ environment: newEnvironment(io, newClient),
newClient: newClient,
}
}
diff --git a/internals/secrethub/run_test.go b/internals/secrethub/run_test.go
index fb6d4295..87ed68a0 100644
--- a/internals/secrethub/run_test.go
+++ b/internals/secrethub/run_test.go
@@ -10,6 +10,8 @@ import (
"strings"
"testing"
+ "github.com/secrethub/secrethub-go/internals/api/uuid"
+
"github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui"
"github.com/secrethub/secrethub-cli/internals/secrethub/tpl"
@@ -657,6 +659,11 @@ func osStatFunc(name string, err error) func(string) (os.FileInfo, error) {
}
func TestRunCommand_environment(t *testing.T) {
+ rootDirUUID := uuid.New()
+ secretUUID := uuid.New()
+
+ const secretPathFoo = "namespace/repo/foo"
+
cases := map[string]struct {
command RunCommand
expectedEnv []string
@@ -764,7 +771,159 @@ func TestRunCommand_environment(t *testing.T) {
expectedSecrets: []string{"bbb"},
expectedEnv: []string{"TEST=bbb"},
},
+ "env file has precedence over secrets-dir flag": {
+ command: RunCommand{
+ environment: &environment{
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ DirService: &fakeclient.DirService{
+ GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
+ return &api.Tree{
+ ParentPath: "namespace",
+ RootDir: &api.Dir{
+ DirID: rootDirUUID,
+ Name: "repo",
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ secretUUID: {
+ SecretID: secretUUID,
+ DirID: rootDirUUID,
+ Name: "foo",
+ },
+ },
+ }, nil
+ },
+ },
+ }, nil
+ },
+ secretsDir: "namespace/repo",
+ dontPromptMissingTemplateVar: true,
+ templateVersion: "2",
+ osEnv: []string{"FOO=bbb"},
+ osStat: osStatFunc("secrethub.env", nil),
+ readFile: readFileFunc("secrethub.env", "FOO= {{ other/secret/path }}"),
+ },
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ SecretService: &fakeclient.SecretService{
+ VersionService: &fakeclient.SecretVersionService{
+ GetWithDataFunc: func(path string) (*api.SecretVersion, error) {
+ if path == secretPathFoo {
+ return &api.SecretVersion{Data: []byte("aaa")}, nil
+ } else if path == "other/secret/path" {
+ return &api.SecretVersion{Data: []byte("bbb")}, nil
+ }
+ return nil, api.ErrSecretNotFound
+ },
+ },
+ },
+ }, nil
+ },
+ },
+ expectedSecrets: []string{"bbb"},
+ expectedEnv: []string{"FOO=bbb"},
+ },
+ "secrets-dir flag has precedence over os environment": {
+ command: RunCommand{
+ environment: &environment{
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ DirService: &fakeclient.DirService{
+ GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
+ return &api.Tree{
+ ParentPath: "namespace",
+ RootDir: &api.Dir{
+ DirID: rootDirUUID,
+ Name: "repo",
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ secretUUID: {
+ SecretID: secretUUID,
+ DirID: rootDirUUID,
+ Name: "foo",
+ },
+ },
+ }, nil
+ },
+ },
+ }, nil
+ },
+ secretsDir: "namespace/repo",
+ dontPromptMissingTemplateVar: true,
+ templateVersion: "2",
+ osEnv: []string{"FOO=bbb"},
+ osStat: osStatFunc("secrethub.env", os.ErrNotExist),
+ },
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ SecretService: &fakeclient.SecretService{
+ VersionService: &fakeclient.SecretVersionService{
+ GetWithDataFunc: func(path string) (*api.SecretVersion, error) {
+ if path == secretPathFoo {
+ return &api.SecretVersion{Data: []byte("aaa")}, nil
+ }
+ return nil, api.ErrSecretNotFound
+ },
+ },
+ },
+ }, nil
+ },
+ },
+ expectedSecrets: []string{"aaa"},
+ expectedEnv: []string{"FOO=aaa"},
+ },
// TODO Add test case for: envar flag has precedence over secret reference - requires refactoring of fakeclient
+ "secret reference has precedence over secrets-dir flag": {
+ command: RunCommand{
+ environment: &environment{
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ DirService: &fakeclient.DirService{
+ GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
+ return &api.Tree{
+ ParentPath: "namespace",
+ RootDir: &api.Dir{
+ DirID: rootDirUUID,
+ Name: "repo",
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ secretUUID: {
+ SecretID: secretUUID,
+ DirID: rootDirUUID,
+ Name: "foo",
+ },
+ },
+ }, nil
+ },
+ },
+ }, nil
+ },
+ secretsDir: "namespace/repo",
+ dontPromptMissingTemplateVar: true,
+ templateVersion: "2",
+ osEnv: []string{"FOO=secrethub://test/test/test"},
+ osStat: osStatFunc("secrethub.env", os.ErrNotExist),
+ },
+ newClient: func() (secrethub.ClientInterface, error) {
+ return fakeclient.Client{
+ SecretService: &fakeclient.SecretService{
+ VersionService: &fakeclient.SecretVersionService{
+ GetWithDataFunc: func(path string) (*api.SecretVersion, error) {
+ if path == "test/test/test" {
+ return &api.SecretVersion{Data: []byte("bbb")}, nil
+ } else if path == secretPathFoo {
+ return &api.SecretVersion{Data: []byte("aaa")}, nil
+ }
+ return nil, api.ErrSecretNotFound
+ },
+ },
+ },
+ }, nil
+ },
+ },
+ expectedSecrets: []string{"bbb"},
+ expectedEnv: []string{"FOO=bbb"},
+ },
"secret reference has precedence over .env file": {
command: RunCommand{
environment: &environment{
diff --git a/internals/secrethub/signup.go b/internals/secrethub/signup.go
index 2d8f3eca..b4bc22a3 100644
--- a/internals/secrethub/signup.go
+++ b/internals/secrethub/signup.go
@@ -4,6 +4,10 @@ import (
"fmt"
"time"
+ "github.com/secrethub/secrethub-go/pkg/secrethub/configdir"
+
+ "github.com/secrethub/secrethub-go/pkg/secrethub"
+
"github.com/secrethub/secrethub-cli/internals/cli/progress"
"github.com/secrethub/secrethub-cli/internals/cli/ui"
"github.com/secrethub/secrethub-cli/internals/secrethub/command"
@@ -18,6 +22,10 @@ var (
ErrLocalAccountFound = errMain.Code("local_account_found").Error("found a local account configuration. To overwrite it, run the same command with the --force or -f flag.")
)
+const credentialCreationMessage = "An account credential will be generated and stored at %s. " +
+ "Losing this credential means you lose the ability to decrypt your secrets. " +
+ "So keep it safe.\n"
+
// SignUpCommand signs up a new user and configures his account for use on this machine.
type SignUpCommand struct {
username string
@@ -44,7 +52,7 @@ func NewSignUpCommand(io ui.IO, newClient newClientFunc, credentialStore Credent
// Register registers the command, arguments and flags on the provided Registerer.
func (cmd *SignUpCommand) Register(r command.Registerer) {
- clause := r.Command("signup", "Create a free personal developer account.")
+ clause := r.Command("signup", "Create a free personal developer account.").Hidden()
clause.Flag("username", "The username you would like to use on SecretHub.").StringVar(&cmd.username)
clause.Flag("full-name", "Your full name.").StringVar(&cmd.fullName)
clause.Flag("email", "Your (work) email address we will use for all correspondence.").StringVar(&cmd.email)
@@ -116,13 +124,7 @@ func (cmd *SignUpCommand) Run() error {
}
}
- fmt.Fprintf(
- cmd.io.Output(),
- "An account credential will be generated and stored at %s. "+
- "Losing this credential means you lose the ability to decrypt your secrets. "+
- "So keep it safe.\n",
- credentialPath,
- )
+ fmt.Fprintf(cmd.io.Output(), credentialCreationMessage, credentialPath)
// Only prompt for a passphrase when the user hasn't used --force.
// Otherwise, we assume the passphrase was intentionally not
@@ -130,7 +132,7 @@ func (cmd *SignUpCommand) Run() error {
var passphrase string
if !cmd.credentialStore.IsPassphraseSet() && !cmd.force {
var err error
- passphrase, err = ui.AskPassphrase(cmd.io, "Please enter a passphrase to protect your local credential (leave empty for no passphrase): ", "Enter the same passphrase again: ", 3)
+ passphrase, err = askCredentialPassphrase(cmd.io)
if err != nil {
return err
}
@@ -150,82 +152,110 @@ func (cmd *SignUpCommand) Run() error {
return err
}
- exportKey := credential.Key
- if passphrase != "" {
- exportKey = exportKey.Passphrase(credentials.FromString(passphrase))
- }
-
- encodedCredential, err := credential.Export()
+ err = writeNewCredential(credential, passphrase, cmd.credentialStore.ConfigDir().Credential())
if err != nil {
cmd.progressPrinter.Stop()
return err
}
- err = cmd.credentialStore.ConfigDir().Credential().Write(encodedCredential)
+
+ secretPath, err := createStartRepo(client, cmd.username, cmd.fullName)
if err != nil {
cmd.progressPrinter.Stop()
return err
}
- // create a start repository and write a fist secret to it, so that
- // the user can start by reading their first secret.
- // This is intended to smoothen onboarding.
- repoPath := secretpath.Join(cmd.username, "start")
- _, err = client.Repos().Create(secretpath.Join(repoPath))
+ cmd.progressPrinter.Stop()
+ fmt.Fprint(cmd.io.Output(), "Created your account.\n\n")
+
+ err = createWorkspace(client, cmd.io, cmd.org, cmd.orgDescription, cmd.progressPrinter)
if err != nil {
- cmd.progressPrinter.Stop()
return err
}
+ fmt.Fprintf(cmd.io.Output(), "Setup complete. To read your first secret, run:\n\n secrethub read %s\n\n", secretPath)
+
+ return nil
+}
+
+// createStartRepo creates a start repository and writes a fist secret to it, so that
+// the user can start by reading their first secret. It returns the secret's path.
+// This is intended to smoothen onboarding.
+func createStartRepo(client secrethub.ClientInterface, username string, fullName string) (string, error) {
+ repoPath := secretpath.Join(username, "start")
+ _, err := client.Repos().Create(secretpath.Join(repoPath))
+ if err != nil {
+ return "", err
+ }
+
secretPath := secretpath.Join(repoPath, "hello")
- message := fmt.Sprintf("Welcome %s! This is your first secret. To write a new version of this secret, run:\n\n secrethub write %s", cmd.fullName, secretPath)
+ message := fmt.Sprintf("Welcome %s! This is your first secret. To write a new version of this secret, run:\n\n secrethub write %s", fullName, secretPath)
_, err = client.Secrets().Write(secretPath, []byte(message))
if err != nil {
- cmd.progressPrinter.Stop()
- return err
+ return "", err
}
+ return secretPath, nil
+}
- cmd.progressPrinter.Stop()
- fmt.Fprint(cmd.io.Output(), "Created your account.\n\n")
-
- createWorkspace := cmd.org != ""
- if !createWorkspace {
- createWorkspace, err = ui.AskYesNo(cmd.io, "Do you want to create a shared workspace for your team?", ui.DefaultYes)
+// createWorkspace creates a new org with the given name and description.
+func createWorkspace(client secrethub.ClientInterface, io ui.IO, org string, orgDescription string, progressPrinter progress.Printer) error {
+ if org == "" {
+ createWorkspace, err := ui.AskYesNo(io, "Do you want to create a shared workspace for your team?", ui.DefaultYes)
if err != nil {
return err
}
- fmt.Fprintln(cmd.io.Output())
+ fmt.Fprintln(io.Output())
if !createWorkspace {
- fmt.Fprint(cmd.io.Output(), "You can create a shared workspace later using `secrethub org init`.\n\n")
+ fmt.Fprint(io.Output(), "You can create a shared workspace later using `secrethub org init`.\n\n")
+ return nil
}
}
- if createWorkspace {
- if cmd.org == "" {
- cmd.org, err = ui.AskAndValidate(cmd.io, "Workspace name (e.g. your company name): ", 2, api.ValidateOrgName)
- if err != nil {
- return err
- }
- }
- if cmd.orgDescription == "" {
- cmd.orgDescription, err = ui.AskAndValidate(cmd.io, "A description (max 144 chars) for your team workspace so others will recognize it:\n", 2, api.ValidateOrgDescription)
- if err != nil {
- return err
- }
- }
- fmt.Fprint(cmd.io.Output(), "Creating your shared workspace...")
- cmd.progressPrinter.Start()
- _, err := client.Orgs().Create(cmd.org, cmd.orgDescription)
- cmd.progressPrinter.Stop()
- if err == api.ErrOrgAlreadyExists {
- fmt.Fprintf(cmd.io.Output(), "The workspace %s already exists. If it is your organization, ask a colleague to invite you to the workspace. You can also create a new one using `secrethub org init`.\n", cmd.org)
- } else if err != nil {
+ var err error
+ if org == "" {
+ org, err = ui.AskAndValidate(io, "Workspace name (e.g. your company name): ", 2, api.ValidateOrgName)
+ if err != nil {
+ return err
+ }
+ }
+ if orgDescription == "" {
+ orgDescription, err = ui.AskAndValidate(io, "A description (max 144 chars) for your team workspace so others will recognize it:\n", 2, api.ValidateOrgDescription)
+ if err != nil {
return err
- } else {
- fmt.Fprint(cmd.io.Output(), "Created your shared workspace.\n\n")
}
}
- fmt.Fprintf(cmd.io.Output(), "Setup complete. To read your first secret, run:\n\n secrethub read %s\n\n", secretPath)
+ fmt.Fprint(io.Output(), "Creating your shared workspace...")
+ progressPrinter.Start()
+
+ _, err = client.Orgs().Create(org, orgDescription)
+ progressPrinter.Stop()
+ if err == api.ErrOrgAlreadyExists {
+ fmt.Fprintf(io.Output(), "The workspace %s already exists. If it is your organization, ask a colleague to invite you to the workspace. You can also create a new one using `secrethub org init`.\n", org)
+ } else if err != nil {
+ return err
+ } else {
+ fmt.Fprint(io.Output(), "Created your shared workspace.\n\n")
+ }
return nil
}
+
+// writeCredential writes the given credential to the configuration directory.
+func writeNewCredential(credential *credentials.KeyCreator, passphrase string, credentialFile *configdir.CredentialFile) error {
+ exportKey := credential.Key
+ if passphrase != "" {
+ exportKey = exportKey.Passphrase(credentials.FromString(passphrase))
+ }
+
+ encodedCredential, err := credential.Export()
+ if err != nil {
+ return err
+ }
+
+ return credentialFile.Write(encodedCredential)
+}
+
+// askCredentialPassphrase prompts the user for a passphrase to protect the local credential.
+func askCredentialPassphrase(io ui.IO) (string, error) {
+ return ui.AskPassphrase(io, "Please enter a passphrase to protect your local credential (leave empty for no passphrase): ", "Enter the same passphrase again: ", 3)
+}
diff --git a/internals/secrethub/tree.go b/internals/secrethub/tree.go
index 3fd756c3..e026b27f 100644
--- a/internals/secrethub/tree.go
+++ b/internals/secrethub/tree.go
@@ -13,9 +13,12 @@ import (
// TreeCommand lists the contents of a directory at a given path in a tree-like format.
type TreeCommand struct {
- path api.DirPath
- io ui.IO
- newClient newClientFunc
+ path api.DirPath
+ io ui.IO
+ fullPaths bool
+ noIndentation bool
+ noReport bool
+ newClient newClientFunc
}
// NewTreeCommand creates a new TreeCommand.
@@ -38,7 +41,7 @@ func (cmd *TreeCommand) Run() error {
return err
}
- printTree(t, cmd.io.Output())
+ cmd.printTree(t, cmd.io.Output())
return nil
}
@@ -47,53 +50,90 @@ func (cmd *TreeCommand) Register(r command.Registerer) {
clause := r.Command("tree", "List contents of a directory in a tree-like format.")
clause.Arg("dir-path", "The path to to show contents for").Required().PlaceHolder(optionalDirPathPlaceHolder).SetValue(&cmd.path)
+ clause.Flag("full-paths", "Print the full path of each directory and secret.").Short('f').BoolVar(&cmd.fullPaths)
+ clause.Flag("no-indentation", "Don't print indentation lines.").Short('i').BoolVar(&cmd.noIndentation)
+ clause.Flag("no-report", "Turn off secret/directory count at end of tree listing.").BoolVar(&cmd.noReport)
+ clause.Flag("noreport", "Turn off secret/directory count at end of tree listing.").Hidden().BoolVar(&cmd.noReport)
+
command.BindAction(clause, cmd.Run)
}
// printTree recursively prints the tree's contents in a tree-like structure.
-func printTree(t *api.Tree, w io.Writer) {
- name := colorizeByStatus(t.RootDir.Status, t.RootDir.Name)
- fmt.Fprintf(w, "%s/\n", name)
-
- printDirContentsRecursively(t.RootDir, "", w)
+func (cmd *TreeCommand) printTree(t *api.Tree, w io.Writer) {
- fmt.Fprintf(w,
- "\n%s, %s\n",
- pluralize("directory", "directories", t.DirCount()),
- pluralize("secret", "secrets", t.SecretCount()),
- )
+ rootDirName := func() string {
+ if cmd.fullPaths {
+ return cmd.path.Value() + "/"
+ }
+ 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, "")
+ }
+ if !cmd.noReport {
+ fmt.Fprintf(w,
+ "\n%s, %s\n",
+ pluralize("directory", "directories", t.DirCount()),
+ pluralize("secret", "secrets", t.SecretCount()),
+ )
+ }
}
// printDirContentsRecursively is a recursive function that prints the directory's contents
// in a tree-like structure, subdirs first followed by secrets.
-func printDirContentsRecursively(dir *api.Dir, prefix string, w io.Writer) {
+func (cmd *TreeCommand) printDirContentsRecursively(dir *api.Dir, prefix string, w io.Writer, prevPath string) {
sort.Sort(api.SortDirByName(dir.SubDirs))
sort.Sort(api.SortSecretByName(dir.Secrets))
total := len(dir.SubDirs) + len(dir.Secrets)
+ if cmd.fullPaths {
+ prevPath += "/"
+ } else {
+ prevPath = ""
+ }
+
i := 0
for _, sub := range dir.SubDirs {
- name := colorizeByStatus(sub.Status, sub.Name)
- if i == total-1 {
- fmt.Fprintf(w, "%s└── %s/\n", prefix, name)
- printDirContentsRecursively(sub, prefix+" ", w)
+ name := sub.Name
+ if cmd.fullPaths {
+ name = prevPath + name
+ }
+ colorName := colorizeByStatus(sub.Status, name+"/")
+
+ if cmd.noIndentation {
+ fmt.Fprintf(w, "%s\n", colorName)
+ cmd.printDirContentsRecursively(sub, prefix, w, name)
+ } else if i == total-1 {
+ fmt.Fprintf(w, "%s└── %s\n", prefix, colorName)
+ cmd.printDirContentsRecursively(sub, prefix+" ", w, name)
} else {
- fmt.Fprintf(w, "%s├── %s/\n", prefix, name)
- printDirContentsRecursively(sub, prefix+"│ ", w)
+ fmt.Fprintf(w, "%s├── %s\n", prefix, colorName)
+ cmd.printDirContentsRecursively(sub, prefix+"│ ", w, name)
}
i++
}
for _, secret := range dir.Secrets {
- name := colorizeByStatus(secret.Status, secret.Name)
+ name := secret.Name
+ if cmd.fullPaths {
+ name = prevPath + name
+ }
+ colorName := colorizeByStatus(secret.Status, name)
- if i == total-1 {
- fmt.Fprintf(w, "%s└── %s\n", prefix, name)
+ if cmd.noIndentation {
+ fmt.Fprintf(w, "%s\n", colorName)
+ } else if i == total-1 {
+ fmt.Fprintf(w, "%s└── %s\n", prefix, colorName)
} else {
- fmt.Fprintf(w, "%s├── %s\n", prefix, name)
+ fmt.Fprintf(w, "%s├── %s\n", prefix, colorName)
}
i++
}
diff --git a/internals/secrethub/tree_test.go b/internals/secrethub/tree_test.go
new file mode 100644
index 00000000..eb857cb0
--- /dev/null
+++ b/internals/secrethub/tree_test.go
@@ -0,0 +1,239 @@
+package secrethub
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/fatih/color"
+ "github.com/secrethub/secrethub-cli/internals/cli/ui"
+ "github.com/secrethub/secrethub-go/internals/api"
+ "github.com/secrethub/secrethub-go/internals/api/uuid"
+ "github.com/secrethub/secrethub-go/internals/assert"
+ "github.com/secrethub/secrethub-go/pkg/secrethub"
+)
+
+func TestSimpleTree(t *testing.T) {
+ uuid0, _ := uuid.FromString("0")
+ uuid1, _ := uuid.FromString("1")
+ tree := &api.Tree{
+ RootDir: &api.Dir{
+ Name: "test/repo",
+ DirID: uuid0,
+ SubDirs: []*api.Dir{
+ {
+ Name: "secretFolder",
+ DirID: uuid1,
+ ParentID: &uuid0,
+ Secrets: []*api.Secret{
+ {Name: "found you"},
+ },
+ },
+ },
+ Secrets: []*api.Secret{
+ {Name: "mySecret"},
+ },
+ },
+ Dirs: map[uuid.UUID]*api.Dir{
+ uuid.New(): {
+ DirID: uuid0,
+ ParentID: &uuid0,
+ },
+ uuid.New(): {
+ Name: "secretFolder",
+ DirID: uuid1,
+ ParentID: &uuid0,
+ Secrets: []*api.Secret{
+ {Name: "found you"},
+ },
+ },
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ uuid.New(): {Name: "found you"},
+ uuid.New(): {Name: "mySecret"},
+ },
+ }
+ cases := map[string]struct {
+ cmd *TreeCommand
+ expectedOutput string
+ }{
+ "simple tree": {
+ cmd: &TreeCommand{
+ io: ui.NewUserIO(),
+ newClient: func() (secrethub.ClientInterface, error) {
+ return &secrethub.Client{}, nil
+ },
+ },
+ expectedOutput: "test/repo/\n" +
+ "├── secretFolder/\n" +
+ "│ └── found you\n" +
+ "└── mySecret\n\n" +
+ "1 directory, 2 secrets\n",
+ },
+ "full path": {
+ cmd: &TreeCommand{
+ path: "test/repo",
+ io: ui.NewUserIO(),
+ fullPaths: true,
+ newClient: func() (secrethub.ClientInterface, error) {
+ return &secrethub.Client{}, nil
+ },
+ },
+ expectedOutput: "test/repo/\n" +
+ "├── test/repo/secretFolder/\n" +
+ "│ └── test/repo/secretFolder/found you\n" +
+ "└── test/repo/mySecret\n\n" +
+ "1 directory, 2 secrets\n",
+ },
+ "no indent": {
+ cmd: &TreeCommand{
+ io: ui.NewUserIO(),
+ noIndentation: true,
+ newClient: func() (secrethub.ClientInterface, error) {
+ return &secrethub.Client{}, nil
+ },
+ },
+ expectedOutput: "test/repo/\n" +
+ "secretFolder/\n" +
+ "found you\n" +
+ "mySecret\n\n" +
+ "1 directory, 2 secrets\n",
+ },
+ "no report": {
+ cmd: &TreeCommand{
+ io: ui.NewUserIO(),
+ noReport: true,
+ newClient: func() (secrethub.ClientInterface, error) {
+ return &secrethub.Client{}, nil
+ },
+ },
+ expectedOutput: "test/repo/\n" +
+ "├── secretFolder/\n" +
+ "│ └── found you\n" +
+ "└── mySecret\n",
+ },
+ "all flags": {
+ cmd: &TreeCommand{
+ path: "test/repo",
+ io: ui.NewUserIO(),
+ fullPaths: true,
+ noIndentation: true,
+ noReport: true,
+ newClient: func() (secrethub.ClientInterface, error) {
+ return &secrethub.Client{}, nil
+ },
+ },
+ expectedOutput: "test/repo/\n" +
+ "test/repo/secretFolder/\n" +
+ "test/repo/secretFolder/found you\n" +
+ "test/repo/mySecret\n",
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ w := &bytes.Buffer{}
+ tc.cmd.printTree(tree, w)
+ assert.Equal(t, w.String(), tc.expectedOutput)
+ })
+ }
+}
+
+func TestTreeColoring(t *testing.T) {
+ color.NoColor = false
+ uuid0, _ := uuid.FromString("0")
+ uuid1, _ := uuid.FromString("1")
+ uuid2, _ := uuid.FromString("2")
+ tree := &api.Tree{
+ RootDir: &api.Dir{
+ Name: "test/repo",
+ Status: api.StatusFlagged,
+ DirID: uuid0,
+ SubDirs: []*api.Dir{
+ {
+ Name: "happy",
+ DirID: uuid1,
+ ParentID: &uuid0,
+ },
+ {
+ Name: "secretFolder",
+ DirID: uuid2,
+ ParentID: &uuid0,
+ Status: api.StatusFlagged,
+ Secrets: []*api.Secret{
+ {
+ Name: "found you",
+ Status: api.StatusFlagged,
+ },
+ },
+ },
+ },
+ Secrets: []*api.Secret{
+ {Name: "mySecret"},
+ },
+ },
+ Dirs: map[uuid.UUID]*api.Dir{
+ uuid.New(): {
+ DirID: uuid0,
+ ParentID: &uuid0,
+ },
+ uuid.New(): {
+ Name: "happy",
+ DirID: uuid1,
+ ParentID: &uuid0,
+ },
+ uuid.New(): {
+ Name: "secretFolder",
+ DirID: uuid2,
+ ParentID: &uuid0,
+ Secrets: []*api.Secret{
+ {Name: "found you"},
+ },
+ },
+ },
+ Secrets: map[uuid.UUID]*api.Secret{
+ uuid.New(): {Name: "found you"},
+ uuid.New(): {Name: "mySecret"},
+ },
+ }
+ cases := map[string]struct {
+ cmd *TreeCommand
+ expectedOutput string
+ }{
+ "simple tree": {
+ cmd: &TreeCommand{
+ newClient: func() (secrethub.ClientInterface, error) {
+ return &secrethub.Client{}, nil
+ },
+ },
+ expectedOutput: red.Sprint("test/repo/") + "\n" +
+ "├── happy/\n" +
+ "├── " + red.Sprint("secretFolder/") + "\n" +
+ "│ └── " + red.Sprint("found you") + "\n" +
+ "└── mySecret\n\n" +
+ "2 directories, 2 secrets\n",
+ },
+ "full path": {
+ cmd: &TreeCommand{
+ path: "test/repo",
+ fullPaths: true,
+ newClient: func() (secrethub.ClientInterface, error) {
+ return &secrethub.Client{}, nil
+ },
+ },
+ expectedOutput: red.Sprint("test/repo/") + "\n" +
+ "├── test/repo/happy/\n" +
+ "├── " + red.Sprint("test/repo/secretFolder/") + "\n" +
+ "│ └── " + red.Sprint("test/repo/secretFolder/found you") + "\n" +
+ "└── test/repo/mySecret\n\n" +
+ "2 directories, 2 secrets\n",
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ w := new(bytes.Buffer)
+ tc.cmd.printTree(tree, w)
+ assert.Equal(t, w.String(), tc.expectedOutput)
+ })
+ }
+}