diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index c4fb45d..d059faf 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -1300,12 +1300,25 @@ func (r Runner) reconcileTerraform(terraformDir string) error { return err } defer os.Remove(planPath) - if err := runTerraform(terraformDir, environment, "terraform", "plan", "-input=false", "-out="+planPath); err != nil { + planArgs := []string{"plan", "-input=false"} + if r.Mode == Reconcile { + planArgs = append(planArgs, terraformReconcileTargets(r.Config)...) + } + planArgs = append(planArgs, "-out="+planPath) + if err := runTerraform(terraformDir, environment, "terraform", planArgs...); err != nil { return err } return runTerraform(terraformDir, environment, "terraform", "apply", "-input=false", "-auto-approve", planPath) } +func terraformReconcileTargets(cfg config.Config) []string { + targets := []string{"-target=proxmox_virtual_environment_download_file.talos_iso"} + if cfg.Talos.Cluster.ManageNetworkBridges { + targets = append(targets, "-target=proxmox_virtual_environment_network_linux_bridge.cluster_bridge") + } + return targets +} + func (r Runner) rebuildTalosVMs(terraformDir string, environment []string) error { if err := r.importConfiguredTalosVMs(terraformDir, environment); err != nil { return err diff --git a/internal/bootstrap/bootstrap_test.go b/internal/bootstrap/bootstrap_test.go index b11332f..bcf17ff 100644 --- a/internal/bootstrap/bootstrap_test.go +++ b/internal/bootstrap/bootstrap_test.go @@ -911,6 +911,17 @@ func TestTerraformPlanPathIsAbsolute(t *testing.T) { } } +func TestTerraformReconcileTargetsStageImageAndManagedBridgesOnly(t *testing.T) { + if targets := strings.Join(terraformReconcileTargets(config.Config{}), " "); targets != "-target=proxmox_virtual_environment_download_file.talos_iso" { + t.Fatalf("default reconcile targets = %q", targets) + } + cfg := config.Config{Talos: config.TalosConfig{Cluster: config.TalosClusterConfig{ManageNetworkBridges: true}}} + targets := strings.Join(terraformReconcileTargets(cfg), " ") + if !strings.Contains(targets, "-target=proxmox_virtual_environment_download_file.talos_iso") || !strings.Contains(targets, "-target=proxmox_virtual_environment_network_linux_bridge.cluster_bridge") || strings.Contains(targets, "proxmox_virtual_environment_vm.vm") { + t.Fatalf("managed bridge reconcile targets = %q", targets) + } +} + func TestTerraformStateResourcesAllowsANSINoStateFile(t *testing.T) { original := runTerraformStateList t.Cleanup(func() { runTerraformStateList = original }) @@ -988,8 +999,14 @@ func TestReconcileTerraformImportsConfiguredTalosVMsBeforePlan(t *testing.T) { if len(state) != 1 || state[0] != importedAddress { return errors.New("VM create collision") } + if strings.Contains(strings.Join(args, " "), "proxmox_virtual_environment_vm.vm") || !strings.Contains(strings.Join(args, " "), "-target=proxmox_virtual_environment_download_file.talos_iso") { + t.Fatalf("reconcile plan did not exclude the Talos VM: %q", args) + } events = append(events, "plan") case "apply": + if strings.Contains(strings.Join(args, " "), "proxmox_virtual_environment_vm.vm") { + t.Fatalf("reconcile apply did not exclude the Talos VM: %q", args) + } events = append(events, "apply") default: t.Fatalf("unexpected Terraform operation %q", args[0]) @@ -1005,6 +1022,65 @@ func TestReconcileTerraformImportsConfiguredTalosVMsBeforePlan(t *testing.T) { } } +func TestRebuildTerraformRetainsFullTalosVMLifecycle(t *testing.T) { + originalVerify := verifyTalosVMs + originalStateList := runTerraformStateList + originalRun := runTerraform + originalDestroy := destroyTalosVMs + t.Cleanup(func() { + verifyTalosVMs = originalVerify + runTerraformStateList = originalStateList + 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"}}, + }} + importedAddress := talosVMImports(cfg)[0].Address + var events, state []string + verifyTalosVMs = func(config.Config) error { + events = append(events, "verify") + return nil + } + runTerraformStateList = func(string, []string) ([]byte, []byte, error) { + return []byte(strings.Join(state, "\n")), nil, nil + } + destroyTalosVMs = func(_ string, _ []string) error { + events = append(events, "destroy") + 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": + events = append(events, "init") + case "import": + state = append(state, importedAddress) + events = append(events, "import") + case "plan": + if strings.Contains(strings.Join(args, " "), "-target=") { + t.Fatalf("rebuild plan must retain the full VM lifecycle: %q", args) + } + events = append(events, "plan") + case "apply": + events = append(events, "apply") + 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,verify,import,destroy,plan,apply" { + t.Fatalf("Terraform phase order = %q", events) + } +} + func TestEnsureLifecycleIdentityStoresMetadataUnderGeneratedDirectory(t *testing.T) { repo := t.TempDir() terraformDir := filepath.Join(repo, "terraform")