terraform/internal/cloud/backend_test.go

905 lines
26 KiB
Go

package cloud
import (
"context"
"fmt"
"os"
"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/internal/backend"
"github.com/hashicorp/terraform/internal/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty"
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
)
func TestCloud(t *testing.T) {
var _ backend.Enhanced = New(nil)
var _ backend.CLI = New(nil)
}
func TestCloud_backendDefault(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
backend.TestBackendStates(t, b)
backend.TestBackendStateLocks(t, b, b)
backend.TestBackendStateForceUnlock(t, b, b)
}
func TestCloud_backendNoDefault(t *testing.T) {
b, bCleanup := testBackendNoDefault(t)
defer bCleanup()
backend.TestBackendStates(t, b)
}
func TestCloud_PrepareConfig(t *testing.T) {
cases := map[string]struct {
config cty.Value
expectedErr string
}{
"null organization": {
config: cty.ObjectVal(map[string]cty.Value{
"organization": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
expectedErr: `Invalid organization value: The "organization" attribute value must not be empty.`,
},
"null workspace": {
config: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("org"),
"workspaces": cty.NullVal(cty.String),
}),
expectedErr: `Invalid workspaces configuration: Either workspace "name" or "prefix" is required.`,
},
"workspace: empty name and empty prefix": {
config: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("org"),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.NullVal(cty.String),
"prefix": cty.NullVal(cty.String),
}),
}),
expectedErr: `Invalid workspaces configuration: Either workspace "name" or "prefix" is required.`,
},
"workspace: name and prefix present": {
config: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("org"),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.StringVal("app-"),
}),
}),
expectedErr: `Invalid workspaces configuration: Only one of workspace "name" or "prefix" is allowed.`,
},
}
for name, tc := range cases {
s := testServer(t)
b := New(testDisco(s))
// Validate
_, valDiags := b.PrepareConfig(tc.config)
if valDiags.Err() != nil && tc.expectedErr != "" {
actualErr := valDiags.Err().Error()
if !strings.Contains(actualErr, tc.expectedErr) {
t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
}
}
}
}
func TestCloud_config(t *testing.T) {
cases := map[string]struct {
config cty.Value
confErr string
valErr string
}{
"with_a_nonexisting_organization": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("nonexisting"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
confErr: "organization \"nonexisting\" at host app.terraform.io not found",
},
"with_an_unknown_host": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.StringVal("nonexisting.local"),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
confErr: "Failed to request discovery document",
},
// localhost advertises TFE services, but has no token in the credentials
"without_a_token": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.StringVal("localhost"),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
confErr: "terraform login localhost",
},
"with_a_name": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
},
"with_a_prefix": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.NullVal(cty.String),
"prefix": cty.StringVal("my-app-"),
}),
}),
},
"without_either_a_name_and_a_prefix": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.NullVal(cty.String),
"prefix": cty.NullVal(cty.String),
}),
}),
valErr: `Either workspace "name" or "prefix" is required`,
},
"with_both_a_name_and_a_prefix": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.StringVal("my-app-"),
}),
}),
valErr: `Only one of workspace "name" or "prefix" is allowed`,
},
"null config": {
config: cty.NullVal(cty.EmptyObject),
},
}
for name, tc := range cases {
s := testServer(t)
b := New(testDisco(s))
// Validate
_, valDiags := b.PrepareConfig(tc.config)
if (valDiags.Err() != nil || tc.valErr != "") &&
(valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) {
t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
}
// Configure
confDiags := b.Configure(tc.config)
if (confDiags.Err() != nil || tc.confErr != "") &&
(confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err())
}
}
}
func TestCloud_setConfigurationFields(t *testing.T) {
originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND")
cases := map[string]struct {
obj cty.Value
expectedHostname string
expectedOrganziation string
expectedWorkspacePrefix string
expectedWorkspaceName string
expectedForceLocal bool
setEnv func()
resetEnv func()
expectedErr string
}{
"with hostname set": {
obj: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("hashicorp"),
"hostname": cty.StringVal("hashicorp.com"),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
expectedHostname: "hashicorp.com",
expectedOrganziation: "hashicorp",
},
"with hostname not set, set to default hostname": {
obj: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("hashicorp"),
"hostname": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
expectedHostname: defaultHostname,
expectedOrganziation: "hashicorp",
},
"with workspace name set": {
obj: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("hashicorp"),
"hostname": cty.StringVal("hashicorp.com"),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
expectedHostname: "hashicorp.com",
expectedOrganziation: "hashicorp",
expectedWorkspaceName: "prod",
},
"with workspace prefix set": {
obj: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("hashicorp"),
"hostname": cty.StringVal("hashicorp.com"),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.NullVal(cty.String),
"prefix": cty.StringVal("prod"),
}),
}),
expectedHostname: "hashicorp.com",
expectedOrganziation: "hashicorp",
expectedWorkspacePrefix: "prod",
},
"with force local set": {
obj: cty.ObjectVal(map[string]cty.Value{
"organization": cty.StringVal("hashicorp"),
"hostname": cty.StringVal("hashicorp.com"),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.NullVal(cty.String),
"prefix": cty.StringVal("prod"),
}),
}),
expectedHostname: "hashicorp.com",
expectedOrganziation: "hashicorp",
expectedWorkspacePrefix: "prod",
setEnv: func() {
os.Setenv("TF_FORCE_LOCAL_BACKEND", "1")
},
resetEnv: func() {
os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv)
},
expectedForceLocal: true,
},
}
for name, tc := range cases {
b := &Cloud{}
// if `setEnv` is set, then we expect `resetEnv` to also be set
if tc.setEnv != nil {
tc.setEnv()
defer tc.resetEnv()
}
errDiags := b.setConfigurationFields(tc.obj)
if errDiags.HasErrors() || tc.expectedErr != "" {
actualErr := errDiags.Err().Error()
if !strings.Contains(actualErr, tc.expectedErr) {
t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err())
}
}
if tc.expectedHostname != "" && b.hostname != tc.expectedHostname {
t.Fatalf("%s: expected hostname %s to match actual hostname %s", name, tc.expectedHostname, b.hostname)
}
if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation {
t.Fatalf("%s: expected organization %s to match actual organization %s", name, tc.expectedOrganziation, b.organization)
}
if tc.expectedWorkspacePrefix != "" && b.prefix != tc.expectedWorkspacePrefix {
t.Fatalf("%s: expected workspace prefix %s to match actual workspace prefix %s", name, tc.expectedWorkspacePrefix, b.prefix)
}
if tc.expectedWorkspaceName != "" && b.workspace != tc.expectedWorkspaceName {
t.Fatalf("%s: expected workspace name %s to match actual workspace name %s", name, tc.expectedWorkspaceName, b.workspace)
}
if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal {
t.Fatalf("%s: expected force local backend to be set ", name)
}
}
}
func TestCloud_versionConstraints(t *testing.T) {
cases := map[string]struct {
config cty.Value
prerelease string
version string
result string
}{
"compatible version": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
version: "0.11.1",
},
"version too old": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
version: "0.0.1",
result: "upgrade Terraform to >= 0.1.0",
},
"version too new": {
config: cty.ObjectVal(map[string]cty.Value{
"hostname": cty.NullVal(cty.String),
"organization": cty.StringVal("hashicorp"),
"token": cty.NullVal(cty.String),
"workspaces": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("prod"),
"prefix": cty.NullVal(cty.String),
}),
}),
version: "10.0.1",
result: "downgrade Terraform to <= 10.0.0",
},
}
// Save and restore the actual version.
p := tfversion.Prerelease
v := tfversion.Version
defer func() {
tfversion.Prerelease = p
tfversion.Version = v
}()
for name, tc := range cases {
s := testServer(t)
b := New(testDisco(s))
// Set the version for this test.
tfversion.Prerelease = tc.prerelease
tfversion.Version = tc.version
// Validate
_, valDiags := b.PrepareConfig(tc.config)
if valDiags.HasErrors() {
t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
}
// Configure
confDiags := b.Configure(tc.config)
if (confDiags.Err() != nil || tc.result != "") &&
(confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.result)) {
t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err())
}
}
}
func TestCloud_localBackend(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
local, ok := b.local.(*backendLocal.Local)
if !ok {
t.Fatalf("expected b.local to be \"*local.Local\", got: %T", b.local)
}
cloud, ok := local.Backend.(*Cloud)
if !ok {
t.Fatalf("expected local.Backend to be *cloud.Cloud, got: %T", cloud)
}
}
func TestCloud_addAndRemoveWorkspacesDefault(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
if _, err := b.Workspaces(); err != backend.ErrWorkspacesNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err)
}
if _, err := b.StateMgr(backend.DefaultStateName); err != nil {
t.Fatalf("expected no error, got %v", err)
}
if _, err := b.StateMgr("prod"); err != backend.ErrWorkspacesNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err)
}
if err := b.DeleteWorkspace(backend.DefaultStateName); err != nil {
t.Fatalf("expected no error, got %v", err)
}
if err := b.DeleteWorkspace("prod"); err != backend.ErrWorkspacesNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err)
}
}
func TestCloud_addAndRemoveWorkspacesNoDefault(t *testing.T) {
b, bCleanup := testBackendNoDefault(t)
defer bCleanup()
states, err := b.Workspaces()
if err != nil {
t.Fatal(err)
}
expectedWorkspaces := []string(nil)
if !reflect.DeepEqual(states, expectedWorkspaces) {
t.Fatalf("expected states %#+v, got %#+v", expectedWorkspaces, states)
}
if _, err := b.StateMgr(backend.DefaultStateName); err != backend.ErrDefaultWorkspaceNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrDefaultWorkspaceNotSupported, err)
}
expectedA := "test_A"
if _, err := b.StateMgr(expectedA); err != nil {
t.Fatal(err)
}
states, err = b.Workspaces()
if err != nil {
t.Fatal(err)
}
expectedWorkspaces = append(expectedWorkspaces, expectedA)
if !reflect.DeepEqual(states, expectedWorkspaces) {
t.Fatalf("expected %#+v, got %#+v", expectedWorkspaces, states)
}
expectedB := "test_B"
if _, err := b.StateMgr(expectedB); err != nil {
t.Fatal(err)
}
states, err = b.Workspaces()
if err != nil {
t.Fatal(err)
}
expectedWorkspaces = append(expectedWorkspaces, expectedB)
if !reflect.DeepEqual(states, expectedWorkspaces) {
t.Fatalf("expected %#+v, got %#+v", expectedWorkspaces, states)
}
if err := b.DeleteWorkspace(backend.DefaultStateName); err != backend.ErrDefaultWorkspaceNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrDefaultWorkspaceNotSupported, err)
}
if err := b.DeleteWorkspace(expectedA); err != nil {
t.Fatal(err)
}
states, err = b.Workspaces()
if err != nil {
t.Fatal(err)
}
expectedWorkspaces = []string{expectedB}
if !reflect.DeepEqual(states, expectedWorkspaces) {
t.Fatalf("expected %#+v got %#+v", expectedWorkspaces, states)
}
if err := b.DeleteWorkspace(expectedB); err != nil {
t.Fatal(err)
}
states, err = b.Workspaces()
if err != nil {
t.Fatal(err)
}
expectedWorkspaces = []string(nil)
if !reflect.DeepEqual(states, expectedWorkspaces) {
t.Fatalf("expected %#+v, got %#+v", expectedWorkspaces, states)
}
}
func TestCloud_checkConstraints(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
cases := map[string]struct {
constraints *disco.Constraints
prerelease string
version string
result string
}{
"compatible version": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Maximum: "0.11.11",
},
version: "0.11.1",
result: "",
},
"version too old": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Maximum: "0.11.11",
},
version: "0.10.1",
result: "upgrade Terraform to >= 0.11.0",
},
"version too new": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Maximum: "0.11.11",
},
version: "0.12.0",
result: "downgrade Terraform to <= 0.11.11",
},
"version excluded - ordered": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Excluding: []string{"0.11.7", "0.11.8"},
Maximum: "0.11.11",
},
version: "0.11.7",
result: "upgrade Terraform to > 0.11.8",
},
"version excluded - unordered": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Excluding: []string{"0.11.8", "0.11.6"},
Maximum: "0.11.11",
},
version: "0.11.6",
result: "upgrade Terraform to > 0.11.8",
},
"list versions": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Maximum: "0.11.11",
},
version: "0.10.1",
result: "versions >= 0.11.0, <= 0.11.11.",
},
"list exclusion": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Excluding: []string{"0.11.6"},
Maximum: "0.11.11",
},
version: "0.11.6",
result: "excluding version 0.11.6.",
},
"list exclusions": {
constraints: &disco.Constraints{
Minimum: "0.11.0",
Excluding: []string{"0.11.8", "0.11.6"},
Maximum: "0.11.11",
},
version: "0.11.6",
result: "excluding versions 0.11.6, 0.11.8.",
},
}
// Save and restore the actual version.
p := tfversion.Prerelease
v := tfversion.Version
defer func() {
tfversion.Prerelease = p
tfversion.Version = v
}()
for name, tc := range cases {
// Set the version for this test.
tfversion.Prerelease = tc.prerelease
tfversion.Version = tc.version
// Check the constraints.
diags := b.checkConstraints(tc.constraints)
if (diags.Err() != nil || tc.result != "") &&
(diags.Err() == nil || !strings.Contains(diags.Err().Error(), tc.result)) {
t.Fatalf("%s: unexpected constraints result: %v", name, diags.Err())
}
}
}
func TestCloud_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 TestCloud_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 TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) {
testCases := []struct {
local string
remote string
operations bool
wantErr bool
}{
{"0.13.5", "0.13.5", true, false},
{"0.14.0", "0.13.5", true, true},
{"0.14.0", "0.13.5", false, false},
{"0.14.0", "0.14.1", true, false},
{"0.14.0", "1.0.99", true, false},
{"0.14.0", "1.1.0", true, true},
{"1.2.0", "1.2.99", true, false},
{"1.2.0", "1.3.0", true, true},
{"0.15.0", "latest", true, false},
}
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))
// 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{
Operations: tfe.Bool(tc.operations),
TerraformVersion: tfe.String(tc.remote),
},
); 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 TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
// Attempting to check the version against a workspace which doesn't exist
// should result in no errors
diags := b.VerifyWorkspaceTerraformVersion("invalid-workspace")
if len(diags) != 0 {
t.Fatalf("unexpected error: %s", diags.Err())
}
// Use a special workspace ID to trigger a 500 error, which should result
// in a failed check
diags = b.VerifyWorkspaceTerraformVersion("network-error")
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 TestCloud_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)
}
}