Skip to content

Add actions to the apply graph #37349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d881d91
make abs action instances referencable
DanielMSchmidt Jul 18, 2025
5048967
add helper to iterate over elements of an addrs map
DanielMSchmidt Jul 18, 2025
a55f63d
add information for graph building into plan action invocations
DanielMSchmidt Jul 18, 2025
805d137
implement applying action invocations
DanielMSchmidt Jul 18, 2025
501a439
use better hook identity
DanielMSchmidt Jul 18, 2025
d38f0c9
add tests involving config
DanielMSchmidt Jul 18, 2025
39aee68
fix action addrs test
DanielMSchmidt Jul 18, 2025
c1e0d4a
terraform init should pick up action providers
DanielMSchmidt Jul 21, 2025
737ca06
ensure provider is correctly set in plan
DanielMSchmidt Jul 21, 2025
c3e9fff
establish dependency between action declaration instance and applyabl…
DanielMSchmidt Jul 21, 2025
50bfcf8
use same attribute mapping as provider protocol
DanielMSchmidt Jul 22, 2025
18f5861
improve error messages
DanielMSchmidt Jul 22, 2025
2d67d09
add graph dotter interface
DanielMSchmidt Jul 22, 2025
d2a9457
add a way for action apply and resource instance nodes to announce th…
DanielMSchmidt Jul 22, 2025
4749ffd
make tests use two providers
DanielMSchmidt Jul 23, 2025
9546a86
mark field 10 as reserved
DanielMSchmidt Jul 23, 2025
2e0742c
make AbsActionInstance not referencable
DanielMSchmidt Jul 23, 2025
635576d
improve error messages
DanielMSchmidt Jul 23, 2025
a458c9f
use existing schema and ensure no linked or lifecycle actions can be …
DanielMSchmidt Jul 24, 2025
6469c24
remove duplicate place to store action invocations from plan
DanielMSchmidt Jul 24, 2025
78d136e
fix protobuf typo
DanielMSchmidt Jul 24, 2025
6534c5d
remove extra newlines
DanielMSchmidt Jul 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/plugin-protocol/tfplugin5.proto
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,8 @@ message GetProviderSchema {
map<string, Function> functions = 7;
map<string, Schema> ephemeral_resource_schemas = 8;
map<string, Schema> list_resource_schemas = 9;
map<string, ActionSchema> action_schemas = 10;
reserved 10; // Field number 10 is used by state stores in version 6
map<string, ActionSchema> action_schemas = 11;
repeated Diagnostic diagnostics = 4;
Schema provider_meta = 5;
ServerCapabilities server_capabilities = 6;
Expand Down Expand Up @@ -936,9 +937,9 @@ message PlanAction {
// linked resources, this should match the order the resources are specified in the schema.
repeated LinkedResource linked_resources = 2;
// config of the action, based on the schema of the actual action
DynamicValue config = 6;
DynamicValue config = 3;
// metadata
ClientCapabilities client_capabilities = 8;
ClientCapabilities client_capabilities = 4;
}

message Response {
Expand Down
4 changes: 2 additions & 2 deletions docs/plugin-protocol/tfplugin6.proto
Original file line number Diff line number Diff line change
Expand Up @@ -974,9 +974,9 @@ message PlanAction {
// linked resources, this should match the order the resources are specified in the schema.
repeated LinkedResource linked_resources = 2;
// config of the action, based on the schema of the actual action
DynamicValue config = 6;
DynamicValue config = 3;
// metadata
ClientCapabilities client_capabilities = 8;
ClientCapabilities client_capabilities = 4;
}

message Response {
Expand Down
19 changes: 0 additions & 19 deletions internal/addrs/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,25 +333,6 @@ type configActionKey string

func (k configActionKey) uniqueKeySigil() {}

// AbsActionInvocationInstance describes the invocation of an action as part of a plan / apply.
type AbsActionInvocationInstance struct {
TriggeringResource AbsResourceInstance
Action AbsActionInstance
TriggerIndex int

// TriggerBlockSourceRange is the location of the action_trigger block
// within the resources lifecycle block that triggered this action.
TriggerBlockSourceRange *tfdiags.SourceRange

// ActionReferenceSourceRange is the location of the action reference
// in the actions list within the action_trigger block.
ActionReferenceSourceRange *tfdiags.SourceRange
}

func (a AbsActionInvocationInstance) String() string {
return fmt.Sprintf("%s.%d.%s", a.TriggeringResource.String(), a.TriggerIndex, a.Action.String())
}

// ParseAbsActionInstanceStr is a helper wrapper around
// ParseAbsActionInstance that takes a string and parses it with the HCL
// native syntax traversal parser before interpreting it.
Expand Down
32 changes: 16 additions & 16 deletions internal/addrs/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ func TestActionInstanceEqual(t *testing.T) {
func TestAbsActionInstanceEqual(t *testing.T) {
actions := []AbsActionInstance{
{
RootModuleInstance,
ActionInstance{
Module: RootModuleInstance,
Action: ActionInstance{
Action: Action{Type: "foo", Name: "bar"},
Key: NoKey,
},
},
{
mustParseModuleInstanceStr("module.child"),
ActionInstance{
Module: mustParseModuleInstanceStr("module.child"),
Action: ActionInstance{
Action: Action{Type: "the", Name: "bloop"},
Key: StringKey("fish"),
},
Expand All @@ -139,15 +139,15 @@ func TestAbsActionInstanceEqual(t *testing.T) {
}{
{ // different keys
AbsActionInstance{
RootModuleInstance,
ActionInstance{
Module: RootModuleInstance,
Action: ActionInstance{
Action: Action{Type: "foo", Name: "bar"},
Key: NoKey,
},
},
AbsActionInstance{
RootModuleInstance,
ActionInstance{
Module: RootModuleInstance,
Action: ActionInstance{
Action: Action{Type: "foo", Name: "bar"},
Key: IntKey(1),
},
Expand All @@ -156,15 +156,15 @@ func TestAbsActionInstanceEqual(t *testing.T) {

{ // different module
AbsActionInstance{
RootModuleInstance,
ActionInstance{
Module: RootModuleInstance,
Action: ActionInstance{
Action: Action{Type: "foo", Name: "bar"},
Key: NoKey,
},
},
AbsActionInstance{
mustParseModuleInstanceStr("module.child[1]"),
ActionInstance{
Module: mustParseModuleInstanceStr("module.child[1]"),
Action: ActionInstance{
Action: Action{Type: "foo", Name: "bar"},
Key: NoKey,
},
Expand All @@ -173,15 +173,15 @@ func TestAbsActionInstanceEqual(t *testing.T) {

{ // totally different
AbsActionInstance{
RootModuleInstance,
ActionInstance{
Module: RootModuleInstance,
Action: ActionInstance{
Action: Action{Type: "oof", Name: "rab"},
Key: NoKey,
},
},
AbsActionInstance{
mustParseModuleInstanceStr("module.foo"),
ActionInstance{
Module: mustParseModuleInstanceStr("module.foo"),
Action: ActionInstance{
Action: Action{Type: "foo", Name: "bar"},
Key: IntKey(11),
},
Expand Down
12 changes: 12 additions & 0 deletions internal/addrs/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package addrs

import "iter"

// Map represents a mapping whose keys are address types that implement
// UniqueKeyer.
//
Expand Down Expand Up @@ -135,3 +137,13 @@ func (m Map[K, V]) Values() []V {
}
return ret
}

func (m Map[K, V]) Iter() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for _, elem := range m.Elements() {
if !yield(elem.Key, elem.Value) {
return
}
}
}
}
8 changes: 4 additions & 4 deletions internal/command/jsonplan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func MarshalForRenderer(
return nil, nil, nil, nil, nil, err
}

if output.ActionInvocations, err = MarshalActionInvocations(p.ActionInvocations); err != nil {
if output.ActionInvocations, err = MarshalActionInvocations(p.Changes.ActionInvocations); err != nil {
return nil, nil, nil, nil, nil, err
}

Expand Down Expand Up @@ -336,9 +336,9 @@ func Marshal(
return nil, fmt.Errorf("error marshaling config: %s", err)
}

// output.ActionInvocations
if p.ActionInvocations != nil {
if output.ActionInvocations, err = MarshalActionInvocations(p.ActionInvocations); err != nil {
// output.Changes.ActionInvocations
if p.Changes.ActionInvocations != nil {
if output.ActionInvocations, err = MarshalActionInvocations(p.Changes.ActionInvocations); err != nil {
return nil, fmt.Errorf("error marshaling action invocations: %s", err)
}
}
Expand Down
73 changes: 36 additions & 37 deletions internal/command/views/hook_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ func testJSONHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResour
}
}

func testJSONHookActionID(addr addrs.AbsActionInvocationInstance) terraform.HookActionIdentity {
return addrs.AbsActionInvocationInstance{
TriggeringResource: addr.TriggeringResource,
Action: addr.Action,
TriggerIndex: addr.TriggerIndex,
func testJSONHookActionID(actionAddr addrs.AbsActionInstance, triggeringResourceAddr addrs.AbsResourceInstance, actionTriggerIndex int, actionsListIndex int) terraform.HookActionIdentity {
return terraform.HookActionIdentity{
Addr: actionAddr,
TriggeringResourceAddr: triggeringResourceAddr,
ActionTriggerBlockIndex: actionTriggerIndex,
ActionsListIndex: actionsListIndex,
}
}

Expand Down Expand Up @@ -593,12 +594,6 @@ func TestJSONHook_actions(t *testing.T) {
Name: "boop",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

invocationA := addrs.AbsActionInvocationInstance{
TriggeringResource: resourceA,
Action: actionA,
TriggerIndex: 23,
}

subModule := addrs.RootModuleInstance.Child("childMod", addrs.StringKey("infra"))
actionB := addrs.AbsActionInstance{
Module: subModule,
Expand All @@ -614,33 +609,31 @@ func TestJSONHook_actions(t *testing.T) {
Name: "boop",
}.Instance(addrs.NoKey).Absolute(subModule)

invocationB := addrs.AbsActionInvocationInstance{
TriggeringResource: resourceB,
Action: actionB,
TriggerIndex: 0,
}
action, err := hook.StartAction(testJSONHookActionID(invocationA))
actionAHookId := testJSONHookActionID(actionA, resourceA, 0, 1)
actionBHookId := testJSONHookActionID(actionB, resourceB, 2, 3)

action, err := hook.StartAction(actionAHookId)
testHookReturnValues(t, action, err)

action, err = hook.ProgressAction(testJSONHookActionID(invocationA), "Hello world from the lambda function")
action, err = hook.ProgressAction(actionAHookId, "Hello world from the lambda function")
testHookReturnValues(t, action, err)

action, err = hook.StartAction(testJSONHookActionID(invocationB))
action, err = hook.StartAction(actionBHookId)
testHookReturnValues(t, action, err)

action, err = hook.ProgressAction(testJSONHookActionID(invocationB), "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]")
action, err = hook.ProgressAction(actionBHookId, "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]")
testHookReturnValues(t, action, err)

action, err = hook.CompleteAction(testJSONHookActionID(invocationB), nil)
action, err = hook.CompleteAction(actionBHookId, nil)
testHookReturnValues(t, action, err)

action, err = hook.CompleteAction(testJSONHookActionID(invocationA), errors.New("lambda terminated with exit code 1"))
action, err = hook.CompleteAction(actionAHookId, errors.New("lambda terminated with exit code 1"))
testHookReturnValues(t, action, err)

want := []map[string]interface{}{
{
"@level": "info",
"@message": "test_instance.boop.trigger[23]: Action Started: action.aws_lambda_invocation.notify_slack[42]",
"@message": "test_instance.boop.trigger[0]: Action Started: action.aws_lambda_invocation.notify_slack[42]",
"@module": "terraform.ui",
"type": "action_start",
"hook": map[string]interface{}{
Expand All @@ -653,6 +646,7 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "notify_slack",
"resource_type": "aws_lambda_invocation",
},
"actions_index": float64(1),
"resource": map[string]interface{}{
"addr": "test_instance.boop",
"implied_provider": "test",
Expand All @@ -662,12 +656,12 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "boop",
"resource_type": "test_instance",
},
"trigger_index": float64(23),
"trigger_index": float64(0),
},
},
{
"@level": "info",
"@message": "test_instance.boop (23): action.aws_lambda_invocation.notify_slack[42] - Hello world from the lambda function",
"@message": "test_instance.boop (0): action.aws_lambda_invocation.notify_slack[42] - Hello world from the lambda function",
"@module": "terraform.ui",
"type": "action_progress",
"hook": map[string]interface{}{
Expand All @@ -680,7 +674,8 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "notify_slack",
"resource_type": "aws_lambda_invocation",
},
"message": "Hello world from the lambda function",
"actions_index": float64(1),
"message": "Hello world from the lambda function",
"resource": map[string]interface{}{
"addr": "test_instance.boop",
"implied_provider": "test",
Expand All @@ -690,12 +685,12 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "boop",
"resource_type": "test_instance",
},
"trigger_index": float64(23),
"trigger_index": float64(0),
},
},
{
"@level": "info",
"@message": "module.childMod[\"infra\"].test_instance.boop.trigger[0]: Action Started: module.childMod[\"infra\"].action.ansible_playbook.webserver",
"@message": "module.childMod[\"infra\"].test_instance.boop.trigger[2]: Action Started: module.childMod[\"infra\"].action.ansible_playbook.webserver",
"@module": "terraform.ui",
"type": "action_start",
"hook": map[string]interface{}{
Expand All @@ -708,6 +703,7 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "webserver",
"resource_type": "ansible_playbook",
},
"actions_index": float64(3),
"resource": map[string]interface{}{
"addr": "module.childMod[\"infra\"].test_instance.boop",
"implied_provider": "test",
Expand All @@ -717,12 +713,12 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "boop",
"resource_type": "test_instance",
},
"trigger_index": float64(0),
"trigger_index": float64(2),
},
},
{
"@level": "info",
"@message": "module.childMod[\"infra\"].test_instance.boop (0): module.childMod[\"infra\"].action.ansible_playbook.webserver - TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]",
"@message": "module.childMod[\"infra\"].test_instance.boop (2): module.childMod[\"infra\"].action.ansible_playbook.webserver - TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]",
"@module": "terraform.ui",
"type": "action_progress",
"hook": map[string]interface{}{
Expand All @@ -735,7 +731,8 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "webserver",
"resource_type": "ansible_playbook",
},
"message": "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]",
"actions_index": float64(3),
"message": "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]",
"resource": map[string]interface{}{
"addr": "module.childMod[\"infra\"].test_instance.boop",
"implied_provider": "test",
Expand All @@ -745,12 +742,12 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "boop",
"resource_type": "test_instance",
},
"trigger_index": float64(0),
"trigger_index": float64(2),
},
},
{
"@level": "info",
"@message": "module.childMod[\"infra\"].test_instance.boop (0): Action Complete: module.childMod[\"infra\"].action.ansible_playbook.webserver",
"@message": "module.childMod[\"infra\"].test_instance.boop (2): Action Complete: module.childMod[\"infra\"].action.ansible_playbook.webserver",
"@module": "terraform.ui",
"type": "action_complete",
"hook": map[string]interface{}{
Expand All @@ -763,6 +760,7 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "webserver",
"resource_type": "ansible_playbook",
},
"actions_index": float64(3),
"resource": map[string]interface{}{
"addr": "module.childMod[\"infra\"].test_instance.boop",
"implied_provider": "test",
Expand All @@ -772,12 +770,12 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "boop",
"resource_type": "test_instance",
},
"trigger_index": float64(0),
"trigger_index": float64(2),
},
},
{
"@level": "info",
"@message": "test_instance.boop (23): Action Errored: action.aws_lambda_invocation.notify_slack[42] - lambda terminated with exit code 1",
"@message": "test_instance.boop (0): Action Errored: action.aws_lambda_invocation.notify_slack[42] - lambda terminated with exit code 1",
"@module": "terraform.ui",
"type": "action_errored",
"hook": map[string]interface{}{
Expand All @@ -790,7 +788,8 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "notify_slack",
"resource_type": "aws_lambda_invocation",
},
"error": "lambda terminated with exit code 1",
"actions_index": float64(1),
"error": "lambda terminated with exit code 1",
"resource": map[string]interface{}{
"addr": "test_instance.boop",
"implied_provider": "test",
Expand All @@ -800,7 +799,7 @@ func TestJSONHook_actions(t *testing.T) {
"resource_name": "boop",
"resource_type": "test_instance",
},
"trigger_index": float64(23),
"trigger_index": float64(0),
},
},
}
Expand Down
Loading