package bootstrap import ( "encoding/base64" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" "time" "github.com/Pingu-Studio/MaidnCLI/internal/cloudflare" "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/Pingu-Studio/MaidnCLI/internal/forgejo" "github.com/Pingu-Studio/MaidnCLI/internal/openbao" "gopkg.in/yaml.v3" ) 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}\nproduction: ${PRODUCTION_BRANCH}\ncatalog: ${TEKTON_CATALOG_REPO_URL}\nref: ${TEKTON_CATALOG_REPO_REF}\n"), 0644); err != nil { t.Fatal(err) } cfg := config.Config{Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "user-org"}, Flux: config.FluxConfig{TektonCatalogRepo: "my-tekton-catalog"}, Delivery: config.DeliveryConfig{ProductionBranch: "production", WebhookHostname: "tekton.example.test", WebhookPath: "/hooks/forgejo"}, Templates: config.TemplateConfig{TektonCatalogRepoRef: "release"}} 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") || !strings.Contains(string(content), "production: production") || !strings.Contains(string(content), "https://git.example.test/user-org/my-tekton-catalog.git") || !strings.Contains(string(content), "ref: release") { t.Fatalf("delivery configuration was not rendered: %s", content) } } func TestGeneratedDeliveryIsGenericAndUsesSafePreviewCleanupContract(t *testing.T) { cfg := config.Config{ Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"}, Flux: config.FluxConfig{Branch: "main", ManifestsRepo: "manifests"}, Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildOutputDirectory: "dist/web-ui", BuildConfiguration: "production"}, } content, err := renderAppDelivery(cfg) if err != nil { t.Fatal(err) } for _, expected := range []string{"maidn-node-static-image", "maidn-preview-orphan-reconciler", "valid_pr_number()", "valid_commit()", "values: [promotion]", "values: [\"production\"]", "cmp -s \"$expected_marker\" \"$marker\"", "values: [closed]"} { if !strings.Contains(string(content), expected) { t.Fatalf("generated delivery does not contain %q", expected) } } if strings.Contains(string(content), "easycsr") || strings.Contains(string(content), "test-org") || strings.Contains(string(content), "git rm -r") { t.Fatal("generated delivery contains a non-generic or unsafe literal") } } func TestGenerateAppDeliveryRequiresCompleteConfig(t *testing.T) { dir := t.TempDir() err := GenerateAppDelivery(dir, config.Config{Git: config.GitConfig{BaseURL: "https://git.example.test"}, Delivery: config.DeliveryConfig{AppName: "legacy-app"}}) if err == nil || !strings.Contains(err.Error(), "delivery appName") { t.Fatalf("GenerateAppDelivery() error = %v, want incomplete delivery error", err) } if _, statErr := os.Stat(filepath.Join(dir, ".tekton")); !os.IsNotExist(statErr) { t.Fatal("GenerateAppDelivery() wrote delivery files before rejecting incomplete config") } } func TestWritePreviewDeliveryConfigIsTrustedAndNonSecret(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte("resources:\n"), 0644); err != nil { t.Fatal(err) } cfg := config.Config{Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform", Token: "secret"}, Flux: config.FluxConfig{ManifestsRepo: "manifests", Branch: "main"}, Delivery: config.DeliveryConfig{AppName: "dynamic-app"}} if err := writePreviewDeliveryConfig(dir, cfg); err != nil { t.Fatal(err) } content, err := os.ReadFile(filepath.Join(dir, "maidn-preview-delivery-config.yaml")) if err != nil || !strings.Contains(string(content), "forgejo-origin: https://git.example.test") || strings.Contains(string(content), "forgejo-base-url") || !strings.Contains(string(content), "manifests-url: https://git.example.test/platform/manifests.git") || strings.Contains(string(content), "secret") || strings.Contains(string(content), "dynamic-app") { t.Fatalf("preview delivery config is not trusted and non-secret: %q, %v", content, err) } } func TestWritePreviewDeliveryConfigRejectsAmbiguousKustomization(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n"), 0644); err != nil { t.Fatal(err) } err := writePreviewDeliveryConfig(dir, config.Config{Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"}, Flux: config.FluxConfig{ManifestsRepo: "manifests", Branch: "main"}}) if err == nil || !strings.Contains(err.Error(), "must define resources") { t.Fatalf("ambiguous Tekton Kustomization was accepted: %v", err) } } func TestCopyAndRenderDeliveryBasesOverwritesExistingMigrationOutput(t *testing.T) { templateDir := t.TempDir() repoDir := t.TempDir() files := []struct { base, name, template, want string }{ {"gateway", "route.yaml", "host: ${WEBHOOK_HOSTNAME}\npath: ${WEBHOOK_PATH}\n", "host: tekton.example.test\npath: /hooks/forgejo\n"}, {"tekton", "catalog-source.yaml", "url: ${TEKTON_CATALOG_REPO_URL}\nref: ${TEKTON_CATALOG_REPO_REF}\n", "url: https://git.example.test/user-org/my-tekton-catalog.git\nref: release\n"}, {"tekton-triggers", "trigger.yaml", "app: ${APP_NAME}\nrepo: ${APP_REPO_URL}\n", "app: demo\nrepo: https://git.example.test/demo.git\n"}, } for _, file := range files { templatePath := filepath.Join(templateDir, "base", file.base, file.name) outputPath := filepath.Join(repoDir, "base", file.base, file.name) if err := os.MkdirAll(filepath.Dir(templatePath), 0755); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(templatePath, []byte(file.template), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(outputPath, []byte("stale: output\n"), 0644); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(templateDir, "base", "tekton", "kustomization.yaml"), []byte("resources:\n"), 0644); err != nil { t.Fatal(err) } customGatewayFile := filepath.Join(repoDir, "base", "gateway", "custom.yaml") if err := os.WriteFile(customGatewayFile, []byte("custom: route\n"), 0644); err != nil { t.Fatal(err) } cfg := config.Config{ Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "user-org"}, Flux: config.FluxConfig{TektonCatalogRepo: "my-tekton-catalog", ManifestsRepo: "manifests", Branch: "main"}, Delivery: config.DeliveryConfig{AppName: "demo", AppRepoURL: "https://git.example.test/demo.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/demo", BuildOutputDirectory: "dist", BuildConfiguration: "production", WebhookHostname: "tekton.example.test", WebhookPath: "/hooks/forgejo"}, Templates: config.TemplateConfig{TektonCatalogRepoRef: "release"}, } if err := copyAndRenderDeliveryBases(templateDir, repoDir, cfg); err != nil { t.Fatal(err) } for _, file := range files { content, err := os.ReadFile(filepath.Join(repoDir, "base", file.base, file.name)) if err != nil || string(content) != file.want { t.Fatalf("%s was not copied and rendered: %q, %v", file.base, content, err) } } content, err := os.ReadFile(customGatewayFile) if err != nil || string(content) != "custom: route\n" { t.Fatalf("custom gateway file was not preserved: %q, %v", content, err) } } func TestCopyTemplateBaseComponentsRefreshesCNPGAndPreservesGeneratedSecrets(t *testing.T) { templateDir := t.TempDir() repoDir := t.TempDir() for _, component := range templateBaseComponents { if err := os.MkdirAll(filepath.Join(templateDir, "base", component), 0755); err != nil { t.Fatal(err) } } cnpgSource := filepath.Join(templateDir, "base", "cnpg", "kustomization.yaml") cnpgDestination := filepath.Join(repoDir, "base", "cnpg", "kustomization.yaml") if err := os.WriteFile(cnpgSource, []byte("resources:\n - operator-kustomization.yaml\n - infrastructure-kustomization.yaml\n"), 0644); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Dir(cnpgDestination), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(cnpgDestination, []byte("resources:\n - release.yaml\n - infrastructure-postgres.yaml\n"), 0644); err != nil { t.Fatal(err) } cloudflareSource := filepath.Join(templateDir, "base", "cloudflare-tunnel", "deployment.yaml") cloudflareDestination := filepath.Join(repoDir, "base", "cloudflare-tunnel", "deployment.yaml") if err := os.WriteFile(cloudflareSource, []byte("command: cloudflared\n"), 0644); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Dir(cloudflareDestination), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(cloudflareDestination, []byte("command: stale\n"), 0644); err != nil { t.Fatal(err) } operatorSource := filepath.Join(templateDir, "base", "cnpg", "operator", "kustomization.yaml") if err := os.MkdirAll(filepath.Dir(operatorSource), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(operatorSource, []byte("resources:\n - release.yaml\n"), 0644); err != nil { t.Fatal(err) } for _, file := range []string{"democratic-csi/secret.sops.yaml", "openbao/unseal.sops.yaml"} { if err := os.WriteFile(filepath.Join(templateDir, "base", file), []byte("sops: template\n"), 0600); err != nil { t.Fatal(err) } destination := filepath.Join(repoDir, "base", file) if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(destination, []byte("sops: generated\n"), 0600); err != nil { t.Fatal(err) } } if err := copyTemplateBaseComponents(templateDir, repoDir, true); err != nil { t.Fatal(err) } content, err := os.ReadFile(cnpgDestination) if err != nil || string(content) != "resources:\n - operator-kustomization.yaml\n - infrastructure-kustomization.yaml\n" { t.Fatalf("CNPG Kustomization was not refreshed: %q, %v", content, err) } content, err = os.ReadFile(filepath.Join(repoDir, "base", "cnpg", "operator", "kustomization.yaml")) if err != nil || string(content) != "resources:\n - release.yaml\n" { t.Fatalf("CNPG operator child was not copied: %q, %v", content, err) } content, err = os.ReadFile(cloudflareDestination) if err != nil || string(content) != "command: cloudflared\n" { t.Fatalf("Cloudflare Tunnel deployment was not refreshed: %q, %v", content, err) } for _, file := range []string{"democratic-csi/secret.sops.yaml", "openbao/unseal.sops.yaml"} { content, err := os.ReadFile(filepath.Join(repoDir, "base", file)) if err != nil || string(content) != "sops: generated\n" { t.Fatalf("generated SOPS file %q was overwritten: %q, %v", file, content, err) } } } func TestCopyAndRenderCiliumBasesRefreshesTemplateWithoutLeavingPlaceholders(t *testing.T) { templateDir := t.TempDir() repoDir := t.TempDir() files := map[string]string{ "cilium/release.yaml": "apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nspec:\n values:\n ipam:\n mode: kubernetes\n", "cilium-config/load-balancer-pool.yaml": "start: ${CILIUM_LB_START}\nstop: ${CILIUM_LB_END}\n", "cilium-config/l2-policy.yaml": "interface: ${CILIUM_TRAFFIC_INTERFACE}\n", } for name, content := range files { path := filepath.Join(templateDir, "base", name) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } } for _, base := range []string{"cilium", "cilium-config"} { if err := os.MkdirAll(filepath.Join(repoDir, "base", base), 0755); err != nil { t.Fatal(err) } } stale := filepath.Join(repoDir, "base", "cilium-config", "load-balancer-pool.yaml") if err := os.WriteFile(stale, []byte("start: stale\n"), 0644); err != nil { t.Fatal(err) } cfg := config.Config{Flux: config.FluxConfig{ClusterDomain: "dev02.nid3.com"}, Cilium: config.CiliumConfig{TrafficInterface: "eth1", LoadBalancerStart: "192.168.45.19", LoadBalancerEnd: "192.168.45.30"}} if err := copyAndRenderCiliumBases(templateDir, repoDir, cfg); err != nil { t.Fatal(err) } for name, want := range map[string]string{ "cilium-config/load-balancer-pool.yaml": "start: 192.168.45.19\nstop: 192.168.45.30\n", "cilium-config/l2-policy.yaml": "interface: eth1\n", } { content, err := os.ReadFile(filepath.Join(repoDir, "base", name)) if err != nil || string(content) != want { t.Fatalf("Cilium file %q was not refreshed and rendered: %q, %v", name, content, err) } } release, err := os.ReadFile(filepath.Join(repoDir, "base", "cilium", "release.yaml")) if err != nil || !strings.Contains(string(release), "clusterDomain: dev02.nid3.com") || strings.Contains(string(release), "cluster.local") { t.Fatalf("Cilium Hubble peer service domain was not rendered: %q, %v", release, err) } } 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), "labels:\n reconcile.fluxcd.io/watch: Enabled") || !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 TestWriteOpenBaoUnsealSecretRendersRecoveryMaterial(t *testing.T) { originalRead := readOpenBaoRecovery originalWrite := writeGeneratedSOPS t.Cleanup(func() { readOpenBaoRecovery = originalRead writeGeneratedSOPS = originalWrite }) readOpenBaoRecovery = func(identityPath, bundlePath string) (openbao.RecoveryMaterial, error) { if identityPath != "recovery-identity" || bundlePath != "recovery-bundle" { t.Fatal("OpenBao recovery paths were not passed to the decryption boundary") } return openbao.RecoveryMaterial{RootToken: "test-root", UnsealThreshold: 2, UnsealKeysB64: []string{"test-share-1", "test-share-2", "test-share-3"}}, nil } var rendered []byte writeGeneratedSOPS = func(path, ageKeyPath string, plaintext []byte) error { if ageKeyPath != "flux-age-identity" { t.Fatal("OpenBao unseal secret used the wrong Flux age identity") } rendered = append([]byte(nil), plaintext...) return os.WriteFile(path, []byte("sops: {}\n"), 0600) } if err := writeOpenBaoUnsealSecret(filepath.Join(t.TempDir(), "unseal.sops.yaml"), "recovery-identity", "recovery-bundle", "flux-age-identity"); err != nil { t.Fatal(err) } var secret struct { Metadata map[string]string `yaml:"metadata"` StringData map[string]string `yaml:"stringData"` } if err := yaml.Unmarshal(rendered, &secret); err != nil || secret.Metadata["name"] != "openbao-unseal" || secret.Metadata["namespace"] != "openbao" || len(secret.StringData) != 4 || secret.StringData["root-token"] == "" || secret.StringData["unseal-3"] == "" { t.Fatal("OpenBao unseal Secret was not rendered") } } func TestWriteOpenBaoUnsealSecretIsIdempotent(t *testing.T) { originalRead := readOpenBaoRecovery originalDecrypt := decryptGeneratedSOPS originalWrite := writeGeneratedSOPS t.Cleanup(func() { readOpenBaoRecovery = originalRead decryptGeneratedSOPS = originalDecrypt writeGeneratedSOPS = originalWrite }) material := openbao.RecoveryMaterial{RootToken: "test-root", UnsealThreshold: 2, UnsealKeysB64: []string{"test-share-1", "test-share-2"}} plaintext, err := renderOpenBaoUnsealSecret(material) if err != nil { t.Fatal(err) } readOpenBaoRecovery = func(string, string) (openbao.RecoveryMaterial, error) { return material, nil } decryptGeneratedSOPS = func(string, string) ([]byte, error) { return plaintext, nil } writes := 0 writeGeneratedSOPS = func(string, string, []byte) error { writes++ return nil } path := filepath.Join(t.TempDir(), "unseal.sops.yaml") if err := os.WriteFile(path, []byte("sops: {}\n"), 0600); err != nil { t.Fatal(err) } if err := writeOpenBaoUnsealSecret(path, "recovery-identity", "recovery-bundle", "flux-age-identity"); err != nil { t.Fatal(err) } if writes != 0 { t.Fatal("matching OpenBao unseal Secret was re-encrypted") } } func TestWriteOpenBaoUnsealSecretRejectsUnreadableExistingSecret(t *testing.T) { originalRead := readOpenBaoRecovery originalDecrypt := decryptGeneratedSOPS originalWrite := writeGeneratedSOPS t.Cleanup(func() { readOpenBaoRecovery = originalRead decryptGeneratedSOPS = originalDecrypt writeGeneratedSOPS = originalWrite }) readOpenBaoRecovery = func(string, string) (openbao.RecoveryMaterial, error) { return openbao.RecoveryMaterial{RootToken: "test-root", UnsealThreshold: 1, UnsealKeysB64: []string{"test-share"}}, nil } decryptGeneratedSOPS = func(string, string) ([]byte, error) { return nil, errors.New("unavailable") } writes := 0 writeGeneratedSOPS = func(string, string, []byte) error { writes++ return nil } path := filepath.Join(t.TempDir(), "unseal.sops.yaml") if err := os.WriteFile(path, []byte("sops: {}\n"), 0600); err != nil { t.Fatal(err) } if err := writeOpenBaoUnsealSecret(path, "recovery-identity", "recovery-bundle", "flux-age-identity"); err == nil || writes != 0 { t.Fatal("unreadable OpenBao unseal Secret was overwritten") } } func TestEnsureOpenBaoUnsealKustomizationIsIdempotent(t *testing.T) { path := filepath.Join(t.TempDir(), "kustomization.yaml") if err := os.WriteFile(path, []byte("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n - namespace.yaml\n"), 0644); err != nil { t.Fatal(err) } if err := ensureOpenBaoUnsealKustomization(path); err != nil { t.Fatal(err) } first, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if err := ensureOpenBaoUnsealKustomization(path); err != nil { t.Fatal(err) } second, err := os.ReadFile(path) if err != nil || !strings.Contains(string(first), " - unseal.sops.yaml\n") || string(first) != string(second) { t.Fatal("OpenBao Kustomization was not updated idempotently") } } 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 TestCopyDirPreservesExistingDeploymentStateWithoutOverwrite(t *testing.T) { source := t.TempDir() destination := t.TempDir() for path, content := range map[string]string{ "apps/production/app.yaml": "template: current\n", "apps/staging/app.yaml": "template: new\n", } { file := filepath.Join(source, path) if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(file, []byte(content), 0644); err != nil { t.Fatal(err) } } existing := filepath.Join(destination, "apps", "production", "app.yaml") if err := os.MkdirAll(filepath.Dir(existing), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(existing, []byte("deployment: existing\n"), 0644); err != nil { t.Fatal(err) } if err := copyDir(source, destination, false); err != nil { t.Fatal(err) } content, err := os.ReadFile(existing) if err != nil || string(content) != "deployment: existing\n" { t.Fatalf("existing deployment state was overwritten: %q, %v", content, err) } content, err = os.ReadFile(filepath.Join(destination, "apps", "staging", "app.yaml")) if err != nil || string(content) != "template: new\n" { t.Fatalf("new template file 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 := os.WriteFile(filepath.Join(dir, "apps", "staging", "kustomization.yaml"), []byte("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n"), 0644); 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) } content, err = os.ReadFile(filepath.Join(dir, "apps", "staging", "kustomization.yaml")) if err != nil || !strings.Contains(string(content), "resources:") { t.Fatalf("staging Kustomization was not repaired: %q, %v", content, err) } } func TestEnsureClusterKustomizationsUsesTemplateExternalSecretsResource(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "kustomization.yaml") if err := os.WriteFile(path, []byte("resources:\n"), 0644); err != nil { t.Fatal(err) } if err := ensureClusterKustomizations(dir, true); err != nil { t.Fatal(err) } content, err := os.ReadFile(path) if err != nil || !strings.Contains(string(content), "snapshot-crds-kustomization.yaml") || !strings.Contains(string(content), "democratic-csi-kustomization.yaml") || !strings.Contains(string(content), "openbao-kustomization.yaml") || !strings.Contains(string(content), "external-secrets-kustomization.yaml") || !strings.Contains(string(content), "cnpg-kustomization.yaml") || !strings.Contains(string(content), "cloudflare-tunnel-kustomization.yaml") || !strings.Contains(string(content), "tekton-kustomization.yaml") || strings.Contains(string(content), "external-secrets-config-kustomization.yaml") { t.Fatalf("cluster Kustomization was not updated: %q, %v", content, err) } } func TestWebhookTargetTimeoutExceedsExternalSecretRefreshInterval(t *testing.T) { if webhookTargetTimeout <= time.Hour { t.Fatal("webhook target timeout must exceed the one-hour ExternalSecret refresh interval") } } func TestRunnerRegisterWebhookSkipsTemplateRevisions(t *testing.T) { originalPreflight := preflight originalGit := runGit originalInitialize := initializeOpenBao originalCommand := runWebhookCommand originalWebhook := ensureForgejoWebhook t.Cleanup(func() { preflight = originalPreflight runGit = originalGit initializeOpenBao = originalInitialize runWebhookCommand = originalCommand ensureForgejoWebhook = originalWebhook }) workspace := t.TempDir() ageKeyPath := filepath.Join(workspace, "age-key.txt") if err := os.WriteFile(ageKeyPath, nil, 0600); err != nil { t.Fatal(err) } preflight = func(config.Config) error { return nil } runGit = func(string, ...string) ([]byte, error) { t.Fatal("webhook-only reconciliation must not access template repositories") return nil, nil } authorization := "Bearer test-webhook-authorization" initializeOpenBao = func(string, string, string, string, string, string) (map[string]map[string]string, error) { return map[string]map[string]string{"cicd/forgejo-webhook": {"authorization": authorization}}, nil } runWebhookCommand = func(_ string, _ string, args ...string) ([]byte, error) { if strings.Contains(strings.Join(args, " "), "secret/forgejo-webhook") { return []byte(base64.StdEncoding.EncodeToString([]byte(authorization))), nil } return nil, nil } ensureForgejoWebhook = func(_ config.Config, _, _, observedAuthorization string) error { if observedAuthorization != authorization { t.Fatal("webhook reconciliation used the wrong authorization") } return nil } cfg := config.Config{ ClusterID: "test-cluster", WorkspaceDir: workspace, Git: config.GitConfig{Provider: "forgejo", BaseURL: "https://git.example.test", Username: "bot", Token: "token", Owner: "test-org", CloneParent: filepath.Join(workspace, "checkouts")}, Flux: config.FluxConfig{RepoName: "cluster", Branch: "main", ClusterPath: "./clusters/test", ClusterDomain: "example.test", ManifestsRepo: "manifests"}, Templates: config.TemplateConfig{TalosRepoURL: "https://git.example.test/talos.git", TalosRepoRef: "main", CICDRepoURL: "https://git.example.test/template.git", CICDRepoRef: "main", ManifestsRepoURL: "https://git.example.test/manifests.git", ManifestsRepoRef: "main"}, Cilium: config.CiliumConfig{TrafficInterface: "eth1", LoadBalancerStart: "192.168.45.19", LoadBalancerEnd: "192.168.45.30"}, DemocraticCSI: config.DemocraticCSIConfig{TrueNASAPIKey: "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"}, Delivery: config.DeliveryConfig{AppName: "app", AppRepoURL: "https://git.example.test/app.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/test/app", WebhookHostname: "tekton.example.test", WebhookPath: "/"}, SOPS: config.SOPSConfig{AgeKeyPath: ageKeyPath}, Talos: config.TalosConfig{ RepoDirName: "talos", TerraformDir: "terraform", GeneratedDir: "generated", ConfigFileName: "terraform.tfvars", Proxmox: config.TalosProxmoxConfig{APIURL: "https://proxmox.example.test:8006", APITokenID: "id", APITokenSecret: "secret"}, Cluster: config.TalosClusterConfig{Name: "test-cluster", Domain: "example.test"}, Image: config.TalosImageConfig{TalosVersion: "v1.13.6", KubernetesVersion: "v1.33.4", SchematicID: "abcdefghijkl"}, Nodes: []config.TalosNode{{Name: "cp-01", VMID: 100, Role: "controlplane", Networks: []config.TalosNetwork{{IP: "192.168.45.3", CIDR: "192.168.45.0/28", Gateway: "192.168.45.1", VLANID: 45}, {IP: "192.168.45.18", CIDR: "192.168.45.16/28", VLANID: 451}}}}, }, } partial := cfg partial.Delivery.ImageRepository = "" preflight = func(config.Config) error { t.Fatal("webhook registration accepted incomplete delivery config") return nil } if err := (Runner{Config: partial, RegisterWebhook: true}).Run(); err == nil || !strings.Contains(err.Error(), "imageRepository") { t.Fatalf("webhook registration error = %v, want incomplete delivery error", err) } preflight = func(config.Config) error { return nil } if err := (Runner{Config: cfg, RegisterWebhook: true}).Run(); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(workspace, "maidn-template-revisions.yaml")); !os.IsNotExist(err) { t.Fatal("webhook-only reconciliation created a template revision lock") } } func TestReconcileWebhookWaitsForRefreshedTargetSecret(t *testing.T) { originalInitialize := initializeOpenBao originalCommand := runWebhookCommand originalWebhook := ensureForgejoWebhook originalTimeout := webhookTargetTimeout originalInterval := webhookTargetPollInterval t.Cleanup(func() { initializeOpenBao = originalInitialize runWebhookCommand = originalCommand ensureForgejoWebhook = originalWebhook webhookTargetTimeout = originalTimeout webhookTargetPollInterval = originalInterval }) authorization := "Bearer test-webhook-authorization" staleTarget := base64.StdEncoding.EncodeToString([]byte("Bearer stale-webhook-authorization")) refreshedTarget := base64.StdEncoding.EncodeToString([]byte(authorization)) targetChecks := 0 refreshedObserved := false runWebhookCommand = func(_ string, name string, args ...string) ([]byte, error) { if name != "kubectl" || strings.Contains(strings.Join(args, " "), authorization) { t.Fatal("webhook target probe used an unexpected command") } if strings.Contains(strings.Join(args, " "), "secret/forgejo-webhook") { targetChecks++ if targetChecks == 1 { return []byte(staleTarget), nil } refreshedObserved = true return []byte(refreshedTarget), nil } return nil, nil } webhookTargetPollInterval = 0 patches := 0 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if !refreshedObserved { t.Error("Forgejo was called before the refreshed webhook Secret was observed") writer.WriteHeader(http.StatusInternalServerError) return } switch request.Method { case http.MethodGet: _ = json.NewEncoder(writer).Encode([]map[string]any{{"id": 7, "url": "https://tekton.example.test/"}}) case http.MethodPatch: var body struct { AuthorizationHeader string `json:"authorization_header"` } if err := json.NewDecoder(request.Body).Decode(&body); err != nil || body.AuthorizationHeader != authorization { t.Error("Forgejo webhook update did not use the observed authorization") writer.WriteHeader(http.StatusBadRequest) return } patches++ writer.WriteHeader(http.StatusOK) default: t.Error("unexpected Forgejo request") writer.WriteHeader(http.StatusMethodNotAllowed) } })) defer server.Close() ensureForgejoWebhook = func(_ config.Config, repo, webhookURL, authorization string) error { manager := forgejo.NewRepoManager(server.URL, "test-token", "owner", "user", "manifests", "flux", "main", "migration") manager.HTTPClient = server.Client() return manager.EnsureWebhook(repo, webhookURL, authorization) } initializeOpenBao = func(string, string, string, string, string, string) (map[string]map[string]string, error) { return map[string]map[string]string{"cicd/forgejo-webhook": {"authorization": authorization}}, nil } cfg := config.Config{Delivery: config.DeliveryConfig{AppName: "app", WebhookHostname: "tekton.example.test", WebhookPath: "/"}} if err := (Runner{Config: cfg}).reconcileWebhook(t.TempDir()); err != nil { t.Fatal(err) } if targetChecks != 2 || patches != 1 { t.Fatal("Forgejo webhook was not updated after the target Secret refreshed") } } func TestReconcileWebhookTimeoutDoesNotUpdateForgejo(t *testing.T) { originalInitialize := initializeOpenBao originalCommand := runWebhookCommand originalWebhook := ensureForgejoWebhook originalTimeout := webhookTargetTimeout originalInterval := webhookTargetPollInterval t.Cleanup(func() { initializeOpenBao = originalInitialize runWebhookCommand = originalCommand ensureForgejoWebhook = originalWebhook webhookTargetTimeout = originalTimeout webhookTargetPollInterval = originalInterval }) authorization := "Bearer test-webhook-authorization" initializeOpenBao = func(string, string, string, string, string, string) (map[string]map[string]string, error) { return map[string]map[string]string{"cicd/forgejo-webhook": {"authorization": authorization}}, nil } runWebhookCommand = func(_ string, _ string, _ ...string) ([]byte, error) { return []byte(base64.StdEncoding.EncodeToString([]byte("Bearer stale-webhook-authorization"))), nil } webhookTargetTimeout = -time.Nanosecond webhookTargetPollInterval = 0 webhookUpdated := false ensureForgejoWebhook = func(config.Config, string, string, string) error { webhookUpdated = true return nil } err := (Runner{Config: config.Config{Delivery: config.DeliveryConfig{AppName: "app"}}}).reconcileWebhook(t.TempDir()) if err == nil || !strings.Contains(err.Error(), "Forgejo webhook was not updated") || !strings.Contains(err.Error(), "--register-webhook") { t.Fatal("webhook refresh timeout did not return a safe rerun error") } if webhookUpdated { t.Fatal("Forgejo webhook update was attempted before the target Secret refreshed") } } 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) } } func TestVerifyStateTalosVMsRejectsForeignAndMissingVMs(t *testing.T) { cfg := config.Config{Talos: config.TalosConfig{Nodes: []config.TalosNode{{Name: "cp-01", ProxmoxNode: "pve", VMID: 100}, {Name: "worker-01", ProxmoxNode: "pve", VMID: 101}}}} if err := verifyStateTalosVMs([]string{`proxmox_virtual_environment_vm.vm["cp-01"]`, `proxmox_virtual_environment_vm.vm["foreign"]`}, cfg, false); err == nil || !strings.Contains(err.Error(), "unconfigured") { t.Fatalf("foreign VM state was accepted: %v", err) } if err := verifyStateTalosVMs([]string{`proxmox_virtual_environment_vm.vm["cp-01"]`}, cfg, true); err == nil || !strings.Contains(err.Error(), "missing configured") { t.Fatalf("incomplete VM state was accepted: %v", err) } } func TestEnsureLifecycleIdentityStoresMetadataUnderGeneratedDirectory(t *testing.T) { repo := t.TempDir() terraformDir := filepath.Join(repo, "terraform") if err := os.MkdirAll(terraformDir, 0755); err != nil { t.Fatal(err) } cfg := config.Config{ClusterID: "test", Talos: config.TalosConfig{GeneratedDir: "generated", Cluster: config.TalosClusterConfig{Name: "test"}}} if err := ensureLifecycleIdentity(terraformDir, cfg); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(repo, "generated", ".maidn", "lifecycle.yaml")); err != nil { t.Fatalf("lifecycle metadata was not generated: %v", err) } if _, err := os.Stat(filepath.Join(terraformDir, ".maidn", "lifecycle.yaml")); !os.IsNotExist(err) { t.Fatal("lifecycle metadata dirtied the Terraform template directory") } } func TestReconcileCloudflareTunnelValidatesManagedState(t *testing.T) { originalRead := readCloudflareOperationalSecrets t.Cleanup(func() { readCloudflareOperationalSecrets = originalRead }) values, err := (cloudflare.StoredTunnel{ Credentials: cloudflare.Credentials{AccountTag: "account", TunnelSecret: "secret", TunnelID: "tunnel"}, Config: cloudflare.NewConfig("tunnel"), }).Values() if err != nil { t.Fatal(err) } readCloudflareOperationalSecrets = func(string, string) (map[string]map[string]string, error) { return map[string]map[string]string{"platform/cloudflare-tunnel": values}, nil } if err := (Runner{Config: config.Config{SOPS: config.SOPSConfig{OperationalSecretsPath: "secrets", AgeKeyPath: "age"}}}).reconcileCloudflareTunnel(); err != nil { t.Fatal(err) } } func TestReconcileCloudflareTunnelMigratesDottedStateAtomically(t *testing.T) { originalRead := readCloudflareOperationalSecrets originalWrite := writeCloudflareOperationalSecrets t.Cleanup(func() { readCloudflareOperationalSecrets = originalRead writeCloudflareOperationalSecrets = originalWrite }) values, err := (cloudflare.StoredTunnel{ Credentials: cloudflare.Credentials{AccountTag: "account", TunnelSecret: "secret", TunnelID: "tunnel"}, Config: cloudflare.NewConfig("tunnel"), }).Values() if err != nil { t.Fatal(err) } secrets := map[string]map[string]string{ "platform/cloudflare-tunnel": {"credentials.json": values["credentials"], "config.yml": values["config"]}, "platform/pihole": {"password": "preserved"}, } readCloudflareOperationalSecrets = func(string, string) (map[string]map[string]string, error) { return secrets, nil } writes := 0 writeCloudflareOperationalSecrets = func(path, ageKeyPath string, updated map[string]map[string]string) error { if path != "secrets" || ageKeyPath != "age" || updated["platform/pihole"]["password"] != "preserved" { t.Fatal("dotted-state migration did not use the encrypted operational state boundary") } secrets = updated writes++ return nil } if err := (Runner{Config: config.Config{SOPS: config.SOPSConfig{OperationalSecretsPath: "secrets", AgeKeyPath: "age"}}}).reconcileCloudflareTunnel(); err != nil { t.Fatal(err) } _, present, legacy, err := cloudflare.ParseStoredTunnelState(secrets["platform/cloudflare-tunnel"]) if err != nil || !present || legacy || writes != 1 || len(secrets["platform/cloudflare-tunnel"]) != 2 { t.Fatal("dotted Cloudflare tunnel state was not rewritten to simple keys") } } func TestReconcileCloudflareTunnelRequiresImportForAbsentOrLegacyState(t *testing.T) { originalRead := readCloudflareOperationalSecrets t.Cleanup(func() { readCloudflareOperationalSecrets = originalRead }) for _, state := range []map[string]map[string]string{ nil, {"platform/cloudflare-tunnel": {"token": "legacy-run-token"}}, } { readCloudflareOperationalSecrets = func(string, string) (map[string]map[string]string, error) { return state, nil } err := (Runner{}).reconcileCloudflareTunnel() if err == nil || !strings.Contains(err.Error(), "cloudflare-tunnel import") || strings.Contains(err.Error(), "legacy-run-token") { t.Fatal("missing Cloudflare tunnel state did not return a safe import instruction") } } } func TestReconcileCloudflareTunnelRejectsMalformedState(t *testing.T) { originalRead := readCloudflareOperationalSecrets t.Cleanup(func() { readCloudflareOperationalSecrets = originalRead }) readCloudflareOperationalSecrets = func(string, string) (map[string]map[string]string, error) { return map[string]map[string]string{"platform/cloudflare-tunnel": {"token": "legacy-run-token", "unexpected": "value"}}, nil } err := (Runner{}).reconcileCloudflareTunnel() if err == nil || strings.Contains(err.Error(), "cloudflare-tunnel import") || strings.Contains(err.Error(), "legacy-run-token") { t.Fatal("malformed Cloudflare tunnel state was not rejected safely") } }