package bootstrap import ( "encoding/base64" "encoding/json" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/Pingu-Studio/MaidnCLI/internal/config" ) func TestRenderCiliumConfig(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "values.yaml") if err := os.WriteFile(path, []byte("host: ${CILIUM_K8S_SERVICE_HOST}\ninterface: ${CILIUM_TRAFFIC_INTERFACE}\nstart: ${CILIUM_LB_START}\nend: ${CILIUM_LB_END}\n"), 0644); err != nil { t.Fatal(err) } cfg := config.Config{Talos: config.TalosConfig{KubeconfigEndpoint: "192.168.45.3"}, Cilium: config.CiliumConfig{TrafficInterface: "eth1", LoadBalancerStart: "192.168.45.19", LoadBalancerEnd: "192.168.45.30"}} if err := renderCiliumConfig(dir, cfg); err != nil { t.Fatal(err) } content, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if strings.Contains(string(content), "${") || !strings.Contains(string(content), "192.168.45.30") { t.Fatalf("Cilium configuration was not rendered: %s", content) } } func TestRenderDeliveryConfig(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "webhook.yaml") if err := os.WriteFile(path, []byte("host: ${WEBHOOK_HOSTNAME}\npath: ${WEBHOOK_PATH}\n"), 0644); err != nil { t.Fatal(err) } cfg := config.Config{Delivery: config.DeliveryConfig{WebhookHostname: "tekton.example.test", WebhookPath: "/hooks/forgejo"}} if err := renderDeliveryConfig(dir, cfg); err != nil { t.Fatal(err) } content, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if strings.Contains(string(content), "${") || !strings.Contains(string(content), "/hooks/forgejo") { t.Fatalf("delivery configuration was not rendered: %s", content) } } func TestRenderDemocraticCSISecret(t *testing.T) { secret, err := renderDemocraticCSISecret(config.DemocraticCSIConfig{TrueNASAPIKey: "api-key", TrueNASHost: "truenas.example.test", TargetPortal: "truenas.example.test:3260", ShareHost: "truenas.example.test", DatasetParentNFS: "pool/kubernetes/nfs/v", DatasetSnapshotsNFS: "pool/kubernetes/nfs/s", AllowedNetworks: "192.168.45.0/24", NameSuffix: "-test", PortalGroup: "1", InitiatorGroup: "1"}) if err != nil { t.Fatal(err) } if !strings.Contains(string(secret), "name: democratic-csi-secrets") || !strings.Contains(string(secret), "dataset-parent-nfs: pool/kubernetes/nfs/v") { t.Fatalf("Democratic CSI secret was not rendered: %s", secret) } } func TestWriteDemocraticCSISecretEncryptsValues(t *testing.T) { if _, err := exec.LookPath("age-keygen"); err != nil { t.Skip("age-keygen is required for bootstrap encryption") } if _, err := exec.LookPath("sops"); err != nil { t.Skip("sops is required for bootstrap encryption") } dir := t.TempDir() identity := filepath.Join(dir, "age-key.txt") if err := exec.Command("age-keygen", "-o", identity).Run(); err != nil { t.Fatal(err) } secretPath := filepath.Join(dir, "secret.sops.yaml") csi := config.DemocraticCSIConfig{TrueNASAPIKey: "test-api-key", TrueNASHost: "truenas.example.test", TargetPortal: "truenas.example.test:3260", ShareHost: "truenas.example.test", DatasetParentNFS: "pool/kubernetes/nfs/v", DatasetSnapshotsNFS: "pool/kubernetes/nfs/s", AllowedNetworks: "192.168.45.0/24", NameSuffix: "-test", PortalGroup: "1", InitiatorGroup: "1"} if err := writeDemocraticCSISecret(secretPath, csi, identity); err != nil { t.Fatal(err) } encrypted, err := os.ReadFile(secretPath) if err != nil { t.Fatal(err) } if strings.Contains(string(encrypted), csi.TrueNASAPIKey) || !strings.Contains(string(encrypted), "sops:") { t.Fatal("Democratic CSI secret was not SOPS encrypted") } if err := writeDemocraticCSISecret(secretPath, csi, identity); err != nil { t.Fatal(err) } unchanged, err := os.ReadFile(secretPath) if err != nil { t.Fatal(err) } if string(encrypted) != string(unchanged) { t.Fatal("Democratic CSI secret was re-encrypted without a configuration change") } } func TestNewWebhookAuthorization(t *testing.T) { authorization, err := NewWebhookAuthorization() if err != nil { t.Fatal(err) } if !strings.HasPrefix(authorization, "Bearer ") || len(authorization) <= len("Bearer ") { t.Fatalf("invalid webhook authorization") } } func TestForgejoRegistryDockerConfig(t *testing.T) { dockerConfig, err := ForgejoRegistryDockerConfig("registry.example.test/team/app", "registry-user", "registry-token") if err != nil { t.Fatal(err) } var document struct { Auths map[string]struct { Auth string `json:"auth"` } `json:"auths"` } if err := json.Unmarshal([]byte(dockerConfig), &document); err != nil { t.Fatal(err) } if document.Auths["registry.example.test"].Auth != base64.StdEncoding.EncodeToString([]byte("registry-user:registry-token")) { t.Fatal("Forgejo registry Docker config has the wrong credentials") } } func TestUpsertOperationalSecretPreservesExistingSecrets(t *testing.T) { if _, err := exec.LookPath("age-keygen"); err != nil { t.Skip("age-keygen is required for bootstrap encryption") } if _, err := exec.LookPath("sops"); err != nil { t.Skip("sops is required for bootstrap encryption") } dir := t.TempDir() identity := filepath.Join(dir, "age-key.txt") if err := exec.Command("age-keygen", "-o", identity).Run(); err != nil { t.Fatal(err) } path := filepath.Join(dir, "operational-secrets.sops.yaml") if err := WriteOperationalSecrets(path, identity, map[string]map[string]string{ "cicd/forgejo": {"token": "existing-token"}, "platform/cloudflare": {"api-token": "existing-api-token"}, "cicd/forgejo-registry": {"dockerconfigjson": "old-config"}, }); err != nil { t.Fatal(err) } if err := UpsertOperationalSecret(path, identity, "cicd/forgejo-registry", "dockerconfigjson", "new-config"); err != nil { t.Fatal(err) } encrypted, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if strings.Contains(string(encrypted), "existing-token") || strings.Contains(string(encrypted), "new-config") || !strings.Contains(string(encrypted), "sops:") { t.Fatal("operational secret upsert was not SOPS encrypted") } secrets, err := ReadOperationalSecrets(path, identity) if err != nil { t.Fatal(err) } if secrets["cicd/forgejo"]["token"] != "existing-token" || secrets["platform/cloudflare"]["api-token"] != "existing-api-token" || secrets["cicd/forgejo-registry"]["dockerconfigjson"] != "new-config" { t.Fatal("operational secret upsert did not preserve unrelated secrets") } } func TestInitializeOpenBaoUsesGeneratedKubeconfig(t *testing.T) { original := initializeOpenBao defer func() { initializeOpenBao = original }() var kubeconfig string initializeOpenBao = func(path, recipient, identityPath, bundlePath, ageKeyPath, operationalSecretsPath string) (map[string]map[string]string, error) { kubeconfig = path if recipient != "recipient" || identityPath != "identity" || bundlePath != "bundle" || ageKeyPath != "age" || operationalSecretsPath != "secrets" { t.Fatal("InitializeOpenBao passed incorrect configured paths") } return nil, nil } cfg := config.Config{ Git: config.GitConfig{CloneParent: "checkout"}, Talos: config.TalosConfig{RepoDirName: "talos", GeneratedDir: "generated"}, SOPS: config.SOPSConfig{RecoveryRecipient: "recipient", RecoveryIdentityPath: "identity", RecoveryBundlePath: "bundle", AgeKeyPath: "age", OperationalSecretsPath: "secrets"}, } if err := InitializeOpenBao(cfg); err != nil { t.Fatal(err) } if kubeconfig != filepath.Join("checkout", "talos", "generated", "kubeconfig") { t.Fatal("InitializeOpenBao did not use the configured generated kubeconfig") } initializeOpenBao = func(string, string, string, string, string, string) (map[string]map[string]string, error) { return nil, errors.New("unavailable") } if err := InitializeOpenBao(cfg); err == nil { t.Fatal("InitializeOpenBao accepted a seeding error") } } func TestCopyDirSkipsGitDirectory(t *testing.T) { source := t.TempDir() destination := t.TempDir() if err := os.Mkdir(filepath.Join(source, ".git"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(source, ".git", "config"), []byte("private"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("template"), 0644); err != nil { t.Fatal(err) } if err := copyDir(source, destination, false); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(destination, ".git")); !os.IsNotExist(err) { t.Fatal("copied template must not contain its Git metadata") } content, err := os.ReadFile(filepath.Join(destination, "README.md")) if err != nil || string(content) != "template" { t.Fatalf("template content was not copied: %q, %v", content, err) } } func TestEnsureManifestsKustomizations(t *testing.T) { dir := t.TempDir() for _, environment := range []string{"previews", "staging", "production"} { if err := os.MkdirAll(filepath.Join(dir, "apps", environment), 0755); err != nil { t.Fatal(err) } } if err := ensureManifestsKustomizations(dir); err != nil { t.Fatal(err) } content, err := os.ReadFile(filepath.Join(dir, "apps", "previews", "kustomization.yaml")) if err != nil || !strings.Contains(string(content), "resources:") { t.Fatalf("preview Kustomization was not created: %q, %v", content, err) } } func TestRenderTerraformTFVarsIsStableAndRedactsToken(t *testing.T) { cfg := config.Config{ ClusterID: "test", Talos: config.TalosConfig{ Proxmox: config.TalosProxmoxConfig{APIURL: "https://proxmox.test", APITokenID: "id", APITokenSecret: "secret", NodeInterfaces: map[string]string{"b": "eno2", "a": "eno1"}}, Cluster: config.TalosClusterConfig{Name: "test", Domain: "test", DiskStorage: "local", AdditionalStorage: "local"}, Image: config.TalosImageConfig{TalosVersion: "v1.13.6", KubernetesVersion: "v1.33.4", SchematicID: "abcdefghijkl", Architecture: "amd64"}, }, } first := renderTerraformTFVars(cfg) second := renderTerraformTFVars(cfg) if first != second || strings.Contains(first, "secret") || strings.Index(first, `"a"`) > strings.Index(first, `"b"`) { t.Fatalf("Terraform rendering is not deterministic or redacted: %s", first) } } func TestTerraformPlanPathIsAbsolute(t *testing.T) { path, err := terraformPlanPath(filepath.Join("maidn-workspace", "terraform"), "cluster") if err != nil || !filepath.IsAbs(path) { t.Fatalf("Terraform plan path is not absolute: %q, %v", path, err) } }