Merge pull request 'fix: handle ANSI Terraform empty state' (#18) from fix/terraform-no-state-ansi into main
Reviewed-on: #18
This commit is contained in:
commit
8b26e1422b
|
|
@ -110,19 +110,18 @@ func listTerraformStateResources(terraformDir string, environment []string) ([]s
|
||||||
return strings.Fields(string(state)), nil
|
return strings.Fields(string(state)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ansiEscapeSequence = regexp.MustCompile(`\x1b\[[0-?]*[ -/]*[@-~]`)
|
||||||
|
|
||||||
func terraformNoStateFile(stderr []byte) bool {
|
func terraformNoStateFile(stderr []byte) bool {
|
||||||
for _, line := range strings.Split(string(stderr), "\n") {
|
return strings.Contains(strings.ToLower(ansiEscapeSequence.ReplaceAllString(string(stderr), "")), "no state file")
|
||||||
if strings.TrimSpace(line) == "No state file was found!" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var destroyTalosVMs = func(terraformDir string, environment []string) error {
|
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")
|
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 webhookTargetTimeout = 70 * time.Minute
|
||||||
|
|
||||||
var webhookTargetPollInterval = 2 * time.Second
|
var webhookTargetPollInterval = 2 * time.Second
|
||||||
|
|
@ -1283,7 +1282,7 @@ func ensureLifecycleIdentity(terraformDir string, cfg config.Config) error {
|
||||||
|
|
||||||
func (r Runner) reconcileTerraform(terraformDir string) 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}
|
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
|
return err
|
||||||
}
|
}
|
||||||
if err := importNetworkBridges(terraformDir, environment, r.Config); err != nil {
|
if err := importNetworkBridges(terraformDir, environment, r.Config); err != nil {
|
||||||
|
|
@ -1293,19 +1292,41 @@ func (r Runner) reconcileTerraform(terraformDir string) error {
|
||||||
if err := r.rebuildTalosVMs(terraformDir, environment); err != nil {
|
if err := r.rebuildTalosVMs(terraformDir, environment); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if err := r.importConfiguredTalosVMs(terraformDir, environment); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
planPath, err := terraformPlanPath(terraformDir, r.Config.ClusterID)
|
planPath, err := terraformPlanPath(terraformDir, r.Config.ClusterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer os.Remove(planPath)
|
defer os.Remove(planPath)
|
||||||
if err := utils.RunCommandInDirEnv(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 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 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 {
|
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 {
|
if err := verifyTalosVMs(r.Config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -1326,7 +1347,7 @@ func (r Runner) rebuildTalosVMs(terraformDir string, environment []string) error
|
||||||
if err := verifyStateTalosVMs(resources, r.Config, true); err != nil {
|
if err := verifyStateTalosVMs(resources, r.Config, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return destroyTalosVMs(terraformDir, environment)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type terraformImport struct {
|
type terraformImport struct {
|
||||||
|
|
@ -1403,7 +1424,7 @@ func importTerraformResources(terraformDir string, environment []string, imports
|
||||||
if resources[resource.Address] {
|
if resources[resource.Address] {
|
||||||
continue
|
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)
|
return fmt.Errorf("import Terraform resource: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -911,11 +911,22 @@ func TestTerraformPlanPathIsAbsolute(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTerraformStateResourcesAllowsNoStateFile(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
|
original := runTerraformStateList
|
||||||
t.Cleanup(func() { runTerraformStateList = original })
|
t.Cleanup(func() { runTerraformStateList = original })
|
||||||
runTerraformStateList = func(string, []string) ([]byte, []byte, error) {
|
runTerraformStateList = func(string, []string) ([]byte, []byte, error) {
|
||||||
return nil, []byte("No state file was found!\n"), errors.New("exit status 1")
|
return nil, []byte("\x1b[31mError:\x1b[0m \x1b[31mNO STATE\x1b[0m FILE was found!\n"), errors.New("exit status 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
resources, err := listTerraformStateResources("terraform", nil)
|
resources, err := listTerraformStateResources("terraform", nil)
|
||||||
|
|
@ -946,6 +957,130 @@ 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")
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
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 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) {
|
func TestEnsureLifecycleIdentityStoresMetadataUnderGeneratedDirectory(t *testing.T) {
|
||||||
repo := t.TempDir()
|
repo := t.TempDir()
|
||||||
terraformDir := filepath.Join(repo, "terraform")
|
terraformDir := filepath.Join(repo, "terraform")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue