blob: 6b3a2ef831b1f92caf3a8e43e8f32e76587b4265 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package views
import (
"bufio"
"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/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
)
// How long to wait between sending heartbeat/progress messages
const heartbeatInterval = 10 * time.Second
func newJSONHook(view *JSONView) *jsonHook {
return &jsonHook{
view: view,
applying: make(map[string]applyProgress),
timeNow: time.Now,
timeAfter: time.After,
}
}
type jsonHook struct {
terraform.NilHook
view *JSONView
// Concurrent map of resource addresses to allow the sequence of pre-apply,
// progress, and post-apply messages to share data about the resource
applying map[string]applyProgress
applyingLock sync.Mutex
// Mockable functions for testing the progress timer goroutine
timeNow func() time.Time
timeAfter func(time.Duration) <-chan time.Time
}
var _ terraform.Hook = (*jsonHook)(nil)
type applyProgress struct {
addr addrs.AbsResourceInstance
action plans.Action
start time.Time
// done is used for post-apply 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(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
if action != plans.NoOp {
idKey, idValue := format.ObjectValueIDOrName(priorState)
h.view.Hook(json.NewApplyStart(addr, action, idKey, idValue))
}
progress := applyProgress{
addr: addr,
action: action,
start: h.timeNow().Round(time.Second),
done: make(chan struct{}),
heartbeatDone: make(chan struct{}),
}
h.applyingLock.Lock()
h.applying[addr.String()] = progress
h.applyingLock.Unlock()
if action != plans.NoOp {
go h.applyingHeartbeat(progress)
}
return terraform.HookActionContinue, nil
}
func (h *jsonHook) applyingHeartbeat(progress applyProgress) {
defer close(progress.heartbeatDone)
for {
select {
case <-progress.done:
return
case <-h.timeAfter(heartbeatInterval):
}
elapsed := h.timeNow().Round(time.Second).Sub(progress.start)
h.view.Hook(json.NewApplyProgress(progress.addr, progress.action, elapsed))
}
}
func (h *jsonHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) {
key := addr.String()
h.applyingLock.Lock()
progress := h.applying[key]
if progress.done != nil {
close(progress.done)
}
delete(h.applying, key)
h.applyingLock.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(addr, progress.action, elapsed))
} else {
idKey, idValue := format.ObjectValueID(newState)
h.view.Hook(json.NewApplyComplete(addr, progress.action, idKey, idValue, elapsed))
}
return terraform.HookActionContinue, nil
}
func (h *jsonHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
h.view.Hook(json.NewProvisionStart(addr, typeName))
return terraform.HookActionContinue, nil
}
func (h *jsonHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, 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(addr, typeName))
} else {
h.view.Hook(json.NewProvisionComplete(addr, typeName))
}
return terraform.HookActionContinue, nil
}
func (h *jsonHook) ProvisionOutput(addr addrs.AbsResourceInstance, 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(addr, typeName, line))
}
}
}
func (h *jsonHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
idKey, idValue := format.ObjectValueID(priorState)
h.view.Hook(json.NewRefreshStart(addr, idKey, idValue))
return terraform.HookActionContinue, nil
}
func (h *jsonHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (terraform.HookAction, error) {
idKey, idValue := format.ObjectValueID(newState)
h.view.Hook(json.NewRefreshComplete(addr, idKey, idValue))
return terraform.HookActionContinue, nil
}