github.com/opentofu/opentofu@v1.7.1/internal/command/views/hook_json.go (about)

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