From dfb4609be2b251fb43124d446c272e2d73ed4ebc Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Mon, 11 Oct 2021 17:44:44 -0400 Subject: [PATCH] Backend State Migration from cloud to cloud. * Add test for tfc to tfc mgiration * Fix old tests, and remove unused code. --- go.mod | 2 +- go.sum | 4 +- internal/cloud/e2e/apply_auto_approve_test.go | 345 ++++++------ internal/cloud/e2e/helper_test.go | 67 ++- internal/cloud/e2e/main_test.go | 27 - .../e2e/migrate_state_multi_to_tfc_test.go | 6 +- .../e2e/migrate_state_single_to_tfc_test.go | 7 +- .../e2e/migrate_state_tfc_to_tfc_test.go | 506 +++++++++++++++++- internal/command/meta_backend_migrate.go | 13 +- 9 files changed, 747 insertions(+), 230 deletions(-) diff --git a/go.mod b/go.mod index 66522fce6..26967a745 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-plugin v1.4.3 github.com/hashicorp/go-retryablehttp v0.7.0 - github.com/hashicorp/go-tfe v0.19.1-0.20211012181137-3666eed9e8e9 + github.com/hashicorp/go-tfe v0.19.1-0.20211015143223-e7e0a0182bbd github.com/hashicorp/go-uuid v1.0.2 github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f diff --git a/go.sum b/go.sum index c5635216d..f1f6be181 100644 --- a/go.sum +++ b/go.sum @@ -379,8 +379,8 @@ github.com/hashicorp/go-slug v0.7.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41 github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-tfe v0.19.1-0.20211012181137-3666eed9e8e9 h1:97QOrhJha4EmU+mUC1ubf15B40CjdcObULUop49+u8c= -github.com/hashicorp/go-tfe v0.19.1-0.20211012181137-3666eed9e8e9/go.mod h1:gyXLXbpBVxA2F/6opah8XBsOkZJxHYQmghl0OWi8keI= +github.com/hashicorp/go-tfe v0.19.1-0.20211015143223-e7e0a0182bbd h1:mn11v5DDNXkZq32QM8JSLNoUSbW2Ud4jMxm8IMpfS2w= +github.com/hashicorp/go-tfe v0.19.1-0.20211015143223-e7e0a0182bbd/go.mod h1:gyXLXbpBVxA2F/6opah8XBsOkZJxHYQmghl0OWi8keI= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= diff --git a/internal/cloud/e2e/apply_auto_approve_test.go b/internal/cloud/e2e/apply_auto_approve_test.go index bcdbaeb53..8e10feb08 100644 --- a/internal/cloud/e2e/apply_auto_approve_test.go +++ b/internal/cloud/e2e/apply_auto_approve_test.go @@ -7,191 +7,175 @@ import ( "context" "io/ioutil" "log" - "strings" + "os" "testing" + expect "github.com/Netflix/go-expect" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/internal/e2e" ) func Test_terraform_apply_autoApprove(t *testing.T) { ctx := context.Background() - cases := map[string]struct { - setup func(t *testing.T) (map[string]string, func()) - commands []tfCommand - validations func(t *testing.T, orgName, wsName string) - }{ - "workspace manual apply, terraform apply without auto-approve": { - setup: func(t *testing.T) (map[string]string, func()) { - org, orgCleanup := createOrganization(t) - wOpts := tfe.WorkspaceCreateOptions{ - Name: tfe.String(randomString(t)), - TerraformVersion: tfe.String(terraformVersion), - AutoApply: tfe.Bool(false), - } - workspace := createWorkspace(t, org.Name, wOpts) - cleanup := func() { - defer orgCleanup() - } - names := map[string]string{ - "organization": org.Name, - "workspace": workspace.Name, - } + tfVersion := "1.1.0-tfc-integration" + if !hasTerraformVersion(version) { + t.Skip("Skipping test because TFC does not have current terraform version.") + } - return names, cleanup - }, - commands: []tfCommand{ + cases := map[string]struct { + operations []operationSets + validations func(t *testing.T, orgName string) + }{ + "workspace manual apply, terraform apply without auto-approve, expect prompt": { + operations: []operationSets{ { - command: []string{"init"}, - expectedCmdOutput: "Terraform has been successfully initialized", - expectedErr: "", - }, - { - command: []string{"apply"}, - expectedCmdOutput: "Do you want to perform these actions in workspace", - expectedErr: "Error asking approve", + prep: func(t *testing.T, orgName, dir string) { + wsName := "app" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String(wsName), + TerraformVersion: tfe.String(tfVersion), + AutoApply: tfe.Bool(false), + }) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "app"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, }, }, - validations: func(t *testing.T, orgName, wsName string) { - workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"}) + validations: func(t *testing.T, orgName string) { + workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, "app", &tfe.WorkspaceReadOptions{Include: "current_run"}) if err != nil { t.Fatal(err) } if workspace.CurrentRun == nil { t.Fatal("Expected workspace to have run, but got nil") } - if workspace.CurrentRun.Status != tfe.RunPlanned { - t.Fatalf("Expected run status to be `planned`, but is %s", workspace.CurrentRun.Status) + if workspace.CurrentRun.Status != tfe.RunApplied { + t.Fatalf("Expected run status to be `applied`, but is %s", workspace.CurrentRun.Status) } }, }, - "workspace auto apply, terraform apply without auto-approve": { - setup: func(t *testing.T) (map[string]string, func()) { - org, orgCleanup := createOrganization(t) - wOpts := tfe.WorkspaceCreateOptions{ - Name: tfe.String(randomString(t)), - TerraformVersion: tfe.String(terraformVersion), - AutoApply: tfe.Bool(true), - } - workspace := createWorkspace(t, org.Name, wOpts) - cleanup := func() { - defer orgCleanup() - } - names := map[string]string{ - "organization": org.Name, - "workspace": workspace.Name, - } - - return names, cleanup - }, - commands: []tfCommand{ + "workspace auto apply, terraform apply without auto-approve, expect prompt": { + operations: []operationSets{ { - command: []string{"init"}, - expectedCmdOutput: "Terraform has been successfully initialized", - expectedErr: "", - }, - { - command: []string{"apply"}, - expectedCmdOutput: "Do you want to perform these actions in workspace", - expectedErr: "Error asking approve", + prep: func(t *testing.T, orgName, dir string) { + wsName := "app" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String(wsName), + TerraformVersion: tfe.String(tfVersion), + AutoApply: tfe.Bool(true), + }) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "app"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, }, }, - validations: func(t *testing.T, orgName, wsName string) { - workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"}) + validations: func(t *testing.T, orgName string) { + workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, "app", &tfe.WorkspaceReadOptions{Include: "current_run"}) if err != nil { t.Fatal(err) } if workspace.CurrentRun == nil { - t.Fatalf("Expected workspace to have run, but got nil") - } - if workspace.CurrentRun.Status != tfe.RunPlanned { - t.Fatalf("Expected run status to be `planned`, but is %s", workspace.CurrentRun.Status) - } - }, - }, - "workspace manual apply, terraform apply auto-approve": { - setup: func(t *testing.T) (map[string]string, func()) { - org, orgCleanup := createOrganization(t) - wOpts := tfe.WorkspaceCreateOptions{ - Name: tfe.String(randomString(t)), - TerraformVersion: tfe.String(terraformVersion), - AutoApply: tfe.Bool(false), - } - workspace := createWorkspace(t, org.Name, wOpts) - cleanup := func() { - defer orgCleanup() - } - names := map[string]string{ - "organization": org.Name, - "workspace": workspace.Name, - } - - return names, cleanup - }, - commands: []tfCommand{ - { - command: []string{"init"}, - expectedCmdOutput: "Terraform has been successfully initialized", - expectedErr: "", - }, - { - command: []string{"apply", "-auto-approve"}, - expectedCmdOutput: "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", - expectedErr: "", - }, - }, - validations: func(t *testing.T, orgName, wsName string) { - workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"}) - if err != nil { - t.Fatal(err) - } - if workspace.CurrentRun == nil { - t.Fatalf("Expected workspace to have run, but got nil") + t.Fatal("Expected workspace to have run, but got nil") } if workspace.CurrentRun.Status != tfe.RunApplied { t.Fatalf("Expected run status to be `applied`, but is %s", workspace.CurrentRun.Status) } }, }, - "workspace auto apply, terraform apply auto-approve": { - setup: func(t *testing.T) (map[string]string, func()) { - org, orgCleanup := createOrganization(t) - - wOpts := tfe.WorkspaceCreateOptions{ - Name: tfe.String(randomString(t)), - TerraformVersion: tfe.String(terraformVersion), - AutoApply: tfe.Bool(true), - } - workspace := createWorkspace(t, org.Name, wOpts) - cleanup := func() { - defer orgCleanup() - } - names := map[string]string{ - "organization": org.Name, - "workspace": workspace.Name, - } - - return names, cleanup - }, - commands: []tfCommand{ + "workspace manual apply, terraform apply with auto-approve, no prompt": { + operations: []operationSets{ { - command: []string{"init"}, - expectedCmdOutput: "Terraform has been successfully initialized", - expectedErr: "", - }, - { - command: []string{"apply", "-auto-approve"}, - expectedCmdOutput: "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", - expectedErr: "", + prep: func(t *testing.T, orgName, dir string) { + wsName := "app" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String(wsName), + TerraformVersion: tfe.String(tfVersion), + AutoApply: tfe.Bool(false), + }) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"apply", "-auto-approve"}, + expectedCmdOutput: `Apply complete!`, + }, + }, }, }, - validations: func(t *testing.T, orgName, wsName string) { - workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"}) + validations: func(t *testing.T, orgName string) { + workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, "app", &tfe.WorkspaceReadOptions{Include: "current_run"}) if err != nil { t.Fatal(err) } if workspace.CurrentRun == nil { - t.Fatalf("Expected workspace to have run, but got nil") + t.Fatal("Expected workspace to have run, but got nil") + } + if workspace.CurrentRun.Status != tfe.RunApplied { + t.Fatalf("Expected run status to be `applied`, but is %s", workspace.CurrentRun.Status) + } + }, + }, + "workspace auto apply, terraform apply with auto-approve, no prompt": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "app" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String(wsName), + TerraformVersion: tfe.String(tfVersion), + AutoApply: tfe.Bool(true), + }) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"apply", "-auto-approve"}, + expectedCmdOutput: `Apply complete!`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, "app", &tfe.WorkspaceReadOptions{Include: "current_run"}) + if err != nil { + t.Fatal(err) + } + if workspace.CurrentRun == nil { + t.Fatal("Expected workspace to have run, but got nil") } if workspace.CurrentRun.Status != tfe.RunApplied { t.Fatalf("Expected run status to be `applied`, but is %s", workspace.CurrentRun.Status) @@ -201,38 +185,73 @@ func Test_terraform_apply_autoApprove(t *testing.T) { } for name, tc := range cases { log.Println("Test: ", name) - resourceData, cleanup := tc.setup(t) + + organization, cleanup := createOrganization(t) defer cleanup() + exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout)) + if err != nil { + t.Fatal(err) + } + defer exp.Close() tmpDir, err := ioutil.TempDir("", "terraform-test") if err != nil { t.Fatal(err) } - orgName := resourceData["organization"] - wsName := resourceData["workspace"] - tfBlock := terraformConfigCloudBackendName(orgName, wsName) - writeMainTF(t, tfBlock, tmpDir) + defer os.RemoveAll(tmpDir) + tf := e2e.NewBinary(terraformBin, tmpDir) - defer tf.Close() - tf.AddEnv("TF_LOG=debug") + tf.AddEnv("TF_LOG=info") tf.AddEnv(cliConfigFileEnv) + defer tf.Close() - for _, cmd := range tc.commands { - stdout, stderr, err := tf.Run(cmd.command...) - if cmd.expectedErr == "" && err != nil { - t.Fatalf("Expected no error, but got %v. stderr\n: %s", err, stderr) - } - if cmd.expectedErr != "" { - if !strings.Contains(stderr, cmd.expectedErr) { - t.Fatalf("Expected to find error %s, but got %s", cmd.expectedErr, stderr) + for _, op := range tc.operations { + op.prep(t, organization.Name, tf.WorkDir()) + for _, tfCmd := range op.commands { + cmd := tf.Cmd(tfCmd.command...) + cmd.Stdin = exp.Tty() + cmd.Stdout = exp.Tty() + cmd.Stderr = exp.Tty() + + err = cmd.Start() + if err != nil { + t.Fatal(err) } - } - if cmd.expectedCmdOutput != "" && !strings.Contains(stdout, cmd.expectedCmdOutput) { - t.Fatalf("Expected to find output %s, but did not find in\n%s", cmd.expectedCmdOutput, stdout) + if tfCmd.expectedCmdOutput != "" { + _, err := exp.ExpectString(tfCmd.expectedCmdOutput) + if err != nil { + t.Fatal(err) + } + } + + lenInput := len(tfCmd.userInput) + lenInputOutput := len(tfCmd.postInputOutput) + if lenInput > 0 { + for i := 0; i < lenInput; i++ { + input := tfCmd.userInput[i] + exp.SendLine(input) + // use the index to find the corresponding + // output that matches the input. + if lenInputOutput-1 >= i { + output := tfCmd.postInputOutput[i] + _, err := exp.ExpectString(output) + if err != nil { + t.Fatal(err) + } + } + } + } + + err = cmd.Wait() + if err != nil { + t.Fatal(err) + } } } - tc.validations(t, orgName, wsName) + if tc.validations != nil { + tc.validations(t, organization.Name) + } } } diff --git a/internal/cloud/e2e/helper_test.go b/internal/cloud/e2e/helper_test.go index 844797f2d..af585004b 100644 --- a/internal/cloud/e2e/helper_test.go +++ b/internal/cloud/e2e/helper_test.go @@ -47,6 +47,18 @@ func createOrganization(t *testing.T) (*tfe.Organization, func()) { t.Fatal(err) } + // TODO: remove this when we are ready to release. This should not need beta + // or internal access as the release will be. Also, we won't be able to access + // admin in production. + opts := tfe.AdminOrganizationUpdateOptions{ + AccessBetaTools: tfe.Bool(true), + AccessInternalTools: tfe.Bool(true), + } + _, err = tfeClient.Admin.Organizations.Update(ctx, org.Name, opts) + if err != nil { + t.Fatal(err) + } + return org, func() { if err := tfeClient.Organizations.Delete(ctx, org.Name); err != nil { t.Errorf("Error destroying organization! WARNING: Dangling resources\n"+ @@ -147,14 +159,10 @@ terraform { } } -resource "random_pet" "server" { - keepers = { - uuid = uuid() - } - - length = 3 +output "tag_val" { + value = "%s" } -`, tfeHostname, org, tag) +`, tfeHostname, org, tag, tag) } func terraformConfigCloudBackendName(org, name string) string { @@ -170,12 +178,8 @@ terraform { } } -resource "random_pet" "server" { - keepers = { - uuid = uuid() - } - - length = 3 +output "val" { + value = "${terraform.workspace}" } `, tfeHostname, org, name) } @@ -191,3 +195,40 @@ func writeMainTF(t *testing.T, block string, dir string) { } f.Close() } + +// Ensure that TFC/E has a particular terraform version. +func hasTerraformVersion(version string) bool { + opts := tfe.AdminTerraformVersionsListOptions{ + ListOptions: tfe.ListOptions{ + PageNumber: 1, + PageSize: 100, + }, + } + hasVersion := false + +findTfVersion: + for { + // TODO: update go-tfe Read() to retrieve a terraform version by name. + // Currently you can only retrieve by ID. + tfVersionList, err := tfeClient.Admin.TerraformVersions.List(context.Background(), opts) + if err != nil { + t.Fatalf("Could not retrieve list of terraform versions: %v", err) + } + for _, item := range tfVersionList.Items { + if item.Version == version { + hasVersion = true + break findTfVersion + } + } + + // Exit the loop when we've seen all pages. + if tfVersionList.CurrentPage >= tfVersionList.TotalPages { + break + } + + // Update the page number to get the next page. + opts.PageNumber = tfVersionList.NextPage + } + + return hasVersion +} diff --git a/internal/cloud/e2e/main_test.go b/internal/cloud/e2e/main_test.go index 58653e002..c13151320 100644 --- a/internal/cloud/e2e/main_test.go +++ b/internal/cloud/e2e/main_test.go @@ -4,7 +4,6 @@ package main import ( - "encoding/json" "fmt" "io/ioutil" "log" @@ -16,7 +15,6 @@ import ( tfe "github.com/hashicorp/go-tfe" ) -var terraformVersion string var terraformBin string var cliConfigFileEnv string @@ -46,7 +44,6 @@ func accTest() bool { func setup() func() { setTfeClient() teardown := setupBinary() - setVersion() return func() { teardown() @@ -117,30 +114,6 @@ func setupBinary() func() { } } -func setVersion() { - log.Println("Retrieving version") - cmd := exec.Command(terraformBin, "version", "-json") - out, err := cmd.Output() - if err != nil { - log.Fatal(fmt.Sprintf("Could not output terraform version: %v", err)) - } - var data map[string]interface{} - if err := json.Unmarshal(out, &data); err != nil { - log.Fatal(fmt.Sprintf("Could not unmarshal version output: %v", err)) - } - - out, err = exec.Command("git", "rev-parse", "HEAD").Output() - if err != nil { - log.Fatal(fmt.Sprintf("Could not execute go build command: %v", err)) - } - - hash := string(out)[0:8] - - fullVersion := data["terraform_version"].(string) - version := strings.Split(fullVersion, "-")[0] - terraformVersion = fmt.Sprintf("%s-%s", version, hash) -} - func writeCredRC(file string) { creds := credentialBlock() f, err := os.Create(file) diff --git a/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go index e72169186..d7916a046 100644 --- a/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go @@ -67,7 +67,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) { expectedCmdOutput: `Do you want to copy only your current workspace?`, userInput: []string{"yes", "yes"}, postInputOutput: []string{ - `Do you want to copy existing state to the new backend?`, + `Do you want to copy existing state to Terraform Cloud?`, `Successfully configured Terraform Cloud!`}, }, { @@ -138,8 +138,8 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) { expectedCmdOutput: `Do you want to copy only your current workspace?`, userInput: []string{"yes", "yes"}, postInputOutput: []string{ - `Do you want to copy existing state to the new backend?`, - `Successfully configured Terraform Cloud!`}, + `Do you want to copy existing state to Terraform Cloud?`, + `Terraform Cloud has been successfully initialized!`}, }, { command: []string{"workspace", "list"}, diff --git a/internal/cloud/e2e/migrate_state_single_to_tfc_test.go b/internal/cloud/e2e/migrate_state_single_to_tfc_test.go index 805a5766d..d89396f4c 100644 --- a/internal/cloud/e2e/migrate_state_single_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_single_to_tfc_test.go @@ -5,7 +5,6 @@ package main import ( "context" - "fmt" "io/ioutil" "os" "testing" @@ -104,7 +103,9 @@ func Test_migrate_single_to_tfc(t *testing.T) { command: []string{"init", "-migrate-state"}, expectedCmdOutput: `Terraform Cloud configuration only allows named workspaces!`, userInput: []string{"new-workspace", "yes"}, - postInputOutput: []string{`Successfully configured Terraform Cloud!`}, + postInputOutput: []string{ + `Do you want to copy existing state to the new backend?`, + `Successfully configured Terraform Cloud!`}, }, { command: []string{"workspace", "list"}, @@ -129,7 +130,7 @@ func Test_migrate_single_to_tfc(t *testing.T) { } for name, tc := range cases { - fmt.Println("Test: ", name) + t.Log("Test: ", name) organization, cleanup := createOrganization(t) defer cleanup() exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout)) diff --git a/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go b/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go index 17bbc0506..39ae98003 100644 --- a/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go @@ -1,21 +1,503 @@ +//go:build e2e +// +build e2e + package main import ( + "context" + "io/ioutil" + "os" "testing" + + expect "github.com/Netflix/go-expect" + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/internal/e2e" ) -/* +func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) { + ctx := context.Background() + tfVersion := "1.1.0-tfc-integration" + if !hasTerraformVersion(version) { + t.Skip("Skipping test because TFC does not have current terraform version.") + } - If org to org, treat it like a new backend. Then go through the multi/single logic + cases := map[string]struct { + setup func(t *testing.T) (string, func()) + operations []operationSets + validations func(t *testing.T, orgName string) + }{ + "migrating from name to name": { + setup: func(t *testing.T) (string, func()) { + organization, cleanup := createOrganization(t) + return organization.Name, cleanup + }, + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "prod" + // Creating the workspace here instead of it being created + // dynamically in the Cloud StateMgr because we want to ensure that + // the terraform version selected for the workspace matches the + // terraform version of this current branch. + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("prod"), + TerraformVersion: tfe.String(tfVersion), + }) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: `prod`, // this comes from the `prep` function + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "prod"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "dev" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String(wsName), + TerraformVersion: tfe.String(tfVersion), + }) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state", "-ignore-remote-version"}, + expectedCmdOutput: `Do you want to copy existing state to the new backend?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Successfully configured the backend "cloud"!`}, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: `dev`, // this comes from the `prep` function + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{}) + if err != nil { + t.Fatal(err) + } + // this workspace name is what exists in the cloud backend configuration block + if len(wsList.Items) != 2 { + t.Fatal("Expected number of workspaces to be 2") + } + }, + }, + "migrating from name to tags": { + setup: func(t *testing.T) (string, func()) { + organization, cleanup := createOrganization(t) + return organization.Name, cleanup + }, + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "prod" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("prod"), + TerraformVersion: tfe.String(tfVersion), + }) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "prod"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + tag := "app" + tfBlock := terraformConfigCloudBackendTags(orgName, tag) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state", "-ignore-remote-version"}, + expectedCmdOutput: `The "cloud" backend configuration only allows named workspaces!`, + userInput: []string{"new-workspace", "yes"}, + postInputOutput: []string{ + `Do you want to copy existing state to the new backend?`, + `Successfully configured the backend "cloud"!`}, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: `new-workspace`, // this comes from the `prep` function + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{ + Tags: tfe.String("app"), + }) + if err != nil { + t.Fatal(err) + } + // this workspace name is what exists in the cloud backend configuration block + if len(wsList.Items) != 1 { + t.Fatal("Expected number of workspaces to be 1") + } + }, + }, + } - If same org, but name/tag changes - config name -> config name - -- straight copy - config name -> config tags - -- jsut add tag to workspace. - config tags -> config name - -- straight copy -*/ -func Test_migrate_tfc_to_tfc(t *testing.T) { - t.Skip("todo: see comments") + for name, tc := range cases { + t.Log("Test: ", name) + exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout)) + if err != nil { + t.Fatal(err) + } + defer exp.Close() + + tmpDir, err := ioutil.TempDir("", "terraform-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + tf := e2e.NewBinary(terraformBin, tmpDir) + defer tf.Close() + tf.AddEnv("TF_LOG=INFO") + tf.AddEnv(cliConfigFileEnv) + + orgName, cleanup := tc.setup(t) + defer cleanup() + for _, op := range tc.operations { + op.prep(t, orgName, tf.WorkDir()) + for _, tfCmd := range op.commands { + t.Log("Running commands: ", tfCmd.command) + cmd := tf.Cmd(tfCmd.command...) + cmd.Stdin = exp.Tty() + cmd.Stdout = exp.Tty() + cmd.Stderr = exp.Tty() + + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + + if tfCmd.expectedCmdOutput != "" { + _, err := exp.ExpectString(tfCmd.expectedCmdOutput) + if err != nil { + t.Fatal(err) + } + } + + lenInput := len(tfCmd.userInput) + lenInputOutput := len(tfCmd.postInputOutput) + if lenInput > 0 { + for i := 0; i < lenInput; i++ { + input := tfCmd.userInput[i] + exp.SendLine(input) + // use the index to find the corresponding + // output that matches the input. + if lenInputOutput-1 >= i { + output := tfCmd.postInputOutput[i] + _, err := exp.ExpectString(output) + if err != nil { + t.Fatal(err) + } + } + } + } + + err = cmd.Wait() + if err != nil { + t.Fatal(err.Error()) + } + } + } + + if tc.validations != nil { + tc.validations(t, orgName) + } + } +} + +func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) { + ctx := context.Background() + tfVersion := "1.1.0-tfc-integration" + if !hasTerraformVersion(version) { + t.Skip("Skipping test because TFC does not have current terraform version.") + } + + cases := map[string]struct { + setup func(t *testing.T) (string, func()) + operations []operationSets + validations func(t *testing.T, orgName string) + }{ + "migrating from multiple workspaces via tags to name": { + setup: func(t *testing.T) (string, func()) { + organization, cleanup := createOrganization(t) + return organization.Name, cleanup + }, + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + tag := "app" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("app-prod"), + Tags: []*tfe.Tag{{Name: tag}}, + TerraformVersion: tfe.String(tfVersion), + }) + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("app-staging"), + Tags: []*tfe.Tag{{Name: tag}}, + TerraformVersion: tfe.String(tfVersion), + }) + tfBlock := terraformConfigCloudBackendTags(orgName, tag) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `The currently selected workspace (default) does not exist.`, + userInput: []string{"1"}, + postInputOutput: []string{`Terraform has been successfully initialized!`}, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "app-prod"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"workspace", "select", "app-staging"}, + expectedCmdOutput: `Switched to workspace "app-staging".`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "app-staging"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"output"}, + expectedCmdOutput: `tag_val = "app"`, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + name := "service" + // Doing this here instead of relying on dynamic workspace creation + // because we want to set the terraform version here so that it is + // using the right version for post init operations. + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String(name), + TerraformVersion: tfe.String(tfVersion), + }) + tfBlock := terraformConfigCloudBackendName(orgName, name) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state", "-ignore-remote-version"}, + expectedCmdOutput: `Do you want to copy only your current workspace?`, + userInput: []string{"yes", "yes"}, + postInputOutput: []string{ + `Do you want to copy existing state to the new backend?`, + `Terraform has been successfully initialized!`}, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: `service`, // this comes from the `prep` function + }, + { + command: []string{"output"}, + expectedCmdOutput: `tag_val = "app"`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + ws, err := tfeClient.Workspaces.Read(ctx, orgName, "service") + if err != nil { + t.Fatal(err) + } + if ws == nil { + t.Fatal("Expected to workspace not be empty") + } + }, + }, + "migrating from multiple workspaces via tags to other tags": { + setup: func(t *testing.T) (string, func()) { + organization, cleanup := createOrganization(t) + return organization.Name, cleanup + }, + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + tag := "app" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("app-prod"), + Tags: []*tfe.Tag{{Name: tag}}, + TerraformVersion: tfe.String(tfVersion), + }) + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("app-staging"), + Tags: []*tfe.Tag{{Name: tag}}, + TerraformVersion: tfe.String(tfVersion), + }) + tfBlock := terraformConfigCloudBackendTags(orgName, tag) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `The currently selected workspace (default) does not exist.`, + userInput: []string{"1"}, + postInputOutput: []string{`Terraform has been successfully initialized!`}, + }, + { + command: []string{"apply", "-auto-approve"}, + expectedCmdOutput: `Apply complete!`, + }, + { + command: []string{"workspace", "select", "app-staging"}, + expectedCmdOutput: `Switched to workspace "app-staging".`, + }, + { + command: []string{"apply", "-auto-approve"}, + expectedCmdOutput: `Apply complete!`, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + tag := "billing" + tfBlock := terraformConfigCloudBackendTags(orgName, tag) + writeMainTF(t, tfBlock, dir) + t.Log(orgName) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state", "-ignore-remote-version"}, + expectedCmdOutput: `Would you like to rename your workspaces?`, + userInput: []string{"1", "new-*", "1"}, + postInputOutput: []string{ + `What pattern would you like to add to all your workspaces?`, + `The currently selected workspace (app-staging) does not exist.`, + `Successfully configured the backend "cloud"!`}, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{ + Tags: tfe.String("billing"), + }) + if err != nil { + t.Fatal(err) + } + if len(wsList.Items) != 2 { + t.Logf("Expected the number of workspaces to be 2, but got %d", len(wsList.Items)) + } + _, empty := getWorkspace(wsList.Items, "new-app-prod") + if empty { + t.Fatalf("expected workspaces to include 'new-app-prod' but didn't.") + } + _, empty = getWorkspace(wsList.Items, "new-app-staging") + if empty { + t.Fatalf("expected workspaces to include 'new-app-staging' but didn't.") + } + }, + }, + } + + for name, tc := range cases { + t.Log("Test: ", name) + exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout)) + if err != nil { + t.Fatal(err) + } + defer exp.Close() + + tmpDir, err := ioutil.TempDir("", "terraform-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + tf := e2e.NewBinary(terraformBin, tmpDir) + defer tf.Close() + tf.AddEnv("TF_LOG=INFO") + tf.AddEnv(cliConfigFileEnv) + + orgName, cleanup := tc.setup(t) + defer cleanup() + for _, op := range tc.operations { + op.prep(t, orgName, tf.WorkDir()) + for _, tfCmd := range op.commands { + t.Log("Running commands: ", tfCmd.command) + cmd := tf.Cmd(tfCmd.command...) + cmd.Stdin = exp.Tty() + cmd.Stdout = exp.Tty() + cmd.Stderr = exp.Tty() + + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + + if tfCmd.expectedCmdOutput != "" { + _, err := exp.ExpectString(tfCmd.expectedCmdOutput) + if err != nil { + t.Fatal(err) + } + } + + lenInput := len(tfCmd.userInput) + lenInputOutput := len(tfCmd.postInputOutput) + if lenInput > 0 { + for i := 0; i < lenInput; i++ { + input := tfCmd.userInput[i] + exp.SendLine(input) + // use the index to find the corresponding + // output that matches the input. + if lenInputOutput-1 >= i { + output := tfCmd.postInputOutput[i] + _, err := exp.ExpectString(output) + if err != nil { + t.Fatal(err) + } + } + } + } + + t.Log(cmd.Stderr) + err = cmd.Wait() + if err != nil { + t.Fatal(err.Error()) + } + } + } + + if tc.validations != nil { + tc.validations(t, orgName) + } + } } diff --git a/internal/command/meta_backend_migrate.go b/internal/command/meta_backend_migrate.go index 92eadfa03..77d2ef05b 100644 --- a/internal/command/meta_backend_migrate.go +++ b/internal/command/meta_backend_migrate.go @@ -545,12 +545,6 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { return fmt.Errorf(strings.TrimSpace(errTFCMigrateNotYetImplemented)) } - // from TFC to TFC - if sourceTFC && destinationTFC { - // TODO: see internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go for notes - panic("not yet implemented") - } - // Everything below, by the above two conditionals, now assumes that the // destination is always Terraform Cloud (TFC). @@ -659,6 +653,13 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa } } + // After migrating multiple workspaces, we want to ensure that a workspace is + // set or we prompt the user to set a workspace. + err = m.selectWorkspace(opts.Destination) + if err != nil { + return err + } + return nil }