mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-24 16:10:46 -06:00
Resource for_each
This commit is contained in:
parent
16fa18c139
commit
7d905f6777
12
addrs/for_each_attr.go
Normal file
12
addrs/for_each_attr.go
Normal file
@ -0,0 +1,12 @@
|
||||
package addrs
|
||||
|
||||
// ForEachAttr is the address of an attribute referencing the current "for_each" object in
|
||||
// the interpolation scope, addressed using the "each" keyword, ex. "each.key" and "each.value"
|
||||
type ForEachAttr struct {
|
||||
referenceable
|
||||
Name string
|
||||
}
|
||||
|
||||
func (f ForEachAttr) String() string {
|
||||
return "each." + f.Name
|
||||
}
|
@ -85,6 +85,14 @@ func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
|
||||
Remaining: remain,
|
||||
}, diags
|
||||
|
||||
case "each":
|
||||
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
||||
return &Reference{
|
||||
Subject: ForEachAttr{Name: name},
|
||||
SourceRange: tfdiags.SourceRangeFromHCL(rng),
|
||||
Remaining: remain,
|
||||
}, diags
|
||||
|
||||
case "data":
|
||||
if len(traversal) < 3 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
|
@ -64,6 +64,52 @@ func TestParseRef(t *testing.T) {
|
||||
`The "count" object does not support this operation.`,
|
||||
},
|
||||
|
||||
// each
|
||||
{
|
||||
`each.key`,
|
||||
&Reference{
|
||||
Subject: ForEachAttr{
|
||||
Name: "key",
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`each.value.blah`,
|
||||
&Reference{
|
||||
Subject: ForEachAttr{
|
||||
Name: "value",
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10},
|
||||
},
|
||||
Remaining: hcl.Traversal{
|
||||
hcl.TraverseAttr{
|
||||
Name: "blah",
|
||||
SrcRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 1, Column: 11, Byte: 10},
|
||||
End: hcl.Pos{Line: 1, Column: 16, Byte: 15},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`each`,
|
||||
nil,
|
||||
`The "each" object cannot be accessed directly. Instead, access one of its attributes.`,
|
||||
},
|
||||
{
|
||||
`each["hello"]`,
|
||||
nil,
|
||||
`The "each" object does not support this operation.`,
|
||||
},
|
||||
// data
|
||||
{
|
||||
`data.external.foo`,
|
||||
|
@ -71,6 +71,10 @@ func (d analysisData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange
|
||||
return cty.UnknownVal(cty.Number), nil
|
||||
}
|
||||
|
||||
func (d analysisData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
return cty.DynamicVal, nil
|
||||
}
|
||||
|
||||
func (d analysisData) GetResourceInstance(instAddr addrs.ResourceInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
log.Printf("[TRACE] configupgrade: Determining type for %s", instAddr)
|
||||
addr := instAddr.Resource
|
||||
|
@ -109,6 +109,11 @@ func TestParserLoadConfigFileFailureMessages(t *testing.T) {
|
||||
hcl.DiagError,
|
||||
"Unsupported block type",
|
||||
},
|
||||
{
|
||||
"invalid-files/resource-count-and-for_each.tf",
|
||||
hcl.DiagError,
|
||||
`Invalid combination of "count" and "for_each"`,
|
||||
},
|
||||
{
|
||||
"invalid-files/resource-lifecycle-badbool.tf",
|
||||
hcl.DiagError,
|
||||
|
@ -111,13 +111,15 @@ func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
|
||||
|
||||
if attr, exists := content.Attributes["for_each"]; exists {
|
||||
r.ForEach = attr.Expr
|
||||
// We currently parse this, but don't yet do anything with it.
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reserved argument name in resource block",
|
||||
Detail: fmt.Sprintf("The name %q is reserved for use in a future version of Terraform.", attr.Name),
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
// Cannot have count and for_each on the same resource block
|
||||
if r.Count != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid combination of "count" and "for_each"`,
|
||||
Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`,
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["provider"]; exists {
|
||||
|
@ -1,3 +1,4 @@
|
||||
resource "test" "foo" {
|
||||
count = 2
|
||||
for_each = ["a"]
|
||||
}
|
@ -23,6 +23,7 @@ type Data interface {
|
||||
StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics
|
||||
|
||||
GetCountAttr(addrs.CountAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetForEachAttr(addrs.ForEachAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetResourceInstance(addrs.ResourceInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetLocalValue(addrs.LocalValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetModuleInstance(addrs.ModuleCallInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
type dataForTests struct {
|
||||
CountAttrs map[string]cty.Value
|
||||
ForEachAttrs map[string]cty.Value
|
||||
ResourceInstances map[string]cty.Value
|
||||
LocalValues map[string]cty.Value
|
||||
Modules map[string]cty.Value
|
||||
@ -26,6 +27,10 @@ func (d *dataForTests) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRang
|
||||
return d.CountAttrs[addr.Name], nil
|
||||
}
|
||||
|
||||
func (d *dataForTests) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
return d.ForEachAttrs[addr.Name], nil
|
||||
}
|
||||
|
||||
func (d *dataForTests) GetResourceInstance(addr addrs.ResourceInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
return d.ResourceInstances[addr.String()], nil
|
||||
}
|
||||
|
10
lang/eval.go
10
lang/eval.go
@ -203,6 +203,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
||||
pathAttrs := map[string]cty.Value{}
|
||||
terraformAttrs := map[string]cty.Value{}
|
||||
countAttrs := map[string]cty.Value{}
|
||||
forEachAttrs := map[string]cty.Value{}
|
||||
var self cty.Value
|
||||
|
||||
for _, ref := range refs {
|
||||
@ -334,6 +335,14 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
||||
self = val
|
||||
}
|
||||
|
||||
case addrs.ForEachAttr:
|
||||
val, valDiags := normalizeRefValue(s.Data.GetForEachAttr(subj, rng))
|
||||
diags = diags.Append(valDiags)
|
||||
forEachAttrs[subj.Name] = val
|
||||
if isSelf {
|
||||
self = val
|
||||
}
|
||||
|
||||
default:
|
||||
// Should never happen
|
||||
panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj))
|
||||
@ -350,6 +359,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
||||
vals["path"] = cty.ObjectVal(pathAttrs)
|
||||
vals["terraform"] = cty.ObjectVal(terraformAttrs)
|
||||
vals["count"] = cty.ObjectVal(countAttrs)
|
||||
vals["each"] = cty.ObjectVal(forEachAttrs)
|
||||
if self != cty.NilVal {
|
||||
vals["self"] = self
|
||||
}
|
||||
|
@ -20,6 +20,10 @@ func TestScopeEvalContext(t *testing.T) {
|
||||
CountAttrs: map[string]cty.Value{
|
||||
"index": cty.NumberIntVal(0),
|
||||
},
|
||||
ForEachAttrs: map[string]cty.Value{
|
||||
"key": cty.StringVal("a"),
|
||||
"value": cty.NumberIntVal(1),
|
||||
},
|
||||
ResourceInstances: map[string]cty.Value{
|
||||
"null_resource.foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr": cty.StringVal("bar"),
|
||||
@ -75,6 +79,22 @@ func TestScopeEvalContext(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
`each.key`,
|
||||
map[string]cty.Value{
|
||||
"each": cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.StringVal("a"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
`each.value`,
|
||||
map[string]cty.Value{
|
||||
"each": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NumberIntVal(1),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
`local.foo`,
|
||||
map[string]cty.Value{
|
||||
|
@ -3351,6 +3351,43 @@ func TestContext2Plan_countIncreaseWithSplatReference(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_forEach(t *testing.T) {
|
||||
m := testModule(t, "plan-for-each")
|
||||
p := testProvider("aws")
|
||||
p.DiffFn = testDiffFn
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
ProviderResolver: providers.ResolverFixed(
|
||||
map[string]providers.Factory{
|
||||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan()
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors: %s", diags.Err())
|
||||
}
|
||||
|
||||
schema := p.GetSchemaReturn.ResourceTypes["aws_instance"]
|
||||
ty := schema.ImpliedType()
|
||||
|
||||
if len(plan.Changes.Resources) != 8 {
|
||||
t.Fatal("expected 8 changes, got", len(plan.Changes.Resources))
|
||||
}
|
||||
|
||||
for _, res := range plan.Changes.Resources {
|
||||
if res.Action != plans.Create {
|
||||
t.Fatalf("expected resource creation, got %s", res.Action)
|
||||
}
|
||||
_, err := res.Decode(ty)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestContext2Plan_destroy(t *testing.T) {
|
||||
m := testModule(t, "plan-destroy")
|
||||
p := testProvider("aws")
|
||||
|
@ -61,7 +61,8 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
|
||||
configVal := cty.NullVal(cty.DynamicPseudoType)
|
||||
if n.Config != nil {
|
||||
var configDiags tfdiags.Diagnostics
|
||||
keyData := EvalDataForInstanceKey(n.Addr.Key)
|
||||
forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
|
||||
keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
|
||||
configVal, _, configDiags = ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData)
|
||||
diags = diags.Append(configDiags)
|
||||
if configDiags.HasErrors() {
|
||||
@ -548,7 +549,8 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*configs.Provisio
|
||||
provisioner := ctx.Provisioner(prov.Type)
|
||||
schema := ctx.ProvisionerSchema(prov.Type)
|
||||
|
||||
keyData := EvalDataForInstanceKey(instanceAddr.Key)
|
||||
// TODO the for_each val is not added here, which might causes issues with provisioners
|
||||
keyData := EvalDataForInstanceKey(instanceAddr.Key, nil)
|
||||
|
||||
// Evaluate the main provisioner configuration.
|
||||
config, _, configDiags := ctx.EvaluateBlock(prov.Config, schema, instanceAddr, keyData)
|
||||
|
@ -133,7 +133,8 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
|
||||
// Should be caught during validation, so we don't bother with a pretty error here
|
||||
return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
|
||||
}
|
||||
keyData := EvalDataForInstanceKey(n.Addr.Key)
|
||||
forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
|
||||
keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
|
||||
configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
||||
diags = diags.Append(configDiags)
|
||||
if configDiags.HasErrors() {
|
||||
|
73
terraform/eval_for_each.go
Normal file
73
terraform/eval_for_each.go
Normal file
@ -0,0 +1,73 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// evaluateResourceForEachExpression interprets a "for_each" argument on a resource.
|
||||
//
|
||||
// Returns a cty.Value map, and diagnostics if necessary. It will return nil if
|
||||
// the expression is nil, and is used to distinguish between an unset for_each and an
|
||||
// empty map
|
||||
func evaluateResourceForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) {
|
||||
if expr == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
forEachVal, forEachDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
|
||||
diags = diags.Append(forEachDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// No-op for dynamic types, so that these pass validation, but are then populated at apply
|
||||
if forEachVal.Type() == cty.DynamicPseudoType {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if forEachVal.IsNull() {
|
||||
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.`,
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if !forEachVal.CanIterateElements() || forEachVal.Type().IsListType() {
|
||||
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.`, forEachVal.Type().FriendlyName()),
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if forEachVal.Type().IsSetType() {
|
||||
if forEachVal.Type().ElementType() != cty.String {
|
||||
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()),
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
}
|
||||
|
||||
// If the map is empty ({}), return an empty map, because cty will return nil when representing {} AsValueMap
|
||||
// Also return an empty map if the value is not known -- as this function
|
||||
// is used to check if the for_each value is valid as well as to apply it, the empty
|
||||
// map will later be filled in.
|
||||
if !forEachVal.IsKnown() || forEachVal.LengthInt() == 0 {
|
||||
return map[string]cty.Value{}, diags
|
||||
}
|
||||
|
||||
return forEachVal.AsValueMap(), nil
|
||||
}
|
@ -95,7 +95,8 @@ func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
|
||||
objTy := schema.ImpliedType()
|
||||
priorVal := cty.NullVal(objTy) // for data resources, prior is always null because we start fresh every time
|
||||
|
||||
keyData := EvalDataForInstanceKey(n.Addr.Key)
|
||||
forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
|
||||
keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
|
||||
|
||||
var configDiags tfdiags.Diagnostics
|
||||
configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
||||
|
@ -370,6 +370,17 @@ func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) {
|
||||
diags = diags.Append(countDiags)
|
||||
}
|
||||
|
||||
if n.Config.ForEach != nil {
|
||||
keyData = InstanceKeyEvalData{
|
||||
EachKey: cty.UnknownVal(cty.String),
|
||||
EachValue: cty.UnknownVal(cty.DynamicPseudoType),
|
||||
}
|
||||
|
||||
// Evaluate the for_each expression here so we can expose the diagnostics
|
||||
_, forEachDiags := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
|
||||
diags = diags.Append(forEachDiags)
|
||||
}
|
||||
|
||||
for _, traversal := range n.Config.DependsOn {
|
||||
ref, refDiags := addrs.ParseRef(traversal)
|
||||
diags = diags.Append(refDiags)
|
||||
|
@ -120,20 +120,24 @@ type InstanceKeyEvalData struct {
|
||||
|
||||
// EvalDataForInstanceKey constructs a suitable InstanceKeyEvalData for
|
||||
// evaluating in a context that has the given instance key.
|
||||
func EvalDataForInstanceKey(key addrs.InstanceKey) InstanceKeyEvalData {
|
||||
// At the moment we don't actually implement for_each, so we only
|
||||
// ever populate CountIndex.
|
||||
// (When we implement for_each later we may need to reorganize this some,
|
||||
// so that we can resolve the ambiguity that an int key may either be
|
||||
// a count.index or an each.key where for_each is over a list.)
|
||||
|
||||
func EvalDataForInstanceKey(key addrs.InstanceKey, forEachMap map[string]cty.Value) InstanceKeyEvalData {
|
||||
var countIdx cty.Value
|
||||
var eachKey cty.Value
|
||||
var eachVal cty.Value
|
||||
|
||||
if intKey, ok := key.(addrs.IntKey); ok {
|
||||
countIdx = cty.NumberIntVal(int64(intKey))
|
||||
}
|
||||
|
||||
if stringKey, ok := key.(addrs.StringKey); ok {
|
||||
eachKey = cty.StringVal(string(stringKey))
|
||||
eachVal = forEachMap[string(stringKey)]
|
||||
}
|
||||
|
||||
return InstanceKeyEvalData{
|
||||
CountIndex: countIdx,
|
||||
EachKey: eachKey,
|
||||
EachValue: eachVal,
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +177,37 @@ func (d *evaluationStateData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.Sou
|
||||
}
|
||||
}
|
||||
|
||||
func (d *evaluationStateData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
var returnVal cty.Value
|
||||
switch addr.Name {
|
||||
|
||||
case "key":
|
||||
returnVal = d.InstanceKeyData.EachKey
|
||||
case "value":
|
||||
returnVal = d.InstanceKeyData.EachValue
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid "each" attribute`,
|
||||
Detail: fmt.Sprintf(`The "each" object does not have an attribute named %q. The supported attributes are each.key and each.value, the current key and value pair of the "for_each" attribute set.`, addr.Name),
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
if returnVal == cty.NilVal {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Reference to "each" in context without for_each`,
|
||||
Detail: fmt.Sprintf(`The "each" object can be used only in "resource" blocks, and only when the "for_each" argument is set.`),
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return cty.UnknownVal(cty.DynamicPseudoType), diags
|
||||
}
|
||||
return returnVal, diags
|
||||
}
|
||||
|
||||
func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
@ -569,7 +604,7 @@ func (d *evaluationStateData) GetResourceInstance(addr addrs.ResourceInstance, r
|
||||
}
|
||||
case states.EachMap:
|
||||
multi = key == addrs.NoKey
|
||||
if _, ok := addr.Key.(addrs.IntKey); !multi && !ok {
|
||||
if _, ok := addr.Key.(addrs.StringKey); !multi && !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid resource index",
|
||||
|
@ -247,6 +247,51 @@ func TestPlanGraphBuilder_targetModule(t *testing.T) {
|
||||
testGraphNotContains(t, g, "module.child1.test_object.foo")
|
||||
}
|
||||
|
||||
func TestPlanGraphBuilder_forEach(t *testing.T) {
|
||||
awsProvider := &MockProvider{
|
||||
GetSchemaReturn: &ProviderSchema{
|
||||
Provider: simpleTestSchema(),
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"aws_instance": simpleTestSchema(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
components := &basicComponentFactory{
|
||||
providers: map[string]providers.Factory{
|
||||
"aws": providers.FactoryFixed(awsProvider),
|
||||
},
|
||||
}
|
||||
|
||||
b := &PlanGraphBuilder{
|
||||
Config: testModule(t, "plan-for-each"),
|
||||
Components: components,
|
||||
Schemas: &Schemas{
|
||||
Providers: map[string]*ProviderSchema{
|
||||
"aws": awsProvider.GetSchemaReturn,
|
||||
},
|
||||
},
|
||||
DisableReduce: true,
|
||||
}
|
||||
|
||||
g, err := b.Build(addrs.RootModuleInstance)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if g.Path.String() != addrs.RootModuleInstance.String() {
|
||||
t.Fatalf("wrong module path %q", g.Path)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
// We're especially looking for the edge here, where aws_instance.bat
|
||||
// has a dependency on aws_instance.boo
|
||||
expected := strings.TrimSpace(testPlanGraphBuilderForEachStr)
|
||||
if actual != expected {
|
||||
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
const testPlanGraphBuilderStr = `
|
||||
aws_instance.web
|
||||
aws_security_group.firewall
|
||||
@ -290,3 +335,34 @@ root
|
||||
provider.openstack (close)
|
||||
var.foo
|
||||
`
|
||||
const testPlanGraphBuilderForEachStr = `
|
||||
aws_instance.bar
|
||||
provider.aws
|
||||
aws_instance.bat
|
||||
aws_instance.boo
|
||||
provider.aws
|
||||
aws_instance.baz
|
||||
provider.aws
|
||||
aws_instance.boo
|
||||
provider.aws
|
||||
aws_instance.foo
|
||||
provider.aws
|
||||
meta.count-boundary (EachMode fixup)
|
||||
aws_instance.bar
|
||||
aws_instance.bat
|
||||
aws_instance.baz
|
||||
aws_instance.boo
|
||||
aws_instance.foo
|
||||
provider.aws
|
||||
provider.aws
|
||||
provider.aws (close)
|
||||
aws_instance.bar
|
||||
aws_instance.bat
|
||||
aws_instance.baz
|
||||
aws_instance.boo
|
||||
aws_instance.foo
|
||||
provider.aws
|
||||
root
|
||||
meta.count-boundary (EachMode fixup)
|
||||
provider.aws (close)
|
||||
`
|
||||
|
@ -187,6 +187,8 @@ func (n *NodeAbstractResource) References() []*addrs.Reference {
|
||||
|
||||
refs, _ := lang.ReferencesInExpr(c.Count)
|
||||
result = append(result, refs...)
|
||||
refs, _ = lang.ReferencesInExpr(c.ForEach)
|
||||
result = append(result, refs...)
|
||||
refs, _ = lang.ReferencesInBlock(c.Config, n.Schema)
|
||||
result = append(result, refs...)
|
||||
if c.Managed != nil {
|
||||
|
@ -101,13 +101,6 @@ func (n *NodeApplyableResourceInstance) References() []*addrs.Reference {
|
||||
func (n *NodeApplyableResourceInstance) EvalTree() EvalNode {
|
||||
addr := n.ResourceInstanceAddr()
|
||||
|
||||
// State still uses legacy-style internal ids, so we need to shim to get
|
||||
// a suitable key to use.
|
||||
stateId := NewLegacyResourceInstanceAddress(addr).stateId()
|
||||
|
||||
// Determine the dependencies for the state.
|
||||
stateDeps := n.StateReferences()
|
||||
|
||||
if n.Config == nil {
|
||||
// This should not be possible, but we've got here in at least one
|
||||
// case as discussed in the following issue:
|
||||
@ -132,15 +125,15 @@ func (n *NodeApplyableResourceInstance) EvalTree() EvalNode {
|
||||
// Eval info is different depending on what kind of resource this is
|
||||
switch n.Config.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
return n.evalTreeManagedResource(addr, stateId, stateDeps)
|
||||
return n.evalTreeManagedResource(addr)
|
||||
case addrs.DataResourceMode:
|
||||
return n.evalTreeDataResource(addr, stateId, stateDeps)
|
||||
return n.evalTreeDataResource(addr)
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode))
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NodeApplyableResourceInstance) evalTreeDataResource(addr addrs.AbsResourceInstance, stateId string, stateDeps []addrs.Referenceable) EvalNode {
|
||||
func (n *NodeApplyableResourceInstance) evalTreeDataResource(addr addrs.AbsResourceInstance) EvalNode {
|
||||
var provider providers.Interface
|
||||
var providerSchema *ProviderSchema
|
||||
var change *plans.ResourceInstanceChange
|
||||
@ -206,7 +199,7 @@ func (n *NodeApplyableResourceInstance) evalTreeDataResource(addr addrs.AbsResou
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsResourceInstance, stateId string, stateDeps []addrs.Referenceable) EvalNode {
|
||||
func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsResourceInstance) EvalNode {
|
||||
// Declare a bunch of variables that are used for state during
|
||||
// evaluation. Most of this are written to by-address below.
|
||||
var provider providers.Interface
|
||||
|
@ -77,6 +77,11 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
||||
return nil, diags.Err()
|
||||
}
|
||||
|
||||
forEachMap, forEachDiags := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
|
||||
if forEachDiags.HasErrors() {
|
||||
return nil, diags.Err()
|
||||
}
|
||||
|
||||
// Next we need to potentially rename an instance address in the state
|
||||
// if we're transitioning whether "count" is set at all.
|
||||
fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1)
|
||||
@ -119,18 +124,20 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
||||
|
||||
// Start creating the steps
|
||||
steps := []GraphTransformer{
|
||||
// Expand the count.
|
||||
// Expand the count or for_each (if present)
|
||||
&ResourceCountTransformer{
|
||||
Concrete: concreteResource,
|
||||
Schema: n.Schema,
|
||||
Count: count,
|
||||
ForEach: forEachMap,
|
||||
Addr: n.ResourceAddr(),
|
||||
},
|
||||
|
||||
// Add the count orphans
|
||||
// Add the count/for_each orphans
|
||||
&OrphanResourceCountTransformer{
|
||||
Concrete: concreteResourceOrphan,
|
||||
Count: count,
|
||||
ForEach: forEachMap,
|
||||
Addr: n.ResourceAddr(),
|
||||
State: state,
|
||||
},
|
||||
|
@ -34,25 +34,18 @@ var (
|
||||
func (n *NodePlannableResourceInstance) EvalTree() EvalNode {
|
||||
addr := n.ResourceInstanceAddr()
|
||||
|
||||
// State still uses legacy-style internal ids, so we need to shim to get
|
||||
// a suitable key to use.
|
||||
stateId := NewLegacyResourceInstanceAddress(addr).stateId()
|
||||
|
||||
// Determine the dependencies for the state.
|
||||
stateDeps := n.StateReferences()
|
||||
|
||||
// Eval info is different depending on what kind of resource this is
|
||||
switch addr.Resource.Resource.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
return n.evalTreeManagedResource(addr, stateId, stateDeps)
|
||||
return n.evalTreeManagedResource(addr)
|
||||
case addrs.DataResourceMode:
|
||||
return n.evalTreeDataResource(addr, stateId, stateDeps)
|
||||
return n.evalTreeDataResource(addr)
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode))
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NodePlannableResourceInstance) evalTreeDataResource(addr addrs.AbsResourceInstance, stateId string, stateDeps []addrs.Referenceable) EvalNode {
|
||||
func (n *NodePlannableResourceInstance) evalTreeDataResource(addr addrs.AbsResourceInstance) EvalNode {
|
||||
config := n.Config
|
||||
var provider providers.Interface
|
||||
var providerSchema *ProviderSchema
|
||||
@ -147,7 +140,7 @@ func (n *NodePlannableResourceInstance) evalTreeDataResource(addr addrs.AbsResou
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NodePlannableResourceInstance) evalTreeManagedResource(addr addrs.AbsResourceInstance, stateId string, stateDeps []addrs.Referenceable) EvalNode {
|
||||
func (n *NodePlannableResourceInstance) evalTreeManagedResource(addr addrs.AbsResourceInstance) EvalNode {
|
||||
config := n.Config
|
||||
var provider providers.Interface
|
||||
var providerSchema *ProviderSchema
|
||||
|
@ -39,6 +39,11 @@ func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph,
|
||||
return nil, diags.Err()
|
||||
}
|
||||
|
||||
forEachMap, forEachDiags := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
|
||||
if forEachDiags.HasErrors() {
|
||||
return nil, diags.Err()
|
||||
}
|
||||
|
||||
// Next we need to potentially rename an instance address in the state
|
||||
// if we're transitioning whether "count" is set at all.
|
||||
fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1)
|
||||
@ -66,6 +71,7 @@ func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph,
|
||||
Concrete: concreteResource,
|
||||
Schema: n.Schema,
|
||||
Count: count,
|
||||
ForEach: forEachMap,
|
||||
Addr: n.ResourceAddr(),
|
||||
},
|
||||
|
||||
@ -74,6 +80,7 @@ func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph,
|
||||
&OrphanResourceCountTransformer{
|
||||
Concrete: concreteResource,
|
||||
Count: count,
|
||||
ForEach: forEachMap,
|
||||
Addr: n.ResourceAddr(),
|
||||
State: state,
|
||||
},
|
||||
|
@ -365,6 +365,8 @@ func NewLegacyResourceInstanceAddress(addr addrs.AbsResourceInstance) *ResourceA
|
||||
ret.Index = -1
|
||||
} else if ik, ok := addr.Resource.Key.(addrs.IntKey); ok {
|
||||
ret.Index = int(ik)
|
||||
} else if _, ok := addr.Resource.Key.(addrs.StringKey); ok {
|
||||
ret.Index = -1
|
||||
} else {
|
||||
panic(fmt.Errorf("cannot shim resource instance with key %#v to legacy ResourceAddress.Index", addr.Resource.Key))
|
||||
}
|
||||
|
32
terraform/testdata/plan-for-each/main.tf
vendored
Normal file
32
terraform/testdata/plan-for-each/main.tf
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# maps
|
||||
resource "aws_instance" "foo" {
|
||||
for_each = {
|
||||
a = "thing"
|
||||
b = "another thing"
|
||||
c = "yet another thing"
|
||||
}
|
||||
num = "3"
|
||||
}
|
||||
|
||||
# sets
|
||||
resource "aws_instance" "bar" {
|
||||
for_each = toset(list("z", "y", "x"))
|
||||
}
|
||||
|
||||
# an empty map should generate no resource
|
||||
resource "aws_instance" "baz" {
|
||||
for_each = {}
|
||||
}
|
||||
|
||||
# references
|
||||
resource "aws_instance" "boo" {
|
||||
foo = aws_instance.foo["a"].num
|
||||
}
|
||||
|
||||
resource "aws_instance" "bat" {
|
||||
for_each = {
|
||||
my_key = aws_instance.boo.foo
|
||||
}
|
||||
foo = each.value
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/dag"
|
||||
"github.com/hashicorp/terraform/states"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// OrphanResourceCountTransformer is a GraphTransformer that adds orphans
|
||||
@ -18,9 +19,10 @@ import (
|
||||
type OrphanResourceCountTransformer struct {
|
||||
Concrete ConcreteResourceInstanceNodeFunc
|
||||
|
||||
Count int // Actual count of the resource, or -1 if count is not set at all
|
||||
Addr addrs.AbsResource // Addr of the resource to look for orphans
|
||||
State *states.State // Full global state
|
||||
Count int // Actual count of the resource, or -1 if count is not set at all
|
||||
ForEach map[string]cty.Value // The ForEach map on the resource
|
||||
Addr addrs.AbsResource // Addr of the resource to look for orphans
|
||||
State *states.State // Full global state
|
||||
}
|
||||
|
||||
func (t *OrphanResourceCountTransformer) Transform(g *Graph) error {
|
||||
@ -34,6 +36,10 @@ func (t *OrphanResourceCountTransformer) Transform(g *Graph) error {
|
||||
haveKeys[key] = struct{}{}
|
||||
}
|
||||
|
||||
// if for_each is set, use that transformer
|
||||
if t.ForEach != nil {
|
||||
return t.transformForEach(haveKeys, g)
|
||||
}
|
||||
if t.Count < 0 {
|
||||
return t.transformNoCount(haveKeys, g)
|
||||
}
|
||||
@ -43,6 +49,25 @@ func (t *OrphanResourceCountTransformer) Transform(g *Graph) error {
|
||||
return t.transformCount(haveKeys, g)
|
||||
}
|
||||
|
||||
func (t *OrphanResourceCountTransformer) transformForEach(haveKeys map[addrs.InstanceKey]struct{}, g *Graph) error {
|
||||
for key := range haveKeys {
|
||||
s, _ := key.(addrs.StringKey)
|
||||
// If the key is present in our current for_each, carry on
|
||||
if _, ok := t.ForEach[string(s)]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
abstract := NewNodeAbstractResourceInstance(t.Addr.Instance(key))
|
||||
var node dag.Vertex = abstract
|
||||
if f := t.Concrete; f != nil {
|
||||
node = f(abstract)
|
||||
}
|
||||
log.Printf("[TRACE] OrphanResourceCount(non-zero): adding %s as %T", t.Addr, node)
|
||||
g.Add(node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *OrphanResourceCountTransformer) transformCount(haveKeys map[addrs.InstanceKey]struct{}, g *Graph) error {
|
||||
// Due to the logic in Transform, we only get in here if our count is
|
||||
// at least one.
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/dag"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// ResourceCountTransformer is a GraphTransformer that expands the count
|
||||
@ -17,12 +18,13 @@ type ResourceCountTransformer struct {
|
||||
// Count is either the number of indexed instances to create, or -1 to
|
||||
// indicate that count is not set at all and thus a no-key instance should
|
||||
// be created.
|
||||
Count int
|
||||
Addr addrs.AbsResource
|
||||
Count int
|
||||
ForEach map[string]cty.Value
|
||||
Addr addrs.AbsResource
|
||||
}
|
||||
|
||||
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||
if t.Count < 0 {
|
||||
if t.Count < 0 && t.ForEach == nil {
|
||||
// Negative count indicates that count is not set at all.
|
||||
addr := t.Addr.Instance(addrs.NoKey)
|
||||
|
||||
@ -37,6 +39,19 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add nodes related to the for_each expression
|
||||
for key := range t.ForEach {
|
||||
addr := t.Addr.Instance(addrs.StringKey(key))
|
||||
abstract := NewNodeAbstractResourceInstance(addr)
|
||||
abstract.Schema = t.Schema
|
||||
var node dag.Vertex = abstract
|
||||
if f := t.Concrete; f != nil {
|
||||
node = f(abstract)
|
||||
}
|
||||
|
||||
g.Add(node)
|
||||
}
|
||||
|
||||
// For each count, build and add the node
|
||||
for i := 0; i < t.Count; i++ {
|
||||
key := addrs.IntKey(i)
|
||||
|
@ -143,7 +143,8 @@ Terraform CLI defines the following meta-arguments, which can be used with
|
||||
any resource type to change the behavior of resources:
|
||||
|
||||
- [`depends_on`, for specifying hidden dependencies][inpage-depend]
|
||||
- [`count`, for creating multiple resource instances][inpage-count]
|
||||
- [`count`, for creating multiple resource instances according to a count][inpage-count]
|
||||
- [`for_each`, to create multiple instances according to a map, or set of strings][inpage-for_each]
|
||||
- [`provider`, for selecting a non-default provider configuration][inpage-provider]
|
||||
- [`lifecycle`, for lifecycle customizations][inpage-lifecycle]
|
||||
- [`provisioner` and `connection`, for taking extra actions after resource creation][inpage-provisioner]
|
||||
@ -221,9 +222,9 @@ The `depends_on` argument should be used only as a last resort. When using it,
|
||||
always include a comment explaining why it is being used, to help future
|
||||
maintainers understand the purpose of the additional dependency.
|
||||
|
||||
### `count`: Multiple Resource Instances
|
||||
### `count`: Multiple Resource Instances By Count
|
||||
|
||||
[inpage-count]: #count-multiple-resource-instances
|
||||
[inpage-count]: #count-multiple-resource-instances-by-count
|
||||
|
||||
By default, a single `resource` block corresponds to only one real
|
||||
infrastructure object. Sometimes it is desirable to instead manage a set
|
||||
@ -299,6 +300,57 @@ intended. The practice of generating multiple instances from lists should
|
||||
be used sparingly, and with due care given to what will happen if the list is
|
||||
changed later.
|
||||
|
||||
### `for_each`: Multiple Resource Instances Defined By a Map, or Set of Strings
|
||||
|
||||
[inpage-for_each]: #for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings
|
||||
|
||||
When the `for_each` meta-argument is present, Terraform will create instances
|
||||
based on the keys and values present in a provided map, or set of strings, and expose the values
|
||||
of the map to the resource for its configuration.
|
||||
|
||||
The keys and values of the map, or strings in the case of a set, are exposed via the `each` attribute,
|
||||
which can only be used in blocks with a `for_each` argument set.
|
||||
|
||||
```hcl
|
||||
resource "azurerm_resource_group" "rg" {
|
||||
for_each = {
|
||||
a_group = "eastus"
|
||||
another_group = "westus2"
|
||||
}
|
||||
name = each.key
|
||||
location = each.value
|
||||
}
|
||||
```
|
||||
|
||||
Resources created by `for_each` are identified by the key associated with the instance -
|
||||
that is, if we have `azurerm_resource_group.rg` as above, the instances will be `azurerm_resource_group.rg["a_group"]`
|
||||
and `azurerm_resource_group.rg["another_group"]`, as those are the keys in the map provided
|
||||
to the `for_each` argument.
|
||||
|
||||
The `for_each` argument also supports a set of strings in addition to maps; convert a list
|
||||
to a set using the `toset` function. As such, we can take the example
|
||||
in `count` and make it safer to use, as we can change items in our set
|
||||
and because the string keys are used to identify the instances,
|
||||
we will only change the items we intend to:
|
||||
|
||||
```hcl
|
||||
variable "subnet_ids" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
resource "aws_instance" "server" {
|
||||
for_each = toset(var.subnet_ids)
|
||||
|
||||
ami = "ami-a1b2c3d4"
|
||||
instance_type = "t2.micro"
|
||||
subnet_id = each.key # note, each.key and each.value will be the same on a set
|
||||
|
||||
tags {
|
||||
Name = "Server ${each.key}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `provider`: Selecting a Non-default Provider Configuration
|
||||
|
||||
[inpage-provider]: #provider-selecting-a-non-default-provider-configuration
|
||||
|
Loading…
Reference in New Issue
Block a user