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

Commit 56e1e99

Browse files
Merge pull request #299 from secrethub/feature/auto-splat
Map all secrets from directory to environment variables
2 parents cbfcae4 + 722cb64 commit 56e1e99

7 files changed

Lines changed: 391 additions & 6 deletions

File tree

internals/secrethub/env.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ func (cmd *EnvCommand) Register(r command.Registerer) {
2424
clause := r.Command("env", "[BETA] Manage environment variables.").Hidden()
2525
clause.HelpLong("This command is hidden because it is still in beta. Future versions may break.")
2626
NewEnvReadCommand(cmd.io, cmd.newClient).Register(clause)
27-
NewEnvListCommand(cmd.io).Register(clause)
27+
NewEnvListCommand(cmd.io, cmd.newClient).Register(clause)
2828
}

internals/secrethub/env_ls.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ type EnvListCommand struct {
1414
}
1515

1616
// NewEnvListCommand creates a new EnvListCommand.
17-
func NewEnvListCommand(io ui.IO) *EnvListCommand {
17+
func NewEnvListCommand(io ui.IO, newClient newClientFunc) *EnvListCommand {
1818
return &EnvListCommand{
1919
io: io,
20-
environment: newEnvironment(io),
20+
environment: newEnvironment(io, newClient),
2121
}
2222
}
2323

internals/secrethub/env_read.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func NewEnvReadCommand(io ui.IO, newClient newClientFunc) *EnvReadCommand {
2020
return &EnvReadCommand{
2121
io: io,
2222
newClient: newClient,
23-
environment: newEnvironment(io),
23+
environment: newEnvironment(io, newClient),
2424
}
2525
}
2626

internals/secrethub/env_source.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"bytes"
66
"errors"
7+
"fmt"
78
"io"
89
"io/ioutil"
910
"os"
@@ -22,8 +23,19 @@ import (
2223
"gopkg.in/yaml.v2"
2324
)
2425

26+
type errNameCollision struct {
27+
name string
28+
firstPath string
29+
secondPath string
30+
}
31+
32+
func (e errNameCollision) Error() string {
33+
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)
34+
}
35+
2536
type environment struct {
2637
io ui.IO
38+
newClient newClientFunc
2739
osEnv []string
2840
readFile func(filename string) ([]byte, error)
2941
osStat func(filename string) (os.FileInfo, error)
@@ -32,12 +44,14 @@ type environment struct {
3244
templateVars map[string]string
3345
templateVersion string
3446
dontPromptMissingTemplateVar bool
47+
secretsDir string
3548
secretsEnvDir string
3649
}
3750

38-
func newEnvironment(io ui.IO) *environment {
51+
func newEnvironment(io ui.IO, newClient newClientFunc) *environment {
3952
return &environment{
4053
io: io,
54+
newClient: newClient,
4155
osEnv: os.Environ(),
4256
readFile: ioutil.ReadFile,
4357
osStat: os.Stat,
@@ -53,6 +67,7 @@ func (env *environment) register(clause *cli.CommandClause) {
5367
clause.Flag("var", "Define the value for a template variable with `VAR=VALUE`, e.g. --var env=prod").Short('v').StringMapVar(&env.templateVars)
5468
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)
5569
clause.Flag("no-prompt", "Do not prompt when a template variable is missing and return an error instead.").BoolVar(&env.dontPromptMissingTemplateVar)
70+
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)
5671
clause.Flag("env", "The name of the environment prepared by the set command (default is `default`)").Default("default").Hidden().StringVar(&env.secretsEnvDir)
5772
}
5873

@@ -75,6 +90,12 @@ func (env *environment) env() (map[string]value, error) {
7590
sources = append(sources, dirSource)
7691
}
7792

93+
// --secrets-dir flag
94+
if env.secretsDir != "" {
95+
secretsDirEnv := newSecretsDirEnv(env.newClient, env.secretsDir)
96+
sources = append(sources, secretsDirEnv)
97+
}
98+
7899
//secrethub.env file
79100
if env.envFile == "" {
80101
_, err := env.osStat(defaultEnvFile)
@@ -173,6 +194,69 @@ func newSecretValue(path string) value {
173194
return &secretValue{path: path}
174195
}
175196

197+
// secretsDirEnv sources environment variables from the directory specified with the --secrets-dir flag.
198+
type secretsDirEnv struct {
199+
newClient newClientFunc
200+
dirPath string
201+
}
202+
203+
// env returns a map of environment variables containing all secrets from the specified path.
204+
// The variable names are the relative paths of their corresponding secrets in uppercase snake case.
205+
// An error is returned if two secret paths map to the same variable name.
206+
func (s *secretsDirEnv) env() (map[string]value, error) {
207+
client, err := s.newClient()
208+
if err != nil {
209+
return nil, err
210+
}
211+
212+
tree, err := client.Dirs().GetTree(s.dirPath, -1, false)
213+
if err != nil {
214+
return nil, err
215+
}
216+
217+
paths := make(map[string]string, tree.SecretCount())
218+
for id := range tree.Secrets {
219+
secretPath, err := tree.AbsSecretPath(id)
220+
if err != nil {
221+
return nil, err
222+
}
223+
path := secretPath.String()
224+
225+
envVarName := s.envVarName(path)
226+
if prevPath, found := paths[envVarName]; found {
227+
return nil, errNameCollision{
228+
name: envVarName,
229+
firstPath: prevPath,
230+
secondPath: path,
231+
}
232+
}
233+
paths[envVarName] = path
234+
}
235+
236+
result := make(map[string]value, len(paths))
237+
for name, path := range paths {
238+
result[name] = newSecretValue(path)
239+
}
240+
return result, nil
241+
}
242+
243+
// envVarName returns the environment variable name corresponding to the secret on the specified path
244+
// by converting the relative path to uppercase snake case.
245+
func (s *secretsDirEnv) envVarName(path string) string {
246+
envVarName := strings.TrimPrefix(path, s.dirPath)
247+
envVarName = strings.TrimPrefix(envVarName, "/")
248+
envVarName = strings.ReplaceAll(envVarName, "/", "_")
249+
envVarName = strings.ToUpper(envVarName)
250+
return envVarName
251+
}
252+
253+
func newSecretsDirEnv(newClient newClientFunc, dirPath string) *secretsDirEnv {
254+
return &secretsDirEnv{
255+
newClient: newClient,
256+
dirPath: dirPath,
257+
}
258+
}
259+
176260
// EnvFlags defines environment variables sourced from command-line flags.
177261
type EnvFlags map[string]string
178262

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package secrethub
2+
3+
import (
4+
"testing"
5+
6+
"github.com/secrethub/secrethub-go/internals/api"
7+
"github.com/secrethub/secrethub-go/internals/api/uuid"
8+
"github.com/secrethub/secrethub-go/internals/assert"
9+
"github.com/secrethub/secrethub-go/pkg/secrethub"
10+
"github.com/secrethub/secrethub-go/pkg/secrethub/fakeclient"
11+
)
12+
13+
func TestSecretsDirEnv(t *testing.T) {
14+
const dirPath = "namespace/repo"
15+
rootDirUUID := uuid.New()
16+
subDirUUID := uuid.New()
17+
secretUUID1 := uuid.New()
18+
secretUUID2 := uuid.New()
19+
20+
cases := map[string]struct {
21+
newClient newClientFunc
22+
expectedValues []string
23+
err error
24+
}{
25+
"success": {
26+
newClient: func() (secrethub.ClientInterface, error) {
27+
return fakeclient.Client{
28+
DirService: &fakeclient.DirService{
29+
GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
30+
return &api.Tree{
31+
ParentPath: "namespace",
32+
RootDir: &api.Dir{
33+
DirID: rootDirUUID,
34+
Name: "repo",
35+
},
36+
Secrets: map[uuid.UUID]*api.Secret{
37+
secretUUID1: {
38+
SecretID: secretUUID1,
39+
DirID: rootDirUUID,
40+
Name: "foo",
41+
},
42+
},
43+
}, nil
44+
},
45+
},
46+
}, nil
47+
},
48+
expectedValues: []string{"FOO"},
49+
},
50+
"success secret in dir": {
51+
newClient: func() (secrethub.ClientInterface, error) {
52+
return fakeclient.Client{
53+
DirService: &fakeclient.DirService{
54+
GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
55+
return &api.Tree{
56+
ParentPath: "namespace",
57+
RootDir: &api.Dir{
58+
DirID: rootDirUUID,
59+
Name: "repo",
60+
},
61+
Dirs: map[uuid.UUID]*api.Dir{
62+
subDirUUID: {
63+
DirID: subDirUUID,
64+
ParentID: &rootDirUUID,
65+
Name: "foo",
66+
},
67+
},
68+
Secrets: map[uuid.UUID]*api.Secret{
69+
secretUUID1: {
70+
SecretID: secretUUID1,
71+
DirID: subDirUUID,
72+
Name: "bar",
73+
},
74+
},
75+
}, nil
76+
},
77+
},
78+
}, nil
79+
},
80+
expectedValues: []string{"FOO_BAR"},
81+
},
82+
"name collision": {
83+
newClient: func() (secrethub.ClientInterface, error) {
84+
return fakeclient.Client{
85+
DirService: &fakeclient.DirService{
86+
GetTreeFunc: func(path string, depth int, ancestors bool) (*api.Tree, error) {
87+
return &api.Tree{
88+
ParentPath: "namespace",
89+
RootDir: &api.Dir{
90+
DirID: rootDirUUID,
91+
Name: "repo",
92+
},
93+
Dirs: map[uuid.UUID]*api.Dir{
94+
subDirUUID: {
95+
DirID: subDirUUID,
96+
ParentID: &rootDirUUID,
97+
Name: "foo",
98+
},
99+
},
100+
Secrets: map[uuid.UUID]*api.Secret{
101+
secretUUID1: {
102+
SecretID: secretUUID1,
103+
DirID: subDirUUID,
104+
Name: "bar",
105+
},
106+
secretUUID2: {
107+
SecretID: secretUUID2,
108+
DirID: rootDirUUID,
109+
Name: "foo_bar",
110+
},
111+
},
112+
}, nil
113+
},
114+
},
115+
}, nil
116+
},
117+
err: errNameCollision{
118+
name: "FOO_BAR",
119+
firstPath: "namespace/repo/foo/bar",
120+
secondPath: "namespace/repo/foo_bar",
121+
},
122+
},
123+
}
124+
125+
for name, tc := range cases {
126+
t.Run(name, func(t *testing.T) {
127+
source := newSecretsDirEnv(tc.newClient, dirPath)
128+
secrets, err := source.env()
129+
if tc.err != nil {
130+
assert.Equal(t, err, tc.err)
131+
} else {
132+
assert.OK(t, err)
133+
assert.Equal(t, len(secrets), len(tc.expectedValues))
134+
for _, name := range tc.expectedValues {
135+
if _, ok := secrets[name]; !ok {
136+
t.Errorf("expected but not found env var with name: %s", name)
137+
}
138+
}
139+
}
140+
})
141+
}
142+
}

internals/secrethub/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func NewRunCommand(io ui.IO, newClient newClientFunc) *RunCommand {
6363
return &RunCommand{
6464
io: io,
6565
osEnv: os.Environ(),
66-
environment: newEnvironment(io),
66+
environment: newEnvironment(io, newClient),
6767
newClient: newClient,
6868
}
6969
}

0 commit comments

Comments
 (0)