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:
973
internal/secrets/secrets_test.go
Normal file
973
internal/secrets/secrets_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user