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.
pull/34738/head
Alisdair McDiarmid 3 years ago committed by Martin Atkins
parent bdeca87f7a
commit 54062a52cb

@ -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(),
},
},
}
}

@ -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

@ -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},
},
},
},

@ -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)

@ -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
}
}

@ -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) + ")"
}
}

@ -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) + ")"
}
}

@ -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
}

@ -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) + ")"
}
}

@ -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{

@ -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.

@ -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
}
Loading…
Cancel
Save