From 15a43f48be4ab3ef0b4862ea07e894bb17d09fa7 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Wed, 5 Jun 2024 16:39:07 +0200 Subject: [PATCH] feat(config): switch to HCLOUD_TOKEN_FILE variable --- internal/config/config.go | 30 ++++++++++++++++++++++-------- internal/config/config_test.go | 19 ++++++++++--------- internal/testsupport/files.go | 19 ++++++++++++------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index ce9e57f5..cbcd9523 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -97,16 +97,30 @@ type HCCMConfiguration struct { Route RouteConfiguration } +// read values from environment variables or from file set via _FILE env var +// values set directly via env var take precedence over values set via file func readFromEnvOrFile(envVar string) (string, error) { - value := os.Getenv(envVar) - if strings.HasPrefix(value, "file:") { - valueBytes, err := os.ReadFile(strings.TrimPrefix(value, "file:")) - if err != nil { - return "", fmt.Errorf("failed to read %s from file: %w", envVar, err) - } - return strings.TrimSpace(string(valueBytes)), nil + // check if the value is set directly via env (e.g. HCLOUD_TOKEN) + value, ok := os.LookupEnv(envVar) + if ok { + return value, nil + } + + // check if the value is set via a file (e.g. HCLOUD_TOKEN_FILE) + value, ok = os.LookupEnv(envVar + "_FILE") + if !ok { + // return no error here, the values could be optional + // and the function "Validate()" below checks that all required variables are set + return "", nil } - return value, nil + + // read file content + valueBytes, err := os.ReadFile(value) + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", envVar+"_FILE", err) + } + + return strings.TrimSpace(string(valueBytes)), nil } // Read evaluates all environment variables and returns a [HCCMConfiguration]. It only validates as far as diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9d58ed92..8b60cec0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -52,9 +52,9 @@ func TestRead(t *testing.T) { { name: "secrets from file", env: []string{ - "HCLOUD_TOKEN", "file:hetzner-token", - "ROBOT_USER", "file:hetzner-user", - "ROBOT_PASSWORD", "file:hetzner-password", + "HCLOUD_TOKEN_FILE", "/tmp/hetzner-token", + "ROBOT_USER_FILE", "/tmp/hetzner-user", + "ROBOT_PASSWORD_FILE", "/tmp/hetzner-password", }, files: map[string]string{ "hetzner-token": "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq", @@ -80,10 +80,11 @@ func TestRead(t *testing.T) { { name: "secrets from unknown file", env: []string{ - "HCLOUD_TOKEN", "file:/etc/hetzner/token", - "ROBOT_USER", "file:/etc/hetzner/user", - "ROBOT_PASSWORD", "file:/etc/hetzner/password", + "HCLOUD_TOKEN_FILE", "/tmp/hetzner-token", + "ROBOT_USER_FILE", "/tmp/hetzner-user", + "ROBOT_PASSWORD_FILE", "/tmp/hetzner-password", }, + files: map[string]string{}, // don't create files want: HCCMConfiguration{ HCloudClient: HCloudClientConfiguration{Token: ""}, Robot: RobotConfiguration{User: "", Password: "", CacheTimeout: 0}, @@ -92,9 +93,9 @@ func TestRead(t *testing.T) { LoadBalancer: LoadBalancerConfiguration{Enabled: false}, Route: RouteConfiguration{Enabled: false}, }, - wantErr: errors.New(`failed to read HCLOUD_TOKEN from file: open /etc/hetzner/token: no such file or directory -failed to read ROBOT_USER from file: open /etc/hetzner/user: no such file or directory -failed to read ROBOT_PASSWORD from file: open /etc/hetzner/password: no such file or directory`), + wantErr: errors.New(`failed to read HCLOUD_TOKEN_FILE: open /tmp/hetzner-token: no such file or directory +failed to read ROBOT_USER_FILE: open /tmp/hetzner-user: no such file or directory +failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or directory`), }, { name: "client", diff --git a/internal/testsupport/files.go b/internal/testsupport/files.go index b8438c38..c3925127 100644 --- a/internal/testsupport/files.go +++ b/internal/testsupport/files.go @@ -5,24 +5,28 @@ import ( "testing" ) +// SetFiles can be used to temporarily create files on the local file system. +// It returns a function that will clean up all files it created. func Setfiles(t *testing.T, files map[string]string) func() { for file, content := range files { + filepath := os.TempDir() + "/" + file + // check if file exists - _, err := os.Stat(file) + _, err := os.Stat(filepath) if err == nil { - t.Fatalf("Trying to set file %s, but it already exists. Please choose another filepath for the test.", file) + t.Fatalf("Trying to set file %s, but it already exists. Please choose another filepath for the test.", filepath) } // create file - f, err := os.Create(file) + f, err := os.Create(filepath) if err != nil { - t.Fatalf("Failed to create file %s: %v", file, err) + t.Fatalf("Failed to create file %s: %v", filepath, err) } // write content to file _, err = f.WriteString(content) if err != nil { - t.Fatalf("Failed to write to file %s: %v", file, err) + t.Fatalf("Failed to write to file %s: %v", filepath, err) } // close file @@ -31,9 +35,10 @@ func Setfiles(t *testing.T, files map[string]string) func() { return func() { for file := range files { - err := os.Remove(file) + filepath := os.TempDir() + "/" + file + err := os.Remove(filepath) if err != nil { - t.Fatalf("Failed to remove file %s: %v", file, err) + t.Fatalf("Failed to remove file %s: %v", filepath, err) } } }