command/init: Be explicit that some options are not relevant for Cloud

There are a few command line options for "terraform init" which are only
relevant when working with traditional backends, with the Cloud
integration previously just mostly ignoring them, or sometimes misbehaving
slightly due to them creating an unreasonable situation.

Now we'll catch these and return explicit errors, in order to be clear
that these options are not needed nor supported in Cloud mode.
This commit is contained in:
Martin Atkins 2021-11-12 17:07:10 -08:00
parent c28b57b4d6
commit bac59d2480
11 changed files with 595 additions and 124 deletions

View File

@ -28,7 +28,7 @@ func Test_backend_apply_before_init(t *testing.T) {
commands: []tfCommand{
{
command: []string{"apply"},
expectedCmdOutput: `Terraform Cloud initialization required, please run "terraform init"`,
expectedCmdOutput: `Terraform Cloud initialization required: please run "terraform init"`,
expectError: true,
},
},
@ -62,7 +62,7 @@ func Test_backend_apply_before_init(t *testing.T) {
commands: []tfCommand{
{
command: []string{"apply"},
expectedCmdOutput: `Terraform Cloud initialization required, please run "terraform init"`,
expectedCmdOutput: `Terraform Cloud initialization required: please run "terraform init"`,
expectError: true,
},
},

View File

@ -60,7 +60,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Do you want to copy only your current workspace?`,
userInput: []string{"yes", "yes"},
postInputOutput: []string{
@ -127,7 +127,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Do you want to copy only your current workspace?`,
userInput: []string{"yes", "yes"},
postInputOutput: []string{
@ -195,7 +195,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Do you want to copy only your current workspace?`,
userInput: []string{"yes", "yes"},
postInputOutput: []string{
@ -268,7 +268,6 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
if tfCmd.expectedCmdOutput != "" {
got, err := exp.ExpectString(tfCmd.expectedCmdOutput)
if err != nil {
t.Fatalf("error while waiting for output\nwant: %s\nerror: %s\noutput\n%s", tfCmd.expectedCmdOutput, err, got)
}
}
@ -365,7 +364,7 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
userInput: []string{"dev", "1", "app-*"},
postInputOutput: []string{
@ -470,7 +469,7 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
userInput: []string{"dev", "1", "app-*"},
postInputOutput: []string{

View File

@ -42,8 +42,8 @@ func Test_migrate_remote_backend_name_to_tfc_name(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Should Terraform migrate your existing state?`,
userInput: []string{"yes"},
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
},
@ -163,7 +163,7 @@ func Test_migrate_remote_backend_name_to_tfc_same_name(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Terraform Cloud has been successfully initialized!`,
},
{
@ -283,8 +283,8 @@ func Test_migrate_remote_backend_name_to_tfc_name_different_org(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Should Terraform migrate your existing state?`,
userInput: []string{"yes"},
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
},
@ -414,11 +414,11 @@ func Test_migrate_remote_backend_name_to_tfc_tags(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
userInput: []string{"cloud-workspace", "yes"},
postInputOutput: []string{
`Do you want to copy existing state to Terraform Cloud?`,
`Should Terraform migrate your existing state?`,
`Terraform Cloud has been successfully initialized!`},
},
{
@ -544,8 +544,8 @@ func Test_migrate_remote_backend_prefix_to_tfc_name_strategy_single_workspace(t
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Should Terraform migrate your existing state?`,
userInput: []string{"yes"},
postInputOutput: []string{
`Terraform Cloud has been successfully initialized!`},
@ -679,7 +679,7 @@ func Test_migrate_remote_backend_prefix_to_tfc_name_strategy_multi_workspace(t *
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy only your current workspace?`,
userInput: []string{"yes"},
postInputOutput: []string{
@ -822,11 +822,11 @@ func Test_migrate_remote_backend_prefix_to_tfc_tags_strategy_single_workspace(t
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
userInput: []string{"cloud-workspace", "yes"},
postInputOutput: []string{
`Do you want to copy existing state to Terraform Cloud?`,
`Should Terraform migrate your existing state?`,
`Terraform Cloud has been successfully initialized!`},
},
{
@ -961,7 +961,7 @@ func Test_migrate_remote_backend_prefix_to_tfc_tags_strategy_multi_workspace(t *
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Do you wish to proceed?`,
userInput: []string{"yes"},
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},

View File

@ -47,8 +47,8 @@ func Test_migrate_single_to_tfc(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
command: []string{"init"},
expectedCmdOutput: `Should Terraform migrate your existing state?`,
userInput: []string{"yes"},
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
},
@ -96,11 +96,11 @@ func Test_migrate_single_to_tfc(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
userInput: []string{"new-workspace", "yes"},
postInputOutput: []string{
`Do you want to copy existing state to Terraform Cloud?`,
`Should Terraform migrate your existing state?`,
`Terraform Cloud has been successfully initialized!`},
},
{

View File

@ -36,7 +36,7 @@ func Test_migrate_tfc_to_other(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Migrating state from Terraform Cloud to another backend is not yet implemented.`,
expectError: true,
},

View File

@ -69,8 +69,8 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Should Terraform migrate your existing state?`,
userInput: []string{"yes"},
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
},
@ -127,11 +127,11 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
userInput: []string{"new-workspace", "yes"},
postInputOutput: []string{
`Do you want to copy existing state to Terraform Cloud?`,
`Should Terraform migrate your existing state?`,
`Terraform Cloud has been successfully initialized!`},
},
{
@ -196,7 +196,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
command: []string{"init"},
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
expectError: true,
userInput: []string{"new-workspace", "yes"},
@ -367,11 +367,11 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy only your current workspace?`,
userInput: []string{"yes", "yes"},
postInputOutput: []string{
`Do you want to copy existing state to Terraform Cloud?`,
`Should Terraform migrate your existing state?`,
`Terraform Cloud has been successfully initialized!`},
},
{
@ -446,7 +446,7 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) {
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
command: []string{"init", "-ignore-remote-version"},
expectedCmdOutput: `Would you like to rename your workspaces?`,
userInput: []string{"1", "new-*", "1"},
postInputOutput: []string{

View File

@ -213,7 +213,7 @@ func (c *InitCommand) Run(args []string) int {
switch {
case config.Module.CloudConfig != nil:
be, backendOutput, backendDiags := c.initCloud(config.Module)
be, backendOutput, backendDiags := c.initCloud(config.Module, flagConfigExtra)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
@ -366,9 +366,18 @@ func (c *InitCommand) getModules(path string, earlyRoot *tfconfig.Module, upgrad
return true, installAbort, diags
}
func (c *InitCommand) initCloud(root *configs.Module) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
func (c *InitCommand) initCloud(root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing Terraform Cloud..."))
if len(extraConfig.AllItems()) != 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid command-line option",
"The -backend-config=... command line option is only for state backends, and is not applicable to Terraform Cloud-based configurations.\n\nTo change the set of workspaces associated with this configuration, edit the Cloud configuration block in the root module.",
))
return nil, true, diags
}
backendConfig := root.CloudConfig.ToBackendConfig()
opts := &BackendOpts{

View File

@ -17,6 +17,7 @@ import (
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
@ -24,6 +25,7 @@ import (
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/states/statemgr"
)
@ -937,6 +939,318 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) {
}
}
func TestInit_backendCloudInvalidOptions(t *testing.T) {
// There are various "terraform init" options that are only for
// traditional backends and not applicable to Terraform Cloud mode.
// For those, we want to return an explicit error rather than
// just silently ignoring them, so that users will be aware that
// Cloud mode has more of an expected "happy path" than the
// less-vertically-integrated backends do, and to avoid these
// unapplicable options becoming compatibility constraints for
// future evolution of Cloud mode.
// We use the same starting fixture for all of these tests, but some
// of them will customize it a bit as part of their work.
setupTempDir := func(t *testing.T) func() {
t.Helper()
td := tempDir(t)
testCopyDir(t, testFixturePath("init-cloud-simple"), td)
unChdir := testChdir(t, td)
return func() {
unChdir()
os.RemoveAll(td)
}
}
// Some of the tests need a non-empty placeholder state file to work
// with.
fakeState := states.BuildState(func(cb *states.SyncState) {
// Having a root module output value should be enough for this
// state file to be considered "non-empty" and thus a candidate
// for migration.
cb.SetOutputValue(
addrs.OutputValue{Name: "a"}.Absolute(addrs.RootModuleInstance),
cty.True,
false,
)
})
fakeStateFile := &statefile.File{
Lineage: "boop",
Serial: 4,
TerraformVersion: version.Must(version.NewVersion("1.0.0")),
State: fakeState,
}
var fakeStateBuf bytes.Buffer
err := statefile.WriteForTest(fakeStateFile, &fakeStateBuf)
if err != nil {
t.Error(err)
}
fakeStateBytes := fakeStateBuf.Bytes()
t.Run("-backend-config", func(t *testing.T) {
defer setupTempDir(t)()
// We have -backend-config as a pragmatic way to dynamically set
// certain settings of backends that tend to vary depending on
// where Terraform is running, such as AWS authentication profiles
// that are naturally local only to the machine where Terraform is
// running. Those needs don't apply to Terraform Cloud, because
// the remote workspace encapsulates all of the details of how
// operations and state work in that case, and so the Cloud
// configuration is only about which workspaces we'll be working
// with.
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-backend-config=anything"}
if code := c.Run(args); code == 0 {
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
}
gotStderr := ui.ErrorWriter.String()
wantStderr := `
Error: Invalid command-line option
The -backend-config=... command line option is only for state backends, and
is not applicable to Terraform Cloud-based configurations.
To change the set of workspaces associated with this configuration, edit the
Cloud configuration block in the root module.
`
if diff := cmp.Diff(wantStderr, gotStderr); diff != "" {
t.Errorf("wrong error output\n%s", diff)
}
})
t.Run("-reconfigure", func(t *testing.T) {
defer setupTempDir(t)()
// The -reconfigure option was originally imagined as a way to force
// skipping state migration when migrating between backends, but it
// has a historical flaw that it doesn't work properly when the
// initial situation is the implicit local backend with a state file
// present. The Terraform Cloud migration path has some additional
// steps to take care of more details automatically, and so
// -reconfigure doesn't really make sense in that context, particularly
// with its design bug with the handling of the implicit local backend.
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-reconfigure"}
if code := c.Run(args); code == 0 {
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
}
gotStderr := ui.ErrorWriter.String()
wantStderr := `
Error: Invalid command-line option
The -reconfigure option is for in-place reconfiguration of state backends
only, and is not needed when changing Terraform Cloud settings.
When using Terraform Cloud, initialization automatically activates any new
Cloud configuration settings.
`
if diff := cmp.Diff(wantStderr, gotStderr); diff != "" {
t.Errorf("wrong error output\n%s", diff)
}
})
t.Run("-reconfigure when migrating in", func(t *testing.T) {
defer setupTempDir(t)()
// We have a slightly different error message for the case where we
// seem to be trying to migrate to Terraform Cloud with existing
// state or explicit backend already present.
if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil {
t.Fatal(err)
}
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-reconfigure"}
if code := c.Run(args); code == 0 {
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
}
gotStderr := ui.ErrorWriter.String()
wantStderr := `
Error: Invalid command-line option
The -reconfigure option is unsupported when migrating to Terraform Cloud,
because activating Terraform Cloud involves some additional steps.
`
if diff := cmp.Diff(wantStderr, gotStderr); diff != "" {
t.Errorf("wrong error output\n%s", diff)
}
})
t.Run("-migrate-state", func(t *testing.T) {
defer setupTempDir(t)()
// In Cloud mode, migrating in or out always proposes migrating state
// and changing configuration while staying in cloud mode never migrates
// state, so this special option isn't relevant.
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-migrate-state"}
if code := c.Run(args); code == 0 {
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
}
gotStderr := ui.ErrorWriter.String()
wantStderr := `
Error: Invalid command-line option
The -migrate-state option is for migration between state backends only, and
is not applicable when using Terraform Cloud.
State storage is handled automatically by Terraform Cloud and so the state
storage location is not configurable.
`
if diff := cmp.Diff(wantStderr, gotStderr); diff != "" {
t.Errorf("wrong error output\n%s", diff)
}
})
t.Run("-migrate-state when migrating in", func(t *testing.T) {
defer setupTempDir(t)()
// We have a slightly different error message for the case where we
// seem to be trying to migrate to Terraform Cloud with existing
// state or explicit backend already present.
if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil {
t.Fatal(err)
}
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-migrate-state"}
if code := c.Run(args); code == 0 {
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
}
gotStderr := ui.ErrorWriter.String()
wantStderr := `
Error: Invalid command-line option
The -migrate-state option is for migration between state backends only, and
is not applicable when using Terraform Cloud.
Terraform Cloud migration has additional steps, configured by interactive
prompts.
`
if diff := cmp.Diff(wantStderr, gotStderr); diff != "" {
t.Errorf("wrong error output\n%s", diff)
}
})
t.Run("-force-copy", func(t *testing.T) {
defer setupTempDir(t)()
// In Cloud mode, migrating in or out always proposes migrating state
// and changing configuration while staying in cloud mode never migrates
// state, so this special option isn't relevant.
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-force-copy"}
if code := c.Run(args); code == 0 {
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
}
gotStderr := ui.ErrorWriter.String()
wantStderr := `
Error: Invalid command-line option
The -force-copy option is for migration between state backends only, and is
not applicable when using Terraform Cloud.
State storage is handled automatically by Terraform Cloud and so the state
storage location is not configurable.
`
if diff := cmp.Diff(wantStderr, gotStderr); diff != "" {
t.Errorf("wrong error output\n%s", diff)
}
})
t.Run("-force-copy when migrating in", func(t *testing.T) {
defer setupTempDir(t)()
// We have a slightly different error message for the case where we
// seem to be trying to migrate to Terraform Cloud with existing
// state or explicit backend already present.
if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil {
t.Fatal(err)
}
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-force-copy"}
if code := c.Run(args); code == 0 {
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
}
gotStderr := ui.ErrorWriter.String()
wantStderr := `
Error: Invalid command-line option
The -force-copy option is for migration between state backends only, and is
not applicable when using Terraform Cloud.
Terraform Cloud migration has additional steps, configured by interactive
prompts.
`
if diff := cmp.Diff(wantStderr, gotStderr); diff != "" {
t.Errorf("wrong error output\n%s", diff)
}
})
}
// make sure inputFalse stops execution on migrate
func TestInit_inputFalse(t *testing.T) {
td := tempDir(t)

View File

@ -603,7 +603,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
return nil, diags
}
if !m.migrateState {
if s.Backend.Type != "cloud" && !m.migrateState {
diags = diags.Append(migrateOrReconfigDiag)
return nil, diags
}
@ -618,7 +618,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
initReason := "Initial configuration of Terraform Cloud"
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Terraform Cloud initialization required, please run \"terraform init\"",
"Terraform Cloud initialization required: please run \"terraform init\"",
fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason),
))
} else {
@ -670,39 +670,51 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
}
log.Printf("[TRACE] Meta.Backend: backend configuration has changed (from type %q to type %q)", s.Backend.Type, c.Type)
cloudMode := cloud.DetectConfigChangeType(s.Backend, c, false)
initReason := ""
switch {
case c.Type == "cloud":
initReason = fmt.Sprintf("Backend configuration changed from %q to Terraform Cloud", s.Backend.Type)
case s.Backend.Type != c.Type:
initReason = fmt.Sprintf("Backend configuration changed from %q to %q", s.Backend.Type, c.Type)
switch cloudMode {
case cloud.ConfigMigrationIn:
initReason = fmt.Sprintf("Changed from backend %q to Terraform Cloud", s.Backend.Type)
case cloud.ConfigMigrationOut:
initReason = fmt.Sprintf("Changed from Terraform Cloud to backend %q", s.Backend.Type)
case cloud.ConfigChangeInPlace:
initReason = "Terraform Cloud configuration has changed"
default:
initReason = fmt.Sprintf("Backend configuration changed for %q", c.Type)
switch {
case s.Backend.Type != c.Type:
initReason = fmt.Sprintf("Backend type changed from %q to %q", s.Backend.Type, c.Type)
default:
initReason = fmt.Sprintf("Configuration changed for backend %q", c.Type)
}
}
if !opts.Init {
if c.Type == "cloud" {
switch cloudMode {
case cloud.ConfigChangeInPlace:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Terraform Cloud initialization required, please run \"terraform init\"",
"Terraform Cloud initialization required: please run \"terraform init\"",
fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason),
))
} else {
case cloud.ConfigMigrationIn:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Backend initialization required, please run \"terraform init\"",
"Terraform Cloud initialization required: please run \"terraform init\"",
fmt.Sprintf(strings.TrimSpace(errBackendInitCloudMigration), initReason),
))
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Backend initialization required: please run \"terraform init\"",
fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason),
))
}
return nil, diags
}
if !m.migrateState {
if c.Type == "cloud" {
diags = diags.Append(migrateOrReconfigDiagCloud)
} else {
diags = diags.Append(migrateOrReconfigDiag)
}
if !cloudMode.InvolvesCloud() && !m.migrateState {
diags = diags.Append(migrateOrReconfigDiag)
return nil, diags
}
@ -809,20 +821,29 @@ func (m *Meta) backendFromState() (backend.Backend, tfdiags.Diagnostics) {
// Unconfiguring a backend (moving from backend => local).
func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool) (backend.Backend, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
s := sMgr.State()
cloudMode := cloud.DetectConfigChangeType(s.Backend, c, false)
diags = diags.Append(m.assertSupportedCloudInitOptions(cloudMode))
if diags.HasErrors() {
return nil, diags
}
// Get the backend type for output
backendType := s.Backend.Type
if s.Backend.Type == "cloud" {
m.Ui.Output(strings.TrimSpace(outputBackendMigrateLocalFromCloud))
if cloudMode == cloud.ConfigMigrationOut {
m.Ui.Output("Migrating from Terraform Cloud to local state.")
} else {
m.Ui.Output(fmt.Sprintf(strings.TrimSpace(outputBackendMigrateLocal), s.Backend.Type))
}
// Grab a purely local backend to get the local state if it exists
localB, diags := m.Backend(&BackendOpts{ForceLocal: true, Init: true})
if diags.HasErrors() {
localB, moreDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true})
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
@ -868,11 +889,7 @@ func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr *clistate.Local
// Configuring a backend for the first time.
func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.LocalState) (backend.Backend, tfdiags.Diagnostics) {
// Get the backend
b, configVal, diags := m.backendInitFromConfig(c)
if diags.HasErrors() {
return nil, diags
}
var diags tfdiags.Diagnostics
// Grab a purely local backend to get the local state if it exists
localB, localBDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true})
@ -908,6 +925,19 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local
}
}
cloudMode := cloud.DetectConfigChangeType(nil, c, len(localStates) > 0)
diags = diags.Append(m.assertSupportedCloudInitOptions(cloudMode))
if diags.HasErrors() {
return nil, diags
}
// Get the backend
b, configVal, moreDiags := m.backendInitFromConfig(c)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return nil, diags
}
if len(localStates) > 0 {
// Perform the migration
err = m.backendMigrateState(&backendMigrateOpts{
@ -999,60 +1029,82 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local
// Changing a previously saved backend.
func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool) (backend.Backend, tfdiags.Diagnostics) {
if output {
// Notify the user
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
"[reset]%s\n",
strings.TrimSpace(outputBackendReconfigure))))
}
var diags tfdiags.Diagnostics
// Get the old state
s := sMgr.State()
// Get the backend
b, configVal, diags := m.backendInitFromConfig(c)
cloudMode := cloud.DetectConfigChangeType(s.Backend, c, false)
diags = diags.Append(m.assertSupportedCloudInitOptions(cloudMode))
if diags.HasErrors() {
return nil, diags
}
// no need to confuse the user if the backend types are the same
if s.Backend.Type != c.Type {
output := fmt.Sprintf(outputBackendMigrateChange, s.Backend.Type, c.Type)
if c.Type == "cloud" {
output = fmt.Sprintf(outputBackendMigrateChangeCloud, s.Backend.Type)
if output {
// Notify the user
switch cloudMode {
case cloud.ConfigChangeInPlace:
m.Ui.Output("Terraform Cloud configuration has changed.")
case cloud.ConfigMigrationIn:
m.Ui.Output(fmt.Sprintf("Migrating from backend %q to Terraform Cloud.", s.Backend.Type))
case cloud.ConfigMigrationOut:
m.Ui.Output(fmt.Sprintf("Migrating from Terraform Cloud to backend %q.", c.Type))
default:
if s.Backend.Type != c.Type {
output := fmt.Sprintf(outputBackendMigrateChange, s.Backend.Type, c.Type)
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
"[reset]%s\n",
strings.TrimSpace(output))))
} else {
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
"[reset]%s\n",
strings.TrimSpace(outputBackendReconfigure))))
}
}
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
"[reset]%s\n",
strings.TrimSpace(output))))
}
// Grab the existing backend
oldB, oldBDiags := m.savedBackend(sMgr)
diags = diags.Append(oldBDiags)
if oldBDiags.HasErrors() {
// Get the backend
b, configVal, moreDiags := m.backendInitFromConfig(c)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
// Perform the migration
err := m.backendMigrateState(&backendMigrateOpts{
SourceType: s.Backend.Type,
DestinationType: c.Type,
Source: oldB,
Destination: b,
})
if err != nil {
diags = diags.Append(err)
return nil, diags
}
if m.stateLock {
view := views.NewStateLocker(arguments.ViewHuman, m.View)
stateLocker := clistate.NewLocker(m.stateLockTimeout, view)
if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil {
diags = diags.Append(fmt.Errorf("Error locking state: %s", err))
// If this is a migration into, out of, or irrelevant to Terraform Cloud
// mode then we will do state migration here. Otherwise, we just update
// the working directory initialization directly, because Terraform Cloud
// doesn't have configurable state storage anyway -- we're only changing
// which workspaces are relevant to this configuration, not where their
// state lives.
if cloudMode != cloud.ConfigChangeInPlace {
// Grab the existing backend
oldB, oldBDiags := m.savedBackend(sMgr)
diags = diags.Append(oldBDiags)
if oldBDiags.HasErrors() {
return nil, diags
}
defer stateLocker.Unlock()
// Perform the migration
err := m.backendMigrateState(&backendMigrateOpts{
SourceType: s.Backend.Type,
DestinationType: c.Type,
Source: oldB,
Destination: b,
})
if err != nil {
diags = diags.Append(err)
return nil, diags
}
if m.stateLock {
view := views.NewStateLocker(arguments.ViewHuman, m.View)
stateLocker := clistate.NewLocker(m.stateLockTimeout, view)
if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil {
diags = diags.Append(fmt.Errorf("Error locking state: %s", err))
return nil, diags
}
defer stateLocker.Unlock()
}
}
configJSON, err := ctyjson.Marshal(configVal, b.ConfigSchema().ImpliedType())
@ -1293,6 +1345,55 @@ func (m *Meta) remoteVersionCheck(b backend.Backend, workspace string) tfdiags.D
return diags
}
// assertSupportedCloudInitOptions returns diagnostics with errors if the
// init-related command line options (implied inside the Meta receiver)
// are incompatible with the given cloud configuration change mode.
func (m *Meta) assertSupportedCloudInitOptions(mode cloud.ConfigChangeMode) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if mode.InvolvesCloud() {
log.Printf("[TRACE] Meta.Backend: Terraform Cloud mode initialization type: %s", mode)
if m.reconfigure {
if mode.IsCloudMigration() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid command-line option",
"The -reconfigure option is unsupported when migrating to Terraform Cloud, because activating Terraform Cloud involves some additional steps.",
))
} else {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid command-line option",
"The -reconfigure option is for in-place reconfiguration of state backends only, and is not needed when changing Terraform Cloud settings.\n\nWhen using Terraform Cloud, initialization automatically activates any new Cloud configuration settings.",
))
}
}
if m.migrateState {
name := "-migrate-state"
if m.forceInitCopy {
// -force copy implies -migrate-state in "terraform init",
// so m.migrateState is forced to true in this case even if
// the user didn't actually specify it. We'll use the other
// name here to avoid being confusing, then.
name = "-force-copy"
}
if mode.IsCloudMigration() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid command-line option",
fmt.Sprintf("The %s option is for migration between state backends only, and is not applicable when using Terraform Cloud.\n\nTerraform Cloud migration has additional steps, configured by interactive prompts.", name),
))
} else {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid command-line option",
fmt.Sprintf("The %s option is for migration between state backends only, and is not applicable when using Terraform Cloud.\n\nState storage is handled automatically by Terraform Cloud and so the state storage location is not configurable.", name),
))
}
}
}
return diags
}
//-------------------------------------------------------------------
// Output constants and initialization code
//-------------------------------------------------------------------
@ -1376,17 +1477,26 @@ hasn't changed and try again. At this point, no changes to your existing
configuration or state have been made.
`
const errBackendInitCloudMigration = `
Reason: %s.
Migrating to Terraform Cloud requires reinitialization, to discover which Terraform Cloud workspaces belong to this configuration and to optionally migrate existing state to the corresponding Terraform Cloud workspaces.
To re-initialize, run:
terraform init
Terraform has not yet made changes to your existing configuration or state.
`
const errBackendInitCloud = `
Reason: %s
Reason: %s.
Changes to the Terraform Cloud configuration block require reinitialization.
This allows Terraform to set up the new configuration, copy existing state, etc.
Please run "terraform init" with either the "-reconfigure" or "-migrate-state"
flags to use the current configuration.
Changes to the Terraform Cloud configuration block require reinitialization, to discover any changes to the available workspaces.
If the change reason above is incorrect, please verify your configuration
hasn't changed and try again. At this point, no changes to your existing
configuration or state have been made.
To re-initialize, run:
terraform init
Terraform has not yet made changes to your existing configuration or state.
`
const errBackendWriteSaved = `
@ -1402,16 +1512,9 @@ const outputBackendMigrateChange = `
Terraform detected that the backend type changed from %q to %q.
`
const outputBackendMigrateChangeCloud = `
Terraform detected that the backend type changed from %q to Terraform Cloud.
`
const outputBackendMigrateLocal = `
Terraform has detected you're unconfiguring your previously set %q backend.
`
const outputBackendMigrateLocalFromCloud = `
Terraform has detected you're unconfiguring Terraform Cloud.
`
const outputBackendReconfigure = `
[reset][bold]Backend configuration changed![reset]
@ -1444,10 +1547,3 @@ var migrateOrReconfigDiag = tfdiags.Sourceless(
"A change in the backend configuration has been detected, which may require migrating existing state.\n\n"+
"If you wish to attempt automatic migration of the state, use \"terraform init -migrate-state\".\n"+
`If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".`)
var migrateOrReconfigDiagCloud = tfdiags.Sourceless(
tfdiags.Error,
"Terraform Cloud configuration changed",
"A change in the Terraform Cloud configuration has been detected, which may require migrating existing state.\n\n"+
"If you wish to attempt automatic migration of the state, use \"terraform init -migrate-state\".\n"+
`If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".`)

View File

@ -569,10 +569,20 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
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
// 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.
//
// We do prompt first though, because state migration is mandatory
// for moving to Cloud and the user should get an opportunity to
// confirm that first.
if migrate, err := m.promptSingleToCloudSingleStateMigration(opts); err != nil {
return err
} else if !migrate {
return nil //skip migrating but return successfully
}
return m.backendMigrateState_s_s(opts)
}
@ -752,6 +762,23 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa
return nil
}
func (m *Meta) promptSingleToCloudSingleStateMigration(opts *backendMigrateOpts) (bool, error) {
migrate := opts.force
if !migrate {
var err error
migrate, err = m.confirm(&terraform.InputOpts{
Id: "backend-migrate-state-single-to-cloud-single",
Query: "Do you wish to proceed?",
Description: strings.TrimSpace(tfcInputBackendMigrateStateSingleToCloudSingle),
})
if err != nil {
return false, fmt.Errorf("Error asking for state migration action: %s", err)
}
}
return migrate, nil
}
func (m *Meta) promptRemotePrefixToCloudTagsMigration(opts *backendMigrateOpts) error {
migrate := opts.force
if !migrate {
@ -937,6 +964,19 @@ strategy in your workspace configuration block instead.
Enter "yes" to proceed or "no" to cancel.
`
const tfcInputBackendMigrateStateSingleToCloudSingle = `
As part of migrating to Terraform Cloud, Terraform can optionally copy your
current workspace state to the configured Terraform Cloud workspace.
Answer "yes" to copy the latest state snapshot to the configured
Terraform Cloud workspace.
Answer "no" to ignore the existing state and just activate the configured
Terraform Cloud workspace with its existing state, if any.
Should Terraform migrate your existing state?
`
const tfcInputBackendMigrateRemoteMultiToCloud = `
When migrating from the 'remote' backend to Terraform's native integration
with Terraform Cloud, Terraform will automatically create or use existing

View File

@ -0,0 +1,13 @@
# This is a simple configuration with Terraform Cloud mode minimally
# activated, but it's suitable only for testing things that we can exercise
# without actually accessing Terraform Cloud, such as checking of invalid
# command-line options to "terraform init".
terraform {
cloud {
organization = "PLACEHOLDER"
workspaces {
name = "PLACEHOLDER"
}
}
}