From c5c1f31db368ef11c7f1f2363b24321479d50a64 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Fri, 13 Nov 2020 16:43:56 -0500 Subject: [PATCH] backend: Validate remote backend Terraform version When using the enhanced remote backend, a subset of all Terraform operations are supported. Of these, only plan and apply can be executed on the remote infrastructure (e.g. Terraform Cloud). Other operations run locally and use the remote backend for state storage. This causes problems when the local version of Terraform does not match the configured version from the remote workspace. If the two versions are incompatible, an `import` or `state mv` operation can cause the remote workspace to be unusable until a manual fix is applied. To prevent this from happening accidentally, this commit introduces a check that the local Terraform version and the configured remote workspace Terraform version are compatible. This check is skipped for commands which do not write state, and can also be disabled by the use of a new command-line flag, `-ignore-remote-version`. Terraform version compatibility is defined as: - For all releases before 0.14.0, local must exactly equal remote, as two different versions cannot share state; - 0.14.0 to 1.0.x are compatible, as we will not change the state version number until at least Terraform 1.1.0; - Versions after 1.1.0 must have the same major and minor versions, as we will not change the state version number in a patch release. If the two versions are incompatible, a diagnostic is displayed, advising that the error can be suppressed with `-ignore-remote-version`. When this flag is used, the diagnostic is still displayed, but as a warning instead of an error. Commands which will not write state can assert this fact by calling the helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the checks. Those which can write state should instead call the helper `meta.remoteBackendVersionCheck`, which will return diagnostics for display. In addition to these explicit paths for managing the version check, we have an implicit check in the remote backend's state manager initialization method. Both of the above helpers will disable this check. This fallback is in place to ensure that future code paths which access state cannot accidentally skip the remote version check. --- backend/remote/backend.go | 120 +++++++++ backend/remote/backend_apply_test.go | 132 +++++++++ backend/remote/backend_context.go | 13 +- backend/remote/backend_mock.go | 16 +- backend/remote/backend_test.go | 250 +++++++++++++++++- command/console.go | 3 + command/graph.go | 3 + command/import.go | 12 + command/init.go | 1 + command/meta.go | 15 ++ command/meta_backend.go | 28 ++ command/output.go | 3 + command/providers.go | 3 + command/providers_schema.go | 3 + command/show.go | 3 + command/state_list.go | 3 + command/state_meta.go | 8 + command/state_mv.go | 44 +-- command/state_pull.go | 3 + command/state_push.go | 17 +- command/state_replace_provider.go | 25 +- command/state_rm.go | 22 +- command/state_show.go | 3 + command/taint.go | 44 +-- command/untaint.go | 46 ++-- command/workspace_delete.go | 3 + command/workspace_list.go | 3 + command/workspace_new.go | 3 + command/workspace_select.go | 3 + website/docs/commands/import.html.md | 5 + website/docs/commands/state/mv.html.md | 5 + website/docs/commands/state/push.html.md | 7 + .../commands/state/replace-provider.html.md | 5 + website/docs/commands/state/rm.html.md | 5 + website/docs/commands/taint.html.markdown | 5 + website/docs/commands/untaint.html.markdown | 5 + 36 files changed, 779 insertions(+), 90 deletions(-) diff --git a/backend/remote/backend.go b/backend/remote/backend.go index 77ad26225..ce400d4d6 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -85,6 +85,12 @@ type Remote struct { // opLock locks operations opLock sync.Mutex + + // ignoreVersionConflict, if true, will disable the requirement that the + // local Terraform version matches the remote workspace's configured + // version. This will also cause VerifyWorkspaceTerraformVersion to return + // a warning diagnostic instead of an error. + ignoreVersionConflict bool } var _ backend.Backend = (*Remote)(nil) @@ -629,6 +635,17 @@ func (b *Remote) StateMgr(name string) (statemgr.Full, error) { } } + // This is a fallback error check. Most code paths should use other + // mechanisms to check the version, then set the ignoreVersionConflict + // field to true. This check is only in place to ensure that we don't + // accidentally upgrade state with a new code path, and the version check + // logic is coarser and simpler. + if !b.ignoreVersionConflict { + if workspace.TerraformVersion != tfversion.String() { + return nil, fmt.Errorf("Remote workspace Terraform version %q does not match local Terraform version %q", workspace.TerraformVersion, tfversion.String()) + } + } + client := &remoteClient{ client: b.client, organization: b.organization, @@ -676,9 +693,17 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend // Check if we need to use the local backend to run the operation. if b.forceLocal || !w.Operations { + if !w.Operations { + // Workspace is explicitly configured for local operations, so its + // configured Terraform version is meaningless + b.IgnoreVersionConflict() + } return b.local.Operation(ctx, op) } + // Running remotely so we don't care about version conflicts + b.IgnoreVersionConflict() + // Set the remote workspace name. op.Workspace = w.Name @@ -837,6 +862,101 @@ func (b *Remote) ReportResult(op *backend.RunningOperation, err error) { } } +// IgnoreVersionConflict allows commands to disable the fall-back check that +// the local Terraform version matches the remote workspace's configured +// Terraform version. This should be called by commands where this check is +// unnecessary, such as those performing remote operations, or read-only +// operations. It will also be called if the user uses a command-line flag to +// override this check. +func (b *Remote) IgnoreVersionConflict() { + b.ignoreVersionConflict = true +} + +// VerifyWorkspaceTerraformVersion compares the local Terraform version against +// the workspace's configured Terraform version. If they are equal, this means +// that there are no compatibility concerns, so it returns no diagnostics. +// +// If the versions differ, +func (b *Remote) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + workspace, err := b.getRemoteWorkspace(context.Background(), workspaceName) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Error looking up workspace", + fmt.Sprintf("Workspace read failed: %s", err), + )) + return diags + } + + remoteVersion, err := version.NewSemver(workspace.TerraformVersion) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Error looking up workspace", + fmt.Sprintf("Invalid Terraform version: %s", err), + )) + return diags + } + + v014 := version.Must(version.NewSemver("0.14.0")) + if tfversion.SemVer.LessThan(v014) || remoteVersion.LessThan(v014) { + // Versions of Terraform prior to 0.14.0 will refuse to load state files + // written by a newer version of Terraform, even if it is only a patch + // level difference. As a result we require an exact match. + if tfversion.SemVer.Equal(remoteVersion) { + return diags + } + } + if tfversion.SemVer.GreaterThanOrEqual(v014) && remoteVersion.GreaterThanOrEqual(v014) { + // Versions of Terraform after 0.14.0 should be compatible with each + // other. At the time this code was written, the only constraints we + // are aware of are: + // + // - 0.14.0 is guaranteed to be compatible with versions up to but not + // including 1.1.0 + v110 := version.Must(version.NewSemver("1.1.0")) + if tfversion.SemVer.LessThan(v110) && remoteVersion.LessThan(v110) { + return diags + } + // - Any new Terraform state version will require at least minor patch + // increment, so x.y.* will always be compatible with each other + tfvs := tfversion.SemVer.Segments64() + rwvs := remoteVersion.Segments64() + if len(tfvs) == 3 && len(rwvs) == 3 && tfvs[0] == rwvs[0] && tfvs[1] == rwvs[1] { + return diags + } + } + + // Even if ignoring version conflicts, it may still be useful to call this + // method and warn the user about a mismatch between the local and remote + // Terraform versions. + severity := tfdiags.Error + if b.ignoreVersionConflict { + severity = tfdiags.Warning + } + + suggestion := " If you're sure you want to upgrade the state, you can force Terraform to continue using the -ignore-remote-version flag. This may result in an unusable workspace." + if b.ignoreVersionConflict { + suggestion = "" + } + diags = diags.Append(tfdiags.Sourceless( + severity, + "Terraform version mismatch", + fmt.Sprintf( + "The local Terraform version (%s) does not match the configured version for remote workspace %s/%s (%s).%s", + tfversion.String(), + b.organization, + workspace.Name, + workspace.TerraformVersion, + suggestion, + ), + )) + + return diags +} + // Colorize returns the Colorize structure that can be used for colorizing // output. This is guaranteed to always return a non-nil value and so useful // as a helper to wrap any potentially colored strings. diff --git a/backend/remote/backend_apply_test.go b/backend/remote/backend_apply_test.go index 1e0107416..40262aab7 100644 --- a/backend/remote/backend_apply_test.go +++ b/backend/remote/backend_apply_test.go @@ -11,12 +11,14 @@ import ( "github.com/google/go-cmp/cmp" tfe "github.com/hashicorp/go-tfe" + version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" + tfversion "github.com/hashicorp/terraform/version" "github.com/mitchellh/cli" ) @@ -1277,3 +1279,133 @@ func TestRemote_applyWithRemoteError(t *testing.T) { t.Fatalf("expected apply error in output: %s", output) } } + +func TestRemote_applyVersionCheck(t *testing.T) { + testCases := map[string]struct { + localVersion string + remoteVersion string + forceLocal bool + hasOperations bool + wantErr string + }{ + "versions can be different for remote apply": { + localVersion: "0.14.0", + remoteVersion: "0.13.5", + hasOperations: true, + }, + "versions can be different for local apply": { + localVersion: "0.14.0", + remoteVersion: "0.13.5", + hasOperations: false, + }, + "error if force local, has remote operations, different versions": { + localVersion: "0.14.0", + remoteVersion: "0.13.5", + forceLocal: true, + hasOperations: true, + wantErr: `Remote workspace Terraform version "0.13.5" does not match local Terraform version "0.14.0"`, + }, + "no error if versions are identical": { + localVersion: "0.14.0", + remoteVersion: "0.14.0", + forceLocal: true, + hasOperations: true, + }, + "no error if force local but workspace has remote operations disabled": { + localVersion: "0.14.0", + remoteVersion: "0.13.5", + forceLocal: true, + hasOperations: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + // SETUP: Save original local version state and restore afterwards + p := tfversion.Prerelease + v := tfversion.Version + s := tfversion.SemVer + defer func() { + tfversion.Prerelease = p + tfversion.Version = v + tfversion.SemVer = s + }() + + // SETUP: Set local version for the test case + tfversion.Prerelease = "" + tfversion.Version = tc.localVersion + tfversion.SemVer = version.Must(version.NewSemver(tc.localVersion)) + + // SETUP: Set force local for the test case + b.forceLocal = tc.forceLocal + + ctx := context.Background() + + // SETUP: set the operations and Terraform Version fields on the + // remote workspace + _, err := b.client.Workspaces.Update( + ctx, + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + Operations: tfe.Bool(tc.hasOperations), + TerraformVersion: tfe.String(tc.remoteVersion), + }, + ) + if err != nil { + t.Fatalf("error creating named workspace: %v", err) + } + + // RUN: prepare the apply operation and run it + op, configCleanup := testOperationApply(t, "./testdata/apply") + defer configCleanup() + + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op.UIIn = input + op.UIOut = b.CLI + op.Workspace = backend.DefaultStateName + + run, err := b.Operation(ctx, op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + // RUN: wait for completion + <-run.Done() + + if tc.wantErr != "" { + // ASSERT: if the test case wants an error, check for failure + // and the error message + if run.Result != backend.OperationFailure { + t.Fatalf("expected run to fail, but result was %#v", run.Result) + } + errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() + if !strings.Contains(errOutput, tc.wantErr) { + t.Fatalf("missing error %q\noutput: %s", tc.wantErr, errOutput) + } + } else { + // ASSERT: otherwise, check for success and appropriate output + // based on whether the run should be local or remote + if run.Result != backend.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + output := b.CLI.(*cli.MockUi).OutputWriter.String() + hasRemote := strings.Contains(output, "Running apply in the remote backend") + if !tc.forceLocal && tc.hasOperations && !hasRemote { + t.Fatalf("missing remote backend header in output: %s", output) + } else if (tc.forceLocal || !tc.hasOperations) && hasRemote { + t.Fatalf("unexpected remote backend header in output: %s", output) + } + if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("expected apply summary in output: %s", output) + } + } + }) + } +} diff --git a/backend/remote/backend_context.go b/backend/remote/backend_context.go index 13202f547..577c92d92 100644 --- a/backend/remote/backend_context.go +++ b/backend/remote/backend_context.go @@ -156,11 +156,20 @@ func (b *Remote) getRemoteWorkspaceName(localWorkspaceName string) string { } } -func (b *Remote) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName string) (string, error) { +func (b *Remote) getRemoteWorkspace(ctx context.Context, localWorkspaceName string) (*tfe.Workspace, error) { remoteWorkspaceName := b.getRemoteWorkspaceName(localWorkspaceName) - log.Printf("[TRACE] backend/remote: looking up workspace id for %s/%s", b.organization, remoteWorkspaceName) + log.Printf("[TRACE] backend/remote: looking up workspace for %s/%s", b.organization, remoteWorkspaceName) remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName) + if err != nil { + return nil, err + } + + return remoteWorkspace, nil +} + +func (b *Remote) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName string) (string, error) { + remoteWorkspace, err := b.getRemoteWorkspace(ctx, localWorkspaceName) if err != nil { return "", err } diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go index 6f12115ef..2d0652921 100644 --- a/backend/remote/backend_mock.go +++ b/backend/remote/backend_mock.go @@ -17,6 +17,7 @@ import ( tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/terraform" + tfversion "github.com/hashicorp/terraform/version" "github.com/mitchellh/copystructure" ) @@ -1124,10 +1125,15 @@ func (m *mockWorkspaces) List(ctx context.Context, organization string, options } func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { + if strings.HasSuffix(*options.Name, "no-operations") { + options.Operations = tfe.Bool(false) + } else if options.Operations == nil { + options.Operations = tfe.Bool(true) + } w := &tfe.Workspace{ ID: generateID("ws-"), Name: *options.Name, - Operations: !strings.HasSuffix(*options.Name, "no-operations"), + Operations: *options.Operations, Permissions: &tfe.WorkspacePermissions{ CanQueueApply: true, CanQueueRun: true, @@ -1139,6 +1145,11 @@ func (m *mockWorkspaces) Create(ctx context.Context, organization string, option if options.VCSRepo != nil { w.VCSRepo = &tfe.VCSRepo{} } + if options.TerraformVersion != nil { + w.TerraformVersion = *options.TerraformVersion + } else { + w.TerraformVersion = tfversion.String() + } m.workspaceIDs[w.ID] = w m.workspaceNames[w.Name] = w return w, nil @@ -1171,6 +1182,9 @@ func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace str return nil, tfe.ErrResourceNotFound } + if options.Operations != nil { + w.Operations = *options.Operations + } if options.Name != nil { w.Name = *options.Name } diff --git a/backend/remote/backend_test.go b/backend/remote/backend_test.go index 8c1e9a80f..46dc5c64a 100644 --- a/backend/remote/backend_test.go +++ b/backend/remote/backend_test.go @@ -1,13 +1,18 @@ package remote import ( + "context" + "fmt" "reflect" "strings" "testing" + tfe "github.com/hashicorp/go-tfe" + version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/version" + "github.com/hashicorp/terraform/tfdiags" + tfversion "github.com/hashicorp/terraform/version" "github.com/zclconf/go-cty/cty" backendLocal "github.com/hashicorp/terraform/backend/local" @@ -196,11 +201,11 @@ func TestRemote_versionConstraints(t *testing.T) { } // Save and restore the actual version. - p := version.Prerelease - v := version.Version + p := tfversion.Prerelease + v := tfversion.Version defer func() { - version.Prerelease = p - version.Version = v + tfversion.Prerelease = p + tfversion.Version = v }() for name, tc := range cases { @@ -208,8 +213,8 @@ func TestRemote_versionConstraints(t *testing.T) { b := New(testDisco(s)) // Set the version for this test. - version.Prerelease = tc.prerelease - version.Version = tc.version + tfversion.Prerelease = tc.prerelease + tfversion.Version = tc.version // Validate _, valDiags := b.PrepareConfig(tc.config) @@ -428,17 +433,17 @@ func TestRemote_checkConstraints(t *testing.T) { } // Save and restore the actual version. - p := version.Prerelease - v := version.Version + p := tfversion.Prerelease + v := tfversion.Version defer func() { - version.Prerelease = p - version.Version = v + tfversion.Prerelease = p + tfversion.Version = v }() for name, tc := range cases { // Set the version for this test. - version.Prerelease = tc.prerelease - version.Version = tc.version + tfversion.Prerelease = tc.prerelease + tfversion.Version = tc.version // Check the constraints. diags := b.checkConstraints(tc.constraints) @@ -448,3 +453,222 @@ func TestRemote_checkConstraints(t *testing.T) { } } } + +func TestRemote_StateMgr_versionCheck(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + // Some fixed versions for testing with. This logic is a simple string + // comparison, so we don't need many test cases. + v0135 := version.Must(version.NewSemver("0.13.5")) + v0140 := version.Must(version.NewSemver("0.14.0")) + + // Save original local version state and restore afterwards + p := tfversion.Prerelease + v := tfversion.Version + s := tfversion.SemVer + defer func() { + tfversion.Prerelease = p + tfversion.Version = v + tfversion.SemVer = s + }() + + // For this test, the local Terraform version is set to 0.14.0 + tfversion.Prerelease = "" + tfversion.Version = v0140.String() + tfversion.SemVer = v0140 + + // Update the mock remote workspace Terraform version to match the local + // Terraform version + if _, err := b.client.Workspaces.Update( + context.Background(), + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + TerraformVersion: tfe.String(v0140.String()), + }, + ); err != nil { + t.Fatalf("error: %v", err) + } + + // This should succeed + if _, err := b.StateMgr(backend.DefaultStateName); err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // Now change the remote workspace to a different Terraform version + if _, err := b.client.Workspaces.Update( + context.Background(), + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + TerraformVersion: tfe.String(v0135.String()), + }, + ); err != nil { + t.Fatalf("error: %v", err) + } + + // This should fail + want := `Remote workspace Terraform version "0.13.5" does not match local Terraform version "0.14.0"` + if _, err := b.StateMgr(backend.DefaultStateName); err.Error() != want { + t.Fatalf("wrong error\n got: %v\nwant: %v", err.Error(), want) + } +} + +func TestRemote_VerifyWorkspaceTerraformVersion(t *testing.T) { + testCases := []struct { + local string + remote string + wantErr bool + }{ + {"0.13.5", "0.13.5", false}, + {"0.14.0", "0.13.5", true}, + {"0.14.0", "0.14.1", false}, + {"0.14.0", "1.0.99", false}, + {"0.14.0", "1.1.0", true}, + {"1.2.0", "1.2.99", false}, + {"1.2.0", "1.3.0", true}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + local := version.Must(version.NewSemver(tc.local)) + remote := version.Must(version.NewSemver(tc.remote)) + + // Save original local version state and restore afterwards + p := tfversion.Prerelease + v := tfversion.Version + s := tfversion.SemVer + defer func() { + tfversion.Prerelease = p + tfversion.Version = v + tfversion.SemVer = s + }() + + // Override local version as specified + tfversion.Prerelease = "" + tfversion.Version = local.String() + tfversion.SemVer = local + + // Update the mock remote workspace Terraform version to the + // specified remote version + if _, err := b.client.Workspaces.Update( + context.Background(), + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + TerraformVersion: tfe.String(remote.String()), + }, + ); err != nil { + t.Fatalf("error: %v", err) + } + + diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) + if tc.wantErr { + if len(diags) != 1 { + t.Fatal("expected diag, but none returned") + } + if got := diags.Err().Error(); !strings.Contains(got, "Terraform version mismatch") { + t.Fatalf("unexpected error: %s", got) + } + } else { + if len(diags) != 0 { + t.Fatalf("unexpected diags: %s", diags.Err()) + } + } + }) + } +} + +func TestRemote_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + // Attempting to check the version against a workspace which doesn't exist + // should fail + diags := b.VerifyWorkspaceTerraformVersion("invalid-workspace") + if len(diags) != 1 { + t.Fatal("expected diag, but none returned") + } + if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Workspace read failed") { + t.Fatalf("unexpected error: %s", got) + } + + // Update the mock remote workspace Terraform version to an invalid version + if _, err := b.client.Workspaces.Update( + context.Background(), + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + TerraformVersion: tfe.String("1.0.cheetarah"), + }, + ); err != nil { + t.Fatalf("error: %v", err) + } + diags = b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) + + if len(diags) != 1 { + t.Fatal("expected diag, but none returned") + } + if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Invalid Terraform version") { + t.Fatalf("unexpected error: %s", got) + } +} + +func TestRemote_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + // If the ignore flag is set, the behaviour changes + b.IgnoreVersionConflict() + + // Different local & remote versions to cause an error + local := version.Must(version.NewSemver("0.14.0")) + remote := version.Must(version.NewSemver("0.13.5")) + + // Save original local version state and restore afterwards + p := tfversion.Prerelease + v := tfversion.Version + s := tfversion.SemVer + defer func() { + tfversion.Prerelease = p + tfversion.Version = v + tfversion.SemVer = s + }() + + // Override local version as specified + tfversion.Prerelease = "" + tfversion.Version = local.String() + tfversion.SemVer = local + + // Update the mock remote workspace Terraform version to the + // specified remote version + if _, err := b.client.Workspaces.Update( + context.Background(), + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + TerraformVersion: tfe.String(remote.String()), + }, + ); err != nil { + t.Fatalf("error: %v", err) + } + + diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) + if len(diags) != 1 { + t.Fatal("expected diag, but none returned") + } + + if got, want := diags[0].Severity(), tfdiags.Warning; got != want { + t.Errorf("wrong severity: got %#v, want %#v", got, want) + } + if got, want := diags[0].Description().Summary, "Terraform version mismatch"; got != want { + t.Errorf("wrong summary: got %s, want %s", got, want) + } + wantDetail := "The local Terraform version (0.14.0) does not match the configured version for remote workspace hashicorp/prod (0.13.5)." + if got := diags[0].Description().Detail; got != wantDetail { + t.Errorf("wrong summary: got %s, want %s", got, wantDetail) + } +} diff --git a/command/console.go b/command/console.go index c6f872b90..c32007323 100644 --- a/command/console.go +++ b/command/console.go @@ -69,6 +69,9 @@ func (c *ConsoleCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // Build the operation opReq := c.Operation(b) opReq.ConfigDir = configPath diff --git a/command/graph.go b/command/graph.go index fba33c6f8..796478ded 100644 --- a/command/graph.go +++ b/command/graph.go @@ -87,6 +87,9 @@ func (c *GraphCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // Build the operation opReq := c.Operation(b) opReq.ConfigDir = configPath diff --git a/command/import.go b/command/import.go index cf22946c5..5623141f4 100644 --- a/command/import.go +++ b/command/import.go @@ -35,6 +35,7 @@ func (c *ImportCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.extendedFlagSet("import") + cmdFlags.BoolVar(&c.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions differ") cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") @@ -198,6 +199,14 @@ func (c *ImportCommand) Run(args []string) int { } } + // Check remote Terraform version is compatible + remoteVersionDiags := c.remoteBackendVersionCheck(b, opReq.Workspace) + diags = diags.Append(remoteVersionDiags) + c.showDiagnostics(diags) + if diags.HasErrors() { + return 1 + } + // Get the context ctx, state, ctxDiags := local.Context(opReq) diags = diags.Append(ctxDiags) @@ -321,6 +330,9 @@ Options: a file. If "terraform.tfvars" or any ".auto.tfvars" files are present, they will be automatically loaded. + -ignore-remote-version Continue even if remote and local Terraform versions + differ. This may result in an unusable workspace, and + should be used with extreme caution. ` return strings.TrimSpace(helpText) diff --git a/command/init.go b/command/init.go index e90449ca5..3dff0580e 100644 --- a/command/init.go +++ b/command/init.go @@ -264,6 +264,7 @@ func (c *InitCommand) Run(args []string) int { // on a previous run) we'll use the current state as a potential source // of provider dependencies. if back != nil { + c.ignoreRemoteBackendVersionConflict(back) workspace, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) diff --git a/command/meta.go b/command/meta.go index d4f008786..41d80bdf2 100644 --- a/command/meta.go +++ b/command/meta.go @@ -205,6 +205,10 @@ type Meta struct { // Used with the import command to allow import of state when no matching config exists. allowMissingConfig bool + + // Used with commands which write state to allow users to write remote + // state even if the remote and local Terraform versions don't match. + ignoreRemoteVersion bool } type testingOverrides struct { @@ -466,6 +470,17 @@ func (m *Meta) defaultFlagSet(n string) *flag.FlagSet { return f } +// ignoreRemoteVersionFlagSet add the ignore-remote version flag to suppress +// the error when the configured Terraform version on the remote workspace +// does not match the local Terraform version. +func (m *Meta) ignoreRemoteVersionFlagSet(n string) *flag.FlagSet { + f := m.defaultFlagSet(n) + + f.BoolVar(&m.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions differ") + + return f +} + // extendedFlagSet adds custom flags that are mostly used by commands // that are used to run an operation like plan or apply. func (m *Meta) extendedFlagSet(n string) *flag.FlagSet { diff --git a/command/meta_backend.go b/command/meta_backend.go index 61c645aaf..7d3fbb502 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/terraform/backend" + remoteBackend "github.com/hashicorp/terraform/backend/remote" "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/plans" @@ -1091,6 +1092,33 @@ func (m *Meta) backendInitRequired(reason string) { "[reset]"+strings.TrimSpace(errBackendInit)+"\n", reason))) } +// Helper method to ignore remote backend version conflicts. Only call this +// for commands which cannot accidentally upgrade remote state files. +func (m *Meta) ignoreRemoteBackendVersionConflict(b backend.Backend) { + if rb, ok := b.(*remoteBackend.Remote); ok { + rb.IgnoreVersionConflict() + } +} + +// Helper method to check the local Terraform version against the configured +// version in the remote workspace, returning diagnostics if they conflict. +func (m *Meta) remoteBackendVersionCheck(b backend.Backend, workspace string) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if rb, ok := b.(*remoteBackend.Remote); ok { + // Allow user override based on command-line flag + if m.ignoreRemoteVersion { + rb.IgnoreVersionConflict() + } + // If the override is set, this check will return a warning instead of + // an error + versionDiags := rb.VerifyWorkspaceTerraformVersion(workspace) + diags = diags.Append(versionDiags) + } + + return diags +} + //------------------------------------------------------------------- // Output constants and initialization code //------------------------------------------------------------------- diff --git a/command/output.go b/command/output.go index 3f5cc2bea..ec27a70ae 100644 --- a/command/output.go +++ b/command/output.go @@ -61,6 +61,9 @@ func (c *OutputCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + env, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) diff --git a/command/providers.go b/command/providers.go index d2042d122..da97ae158 100644 --- a/command/providers.go +++ b/command/providers.go @@ -82,6 +82,9 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // Get the state env, err := c.Workspace() if err != nil { diff --git a/command/providers_schema.go b/command/providers_schema.go index 00634cf2f..3584be9f4 100644 --- a/command/providers_schema.go +++ b/command/providers_schema.go @@ -67,6 +67,9 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // we expect that the config dir is the cwd cwd, err := os.Getwd() if err != nil { diff --git a/command/show.go b/command/show.go index 56d0e34b9..852999542 100644 --- a/command/show.go +++ b/command/show.go @@ -68,6 +68,9 @@ func (c *ShowCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // the show command expects the config dir to always be the cwd cwd, err := os.Getwd() if err != nil { diff --git a/command/state_list.go b/command/state_list.go index 8c8a23906..15297c1ec 100644 --- a/command/state_list.go +++ b/command/state_list.go @@ -40,6 +40,9 @@ func (c *StateListCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // Get the state env, err := c.Workspace() if err != nil { diff --git a/command/state_meta.go b/command/state_meta.go index bc70649ac..c2c98005f 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -41,6 +41,14 @@ func (c *StateMeta) State() (statemgr.Full, error) { if err != nil { return nil, err } + + // Check remote Terraform version is compatible + remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + c.showDiagnostics(remoteVersionDiags) + if remoteVersionDiags.HasErrors() { + return nil, fmt.Errorf("Error checking remote Terraform version") + } + // Get the state s, err := b.StateMgr(workspace) if err != nil { diff --git a/command/state_mv.go b/command/state_mv.go index dcaaf23a6..13ca64be7 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -23,7 +23,7 @@ func (c *StateMvCommand) Run(args []string) int { var backupPathOut, statePathOut string var dryRun bool - cmdFlags := c.Meta.defaultFlagSet("state mv") + cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state mv") cmdFlags.BoolVar(&dryRun, "dry-run", false, "dry run") cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup") cmdFlags.StringVar(&backupPathOut, "backup-out", "-", "backup") @@ -465,31 +465,35 @@ Usage: terraform state mv [options] SOURCE DESTINATION Options: - -dry-run If set, prints out what would've been moved but doesn't - actually move anything. + -dry-run If set, prints out what would've been moved but doesn't + actually move anything. - -backup=PATH Path where Terraform should write the backup for the original - state. This can't be disabled. If not set, Terraform - will write it to the same path as the statefile with - a ".backup" extension. + -backup=PATH Path where Terraform should write the backup for the + original state. This can't be disabled. If not set, + Terraform will write it to the same path as the + statefile with a ".backup" extension. - -backup-out=PATH Path where Terraform should write the backup for the destination - state. This can't be disabled. If not set, Terraform - will write it to the same path as the destination state - file with a backup extension. This only needs - to be specified if -state-out is set to a different path - than -state. + -backup-out=PATH Path where Terraform should write the backup for the + destination state. This can't be disabled. If not + set, Terraform will write it to the same path as the + destination state file with a backup extension. This + only needs to be specified if -state-out is set to a + different path than -state. - -lock=true Lock the state files when locking is supported. + -lock=true Lock the state files when locking is supported. - -lock-timeout=0s Duration to retry a state lock. + -lock-timeout=0s Duration to retry a state lock. - -state=PATH Path to the source state file. Defaults to the configured - backend, or "terraform.tfstate" + -state=PATH Path to the source state file. Defaults to the + configured backend, or "terraform.tfstate" - -state-out=PATH Path to the destination state file to write to. If this - isn't specified, the source state file will be used. This - can be a new or existing path. + -state-out=PATH Path to the destination state file to write to. If + this isn't specified, the source state file will be + used. This can be a new or existing path. + + -ignore-remote-version Continue even if remote and local Terraform versions + differ. This may result in an unusable workspace, and + should be used with extreme caution. ` return strings.TrimSpace(helpText) diff --git a/command/state_pull.go b/command/state_pull.go index 6ab6328a2..8b0e297fa 100644 --- a/command/state_pull.go +++ b/command/state_pull.go @@ -31,6 +31,9 @@ func (c *StatePullCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // Get the state manager for the current workspace env, err := c.Workspace() if err != nil { diff --git a/command/state_push.go b/command/state_push.go index facbf786a..66bfbdaf7 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -22,7 +22,7 @@ type StatePushCommand struct { func (c *StatePushCommand) Run(args []string) int { args = c.Meta.process(args) var flagForce bool - cmdFlags := c.Meta.defaultFlagSet("state push") + cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state push") cmdFlags.BoolVar(&flagForce, "force", false, "") cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") @@ -71,13 +71,22 @@ func (c *StatePushCommand) Run(args []string) int { return 1 } - // Get the state manager for the currently-selected workspace - env, err := c.Workspace() + // Determine the workspace name + workspace, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) return 1 } - stateMgr, err := b.StateMgr(env) + + // Check remote Terraform version is compatible + remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + c.showDiagnostics(remoteVersionDiags) + if remoteVersionDiags.HasErrors() { + return 1 + } + + // Get the state manager for the currently-selected workspace + stateMgr, err := b.StateMgr(workspace) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) return 1 diff --git a/command/state_replace_provider.go b/command/state_replace_provider.go index 3d5acf678..72a07a1d2 100644 --- a/command/state_replace_provider.go +++ b/command/state_replace_provider.go @@ -25,7 +25,7 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { args = c.Meta.process(args) var autoApprove bool - cmdFlags := c.Meta.defaultFlagSet("state replace-provider") + cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state replace-provider") cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of replacements") cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup") cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock states") @@ -172,19 +172,24 @@ Usage: terraform state replace-provider [options] FROM_PROVIDER_FQN TO_PROVIDER_ Options: - -auto-approve Skip interactive approval. + -auto-approve Skip interactive approval. - -backup=PATH Path where Terraform should write the backup for the - state file. This can't be disabled. If not set, Terraform - will write it to the same path as the state file with - a ".backup" extension. + -backup=PATH Path where Terraform should write the backup for the + state file. This can't be disabled. If not set, + Terraform will write it to the same path as the state + file with a ".backup" extension. - -lock=true Lock the state files when locking is supported. + -lock=true Lock the state files when locking is supported. - -lock-timeout=0s Duration to retry a state lock. + -lock-timeout=0s Duration to retry a state lock. + + -state=PATH Path to the state file to update. Defaults to the + configured backend, or "terraform.tfstate" + + -ignore-remote-version Continue even if remote and local Terraform versions + differ. This may result in an unusable workspace, and + should be used with extreme caution. - -state=PATH Path to the state file to update. Defaults to the configured - backend, or "terraform.tfstate" ` return strings.TrimSpace(helpText) } diff --git a/command/state_rm.go b/command/state_rm.go index 1254de417..3ec9ec9b0 100644 --- a/command/state_rm.go +++ b/command/state_rm.go @@ -19,7 +19,7 @@ type StateRmCommand struct { func (c *StateRmCommand) Run(args []string) int { args = c.Meta.process(args) var dryRun bool - cmdFlags := c.Meta.defaultFlagSet("state rm") + cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state rm") cmdFlags.BoolVar(&dryRun, "dry-run", false, "dry run") cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup") cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") @@ -146,18 +146,22 @@ Usage: terraform state rm [options] ADDRESS... Options: - -dry-run If set, prints out what would've been removed but - doesn't actually remove anything. + -dry-run If set, prints out what would've been removed but + doesn't actually remove anything. - -backup=PATH Path where Terraform should write the backup - state. + -backup=PATH Path where Terraform should write the backup + state. - -lock=true Lock the state file when locking is supported. + -lock=true Lock the state file when locking is supported. - -lock-timeout=0s Duration to retry a state lock. + -lock-timeout=0s Duration to retry a state lock. - -state=PATH Path to the state file to update. Defaults to the current - workspace state. + -state=PATH Path to the state file to update. Defaults to the + current workspace state. + + -ignore-remote-version Continue even if remote and local Terraform versions + differ. This may result in an unusable workspace, and + should be used with extreme caution. ` return strings.TrimSpace(helpText) diff --git a/command/state_show.go b/command/state_show.go index dd6e292bc..2c40cd6cb 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -53,6 +53,9 @@ func (c *StateShowCommand) Run(args []string) int { return 1 } + // This is a read-only command + c.ignoreRemoteBackendVersionConflict(b) + // Check if the address can be parsed addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) if addrDiags.HasErrors() { diff --git a/command/taint.go b/command/taint.go index ad4f10f06..70d3710df 100644 --- a/command/taint.go +++ b/command/taint.go @@ -23,7 +23,7 @@ func (c *TaintCommand) Run(args []string) int { args = c.Meta.process(args) var module string var allowMissing bool - cmdFlags := c.Meta.defaultFlagSet("taint") + cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("taint") cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "module") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") @@ -100,13 +100,23 @@ func (c *TaintCommand) Run(args []string) int { return 1 } - // Get the state - env, err := c.Workspace() + // Determine the workspace name + workspace, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) return 1 } - stateMgr, err := b.StateMgr(env) + + // Check remote Terraform version is compatible + remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + diags = diags.Append(remoteVersionDiags) + c.showDiagnostics(diags) + if diags.HasErrors() { + return 1 + } + + // Get the state + stateMgr, err := b.StateMgr(workspace) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -224,22 +234,26 @@ Usage: terraform taint [options]
Options: - -allow-missing If specified, the command will succeed (exit code 0) - even if the resource is missing. + -allow-missing If specified, the command will succeed (exit code 0) + even if the resource is missing. - -backup=path Path to backup the existing state file before - modifying. Defaults to the "-state-out" path with - ".backup" extension. Set to "-" to disable backup. + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state-out" path with + ".backup" extension. Set to "-" to disable backup. - -lock=true Lock the state file when locking is supported. + -lock=true Lock the state file when locking is supported. - -lock-timeout=0s Duration to retry a state lock. + -lock-timeout=0s Duration to retry a state lock. - -state=path Path to read and save state (unless state-out - is specified). Defaults to "terraform.tfstate". + -state=path Path to read and save state (unless state-out + is specified). Defaults to "terraform.tfstate". - -state-out=path Path to write updated state file. By default, the - "-state" path will be used. + -state-out=path Path to write updated state file. By default, the + "-state" path will be used. + + -ignore-remote-version Continue even if remote and local Terraform versions + differ. This may result in an unusable workspace, and + should be used with extreme caution. ` return strings.TrimSpace(helpText) diff --git a/command/untaint.go b/command/untaint.go index 96493bcb0..13d2bd717 100644 --- a/command/untaint.go +++ b/command/untaint.go @@ -21,7 +21,7 @@ func (c *UntaintCommand) Run(args []string) int { args = c.Meta.process(args) var module string var allowMissing bool - cmdFlags := c.Meta.defaultFlagSet("untaint") + cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("untaint") cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "module") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") @@ -65,12 +65,22 @@ func (c *UntaintCommand) Run(args []string) int { return 1 } - // Get the state + // Determine the workspace name workspace, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) return 1 } + + // Check remote Terraform version is compatible + remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + diags = diags.Append(remoteVersionDiags) + c.showDiagnostics(diags) + if diags.HasErrors() { + return 1 + } + + // Get the state stateMgr, err := b.StateMgr(workspace) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) @@ -189,26 +199,30 @@ Usage: terraform untaint [options] name Options: - -allow-missing If specified, the command will succeed (exit code 0) - even if the resource is missing. + -allow-missing If specified, the command will succeed (exit code 0) + even if the resource is missing. - -backup=path Path to backup the existing state file before - modifying. Defaults to the "-state-out" path with - ".backup" extension. Set to "-" to disable backup. + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state-out" path with + ".backup" extension. Set to "-" to disable backup. - -lock=true Lock the state file when locking is supported. + -lock=true Lock the state file when locking is supported. - -lock-timeout=0s Duration to retry a state lock. + -lock-timeout=0s Duration to retry a state lock. - -module=path The module path where the resource lives. By - default this will be root. Child modules can be specified - by names. Ex. "consul" or "consul.vpc" (nested modules). + -module=path The module path where the resource lives. By default + this will be root. Child modules can be specified by + names. Ex. "consul" or "consul.vpc" (nested modules). - -state=path Path to read and save state (unless state-out - is specified). Defaults to "terraform.tfstate". + -state=path Path to read and save state (unless state-out + is specified). Defaults to "terraform.tfstate". - -state-out=path Path to write updated state file. By default, the - "-state" path will be used. + -state-out=path Path to write updated state file. By default, the + "-state" path will be used. + + -ignore-remote-version Continue even if remote and local Terraform versions + differ. This may result in an unusable workspace, and + should be used with extreme caution. ` return strings.TrimSpace(helpText) diff --git a/command/workspace_delete.go b/command/workspace_delete.go index ebb7c5eed..522b38e05 100644 --- a/command/workspace_delete.go +++ b/command/workspace_delete.go @@ -65,6 +65,9 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { return 1 } + // This command will not write state + c.ignoreRemoteBackendVersionConflict(b) + workspaces, err := b.Workspaces() if err != nil { c.Ui.Error(err.Error()) diff --git a/command/workspace_list.go b/command/workspace_list.go index 03fdf4a50..7a5e4f1fd 100644 --- a/command/workspace_list.go +++ b/command/workspace_list.go @@ -51,6 +51,9 @@ func (c *WorkspaceListCommand) Run(args []string) int { return 1 } + // This command will not write state + c.ignoreRemoteBackendVersionConflict(b) + states, err := b.Workspaces() if err != nil { c.Ui.Error(err.Error()) diff --git a/command/workspace_new.go b/command/workspace_new.go index 549beebae..40a774559 100644 --- a/command/workspace_new.go +++ b/command/workspace_new.go @@ -81,6 +81,9 @@ func (c *WorkspaceNewCommand) Run(args []string) int { return 1 } + // This command will not write state + c.ignoreRemoteBackendVersionConflict(b) + workspaces, err := b.Workspaces() if err != nil { c.Ui.Error(fmt.Sprintf("Failed to get configured named states: %s", err)) diff --git a/command/workspace_select.go b/command/workspace_select.go index 9667ff9dc..b46834c1d 100644 --- a/command/workspace_select.go +++ b/command/workspace_select.go @@ -67,6 +67,9 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { return 1 } + // This command will not write state + c.ignoreRemoteBackendVersionConflict(b) + name := args[0] if !validWorkspaceName(name) { c.Ui.Error(fmt.Sprintf(envInvalidName, name)) diff --git a/website/docs/commands/import.html.md b/website/docs/commands/import.html.md index c29fa90e2..7e7118473 100644 --- a/website/docs/commands/import.html.md +++ b/website/docs/commands/import.html.md @@ -87,6 +87,11 @@ in the configuration for the target resource, and that is the best behavior in m the working directory. This flag can be used multiple times. This is only useful with the `-config` flag. +* `-ignore-remote-version` - When using the enhanced remote backend with + Terraform Cloud, continue even if remote and local Terraform versions differ. + This may result in an unusable Terraform Cloud workspace, and should be used + with extreme caution. + ## Provider Configuration Terraform will attempt to load configuration files that configure the diff --git a/website/docs/commands/state/mv.html.md b/website/docs/commands/state/mv.html.md index ce5f7449c..683bbcbc6 100644 --- a/website/docs/commands/state/mv.html.md +++ b/website/docs/commands/state/mv.html.md @@ -57,6 +57,11 @@ The command-line flags are all optional. The list of available flags are: isn't specified the source state file will be used. This can be a new or existing path. +* `-ignore-remote-version` - When using the enhanced remote backend with + Terraform Cloud, continue even if remote and local Terraform versions differ. + This may result in an unusable Terraform Cloud workspace, and should be used + with extreme caution. + ## Example: Rename a Resource The example below renames the `packet_device` resource named `worker` to `helper`: diff --git a/website/docs/commands/state/push.html.md b/website/docs/commands/state/push.html.md index 4ba3f47c2..0d4258490 100644 --- a/website/docs/commands/state/push.html.md +++ b/website/docs/commands/state/push.html.md @@ -42,3 +42,10 @@ making changes that appear to be unsafe: Both of these safety checks can be disabled with the `-force` flag. **This is not recommended.** If you disable the safety checks and are pushing state, the destination state will be overwritten. + +Other available flags: + +* `-ignore-remote-version` - When using the enhanced remote backend with + Terraform Cloud, continue even if remote and local Terraform versions differ. + This may result in an unusable Terraform Cloud workspace, and should be used + with extreme caution. diff --git a/website/docs/commands/state/replace-provider.html.md b/website/docs/commands/state/replace-provider.html.md index c1b4d511f..f950943d2 100644 --- a/website/docs/commands/state/replace-provider.html.md +++ b/website/docs/commands/state/replace-provider.html.md @@ -38,6 +38,11 @@ The command-line flags are all optional. The list of available flags are: * `-state=path` - Path to the source state file to read from. Defaults to the configured backend, or "terraform.tfstate". +* `-ignore-remote-version` - When using the enhanced remote backend with + Terraform Cloud, continue even if remote and local Terraform versions differ. + This may result in an unusable Terraform Cloud workspace, and should be used + with extreme caution. + ## Example The example below replaces the `hashicorp/aws` provider with a fork by `acme`, hosted at a private registry at `registry.acme.corp`: diff --git a/website/docs/commands/state/rm.html.md b/website/docs/commands/state/rm.html.md index 3930821db..60f88f337 100644 --- a/website/docs/commands/state/rm.html.md +++ b/website/docs/commands/state/rm.html.md @@ -51,6 +51,11 @@ The command-line flags are all optional. The list of available flags are: Terraform-managed resources. By default it will use the configured backend, or the default "terraform.tfstate" if it exists. +* `-ignore-remote-version` - When using the enhanced remote backend with + Terraform Cloud, continue even if remote and local Terraform versions differ. + This may result in an unusable Terraform Cloud workspace, and should be used + with extreme caution. + ## Example: Remove a Resource The example below removes the `packet_device` resource named `worker`: diff --git a/website/docs/commands/taint.html.markdown b/website/docs/commands/taint.html.markdown index 65c758d9c..14dbf039c 100644 --- a/website/docs/commands/taint.html.markdown +++ b/website/docs/commands/taint.html.markdown @@ -65,6 +65,11 @@ The command-line flags are all optional. The list of available flags are: `-state` path will be used. Ignored when [remote state](/docs/state/remote.html) is used. +* `-ignore-remote-version` - When using the enhanced remote backend with + Terraform Cloud, continue even if remote and local Terraform versions differ. + This may result in an unusable Terraform Cloud workspace, and should be used + with extreme caution. + ## Example: Tainting a Single Resource This example will taint a single resource: diff --git a/website/docs/commands/untaint.html.markdown b/website/docs/commands/untaint.html.markdown index c0bf081a6..520795fc6 100644 --- a/website/docs/commands/untaint.html.markdown +++ b/website/docs/commands/untaint.html.markdown @@ -57,3 +57,8 @@ certain cases, see above note). The list of available flags are: * `-state-out=path` - Path to write updated state file. By default, the `-state` path will be used. Ignored when [remote state](/docs/state/remote.html) is used. + +* `-ignore-remote-version` - When using the enhanced remote backend with + Terraform Cloud, continue even if remote and local Terraform versions differ. + This may result in an unusable Terraform Cloud workspace, and should be used + with extreme caution.