mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
This prevents a cyclic dependency and also makes sense semantically. The arguments package will collect the unparsed variable values and the backendrun helpers will work to collect the values and transform them into terraform.InputValue.
638 lines
19 KiB
Go
638 lines
19 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cloud
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/cli"
|
|
tfe "github.com/hashicorp/go-tfe"
|
|
svchost "github.com/hashicorp/terraform-svchost"
|
|
"github.com/hashicorp/terraform-svchost/auth"
|
|
"github.com/hashicorp/terraform-svchost/disco"
|
|
"github.com/mitchellh/colorstring"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
|
"github.com/hashicorp/terraform/internal/configs"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/httpclient"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"github.com/hashicorp/terraform/version"
|
|
|
|
"github.com/hashicorp/terraform/internal/backend/backendrun"
|
|
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
|
|
)
|
|
|
|
const (
|
|
testCred = "test-auth-token"
|
|
)
|
|
|
|
var (
|
|
tfeHost = svchost.Hostname(defaultHostname)
|
|
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
|
|
tfeHost: {"token": testCred},
|
|
})
|
|
testBackendSingleWorkspaceName = "app-prod"
|
|
defaultTFCPing = 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.5")
|
|
w.Header().Set("TFP-AppName", "HCP Terraform")
|
|
},
|
|
}
|
|
)
|
|
|
|
// mockInput is a mock implementation of terraform.UIInput.
|
|
type mockInput struct {
|
|
answers map[string]string
|
|
}
|
|
|
|
func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
|
|
v, ok := m.answers[opts.Id]
|
|
if !ok {
|
|
return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
|
|
}
|
|
if v == "wait-for-external-update" {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(time.Minute):
|
|
}
|
|
}
|
|
delete(m.answers, opts.Id)
|
|
return v, nil
|
|
}
|
|
|
|
func testInput(t *testing.T, answers map[string]string) *mockInput {
|
|
return &mockInput{answers: answers}
|
|
}
|
|
|
|
func testBackendWithName(t *testing.T) (*Cloud, func()) {
|
|
b, _, c := testBackendAndMocksWithName(t)
|
|
return b, c
|
|
}
|
|
|
|
func testBackendAndMocksWithName(t *testing.T) (*Cloud, *MockClient, func()) {
|
|
obj := 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(testBackendSingleWorkspaceName),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
return testBackend(t, obj, defaultTFCPing)
|
|
}
|
|
|
|
func testBackendWithTags(t *testing.T) (*Cloud, func()) {
|
|
obj := 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),
|
|
}),
|
|
})
|
|
b, _, c := testBackend(t, obj, nil)
|
|
return b, c
|
|
}
|
|
|
|
func testBackendWithKVTags(t *testing.T) (*Cloud, func()) {
|
|
obj := 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"),
|
|
"costcenter": cty.StringVal("101"),
|
|
}),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
b, _, c := testBackend(t, obj, nil)
|
|
return b, c
|
|
}
|
|
|
|
func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
|
|
obj := cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.NullVal(cty.String),
|
|
"organization": cty.StringVal("no-operations"),
|
|
"token": cty.NullVal(cty.String),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.StringVal(testBackendSingleWorkspaceName),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
b, _, c := testBackend(t, obj, nil)
|
|
return b, c
|
|
}
|
|
|
|
func testBackendWithHandlers(t *testing.T, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, func()) {
|
|
obj := 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(testBackendSingleWorkspaceName),
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
"project": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
b, _, c := testBackend(t, obj, handlers)
|
|
return b, c
|
|
}
|
|
|
|
func testCloudState(t *testing.T) *State {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
raw, sDiags := b.StateMgr(testBackendSingleWorkspaceName)
|
|
if sDiags.HasErrors() {
|
|
t.Fatalf("error: %v", sDiags.Err())
|
|
}
|
|
|
|
return raw.(*State)
|
|
}
|
|
|
|
func testBackendWithOutputs(t *testing.T) (*Cloud, func()) {
|
|
b, cleanup := testBackendWithName(t)
|
|
|
|
// Get a new mock client to use for adding outputs
|
|
mc := NewMockClient()
|
|
|
|
mc.StateVersionOutputs.create("svo-abcd", &tfe.StateVersionOutput{
|
|
ID: "svo-abcd",
|
|
Value: "foobar",
|
|
Sensitive: true,
|
|
Type: "string",
|
|
Name: "sensitive_output",
|
|
DetailedType: "string",
|
|
})
|
|
|
|
mc.StateVersionOutputs.create("svo-zyxw", &tfe.StateVersionOutput{
|
|
ID: "svo-zyxw",
|
|
Value: "bazqux",
|
|
Type: "string",
|
|
Name: "nonsensitive_output",
|
|
DetailedType: "string",
|
|
})
|
|
|
|
var dt interface{}
|
|
var val interface{}
|
|
err := json.Unmarshal([]byte(`["object", {"foo":"string"}]`), &dt)
|
|
if err != nil {
|
|
t.Fatalf("could not unmarshal detailed type: %s", err)
|
|
}
|
|
err = json.Unmarshal([]byte(`{"foo":"bar"}`), &val)
|
|
if err != nil {
|
|
t.Fatalf("could not unmarshal value: %s", err)
|
|
}
|
|
mc.StateVersionOutputs.create("svo-efgh", &tfe.StateVersionOutput{
|
|
ID: "svo-efgh",
|
|
Value: val,
|
|
Type: "object",
|
|
Name: "object_output",
|
|
DetailedType: dt,
|
|
})
|
|
|
|
err = json.Unmarshal([]byte(`["list", "bool"]`), &dt)
|
|
if err != nil {
|
|
t.Fatalf("could not unmarshal detailed type: %s", err)
|
|
}
|
|
err = json.Unmarshal([]byte(`[true, false, true, true]`), &val)
|
|
if err != nil {
|
|
t.Fatalf("could not unmarshal value: %s", err)
|
|
}
|
|
mc.StateVersionOutputs.create("svo-ijkl", &tfe.StateVersionOutput{
|
|
ID: "svo-ijkl",
|
|
Value: val,
|
|
Type: "array",
|
|
Name: "list_output",
|
|
DetailedType: dt,
|
|
})
|
|
|
|
b.client.StateVersionOutputs = mc.StateVersionOutputs
|
|
|
|
return b, cleanup
|
|
}
|
|
|
|
func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, *MockClient, func()) {
|
|
var s *httptest.Server
|
|
if handlers != nil {
|
|
s = TestServerWithHandlers(t, handlers)
|
|
} else {
|
|
s = TestServer(t)
|
|
}
|
|
b := New(testDisco(s))
|
|
|
|
// Configure the backend so the client is created.
|
|
newObj, valDiags := b.PrepareConfig(obj)
|
|
if len(valDiags) != 0 {
|
|
t.Fatalf("testBackend: backend.PrepareConfig() failed: %s", valDiags.ErrWithWarnings())
|
|
}
|
|
obj = newObj
|
|
|
|
confDiags := b.Configure(obj)
|
|
if len(confDiags) != 0 {
|
|
t.Fatalf("testBackend: backend.Configure() failed: %s", confDiags.ErrWithWarnings())
|
|
}
|
|
|
|
// Get a new mock client.
|
|
mc := NewMockClient()
|
|
|
|
// Replace the services we use with our mock services.
|
|
b.CLI = cli.NewMockUi()
|
|
b.client.Applies = mc.Applies
|
|
b.client.ConfigurationVersions = mc.ConfigurationVersions
|
|
b.client.CostEstimates = mc.CostEstimates
|
|
b.client.Organizations = mc.Organizations
|
|
b.client.Plans = mc.Plans
|
|
b.client.TaskStages = mc.TaskStages
|
|
b.client.PolicySetOutcomes = mc.PolicySetOutcomes
|
|
b.client.PolicyChecks = mc.PolicyChecks
|
|
b.client.QueryRuns = mc.QueryRuns
|
|
b.client.Runs = mc.Runs
|
|
b.client.RunEvents = mc.RunEvents
|
|
b.client.StateVersions = mc.StateVersions
|
|
b.client.StateVersionOutputs = mc.StateVersionOutputs
|
|
b.client.Variables = mc.Variables
|
|
b.client.Workspaces = mc.Workspaces
|
|
|
|
// Set local to a local test backend.
|
|
b.local = testLocalBackend(t, b)
|
|
b.input = true
|
|
|
|
baseURL, err := url.Parse("https://app.terraform.io")
|
|
if err != nil {
|
|
t.Fatalf("testBackend: failed to parse base URL for client")
|
|
}
|
|
baseURL.Path = "/api/v2/"
|
|
|
|
readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) {
|
|
return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create the organization.
|
|
_, err = b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
|
|
Name: tfe.String(b.Organization),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
|
|
// Create the default workspace if required.
|
|
if b.WorkspaceMapping.Name != "" {
|
|
_, err = b.client.Workspaces.Create(ctx, b.Organization, tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String(b.WorkspaceMapping.Name),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
}
|
|
|
|
return b, mc, s.Close
|
|
}
|
|
|
|
// testUnconfiguredBackend is used for testing the configuration of the backend
|
|
// with the mock client
|
|
func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) {
|
|
s := TestServer(t)
|
|
b := New(testDisco(s))
|
|
|
|
// Normally, the client is created during configuration, but the configuration uses the
|
|
// client to read entitlements.
|
|
var err error
|
|
b.client, err = tfe.NewClient(&tfe.Config{
|
|
Token: "fake-token",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Get a new mock client.
|
|
mc := NewMockClient()
|
|
|
|
// Replace the services we use with our mock services.
|
|
b.CLI = cli.NewMockUi()
|
|
b.client.Applies = mc.Applies
|
|
b.client.ConfigurationVersions = mc.ConfigurationVersions
|
|
b.client.CostEstimates = mc.CostEstimates
|
|
b.client.Organizations = mc.Organizations
|
|
b.client.Plans = mc.Plans
|
|
b.client.PolicySetOutcomes = mc.PolicySetOutcomes
|
|
b.client.PolicyChecks = mc.PolicyChecks
|
|
b.client.QueryRuns = mc.QueryRuns
|
|
b.client.Runs = mc.Runs
|
|
b.client.RunEvents = mc.RunEvents
|
|
b.client.StateVersions = mc.StateVersions
|
|
b.client.StateVersionOutputs = mc.StateVersionOutputs
|
|
b.client.Variables = mc.Variables
|
|
b.client.Workspaces = mc.Workspaces
|
|
|
|
baseURL, err := url.Parse("https://app.terraform.io")
|
|
if err != nil {
|
|
t.Fatalf("testBackend: failed to parse base URL for client")
|
|
}
|
|
baseURL.Path = "/api/v2/"
|
|
|
|
readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) {
|
|
return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
|
|
}
|
|
|
|
// Set local to a local test backend.
|
|
b.local = testLocalBackend(t, b)
|
|
|
|
return b, s.Close
|
|
}
|
|
|
|
func testLocalBackend(t *testing.T, cloud *Cloud) backendrun.OperationsBackend {
|
|
b := backendLocal.NewWithBackend(cloud)
|
|
|
|
// Add a test provider to the local backend.
|
|
p := backendLocal.TestLocalProvider(t, b, "null", providers.ProviderSchema{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"null_resource": {
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("yes"),
|
|
})}
|
|
|
|
return b
|
|
}
|
|
|
|
// TestServer returns a started *httptest.Server used for local testing with the default set of
|
|
// request handlers.
|
|
func TestServer(t *testing.T) *httptest.Server {
|
|
return TestServerWithHandlers(t, testDefaultRequestHandlers)
|
|
}
|
|
|
|
// TestServerWithHandlers returns a started *httptest.Server with the given set of request handlers
|
|
// overriding any default request handlers (testDefaultRequestHandlers).
|
|
func TestServerWithHandlers(t *testing.T, handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server {
|
|
mux := http.NewServeMux()
|
|
for route, handler := range handlers {
|
|
mux.HandleFunc(route, handler)
|
|
}
|
|
for route, handler := range testDefaultRequestHandlers {
|
|
if handlers[route] == nil {
|
|
mux.HandleFunc(route, handler)
|
|
}
|
|
}
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
|
t.Logf("unexpected %s request received for %q", req.Method, req.URL.String())
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
})
|
|
|
|
return httptest.NewServer(mux)
|
|
}
|
|
|
|
func testServerWithSnapshotsEnabled(t *testing.T, enabled bool) *httptest.Server {
|
|
var serverURL string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Log(r.Method, r.URL.String())
|
|
|
|
if r.URL.Path == "/state-json" {
|
|
t.Log("pretending to be Archivist")
|
|
fakeState := states.NewState()
|
|
fakeStateFile := statefile.New(fakeState, "boop", 1)
|
|
var buf bytes.Buffer
|
|
statefile.Write(fakeStateFile, &buf)
|
|
respBody := buf.Bytes()
|
|
w.Header().Set("content-type", "application/json")
|
|
w.Header().Set("content-length", strconv.FormatInt(int64(len(respBody)), 10))
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(respBody)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == "/api/ping" {
|
|
t.Log("pretending to be Ping")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
fakeBody := map[string]any{
|
|
"data": map[string]any{
|
|
"type": "state-versions",
|
|
"id": GenerateID("sv-"),
|
|
"attributes": map[string]any{
|
|
"hosted-state-download-url": serverURL + "/state-json",
|
|
"hosted-state-upload-url": serverURL + "/state-json",
|
|
},
|
|
},
|
|
}
|
|
fakeBodyRaw, err := json.Marshal(fakeBody)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
w.Header().Set("content-type", tfe.ContentTypeJSONAPI)
|
|
w.Header().Set("content-length", strconv.FormatInt(int64(len(fakeBodyRaw)), 10))
|
|
|
|
switch r.Method {
|
|
case "POST":
|
|
t.Log("pretending to be Create a State Version")
|
|
if enabled {
|
|
w.Header().Set("x-terraform-snapshot-interval", "300")
|
|
}
|
|
w.WriteHeader(http.StatusAccepted)
|
|
case "GET":
|
|
t.Log("pretending to be Fetch the Current State Version for a Workspace")
|
|
if enabled {
|
|
w.Header().Set("x-terraform-snapshot-interval", "300")
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
case "PUT":
|
|
t.Log("pretending to be Archivist")
|
|
default:
|
|
t.Fatal("don't know what API operation this was supposed to be")
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(fakeBodyRaw)
|
|
}))
|
|
serverURL = server.URL
|
|
return server
|
|
}
|
|
|
|
// testDefaultRequestHandlers is a map of request handlers intended to be used in a request
|
|
// multiplexer for a test server. A caller may use testServerWithHandlers to start a server with
|
|
// this base set of routes, and override a particular route for whatever edge case is being tested.
|
|
var testDefaultRequestHandlers = map[string]func(http.ResponseWriter, *http.Request){
|
|
// Respond to service discovery calls.
|
|
"/well-known/terraform.json": func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
io.WriteString(w, `{
|
|
"tfe.v2": "/api/v2/",
|
|
}`)
|
|
},
|
|
|
|
// Respond to service version constraints calls.
|
|
"/v1/versions/": func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
io.WriteString(w, fmt.Sprintf(`{
|
|
"service": "%s",
|
|
"product": "terraform",
|
|
"minimum": "0.1.0",
|
|
"maximum": "10.0.0"
|
|
}`, path.Base(r.URL.Path)))
|
|
},
|
|
|
|
// Respond to pings to get the API version header.
|
|
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("TFP-API-Version", "2.5")
|
|
},
|
|
|
|
// Respond to the initial query to read the hashicorp org entitlements.
|
|
"/api/v2/organizations/hashicorp/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/vnd.api+json")
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "org-GExadygjSbKP8hsY",
|
|
"type": "entitlement-sets",
|
|
"attributes": {
|
|
"operations": true,
|
|
"private-module-registry": true,
|
|
"sentinel": true,
|
|
"state-storage": true,
|
|
"teams": true,
|
|
"vcs-integrations": true
|
|
}
|
|
}
|
|
}`)
|
|
},
|
|
|
|
// Respond to the initial query to read the no-operations org entitlements.
|
|
"/api/v2/organizations/no-operations/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/vnd.api+json")
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "org-ufxa3y8jSbKP8hsT",
|
|
"type": "entitlement-sets",
|
|
"attributes": {
|
|
"operations": false,
|
|
"private-module-registry": true,
|
|
"sentinel": true,
|
|
"state-storage": true,
|
|
"teams": true,
|
|
"vcs-integrations": true
|
|
}
|
|
}
|
|
}`)
|
|
},
|
|
|
|
// All tests that are assumed to pass will use the hashicorp organization,
|
|
// so for all other organization requests we will return a 404.
|
|
"/api/v2/organizations/": func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(404)
|
|
io.WriteString(w, `{
|
|
"errors": [
|
|
{
|
|
"status": "404",
|
|
"title": "not found"
|
|
}
|
|
]
|
|
}`)
|
|
},
|
|
}
|
|
|
|
func mockColorize() *colorstring.Colorize {
|
|
colors := make(map[string]string)
|
|
for k, v := range colorstring.DefaultColors {
|
|
colors[k] = v
|
|
}
|
|
colors["purple"] = "38;5;57"
|
|
|
|
return &colorstring.Colorize{
|
|
Colors: colors,
|
|
Disable: false,
|
|
Reset: true,
|
|
}
|
|
}
|
|
|
|
func mockSROWorkspace(t *testing.T, b *Cloud, workspaceName string) {
|
|
_, err := b.client.Workspaces.Update(context.Background(), "hashicorp", workspaceName, tfe.WorkspaceUpdateOptions{
|
|
StructuredRunOutputEnabled: tfe.Bool(true),
|
|
TerraformVersion: tfe.String("1.4.0"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Error enabling SRO on workspace %s: %v", workspaceName, err)
|
|
}
|
|
}
|
|
|
|
// testDisco returns a *disco.Disco mapping app.terraform.io and
|
|
// localhost to a local test server.
|
|
func testDisco(s *httptest.Server) *disco.Disco {
|
|
services := map[string]interface{}{
|
|
"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
|
|
}
|
|
d := disco.NewWithCredentialsSource(credsSrc)
|
|
d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
|
|
|
d.ForceHostServices(svchost.Hostname(defaultHostname), services)
|
|
d.ForceHostServices(svchost.Hostname("localhost"), services)
|
|
d.ForceHostServices(svchost.Hostname("nontfe.local"), nil)
|
|
return d
|
|
}
|
|
|
|
type unparsedVariableValue struct {
|
|
value string
|
|
source terraform.ValueSourceType
|
|
}
|
|
|
|
func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
|
|
return &terraform.InputValue{
|
|
Value: cty.StringVal(v.value),
|
|
SourceType: v.source,
|
|
}, tfdiags.Diagnostics{}
|
|
}
|
|
|
|
// testVariable returns a arguments.UnparsedVariableValue used for testing.
|
|
func testVariables(s terraform.ValueSourceType, vs ...string) map[string]arguments.UnparsedVariableValue {
|
|
vars := make(map[string]arguments.UnparsedVariableValue, len(vs))
|
|
for _, v := range vs {
|
|
vars[v] = &unparsedVariableValue{
|
|
value: v,
|
|
source: s,
|
|
}
|
|
}
|
|
return vars
|
|
}
|