Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: read HCLOUD_TOKEN from file #652

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions chart/templates/daemonset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions chart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -103,3 +113,7 @@ nodeSelector: {}
robot:
# Set to true to enable support for Robot (Dedicated) servers.
enabled: false

podLabels: {}

podAnnotations: {}
42 changes: 39 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"
)

Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestRead(t *testing.T) {
tests := []struct {
name string
env []string
files map[string]string
want HCCMConfiguration
wantErr error
}{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down
45 changes: 45 additions & 0 deletions internal/testsupport/files.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Loading