diff --git a/chart/templates/daemonset.yaml b/chart/templates/daemonset.yaml index 12242b8b40af2af2ee07df92844f910ec18ae8b7..35d5b3519d4540653c799f86cea1bdedf3ac008c 100644 --- a/chart/templates/daemonset.yaml +++ b/chart/templates/daemonset.yaml @@ -13,6 +13,13 @@ spec: metadata: labels: {{- include "hcloud-cloud-controller-manager.selectorLabels" . | nindent 8 }} + {{- if .Values.podLabels }} + {{- toYaml .Values.podLabels | nindent 8 }} + {{- end }} + {{- if .Values.podAnnotations }} + annotations: + {{- toYaml .Values.podAnnotations | nindent 8 }} + {{- end }} spec: serviceAccountName: {{ include "hcloud-cloud-controller-manager.name" . }} dnsPolicy: Default diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index b59daa2a89b7c5e197e5f7aa86de27f73c6635f7..782a58df4774e55e620b873fbdcce05f0c1c8317 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -14,6 +14,13 @@ spec: metadata: labels: {{- include "hcloud-cloud-controller-manager.selectorLabels" . | nindent 8 }} + {{- if .Values.podLabels }} + {{- toYaml .Values.podLabels | nindent 8 }} + {{- end }} + {{- if .Values.podAnnotations }} + annotations: + {{- toYaml .Values.podAnnotations | nindent 8 }} + {{- end }} spec: serviceAccountName: {{ include "hcloud-cloud-controller-manager.name" . }} dnsPolicy: Default diff --git a/chart/values.yaml b/chart/values.yaml index 795109d51948bda91698863508ead4046bd6a728..041cb4ea0eba53d895ebec8f7975916172d4dd91 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -29,6 +29,16 @@ env: # HCLOUD_NETWORK - see networking.enabled # ROBOT_ENABLED - see robot.enabled + # You can also use a file to provide secrets to the hcloud-cloud-controller-manager. + # This is currently possible for HCLOUD_TOKEN, ROBOT_USER, and ROBOT_PASSWORD. + # Use the env var appended with _FILE (e.g. HCLOUD_TOKEN_FILE) and set the value to the file path that should be read + # The file must be provided externally (e.g. via secret injection). + # Example: + # HCLOUD_TOKEN_FILE: + # value: "/etc/hetzner/token" + # to disable reading the token from the secret you have to disable the original env var: + # HCLOUD_TOKEN: null + HCLOUD_TOKEN: valueFrom: secretKeyRef: @@ -103,3 +113,7 @@ nodeSelector: {} robot: # Set to true to enable support for Robot (Dedicated) servers. enabled: false + +podLabels: {} + +podAnnotations: {} diff --git a/internal/config/config.go b/internal/config/config.go index 9c97e2a6220ff79241c51bf10ad071fe459cf51e..504858a4a9bc4001cdbfb8e7fc728901bc2eb3b2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "time" ) @@ -96,6 +97,32 @@ 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) { + // 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 + } + + // 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 // it needs to parse the values. For business logic validation, check out [HCCMConfiguration.Validate]. func Read() (HCCMConfiguration, error) { @@ -106,7 +133,10 @@ func Read() (HCCMConfiguration, error) { var errs []error var cfg HCCMConfiguration - cfg.HCloudClient.Token = os.Getenv(hcloudToken) + cfg.HCloudClient.Token, err = readFromEnvOrFile(hcloudToken) + if err != nil { + errs = append(errs, err) + } cfg.HCloudClient.Endpoint = os.Getenv(hcloudEndpoint) cfg.HCloudClient.Debug, err = getEnvBool(hcloudDebug, false) if err != nil { @@ -117,8 +147,14 @@ func Read() (HCCMConfiguration, error) { if err != nil { errs = append(errs, err) } - cfg.Robot.User = os.Getenv(robotUser) - cfg.Robot.Password = os.Getenv(robotPassword) + cfg.Robot.User, err = readFromEnvOrFile(robotUser) + if err != nil { + errs = append(errs, err) + } + cfg.Robot.Password, err = readFromEnvOrFile(robotPassword) + if err != nil { + errs = append(errs, err) + } cfg.Robot.CacheTimeout, err = getEnvDuration(robotCacheTimeout) if err != nil { errs = append(errs, err) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 367b640d48ad728c7b3acccef240b0e821320895..ac8d6f9785f99c327c9b9d58acfa7c576ff2ea71 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -14,6 +14,7 @@ func TestRead(t *testing.T) { tests := []struct { name string env []string + files map[string]string want HCCMConfiguration wantErr error }{ @@ -48,6 +49,54 @@ func TestRead(t *testing.T) { }, wantErr: nil, }, + { + name: "secrets from file", + env: []string{ + "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", + "hetzner-user": "foobar", + "hetzner-password": `secret-password`, + }, + want: HCCMConfiguration{ + HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"}, + Robot: RobotConfiguration{ + Enabled: false, + User: "foobar", + Password: "secret-password", + CacheTimeout: 5 * time.Minute, + RateLimitWaitTime: 0, + }, + Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"}, + Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4}, + LoadBalancer: LoadBalancerConfiguration{Enabled: true}, + Route: RouteConfiguration{Enabled: false}, + }, + wantErr: nil, + }, + { + name: "secrets from unknown file", + env: []string{ + "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}, + Metrics: MetricsConfiguration{Enabled: false}, + Instance: InstanceConfiguration{}, + LoadBalancer: LoadBalancerConfiguration{Enabled: false}, + Route: RouteConfiguration{Enabled: false}, + }, + 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", env: []string{ @@ -207,6 +256,8 @@ failed to parse ROBOT_RATE_LIMIT_WAIT_TIME: time: unknown unit "fortnights" in d t.Run(tt.name, func(t *testing.T) { resetEnv := testsupport.Setenv(t, tt.env...) defer resetEnv() + resetFiles := testsupport.SetFiles(t, tt.files) + defer resetFiles() got, err := Read() if tt.wantErr == nil { diff --git a/internal/testsupport/files.go b/internal/testsupport/files.go new file mode 100644 index 0000000000000000000000000000000000000000..1872d73d0f6dcfa14aa96870768f782863d63a07 --- /dev/null +++ b/internal/testsupport/files.go @@ -0,0 +1,45 @@ +package testsupport + +import ( + "os" + "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(filepath) + if err == nil { + 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(filepath) + if err != nil { + 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", filepath, err) + } + + // close file + f.Close() + } + + return func() { + for file := range files { + filepath := os.TempDir() + "/" + file + err := os.Remove(filepath) + if err != nil { + t.Fatalf("Failed to remove file %s: %v", filepath, err) + } + } + } +}