blob: 02ffac57ff1e94f687e71886c0f777f67963a8f6 [file] [log] [blame]
// 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"
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
"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
expectedErr string
}{
"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[addrs.RootProviderConfig]addrs.Map[addrs.Targetable, *configs.Override]) {
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
}] = addrs.MakeMap[addrs.Targetable, *configs.Override]()
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("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[addrs.RootProviderConfig]addrs.Map[addrs.Targetable, *configs.Override]) {
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Alias: "secondary",
}] = addrs.MakeMap[addrs.Targetable, *configs.Override]()
// Test should not apply this override, as this provider is
// not being used for this resource.
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Alias: "secondary",
}].Put(mustResourceInstanceAddr("test_instance.primary"), &configs.Override{
Values: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("primary_id"),
}),
})
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Alias: "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[addrs.RootProviderConfig]addrs.Map[addrs.Targetable, *configs.Override]) {
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
}] = addrs.MakeMap[addrs.Targetable, *configs.Override]()
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("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[addrs.RootProviderConfig]addrs.Map[addrs.Targetable, *configs.Override]) {
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
}] = addrs.MakeMap[addrs.Targetable, *configs.Override]()
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("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[addrs.RootProviderConfig]addrs.Map[addrs.Targetable, *configs.Override]) {
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
}] = addrs.MakeMap[addrs.Targetable, *configs.Override]()
overrides[addrs.RootProviderConfig{
Provider: addrs.NewDefaultProvider("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!"),
}),
}),
},
"imports": {
configs: map[string]string{
"main.tf": `
provider "test" {}
import {
id = "29C1E645FF91"
to = test_instance.instance
}
resource "test_instance" "instance" {
value = "Hello, world!"
}
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("29C1E645FF91"),
}),
})
}),
outputs: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("29C1E645FF91"),
}),
},
// This test is designed to fail as documentation that we do not support
// config generation during tests. It's actually impossible in normal
// usage to do this since `terraform test` never triggers config
// generation.
"imports_config_gen": {
configs: map[string]string{
"main.tf": `
provider "test" {}
import {
id = "29C1E645FF91"
to = test_instance.instance
}
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("29C1E645FF91"),
}),
})
}),
outputs: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("29C1E645FF91"),
}),
expectedErr: "override blocks do not support config generation",
},
"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!"),
}),
}),
},
"expansion inside overridden module": {
configs: map[string]string{
"main.tf": `
module "test" {
source = "./mod"
}
`,
"mod/main.tf": `
locals {
instances = 2
value = "Hello, world!"
}
resource "test_instance" "resource" {
count = local.instances
string = local.value
}
output "id" {
value = test_instance.resource[0].id
}
`,
},
overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) {
overrides.Put(mustModuleInstance("module.test"), &configs.Override{
Values: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("h3ll0"),
}),
})
}),
outputs: cty.EmptyObjectVal,
},
"expansion inside deeply nested overridden module": {
configs: map[string]string{
"main.tf": `
module "test" {
source = "./child"
}
`,
"child/main.tf": `
module "grandchild" {
source = "../grandchild"
}
locals {
instances = 2
value = "Hello, world!"
}
resource "test_instance" "resource" {
count = local.instances
string = local.value
}
output "id" {
value = test_instance.resource[0].id
}
`,
"grandchild/main.tf": `
locals {
instances = 2
value = "Hello, world!"
}
resource "test_instance" "resource" {
count = local.instances
string = local.value
}
output "id" {
value = test_instance.resource[0].id
}
`,
},
overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) {
overrides.Put(mustModuleInstance("module.test"), &configs.Override{
Values: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("h3ll0"),
}),
})
}),
outputs: cty.EmptyObjectVal,
},
"legacy provider config inside overridden module": {
configs: map[string]string{
"main.tf": `
module "test" {
source = "./child"
}
`,
"child/main.tf": `
module "grandchild" {
source = "../grandchild"
}
output "id" {
value = "child"
}
`,
"grandchild/main.tf": `
variable "in" {
default = "test_value"
}
provider "test" {
value = var.in
}
resource "test_instance" "resource" {
}
`,
},
overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) {
overrides.Put(mustModuleInstance("module.test"), &configs.Override{
Values: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("h3ll0"),
}),
})
}),
outputs: cty.EmptyObjectVal,
},
}
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,
GenerateConfigPath: "out.tf",
})
if len(tc.expectedErr) > 0 {
if diags.ErrWithWarnings().Error() != tc.expectedErr {
t.Fatal(diags)
}
return // Don't do the rest of the test if we were expecting errors.
}
if diags.HasErrors() {
t.Fatal(diags.Err())
}
state, diags := ctx.Apply(plan, cfg, nil)
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, nil)
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 = &testing_provider.MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Optional: true,
},
},
},
},
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Body: &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": {
Body: &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.")
},
ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
panic("ImportResourceStateFn called, should have been overridden.")
},
}