mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-12 09:01:58 -06:00
2f5dcd5c0a
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
633 lines
18 KiB
Go
633 lines
18 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package cloud
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
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/cli"
|
|
"github.com/mitchellh/colorstring"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
"github.com/opentofu/opentofu/internal/httpclient"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
"github.com/opentofu/opentofu/version"
|
|
|
|
backendLocal "github.com/opentofu/opentofu/internal/backend/local"
|
|
)
|
|
|
|
const (
|
|
testCred = "test-auth-token"
|
|
)
|
|
|
|
var (
|
|
tfeHost = "app.terraform.io"
|
|
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
|
|
svchost.Hostname(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", "Terraform Cloud")
|
|
},
|
|
}
|
|
)
|
|
|
|
func skipIfTFENotEnabled(t *testing.T) {
|
|
if os.Getenv("TF_TFC_TEST") == "" {
|
|
t.Skip("this test accesses " + tfeHost + "; set TF_TFC_TEST=1 to run it")
|
|
}
|
|
}
|
|
|
|
// mockInput is a mock implementation of tofu.UIInput.
|
|
type mockInput struct {
|
|
answers map[string]string
|
|
}
|
|
|
|
func (m *mockInput) Input(ctx context.Context, opts *tofu.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 {
|
|
skipIfTFENotEnabled(t)
|
|
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.StringVal(tfeHost),
|
|
"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.StringVal(tfeHost),
|
|
"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 testBackendNoOperations(t *testing.T) (*Cloud, func()) {
|
|
obj := cty.ObjectVal(map[string]cty.Value{
|
|
"hostname": cty.StringVal(tfeHost),
|
|
"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.StringVal(tfeHost),
|
|
"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, err := b.StateMgr(testBackendSingleWorkspaceName)
|
|
if err != nil {
|
|
t.Fatalf("error: %v", 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()) {
|
|
skipIfTFENotEnabled(t)
|
|
var s *httptest.Server
|
|
if handlers != nil {
|
|
s = testServerWithHandlers(handlers)
|
|
} else {
|
|
s = testServer(t)
|
|
}
|
|
b := New(testDisco(s), encryption.StateEncryptionDisabled())
|
|
|
|
// 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.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://" + tfeHost)
|
|
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()) {
|
|
skipIfTFENotEnabled(t)
|
|
|
|
s := testServer(t)
|
|
b := New(testDisco(s), encryption.StateEncryptionDisabled())
|
|
|
|
// 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.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://" + tfeHost)
|
|
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) backend.Enhanced {
|
|
skipIfTFENotEnabled(t)
|
|
|
|
b := backendLocal.NewWithBackend(cloud, nil)
|
|
|
|
// Add a test provider to the local backend.
|
|
p := backendLocal.TestLocalProvider(t, b, "null", providers.ProviderSchema{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"null_resource": {
|
|
Block: &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 {
|
|
skipIfTFENotEnabled(t)
|
|
|
|
return testServerWithHandlers(testDefaultRequestHandlers)
|
|
}
|
|
|
|
// testServerWithHandlers returns a started *httptest.Server with the given set of request handlers
|
|
// overriding any default request handlers (testDefaultRequestHandlers).
|
|
func testServerWithHandlers(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)
|
|
}
|
|
}
|
|
|
|
return httptest.NewServer(mux)
|
|
}
|
|
|
|
func testServerWithSnapshotsEnabled(t *testing.T, enabled bool) *httptest.Server {
|
|
skipIfTFENotEnabled(t)
|
|
|
|
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, encryption.StateEncryptionDisabled())
|
|
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.OpenTofuUserAgent(version.String()))
|
|
|
|
d.ForceHostServices(svchost.Hostname(tfeHost), services)
|
|
d.ForceHostServices(svchost.Hostname("localhost"), services)
|
|
d.ForceHostServices(svchost.Hostname("nontfe.local"), nil)
|
|
return d
|
|
}
|
|
|
|
type unparsedVariableValue struct {
|
|
value string
|
|
source tofu.ValueSourceType
|
|
}
|
|
|
|
func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*tofu.InputValue, tfdiags.Diagnostics) {
|
|
return &tofu.InputValue{
|
|
Value: cty.StringVal(v.value),
|
|
SourceType: v.source,
|
|
}, tfdiags.Diagnostics{}
|
|
}
|
|
|
|
// testVariable returns a backend.UnparsedVariableValue used for testing.
|
|
func testVariables(s tofu.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue {
|
|
vars := make(map[string]backend.UnparsedVariableValue, len(vs))
|
|
for _, v := range vs {
|
|
vars[v] = &unparsedVariableValue{
|
|
value: v,
|
|
source: s,
|
|
}
|
|
}
|
|
return vars
|
|
}
|