Skip to content
This repository has been archived by the owner on Feb 16, 2023. It is now read-only.

Commit

Permalink
Merge pull request #299 from secrethub/feature/auto-splat
Browse files Browse the repository at this point in the history
Map all secrets from directory to environment variables
  • Loading branch information
SimonBarendse authored Jul 21, 2020
2 parents cbfcae4 + 722cb64 commit 56e1e99
Show file tree
Hide file tree
Showing 7 changed files with 391 additions and 6 deletions.
2 changes: 1 addition & 1 deletion internals/secrethub/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 2 additions & 2 deletions internals/secrethub/env_ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down
2 changes: 1 addition & 1 deletion internals/secrethub/env_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down
86 changes: 85 additions & 1 deletion internals/secrethub/env_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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)
}

Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down
142 changes: 142 additions & 0 deletions internals/secrethub/env_source_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
})
}
}
2 changes: 1 addition & 1 deletion internals/secrethub/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down
Loading

0 comments on commit 56e1e99

Please sign in to comment.