diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 5f9ffbaa07..e1854bdaa7 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -715,13 +715,18 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } } +// determineInitReason is used in non-Init commands to interrupt the command early and prompt users to instead run an init command. +// That prompt needs to include the reason why init needs to be run, and it is determined here. +// +// Note: the calling code is responsible for determining that a change has occurred before invoking this +// method. This makes the default cases (config has changed) valid. func (m *Meta) determineInitReason(previousBackendType string, currentBackendType string, cloudMode cloud.ConfigChangeMode) tfdiags.Diagnostics { initReason := "" switch cloudMode { case cloud.ConfigMigrationIn: initReason = fmt.Sprintf("Changed from backend %q to HCP Terraform", previousBackendType) case cloud.ConfigMigrationOut: - initReason = fmt.Sprintf("Changed from HCP Terraform to backend %q", previousBackendType) + initReason = fmt.Sprintf("Changed from HCP Terraform to backend %q", currentBackendType) case cloud.ConfigChangeInPlace: initReason = "HCP Terraform configuration block has changed" default: diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index e0a08750cc..edfad4d00b 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -16,12 +16,15 @@ import ( "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/cloud" + "github.com/hashicorp/terraform/internal/command/workdir" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/copy" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/zclconf/go-cty/cty" backendInit "github.com/hashicorp/terraform/internal/backend/init" @@ -1956,6 +1959,105 @@ func TestBackendFromState(t *testing.T) { } } +func Test_determineInitReason(t *testing.T) { + + cases := map[string]struct { + cloudMode cloud.ConfigChangeMode + backendState workdir.BackendStateFile + backendConfig configs.Backend + + wantErr string + }{ + // All scenarios involving Cloud backend + "change in cloud config": { + cloudMode: cloud.ConfigChangeInPlace, + backendState: workdir.BackendStateFile{ + Backend: &workdir.BackendConfigState{ + Type: "cloud", + // Other fields unnecessary + }, + }, + backendConfig: configs.Backend{ + Type: "cloud", + // Other fields unnecessary + }, + wantErr: `HCP Terraform configuration block has changed`, + }, + "migrate backend to cloud": { + cloudMode: cloud.ConfigMigrationIn, + backendState: workdir.BackendStateFile{ + Backend: &workdir.BackendConfigState{ + Type: "foobar", + // Other fields unnecessary + }, + }, + backendConfig: configs.Backend{ + Type: "cloud", + // Other fields unnecessary + }, + wantErr: `Changed from backend "foobar" to HCP Terraform`, + }, + "migrate cloud to backend": { + cloudMode: cloud.ConfigMigrationOut, + backendState: workdir.BackendStateFile{ + Backend: &workdir.BackendConfigState{ + Type: "cloud", + // Other fields unnecessary + }, + }, + backendConfig: configs.Backend{ + Type: "foobar", + // Other fields unnecessary + }, + wantErr: `Changed from HCP Terraform to backend "foobar"`, + }, + + // Changes within the backend config block + "backend type changed": { + cloudMode: cloud.ConfigChangeIrrelevant, + backendState: workdir.BackendStateFile{ + Backend: &workdir.BackendConfigState{ + Type: "foobar1", + // Other fields unnecessary + }, + }, + backendConfig: configs.Backend{ + Type: "foobar2", + // Other fields unnecessary + }, + wantErr: `Backend type changed from "foobar1" to "foobar2`, + }, + "backend config changed": { + // Note that we don't need to include differing config to trigger this + // scenario, as we're hitting the default case. If the types match, then + // only the config is left to differ. + // See the comment above determineInitReason for more info. + cloudMode: cloud.ConfigChangeIrrelevant, + backendState: workdir.BackendStateFile{ + Backend: &workdir.BackendConfigState{ + Type: "foobar", + // Other fields unnecessary + }, + }, + backendConfig: configs.Backend{ + Type: "foobar", + // Other fields unnecessary + }, + wantErr: `Backend configuration block has changed`, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + m := Meta{} + diags := m.determineInitReason(tc.backendState.Backend.Type, tc.backendConfig.Type, tc.cloudMode) + if !strings.Contains(diags.Err().Error(), tc.wantErr) { + t.Fatalf("expected error diagnostic detail to include \"%s\" but it's missing: %s", tc.wantErr, diags.Err()) + } + }) + } +} + func testMetaBackend(t *testing.T, args []string) *Meta { var m Meta m.Ui = new(cli.MockUi)