From f410a5bb26bbcce4169f2ea93dfc6fa24d8ecf08 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Tue, 28 Aug 2018 14:02:18 +0200 Subject: [PATCH] backend/migrations: migrate the default state Certain backends (currently only the `remote` backend) do not support using both the default and named workspaces at the same time. To make the migration easier for users that currently use both types of workspaces, this commit adds logic to ask the user for a new workspace name during the migration process. --- backend/backend.go | 10 +- command/meta_backend_migrate.go | 147 +++++++++++++++--- command/meta_backend_test.go | 61 ++++++-- .../local-state.tfstate | 2 +- 4 files changed, 182 insertions(+), 38 deletions(-) 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" }