From dedb296773d4f219f00fa43d5eb11b2ae63b36db Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Tue, 14 Nov 2023 08:50:37 +0100 Subject: [PATCH] terraform test: connect mock behaviour to test framework (#34205) * connect mock behaviour to test framework * fix calling unconfigured providers --- internal/backend/local/test.go | 3 + internal/command/test_test.go | 4 ++ .../testdata/test/mocking/child/main.tf | 30 ++++++++ .../command/testdata/test/mocking/main.tf | 46 ++++++++++++ .../mocking/tests/module_mocked.tftest.hcl | 44 ++++++++++++ .../tests/module_mocked_overridden.tftest.hcl | 70 +++++++++++++++++++ .../test/mocking/tests/no_mocks.tftest.hcl | 7 ++ .../mocking/tests/primary_mocked.tftest.hcl | 39 +++++++++++ .../primary_mocked_overridden.tftest.hcl | 52 ++++++++++++++ internal/moduletest/config/config.go | 7 +- internal/providers/mock.go | 59 +++++++++++++--- internal/terraform/node_module_expand.go | 17 ++++- 12 files changed, 365 insertions(+), 13 deletions(-) create mode 100644 internal/command/testdata/test/mocking/child/main.tf create mode 100644 internal/command/testdata/test/mocking/main.tf create mode 100644 internal/command/testdata/test/mocking/tests/module_mocked.tftest.hcl create mode 100644 internal/command/testdata/test/mocking/tests/module_mocked_overridden.tftest.hcl create mode 100644 internal/command/testdata/test/mocking/tests/no_mocks.tftest.hcl create mode 100644 internal/command/testdata/test/mocking/tests/primary_mocked.tftest.hcl create mode 100644 internal/command/testdata/test/mocking/tests/primary_mocked_overridden.tftest.hcl diff --git a/internal/backend/local/test.go b/internal/backend/local/test.go index d6d0e91455..652ae05a95 100644 --- a/internal/backend/local/test.go +++ b/internal/backend/local/test.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/terraform/internal/moduletest" configtest "github.com/hashicorp/terraform/internal/moduletest/config" hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" @@ -605,6 +606,7 @@ func (runner *TestFileRunner) destroy(config *configs.Config, state *states.Stat planOpts := &terraform.PlanOpts{ Mode: plans.DestroyMode, SetVariables: setVariables, + Overrides: mocking.PackageOverrides(run.Config, file.Config, config), } tfCtx, ctxDiags := terraform.NewContext(runner.Suite.Opts) @@ -675,6 +677,7 @@ func (runner *TestFileRunner) plan(config *configs.Config, state *states.State, SkipRefresh: !run.Config.Options.Refresh, SetVariables: variables, ExternalReferences: references, + Overrides: mocking.PackageOverrides(run.Config, file.Config, config), } tfCtx, ctxDiags := terraform.NewContext(runner.Suite.Opts) diff --git a/internal/command/test_test.go b/internal/command/test_test.go index ff2ea2006d..b9438642f1 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -193,6 +193,10 @@ func TestTest(t *testing.T) { expected: "1 passed, 0 failed.", code: 0, }, + "mocking": { + expected: "5 passed, 0 failed.", + code: 0, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { diff --git a/internal/command/testdata/test/mocking/child/main.tf b/internal/command/testdata/test/mocking/child/main.tf new file mode 100644 index 0000000000..2ef4e4d979 --- /dev/null +++ b/internal/command/testdata/test/mocking/child/main.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [test.primary, test.secondary] + } + } +} + +variable "instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +output "primary" { + value = test_resource.primary +} + +output "secondary" { + value = test_resource.secondary +} diff --git a/internal/command/testdata/test/mocking/main.tf b/internal/command/testdata/test/mocking/main.tf new file mode 100644 index 0000000000..49506e06c3 --- /dev/null +++ b/internal/command/testdata/test/mocking/main.tf @@ -0,0 +1,46 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +provider "test" { + alias = "primary" +} + +provider "test" { + alias = "secondary" +} + +variable "instances" { + type = number +} + +variable "child_instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +module "child" { + count = var.instances + + source = "./child" + + providers = { + test.primary = test.primary + test.secondary = test.secondary + } + + instances = var.child_instances +} diff --git a/internal/command/testdata/test/mocking/tests/module_mocked.tftest.hcl b/internal/command/testdata/test/mocking/tests/module_mocked.tftest.hcl new file mode 100644 index 0000000000..f1f96e934b --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/module_mocked.tftest.hcl @@ -0,0 +1,44 @@ +override_module { + target = module.child[1] + outputs = { + primary = [ + { + id = "bbbb" + } + ] + secondary = [ + { + id = "cccc" + } + ] + } +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + + assert { + condition = module.child[0].primary[0].id != "bbbb" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[1].primary[0].id == "bbbb" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[1].secondary[0].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[2].secondary[0].id != "cccc" + error_message = "wrongly applied mocks" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/module_mocked_overridden.tftest.hcl b/internal/command/testdata/test/mocking/tests/module_mocked_overridden.tftest.hcl new file mode 100644 index 0000000000..26114fb1df --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/module_mocked_overridden.tftest.hcl @@ -0,0 +1,70 @@ +override_module { + target = module.child + outputs = { + primary = [ + { + id = "bbbb" + } + ] + secondary = [ + { + id = "cccc" + } + ] + } +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + + override_module { + target = module.child[1] + outputs = { + primary = [ + { + id = "aaaa" + } + ] + secondary = [ + { + id = "dddd" + } + ] + } + } + + assert { + condition = module.child[0].primary[0].id == "bbbb" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[0].secondary[0].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[2].primary[0].id == "bbbb" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[2].secondary[0].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[1].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[1].secondary[0].id == "dddd" + error_message = "did not apply mocks" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/no_mocks.tftest.hcl b/internal/command/testdata/test/mocking/tests/no_mocks.tftest.hcl new file mode 100644 index 0000000000..f814dc79e7 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/no_mocks.tftest.hcl @@ -0,0 +1,7 @@ + +variables { + instances = 1 + child_instances = 0 +} + +run "test" {} diff --git a/internal/command/testdata/test/mocking/tests/primary_mocked.tftest.hcl b/internal/command/testdata/test/mocking/tests/primary_mocked.tftest.hcl new file mode 100644 index 0000000000..e9ea31a880 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/primary_mocked.tftest.hcl @@ -0,0 +1,39 @@ +mock_provider "test" { + alias = "primary" + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } +} + +variables { + instances = 1 + child_instances = 1 +} + + +run "test" { + + assert { + condition = test_resource.primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[0].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = test_resource.secondary[0].id != "aaaa" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[0].secondary[0].id != "aaaa" + error_message = "wrongly applied mocks" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/primary_mocked_overridden.tftest.hcl b/internal/command/testdata/test/mocking/tests/primary_mocked_overridden.tftest.hcl new file mode 100644 index 0000000000..16f92305cf --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/primary_mocked_overridden.tftest.hcl @@ -0,0 +1,52 @@ +mock_provider "test" { + alias = "primary" + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } + + override_resource { + target = test_resource.primary + values = { + id = "bbbb" + } + } +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + + override_resource { + target = test_resource.primary[1] + values = { + id = "cccc" + } + } + + assert { + condition = test_resource.primary[0].id == "bbbb" + error_message = "did not apply mocks" + } + + assert { + condition = test_resource.primary[1].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = test_resource.primary[2].id == "bbbb" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[0].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + +} diff --git a/internal/moduletest/config/config.go b/internal/moduletest/config/config.go index de793daf50..5bf8a0ea12 100644 --- a/internal/moduletest/config/config.go +++ b/internal/moduletest/config/config.go @@ -67,9 +67,7 @@ func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *m if len(run.Config.Providers) > 0 { // Then we'll only copy over and overwrite the specific providers asked // for by this run block. - for _, ref := range run.Config.Providers { - testProvider, ok := file.Config.Providers[ref.InParent.String()] if !ok { // Then this reference was invalid as we didn't have the @@ -96,9 +94,10 @@ func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *m AvailableVariables: availableVariables, AvailableRunBlocks: availableRunBlocks, }, + Mock: testProvider.Mock, + MockData: testProvider.MockData, DeclRange: testProvider.DeclRange, } - } } else { // Otherwise, let's copy over and overwrite all providers specified by @@ -123,6 +122,8 @@ func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *m AvailableVariables: availableVariables, AvailableRunBlocks: availableRunBlocks, }, + Mock: provider.Mock, + MockData: provider.MockData, DeclRange: provider.DeclRange, } } diff --git a/internal/providers/mock.go b/internal/providers/mock.go index 662f3ce268..cce51e0d72 100644 --- a/internal/providers/mock.go +++ b/internal/providers/mock.go @@ -7,8 +7,10 @@ import ( "fmt" "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -54,27 +56,66 @@ func (m *Mock) ValidateProviderConfig(request ValidateProviderConfigRequest) (re func (m *Mock) ValidateResourceConfig(request ValidateResourceConfigRequest) ValidateResourceConfigResponse { // We'll just pass this through to the underlying provider. The mock should - // support the same resource syntax as the original provider. + // support the same resource syntax as the original provider and we can call + // validate without needing to configure the provider first. return m.Provider.ValidateResourceConfig(request) } func (m *Mock) ValidateDataResourceConfig(request ValidateDataResourceConfigRequest) ValidateDataResourceConfigResponse { // We'll just pass this through to the underlying provider. The mock should - // support the same data source syntax as the original provider. + // support the same data source syntax as the original provider and we can + // call validate without needing to configure the provider first. return m.Provider.ValidateDataResourceConfig(request) } -func (m *Mock) UpgradeResourceState(request UpgradeResourceStateRequest) UpgradeResourceStateResponse { - // It's unlikely this will ever be called on a mocked provider, given they - // can only execute from inside tests. But we don't need to anything special - // here, let's just have the original provider handle it. - return m.Provider.UpgradeResourceState(request) +func (m *Mock) UpgradeResourceState(request UpgradeResourceStateRequest) (response UpgradeResourceStateResponse) { + // We can't do this from a mocked provider, so we just return whatever state + // is in the request back unchanged. + + schema := m.GetProviderSchema() + response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics) + if schema.Diagnostics.HasErrors() { + // We couldn't retrieve the schema for some reason, so the mock + // provider can't really function. + return response + } + + resource, exists := schema.ResourceTypes[request.TypeName] + if !exists { + // This means something has gone wrong much earlier, we should have + // failed a validation somewhere if a resource type doesn't exist. + panic(fmt.Errorf("failed to retrieve schema for resource %s", request.TypeName)) + } + + schemaType := resource.Block.ImpliedType() + + var value cty.Value + var err error + + switch { + case request.RawStateFlatmap != nil: + value, err = hcl2shim.HCL2ValueFromFlatmap(request.RawStateFlatmap, schemaType) + case len(request.RawStateJSON) > 0: + value, err = ctyjson.Unmarshal(request.RawStateJSON, schemaType) + } + + if err != nil { + // Generally, we shouldn't get an error here. The mocked providers are + // only used in tests, and we can't use different versions of providers + // within/between tests so the types should always match up. As such, + // we're not gonna return a super detailed error here. + response.Diagnostics = response.Diagnostics.Append(err) + return response + } + response.UpgradedState = value + return response } func (m *Mock) ConfigureProvider(request ConfigureProviderRequest) (response ConfigureProviderResponse) { // Do nothing here, we don't have anything to configure within the mocked - // providers and we don't want to call the original providers from here as - // they may try to talk to their underlying cloud providers. + // providers. We don't want to call the original providers from here as + // they may try to talk to their underlying cloud providers and we + // definitely don't have the right configuration or credentials for this. return response } diff --git a/internal/terraform/node_module_expand.go b/internal/terraform/node_module_expand.go index 3ed94fff4f..9fc0495c95 100644 --- a/internal/terraform/node_module_expand.go +++ b/internal/terraform/node_module_expand.go @@ -200,8 +200,23 @@ func (n *nodeCloseModule) Execute(ctx EvalContext, op walkOperation) (diags tfdi } } + // we don't ever remove a module that's been overridden - it will + // have outputs that have been set by the user and wouldn't be + // removed during normal operations as the module would have created + // resources. Overrides are only set during tests, and stop the + // module creating resources but we still care about the outputs. + overridden := false + if overrides := ctx.Overrides(); !overrides.Empty() { + _, overridden = overrides.GetOverride(mod.Addr) + + if !overridden && len(mod.Addr) > 0 && mod.Addr[len(mod.Addr)-1].InstanceKey != addrs.NoKey { + // Could be all module instances are overridden. + _, overridden = overrides.GetOverride(mod.Addr.ContainingModule()) + } + } + // empty child modules are always removed - if len(mod.Resources) == 0 && !mod.Addr.IsRoot() { + if len(mod.Resources) == 0 && !mod.Addr.IsRoot() && !overridden { delete(state.Modules, modKey) } }