mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
Some checks are pending
build / Determine intended Terraform version (push) Waiting to run
build / Determine Go toolchain version (push) Waiting to run
build / Generate release metadata (push) Blocked by required conditions
build / Build for freebsd_386 (push) Blocked by required conditions
build / Build for linux_386 (push) Blocked by required conditions
build / Build for openbsd_386 (push) Blocked by required conditions
build / Build for windows_386 (push) Blocked by required conditions
build / Build for darwin_amd64 (push) Blocked by required conditions
build / Build for freebsd_amd64 (push) Blocked by required conditions
build / Build for linux_amd64 (push) Blocked by required conditions
build / Build for openbsd_amd64 (push) Blocked by required conditions
build / Build for solaris_amd64 (push) Blocked by required conditions
build / Build for windows_amd64 (push) Blocked by required conditions
build / Build for freebsd_arm (push) Blocked by required conditions
build / Build for linux_arm (push) Blocked by required conditions
build / Build for darwin_arm64 (push) Blocked by required conditions
build / Build for linux_arm64 (push) Blocked by required conditions
build / Build for windows_arm64 (push) Blocked by required conditions
build / Build Docker image for linux_386 (push) Blocked by required conditions
build / Build Docker image for linux_amd64 (push) Blocked by required conditions
build / Build Docker image for linux_arm (push) Blocked by required conditions
build / Build Docker image for linux_arm64 (push) Blocked by required conditions
build / Build e2etest for linux_386 (push) Blocked by required conditions
build / Build e2etest for windows_386 (push) Blocked by required conditions
build / Build e2etest for darwin_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_amd64 (push) Blocked by required conditions
build / Build e2etest for windows_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_arm (push) Blocked by required conditions
build / Build e2etest for darwin_arm64 (push) Blocked by required conditions
build / Build e2etest for linux_arm64 (push) Blocked by required conditions
build / Run e2e test for linux_386 (push) Blocked by required conditions
build / Run e2e test for windows_386 (push) Blocked by required conditions
build / Run e2e test for darwin_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_amd64 (push) Blocked by required conditions
build / Run e2e test for windows_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_arm (push) Blocked by required conditions
build / Run e2e test for linux_arm64 (push) Blocked by required conditions
build / Run terraform-exec test for linux amd64 (push) Blocked by required conditions
Quick Checks / Unit Tests (push) Waiting to run
Quick Checks / Race Tests (push) Waiting to run
Quick Checks / End-to-end Tests (push) Waiting to run
Quick Checks / Code Consistency Checks (push) Waiting to run
* backend/inmem: Make it easier to use the backend in tests * cloud: Make the cloud backend testable * command/views: Introduce migration UI messages * command/init: Add 3 tests for migrations from a backend to PSS - TestInit_backend_to_stateStore_singleWorkspace - TestInit_backend_to_stateStore_multipleWorkspaces - TestInit_cloud_to_stateStore * command/init: Implement migration from a backend to PSS * address PR feedback * remove local state after migration
1719 lines
54 KiB
Go
1719 lines
54 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cloud
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
tfe "github.com/hashicorp/go-tfe"
|
|
version "github.com/hashicorp/go-version"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/backend"
|
|
"github.com/hashicorp/terraform/internal/backend/backendrun"
|
|
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
tfversion "github.com/hashicorp/terraform/version"
|
|
)
|
|
|
|
func TestCloud(t *testing.T) {
|
|
var _ backendrun.OperationsBackend = New(nil)
|
|
var _ backendrun.CLI = New(nil)
|
|
}
|
|
|
|
func TestCloud_backendWithName(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
workspaces, diags := b.Workspaces()
|
|
if diags.HasErrors() {
|
|
t.Fatalf("error: %v", diags.Err())
|
|
}
|
|
|
|
if len(workspaces) != 1 || workspaces[0] != testBackendSingleWorkspaceName {
|
|
t.Fatalf("should only have a single configured workspace matching the configured 'name' strategy, but got: %#v", workspaces)
|
|
}
|
|
|
|
if _, sDiags := b.StateMgr("foo"); sDiags.Err().Error() != backend.ErrWorkspacesNotSupported.Error() {
|
|
t.Fatalf("expected fetching a state which is NOT the single configured workspace to have an ErrWorkspacesNotSupported error, but got: %v", sDiags.Err())
|
|
}
|
|
|
|
if diags := b.DeleteWorkspace(testBackendSingleWorkspaceName, true); !diags.HasErrors() || diags.Err().Error() != backend.ErrWorkspacesNotSupported.Error() {
|
|
t.Fatalf("expected deleting the single configured workspace name to result in an error, but got: %v", diags.Err())
|
|
}
|
|
|
|
if diags := b.DeleteWorkspace("foo", true); !diags.HasErrors() || diags.Err().Error() != backend.ErrWorkspacesNotSupported.Error() {
|
|
t.Fatalf("expected deleting a workspace which is NOT the configured workspace name to result in an error, but got: %v", diags.Err())
|
|
}
|
|
}
|
|
|
|
func TestCloud_backendWithTags(t *testing.T) {
|
|
b, bCleanup := testBackendWithTags(t)
|
|
defer bCleanup()
|
|
|
|
backend.TestBackendStates(t, b)
|
|
|
|
// Test pagination works
|
|
for i := 0; i < 25; i++ {
|
|
_, sDiags := b.StateMgr(fmt.Sprintf("foo-%d", i+1))
|
|
if sDiags.HasErrors() {
|
|
t.Fatalf("error: %s", sDiags.Err())
|
|
}
|
|
}
|
|
|
|
workspaces, wDiags := b.Workspaces()
|
|
if wDiags.HasErrors() {
|
|
t.Fatalf("error: %s", wDiags.Err())
|
|
}
|
|
actual := len(workspaces)
|
|
if actual != 26 {
|
|
t.Errorf("expected 26 workspaces (over one standard paginated response), got %d", actual)
|
|
}
|
|
}
|
|
|
|
func TestCloud_backendWithKVTags(t *testing.T) {
|
|
b, bCleanup := testBackendWithKVTags(t)
|
|
defer bCleanup()
|
|
|
|
_, err := b.client.Workspaces.Create(context.Background(), "hashicorp", tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("ws-billing-101"),
|
|
TagBindings: []*tfe.TagBinding{
|
|
{Key: "dept", Value: "billing"},
|
|
{Key: "costcenter", Value: "101"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error creating workspace: %s", err)
|
|
}
|
|
|
|
workspaces, diags := b.Workspaces()
|
|
if diags.HasErrors() {
|
|
t.Fatalf("error: %s", diags)
|
|
}
|
|
|
|
actual := len(workspaces)
|
|
if actual != 1 {
|
|
t.Fatalf("expected 1 workspaces, got %d", actual)
|
|
}
|
|
|
|
if workspaces[0] != "ws-billing-101" {
|
|
t.Fatalf("expected workspace name to be 'ws-billing-101', got %s", workspaces[0])
|
|
}
|
|
}
|
|
|
|
func TestCloud_DescribeTags(t *testing.T) {
|
|
cases := map[string]struct {
|
|
expected string
|
|
expectedOr []string
|
|
config cty.Value
|
|
}{
|
|
"one set tag": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("org"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.SetVal([]cty.Value{
|
|
cty.StringVal("billing"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expected: "billing",
|
|
},
|
|
"two set tags": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("org"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.SetVal([]cty.Value{
|
|
cty.StringVal("billing"),
|
|
cty.StringVal("cc101"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expected: "billing, cc101",
|
|
},
|
|
"one kv tag": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("org"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.MapVal(map[string]cty.Value{
|
|
"dept": cty.StringVal("billing"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expected: "dept=billing",
|
|
},
|
|
"two kv tags": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("org"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.MapVal(map[string]cty.Value{
|
|
"dept": cty.StringVal("billing"),
|
|
"costcenter": cty.StringVal("101"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedOr: []string{"costcenter=101, dept=billing", "dept=billing, costcenter=101"},
|
|
},
|
|
"no tags": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("org"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("default"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
b, cleanup := testUnconfiguredBackend(t)
|
|
t.Cleanup(cleanup)
|
|
|
|
_, valDiags := b.PrepareConfig(tc.config)
|
|
if valDiags.Err() != nil {
|
|
t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
|
|
}
|
|
|
|
diags := b.Configure(tc.config)
|
|
if diags.Err() != nil {
|
|
t.Fatalf("%s: unexpected configure result: %v", name, diags.Err())
|
|
}
|
|
|
|
actual := b.WorkspaceMapping.DescribeTags()
|
|
if tc.expectedOr != nil {
|
|
for _, expected := range tc.expectedOr {
|
|
if actual == expected {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("expected one of %v, got %q", tc.expectedOr, actual)
|
|
}
|
|
|
|
if actual != tc.expected {
|
|
t.Fatalf("expected %q, got %q", tc.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test b.PrepareConfig, which actually does very little; most real validation
|
|
// happens later in resolveCloudConfig.
|
|
func TestCloud_PrepareConfig(t *testing.T) {
|
|
cases := map[string]struct {
|
|
config cty.Value
|
|
expectedErr string
|
|
}{
|
|
"workspace: name and tags conflict": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("org"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("billing"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`,
|
|
},
|
|
"totally empty config block is ok": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.NullVal(cty.String),
|
|
"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"name": cty.String,
|
|
"tags": cty.Set(cty.String),
|
|
"project": cty.String,
|
|
})),
|
|
}),
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
s := TestServer(t)
|
|
b := New(testDisco(s))
|
|
|
|
// Validate
|
|
_, valDiags := b.PrepareConfig(tc.config)
|
|
|
|
if tc.expectedErr != "" {
|
|
if valDiags.Err() == nil {
|
|
t.Fatalf("%s: expected validation error %q but didn't get one", name, tc.expectedErr)
|
|
} else {
|
|
actualErr := valDiags.Err().Error()
|
|
if !strings.Contains(actualErr, tc.expectedErr) {
|
|
t.Fatalf("%s: expected validation error %q but instead got %q", name, tc.expectedErr, actualErr)
|
|
}
|
|
}
|
|
} else {
|
|
if valDiags.Err() != nil {
|
|
t.Fatalf("%s: expected validation to pass but got error %q", name, valDiags.Err().Error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloud_configWithEnvVars(t *testing.T) {
|
|
cases := map[string]struct {
|
|
setup func(b *Cloud)
|
|
config cty.Value
|
|
vars map[string]string
|
|
expectedOrganization string
|
|
expectedHostname string
|
|
expectedWorkspaceName string
|
|
expectedProjectName string
|
|
expectedErr string
|
|
}{
|
|
"with no organization specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_ORGANIZATION": "hashicorp",
|
|
},
|
|
expectedOrganization: "hashicorp",
|
|
},
|
|
"with both organization and env var specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_ORGANIZATION": "we-should-not-see-this",
|
|
},
|
|
expectedOrganization: "hashicorp",
|
|
},
|
|
"with no hostname specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_HOSTNAME": "private.hashicorp.engineering",
|
|
},
|
|
expectedHostname: "private.hashicorp.engineering",
|
|
},
|
|
"with hostname and env var specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.StringVal("private.hashicorp.engineering"),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_HOSTNAME": "mycool.tfe-host.io",
|
|
},
|
|
expectedHostname: "private.hashicorp.engineering",
|
|
},
|
|
"an invalid workspace env var": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"name": cty.String,
|
|
"tags": cty.Set(cty.String),
|
|
"project": cty.String,
|
|
})),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_WORKSPACE": "i-dont-exist-in-org",
|
|
},
|
|
expectedErr: `Invalid workspace selection: Terraform failed to find workspace "i-dont-exist-in-org" in organization hashicorp`,
|
|
},
|
|
"workspaces and env var specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("mordor"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("mt-doom"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_WORKSPACE": "shire",
|
|
},
|
|
expectedErr: `conflicts with TF_WORKSPACE environment variable`,
|
|
},
|
|
"env var workspace does not have specified tag": {
|
|
setup: func(b *Cloud) {
|
|
b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
|
|
Name: tfe.String("mordor"),
|
|
})
|
|
|
|
b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("shire"),
|
|
})
|
|
},
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("mordor"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.SetVal([]cty.Value{
|
|
cty.StringVal("cloud"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_WORKSPACE": "shire",
|
|
},
|
|
expectedErr: "Terraform failed to find workspace \"shire\" with the tags specified in your configuration:\n[cloud]",
|
|
},
|
|
"env var workspace has specified tag": {
|
|
setup: func(b *Cloud) {
|
|
b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
|
|
Name: tfe.String("mordor"),
|
|
})
|
|
|
|
b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("shire"),
|
|
Tags: []*tfe.Tag{
|
|
{
|
|
Name: "hobbity",
|
|
},
|
|
},
|
|
})
|
|
},
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("mordor"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.SetVal([]cty.Value{
|
|
cty.StringVal("hobbity"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_WORKSPACE": "shire",
|
|
},
|
|
expectedWorkspaceName: "", // No error is raised, but workspace is not set
|
|
},
|
|
"project specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("mordor"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("mt-doom"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.StringVal("my-project"),
|
|
}),
|
|
}),
|
|
expectedWorkspaceName: "mt-doom",
|
|
expectedProjectName: "my-project",
|
|
},
|
|
"project env var specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("mordor"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("mt-doom"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_PROJECT": "other-project",
|
|
},
|
|
expectedWorkspaceName: "mt-doom",
|
|
expectedProjectName: "other-project",
|
|
},
|
|
"project and env var specified": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("mordor"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("mt-doom"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.StringVal("my-project"),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_PROJECT": "other-project",
|
|
},
|
|
expectedWorkspaceName: "mt-doom",
|
|
expectedProjectName: "my-project",
|
|
},
|
|
"workspace exists but in different project": {
|
|
setup: func(b *Cloud) {
|
|
b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
|
|
Name: tfe.String("mordor"),
|
|
})
|
|
|
|
project, _ := b.client.Projects.Create(context.Background(), "mordor", tfe.ProjectCreateOptions{
|
|
Name: "another-project",
|
|
})
|
|
|
|
b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("shire"),
|
|
Project: project,
|
|
})
|
|
},
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("mordor"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.SetVal([]cty.Value{
|
|
cty.StringVal("hobbity"),
|
|
}),
|
|
"project": cty.StringVal("my-project"),
|
|
}),
|
|
}),
|
|
expectedProjectName: "my-project",
|
|
// No error is raised, and workspace is still in its original
|
|
// project, but the configured project for any future workspaces
|
|
// created with `terraform workspace new` is unaffected.
|
|
},
|
|
"with everything set as env vars": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.NullVal(cty.String),
|
|
"workspaces": cty.NullVal(cty.String),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_ORGANIZATION": "mordor",
|
|
"TF_WORKSPACE": "mt-doom",
|
|
"TF_CLOUD_HOSTNAME": "mycool.tfe-host.io",
|
|
"TF_CLOUD_PROJECT": "my-project",
|
|
},
|
|
expectedOrganization: "mordor",
|
|
expectedWorkspaceName: "mt-doom",
|
|
expectedHostname: "mycool.tfe-host.io",
|
|
expectedProjectName: "my-project",
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
b, cleanup := testUnconfiguredBackend(t)
|
|
t.Cleanup(cleanup)
|
|
|
|
for k, v := range tc.vars {
|
|
os.Setenv(k, v)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
for k := range tc.vars {
|
|
os.Unsetenv(k)
|
|
}
|
|
})
|
|
|
|
_, valDiags := b.PrepareConfig(tc.config)
|
|
if valDiags.Err() != nil {
|
|
t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
|
|
}
|
|
|
|
if tc.setup != nil {
|
|
tc.setup(b)
|
|
}
|
|
|
|
diags := b.Configure(tc.config)
|
|
if (diags.Err() != nil || tc.expectedErr != "") &&
|
|
(diags.Err() == nil || !strings.Contains(diags.Err().Error(), tc.expectedErr)) {
|
|
t.Fatalf("%s: unexpected configure result: %v", name, diags.Err())
|
|
}
|
|
|
|
if tc.expectedOrganization != "" && tc.expectedOrganization != b.Organization {
|
|
t.Fatalf("%s: organization not valid: %s, expected: %s", name, b.Organization, tc.expectedOrganization)
|
|
}
|
|
|
|
if tc.expectedHostname != "" && tc.expectedHostname != b.Hostname {
|
|
t.Fatalf("%s: hostname not valid: %s, expected: %s", name, b.Hostname, tc.expectedHostname)
|
|
}
|
|
|
|
if tc.expectedWorkspaceName != "" && tc.expectedWorkspaceName != b.WorkspaceMapping.Name {
|
|
t.Fatalf("%s: workspace name not valid: %s, expected: %s", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName)
|
|
}
|
|
|
|
if tc.expectedProjectName != "" && tc.expectedProjectName != b.WorkspaceMapping.Project {
|
|
t.Fatalf("%s: project name not valid: %s, expected: %s", name, b.WorkspaceMapping.Project, tc.expectedProjectName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCloud_config(t *testing.T) {
|
|
cases := map[string]struct {
|
|
config cty.Value
|
|
confErr string
|
|
valErr string
|
|
}{
|
|
"with_a_non_tfe_host": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.StringVal("nontfe.local"),
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
confErr: "Host nontfe.local does not provide a tfe service",
|
|
},
|
|
// 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"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
confErr: "terraform login localhost",
|
|
},
|
|
"with_tags": {
|
|
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),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("billing"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
},
|
|
"with_kv_tags": {
|
|
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),
|
|
"tags": cty.MapVal(map[string]cty.Value{
|
|
"dept": cty.StringVal("billing"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})),
|
|
},
|
|
"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"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
},
|
|
"without_a_name_tags": {
|
|
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),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
confErr: `Missing workspace mapping strategy.`,
|
|
},
|
|
"with_both_a_name_and_tags": {
|
|
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"),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("billing"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
valErr: `Only one of workspace "tags" or "name" is allowed.`,
|
|
},
|
|
"invalid tags dynamic type": {
|
|
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),
|
|
"tags": cty.StringVal("dept:billing"),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})),
|
|
confErr: `tags must be a set or object, not string`,
|
|
},
|
|
"invalid tags dynamic type, object with non-string": {
|
|
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),
|
|
"tags": cty.MapVal(map[string]cty.Value{
|
|
"dept": cty.NumberIntVal(1),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})),
|
|
confErr: `tag object values must be strings`,
|
|
},
|
|
"invalid tags dynamic type, tuple non-string": {
|
|
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),
|
|
"tags": cty.TupleVal([]cty.Value{cty.NumberIntVal(1)}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})),
|
|
confErr: `tag elements must be strings`,
|
|
},
|
|
"null config": {
|
|
config: cty.NullVal(cty.EmptyObject),
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
b, cleanup := testUnconfiguredBackend(t)
|
|
t.Cleanup(cleanup)
|
|
|
|
// Validate
|
|
_, valDiags := b.PrepareConfig(tc.config)
|
|
if (valDiags.Err() != nil || tc.valErr != "") &&
|
|
(valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) {
|
|
t.Fatalf("unexpected validation result: %v", 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("unexpected configure result: %v", confDiags.Err())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) {
|
|
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),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("billing"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
|
|
handlers := map[string]func(http.ResponseWriter, *http.Request){
|
|
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("TFP-API-Version", "2.4")
|
|
},
|
|
}
|
|
s := TestServerWithHandlers(t, handlers)
|
|
|
|
b := New(testDisco(s))
|
|
|
|
confDiags := b.Configure(config)
|
|
if confDiags.Err() == nil {
|
|
t.Fatalf("expected configure to error")
|
|
}
|
|
|
|
expected := `The 'cloud' option is not supported with this version of Terraform Enterprise.`
|
|
if !strings.Contains(confDiags.Err().Error(), expected) {
|
|
t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) {
|
|
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),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("billing"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
|
|
handlers := map[string]func(http.ResponseWriter, *http.Request){
|
|
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("TFP-API-Version", "2.4")
|
|
},
|
|
}
|
|
s := TestServerWithHandlers(t, handlers)
|
|
|
|
b := New(testDisco(s))
|
|
b.runningInAutomation = true
|
|
|
|
confDiags := b.Configure(config)
|
|
if confDiags.Err() == nil {
|
|
t.Fatalf("expected configure to error")
|
|
}
|
|
|
|
expected := `This version of HCP Terraform does not support the state mechanism
|
|
attempting to be used by the platform. This should never happen.`
|
|
if !strings.Contains(confDiags.Err().Error(), expected) {
|
|
t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_setUnavailableTerraformVersion(t *testing.T) {
|
|
// go-tfe returns an error IRL if you try to set a Terraform version that's
|
|
// not available in your HCP Terraform instance. To test this, tfe_client_mock errors if
|
|
// you try to set any Terraform version for this specific workspace name.
|
|
workspaceName := "unavailable-terraform-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.NullVal(cty.String),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("sometag"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
|
|
b, _, bCleanup := testBackend(t, config, nil)
|
|
defer bCleanup()
|
|
|
|
// Make sure the workspace doesn't exist yet -- otherwise, we can't test what
|
|
// happens when a workspace gets created. This is why we can't use "name" in
|
|
// the backend config above, btw: if you do, testBackend() creates the default
|
|
// workspace before we get a chance to do anything.
|
|
_, err := b.client.Workspaces.Read(context.Background(), b.Organization, workspaceName)
|
|
if err != tfe.ErrResourceNotFound {
|
|
t.Fatalf("the workspace we were about to try and create (%s/%s) already exists in the mocks somehow, so this test isn't trustworthy anymore", b.Organization, workspaceName)
|
|
}
|
|
|
|
_, sDiags := b.StateMgr(workspaceName)
|
|
if sDiags.HasErrors() {
|
|
t.Fatalf("expected no error from StateMgr, despite not being able to set remote Terraform version: %#v", sDiags.Err())
|
|
}
|
|
// Make sure the workspace was created:
|
|
workspace, err := b.client.Workspaces.Read(context.Background(), b.Organization, workspaceName)
|
|
if err != nil {
|
|
t.Fatalf("b.StateMgr() didn't actually create the desired workspace")
|
|
}
|
|
// Make sure our mocks still error as expected, using the same update function b.StateMgr() would call:
|
|
_, err = b.client.Workspaces.UpdateByID(
|
|
context.Background(),
|
|
workspace.ID,
|
|
tfe.WorkspaceUpdateOptions{TerraformVersion: tfe.String("1.1.0")},
|
|
)
|
|
if err == nil {
|
|
t.Fatalf("the mocks aren't emulating a nonexistent remote Terraform version correctly, so this test isn't trustworthy anymore")
|
|
}
|
|
}
|
|
|
|
func TestCloud_resolveCloudConfig(t *testing.T) {
|
|
cases := map[string]struct {
|
|
config cty.Value
|
|
vars map[string]string
|
|
expectedResult cloudConfig
|
|
expectedErr string
|
|
}{
|
|
"hostname/org/name/token/project all set": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.StringVal("app.staging.terraform.io"),
|
|
"organization": cty.StringVal("examplecorp"),
|
|
"token": cty.StringVal("password123"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.StringVal("networking"),
|
|
}),
|
|
}),
|
|
expectedResult: cloudConfig{
|
|
hostname: "app.staging.terraform.io",
|
|
organization: "examplecorp",
|
|
token: "password123",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "prod",
|
|
Project: "networking",
|
|
},
|
|
},
|
|
},
|
|
"totally empty config block": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.NullVal(cty.String),
|
|
"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"name": cty.String,
|
|
"tags": cty.Set(cty.String),
|
|
"project": cty.String,
|
|
})),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_HOSTNAME": "app.staging.terraform.io",
|
|
"TF_CLOUD_ORGANIZATION": "examplecorp",
|
|
"TF_WORKSPACE": "prod",
|
|
"TF_CLOUD_PROJECT": "networking",
|
|
},
|
|
expectedResult: cloudConfig{
|
|
hostname: "app.staging.terraform.io",
|
|
organization: "examplecorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "prod",
|
|
Project: "networking",
|
|
},
|
|
},
|
|
},
|
|
"null organization": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.NullVal(cty.String),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedErr: `Invalid or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`,
|
|
},
|
|
"null workspace": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("org"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"name": cty.String,
|
|
"tags": cty.Set(cty.String),
|
|
"project": cty.String,
|
|
})),
|
|
}),
|
|
expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`,
|
|
},
|
|
"workspace: empty tags, name": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("org"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`,
|
|
},
|
|
"workspace: name and tags present": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("org"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("billing"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`,
|
|
},
|
|
"organization from environment": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.NullVal(cty.String),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_ORGANIZATION": "example-org",
|
|
},
|
|
expectedResult: cloudConfig{
|
|
hostname: defaultHostname,
|
|
organization: "example-org",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "prod",
|
|
},
|
|
},
|
|
},
|
|
"null workspace, TF_WORKSPACE present": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"name": cty.String,
|
|
"tags": cty.Set(cty.String),
|
|
"project": cty.String,
|
|
})),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_WORKSPACE": "my-workspace",
|
|
},
|
|
expectedResult: cloudConfig{
|
|
hostname: defaultHostname,
|
|
organization: "hashicorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "my-workspace",
|
|
},
|
|
},
|
|
},
|
|
"organization and workspace and project env var": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"organization": cty.NullVal(cty.String),
|
|
"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"name": cty.String,
|
|
"tags": cty.Set(cty.String),
|
|
"project": cty.String,
|
|
})),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_ORGANIZATION": "hashicorp",
|
|
"TF_WORKSPACE": "my-workspace",
|
|
"TF_CLOUD_PROJECT": "example-project",
|
|
},
|
|
expectedResult: cloudConfig{
|
|
hostname: defaultHostname,
|
|
organization: "hashicorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "my-workspace",
|
|
Project: "example-project",
|
|
},
|
|
},
|
|
},
|
|
"with no project": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("examplecorp"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedResult: cloudConfig{
|
|
hostname: defaultHostname,
|
|
organization: "examplecorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "prod",
|
|
},
|
|
},
|
|
},
|
|
"with project from environment": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("examplecorp"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_PROJECT": "example-project",
|
|
},
|
|
expectedResult: cloudConfig{
|
|
hostname: defaultHostname,
|
|
organization: "examplecorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "prod",
|
|
Project: "example-project",
|
|
},
|
|
},
|
|
},
|
|
"with both project env var and config value": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("examplecorp"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.StringVal("config-project"),
|
|
}),
|
|
}),
|
|
vars: map[string]string{
|
|
"TF_CLOUD_PROJECT": "env-project",
|
|
},
|
|
expectedResult: cloudConfig{
|
|
hostname: defaultHostname,
|
|
organization: "examplecorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "prod",
|
|
Project: "config-project", // config wins
|
|
},
|
|
},
|
|
},
|
|
"with hostname not set, set to default hostname": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedResult: cloudConfig{
|
|
hostname: defaultHostname,
|
|
organization: "hashicorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
Name: "prod",
|
|
},
|
|
},
|
|
},
|
|
"with workspace tags set": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"hostname": cty.StringVal("hashicorp.com"),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.SetVal(
|
|
[]cty.Value{
|
|
cty.StringVal("billing"),
|
|
cty.StringVal("applications"),
|
|
},
|
|
),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedResult: cloudConfig{
|
|
hostname: "hashicorp.com",
|
|
organization: "hashicorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
TagsAsSet: []string{"billing", "applications"},
|
|
},
|
|
},
|
|
},
|
|
"with kv tags set": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"hostname": cty.StringVal("hashicorp.com"),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"tags": cty.MapVal(map[string]cty.Value{
|
|
"dept": cty.StringVal("billing"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
expectedResult: cloudConfig{
|
|
hostname: "hashicorp.com",
|
|
organization: "hashicorp",
|
|
token: "",
|
|
workspaceMapping: WorkspaceMapping{
|
|
TagsAsMap: map[string]string{"dept": "billing"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
for k, v := range tc.vars {
|
|
os.Setenv(k, v)
|
|
}
|
|
t.Cleanup(func() {
|
|
for k := range tc.vars {
|
|
os.Unsetenv(k)
|
|
}
|
|
})
|
|
|
|
result, diags := resolveCloudConfig(tc.config)
|
|
|
|
// Validate either expected error or result, not both
|
|
if tc.expectedErr != "" {
|
|
if diags.Err() == nil {
|
|
t.Fatalf("%s: expected validation error %q but didn't get one", name, tc.expectedErr)
|
|
} else {
|
|
actualErr := diags.Err().Error()
|
|
if !strings.Contains(actualErr, tc.expectedErr) {
|
|
t.Fatalf("%s: expected validation error %q but instead got %q", name, tc.expectedErr, actualErr)
|
|
}
|
|
}
|
|
} else { // check result
|
|
// Sort tags first to avoid flaking, since the slice order from
|
|
// a cty.Set isn't guaranteed:
|
|
sort.Strings(tc.expectedResult.workspaceMapping.TagsAsSet)
|
|
sort.Strings(result.workspaceMapping.TagsAsSet)
|
|
if !reflect.DeepEqual(tc.expectedResult, result) {
|
|
t.Fatalf("%s: expected final config of %#v but instead got %#v", name, tc.expectedResult, result)
|
|
}
|
|
|
|
if !reflect.DeepEqual(tc.expectedResult.workspaceMapping.asTFETagBindings(), result.workspaceMapping.asTFETagBindings()) {
|
|
t.Fatalf("%s: expected final config of %#v but instead got %#v", name, tc.expectedResult, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCloud_forceLocalBackend(t *testing.T) {
|
|
b, cleanup := testUnconfiguredBackend(t)
|
|
t.Cleanup(cleanup)
|
|
|
|
os.Setenv("TF_FORCE_LOCAL_BACKEND", "true")
|
|
t.Cleanup(func() {
|
|
os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
|
|
})
|
|
|
|
obj := cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"hostname": cty.NullVal(cty.String),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
|
|
obj, valDiags := b.PrepareConfig(obj)
|
|
if valDiags.Err() != nil {
|
|
t.Fatalf("unexpected validation result: %v", valDiags.Err())
|
|
}
|
|
|
|
diags := b.Configure(obj)
|
|
if diags.Err() != nil {
|
|
t.Fatalf("unexpected configure result: %v", diags.Err())
|
|
}
|
|
|
|
if b.forceLocal != true {
|
|
t.Fatalf("expected force local backend to be set due to env var")
|
|
}
|
|
}
|
|
|
|
func TestCloud_localBackend(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(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 := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
if _, sDiags := b.StateMgr(testBackendSingleWorkspaceName); sDiags.HasErrors() {
|
|
t.Fatalf("expected no error, got %v", sDiags.Err())
|
|
}
|
|
|
|
if diags := b.DeleteWorkspace(testBackendSingleWorkspaceName, true); !diags.HasErrors() || diags.Err().Error() != backend.ErrWorkspacesNotSupported.Error() {
|
|
t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, diags.Err())
|
|
}
|
|
}
|
|
|
|
func TestCloud_StateMgr_versionCheck(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(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.WorkspaceMapping.Name,
|
|
tfe.WorkspaceUpdateOptions{
|
|
TerraformVersion: tfe.String(v0140.String()),
|
|
},
|
|
); err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
|
|
// This should succeed
|
|
if _, sDiags := b.StateMgr(testBackendSingleWorkspaceName); sDiags.HasErrors() {
|
|
t.Fatalf("expected no error, got %v", sDiags.Err())
|
|
}
|
|
|
|
// Now change the remote workspace to a different Terraform version
|
|
if _, err := b.client.Workspaces.Update(
|
|
context.Background(),
|
|
b.Organization,
|
|
b.WorkspaceMapping.Name,
|
|
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 _, sDiags := b.StateMgr(testBackendSingleWorkspaceName); sDiags.Err().Error() != want {
|
|
t.Fatalf("wrong error\n got: %v\nwant: %v", sDiags.Err(), want)
|
|
}
|
|
}
|
|
|
|
func TestCloud_StateMgr_versionCheckLatest(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(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.WorkspaceMapping.Name,
|
|
tfe.WorkspaceUpdateOptions{
|
|
TerraformVersion: tfe.String("latest"),
|
|
},
|
|
); err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
|
|
// This should succeed despite not being a string match
|
|
if _, sDiags := b.StateMgr(testBackendSingleWorkspaceName); sDiags.HasErrors() {
|
|
t.Fatalf("expected no error, got %v", sDiags.Err())
|
|
}
|
|
}
|
|
|
|
func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) {
|
|
testCases := []struct {
|
|
local string
|
|
remote string
|
|
executionMode string
|
|
wantErr bool
|
|
}{
|
|
{"0.13.5", "0.13.5", "agent", false},
|
|
{"0.14.0", "0.13.5", "remote", true},
|
|
{"0.14.0", "0.13.5", "local", false},
|
|
{"0.14.0", "0.14.1", "remote", false},
|
|
{"0.14.0", "1.0.99", "remote", false},
|
|
{"0.14.0", "1.1.0", "remote", false},
|
|
{"0.14.0", "1.3.0", "remote", true},
|
|
{"1.2.0", "1.2.99", "remote", false},
|
|
{"1.2.0", "1.3.0", "remote", true},
|
|
{"0.15.0", "latest", "remote", false},
|
|
{"1.1.5", "~> 1.1.1", "remote", false},
|
|
{"1.1.5", "> 1.1.0, < 1.3.0", "remote", false},
|
|
{"1.1.5", "~> 1.0.1", "remote", true},
|
|
// pre-release versions are comparable within their pre-release stage (dev,
|
|
// alpha, beta), but not comparable to different stages and not comparable
|
|
// to final releases.
|
|
{"1.1.0-beta1", "1.1.0-beta1", "remote", false},
|
|
{"1.1.0-beta1", "~> 1.1.0-beta", "remote", false},
|
|
{"1.1.0", "~> 1.1.0-beta", "remote", true},
|
|
{"1.1.0-beta1", "~> 1.1.0-dev", "remote", true},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(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.WorkspaceMapping.Name,
|
|
tfe.WorkspaceUpdateOptions{
|
|
ExecutionMode: &tc.executionMode,
|
|
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, "Incompatible Terraform version") {
|
|
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 := testBackendWithName(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.WorkspaceMapping.Name,
|
|
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, "Incompatible Terraform version: The remote workspace specified") {
|
|
t.Fatalf("unexpected error: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(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.WorkspaceMapping.Name,
|
|
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, "Incompatible Terraform version"; got != want {
|
|
t.Errorf("wrong summary: got %s, want %s", got, want)
|
|
}
|
|
wantDetail := "The local Terraform version (0.14.0) does not meet the version requirements for remote workspace hashicorp/app-prod (0.13.5)."
|
|
if got := diags[0].Description().Detail; got != wantDetail {
|
|
t.Errorf("wrong summary: got %s, want %s", got, wantDetail)
|
|
}
|
|
}
|
|
|
|
func TestCloudBackend_DeleteWorkspace_SafeAndForce(t *testing.T) {
|
|
b, bCleanup := testBackendWithTags(t)
|
|
defer bCleanup()
|
|
safeDeleteWorkspaceName := "safe-delete-workspace"
|
|
forceDeleteWorkspaceName := "force-delete-workspace"
|
|
|
|
_, sDiags := b.StateMgr(safeDeleteWorkspaceName)
|
|
if sDiags.HasErrors() {
|
|
t.Fatalf("error: %s", sDiags.Err())
|
|
}
|
|
|
|
_, sDiags = b.StateMgr(forceDeleteWorkspaceName)
|
|
if sDiags.HasErrors() {
|
|
t.Fatalf("error: %s", sDiags.Err())
|
|
}
|
|
|
|
// sanity check that the mock now contains two workspaces
|
|
wl, wDiags := b.Workspaces()
|
|
if wDiags.HasErrors() {
|
|
t.Fatalf("error fetching workspace names: %v", wDiags.Err())
|
|
}
|
|
if len(wl) != 2 {
|
|
t.Fatalf("expected 2 workspaces but got %d", len(wl))
|
|
}
|
|
|
|
c := context.Background()
|
|
safeDeleteWorkspace, err := b.client.Workspaces.Read(c, b.Organization, safeDeleteWorkspaceName)
|
|
if err != nil {
|
|
t.Fatalf("error fetching workspace: %v", err)
|
|
}
|
|
|
|
// Lock a workspace so that it should fail to be safe deleted
|
|
_, err = b.client.Workspaces.Lock(context.Background(), safeDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")})
|
|
if err != nil {
|
|
t.Fatalf("error locking workspace: %v", err)
|
|
}
|
|
dwDiags := b.DeleteWorkspace(safeDeleteWorkspaceName, false)
|
|
if !dwDiags.HasErrors() {
|
|
t.Fatalf("workspace should have failed to safe delete")
|
|
}
|
|
|
|
// unlock the workspace and confirm that safe-delete now works
|
|
_, err = b.client.Workspaces.Unlock(context.Background(), safeDeleteWorkspace.ID)
|
|
if err != nil {
|
|
t.Fatalf("error unlocking workspace: %v", err)
|
|
}
|
|
dwDiags = b.DeleteWorkspace(safeDeleteWorkspaceName, false)
|
|
if dwDiags.HasErrors() {
|
|
t.Fatalf("error safe deleting workspace: %v", dwDiags)
|
|
}
|
|
|
|
// lock a workspace and then confirm that force deleting it works
|
|
forceDeleteWorkspace, err := b.client.Workspaces.Read(c, b.Organization, forceDeleteWorkspaceName)
|
|
if err != nil {
|
|
t.Fatalf("error fetching workspace: %v", err)
|
|
}
|
|
_, err = b.client.Workspaces.Lock(context.Background(), forceDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")})
|
|
if err != nil {
|
|
t.Fatalf("error locking workspace: %v", err)
|
|
}
|
|
dwDiags = b.DeleteWorkspace(forceDeleteWorkspaceName, true)
|
|
if dwDiags.HasErrors() {
|
|
t.Fatalf("error force deleting workspace: %v", dwDiags.Err())
|
|
}
|
|
}
|
|
|
|
func TestCloudBackend_DeleteWorkspace_DoesNotExist(t *testing.T) {
|
|
b, bCleanup := testBackendWithTags(t)
|
|
defer bCleanup()
|
|
|
|
diags := b.DeleteWorkspace("non-existent-workspace", false)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("expected deleting a workspace which does not exist to succeed")
|
|
}
|
|
}
|
|
|
|
func TestCloud_ServiceDiscoveryAliases(t *testing.T) {
|
|
s := TestServer(t)
|
|
b := New(testDisco(s))
|
|
|
|
diag := b.Configure(cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String), // Forces aliasing to test server
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal("prod"),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
}))
|
|
if diag.HasErrors() {
|
|
t.Fatalf("expected no diagnostic errors, got %s", diag.Err())
|
|
}
|
|
|
|
aliases, err := b.ServiceDiscoveryAliases()
|
|
if err != nil {
|
|
t.Fatalf("expected no errors, got %s", err)
|
|
}
|
|
if len(aliases) != 1 {
|
|
t.Fatalf("expected 1 alias but got %d", len(aliases))
|
|
}
|
|
}
|
|
|
|
// When a user tries to view a cloud plan without having a cloud backend in their
|
|
// configuration, a call to AppName() would fail with a nil pointer exception
|
|
// See: https://github.com/hashicorp/terraform/issues/37748
|
|
func TestCloud_AppName_with_nil(t *testing.T) {
|
|
var backend *Cloud = nil
|
|
|
|
name := backend.AppName()
|
|
if name != "HCP Terraform" {
|
|
t.Fatalf("expected name to be HCP Terraform, got %q", name)
|
|
}
|
|
}
|