feat: Extract Wild Central as standalone Go service

Extract the Central networking functionality from wild-cloud/api into
a standalone service. Wild Central manages DNS (dnsmasq), gateway
(HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot),
security (CrowdSec), and DDNS — all the network appliance concerns.

All Cloud-specific code (instances, clusters, nodes, apps, backups,
operations, kubectl/talosctl tooling) has been removed. The API struct
and route registration contain only Central endpoints. Tests updated
to match the new API signature.

Builds and all tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:31:16 +00:00
commit beb643f76f
52 changed files with 12814 additions and 0 deletions

237
internal/secrets/secrets.go Normal file
View File

@@ -0,0 +1,237 @@
package secrets
import (
"bytes"
"crypto/rand"
"fmt"
"math/big"
"os/exec"
"path/filepath"
"strings"
"github.com/wild-cloud/wild-central/internal/storage"
)
// YQ provides a wrapper around the yq command-line tool
type YQ struct {
yqPath string
}
// NewYQ creates a new YQ wrapper
func NewYQ() *YQ {
path, err := exec.LookPath("yq")
if err != nil {
path = "yq"
}
return &YQ{yqPath: path}
}
// Get retrieves a value from a YAML file using a yq expression
func (y *YQ) Get(filePath, expression string) (string, error) {
cmd := exec.Command(y.yqPath, expression, filePath)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("yq get failed: %w, stderr: %s", err, stderr.String())
}
return strings.TrimSpace(stdout.String()), nil
}
// Set sets a value in a YAML file using a yq expression
func (y *YQ) Set(filePath, expression, value string) error {
if !strings.HasPrefix(expression, ".") {
expression = "." + expression
}
quotedValue := fmt.Sprintf(`"%s"`, strings.ReplaceAll(value, `"`, `\"`))
setExpr := fmt.Sprintf("%s = %s", expression, quotedValue)
cmd := exec.Command(y.yqPath, "-i", setExpr, filePath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("yq set failed: %w, stderr: %s", err, stderr.String())
}
return nil
}
// Delete removes a key from a YAML file
func (y *YQ) Delete(filePath, expression string) error {
delExpr := fmt.Sprintf("del(%s)", expression)
cmd := exec.Command(y.yqPath, "-i", delExpr, filePath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("yq delete failed: %w, stderr: %s", err, stderr.String())
}
return nil
}
// Validate checks if a YAML file is valid
func (y *YQ) Validate(filePath string) error {
cmd := exec.Command(y.yqPath, "eval", ".", filePath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("yaml validation failed: %w, stderr: %s", err, stderr.String())
}
return nil
}
const (
// DefaultSecretLength is 32 characters
DefaultSecretLength = 32
// Alphanumeric characters for secret generation
alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
// Manager handles secret generation and storage
type Manager struct {
yq *YQ
}
// NewManager creates a new secrets manager
func NewManager() *Manager {
return &Manager{
yq: NewYQ(),
}
}
// GenerateSecret generates a cryptographically secure random alphanumeric string
func GenerateSecret(length int) (string, error) {
if length <= 0 {
length = DefaultSecretLength
}
result := make([]byte, length)
for i := range result {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphanumeric))))
if err != nil {
return "", fmt.Errorf("generating random number: %w", err)
}
result[i] = alphanumeric[num.Int64()]
}
return string(result), nil
}
// EnsureSecretsFile ensures a secrets file exists with proper structure and permissions
func (m *Manager) EnsureSecretsFile(dataDir, instanceName string) error {
secretsPath := filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml")
// Check if secrets file already exists
if storage.FileExists(secretsPath) {
// Ensure proper permissions
if err := storage.EnsureFilePermissions(secretsPath, 0600); err != nil {
return err
}
return nil
}
// Create minimal secrets structure
initialSecrets := `# Wild Cloud Instance Secrets
# WARNING: This file contains sensitive data. Keep secure!
cluster:
talosSecrets: ""
kubeconfig: ""
cloudflare:
token: ""
`
// Ensure config directory exists
if err := storage.EnsureDir(filepath.Join(dataDir, "instances", instanceName, "config"), 0755); err != nil {
return err
}
// Write secrets file with restrictive permissions (0600)
if err := storage.WriteFile(secretsPath, []byte(initialSecrets), 0600); err != nil {
return err
}
return nil
}
// GetSecret retrieves a secret value from a secrets file
func (m *Manager) GetSecret(secretsPath, key string) (string, error) {
if !storage.FileExists(secretsPath) {
return "", fmt.Errorf("secrets file not found: %s", secretsPath)
}
value, err := m.yq.Get(secretsPath, fmt.Sprintf(".%s", key))
if err != nil {
return "", fmt.Errorf("getting secret %s: %w", key, err)
}
// yq returns "null" for non-existent keys
if value == "" || value == "null" {
return "", fmt.Errorf("secret not found: %s", key)
}
return value, nil
}
// SetSecret sets a secret value in a secrets file
func (m *Manager) SetSecret(secretsPath, key, value string) error {
if !storage.FileExists(secretsPath) {
return fmt.Errorf("secrets file not found: %s", secretsPath)
}
// Acquire lock before modifying
lockPath := secretsPath + ".lock"
return storage.WithLock(lockPath, func() error {
// Don't wrap value in quotes - yq handles YAML quoting automatically
if err := m.yq.Set(secretsPath, fmt.Sprintf(".%s", key), value); err != nil {
return err
}
// Ensure permissions remain secure after modification
return storage.EnsureFilePermissions(secretsPath, 0600)
})
}
// EnsureSecret generates and sets a secret only if it doesn't exist (idempotent)
func (m *Manager) EnsureSecret(secretsPath, key string, length int) (string, error) {
if !storage.FileExists(secretsPath) {
return "", fmt.Errorf("secrets file not found: %s", secretsPath)
}
// Check if secret already exists
existingSecret, err := m.GetSecret(secretsPath, key)
if err == nil && existingSecret != "" && existingSecret != "null" {
// Secret already exists, return it
return existingSecret, nil
}
// Generate new secret
secret, err := GenerateSecret(length)
if err != nil {
return "", err
}
// Set the secret
if err := m.SetSecret(secretsPath, key, secret); err != nil {
return "", err
}
return secret, nil
}
// GenerateAndStoreSecret is a convenience function that generates a secret and stores it
func (m *Manager) GenerateAndStoreSecret(secretsPath, key string) (string, error) {
return m.EnsureSecret(secretsPath, key, DefaultSecretLength)
}
// DeleteSecret removes a secret from a secrets file
func (m *Manager) DeleteSecret(secretsPath, key string) error {
if !storage.FileExists(secretsPath) {
return fmt.Errorf("secrets file not found: %s", secretsPath)
}
// Acquire lock before modifying
lockPath := secretsPath + ".lock"
return storage.WithLock(lockPath, func() error {
if err := m.yq.Delete(secretsPath, fmt.Sprintf(".%s", key)); err != nil {
return err
}
// Ensure permissions remain secure after modification
return storage.EnsureFilePermissions(secretsPath, 0600)
})
}

View File

@@ -0,0 +1,973 @@
package secrets
import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/wild-cloud/wild-central/internal/storage"
)
// Test: GenerateSecret generates valid secrets
func TestGenerateSecret(t *testing.T) {
tests := []struct {
name string
length int
want int
}{
{
name: "default length",
length: DefaultSecretLength,
want: DefaultSecretLength,
},
{
name: "custom length 64",
length: 64,
want: 64,
},
{
name: "custom length 128",
length: 128,
want: 128,
},
{
name: "zero length defaults to DefaultSecretLength",
length: 0,
want: DefaultSecretLength,
},
{
name: "negative length defaults to DefaultSecretLength",
length: -1,
want: DefaultSecretLength,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret, err := GenerateSecret(tt.length)
if err != nil {
t.Fatalf("GenerateSecret failed: %v", err)
}
if len(secret) != tt.want {
t.Errorf("got length %d, want %d", len(secret), tt.want)
}
// Verify only alphanumeric characters
for _, c := range secret {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
t.Errorf("non-alphanumeric character found: %c", c)
}
}
})
}
}
// Test: GenerateSecret produces unique values
func TestGenerateSecret_Uniqueness(t *testing.T) {
const numSecrets = 100
secrets := make(map[string]bool, numSecrets)
for i := 0; i < numSecrets; i++ {
secret, err := GenerateSecret(32)
if err != nil {
t.Fatalf("GenerateSecret failed: %v", err)
}
if secrets[secret] {
t.Errorf("duplicate secret generated: %s", secret)
}
secrets[secret] = true
}
if len(secrets) != numSecrets {
t.Errorf("expected %d unique secrets, got %d", numSecrets, len(secrets))
}
}
// Test: NewManager creates manager successfully
func TestNewManager(t *testing.T) {
m := NewManager()
if m == nil || m.yq == nil {
t.Fatal("NewManager returned nil or Manager.yq is nil")
}
}
// Test: EnsureSecretsFile creates secrets file with proper structure and permissions
func TestEnsureSecretsFile(t *testing.T) {
const instanceName = "test-instance"
tests := []struct {
name string
setupFunc func(t *testing.T, dataDir string)
wantErr bool
errContains string
}{
{
name: "creates secrets file when not exists",
setupFunc: nil,
wantErr: false,
},
{
name: "returns nil when secrets file exists",
setupFunc: func(t *testing.T, dataDir string) {
secretsPath := filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml")
if err := os.MkdirAll(filepath.Dir(secretsPath), 0755); err != nil {
t.Fatalf("setup mkdir failed: %v", err)
}
content := `# Wild Cloud Instance Secrets
cluster:
talosSecrets: ""
kubeconfig: ""
certManager:
cloudflare:
apiToken: ""
`
if err := storage.WriteFile(secretsPath, []byte(content), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
},
wantErr: false,
},
{
name: "corrects permissions on existing file",
setupFunc: func(t *testing.T, dataDir string) {
secretsPath := filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml")
if err := os.MkdirAll(filepath.Dir(secretsPath), 0755); err != nil {
t.Fatalf("setup mkdir failed: %v", err)
}
content := `# Wild Cloud Instance Secrets
cluster:
talosSecrets: "existing-secret"
kubeconfig: ""
certManager:
cloudflare:
apiToken: ""
`
// Create with wrong permissions
if err := storage.WriteFile(secretsPath, []byte(content), 0644); err != nil {
t.Fatalf("setup failed: %v", err)
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dataDir := t.TempDir()
m := NewManager()
if tt.setupFunc != nil {
tt.setupFunc(t, dataDir)
}
err := m.EnsureSecretsFile(dataDir, instanceName)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// Verify secrets file exists
secretsPath := filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml")
if !storage.FileExists(secretsPath) {
t.Error("secrets file not created")
}
// Verify permissions are 0600 (secure)
info, err := os.Stat(secretsPath)
if err != nil {
t.Fatalf("failed to stat secrets file: %v", err)
}
if info.Mode().Perm() != 0600 {
t.Errorf("expected permissions 0600, got %o", info.Mode().Perm())
}
// Verify file has expected structure
content, err := storage.ReadFile(secretsPath)
if err != nil {
t.Fatalf("failed to read secrets: %v", err)
}
contentStr := string(content)
requiredFields := []string{"cluster:", "cloudflare:"}
for _, field := range requiredFields {
if !strings.Contains(contentStr, field) {
t.Errorf("secrets missing required field: %s", field)
}
}
})
}
}
// Test: GetSecret retrieves secrets correctly
func TestGetSecret(t *testing.T) {
tests := []struct {
name string
secretsYAML string
key string
want string
wantErr bool
errContains string
}{
{
name: "get simple string value",
secretsYAML: `cluster:
talosSecrets: "my-secret-value"
`,
key: "cluster.talosSecrets",
want: "my-secret-value",
wantErr: false,
},
{
name: "get nested value with dot notation",
secretsYAML: `certManager:
cloudflare:
apiToken: "cf-token-12345"
`,
key: "certManager.cloudflare.apiToken",
want: "cf-token-12345",
wantErr: false,
},
{
name: "get non-existent key returns error",
secretsYAML: `cluster:
talosSecrets: "value"
`,
key: "nonexistent",
wantErr: true,
errContains: "secret not found",
},
{
name: "get empty string value returns error",
secretsYAML: `cluster:
talosSecrets: ""
`,
key: "cluster.talosSecrets",
wantErr: true,
errContains: "secret not found",
},
{
name: "get null value returns error",
secretsYAML: `cluster:
talosSecrets: null
`,
key: "cluster.talosSecrets",
wantErr: true,
errContains: "secret not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
if err := storage.WriteFile(secretsPath, []byte(tt.secretsYAML), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
m := NewManager()
got, err := m.GetSecret(secretsPath, tt.key)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
// Test: GetSecret error cases
func TestGetSecret_Errors(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) string
key string
errContains string
}{
{
name: "non-existent file",
setupFunc: func(t *testing.T) string {
return filepath.Join(t.TempDir(), "nonexistent.yaml")
},
key: "cluster.talosSecrets",
errContains: "secrets file not found",
},
{
name: "malformed yaml",
setupFunc: func(t *testing.T) string {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
content := `invalid: yaml: [[[`
if err := storage.WriteFile(secretsPath, []byte(content), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
return secretsPath
},
key: "cluster.talosSecrets",
errContains: "getting secret",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secretsPath := tt.setupFunc(t)
m := NewManager()
_, err := m.GetSecret(secretsPath, tt.key)
if err == nil {
t.Error("expected error, got nil")
} else if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
}
})
}
}
// Test: GetSecret does not leak secrets in error messages
func TestGetSecret_NoSecretLeakage(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
secretValue := "super-secret-password-12345"
secretsYAML := `cluster:
talosSecrets: "` + secretValue + `"
`
if err := storage.WriteFile(secretsPath, []byte(secretsYAML), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
m := NewManager()
// Try to get a non-existent key - error should not contain actual secret values
_, err := m.GetSecret(secretsPath, "nonexistent.key")
if err == nil {
t.Fatal("expected error, got nil")
}
// Error message should not contain the secret value
if strings.Contains(err.Error(), secretValue) {
t.Errorf("error message leaked secret value: %v", err)
}
}
// Test: SetSecret sets secrets correctly
func TestSetSecret(t *testing.T) {
tests := []struct {
name string
initialYAML string
key string
value string
verifyFunc func(t *testing.T, secretsPath string)
}{
{
name: "set simple value",
initialYAML: `cluster:
talosSecrets: ""
`,
key: "cluster.talosSecrets",
value: "new-secret-value",
verifyFunc: func(t *testing.T, secretsPath string) {
m := NewManager()
got, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
if err != nil {
t.Fatalf("verify failed: %v", err)
}
if got != "new-secret-value" {
t.Errorf("got %q, want %q", got, "new-secret-value")
}
},
},
{
name: "set nested value",
initialYAML: `certManager:
cloudflare:
apiToken: ""
`,
key: "certManager.cloudflare.apiToken",
value: "cf-token-xyz",
verifyFunc: func(t *testing.T, secretsPath string) {
m := NewManager()
got, err := m.GetSecret(secretsPath, "certManager.cloudflare.apiToken")
if err != nil {
t.Fatalf("verify failed: %v", err)
}
if got != "cf-token-xyz" {
t.Errorf("got %q, want %q", got, "cf-token-xyz")
}
},
},
{
name: "update existing value",
initialYAML: `cluster:
talosSecrets: "old-secret"
`,
key: "cluster.talosSecrets",
value: "new-secret",
verifyFunc: func(t *testing.T, secretsPath string) {
m := NewManager()
got, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
if err != nil {
t.Fatalf("verify failed: %v", err)
}
if got != "new-secret" {
t.Errorf("got %q, want %q", got, "new-secret")
}
},
},
{
name: "create new nested path",
initialYAML: `cluster: {}
`,
key: "cluster.newSecret",
value: "newValue",
verifyFunc: func(t *testing.T, secretsPath string) {
m := NewManager()
got, err := m.GetSecret(secretsPath, "cluster.newSecret")
if err != nil {
t.Fatalf("verify failed: %v", err)
}
if got != "newValue" {
t.Errorf("got %q, want %q", got, "newValue")
}
},
},
{
name: "set value with special characters",
initialYAML: `cluster:
talosSecrets: ""
`,
key: "cluster.talosSecrets",
value: `special"quotes'and\backslashes`,
verifyFunc: func(t *testing.T, secretsPath string) {
m := NewManager()
got, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
if err != nil {
t.Fatalf("verify failed: %v", err)
}
if got != `special"quotes'and\backslashes` {
t.Errorf("got %q, want %q", got, `special"quotes'and\backslashes`)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
if err := storage.WriteFile(secretsPath, []byte(tt.initialYAML), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
m := NewManager()
if err := m.SetSecret(secretsPath, tt.key, tt.value); err != nil {
t.Errorf("SetSecret failed: %v", err)
return
}
// Verify the value was set correctly
tt.verifyFunc(t, secretsPath)
// Verify permissions remain secure (0600)
info, err := os.Stat(secretsPath)
if err != nil {
t.Fatalf("failed to stat secrets file: %v", err)
}
if info.Mode().Perm() != 0600 {
t.Errorf("permissions changed after SetSecret: got %o, want 0600", info.Mode().Perm())
}
})
}
}
// Test: SetSecret error cases
func TestSetSecret_Errors(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) string
key string
value string
errContains string
}{
{
name: "non-existent file",
setupFunc: func(t *testing.T) string {
return filepath.Join(t.TempDir(), "nonexistent.yaml")
},
key: "cluster.talosSecrets",
value: "secret",
errContains: "secrets file not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secretsPath := tt.setupFunc(t)
m := NewManager()
err := m.SetSecret(secretsPath, tt.key, tt.value)
if err == nil {
t.Error("expected error, got nil")
} else if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
}
})
}
}
// Test: SetSecret with concurrent access
func TestSetSecret_ConcurrentAccess(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
initialYAML := `counter: "0"
`
if err := storage.WriteFile(secretsPath, []byte(initialYAML), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
m := NewManager()
const numGoroutines = 10
var wg sync.WaitGroup
errors := make(chan error, numGoroutines)
// Launch multiple goroutines trying to write different values
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
key := "counter"
value := string(rune('0' + val))
if err := m.SetSecret(secretsPath, key, value); err != nil {
errors <- err
}
}(i)
}
wg.Wait()
close(errors)
// Check if any errors occurred
for err := range errors {
t.Errorf("concurrent write error: %v", err)
}
// Verify permissions remain secure after concurrent access
info, err := os.Stat(secretsPath)
if err != nil {
t.Fatalf("failed to stat secrets file: %v", err)
}
if info.Mode().Perm() != 0600 {
t.Errorf("permissions changed after concurrent writes: got %o, want 0600", info.Mode().Perm())
}
// Verify we can read a value (should be one of the written values)
value, err := m.GetSecret(secretsPath, "counter")
if err != nil {
t.Errorf("failed to read value after concurrent writes: %v", err)
}
if value == "" || value == "null" {
t.Error("counter value is empty after concurrent writes")
}
}
// Test: EnsureSecret generates and sets secret only when not set
func TestEnsureSecret(t *testing.T) {
tests := []struct {
name string
initialYAML string
key string
length int
expectNew bool
}{
{
name: "generates secret when empty string",
initialYAML: `cluster:
talosSecrets: ""
`,
key: "cluster.talosSecrets",
length: 32,
expectNew: true,
},
{
name: "generates secret when null",
initialYAML: `cluster:
talosSecrets: null
`,
key: "cluster.talosSecrets",
length: 32,
expectNew: true,
},
{
name: "does not generate when secret exists",
initialYAML: `cluster:
talosSecrets: "existing-secret"
`,
key: "cluster.talosSecrets",
length: 32,
expectNew: false,
},
{
name: "generates secret for non-existent key",
initialYAML: `cluster: {}
`,
key: "cluster.newSecret",
length: 64,
expectNew: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
if err := storage.WriteFile(secretsPath, []byte(tt.initialYAML), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
m := NewManager()
// Get initial value if exists
initialVal, _ := m.GetSecret(secretsPath, tt.key)
// Call EnsureSecret
secret, err := m.EnsureSecret(secretsPath, tt.key, tt.length)
if err != nil {
t.Errorf("EnsureSecret failed: %v", err)
return
}
// Verify secret is returned
if secret == "" {
t.Error("EnsureSecret returned empty secret")
}
// Verify length
if tt.expectNew && len(secret) != tt.length {
t.Errorf("expected secret length %d, got %d", tt.length, len(secret))
}
// Get final value
finalVal, err := m.GetSecret(secretsPath, tt.key)
if err != nil {
t.Fatalf("GetSecret failed: %v", err)
}
if tt.expectNew {
// Should have generated new secret
if initialVal != "" && finalVal == initialVal {
t.Errorf("expected new secret, got same value: %q", finalVal)
}
} else {
// Should have kept existing secret
if finalVal != initialVal {
t.Errorf("expected to keep existing secret %q, got %q", initialVal, finalVal)
}
}
// Call EnsureSecret again - should be idempotent
secret2, err := m.EnsureSecret(secretsPath, tt.key, tt.length)
if err != nil {
t.Errorf("second EnsureSecret failed: %v", err)
return
}
// Secret should not change on second call
if secret2 != secret {
t.Errorf("secret changed on second ensure: %q -> %q", secret, secret2)
}
})
}
}
// Test: GenerateAndStoreSecret convenience function
func TestGenerateAndStoreSecret(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
initialYAML := `cluster:
talosSecrets: ""
`
if err := storage.WriteFile(secretsPath, []byte(initialYAML), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
m := NewManager()
// Generate and store secret
secret, err := m.GenerateAndStoreSecret(secretsPath, "cluster.talosSecrets")
if err != nil {
t.Fatalf("GenerateAndStoreSecret failed: %v", err)
}
// Verify secret length matches default
if len(secret) != DefaultSecretLength {
t.Errorf("expected length %d, got %d", DefaultSecretLength, len(secret))
}
// Verify secret is stored
stored, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
if err != nil {
t.Fatalf("GetSecret failed: %v", err)
}
if stored != secret {
t.Errorf("stored secret %q does not match returned secret %q", stored, secret)
}
}
// Test: DeleteSecret removes secrets correctly
func TestDeleteSecret(t *testing.T) {
tests := []struct {
name string
initialYAML string
key string
wantErr bool
}{
{
name: "delete existing secret",
initialYAML: `cluster:
talosSecrets: "secret-to-delete"
kubeconfig: "other-secret"
`,
key: "cluster.talosSecrets",
wantErr: false,
},
{
name: "delete nested secret",
initialYAML: `certManager:
cloudflare:
apiToken: "token-to-delete"
`,
key: "certManager.cloudflare.apiToken",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "secrets.yaml")
if err := storage.WriteFile(secretsPath, []byte(tt.initialYAML), 0600); err != nil {
t.Fatalf("setup failed: %v", err)
}
m := NewManager()
// Verify secret exists before deletion
_, err := m.GetSecret(secretsPath, tt.key)
if err != nil {
t.Fatalf("secret should exist before deletion: %v", err)
}
// Delete secret
err = m.DeleteSecret(secretsPath, tt.key)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// Verify secret no longer exists
_, err = m.GetSecret(secretsPath, tt.key)
if err == nil {
t.Error("secret should not exist after deletion")
}
// Verify permissions remain secure (0600)
info, err := os.Stat(secretsPath)
if err != nil {
t.Fatalf("failed to stat secrets file: %v", err)
}
if info.Mode().Perm() != 0600 {
t.Errorf("permissions changed after DeleteSecret: got %o, want 0600", info.Mode().Perm())
}
})
}
}
// Test: DeleteSecret error cases
func TestDeleteSecret_Errors(t *testing.T) {
t.Run("non-existent file", func(t *testing.T) {
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "nonexistent.yaml")
m := NewManager()
err := m.DeleteSecret(secretsPath, "cluster.talosSecrets")
if err == nil {
t.Error("expected error, got nil")
} else if !strings.Contains(err.Error(), "secrets file not found") {
t.Errorf("error %q does not contain 'secrets file not found'", err.Error())
}
})
}
// Test: File permissions are always 0600
func TestEnsureSecretsFile_FilePermissions(t *testing.T) {
const instanceName = "test-instance"
tempDir := t.TempDir()
m := NewManager()
if err := m.EnsureSecretsFile(tempDir, instanceName); err != nil {
t.Fatalf("EnsureSecretsFile failed: %v", err)
}
secretsPath := filepath.Join(tempDir, "instances", instanceName, "config", "secrets.yaml")
info, err := os.Stat(secretsPath)
if err != nil {
t.Fatalf("failed to stat secrets file: %v", err)
}
// Verify file has 0600 permissions (read/write for owner only)
if info.Mode().Perm() != 0600 {
t.Errorf("expected permissions 0600, got %o", info.Mode().Perm())
}
}
// Test: Idempotent secrets creation
func TestEnsureSecretsFile_Idempotent(t *testing.T) {
const instanceName = "test-instance"
tempDir := t.TempDir()
m := NewManager()
// First call creates secrets
if err := m.EnsureSecretsFile(tempDir, instanceName); err != nil {
t.Fatalf("first EnsureSecretsFile failed: %v", err)
}
secretsPath := filepath.Join(tempDir, "instances", instanceName, "config", "secrets.yaml")
firstContent, err := storage.ReadFile(secretsPath)
if err != nil {
t.Fatalf("failed to read secrets: %v", err)
}
// Second call should not modify secrets
if err := m.EnsureSecretsFile(tempDir, instanceName); err != nil {
t.Fatalf("second EnsureSecretsFile failed: %v", err)
}
secondContent, err := storage.ReadFile(secretsPath)
if err != nil {
t.Fatalf("failed to read secrets: %v", err)
}
if string(firstContent) != string(secondContent) {
t.Error("secrets content changed on second call")
}
}
// Test: Secrets structure contains required fields
func TestEnsureSecretsFile_RequiredFields(t *testing.T) {
const instanceName = "test-instance"
tempDir := t.TempDir()
m := NewManager()
if err := m.EnsureSecretsFile(tempDir, instanceName); err != nil {
t.Fatalf("EnsureSecretsFile failed: %v", err)
}
secretsPath := filepath.Join(tempDir, "instances", instanceName, "config", "secrets.yaml")
content, err := storage.ReadFile(secretsPath)
if err != nil {
t.Fatalf("failed to read secrets: %v", err)
}
contentStr := string(content)
requiredFields := []string{
"cluster:",
"talosSecrets:",
"kubeconfig:",
"cloudflare:",
"token:",
}
for _, field := range requiredFields {
if !strings.Contains(contentStr, field) {
t.Errorf("secrets missing required field: %s", field)
}
}
// Verify warning comment exists
if !strings.Contains(contentStr, "WARNING") || !strings.Contains(contentStr, "sensitive") {
t.Error("secrets file missing security warning comment")
}
}
// Test: Secrets are more restrictive than config
func TestSecretsPermissions_MoreRestrictiveThanConfig(t *testing.T) {
const instanceName = "test-instance"
tempDir := t.TempDir()
secretsPath := filepath.Join(tempDir, "instances", instanceName, "config", "secrets.yaml")
configPath := filepath.Join(tempDir, "config.yaml")
// Create secrets file
m := NewManager()
if err := m.EnsureSecretsFile(tempDir, instanceName); err != nil {
t.Fatalf("EnsureSecretsFile failed: %v", err)
}
// Create config file (typically 0644)
configContent := `baseDomain: "example.com"`
if err := storage.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
// Check permissions
secretsInfo, err := os.Stat(secretsPath)
if err != nil {
t.Fatalf("failed to stat secrets: %v", err)
}
configInfo, err := os.Stat(configPath)
if err != nil {
t.Fatalf("failed to stat config: %v", err)
}
secretsPerm := secretsInfo.Mode().Perm()
configPerm := configInfo.Mode().Perm()
// Secrets (0600) should be more restrictive than config (0644)
if secretsPerm >= configPerm {
t.Errorf("secrets permissions %o should be more restrictive than config %o", secretsPerm, configPerm)
}
// Secrets should not be group or world readable
if secretsPerm&0077 != 0 {
t.Errorf("secrets file should not have group/world permissions, got %o", secretsPerm)
}
}