diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index d40da1e..c4fb45d 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -120,6 +120,8 @@ var destroyTalosVMs = func(terraformDir string, environment []string) error { return utils.RunCommandInDirEnv(terraformDir, environment, "terraform", "destroy", "-auto-approve", "-input=false", "-target=proxmox_virtual_environment_vm.vm") } +var runTerraform = utils.RunCommandInDirEnv + var webhookTargetTimeout = 70 * time.Minute var webhookTargetPollInterval = 2 * time.Second @@ -1280,7 +1282,7 @@ func ensureLifecycleIdentity(terraformDir string, cfg config.Config) error { func (r Runner) reconcileTerraform(terraformDir string) error { environment := []string{"TF_VAR_proxmox_api_token=" + r.Config.Talos.Proxmox.APITokenID + "=" + r.Config.Talos.Proxmox.APITokenSecret} - if err := utils.RunCommandInDirEnv(terraformDir, environment, "terraform", "init", "-input=false"); err != nil { + if err := runTerraform(terraformDir, environment, "terraform", "init", "-input=false"); err != nil { return err } if err := importNetworkBridges(terraformDir, environment, r.Config); err != nil { @@ -1290,19 +1292,28 @@ func (r Runner) reconcileTerraform(terraformDir string) error { if err := r.rebuildTalosVMs(terraformDir, environment); err != nil { return err } + } else if err := r.importConfiguredTalosVMs(terraformDir, environment); err != nil { + return err } planPath, err := terraformPlanPath(terraformDir, r.Config.ClusterID) if err != nil { return err } defer os.Remove(planPath) - if err := utils.RunCommandInDirEnv(terraformDir, environment, "terraform", "plan", "-input=false", "-out="+planPath); err != nil { + if err := runTerraform(terraformDir, environment, "terraform", "plan", "-input=false", "-out="+planPath); err != nil { return err } - return utils.RunCommandInDirEnv(terraformDir, environment, "terraform", "apply", "-input=false", "-auto-approve", planPath) + return runTerraform(terraformDir, environment, "terraform", "apply", "-input=false", "-auto-approve", planPath) } func (r Runner) rebuildTalosVMs(terraformDir string, environment []string) error { + if err := r.importConfiguredTalosVMs(terraformDir, environment); err != nil { + return err + } + return destroyTalosVMs(terraformDir, environment) +} + +func (r Runner) importConfiguredTalosVMs(terraformDir string, environment []string) error { if err := verifyTalosVMs(r.Config); err != nil { return err } @@ -1323,7 +1334,7 @@ func (r Runner) rebuildTalosVMs(terraformDir string, environment []string) error if err := verifyStateTalosVMs(resources, r.Config, true); err != nil { return err } - return destroyTalosVMs(terraformDir, environment) + return nil } type terraformImport struct { @@ -1400,7 +1411,7 @@ func importTerraformResources(terraformDir string, environment []string, imports if resources[resource.Address] { continue } - if err := utils.RunCommandInDirEnv(terraformDir, environment, "terraform", "import", "-input=false", resource.Address, resource.ID); err != nil { + if err := runTerraform(terraformDir, environment, "terraform", "import", "-input=false", resource.Address, resource.ID); err != nil { return fmt.Errorf("import Terraform resource: %w", err) } } diff --git a/internal/bootstrap/bootstrap_test.go b/internal/bootstrap/bootstrap_test.go index e6df307..b11332f 100644 --- a/internal/bootstrap/bootstrap_test.go +++ b/internal/bootstrap/bootstrap_test.go @@ -946,6 +946,65 @@ func TestVerifyStateTalosVMsRejectsForeignAndMissingVMs(t *testing.T) { } } +func TestReconcileTerraformImportsConfiguredTalosVMsBeforePlan(t *testing.T) { + originalVerify := verifyTalosVMs + originalStateList := runTerraformStateList + originalRun := runTerraform + t.Cleanup(func() { + verifyTalosVMs = originalVerify + runTerraformStateList = originalStateList + runTerraform = originalRun + }) + + 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(got config.Config) error { + if len(got.Talos.Nodes) != 1 || got.Talos.Nodes[0].Name != "cp-01" || got.Talos.Nodes[0].ProxmoxNode != "pve" || got.Talos.Nodes[0].VMID != 100 { + t.Fatal("Talos VM identity verification used the wrong config") + } + events = append(events, "verify") + return nil + } + runTerraformStateList = func(string, []string) ([]byte, []byte, error) { + return []byte(strings.Join(state, "\n")), nil, 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": + if len(args) != 4 || args[2] != importedAddress || args[3] != "pve/100" { + t.Fatalf("unexpected Talos VM import: %q", args) + } + events = append(events, "import") + state = append(state, importedAddress) + case "plan": + if len(state) != 1 || state[0] != importedAddress { + return errors.New("VM create collision") + } + 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: Reconcile}).reconcileTerraform(t.TempDir()); err != nil { + t.Fatal(err) + } + if strings.Join(events, ",") != "init,verify,import,plan,apply" { + t.Fatalf("Terraform phase order = %q", events) + } +} + func TestEnsureLifecycleIdentityStoresMetadataUnderGeneratedDirectory(t *testing.T) { repo := t.TempDir() terraformDir := filepath.Join(repo, "terraform")