Skip to content

Commit a4343b8

Browse files
simonostendorfjooolaapricote
authored
feat: read HCLOUD_TOKEN from file (#652)
This allows the `HCLOUD_TOKEN` (and `ROBOT_USER` and `ROBOT_PASSWORD`) to be read from a file. This can be useful if the token is injected using secret injection (e.g. with the vault agent injector). If someone is interested in using this with the vault agent injector, I used the following helm values: ```yaml image: repository: <custom-image-because-changes-are-not-released> tag: <custom-image-because-changes-are-not-released> podAnnotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/log-format: json vault.hashicorp.com/role: <your-vault-role-name> vault.hashicorp.com/secret-volume-path-token: /vault/secrets vault.hashicorp.com/agent-inject-file-token: token vault.hashicorp.com/agent-inject-secret-token: <your-vault-mount>/data/<your-vault-path> vault.hashicorp.com/agent-inject-template-token: | {{ with secret "<your-vault-mount>/data/<your-vault-path>" -}} {{ .Data.data.token }} {{- end }} env: HCLOUD_TOKEN_FILE: value: "/vault/secrets/token" HCLOUD_TOKEN: null # must be set because helm results in using value and valueFrom and that results in an error ``` This change is inspired from [external-dns cloudflare provider](https://github.com/kubernetes-sigs/external-dns/blob/master/provider/cloudflare/cloudflare.go#L171). I requested the same change for the [csi-driver](hetznercloud/csi-driver#617) to keep consistency in reading HCLOUD_TOKEN from file. Closes #595 --------- Co-authored-by: Jonas L. <[email protected]> Co-authored-by: Julian Tölle <[email protected]>
1 parent e2b0ed6 commit a4343b8

File tree

6 files changed

+163
-3
lines changed

6 files changed

+163
-3
lines changed

chart/templates/daemonset.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ spec:
1313
metadata:
1414
labels:
1515
{{- include "hcloud-cloud-controller-manager.selectorLabels" . | nindent 8 }}
16+
{{- if .Values.podLabels }}
17+
{{- toYaml .Values.podLabels | nindent 8 }}
18+
{{- end }}
19+
{{- if .Values.podAnnotations }}
20+
annotations:
21+
{{- toYaml .Values.podAnnotations | nindent 8 }}
22+
{{- end }}
1623
spec:
1724
serviceAccountName: {{ include "hcloud-cloud-controller-manager.name" . }}
1825
dnsPolicy: Default

chart/templates/deployment.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ spec:
1414
metadata:
1515
labels:
1616
{{- include "hcloud-cloud-controller-manager.selectorLabels" . | nindent 8 }}
17+
{{- if .Values.podLabels }}
18+
{{- toYaml .Values.podLabels | nindent 8 }}
19+
{{- end }}
20+
{{- if .Values.podAnnotations }}
21+
annotations:
22+
{{- toYaml .Values.podAnnotations | nindent 8 }}
23+
{{- end }}
1724
spec:
1825
serviceAccountName: {{ include "hcloud-cloud-controller-manager.name" . }}
1926
dnsPolicy: Default

chart/values.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ env:
2929
# HCLOUD_NETWORK - see networking.enabled
3030
# ROBOT_ENABLED - see robot.enabled
3131

32+
# You can also use a file to provide secrets to the hcloud-cloud-controller-manager.
33+
# This is currently possible for HCLOUD_TOKEN, ROBOT_USER, and ROBOT_PASSWORD.
34+
# Use the env var appended with _FILE (e.g. HCLOUD_TOKEN_FILE) and set the value to the file path that should be read
35+
# The file must be provided externally (e.g. via secret injection).
36+
# Example:
37+
# HCLOUD_TOKEN_FILE:
38+
# value: "/etc/hetzner/token"
39+
# to disable reading the token from the secret you have to disable the original env var:
40+
# HCLOUD_TOKEN: null
41+
3242
HCLOUD_TOKEN:
3343
valueFrom:
3444
secretKeyRef:
@@ -103,3 +113,7 @@ nodeSelector: {}
103113
robot:
104114
# Set to true to enable support for Robot (Dedicated) servers.
105115
enabled: false
116+
117+
podLabels: {}
118+
119+
podAnnotations: {}

internal/config/config.go

+39-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"strconv"
8+
"strings"
89
"time"
910
)
1011

@@ -96,6 +97,32 @@ type HCCMConfiguration struct {
9697
Route RouteConfiguration
9798
}
9899

100+
// read values from environment variables or from file set via _FILE env var
101+
// values set directly via env var take precedence over values set via file.
102+
func readFromEnvOrFile(envVar string) (string, error) {
103+
// check if the value is set directly via env (e.g. HCLOUD_TOKEN)
104+
value, ok := os.LookupEnv(envVar)
105+
if ok {
106+
return value, nil
107+
}
108+
109+
// check if the value is set via a file (e.g. HCLOUD_TOKEN_FILE)
110+
value, ok = os.LookupEnv(envVar + "_FILE")
111+
if !ok {
112+
// return no error here, the values could be optional
113+
// and the function "Validate()" below checks that all required variables are set
114+
return "", nil
115+
}
116+
117+
// read file content
118+
valueBytes, err := os.ReadFile(value)
119+
if err != nil {
120+
return "", fmt.Errorf("failed to read %s: %w", envVar+"_FILE", err)
121+
}
122+
123+
return strings.TrimSpace(string(valueBytes)), nil
124+
}
125+
99126
// Read evaluates all environment variables and returns a [HCCMConfiguration]. It only validates as far as
100127
// it needs to parse the values. For business logic validation, check out [HCCMConfiguration.Validate].
101128
func Read() (HCCMConfiguration, error) {
@@ -106,7 +133,10 @@ func Read() (HCCMConfiguration, error) {
106133
var errs []error
107134
var cfg HCCMConfiguration
108135

109-
cfg.HCloudClient.Token = os.Getenv(hcloudToken)
136+
cfg.HCloudClient.Token, err = readFromEnvOrFile(hcloudToken)
137+
if err != nil {
138+
errs = append(errs, err)
139+
}
110140
cfg.HCloudClient.Endpoint = os.Getenv(hcloudEndpoint)
111141
cfg.HCloudClient.Debug, err = getEnvBool(hcloudDebug, false)
112142
if err != nil {
@@ -117,8 +147,14 @@ func Read() (HCCMConfiguration, error) {
117147
if err != nil {
118148
errs = append(errs, err)
119149
}
120-
cfg.Robot.User = os.Getenv(robotUser)
121-
cfg.Robot.Password = os.Getenv(robotPassword)
150+
cfg.Robot.User, err = readFromEnvOrFile(robotUser)
151+
if err != nil {
152+
errs = append(errs, err)
153+
}
154+
cfg.Robot.Password, err = readFromEnvOrFile(robotPassword)
155+
if err != nil {
156+
errs = append(errs, err)
157+
}
122158
cfg.Robot.CacheTimeout, err = getEnvDuration(robotCacheTimeout)
123159
if err != nil {
124160
errs = append(errs, err)

internal/config/config_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestRead(t *testing.T) {
1414
tests := []struct {
1515
name string
1616
env []string
17+
files map[string]string
1718
want HCCMConfiguration
1819
wantErr error
1920
}{
@@ -48,6 +49,54 @@ func TestRead(t *testing.T) {
4849
},
4950
wantErr: nil,
5051
},
52+
{
53+
name: "secrets from file",
54+
env: []string{
55+
"HCLOUD_TOKEN_FILE", "/tmp/hetzner-token",
56+
"ROBOT_USER_FILE", "/tmp/hetzner-user",
57+
"ROBOT_PASSWORD_FILE", "/tmp/hetzner-password",
58+
},
59+
files: map[string]string{
60+
"hetzner-token": "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq",
61+
"hetzner-user": "foobar",
62+
"hetzner-password": `secret-password`,
63+
},
64+
want: HCCMConfiguration{
65+
HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
66+
Robot: RobotConfiguration{
67+
Enabled: false,
68+
User: "foobar",
69+
Password: "secret-password",
70+
CacheTimeout: 5 * time.Minute,
71+
RateLimitWaitTime: 0,
72+
},
73+
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
74+
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
75+
LoadBalancer: LoadBalancerConfiguration{Enabled: true},
76+
Route: RouteConfiguration{Enabled: false},
77+
},
78+
wantErr: nil,
79+
},
80+
{
81+
name: "secrets from unknown file",
82+
env: []string{
83+
"HCLOUD_TOKEN_FILE", "/tmp/hetzner-token",
84+
"ROBOT_USER_FILE", "/tmp/hetzner-user",
85+
"ROBOT_PASSWORD_FILE", "/tmp/hetzner-password",
86+
},
87+
files: map[string]string{}, // don't create files
88+
want: HCCMConfiguration{
89+
HCloudClient: HCloudClientConfiguration{Token: ""},
90+
Robot: RobotConfiguration{User: "", Password: "", CacheTimeout: 0},
91+
Metrics: MetricsConfiguration{Enabled: false},
92+
Instance: InstanceConfiguration{},
93+
LoadBalancer: LoadBalancerConfiguration{Enabled: false},
94+
Route: RouteConfiguration{Enabled: false},
95+
},
96+
wantErr: errors.New(`failed to read HCLOUD_TOKEN_FILE: open /tmp/hetzner-token: no such file or directory
97+
failed to read ROBOT_USER_FILE: open /tmp/hetzner-user: no such file or directory
98+
failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or directory`),
99+
},
51100
{
52101
name: "client",
53102
env: []string{
@@ -207,6 +256,8 @@ failed to parse ROBOT_RATE_LIMIT_WAIT_TIME: time: unknown unit "fortnights" in d
207256
t.Run(tt.name, func(t *testing.T) {
208257
resetEnv := testsupport.Setenv(t, tt.env...)
209258
defer resetEnv()
259+
resetFiles := testsupport.SetFiles(t, tt.files)
260+
defer resetFiles()
210261

211262
got, err := Read()
212263
if tt.wantErr == nil {

internal/testsupport/files.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package testsupport
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
// SetFiles can be used to temporarily create files on the local file system.
9+
// It returns a function that will clean up all files it created.
10+
func SetFiles(t *testing.T, files map[string]string) func() {
11+
for file, content := range files {
12+
filepath := os.TempDir() + "/" + file
13+
14+
// check if file exists
15+
_, err := os.Stat(filepath)
16+
if err == nil {
17+
t.Fatalf("Trying to set file %s, but it already exists. Please choose another filepath for the test.", filepath)
18+
}
19+
20+
// create file
21+
f, err := os.Create(filepath)
22+
if err != nil {
23+
t.Fatalf("Failed to create file %s: %v", filepath, err)
24+
}
25+
26+
// write content to file
27+
_, err = f.WriteString(content)
28+
if err != nil {
29+
t.Fatalf("Failed to write to file %s: %v", filepath, err)
30+
}
31+
32+
// close file
33+
f.Close()
34+
}
35+
36+
return func() {
37+
for file := range files {
38+
filepath := os.TempDir() + "/" + file
39+
err := os.Remove(filepath)
40+
if err != nil {
41+
t.Fatalf("Failed to remove file %s: %v", filepath, err)
42+
}
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)