diff --git a/addrs/instance_key.go b/addrs/instance_key.go index cef8b2796..ff128be5b 100644 --- a/addrs/instance_key.go +++ b/addrs/instance_key.go @@ -18,6 +18,10 @@ import ( type InstanceKey interface { instanceKeySigil() String() string + + // Value returns the cty.Value of the appropriate type for the InstanceKey + // value. + Value() cty.Value } // ParseInstanceKey returns the instance key corresponding to the given value, @@ -56,6 +60,10 @@ func (k IntKey) String() string { return fmt.Sprintf("[%d]", int(k)) } +func (k IntKey) Value() cty.Value { + return cty.NumberIntVal(int64(k)) +} + // StringKey is the InstanceKey representation representing string indices, as // used when the "for_each" argument is specified with a map or object type. type StringKey string @@ -69,6 +77,10 @@ func (k StringKey) String() string { return fmt.Sprintf("[%q]", string(k)) } +func (k StringKey) Value() cty.Value { + return cty.StringVal(string(k)) +} + // InstanceKeyLess returns true if the first given instance key i should sort // before the second key j, and false otherwise. func InstanceKeyLess(i, j InstanceKey) bool { diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index af5916fbb..2bf81d050 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -5266,28 +5266,23 @@ func TestContext2Apply_provisionerDestroy(t *testing.T) { p.DiffFn = testDiffFn pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { val, ok := c.Config["command"] - if !ok || val != "destroy" { + if !ok || val != "destroy a" { t.Fatalf("bad value for foo: %v %#v", val, c) } return nil } - state := MustShimLegacyState(&State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, + state := states.NewState() + root := state.RootModule() + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), }, - }) + mustProviderConfig(`provider["registry.terraform.io/-/aws"]`), + ) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5331,21 +5326,16 @@ func TestContext2Apply_provisionerDestroyFail(t *testing.T) { return fmt.Errorf("provisioner error") } - state := MustShimLegacyState(&State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, + state := states.NewState() + root := state.RootModule() + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), }, - }) + mustProviderConfig(`provider["registry.terraform.io/-/aws"]`), + ) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5371,7 +5361,7 @@ func TestContext2Apply_provisionerDestroyFail(t *testing.T) { } checkStateString(t, state, ` -aws_instance.foo: +aws_instance.foo["a"]: ID = bar provider = provider["registry.terraform.io/-/aws"] `) @@ -5405,21 +5395,16 @@ func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) { return fmt.Errorf("provisioner error") } - state := MustShimLegacyState(&State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, + state := states.NewState() + root := state.RootModule() + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), }, - }) + mustProviderConfig(`provider["registry.terraform.io/-/aws"]`), + ) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5547,7 +5532,7 @@ func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { destroyCalled := false pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { - expected := "create" + expected := "create a b" if rs.ID == "bar" { destroyCalled = true return nil @@ -5561,22 +5546,16 @@ func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { return nil } - state := MustShimLegacyState(&State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - Tainted: true, - }, - }, - }, - }, + state := states.NewState() + root := state.RootModule() + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"id":"bar"}`), }, - }) + mustProviderConfig(`provider["registry.terraform.io/-/aws"]`), + ) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5589,6 +5568,14 @@ func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { Provisioners: map[string]ProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, + Variables: InputValues{ + "input": &InputValue{ + Value: cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + }), + SourceType: ValueFromInput, + }, + }, }) if _, diags := ctx.Plan(); diags.HasErrors() { @@ -5601,7 +5588,7 @@ func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { } checkStateString(t, state, ` -aws_instance.foo: +aws_instance.foo["a"]: ID = foo provider = provider["registry.terraform.io/-/aws"] foo = bar diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index dbb3667ef..0755c6b9f 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -561,8 +561,18 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*configs.Provisio provisioner := ctx.Provisioner(prov.Type) schema := ctx.ProvisionerSchema(prov.Type) - forEach, forEachDiags := evaluateResourceForEachExpression(n.ResourceConfig.ForEach, ctx) - diags = diags.Append(forEachDiags) + var forEach map[string]cty.Value + + // For a destroy-time provisioner forEach is intentionally nil here, + // which EvalDataForInstanceKey responds to by not populating EachValue + // in its result. That's okay because each.value is prohibited for + // destroy-time provisioners. + if n.When != configs.ProvisionerWhenDestroy { + m, forEachDiags := evaluateResourceForEachExpression(n.ResourceConfig.ForEach, ctx) + diags = diags.Append(forEachDiags) + forEach = m + } + keyData := EvalDataForInstanceKey(instanceAddr.Key, forEach) // Evaluate the main provisioner configuration. diff --git a/terraform/evaluate.go b/terraform/evaluate.go index 65a7172eb..96a981070 100644 --- a/terraform/evaluate.go +++ b/terraform/evaluate.go @@ -104,25 +104,26 @@ type InstanceKeyEvalData = instances.RepetitionData // EvalDataForInstanceKey constructs a suitable InstanceKeyEvalData for // evaluating in a context that has the given instance key. +// +// The forEachMap argument can be nil when preparing for evaluation +// in a context where each.value is prohibited, such as a destroy-time +// provisioner. In that case, the returned EachValue will always be +// cty.NilVal. 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)) + var evalData InstanceKeyEvalData + if key == nil { + return evalData } - if stringKey, ok := key.(addrs.StringKey); ok { - eachKey = cty.StringVal(string(stringKey)) - eachVal = forEachMap[string(stringKey)] - } - - return InstanceKeyEvalData{ - CountIndex: countIdx, - EachKey: eachKey, - EachValue: eachVal, + keyValue := key.Value() + switch keyValue.Type() { + case cty.String: + evalData.EachKey = keyValue + evalData.EachValue = forEachMap[keyValue.AsString()] + case cty.Number: + evalData.CountIndex = keyValue } + return evalData } // EvalDataForNoInstanceKey is a value of InstanceKeyData that sets no instance diff --git a/terraform/testdata/apply-provisioner-destroy/main.tf b/terraform/testdata/apply-provisioner-destroy/main.tf index 38410ccc0..d5fc54e12 100644 --- a/terraform/testdata/apply-provisioner-destroy/main.tf +++ b/terraform/testdata/apply-provisioner-destroy/main.tf @@ -1,12 +1,18 @@ resource "aws_instance" "foo" { + for_each = var.input foo = "bar" provisioner "shell" { - command = "create" + command = "create ${each.key} ${each.value}" } provisioner "shell" { - command = "destroy" when = "destroy" + command = "destroy ${each.key}" } } + +variable "input" { + type = map(string) + default = {} +}