From 54062a52cb65c42417f1490f506901663a368b8c Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Thu, 7 Sep 2023 15:46:10 -0400 Subject: [PATCH] stacks: More extensive in-progress plan events Expand the existing hooks to emit events throughout the planning process, providing enough information for the Terraform Cloud UI to render a live-updating representation of the plan. We also sketch out the equivalent hooks for the apply operation. --- internal/rpcapi/stacks.go | 95 +++++++++++++++ internal/stacks/stackplan/planned_change.go | 8 +- .../stacks/stackplan/planned_change_test.go | 22 ++-- .../stacks/stackruntime/hooks/callbacks.go | 8 ++ .../stackruntime/hooks/component_instance.go | 43 +++++++ .../hooks/componentinstancestatus_string.go | 49 ++++++++ .../hooks/provisionerstatus_string.go | 37 ++++++ .../stackruntime/hooks/resource_instance.go | 105 +++++++++++++++++ .../hooks/resourceinstancestatus_string.go | 57 +++++++++ .../internal/stackeval/component_instance.go | 44 +++++++ .../stackruntime/internal/stackeval/hooks.go | 71 +++++++++++ .../internal/stackeval/terraform_hook.go | 111 ++++++++++++++++++ 12 files changed, 639 insertions(+), 11 deletions(-) create mode 100644 internal/stacks/stackruntime/hooks/component_instance.go create mode 100644 internal/stacks/stackruntime/hooks/componentinstancestatus_string.go create mode 100644 internal/stacks/stackruntime/hooks/provisionerstatus_string.go create mode 100644 internal/stacks/stackruntime/hooks/resource_instance.go create mode 100644 internal/stacks/stackruntime/hooks/resourceinstancestatus_string.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/terraform_hook.go diff --git a/internal/rpcapi/stacks.go b/internal/rpcapi/stacks.go index 619738b9a0..68faacbc88 100644 --- a/internal/rpcapi/stacks.go +++ b/internal/rpcapi/stacks.go @@ -16,9 +16,11 @@ import ( "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providercache" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackconfig" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/stacks/stackruntime" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -325,5 +327,98 @@ func stackPlanHooks(evts terraform1.Stacks_PlanStackChangesServer, mainStackSour span.(trace.Span).End() return nil }, + + // For each component instance, we emit a series of events to the + // client, reporting the status of the plan operation. We also create a + // nested tracing span for the component instance. + PendingComponentInstancePlan: func(ctx context.Context, ci stackaddrs.AbsComponentInstance) { + evts.Send(evtComponentInstanceStatus(ci, hooks.ComponentInstancePending)) + }, + BeginComponentInstancePlan: func(ctx context.Context, ci stackaddrs.AbsComponentInstance) any { + evts.Send(evtComponentInstanceStatus(ci, hooks.ComponentInstancePlanning)) + _, span := tracer.Start(ctx, "planning", trace.WithAttributes( + attribute.String("component_instance", ci.String()), + )) + return span + }, + EndComponentInstancePlan: func(ctx context.Context, span any, ci stackaddrs.AbsComponentInstance) any { + evts.Send(evtComponentInstanceStatus(ci, hooks.ComponentInstancePlanned)) + span.(trace.Span).End() + return nil + }, + ErrorComponentInstancePlan: func(ctx context.Context, span any, ci stackaddrs.AbsComponentInstance) any { + evts.Send(evtComponentInstanceStatus(ci, hooks.ComponentInstanceErrored)) + span.(trace.Span).End() + return nil + }, + + // When Terraform core reports a resource instance plan status, we + // forward it to the events client. + ReportResourceInstanceStatus: func(ctx context.Context, span any, rihd *hooks.ResourceInstanceStatusHookData) any { + evts.Send(&terraform1.PlanStackChanges_Event{ + Event: &terraform1.PlanStackChanges_Event_ResourceInstanceStatus{ + ResourceInstanceStatus: &terraform1.ResourceInstanceStatus{ + Addr: &terraform1.ResourceInstanceInStackAddr{ + ComponentInstanceAddr: rihd.Addr.Component.String(), + ResourceInstanceAddr: rihd.Addr.Item.String(), + }, + Status: rihd.Status.ForProtobuf(), + }, + }, + }) + return span + }, + + // Upon completion of a component instance plan, we emit a planned + // change sumary event to the client for each resource instance. + ReportResourceInstancePlanned: func(ctx context.Context, span any, ric *hooks.ResourceInstanceChange) any { + actions, err := terraform1.ChangeTypesForPlanAction(ric.Change.Action) + if err != nil { + // TODO: what do we do? + return span + } + + moved := &terraform1.ResourceInstancePlannedChange_Moved{} + if !ric.Change.PrevRunAddr.Equal(ric.Change.Addr) { + moved.PrevAddr = &terraform1.ResourceInstanceInStackAddr{ + ComponentInstanceAddr: ric.Addr.Component.String(), + ResourceInstanceAddr: ric.Change.PrevRunAddr.String(), + } + } + + imported := &terraform1.ResourceInstancePlannedChange_Imported{} + if ric.Change.Importing != nil { + imported.ImportId = ric.Change.Importing.ID + } + + evts.Send(&terraform1.PlanStackChanges_Event{ + Event: &terraform1.PlanStackChanges_Event_ResourceInstancePlannedChange{ + ResourceInstancePlannedChange: &terraform1.ResourceInstancePlannedChange{ + Addr: &terraform1.ResourceInstanceInStackAddr{ + ComponentInstanceAddr: ric.Addr.Component.String(), + ResourceInstanceAddr: ric.Addr.Item.String(), + }, + Actions: actions, + Moved: moved, + Imported: imported, + }, + }, + }) + return span + }, + } +} + +func evtComponentInstanceStatus(ci stackaddrs.AbsComponentInstance, status hooks.ComponentInstanceStatus) *terraform1.PlanStackChanges_Event { + return &terraform1.PlanStackChanges_Event{ + Event: &terraform1.PlanStackChanges_Event_ComponentInstanceStatus{ + ComponentInstanceStatus: &terraform1.ComponentInstanceStatus{ + Addr: &terraform1.ComponentInstanceInStackAddr{ + ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(ci).String(), + ComponentInstanceAddr: ci.String(), + }, + Status: status.ForProtobuf(), + }, + }, } } diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index 1f43dedf59..c9542c3404 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -73,9 +73,11 @@ func (pc *PlannedChangeComponentInstance) PlannedChangeProto() (*terraform1.Plan Raw: []*anypb.Any{&raw}, Description: &terraform1.PlannedChange_ComponentInstancePlanned{ ComponentInstancePlanned: &terraform1.PlannedChange_ComponentInstance{ - ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(pc.Addr).String(), - ComponentInstanceAddr: pc.Addr.String(), - Actions: protoChangeTypes, + Addr: &terraform1.ComponentInstanceInStackAddr{ + ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(pc.Addr).String(), + ComponentInstanceAddr: pc.Addr.String(), + }, + Actions: protoChangeTypes, }, }, }, nil diff --git a/internal/stacks/stackplan/planned_change_test.go b/internal/stacks/stackplan/planned_change_test.go index 1c1a77d7ed..90df932ea5 100644 --- a/internal/stacks/stackplan/planned_change_test.go +++ b/internal/stacks/stackplan/planned_change_test.go @@ -92,9 +92,11 @@ func TestPlannedChangeAsProto(t *testing.T) { }, Description: &terraform1.PlannedChange_ComponentInstancePlanned{ ComponentInstancePlanned: &terraform1.PlannedChange_ComponentInstance{ - ComponentAddr: "component.foo", - ComponentInstanceAddr: "component.foo", - Actions: []terraform1.ChangeType{terraform1.ChangeType_CREATE}, + Addr: &terraform1.ComponentInstanceInStackAddr{ + ComponentAddr: "component.foo", + ComponentInstanceAddr: "component.foo", + }, + Actions: []terraform1.ChangeType{terraform1.ChangeType_CREATE}, }, }, }, @@ -118,8 +120,10 @@ func TestPlannedChangeAsProto(t *testing.T) { }, Description: &terraform1.PlannedChange_ComponentInstancePlanned{ ComponentInstancePlanned: &terraform1.PlannedChange_ComponentInstance{ - ComponentAddr: "component.foo", - ComponentInstanceAddr: `component.foo["bar"]`, + Addr: &terraform1.ComponentInstanceInStackAddr{ + ComponentAddr: "component.foo", + ComponentInstanceAddr: `component.foo["bar"]`, + }, }, }, }, @@ -142,9 +146,11 @@ func TestPlannedChangeAsProto(t *testing.T) { }, Description: &terraform1.PlannedChange_ComponentInstancePlanned{ ComponentInstancePlanned: &terraform1.PlannedChange_ComponentInstance{ - ComponentAddr: "stack.a.component.foo", - ComponentInstanceAddr: `stack.a["boop"].component.foo`, - Actions: []terraform1.ChangeType{terraform1.ChangeType_DELETE}, + Addr: &terraform1.ComponentInstanceInStackAddr{ + ComponentAddr: "stack.a.component.foo", + ComponentInstanceAddr: `stack.a["boop"].component.foo`, + }, + Actions: []terraform1.ChangeType{terraform1.ChangeType_DELETE}, }, }, }, diff --git a/internal/stacks/stackruntime/hooks/callbacks.go b/internal/stacks/stackruntime/hooks/callbacks.go index e59a6c3db4..d83b2c4350 100644 --- a/internal/stacks/stackruntime/hooks/callbacks.go +++ b/internal/stacks/stackruntime/hooks/callbacks.go @@ -61,3 +61,11 @@ type MoreFunc[Msg any] func(context.Context, any, Msg) any // should always return nil, because there is no way to mutate the context // with a new tracking value after the fact. type ContextAttachFunc func(parent context.Context, tracking any) context.Context + +// SingleFunc is the signature of a callback for a hook which operates in +// isolation, and has no related or enclosed events. +// +// The given context is guaranteed to preserve the values from whichever +// context was passed to the top-level [stackruntime.Plan] or +// [stackruntime.Apply] call. +type SingleFunc[Msg any] func(context.Context, Msg) diff --git a/internal/stacks/stackruntime/hooks/component_instance.go b/internal/stacks/stackruntime/hooks/component_instance.go new file mode 100644 index 0000000000..ec89737739 --- /dev/null +++ b/internal/stacks/stackruntime/hooks/component_instance.go @@ -0,0 +1,43 @@ +package hooks + +import ( + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" +) + +// ComponentInstanceStatus is a UI-focused description of the overall status +// for a given component instance undergoing a Terraform plan or apply +// operation. The "pending" and "errored" status are used for both operation +// types, and the others will be used only for one of plan or apply. +type ComponentInstanceStatus rune + +//go:generate go run golang.org/x/tools/cmd/stringer -type=ComponentInstanceStatus component_instance.go + +const ( + ComponentInstanceStatusInvalid ComponentInstanceStatus = 0 + ComponentInstancePending ComponentInstanceStatus = '.' + ComponentInstancePlanning ComponentInstanceStatus = 'p' + ComponentInstancePlanned ComponentInstanceStatus = 'P' + ComponentInstanceApplying ComponentInstanceStatus = 'a' + ComponentInstanceApplied ComponentInstanceStatus = 'A' + ComponentInstanceErrored ComponentInstanceStatus = 'E' +) + +// TODO: move this into the rpcapi package somewhere +func (s ComponentInstanceStatus) ForProtobuf() terraform1.ComponentInstanceStatus_Status { + switch s { + case ComponentInstancePending: + return terraform1.ComponentInstanceStatus_PENDING + case ComponentInstancePlanning: + return terraform1.ComponentInstanceStatus_PLANNING + case ComponentInstancePlanned: + return terraform1.ComponentInstanceStatus_PLANNED + case ComponentInstanceApplying: + return terraform1.ComponentInstanceStatus_APPLYING + case ComponentInstanceApplied: + return terraform1.ComponentInstanceStatus_APPLIED + case ComponentInstanceErrored: + return terraform1.ComponentInstanceStatus_ERRORED + default: + return terraform1.ComponentInstanceStatus_INVALID + } +} diff --git a/internal/stacks/stackruntime/hooks/componentinstancestatus_string.go b/internal/stacks/stackruntime/hooks/componentinstancestatus_string.go new file mode 100644 index 0000000000..76111383f7 --- /dev/null +++ b/internal/stacks/stackruntime/hooks/componentinstancestatus_string.go @@ -0,0 +1,49 @@ +// Code generated by "stringer -type=ComponentInstanceStatus component_instance.go"; DO NOT EDIT. + +package hooks + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ComponentInstanceStatusInvalid-0] + _ = x[ComponentInstancePending-46] + _ = x[ComponentInstancePlanning-112] + _ = x[ComponentInstancePlanned-80] + _ = x[ComponentInstanceApplying-97] + _ = x[ComponentInstanceApplied-65] + _ = x[ComponentInstanceErrored-69] +} + +const ( + _ComponentInstanceStatus_name_0 = "ComponentInstanceStatusInvalid" + _ComponentInstanceStatus_name_1 = "ComponentInstancePending" + _ComponentInstanceStatus_name_2 = "ComponentInstanceApplied" + _ComponentInstanceStatus_name_3 = "ComponentInstanceErrored" + _ComponentInstanceStatus_name_4 = "ComponentInstancePlanned" + _ComponentInstanceStatus_name_5 = "ComponentInstanceApplying" + _ComponentInstanceStatus_name_6 = "ComponentInstancePlanning" +) + +func (i ComponentInstanceStatus) String() string { + switch { + case i == 0: + return _ComponentInstanceStatus_name_0 + case i == 46: + return _ComponentInstanceStatus_name_1 + case i == 65: + return _ComponentInstanceStatus_name_2 + case i == 69: + return _ComponentInstanceStatus_name_3 + case i == 80: + return _ComponentInstanceStatus_name_4 + case i == 97: + return _ComponentInstanceStatus_name_5 + case i == 112: + return _ComponentInstanceStatus_name_6 + default: + return "ComponentInstanceStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackruntime/hooks/provisionerstatus_string.go b/internal/stacks/stackruntime/hooks/provisionerstatus_string.go new file mode 100644 index 0000000000..f2fe900c8f --- /dev/null +++ b/internal/stacks/stackruntime/hooks/provisionerstatus_string.go @@ -0,0 +1,37 @@ +// Code generated by "stringer -type=ProvisionerStatus resource_instance.go"; DO NOT EDIT. + +package hooks + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ProvisionerStatusInvalid-0] + _ = x[ProvisionerProvisioning-112] + _ = x[ProvisionerProvisioned-80] + _ = x[ProvisionerErrored-69] +} + +const ( + _ProvisionerStatus_name_0 = "ProvisionerStatusInvalid" + _ProvisionerStatus_name_1 = "ProvisionerErrored" + _ProvisionerStatus_name_2 = "ProvisionerProvisioned" + _ProvisionerStatus_name_3 = "ProvisionerProvisioning" +) + +func (i ProvisionerStatus) String() string { + switch { + case i == 0: + return _ProvisionerStatus_name_0 + case i == 69: + return _ProvisionerStatus_name_1 + case i == 80: + return _ProvisionerStatus_name_2 + case i == 112: + return _ProvisionerStatus_name_3 + default: + return "ProvisionerStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackruntime/hooks/resource_instance.go b/internal/stacks/stackruntime/hooks/resource_instance.go new file mode 100644 index 0000000000..9a158e8da4 --- /dev/null +++ b/internal/stacks/stackruntime/hooks/resource_instance.go @@ -0,0 +1,105 @@ +package hooks + +import ( + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +// ResourceInstanceStatus is a UI-focused description of the overall status +// for a given resource instance undergoing a Terraform plan or apply +// operation. The "pending" and "errored" status are used for both operation +// types, and the others will be used only for one of plan or apply. +type ResourceInstanceStatus rune + +//go:generate go run golang.org/x/tools/cmd/stringer -type=ResourceInstanceStatus resource_instance.go + +const ( + ResourceInstanceStatusInvalid ResourceInstanceStatus = 0 + ResourceInstancePending ResourceInstanceStatus = '.' + ResourceInstanceRefreshing ResourceInstanceStatus = 'r' + ResourceInstanceRefreshed ResourceInstanceStatus = 'R' + ResourceInstancePlanning ResourceInstanceStatus = 'p' + ResourceInstancePlanned ResourceInstanceStatus = 'P' + ResourceInstanceApplying ResourceInstanceStatus = 'a' + ResourceInstanceApplied ResourceInstanceStatus = 'A' + ResourceInstanceErrored ResourceInstanceStatus = 'E' +) + +// TODO: move this into the rpcapi package somewhere +func (s ResourceInstanceStatus) ForProtobuf() terraform1.ResourceInstanceStatus_Status { + switch s { + case ResourceInstancePending: + return terraform1.ResourceInstanceStatus_PENDING + case ResourceInstanceRefreshing: + return terraform1.ResourceInstanceStatus_REFRESHING + case ResourceInstanceRefreshed: + return terraform1.ResourceInstanceStatus_REFRESHED + case ResourceInstancePlanning: + return terraform1.ResourceInstanceStatus_PLANNING + case ResourceInstancePlanned: + return terraform1.ResourceInstanceStatus_PLANNED + case ResourceInstanceApplying: + return terraform1.ResourceInstanceStatus_APPLYING + case ResourceInstanceApplied: + return terraform1.ResourceInstanceStatus_APPLIED + case ResourceInstanceErrored: + return terraform1.ResourceInstanceStatus_ERRORED + default: + return terraform1.ResourceInstanceStatus_INVALID + } +} + +// ProvisionerStatus is a UI-focused description of the progress of a given +// resource instance's provisioner during a Terraform apply operation. Each +// specified provisioner will start in "provisioning" state, and progress to +// either "provisioned" or "errored". +type ProvisionerStatus rune + +//go:generate go run golang.org/x/tools/cmd/stringer -type=ProvisionerStatus resource_instance.go + +const ( + ProvisionerStatusInvalid ProvisionerStatus = 0 + ProvisionerProvisioning ProvisionerStatus = 'p' + ProvisionerProvisioned ProvisionerStatus = 'P' + ProvisionerErrored ProvisionerStatus = 'E' +) + +// TODO: move this into the rpcapi package somewhere +func (s ProvisionerStatus) ForProtobuf() terraform1.ProvisionerStatus_Status { + switch s { + case ProvisionerProvisioning: + return terraform1.ProvisionerStatus_PROVISIONING + case ProvisionerProvisioned: + return terraform1.ProvisionerStatus_PROVISIONING + case ProvisionerErrored: + return terraform1.ProvisionerStatus_ERRORED + default: + return terraform1.ProvisionerStatus_INVALID + } +} + +// ResourceInstanceStatusHookData is the argument type for hook callbacks which +// signal a resource instance's status updates. +type ResourceInstanceStatusHookData struct { + Addr stackaddrs.AbsResourceInstance + Status ResourceInstanceStatus +} + +// ResourceInstanceProvisionerHookData is the argument type for hook callbacks +// which signal a resource instance's provisioner progress, including both +// status updates and optional provisioner output data. +type ResourceInstanceProvisionerHookData struct { + Addr stackaddrs.AbsResourceInstance + Name string + Status ProvisionerStatus + Output *string +} + +// ResourceInstanceChangeSrc is the argument type for hook callbacks which +// signal a detected or planned change for a resource instance resulting from a +// plan operation. +type ResourceInstanceChange struct { + Addr stackaddrs.AbsResourceInstance + Change *plans.ResourceInstanceChangeSrc +} diff --git a/internal/stacks/stackruntime/hooks/resourceinstancestatus_string.go b/internal/stacks/stackruntime/hooks/resourceinstancestatus_string.go new file mode 100644 index 0000000000..217e48460e --- /dev/null +++ b/internal/stacks/stackruntime/hooks/resourceinstancestatus_string.go @@ -0,0 +1,57 @@ +// Code generated by "stringer -type=ResourceInstanceStatus resource_instance.go"; DO NOT EDIT. + +package hooks + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ResourceInstanceStatusInvalid-0] + _ = x[ResourceInstancePending-46] + _ = x[ResourceInstanceRefreshing-114] + _ = x[ResourceInstanceRefreshed-82] + _ = x[ResourceInstancePlanning-112] + _ = x[ResourceInstancePlanned-80] + _ = x[ResourceInstanceApplying-97] + _ = x[ResourceInstanceApplied-65] + _ = x[ResourceInstanceErrored-69] +} + +const ( + _ResourceInstanceStatus_name_0 = "ResourceInstanceStatusInvalid" + _ResourceInstanceStatus_name_1 = "ResourceInstancePending" + _ResourceInstanceStatus_name_2 = "ResourceInstanceApplied" + _ResourceInstanceStatus_name_3 = "ResourceInstanceErrored" + _ResourceInstanceStatus_name_4 = "ResourceInstancePlanned" + _ResourceInstanceStatus_name_5 = "ResourceInstanceRefreshed" + _ResourceInstanceStatus_name_6 = "ResourceInstanceApplying" + _ResourceInstanceStatus_name_7 = "ResourceInstancePlanning" + _ResourceInstanceStatus_name_8 = "ResourceInstanceRefreshing" +) + +func (i ResourceInstanceStatus) String() string { + switch { + case i == 0: + return _ResourceInstanceStatus_name_0 + case i == 46: + return _ResourceInstanceStatus_name_1 + case i == 65: + return _ResourceInstanceStatus_name_2 + case i == 69: + return _ResourceInstanceStatus_name_3 + case i == 80: + return _ResourceInstanceStatus_name_4 + case i == 82: + return _ResourceInstanceStatus_name_5 + case i == 97: + return _ResourceInstanceStatus_name_6 + case i == 112: + return _ResourceInstanceStatus_name_7 + case i == 114: + return _ResourceInstanceStatus_name_8 + default: + return "ResourceInstanceStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index 94242e047a..6ea367c00e 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -364,6 +365,11 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla ctx, &c.moduleTreePlan, c.main, func(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics + + addr := c.Addr() + h := hooksFromContext(ctx) + seq, ctx := hookBegin(ctx, h.BeginComponentInstancePlan, h.ContextAttach, addr) + decl := c.call.Declaration(ctx) // This is our main bridge from the stacks language into the main Terraform @@ -408,6 +414,14 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla } tfCtx, err := terraform.NewContext(&terraform.ContextOpts{ + Hooks: []terraform.Hook{ + &componentInstanceTerraformHook{ + ctx: ctx, + seq: seq, + hooks: hooksFromContext(ctx), + addr: c.Addr(), + }, + }, PreloadedProviderSchemas: providerSchemas, }) if err != nil { @@ -480,6 +494,34 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla ExternalProviders: providerInsts, }) diags = diags.Append(moreDiags) + + if plan != nil { + for _, rsrcChange := range plan.DriftedResources { + hookMore(ctx, seq, h.ReportResourceInstanceDrift, &hooks.ResourceInstanceChange{ + Addr: stackaddrs.AbsResourceInstance{ + Component: addr, + Item: rsrcChange.Addr, + }, + Change: rsrcChange, + }) + } + for _, rsrcChange := range plan.Changes.Resources { + hookMore(ctx, seq, h.ReportResourceInstancePlanned, &hooks.ResourceInstanceChange{ + Addr: stackaddrs.AbsResourceInstance{ + Component: addr, + Item: rsrcChange.Addr, + }, + Change: rsrcChange, + }) + } + } + + if diags.HasErrors() { + hookMore(ctx, seq, h.ErrorComponentInstancePlan, addr) + } else { + hookMore(ctx, seq, h.EndComponentInstancePlan, addr) + } + return plan, diags }, ) @@ -531,6 +573,8 @@ func (c *ComponentInstance) PlanChanges(ctx context.Context) ([]stackplan.Planne var changes []stackplan.PlannedChange var diags tfdiags.Diagnostics + hookSingle(ctx, hooksFromContext(ctx).PendingComponentInstancePlan, c.Addr()) + // We must always at least announce that the component instance exists, // and that must come before any resource instance changes referring to it. changes = append(changes, &stackplan.PlannedChangeComponentInstance{ diff --git a/internal/stacks/stackruntime/internal/stackeval/hooks.go b/internal/stacks/stackruntime/internal/stackeval/hooks.go index 862ee0af76..85490c1d3d 100644 --- a/internal/stacks/stackruntime/internal/stackeval/hooks.go +++ b/internal/stacks/stackruntime/internal/stackeval/hooks.go @@ -4,6 +4,7 @@ import ( "context" "sync" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" ) @@ -30,6 +31,69 @@ type Hooks struct { // information is provided by other means. EndPlan hooks.MoreFunc[struct{}] + // PendingComponentInstancePlan is called at the start of the plan + // operation, before evaluating the component instance's inputs and + // providers. + PendingComponentInstancePlan hooks.SingleFunc[stackaddrs.AbsComponentInstance] + + // BeginComponentInstancePlan is called when the component instance's + // inputs and providers are ready and planning begins, and can be used to + // establish a nested tracing context wrapping the plan operation. + BeginComponentInstancePlan hooks.BeginFunc[stackaddrs.AbsComponentInstance] + + // EndComponentInstancePlan is called when the component instance plan + // started at [Hooks.BeginComponentInstancePlan] completes successfully. If + // a context is established by [Hooks.BeginComponentInstancePlan] then this + // hook should end it. + EndComponentInstancePlan hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // ErrorComponentInstancePlan is similar to [Hooks.EndComponentInstancePlan], but + // is called when the plan operation failed. + ErrorComponentInstancePlan hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // PendingComponentInstanceApply is called at the start of the apply + // operation. + PendingComponentInstanceApply hooks.SingleFunc[stackaddrs.AbsComponentInstance] + + // BeginComponentInstanceApply is called when the component instance starts + // applying the plan, and can be used to establish a nested tracing context + // wrapping the apply operation. + BeginComponentInstanceApply hooks.BeginFunc[stackaddrs.AbsComponentInstance] + + // EndComponentInstanceApply is called when the component instance plan + // started at [Hooks.BeginComponentInstanceApply] completes successfully. If + // a context is established by [Hooks.BeginComponentInstanceApply] then + // this hook should end it. + EndComponentInstanceApply hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // ErrorComponentInstanceApply is similar to [Hooks.EndComponentInstanceApply], but + // is called when the apply operation failed. + ErrorComponentInstanceApply hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // ReportResourceInstanceStatus is called when a resource instance's status + // changes during a plan or apply operation. It should be called inside a + // tracing context established by [Hooks.BeginComponentInstancePlan] or + // [Hooks.BeginComponentInstanceApply]. + ReportResourceInstanceStatus hooks.MoreFunc[*hooks.ResourceInstanceStatusHookData] + + // ReportResourceInstanceProvisionerStatus is called when a provisioner for + // a resource instance begins or ends. It should be called inside a tracing + // context established by [Hooks.BeginComponentInstancePlan] or + // [Hooks.BeginComponentInstanceApply]. + ReportResourceInstanceProvisionerStatus hooks.MoreFunc[*hooks.ResourceInstanceProvisionerHookData] + + // ReportResourceInstanceDrift is called after a component instance's plan + // determines that a resource instance has experienced changes outside of + // Terraform. It should be called inside a tracing context established by + // [Hooks.BeginComponentInstancePlan]. + ReportResourceInstanceDrift hooks.MoreFunc[*hooks.ResourceInstanceChange] + + // ReportResourceInstanceDrift is called after a component instance's plan + // results in proposed changes for a resource instance. It should be called + // inside a tracing context established by + // [Hooks.BeginComponentInstancePlan]. + ReportResourceInstancePlanned hooks.MoreFunc[*hooks.ResourceInstanceChange] + // ContextAttach is an optional callback for wrapping a non-nil value // returned by a [hooks.BeginFunc] into a [context.Context] to be passed // to other context-aware operations that descend from the operation that @@ -115,6 +179,13 @@ func hookMore[Msg any](ctx context.Context, seq *hookSeq, cb hooks.MoreFunc[Msg] seq.mu.Unlock() } +// hookSingle calls an isolated [hooks.SingleFunc] callback, if it is non-nil. +func hookSingle[Msg any](ctx context.Context, cb hooks.SingleFunc[Msg], msg Msg) { + if cb != nil { + cb(ctx, msg) + } +} + // runHookBegin is a lower-level helper that just directly runs a given // callback if it isn't nil and returns its result. If the given callback is // nil then runHookBegin immediately returns nil. diff --git a/internal/stacks/stackruntime/internal/stackeval/terraform_hook.go b/internal/stacks/stackruntime/internal/stackeval/terraform_hook.go new file mode 100644 index 0000000000..12e2bdc9c6 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/terraform_hook.go @@ -0,0 +1,111 @@ +package stackeval + +import ( + "context" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/zclconf/go-cty/cty" +) + +// componentInstanceTerraformHook implements terraform.Hook for plan and apply +// operations on a specified component instance. It connects the standard +// terraform.Hook callbacks to the given stackruntime.Hooks callbacks. +// +// We unfortunately must embed a context.Context in this type, as the existing +// Terraform core hook interface does not support threading a context through. +// The lifetime of this hook instance is strictly smaller than its surrounding +// context, but we should migrate away from this for clarity when possible. +type componentInstanceTerraformHook struct { + terraform.NilHook + + ctx context.Context + seq *hookSeq + hooks *Hooks + addr stackaddrs.AbsComponentInstance +} + +func (h *componentInstanceTerraformHook) resourceInstanceAddr(addr addrs.AbsResourceInstance) stackaddrs.AbsResourceInstance { + return stackaddrs.AbsResourceInstance{ + Component: h.addr, + Item: addr, + } +} + +func (h *componentInstanceTerraformHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (terraform.HookAction, error) { + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: h.resourceInstanceAddr(addr), + Status: hooks.ResourceInstancePlanning, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: h.resourceInstanceAddr(addr), + Status: hooks.ResourceInstancePlanned, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { + if action != plans.NoOp { + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: h.resourceInstanceAddr(addr), + Status: hooks.ResourceInstanceApplying, + }) + } + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) { + // FIXME: need to emit nothing if this was a no-op, which means tracking + // the `action` argument to `PreApply`. See `jsonHook` for more on this. + status := hooks.ResourceInstanceApplied + if err != nil { + status = hooks.ResourceInstanceErrored + } + + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: h.resourceInstanceAddr(addr), + Status: status, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) { + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceProvisionerStatus, &hooks.ResourceInstanceProvisionerHookData{ + Addr: h.resourceInstanceAddr(addr), + Name: typeName, + Status: hooks.ProvisionerProvisioning, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) { + // TODO: determine whether we should continue line splitting as we do with jsonHook + output := msg + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceProvisionerStatus, &hooks.ResourceInstanceProvisionerHookData{ + Addr: h.resourceInstanceAddr(addr), + Name: typeName, + Status: hooks.ProvisionerProvisioning, + Output: &output, + }) +} + +func (h *componentInstanceTerraformHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (terraform.HookAction, error) { + status := hooks.ProvisionerProvisioned + if err != nil { + status = hooks.ProvisionerErrored + } + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceProvisionerStatus, &hooks.ResourceInstanceProvisionerHookData{ + Addr: h.resourceInstanceAddr(addr), + Name: typeName, + Status: status, + }) + return terraform.HookActionContinue, nil +}