mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Add more tests and refactor import for_each (#1645)
Signed-off-by: Ronny Orot <ronny.orot@gmail.com>
This commit is contained in:
parent
a003782fec
commit
884410e63a
@ -122,8 +122,11 @@ func (ri *ImportResolver) ExpandAndResolveImport(importTarget *ImportTarget, ctx
|
||||
rootCtx := ctx.WithPath(addrs.RootModuleInstance)
|
||||
|
||||
if importTarget.Config.ForEach != nil {
|
||||
const unknownsNotAllowed = false
|
||||
const tupleAllowed = true
|
||||
|
||||
// The import target has a for_each attribute, so we need to expand it
|
||||
forEachVal, evalDiags := evaluateForEachExpressionValue(importTarget.Config.ForEach, rootCtx, false, true)
|
||||
forEachVal, evalDiags := evaluateForEachExpressionValue(importTarget.Config.ForEach, rootCtx, unknownsNotAllowed, tupleAllowed)
|
||||
diags = diags.Append(evalDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
@ -166,7 +169,7 @@ func (ri *ImportResolver) resolveImport(importTarget *ImportTarget, ctx EvalCont
|
||||
return diags
|
||||
}
|
||||
|
||||
importAddress, addressDiags := ctx.EvaluateImportAddress(importTarget.Config.To, keyData)
|
||||
importAddress, addressDiags := evaluateImportAddress(ctx, importTarget.Config.To, keyData)
|
||||
diags = diags.Append(addressDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
|
@ -4646,6 +4646,171 @@ import {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
|
||||
type TestConfiguration struct {
|
||||
Description string
|
||||
expectedError string
|
||||
inlineConfiguration map[string]string
|
||||
}
|
||||
configurations := []TestConfiguration{
|
||||
{
|
||||
Description: "To address index value is null",
|
||||
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
variable "index" {
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 1
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[var.index]
|
||||
id = "123"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "To address index is not a number or a string",
|
||||
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
index = toset(["foo"])
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = toset(["foo"])
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[local.index]
|
||||
id = "123"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "To address index value is sensitive",
|
||||
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
index = sensitive("foo")
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = toset(["foo"])
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[local.index]
|
||||
id = "123"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "To address index value will only be known after apply",
|
||||
expectedError: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "reference" {
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 1
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[test_object.reference.id]
|
||||
id = "123"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, configuration := range configurations {
|
||||
t.Run(configuration.Description, func(t *testing.T) {
|
||||
m := testModuleInline(t, configuration.inlineConfiguration)
|
||||
|
||||
providerSchema := &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"test_string": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := &MockProvider{
|
||||
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
|
||||
Provider: providers.Schema{Block: providerSchema},
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_object": providers.Schema{Block: providerSchema},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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{
|
||||
"test_string": cty.StringVal("foo"),
|
||||
}),
|
||||
}
|
||||
|
||||
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
||||
testStringVal := req.ProposedNewState.GetAttr("test_string")
|
||||
return providers.PlanResourceChangeResponse{
|
||||
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
||||
"test_string": testStringVal,
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
|
||||
ImportedResources: []providers.ImportedResource{
|
||||
{
|
||||
TypeName: "test_object",
|
||||
State: cty.ObjectVal(map[string]cty.Value{
|
||||
"test_string": cty.StringVal("foo"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
|
||||
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("succeeded; want errors")
|
||||
}
|
||||
if got, want := diags.Err().Error(), configuration.expectedError; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_importForEach(t *testing.T) {
|
||||
type ImportResult struct {
|
||||
ResolvedAddress string
|
||||
@ -4831,7 +4996,7 @@ import {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
|
||||
func TestContext2Plan_importWithInvalidForEach(t *testing.T) {
|
||||
type TestConfiguration struct {
|
||||
Description string
|
||||
expectedError string
|
||||
@ -4839,84 +5004,221 @@ func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
|
||||
}
|
||||
configurations := []TestConfiguration{
|
||||
{
|
||||
Description: "To address index value is null",
|
||||
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
|
||||
Description: "for_each value is null",
|
||||
expectedError: "Invalid import id argument: The import ID cannot be null",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
variable "index" {
|
||||
locals {
|
||||
map = {
|
||||
"key1" = null
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = local.map
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "for_each key is null",
|
||||
expectedError: "Null value as key: Can't use a null value as a key.",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
variable "nil" {
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
map = {
|
||||
(var.nil) = "val1"
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = local.map
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "for_each expression is null",
|
||||
expectedError: `Invalid for_each argument: The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, set of strings, or a tuple, and you have provided a value of type dynamic.`,
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
variable "map" {
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 1
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[var.index]
|
||||
id = "123"
|
||||
for_each = var.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "To address index is not a number or a string",
|
||||
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
index = toset(["foo"])
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = toset(["foo"])
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[local.index]
|
||||
id = "123"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "To address index value is sensitive",
|
||||
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
index = sensitive("foo")
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = toset(["foo"])
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[local.index]
|
||||
id = "123"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "To address index value will only be known after apply",
|
||||
expectedError: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
|
||||
Description: "for_each key is unknown",
|
||||
expectedError: `Invalid for_each argument: 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.`,
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "reference" {
|
||||
}
|
||||
|
||||
locals {
|
||||
map = {
|
||||
(test_object.reference.id) = "val1"
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 1
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
import {
|
||||
to = test_object.a[test_object.reference.id]
|
||||
id = "123"
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "for_each value is unknown",
|
||||
expectedError: `Invalid import id argument: The import block "id" argument depends on resource attributes that cannot be determined until apply, so OpenTofu cannot plan to import this resource.`,
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "reference" {
|
||||
}
|
||||
|
||||
locals {
|
||||
map = {
|
||||
"key1" = (test_object.reference.id)
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 1
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "for_each expression is unknown",
|
||||
expectedError: `Invalid for_each argument: 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.`,
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "reference" {
|
||||
}
|
||||
|
||||
locals {
|
||||
map = (test_object.reference.id)
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 1
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "for_each value is sensitive",
|
||||
expectedError: "Invalid import id argument: The import ID cannot be sensitive.",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
index = sensitive("foo")
|
||||
map = {
|
||||
"key1" = local.index
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
for_each = local.map
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "for_each key is sensitive",
|
||||
expectedError: "Invalid for_each argument: Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
index = sensitive("foo")
|
||||
map = {
|
||||
(local.index) = "val1"
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 1
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "for_each expression is sensitive",
|
||||
expectedError: "Invalid for_each argument: Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
|
||||
inlineConfiguration: map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "reference" {
|
||||
}
|
||||
|
||||
locals {
|
||||
map = sensitive({
|
||||
"key1" = "val1"
|
||||
})
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
count = 0
|
||||
}
|
||||
|
||||
import {
|
||||
for_each = local.map
|
||||
to = test_object.a[each.key]
|
||||
id = each.value
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
@ -129,10 +129,6 @@ type EvalContext interface {
|
||||
// indicating if that reference forces replacement.
|
||||
EvaluateReplaceTriggeredBy(expr hcl.Expression, repData instances.RepetitionData) (*addrs.Reference, bool, tfdiags.Diagnostics)
|
||||
|
||||
// EvaluateImportAddress takes the raw reference expression of the import address
|
||||
// from the config, and returns the evaluated address addrs.AbsResourceInstance
|
||||
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.
|
||||
EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
|
||||
@ -22,7 +21,6 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/opentofu/opentofu/internal/provisioners"
|
||||
@ -409,110 +407,6 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r
|
||||
return ref, replace, diags
|
||||
}
|
||||
|
||||
// EvaluateImportAddress takes the raw reference expression of the import address
|
||||
// from the config, and returns the evaluated address addrs.AbsResourceInstance
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
|
||||
return addrs.ParseAbsResourceInstance(traversal)
|
||||
}
|
||||
|
||||
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, keyData)
|
||||
diags = diags.Append(d)
|
||||
traversal = append(traversal, t...)
|
||||
|
||||
tIndex, dIndex := ctx.parseImportIndexKeyExpr(e.Key, keyData)
|
||||
diags = diags.Append(dIndex)
|
||||
traversal = append(traversal, tIndex)
|
||||
case *hclsyntax.RelativeTraversalExpr:
|
||||
t, d := ctx.traversalForImportExpr(e.Source, keyData)
|
||||
diags = diags.Append(d)
|
||||
traversal = append(traversal, t...)
|
||||
traversal = append(traversal, e.Traversal...)
|
||||
case *hclsyntax.ScopeTraversalExpr:
|
||||
traversal = append(traversal, e.Traversal...)
|
||||
default:
|
||||
// This should not happen, as it should have failed validation earlier, in config.AbsTraversalForImportToExpr
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid import address expression",
|
||||
Detail: "Import address must be a reference to a resource's address, and only allows for indexing with dynamic keys. For example: module.my_module[expression1].aws_s3_bucket.my_buckets[expression2] for resources inside of modules, or simply aws_s3_bucket.my_bucket for a resource in the root module",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseImportIndexKeyExpr parses an expression that is used as a key in an index, of an HCL expression representing an
|
||||
// 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, keyData instances.RepetitionData) (hcl.TraverseIndex, tfdiags.Diagnostics) {
|
||||
idx := hcl.TraverseIndex{
|
||||
SrcRange: expr.Range(),
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if !val.IsKnown() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return idx, diags
|
||||
}
|
||||
|
||||
if val.IsNull() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return idx, diags
|
||||
}
|
||||
|
||||
if val.Type() != cty.String && val.Type() != cty.Number {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return idx, diags
|
||||
}
|
||||
|
||||
unmarkedVal, valMarks := val.Unmark()
|
||||
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
idx.Key = unmarkedVal
|
||||
return idx, diags
|
||||
}
|
||||
|
||||
func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope {
|
||||
if !ctx.pathSet {
|
||||
panic("context path not set")
|
||||
|
@ -275,10 +275,6 @@ func (c *MockEvalContext) EvaluateReplaceTriggeredBy(hcl.Expression, instances.R
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (c *MockEvalContext) EvaluateImportAddress(expression hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
|
||||
return addrs.AbsResourceInstance{}, nil
|
||||
}
|
||||
|
||||
// installSimpleEval is a helper to install a simple mock implementation of
|
||||
// both EvaluateBlock and EvaluateExpr into the receiver.
|
||||
//
|
||||
|
@ -26,7 +26,9 @@ 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, false)
|
||||
const unknownsNotAllowed = false
|
||||
const tupleNotAllowed = false
|
||||
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, unknownsNotAllowed, tupleNotAllowed)
|
||||
// forEachVal might be unknown, but if it is then there should already
|
||||
// be an error about it in diags, which we'll return below.
|
||||
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
@ -84,3 +86,110 @@ func evaluateExprWithRepetitionData(ctx EvalContext, expr hcl.Expression, wantTy
|
||||
scope := ctx.EvaluationScope(nil, nil, keyData)
|
||||
return scope.EvalExpr(expr, wantType)
|
||||
}
|
||||
|
||||
// EvaluateImportAddress takes the raw reference expression of the import address
|
||||
// from the config, and returns the evaluated address addrs.AbsResourceInstance
|
||||
//
|
||||
// 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 evaluateImportAddress(ctx EvalContext, expr hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
|
||||
traversal, diags := traversalForImportExpr(ctx, expr, keyData)
|
||||
if diags.HasErrors() {
|
||||
return addrs.AbsResourceInstance{}, diags
|
||||
}
|
||||
|
||||
return addrs.ParseAbsResourceInstance(traversal)
|
||||
}
|
||||
|
||||
func traversalForImportExpr(ctx EvalContext, expr hcl.Expression, keyData instances.RepetitionData) (hcl.Traversal, tfdiags.Diagnostics) {
|
||||
var traversal hcl.Traversal
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
switch e := expr.(type) {
|
||||
case *hclsyntax.IndexExpr:
|
||||
t, d := traversalForImportExpr(ctx, e.Collection, keyData)
|
||||
diags = diags.Append(d)
|
||||
traversal = append(traversal, t...)
|
||||
|
||||
tIndex, dIndex := parseImportIndexKeyExpr(ctx, e.Key, keyData)
|
||||
diags = diags.Append(dIndex)
|
||||
traversal = append(traversal, tIndex)
|
||||
case *hclsyntax.RelativeTraversalExpr:
|
||||
t, d := traversalForImportExpr(ctx, e.Source, keyData)
|
||||
diags = diags.Append(d)
|
||||
traversal = append(traversal, t...)
|
||||
traversal = append(traversal, e.Traversal...)
|
||||
case *hclsyntax.ScopeTraversalExpr:
|
||||
traversal = append(traversal, e.Traversal...)
|
||||
default:
|
||||
// This should not happen, as it should have failed validation earlier, in config.AbsTraversalForImportToExpr
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid import address expression",
|
||||
Detail: "Import address must be a reference to a resource's address, and only allows for indexing with dynamic keys. For example: module.my_module[expression1].aws_s3_bucket.my_buckets[expression2] for resources inside of modules, or simply aws_s3_bucket.my_bucket for a resource in the root module",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return traversal, diags
|
||||
}
|
||||
|
||||
// parseImportIndexKeyExpr parses an expression that is used as a key in an index, of an HCL expression representing an
|
||||
// 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 parseImportIndexKeyExpr(ctx EvalContext, expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, tfdiags.Diagnostics) {
|
||||
idx := hcl.TraverseIndex{
|
||||
SrcRange: expr.Range(),
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if !val.IsKnown() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return idx, diags
|
||||
}
|
||||
|
||||
if val.IsNull() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return idx, diags
|
||||
}
|
||||
|
||||
if val.Type() != cty.String && val.Type() != cty.Number {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return idx, diags
|
||||
}
|
||||
|
||||
unmarkedVal, valMarks := val.Unmark()
|
||||
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Import block 'to' address contains an invalid key",
|
||||
Detail: "Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
idx.Key = unmarkedVal
|
||||
return idx, diags
|
||||
}
|
||||
|
@ -258,7 +258,9 @@ 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, false)
|
||||
const unknownsAllowed = true
|
||||
const tupleNotAllowed = false
|
||||
_, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx, unknownsAllowed, tupleNotAllowed)
|
||||
diags = diags.Append(forEachDiags)
|
||||
}
|
||||
|
||||
|
@ -565,7 +565,10 @@ 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, false)
|
||||
const unknownsAllowed = true
|
||||
const tupleNotAllowed = false
|
||||
|
||||
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, unknownsAllowed, tupleNotAllowed)
|
||||
// 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