| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package views | |
| import ( | |
| "bufio" | |
| "fmt" | |
| "strings" | |
| "sync" | |
| "time" | |
| "unicode" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/command/format" | |
| "github.com/hashicorp/terraform/internal/command/views/json" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/genconfig" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/policy" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| ) | |
| func newJSONHook(view *JSONView) *jsonHook { | |
| return &jsonHook{ | |
| view: view, | |
| resourceProgress: make(map[string]resourceProgress), | |
| timeNow: time.Now, | |
| timeAfter: time.After, | |
| periodicUiTimer: defaultPeriodicUiTimer, | |
| } | |
| } | |
| type jsonHook struct { | |
| terraform.NilHook | |
| view *JSONView | |
| // Concurrent map of resource addresses to allow tracking | |
| // progress, and post-action messages to share data about the resource | |
| resourceProgress map[string]resourceProgress | |
| resourceProgressMu sync.Mutex | |
| // Mockable functions for testing the progress timer goroutine | |
| timeNow func() time.Time | |
| timeAfter func(time.Duration) <-chan time.Time | |
| periodicUiTimer time.Duration | |
| } | |
| func (h *jsonHook) PolicyResult(addr string, resp policy.EvaluationResponse) (terraform.HookAction, error) { | |
| h.view.PolicyResult(addr, resp) | |
| return terraform.HookActionContinue, nil | |
| } | |
| var _ terraform.Hook = (*jsonHook)(nil) | |
| type resourceProgress struct { | |
| addr addrs.AbsResourceInstance | |
| action plans.Action | |
| start time.Time | |
| // done is used for post-action to stop the progress goroutine | |
| done chan struct{} | |
| // heartbeatDone is used to allow tests to safely wait for the progress | |
| // goroutine to finish | |
| heartbeatDone chan struct{} | |
| } | |
| func (h *jsonHook) PreApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { | |
| if action != plans.NoOp { | |
| idKey, idValue := format.ObjectValueIDOrName(priorState) | |
| h.view.Hook(json.NewApplyStart(id.Addr, action, idKey, idValue)) | |
| } | |
| progress := resourceProgress{ | |
| addr: id.Addr, | |
| action: action, | |
| start: h.timeNow().Round(time.Second), | |
| done: make(chan struct{}), | |
| heartbeatDone: make(chan struct{}), | |
| } | |
| h.resourceProgressMu.Lock() | |
| h.resourceProgress[id.Addr.String()] = progress | |
| h.resourceProgressMu.Unlock() | |
| if action != plans.NoOp { | |
| go h.applyingHeartbeat(progress) | |
| } | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) applyingHeartbeat(progress resourceProgress) { | |
| defer close(progress.heartbeatDone) | |
| for { | |
| select { | |
| case <-progress.done: | |
| return | |
| case <-h.timeAfter(h.periodicUiTimer): | |
| } | |
| elapsed := h.timeNow().Round(time.Second).Sub(progress.start) | |
| h.view.Hook(json.NewApplyProgress(progress.addr, progress.action, elapsed)) | |
| } | |
| } | |
| func (h *jsonHook) PostApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (terraform.HookAction, error) { | |
| key := id.Addr.String() | |
| h.resourceProgressMu.Lock() | |
| progress := h.resourceProgress[key] | |
| if progress.done != nil { | |
| close(progress.done) | |
| } | |
| delete(h.resourceProgress, key) | |
| h.resourceProgressMu.Unlock() | |
| if progress.action == plans.NoOp { | |
| return terraform.HookActionContinue, nil | |
| } | |
| elapsed := h.timeNow().Round(time.Second).Sub(progress.start) | |
| if err != nil { | |
| // Errors are collected and displayed post-apply, so no need to | |
| // re-render them here. Instead just signal that this resource failed | |
| // to apply. | |
| h.view.Hook(json.NewApplyErrored(id.Addr, progress.action, elapsed)) | |
| } else { | |
| idKey, idValue := format.ObjectValueID(newState) | |
| h.view.Hook(json.NewApplyComplete(id.Addr, progress.action, idKey, idValue, elapsed)) | |
| } | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) PreProvisionInstanceStep(id terraform.HookResourceIdentity, typeName string) (terraform.HookAction, error) { | |
| h.view.Hook(json.NewProvisionStart(id.Addr, typeName)) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) PostProvisionInstanceStep(id terraform.HookResourceIdentity, typeName string, err error) (terraform.HookAction, error) { | |
| if err != nil { | |
| // Errors are collected and displayed post-apply, so no need to | |
| // re-render them here. Instead just signal that this provisioner step | |
| // failed. | |
| h.view.Hook(json.NewProvisionErrored(id.Addr, typeName)) | |
| } else { | |
| h.view.Hook(json.NewProvisionComplete(id.Addr, typeName)) | |
| } | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) ProvisionOutput(id terraform.HookResourceIdentity, typeName string, msg string) { | |
| s := bufio.NewScanner(strings.NewReader(msg)) | |
| s.Split(scanLines) | |
| for s.Scan() { | |
| line := strings.TrimRightFunc(s.Text(), unicode.IsSpace) | |
| if line != "" { | |
| h.view.Hook(json.NewProvisionProgress(id.Addr, typeName, line)) | |
| } | |
| } | |
| } | |
| func (h *jsonHook) PreRefresh(id terraform.HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (terraform.HookAction, error) { | |
| idKey, idValue := format.ObjectValueID(priorState) | |
| h.view.Hook(json.NewRefreshStart(id.Addr, idKey, idValue)) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) PostRefresh(id terraform.HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value, newState cty.Value) (terraform.HookAction, error) { | |
| idKey, idValue := format.ObjectValueID(newState) | |
| h.view.Hook(json.NewRefreshComplete(id.Addr, idKey, idValue)) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) PreEphemeralOp(id terraform.HookResourceIdentity, action plans.Action) (terraform.HookAction, error) { | |
| // this uses the same plans.Read action as a data source to indicate that | |
| // the ephemeral resource can't be processed until apply, so there is no | |
| // progress hook | |
| if action == plans.Read { | |
| return terraform.HookActionContinue, nil | |
| } | |
| h.view.Hook(json.NewEphemeralOpStart(id.Addr, action)) | |
| progress := resourceProgress{ | |
| addr: id.Addr, | |
| action: action, | |
| start: h.timeNow().Round(time.Second), | |
| done: make(chan struct{}), | |
| heartbeatDone: make(chan struct{}), | |
| } | |
| h.resourceProgressMu.Lock() | |
| h.resourceProgress[id.Addr.String()] = progress | |
| h.resourceProgressMu.Unlock() | |
| go h.ephemeralOpHeartbeat(progress) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) ephemeralOpHeartbeat(progress resourceProgress) { | |
| defer close(progress.heartbeatDone) | |
| for { | |
| select { | |
| case <-progress.done: | |
| return | |
| case <-h.timeAfter(h.periodicUiTimer): | |
| } | |
| elapsed := h.timeNow().Round(time.Second).Sub(progress.start) | |
| h.view.Hook(json.NewEphemeralOpProgress(progress.addr, progress.action, elapsed)) | |
| } | |
| } | |
| func (h *jsonHook) PostEphemeralOp(id terraform.HookResourceIdentity, action plans.Action, opErr error) (terraform.HookAction, error) { | |
| key := id.Addr.String() | |
| h.resourceProgressMu.Lock() | |
| progress := h.resourceProgress[key] | |
| if progress.done != nil { | |
| close(progress.done) | |
| } | |
| delete(h.resourceProgress, key) | |
| h.resourceProgressMu.Unlock() | |
| if progress.action == plans.NoOp { | |
| return terraform.HookActionContinue, nil | |
| } | |
| elapsed := h.timeNow().Round(time.Second).Sub(progress.start) | |
| if opErr != nil { | |
| // Errors are collected and displayed post-operation, so no need to | |
| // re-render them here. Instead just signal that this operation failed. | |
| h.view.Hook(json.NewEphemeralOpErrored(id.Addr, progress.action, elapsed)) | |
| } else { | |
| h.view.Hook(json.NewEphemeralOpComplete(id.Addr, progress.action, elapsed)) | |
| } | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) PreListQuery(id terraform.HookResourceIdentity, input_config cty.Value, configSchema *configschema.Block) (terraform.HookAction, error) { | |
| addr := id.Addr | |
| h.view.log.Info( | |
| fmt.Sprintf("%s: Starting query...", addr.String()), | |
| "type", json.MessageListStart, | |
| json.MessageListStart, json.NewQueryStart(addr, input_config, configSchema), | |
| ) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) PostListQuery(id terraform.HookResourceIdentity, results plans.QueryResults, identityVersion int64) (terraform.HookAction, error) { | |
| addr := id.Addr | |
| data := results.Value.GetAttr("data") | |
| iter := data.ElementIterator() | |
| for idx := 0; iter.Next(); idx++ { | |
| _, value := iter.Element() | |
| var generated *genconfig.ResourceImport | |
| if len(results.Generated.Imports) > 0 { | |
| generated = &results.Generated.Imports[idx] | |
| } | |
| result := json.NewQueryResult(addr, value, identityVersion, generated) | |
| h.view.log.Info( | |
| fmt.Sprintf("%s: Result found", addr.String()), | |
| "type", json.MessageListResourceFound, | |
| json.MessageListResourceFound, result, | |
| ) | |
| } | |
| h.view.log.Info( | |
| fmt.Sprintf("%s: List complete", addr.String()), | |
| "type", json.MessageListComplete, | |
| json.MessageListComplete, json.NewQueryComplete(addr, data.LengthInt()), | |
| ) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) StartAction(id terraform.HookActionIdentity) (terraform.HookAction, error) { | |
| h.view.Hook(json.NewActionStart(id)) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) ProgressAction(id terraform.HookActionIdentity, progress string) (terraform.HookAction, error) { | |
| h.view.Hook(json.NewActionProgress(id, progress)) | |
| return terraform.HookActionContinue, nil | |
| } | |
| func (h *jsonHook) CompleteAction(id terraform.HookActionIdentity, err error) (terraform.HookAction, error) { | |
| if err != nil { | |
| h.view.Hook(json.NewActionErrored(id, err)) | |
| } else { | |
| h.view.Hook(json.NewActionComplete(id)) | |
| } | |
| return terraform.HookActionContinue, nil | |
| } |