diff --git a/internal/backend/remote/backend.go b/internal/backend/remote/backend.go index bc4c03175..4f51966c5 100644 --- a/internal/backend/remote/backend.go +++ b/internal/backend/remote/backend.go @@ -569,6 +569,17 @@ func (b *Remote) workspaces() ([]string, error) { return names, nil } +// WorkspaceNamePattern provides an appropriate workspace renaming pattern for backend migration +// purposes (handled outside of this package), based on previous usage of this backend with the +// 'prefix' workspace functionality. As of this writing, see meta_backend.migrate.go +func (b *Remote) WorkspaceNamePattern() string { + if b.prefix != "" { + return b.prefix + "*" + } + + return "" +} + // DeleteWorkspace implements backend.Enhanced. func (b *Remote) DeleteWorkspace(name string) error { if b.workspace == "" && name == backend.DefaultStateName { diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 2ebc61c6f..005237845 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1002,7 +1002,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista if output { // Notify the user m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset]%s\n\n", + "[reset]%s\n", strings.TrimSpace(outputBackendReconfigure)))) } @@ -1021,7 +1021,9 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista if c.Type == "cloud" { output = fmt.Sprintf(outputBackendMigrateChangeCloud, s.Backend.Type) } - m.Ui.Output(strings.TrimSpace(output)) + m.Ui.Output(m.Colorize().Color(fmt.Sprintf( + "[reset]%s\n", + strings.TrimSpace(output)))) } // Grab the existing backend diff --git a/internal/command/meta_backend_migrate.go b/internal/command/meta_backend_migrate.go index 2999a8c59..508199ade 100644 --- a/internal/command/meta_backend_migrate.go +++ b/internal/command/meta_backend_migrate.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/remote" "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" @@ -635,9 +636,32 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa defaultNewName[sourceWorkspaces[i]] = newName } } - pattern, err := m.promptMultiStateMigrationPattern(opts.SourceType) - if err != nil { - return err + + // Fetch the pattern that will be used to rename the workspaces for Terraform Cloud. + // + // * For the general case, this will be a pattern provided by the user. + // + // * Specifically for a migration from the "remote" backend using 'prefix', we will + // instead 'migrate' the workspaces using a pattern based on the old prefix+name, + // not allowing a user to accidentally input the wrong pattern to line up with + // what the the remote backend was already using before (which presumably already + // meets the naming considerations for Terraform Cloud). + // In other words, this is a fast-track migration path from the remote backend, retaining + // how things already are in Terraform Cloud with no user intervention needed. + pattern := "" + if remoteBackend, ok := opts.Source.(*remote.Remote); ok { + if err := m.promptRemotePrefixToCloudTagsMigration(opts); err != nil { + return err + } + pattern = remoteBackend.WorkspaceNamePattern() + log.Printf("[TRACE] backendMigrateTFC: Remote backend reports workspace name pattern as: %q", pattern) + } + + if pattern == "" { + pattern, err = m.promptMultiStateMigrationPattern(opts.SourceType) + if err != nil { + return err + } } // Go through each and migrate @@ -712,6 +736,27 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa return nil } +func (m *Meta) promptRemotePrefixToCloudTagsMigration(opts *backendMigrateOpts) error { + migrate := opts.force + if !migrate { + var err error + migrate, err = m.confirm(&terraform.InputOpts{ + Id: "backend-migrate-remote-multistate-to-cloud", + Query: "Do you wish to proceed?", + Description: strings.TrimSpace(tfcInputBackendMigrateRemoteMultiToCloud), + }) + if err != nil { + return fmt.Errorf("Error asking for state migration action: %s", err) + } + } + + if !migrate { + return fmt.Errorf("Migration aborted by user.") + } + + return nil +} + // Multi-state to single state. func (m *Meta) promptMultiToSingleCloudMigration(opts *backendMigrateOpts) error { migrate := opts.force @@ -867,11 +912,26 @@ For more information on workspace naming, see https://www.terraform.io/docs/clou ` const tfcInputBackendMigrateMultiToSingle = ` -The previous backend %[1]q has multiple workspaces, but Terraform Cloud has been -configured to use a single workspace (%[2]q). By continuing, you will only -migrate your current workspace. If you wish to migrate all workspaces from the -previous backend, use the 'tags' strategy in your workspace configuration block -instead. +The previous backend %[1]q has multiple workspaces, but Terraform Cloud has +been configured to use a single workspace (%[2]q). By continuing, you will +only migrate your current workspace. If you wish to migrate all workspaces +from the previous backend, you may cancel this operation and use the 'tags' +strategy in your workspace configuration block instead. + +Enter "yes" to proceed or "no" to cancel. +` + +const tfcInputBackendMigrateRemoteMultiToCloud = ` +When migrating from the 'remote' backend to Terraform's native integration +with Terraform Cloud, Terraform will automatically create or use existing +workspaces based on the previous backend configuration's 'prefix' value. + +When the migration is complete, workspace names in Terraform will match the +fully qualified Terraform Cloud workspace name. If necessary, the workspace +tags configured in the 'cloud' option block will be added to the associated +Terraform Cloud workspaces. + +Enter "yes" to proceed or "no" to cancel. ` const inputBackendMigrateEmpty = `