mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 08:58:34 -04:00
Merge pull request #38677 from hashicorp/jbardin/reaction-destroy
Action config block evaluation from within destroy context
This commit is contained in:
commit
2d89c84b88
17 changed files with 667 additions and 248 deletions
|
|
@ -67,9 +67,22 @@ const (
|
|||
Invoke
|
||||
)
|
||||
|
||||
func (e ActionTriggerEvent) IsBefore() bool {
|
||||
return slices.Contains(BeforeEvents, e)
|
||||
}
|
||||
|
||||
func (e ActionTriggerEvent) IsAfter() bool {
|
||||
return slices.Contains(AfterEvents, e)
|
||||
}
|
||||
|
||||
func (e ActionTriggerEvent) IsDestroy() bool {
|
||||
return slices.Contains(DestroyEvents, e)
|
||||
}
|
||||
|
||||
var (
|
||||
BeforeEvents = []ActionTriggerEvent{BeforeCreate, BeforeUpdate, BeforeDestroy}
|
||||
AfterEvents = []ActionTriggerEvent{AfterCreate, AfterUpdate, AfterDestroy}
|
||||
BeforeEvents = []ActionTriggerEvent{BeforeCreate, BeforeUpdate, BeforeDestroy}
|
||||
AfterEvents = []ActionTriggerEvent{AfterCreate, AfterUpdate, AfterDestroy}
|
||||
DestroyEvents = []ActionTriggerEvent{BeforeDestroy, AfterDestroy}
|
||||
)
|
||||
|
||||
// ActionRef represents a reference to a configured Action
|
||||
|
|
@ -115,11 +128,15 @@ func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics
|
|||
containsBefore = true
|
||||
case "after_update":
|
||||
event = AfterUpdate
|
||||
case "before_destroy":
|
||||
event = BeforeDestroy
|
||||
case "after_destroy":
|
||||
event = AfterDestroy
|
||||
default:
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Invalid \"event\" value %s", hcl.ExprAsKeyword(expr)),
|
||||
Detail: "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update.",
|
||||
Detail: "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update, before_destroy, after_destroy.",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ func TestDecodeActionTriggerBlock(t *testing.T) {
|
|||
},
|
||||
},
|
||||
[]string{
|
||||
"MockExprTraversal:0,0-12: Invalid \"event\" value not_an_event; The \"event\" argument supports the following values: before_create, after_create, before_update, after_update.",
|
||||
"MockExprTraversal:0,0-12: Invalid \"event\" value not_an_event; The \"event\" argument supports the following values: before_create, after_create, before_update, after_update, before_destroy, after_destroy.",
|
||||
":0,0-0: No events specified; At least one event must be specified for an action_trigger.",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -79,6 +79,34 @@ func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value,
|
|||
return val, diags
|
||||
}
|
||||
|
||||
// EvalActionBlock is a special case for action blocks that allows the caller to
|
||||
// directly pass in the "caller" value. This allows for the evaluation of a
|
||||
// resource value which may be different from what's expected in the global
|
||||
// context, like for example when a destroy action needs to evaluate the
|
||||
// "before" value of the resource change.
|
||||
func (s *Scope) EvalActionBlock(body hcl.Body, schema *configschema.Block, caller cty.Value) (val cty.Value, diags tfdiags.Diagnostics) {
|
||||
|
||||
spec := schema.DecoderSpec()
|
||||
|
||||
refs, diags := langrefs.ReferencesInBlock(s.ParseRef, body, schema)
|
||||
|
||||
ctx, ctxDiags := s.EvalContext(refs)
|
||||
diags = diags.Append(ctxDiags)
|
||||
|
||||
if diags.HasErrors() {
|
||||
// We'll stop early if we found problems in the references, because
|
||||
// it's likely evaluation will produce redundant copies of the same errors.
|
||||
return cty.UnknownVal(schema.ImpliedType()), diags
|
||||
}
|
||||
|
||||
ctx.Variables["caller"] = caller
|
||||
|
||||
val, evalDiags := hcldec.Decode(body, spec, ctx)
|
||||
diags = diags.Append(CheckForUnknownFunctionDiags(evalDiags, s.IgnoreUnknownProviderFunctions))
|
||||
|
||||
return val, diags
|
||||
}
|
||||
|
||||
// EvalSelfBlock evaluates the given body only within the scope of the provided
|
||||
// object and instance key data. References to the object must use self, and the
|
||||
// key data will only contain count.index or each.key. The static values for
|
||||
|
|
@ -479,6 +507,8 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
|||
|
||||
if self != cty.NilVal {
|
||||
vals["self"] = self
|
||||
// "caller" is used directly when an action in invoked from the CLI,
|
||||
// because we need to automatically retrieve the resource from state
|
||||
vals["caller"] = self
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
|
|
@ -29,10 +30,10 @@ var (
|
|||
_ GraphNodeProviderConsumer = (*actionTriggerApplyInstance)(nil)
|
||||
)
|
||||
|
||||
func (n *actionTriggerApplyInstance) invoke(ctx EvalContext, caller addrs.Referenceable) tfdiags.Diagnostics {
|
||||
func (n *actionTriggerApplyInstance) Invoke(ctx EvalContext, caller addrs.Referenceable, callerVal cty.Value, fromPlan bool) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
provider, _, err := getProvider(ctx, n.ActionInvocation.ProviderAddr)
|
||||
provider, actionProviderSchema, err := getProvider(ctx, n.ActionInvocation.ProviderAddr)
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
|
|
@ -53,32 +54,37 @@ func (n *actionTriggerApplyInstance) invoke(ctx EvalContext, caller addrs.Refere
|
|||
return diags
|
||||
}
|
||||
|
||||
// TODO: we will need to decode the saved config value for our initial attempt at destroy actions
|
||||
//
|
||||
// actionSchema, ok := actionProviderSchema.Actions[n.ActionInvocation.Addr.Action.Action.Type]
|
||||
// if !ok {
|
||||
// // This should have been caught earlier, but we don't want to panic
|
||||
// diags = diags.Append(&hcl.Diagnostic{
|
||||
// Severity: hcl.DiagError,
|
||||
// Summary: fmt.Sprintf("Action %s not found in provider schema", n.ActionInvocation.Addr),
|
||||
// Detail: fmt.Sprintf("The action %s was not found in the provider schema for %s", n.ActionInvocation.Addr, n.ActionInvocation.ProviderAddr),
|
||||
// Subject: n.actionNode.Config.DeclRange.Ptr(),
|
||||
// })
|
||||
// return diags
|
||||
// }
|
||||
// inv, err := n.ActionInvocation.Decode(&actionSchema)
|
||||
// if err != nil {
|
||||
// return diags.Append(err)
|
||||
// }
|
||||
// configValue := inv.ConfigValue
|
||||
var configVal cty.Value
|
||||
|
||||
configValue, actionDiags := n.actionNode.EvalInstance(ctx, n.ActionInvocation.Addr, nil, caller)
|
||||
diags = diags.Append(actionDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
// fromPlan indicates we can use the entire planned value for the action,
|
||||
// and should not attempt to reevaluate the config.
|
||||
if fromPlan {
|
||||
actionSchema, ok := actionProviderSchema.Actions[n.ActionInvocation.Addr.Action.Action.Type]
|
||||
if !ok {
|
||||
// This should have been caught earlier, but we don't want to panic
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Action %s not found in provider schema", n.ActionInvocation.Addr),
|
||||
Detail: fmt.Sprintf("The action %s was not found in the provider schema for %s", n.ActionInvocation.Addr, n.ActionInvocation.ProviderAddr),
|
||||
Subject: n.actionNode.Config.DeclRange.Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
inv, err := n.ActionInvocation.Decode(&actionSchema)
|
||||
if err != nil {
|
||||
return diags.Append(err)
|
||||
}
|
||||
configVal = inv.ConfigValue
|
||||
} else {
|
||||
val, actionDiags := n.actionNode.EvalInstance(ctx, n.ActionInvocation.Addr, nil, caller, callerVal)
|
||||
diags = diags.Append(actionDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
configVal = val
|
||||
}
|
||||
|
||||
if !configValue.IsWhollyKnown() {
|
||||
if !configVal.IsWhollyKnown() {
|
||||
return diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Action configuration unknown during apply",
|
||||
|
|
@ -103,7 +109,7 @@ func (n *actionTriggerApplyInstance) invoke(ctx EvalContext, caller addrs.Refere
|
|||
// We don't want to send the marks, but all marks are okay in the context
|
||||
// of an action invocation. We can't reuse our ephemeral free value from
|
||||
// above because we want the ephemeral values to be included.
|
||||
unmarkedConfigValue, _ := configValue.UnmarkDeep()
|
||||
unmarkedConfigValue, _ := configVal.UnmarkDeep()
|
||||
resp := provider.InvokeAction(providers.InvokeActionRequest{
|
||||
ActionType: n.ActionInvocation.Addr.Action.Action.Type,
|
||||
PlannedActionData: unmarkedConfigValue,
|
||||
|
|
|
|||
|
|
@ -175,6 +175,57 @@ resource "test_object" "a" {
|
|||
expectInvokeActionCalled: true,
|
||||
},
|
||||
|
||||
"after_destroy triggered": {
|
||||
module: map[string]string{
|
||||
"main.tf": `
|
||||
action "action_example" "hello" {
|
||||
config {
|
||||
attr = caller.test_string
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_object" "a" {
|
||||
test_string = "new name"
|
||||
lifecycle {
|
||||
action_trigger {
|
||||
events = [after_destroy]
|
||||
actions = [action.action_example.hello]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
prevRunState: states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.a"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"test_string":"old name"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
}),
|
||||
events: func(req providers.InvokeActionRequest) []providers.InvokeActionEvent {
|
||||
attr := req.PlannedActionData.GetAttr("attr").AsString()
|
||||
if attr != "old name" {
|
||||
return []providers.InvokeActionEvent{
|
||||
providers.InvokeActionEvent_Completed{
|
||||
Diagnostics: tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"destroy used wrong state value",
|
||||
"action invoked with "+attr,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return []providers.InvokeActionEvent{
|
||||
providers.InvokeActionEvent_Completed{},
|
||||
}
|
||||
},
|
||||
expectInvokeActionCalled: true,
|
||||
},
|
||||
|
||||
"before_create failing": {
|
||||
module: map[string]string{
|
||||
"main.tf": `
|
||||
|
|
@ -1438,7 +1489,7 @@ resource "test_object" "a" {
|
|||
expectInvokeActionCalled: true,
|
||||
},
|
||||
|
||||
"before_create references caller": {
|
||||
"before_update references caller": {
|
||||
module: map[string]string{
|
||||
"main.tf": `
|
||||
action "action_example" "test" {
|
||||
|
|
@ -1451,7 +1502,6 @@ resource "test_object" "a" {
|
|||
lifecycle {
|
||||
action_trigger {
|
||||
events = [before_update]
|
||||
condition = self.test_string == "new"
|
||||
actions = [action.action_example.test]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1449,6 +1449,222 @@ resource "test_object" "a" {
|
|||
})
|
||||
},
|
||||
},
|
||||
"after_destroy": {
|
||||
module: map[string]string{
|
||||
"main.tf": `
|
||||
action "test_action" "test" {
|
||||
config {
|
||||
attr = caller.name
|
||||
}
|
||||
}
|
||||
resource "test_object" "a" {
|
||||
name = "new"
|
||||
lifecycle {
|
||||
action_trigger {
|
||||
events = [after_destroy]
|
||||
actions = [action.test_action.test]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
planResourceFn: func(t *testing.T, req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||
resp.PlannedState = cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("new"),
|
||||
})
|
||||
resp.RequiresReplace = []cty.Path{cty.GetAttrPath("name")}
|
||||
return resp
|
||||
},
|
||||
buildState: func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.a"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"name":"current"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
},
|
||||
planActionFn: func(t *testing.T, req providers.PlanActionRequest) providers.PlanActionResponse {
|
||||
attr := req.ProposedActionData.GetAttr("attr").AsString()
|
||||
if attr != "current" {
|
||||
t.Fatalf("expected action plan to be 'current', got %s\n", attr)
|
||||
}
|
||||
return providers.PlanActionResponse{}
|
||||
},
|
||||
expectPlanActionCalled: true,
|
||||
},
|
||||
|
||||
"no ephemeral in destroy": {
|
||||
module: map[string]string{
|
||||
"main.tf": `
|
||||
action "test_action_wo" "test" {
|
||||
config {
|
||||
attr = terraform.applying
|
||||
}
|
||||
}
|
||||
resource "test_object" "a" {
|
||||
name = "new"
|
||||
lifecycle {
|
||||
action_trigger {
|
||||
events = [before_destroy]
|
||||
actions = [action.test_action_wo.test]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
planResourceFn: func(t *testing.T, req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||
resp.PlannedState = cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("new"),
|
||||
})
|
||||
resp.RequiresReplace = []cty.Path{cty.GetAttrPath("name")}
|
||||
return resp
|
||||
},
|
||||
buildState: func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.a"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"name":"current"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
},
|
||||
expectPlanActionCalled: false,
|
||||
expectPlanDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) {
|
||||
return diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Action config contains ephemeral values",
|
||||
Detail: "A destroy action configuration must be fully planned, and cannot contain ephemeral values.",
|
||||
Subject: &hcl.Range{
|
||||
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
|
||||
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
|
||||
End: hcl.Pos{Line: 2, Column: 31, Byte: 31},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
"destroy action must be known": {
|
||||
module: map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "new" {
|
||||
}
|
||||
|
||||
action "test_action" "test" {
|
||||
config {
|
||||
attr = test_object.new.name
|
||||
}
|
||||
}
|
||||
resource "test_object" "a" {
|
||||
name = "new"
|
||||
lifecycle {
|
||||
action_trigger {
|
||||
events = [after_destroy]
|
||||
actions = [action.test_action.test]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
planResourceFn: func(t *testing.T, req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||
if req.PriorState.IsNull() {
|
||||
resp.PlannedState = cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.UnknownVal(cty.String),
|
||||
})
|
||||
return resp
|
||||
}
|
||||
resp.PlannedState = cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("new"),
|
||||
})
|
||||
resp.RequiresReplace = []cty.Path{cty.GetAttrPath("name")}
|
||||
return resp
|
||||
},
|
||||
buildState: func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.a"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"name":"current"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
},
|
||||
expectPlanActionCalled: false,
|
||||
expectPlanDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) {
|
||||
return diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unknown action config",
|
||||
Detail: "Action configuration must be known to plan a destroy action.",
|
||||
Subject: &hcl.Range{
|
||||
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
|
||||
Start: hcl.Pos{Line: 5, Column: 1, Byte: 35},
|
||||
End: hcl.Pos{Line: 5, Column: 28, Byte: 62},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
"destroy condition must be known": {
|
||||
module: map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "new" {
|
||||
}
|
||||
|
||||
action "test_action" "test" {
|
||||
config {
|
||||
attr = caller.name
|
||||
}
|
||||
}
|
||||
resource "test_object" "a" {
|
||||
name = "new"
|
||||
lifecycle {
|
||||
action_trigger {
|
||||
condition = test_object.new.name == "ready"
|
||||
events = [after_destroy]
|
||||
actions = [action.test_action.test]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
planResourceFn: func(t *testing.T, req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||
if req.PriorState.IsNull() {
|
||||
// this is the new instance being created
|
||||
resp.PlannedState = cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.UnknownVal(cty.String),
|
||||
})
|
||||
return resp
|
||||
}
|
||||
|
||||
// this is directing the existing "a" instance to be replaced
|
||||
resp.PlannedState = cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("new"),
|
||||
})
|
||||
resp.RequiresReplace = []cty.Path{cty.GetAttrPath("name")}
|
||||
return resp
|
||||
},
|
||||
buildState: func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.a"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"name":"current"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
},
|
||||
expectPlanActionCalled: false,
|
||||
expectPlanDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) {
|
||||
return diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unknown action trigger condition",
|
||||
Detail: "Condition expression must be known to plan a destroy action.",
|
||||
Subject: &hcl.Range{
|
||||
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
|
||||
Start: hcl.Pos{Line: 14, Column: 19, Byte: 202},
|
||||
End: hcl.Pos{Line: 14, Column: 50, Byte: 233},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
"caller can be used for triggering resource": {
|
||||
module: map[string]string{
|
||||
|
|
@ -4052,6 +4268,7 @@ resource "test_object" "b" {
|
|||
"name": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ func (n *NodeActionConfig) recordActionExpansion(ctx EvalContext) tfdiags.Diagno
|
|||
|
||||
// Validate validates the action config, with an optional caller address if the
|
||||
// action is invoked from a resource action trigger.
|
||||
func (n *NodeActionConfig) Validate(ctx EvalContext, caller addrs.Referenceable) tfdiags.Diagnostics {
|
||||
func (n *NodeActionConfig) Validate(ctx EvalContext, caller addrs.Referenceable, callerVal cty.Value) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
provider, _, err := getProvider(ctx, n.ResolvedProvider)
|
||||
|
|
@ -169,7 +169,8 @@ func (n *NodeActionConfig) Validate(ctx EvalContext, caller addrs.Referenceable)
|
|||
config = hcl.EmptyBody()
|
||||
}
|
||||
|
||||
configVal, _, valDiags := ctx.EvaluateBlock(config, n.Schema.ConfigSchema, caller, keyData)
|
||||
scope := ctx.EvaluationScope(caller, nil, keyData)
|
||||
configVal, valDiags := scope.EvalActionBlock(config, n.Schema.ConfigSchema, callerVal)
|
||||
if valDiags.HasErrors() {
|
||||
// If there was no config block at all, we'll add a Context range to the returned diagnostic
|
||||
if n.Config.Config == nil {
|
||||
|
|
@ -291,7 +292,7 @@ func (n *NodeActionConfig) AttachDependencies(deps []addrs.ConfigResource) {
|
|||
// addrs.NoKey This function uses addrs.ActionInstance even though it only needs
|
||||
// the key because we need to use use a full instance addr for the resulting map
|
||||
// keys anyway.
|
||||
func (n *NodeActionConfig) EvalInstances(ctx EvalContext, addr addrs.ActionInstance, callRange *hcl.Range, caller addrs.Referenceable) (addrs.Map[addrs.ActionInstance, cty.Value], tfdiags.Diagnostics) {
|
||||
func (n *NodeActionConfig) EvalInvokedInstances(ctx EvalContext, addr addrs.ActionInstance, callRange *hcl.Range, caller addrs.Referenceable) (addrs.Map[addrs.ActionInstance, cty.Value], tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
all := addrs.MakeMap[addrs.ActionInstance, cty.Value]()
|
||||
|
||||
|
|
@ -310,7 +311,7 @@ func (n *NodeActionConfig) EvalInstances(ctx EvalContext, addr addrs.ActionInsta
|
|||
|
||||
for _, instAddr := range instances {
|
||||
repData := expander.GetActionInstanceRepetitionData(instAddr)
|
||||
val, evalDiags := n.evalInstance(ctx, repData, callRange, caller)
|
||||
val, evalDiags := n.evalInstance(ctx, repData, callRange, caller, cty.NilVal)
|
||||
diags = diags.Append(evalDiags)
|
||||
if diags.HasErrors() {
|
||||
return all, diags
|
||||
|
|
@ -403,7 +404,7 @@ func (n *NodeActionConfig) validateInstanceKey(addr addrs.AbsActionInstance, cal
|
|||
}
|
||||
|
||||
// EvalInstance returns the value from the expanded action block
|
||||
func (n *NodeActionConfig) EvalInstance(ctx EvalContext, inst addrs.AbsActionInstance, callRange *hcl.Range, caller addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) {
|
||||
func (n *NodeActionConfig) EvalInstance(ctx EvalContext, inst addrs.AbsActionInstance, callRange *hcl.Range, caller addrs.Referenceable, callerVal cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
diags = diags.Append(n.validateInstanceKey(inst, callRange))
|
||||
|
|
@ -420,6 +421,7 @@ func (n *NodeActionConfig) EvalInstance(ctx EvalContext, inst addrs.AbsActionIns
|
|||
for _, instAddr := range instances {
|
||||
if instAddr.Equal(inst) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
|
|
@ -434,13 +436,13 @@ func (n *NodeActionConfig) EvalInstance(ctx EvalContext, inst addrs.AbsActionIns
|
|||
|
||||
repData := expander.GetActionInstanceRepetitionData(instAddr)
|
||||
|
||||
return n.evalInstance(ctx, repData, callRange, caller)
|
||||
return n.evalInstance(ctx, repData, callRange, caller, callerVal)
|
||||
}
|
||||
|
||||
// Eval one or more instances of the action. This function expects that the key
|
||||
// is already validated for the the calling context, and will not produce
|
||||
// diagnostics for incorrect key types.
|
||||
func (n *NodeActionConfig) evalInstance(ctx EvalContext, repData instances.RepetitionData, callRange *hcl.Range, caller addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) {
|
||||
func (n *NodeActionConfig) evalInstance(ctx EvalContext, repData instances.RepetitionData, callRange *hcl.Range, caller addrs.Referenceable, callerVal cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// This should have been caught already
|
||||
|
|
@ -449,20 +451,34 @@ func (n *NodeActionConfig) evalInstance(ctx EvalContext, repData instances.Repet
|
|||
}
|
||||
|
||||
configVal := cty.NullVal(n.Schema.ConfigSchema.ImpliedType())
|
||||
if n.Config.Config != nil {
|
||||
var configDiags tfdiags.Diagnostics
|
||||
configVal, _, configDiags = ctx.EvaluateBlock(n.Config.Config, n.Schema.ConfigSchema.DeepCopy(), caller, repData)
|
||||
diags = diags.Append(configDiags)
|
||||
if configDiags.HasErrors() {
|
||||
if n.Config.Config == nil {
|
||||
return configVal, nil
|
||||
}
|
||||
|
||||
// For invoke we have no callerVal, but can use the normal self evaluation
|
||||
// mechanisms because the instance must be in state already.
|
||||
var evalDiags tfdiags.Diagnostics
|
||||
if callerVal == cty.NilVal {
|
||||
configVal, _, evalDiags = ctx.EvaluateBlock(n.Config.Config, n.Schema.ConfigSchema, caller, repData)
|
||||
diags = diags.Append(evalDiags)
|
||||
if diags.HasErrors() {
|
||||
return configVal, diags
|
||||
}
|
||||
} else {
|
||||
scope := ctx.EvaluationScope(caller, nil, repData)
|
||||
configVal, evalDiags = scope.EvalActionBlock(n.Config.Config, n.Schema.ConfigSchema, callerVal)
|
||||
diags = diags.Append(evalDiags)
|
||||
if diags.HasErrors() {
|
||||
return configVal, diags
|
||||
}
|
||||
|
||||
valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, n.Schema.ConfigSchema)
|
||||
diags = diags.Append(valDiags.InConfigBody(n.Config.Config, n.Addr.String()))
|
||||
|
||||
var deprecationDiags tfdiags.Diagnostics
|
||||
configVal, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(configVal, n.Schema.ConfigSchema, n.ModulePath())
|
||||
diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.Addr.String()))
|
||||
}
|
||||
|
||||
valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, n.Schema.ConfigSchema)
|
||||
diags = diags.Append(valDiags.InConfigBody(n.Config.Config, n.Addr.String()))
|
||||
|
||||
var deprecationDiags tfdiags.Diagnostics
|
||||
configVal, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(configVal, n.Schema.ConfigSchema, n.ModulePath())
|
||||
diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.Addr.String()))
|
||||
|
||||
return configVal, diags
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ func (n *nodeActionPlanInvoke) planActions(ctx EvalContext) tfdiags.Diagnostics
|
|||
// We're relying on the given addr derived from the action target to
|
||||
// determine which action instance to evaluate. If the address has no key
|
||||
// and the action is expanded, we will plan all instances.
|
||||
actionVals, actionDiags := n.ActionConfig.EvalInstances(ctx, n.Addr.Action, nil, n.Caller)
|
||||
actionVals, actionDiags := n.ActionConfig.EvalInvokedInstances(ctx, n.Addr.Action, nil, n.Caller)
|
||||
diags = diags.Append(actionDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
|
|
@ -257,7 +257,8 @@ func (n *nodeActionInvokeApplyInstance) Name() string {
|
|||
}
|
||||
|
||||
func (n *nodeActionInvokeApplyInstance) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics {
|
||||
return n.invoke(ctx, n.ActionInvocation.Caller)
|
||||
// FIXME: caller!
|
||||
return n.Invoke(ctx, n.ActionInvocation.Caller, cty.NilVal, true)
|
||||
}
|
||||
|
||||
func (n *nodeActionInvokeApplyInstance) References() []*addrs.Reference {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package terraform
|
|||
import (
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// NodeValidatableAction represents an action that is used for validation only.
|
||||
|
|
@ -29,5 +30,5 @@ func (n *NodeValidatableAction) Path() addrs.ModuleInstance {
|
|||
}
|
||||
|
||||
func (n *NodeValidatableAction) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics {
|
||||
return n.Validate(ctx, nil)
|
||||
return n.Validate(ctx, nil, cty.DynamicVal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package terraform
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
|
|
@ -53,6 +54,8 @@ type NodeAbstractResourceInstance struct {
|
|||
|
||||
// override is set by the graph itself, just before this node executes.
|
||||
override *configs.Override
|
||||
|
||||
actionApplyTriggers []*actionTriggerApplyInstance
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -3108,3 +3111,113 @@ func getRequiredReplaces(priorVal, plannedNewVal cty.Value, writeOnly []cty.Path
|
|||
|
||||
return reqRep, diags
|
||||
}
|
||||
|
||||
// pre-check for before actions since being able to evaluate actions requires a
|
||||
// slightly different behavior for diff handling, we guard that change by seeing if
|
||||
// it's needed at all.
|
||||
func (n *NodeAbstractResourceInstance) hasBeforeActions() bool {
|
||||
for _, trigger := range n.actionApplyTriggers {
|
||||
event := trigger.ActionInvocation.ActionTrigger.TriggerEvent()
|
||||
if event.IsBefore() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// invokeDestroyAction currently cannot reevaluate the action config due to not
|
||||
// being able to order dependencies without causing cycles. The entire config
|
||||
// and condition must be known at plan time, so if we have a planned action we
|
||||
// simply decode and call invoke.
|
||||
func (n *NodeAbstractResourceInstance) invokeDestroyActions(ctx EvalContext, forEvent configs.ActionTriggerEvent) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
for _, trigger := range n.actionApplyTriggers {
|
||||
event := trigger.ActionInvocation.ActionTrigger.TriggerEvent()
|
||||
if event != forEvent {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] NodeAbstractesourceInstance: invoking destroy action %s", trigger.ActionInvocation.Addr)
|
||||
diags = diags.Append(trigger.Invoke(ctx, n.Addr.Resource, cty.DynamicVal, true))
|
||||
if diags.HasErrors() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// invokeActions invokes any actions triggered for the listed events. Condition
|
||||
// expressions are reevaluated here when they exist, and failing conditions are
|
||||
// skipped.
|
||||
func (n *NodeAbstractResourceInstance) invokeActions(ctx EvalContext, repData instances.RepetitionData, forEvents []configs.ActionTriggerEvent, callerVal cty.Value) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
for _, trigger := range n.actionApplyTriggers {
|
||||
event := trigger.ActionInvocation.ActionTrigger.TriggerEvent()
|
||||
if !slices.Contains(forEvents, event) {
|
||||
continue
|
||||
}
|
||||
|
||||
condOK, condDiags := n.evalActionCondition(ctx, trigger, repData)
|
||||
diags = diags.Append(condDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
if !condOK {
|
||||
log.Printf("[DEBUG] NodeAbstractesourceInstance: action condition false, skipping %s", trigger.ActionInvocation.Addr)
|
||||
continue
|
||||
}
|
||||
|
||||
diags = diags.Append(trigger.Invoke(ctx, n.Addr.Resource, callerVal, false))
|
||||
if diags.HasErrors() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// We need to lookup any condition expression from the action block before
|
||||
// execution, because the condition is part of the resource config, while the
|
||||
// action is planned as an ActionInvocation.
|
||||
func (n *NodeAbstractResourceInstance) evalActionCondition(ctx EvalContext, trigger *actionTriggerApplyInstance, repData instances.RepetitionData) (bool, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// this can't be an invoked trigger
|
||||
rat := trigger.ActionInvocation.ActionTrigger.(*plans.ResourceActionTrigger)
|
||||
triggerBlock := n.Config.Managed.ActionTriggers[rat.ActionTriggerBlockIndex]
|
||||
|
||||
if triggerBlock.Condition == nil {
|
||||
return true, diags
|
||||
}
|
||||
|
||||
scope := ctx.EvaluationScope(n.Addr.Resource, nil, repData)
|
||||
cond, conditionEvalDiags := scope.EvalExpr(triggerBlock.Condition, cty.Bool)
|
||||
diags = diags.Append(conditionEvalDiags)
|
||||
if diags.HasErrors() {
|
||||
return false, diags
|
||||
}
|
||||
|
||||
if !cond.IsKnown() {
|
||||
// this should not happen, but give the user a good diagnostic to help
|
||||
// reproduce the problem in case it does.
|
||||
return false, diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unknown condition when invoking action before apply",
|
||||
Detail: "The action trigger condition must be known before it can be invoked.",
|
||||
Subject: triggerBlock.Condition.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return cond.True(), diags
|
||||
}
|
||||
|
||||
func (n *NodeAbstractResourceInstance) ActionProviders() []ProviderRef {
|
||||
var refs []ProviderRef
|
||||
for _, trigger := range n.actionApplyTriggers {
|
||||
refs = append(refs, ProviderRef{Addr: trigger.ActionInvocation.ProviderAddr, Resolved: true})
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ package terraform
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
|
@ -36,8 +35,6 @@ type NodeApplyableResourceInstance struct {
|
|||
// forceReplace indicates that this resource is being replaced for external
|
||||
// reasons, like a -replace flag or via replace_triggered_by.
|
||||
forceReplace bool
|
||||
|
||||
actionTriggers []*actionTriggerApplyInstance
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -232,7 +229,7 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext)
|
|||
|
||||
// Make a new diff, in case we've learned new values in the state
|
||||
// during apply which we can now incorporate.
|
||||
diffApply, instancePlannedState, deferred, planDiags := n.plan(ctx, diff, state, false, n.forceReplace, repData)
|
||||
diffApply, _, deferred, planDiags := n.plan(ctx, diff, state, false, n.forceReplace, repData)
|
||||
diags = diags.Append(planDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
|
|
@ -275,27 +272,9 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext)
|
|||
return diags.Append(n.managedResourcePostconditions(ctx, repData))
|
||||
}
|
||||
|
||||
// In order to ensure the action can be evaluated, we need to update the
|
||||
// stored change and state in case it provides some newly known values. We
|
||||
// hedge against any unexpected changes in diff handling for existing
|
||||
// configurations by only updating these when we have actions to evaluate.
|
||||
if n.hasBeforeActions() {
|
||||
changes := ctx.Changes()
|
||||
changes.RemoveResourceInstanceChange(n.Addr, deposedKey)
|
||||
changes.AppendResourceInstanceChange(diffApply)
|
||||
|
||||
// we also have to update the state to reflect the pending changes
|
||||
// again, or else evaluation will return the old state. Since before
|
||||
// actions are the first time we've encountered this eval-before-applied
|
||||
// situation, all instances in the state were previously just left as
|
||||
// ObjectReady.
|
||||
diags = diags.Append(n.writeResourceInstanceState(ctx, instancePlannedState, workingState))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] NodeApplyableResourceInstance: invoking before actions for %s", n.Addr)
|
||||
diags = diags.Append(n.invokeActions(ctx, repData, configs.BeforeEvents))
|
||||
diags = diags.Append(n.invokeActions(ctx, repData, configs.BeforeEvents, diffApply.After))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
|
@ -391,99 +370,11 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext)
|
|||
// until the user makes the condition succeed.
|
||||
diags = diags.Append(n.managedResourcePostconditions(ctx, repData))
|
||||
|
||||
diags = diags.Append(n.invokeActions(ctx, repData, configs.AfterEvents))
|
||||
diags = diags.Append(n.invokeActions(ctx, repData, configs.AfterEvents, state.Value))
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// pre-check for before actions since being able to evaluate actions requires a
|
||||
// slightly different behavior for diff handling, we guard that change by seeing if
|
||||
// it's needed at all.
|
||||
func (n *NodeApplyableResourceInstance) hasBeforeActions() bool {
|
||||
for _, trigger := range n.actionTriggers {
|
||||
event := trigger.ActionInvocation.ActionTrigger.TriggerEvent()
|
||||
if slices.Contains(configs.BeforeEvents, event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// invokeActions invokes any actions triggered for the listed events. Condition
|
||||
// expressions are reevaluated here when they exist, and failing conditions are
|
||||
// skipped.
|
||||
func (n *NodeApplyableResourceInstance) invokeActions(ctx EvalContext, repData instances.RepetitionData, forEvents []configs.ActionTriggerEvent) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
for _, trigger := range n.actionTriggers {
|
||||
event := trigger.ActionInvocation.ActionTrigger.TriggerEvent()
|
||||
if !slices.Contains(forEvents, event) {
|
||||
continue
|
||||
}
|
||||
|
||||
condOK, condDiags := n.evalActionCondition(ctx, trigger, repData)
|
||||
diags = diags.Append(condDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
if !condOK {
|
||||
log.Printf("[DEBUG] NodeApplyableResourceInstance: action condition false, skipping %s", trigger.ActionInvocation.Addr)
|
||||
continue
|
||||
}
|
||||
|
||||
diags = diags.Append(trigger.invoke(ctx, n.Addr.Resource))
|
||||
if diags.HasErrors() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// We need to lookup any condition expression from the action block before
|
||||
// execution, because the condition is part of the resource config, while the
|
||||
// action is planned as an ActionInvocation.
|
||||
func (n *NodeApplyableResourceInstance) evalActionCondition(ctx EvalContext, trigger *actionTriggerApplyInstance, repData instances.RepetitionData) (bool, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// this can't be an invoked trigger
|
||||
rat := trigger.ActionInvocation.ActionTrigger.(*plans.ResourceActionTrigger)
|
||||
triggerBlock := n.Config.Managed.ActionTriggers[rat.ActionTriggerBlockIndex]
|
||||
|
||||
if triggerBlock.Condition == nil {
|
||||
return true, diags
|
||||
}
|
||||
|
||||
scope := ctx.EvaluationScope(n.Addr.Resource, nil, repData)
|
||||
cond, conditionEvalDiags := scope.EvalExpr(triggerBlock.Condition, cty.Bool)
|
||||
diags = diags.Append(conditionEvalDiags)
|
||||
if diags.HasErrors() {
|
||||
return false, diags
|
||||
}
|
||||
|
||||
if !cond.IsKnown() {
|
||||
// this should not happen, but give the user a good diagnostic to help
|
||||
// reproduce the problem in case it does.
|
||||
return false, diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unknown condition when invoking action before apply",
|
||||
Detail: "The action trigger condition must be known before it can be invoked.",
|
||||
Subject: triggerBlock.Condition.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return cond.True(), diags
|
||||
}
|
||||
|
||||
func (n *NodeApplyableResourceInstance) ActionProviders() []ProviderRef {
|
||||
var refs []ProviderRef
|
||||
for _, trigger := range n.actionTriggers {
|
||||
refs = append(refs, ProviderRef{Addr: trigger.actionNode.ResolvedProvider, Resolved: true})
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func (n *NodeApplyableResourceInstance) managedResourcePostconditions(ctx EvalContext, repeatData instances.RepetitionData) (diags tfdiags.Diagnostics) {
|
||||
|
||||
checkDiags := evalCheckRules(
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import (
|
|||
// destroyed.
|
||||
type NodeDestroyResourceInstance struct {
|
||||
*NodeAbstractResourceInstance
|
||||
|
||||
actionTriggers []*actionTriggerApplyInstance
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -183,6 +181,14 @@ func (n *NodeDestroyResourceInstance) managedResourceExecute(ctx EvalContext) (d
|
|||
}
|
||||
}
|
||||
|
||||
if n.hasBeforeActions() {
|
||||
log.Printf("[DEBUG] NodeApplyableResourceInstance: invoking before actions for %s", n.Addr)
|
||||
diags = diags.Append(n.invokeDestroyActions(ctx, configs.BeforeDestroy))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
}
|
||||
|
||||
// Managed resources need to be destroyed, while data sources
|
||||
// are only removed from state.
|
||||
// we pass a nil configuration to apply because we are destroying
|
||||
|
|
@ -212,6 +218,9 @@ func (n *NodeDestroyResourceInstance) managedResourceExecute(ctx EvalContext) (d
|
|||
})
|
||||
}
|
||||
|
||||
// after destroy we continue to use the before value, since there is no after
|
||||
diags = diags.Append(n.invokeDestroyActions(ctx, configs.AfterDestroy))
|
||||
|
||||
// create the err value for postApplyHook
|
||||
diags = diags.Append(n.postApplyHook(ctx, state, diags.Err()))
|
||||
diags = diags.Append(updateStateHook(ctx))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/dag"
|
||||
"github.com/hashicorp/terraform/internal/instances"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
|
|
@ -312,6 +313,14 @@ func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op w
|
|||
return diags
|
||||
}
|
||||
|
||||
if n.hasBeforeActions() {
|
||||
log.Printf("[DEBUG] NodeApplyableResourceInstance: invoking before actions for %s", n.Addr)
|
||||
diags = diags.Append(n.invokeDestroyActions(ctx, configs.BeforeDestroy))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
}
|
||||
|
||||
// we pass a nil configuration to apply because we are destroying
|
||||
state, applyDiags := n.apply(ctx, state, change, nil, instances.RepetitionData{}, false)
|
||||
diags = diags.Append(applyDiags)
|
||||
|
|
@ -321,11 +330,14 @@ func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op w
|
|||
// was successfully destroyed it will be pruned. If it was not, it will
|
||||
// be caught on the next run.
|
||||
writeDiags := n.writeResourceInstanceState(ctx, state)
|
||||
diags.Append(writeDiags)
|
||||
diags = diags.Append(writeDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// after destroy we continue to use the before value, since there is no after
|
||||
diags = diags.Append(n.invokeDestroyActions(ctx, configs.AfterDestroy))
|
||||
|
||||
diags = diags.Append(n.postApplyHook(ctx, state, diags.Err()))
|
||||
|
||||
return diags.Append(updateStateHook(ctx))
|
||||
|
|
|
|||
|
|
@ -606,8 +606,10 @@ func (n *NodePlannableResourceInstance) planActionTriggers(ctx EvalContext, resR
|
|||
|
||||
for _, trigger := range n.actionTriggers {
|
||||
scope := ctx.EvaluationScope(n.Addr.Resource, nil, resRepData)
|
||||
cond := cty.True
|
||||
if trigger.config.Condition != nil {
|
||||
cond, conditionEvalDiags := scope.EvalExpr(trigger.config.Condition, cty.Bool)
|
||||
var conditionEvalDiags tfdiags.Diagnostics
|
||||
cond, conditionEvalDiags = scope.EvalExpr(trigger.config.Condition, cty.Bool)
|
||||
diags = diags.Append(conditionEvalDiags)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
|
|
@ -625,6 +627,16 @@ func (n *NodePlannableResourceInstance) planActionTriggers(ctx EvalContext, resR
|
|||
// though because the event is set within a nested interface inside a
|
||||
// pointer to the ActionInvocationInstance.
|
||||
for _, event := range eventsForPlannedAction(trigger.config.Events, n.change.Action) {
|
||||
if event.IsDestroy() && !cond.IsKnown() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unknown action trigger condition",
|
||||
Detail: "Condition expression must be known to plan a destroy action.",
|
||||
Subject: trigger.config.Condition.Range().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
for _, action := range trigger.actionRefs {
|
||||
ai, deferred, planDiags := n.planActionTrigger(ctx, resRepData, action, event)
|
||||
diags = diags.Append(planDiags)
|
||||
|
|
@ -696,12 +708,42 @@ func (n *NodePlannableResourceInstance) planActionTrigger(ctx EvalContext, resRe
|
|||
return
|
||||
}
|
||||
|
||||
actionVal, actionDiags := actionRef.actionNode.EvalInstance(ctx, actionInst.Absolute(ctx.Path()), actionRef.configRef.Expr.Range().Ptr(), n.Addr.Resource)
|
||||
callerVal := n.change.After
|
||||
// If the resource is being destroyed, we want the before val. This works
|
||||
// for replacement (this node doesn't handle full destroys), because the
|
||||
// caller is associated with the existing resource instance rather than the
|
||||
if event == configs.BeforeDestroy || event == configs.AfterDestroy {
|
||||
callerVal = n.change.Before
|
||||
}
|
||||
|
||||
actionVal, actionDiags := actionRef.actionNode.EvalInstance(ctx, actionInst.Absolute(ctx.Path()), actionRef.configRef.Expr.Range().Ptr(), n.Addr.Resource, callerVal)
|
||||
diags = diags.Append(actionDiags)
|
||||
if diags.HasErrors() {
|
||||
return
|
||||
}
|
||||
|
||||
if event.IsDestroy() {
|
||||
if !actionVal.IsWhollyKnown() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unknown action config",
|
||||
Detail: "Action configuration must be known to plan a destroy action.",
|
||||
Subject: actionRef.actionNode.Config.DeclRange.Ptr(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(ephemeral.EphemeralValuePaths(actionVal)) > 0 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Action config contains ephemeral values",
|
||||
Detail: "A destroy action configuration must be fully planned, and cannot contain ephemeral values.",
|
||||
Subject: actionRef.actionNode.Config.DeclRange.Ptr(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
provider, _, err := getProvider(ctx, actionRef.actionNode.ResolvedProvider)
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ func (n *NodeValidatableResource) validateActions(ctx EvalContext) tfdiags.Diagn
|
|||
_, self := n.stubRepetitionData()
|
||||
|
||||
for _, ref := range actions.Iter() {
|
||||
diags = diags.Append(ref.actionNode.Validate(ctx, self))
|
||||
diags = diags.Append(ref.actionNode.Validate(ctx, self, cty.UnknownVal(n.Schema.Body.ImpliedType())))
|
||||
}
|
||||
|
||||
return diags
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ func (t *ActionDiffTransformer) Transform(g *Graph) error {
|
|||
for _, atn := range atns {
|
||||
if destroy {
|
||||
if n, ok := atn.(*NodeDestroyResourceInstance); ok {
|
||||
// FIXME: this doesn't deal with deposed or forget instances
|
||||
n.actionTriggers = append(n.actionTriggers, &actionTriggerApplyInstance{
|
||||
n.actionApplyTriggers = append(n.actionApplyTriggers, &actionTriggerApplyInstance{
|
||||
ActionInvocation: ai,
|
||||
actionNode: actionConfig,
|
||||
})
|
||||
|
|
@ -65,7 +64,7 @@ func (t *ActionDiffTransformer) Transform(g *Graph) error {
|
|||
}
|
||||
|
||||
if n, ok := atn.(*NodeApplyableResourceInstance); ok {
|
||||
n.actionTriggers = append(n.actionTriggers, &actionTriggerApplyInstance{
|
||||
n.actionApplyTriggers = append(n.actionApplyTriggers, &actionTriggerApplyInstance{
|
||||
ActionInvocation: ai,
|
||||
actionNode: actionConfig,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -171,25 +171,20 @@ func (t *ProviderTransformer) Transform(g *Graph) error {
|
|||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// To start, we'll collect the _requested_ provider addresses for each
|
||||
// To start, we'll collect the _requested_ provider address for each
|
||||
// node, which we'll then resolve (handling provider inheritance, etc) in
|
||||
// the next step.
|
||||
// Our "requested" map is from graph vertices to string representations of
|
||||
// provider config addresses (for deduping) to requests.
|
||||
requested := map[dag.Vertex]map[string]ProviderRef{}
|
||||
requested := map[dag.Vertex]ProviderRef{}
|
||||
needConfigured := map[string]addrs.AbsProviderConfig{}
|
||||
|
||||
// forActions stores provider used only for actions by a resource. These are
|
||||
// only to connect the resource to the correct nodes, and are not for
|
||||
// resolution of the resource's own provider.
|
||||
forActions := map[dag.Vertex]map[string]ProviderRef{}
|
||||
forActions := map[dag.Vertex][]ProviderRef{}
|
||||
|
||||
for _, v := range g.Vertices() {
|
||||
if pv, ok := v.(GraphNodeActionProviderConsumer); ok {
|
||||
for _, ref := range pv.ActionProviders() {
|
||||
forActions[v] = make(map[string]ProviderRef)
|
||||
forActions[v][ref.String()] = ref
|
||||
}
|
||||
forActions[v] = pv.ActionProviders()
|
||||
}
|
||||
|
||||
if pv, ok := v.(GraphNodeProviderConsumer); ok {
|
||||
|
|
@ -199,90 +194,110 @@ func (t *ProviderTransformer) Transform(g *Graph) error {
|
|||
continue
|
||||
}
|
||||
|
||||
requested[v] = make(map[string]ProviderRef)
|
||||
requested[v][ref.String()] = ref
|
||||
requested[v] = ref
|
||||
|
||||
// Direct references need the provider configured as well as initialized
|
||||
needConfigured[ref.String()] = ref.AbsProviderConfig()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now we'll go through all the requested addresses we just collected and
|
||||
// figure out which _actual_ config address each belongs to, after resolving
|
||||
// for provider inheritance and passing.
|
||||
m := providerVertexMap(g)
|
||||
for v, reqs := range requested {
|
||||
for key, req := range reqs {
|
||||
absProvider := req.AbsProviderConfig()
|
||||
target := m[key]
|
||||
|
||||
_, ok := v.(GraphNodeModulePath)
|
||||
if !ok && target == nil {
|
||||
// No target and no path to traverse up from
|
||||
diags = diags.Append(fmt.Errorf("%s: provider %s couldn't be found", dag.VertexName(v), absProvider))
|
||||
continue
|
||||
}
|
||||
// We need to run this separately for both requested and forActions maps.
|
||||
// TODO: this probably shouldn't be a closure, but this is the most
|
||||
// straightforward refactor from the existing nested loos
|
||||
resolveProvider := func(v dag.Vertex, ref ProviderRef) GraphNodeProvider {
|
||||
absProvider := ref.AbsProviderConfig()
|
||||
target := m[ref.String()]
|
||||
|
||||
if target != nil {
|
||||
log.Printf("[TRACE] ProviderTransformer: exact match for %s serving %s", absProvider, dag.VertexName(v))
|
||||
}
|
||||
_, ok := v.(GraphNodeModulePath)
|
||||
if !ok && target == nil {
|
||||
// No target and no path to traverse up from
|
||||
diags = diags.Append(fmt.Errorf("%s: provider %s couldn't be found", dag.VertexName(v), absProvider))
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we don't have a provider at this level, walk up the path looking for one,
|
||||
// unless we were told to be exact.
|
||||
if target == nil && !req.Resolved {
|
||||
for pp, ok := absProvider.Inherited(); ok; pp, ok = pp.Inherited() {
|
||||
key := pp.String()
|
||||
target = m[key]
|
||||
if target != nil {
|
||||
log.Printf("[TRACE] ProviderTransformer: %s uses inherited configuration %s", dag.VertexName(v), pp)
|
||||
break
|
||||
}
|
||||
log.Printf("[TRACE] ProviderTransformer: looking for %s to serve %s", pp, dag.VertexName(v))
|
||||
if target != nil {
|
||||
log.Printf("[TRACE] ProviderTransformer: exact match for %s serving %s", absProvider, dag.VertexName(v))
|
||||
}
|
||||
|
||||
// if we don't have a provider at this level, walk up the path looking for one,
|
||||
// unless we were told to be exact.
|
||||
if target == nil && !ref.Resolved {
|
||||
for pp, ok := absProvider.Inherited(); ok; pp, ok = pp.Inherited() {
|
||||
key := pp.String()
|
||||
target = m[key]
|
||||
if target != nil {
|
||||
log.Printf("[TRACE] ProviderTransformer: %s uses inherited configuration %s", dag.VertexName(v), pp)
|
||||
break
|
||||
}
|
||||
log.Printf("[TRACE] ProviderTransformer: looking for %s to serve %s", pp, dag.VertexName(v))
|
||||
}
|
||||
}
|
||||
|
||||
// If this provider doesn't need to be configured then we can just
|
||||
// stub it out with an init-only provider node, which will just
|
||||
// start up the provider and fetch its schema.
|
||||
if _, exists := needConfigured[key]; target == nil && !exists {
|
||||
stubAddr := addrs.AbsProviderConfig{
|
||||
Module: addrs.RootModule,
|
||||
Provider: absProvider.Provider,
|
||||
}
|
||||
stub := &NodeEvalableProvider{
|
||||
&NodeAbstractProvider{
|
||||
Addr: stubAddr,
|
||||
},
|
||||
}
|
||||
m[stubAddr.String()] = stub
|
||||
log.Printf("[TRACE] ProviderTransformer: creating init-only node for %s", stubAddr)
|
||||
target = stub
|
||||
g.Add(target)
|
||||
// If this provider doesn't need to be configured then we can just
|
||||
// stub it out with an init-only provider node, which will just
|
||||
// start up the provider and fetch its schema.
|
||||
if _, exists := needConfigured[ref.String()]; target == nil && !exists {
|
||||
stubAddr := addrs.AbsProviderConfig{
|
||||
Module: addrs.RootModule,
|
||||
Provider: absProvider.Provider,
|
||||
}
|
||||
stub := &NodeEvalableProvider{
|
||||
&NodeAbstractProvider{
|
||||
Addr: stubAddr,
|
||||
},
|
||||
}
|
||||
m[stubAddr.String()] = stub
|
||||
log.Printf("[TRACE] ProviderTransformer: creating init-only node for %s", stubAddr)
|
||||
target = stub
|
||||
g.Add(target)
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Provider configuration not present",
|
||||
fmt.Sprintf(
|
||||
"To work with %s its original provider configuration at %s is required, but it has been removed. This occurs when a provider configuration is removed while objects created by that provider still exist in the state. Re-add the provider configuration to destroy %s, after which you can remove the provider configuration again.",
|
||||
dag.VertexName(v), absProvider, dag.VertexName(v),
|
||||
),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
// see if this is a proxy provider pointing to another concrete config
|
||||
if p, ok := target.(*graphNodeProxyProvider); ok {
|
||||
g.Remove(p)
|
||||
target = p.Target()
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
for v, ref := range requested {
|
||||
target := resolveProvider(v, ref)
|
||||
if target == nil {
|
||||
// something happened, and we already have the diags
|
||||
return diags.Err()
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] ProviderTransformer: %q (%T) needs %s", dag.VertexName(v), v, dag.VertexName(target))
|
||||
if pv, ok := v.(GraphNodeProviderConsumer); ok {
|
||||
pv.SetProvider(target.ProviderAddr())
|
||||
}
|
||||
g.Connect(dag.BasicEdge(v, target))
|
||||
}
|
||||
|
||||
for v, refs := range forActions {
|
||||
for _, ref := range refs {
|
||||
target := resolveProvider(v, ref)
|
||||
if target == nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Provider configuration not present",
|
||||
fmt.Sprintf(
|
||||
"To work with %s its original provider configuration at %s is required, but it has been removed. This occurs when a provider configuration is removed while objects created by that provider still exist in the state. Re-add the provider configuration to destroy %s, after which you can remove the provider configuration again.",
|
||||
dag.VertexName(v), absProvider, dag.VertexName(v),
|
||||
),
|
||||
))
|
||||
break
|
||||
}
|
||||
|
||||
// see if this is a proxy provider pointing to another concrete config
|
||||
if p, ok := target.(*graphNodeProxyProvider); ok {
|
||||
g.Remove(p)
|
||||
target = p.Target()
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] ProviderTransformer: %q (%T) needs %s", dag.VertexName(v), v, dag.VertexName(target))
|
||||
if pv, ok := v.(GraphNodeProviderConsumer); ok {
|
||||
pv.SetProvider(target.ProviderAddr())
|
||||
return diags.Err()
|
||||
}
|
||||
log.Printf("[DEBUG] ProviderTransformer: %q (%T) actions need %s", dag.VertexName(v), v, dag.VertexName(target))
|
||||
g.Connect(dag.BasicEdge(v, target))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue