diff --git a/hcloud/exp/kit/envutils/env.go b/hcloud/exp/kit/envutils/env.go new file mode 100644 index 00000000..387dd21f --- /dev/null +++ b/hcloud/exp/kit/envutils/env.go @@ -0,0 +1,40 @@ +package envutils + +import ( + "fmt" + "os" + "strings" +) + +// LookupEnvWithFile retrieves the value of the environment variable named by the key (e.g. +// HCLOUD_TOKEN). If the previous environment variable is not set, it retrieves the +// content of the file located by a second environment variable named by the key + +// '_FILE' (.e.g HCLOUD_TOKEN_FILE). +// +// For both cases, the returned value may be empty. +// +// The value from the environment takes precedence over the value from the file. +func LookupEnvWithFile(key string) (string, error) { + // Check if the value is set in the environment (e.g. HCLOUD_TOKEN) + value, ok := os.LookupEnv(key) + if ok { + return value, nil + } + + key += "_FILE" + + // Check if the value is set via a file (e.g. HCLOUD_TOKEN_FILE) + valueFile, ok := os.LookupEnv(key) + if !ok { + // Validation of the value happens outside of this function + return "", nil + } + + // Read the content of the file + valueBytes, err := os.ReadFile(valueFile) + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", key, err) + } + + return strings.TrimSpace(string(valueBytes)), nil +} diff --git a/hcloud/exp/kit/envutils/env_test.go b/hcloud/exp/kit/envutils/env_test.go new file mode 100644 index 00000000..930e5769 --- /dev/null +++ b/hcloud/exp/kit/envutils/env_test.go @@ -0,0 +1,106 @@ +package envutils + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// nolint:unparam +func writeTmpFile(t *testing.T, tmpDir, filename, content string) string { + filepath := path.Join(tmpDir, filename) + + err := os.WriteFile(filepath, []byte(content), 0644) + require.NoError(t, err) + + return filepath +} + +func TestLookupEnvWithFile(t *testing.T) { + testCases := []struct { + name string + setup func(t *testing.T, tmpDir string) + want func(t *testing.T, value string, err error) + }{ + { + name: "without any environment", + setup: func(_ *testing.T, _ string) {}, + want: func(t *testing.T, value string, err error) { + assert.NoError(t, err) + assert.Equal(t, "", value) + }, + }, + { + name: "value from environment", + setup: func(t *testing.T, tmpDir string) { + t.Setenv("CONFIG", "value") + + // Test for precedence + filepath := writeTmpFile(t, tmpDir, "config", "content") + t.Setenv("CONFIG_FILE", filepath) + }, + want: func(t *testing.T, value string, err error) { + assert.NoError(t, err) + assert.Equal(t, "value", value) + }, + }, + { + name: "empty value from environment", + setup: func(t *testing.T, tmpDir string) { + t.Setenv("CONFIG", "") + + // Test for precedence + filepath := writeTmpFile(t, tmpDir, "config", "content") + t.Setenv("CONFIG_FILE", filepath) + }, + want: func(t *testing.T, value string, err error) { + assert.NoError(t, err) + assert.Equal(t, "", value) + }, + }, + { + name: "value from file", + setup: func(t *testing.T, tmpDir string) { + // The extra spaces ensure that the value is sanitized + filepath := writeTmpFile(t, tmpDir, "config", "content ") + t.Setenv("CONFIG_FILE", filepath) + }, + want: func(t *testing.T, value string, err error) { + assert.NoError(t, err) + assert.Equal(t, "content", value) + }, + }, + { + name: "empty value from file", + setup: func(t *testing.T, tmpDir string) { + filepath := writeTmpFile(t, tmpDir, "config", "") + t.Setenv("CONFIG_FILE", filepath) + }, + want: func(t *testing.T, value string, err error) { + assert.NoError(t, err) + assert.Equal(t, "", value) + }, + }, + { + name: "missing file", + setup: func(t *testing.T, _ string) { + t.Setenv("CONFIG_FILE", "/tmp/this-file-does-not-exits") + }, + want: func(t *testing.T, value string, err error) { + assert.Error(t, err, "failed to read CONFIG_FILE: open /tmp/this-file-does-not-exits: no such file or directory") + assert.Equal(t, "", value) + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + tmpDir := t.TempDir() + testCase.setup(t, tmpDir) + value, err := LookupEnvWithFile("CONFIG") + testCase.want(t, value, err) + }) + } +}