From dc76bbee73bd232a47762a0797fbe3b9f59b6ff4 Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Sat, 9 Oct 2021 08:47:12 -0400 Subject: [PATCH] Backend State Migration: Add remote backend test --- internal/cloud/e2e/apply_auto_approve_test.go | 8 +- internal/cloud/e2e/helper_test.go | 51 +- .../e2e/migrate_state_multi_to_tfc_test.go | 6 +- ...igrate_state_remote_backend_to_tfc_test.go | 918 +++++++++++++++++- internal/cloud/e2e/run_variables_test.go | 39 +- internal/command/meta_backend_migrate.go | 34 +- 6 files changed, 989 insertions(+), 67 deletions(-) diff --git a/internal/cloud/e2e/apply_auto_approve_test.go b/internal/cloud/e2e/apply_auto_approve_test.go index 1614e4f1e..bcdbaeb53 100644 --- a/internal/cloud/e2e/apply_auto_approve_test.go +++ b/internal/cloud/e2e/apply_auto_approve_test.go @@ -29,7 +29,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { TerraformVersion: tfe.String(terraformVersion), AutoApply: tfe.Bool(false), } - workspace := createWorkspace(t, org, wOpts) + workspace := createWorkspace(t, org.Name, wOpts) cleanup := func() { defer orgCleanup() } @@ -73,7 +73,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { TerraformVersion: tfe.String(terraformVersion), AutoApply: tfe.Bool(true), } - workspace := createWorkspace(t, org, wOpts) + workspace := createWorkspace(t, org.Name, wOpts) cleanup := func() { defer orgCleanup() } @@ -117,7 +117,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { TerraformVersion: tfe.String(terraformVersion), AutoApply: tfe.Bool(false), } - workspace := createWorkspace(t, org, wOpts) + workspace := createWorkspace(t, org.Name, wOpts) cleanup := func() { defer orgCleanup() } @@ -162,7 +162,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { TerraformVersion: tfe.String(terraformVersion), AutoApply: tfe.Bool(true), } - workspace := createWorkspace(t, org, wOpts) + workspace := createWorkspace(t, org.Name, wOpts) cleanup := func() { defer orgCleanup() } diff --git a/internal/cloud/e2e/helper_test.go b/internal/cloud/e2e/helper_test.go index f8dad3a79..844797f2d 100644 --- a/internal/cloud/e2e/helper_test.go +++ b/internal/cloud/e2e/helper_test.go @@ -56,9 +56,9 @@ func createOrganization(t *testing.T) (*tfe.Organization, func()) { } } -func createWorkspace(t *testing.T, org *tfe.Organization, wOpts tfe.WorkspaceCreateOptions) *tfe.Workspace { +func createWorkspace(t *testing.T, orgName string, wOpts tfe.WorkspaceCreateOptions) *tfe.Workspace { ctx := context.Background() - w, err := tfeClient.Workspaces.Create(ctx, org.Name, wOpts) + w, err := tfeClient.Workspaces.Create(ctx, orgName, wOpts) if err != nil { t.Fatal(err) } @@ -66,6 +66,15 @@ func createWorkspace(t *testing.T, org *tfe.Organization, wOpts tfe.WorkspaceCre return w } +func getWorkspace(workspaces []*tfe.Workspace, workspace string) (*tfe.Workspace, bool) { + for _, ws := range workspaces { + if ws.Name == workspace { + return ws, false + } + } + return nil, true +} + func randomString(t *testing.T) string { v, err := uuid.GenerateUUID() if err != nil { @@ -87,6 +96,44 @@ output "val" { `) } +func terraformConfigRemoteBackendName(org, name string) string { + return fmt.Sprintf(` +terraform { + backend "remote" { + hostname = "%s" + organization = "%s" + + workspaces { + name = "%s" + } + } +} + +output "val" { + value = "${terraform.workspace}" +} +`, tfeHostname, org, name) +} + +func terraformConfigRemoteBackendPrefix(org, prefix string) string { + return fmt.Sprintf(` +terraform { + backend "remote" { + hostname = "%s" + organization = "%s" + + workspaces { + prefix = "%s" + } + } +} + +output "val" { + value = "${terraform.workspace}" +} +`, tfeHostname, org, prefix) +} + func terraformConfigCloudBackendTags(org, tag string) string { return fmt.Sprintf(` terraform { 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 f4160a61a..dd3ae2c26 100644 --- a/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go @@ -5,7 +5,6 @@ package main import ( "context" - "fmt" "io/ioutil" "os" "testing" @@ -168,7 +167,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(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)) @@ -192,6 +191,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) { op.prep(t, organization.Name, tf.WorkDir()) for _, tfCmd := range op.commands { t.Log("Running commands: ", tfCmd.command) + tfCmd.command = append(tfCmd.command, "-ignore-remote-version") cmd := tf.Cmd(tfCmd.command...) cmd.Stdin = exp.Tty() cmd.Stdout = exp.Tty() @@ -357,7 +357,7 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) { } for name, tc := range cases { - fmt.Println("Test: ", name) + t.Log("Test: ", name) organization, cleanup := createOrganization(t) t.Log(organization.Name) defer cleanup() diff --git a/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go b/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go index 0381216a6..33c1686c8 100644 --- a/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go @@ -4,56 +4,912 @@ 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" ) -// REMOTE BACKEND -/* - - RB name -> TFC name - -- straight copy if only if different name, or same WS name in diff org - -- other - -- ensure that the local workspace, after migration, is the new name (in the tfc config block) - - RB name -> TFC tags - -- just add tag, if in same org - -- If new org, if WS exists, just add tag - -- If new org, if WS not exists, create and add tag - - RB prefix -> TFC name - -- create if not exists - -- migrate the current worksapce state to ws name - - RB prefix -> TFC tags - -- update previous workspaces (prefix + local) with cloud config tag - -- Rename the local workspaces to match the TFC workspaces (prefix + former local, ie app-prod). inform user - -*/ -func Test_migrate_remote_backend_name_to_tfc(t *testing.T) { - t.Skip("TODO: see comments") - _ = map[string]struct { +func Test_migrate_remote_backend_name_to_tfc_name(t *testing.T) { + ctx := context.Background() + cases := map[string]struct { operations []operationSets validations func(t *testing.T, orgName string) }{ - "single workspace with backend name strategy, to cloud with name strategy": {}, - "single workspace with backend name strategy, to cloud with tags strategy": {}, + "backend name strategy, to cloud with name strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + remoteWorkspace := "remote-workspace" + tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "remote"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "cloud-workspace" + 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: `cloud-workspace`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + expectedName := "cloud-workspace" + ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName) + if err != nil { + t.Fatal(err) + } + if ws == nil { + t.Fatalf("Expected workspace %s to be present, but is not.", expectedName) + } + }, + }, + "backend name strategy, to cloud name strategy, using the same name": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + remoteWorkspace := "remote-workspace" + tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "remote"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "remote-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state", "-ignore-remote-version"}, + expectedCmdOutput: `Terraform has been successfully initialized!`, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: `remote-workspace`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + expectedName := "remote-workspace" + ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName) + if err != nil { + t.Fatal(err) + } + if ws == nil { + t.Fatalf("Expected workspace %s to be present, but is not.", expectedName) + } + }, + }, + } + + 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) + tf.AddEnv("TF_LOG=INFO") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + organization, cleanup := createOrganization(t) + defer cleanup() + 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 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) + } + } + } + + if tc.validations != nil { + tc.validations(t, organization.Name) + } + } +} + +func Test_migrate_remote_backend_name_to_tfc_name_different_org(t *testing.T) { + ctx := context.Background() + cases := map[string]struct { + operations []operationSets + validations func(t *testing.T, orgName string) + }{ + "backend name strategy, to cloud name strategy, using the same name, different organization": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + remoteWorkspace := "remote-workspace" + tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "remote"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "remote-workspace" + 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: `remote-workspace`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + expectedName := "remote-workspace" + ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName) + if err != nil { + t.Fatal(err) + } + if ws == nil { + t.Fatalf("Expected workspace %s to be present, but is not.", expectedName) + } + }, + }, + } + + 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) + tf.AddEnv("TF_LOG=INFO") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + orgOne, cleanupOne := createOrganization(t) + orgTwo, cleanupTwo := createOrganization(t) + defer cleanupOne() + defer cleanupTwo() + orgs := []string{orgOne.Name, orgTwo.Name} + var orgName string + for index, op := range tc.operations { + orgName = orgs[index] + op.prep(t, orgName, 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 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) + } + } + } + + if tc.validations != nil { + tc.validations(t, orgName) + } + } +} + +func Test_migrate_remote_backend_name_to_tfc_tags(t *testing.T) { + ctx := context.Background() + cases := map[string]struct { + operations []operationSets + validations func(t *testing.T, orgName string) + }{ + "single workspace with backend name strategy, to cloud with tags strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + remoteWorkspace := "remote-workspace" + tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "remote"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: `default`, + }, + }, + }, + { + 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{"cloud-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: `cloud-workspace`, + }, + }, + }, + }, + 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) + } + if len(wsList.Items) != 1 { + t.Fatalf("Expected number of workspaces to be 1, but got %d", len(wsList.Items)) + } + ws := wsList.Items[0] + if ws.Name != "cloud-workspace" { + t.Fatalf("Expected workspace to be `cloud-workspace`, but is %s", ws.Name) + } + }, + }, + } + + 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) + tf.AddEnv("TF_LOG=INFO") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + organization, cleanup := createOrganization(t) + defer cleanup() + 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 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) + } + } + } + + if tc.validations != nil { + tc.validations(t, organization.Name) + } } } func Test_migrate_remote_backend_prefix_to_tfc_name(t *testing.T) { - t.Skip("TODO: see comments") - _ = map[string]struct { + ctx := context.Background() + cases := map[string]struct { operations []operationSets validations func(t *testing.T, orgName string) }{ - "single workspace with backend prefix strategy, to cloud with name strategy": {}, - "multiple workspaces with backend prefix strategy, to cloud with name strategy": {}, + "single workspace with backend prefix strategy, to cloud with name strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")}) + prefix := "app-" + tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Terraform has been successfully initialized!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "app-one"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "cloud-workspace" + 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: `cloud-workspace`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + expectedName := "cloud-workspace" + ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName) + if err != nil { + t.Fatal(err) + } + if ws == nil { + t.Fatalf("Expected workspace %s to be present, but is not.", expectedName) + } + }, + }, + "multiple workspaces with backend prefix strategy, to cloud with name strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")}) + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-two")}) + prefix := "app-" + tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix) + 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-one"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"workspace", "list"}, + expectedCmdOutput: "* one", // app name retrieved via prefix + }, + { + command: []string{"workspace", "select", "two"}, + expectedCmdOutput: `Switched to workspace "two".`, // app name retrieved via prefix + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "cloud-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + 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"}, + postInputOutput: []string{ + `Successfully configured the backend "cloud"!`}, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: `cloud-workspace`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + expectedName := "cloud-workspace" + ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName) + if err != nil { + t.Fatal(err) + } + if ws == nil { + t.Fatalf("Expected workspace %s to be present, but is not.", expectedName) + } + wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{}) + if err != nil { + t.Fatal(err) + } + if len(wsList.Items) != 3 { + t.Fatalf("expected number of workspaces in this org to be 3, but got %d", len(wsList.Items)) + } + ws, empty := getWorkspace(wsList.Items, "cloud-workspace") + if empty { + t.Fatalf("expected workspaces to include 'cloud-workspace' but didn't.") + } + ws, empty = getWorkspace(wsList.Items, "app-one") + if empty { + t.Fatalf("expected workspaces to include 'app-one' but didn't.") + } + ws, empty = getWorkspace(wsList.Items, "app-two") + if empty { + t.Fatalf("expected workspaces to include 'app-two' 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) + tf.AddEnv("TF_LOG=INFO") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + organization, cleanup := createOrganization(t) + defer cleanup() + 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 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) + } + } + } + + if tc.validations != nil { + tc.validations(t, organization.Name) + } } } func Test_migrate_remote_backend_prefix_to_tfc_tags(t *testing.T) { - t.Skip("TODO: see comments") - _ = map[string]struct { + ctx := context.Background() + cases := map[string]struct { operations []operationSets validations func(t *testing.T, orgName string) }{ - "single workspace with backend prefix strategy, to cloud with tags strategy": {}, - "multiple workspaces with backend prefix strategy, to cloud with tags strategy": {}, + "single workspace with backend prefix strategy, to cloud with tags strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")}) + prefix := "app-" + tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Terraform has been successfully initialized!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "app-one"?`, + 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{"cloud-workspace", "yes"}, + postInputOutput: []string{ + `Do you want to copy existing state to the new backend?`, + `Successfully configured the backend "cloud"!`}, + }, + { + command: []string{"workspace", "list"}, + expectedCmdOutput: `cloud-workspace`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + expectedName := "cloud-workspace" + ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName) + if err != nil { + t.Fatal(err) + } + if ws == nil { + t.Fatalf("Expected workspace %s to be present, but is not.", expectedName) + } + }, + }, + "multiple workspaces with backend prefix strategy, to cloud with tags strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")}) + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-two")}) + prefix := "app-" + tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix) + 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-one"?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"workspace", "select", "two"}, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions in workspace "app-two"?`, + 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: `Would you like to rename your workspaces?`, + userInput: []string{"1", "*"}, + postInputOutput: []string{`What pattern would you like to add to all your workspaces?`, + `Successfully configured the backend "cloud"!`}, + }, + { + command: []string{"workspace", "show"}, + expectedCmdOutput: "two", // this comes from the original workspace name from the previous backend. + }, + { + command: []string{"workspace", "select", "one"}, + expectedCmdOutput: `Switched to workspace "one".`, // this comes from the original workspace name from the previous backend. + }, + }, + }, + }, + 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) + } + if len(wsList.Items) != 2 { + t.Logf("Expected the number of workspaces to be 2, but got %d", len(wsList.Items)) + } + ws, empty := getWorkspace(wsList.Items, "one") + if empty { + t.Fatalf("expected workspaces to include 'one' but didn't.") + } + if len(ws.TagNames) == 0 { + t.Fatalf("expected workspaces 'one' to have tags.") + } + ws, empty = getWorkspace(wsList.Items, "two") + if empty { + t.Fatalf("expected workspaces to include 'two' but didn't.") + } + if len(ws.TagNames) == 0 { + t.Fatalf("expected workspaces 'two' to have tags.") + } + }, + }, + } + + 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) + tf.AddEnv("TF_LOG=INFO") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + organization, cleanup := createOrganization(t) + defer cleanup() + 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 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) + } + } + } + + if tc.validations != nil { + tc.validations(t, organization.Name) + } } } diff --git a/internal/cloud/e2e/run_variables_test.go b/internal/cloud/e2e/run_variables_test.go index ba20077a1..6029066b7 100644 --- a/internal/cloud/e2e/run_variables_test.go +++ b/internal/cloud/e2e/run_variables_test.go @@ -57,16 +57,16 @@ func Test_cloud_run_variables(t *testing.T) { }, commands: []tfCommand{ { - command: []string{"init"}, - expectedOutput: `Successfully configured the backend "cloud"!`, + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "cloud"!`, }, { - command: []string{"plan", "-var", "foo=bar"}, - expectedOutput: ` + test_cli = "bar"`, + command: []string{"plan", "-var", "foo=bar"}, + expectedCmdOutput: ` + test_cli = "bar"`, }, { - command: []string{"plan", "-var", "foo=bar"}, - expectedOutput: ` + test_env = "qux"`, + command: []string{"plan", "-var", "foo=bar"}, + expectedCmdOutput: ` + test_env = "qux"`, }, }, }, @@ -110,23 +110,28 @@ func Test_cloud_run_variables(t *testing.T) { t.Fatal(err) } - if tfCmd.expectedOutput != "" { - _, err := exp.ExpectString(tfCmd.expectedOutput) + if tfCmd.expectedCmdOutput != "" { + _, err := exp.ExpectString(tfCmd.expectedCmdOutput) if err != nil { t.Fatal(err) } } - if len(tfCmd.userInput) > 0 { - for _, input := range tfCmd.userInput { + lenInput := len(tfCmd.userInput) + lenInputOutput := len(tfCmd.postInputOutput) + if lenInput > 0 { + for i := 0; i < lenInput; i++ { + input := tfCmd.userInput[i] exp.SendLine(input) - } - } - - if tfCmd.postInputOutput != "" { - _, err := exp.ExpectString(tfCmd.postInputOutput) - if err != nil { - t.Fatal(err) + // 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) + } + } } } diff --git a/internal/command/meta_backend_migrate.go b/internal/command/meta_backend_migrate.go index 17dcd930c..fd4c176dd 100644 --- a/internal/command/meta_backend_migrate.go +++ b/internal/command/meta_backend_migrate.go @@ -44,7 +44,7 @@ type backendMigrateOpts struct { // // This will attempt to lock both states for the migration. func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { - log.Printf("[TRACE] backendMigrateState: need to migrate from %q to %q backend config", opts.SourceType, opts.DestinationType) + log.Printf("[INFO] backendMigrateState: need to migrate from %q to %q backend config", opts.SourceType, opts.DestinationType) // We need to check what the named state status is. If we're converting // from multi-state to single-state for example, we need to handle that. var sourceSingleState, destinationSingleState, sourceTFC, destinationTFC bool @@ -154,7 +154,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { // Multi-state to multi-state. func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error { - log.Print("[TRACE] backendMigrateState: migrating all named workspaces") + log.Print("[INFO] backendMigrateState: migrating all named workspaces") migrate := opts.force if !migrate { @@ -209,9 +209,9 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error { // Multi-state to single state. func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { - log.Printf("[TRACE] backendMigrateState: destination backend type %q does not support named workspaces", opts.DestinationType) + log.Printf("[INFO] backendMigrateState: destination backend type %q does not support named workspaces", opts.DestinationType) - currentEnv, err := m.Workspace() + currentWorkspace, err := m.Workspace() if err != nil { return err } @@ -228,7 +228,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { opts.DestinationType), Description: fmt.Sprintf( strings.TrimSpace(inputBackendMigrateMultiToSingle), - opts.SourceType, opts.DestinationType, currentEnv), + opts.SourceType, opts.DestinationType, currentWorkspace), }) if err != nil { return fmt.Errorf( @@ -241,7 +241,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { } // Copy the default state - opts.sourceWorkspace = currentEnv + opts.sourceWorkspace = currentWorkspace // now switch back to the default env so we can acccess the new backend m.SetWorkspace(backend.DefaultStateName) @@ -251,7 +251,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { // Single state to single state, assumed default state name. func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { - log.Printf("[TRACE] backendMigrateState: migrating %q workspace to %q workspace", opts.sourceWorkspace, opts.destinationWorkspace) + log.Printf("[INFO] backendMigrateState: single-to-single migrating %q workspace to %q workspace", opts.sourceWorkspace, opts.destinationWorkspace) sourceState, err := opts.Source.StateMgr(opts.sourceWorkspace) if err != nil { @@ -534,7 +534,7 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { // Everything below, by the above two conditionals, now assumes that the // destination is always Terraform Cloud (TFC). - sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1 && sourceWorkspaces[0] == backend.DefaultStateName) + sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1) if sourceSingle { if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy { // If we know the name via WorkspaceNameStrategy, then set the @@ -543,6 +543,14 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { // in TFC if it does not exist. opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name } + + currentWorkspace, err := m.Workspace() + if err != nil { + return err + } + opts.sourceWorkspace = currentWorkspace + + log.Printf("[INFO] backendMigrateTFC: single-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace) // Run normal single-to-single state migration // This will handle both situations where the new cloud backend // configuration is using a workspace.name strategy or workspace.tags @@ -559,13 +567,14 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { return err } - currentEnv, err := m.Workspace() + currentWorkspace, err := m.Workspace() if err != nil { return err } - opts.sourceWorkspace = currentEnv + opts.sourceWorkspace = currentWorkspace opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name + log.Printf("[INFO] backendMigrateTFC: multi-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace) return m.backendMigrateState_s_s(opts) } @@ -574,9 +583,13 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { // workspace over to new one, prompt for workspace name pattern (*), // and start migrating, and create tags for each workspace. if multiSource && destinationTagsStrategy { + log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration from source workspaces %q", sourceWorkspaces) return m.backendMigrateState_S_TFC(opts, sourceWorkspaces) } + // TODO(omar): after the check for sourceSingle is done, everything following + // it has to be multi. So rework the code to not need to check for multi, adn + // return m.backendMigrateState_S_TFC here. return nil } @@ -619,6 +632,7 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa opts.force = true // Perform the migration + log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration, source workspace %q to destination workspace %q", opts.sourceWorkspace, opts.destinationWorkspace) if err := m.backendMigrateState_s_s(opts); err != nil { return fmt.Errorf(strings.TrimSpace( errMigrateMulti), name, opts.SourceType, opts.DestinationType, err)