github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/hook_json.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"bufio"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  	"unicode"
    12  
    13  	"github.com/zclconf/go-cty/cty"
    14  
    15  	"github.com/terramate-io/tf/addrs"
    16  	"github.com/terramate-io/tf/command/format"
    17  	"github.com/terramate-io/tf/command/views/json"
    18  	"github.com/terramate-io/tf/plans"
    19  	"github.com/terramate-io/tf/states"
    20  	"github.com/terramate-io/tf/terraform"
    21  )
    22  
    23  // How long to wait between sending heartbeat/progress messages
    24  const heartbeatInterval = 10 * time.Second
    25  
    26  func newJSONHook(view *JSONView) *jsonHook {
    27  	return &jsonHook{
    28  		view:      view,
    29  		applying:  make(map[string]applyProgress),
    30  		timeNow:   time.Now,
    31  		timeAfter: time.After,
    32  	}
    33  }
    34  
    35  type jsonHook struct {
    36  	terraform.NilHook
    37  
    38  	view *JSONView
    39  
    40  	// Concurrent map of resource addresses to allow the sequence of pre-apply,
    41  	// progress, and post-apply messages to share data about the resource
    42  	applying     map[string]applyProgress
    43  	applyingLock sync.Mutex
    44  
    45  	// Mockable functions for testing the progress timer goroutine
    46  	timeNow   func() time.Time
    47  	timeAfter func(time.Duration) <-chan time.Time
    48  }
    49  
    50  var _ terraform.Hook = (*jsonHook)(nil)
    51  
    52  type applyProgress struct {
    53  	addr   addrs.AbsResourceInstance
    54  	action plans.Action
    55  	start  time.Time
    56  
    57  	// done is used for post-apply to stop the progress goroutine
    58  	done chan struct{}
    59  
    60  	// heartbeatDone is used to allow tests to safely wait for the progress
    61  	// goroutine to finish
    62  	heartbeatDone chan struct{}
    63  }
    64  
    65  func (h *jsonHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
    66  	if action != plans.NoOp {
    67  		idKey, idValue := format.ObjectValueIDOrName(priorState)
    68  		h.view.Hook(json.NewApplyStart(addr, action, idKey, idValue))
    69  	}
    70  
    71  	progress := applyProgress{
    72  		addr:          addr,
    73  		action:        action,
    74  		start:         h.timeNow().Round(time.Second),
    75  		done:          make(chan struct{}),
    76  		heartbeatDone: make(chan struct{}),
    77  	}
    78  	h.applyingLock.Lock()
    79  	h.applying[addr.String()] = progress
    80  	h.applyingLock.Unlock()
    81  
    82  	if action != plans.NoOp {
    83  		go h.applyingHeartbeat(progress)
    84  	}
    85  	return terraform.HookActionContinue, nil
    86  }
    87  
    88  func (h *jsonHook) applyingHeartbeat(progress applyProgress) {
    89  	defer close(progress.heartbeatDone)
    90  	for {
    91  		select {
    92  		case <-progress.done:
    93  			return
    94  		case <-h.timeAfter(heartbeatInterval):
    95  		}
    96  
    97  		elapsed := h.timeNow().Round(time.Second).Sub(progress.start)
    98  		h.view.Hook(json.NewApplyProgress(progress.addr, progress.action, elapsed))
    99  	}
   100  }
   101  
   102  func (h *jsonHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) {
   103  	key := addr.String()
   104  	h.applyingLock.Lock()
   105  	progress := h.applying[key]
   106  	if progress.done != nil {
   107  		close(progress.done)
   108  	}
   109  	delete(h.applying, key)
   110  	h.applyingLock.Unlock()
   111  
   112  	if progress.action == plans.NoOp {
   113  		return terraform.HookActionContinue, nil
   114  	}
   115  
   116  	elapsed := h.timeNow().Round(time.Second).Sub(progress.start)
   117  
   118  	if err != nil {
   119  		// Errors are collected and displayed post-apply, so no need to
   120  		// re-render them here. Instead just signal that this resource failed
   121  		// to apply.
   122  		h.view.Hook(json.NewApplyErrored(addr, progress.action, elapsed))
   123  	} else {
   124  		idKey, idValue := format.ObjectValueID(newState)
   125  		h.view.Hook(json.NewApplyComplete(addr, progress.action, idKey, idValue, elapsed))
   126  	}
   127  	return terraform.HookActionContinue, nil
   128  }
   129  
   130  func (h *jsonHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
   131  	h.view.Hook(json.NewProvisionStart(addr, typeName))
   132  	return terraform.HookActionContinue, nil
   133  }
   134  
   135  func (h *jsonHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (terraform.HookAction, error) {
   136  	if err != nil {
   137  		// Errors are collected and displayed post-apply, so no need to
   138  		// re-render them here. Instead just signal that this provisioner step
   139  		// failed.
   140  		h.view.Hook(json.NewProvisionErrored(addr, typeName))
   141  	} else {
   142  		h.view.Hook(json.NewProvisionComplete(addr, typeName))
   143  	}
   144  	return terraform.HookActionContinue, nil
   145  }
   146  
   147  func (h *jsonHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) {
   148  	s := bufio.NewScanner(strings.NewReader(msg))
   149  	s.Split(scanLines)
   150  	for s.Scan() {
   151  		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
   152  		if line != "" {
   153  			h.view.Hook(json.NewProvisionProgress(addr, typeName, line))
   154  		}
   155  	}
   156  }
   157  
   158  func (h *jsonHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
   159  	idKey, idValue := format.ObjectValueID(priorState)
   160  	h.view.Hook(json.NewRefreshStart(addr, idKey, idValue))
   161  	return terraform.HookActionContinue, nil
   162  }
   163  
   164  func (h *jsonHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (terraform.HookAction, error) {
   165  	idKey, idValue := format.ObjectValueID(newState)
   166  	h.view.Hook(json.NewRefreshComplete(addr, idKey, idValue))
   167  	return terraform.HookActionContinue, nil
   168  }