From 4ce385a19b93cf7f1b7780d9b2d3cadc5d0ddb31 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Fri, 10 Nov 2023 10:07:04 +0100 Subject: [PATCH] testing framework: implement overrides in terraform graph (#34169) * testing framework: implement overrides in terraform graph * fix bug for modules and module instances * add test for ModuleInstance.ContainingModule() --- internal/addrs/module_instance.go | 12 + internal/addrs/module_instance_test.go | 41 ++ internal/moduletest/mocking/overrides.go | 204 ++++++ internal/moduletest/mocking/overrides_test.go | 119 ++++ internal/moduletest/mocking/testing.go | 29 + internal/plans/plan.go | 8 + internal/terraform/context_apply.go | 1 + .../terraform/context_apply_overrides_test.go | 614 ++++++++++++++++++ internal/terraform/context_plan.go | 7 + internal/terraform/context_walk.go | 6 + internal/terraform/eval_context.go | 5 + internal/terraform/eval_context_builtin.go | 6 + internal/terraform/eval_context_mock.go | 9 + internal/terraform/graph.go | 103 +++ internal/terraform/graph_walk_context.go | 3 + internal/terraform/node_output.go | 59 +- internal/terraform/node_overridable.go | 18 + internal/terraform/node_resource_abstract.go | 1 + .../node_resource_abstract_instance.go | 231 +++++-- internal/terraform/terraform_test.go | 8 + 20 files changed, 1393 insertions(+), 91 deletions(-) create mode 100644 internal/moduletest/mocking/overrides.go create mode 100644 internal/moduletest/mocking/overrides_test.go create mode 100644 internal/moduletest/mocking/testing.go create mode 100644 internal/terraform/context_apply_overrides_test.go create mode 100644 internal/terraform/node_overridable.go diff --git a/internal/addrs/module_instance.go b/internal/addrs/module_instance.go index d2a2c2e417..690e14fd24 100644 --- a/internal/addrs/module_instance.go +++ b/internal/addrs/module_instance.go @@ -504,6 +504,18 @@ func (m ModuleInstance) Module() Module { return ret } +// ContainingModule returns the address of the module instance as if the last +// step wasn't instanced. For example, it turns module.child[0] into +// module.child and module[0].child[0] into module[0].child. +func (m ModuleInstance) ContainingModule() ModuleInstance { + if len(m) == 0 { + return nil + } + + ret := m.Parent() + return ret.Child(m[len(m)-1].Name, NoKey) +} + func (m ModuleInstance) AddrType() TargetableAddrType { return ModuleInstanceAddrType } diff --git a/internal/addrs/module_instance_test.go b/internal/addrs/module_instance_test.go index 0014522dc5..bcf809e013 100644 --- a/internal/addrs/module_instance_test.go +++ b/internal/addrs/module_instance_test.go @@ -164,6 +164,47 @@ func TestModuleInstance_IsDeclaredByCall(t *testing.T) { } } +func TestModuleInstance_ContainingModule(t *testing.T) { + tcs := map[string]struct { + module string + expected string + }{ + "no_instances": { + module: "module.parent.module.child", + expected: "module.parent.module.child", + }, + "last_instance": { + module: "module.parent.module.child[0]", + expected: "module.parent.module.child", + }, + "middle_instance": { + module: "module.parent[0].module.child", + expected: "module.parent[0].module.child", + }, + "all_instances": { + module: "module.parent[0].module.child[0]", + expected: "module.parent[0].module.child", + }, + "single_no_instance": { + module: "module.parent", + expected: "module.parent", + }, + "single_instance": { + module: "module.parent[0]", + expected: "module.parent", + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + module := mustParseModuleInstanceStr(tc.module) + actual, expected := module.ContainingModule().String(), tc.expected + if actual != expected { + t.Errorf("expected: %s\nactual: %s", expected, actual) + } + }) + } +} + func mustParseModuleInstanceStr(str string) ModuleInstance { mi, diags := ParseModuleInstanceStr(str) if diags.HasErrors() { diff --git a/internal/moduletest/mocking/overrides.go b/internal/moduletest/mocking/overrides.go new file mode 100644 index 0000000000..3f6ed5bfa0 --- /dev/null +++ b/internal/moduletest/mocking/overrides.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +// Overrides contains a summary of all the overrides that should apply for a +// test run. +// +// This requires us to deduplicate between run blocks and test files, and mock +// providers. +type Overrides struct { + providerOverrides map[string]addrs.Map[addrs.Targetable, *configs.Override] + localOverrides addrs.Map[addrs.Targetable, *configs.Override] +} + +func PackageOverrides(run *configs.TestRun, file *configs.TestFile, config *configs.Config) *Overrides { + overrides := &Overrides{ + providerOverrides: make(map[string]addrs.Map[addrs.Targetable, *configs.Override]), + localOverrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + + // The run block overrides have the highest priority, we always include all + // of them. + for _, elem := range run.Overrides.Elems { + overrides.localOverrides.PutElement(elem) + } + + // The file overrides are second, we include these as long as there isn't + // a direct replacement in the current run block or the run block doesn't + // override an entire module that a file override would be inside. + for _, elem := range file.Overrides.Elems { + target := elem.Key + + if overrides.localOverrides.Has(target) { + // The run block provided a value already. + continue + } + + overrides.localOverrides.PutElement(elem) + } + + // Finally, we want to include the overrides for any mock providers we have. + for key, provider := range config.Module.ProviderConfigs { + if !provider.Mock { + // Only mock providers can supply overrides. + continue + } + + for _, elem := range provider.MockData.Overrides.Elems { + target := elem.Key + + if overrides.localOverrides.Has(target) { + // Then the file or the run block is providing an override with + // higher precedence. + continue + } + + if _, exists := overrides.providerOverrides[key]; !exists { + overrides.providerOverrides[key] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + } + overrides.providerOverrides[key].PutElement(elem) + } + } + + return overrides +} + +// IsOverridden returns true if the module is either overridden directly or +// nested within another module that is already being overridden. +// +// For this function, we know that overrides defined within mock providers +// cannot target modules directly. Therefore, we only need to check the local +// overrides within this function. +func (overrides *Overrides) IsOverridden(module addrs.ModuleInstance) bool { + if overrides.localOverrides.Has(module) { + // Short circuit things, if we have an exact match just return now. + return true + } + + // Otherwise, check for parents. + for _, elem := range overrides.localOverrides.Elems { + if elem.Key.TargetContains(module) { + // Then we have an ancestor of module being overridden instead of + // module being overridden directly. + return true + } + } + + return false +} + +// IsDeeplyOverridden returns true if an ancestor of this module is overridden +// but not if the module is overridden directly. +// +// This function doesn't consider an instanced module to be deeply overridden +// by the uninstanced reference to the same module. So, +// IsDeeplyOverridden("mod.child[0]") would return false if "mod.child" has been +// overridden. +// +// For this function, we know that overrides defined within mock providers +// cannot target modules directly. Therefore, we only need to check the local +// overrides within this function. +func (overrides *Overrides) IsDeeplyOverridden(module addrs.ModuleInstance) bool { + for _, elem := range overrides.localOverrides.Elems { + target := elem.Key + + if target.TargetContains(module) { + // So we do think it contains it, but it could be matching here + // because of equality or because we have an instanced module. + if instance, ok := target.(addrs.ModuleInstance); ok { + if instance.Equal(module) { + // Then we're exactly equal, so not deeply nested. + continue + } + + if instance.Module().Equal(module.Module()) { + // Then we're an instanced version of they other one, so + // also not deeply nested by our definition of deeply. + continue + } + + } + + // Otherwise, it's deeply nested. + return true + } + } + return false +} + +// GetOverrideInclProviders retrieves the override for target if it exists. +// +// This function also checks the provider specific overrides using the provider +// argument. +func (overrides *Overrides) GetOverrideInclProviders(target addrs.Targetable, provider addrs.AbsProviderConfig) (*configs.Override, bool) { + // If we have a local override, then apply that first. + if override, ok := overrides.GetOverride(target); ok { + return override, true + } + + // Otherwise, check if we have overrides for this provider. + providerOverrides, ok := overrides.ProviderMatch(provider) + if ok { + if override, ok := providerOverrides.GetOk(target); ok { + return override, true + } + } + + // If we have no overrides, that's okay. + return nil, false +} + +// GetOverride retrieves the override for target from the local overrides if +// it exists. +func (overrides *Overrides) GetOverride(target addrs.Targetable) (*configs.Override, bool) { + return overrides.localOverrides.GetOk(target) +} + +// ProviderMatch returns true if we have overrides for the given provider. +// +// This is so that we can selectively apply overrides to resources that are +// being supplied by a given provider. +func (overrides *Overrides) ProviderMatch(provider addrs.AbsProviderConfig) (addrs.Map[addrs.Targetable, *configs.Override], bool) { + if !provider.Module.IsRoot() { + // We can only set mock providers within the root module. + return addrs.Map[addrs.Targetable, *configs.Override]{}, false + } + + name := provider.Provider.Type + if len(provider.Alias) > 0 { + name = fmt.Sprintf("%s.%s", name, provider.Alias) + } + + data, exists := overrides.providerOverrides[name] + return data, exists +} + +// Empty returns true if we have no actual overrides. +// +// This is just a convenience function to make checking for overrides easier. +func (overrides *Overrides) Empty() bool { + if overrides == nil { + return true + } + + if overrides.localOverrides.Len() > 0 { + return false + } + + for _, value := range overrides.providerOverrides { + if value.Len() > 0 { + return false + } + } + + return true +} diff --git a/internal/moduletest/mocking/overrides_test.go b/internal/moduletest/mocking/overrides_test.go new file mode 100644 index 0000000000..7196f986e3 --- /dev/null +++ b/internal/moduletest/mocking/overrides_test.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +func TestPackageOverrides(t *testing.T) { + mustResourceInstance := func(s string) addrs.AbsResourceInstance { + addr, diags := addrs.ParseAbsResourceInstanceStr(s) + if len(diags) > 0 { + t.Fatal(diags) + } + return addr + } + + primary := mustResourceInstance("test_instance.primary") + secondary := mustResourceInstance("test_instance.secondary") + tertiary := mustResourceInstance("test_instance.tertiary") + + testrun := mustResourceInstance("test_instance.test_run") + testfile := mustResourceInstance("test_instance.test_file") + provider := mustResourceInstance("test_instance.provider") + + // Add a single override to the test run. + run := &configs.TestRun{ + Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + run.Overrides.Put(primary, &configs.Override{ + Target: &addrs.Target{ + Subject: testrun, + }, + }) + + // Add a unique item to the test file, and duplicate the test run data. + file := &configs.TestFile{ + Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + file.Overrides.Put(primary, &configs.Override{ + Target: &addrs.Target{ + Subject: testfile, + }, + }) + file.Overrides.Put(secondary, &configs.Override{ + Target: &addrs.Target{ + Subject: testfile, + }, + }) + + // Add all data from the file and run block are duplicating here, and then + // a unique one. + config := &configs.Config{ + Module: &configs.Module{ + ProviderConfigs: map[string]*configs.Provider{ + "mock": { + Mock: true, + MockData: &configs.MockData{ + Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + }, + }, + "real": {}, + }, + }, + } + config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(primary, &configs.Override{ + Target: &addrs.Target{ + Subject: provider, + }, + }) + config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(secondary, &configs.Override{ + Target: &addrs.Target{ + Subject: provider, + }, + }) + config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(tertiary, &configs.Override{ + Target: &addrs.Target{ + Subject: provider, + }, + }) + + overrides := PackageOverrides(run, file, config) + + // We now expect that the run and file overrides took precedence. + first, pOk := overrides.GetOverride(primary) + second, sOk := overrides.GetOverride(secondary) + third, tOk := overrides.GetOverrideInclProviders(tertiary, addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Type: "mock", + }, + }) + + if !pOk || !sOk || !tOk { + t.Fatalf("expected to find all overrides, but got %t %t %t", pOk, sOk, tOk) + } + + if !first.Target.Subject.(addrs.AbsResourceInstance).Equal(testrun) { + t.Errorf("expected %s but got %s for primary", testrun, first.Target.Subject) + } + + if !second.Target.Subject.(addrs.AbsResourceInstance).Equal(testfile) { + t.Errorf("expected %s but got %s for primary", testfile, second.Target.Subject) + } + + if !third.Target.Subject.(addrs.AbsResourceInstance).Equal(provider) { + t.Errorf("expected %s but got %s for primary", provider, third.Target.Subject) + } + + // Also, final sanity check. + _, ok := overrides.providerOverrides["real"] + if ok { + t.Errorf("shouldn't have stored the real provider but did") + } + +} diff --git a/internal/moduletest/mocking/testing.go b/internal/moduletest/mocking/testing.go new file mode 100644 index 0000000000..6ec498f315 --- /dev/null +++ b/internal/moduletest/mocking/testing.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +type InitProviderOverrides func(map[string]addrs.Map[addrs.Targetable, *configs.Override]) +type InitLocalOverrides func(addrs.Map[addrs.Targetable, *configs.Override]) + +func OverridesForTesting(providers InitProviderOverrides, locals InitLocalOverrides) *Overrides { + overrides := &Overrides{ + providerOverrides: make(map[string]addrs.Map[addrs.Targetable, *configs.Override]), + localOverrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + + if providers != nil { + providers(overrides.providerOverrides) + } + + if locals != nil { + locals(overrides.localOverrides) + } + + return overrides +} diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 18ef1a3642..1d1873344f 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/globalref" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/states" ) @@ -112,6 +113,13 @@ type Plan struct { // representation of the plan. ExternalReferences []*addrs.Reference + // Overrides contains the set of overrides that were applied while making + // this plan. We need to provide the same set of overrides when applying + // the plan so we preserve them here. As with PlannedState and + // ExternalReferences, this is only used by the testing framework and so + // isn't written into any external representation of the plan. + Overrides *mocking.Overrides + // Timestamp is the record of truth for when the plan happened. Timestamp time.Time } diff --git a/internal/terraform/context_apply.go b/internal/terraform/context_apply.go index 68c3c5460a..5baa19a7c7 100644 --- a/internal/terraform/context_apply.go +++ b/internal/terraform/context_apply.go @@ -62,6 +62,7 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State Config: config, InputState: workingState, Changes: plan.Changes, + Overrides: plan.Overrides, // We need to propagate the check results from the plan phase, // because that will tell us which checkable objects we're expecting diff --git a/internal/terraform/context_apply_overrides_test.go b/internal/terraform/context_apply_overrides_test.go new file mode 100644 index 0000000000..217e1f7b71 --- /dev/null +++ b/internal/terraform/context_apply_overrides_test.go @@ -0,0 +1,614 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" +) + +// This file contains 'integration' tests for the Terraform test overrides +// functionality. +// +// These tests could live in context_apply_test or context_apply2_test but given +// the size of those files, it makes sense to keep these tests grouped together. + +func TestContextOverrides(t *testing.T) { + + // The approach to the testing here, is to create some configuration that + // would panic if executed normally because of the underlying provider. + // + // We then write overrides that make sure the underlying provider is never + // called. + // + // We then run a plan, apply, refresh, destroy sequence that tests all the + // potential function calls to the underlying provider to make sure we + // have covered everything. + // + // Finally, we validate some expected values after the apply stage to make + // sure the overrides are returning the values we want them to. + + tcs := map[string]struct { + configs map[string]string + overrides *mocking.Overrides + outputs cty.Value + }{ + "resource": { + configs: map[string]string{ + "main.tf": ` +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +}`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustResourceInstanceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "resource_from_provider": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +}`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "selectively_applies_provider": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +provider "test" { + alias = "secondary" +} + +resource "test_instance" "primary" { + value = "primary" +} + +resource "test_instance" "secondary" { + provider = test.secondary + value = "secondary" +} + +output "primary_value" { + value = test_instance.primary.value +} + +output "primary_id" { + value = test_instance.primary.id +} + +output "secondary_value" { + value = test_instance.secondary.value +} + +output "secondary_id" { + value = test_instance.secondary.id +}`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test.secondary"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + // Test should not apply this override, as this provider is + // not being used for this resource. + overrides["test.secondary"].Put(mustResourceInstanceAddr("test_instance.primary"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("primary_id"), + }), + }) + overrides["test.secondary"].Put(mustResourceInstanceAddr("test_instance.secondary"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("secondary_id"), + }), + }) + }, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustResourceInstanceAddr("test_instance.primary"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "primary_id": cty.StringVal("h3ll0"), + "primary_value": cty.StringVal("primary"), + "secondary_id": cty.StringVal("secondary_id"), + "secondary_value": cty.StringVal("secondary"), + }), + }, + "propagates_provider_to_modules_explicit": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + source = "./mod" + + providers = { + test = test + } +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +provider "test" {} + +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} +`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("module.mod.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "propagates_provider_to_modules_implicit": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + source = "./mod" +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("module.mod.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "data_source": { + configs: map[string]string{ + "main.tf": ` +data "test_instance" "instance" { + id = "data-source" +} + +resource "test_instance" "instance" { + value = data.test_instance.instance.value +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +}`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustResourceInstanceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + overrides.Put(mustResourceInstanceAddr("data.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "module": { + configs: map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +resource "test_instance" "instance" { + value = "random" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +check "value" { + assert { + condition = test_instance.instance.value == "definitely wrong" + error_message = "bad value" + } +} +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance("module.mod"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "provider_type_override": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + source = "./mod" +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +terraform { + required_providers { + replaced = { + source = "hashicorp/test" + } + } +} + +resource "test_instance" "instance" { + provider = replaced + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("module.mod.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "resource_instance_overrides": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +resource "test_instance" "instance" { + count = 3 + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.*.value +} + +output "id" { + value = test_instance.instance.*.id +}`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustAbsResourceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("generic"), + }), + }) + overrides.Put(mustResourceInstanceAddr("test_instance.instance[1]"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("specific"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.TupleVal([]cty.Value{ + cty.StringVal("generic"), + cty.StringVal("specific"), + cty.StringVal("generic"), + }), + "value": cty.TupleVal([]cty.Value{ + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + }), + }), + }, + "module_instance_overrides": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + count = 3 + source = "./mod" +} + +output "value" { + value = module.mod.*.value +} + +output "id" { + value = module.mod.*.id +}`, + "mod/main.tf": ` +terraform { + required_providers { + replaced = { + source = "hashicorp/test" + } + } +} + +resource "test_instance" "instance" { + provider = replaced + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance("module.mod"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("generic"), + "value": cty.StringVal("Hello, world!"), + }), + }) + overrides.Put(mustModuleInstance("module.mod[1]"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("specific"), + "value": cty.StringVal("Hello, world!"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.TupleVal([]cty.Value{ + cty.StringVal("generic"), + cty.StringVal("specific"), + cty.StringVal("generic"), + }), + "value": cty.TupleVal([]cty.Value{ + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + }), + }), + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + cfg := testModuleInline(t, tc.configs) + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(underlyingOverridesProvider), + }, + }) + + plan, diags := ctx.Plan(cfg, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + Overrides: tc.overrides, + }) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + state, diags := ctx.Apply(plan, cfg) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + outputs := make(map[string]cty.Value, len(cfg.Module.Outputs)) + for _, output := range cfg.Module.Outputs { + outputs[output.Name] = state.OutputValue(output.Addr().Absolute(addrs.RootModuleInstance)).Value + } + actual := cty.ObjectVal(outputs) + + if !actual.RawEquals(tc.outputs) { + t.Fatalf("expected:\n%s\nactual:\n%s", tc.outputs.GoString(), actual.GoString()) + } + + _, diags = ctx.Plan(cfg, state, &PlanOpts{ + Mode: plans.RefreshOnlyMode, + Overrides: tc.overrides, + }) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + destroyPlan, diags := ctx.Plan(cfg, state, &PlanOpts{ + Mode: plans.DestroyMode, + Overrides: tc.overrides, + }) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + _, diags = ctx.Apply(destroyPlan, cfg) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + }) + } + +} + +// underlyingOverridesProvider returns a provider that always panics for +// important calls. This is to validate the behaviour of the overrides +// functionality, in that they should stop the provider from being executed. +var underlyingOverridesProvider = &MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_instance": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test_instance": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadResourceFn: func(request providers.ReadResourceRequest) providers.ReadResourceResponse { + panic("ReadResourceFn called, should have been overridden.") + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + panic("PlanResourceChangeFn called, should have been overridden.") + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + panic("ApplyResourceChangeFn called, should have been overridden.") + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + panic("ReadDataSourceFn called, should have been overridden.") + }, +} diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index bbae4b8ace..7d50aa5bd1 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang/globalref" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/states" @@ -79,6 +80,10 @@ type PlanOpts struct { // the actual graph. ExternalReferences []*addrs.Reference + // Overrides provides a set of override objects that should be applied + // during this plan. + Overrides *mocking.Overrides + // ImportTargets is a list of target resources to import. These resources // will be added to the plan graph. ImportTargets []*ImportTarget @@ -570,6 +575,7 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o Changes: changes, MoveResults: moveResults, PlanTimeTimestamp: timestamp, + Overrides: opts.Overrides, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) @@ -620,6 +626,7 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o PriorState: priorState, PlannedState: walker.State.Close(), ExternalReferences: opts.ExternalReferences, + Overrides: opts.Overrides, Checks: states.NewCheckResults(walker.Checks), Timestamp: timestamp, diff --git a/internal/terraform/context_walk.go b/internal/terraform/context_walk.go index 002cd878bf..db371d52fa 100644 --- a/internal/terraform/context_walk.go +++ b/internal/terraform/context_walk.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/states" @@ -41,6 +42,10 @@ type graphWalkOpts struct { // the apply phase. PlanTimeTimestamp time.Time + // Overrides contains the set of overrides we should apply during this + // operation. + Overrides *mocking.Overrides + MoveResults refactoring.MoveResults } @@ -150,5 +155,6 @@ func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *Con Operation: operation, StopContext: c.runContext, PlanTimestamp: opts.PlanTimeTimestamp, + Overrides: opts.Overrides, } } diff --git a/internal/terraform/eval_context.go b/internal/terraform/eval_context.go index f2728e7af1..e16d034aef 100644 --- a/internal/terraform/eval_context.go +++ b/internal/terraform/eval_context.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" @@ -203,6 +204,10 @@ type EvalContext interface { // objects accessible through it. MoveResults() refactoring.MoveResults + // Overrides contains the modules and resources we should mock as part of + // this execution. + Overrides() *mocking.Overrides + // WithPath returns a copy of the context with the internal path set to the // path argument. WithPath(path addrs.ModuleInstance) EvalContext diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index 5d5bce9886..e8252dc5c5 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" @@ -74,6 +75,7 @@ type BuiltinEvalContext struct { PrevRunStateValue *states.SyncState InstanceExpanderValue *instances.Expander MoveResultsValue refactoring.MoveResults + OverrideValues *mocking.Overrides } // BuiltinEvalContext implements EvalContext @@ -520,3 +522,7 @@ func (ctx *BuiltinEvalContext) InstanceExpander() *instances.Expander { func (ctx *BuiltinEvalContext) MoveResults() refactoring.MoveResults { return ctx.MoveResultsValue } + +func (ctx *BuiltinEvalContext) Overrides() *mocking.Overrides { + return ctx.OverrideValues +} diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index d01a4fb113..18d174c4aa 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" @@ -153,6 +154,9 @@ type MockEvalContext struct { InstanceExpanderCalled bool InstanceExpanderExpander *instances.Expander + + OverridesCalled bool + OverrideValues *mocking.Overrides } // MockEvalContext implements EvalContext @@ -404,3 +408,8 @@ func (c *MockEvalContext) InstanceExpander() *instances.Expander { c.InstanceExpanderCalled = true return c.InstanceExpanderExpander } + +func (c *MockEvalContext) Overrides() *mocking.Overrides { + c.OverridesCalled = true + return c.OverrideValues +} diff --git a/internal/terraform/graph.go b/internal/terraform/graph.go index e775a9a4a8..199078492b 100644 --- a/internal/terraform/graph.go +++ b/internal/terraform/graph.go @@ -8,7 +8,10 @@ import ( "log" "strings" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/internal/addrs" @@ -73,6 +76,12 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics { defer walker.ExitPath(pn.Path()) } + if g.checkAndApplyOverrides(ctx.Overrides(), v) { + // We can skip whole vertices if they are in a module that has been + // overridden. + return + } + // If the node is exec-able, then execute it. if ev, ok := v.(GraphNodeExecutable); ok { diags = diags.Append(walker.Execute(vertexCtx, ev)) @@ -136,3 +145,97 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics { return g.AcyclicGraph.Walk(walkFn) } + +// checkAndApplyOverrides checks if target has any data that needs to be overridden. +// +// If this function returns true, then the whole vertex should be skipped and +// not executed. +// +// The logic for a vertex is that if it is within an overridden module then we +// don't want to execute it. Instead, we want to just set the values on the +// output nodes for that module directly. So if a node is a +// GraphNodeModuleInstance we want to skip it if there is an entry in our +// overrides data structure that either matches the module for the vertex or +// is a parent of the module for the vertex. +// +// We also want to actually set the new values for any outputs, resources or +// data sources we encounter that should be overridden. +func (g *Graph) checkAndApplyOverrides(overrides *mocking.Overrides, target dag.Vertex) bool { + if overrides.Empty() { + return false + } + + switch v := target.(type) { + case GraphNodeOverridable: + // For resource and data sources, we want to skip them completely if + // they are within an overridden module. + resourceInstance := v.ResourceInstanceAddr() + if overrides.IsOverridden(resourceInstance.Module) { + return true + } + + if override, ok := overrides.GetOverrideInclProviders(resourceInstance, v.ConfigProvider()); ok { + v.SetOverride(override) + return false + } + + if override, ok := overrides.GetOverrideInclProviders(resourceInstance.ContainingResource(), v.ConfigProvider()); ok { + v.SetOverride(override) + return false + } + + case *NodeApplyableOutput: + // For outputs, we want to skip them completely if they are deeply + // nested within an overridden module. + module := v.Path() + if overrides.IsDeeplyOverridden(module) { + // If the output is deeply nested under an overridden module we want + // to skip + return true + } + + setOverride := func(values cty.Value) { + key := v.Addr.OutputValue.Name + if values.Type().HasAttribute(key) { + v.override = values.GetAttr(key) + } else { + // If we don't have a value provided for an output, then we'll + // just set it to be null. + // + // TODO(liamcervante): Can we generate a value here? Probably + // not as we don't know the type. + v.override = cty.NullVal(cty.DynamicPseudoType) + } + } + + // Otherwise, if we are in a directly overridden module then we want to + // apply the overridden output values. + if override, ok := overrides.GetOverride(module); ok { + setOverride(override.Values) + return false + } + + lastStepInstanced := len(module) > 0 && module[len(module)-1].InstanceKey != addrs.NoKey + if lastStepInstanced { + // Then we could have overridden all the instances of this module. + if override, ok := overrides.GetOverride(module.ContainingModule()); ok { + setOverride(override.Values) + return false + } + } + + case GraphNodeModuleInstance: + // Then this node is simply in a module. It might be that this entire + // module has been overridden, in which case this node shouldn't + // execute. + // + // We checked for resources and outputs earlier, so we know this isn't + // anything special. + module := v.Path() + if overrides.IsOverridden(module) { + return true + } + } + + return false +} diff --git a/internal/terraform/graph_walk_context.go b/internal/terraform/graph_walk_context.go index d56f88f057..1d1b205b89 100644 --- a/internal/terraform/graph_walk_context.go +++ b/internal/terraform/graph_walk_context.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" @@ -43,6 +44,7 @@ type ContextGraphWalker struct { RootVariableValues InputValues Config *configs.Config PlanTimestamp time.Time + Overrides *mocking.Overrides // This is an output. Do not set this, nor read it while a graph walk // is in progress. @@ -114,6 +116,7 @@ func (w *ContextGraphWalker) EvalContext() EvalContext { Evaluator: evaluator, VariableValues: w.variableValues, VariableValuesLock: &w.variableValuesLock, + OverrideValues: w.Overrides, } return ctx diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index e7dbafe91c..249365a65f 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -205,6 +205,9 @@ type NodeApplyableOutput struct { DestroyApply bool Planning bool + + // override is set by the graph itself, just before this node executes. + override cty.Value } var ( @@ -322,7 +325,9 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags // Checks are not evaluated during a destroy. The checks may fail, may not // be valid, or may not have been registered at all. - if !n.DestroyApply { + // We also don't evaluate checks for overridden outputs. This is because + // any references within the checks will likely not have been created. + if !n.DestroyApply && n.override == cty.NilVal { checkRuleSeverity := tfdiags.Error if n.RefreshOnly { checkRuleSeverity = tfdiags.Warning @@ -342,32 +347,38 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags // If there was no change recorded, or the recorded change was not wholly // known, then we need to re-evaluate the output if !changeRecorded || !val.IsWhollyKnown() { - // This has to run before we have a state lock, since evaluation also - // reads the state - var evalDiags tfdiags.Diagnostics - val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil) - diags = diags.Append(evalDiags) - - // We'll handle errors below, after we have loaded the module. - // Outputs don't have a separate mode for validation, so validate - // depends_on expressions here too - diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) - - // For root module outputs in particular, an output value must be - // statically declared as sensitive in order to dynamically return - // a sensitive result, to help avoid accidental exposure in the state - // of a sensitive value that the user doesn't want to include there. - if n.Addr.Module.IsRoot() { - if !n.Config.Sensitive && marks.Contains(val, marks.Sensitive) { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Output refers to sensitive values", - Detail: `To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires that any root module output containing sensitive data be explicitly marked as sensitive, to confirm your intent. + + // First, we check if we have an overridden value. If we do, then we + // use that and we don't try and evaluate the underlying expression. + val = n.override + if val == cty.NilVal { + // This has to run before we have a state lock, since evaluation also + // reads the state + var evalDiags tfdiags.Diagnostics + val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil) + diags = diags.Append(evalDiags) + + // We'll handle errors below, after we have loaded the module. + // Outputs don't have a separate mode for validation, so validate + // depends_on expressions here too + diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) + + // For root module outputs in particular, an output value must be + // statically declared as sensitive in order to dynamically return + // a sensitive result, to help avoid accidental exposure in the state + // of a sensitive value that the user doesn't want to include there. + if n.Addr.Module.IsRoot() { + if !n.Config.Sensitive && marks.Contains(val, marks.Sensitive) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Output refers to sensitive values", + Detail: `To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires that any root module output containing sensitive data be explicitly marked as sensitive, to confirm your intent. If you do intend to export this data, annotate the output value as sensitive by adding the following argument: sensitive = true`, - Subject: n.Config.DeclRange.Ptr(), - }) + Subject: n.Config.DeclRange.Ptr(), + }) + } } } } diff --git a/internal/terraform/node_overridable.go b/internal/terraform/node_overridable.go new file mode 100644 index 0000000000..de8da889fb --- /dev/null +++ b/internal/terraform/node_overridable.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +// GraphNodeOverridable represents a node in the graph that can be overridden +// by the testing framework. +type GraphNodeOverridable interface { + GraphNodeResourceInstance + + ConfigProvider() addrs.AbsProviderConfig + SetOverride(override *configs.Override) +} diff --git a/internal/terraform/node_resource_abstract.go b/internal/terraform/node_resource_abstract.go index c185a7a19f..b8ffec310d 100644 --- a/internal/terraform/node_resource_abstract.go +++ b/internal/terraform/node_resource_abstract.go @@ -122,6 +122,7 @@ var ( _ GraphNodeAttachProvisionerSchema = (*NodeAbstractResourceInstance)(nil) _ GraphNodeAttachProviderMetaConfigs = (*NodeAbstractResourceInstance)(nil) _ GraphNodeTargetable = (*NodeAbstractResourceInstance)(nil) + _ GraphNodeOverridable = (*NodeAbstractResourceInstance)(nil) _ dag.GraphNodeDotter = (*NodeAbstractResourceInstance)(nil) ) diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index f542bc44e5..ad4cee0035 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/objchange" "github.com/hashicorp/terraform/internal/providers" @@ -43,6 +44,9 @@ type NodeAbstractResourceInstance struct { // During import we may generate configuration for a resource, which needs // to be stored in the final change. generatedConfigHCL string + + // override is set by the graph itself, just before this node executes. + override *configs.Override } // NewNodeAbstractResourceInstance creates an abstract resource instance graph @@ -135,6 +139,16 @@ func (n *NodeAbstractResourceInstance) AttachResourceState(s *states.Resource) { n.storedProviderConfig = s.ProviderConfig } +// GraphNodeOverridable +func (n *NodeAbstractResourceInstance) ConfigProvider() addrs.AbsProviderConfig { + return n.ResolvedProvider +} + +// GraphNodeOverridable +func (n *NodeAbstractResourceInstance) SetOverride(override *configs.Override) { + n.override = override +} + // readDiff returns the planned change for a particular resource instance // object. func (n *NodeAbstractResourceInstance) readDiff(ctx EvalContext, providerSchema providers.ProviderSchema) (*plans.ResourceInstanceChange, error) { @@ -399,39 +413,50 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState return plan, diags } - // Allow the provider to check the destroy plan, and insert any necessary - // private data. - resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - Config: nullVal, - PriorState: unmarkedPriorVal, - ProposedNewState: nullVal, - PriorPrivate: currentState.Private, - ProviderMeta: metaConfigVal, - }) + var resp providers.PlanResourceChangeResponse + if n.override != nil { + // If we have an overridden value from the test framework, that means + // this value was created without consulting the provider previously. + // We can just set the planned state to deleted without consulting the + // provider. + resp = providers.PlanResourceChangeResponse{ + PlannedState: nullVal, + } + } else { + // Allow the provider to check the destroy plan, and insert any + // necessary private data. + resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + Config: nullVal, + PriorState: unmarkedPriorVal, + ProposedNewState: nullVal, + PriorPrivate: currentState.Private, + ProviderMeta: metaConfigVal, + }) - // We may not have a config for all destroys, but we want to reference it in - // the diagnostics if we do. - if n.Config != nil { - resp.Diagnostics = resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String()) - } - diags = diags.Append(resp.Diagnostics) - if diags.HasErrors() { - return plan, diags - } + // We may not have a config for all destroys, but we want to reference + // it in the diagnostics if we do. + if n.Config != nil { + resp.Diagnostics = resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String()) + } + diags = diags.Append(resp.Diagnostics) + if diags.HasErrors() { + return plan, diags + } - // Check that the provider returned a null value here, since that is the - // only valid value for a destroy plan. - if !resp.PlannedState.IsNull() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced invalid plan", - fmt.Sprintf( - "Provider %q planned a non-null destroy value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider.Provider, n.Addr), - ), - ) - return plan, diags + // Check that the provider returned a null value here, since that is the + // only valid value for a destroy plan. + if !resp.PlannedState.IsNull() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + fmt.Sprintf( + "Provider %q planned a non-null destroy value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr), + ), + ) + return plan, diags + } } // Plan is always the same for a destroy. @@ -563,14 +588,21 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state priorVal, priorPaths = priorVal.UnmarkDeepWithPaths() } - providerReq := providers.ReadResourceRequest{ - TypeName: n.Addr.Resource.Resource.Type, - PriorState: priorVal, - Private: state.Private, - ProviderMeta: metaConfigVal, + var resp providers.ReadResourceResponse + if n.override != nil { + // If we have an override set for this resource, we don't want to talk + // to the provider so we'll just return whatever was in state. + resp = providers.ReadResourceResponse{ + NewState: priorVal, + } + } else { + resp = provider.ReadResource(providers.ReadResourceRequest{ + TypeName: n.Addr.Resource.Resource.Type, + PriorState: priorVal, + Private: state.Private, + ProviderMeta: metaConfigVal, + }) } - - resp := provider.ReadResource(providerReq) if n.Config != nil { resp.Diagnostics = resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String()) } @@ -798,14 +830,36 @@ func (n *NodeAbstractResourceInstance) plan( return nil, nil, keyData, diags } - resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - Config: unmarkedConfigVal, - PriorState: unmarkedPriorVal, - ProposedNewState: proposedNewVal, - PriorPrivate: priorPrivate, - ProviderMeta: metaConfigVal, - }) + var resp providers.PlanResourceChangeResponse + if n.override != nil { + // Then we have an override to apply for this change. But, overrides + // only matter when we are creating a resource for the first time as we + // only apply computed values. + if priorVal.IsNull() { + // Then we are actually creating something, so let's populate the + // computed values from our override value. + override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, schema) + resp = providers.PlanResourceChangeResponse{ + PlannedState: override, + Diagnostics: overrideDiags, + } + } else { + // This is an update operation, and we don't actually have any + // computed values that need to be applied. + resp = providers.PlanResourceChangeResponse{ + PlannedState: proposedNewVal, + } + } + } else { + resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + Config: unmarkedConfigVal, + PriorState: unmarkedPriorVal, + ProposedNewState: proposedNewVal, + PriorPrivate: priorPrivate, + ProviderMeta: metaConfigVal, + }) + } diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { return nil, nil, keyData, diags @@ -1032,14 +1086,24 @@ func (n *NodeAbstractResourceInstance) plan( // create a new proposed value from the null state and the config proposedNewVal = objchange.ProposedNew(schema, nullPriorVal, unmarkedConfigVal) - resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - Config: unmarkedConfigVal, - PriorState: nullPriorVal, - ProposedNewState: proposedNewVal, - PriorPrivate: plannedPrivate, - ProviderMeta: metaConfigVal, - }) + if n.override != nil { + // In this case, we are always creating the resource so we don't + // do any validation, and just call out to the mocking library. + override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, schema) + resp = providers.PlanResourceChangeResponse{ + PlannedState: override, + Diagnostics: overrideDiags, + } + } else { + resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + Config: unmarkedConfigVal, + PriorState: nullPriorVal, + ProposedNewState: proposedNewVal, + PriorPrivate: plannedPrivate, + ProviderMeta: metaConfigVal, + }) + } // We need to tread carefully here, since if there are any warnings // in here they probably also came out of our previous call to // PlanResourceChange above, and so we don't want to repeat them. @@ -1447,11 +1511,23 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal return newVal, diags } - resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ - TypeName: n.Addr.ContainingResource().Resource.Type, - Config: configVal, - ProviderMeta: metaConfigVal, - }) + var resp providers.ReadDataSourceResponse + if n.override != nil { + override, overrideDiags := mocking.ComputedValuesForDataSource(configVal, mocking.ReplacementValue{ + Value: n.override.Values, + Range: n.override.ValuesRange, + }, schema) + resp = providers.ReadDataSourceResponse{ + State: override, + Diagnostics: overrideDiags, + } + } else { + resp = provider.ReadDataSource(providers.ReadDataSourceRequest{ + TypeName: n.Addr.ContainingResource().Resource.Type, + Config: configVal, + ProviderMeta: metaConfigVal, + }) + } diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { return newVal, diags @@ -2293,14 +2369,35 @@ func (n *NodeAbstractResourceInstance) apply( return newState, diags } - resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - PriorState: unmarkedBefore, - Config: unmarkedConfigVal, - PlannedState: unmarkedAfter, - PlannedPrivate: change.Private, - ProviderMeta: metaConfigVal, - }) + var resp providers.ApplyResourceChangeResponse + if n.override != nil { + // As with the planning stage, we only need to worry about computed + // values the first time the object is created. Otherwise, we're happy + // to just apply whatever the user asked for. + if change.Action == plans.Create { + override, overrideDiags := mocking.ApplyComputedValuesForResource(unmarkedAfter, mocking.ReplacementValue{ + Value: n.override.Values, + Range: n.override.ValuesRange, + }, schema) + resp = providers.ApplyResourceChangeResponse{ + NewState: override, + Diagnostics: overrideDiags, + } + } else { + resp = providers.ApplyResourceChangeResponse{ + NewState: unmarkedAfter, + } + } + } else { + resp = provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + PriorState: unmarkedBefore, + Config: unmarkedConfigVal, + PlannedState: unmarkedAfter, + PlannedPrivate: change.Private, + ProviderMeta: metaConfigVal, + }) + } applyDiags := resp.Diagnostics if applyConfig != nil { applyDiags = applyDiags.InConfigBody(applyConfig.Config, n.Addr.String()) diff --git a/internal/terraform/terraform_test.go b/internal/terraform/terraform_test.go index 0087679b39..c4b5787984 100644 --- a/internal/terraform/terraform_test.go +++ b/internal/terraform/terraform_test.go @@ -241,6 +241,14 @@ func mustReference(s string) *addrs.Reference { return p } +func mustModuleInstance(s string) addrs.ModuleInstance { + p, diags := addrs.ParseModuleInstanceStr(s) + if diags.HasErrors() { + panic(diags.Err()) + } + return p +} + // HookRecordApplyOrder is a test hook that records the order of applies // by recording the PreApply event. type HookRecordApplyOrder struct {