2023-05-02 11:33:06 -04:00
|
|
|
// Copyright IBM Corp. 2014, 2026
|
2023-08-10 18:43:27 -04:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-05-02 11:33:06 -04:00
|
|
|
|
2021-08-12 15:30:24 -04:00
|
|
|
package cloud
|
|
|
|
|
|
|
|
|
|
import (
|
2023-05-24 20:25:28 -04:00
|
|
|
"bytes"
|
2021-08-12 15:30:24 -04:00
|
|
|
"context"
|
2022-03-17 00:47:06 -04:00
|
|
|
"encoding/json"
|
2021-08-12 15:30:24 -04:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2023-02-09 07:35:48 -05:00
|
|
|
"net/url"
|
2021-08-12 15:30:24 -04:00
|
|
|
"path"
|
2023-05-24 20:25:28 -04:00
|
|
|
"strconv"
|
2021-08-12 15:30:24 -04:00
|
|
|
"testing"
|
2021-09-07 16:35:32 -04:00
|
|
|
"time"
|
2021-08-12 15:30:24 -04:00
|
|
|
|
2023-12-20 06:04:10 -05:00
|
|
|
"github.com/hashicorp/cli"
|
2021-08-12 15:30:24 -04:00
|
|
|
tfe "github.com/hashicorp/go-tfe"
|
|
|
|
|
svchost "github.com/hashicorp/terraform-svchost"
|
|
|
|
|
"github.com/hashicorp/terraform-svchost/auth"
|
|
|
|
|
"github.com/hashicorp/terraform-svchost/disco"
|
2023-02-09 07:35:48 -05:00
|
|
|
"github.com/mitchellh/colorstring"
|
2022-07-25 16:49:31 -04:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
|
|
2026-02-17 08:56:46 -05:00
|
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
2021-08-12 15:30:24 -04:00
|
|
|
"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"
|
2023-05-24 20:25:28 -04:00
|
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
|
|
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
2021-08-12 15:30:24 -04:00
|
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
|
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
|
|
|
"github.com/hashicorp/terraform/version"
|
|
|
|
|
|
backendrun: Separate the types/etc for backends that support operations
We previously had all of the types and helpers for all kinds of backends
together in package backend. That kept things relatively simple, but it
also meant that the majority of backends that only deal with remote state
storage ended up still indirectly depending on the entire Terraform modules
runtime, configuration loader, etc, etc, which brings into scope a bunch
of external dependencies that the remote state backends don't really need.
Since backends that support operations are a rare exception, we'll move the
types and helpers for those into a separate package "backendrun", and
then the main package backend can have a much more modest set of types and,
more importantly, a modest set of dependencies on other packages in this
codebase.
This is part of an ongoing effort to reduce the exposure of Terraform Core
and CLI code to the remote backends and vice-versa, so that in the long
run we can more often treat them as separate for dependency maintenance
purposes.
2024-03-11 19:27:44 -04:00
|
|
|
"github.com/hashicorp/terraform/internal/backend/backendrun"
|
2021-08-12 15:30:24 -04:00
|
|
|
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},
|
|
|
|
|
})
|
2021-09-20 14:07:53 -04:00
|
|
|
testBackendSingleWorkspaceName = "app-prod"
|
2023-04-11 16:48:36 -04:00
|
|
|
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")
|
2024-04-22 15:21:52 -04:00
|
|
|
w.Header().Set("TFP-AppName", "HCP Terraform")
|
2023-04-11 16:48:36 -04:00
|
|
|
},
|
|
|
|
|
}
|
2021-08-12 15:30:24 -04:00
|
|
|
)
|
|
|
|
|
|
2021-09-07 16:35:32 -04:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-12 15:30:24 -04:00
|
|
|
func testInput(t *testing.T, answers map[string]string) *mockInput {
|
|
|
|
|
return &mockInput{answers: answers}
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-20 17:54:41 -04:00
|
|
|
func testBackendWithName(t *testing.T) (*Cloud, func()) {
|
Modify tfe client mocks to meet some new requirements
- Add plausible unredacted plan json for `plan-json-{basic,full}` testdata --
Created by just running the relevant terraform commands locally.
- Add plan-json-no-changes testdata --
The unredacted json was organically grown, but I edited the log and redacted
json by hand to match what I observed from a real but unrelated
planned-and-finished run in TFC.
- Add plan-json-basic-no-unredacted testdata --
This mimics a lack of admin permissions, resulting in a 404.
- Hook up `MockPlans.ReadJSONOutput` to test fixtures, when present.
This method has been implemented for ages, and has had a backing store for
unredacted plan json, but has been effectively a no-op since nothing ever
fills that backing store. So, when creating a mock plan, make an attempt to
read unredacted json and stow it in the mocks on success.
- Make it possible to get the entire MockClient for a test backend
In order to test some things, I'm going to need to mess with the internal
state of runs and plans beyond what the go-tfe client API allows. I could add
magic special-casing to the mock API methods, or I could locate the
shenanigans next to the test that actually exploits it. The latter seems more
comprehensible, but I need access to the full mock client struct in order to
mess with its interior.
- Fill in some missing expectations around HasChanges when retrieving a run +
plan.
2023-06-28 19:47:08 -04:00
|
|
|
b, _, c := testBackendAndMocksWithName(t)
|
|
|
|
|
return b, c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testBackendAndMocksWithName(t *testing.T) (*Cloud, *MockClient, func()) {
|
2021-08-12 15:30:24 -04:00
|
|
|
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{
|
2023-08-01 16:43:07 -04:00
|
|
|
"name": cty.StringVal(testBackendSingleWorkspaceName),
|
|
|
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
|
|
|
"project": cty.NullVal(cty.String),
|
2021-08-12 15:30:24 -04:00
|
|
|
}),
|
|
|
|
|
})
|
2023-04-11 16:48:36 -04:00
|
|
|
return testBackend(t, obj, defaultTFCPing)
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
|
2021-09-15 01:04:15 -04:00
|
|
|
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{
|
2021-10-11 17:26:07 -04:00
|
|
|
"name": cty.NullVal(cty.String),
|
2021-09-15 01:04:15 -04:00
|
|
|
"tags": cty.SetVal(
|
|
|
|
|
[]cty.Value{
|
|
|
|
|
cty.StringVal("billing"),
|
|
|
|
|
},
|
|
|
|
|
),
|
2023-08-01 16:43:07 -04:00
|
|
|
"project": cty.NullVal(cty.String),
|
2021-09-15 01:04:15 -04:00
|
|
|
}),
|
|
|
|
|
})
|
Modify tfe client mocks to meet some new requirements
- Add plausible unredacted plan json for `plan-json-{basic,full}` testdata --
Created by just running the relevant terraform commands locally.
- Add plan-json-no-changes testdata --
The unredacted json was organically grown, but I edited the log and redacted
json by hand to match what I observed from a real but unrelated
planned-and-finished run in TFC.
- Add plan-json-basic-no-unredacted testdata --
This mimics a lack of admin permissions, resulting in a 404.
- Hook up `MockPlans.ReadJSONOutput` to test fixtures, when present.
This method has been implemented for ages, and has had a backing store for
unredacted plan json, but has been effectively a no-op since nothing ever
fills that backing store. So, when creating a mock plan, make an attempt to
read unredacted json and stow it in the mocks on success.
- Make it possible to get the entire MockClient for a test backend
In order to test some things, I'm going to need to mess with the internal
state of runs and plans beyond what the go-tfe client API allows. I could add
magic special-casing to the mock API methods, or I could locate the
shenanigans next to the test that actually exploits it. The latter seems more
comprehensible, but I need access to the full mock client struct in order to
mess with its interior.
- Fill in some missing expectations around HasChanges when retrieving a run +
plan.
2023-06-28 19:47:08 -04:00
|
|
|
b, _, c := testBackend(t, obj, nil)
|
|
|
|
|
return b, c
|
2021-09-15 01:04:15 -04:00
|
|
|
}
|
|
|
|
|
|
2024-10-31 14:29:23 -04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-12 15:30:24 -04:00
|
|
|
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{
|
2023-08-01 16:43:07 -04:00
|
|
|
"name": cty.StringVal(testBackendSingleWorkspaceName),
|
|
|
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
|
|
|
"project": cty.NullVal(cty.String),
|
2021-08-12 15:30:24 -04:00
|
|
|
}),
|
|
|
|
|
})
|
Modify tfe client mocks to meet some new requirements
- Add plausible unredacted plan json for `plan-json-{basic,full}` testdata --
Created by just running the relevant terraform commands locally.
- Add plan-json-no-changes testdata --
The unredacted json was organically grown, but I edited the log and redacted
json by hand to match what I observed from a real but unrelated
planned-and-finished run in TFC.
- Add plan-json-basic-no-unredacted testdata --
This mimics a lack of admin permissions, resulting in a 404.
- Hook up `MockPlans.ReadJSONOutput` to test fixtures, when present.
This method has been implemented for ages, and has had a backing store for
unredacted plan json, but has been effectively a no-op since nothing ever
fills that backing store. So, when creating a mock plan, make an attempt to
read unredacted json and stow it in the mocks on success.
- Make it possible to get the entire MockClient for a test backend
In order to test some things, I'm going to need to mess with the internal
state of runs and plans beyond what the go-tfe client API allows. I could add
magic special-casing to the mock API methods, or I could locate the
shenanigans next to the test that actually exploits it. The latter seems more
comprehensible, but I need access to the full mock client struct in order to
mess with its interior.
- Fill in some missing expectations around HasChanges when retrieving a run +
plan.
2023-06-28 19:47:08 -04:00
|
|
|
b, _, c := testBackend(t, obj, nil)
|
|
|
|
|
return b, c
|
2023-04-11 16:48:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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{
|
2023-08-01 16:43:07 -04:00
|
|
|
"name": cty.StringVal(testBackendSingleWorkspaceName),
|
|
|
|
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
|
|
|
|
"project": cty.NullVal(cty.String),
|
2023-04-11 16:48:36 -04:00
|
|
|
}),
|
|
|
|
|
})
|
Modify tfe client mocks to meet some new requirements
- Add plausible unredacted plan json for `plan-json-{basic,full}` testdata --
Created by just running the relevant terraform commands locally.
- Add plan-json-no-changes testdata --
The unredacted json was organically grown, but I edited the log and redacted
json by hand to match what I observed from a real but unrelated
planned-and-finished run in TFC.
- Add plan-json-basic-no-unredacted testdata --
This mimics a lack of admin permissions, resulting in a 404.
- Hook up `MockPlans.ReadJSONOutput` to test fixtures, when present.
This method has been implemented for ages, and has had a backing store for
unredacted plan json, but has been effectively a no-op since nothing ever
fills that backing store. So, when creating a mock plan, make an attempt to
read unredacted json and stow it in the mocks on success.
- Make it possible to get the entire MockClient for a test backend
In order to test some things, I'm going to need to mess with the internal
state of runs and plans beyond what the go-tfe client API allows. I could add
magic special-casing to the mock API methods, or I could locate the
shenanigans next to the test that actually exploits it. The latter seems more
comprehensible, but I need access to the full mock client struct in order to
mess with its interior.
- Fill in some missing expectations around HasChanges when retrieving a run +
plan.
2023-06-28 19:47:08 -04:00
|
|
|
b, _, c := testBackend(t, obj, handlers)
|
|
|
|
|
return b, c
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
|
2022-08-29 17:26:06 -04:00
|
|
|
func testCloudState(t *testing.T) *State {
|
2021-09-20 17:54:41 -04:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 15:30:24 -04:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
2025-09-04 06:14:35 -04:00
|
|
|
raw, sDiags := b.StateMgr(testBackendSingleWorkspaceName)
|
|
|
|
|
if sDiags.HasErrors() {
|
|
|
|
|
t.Fatalf("error: %v", sDiags.Err())
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
|
2022-08-29 17:26:06 -04:00
|
|
|
return raw.(*State)
|
2022-03-17 00:47:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
|
Modify tfe client mocks to meet some new requirements
- Add plausible unredacted plan json for `plan-json-{basic,full}` testdata --
Created by just running the relevant terraform commands locally.
- Add plan-json-no-changes testdata --
The unredacted json was organically grown, but I edited the log and redacted
json by hand to match what I observed from a real but unrelated
planned-and-finished run in TFC.
- Add plan-json-basic-no-unredacted testdata --
This mimics a lack of admin permissions, resulting in a 404.
- Hook up `MockPlans.ReadJSONOutput` to test fixtures, when present.
This method has been implemented for ages, and has had a backing store for
unredacted plan json, but has been effectively a no-op since nothing ever
fills that backing store. So, when creating a mock plan, make an attempt to
read unredacted json and stow it in the mocks on success.
- Make it possible to get the entire MockClient for a test backend
In order to test some things, I'm going to need to mess with the internal
state of runs and plans beyond what the go-tfe client API allows. I could add
magic special-casing to the mock API methods, or I could locate the
shenanigans next to the test that actually exploits it. The latter seems more
comprehensible, but I need access to the full mock client struct in order to
mess with its interior.
- Fill in some missing expectations around HasChanges when retrieving a run +
plan.
2023-06-28 19:47:08 -04:00
|
|
|
func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, *MockClient, func()) {
|
2023-04-11 16:48:36 -04:00
|
|
|
var s *httptest.Server
|
|
|
|
|
if handlers != nil {
|
2026-02-10 06:39:33 -05:00
|
|
|
s = TestServerWithHandlers(t, handlers)
|
2023-04-11 16:48:36 -04:00
|
|
|
} else {
|
2026-02-10 06:39:33 -05:00
|
|
|
s = TestServer(t)
|
2023-04-11 16:48:36 -04:00
|
|
|
}
|
2021-08-12 15:30:24 -04:00
|
|
|
b := New(testDisco(s))
|
|
|
|
|
|
|
|
|
|
// Configure the backend so the client is created.
|
|
|
|
|
newObj, valDiags := b.PrepareConfig(obj)
|
|
|
|
|
if len(valDiags) != 0 {
|
2021-10-08 00:42:41 -04:00
|
|
|
t.Fatalf("testBackend: backend.PrepareConfig() failed: %s", valDiags.ErrWithWarnings())
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
obj = newObj
|
|
|
|
|
|
|
|
|
|
confDiags := b.Configure(obj)
|
|
|
|
|
if len(confDiags) != 0 {
|
2021-10-08 00:42:41 -04:00
|
|
|
t.Fatalf("testBackend: backend.Configure() failed: %s", confDiags.ErrWithWarnings())
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get a new mock client.
|
2021-09-07 16:35:32 -04:00
|
|
|
mc := NewMockClient()
|
2021-08-12 15:30:24 -04:00
|
|
|
|
|
|
|
|
// 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
|
2022-11-29 22:31:10 -05:00
|
|
|
b.client.TaskStages = mc.TaskStages
|
2022-11-28 23:10:23 -05:00
|
|
|
b.client.PolicySetOutcomes = mc.PolicySetOutcomes
|
2021-08-12 15:30:24 -04:00
|
|
|
b.client.PolicyChecks = mc.PolicyChecks
|
2025-09-15 07:21:05 -04:00
|
|
|
b.client.QueryRuns = mc.QueryRuns
|
2021-08-12 15:30:24 -04:00
|
|
|
b.client.Runs = mc.Runs
|
2023-04-17 11:53:47 -04:00
|
|
|
b.client.RunEvents = mc.RunEvents
|
2021-08-12 15:30:24 -04:00
|
|
|
b.client.StateVersions = mc.StateVersions
|
2022-03-17 00:47:06 -04:00
|
|
|
b.client.StateVersionOutputs = mc.StateVersionOutputs
|
2021-08-12 15:30:24 -04:00
|
|
|
b.client.Variables = mc.Variables
|
|
|
|
|
b.client.Workspaces = mc.Workspaces
|
|
|
|
|
|
|
|
|
|
// Set local to a local test backend.
|
|
|
|
|
b.local = testLocalBackend(t, b)
|
2022-04-13 13:34:11 -04:00
|
|
|
b.input = true
|
2021-08-12 15:30:24 -04:00
|
|
|
|
2023-02-09 07:35:48 -05:00
|
|
|
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/"
|
|
|
|
|
|
2023-06-26 20:41:24 -04:00
|
|
|
readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) {
|
2023-02-09 07:35:48 -05:00
|
|
|
return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-12 15:30:24 -04:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
// Create the organization.
|
2023-02-09 07:35:48 -05:00
|
|
|
_, err = b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
|
2023-12-20 20:59:35 -05:00
|
|
|
Name: tfe.String(b.Organization),
|
2021-08-12 15:30:24 -04:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the default workspace if required.
|
2021-09-20 14:07:53 -04:00
|
|
|
if b.WorkspaceMapping.Name != "" {
|
2023-12-20 20:59:35 -05:00
|
|
|
_, err = b.client.Workspaces.Create(ctx, b.Organization, tfe.WorkspaceCreateOptions{
|
2021-09-20 14:07:53 -04:00
|
|
|
Name: tfe.String(b.WorkspaceMapping.Name),
|
2021-08-12 15:30:24 -04:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Modify tfe client mocks to meet some new requirements
- Add plausible unredacted plan json for `plan-json-{basic,full}` testdata --
Created by just running the relevant terraform commands locally.
- Add plan-json-no-changes testdata --
The unredacted json was organically grown, but I edited the log and redacted
json by hand to match what I observed from a real but unrelated
planned-and-finished run in TFC.
- Add plan-json-basic-no-unredacted testdata --
This mimics a lack of admin permissions, resulting in a 404.
- Hook up `MockPlans.ReadJSONOutput` to test fixtures, when present.
This method has been implemented for ages, and has had a backing store for
unredacted plan json, but has been effectively a no-op since nothing ever
fills that backing store. So, when creating a mock plan, make an attempt to
read unredacted json and stow it in the mocks on success.
- Make it possible to get the entire MockClient for a test backend
In order to test some things, I'm going to need to mess with the internal
state of runs and plans beyond what the go-tfe client API allows. I could add
magic special-casing to the mock API methods, or I could locate the
shenanigans next to the test that actually exploits it. The latter seems more
comprehensible, but I need access to the full mock client struct in order to
mess with its interior.
- Fill in some missing expectations around HasChanges when retrieving a run +
plan.
2023-06-28 19:47:08 -04:00
|
|
|
return b, mc, s.Close
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
|
2022-04-12 15:14:01 -04:00
|
|
|
// testUnconfiguredBackend is used for testing the configuration of the backend
|
|
|
|
|
// with the mock client
|
|
|
|
|
func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) {
|
2026-02-10 06:39:33 -05:00
|
|
|
s := TestServer(t)
|
2022-04-12 15:14:01 -04:00
|
|
|
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
|
2022-11-28 23:10:23 -05:00
|
|
|
b.client.PolicySetOutcomes = mc.PolicySetOutcomes
|
2022-04-12 15:14:01 -04:00
|
|
|
b.client.PolicyChecks = mc.PolicyChecks
|
2025-09-15 07:21:05 -04:00
|
|
|
b.client.QueryRuns = mc.QueryRuns
|
2022-04-12 15:14:01 -04:00
|
|
|
b.client.Runs = mc.Runs
|
2023-04-17 11:53:47 -04:00
|
|
|
b.client.RunEvents = mc.RunEvents
|
2022-04-12 15:14:01 -04:00
|
|
|
b.client.StateVersions = mc.StateVersions
|
2023-06-20 22:06:18 -04:00
|
|
|
b.client.StateVersionOutputs = mc.StateVersionOutputs
|
2022-04-12 15:14:01 -04:00
|
|
|
b.client.Variables = mc.Variables
|
|
|
|
|
b.client.Workspaces = mc.Workspaces
|
|
|
|
|
|
2023-02-09 07:35:48 -05:00
|
|
|
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/"
|
|
|
|
|
|
2023-06-26 20:41:24 -04:00
|
|
|
readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) {
|
2023-02-09 07:35:48 -05:00
|
|
|
return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-12 15:14:01 -04:00
|
|
|
// Set local to a local test backend.
|
|
|
|
|
b.local = testLocalBackend(t, b)
|
|
|
|
|
|
|
|
|
|
return b, s.Close
|
|
|
|
|
}
|
|
|
|
|
|
backendrun: Separate the types/etc for backends that support operations
We previously had all of the types and helpers for all kinds of backends
together in package backend. That kept things relatively simple, but it
also meant that the majority of backends that only deal with remote state
storage ended up still indirectly depending on the entire Terraform modules
runtime, configuration loader, etc, etc, which brings into scope a bunch
of external dependencies that the remote state backends don't really need.
Since backends that support operations are a rare exception, we'll move the
types and helpers for those into a separate package "backendrun", and
then the main package backend can have a much more modest set of types and,
more importantly, a modest set of dependencies on other packages in this
codebase.
This is part of an ongoing effort to reduce the exposure of Terraform Core
and CLI code to the remote backends and vice-versa, so that in the long
run we can more often treat them as separate for dependency maintenance
purposes.
2024-03-11 19:27:44 -04:00
|
|
|
func testLocalBackend(t *testing.T, cloud *Cloud) backendrun.OperationsBackend {
|
2021-08-12 15:30:24 -04:00
|
|
|
b := backendLocal.NewWithBackend(cloud)
|
|
|
|
|
|
|
|
|
|
// Add a test provider to the local backend.
|
2023-07-06 10:35:33 -04:00
|
|
|
p := backendLocal.TestLocalProvider(t, b, "null", providers.ProviderSchema{
|
2023-07-06 10:22:57 -04:00
|
|
|
ResourceTypes: map[string]providers.Schema{
|
2021-08-12 15:30:24 -04:00
|
|
|
"null_resource": {
|
2025-03-04 10:33:43 -05:00
|
|
|
Body: &configschema.Block{
|
2023-07-06 10:22:57 -04:00
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
|
"id": {Type: cty.String, Computed: true},
|
|
|
|
|
},
|
2021-08-12 15:30:24 -04:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
|
|
|
|
|
"id": cty.StringVal("yes"),
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 06:39:33 -05:00
|
|
|
// TestServer returns a started *httptest.Server used for local testing with the default set of
|
2021-10-08 00:42:41 -04:00
|
|
|
// request handlers.
|
2026-02-10 06:39:33 -05:00
|
|
|
func TestServer(t *testing.T) *httptest.Server {
|
|
|
|
|
return TestServerWithHandlers(t, testDefaultRequestHandlers)
|
2021-10-08 00:42:41 -04:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 06:39:33 -05:00
|
|
|
// TestServerWithHandlers returns a started *httptest.Server with the given set of request handlers
|
2021-10-08 00:42:41 -04:00
|
|
|
// overriding any default request handlers (testDefaultRequestHandlers).
|
2026-02-10 06:39:33 -05:00
|
|
|
func TestServerWithHandlers(t *testing.T, handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server {
|
2021-08-12 15:30:24 -04:00
|
|
|
mux := http.NewServeMux()
|
2021-10-08 00:42:41 -04:00
|
|
|
for route, handler := range handlers {
|
|
|
|
|
mux.HandleFunc(route, handler)
|
|
|
|
|
}
|
|
|
|
|
for route, handler := range testDefaultRequestHandlers {
|
|
|
|
|
if handlers[route] == nil {
|
|
|
|
|
mux.HandleFunc(route, handler)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 06:39:33 -05:00
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
|
2021-10-08 00:42:41 -04:00
|
|
|
return httptest.NewServer(mux)
|
|
|
|
|
}
|
2021-08-12 15:30:24 -04:00
|
|
|
|
2023-06-08 19:29:32 -04:00
|
|
|
func testServerWithSnapshotsEnabled(t *testing.T, enabled bool) *httptest.Server {
|
|
|
|
|
var serverURL string
|
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2023-05-24 20:25:28 -04:00
|
|
|
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
|
|
|
|
|
}
|
2023-06-08 19:29:32 -04:00
|
|
|
|
2023-05-24 20:25:28 -04:00
|
|
|
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",
|
2023-06-08 19:29:32 -04:00
|
|
|
"id": GenerateID("sv-"),
|
2023-05-24 20:25:28 -04:00
|
|
|
"attributes": map[string]any{
|
|
|
|
|
"hosted-state-download-url": serverURL + "/state-json",
|
2023-06-08 19:29:32 -04:00
|
|
|
"hosted-state-upload-url": serverURL + "/state-json",
|
2023-05-24 20:25:28 -04:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
fakeBodyRaw, err := json.Marshal(fakeBody)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-07 16:42:07 -04:00
|
|
|
w.Header().Set("content-type", tfe.ContentTypeJSONAPI)
|
2023-05-24 20:25:28 -04:00
|
|
|
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)
|
2023-06-08 19:29:32 -04:00
|
|
|
case "PUT":
|
|
|
|
|
t.Log("pretending to be Archivist")
|
2023-05-24 20:25:28 -04:00
|
|
|
default:
|
|
|
|
|
t.Fatal("don't know what API operation this was supposed to be")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
w.Write(fakeBodyRaw)
|
|
|
|
|
}))
|
2023-06-08 19:29:32 -04:00
|
|
|
serverURL = server.URL
|
|
|
|
|
return server
|
2023-05-24 20:25:28 -04:00
|
|
|
}
|
|
|
|
|
|
2021-10-08 00:42:41 -04:00
|
|
|
// 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){
|
2021-08-12 15:30:24 -04:00
|
|
|
// Respond to service discovery calls.
|
2021-10-08 00:42:41 -04:00
|
|
|
"/well-known/terraform.json": func(w http.ResponseWriter, r *http.Request) {
|
2021-08-12 15:30:24 -04:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
io.WriteString(w, `{
|
2021-10-08 00:42:41 -04:00
|
|
|
"tfe.v2": "/api/v2/",
|
2021-08-12 15:30:24 -04:00
|
|
|
}`)
|
2021-10-08 00:42:41 -04:00
|
|
|
},
|
2021-08-12 15:30:24 -04:00
|
|
|
|
|
|
|
|
// Respond to service version constraints calls.
|
2021-10-08 00:42:41 -04:00
|
|
|
"/v1/versions/": func(w http.ResponseWriter, r *http.Request) {
|
2021-08-12 15:30:24 -04:00
|
|
|
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)))
|
2021-10-08 00:42:41 -04:00
|
|
|
},
|
2021-08-12 15:30:24 -04:00
|
|
|
|
|
|
|
|
// Respond to pings to get the API version header.
|
2021-10-08 00:42:41 -04:00
|
|
|
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
|
2021-08-12 15:30:24 -04:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2021-10-08 00:42:41 -04:00
|
|
|
w.Header().Set("TFP-API-Version", "2.5")
|
|
|
|
|
},
|
2021-08-12 15:30:24 -04:00
|
|
|
|
|
|
|
|
// Respond to the initial query to read the hashicorp org entitlements.
|
2021-10-08 00:42:41 -04:00
|
|
|
"/api/v2/organizations/hashicorp/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
|
2021-08-12 15:30:24 -04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}`)
|
2021-10-08 00:42:41 -04:00
|
|
|
},
|
2021-08-12 15:30:24 -04:00
|
|
|
|
|
|
|
|
// Respond to the initial query to read the no-operations org entitlements.
|
2021-10-08 00:42:41 -04:00
|
|
|
"/api/v2/organizations/no-operations/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
|
2021-08-12 15:30:24 -04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}`)
|
2021-10-08 00:42:41 -04:00
|
|
|
},
|
2021-08-12 15:30:24 -04:00
|
|
|
|
|
|
|
|
// All tests that are assumed to pass will use the hashicorp organization,
|
|
|
|
|
// so for all other organization requests we will return a 404.
|
2021-10-08 00:42:41 -04:00
|
|
|
"/api/v2/organizations/": func(w http.ResponseWriter, r *http.Request) {
|
2021-08-12 15:30:24 -04:00
|
|
|
w.WriteHeader(404)
|
|
|
|
|
io.WriteString(w, `{
|
|
|
|
|
"errors": [
|
|
|
|
|
{
|
|
|
|
|
"status": "404",
|
|
|
|
|
"title": "not found"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}`)
|
2021-10-08 00:42:41 -04:00
|
|
|
},
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
|
2023-02-09 07:35:48 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-12 15:30:24 -04:00
|
|
|
// 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{}{
|
2021-10-08 00:42:41 -04:00
|
|
|
"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
|
2021-08-12 15:30:24 -04:00
|
|
|
}
|
|
|
|
|
d := disco.NewWithCredentialsSource(credsSrc)
|
|
|
|
|
d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
|
|
|
|
|
|
|
|
|
d.ForceHostServices(svchost.Hostname(defaultHostname), services)
|
|
|
|
|
d.ForceHostServices(svchost.Hostname("localhost"), services)
|
2023-03-06 09:32:03 -05:00
|
|
|
d.ForceHostServices(svchost.Hostname("nontfe.local"), nil)
|
2021-08-12 15:30:24 -04:00
|
|
|
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{}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 08:56:46 -05:00
|
|
|
// 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))
|
2021-08-12 15:30:24 -04:00
|
|
|
for _, v := range vs {
|
|
|
|
|
vars[v] = &unparsedVariableValue{
|
|
|
|
|
value: v,
|
|
|
|
|
source: s,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return vars
|
|
|
|
|
}
|