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.
This commit is contained in:
Alisdair McDiarmid 2020-11-13 16:43:56 -05:00
parent b8d448de83
commit c5c1f31db3
36 changed files with 779 additions and 90 deletions

View File

@ -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.

View File

@ -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)
}
}
})
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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 {

View File

@ -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
//-------------------------------------------------------------------

View File

@ -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))

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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() {

View File

@ -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] <address>
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)

View File

@ -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)

View File

@ -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())

View File

@ -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())

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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`:

View File

@ -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.

View File

@ -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`:

View File

@ -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`:

View File

@ -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:

View File

@ -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.