From dad40bb23ca02a9f22044228130ce404e37719b2 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sun, 24 Mar 2024 13:09:15 -0500 Subject: [PATCH] Add system env variable expansion to envVars function (#186) * Add system env variable expansion to envVars function The function `envVars` has been updated to enable the application to also read system environment variables, in addition to CLI and env file variables. This change includes an update of the function's comment to reflect the new behavior, and the addition of new test cases to validate the feature. CLI variables override env file variables, which in turn override system environment variables if they share the same name. * add a test for env file with expanded os env * add docs about os env expansion --- README.md | 4 ++-- cmd/spot/main.go | 12 +++++++++--- cmd/spot/main_test.go | 42 +++++++++++++++++++++++++++++++++++------- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a077d06c..3093a102 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,8 @@ Spot supports the following command-line options: - `-k`, `--key=`: Specifies the SSH key for connecting to remote hosts. Overrides the key defined in the playbook file. - `-s`, `--skip=`: Skips the specified commands during the task execution. Providing the `-s` flag multiple times with different command names skips multiple commands. - `-o`, `--only=`: Runs only the specified commands during the task execution. Providing the `-o` flag multiple times with different command names runs only multiple commands. -- `-e`, `--env=`: Sets the environment variables to be used during the task execution. Providing the `-e` flag multiple times with different environment variables sets multiple environment variables, e.g., `-e VAR1:VALUE1 -e VAR2:VALUE2`. -- `-E`, `--env-file=`: Sets the environment variables from the file to be used during the task execution. The default is env.yml. Can also be set with the environment variable `SPOT_ENV_FILE`. +- `-e`, `--env=`: Sets the environment variables to be used during the task execution. Providing the `-e` flag multiple times with different environment variables sets multiple environment variables, e.g., `-e VAR1:VALUE1 -e VAR2:VALUE2`. Values could be taken from the OS environment variables as well, e.g., `-e VAR1:$ENV_VAR1` or `-e VAR1:${ENV_VAR1}`. +- `-E`, `--env-file=`: Sets the environment variables from the file to be used during the task execution. The file can have values from the OS environment variables as well. The default is env.yml. Can also be set with the environment variable `SPOT_ENV_FILE`. - `--no-color`: disable the colorized output. It can also be set with the environment variable `SPOT_NO_COLOR`. - `--dry`: Enables dry-run mode, which prints out the commands to be executed without actually executing them. - `-v`, `--verbose`: Enables verbose mode, providing more detailed output and error messages during the task execution. diff --git a/cmd/spot/main.go b/cmd/spot/main.go index 9a7f80a2..e500f8a0 100644 --- a/cmd/spot/main.go +++ b/cmd/spot/main.go @@ -450,10 +450,16 @@ func setAdHocSSH(opts options, pbook *config.PlayBook) (*config.PlayBook, error) return pbook, nil } -// envVars returns a map of environment variables from the cli and env file, cli vars override env file vars if duplicated +// envVars returns a map of environment variables from the cli, env file, and system env. +// cli vars override env file vars, which in turn override system env vars if duplicated. func envVars(vars map[string]string, envFile string) (map[string]string, error) { res := make(map[string]string) + expandEnv := func(value string) string { + // expand environment variables denoted by $var or ${var} + return os.Expand(value, os.Getenv) + } + // load env file vars envFileData := struct { Vars map[string]string `yaml:"vars"` @@ -465,13 +471,13 @@ func envVars(vars map[string]string, envFile string) (map[string]string, error) log.Printf("[WARN] can't parse env file %q: %v", envFile, err) } for k, v := range envFileData.Vars { - res[k] = v + res[k] = expandEnv(v) } } // cli vars override env file vars for k, v := range vars { - res[k] = v + res[k] = expandEnv(v) } return res, nil diff --git a/cmd/spot/main_test.go b/cmd/spot/main_test.go index bb320f82..2978f56d 100644 --- a/cmd/spot/main_test.go +++ b/cmd/spot/main_test.go @@ -717,6 +717,9 @@ func Test_targetsForTask(t *testing.T) { } func TestEnvVars(t *testing.T) { + os.Setenv("ENV_VAR", "envValue") + defer os.Unsetenv("ENV_VAR") + tests := []struct { name string cliVars map[string]string @@ -734,11 +737,17 @@ func TestEnvVars(t *testing.T) { key1: fileValue1 key2: fileValue2 key3: fileValue3 + key4: "${ENV_VAR}" + key5: "${ENV_VAR_NOT_FOUND}" + key6: "$ENV_VAR" `, expectedVars: map[string]string{ "key1": "cliValue1", "key2": "cliValue2", "key3": "fileValue3", + "key4": "envValue", + "key5": "", + "key6": "envValue", }, expectedError: false, }, @@ -755,34 +764,53 @@ func TestEnvVars(t *testing.T) { }, expectedError: false, }, + { + name: "system env var replacement", + cliVars: map[string]string{ + "key1": "$ENV_VAR", + "key2": "${ENV_VAR}", + "key3": "${ENV_VAR_NOT_FOUND}", + }, + envFileData: "", + expectedVars: map[string]string{ + "key1": "envValue", + "key2": "envValue", + "key3": "", + }, + expectedError: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - envFile := "/tmp/env-not-exist.yaml" if tt.envFileData != "" { - // create a temp file file, err := os.CreateTemp("", "*.yaml") if err != nil { t.Fatalf("could not create temp file: %v", err) } - defer os.Remove(file.Name()) + defer os.Remove(file.Name()) // Clean up - // write data to temp file if _, err = file.WriteString(tt.envFileData); err != nil { t.Fatalf("could not write to temp file: %v", err) } - // close file if err = file.Close(); err != nil { t.Fatalf("could not close temp file: %v", err) } envFile = file.Name() } - // get environment variables + actualVars, err := envVars(tt.cliVars, envFile) - assert.Equal(t, tt.expectedError, err != nil) + if err != nil && !tt.expectedError { + t.Errorf("envVars() error = %v, expectedError %v", err, tt.expectedError) + return + } + if err == nil && tt.expectedError { + t.Errorf("envVars() expected error, got none") + return + } + assert.Equal(t, tt.expectedVars, actualVars) }) }