Skip to content

Commit c97044b

Browse files
authored
Vault arbitrary env passthrough (#52)
* resolve #46 Adds a new option: pass-env, which when true will pass the pico process environment to children. Defaults to false to promote separation of environments. Adds support for passing prefixed variables from the global Pico. The prefix is GLOBAL_ and is not configurable because I felt the config flags are growing. Adds some better unit tests for execution config and environment merging. * correctly strip the prefix for global configuration values pulled from the secret store
1 parent 42d7c8d commit c97044b

File tree

7 files changed

+165
-45
lines changed

7 files changed

+165
-45
lines changed

executor/cmd.go

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@ var _ Executor = &CommandExecutor{}
1212

1313
// CommandExecutor handles command invocation targets
1414
type CommandExecutor struct {
15-
secrets secret.Store
15+
secrets secret.Store
16+
passEnvironment bool // pass the Pico process environment to children
17+
configSecretPath string // path to global secrets to pass to children
18+
configSecretPrefix string // only pass secrets with this prefix, usually GLOBAL_
1619
}
1720

1821
// NewCommandExecutor creates a new CommandExecutor
19-
func NewCommandExecutor(secrets secret.Store) CommandExecutor {
22+
func NewCommandExecutor(
23+
secrets secret.Store,
24+
passEnvironment bool,
25+
configSecretPath string,
26+
configSecretPrefix string,
27+
) CommandExecutor {
2028
return CommandExecutor{
21-
secrets: secrets,
29+
secrets: secrets,
30+
passEnvironment: passEnvironment,
31+
configSecretPath: configSecretPath,
32+
configSecretPrefix: configSecretPrefix,
2233
}
2334
}
2435

@@ -34,34 +45,66 @@ func (e *CommandExecutor) Subscribe(bus chan task.ExecutionTask) {
3445
}
3546
}
3647

37-
func (e *CommandExecutor) execute(
38-
target task.Target,
48+
type exec struct {
49+
path string
50+
env map[string]string
51+
shutdown bool
52+
passEnvironment bool
53+
}
54+
55+
func (e *CommandExecutor) prepare(
56+
name string,
3957
path string,
4058
shutdown bool,
4159
execEnv map[string]string,
42-
) (err error) {
43-
secrets, err := e.secrets.GetSecretsForTarget(target.Name)
60+
) (exec, error) {
61+
// get global secrets from the Pico config path in the secret store.
62+
// only secrets with the prefix are retrieved.
63+
global, err := secret.GetPrefixedSecrets(e.secrets, e.configSecretPath, e.configSecretPrefix)
4464
if err != nil {
45-
return errors.Wrap(err, "failed to get secrets for target")
65+
return exec{}, errors.Wrap(err, "failed to get global secrets for target")
66+
}
67+
68+
secrets, err := e.secrets.GetSecretsForTarget(name)
69+
if err != nil {
70+
return exec{}, errors.Wrap(err, "failed to get secrets for target")
4671
}
4772

4873
env := make(map[string]string)
4974

50-
// merge execution environment with secrets
75+
// merge execution environment with secrets in the following order:
76+
// globals first, then execution environment, then per-target secrets
77+
for k, v := range global {
78+
env[k] = v
79+
}
5180
for k, v := range execEnv {
5281
env[k] = v
5382
}
5483
for k, v := range secrets {
5584
env[k] = v
5685
}
5786

87+
return exec{path, env, shutdown, e.passEnvironment}, nil
88+
}
89+
90+
func (e *CommandExecutor) execute(
91+
target task.Target,
92+
path string,
93+
shutdown bool,
94+
execEnv map[string]string,
95+
) (err error) {
96+
ex, err := e.prepare(target.Name, path, shutdown, execEnv)
97+
if err != nil {
98+
return err
99+
}
100+
58101
zap.L().Debug("executing with secrets",
59102
zap.String("target", target.Name),
60103
zap.Strings("cmd", target.Up),
61104
zap.String("url", target.RepoURL),
62105
zap.String("dir", path),
63-
zap.Int("env", len(env)),
64-
zap.Int("secrets", len(secrets)))
106+
zap.Int("env", len(ex.env)),
107+
zap.Bool("passthrough", e.passEnvironment))
65108

66-
return target.Execute(path, env, shutdown)
109+
return target.Execute(ex.path, ex.env, ex.shutdown, ex.passEnvironment)
67110
}

executor/cmd_test.go

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package executor_test
1+
package executor
22

33
import (
44
"os"
@@ -7,9 +7,9 @@ import (
77

88
"golang.org/x/sync/errgroup"
99

10-
"github.com/picostack/pico/executor"
1110
"github.com/picostack/pico/secret/memory"
1211
"github.com/picostack/pico/task"
12+
"github.com/stretchr/testify/assert"
1313

1414
_ "github.com/picostack/pico/logger"
1515
)
@@ -20,11 +20,13 @@ func TestMain(m *testing.M) {
2020
}
2121

2222
func TestCommandExecutor(t *testing.T) {
23-
ce := executor.NewCommandExecutor(&memory.MemorySecrets{
24-
Secrets: map[string]string{
25-
"SOME_SECRET": "123",
23+
ce := NewCommandExecutor(&memory.MemorySecrets{
24+
Secrets: map[string]map[string]string{
25+
"test": map[string]string{
26+
"SOME_SECRET": "123",
27+
},
2628
},
27-
})
29+
}, false, "pico", "GLOBAL_")
2830
bus := make(chan task.ExecutionTask)
2931

3032
g := errgroup.Group{}
@@ -55,3 +57,50 @@ func TestCommandExecutor(t *testing.T) {
5557

5658
os.RemoveAll(".test/.git")
5759
}
60+
61+
func TestCommandPrepareWithoutPassthrough(t *testing.T) {
62+
ce := NewCommandExecutor(&memory.MemorySecrets{
63+
Secrets: map[string]map[string]string{
64+
"test": map[string]string{
65+
"SOME_SECRET": "123",
66+
},
67+
},
68+
}, false, "pico", "GLOBAL_")
69+
70+
ex, err := ce.prepare("test", "./", false, nil)
71+
assert.NoError(t, err)
72+
assert.Equal(t, exec{
73+
path: "./",
74+
env: map[string]string{
75+
"SOME_SECRET": "123",
76+
},
77+
shutdown: false,
78+
passEnvironment: false,
79+
}, ex)
80+
}
81+
82+
func TestCommandPrepareWithGlobal(t *testing.T) {
83+
ce := NewCommandExecutor(&memory.MemorySecrets{
84+
Secrets: map[string]map[string]string{
85+
"test": map[string]string{
86+
"SOME_SECRET": "123",
87+
},
88+
"pico": map[string]string{
89+
"GLOBAL_SECRET": "456",
90+
"IGNORE": "this",
91+
},
92+
},
93+
}, false, "pico", "GLOBAL_")
94+
95+
ex, err := ce.prepare("test", "./", false, nil)
96+
assert.NoError(t, err)
97+
assert.Equal(t, exec{
98+
path: "./",
99+
env: map[string]string{
100+
"SOME_SECRET": "123",
101+
"SECRET": "456",
102+
},
103+
shutdown: false,
104+
passEnvironment: false,
105+
}, ex)
106+
}

main.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ this repository has new commits, Pico will automatically reconfigure.`,
4343
cli.StringFlag{Name: "git-password", EnvVar: "GIT_PASSWORD"},
4444
cli.StringFlag{Name: "hostname", EnvVar: "HOSTNAME"},
4545
cli.StringFlag{Name: "directory", EnvVar: "DIRECTORY", Value: "./cache/"},
46+
cli.DurationFlag{Name: "pass-env", EnvVar: "PASS_ENV"},
4647
cli.BoolFlag{Name: "ssh", EnvVar: "SSH"},
4748
cli.DurationFlag{Name: "check-interval", EnvVar: "CHECK_INTERVAL", Value: time.Second * 10},
4849
cli.StringFlag{Name: "vault-addr", EnvVar: "VAULT_ADDR"},
@@ -77,15 +78,16 @@ this repository has new commits, Pico will automatically reconfigure.`,
7778
User: c.String("git-username"),
7879
Pass: c.String("git-password"),
7980
},
80-
Hostname: hostname,
81-
Directory: c.String("directory"),
82-
SSH: c.Bool("ssh"),
83-
CheckInterval: c.Duration("check-interval"),
84-
VaultAddress: c.String("vault-addr"),
85-
VaultToken: c.String("vault-token"),
86-
VaultPath: c.String("vault-path"),
87-
VaultRenewal: c.Duration("vault-renew-interval"),
88-
VaultConfig: c.String("vault-config-path"),
81+
Hostname: hostname,
82+
Directory: c.String("directory"),
83+
PassEnvironment: c.Bool("pass-env"),
84+
SSH: c.Bool("ssh"),
85+
CheckInterval: c.Duration("check-interval"),
86+
VaultAddress: c.String("vault-addr"),
87+
VaultToken: c.String("vault-token"),
88+
VaultPath: c.String("vault-path"),
89+
VaultRenewal: c.Duration("vault-renew-interval"),
90+
VaultConfig: c.String("vault-config-path"),
8991
})
9092
if err != nil {
9193
return errors.Wrap(err, "failed to initialise")

secret/memory/memory.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import (
66

77
// MemorySecrets implements a simple in-memory secret.Store for testing
88
type MemorySecrets struct {
9-
Secrets map[string]string
9+
Secrets map[string]map[string]string
1010
}
1111

1212
var _ secret.Store = &MemorySecrets{}
1313

1414
// GetSecretsForTarget implements secret.Store
1515
func (v *MemorySecrets) GetSecretsForTarget(name string) (map[string]string, error) {
16-
return v.Secrets, nil
16+
table, ok := v.Secrets[name]
17+
if !ok {
18+
return nil, nil
19+
}
20+
return table, nil
1721
}

secret/secret.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,24 @@
33
// any secrets that match it.
44
package secret
55

6+
import "strings"
7+
68
// Store describes a type that can securely obtain secrets for services.
79
type Store interface {
810
GetSecretsForTarget(name string) (map[string]string, error)
911
}
12+
13+
// GetPrefixedSecrets uses a Store to get a set of secrets that use a prefix.
14+
func GetPrefixedSecrets(s Store, path, prefix string) (map[string]string, error) {
15+
all, err := s.GetSecretsForTarget(path)
16+
if err != nil {
17+
return nil, err
18+
}
19+
pass := make(map[string]string)
20+
for k, v := range all {
21+
if strings.HasPrefix(k, prefix) {
22+
pass[strings.TrimPrefix(k, prefix)] = v
23+
}
24+
}
25+
return pass, nil
26+
}

service/service.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ import (
2626

2727
// Config specifies static configuration parameters (from CLI or environment)
2828
type Config struct {
29-
Target task.Repo
30-
Hostname string
31-
SSH bool
32-
Directory string
33-
CheckInterval time.Duration
34-
VaultAddress string
35-
VaultToken string
36-
VaultPath string
37-
VaultRenewal time.Duration
38-
VaultConfig string
29+
Target task.Repo
30+
Hostname string
31+
SSH bool
32+
Directory string
33+
PassEnvironment bool
34+
CheckInterval time.Duration
35+
VaultAddress string
36+
VaultToken string
37+
VaultPath string
38+
VaultRenewal time.Duration
39+
VaultConfig string
3940
}
4041

4142
// App stores application state
@@ -119,7 +120,7 @@ func (app *App) Start(ctx context.Context) error {
119120
// states and potentially retry in some circumstances. Pico should be the
120121
// kind of service that barely goes down, only when absolutely necessary.
121122

122-
ce := executor.NewCommandExecutor(app.secrets)
123+
ce := executor.NewCommandExecutor(app.secrets, app.config.PassEnvironment, app.config.VaultConfig, "GLOBAL_")
123124
g.Go(func() error {
124125
ce.Subscribe(app.bus)
125126
return nil

task/target.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Target struct {
5252

5353
// Execute runs the target's command in the specified directory with the
5454
// specified environment variables
55-
func (t *Target) Execute(dir string, env map[string]string, shutdown bool) (err error) {
55+
func (t *Target) Execute(dir string, env map[string]string, shutdown bool, inheritEnv bool) (err error) {
5656
if env == nil {
5757
env = make(map[string]string)
5858
}
@@ -67,10 +67,10 @@ func (t *Target) Execute(dir string, env map[string]string, shutdown bool) (err
6767
command = t.Up
6868
}
6969

70-
return execute(dir, env, command)
70+
return execute(dir, env, command, inheritEnv)
7171
}
7272

73-
func execute(dir string, env map[string]string, command []string) (err error) {
73+
func execute(dir string, env map[string]string, command []string, inheritEnv bool) (err error) {
7474
if len(command) == 0 {
7575
return errors.New("attempt to execute target with empty command")
7676
}
@@ -83,10 +83,14 @@ func execute(dir string, env map[string]string, command []string) (err error) {
8383
cmd.Stdout = os.Stdout
8484
cmd.Stderr = os.Stdout
8585

86-
cmd.Env = os.Environ()
86+
var cmdEnv []string
87+
if inheritEnv {
88+
cmdEnv = os.Environ()
89+
}
8790
for k, v := range env {
88-
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
91+
cmdEnv = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
8992
}
93+
cmd.Env = cmdEnv
9094

9195
return cmd.Run()
9296
}

0 commit comments

Comments
 (0)