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.