mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
7e2e834aff
The cloud backend, which communicates with TFC like APIs, can create runs which may have one more configuration parameters altered. These alterations are emitted as run-events on the run so that API clients can consume and display them to users. This commit adds a step in plan operation to query the run-events once a run is created and then emit specific run-event descriptions to the console as warnings for the user.
323 lines
9.0 KiB
Go
323 lines
9.0 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path"
|
|
"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/hashicorp/terraform/internal/backend"
|
|
"github.com/hashicorp/terraform/internal/cloud"
|
|
"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/remote"
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"github.com/hashicorp/terraform/version"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
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},
|
|
})
|
|
)
|
|
|
|
// 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 testBackendDefault(t *testing.T) (*Remote, 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("prod"),
|
|
"prefix": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
return testBackend(t, obj)
|
|
}
|
|
|
|
func testBackendNoDefault(t *testing.T) (*Remote, 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),
|
|
"prefix": cty.StringVal("my-app-"),
|
|
}),
|
|
})
|
|
return testBackend(t, obj)
|
|
}
|
|
|
|
func testBackendNoOperations(t *testing.T) (*Remote, 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("prod"),
|
|
"prefix": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
return testBackend(t, obj)
|
|
}
|
|
|
|
func testRemoteClient(t *testing.T) remote.Client {
|
|
b, bCleanup := testBackendDefault(t)
|
|
defer bCleanup()
|
|
|
|
raw, err := b.StateMgr(backend.DefaultStateName)
|
|
if err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
|
|
return raw.(*remote.State).Client
|
|
}
|
|
|
|
func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) {
|
|
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.Fatal(valDiags.ErrWithWarnings())
|
|
}
|
|
obj = newObj
|
|
|
|
confDiags := b.Configure(obj)
|
|
if len(confDiags) != 0 {
|
|
t.Fatal(confDiags.ErrWithWarnings())
|
|
}
|
|
|
|
// Get a new mock client.
|
|
mc := cloud.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.PolicyChecks = mc.PolicyChecks
|
|
b.client.Runs = mc.Runs
|
|
b.client.RunEvents = mc.RunEvents
|
|
b.client.StateVersions = mc.StateVersions
|
|
b.client.Variables = mc.Variables
|
|
b.client.Workspaces = mc.Workspaces
|
|
|
|
// Set local to a local test backend.
|
|
b.local = testLocalBackend(t, b)
|
|
|
|
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.workspace != "" {
|
|
_, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String(b.workspace),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
}
|
|
|
|
return b, s.Close
|
|
}
|
|
|
|
func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced {
|
|
b := backendLocal.NewWithBackend(remote)
|
|
|
|
// Add a test provider to the local backend.
|
|
p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{
|
|
ResourceTypes: map[string]*configschema.Block{
|
|
"null_resource": {
|
|
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 *httptest.Server used for local testing.
|
|
func testServer(t *testing.T) *httptest.Server {
|
|
mux := http.NewServeMux()
|
|
|
|
// Respond to service discovery calls.
|
|
mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
io.WriteString(w, `{
|
|
"state.v2": "/api/v2/",
|
|
"tfe.v2.1": "/api/v2/",
|
|
"versions.v1": "/v1/versions/"
|
|
}`)
|
|
})
|
|
|
|
// Respond to service version constraints calls.
|
|
mux.HandleFunc("/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.
|
|
mux.HandleFunc("/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")
|
|
})
|
|
|
|
// Respond to the initial query to read the hashicorp org entitlements.
|
|
mux.HandleFunc("/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.
|
|
mux.HandleFunc("/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.
|
|
mux.HandleFunc("/api/v2/organizations/", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(404)
|
|
io.WriteString(w, `{
|
|
"errors": [
|
|
{
|
|
"status": "404",
|
|
"title": "not found"
|
|
}
|
|
]
|
|
}`)
|
|
})
|
|
|
|
return httptest.NewServer(mux)
|
|
}
|
|
|
|
// 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{}{
|
|
"state.v2": fmt.Sprintf("%s/api/v2/", s.URL),
|
|
"tfe.v2.1": fmt.Sprintf("%s/api/v2/", s.URL),
|
|
"versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL),
|
|
}
|
|
d := disco.NewWithCredentialsSource(credsSrc)
|
|
d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
|
|
|
d.ForceHostServices(svchost.Hostname(defaultHostname), services)
|
|
d.ForceHostServices(svchost.Hostname("localhost"), services)
|
|
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 backend.UnparsedVariableValue used for testing.
|
|
func testVariables(s terraform.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
|
|
}
|