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

Commit 8d8bf9e

Browse files
committed
Merge remote-tracking branch 'origin/develop' into fix/env-source-precedence
2 parents 6d8decd + 7b9ff43 commit 8d8bf9e

2 files changed

Lines changed: 346 additions & 199 deletions

File tree

internals/secrethub/run.go

Lines changed: 123 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ var (
3737
ErrSignalFailed = errRun.Code("signal_failed").ErrorPref("error while propagating signal to process: %s")
3838
ErrReadEnvDir = errRun.Code("env_dir_read_error").ErrorPref("could not read the environment directory: %s")
3939
ErrReadEnvFile = errRun.Code("env_file_read_error").ErrorPref("could not read the environment file %s: %s")
40-
ErrEnvDirNotFound = errRun.Code("env_dir_not_found").Error(fmt.Sprintf("could not find specified environment. Make sure you have executed `%s set`.", ApplicationName))
40+
ErrReadDefaultEnvFile = errRun.Code("default_env_file_read_error").ErrorPref("could not read default run env-file %s: %s")
4141
ErrTemplate = errRun.Code("invalid_template").ErrorPref("could not parse template at line %d: %s")
4242
ErrParsingTemplate = errRun.Code("template_parsing_failed").ErrorPref("error while processing template file '%s': %s")
4343
ErrInvalidTemplateVar = errRun.Code("invalid_template_var").ErrorPref("template variable '%s' is invalid: template variables may only contain uppercase letters, digits, and the '_' (underscore) and are not allowed to start with a number")
4444
ErrSecretsNotAllowedInKey = errRun.Code("secret_in_key").Error("secrets are not allowed in run template keys")
4545
)
4646

4747
const (
48-
maskString = "<redacted by SecretHub>"
48+
defaultEnvFile = "secrethub.env"
49+
maskString = "<redacted by SecretHub>"
4950
// templateVarEnvVarPrefix is used to prefix environment variables
5051
// that should be used as template variables.
5152
templateVarEnvVarPrefix = "SECRETHUB_VAR_"
@@ -58,9 +59,11 @@ const (
5859
// defined with --envar or --env-file flags and secrets.yml files.
5960
// The yml files write to .secretsenv/<env-name> when running the set command.
6061
type RunCommand struct {
61-
command []string
6262
io ui.IO
6363
osEnv []string
64+
readFile func(filename string) ([]byte, error)
65+
osStat func(filename string) (os.FileInfo, error)
66+
command []string
6467
envar map[string]string
6568
envFile string
6669
templateVars map[string]string
@@ -78,6 +81,8 @@ func NewRunCommand(io ui.IO, newClient newClientFunc) *RunCommand {
7881
return &RunCommand{
7982
io: io,
8083
osEnv: os.Environ(),
84+
readFile: ioutil.ReadFile,
85+
osStat: os.Stat,
8186
envar: make(map[string]string),
8287
templateVars: make(map[string]string),
8388
newClient: newClient,
@@ -112,66 +117,155 @@ func (cmd *RunCommand) Register(r command.Registerer) {
112117
// Run reads files from the .secretsenv/<env-name> directory, sets them as environment variables and runs the given command.
113118
// Note that the environment variables are only passed to the child process and not exported globally, which is nice.
114119
func (cmd *RunCommand) Run() error {
115-
// Parse
116-
envSources := []EnvSource{}
120+
environment, secrets, err := cmd.sourceEnvironment()
121+
if err != nil {
122+
return err
123+
}
124+
125+
// This makes sure commands encapsulated in quotes also work.
126+
if len(cmd.command) == 1 {
127+
cmd.command = strings.Split(cmd.command[0], " ")
128+
}
129+
130+
valuesToMask := make([][]byte, 0, len(secrets))
131+
for _, val := range secrets {
132+
if val != "" {
133+
valuesToMask = append(valuesToMask, []byte(val))
134+
}
135+
}
136+
137+
maskedStdout := masker.NewMaskedWriter(cmd.io.Stdout(), valuesToMask, maskString, cmd.maskingTimeout)
138+
maskedStderr := masker.NewMaskedWriter(os.Stderr, valuesToMask, maskString, cmd.maskingTimeout)
139+
140+
command := exec.Command(cmd.command[0], cmd.command[1:]...)
141+
command.Env = environment
142+
command.Stdin = os.Stdin
143+
if cmd.noMasking {
144+
command.Stdout = cmd.io.Stdout()
145+
command.Stderr = os.Stderr
146+
} else {
147+
command.Stdout = maskedStdout
148+
command.Stderr = maskedStderr
149+
150+
go maskedStdout.Run()
151+
go maskedStderr.Run()
152+
}
153+
154+
err = command.Start()
155+
if err != nil {
156+
return ErrStartFailed(err)
157+
}
158+
159+
done := make(chan bool, 1)
160+
161+
// Pass all signals to child process
162+
signals := make(chan os.Signal, 1)
163+
signal.Notify(signals)
164+
165+
go func() {
166+
select {
167+
case s := <-signals:
168+
err := command.Process.Signal(s)
169+
if err != nil && !strings.Contains(err.Error(), "process already finished") {
170+
fmt.Fprintln(os.Stderr, ErrSignalFailed(err))
171+
}
172+
case <-done:
173+
signal.Stop(signals)
174+
return
175+
}
176+
}()
117177

178+
commandErr := command.Wait()
179+
done <- true
180+
181+
if !cmd.noMasking {
182+
err = maskedStdout.Flush()
183+
if err != nil {
184+
fmt.Fprintln(os.Stderr, err)
185+
}
186+
err = maskedStderr.Flush()
187+
if err != nil {
188+
fmt.Fprintln(os.Stderr, err)
189+
}
190+
}
191+
192+
if commandErr != nil {
193+
// Check if the program exited with an error
194+
exitErr, ok := commandErr.(*exec.ExitError)
195+
if ok {
196+
waitStatus, ok := exitErr.Sys().(syscall.WaitStatus)
197+
if ok {
198+
// Return the status code returned by the process
199+
os.Exit(waitStatus.ExitStatus())
200+
return nil
201+
}
202+
203+
}
204+
return commandErr
205+
}
206+
207+
return nil
208+
}
209+
210+
// sourceEnvironment returns the environment of the subcommand, with all the secrets sourced
211+
// and the secret values that need to be masked.
212+
func (cmd *RunCommand) sourceEnvironment() ([]string, []string, error) {
118213
osEnv, passthroughEnv := parseKeyValueStringsToMap(cmd.osEnv)
119214

215+
envSources := []EnvSource{}
216+
120217
// TODO: Validate the flags when parsing by implementing the Flag interface for EnvFlags.
121218
flagSource, err := NewEnvFlags(cmd.envar)
122219
if err != nil {
123-
return err
220+
return nil, nil, err
124221
}
125222
envSources = append(envSources, flagSource)
126223

127224
referenceEnv := newReferenceEnv(osEnv)
128225
envSources = append(envSources, referenceEnv)
129226

130227
if cmd.envFile == "" {
131-
const defaultEnvFile = "secrethub.env"
132-
_, err := os.Stat(defaultEnvFile)
133-
if err != nil {
134-
if !os.IsNotExist(err) {
135-
return fmt.Errorf("could not read default run env-file %s: %s", defaultEnvFile, err)
136-
}
137-
} else {
228+
_, err := cmd.osStat(defaultEnvFile)
229+
if err == nil {
138230
cmd.envFile = defaultEnvFile
231+
} else if !os.IsNotExist(err) {
232+
return nil, nil, ErrReadDefaultEnvFile(defaultEnvFile, err)
139233
}
140234
}
141235

142236
if cmd.envFile != "" {
143237
templateVariableReader, err := newVariableReader(osEnv, cmd.templateVars)
144238
if err != nil {
145-
return err
239+
return nil, nil, err
146240
}
147241

148242
if !cmd.dontPromptMissingTemplateVar {
149243
templateVariableReader = newPromptMissingVariableReader(templateVariableReader, cmd.io)
150244
}
151245

152-
raw, err := ioutil.ReadFile(cmd.envFile)
246+
raw, err := cmd.readFile(cmd.envFile)
153247
if err != nil {
154-
return ErrCannotReadFile(cmd.envFile, err)
248+
return nil, nil, ErrCannotReadFile(cmd.envFile, err)
155249
}
156250

157251
parser, err := getTemplateParser(raw, cmd.templateVersion)
158252
if err != nil {
159-
return err
253+
return nil, nil, err
160254
}
161255

162-
envFile, err := ReadEnvFile(cmd.envFile, templateVariableReader, parser)
256+
envFile, err := ReadEnvFile(cmd.envFile, bytes.NewReader(raw), templateVariableReader, parser)
163257
if err != nil {
164-
return err
258+
return nil, nil, err
165259
}
166260
envSources = append(envSources, envFile)
167261
}
168262

169263
envDir := filepath.Join(secretspec.SecretEnvPath, cmd.env)
170-
_, err = os.Stat(envDir)
264+
_, err = cmd.osStat(envDir)
171265
if err == nil {
172266
dirSource, err := NewEnvDir(envDir)
173267
if err != nil {
174-
return err
268+
return nil, nil, err
175269
}
176270
envSources = append(envSources, dirSource)
177271
}
@@ -193,7 +287,7 @@ func (cmd *RunCommand) Run() error {
193287
for path := range secrets {
194288
secret, err := secretReader.ReadSecret(path)
195289
if err != nil {
196-
return err
290+
return nil, nil, err
197291
}
198292
secrets[path] = secret
199293
}
@@ -203,7 +297,7 @@ func (cmd *RunCommand) Run() error {
203297
for _, source := range envSources {
204298
pairs, err := source.Env(secrets, secretReader)
205299
if err != nil {
206-
return err
300+
return nil, nil, err
207301
}
208302

209303
for key, value := range pairs {
@@ -215,7 +309,7 @@ func (cmd *RunCommand) Run() error {
215309
}
216310
}
217311

218-
// Finally, source the remaining envars from the OS environment.
312+
// Source the remaining envars from the OS environment.
219313
for key, value := range osEnv {
220314
// Only set a variable if it wasn't set by a configured source.
221315
_, found := environment[key]
@@ -224,91 +318,10 @@ func (cmd *RunCommand) Run() error {
224318
}
225319
}
226320

227-
// This makes sure commands encapsulated in quotes also work.
228-
if len(cmd.command) == 1 {
229-
cmd.command = strings.Split(cmd.command[0], " ")
230-
}
231-
232-
values := secretReader.Values()
321+
// Finally add the unparsed variables
322+
processedOsEnv := append(passthroughEnv, mapToKeyValueStrings(environment)...)
233323

234-
valuesToMask := make([][]byte, 0, len(values))
235-
for _, val := range values {
236-
if val != "" {
237-
valuesToMask = append(valuesToMask, []byte(val))
238-
}
239-
}
240-
241-
maskedStdout := masker.NewMaskedWriter(cmd.io.Stdout(), valuesToMask, maskString, cmd.maskingTimeout)
242-
maskedStderr := masker.NewMaskedWriter(os.Stderr, valuesToMask, maskString, cmd.maskingTimeout)
243-
244-
command := exec.Command(cmd.command[0], cmd.command[1:]...)
245-
command.Env = append(passthroughEnv, mapToKeyValueStrings(environment)...)
246-
command.Stdin = os.Stdin
247-
if cmd.noMasking {
248-
command.Stdout = cmd.io.Stdout()
249-
command.Stderr = os.Stderr
250-
} else {
251-
command.Stdout = maskedStdout
252-
command.Stderr = maskedStderr
253-
254-
go maskedStdout.Run()
255-
go maskedStderr.Run()
256-
}
257-
258-
err = command.Start()
259-
if err != nil {
260-
return ErrStartFailed(err)
261-
}
262-
263-
done := make(chan bool, 1)
264-
265-
// Pass all signals to child process
266-
signals := make(chan os.Signal, 1)
267-
signal.Notify(signals)
268-
269-
go func() {
270-
select {
271-
case s := <-signals:
272-
err := command.Process.Signal(s)
273-
if err != nil && !strings.Contains(err.Error(), "process already finished") {
274-
fmt.Fprintln(os.Stderr, ErrSignalFailed(err))
275-
}
276-
case <-done:
277-
signal.Stop(signals)
278-
return
279-
}
280-
}()
281-
282-
commandErr := command.Wait()
283-
done <- true
284-
285-
if !cmd.noMasking {
286-
err = maskedStdout.Flush()
287-
if err != nil {
288-
fmt.Fprintln(os.Stderr, err)
289-
}
290-
err = maskedStderr.Flush()
291-
if err != nil {
292-
fmt.Fprintln(os.Stderr, err)
293-
}
294-
}
295-
296-
if commandErr != nil {
297-
// Check if the program exited with an error
298-
exitErr, ok := commandErr.(*exec.ExitError)
299-
if ok {
300-
waitStatus, ok := exitErr.Sys().(syscall.WaitStatus)
301-
if ok {
302-
// Return the status code returned by the process
303-
os.Exit(waitStatus.ExitStatus())
304-
return nil
305-
}
306-
307-
}
308-
return commandErr
309-
}
310-
311-
return nil
324+
return processedOsEnv, secretReader.Values(), nil
312325
}
313326

314327
// mapToKeyValueStrings converts a map to a slice of key=value pairs.
@@ -412,12 +425,8 @@ func (t envTemplate) Secrets() []string {
412425
}
413426

414427
// ReadEnvFile reads and parses a .env file.
415-
func ReadEnvFile(filepath string, varReader tpl.VariableReader, parser tpl.Parser) (EnvFile, error) {
416-
r, err := os.Open(filepath)
417-
if err != nil {
418-
return EnvFile{}, ErrCannotReadFile(filepath, err)
419-
}
420-
env, err := NewEnv(r, varReader, parser)
428+
func ReadEnvFile(filepath string, reader io.Reader, varReader tpl.VariableReader, parser tpl.Parser) (EnvFile, error) {
429+
env, err := NewEnv(reader, varReader, parser)
421430
if err != nil {
422431
return EnvFile{}, ErrParsingTemplate(filepath, err)
423432
}
@@ -667,13 +676,6 @@ type EnvDir map[string]string
667676
// NewEnvDir sources environment variables from files in a given directory,
668677
// using the file name as key and contents as value.
669678
func NewEnvDir(path string) (EnvDir, error) {
670-
_, err := os.Stat(path)
671-
if os.IsNotExist(err) {
672-
return nil, ErrEnvDirNotFound
673-
} else if err != nil {
674-
return nil, ErrReadEnvDir(err)
675-
}
676-
677679
files, err := ioutil.ReadDir(path)
678680
if err != nil {
679681
return nil, ErrReadEnvDir(err)

0 commit comments

Comments
 (0)