diff --git a/backend/backend.go b/backend/backend.go index f10c27c9f..f45be9dc5 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -22,15 +22,15 @@ const DefaultStateName = "default" // This must be returned rather than a custom error so that the Terraform // CLI can detect it and handle it appropriately. var ( + // ErrDefaultStateNotSupported is returned when an operation does not support + // using the default state, but requires a named state to be selected. + ErrDefaultStateNotSupported = errors.New("default state not supported\n" + + "You can create a new workspace with the \"workspace new\" command.") + // ErrNamedStatesNotSupported is returned when a named state operation // isn't supported. ErrNamedStatesNotSupported = errors.New("named states not supported") - // ErrDefaultStateNotSupported is returned when an operation does not support - // using the default state, but requires a named state to be selected. - ErrDefaultStateNotSupported = errors.New("default state not supported\n\n" + - "You can create a new workspace wth the \"workspace new\" command") - // ErrOperationNotSupported is returned when an unsupported operation // is detected by the configured backend. ErrOperationNotSupported = errors.New("operation not supported") diff --git a/command/meta_backend_migrate.go b/command/meta_backend_migrate.go index dc8d503b2..4c5aedd9a 100644 --- a/command/meta_backend_migrate.go +++ b/command/meta_backend_migrate.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "strings" "github.com/hashicorp/terraform/backend" @@ -16,6 +17,17 @@ import ( "github.com/hashicorp/terraform/terraform" ) +type backendMigrateOpts struct { + OneType, TwoType string + One, Two backend.Backend + + // Fields below are set internally when migrate is called + + oneEnv string // source env + twoEnv string // dest env + force bool // if true, won't ask for confirmation +} + // backendMigrateState handles migrating (copying) state from one backend // to another. This function handles asking the user for confirmation // as well as the copy itself. @@ -157,7 +169,56 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error { } } - return nil + // Its possible that the currently selected workspace is not migrated, + // so we call selectWorkspace to ensure a valid workspace is selected. + return m.selectWorkspace(opts.Two) +} + +// selectWorkspace gets a list of migrated workspaces and then checks +// if the currently selected workspace is valid. If not, it will ask +// the user to select a workspace from the list. +func (m *Meta) selectWorkspace(b backend.Backend) error { + workspaces, err := b.States() + if err != nil { + return fmt.Errorf("Failed to get migrated workspaces: %s", err) + } + if len(workspaces) == 0 { + return fmt.Errorf(errBackendNoMigratedWorkspaces) + } + + // Get the currently selected workspace. + workspace := m.Workspace() + + // Check if any of the migrated workspaces match the selected workspace + // and create a numbered list with migrated workspaces. + var list strings.Builder + for i, w := range workspaces { + if w == workspace { + return nil + } + fmt.Fprintf(&list, "%d. %s\n", i+1, w) + } + + // If the selected workspace is not migrated, ask the user to select + // a workspace from the list of migrated workspaces. + v, err := m.UIInput().Input(&terraform.InputOpts{ + Id: "select-workspace", + Query: fmt.Sprintf( + "[reset][bold][yellow]The currently selected workspace (%s) is not migrated.[reset]", + workspace), + Description: fmt.Sprintf( + strings.TrimSpace(inputBackendSelectWorkspace), list.String()), + }) + if err != nil { + return fmt.Errorf("Error asking to select workspace: %s", err) + } + + idx, err := strconv.Atoi(v) + if err != nil || (idx < 1 || idx > len(workspaces)) { + return fmt.Errorf("Error selecting workspace: input not a valid number") + } + + return m.SetWorkspace(workspaces[idx-1]) } // Multi-state to single state. @@ -209,15 +270,48 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { errMigrateSingleLoadDefault), opts.OneType, err) } + // Do not migrate workspaces without state. + if stateOne.State() == nil { + return nil + } + stateTwo, err := opts.Two.State(opts.twoEnv) + if err == backend.ErrDefaultStateNotSupported { + // If the backend doesn't support using the default state, we ask the user + // for a new name and migrate the default state to the given named state. + stateTwo, err = func() (state.State, error) { + name, err := m.UIInput().Input(&terraform.InputOpts{ + Id: "new-state-name", + Query: fmt.Sprintf( + "[reset][bold][yellow]The %q backend configuration only allows "+ + "named workspaces![reset]", + opts.TwoType), + Description: strings.TrimSpace(inputBackendNewWorkspaceName), + }) + if err != nil { + return nil, fmt.Errorf("Error asking for new state name: %s", err) + } + + // Update the name of the target state. + opts.twoEnv = name + + stateTwo, err := opts.Two.State(opts.twoEnv) + if err != nil { + return nil, err + } + + // If the currently selected workspace is the default workspace, then set + // the named workspace as the new selected workspace. + if m.Workspace() == backend.DefaultStateName { + if err := m.SetWorkspace(opts.twoEnv); err != nil { + return nil, fmt.Errorf("Failed to set new workspace: %s", err) + } + } + + return stateTwo, nil + }() + } if err != nil { - if err == backend.ErrDefaultStateNotSupported && stateOne.State() == nil { - // When using named workspaces it is common that the default - // workspace is not actually used. So we first check if there - // actually is a state to be migrated, if not we just return - // and silently ignore the unused default worksopace. - return nil - } return fmt.Errorf(strings.TrimSpace( errMigrateSingleLoadDefault), opts.TwoType, err) } @@ -392,17 +486,6 @@ func (m *Meta) backendMigrateNonEmptyConfirm( return m.confirm(inputOpts) } -type backendMigrateOpts struct { - OneType, TwoType string - One, Two backend.Backend - - // Fields below are set internally when migrate is called - - oneEnv string // source env - twoEnv string // dest env - force bool // if true, won't ask for confirmation -} - const errMigrateLoadStates = ` Error inspecting states in the %q backend: %s @@ -447,6 +530,14 @@ The state in the previous backend remains intact and unmodified. Please resolve the error above and try again. ` +const errBackendNoMigratedWorkspaces = ` +No workspaces are migrated. Use the "terraform workspace" command to create +and select a new workspace. + +If the backend already contains existing workspaces, you may need to update +the workspace name or prefix in the backend configuration. +` + 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 @@ -478,9 +569,9 @@ up, or cancel altogether, answer "no" and Terraform will abort. ` const inputBackendMigrateMultiToMulti = ` -Both the existing %[1]q backend and the newly configured %[2]q backend support -workspaces. When migrating between backends, Terraform will copy all -workspaces (with the same names). THIS WILL OVERWRITE any conflicting +Both the existing %[1]q backend and the newly configured %[2]q backend +support workspaces. When migrating between backends, Terraform will copy +all workspaces (with the same names). THIS WILL OVERWRITE any conflicting states in the destination. Terraform initialization doesn't currently migrate only select workspaces. @@ -490,3 +581,15 @@ pull and push those states. If you answer "yes", Terraform will migrate all states. If you answer "no", Terraform will abort. ` + +const inputBackendNewWorkspaceName = ` +Please provide a new workspace name (e.g. dev, test) that will be used +to migrate the existing default workspace. +` + +const inputBackendSelectWorkspace = ` +This is expected behavior when the selected workspace did not have an +existing non-empty state. Please enter a number to select a workspace: + +%s +` diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index 8e3059379..701403a05 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -1098,7 +1098,6 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) { // Ask input defer testInputMap(t, map[string]string{ - "backend-migrate-to-new": "yes", "backend-migrate-copy-to-empty": "yes", })() @@ -1154,7 +1153,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) { // Ask input defer testInputMap(t, map[string]string{ - "backend-migrate-to-new": "yes", "backend-migrate-copy-to-empty": "yes", })() @@ -1209,7 +1207,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) { // Ask input defer testInputMap(t, map[string]string{ - "backend-migrate-to-new": "yes", "backend-migrate-multistate-to-single": "yes", "backend-migrate-copy-to-empty": "yes", })() @@ -1276,7 +1273,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) // Ask input defer testInputMap(t, map[string]string{ - "backend-migrate-to-new": "yes", "backend-migrate-multistate-to-single": "yes", "backend-migrate-copy-to-empty": "yes", })() @@ -1339,7 +1335,6 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { // Ask input defer testInputMap(t, map[string]string{ - "backend-migrate-to-new": "yes", "backend-migrate-multistate-to-multistate": "yes", })() @@ -1438,17 +1433,63 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing // Ask input defer testInputMap(t, map[string]string{ - "backend-migrate-to-new": "yes", "backend-migrate-multistate-to-multistate": "yes", + "new-state-name": "env1", })() // Setup the meta m := testMetaBackend(t, nil) // Get the backend - _, err := m.Backend(&BackendOpts{Init: true}) - if err == nil || !strings.Contains(err.Error(), "default state not supported") { - t.Fatalf("expected error to contain %q\ngot: %s", "default state not supported", err) + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check resulting states + states, err := b.States() + if err != nil { + t.Fatalf("bad: %s", err) + } + + sort.Strings(states) + expected := []string{"env1", "env2"} + if !reflect.DeepEqual(states, expected) { + t.Fatalf("bad: %#v", states) + } + + { + // Check the renamed default state + s, err := b.State("env1") + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state should not be nil") + } + if state.Lineage != "backend-change-env1" { + t.Fatalf("bad: %#v", state) + } + } + + { + // Verify existing workspaces exist + envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename) + if _, err := os.Stat(envPath); err != nil { + t.Fatal("env should exist") + } + } + + { + // Verify new workspaces exist + envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename) + if _, err := os.Stat(envPath); err != nil { + t.Fatal("env should exist") + } } } @@ -1468,8 +1509,8 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *test // Ask input defer testInputMap(t, map[string]string{ - "backend-migrate-to-new": "yes", "backend-migrate-multistate-to-multistate": "yes", + "select-workspace": "1", })() // Setup the meta diff --git a/command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate b/command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate index 88c1d86ec..980f732f6 100644 --- a/command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate +++ b/command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate @@ -2,5 +2,5 @@ "version": 3, "terraform_version": "0.8.2", "serial": 7, - "lineage": "backend-change" + "lineage": "backend-change-env1" }