From 4e998bb9251854fbf2b3556bf3bfc5ed8cc81689 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 3 Sep 2025 15:41:23 -0400 Subject: [PATCH] actions: validate that action referenced in action_trigger exists in config during transform --- .../terraform/context_plan_actions_test.go | 29 +++++++++++ internal/terraform/transform_config.go | 49 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index 610d5e34597a..6e0cf303a65c 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -3257,6 +3257,35 @@ resource "test_object" "a" { } } +func TestContextPlan_validateActionInTriggerExists(t *testing.T) { + // this validation occurs during TransformConfig + module := ` +resource "test_object" "a" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.act_unlinked.hello] + } + } +} +` + m := testModuleInline(t, map[string]string{"main.tf": module}) + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, nil, DefaultPlanOpts) + if !diags.HasErrors() { + t.Fatal("expected errors, got success!") + } + if diags.Err().Error() != "Configuration for triggered action does not exist: The configuration for the given action action.act_unlinked.hello does not exist. All triggered actions must have an associated configuration." { + t.Fatal("wrong error!") + } +} + func mustActionInstanceAddr(t *testing.T, address string) addrs.AbsActionInstance { action, diags := addrs.ParseAbsActionInstanceStr(address) if len(diags) > 0 { diff --git a/internal/terraform/transform_config.go b/internal/terraform/transform_config.go index 0f7b69e03869..236e8ee6a945 100644 --- a/internal/terraform/transform_config.go +++ b/internal/terraform/transform_config.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -131,9 +132,14 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er } } + // collect all the Action Declarations (configs.Actions) in this module so + // we can validate that actions referenced in a resource's ActionTriggers + // exist in this module. + allConfigActions := make(map[string]*configs.Action) for _, a := range module.Actions { if a != nil { addr := a.Addr().InModule(path) + allConfigActions[addr.String()] = a log.Printf("[TRACE] ConfigTransformer: Adding action %s", addr) abstract := &NodeAbstractAction{ Addr: addr, @@ -162,6 +168,49 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er continue } + // Verify that any actions referenced in the resource's ActionTriggers exist in this module + var diags tfdiags.Diagnostics + if r.Managed != nil && r.Managed.ActionTriggers != nil { + for i, at := range r.Managed.ActionTriggers { + for _, action := range at.Actions { + + refs, parseRefDiags := langrefs.ReferencesInExpr(addrs.ParseRef, action.Expr) + if parseRefDiags != nil { + return parseRefDiags.Err() + } + + var configAction addrs.ConfigAction + + for _, ref := range refs { + switch a := ref.Subject.(type) { + case addrs.Action: + configAction = a.InModule(config.Path) + case addrs.ActionInstance: + configAction = a.Action.InModule(config.Path) + case addrs.CountAttr, addrs.ForEachAttr: + // nothing to do, these will get evaluated later + default: + // This should have been caught during validation + panic(fmt.Sprintf("unexpected action address %T", a)) + } + } + + _, ok := allConfigActions[configAction.String()] + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Configuration for triggered action does not exist", + Detail: fmt.Sprintf("The configuration for the given action %s does not exist. All triggered actions must have an associated configuration.", configAction.String()), + Subject: &r.Managed.ActionTriggers[i].DeclRange, + }) + } + } + } + } + if diags.HasErrors() { + return diags.Err() + } + // If any of the import targets can apply to this node's instances, // filter them down to the applicable addresses. var imports []*ImportTarget