Resource for_each

This commit is contained in:
Pam Selle 2019-06-12 11:07:32 -04:00
parent 16fa18c139
commit 7d905f6777
29 changed files with 530 additions and 52 deletions

12
addrs/for_each_attr.go Normal file
View 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
}

View File

@ -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{

View File

@ -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`,

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -1,3 +1,4 @@
resource "test" "foo" {
count = 2
for_each = ["a"]
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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{

View File

@ -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")

View File

@ -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)

View File

@ -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() {

View 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
}

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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)
`

View File

@ -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 {

View File

@ -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

View File

@ -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,
},

View File

@ -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

View File

@ -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,
},

View File

@ -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))
}

View 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
}

View File

@ -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.

View File

@ -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)

View File

@ -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