| // Copyright (c) The OpenTofu Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Copyright (c) 2023 HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package json | |
| import ( | |
| "fmt" | |
| "time" | |
| "github.com/opentofu/opentofu/internal/addrs" | |
| "github.com/opentofu/opentofu/internal/command/jsonentities" | |
| "github.com/opentofu/opentofu/internal/plans" | |
| ) | |
| type Hook interface { | |
| HookType() MessageType | |
| String() string | |
| } | |
| // ApplyStart: triggered by PreApply hook | |
| type applyStart struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Action jsonentities.ChangeAction `json:"action"` | |
| IDKey string `json:"id_key,omitempty"` | |
| IDValue string `json:"id_value,omitempty"` | |
| actionVerb string | |
| } | |
| var _ Hook = (*applyStart)(nil) | |
| func (h *applyStart) HookType() MessageType { | |
| return MessageApplyStart | |
| } | |
| func (h *applyStart) String() string { | |
| var id string | |
| if h.IDKey != "" && h.IDValue != "" { | |
| id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) | |
| } | |
| return fmt.Sprintf("%s: %s...%s", h.Resource.Addr, h.actionVerb, id) | |
| } | |
| func NewApplyStart(addr addrs.AbsResourceInstance, action plans.Action, idKey string, idValue string) Hook { | |
| hook := &applyStart{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Action: jsonentities.ParseChangeAction(action), | |
| IDKey: idKey, | |
| IDValue: idValue, | |
| actionVerb: startActionVerb(action), | |
| } | |
| return hook | |
| } | |
| // ApplyProgress: currently triggered by a timer started on PreApply. In | |
| // future, this might also be triggered by provider progress reporting. | |
| type applyProgress struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Action jsonentities.ChangeAction `json:"action"` | |
| Elapsed float64 `json:"elapsed_seconds"` | |
| actionVerb string | |
| elapsed time.Duration | |
| } | |
| var _ Hook = (*applyProgress)(nil) | |
| func (h *applyProgress) HookType() MessageType { | |
| return MessageApplyProgress | |
| } | |
| func (h *applyProgress) String() string { | |
| return fmt.Sprintf("%s: Still %s... [%s elapsed]", h.Resource.Addr, h.actionVerb, h.elapsed) | |
| } | |
| func NewApplyProgress(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { | |
| return &applyProgress{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Action: jsonentities.ParseChangeAction(action), | |
| Elapsed: elapsed.Seconds(), | |
| actionVerb: progressActionVerb(action), | |
| elapsed: elapsed, | |
| } | |
| } | |
| // ApplyComplete: triggered by PostApply hook | |
| type applyComplete struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Action jsonentities.ChangeAction `json:"action"` | |
| IDKey string `json:"id_key,omitempty"` | |
| IDValue string `json:"id_value,omitempty"` | |
| Elapsed float64 `json:"elapsed_seconds"` | |
| actionNoun string | |
| elapsed time.Duration | |
| } | |
| var _ Hook = (*applyComplete)(nil) | |
| func (h *applyComplete) HookType() MessageType { | |
| return MessageApplyComplete | |
| } | |
| func (h *applyComplete) String() string { | |
| var id string | |
| if h.IDKey != "" && h.IDValue != "" { | |
| id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) | |
| } | |
| return fmt.Sprintf("%s: %s complete after %s%s", h.Resource.Addr, h.actionNoun, h.elapsed, id) | |
| } | |
| func NewApplyComplete(addr addrs.AbsResourceInstance, action plans.Action, idKey, idValue string, elapsed time.Duration) Hook { | |
| return &applyComplete{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Action: jsonentities.ParseChangeAction(action), | |
| IDKey: idKey, | |
| IDValue: idValue, | |
| Elapsed: elapsed.Seconds(), | |
| actionNoun: actionNoun(action), | |
| elapsed: elapsed, | |
| } | |
| } | |
| // ApplyErrored: triggered by PostApply hook on failure. This will be followed | |
| // by diagnostics when the apply finishes. | |
| type applyErrored struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Action jsonentities.ChangeAction `json:"action"` | |
| Elapsed float64 `json:"elapsed_seconds"` | |
| actionNoun string | |
| elapsed time.Duration | |
| } | |
| var _ Hook = (*applyErrored)(nil) | |
| func (h *applyErrored) HookType() MessageType { | |
| return MessageApplyErrored | |
| } | |
| func (h *applyErrored) String() string { | |
| return fmt.Sprintf("%s: %s errored after %s", h.Resource.Addr, h.actionNoun, h.elapsed) | |
| } | |
| func NewApplyErrored(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { | |
| return &applyErrored{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Action: jsonentities.ParseChangeAction(action), | |
| Elapsed: elapsed.Seconds(), | |
| actionNoun: actionNoun(action), | |
| elapsed: elapsed, | |
| } | |
| } | |
| // ProvisionStart: triggered by PreProvisionInstanceStep hook | |
| type provisionStart struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Provisioner string `json:"provisioner"` | |
| } | |
| var _ Hook = (*provisionStart)(nil) | |
| func (h *provisionStart) HookType() MessageType { | |
| return MessageProvisionStart | |
| } | |
| func (h *provisionStart) String() string { | |
| return fmt.Sprintf("%s: Provisioning with '%s'...", h.Resource.Addr, h.Provisioner) | |
| } | |
| func NewProvisionStart(addr addrs.AbsResourceInstance, provisioner string) Hook { | |
| return &provisionStart{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Provisioner: provisioner, | |
| } | |
| } | |
| // ProvisionProgress: triggered by ProvisionOutput hook | |
| type provisionProgress struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Provisioner string `json:"provisioner"` | |
| Output string `json:"output"` | |
| } | |
| var _ Hook = (*provisionProgress)(nil) | |
| func (h *provisionProgress) HookType() MessageType { | |
| return MessageProvisionProgress | |
| } | |
| func (h *provisionProgress) String() string { | |
| return fmt.Sprintf("%s: (%s): %s", h.Resource.Addr, h.Provisioner, h.Output) | |
| } | |
| func NewProvisionProgress(addr addrs.AbsResourceInstance, provisioner string, output string) Hook { | |
| return &provisionProgress{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Provisioner: provisioner, | |
| Output: output, | |
| } | |
| } | |
| // ProvisionComplete: triggered by PostProvisionInstanceStep hook | |
| type provisionComplete struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Provisioner string `json:"provisioner"` | |
| } | |
| var _ Hook = (*provisionComplete)(nil) | |
| func (h *provisionComplete) HookType() MessageType { | |
| return MessageProvisionComplete | |
| } | |
| func (h *provisionComplete) String() string { | |
| return fmt.Sprintf("%s: (%s) Provisioning complete", h.Resource.Addr, h.Provisioner) | |
| } | |
| func NewProvisionComplete(addr addrs.AbsResourceInstance, provisioner string) Hook { | |
| return &provisionComplete{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Provisioner: provisioner, | |
| } | |
| } | |
| // ProvisionErrored: triggered by PostProvisionInstanceStep hook on failure. | |
| // This will be followed by diagnostics when the apply finishes. | |
| type provisionErrored struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Provisioner string `json:"provisioner"` | |
| } | |
| var _ Hook = (*provisionErrored)(nil) | |
| func (h *provisionErrored) HookType() MessageType { | |
| return MessageProvisionErrored | |
| } | |
| func (h *provisionErrored) String() string { | |
| return fmt.Sprintf("%s: (%s) Provisioning errored", h.Resource.Addr, h.Provisioner) | |
| } | |
| func NewProvisionErrored(addr addrs.AbsResourceInstance, provisioner string) Hook { | |
| return &provisionErrored{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Provisioner: provisioner, | |
| } | |
| } | |
| // RefreshStart: triggered by PreRefresh hook | |
| type refreshStart struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| IDKey string `json:"id_key,omitempty"` | |
| IDValue string `json:"id_value,omitempty"` | |
| } | |
| var _ Hook = (*refreshStart)(nil) | |
| func (h *refreshStart) HookType() MessageType { | |
| return MessageRefreshStart | |
| } | |
| func (h *refreshStart) String() string { | |
| var id string | |
| if h.IDKey != "" && h.IDValue != "" { | |
| id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) | |
| } | |
| return fmt.Sprintf("%s: Refreshing state...%s", h.Resource.Addr, id) | |
| } | |
| func NewRefreshStart(addr addrs.AbsResourceInstance, idKey, idValue string) Hook { | |
| return &refreshStart{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| IDKey: idKey, | |
| IDValue: idValue, | |
| } | |
| } | |
| // RefreshComplete: triggered by PostRefresh hook | |
| type refreshComplete struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| IDKey string `json:"id_key,omitempty"` | |
| IDValue string `json:"id_value,omitempty"` | |
| } | |
| var _ Hook = (*refreshComplete)(nil) | |
| func (h *refreshComplete) HookType() MessageType { | |
| return MessageRefreshComplete | |
| } | |
| func (h *refreshComplete) String() string { | |
| var id string | |
| if h.IDKey != "" && h.IDValue != "" { | |
| id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) | |
| } | |
| return fmt.Sprintf("%s: Refresh complete%s", h.Resource.Addr, id) | |
| } | |
| func NewRefreshComplete(addr addrs.AbsResourceInstance, idKey, idValue string) Hook { | |
| return &refreshComplete{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| IDKey: idKey, | |
| IDValue: idValue, | |
| } | |
| } | |
| // EphemeralStart: triggered by PreOpen, PreRenew and PreClose hooks | |
| type ephemeralStart struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Msg string | |
| } | |
| var _ Hook = (*ephemeralStart)(nil) | |
| func (h *ephemeralStart) HookType() MessageType { | |
| return MessageEphemeralActionStart | |
| } | |
| func (h *ephemeralStart) String() string { | |
| return fmt.Sprintf("%s: %s", h.Resource.Addr, h.Msg) | |
| } | |
| func NewEphemeralStart(addr addrs.AbsResourceInstance, startMsg string) Hook { | |
| return &ephemeralStart{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Msg: startMsg, | |
| } | |
| } | |
| // EphemeralStop: triggered by PostOpen, PostRenew and PostClose hooks | |
| type ephemeralStop struct { | |
| Resource jsonentities.ResourceAddr `json:"resource"` | |
| Msg string | |
| } | |
| var _ Hook = (*ephemeralStop)(nil) | |
| func (h *ephemeralStop) HookType() MessageType { | |
| return MessageEphemeralActionComplete | |
| } | |
| func (h *ephemeralStop) String() string { | |
| return fmt.Sprintf("%s: %s", h.Resource.Addr, h.Msg) | |
| } | |
| func NewEphemeralStop(addr addrs.AbsResourceInstance, startMsg string) Hook { | |
| return &ephemeralStop{ | |
| Resource: jsonentities.NewResourceAddr(addr), | |
| Msg: startMsg, | |
| } | |
| } | |
| // Convert the subset of plans.Action values we expect to receive into a | |
| // present-tense verb for the applyStart hook message. | |
| // | |
| // NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks. | |
| func startActionVerb(action plans.Action) string { | |
| switch action { | |
| case plans.Create: | |
| return "Creating" | |
| case plans.Update: | |
| return "Modifying" | |
| case plans.Delete: | |
| return "Destroying" | |
| case plans.Read: | |
| return "Refreshing" | |
| case plans.CreateThenDelete, plans.DeleteThenCreate: | |
| // This is not currently possible to reach, as we receive separate | |
| // passes for create and delete | |
| return "Replacing" | |
| case plans.Forget: | |
| return "Removing" | |
| case plans.NoOp: | |
| // This should never be possible: a no-op planned change should not | |
| // be applied. We'll fall back to "Applying". | |
| fallthrough | |
| default: | |
| return "Applying" | |
| } | |
| } | |
| // Convert the subset of plans.Action values we expect to receive into a | |
| // present-tense verb for the applyProgress hook message. This will be | |
| // prefixed with "Still ", so it is lower-case. | |
| // | |
| // NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks. | |
| func progressActionVerb(action plans.Action) string { | |
| switch action { | |
| case plans.Create: | |
| return "creating" | |
| case plans.Update: | |
| return "modifying" | |
| case plans.Delete: | |
| return "destroying" | |
| case plans.Read: | |
| return "refreshing" | |
| case plans.CreateThenDelete, plans.DeleteThenCreate: | |
| // This is not currently possible to reach, as we receive separate | |
| // passes for create and delete | |
| return "replacing" | |
| case plans.Forget: | |
| return "removing" | |
| case plans.NoOp: | |
| // This should never be possible: a no-op planned change should not | |
| // be applied. We'll fall back to "applying". | |
| fallthrough | |
| default: | |
| return "applying" | |
| } | |
| } | |
| // Convert the subset of plans.Action values we expect to receive into a | |
| // noun for the applyComplete and applyErrored hook messages. This will be | |
| // combined into a phrase like "Creation complete after 1m4s". | |
| // | |
| // NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks. | |
| func actionNoun(action plans.Action) string { | |
| switch action { | |
| case plans.Create: | |
| return "Creation" | |
| case plans.Update: | |
| return "Modifications" | |
| case plans.Delete: | |
| return "Destruction" | |
| case plans.Read: | |
| return "Refresh" | |
| case plans.CreateThenDelete, plans.DeleteThenCreate: | |
| // This is not currently possible to reach, as we receive separate | |
| // passes for create and delete | |
| return "Replacement" | |
| case plans.Forget: | |
| return "Removal" | |
| case plans.NoOp: | |
| // This should never be possible: a no-op planned change should not | |
| // be applied. We'll fall back to "Apply". | |
| fallthrough | |
| default: | |
| return "Apply" | |
| } | |
| } |