From 54a2ae283877e6d6f97d4c1e0158acf7bc5a55f0 Mon Sep 17 00:00:00 2001 From: eding Date: Sun, 6 Sep 2026 09:35:05 +0200 Subject: [PATCH 1/5] fix: recover externally removed Talos VMs --- internal/bootstrap/bootstrap.go | 4 +++ internal/bootstrap/bootstrap_test.go | 44 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index ce06890..6afab1d 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -1406,6 +1406,10 @@ func terraformReconcileTargets(cfg config.Config) []string { func (r Runner) rebuildTalosVMs(terraformDir string, environment []string) error { if err := r.importConfiguredTalosVMs(terraformDir, environment); err != nil { + var apiErr proxmox.APIError + if errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + return nil + } return err } return destroyTalosVMs(terraformDir, environment) diff --git a/internal/bootstrap/bootstrap_test.go b/internal/bootstrap/bootstrap_test.go index 688f4eb..2d33897 100644 --- a/internal/bootstrap/bootstrap_test.go +++ b/internal/bootstrap/bootstrap_test.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" "net/http" "net/http/httptest" "os" @@ -17,6 +18,7 @@ import ( "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/Pingu-Studio/MaidnCLI/internal/forgejo" "github.com/Pingu-Studio/MaidnCLI/internal/openbao" + "github.com/Pingu-Studio/MaidnCLI/internal/proxmox" "gopkg.in/yaml.v3" ) @@ -1278,6 +1280,48 @@ func TestRebuildTerraformRetainsFullTalosVMLifecycle(t *testing.T) { } } +func TestRebuildTerraformSkipsAlreadyAbsentTalosVM(t *testing.T) { + originalVerify := verifyTalosVMs + originalRun := runTerraform + originalDestroy := destroyTalosVMs + t.Cleanup(func() { + verifyTalosVMs = originalVerify + runTerraform = originalRun + destroyTalosVMs = originalDestroy + }) + + cfg := config.Config{ClusterID: "test-cluster", Talos: config.TalosConfig{ + Nodes: []config.TalosNode{{Name: "cp-01", ProxmoxNode: "pve", VMID: 100, Role: "controlplane"}}, + }} + var events []string + verifyTalosVMs = func(config.Config) error { + return fmt.Errorf("inspect configured Talos VM: %w", proxmox.APIError{StatusCode: 404}) + } + destroyTalosVMs = func(string, []string) error { + t.Fatal("destroy ran for an already-absent VM") + return nil + } + runTerraform = func(_ string, _ []string, name string, args ...string) error { + if name != "terraform" { + t.Fatalf("unexpected command %q", name) + } + switch args[0] { + case "init", "plan", "apply": + events = append(events, args[0]) + default: + t.Fatalf("unexpected Terraform operation %q", args[0]) + } + return nil + } + + if err := (Runner{Config: cfg, Mode: Rebuild}).reconcileTerraform(t.TempDir()); err != nil { + t.Fatal(err) + } + if strings.Join(events, ",") != "init,plan,apply" { + t.Fatalf("Terraform phase order = %q", events) + } +} + func TestEnsureLifecycleIdentityStoresMetadataUnderGeneratedDirectory(t *testing.T) { repo := t.TempDir() terraformDir := filepath.Join(repo, "terraform") -- 2.43.7 From 62540c69a2dce928f3daeec3fe1f04a5293108d8 Mon Sep 17 00:00:00 2001 From: eding Date: Sun, 6 Sep 2026 09:38:46 +0200 Subject: [PATCH 2/5] fix: detect absent Proxmox VM response --- internal/bootstrap/bootstrap.go | 14 +++---- internal/bootstrap/bootstrap_test.go | 60 ++++++++++++++++++---------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 6afab1d..b493f7c 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -40,12 +40,12 @@ const ( ) type Runner struct { - Config config.Config - Mode Mode - ConfirmRebuild bool - RegisterWebhook bool - EnableDelivery bool - SkipDeliveryScaffolding bool + Config config.Config + Mode Mode + ConfirmRebuild bool + RegisterWebhook bool + EnableDelivery bool + SkipDeliveryScaffolding bool AutoMergeBootstrapMigration bool } @@ -1407,7 +1407,7 @@ func terraformReconcileTargets(cfg config.Config) []string { func (r Runner) rebuildTalosVMs(terraformDir string, environment []string) error { if err := r.importConfiguredTalosVMs(terraformDir, environment); err != nil { var apiErr proxmox.APIError - if errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + if errors.As(err, &apiErr) && (apiErr.StatusCode == 404 || apiErr.StatusCode == 500 && apiErr.Message == `{"data":null}` && strings.HasSuffix(apiErr.Path, "/config")) { return nil } return err diff --git a/internal/bootstrap/bootstrap_test.go b/internal/bootstrap/bootstrap_test.go index 2d33897..c930b6b 100644 --- a/internal/bootstrap/bootstrap_test.go +++ b/internal/bootstrap/bootstrap_test.go @@ -1294,31 +1294,47 @@ func TestRebuildTerraformSkipsAlreadyAbsentTalosVM(t *testing.T) { Nodes: []config.TalosNode{{Name: "cp-01", ProxmoxNode: "pve", VMID: 100, Role: "controlplane"}}, }} var events []string + for _, apiErr := range []proxmox.APIError{{StatusCode: 404}, {StatusCode: 500, Message: `{"data":null}`, Path: "/nodes/pve/qemu/100/config"}} { + verifyTalosVMs = func(config.Config) error { + return fmt.Errorf("inspect configured Talos VM: %w", apiErr) + } + destroyTalosVMs = func(string, []string) error { + t.Fatal("destroy ran for an already-absent VM") + return nil + } + runTerraform = func(_ string, _ []string, name string, args ...string) error { + if name != "terraform" { + t.Fatalf("unexpected command %q", name) + } + switch args[0] { + case "init", "plan", "apply": + events = append(events, args[0]) + default: + t.Fatalf("unexpected Terraform operation %q", args[0]) + } + return nil + } + + if err := (Runner{Config: cfg, Mode: Rebuild}).reconcileTerraform(t.TempDir()); err != nil { + t.Fatal(err) + } + if strings.Join(events, ",") != "init,plan,apply" { + t.Fatalf("Terraform phase order = %q", events) + } + events = nil + } +} + +func TestRebuildTerraformRejectsUnexpectedProxmoxError(t *testing.T) { + originalVerify := verifyTalosVMs + t.Cleanup(func() { verifyTalosVMs = originalVerify }) verifyTalosVMs = func(config.Config) error { - return fmt.Errorf("inspect configured Talos VM: %w", proxmox.APIError{StatusCode: 404}) - } - destroyTalosVMs = func(string, []string) error { - t.Fatal("destroy ran for an already-absent VM") - return nil - } - runTerraform = func(_ string, _ []string, name string, args ...string) error { - if name != "terraform" { - t.Fatalf("unexpected command %q", name) - } - switch args[0] { - case "init", "plan", "apply": - events = append(events, args[0]) - default: - t.Fatalf("unexpected Terraform operation %q", args[0]) - } - return nil + return fmt.Errorf("inspect configured Talos VM: %w", proxmox.APIError{StatusCode: 500, Message: "server unavailable", Path: "/nodes/pve/qemu/100/config"}) } - if err := (Runner{Config: cfg, Mode: Rebuild}).reconcileTerraform(t.TempDir()); err != nil { - t.Fatal(err) - } - if strings.Join(events, ",") != "init,plan,apply" { - t.Fatalf("Terraform phase order = %q", events) + err := (Runner{Config: config.Config{Talos: config.TalosConfig{Nodes: []config.TalosNode{{Name: "cp-01", ProxmoxNode: "pve", VMID: 100}}}}, Mode: Rebuild}).rebuildTalosVMs(t.TempDir(), nil) + if err == nil || !strings.Contains(err.Error(), "server unavailable") { + t.Fatalf("unexpected Proxmox error was accepted: %v", err) } } -- 2.43.7 From 214b00720a6c3c89e5dec8294e705e6a0da7319c Mon Sep 17 00:00:00 2001 From: eding Date: Sun, 6 Sep 2026 09:51:05 +0200 Subject: [PATCH 3/5] fix: defer webhook refresh until Tekton exists --- internal/openbao/bootstrap.go | 7 +++++++ internal/openbao/bootstrap_test.go | 21 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/openbao/bootstrap.go b/internal/openbao/bootstrap.go index 7cdf029..5a061dc 100644 --- a/internal/openbao/bootstrap.go +++ b/internal/openbao/bootstrap.go @@ -330,6 +330,13 @@ func refreshExternalSecrets(kubeconfig string) error { if err != nil { return fmt.Errorf("refresh OpenBao secret store after seed: %w", err) } + webhook, err := kubectlOutput(kubeconfig, "get", "externalsecret", "forgejo-webhook", "-n", "tekton-pipelines", "--ignore-not-found", "-o=name") + if err != nil { + return fmt.Errorf("check Forgejo webhook ExternalSecret after OpenBao seed: %w", err) + } + if strings.TrimSpace(string(webhook)) == "" { + return nil + } _, err = kubectlOutput(kubeconfig, externalSecretRefreshArgs(timestamp)...) if err != nil { return fmt.Errorf("refresh ExternalSecrets after OpenBao seed: %w", err) diff --git a/internal/openbao/bootstrap_test.go b/internal/openbao/bootstrap_test.go index 4bc7f90..49ae22b 100644 --- a/internal/openbao/bootstrap_test.go +++ b/internal/openbao/bootstrap_test.go @@ -84,13 +84,32 @@ func TestRefreshExternalSecretsIsReadyGatedAndScoped(t *testing.T) { if len(calls) == 1 { return []byte("True"), nil } + if len(calls) == 3 { + return []byte("externalsecret.external-secrets.io/forgejo-webhook"), nil + } return nil, nil } - if err := refreshExternalSecrets("kubeconfig"); err != nil || len(calls) != 3 || !strings.Contains(calls[0], "get deployment/external-secrets") || !strings.Contains(calls[1], "annotate clustersecretstore openbao") || !strings.Contains(calls[2], "annotate externalsecret forgejo-webhook") || strings.Contains(calls[1], "--all") || strings.Contains(calls[2], "--all") { + if err := refreshExternalSecrets("kubeconfig"); err != nil || len(calls) != 4 || !strings.Contains(calls[0], "get deployment/external-secrets") || !strings.Contains(calls[1], "annotate clustersecretstore openbao") || !strings.Contains(calls[2], "get externalsecret forgejo-webhook") || !strings.Contains(calls[3], "annotate externalsecret forgejo-webhook") || strings.Contains(calls[1], "--all") || strings.Contains(calls[3], "--all") { t.Fatalf("ExternalSecret refresh was not readiness-gated and scoped: %q, %v", calls, err) } } +func TestRefreshExternalSecretsSkipsWebhookBeforeTekton(t *testing.T) { + original := kubectlOutput + t.Cleanup(func() { kubectlOutput = original }) + var calls []string + kubectlOutput = func(_ string, args ...string) ([]byte, error) { + calls = append(calls, strings.Join(args, " ")) + if len(calls) == 1 { + return []byte("True"), nil + } + return nil, nil + } + if err := refreshExternalSecrets("kubeconfig"); err != nil || len(calls) != 3 || !strings.Contains(calls[2], "--ignore-not-found") { + t.Fatalf("missing webhook ExternalSecret was not safely skipped: %q, %v", calls, err) + } +} + func TestReadRecoveryMaterialDecryptsAndValidatesBundle(t *testing.T) { original := decryptRecovery t.Cleanup(func() { decryptRecovery = original }) -- 2.43.7 From 8e4672010bfd21fbba433139c0dcbf36af1daa67 Mon Sep 17 00:00:00 2001 From: eding Date: Sun, 6 Sep 2026 09:59:14 +0200 Subject: [PATCH 4/5] fix: publish app from external source checkout --- cmd/bootstrap.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cmd/bootstrap.go b/cmd/bootstrap.go index 4179a9d..3992831 100644 --- a/cmd/bootstrap.go +++ b/cmd/bootstrap.go @@ -143,13 +143,6 @@ func runBootstrap(cmd *cobra.Command, args []string) error { if err := ensurePublishAppCheckoutClean(bootstrapPublishAppFrom); err != nil { return err } - origin, err := forgejo.CheckoutOrigin(bootstrapPublishAppFrom) - if err != nil { - return err - } - if config.RedactURL(origin) != cfg.Delivery.AppRepoURL { - return fmt.Errorf("--publish-app-from origin does not match delivery appRepoUrl") - } branch, err := forgejo.CurrentBranch(bootstrapPublishAppFrom) if err != nil { return err -- 2.43.7 From 50989db2fe10a5824b9b44bee2751cd82bc44aca Mon Sep 17 00:00:00 2001 From: eding Date: Sun, 6 Sep 2026 10:02:51 +0200 Subject: [PATCH 5/5] fix: replace delivery contract on import branch --- internal/forgejo/repo.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/forgejo/repo.go b/internal/forgejo/repo.go index c65eb9d..4bb5922 100644 --- a/internal/forgejo/repo.go +++ b/internal/forgejo/repo.go @@ -465,9 +465,6 @@ func (rm *RepoManager) PublishDeliveryBranch(sourceDir, sourceBranch, repoURL, d if err := runGit("", os.Environ(), "clone", "--no-local", "--branch", sourceBranch, sourceDir, temporary); err != nil { return err } - if err := generate(temporary); err != nil { - return err - } cleanupAskPass, environment, err := rm.gitEnvironment() if err != nil { return err @@ -476,6 +473,12 @@ func (rm *RepoManager) PublishDeliveryBranch(sourceDir, sourceBranch, repoURL, d if err := runGit(temporary, environment, "checkout", "-B", deliveryBranch); err != nil { return err } + if err := os.RemoveAll(filepath.Join(temporary, ".tekton")); err != nil { + return err + } + if err := generate(temporary); err != nil { + return err + } if err := runGit(temporary, environment, "add", ".tekton"); err != nil { return err } -- 2.43.7