diff --git a/backend/remote/backend.go b/backend/remote/backend.go index ce400d4d6..3e9173e40 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -641,7 +641,10 @@ func (b *Remote) StateMgr(name string) (statemgr.Full, error) { // 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() { + wsv := workspace.TerraformVersion + // Explicitly ignore the pseudo-version "latest" here, as it will cause + // plan and apply to always fail. + if wsv != tfversion.String() && wsv != "latest" { return nil, fmt.Errorf("Remote workspace Terraform version %q does not match local Terraform version %q", workspace.TerraformVersion, tfversion.String()) } } @@ -890,6 +893,13 @@ func (b *Remote) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.D return diags } + // If the workspace has the pseudo-version "latest", all bets are off. We + // cannot reasonably determine what the intended Terraform version is, so + // we'll skip version verification. + if workspace.TerraformVersion == "latest" { + return nil + } + remoteVersion, err := version.NewSemver(workspace.TerraformVersion) if err != nil { diags = diags.Append(tfdiags.Sourceless( diff --git a/backend/remote/backend_test.go b/backend/remote/backend_test.go index 46dc5c64a..139ddafd3 100644 --- a/backend/remote/backend_test.go +++ b/backend/remote/backend_test.go @@ -515,6 +515,45 @@ func TestRemote_StateMgr_versionCheck(t *testing.T) { } } +func TestRemote_StateMgr_versionCheckLatest(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + 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 remote workspace to the pseudo-version "latest" + if _, err := b.client.Workspaces.Update( + context.Background(), + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + TerraformVersion: tfe.String("latest"), + }, + ); err != nil { + t.Fatalf("error: %v", err) + } + + // This should succeed despite not being a string match + if _, err := b.StateMgr(backend.DefaultStateName); err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + func TestRemote_VerifyWorkspaceTerraformVersion(t *testing.T) { testCases := []struct { local string @@ -528,6 +567,7 @@ func TestRemote_VerifyWorkspaceTerraformVersion(t *testing.T) { {"0.14.0", "1.1.0", true}, {"1.2.0", "1.2.99", false}, {"1.2.0", "1.3.0", true}, + {"0.15.0", "latest", false}, } for _, tc := range testCases { t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) { @@ -535,7 +575,6 @@ func TestRemote_VerifyWorkspaceTerraformVersion(t *testing.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 @@ -559,7 +598,7 @@ func TestRemote_VerifyWorkspaceTerraformVersion(t *testing.T) { b.organization, b.workspace, tfe.WorkspaceUpdateOptions{ - TerraformVersion: tfe.String(remote.String()), + TerraformVersion: tfe.String(tc.remote), }, ); err != nil { t.Fatalf("error: %v", err)