mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06: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.
This commit is contained in:
parent
ed27fa096e
commit
7f6b827987
@ -650,7 +650,7 @@ func TestCloud_setUnavailableTerraformVersion(t *testing.T) {
|
||||
}),
|
||||
})
|
||||
|
||||
b, bCleanup := testBackend(t, config, nil)
|
||||
b, _, bCleanup := testBackend(t, config, nil)
|
||||
defer bCleanup()
|
||||
|
||||
// Make sure the workspace doesn't exist yet -- otherwise, we can't test what
|
||||
|
1
internal/cloud/testdata/plan-json-basic-no-unredacted/main.tf
vendored
Normal file
1
internal/cloud/testdata/plan-json-basic-no-unredacted/main.tf
vendored
Normal file
@ -0,0 +1 @@
|
||||
resource "null_resource" "foo" {}
|
116
internal/cloud/testdata/plan-json-basic-no-unredacted/plan-redacted.json
vendored
Normal file
116
internal/cloud/testdata/plan-json-basic-no-unredacted/plan-redacted.json
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"plan_format_version": "1.1",
|
||||
"resource_drift": [],
|
||||
"resource_changes": [
|
||||
{
|
||||
"address": "null_resource.foo",
|
||||
"mode": "managed",
|
||||
"type": "null_resource",
|
||||
"name": "foo",
|
||||
"provider_name": "registry.terraform.io/hashicorp/null",
|
||||
"change": {
|
||||
"actions": [
|
||||
"create"
|
||||
],
|
||||
"before": null,
|
||||
"after": {
|
||||
"triggers": null
|
||||
},
|
||||
"after_unknown": {
|
||||
"id": true
|
||||
},
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"relevant_attributes": [],
|
||||
"output_changes": {},
|
||||
"provider_schemas": {
|
||||
"registry.terraform.io/hashicorp/null": {
|
||||
"provider": {
|
||||
"version": 0,
|
||||
"block": {
|
||||
"description_kind": "plain"
|
||||
}
|
||||
},
|
||||
"resource_schemas": {
|
||||
"null_resource": {
|
||||
"version": 0,
|
||||
"block": {
|
||||
"attributes": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "This is set to a random value at create time.",
|
||||
"description_kind": "plain",
|
||||
"computed": true
|
||||
},
|
||||
"triggers": {
|
||||
"type": [
|
||||
"map",
|
||||
"string"
|
||||
],
|
||||
"description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.",
|
||||
"description_kind": "plain",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.",
|
||||
"description_kind": "plain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"data_source_schemas": {
|
||||
"null_data_source": {
|
||||
"version": 0,
|
||||
"block": {
|
||||
"attributes": {
|
||||
"has_computed_default": {
|
||||
"type": "string",
|
||||
"description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.",
|
||||
"description_kind": "plain",
|
||||
"optional": true,
|
||||
"computed": true
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.",
|
||||
"description_kind": "plain",
|
||||
"deprecated": true,
|
||||
"computed": true
|
||||
},
|
||||
"inputs": {
|
||||
"type": [
|
||||
"map",
|
||||
"string"
|
||||
],
|
||||
"description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.",
|
||||
"description_kind": "plain",
|
||||
"optional": true
|
||||
},
|
||||
"outputs": {
|
||||
"type": [
|
||||
"map",
|
||||
"string"
|
||||
],
|
||||
"description": "After the data source is \"read\", a copy of the `inputs` map.",
|
||||
"description_kind": "plain",
|
||||
"computed": true
|
||||
},
|
||||
"random": {
|
||||
"type": "string",
|
||||
"description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.",
|
||||
"description_kind": "plain",
|
||||
"computed": true
|
||||
}
|
||||
},
|
||||
"description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://www.terraform.io/docs/language/values/locals.html).\n",
|
||||
"description_kind": "plain",
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"provider_format_version": "1.0"
|
||||
}
|
3
internal/cloud/testdata/plan-json-basic-no-unredacted/plan.log
vendored
Normal file
3
internal/cloud/testdata/plan-json-basic-no-unredacted/plan.log
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.409143-05:00","terraform":"1.3.7","type":"version","ui":"1.0"}
|
||||
{"@level":"info","@message":"null_resource.foo: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605841-05:00","change":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"}
|
||||
{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605906-05:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}
|
1
internal/cloud/testdata/plan-json-basic/plan-unredacted.json
vendored
Normal file
1
internal/cloud/testdata/plan-json-basic/plan-unredacted.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"format_version":"1.1","terraform_version":"1.4.4","planned_values":{"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":null},"sensitive_values":{}}]}},"resource_changes":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true},"before_sensitive":false,"after_sensitive":{}}}],"configuration":{"provider_config":{"null":{"name":"null","full_name":"registry.terraform.io/hashicorp/null"}},"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","schema_version":0}]}}}
|
1
internal/cloud/testdata/plan-json-full/plan-unredacted.json
vendored
Normal file
1
internal/cloud/testdata/plan-json-full/plan-unredacted.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
internal/cloud/testdata/plan-json-no-changes/main.tf
vendored
Normal file
1
internal/cloud/testdata/plan-json-no-changes/main.tf
vendored
Normal file
@ -0,0 +1 @@
|
||||
resource "null_resource" "foo" {}
|
118
internal/cloud/testdata/plan-json-no-changes/plan-redacted.json
vendored
Normal file
118
internal/cloud/testdata/plan-json-no-changes/plan-redacted.json
vendored
Normal file
@ -0,0 +1,118 @@
|
||||
{
|
||||
"plan_format_version": "1.1",
|
||||
"resource_drift": [],
|
||||
"resource_changes": [
|
||||
{
|
||||
"address": "null_resource.foo",
|
||||
"mode": "managed",
|
||||
"type": "null_resource",
|
||||
"name": "foo",
|
||||
"provider_name": "registry.terraform.io/hashicorp/null",
|
||||
"change": {
|
||||
"actions": [
|
||||
"no-op"
|
||||
],
|
||||
"before": {
|
||||
"id": "3549869958859575216",
|
||||
"triggers": null
|
||||
},
|
||||
"after": {
|
||||
"id": "3549869958859575216",
|
||||
"triggers": null
|
||||
},
|
||||
"after_unknown": {},
|
||||
"before_sensitive": {},
|
||||
"after_sensitive": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"relevant_attributes": [],
|
||||
"output_changes": {},
|
||||
"provider_schemas": {
|
||||
"registry.terraform.io/hashicorp/null": {
|
||||
"provider": {
|
||||
"version": 0,
|
||||
"block": {
|
||||
"description_kind": "plain"
|
||||
}
|
||||
},
|
||||
"resource_schemas": {
|
||||
"null_resource": {
|
||||
"version": 0,
|
||||
"block": {
|
||||
"attributes": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "This is set to a random value at create time.",
|
||||
"description_kind": "plain",
|
||||
"computed": true
|
||||
},
|
||||
"triggers": {
|
||||
"type": [
|
||||
"map",
|
||||
"string"
|
||||
],
|
||||
"description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.",
|
||||
"description_kind": "plain",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.",
|
||||
"description_kind": "plain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"data_source_schemas": {
|
||||
"null_data_source": {
|
||||
"version": 0,
|
||||
"block": {
|
||||
"attributes": {
|
||||
"has_computed_default": {
|
||||
"type": "string",
|
||||
"description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.",
|
||||
"description_kind": "plain",
|
||||
"optional": true,
|
||||
"computed": true
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.",
|
||||
"description_kind": "plain",
|
||||
"deprecated": true,
|
||||
"computed": true
|
||||
},
|
||||
"inputs": {
|
||||
"type": [
|
||||
"map",
|
||||
"string"
|
||||
],
|
||||
"description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.",
|
||||
"description_kind": "plain",
|
||||
"optional": true
|
||||
},
|
||||
"outputs": {
|
||||
"type": [
|
||||
"map",
|
||||
"string"
|
||||
],
|
||||
"description": "After the data source is \"read\", a copy of the `inputs` map.",
|
||||
"description_kind": "plain",
|
||||
"computed": true
|
||||
},
|
||||
"random": {
|
||||
"type": "string",
|
||||
"description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.",
|
||||
"description_kind": "plain",
|
||||
"computed": true
|
||||
}
|
||||
},
|
||||
"description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://www.terraform.io/docs/language/values/locals.html).\n",
|
||||
"description_kind": "plain",
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"provider_format_version": "1.0"
|
||||
}
|
1
internal/cloud/testdata/plan-json-no-changes/plan-unredacted.json
vendored
Normal file
1
internal/cloud/testdata/plan-json-no-changes/plan-unredacted.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"format_version":"1.1","terraform_version":"1.4.4","planned_values":{"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"3549869958859575216","triggers":null},"sensitive_values":{}}]}},"resource_changes":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["no-op"],"before":{"id":"3549869958859575216","triggers":null},"after":{"id":"3549869958859575216","triggers":null},"after_unknown":{},"before_sensitive":{},"after_sensitive":{}}}],"prior_state":{"format_version":"1.0","terraform_version":"1.4.4","values":{"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"3549869958859575216","triggers":null},"sensitive_values":{}}]}}},"configuration":{"provider_config":{"null":{"name":"null","full_name":"registry.terraform.io/hashicorp/null"}},"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","schema_version":0}]}}}
|
2
internal/cloud/testdata/plan-json-no-changes/plan.log
vendored
Normal file
2
internal/cloud/testdata/plan-json-no-changes/plan.log
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.409143-05:00","terraform":"1.3.7","type":"version","ui":"1.0"}
|
||||
{"@level":"info","@message":"Plan: 0 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605906-05:00","changes":{"add":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}
|
@ -83,6 +83,11 @@ func testInput(t *testing.T, answers map[string]string) *mockInput {
|
||||
}
|
||||
|
||||
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"),
|
||||
@ -109,7 +114,8 @@ func testBackendWithTags(t *testing.T) (*Cloud, func()) {
|
||||
),
|
||||
}),
|
||||
})
|
||||
return testBackend(t, obj, nil)
|
||||
b, _, c := testBackend(t, obj, nil)
|
||||
return b, c
|
||||
}
|
||||
|
||||
func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
|
||||
@ -122,7 +128,8 @@ func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
}),
|
||||
})
|
||||
return testBackend(t, obj, nil)
|
||||
b, _, c := testBackend(t, obj, nil)
|
||||
return b, c
|
||||
}
|
||||
|
||||
func testBackendWithHandlers(t *testing.T, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, func()) {
|
||||
@ -135,7 +142,8 @@ func testBackendWithHandlers(t *testing.T, handlers map[string]func(http.Respons
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
}),
|
||||
})
|
||||
return testBackend(t, obj, handlers)
|
||||
b, _, c := testBackend(t, obj, handlers)
|
||||
return b, c
|
||||
}
|
||||
|
||||
func testCloudState(t *testing.T) *State {
|
||||
@ -212,7 +220,7 @@ func testBackendWithOutputs(t *testing.T) (*Cloud, func()) {
|
||||
return b, cleanup
|
||||
}
|
||||
|
||||
func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, func()) {
|
||||
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(handlers)
|
||||
@ -287,7 +295,7 @@ func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.Resp
|
||||
}
|
||||
}
|
||||
|
||||
return b, s.Close
|
||||
return b, mc, s.Close
|
||||
}
|
||||
|
||||
// testUnconfiguredBackend is used for testing the configuration of the backend
|
||||
|
@ -513,7 +513,7 @@ func (m *MockRedactedPlans) Read(ctx context.Context, hostname, token, planID st
|
||||
type MockPlans struct {
|
||||
client *MockClient
|
||||
logs map[string]string
|
||||
planOutputs map[string]string
|
||||
planOutputs map[string][]byte
|
||||
plans map[string]*tfe.Plan
|
||||
}
|
||||
|
||||
@ -521,7 +521,7 @@ func newMockPlans(client *MockClient) *MockPlans {
|
||||
return &MockPlans{
|
||||
client: client,
|
||||
logs: make(map[string]string),
|
||||
planOutputs: make(map[string]string),
|
||||
planOutputs: make(map[string][]byte),
|
||||
plans: make(map[string]*tfe.Plan),
|
||||
}
|
||||
}
|
||||
@ -548,6 +548,17 @@ func (m *MockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) {
|
||||
w.WorkingDirectory,
|
||||
"plan.log",
|
||||
)
|
||||
|
||||
// Try to load unredacted json output, if it exists
|
||||
outputPath := filepath.Join(
|
||||
m.client.ConfigurationVersions.uploadPaths[cvID],
|
||||
w.WorkingDirectory,
|
||||
"plan-unredacted.json",
|
||||
)
|
||||
if outBytes, err := os.ReadFile(outputPath); err == nil {
|
||||
m.planOutputs[p.ID] = outBytes
|
||||
}
|
||||
|
||||
m.plans[p.ID] = p
|
||||
|
||||
return p, nil
|
||||
@ -608,7 +619,7 @@ func (m *MockPlans) ReadJSONOutput(ctx context.Context, planID string) ([]byte,
|
||||
return nil, tfe.ErrResourceNotFound
|
||||
}
|
||||
|
||||
return []byte(planOutput), nil
|
||||
return planOutput, nil
|
||||
}
|
||||
|
||||
type MockTaskStages struct {
|
||||
@ -1101,7 +1112,7 @@ func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, options *t
|
||||
}
|
||||
|
||||
logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL])
|
||||
if r.Status == tfe.RunPlanning && r.Plan.Status == tfe.PlanFinished {
|
||||
if (r.Status == tfe.RunPlanning || r.Status == tfe.RunPlannedAndSaved) && r.Plan.Status == tfe.PlanFinished {
|
||||
hasChanges := r.IsDestroy ||
|
||||
bytes.Contains(logs, []byte("1 to add")) ||
|
||||
bytes.Contains(logs, []byte("1 to change")) ||
|
||||
@ -1110,6 +1121,7 @@ func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, options *t
|
||||
r.Actions.IsCancelable = false
|
||||
r.Actions.IsConfirmable = true
|
||||
r.HasChanges = true
|
||||
r.Plan.HasChanges = true
|
||||
r.Permissions.CanApply = true
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user