From 55fc590904c90c07e91b205f727c5106e560b6ed Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Wed, 22 Sep 2021 17:53:33 -0400 Subject: [PATCH] Teraform Cloud Backend State Migration * determining source or destination to cloud * handling single to single state migrations to cloud, using a name strategy or a tags strategy * Add end-to-end tests for state migration. --- go.mod | 2 + go.sum | 3 + internal/cloud/e2e/apply_auto_approve_test.go | 30 +-- internal/cloud/e2e/helper_test.go | 78 +++++++ internal/cloud/e2e/main_test.go | 38 ---- .../e2e/migrate_state_multi_to_tfc_test.go | 18 ++ ...igrate_state_remote_backend_to_tfc_test.go | 31 +++ .../e2e/migrate_state_single_to_tfc_test.go | 196 ++++++++++++++++++ .../e2e/migrate_state_tfc_to_other_test.go | 113 ++++++++++ .../e2e/migrate_state_tfc_to_tfc_test.go | 21 ++ internal/command/meta_backend_migrate.go | 122 +++++++++-- 11 files changed, 568 insertions(+), 84 deletions(-) create mode 100644 internal/cloud/e2e/migrate_state_multi_to_tfc_test.go create mode 100644 internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go create mode 100644 internal/cloud/e2e/migrate_state_single_to_tfc_test.go create mode 100644 internal/cloud/e2e/migrate_state_tfc_to_other_test.go create mode 100644 internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go diff --git a/go.mod b/go.mod index 6dfa7bdcc..9910399a3 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( cloud.google.com/go/storage v1.10.0 github.com/Azure/azure-sdk-for-go v52.5.0+incompatible github.com/Azure/go-autorest/autorest v0.11.18 + github.com/Netflix/go-expect v0.0.0-20211003183012-e1a7c020ce25 github.com/agext/levenshtein v1.2.3 github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70 @@ -154,6 +155,7 @@ require ( github.com/jstemmer/go-junit-report v0.9.1 // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/klauspost/compress v1.11.2 // indirect + github.com/kr/pty v1.1.1 // indirect github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect github.com/mattn/go-colorable v0.1.6 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect diff --git a/go.sum b/go.sum index aaba3e315..4b6b9daa3 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,8 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Netflix/go-expect v0.0.0-20211003183012-e1a7c020ce25 h1:hWfsqBaNZUHztXA78g7Y2Jj3rDQaTCZhhFwz43i2VlA= +github.com/Netflix/go-expect v0.0.0-20211003183012-e1a7c020ce25/go.mod h1:68ORG0HSEWDuH5Eh73AFbYWZ1zT4Y+b0vhOa+vZRUdI= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= @@ -453,6 +455,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= diff --git a/internal/cloud/e2e/apply_auto_approve_test.go b/internal/cloud/e2e/apply_auto_approve_test.go index 919c457e2..8ece4836d 100644 --- a/internal/cloud/e2e/apply_auto_approve_test.go +++ b/internal/cloud/e2e/apply_auto_approve_test.go @@ -16,12 +16,6 @@ import ( "github.com/hashicorp/terraform/internal/e2e" ) -type tfCommand struct { - command []string - expectedOutput string - expectedErr string -} - func Test_terraform_apply_autoApprove(t *testing.T) { ctx := context.Background() cases := map[string]struct { @@ -218,7 +212,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { } orgName := resourceData["organization"] wsName := resourceData["workspace"] - tfBlock := createTerraformBlock(orgName, wsName) + tfBlock := terraformConfigCloudBackendName(orgName, wsName) writeMainTF(t, tfBlock, tmpDir) tf := e2e.NewBinary(terraformBin, tmpDir) defer tf.Close() @@ -245,28 +239,6 @@ func Test_terraform_apply_autoApprove(t *testing.T) { } } -func createTerraformBlock(org, ws string) string { - return fmt.Sprintf( - `terraform { - cloud { - hostname = "%s" - organization = "%s" - - workspaces { - name = "%s" - } - } -} - -resource "random_pet" "server" { - keepers = { - uuid = uuid() - } - - length = 3 -}`, tfeHostname, org, ws) -} - func writeMainTF(t *testing.T, block string, dir string) { f, err := os.Create(fmt.Sprintf("%s/main.tf", dir)) if err != nil { diff --git a/internal/cloud/e2e/helper_test.go b/internal/cloud/e2e/helper_test.go index 7793b917f..cad01a5c1 100644 --- a/internal/cloud/e2e/helper_test.go +++ b/internal/cloud/e2e/helper_test.go @@ -7,11 +7,30 @@ import ( "context" "fmt" "testing" + "time" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/go-uuid" ) +const ( + expectConsoleTimeout = 15 * time.Second +) + +type tfCommand struct { + command []string + expectedOutput string + expectedErr string + expectError bool + userInput []string + postInputOutput string +} + +type operationSets struct { + commands []tfCommand + prep func(t *testing.T, orgName, dir string) +} + func createOrganization(t *testing.T) (*tfe.Organization, func()) { ctx := context.Background() org, err := tfeClient.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ @@ -48,3 +67,62 @@ func randomString(t *testing.T) string { } return v } + +func terraformConfigLocalBackend() string { + return fmt.Sprintf(` +terraform { + backend "local" { + } +} + +output "val" { + value = "${terraform.workspace}" +} +`) +} + +func terraformConfigCloudBackendTags(org, tag string) string { + return fmt.Sprintf(` +terraform { + cloud { + hostname = "%s" + organization = "%s" + + workspaces { + tags = ["%s"] + } + } +} + +resource "random_pet" "server" { + keepers = { + uuid = uuid() + } + + length = 3 +} +`, tfeHostname, org, tag) +} + +func terraformConfigCloudBackendName(org, name string) string { + return fmt.Sprintf(` +terraform { + cloud { + hostname = "%s" + organization = "%s" + + workspaces { + name = "%s" + } + } +} + +resource "random_pet" "server" { + keepers = { + uuid = uuid() + } + + length = 3 +} +`, tfeHostname, org, name) +} diff --git a/internal/cloud/e2e/main_test.go b/internal/cloud/e2e/main_test.go index 310032848..58653e002 100644 --- a/internal/cloud/e2e/main_test.go +++ b/internal/cloud/e2e/main_test.go @@ -4,7 +4,6 @@ package main import ( - "context" "encoding/json" "fmt" "io/ioutil" @@ -48,7 +47,6 @@ func setup() func() { setTfeClient() teardown := setupBinary() setVersion() - ensureVersionExists() return func() { teardown() @@ -143,42 +141,6 @@ func setVersion() { terraformVersion = fmt.Sprintf("%s-%s", version, hash) } -func ensureVersionExists() { - opts := tfe.AdminTerraformVersionsListOptions{ - ListOptions: tfe.ListOptions{ - PageNumber: 1, - PageSize: 100, - }, - } - hasVersion := false - -findTfVersion: - for { - tfVersionList, err := tfeClient.Admin.TerraformVersions.List(context.Background(), opts) - if err != nil { - log.Fatalf("Could not retrieve list of terraform versions: %v", err) - } - for _, item := range tfVersionList.Items { - if item.Version == terraformVersion { - 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 - } - - if !hasVersion { - log.Fatalf("Terraform Version %s does not exist in the list. Please add it.", terraformVersion) - } -} - 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 new file mode 100644 index 000000000..a6c4f8190 --- /dev/null +++ b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "testing" +) + +/* + "multi" == multi-backend, multiple workspaces + -- when cloud config == name -> + ---- prompt -> do you want to ONLY migrate the current workspace + + -- when cloud config == tags + -- If Default present, prompt to rename default. + -- Then -> Prompt with * +*/ +func Test_migrate_multi_to_tfc(t *testing.T) { + t.Skip("todo: see comments") +} 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 new file mode 100644 index 000000000..6819ea7aa --- /dev/null +++ b/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "testing" +) + +// 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") +} + +func Test_migrate_remote_backend_prefix_to_tfc(t *testing.T) { + t.Skip("todo: see comments") +} diff --git a/internal/cloud/e2e/migrate_state_single_to_tfc_test.go b/internal/cloud/e2e/migrate_state_single_to_tfc_test.go new file mode 100644 index 000000000..28292419c --- /dev/null +++ b/internal/cloud/e2e/migrate_state_single_to_tfc_test.go @@ -0,0 +1,196 @@ +//go:build e2e +// +build e2e + +package main + +import ( + "context" + "fmt" + "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_single_to_tfc(t *testing.T) { + ctx := context.Background() + + cases := map[string]struct { + operations []operationSets + validations func(t *testing.T, orgName string) + }{ + "migrate using cloud workspace name strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + tfBlock := terraformConfigLocalBackend() + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedOutput: `Successfully configured the backend "local"!`, + }, + { + command: []string{"apply"}, + userInput: []string{"yes"}, + expectedOutput: `Do you want to perform these actions?`, + postInputOutput: `Apply complete!`, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "new-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state"}, + expectedOutput: `Do you want to copy existing state to the new backend?`, + userInput: []string{"yes"}, + postInputOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"workspace", "list"}, + expectedOutput: `new-workspace`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{}) + if err != nil { + t.Fatal(err) + } + ws := wsList.Items[0] + if ws.Name != "new-workspace" { + t.Fatalf("Expected workspace to be `new-workspace`, but is %s", ws.Name) + } + }, + }, + "migrate using cloud workspace tags strategy": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + tfBlock := terraformConfigLocalBackend() + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedOutput: `Successfully configured the backend "local"!`, + }, + { + command: []string{"apply"}, + userInput: []string{"yes"}, + expectedOutput: `Do you want to perform these actions?`, + postInputOutput: `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"}, + expectedOutput: `The "cloud" backend configuration only allows named workspaces!`, + userInput: []string{"new-workspace", "yes"}, + postInputOutput: `Successfully configured the backend "cloud"!`, + }, + { + command: []string{"workspace", "list"}, + expectedOutput: `new-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) + } + ws := wsList.Items[0] + if ws.Name != "new-workspace" { + t.Fatalf("Expected workspace to be `new-workspace`, but is %s", ws.Name) + } + }, + }, + } + + for name, tc := range cases { + fmt.Println("Test: ", name) + 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) + } + defer os.RemoveAll(tmpDir) + + tf := e2e.NewBinary(terraformBin, tmpDir) + tf.AddEnv("TF_LOG=info") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + 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.expectedOutput != "" { + _, err := exp.ExpectString(tfCmd.expectedOutput) + if err != nil { + t.Fatal(err) + } + } + + if len(tfCmd.userInput) > 0 { + for _, input := range tfCmd.userInput { + exp.SendLine(input) + } + } + + if tfCmd.postInputOutput != "" { + _, err := exp.ExpectString(tfCmd.postInputOutput) + 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/migrate_state_tfc_to_other_test.go b/internal/cloud/e2e/migrate_state_tfc_to_other_test.go new file mode 100644 index 000000000..750a315b8 --- /dev/null +++ b/internal/cloud/e2e/migrate_state_tfc_to_other_test.go @@ -0,0 +1,113 @@ +//go:build e2e +// +build e2e + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + expect "github.com/Netflix/go-expect" + "github.com/hashicorp/terraform/internal/e2e" +) + +func Test_migrate_tfc_to_other(t *testing.T) { + cases := map[string]struct { + operations []operationSets + }{ + "migrate from cloud to local backend": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "new-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedOutput: `Successfully configured the backend "cloud"!`, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + tfBlock := terraformConfigLocalBackend() + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state"}, + expectedOutput: `Migrating state from Terraform Cloud to another backend is not yet implemented.`, + expectError: true, + }, + }, + }, + }, + }, + } + + for name, tc := range cases { + fmt.Println("Test: ", name) + 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) + } + defer os.RemoveAll(tmpDir) + + tf := e2e.NewBinary(terraformBin, tmpDir) + tf.AddEnv("TF_LOG=info") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + 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.expectedOutput != "" { + _, err := exp.ExpectString(tfCmd.expectedOutput) + if err != nil { + t.Fatal(err) + } + } + + if len(tfCmd.userInput) > 0 { + for _, input := range tfCmd.userInput { + exp.SendLine(input) + } + } + + if tfCmd.postInputOutput != "" { + _, err := exp.ExpectString(tfCmd.postInputOutput) + if err != nil { + t.Fatal(err) + } + } + + err = cmd.Wait() + if err != nil && !tfCmd.expectError { + t.Fatal(err) + } + } + } + } +} diff --git a/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go b/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go new file mode 100644 index 000000000..17bbc0506 --- /dev/null +++ b/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" +) + +/* + + If org to org, treat it like a new backend. Then go through the multi/single logic + + 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") +} diff --git a/internal/command/meta_backend_migrate.go b/internal/command/meta_backend_migrate.go index e998b0ffd..132bd96ef 100644 --- a/internal/command/meta_backend_migrate.go +++ b/internal/command/meta_backend_migrate.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -46,25 +47,18 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { log.Printf("[TRACE] 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 bool - sourceWorkspaces, err := opts.Source.Workspaces() - if err == backend.ErrWorkspacesNotSupported { - sourceSingleState = true - err = nil - } - if err != nil { - return fmt.Errorf(strings.TrimSpace( - errMigrateLoadStates), opts.SourceType, err) - } + var sourceSingleState, destinationSingleState, sourceTFC, destinationTFC bool - destinationWorkspaces, err := opts.Destination.Workspaces() - if err == backend.ErrWorkspacesNotSupported { - destinationSingleState = true - err = nil - } + _, sourceTFC = opts.Source.(*cloud.Cloud) + _, destinationTFC = opts.Destination.(*cloud.Cloud) + + sourceWorkspaces, sourceSingleState, err := retrieveWorkspaces(opts.Source, opts.SourceType) if err != nil { - return fmt.Errorf(strings.TrimSpace( - errMigrateLoadStates), opts.DestinationType, err) + return err + } + destinationWorkspaces, destinationSingleState, err := retrieveWorkspaces(opts.Destination, opts.SourceType) + if err != nil { + return err } // Set up defaults @@ -103,6 +97,9 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { // Determine migration behavior based on whether the source/destination // supports multi-state. switch { + case sourceTFC || destinationTFC: + return m.backendMigrateTFC(opts) + // Single-state to single-state. This is the easiest case: we just // copy the default state directly. case sourceSingleState && destinationSingleState: @@ -497,6 +494,91 @@ func (m *Meta) backendMigrateNonEmptyConfirm( return m.confirm(inputOpts) } +func retrieveWorkspaces(back backend.Backend, sourceType string) ([]string, bool, error) { + var singleState bool + var err error + workspaces, err := back.Workspaces() + if err == backend.ErrWorkspacesNotSupported { + singleState = true + err = nil + } + if err != nil { + return nil, singleState, fmt.Errorf(strings.TrimSpace( + errMigrateLoadStates), sourceType, err) + } + + return workspaces, singleState, err +} + +func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { + _, sourceTFC := opts.Source.(*cloud.Cloud) + cloudBackendDestination, destinationTFC := opts.Destination.(*cloud.Cloud) + + sourceWorkspaces, sourceSingleState, err := retrieveWorkspaces(opts.Source, opts.SourceType) + if err != nil { + return err + } + //to be used below, not yet implamented + // destinationWorkspaces, destinationSingleState + _, _, err = retrieveWorkspaces(opts.Destination, opts.SourceType) + if err != nil { + return err + } + + // from TFC to non-TFC backend + if sourceTFC && !destinationTFC { + // From Terraform Cloud to another backend. This is not yet implemented, and + // we recommend people to use the TFC API. + 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). + + sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1 && sourceWorkspaces[0] == backend.DefaultStateName) + if sourceSingle { + if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy { + // If we know the name via WorkspaceNameStrategy, then set the + // destinationWorkspace to the new Name and skip the user prompt. Here the + // destinationWorkspace is not set to `default` thereby we will create it + // in TFC if it does not exist. + opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name + } + // 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 + // strategy. + return m.backendMigrateState_s_s(opts) + } + + destinationTagsStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceTagsStrategy + destinationNameStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy + + multiSource := !sourceSingleState && len(sourceWorkspaces) > 1 + if multiSource && destinationNameStrategy { + // we have to take the current workspace from the source and migrate that + // over to destination. Since there is multiple sources, and we are using a + // name strategy, we will only migrate the current workspace. + panic("not yet implemented") + } + + // Multiple sources, and using tags strategy. So migrate every source + // workspace over to new one, prompt for workspace name pattern (*), + // and start migrating, and create tags for each workspace. + if multiSource && destinationTagsStrategy { + // TODO: see internal/cloud/e2e/migrate_state_multi_to_tfc_test.go for notes + panic("not yet implemented") + } + + return nil +} + const errMigrateLoadStates = ` Error inspecting states in the %q backend: %s @@ -541,6 +623,12 @@ The state in the previous backend remains intact and unmodified. Please resolve the error above and try again. ` +const errTFCMigrateNotYetImplemented = ` +Migrating state from Terraform Cloud to another backend is not yet implemented. + +Please use the API to do this: https://www.terraform.io/docs/cloud/api/state-versions.html +` + const inputBackendMigrateEmpty = ` Pre-existing state was found while migrating the previous %q backend to the newly configured %q backend. No existing state was found in the newly