Skip to content
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ See [CAPH docs](https://syself.com/docs/caph/topics/baremetal/creating-workload-

## Usage

We recommend to mount the secret `hetzner` as volume and make it avaiable for the container as `/etc/hetzner-secret`.
We recommend to mount the secret `hetzner` as volume and make it available for the container as `/etc/hetzner-secret`.
Then the credentials are automatically reloaded, when the secret changes.
When you use hot-reloading, the secret keys must be named `hcloud` (or `token`, for upstream hcloud-ccm compatibility), `robot-user`, and `robot-password`.
You see an example in the [ccm helm chart](https://github.com/syself/charts/tree/main/charts/ccm-hetzner)

## Env Variables
Expand Down
20 changes: 14 additions & 6 deletions internal/credentials/hotreload.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package credentials

import (
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -91,7 +92,7 @@ func handleEvent(credentialsDir, baseName string, hcloudClient *hcloud.Client, r
// This case is executed, when the process is running on a local machine.
return loadRobotCredentials(credentialsDir, robotClient)

case "hcloud":
case "hcloud", "token":
// This case is executed, when the process is running on a local machine.
err := loadHcloudCredentials(credentialsDir, hcloudClient)
if err != nil {
Expand Down Expand Up @@ -237,12 +238,19 @@ func GetInitialHcloudCredentialsFromDirectory(credentialsDir string) (string, er
}

func readHcloudCredentials(credentialsDir string) (string, error) {
hcloudTokenFile := filepath.Join(credentialsDir, "hcloud")
data, err := os.ReadFile(hcloudTokenFile)
if err != nil {
return "", fmt.Errorf("reading hcloud token from %q failed: %w", hcloudTokenFile, err)
var allErrors []error
for _, key := range []string{"hcloud", "token"} {
// The upstream hcloud-ccm uses "token" as key name in the mounted secret.
// To ease migration between both CCMs, we support both key names.
hcloudTokenFile := filepath.Join(credentialsDir, key)
data, err := os.ReadFile(hcloudTokenFile)
if err != nil {
allErrors = append(allErrors, fmt.Errorf("reading hcloud token from %q failed: %w", hcloudTokenFile, err))
continue
}
return strings.TrimSpace(string(data)), nil
}
return strings.TrimSpace(string(data)), nil
return "", errors.Join(allErrors...)
}

// GetDirectory returns the directory where the credentials are stored.
Expand Down
79 changes: 79 additions & 0 deletions internal/credentials/hotreload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package credentials

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestReadHcloudCredentials(t *testing.T) {
t.Run("reads hcloud key", func(t *testing.T) {
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "hcloud"), []byte(" token-from-hcloud \n"), 0o600)
if err != nil {
t.Fatalf("write hcloud token: %v", err)
}

token, err := readHcloudCredentials(dir)
if err != nil {
t.Fatalf("readHcloudCredentials() error = %v", err)
}
if token != "token-from-hcloud" {
t.Fatalf("readHcloudCredentials() token = %q, want %q", token, "token-from-hcloud")
}
})

t.Run("falls back to token key", func(t *testing.T) {
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "token"), []byte("token-from-token-key"), 0o600)
if err != nil {
t.Fatalf("write token key: %v", err)
}

token, err := readHcloudCredentials(dir)
if err != nil {
t.Fatalf("readHcloudCredentials() error = %v", err)
}
if token != "token-from-token-key" {
t.Fatalf("readHcloudCredentials() token = %q, want %q", token, "token-from-token-key")
}
})

t.Run("prefers hcloud key over token key", func(t *testing.T) {
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "hcloud"), []byte("first-token"), 0o600)
if err != nil {
t.Fatalf("write hcloud token: %v", err)
}
err = os.WriteFile(filepath.Join(dir, "token"), []byte("second-token"), 0o600)
if err != nil {
t.Fatalf("write token key: %v", err)
}

token, err := readHcloudCredentials(dir)
if err != nil {
t.Fatalf("readHcloudCredentials() error = %v", err)
}
if token != "first-token" {
t.Fatalf("readHcloudCredentials() token = %q, want %q", token, "first-token")
}
})

t.Run("returns error if no key exists", func(t *testing.T) {
dir := t.TempDir()

_, err := readHcloudCredentials(dir)
if err == nil {
t.Fatal("readHcloudCredentials() error = nil, want non-nil")
}

errText := err.Error()
if !strings.Contains(errText, filepath.Join(dir, "hcloud")) {
t.Fatalf("error does not mention hcloud key path: %q", errText)
}
if !strings.Contains(errText, filepath.Join(dir, "token")) {
t.Fatalf("error does not mention token key path: %q", errText)
}
})
}
Loading