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

Commit e0be4ce

Browse files
Merge pull request #235 from secrethub/feature/prompt-missing-template-vars
Prompt missing template variables
2 parents 0e0bf2c + 3ec79c7 commit e0be4ce

11 files changed

Lines changed: 366 additions & 97 deletions

File tree

internals/secrethub/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func (app *App) registerCommands() {
177177
NewInspectCommand(app.io, app.clientFactory.NewClient).Register(app.cli)
178178
NewAuditCommand(app.io, app.clientFactory.NewClient).Register(app.cli)
179179
NewInjectCommand(app.io, app.clientFactory.NewClient).Register(app.cli)
180-
NewRunCommand(app.clientFactory.NewClient).Register(app.cli)
180+
NewRunCommand(app.io, app.clientFactory.NewClient).Register(app.cli)
181181
NewPrintEnvCommand(app.cli, app.io).Register(app.cli)
182182

183183
// Hidden commands

internals/secrethub/inject.go

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import (
55
"io/ioutil"
66
"os"
77
"path/filepath"
8-
"strings"
98
"time"
109

10+
"github.com/secrethub/secrethub-cli/internals/secrethub/tpl"
11+
1112
"github.com/secrethub/secrethub-cli/internals/cli/clip"
1213
"github.com/secrethub/secrethub-cli/internals/cli/filemode"
1314
"github.com/secrethub/secrethub-cli/internals/cli/posix"
1415
"github.com/secrethub/secrethub-cli/internals/cli/ui"
15-
"github.com/secrethub/secrethub-cli/internals/cli/validation"
1616
"github.com/secrethub/secrethub-cli/internals/secrethub/command"
1717

1818
"github.com/docker/go-units"
@@ -26,17 +26,18 @@ var (
2626

2727
// InjectCommand is a command to read a secret.
2828
type InjectCommand struct {
29-
outFile string
30-
inFile string
31-
fileMode filemode.FileMode
32-
force bool
33-
io ui.IO
34-
useClipboard bool
35-
clearClipboardAfter time.Duration
36-
clipper clip.Clipper
37-
newClient newClientFunc
38-
templateVars map[string]string
39-
templateVersion string
29+
outFile string
30+
inFile string
31+
fileMode filemode.FileMode
32+
force bool
33+
io ui.IO
34+
useClipboard bool
35+
clearClipboardAfter time.Duration
36+
clipper clip.Clipper
37+
newClient newClientFunc
38+
templateVars map[string]string
39+
templateVersion string
40+
dontPromptMissingTemplateVars bool
4041
}
4142

4243
// NewInjectCommand creates a new InjectCommand.
@@ -67,6 +68,7 @@ func (cmd *InjectCommand) Register(r command.Registerer) {
6768
clause.Flag("file-mode", "Set filemode for the output file if it does not yet exist. Defaults to 0600 (read and write for current user) and is ignored without the --out-file flag.").Default("0600").SetValue(&cmd.fileMode)
6869
clause.Flag("var", "Define the value for a template variable with `VAR=VALUE`, e.g. --var env=prod").Short('v').StringMapVar(&cmd.templateVars)
6970
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(&cmd.templateVersion)
71+
clause.Flag("no-prompt", "Do not prompt when a template variable is missing and return an error instead.").BoolVar(&cmd.dontPromptMissingTemplateVars)
7072
registerForceFlag(clause).BoolVar(&cmd.force)
7173

7274
command.BindAction(clause, cmd.Run)
@@ -97,25 +99,16 @@ func (cmd *InjectCommand) Run() error {
9799
}
98100
}
99101

100-
templateVars := make(map[string]string)
101-
102102
osEnv, _ := parseKeyValueStringsToMap(os.Environ())
103103

104-
for k, v := range osEnv {
105-
if strings.HasPrefix(k, templateVarEnvVarPrefix) {
106-
k = strings.TrimPrefix(k, templateVarEnvVarPrefix)
107-
templateVars[strings.ToLower(k)] = v
108-
}
109-
}
110-
111-
for k, v := range cmd.templateVars {
112-
templateVars[strings.ToLower(k)] = v
104+
var templateVariableReader tpl.VariableReader
105+
templateVariableReader, err = newVariableReader(osEnv, cmd.templateVars)
106+
if err != nil {
107+
return err
113108
}
114109

115-
for k := range templateVars {
116-
if !validation.IsEnvarNamePosix(k) {
117-
return ErrInvalidTemplateVar(k)
118-
}
110+
if !cmd.dontPromptMissingTemplateVars {
111+
templateVariableReader = newPromptMissingVariableReader(templateVariableReader, cmd.io)
119112
}
120113

121114
parser, err := getTemplateParser(raw, cmd.templateVersion)
@@ -128,7 +121,7 @@ func (cmd *InjectCommand) Run() error {
128121
return err
129122
}
130123

131-
injected, err := template.Evaluate(templateVars, newSecretReader(cmd.newClient))
124+
injected, err := template.Evaluate(templateVariableReader, newSecretReader(cmd.newClient))
132125
if err != nil {
133126
return err
134127
}

internals/secrethub/run.go

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"time"
1717
"unicode"
1818

19+
"github.com/secrethub/secrethub-cli/internals/cli/ui"
20+
1921
"github.com/secrethub/secrethub-cli/internals/cli/masker"
2022
"github.com/secrethub/secrethub-cli/internals/cli/validation"
2123
"github.com/secrethub/secrethub-cli/internals/secrethub/command"
@@ -53,21 +55,24 @@ const (
5355
// defined with --envar or --env-file flags and secrets.yml files.
5456
// The yml files write to .secretsenv/<env-name> when running the set command.
5557
type RunCommand struct {
56-
command []string
57-
envar map[string]string
58-
envFile string
59-
templateVars map[string]string
60-
templateVersion string
61-
env string
62-
noMasking bool
63-
maskingTimeout time.Duration
64-
newClient newClientFunc
65-
ignoreMissingSecrets bool
58+
command []string
59+
io ui.IO
60+
envar map[string]string
61+
envFile string
62+
templateVars map[string]string
63+
templateVersion string
64+
env string
65+
noMasking bool
66+
maskingTimeout time.Duration
67+
newClient newClientFunc
68+
ignoreMissingSecrets bool
69+
dontPromptMissingTemplateVar bool
6670
}
6771

6872
// NewRunCommand creates a new RunCommand.
69-
func NewRunCommand(newClient newClientFunc) *RunCommand {
73+
func NewRunCommand(io ui.IO, newClient newClientFunc) *RunCommand {
7074
return &RunCommand{
75+
io: io,
7176
envar: make(map[string]string),
7277
templateVars: make(map[string]string),
7378
newClient: newClient,
@@ -94,6 +99,7 @@ func (cmd *RunCommand) Register(r command.Registerer) {
9499
clause.Flag("masking-timeout", "The maximum time output is buffered. Warning: lowering this value increases the chance of secrets not being masked.").Default("1s").DurationVar(&cmd.maskingTimeout)
95100
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(&cmd.templateVersion)
96101
clause.Flag("ignore-missing-secrets", "Do not return an error when a secret does not exist and use an empty value instead.").BoolVar(&cmd.ignoreMissingSecrets)
102+
clause.Flag("no-prompt", "Do not prompt when a template variable is missing and return an error instead.").BoolVar(&cmd.dontPromptMissingTemplateVar)
97103

98104
command.BindAction(clause, cmd.Run)
99105
}
@@ -128,26 +134,16 @@ func (cmd *RunCommand) Run() error {
128134
return err
129135
}
130136

131-
templateVars := make(map[string]string)
132-
133-
for k, v := range osEnv {
134-
if strings.HasPrefix(k, templateVarEnvVarPrefix) {
135-
k = strings.TrimPrefix(k, templateVarEnvVarPrefix)
136-
templateVars[strings.ToLower(k)] = v
137+
if cmd.envFile != "" {
138+
templateVariableReader, err := newVariableReader(osEnv, cmd.templateVars)
139+
if err != nil {
140+
return err
137141
}
138-
}
139-
140-
for k, v := range cmd.templateVars {
141-
templateVars[strings.ToLower(k)] = v
142-
}
143142

144-
for k := range templateVars {
145-
if !validation.IsEnvarNamePosix(k) {
146-
return ErrInvalidTemplateVar(k)
143+
if !cmd.dontPromptMissingTemplateVar {
144+
templateVariableReader = newPromptMissingVariableReader(templateVariableReader, cmd.io)
147145
}
148-
}
149146

150-
if cmd.envFile != "" {
151147
raw, err := ioutil.ReadFile(cmd.envFile)
152148
if err != nil {
153149
return ErrCannotReadFile(cmd.envFile, err)
@@ -158,7 +154,7 @@ func (cmd *RunCommand) Run() error {
158154
return err
159155
}
160156

161-
envFile, err := ReadEnvFile(cmd.envFile, templateVars, parser)
157+
envFile, err := ReadEnvFile(cmd.envFile, templateVariableReader, parser)
162158
if err != nil {
163159
return err
164160
}
@@ -356,8 +352,8 @@ type EnvSource interface {
356352
}
357353

358354
type envTemplate struct {
359-
envVars []envvarTpls
360-
templateVars map[string]string
355+
envVars []envvarTpls
356+
templateVarReader tpl.VariableReader
361357
}
362358

363359
type envvarTpls struct {
@@ -377,7 +373,7 @@ func (sr secretReaderNotAllowed) ReadSecret(path string) (string, error) {
377373
func (t envTemplate) Env(secrets map[string]string, sr tpl.SecretReader) (map[string]string, error) {
378374
result := make(map[string]string)
379375
for _, tpls := range t.envVars {
380-
key, err := tpls.key.Evaluate(t.templateVars, secretReaderNotAllowed{})
376+
key, err := tpls.key.Evaluate(t.templateVarReader, secretReaderNotAllowed{})
381377
if err != nil {
382378
return nil, err
383379
}
@@ -387,7 +383,7 @@ func (t envTemplate) Env(secrets map[string]string, sr tpl.SecretReader) (map[st
387383
return nil, templateError(tpls.lineNo, err)
388384
}
389385

390-
value, err := tpls.value.Evaluate(t.templateVars, sr)
386+
value, err := tpls.value.Evaluate(t.templateVarReader, sr)
391387
if err != nil {
392388
return nil, err
393389
}
@@ -411,12 +407,12 @@ func (t envTemplate) Secrets() []string {
411407
}
412408

413409
// ReadEnvFile reads and parses a .env file.
414-
func ReadEnvFile(filepath string, vars map[string]string, parser tpl.Parser) (EnvFile, error) {
410+
func ReadEnvFile(filepath string, varReader tpl.VariableReader, parser tpl.Parser) (EnvFile, error) {
415411
r, err := os.Open(filepath)
416412
if err != nil {
417413
return EnvFile{}, ErrCannotReadFile(filepath, err)
418414
}
419-
env, err := NewEnv(r, vars, parser)
415+
env, err := NewEnv(r, varReader, parser)
420416
if err != nil {
421417
return EnvFile{}, ErrParsingTemplate(filepath, err)
422418
}
@@ -448,7 +444,7 @@ func (e EnvFile) Secrets() []string {
448444

449445
// NewEnv loads an environment of key-value pairs from a string.
450446
// The format of the string can be `key: value` or `key=value` pairs.
451-
func NewEnv(r io.Reader, vars map[string]string, parser tpl.Parser) (EnvSource, error) {
447+
func NewEnv(r io.Reader, varReader tpl.VariableReader, parser tpl.Parser) (EnvSource, error) {
452448
env, err := parseEnvironment(r)
453449
if err != nil {
454450
return nil, err
@@ -479,8 +475,8 @@ func NewEnv(r io.Reader, vars map[string]string, parser tpl.Parser) (EnvSource,
479475
}
480476

481477
return envTemplate{
482-
envVars: secretTemplates,
483-
templateVars: vars,
478+
envVars: secretTemplates,
479+
templateVarReader: varReader,
484480
}, nil
485481
}
486482

internals/secrethub/run_test.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,11 @@ func TestParseYML(t *testing.T) {
359359

360360
func TestNewEnv(t *testing.T) {
361361
cases := map[string]struct {
362-
raw string
363-
replacements map[string]string
364-
templateVars map[string]string
365-
expected map[string]string
366-
err error
362+
raw string
363+
replacements map[string]string
364+
templateVarReader tpl.VariableReader
365+
expected map[string]string
366+
err error
367367
}{
368368
"success": {
369369
raw: "foo=bar\nbaz={{path/to/secret}}",
@@ -380,8 +380,10 @@ func TestNewEnv(t *testing.T) {
380380
replacements: map[string]string{
381381
"company/application/db/pass": "secret",
382382
},
383-
templateVars: map[string]string{
384-
"app": "company/application",
383+
templateVarReader: fakes.FakeVariableReader{
384+
Variables: map[string]string{
385+
"app": "company/application",
386+
},
385387
},
386388
expected: map[string]string{
387389
"foo": "bar",
@@ -390,8 +392,10 @@ func TestNewEnv(t *testing.T) {
390392
},
391393
"success with var in key": {
392394
raw: "${var}=value",
393-
templateVars: map[string]string{
394-
"var": "key",
395+
templateVarReader: fakes.FakeVariableReader{
396+
Variables: map[string]string{
397+
"var": "key",
398+
},
395399
},
396400
expected: map[string]string{
397401
"key": "value",
@@ -434,7 +438,7 @@ func TestNewEnv(t *testing.T) {
434438
parser, err := getTemplateParser([]byte(tc.raw), "auto")
435439
assert.OK(t, err)
436440

437-
env, err := NewEnv(strings.NewReader(tc.raw), tc.templateVars, parser)
441+
env, err := NewEnv(strings.NewReader(tc.raw), tc.templateVarReader, parser)
438442
if err != nil {
439443
assert.Equal(t, err, tc.err)
440444
} else {
@@ -522,6 +526,7 @@ func TestRunCommand_Run(t *testing.T) {
522526
},
523527
"invalid template var: start with a number": {
524528
command: RunCommand{
529+
envFile: "secrethub.env",
525530
templateVars: map[string]string{
526531
"0foo": "value",
527532
},
@@ -531,6 +536,7 @@ func TestRunCommand_Run(t *testing.T) {
531536
},
532537
"invalid template var: illegal character": {
533538
command: RunCommand{
539+
envFile: "secrethub.env",
534540
templateVars: map[string]string{
535541
"foo@bar": "value",
536542
},
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package fakes
2+
3+
import "errors"
4+
5+
type FakeVariableReader struct {
6+
Variables map[string]string
7+
}
8+
9+
func (r FakeVariableReader) ReadVariable(name string) (string, error) {
10+
variable, ok := r.Variables[name]
11+
if !ok {
12+
return "", errors.New("variable not found: " + name)
13+
}
14+
return variable, nil
15+
}

internals/secrethub/tpl/template.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type Parser interface {
2020
type Template interface {
2121
// Evaluate renders a template. It replaces all variable- and secret tags in the template.
2222
// The supplied variables should have lowercase keys.
23-
Evaluate(vars map[string]string, sr SecretReader) (string, error)
23+
Evaluate(varReader VariableReader, sr SecretReader) (string, error)
2424
}
2525

2626
// NewParser returns a parser for the latest template syntax.

internals/secrethub/tpl/v1.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ import (
44
"github.com/secrethub/secrethub-cli/internals/tpl"
55
)
66

7-
// Errors
8-
var (
9-
ErrTemplateVarsNotSupported = tplError.Code("template_vars_not_supported").Error("the v1 template syntax does not support template variables")
10-
)
11-
127
// NewV1Parser returns a parser for the v1 template syntax.
138
//
149
// V1 templates can contain secret paths between ${}:
@@ -40,11 +35,7 @@ func (p parserV1) Parse(raw string, _, _ int) (Template, error) {
4035

4136
// InjectVars takes a map of template variables with their corresponding values. It replaces
4237
// the template variables with their values in the template.
43-
func (t templateV1) Evaluate(vars map[string]string, sr SecretReader) (string, error) {
44-
if len(vars) > 0 {
45-
return "", ErrTemplateVarsNotSupported
46-
}
47-
38+
func (t templateV1) Evaluate(_ VariableReader, sr SecretReader) (string, error) {
4839
keys := t.template.Keys()
4940
secrets := make(map[string]string, len(keys))
5041
for _, path := range keys {

0 commit comments

Comments
 (0)