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:
parent
c28b57b4d6
commit
bac59d2480
|
@ -28,7 +28,7 @@ func Test_backend_apply_before_init(t *testing.T) {
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"apply"},
|
command: []string{"apply"},
|
||||||
expectedCmdOutput: `Terraform Cloud initialization required, please run "terraform init"`,
|
expectedCmdOutput: `Terraform Cloud initialization required: please run "terraform init"`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -62,7 +62,7 @@ func Test_backend_apply_before_init(t *testing.T) {
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"apply"},
|
command: []string{"apply"},
|
||||||
expectedCmdOutput: `Terraform Cloud initialization required, please run "terraform init"`,
|
expectedCmdOutput: `Terraform Cloud initialization required: please run "terraform init"`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,7 +60,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
||||||
userInput: []string{"yes", "yes"},
|
userInput: []string{"yes", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
|
@ -127,7 +127,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
||||||
userInput: []string{"yes", "yes"},
|
userInput: []string{"yes", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
|
@ -195,7 +195,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
||||||
userInput: []string{"yes", "yes"},
|
userInput: []string{"yes", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
|
@ -268,7 +268,6 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
|
||||||
if tfCmd.expectedCmdOutput != "" {
|
if tfCmd.expectedCmdOutput != "" {
|
||||||
got, err := exp.ExpectString(tfCmd.expectedCmdOutput)
|
got, err := exp.ExpectString(tfCmd.expectedCmdOutput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
t.Fatalf("error while waiting for output\nwant: %s\nerror: %s\noutput\n%s", tfCmd.expectedCmdOutput, err, got)
|
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{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
||||||
userInput: []string{"dev", "1", "app-*"},
|
userInput: []string{"dev", "1", "app-*"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
|
@ -470,7 +469,7 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
||||||
userInput: []string{"dev", "1", "app-*"},
|
userInput: []string{"dev", "1", "app-*"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
|
|
|
@ -42,8 +42,8 @@ func Test_migrate_remote_backend_name_to_tfc_name(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
|
command: []string{"init", "-ignore-remote-version"},
|
||||||
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
|
expectedCmdOutput: `Should Terraform migrate your existing state?`,
|
||||||
userInput: []string{"yes"},
|
userInput: []string{"yes"},
|
||||||
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
|
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{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
|
command: []string{"init", "-ignore-remote-version"},
|
||||||
expectedCmdOutput: `Terraform Cloud has been successfully initialized!`,
|
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{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
|
command: []string{"init", "-ignore-remote-version"},
|
||||||
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
|
expectedCmdOutput: `Should Terraform migrate your existing state?`,
|
||||||
userInput: []string{"yes"},
|
userInput: []string{"yes"},
|
||||||
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
|
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{
|
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.`,
|
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
||||||
userInput: []string{"cloud-workspace", "yes"},
|
userInput: []string{"cloud-workspace", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
`Do you want to copy existing state to Terraform Cloud?`,
|
`Should Terraform migrate your existing state?`,
|
||||||
`Terraform Cloud has been successfully initialized!`},
|
`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{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
|
command: []string{"init", "-ignore-remote-version"},
|
||||||
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
|
expectedCmdOutput: `Should Terraform migrate your existing state?`,
|
||||||
userInput: []string{"yes"},
|
userInput: []string{"yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
`Terraform Cloud has been successfully initialized!`},
|
`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{
|
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?`,
|
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
||||||
userInput: []string{"yes"},
|
userInput: []string{"yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
|
@ -822,11 +822,11 @@ func Test_migrate_remote_backend_prefix_to_tfc_tags_strategy_single_workspace(t
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
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.`,
|
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
||||||
userInput: []string{"cloud-workspace", "yes"},
|
userInput: []string{"cloud-workspace", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
`Do you want to copy existing state to Terraform Cloud?`,
|
`Should Terraform migrate your existing state?`,
|
||||||
`Terraform Cloud has been successfully initialized!`},
|
`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{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
|
command: []string{"init", "-ignore-remote-version"},
|
||||||
expectedCmdOutput: `Do you wish to proceed?`,
|
expectedCmdOutput: `Do you wish to proceed?`,
|
||||||
userInput: []string{"yes"},
|
userInput: []string{"yes"},
|
||||||
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
|
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
|
||||||
|
|
|
@ -47,8 +47,8 @@ func Test_migrate_single_to_tfc(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
|
expectedCmdOutput: `Should Terraform migrate your existing state?`,
|
||||||
userInput: []string{"yes"},
|
userInput: []string{"yes"},
|
||||||
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
|
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
|
||||||
},
|
},
|
||||||
|
@ -96,11 +96,11 @@ func Test_migrate_single_to_tfc(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
||||||
userInput: []string{"new-workspace", "yes"},
|
userInput: []string{"new-workspace", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
`Do you want to copy existing state to Terraform Cloud?`,
|
`Should Terraform migrate your existing state?`,
|
||||||
`Terraform Cloud has been successfully initialized!`},
|
`Terraform Cloud has been successfully initialized!`},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -36,7 +36,7 @@ func Test_migrate_tfc_to_other(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Migrating state from Terraform Cloud to another backend is not yet implemented.`,
|
expectedCmdOutput: `Migrating state from Terraform Cloud to another backend is not yet implemented.`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -69,8 +69,8 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
|
command: []string{"init", "-ignore-remote-version"},
|
||||||
expectedCmdOutput: `Do you want to copy existing state to Terraform Cloud?`,
|
expectedCmdOutput: `Should Terraform migrate your existing state?`,
|
||||||
userInput: []string{"yes"},
|
userInput: []string{"yes"},
|
||||||
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
|
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{
|
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.`,
|
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
||||||
userInput: []string{"new-workspace", "yes"},
|
userInput: []string{"new-workspace", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
`Do you want to copy existing state to Terraform Cloud?`,
|
`Should Terraform migrate your existing state?`,
|
||||||
`Terraform Cloud has been successfully initialized!`},
|
`Terraform Cloud has been successfully initialized!`},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -196,7 +196,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state"},
|
command: []string{"init"},
|
||||||
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
userInput: []string{"new-workspace", "yes"},
|
userInput: []string{"new-workspace", "yes"},
|
||||||
|
@ -367,11 +367,11 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
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?`,
|
expectedCmdOutput: `Do you want to copy only your current workspace?`,
|
||||||
userInput: []string{"yes", "yes"},
|
userInput: []string{"yes", "yes"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
`Do you want to copy existing state to Terraform Cloud?`,
|
`Should Terraform migrate your existing state?`,
|
||||||
`Terraform Cloud has been successfully initialized!`},
|
`Terraform Cloud has been successfully initialized!`},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -446,7 +446,7 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) {
|
||||||
},
|
},
|
||||||
commands: []tfCommand{
|
commands: []tfCommand{
|
||||||
{
|
{
|
||||||
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
|
command: []string{"init", "-ignore-remote-version"},
|
||||||
expectedCmdOutput: `Would you like to rename your workspaces?`,
|
expectedCmdOutput: `Would you like to rename your workspaces?`,
|
||||||
userInput: []string{"1", "new-*", "1"},
|
userInput: []string{"1", "new-*", "1"},
|
||||||
postInputOutput: []string{
|
postInputOutput: []string{
|
||||||
|
|
|
@ -213,7 +213,7 @@ func (c *InitCommand) Run(args []string) int {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case config.Module.CloudConfig != nil:
|
case config.Module.CloudConfig != nil:
|
||||||
be, backendOutput, backendDiags := c.initCloud(config.Module)
|
be, backendOutput, backendDiags := c.initCloud(config.Module, flagConfigExtra)
|
||||||
diags = diags.Append(backendDiags)
|
diags = diags.Append(backendDiags)
|
||||||
if backendDiags.HasErrors() {
|
if backendDiags.HasErrors() {
|
||||||
c.showDiagnostics(diags)
|
c.showDiagnostics(diags)
|
||||||
|
@ -366,9 +366,18 @@ func (c *InitCommand) getModules(path string, earlyRoot *tfconfig.Module, upgrad
|
||||||
return true, installAbort, diags
|
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..."))
|
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()
|
backendConfig := root.CloudConfig.ToBackendConfig()
|
||||||
|
|
||||||
opts := &BackendOpts{
|
opts := &BackendOpts{
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||||
|
@ -24,6 +25,7 @@ import (
|
||||||
"github.com/hashicorp/terraform/internal/getproviders"
|
"github.com/hashicorp/terraform/internal/getproviders"
|
||||||
"github.com/hashicorp/terraform/internal/providercache"
|
"github.com/hashicorp/terraform/internal/providercache"
|
||||||
"github.com/hashicorp/terraform/internal/states"
|
"github.com/hashicorp/terraform/internal/states"
|
||||||
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||||
"github.com/hashicorp/terraform/internal/states/statemgr"
|
"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
|
// make sure inputFalse stops execution on migrate
|
||||||
func TestInit_inputFalse(t *testing.T) {
|
func TestInit_inputFalse(t *testing.T) {
|
||||||
td := tempDir(t)
|
td := tempDir(t)
|
||||||
|
|
|
@ -603,7 +603,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.migrateState {
|
if s.Backend.Type != "cloud" && !m.migrateState {
|
||||||
diags = diags.Append(migrateOrReconfigDiag)
|
diags = diags.Append(migrateOrReconfigDiag)
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
@ -618,7 +618,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
|
||||||
initReason := "Initial configuration of Terraform Cloud"
|
initReason := "Initial configuration of Terraform Cloud"
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Terraform Cloud initialization required, please run \"terraform init\"",
|
"Terraform Cloud initialization required: please run \"terraform init\"",
|
||||||
fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason),
|
fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason),
|
||||||
))
|
))
|
||||||
} else {
|
} 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)
|
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 := ""
|
initReason := ""
|
||||||
switch {
|
switch cloudMode {
|
||||||
case c.Type == "cloud":
|
case cloud.ConfigMigrationIn:
|
||||||
initReason = fmt.Sprintf("Backend configuration changed from %q to Terraform Cloud", s.Backend.Type)
|
initReason = fmt.Sprintf("Changed from backend %q to Terraform Cloud", s.Backend.Type)
|
||||||
case s.Backend.Type != c.Type:
|
case cloud.ConfigMigrationOut:
|
||||||
initReason = fmt.Sprintf("Backend configuration changed from %q to %q", s.Backend.Type, c.Type)
|
initReason = fmt.Sprintf("Changed from Terraform Cloud to backend %q", s.Backend.Type)
|
||||||
|
case cloud.ConfigChangeInPlace:
|
||||||
|
initReason = "Terraform Cloud configuration has changed"
|
||||||
default:
|
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 !opts.Init {
|
||||||
if c.Type == "cloud" {
|
switch cloudMode {
|
||||||
|
case cloud.ConfigChangeInPlace:
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Terraform Cloud initialization required, please run \"terraform init\"",
|
"Terraform Cloud initialization required: please run \"terraform init\"",
|
||||||
fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason),
|
fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason),
|
||||||
))
|
))
|
||||||
} else {
|
case cloud.ConfigMigrationIn:
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
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),
|
fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.migrateState {
|
if !cloudMode.InvolvesCloud() && !m.migrateState {
|
||||||
if c.Type == "cloud" {
|
diags = diags.Append(migrateOrReconfigDiag)
|
||||||
diags = diags.Append(migrateOrReconfigDiagCloud)
|
|
||||||
} else {
|
|
||||||
diags = diags.Append(migrateOrReconfigDiag)
|
|
||||||
}
|
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -809,20 +821,29 @@ func (m *Meta) backendFromState() (backend.Backend, tfdiags.Diagnostics) {
|
||||||
|
|
||||||
// Unconfiguring a backend (moving from backend => local).
|
// 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) {
|
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()
|
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
|
// Get the backend type for output
|
||||||
backendType := s.Backend.Type
|
backendType := s.Backend.Type
|
||||||
|
|
||||||
if s.Backend.Type == "cloud" {
|
if cloudMode == cloud.ConfigMigrationOut {
|
||||||
m.Ui.Output(strings.TrimSpace(outputBackendMigrateLocalFromCloud))
|
m.Ui.Output("Migrating from Terraform Cloud to local state.")
|
||||||
} else {
|
} else {
|
||||||
m.Ui.Output(fmt.Sprintf(strings.TrimSpace(outputBackendMigrateLocal), s.Backend.Type))
|
m.Ui.Output(fmt.Sprintf(strings.TrimSpace(outputBackendMigrateLocal), s.Backend.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab a purely local backend to get the local state if it exists
|
// Grab a purely local backend to get the local state if it exists
|
||||||
localB, diags := m.Backend(&BackendOpts{ForceLocal: true, Init: true})
|
localB, moreDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true})
|
||||||
if diags.HasErrors() {
|
diags = diags.Append(moreDiags)
|
||||||
|
if moreDiags.HasErrors() {
|
||||||
return nil, diags
|
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.
|
// 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) {
|
func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.LocalState) (backend.Backend, tfdiags.Diagnostics) {
|
||||||
// Get the backend
|
var diags tfdiags.Diagnostics
|
||||||
b, configVal, diags := m.backendInitFromConfig(c)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return nil, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab a purely local backend to get the local state if it exists
|
// Grab a purely local backend to get the local state if it exists
|
||||||
localB, localBDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true})
|
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 {
|
if len(localStates) > 0 {
|
||||||
// Perform the migration
|
// Perform the migration
|
||||||
err = m.backendMigrateState(&backendMigrateOpts{
|
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.
|
// 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) {
|
func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool) (backend.Backend, tfdiags.Diagnostics) {
|
||||||
if output {
|
var diags tfdiags.Diagnostics
|
||||||
// Notify the user
|
|
||||||
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset]%s\n",
|
|
||||||
strings.TrimSpace(outputBackendReconfigure))))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the old state
|
// Get the old state
|
||||||
s := sMgr.State()
|
s := sMgr.State()
|
||||||
|
|
||||||
// Get the backend
|
cloudMode := cloud.DetectConfigChangeType(s.Backend, c, false)
|
||||||
b, configVal, diags := m.backendInitFromConfig(c)
|
diags = diags.Append(m.assertSupportedCloudInitOptions(cloudMode))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// no need to confuse the user if the backend types are the same
|
if output {
|
||||||
if s.Backend.Type != c.Type {
|
// Notify the user
|
||||||
output := fmt.Sprintf(outputBackendMigrateChange, s.Backend.Type, c.Type)
|
switch cloudMode {
|
||||||
if c.Type == "cloud" {
|
case cloud.ConfigChangeInPlace:
|
||||||
output = fmt.Sprintf(outputBackendMigrateChangeCloud, s.Backend.Type)
|
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
|
// Get the backend
|
||||||
oldB, oldBDiags := m.savedBackend(sMgr)
|
b, configVal, moreDiags := m.backendInitFromConfig(c)
|
||||||
diags = diags.Append(oldBDiags)
|
diags = diags.Append(moreDiags)
|
||||||
if oldBDiags.HasErrors() {
|
if moreDiags.HasErrors() {
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the migration
|
// If this is a migration into, out of, or irrelevant to Terraform Cloud
|
||||||
err := m.backendMigrateState(&backendMigrateOpts{
|
// mode then we will do state migration here. Otherwise, we just update
|
||||||
SourceType: s.Backend.Type,
|
// the working directory initialization directly, because Terraform Cloud
|
||||||
DestinationType: c.Type,
|
// doesn't have configurable state storage anyway -- we're only changing
|
||||||
Source: oldB,
|
// which workspaces are relevant to this configuration, not where their
|
||||||
Destination: b,
|
// state lives.
|
||||||
})
|
if cloudMode != cloud.ConfigChangeInPlace {
|
||||||
if err != nil {
|
// Grab the existing backend
|
||||||
diags = diags.Append(err)
|
oldB, oldBDiags := m.savedBackend(sMgr)
|
||||||
return nil, diags
|
diags = diags.Append(oldBDiags)
|
||||||
}
|
if oldBDiags.HasErrors() {
|
||||||
|
|
||||||
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
|
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())
|
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
|
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
|
// 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.
|
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 = `
|
const errBackendInitCloud = `
|
||||||
Reason: %s
|
Reason: %s.
|
||||||
|
|
||||||
Changes to the Terraform Cloud configuration block require reinitialization.
|
Changes to the Terraform Cloud configuration block require reinitialization, to discover any changes to the available workspaces.
|
||||||
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.
|
|
||||||
|
|
||||||
If the change reason above is incorrect, please verify your configuration
|
To re-initialize, run:
|
||||||
hasn't changed and try again. At this point, no changes to your existing
|
terraform init
|
||||||
configuration or state have been made.
|
|
||||||
|
Terraform has not yet made changes to your existing configuration or state.
|
||||||
`
|
`
|
||||||
|
|
||||||
const errBackendWriteSaved = `
|
const errBackendWriteSaved = `
|
||||||
|
@ -1402,16 +1512,9 @@ const outputBackendMigrateChange = `
|
||||||
Terraform detected that the backend type changed from %q to %q.
|
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 = `
|
const outputBackendMigrateLocal = `
|
||||||
Terraform has detected you're unconfiguring your previously set %q backend.
|
Terraform has detected you're unconfiguring your previously set %q backend.
|
||||||
`
|
`
|
||||||
const outputBackendMigrateLocalFromCloud = `
|
|
||||||
Terraform has detected you're unconfiguring Terraform Cloud.
|
|
||||||
`
|
|
||||||
|
|
||||||
const outputBackendReconfigure = `
|
const outputBackendReconfigure = `
|
||||||
[reset][bold]Backend configuration changed![reset]
|
[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"+
|
"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 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".`)
|
`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".`)
|
|
||||||
|
|
|
@ -569,10 +569,20 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
|
||||||
opts.sourceWorkspace = currentWorkspace
|
opts.sourceWorkspace = currentWorkspace
|
||||||
|
|
||||||
log.Printf("[INFO] backendMigrateTFC: single-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace)
|
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
|
// This will handle both situations where the new cloud backend
|
||||||
// configuration is using a workspace.name strategy or workspace.tags
|
// configuration is using a workspace.name strategy or workspace.tags
|
||||||
// strategy.
|
// 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)
|
return m.backendMigrateState_s_s(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -752,6 +762,23 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa
|
||||||
return nil
|
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 {
|
func (m *Meta) promptRemotePrefixToCloudTagsMigration(opts *backendMigrateOpts) error {
|
||||||
migrate := opts.force
|
migrate := opts.force
|
||||||
if !migrate {
|
if !migrate {
|
||||||
|
@ -937,6 +964,19 @@ strategy in your workspace configuration block instead.
|
||||||
Enter "yes" to proceed or "no" to cancel.
|
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 = `
|
const tfcInputBackendMigrateRemoteMultiToCloud = `
|
||||||
When migrating from the 'remote' backend to Terraform's native integration
|
When migrating from the 'remote' backend to Terraform's native integration
|
||||||
with Terraform Cloud, Terraform will automatically create or use existing
|
with Terraform Cloud, Terraform will automatically create or use existing
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue