From dc4e328e6202d4ea607dbf70afebe4866cb7b725 Mon Sep 17 00:00:00 2001 From: Roniece Date: Wed, 18 Mar 2026 21:09:20 -0400 Subject: [PATCH] Restore apply behavior --- internal/stacks/stackplan/component.go | 38 +++++++ internal/stacks/stackplan/from_proto.go | 78 ++++++++++++- internal/stacks/stackplan/from_proto_test.go | 111 +++++++++++++++++++ 3 files changed, 226 insertions(+), 1 deletion(-) diff --git a/internal/stacks/stackplan/component.go b/internal/stacks/stackplan/component.go index 87b564f2aa..99b639960e 100644 --- a/internal/stacks/stackplan/component.go +++ b/internal/stacks/stackplan/component.go @@ -52,6 +52,14 @@ type Component struct { // that have changes that are deferred to a later plan and apply cycle. DeferredResourceInstanceChanges addrs.Map[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc] + // ActionInvocations describes planned action invocations that should be + // preserved into the modules runtime apply plan. + ActionInvocations []*plans.ActionInvocationInstanceSrc + + // DeferredActionInvocations describes action invocations that were deferred + // to a later plan and apply cycle. + DeferredActionInvocations []*plans.DeferredActionInvocationSrc + // PlanTimestamp is the time Terraform Core recorded as the single "plan // timestamp", which is used only for the result of the "plantimestamp" // function during apply and must not be used for any other purpose. @@ -114,6 +122,18 @@ func (c *Component) ForModulesRuntime() (*plans.Plan, error) { } } + for _, action := range c.ActionInvocations { + if action != nil { + changes.ActionInvocations = append(changes.ActionInvocations, action) + } + } + + for _, deferredAction := range c.DeferredActionInvocations { + if deferredAction != nil { + plan.DeferredActionInvocations = append(plan.DeferredActionInvocations, deferredAction) + } + } + priorState := states.NewState() ss := priorState.SyncWrapper() for _, elem := range c.ResourceInstancePriorState.Elems { @@ -163,5 +183,23 @@ func (c *Component) RequiredProviderInstances() addrs.Set[addrs.RootProviderConf Alias: elem.Value.Alias, }) } + for _, action := range c.ActionInvocations { + if action == nil { + continue + } + providerInstances.Add(addrs.RootProviderConfig{ + Provider: action.ProviderAddr.Provider, + Alias: action.ProviderAddr.Alias, + }) + } + for _, deferredAction := range c.DeferredActionInvocations { + if deferredAction == nil || deferredAction.ActionInvocationInstanceSrc == nil { + continue + } + providerInstances.Add(addrs.RootProviderConfig{ + Provider: deferredAction.ActionInvocationInstanceSrc.ProviderAddr.Provider, + Alias: deferredAction.ActionInvocationInstanceSrc.ProviderAddr.Alias, + }) + } return providerInstances } diff --git a/internal/stacks/stackplan/from_proto.go b/internal/stacks/stackplan/from_proto.go index 31fd11c97b..db9b440280 100644 --- a/internal/stacks/stackplan/from_proto.go +++ b/internal/stacks/stackplan/from_proto.go @@ -237,6 +237,8 @@ func (l *Loader) AddRaw(rawMsg *anypb.Any) error { ResourceInstancePriorState: addrs.MakeMap[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc](), ResourceInstanceProviderConfig: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig](), DeferredResourceInstanceChanges: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc](), + ActionInvocations: make([]*plans.ActionInvocationInstanceSrc, 0), + DeferredActionInvocations: make([]*plans.DeferredActionInvocationSrc, 0), }) err = c.PlanTimestamp.UnmarshalText([]byte(msg.PlanTimestamp)) if err != nil { @@ -302,7 +304,42 @@ func (l *Loader) AddRaw(rawMsg *anypb.Any) error { }) case *tfstackdata1.PlanActionInvocationPlanned: - // TODO: Implemented in a future apply-related PR. + c, fullAddr, err := LoadComponentForActionInvocation(l.ret, msg) + if err != nil { + return err + } + + action, err := ValidateActionInvocation(msg, fullAddr) + if err != nil { + return err + } + if action != nil { + c.ActionInvocations = append(c.ActionInvocations, action) + } + + case *tfstackdata1.PlanDeferredActionInvocation: + if msg.Deferred == nil { + return fmt.Errorf("missing deferred from PlanDeferredActionInvocation") + } + if msg.Invocation == nil { + return fmt.Errorf("missing invocation from PlanDeferredActionInvocation") + } + + c, fullAddr, err := LoadComponentForActionInvocation(l.ret, msg.Invocation) + if err != nil { + return err + } + + action, err := ValidateActionInvocation(msg.Invocation, fullAddr) + if err != nil { + return err + } + + deferredReason, _ := planfile.DeferredReasonFromProto(msg.Deferred.Reason) + c.DeferredActionInvocations = append(c.DeferredActionInvocations, &plans.DeferredActionInvocationSrc{ + DeferredReason: deferredReason, + ActionInvocationInstanceSrc: action, + }) default: // Should not get here, because a stack plan can only be loaded by @@ -472,3 +509,42 @@ func LoadComponentForPartialResourceInstance(plan *Plan, change *tfstackdata1.Pl return c, fullAddr, providerConfigAddr, nil } + +func ValidateActionInvocation(change *tfstackdata1.PlanActionInvocationPlanned, fullAddr stackaddrs.AbsActionInvocationInstance) (*plans.ActionInvocationInstanceSrc, error) { + if change.Invocation == nil { + return nil, nil + } + + action, err := planfile.ActionInvocationFromProto(change.Invocation) + if err != nil { + return nil, fmt.Errorf("invalid action invocation: %w", err) + } + if !action.Addr.Equal(fullAddr.Item) { + return nil, fmt.Errorf("planned action invocation has inconsistent address to its containing object") + } + return action, nil +} + +func LoadComponentForActionInvocation(plan *Plan, change *tfstackdata1.PlanActionInvocationPlanned) (*Component, stackaddrs.AbsActionInvocationInstance, error) { + cAddr, diags := stackaddrs.ParsePartialComponentInstanceStr(change.ComponentInstanceAddr) + if diags.HasErrors() { + return nil, stackaddrs.AbsActionInvocationInstance{}, fmt.Errorf("invalid component instance address syntax in %q", change.ComponentInstanceAddr) + } + + actionAddr, diags := addrs.ParseAbsActionInstanceStr(change.ActionInvocationAddr) + if diags.HasErrors() { + return nil, stackaddrs.AbsActionInvocationInstance{}, fmt.Errorf("invalid action invocation address syntax in %q", change.ActionInvocationAddr) + } + + fullAddr := stackaddrs.AbsActionInvocationInstance{ + Component: cAddr, + Item: actionAddr, + } + + c, ok := plan.Root.GetOk(cAddr) + if !ok { + return nil, stackaddrs.AbsActionInvocationInstance{}, fmt.Errorf("action invocation change for unannounced component instance %s", cAddr) + } + + return c, fullAddr, nil +} diff --git a/internal/stacks/stackplan/from_proto_test.go b/internal/stacks/stackplan/from_proto_test.go index 43e1a458f7..0950c3c556 100644 --- a/internal/stacks/stackplan/from_proto_test.go +++ b/internal/stacks/stackplan/from_proto_test.go @@ -11,9 +11,14 @@ import ( "github.com/zclconf/go-cty/cty" "google.golang.org/protobuf/types/known/anypb" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" ) @@ -100,3 +105,109 @@ func TestAddRaw(t *testing.T) { }) } } + +func TestAddRaw_ActionInvocations(t *testing.T) { + provider := addrs.MustParseProviderSourceString("example.com/test/actions") + providerConfig := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: provider, + } + action := &plans.ActionInvocationInstanceSrc{ + Addr: addrs.RootModuleInstance.ActionInstance("webhook", "notify", addrs.NoKey), + ActionTrigger: &plans.ResourceActionTrigger{ + TriggeringResourceAddr: addrs.RootModuleInstance.ResourceInstance(addrs.ManagedResourceMode, "example_resource", "main", addrs.NoKey), + ActionTriggerEvent: configs.AfterCreate, + ActionTriggerBlockIndex: 0, + ActionsListIndex: 0, + }, + ProviderAddr: providerConfig, + } + rawAction, err := planfile.ActionInvocationToProto(action) + if err != nil { + t.Fatal(err) + } + + loader := NewLoader() + err = loader.AddRaw(mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{ + ComponentInstanceAddr: "component.web", + PlannedAction: planproto.Action_NOOP, + Mode: planproto.Mode_NORMAL, + PlanTimestamp: "2017-03-27T10:00:00-08:00", + })) + if err != nil { + t.Fatalf("adding component: %v", err) + } + err = loader.AddRaw(mustMarshalAnyPb(&tfstackdata1.PlanActionInvocationPlanned{ + ComponentInstanceAddr: "component.web", + ActionInvocationAddr: action.Addr.String(), + ProviderConfigAddr: provider.String(), + Invocation: rawAction, + })) + if err != nil { + t.Fatalf("adding planned action invocation: %v", err) + } + err = loader.AddRaw(mustMarshalAnyPb(&tfstackdata1.PlanDeferredActionInvocation{ + Deferred: &planproto.Deferred{ + Reason: planproto.DeferredReason_DEFERRED_PREREQ, + }, + Invocation: &tfstackdata1.PlanActionInvocationPlanned{ + ComponentInstanceAddr: "component.web", + ActionInvocationAddr: action.Addr.String(), + ProviderConfigAddr: provider.String(), + Invocation: rawAction, + }, + })) + if err != nil { + t.Fatalf("adding deferred action invocation: %v", err) + } + + componentAddr, diags := stackaddrs.ParseAbsComponentInstanceStr("component.web") + if diags.HasErrors() { + t.Fatalf("parsing component address: %s", diags.Err()) + } + component := loader.ret.GetComponent(componentAddr) + if component == nil { + t.Fatal("expected component to be loaded") + } + + if len(component.ActionInvocations) != 1 { + t.Fatalf("expected 1 planned action invocation, got %d", len(component.ActionInvocations)) + } + if diff := cmp.Diff(action, component.ActionInvocations[0], ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong planned action invocation (-want +got):\n%s", diff) + } + if len(component.DeferredActionInvocations) != 1 { + t.Fatalf("expected 1 deferred action invocation, got %d", len(component.DeferredActionInvocations)) + } + if diff := cmp.Diff(&plans.DeferredActionInvocationSrc{ + DeferredReason: providers.DeferredReasonDeferredPrereq, + ActionInvocationInstanceSrc: action, + }, component.DeferredActionInvocations[0], ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong deferred action invocation (-want +got):\n%s", diff) + } + + modulesPlan, err := component.ForModulesRuntime() + if err != nil { + t.Fatalf("ForModulesRuntime: %v", err) + } + if len(modulesPlan.Changes.ActionInvocations) != 1 { + t.Fatalf("expected 1 planned action invocation in modules runtime plan, got %d", len(modulesPlan.Changes.ActionInvocations)) + } + if diff := cmp.Diff(action, modulesPlan.Changes.ActionInvocations[0], ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong modules runtime action invocation (-want +got):\n%s", diff) + } + if len(modulesPlan.DeferredActionInvocations) != 1 { + t.Fatalf("expected 1 deferred action invocation in modules runtime plan, got %d", len(modulesPlan.DeferredActionInvocations)) + } + if diff := cmp.Diff(&plans.DeferredActionInvocationSrc{ + DeferredReason: providers.DeferredReasonDeferredPrereq, + ActionInvocationInstanceSrc: action, + }, modulesPlan.DeferredActionInvocations[0], ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong modules runtime deferred action invocation (-want +got):\n%s", diff) + } + + requiredProviders := component.RequiredProviderInstances() + if !requiredProviders.Has(addrs.RootProviderConfig{Provider: provider}) { + t.Fatalf("expected action provider %s to be required", provider) + } +}