Detect when provider and resource/module have identical for_each (#2186)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Christian Mesh 2024-12-03 14:02:27 -05:00 committed by GitHub
parent 91b43aecd1
commit 8fb8f066c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 628 additions and 36 deletions

100
internal/addrs/traversal.go Normal file
View File

@ -0,0 +1,100 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package addrs
import (
"bytes"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
// TraversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
//
// This function is primarily to help with including traversal strings in
// the UI, and in particular should not be used for comparing traversals.
// Use [TraversalsEquivalent] to determine whether two traversals have
// the same meaning.
func TraversalStr(traversal hcl.Traversal) string {
var buf bytes.Buffer
for _, step := range traversal {
switch tStep := step.(type) {
case hcl.TraverseRoot:
buf.WriteString(tStep.Name)
case hcl.TraverseAttr:
buf.WriteByte('.')
buf.WriteString(tStep.Name)
case hcl.TraverseIndex:
buf.WriteByte('[')
switch tStep.Key.Type() {
case cty.String:
buf.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
case cty.Number:
bf := tStep.Key.AsBigFloat()
//nolint:mnd // numerical precision
buf.WriteString(bf.Text('g', 10))
default:
buf.WriteString("...")
}
buf.WriteByte(']')
}
}
return buf.String()
}
// TraversalsEquivalent returns true if the two given traversals represent
// the same meaning to HCL in all contexts.
//
// Unfortunately there is some ambiguity in interpreting traversal equivalence
// because HCL treats them differently depending on the context. If a
// traversal is involved in expression evaluation then the [hcl.Index]
// and [hcl.GetAttr] functions perform some automatic type conversions and
// allow interchanging index vs. attribute syntax for map and object types,
// but when interpreting traversals just for their syntax (as we do in
// [ParseRef], for example) these distinctions can potentially be significant.
//
// This function takes the stricter interpretation of ignoring the automatic
// adaptations made during expression evaluation, and so for example
// foo.bar and foo["bar"] are NOT considered to be equivalent by this function.
func TraversalsEquivalent(a, b hcl.Traversal) bool {
if len(a) != len(b) {
return false
}
for idx, stepA := range a {
stepB := b[idx]
switch stepA := stepA.(type) {
case hcl.TraverseRoot:
stepB, ok := stepB.(hcl.TraverseRoot)
if !ok || stepA.Name != stepB.Name {
return false
}
case hcl.TraverseAttr:
stepB, ok := stepB.(hcl.TraverseAttr)
if !ok || stepA.Name != stepB.Name {
return false
}
case hcl.TraverseIndex:
stepB, ok := stepB.(hcl.TraverseIndex)
if !ok || stepA.Key.Equals(stepB.Key) != cty.True {
return false
}
default:
// The above should be exhaustive for all traversal
// step types that HCL can possibly generate. We'll
// treat any unsupported stepA types as non-equal
// because that matches what would happen if
// any stepB were unsupported: the type assertions
// in the above cases would fail.
return false
}
}
return true
}

View File

@ -0,0 +1,169 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package addrs
import (
"fmt"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func TestTraversalsEquivalent(t *testing.T) {
tests := []struct {
A, B string
Equivalent bool
}{
{
`foo`,
`foo`,
true,
},
{
`foo`,
`foo.bar`,
false,
},
{
`foo.bar`,
`foo`,
false,
},
{
`foo`,
`bar`,
false,
},
{
`foo.bar`,
`foo.bar`,
true,
},
{
`foo.bar`,
`foo.baz`,
false,
},
{
`foo["bar"]`,
`foo["bar"]`,
true,
},
{
`foo["bar"]`,
`foo["baz"]`,
false,
},
{
`foo[0]`,
`foo[0]`,
true,
},
{
`foo[0]`,
`foo[1]`,
false,
},
{
`foo[0]`,
`foo["0"]`,
false,
},
{
`foo["0"]`,
`foo[0]`,
false,
},
{
// HCL considers these distinct syntactically but considers them
// equivalent during expression evaluation, so whether to consider
// these equivalent is unfortunately context-dependent. We take
// the more conservative interpretation of considering them to
// be distinct.
`foo["bar"]`,
`foo.bar`,
false,
},
{
// The following strings differ only in the level of unicode
// normalization. HCL considers two strings to be equal if they
// have identical unicode normalization.
`foo["ba\u0301z"]`,
`foo["b\u00e1z"]`,
true,
},
{
`foo[1.0]`,
`foo[1]`,
true,
},
{
`foo[01]`,
`foo[1]`,
true,
},
{
// A traversal with a non-integral numeric index is strange, but
// is permitted by HCL syntactically. It would be rejected during
// expression evaluation.
`foo[1.2]`,
`foo[1]`,
false,
},
{
// A traversal with a non-integral numeric index is strange, but
// is permitted by HCL syntactically. It would be rejected during
// expression evaluation.
`foo[1.2]`,
`foo[1.2]`,
true,
},
{
// Integers too large to fit into the significand of a float64
// historically caused some grief for HCL and cty, but this should
// be fixed now and so the following should compare as different.
`foo[9223372036854775807]`,
`foo[9223372036854775808]`,
false,
},
{
// As above, but these two _equal_ large integers should compare
// as equivalent.
`foo[9223372036854775807]`,
`foo[9223372036854775807]`,
true,
},
{
`foo[3.14159265358979323846264338327950288419716939937510582097494459]`,
`foo[3.14159265358979323846264338327950288419716939937510582097494459]`,
true,
},
// HCL and cty also have some numeric comparison quirks with floats
// that lack an exact base-2 representation and zero vs. negative zero,
// but those quirks can't arise from parsing a traversal -- only from
// dynamic expression evaluation -- so we don't need to (and cannot)
// check them here.
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s ≡ %s", test.A, test.B), func(t *testing.T) {
a, diags := hclsyntax.ParseTraversalAbs([]byte(test.A), "", hcl.InitialPos)
if diags.HasErrors() {
t.Fatalf("input A has invalid syntax: %s", diags.Error())
}
b, diags := hclsyntax.ParseTraversalAbs([]byte(test.B), "", hcl.InitialPos)
if diags.HasErrors() {
t.Fatalf("input B has invalid syntax: %s", diags.Error())
}
got := TraversalsEquivalent(a, b)
if want := test.Equivalent; got != want {
t.Errorf("wrong result\ninput A: %s\ninput B: %s\ngot: %t\nwant: %t", test.A, test.B, got, want)
}
})
}
}

View File

@ -6,7 +6,6 @@
package jsonconfig
import (
"bytes"
"encoding/json"
"fmt"
@ -59,7 +58,7 @@ func marshalExpression(ex hcl.Expression) expression {
// into parts until we end up at the smallest referenceable address.
remains := ref.Remaining
for len(remains) > 0 {
varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, traversalStr(remains)))
varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, addrs.TraversalStr(remains)))
remains = remains[:(len(remains) - 1)]
}
varString = append(varString, ref.Subject.String())
@ -157,31 +156,3 @@ func marshalExpressions(body hcl.Body, schema *configschema.Block) expressions {
return ret
}
// traversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
//
// This was copied (and simplified) from internal/command/views/json/diagnostic.go.
func traversalStr(traversal hcl.Traversal) string {
var buf bytes.Buffer
for _, step := range traversal {
switch tStep := step.(type) {
case hcl.TraverseRoot:
buf.WriteString(tStep.Name)
case hcl.TraverseAttr:
buf.WriteByte('.')
buf.WriteString(tStep.Name)
case hcl.TraverseIndex:
buf.WriteByte('[')
switch tStep.Key.Type() {
case cty.String:
buf.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
case cty.Number:
bf := tStep.Key.AsBigFloat()
buf.WriteString(bf.Text('g', 10))
}
buf.WriteByte(']')
}
}
return buf.String()
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/addrs"
)
@ -322,7 +323,9 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf
// of the configuration block declaration.
configured := map[string]hcl.Range{}
instanced := map[string]bool{}
// the set of providers with a for_each expression. Used to detect
// foot-guns
instanced := map[string]hcl.Expression{}
// the set of configuration_aliases defined in the required_providers
// block, with the fully qualified provider type.
@ -342,7 +345,7 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf
emptyConfigs[name] = pc.DeclRange
}
instanced[name] = len(pc.Instances) != 0
instanced[name] = pc.ForEach
}
if mod.ProviderRequirements != nil {
@ -486,8 +489,13 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf
})
}
isInstanced := instanced[providerName(passed.InParent.Name, passed.InParent.Alias)]
diags = diags.Extend(passed.InParent.InstanceValidation("module", isInstanced))
instanceExpr := instanced[providerName(passed.InParent.Name, passed.InParent.Alias)]
diags = diags.Extend(passed.InParent.InstanceValidation("module", instanceExpr != nil))
// We could theoretically check here if there are resources (ignoring data blocks) within this submodule graph.
// The foot-gun only exists in that scenario, but the complexity of differentiating at the moment is not worth it
if passed.InParent.KeyExpression != nil {
diags = diags.Extend(providerIterationIdenticalWarning("module", fmt.Sprintf("-target=%s", cfg.Path.Child(modCall.Name)), modCall.ForEach, instanceExpr))
}
}
}
// Validate that resources using provider keys are properly configured
@ -498,8 +506,15 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf
continue
}
isInstanced := instanced[providerName(r.ProviderConfigRef.Name, r.ProviderConfigRef.Alias)]
diags = diags.Extend(r.ProviderConfigRef.InstanceValidation("resource", isInstanced))
instanceExpr := instanced[providerName(r.ProviderConfigRef.Name, r.ProviderConfigRef.Alias)]
diags = diags.Extend(r.ProviderConfigRef.InstanceValidation("resource", instanceExpr != nil))
if r.ProviderConfigRef.KeyExpression != nil && r.Mode == addrs.ManagedResourceMode {
addr := fmt.Sprintf("%s.%s", r.Type, r.Name)
if !cfg.Path.IsRoot() {
addr = fmt.Sprintf("%s.%s", cfg.Path, addr)
}
diags = diags.Extend(providerIterationIdenticalWarning("resource", fmt.Sprintf("-target=%s", addr), r.ForEach, instanceExpr))
}
}
}
checkProviderKeys(mod.ManagedResources)
@ -831,3 +846,141 @@ func providerName(name, alias string) string {
}
return name
}
// See rfc/20240513-static-evaluation-providers.md for explicit logic and reasoning behind these comparisons
func providerIterationIdenticalWarning(blockType, target string, sourceExpr, instanceExpr hcl.Expression) hcl.Diagnostics {
if sourceExpr == nil || instanceExpr == nil {
return nil
}
if len(sourceExpr.Variables()) == 0 || len(instanceExpr.Variables()) == 0 {
return nil
}
if !providerIterationIdentical(sourceExpr, instanceExpr) {
return nil
}
// foot, meet gun
return hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Provider configuration for_each matches " + blockType,
Detail: fmt.Sprintf(
"This provider configuration uses the same for_each expression as a %s, which means that subsequent removal of elements from this collection would cause a planning error.\n\nOpenTofu relies on a provider instance to destroy resource instances that are associated with it, and so the provider instance must outlive all of its resource instances by at least one plan/apply round. For removal of instances to succeed in future you must structure the configuration so that the provider block's for_each expression can produce a superset of the instances of the resources associated with the provider configuration. Refer to the OpenTofu documentation for specific suggestions.\n\nTo destroy this object before removing the provider configuration, consider first performing a targeted destroy:\n tofu apply -destroy %s",
blockType, target,
),
Subject: sourceExpr.Range().Ptr(),
}}
}
// Compares two for_each statements to see if they are "identical". This is on a best-effort basis to help prevent foot-guns.
//
//nolint:funlen,gocognit,gocyclo,cyclop // just a lot of branches
func providerIterationIdentical(a, b hcl.Expression) bool {
if a == nil && b == nil {
return true
}
// Remove parenthesis
if ae, ok := a.(*hclsyntax.ParenthesesExpr); ok {
return providerIterationIdentical(ae.Expression, b)
}
if be, ok := b.(*hclsyntax.ParenthesesExpr); ok {
return providerIterationIdentical(a, be.Expression)
}
if ae, ok := a.(*hclsyntax.ObjectConsKeyExpr); ok {
return providerIterationIdentical(ae.Wrapped, b)
}
if be, ok := b.(*hclsyntax.ObjectConsKeyExpr); ok {
return providerIterationIdentical(a, be.Wrapped)
}
switch as := a.(type) {
case *hclsyntax.ScopeTraversalExpr:
if bs, bok := b.(*hclsyntax.ScopeTraversalExpr); bok {
return addrs.TraversalsEquivalent(as.Traversal, bs.Traversal)
}
case *hclsyntax.LiteralValueExpr:
if bs, bok := b.(*hclsyntax.LiteralValueExpr); bok {
return as.Val.Equals(bs.Val).True()
}
case *hclsyntax.RelativeTraversalExpr:
if bs, bok := b.(*hclsyntax.RelativeTraversalExpr); bok {
return addrs.TraversalsEquivalent(as.Traversal, bs.Traversal) &&
providerIterationIdentical(as.Source, bs.Source)
}
case *hclsyntax.FunctionCallExpr:
if bs, bok := b.(*hclsyntax.FunctionCallExpr); bok {
if as.Name == bs.Name && len(as.Args) == len(bs.Args) {
for i := range as.Args {
if !providerIterationIdentical(as.Args[i], bs.Args[i]) {
return false
}
}
return true
}
}
case *hclsyntax.ConditionalExpr:
if bs, bok := b.(*hclsyntax.ConditionalExpr); bok {
return providerIterationIdentical(as.Condition, bs.Condition) &&
providerIterationIdentical(as.TrueResult, bs.TrueResult) &&
providerIterationIdentical(as.FalseResult, bs.FalseResult)
}
case *hclsyntax.IndexExpr:
if bs, bok := b.(*hclsyntax.IndexExpr); bok {
return providerIterationIdentical(as.Collection, bs.Collection) &&
providerIterationIdentical(as.Key, bs.Key)
}
case *hclsyntax.TupleConsExpr:
if bs, bok := b.(*hclsyntax.TupleConsExpr); bok && len(as.Exprs) == len(bs.Exprs) {
for i := range as.Exprs {
if !providerIterationIdentical(as.Exprs[i], bs.Exprs[i]) {
return false
}
}
return true
}
case *hclsyntax.ObjectConsExpr:
if bs, bok := b.(*hclsyntax.ObjectConsExpr); bok && len(as.Items) == len(bs.Items) {
for i := range as.Items {
if !providerIterationIdentical(as.Items[i].KeyExpr, bs.Items[i].KeyExpr) ||
!providerIterationIdentical(as.Items[i].ValueExpr, bs.Items[i].ValueExpr) {
return false
}
}
return true
}
case *hclsyntax.ForExpr:
if bs, bok := b.(*hclsyntax.ForExpr); bok {
return as.KeyVar == bs.KeyVar &&
as.ValVar == bs.ValVar &&
providerIterationIdentical(as.CollExpr, bs.CollExpr) &&
providerIterationIdentical(as.KeyExpr, bs.KeyExpr) &&
providerIterationIdentical(as.ValExpr, bs.ValExpr) &&
providerIterationIdentical(as.CondExpr, bs.CondExpr) &&
as.Group == bs.Group
}
case *hclsyntax.BinaryOpExpr:
if bs, bok := b.(*hclsyntax.BinaryOpExpr); bok {
return providerIterationIdentical(as.LHS, bs.LHS) &&
as.Op == bs.Op &&
providerIterationIdentical(as.RHS, bs.RHS)
}
case *hclsyntax.UnaryOpExpr:
if bs, bok := b.(*hclsyntax.UnaryOpExpr); bok {
return as.Op == bs.Op &&
providerIterationIdentical(as.Val, bs.Val)
}
case *hclsyntax.TemplateExpr:
if bs, bok := b.(*hclsyntax.TemplateExpr); bok && len(as.Parts) == len(bs.Parts) {
for i := range as.Parts {
if !providerIterationIdentical(as.Parts[i], bs.Parts[i]) {
return false
}
}
return true
}
}
// As this is best effort, all other cases are ignored
return false
}

View File

@ -0,0 +1,68 @@
locals {
stuff = toset([])
bval = true
i = 1
}
provider "null" { # Constant, no warning
for_each = {"foo": "bar"}
alias = "plain"
}
resource "null_resource" "plain" {
for_each = {"foo": "bar"}
provider = null.plain[each.key]
}
provider "null" { # Constant, no warning
for_each = toset(["foo", "bar"])
alias = "toset"
}
resource "null_resource" "toset" {
for_each = toset(["foo", "bar"])
provider = null.toset[each.key]
}
provider "null" {
for_each = local.stuff
alias = "value"
}
resource "null_resource" "value" {
for_each = local.stuff
provider = null.value[each.key]
}
provider "null" {
for_each = local.stuff
alias = "parens"
}
resource "null_resource" "parens" {
for_each = (local.stuff)
provider = null.parens[each.key]
}
provider "null" {
for_each = local.bval ? {"foo": "bar"} : {}
alias = "cond"
}
resource "null_resource" "cond" {
for_each = local.bval ? {"foo": "bar"} : {}
provider = null.cond[each.key]
}
provider "null" {
for_each = {"foo": local.i + -2}
alias = "op"
}
resource "null_resource" "op" {
for_each = {"foo": local.i + -2}
provider = null.op[each.key]
}
provider "null" {
for_each = {for s in local.stuff : s => upper(s)}
alias = "for"
}
resource "null_resource" "for" {
for_each = {for s in local.stuff : s => upper(s)}
provider = null.for[each.key]
}

View File

@ -0,0 +1,5 @@
testdata/config-diagnostics/provider-foreach/main.tf:30,20-31: Provider configuration for_each matches resource; This provider configuration uses the same for_each expression as a resource, which means that subsequent removal of elements from this collection would cause a planning error.
testdata/config-diagnostics/provider-foreach/main.tf:39,20-33: Provider configuration for_each matches resource; This provider configuration uses the same for_each expression as a resource, which means that subsequent removal of elements from this collection would cause a planning error.
testdata/config-diagnostics/provider-foreach/main.tf:48,20-52: Provider configuration for_each matches resource; This provider configuration uses the same for_each expression as a resource, which means that subsequent removal of elements from this collection would cause a planning error.
testdata/config-diagnostics/provider-foreach/main.tf:57,20-41: Provider configuration for_each matches resource; This provider configuration uses the same for_each expression as a resource, which means that subsequent removal of elements from this collection would cause a planning error.
testdata/config-diagnostics/provider-foreach/main.tf:66,20-58: Provider configuration for_each matches resource; This provider configuration uses the same for_each expression as a resource, which means that subsequent removal of elements from this collection would cause a planning error.

View File

@ -262,6 +262,118 @@ run "remove_b" {
There are scenarios that module authors might think could work, but that we don't intend to support initially so that we can release an initial increment and then react to feedback.
##### The same expression for `for_each` in both a provider configuration and its associated resources
Someone encountering this feature for the first time is highly likely to try to write something like the following:
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
variable "aws_regions" {
type = map(object({
vpc_cidr_block = string
}))
}
provider "aws" {
alias = "by_region"
for_each = var.aws_regions
region = each.key
}
resource "aws_vpc" "example" {
for_each = var.aws_regions
provider = aws.by_region[each.key]
cidr_block = each.value.vpc_cidr_block
}
```
That example declares that each region represented by an element of `var.aws_regions` should have both an instance of the `hashicorp/aws` provider and an `aws_vpc.example` resource instance belonging to that provider.
This example would work during initial creation, and would support adding new elements to `var.aws_regions` later. However, this example is problematic if the operator would ever want to remove an element from `var.aws_regions`, because that would effectively remove both the resource instance and the provider instance that manages it at the same time. OpenTofu needs to use a provider instance to plan and apply the destruction of a resource instance, so the provider instance must always live for at least one more plan/apply round than the resource instances it is managing.
To draw attention to this trap, OpenTofu will detect when the `for_each` expression in the `provider` block seems too similar to the `for_each` expression in one of the `module` or `resource` blocks that refers to it and will produce a warning explaining this risk.
"Too similar" will initially be defined as follows:
- An expression that contains no references can never be "too similar" to any other expression, because the problem we're drawing attention to arises only when the two `for_each` arguments are based on the same source of data.
- The rest of the comparison rules depend on specific HCL expression nodes, evaluated recursively:
- An expression in parentheses is "too similar" to another expression without parentheses if the two expressions match another one of these rules.
- Two reference expressions (e.g. `var.foo["bar"]`), attribute accesses from another value, or indexes of another value with a constant expression, (all of which are "traversals" in HCL's model) are "too similar" if all of the traversal steps are equivalent.
"Equivalent" means that, for all steps of each traversal:
- The steps at a particular position in each traversal are of the same type.
- "Root" names (i.e. `var` in the example above) are character-for-character equal.
- Attribute steps (i.e. `.foo` in the example above) have names that are character-for-character equal.
- Index steps (i.e. `["bar"]` in the example above) have an index value that is equal by the same rules as for the `==` operator.
- Two literal value expressions are "too similar" if their results are equal by the same rules as for the `==` operator. (But note that this applies only when a literal value appears as part of a larger expression that also involves a reference.)
- Two function call expressions are "too similar" if the called function names are identical, if both calls have the same number of arguments, and the expressions given for the arguments are each also "too similar" after recursively applying these rules.
- Two conditional expressions are "too similar" if the predicate expression and the two result expressions are each "too similar" after recursively applying these rules.
- Two index expressions with dynamic key expressions are "too similar" if the expressions they are applied to are "too similar" and their key expressions are "too similar" after recursively applying these rules to both.
- Two tuple constructor or object constructor expressions are "too similar" if they are both of the same type, they have the same number of constituent element expressions, and each of those constituent expressions are "too similar" after recursively applying these rules.
- Two `for` expressions are "too similar" if their temporary key/value symbol names are the same, and the source collection expressions, result key expressions, result value expressions, and filter predicate expressions are each "too similar" after recursively applying these rules.
- Two binary operation expressions (e.g. `1 + 1`) are "too similar" if they both have the same operation and the left and right operand expressions of each operation are each "too similar" after recursively applying these rules.
- Two unary operation expressions (e.g. `-var.foo`) are "too similar" if they both have the same operation and the operand expressions of each operation are "too similar" after recursively applying these rules.
- Two template expressions are "too similar" if they have the same number of template parts and each of the template parts are "too similar" after recursively applying these rules.
- No other combinations are considered to be "too similar". This is a best-effort heuristic that does not intend to achieve full coverage of all possible expression types.
Although the problem only really affects managed resources, since they are the only object that needs provider envolvement to destroy, the warning _would_ appear for a call to a `module` block that does not contain any `resource` blocks because the warning is generated only based on syntax in the configuration layer, rather than at runtime, and so it cannot "see into" the child module.
Authors are expected to respond to this warning by somehow changing the expressions on the `module` and `resource` blocks to filter out a subset of the elements used with the `provider` block's `for_each` based on something in the source value. For example, an author might choose to use a null map element to represent that the provider instance should be declared but the resource instances must not, or might add an `enabled` attribute to the element object type which defaults to `true` and disables the resource instances if overridden to `false`. For example:
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
variable "aws_regions" {
type = map(object({
vpc_cidr_block = string
enabled = optional(bool, true)
}))
}
locals {
# enabled_regions includes only the elements of var.aws_regions
# where enabled = true, and thus resource instances should be
# declared.
enabled_regions = tomap({
for region, config in var.aws_regions : region => config
if config.enabled
})
}
provider "aws" {
alias = "by_region"
for_each = var.aws_regions
region = each.key
}
resource "aws_vpc" "example" {
for_each = local.enabled_regions
provider = aws.by_region[each.key]
cidr_block = each.value.vpc_cidr_block
}
```
In practice _any_ change to either of the expressions that causes them to no longer be considered "too similar" is sufficient to quiet the warning, regardless of whether the difference actually solves the problem the warning is describing. It's the module author's responsibility to ensure that their solution actually solves the problem.
(For some alternatives we considered and why we ultimately chose this path, refer to [Alternatives to the warning about `for_each` expressions being "too similar"](#alternatives-to-the-warning-about-for_each-expressions-being-too-similar).)
##### Provider references as normal values
```hcl
@ -705,3 +817,17 @@ Go the route of expanded modules/resources as detailed in [Static Module Expansi
- Not yet explored for resources
- Massive development and testing effort!
### Alternatives to the warning about `for_each` expressions being "too similar"
The behavior described in [The same expression for `for_each` in both a provider configuration and its associated resources](#the-same-expression-for-for_each-in-both-a-provider-configuration-and-its-associated-resources) is a compromise intended to allow module authors maximum flexibility, which comes at the expense of us therefore being unable to give strong guidance as to exactly how an author might solve the problem the warning describes.
We also considered various options that would involve being more opinionated about exactly how a module author should filter their input collection in `resource`/`module` block `for_each`, including but not limited to:
- Forcing the use of `null` element values to represent "disabled", providing a built-in function that automatically removes such elements from a given map, and then _requiring_ that function to be used in the `for_each` of any `resource`/`module` block associated with a multi-instance provider configuration.
- Introducing a function that takes a map and a set whose elements are a subset of the keys from the map, which returns a new map with those keys removed, and then _requiring_ that function to be used in the `for_each` of any `resource`/`module` block associated with a multi-instance provider configuration.
Both of these would both have a higher likelihood of a configuration meeting the requirements actually being correct, and would allow our error messages to be considerably more specific about what is required to solve the problem, but they also both force a particular module design strategy that is unlikely to match all module authors' tastes.
Ultimately we prefer to let module authors freely decide how to solve this problem, even at the risk of them accidentally writing something that is sufficient to quiet the warning but not actually sufficient to solve the problem the warning describes.
Anyone who addresses this problem incorrectly and thus ends up in the "trap" despite their efforts will be able to move forward by using `tofu destroy` with the `-target=...` option to force destroying the managed resource instances before removing the provider instance that manages them.