mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
Support for_each
syntax in import
block (#1492)
Signed-off-by: RLRabinowitz <rlrabinowitz2@gmail.com> Signed-off-by: Ronny Orot <ronny.orot@gmail.com> Co-authored-by: RLRabinowitz <rlrabinowitz2@gmail.com>
This commit is contained in:
parent
1f3db74281
commit
dcc1fa3b88
@ -16,6 +16,7 @@ STATE ENCRYPTION
|
||||
NEW FEATURES:
|
||||
* Add support for a `removed` block that allows users to remove resources or modules from the state without destroying them. ([#1158](https://github.com/opentofu/opentofu/pull/1158))
|
||||
* Provider-defined functions are now available. They may be referenced via `provider::<provider_name>::<funcname>(args)`. ([#1439](https://github.com/opentofu/opentofu/pull/1439))
|
||||
* Support `for_each` in `import` blocks ([#1492](https://github.com/opentofu/opentofu/pull/1492)
|
||||
|
||||
ENHANCEMENTS:
|
||||
* Added support to use `.tfvars` files from tests folder. ([#1386](https://github.com/opentofu/opentofu/pull/1386))
|
||||
|
@ -43,6 +43,8 @@ type Import struct {
|
||||
// import blocks targeting the same resource
|
||||
ResolvedTo *addrs.AbsResourceInstance
|
||||
|
||||
ForEach hcl.Expression
|
||||
|
||||
ProviderConfigRef *ProviderConfigRef
|
||||
Provider addrs.Provider
|
||||
|
||||
@ -93,6 +95,10 @@ func decodeImportBlock(block *hcl.Block) (*Import, hcl.Diagnostics) {
|
||||
diags = append(diags, providerDiags...)
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["for_each"]; exists {
|
||||
imp.ForEach = attr.Expr
|
||||
}
|
||||
|
||||
return imp, diags
|
||||
}
|
||||
|
||||
@ -109,6 +115,9 @@ var importBlockSchema = &hcl.BodySchema{
|
||||
Name: "to",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "for_each",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -11,9 +11,9 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/states"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
@ -107,12 +107,13 @@ func NewImportResolver() *ImportResolver {
|
||||
return &ImportResolver{imports: make(map[string]EvaluatedConfigImportTarget)}
|
||||
}
|
||||
|
||||
// ResolveImport resolves the ID and address (soon, when it will be necessary) of an ImportTarget originating
|
||||
// from an import block, when we have the context necessary to resolve them. The resolved import target would be an
|
||||
// EvaluatedConfigImportTarget.
|
||||
// This function mutates the EvalContext's ImportResolver, adding the resolved import target
|
||||
// The function errors if we failed to evaluate the ID or the address (soon)
|
||||
func (ri *ImportResolver) ResolveImport(importTarget *ImportTarget, ctx EvalContext) tfdiags.Diagnostics {
|
||||
// ExpandAndResolveImport is responsible for two operations:
|
||||
// 1. Expands the ImportTarget (originating from an import block) if it contains a 'for_each' attribute.
|
||||
// 2. Goes over the expanded imports and resolves the ID and address, when we have the context necessary to resolve
|
||||
// them. The resolved import target would be an EvaluatedConfigImportTarget.
|
||||
// This function mutates the EvalContext's ImportResolver, adding the resolved import target.
|
||||
// The function errors if we failed to evaluate the ID or the address.
|
||||
func (ri *ImportResolver) ExpandAndResolveImport(importTarget *ImportTarget, ctx EvalContext) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// The import block expressions are declared within the root module.
|
||||
@ -120,13 +121,52 @@ func (ri *ImportResolver) ResolveImport(importTarget *ImportTarget, ctx EvalCont
|
||||
// relative to the root module
|
||||
rootCtx := ctx.WithPath(addrs.RootModuleInstance)
|
||||
|
||||
importId, evalDiags := evaluateImportIdExpression(importTarget.Config.ID, rootCtx)
|
||||
if importTarget.Config.ForEach != nil {
|
||||
// The import target has a for_each attribute, so we need to expand it
|
||||
forEachVal, evalDiags := evaluateForEachExpressionValue(importTarget.Config.ForEach, rootCtx, false, true)
|
||||
diags = diags.Append(evalDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// We are building an instances.RepetitionData based on each for_each key and val combination
|
||||
var repetitions []instances.RepetitionData
|
||||
|
||||
it := forEachVal.ElementIterator()
|
||||
for it.Next() {
|
||||
k, v := it.Element()
|
||||
repetitions = append(repetitions, instances.RepetitionData{
|
||||
EachKey: k,
|
||||
EachValue: v,
|
||||
})
|
||||
}
|
||||
|
||||
for _, keyData := range repetitions {
|
||||
diags = diags.Append(ri.resolveImport(importTarget, rootCtx, keyData))
|
||||
}
|
||||
} else {
|
||||
// The import target is singular, no need to expand
|
||||
diags = diags.Append(ri.resolveImport(importTarget, rootCtx, EvalDataForNoInstanceKey))
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// resolveImport resolves the ID and address of an ImportTarget originating from an import block,
|
||||
// when we have the context necessary to resolve them. The resolved import target would be an
|
||||
// EvaluatedConfigImportTarget.
|
||||
// This function mutates the EvalContext's ImportResolver, adding the resolved import target.
|
||||
// The function errors if we failed to evaluate the ID or the address.
|
||||
func (ri *ImportResolver) resolveImport(importTarget *ImportTarget, ctx EvalContext, keyData instances.RepetitionData) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
importId, evalDiags := evaluateImportIdExpression(importTarget.Config.ID, ctx, keyData)
|
||||
diags = diags.Append(evalDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
importAddress, addressDiags := rootCtx.EvaluateImportAddress(importTarget.Config.To)
|
||||
importAddress, addressDiags := ctx.EvaluateImportAddress(importTarget.Config.To, keyData)
|
||||
diags = diags.Append(addressDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
@ -152,6 +192,12 @@ func (ri *ImportResolver) ResolveImport(importTarget *ImportTarget, ctx EvalCont
|
||||
ID: importId,
|
||||
}
|
||||
|
||||
if keyData == EvalDataForNoInstanceKey {
|
||||
log.Printf("[TRACE] importResolver: resolved a singular import target %s", importAddress)
|
||||
} else {
|
||||
log.Printf("[TRACE] importResolver: resolved an expanded import target %s", importAddress)
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
|
@ -4533,6 +4533,191 @@ import {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_importForEach(t *testing.T) {
|
||||
type ImportResult struct {
|
||||
ResolvedAddress string
|
||||
ResolvedId string
|
||||
}
|
||||
type TestConfiguration struct {
|
||||
Description string
|
||||
ImportResults []ImportResult
|
||||
inlineConfiguration map[string]string
|
||||
}
|
||||
configurations := []TestConfiguration{
|
||||
{
|
||||
Description: "valid map",
|
||||
ImportResults: []ImportResult{{ResolvedAddress: `test_object.a["key1"]`, ResolvedId: "val1"}, {ResolvedAddress: `test_object.a["key2"]`, ResolvedId: "val2"}, {ResolvedAddress: `test_object.a["key3"]`, ResolvedId: "val3"}},
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
map = {
|
||||
"key1" = "val1"
|
||||
"key2" = "val2"
|
||||
"key3" = "val3"
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = local.map
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "valid set",
|
||||
ImportResults: []ImportResult{{ResolvedAddress: `test_object.a["val0"]`, ResolvedId: "val0"}, {ResolvedAddress: `test_object.a["val1"]`, ResolvedId: "val1"}, {ResolvedAddress: `test_object.a["val2"]`, ResolvedId: "val2"}},
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
variable "set" {
|
||||
type = set(string)
|
||||
default = ["val0", "val1", "val2"]
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = var.set
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = var.set
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "valid tuple",
|
||||
ImportResults: []ImportResult{{ResolvedAddress: `module.mod[0].test_object.a["resKey1"]`, ResolvedId: "val1"}, {ResolvedAddress: `module.mod[0].test_object.a["resKey2"]`, ResolvedId: "val2"}, {ResolvedAddress: `module.mod[1].test_object.a["resKey1"]`, ResolvedId: "val3"}, {ResolvedAddress: `module.mod[1].test_object.a["resKey2"]`, ResolvedId: "val4"}},
|
||||
inlineConfiguration: map[string]string{
|
||||
"mod/main.tf": `
|
||||
variable "set" {
|
||||
type = set(string)
|
||||
default = ["resKey1", "resKey2"]
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = var.set
|
||||
}
|
||||
`,
|
||||
"main.tf": `
|
||||
locals {
|
||||
tuple = [
|
||||
{
|
||||
moduleKey = 0
|
||||
resourceKey = "resKey1"
|
||||
id = "val1"
|
||||
},
|
||||
{
|
||||
moduleKey = 0
|
||||
resourceKey = "resKey2"
|
||||
id = "val2"
|
||||
},
|
||||
{
|
||||
moduleKey = 1
|
||||
resourceKey = "resKey1"
|
||||
id = "val3"
|
||||
},
|
||||
{
|
||||
moduleKey = 1
|
||||
resourceKey = "resKey2"
|
||||
id = "val4"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
module "mod" {
|
||||
count = 2
|
||||
source = "./mod"
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.tuple
|
||||
id = each.value.id
|
||||
to = module.mod[each.value.moduleKey].test_object.a[each.value.resourceKey]
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, configuration := range configurations {
|
||||
t.Run(configuration.Description, func(t *testing.T) {
|
||||
m := testModuleInline(t, configuration.inlineConfiguration)
|
||||
p := simpleMockProvider()
|
||||
|
||||
hook := new(MockHook)
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Hooks: []Hook{hook},
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
p.ReadResourceResponse = &providers.ReadResourceResponse{
|
||||
NewState: cty.ObjectVal(map[string]cty.Value{}),
|
||||
}
|
||||
|
||||
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
|
||||
ImportedResources: []providers.ImportedResource{
|
||||
{
|
||||
TypeName: "test_object",
|
||||
State: cty.ObjectVal(map[string]cty.Value{}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
if len(plan.Changes.Resources) != len(configuration.ImportResults) {
|
||||
t.Fatalf("excpected %d resource chnages in the plan, got %d instead", len(configuration.ImportResults), len(plan.Changes.Resources))
|
||||
}
|
||||
|
||||
for _, importResult := range configuration.ImportResults {
|
||||
addr := mustResourceInstanceAddr(importResult.ResolvedAddress)
|
||||
|
||||
t.Run(addr.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(addr)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addr)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.NoOp; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if instPlan.Importing.ID != importResult.ResolvedId {
|
||||
t.Errorf("expected import change from \"%s\", got non-import change", importResult.ResolvedId)
|
||||
}
|
||||
|
||||
if !hook.PrePlanImportCalled {
|
||||
t.Fatalf("PostPlanImport hook not called")
|
||||
}
|
||||
|
||||
if !hook.PostPlanImportCalled {
|
||||
t.Fatalf("PostPlanImport hook not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
|
||||
type TestConfiguration struct {
|
||||
Description string
|
||||
|
@ -131,7 +131,7 @@ type EvalContext interface {
|
||||
|
||||
// EvaluateImportAddress takes the raw reference expression of the import address
|
||||
// from the config, and returns the evaluated address addrs.AbsResourceInstance
|
||||
EvaluateImportAddress(expr hcl.Expression) (addrs.AbsResourceInstance, tfdiags.Diagnostics)
|
||||
EvaluateImportAddress(expr hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics)
|
||||
|
||||
// EvaluationScope returns a scope that can be used to evaluate reference
|
||||
// addresses in this context.
|
||||
|
@ -404,8 +404,9 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r
|
||||
// The implementation is inspired by config.AbsTraversalForImportToExpr, but this time we can evaluate the expression
|
||||
// in the indexes of expressions. If we encounter a hclsyntax.IndexExpr, we can evaluate the Key expression and create
|
||||
// an Index Traversal, adding it to the Traverser
|
||||
func (ctx *BuiltinEvalContext) EvaluateImportAddress(expr hcl.Expression) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
|
||||
traversal, diags := ctx.traversalForImportExpr(expr)
|
||||
// TODO move this function into eval_import.go
|
||||
func (ctx *BuiltinEvalContext) EvaluateImportAddress(expr hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
|
||||
traversal, diags := ctx.traversalForImportExpr(expr, keyData)
|
||||
if diags.HasErrors() {
|
||||
return addrs.AbsResourceInstance{}, diags
|
||||
}
|
||||
@ -413,18 +414,18 @@ func (ctx *BuiltinEvalContext) EvaluateImportAddress(expr hcl.Expression) (addrs
|
||||
return addrs.ParseAbsResourceInstance(traversal)
|
||||
}
|
||||
|
||||
func (ctx *BuiltinEvalContext) traversalForImportExpr(expr hcl.Expression) (traversal hcl.Traversal, diags tfdiags.Diagnostics) {
|
||||
func (ctx *BuiltinEvalContext) traversalForImportExpr(expr hcl.Expression, keyData instances.RepetitionData) (traversal hcl.Traversal, diags tfdiags.Diagnostics) {
|
||||
switch e := expr.(type) {
|
||||
case *hclsyntax.IndexExpr:
|
||||
t, d := ctx.traversalForImportExpr(e.Collection)
|
||||
t, d := ctx.traversalForImportExpr(e.Collection, keyData)
|
||||
diags = diags.Append(d)
|
||||
traversal = append(traversal, t...)
|
||||
|
||||
tIndex, dIndex := ctx.parseImportIndexKeyExpr(e.Key)
|
||||
tIndex, dIndex := ctx.parseImportIndexKeyExpr(e.Key, keyData)
|
||||
diags = diags.Append(dIndex)
|
||||
traversal = append(traversal, tIndex)
|
||||
case *hclsyntax.RelativeTraversalExpr:
|
||||
t, d := ctx.traversalForImportExpr(e.Source)
|
||||
t, d := ctx.traversalForImportExpr(e.Source, keyData)
|
||||
diags = diags.Append(d)
|
||||
traversal = append(traversal, t...)
|
||||
traversal = append(traversal, e.Traversal...)
|
||||
@ -446,12 +447,13 @@ func (ctx *BuiltinEvalContext) traversalForImportExpr(expr hcl.Expression) (trav
|
||||
// import target address, into a traversal of type hcl.TraverseIndex.
|
||||
// After evaluation, the expression must be known, not null, not sensitive, and must be a string (for_each) or a number
|
||||
// (count)
|
||||
func (ctx *BuiltinEvalContext) parseImportIndexKeyExpr(expr hcl.Expression) (hcl.TraverseIndex, tfdiags.Diagnostics) {
|
||||
func (ctx *BuiltinEvalContext) parseImportIndexKeyExpr(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, tfdiags.Diagnostics) {
|
||||
idx := hcl.TraverseIndex{
|
||||
SrcRange: expr.Range(),
|
||||
}
|
||||
|
||||
val, diags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
|
||||
// evaluate and take into consideration the for_each key (if exists)
|
||||
val, diags := evaluateExprWithRepetitionData(ctx, expr, cty.DynamicPseudoType, keyData)
|
||||
if diags.HasErrors() {
|
||||
return idx, diags
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ func (c *MockEvalContext) EvaluateReplaceTriggeredBy(hcl.Expression, instances.R
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (c *MockEvalContext) EvaluateImportAddress(expression hcl.Expression) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
|
||||
func (c *MockEvalContext) EvaluateImportAddress(expression hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
|
||||
return addrs.AbsResourceInstance{}, nil
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ import (
|
||||
// returning an error if the count value is not known, and converting the
|
||||
// cty.Value to a map[string]cty.Value for compatibility with other calls.
|
||||
func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) {
|
||||
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, false)
|
||||
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, false, false)
|
||||
// forEachVal might be unknown, but if it is then there should already
|
||||
// be an error about it in diags, which we'll return below.
|
||||
|
||||
@ -40,7 +40,9 @@ func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach ma
|
||||
|
||||
// evaluateForEachExpressionValue is like evaluateForEachExpression
|
||||
// except that it returns a cty.Value map or set which can be unknown.
|
||||
func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) {
|
||||
// The 'allowTuple' argument is used to support evaluating for_each from tuple
|
||||
// values, and is currently supported when using for_each in import blocks.
|
||||
func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool, allowTuple bool) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType))
|
||||
|
||||
@ -86,6 +88,16 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
}
|
||||
ty := forEachVal.Type()
|
||||
|
||||
var isAllowedType bool
|
||||
var allowedTypesMessage string
|
||||
if allowTuple {
|
||||
isAllowedType = ty.IsMapType() || ty.IsSetType() || ty.IsObjectType() || ty.IsTupleType()
|
||||
allowedTypesMessage = "map, set of strings, or a tuple"
|
||||
} else {
|
||||
isAllowedType = ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()
|
||||
allowedTypesMessage = "map, or set of strings"
|
||||
}
|
||||
|
||||
const errInvalidUnknownDetailMap = "The \"for_each\" map includes keys derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
|
||||
const errInvalidUnknownDetailSet = "The \"for_each\" set includes values derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
|
||||
|
||||
@ -94,7 +106,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each argument",
|
||||
Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`,
|
||||
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A %s is allowed.`, allowedTypesMessage),
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
@ -123,11 +135,11 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
// ensure that we have a map, and not a DynamicValue
|
||||
return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), diags
|
||||
|
||||
case !(ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()):
|
||||
case !(isAllowedType):
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each argument",
|
||||
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type %s.`, ty.FriendlyName()),
|
||||
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a %s, and you have provided a value of type %s.`, allowedTypesMessage, ty.FriendlyName()),
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
@ -163,7 +175,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid for_each set argument",
|
||||
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
|
||||
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
|
||||
Subject: expr.Range().Ptr(),
|
||||
Expression: expr,
|
||||
EvalContext: hclCtx,
|
||||
|
@ -145,7 +145,7 @@ func TestEvaluateForEachExpression_errors(t *testing.T) {
|
||||
"set containing booleans": {
|
||||
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.BoolVal(true)})),
|
||||
"Invalid for_each set argument",
|
||||
"supports maps and sets of strings, but you have provided a set containing type bool",
|
||||
"supports sets of strings, but you have provided a set containing type bool",
|
||||
false, false,
|
||||
},
|
||||
"set containing null": {
|
||||
@ -217,13 +217,14 @@ func TestEvaluateForEachExpressionKnown(t *testing.T) {
|
||||
tests := map[string]hcl.Expression{
|
||||
"unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
|
||||
"unknown map": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
|
||||
"unknown tuple": hcltest.MockExprLiteral(cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Number, cty.Bool}))),
|
||||
}
|
||||
|
||||
for name, expr := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true)
|
||||
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true, true)
|
||||
|
||||
if len(diags) != 0 {
|
||||
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
||||
@ -235,3 +236,54 @@ func TestEvaluateForEachExpressionKnown(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateForEachExpressionValueTuple(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Expr hcl.Expression
|
||||
AllowTuple bool
|
||||
ExpectedError string
|
||||
}{
|
||||
"valid tuple": {
|
||||
Expr: hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
|
||||
AllowTuple: true,
|
||||
},
|
||||
"empty tuple": {
|
||||
Expr: hcltest.MockExprLiteral(cty.EmptyTupleVal),
|
||||
AllowTuple: true,
|
||||
},
|
||||
"null tuple": {
|
||||
Expr: hcltest.MockExprLiteral(cty.NullVal(cty.Tuple([]cty.Type{}))),
|
||||
AllowTuple: true,
|
||||
ExpectedError: "the given \"for_each\" argument value is null",
|
||||
},
|
||||
"sensitive tuple": {
|
||||
Expr: hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Sensitive)),
|
||||
AllowTuple: true,
|
||||
ExpectedError: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
|
||||
},
|
||||
"allow tuple is off": {
|
||||
Expr: hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
|
||||
AllowTuple: false,
|
||||
ExpectedError: "the \"for_each\" argument must be a map, or set of strings, and you have provided a value of type tuple.",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
_, diags := evaluateForEachExpressionValue(test.Expr, ctx, true, test.AllowTuple)
|
||||
|
||||
if test.ExpectedError == "" {
|
||||
if len(diags) != 0 {
|
||||
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
||||
}
|
||||
} else {
|
||||
if got, want := diags[0].Description().Detail, test.ExpectedError; test.ExpectedError != "" && !strings.Contains(got, want) {
|
||||
t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext) (string, tfdiags.Diagnostics) {
|
||||
func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext, keyData instances.RepetitionData) (string, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if expr == nil {
|
||||
@ -27,7 +28,8 @@ func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext) (string, t
|
||||
})
|
||||
}
|
||||
|
||||
importIdVal, evalDiags := ctx.EvaluateExpr(expr, cty.String, nil)
|
||||
// evaluate the import ID and take into consideration the for_each key (if exists)
|
||||
importIdVal, evalDiags := evaluateExprWithRepetitionData(ctx, expr, cty.String, keyData)
|
||||
diags = diags.Append(evalDiags)
|
||||
|
||||
if importIdVal.IsNull() {
|
||||
@ -73,3 +75,12 @@ func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext) (string, t
|
||||
|
||||
return importId, diags
|
||||
}
|
||||
|
||||
// evaluateExprWithRepetitionData takes the given HCL expression and evaluates
|
||||
// it to produce a value, while taking into consideration any repetition key
|
||||
// (a single combination of each.key and each.value of a for_each argument)
|
||||
// that should be a part of the scope.
|
||||
func evaluateExprWithRepetitionData(ctx EvalContext, expr hcl.Expression, wantType cty.Type, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) {
|
||||
scope := ctx.EvaluationScope(nil, nil, keyData)
|
||||
return scope.EvalExpr(expr, wantType)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcltest"
|
||||
"github.com/opentofu/opentofu/internal/lang"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
@ -18,6 +19,7 @@ import (
|
||||
func TestEvaluateImportIdExpression_SensitiveValue(t *testing.T) {
|
||||
ctx := &MockEvalContext{}
|
||||
ctx.installSimpleEval()
|
||||
ctx.EvaluationScopeScope = &lang.Scope{}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@ -53,7 +55,7 @@ func TestEvaluateImportIdExpression_SensitiveValue(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, diags := evaluateImportIdExpression(tc.expr, ctx)
|
||||
_, diags := evaluateImportIdExpression(tc.expr, ctx, EvalDataForNoInstanceKey)
|
||||
|
||||
if tc.wantErr != "" {
|
||||
if len(diags) != 1 {
|
||||
|
@ -243,7 +243,7 @@ func (n *nodeValidateModule) Execute(ctx EvalContext, op walkOperation) (diags t
|
||||
diags = diags.Append(countDiags)
|
||||
|
||||
case n.ModuleCall.ForEach != nil:
|
||||
_, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx, true)
|
||||
_, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx, true, false)
|
||||
diags = diags.Append(forEachDiags)
|
||||
}
|
||||
|
||||
|
@ -258,8 +258,9 @@ func (n *NodeAbstractResource) RootReferences() []*addrs.Reference {
|
||||
}
|
||||
|
||||
refs, _ := referencesInImportAddress(importTarget.Config.To)
|
||||
root = append(root, refs...)
|
||||
|
||||
// TODO - Add RootReferences of ForEach here later one, once for_each is added
|
||||
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ForEach)
|
||||
root = append(root, refs...)
|
||||
|
||||
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ID)
|
||||
|
@ -156,7 +156,7 @@ func (n *nodeExpandPlannableResource) DynamicExpand(ctx EvalContext) (*Graph, er
|
||||
var diags tfdiags.Diagnostics
|
||||
for _, importTarget := range n.importTargets {
|
||||
if importTarget.IsFromImportBlock() {
|
||||
err := importResolver.ResolveImport(importTarget, ctx)
|
||||
err := importResolver.ExpandAndResolveImport(importTarget, ctx)
|
||||
diags = diags.Append(err)
|
||||
}
|
||||
}
|
||||
|
@ -565,7 +565,7 @@ func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnost
|
||||
}
|
||||
|
||||
func validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
|
||||
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true)
|
||||
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true, false)
|
||||
// If the value isn't known then that's the best we can do for now, but
|
||||
// we'll check more thoroughly during the plan walk
|
||||
if !val.IsKnown() {
|
||||
|
Loading…
Reference in New Issue
Block a user