mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-06 22:23:43 -06:00
275a44f552
We used to treat the "id" attribute of a resource as special and elevate it into its own struct field "ID" in the state, but the new state format and provider protocol treats it just as any other attribute. However, it's still useful to show the value of a single identifying attribute when there isn't room in the UI for showing all of the attributes, and so here we take a new strategy of considering "id" along with some other conventional names as special only in the UI layer. This new heuristic approach can be adjusted over time as new provider patterns emerge, but for now it covers some common conventions we've seen in real providers. With that said, since all existing providers made for Terraform versions prior to v0.12 were forced to set "id", we won't see any use of other attributes here until providers are updated to remove the placeholder ids they were generating in cases where an id was not actually relevant but was forced by the old protocol. At that point the UX should be improved by showing a more relevant attribute instead. We now also allow for the possibility of no id at all, since that is valid for resources that exist only within the Terraform state, like the ones from the "random" and "tls" providers.
403 lines
9.7 KiB
Go
403 lines
9.7 KiB
Go
package command
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/mitchellh/cli"
|
|
"github.com/mitchellh/colorstring"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/command/format"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/providers"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
const defaultPeriodicUiTimer = 10 * time.Second
|
|
const maxIdLen = 80
|
|
|
|
type UiHook struct {
|
|
terraform.NilHook
|
|
|
|
Colorize *colorstring.Colorize
|
|
Ui cli.Ui
|
|
PeriodicUiTimer time.Duration
|
|
|
|
l sync.Mutex
|
|
once sync.Once
|
|
resources map[string]uiResourceState
|
|
ui cli.Ui
|
|
}
|
|
|
|
var _ terraform.Hook = (*UiHook)(nil)
|
|
|
|
// uiResourceState tracks the state of a single resource
|
|
type uiResourceState struct {
|
|
DispAddr string
|
|
IDKey, IDValue string
|
|
Op uiResourceOp
|
|
Start time.Time
|
|
|
|
DoneCh chan struct{} // To be used for cancellation
|
|
|
|
done chan struct{} // used to coordinate tests
|
|
}
|
|
|
|
// uiResourceOp is an enum for operations on a resource
|
|
type uiResourceOp byte
|
|
|
|
const (
|
|
uiResourceUnknown uiResourceOp = iota
|
|
uiResourceCreate
|
|
uiResourceModify
|
|
uiResourceDestroy
|
|
)
|
|
|
|
func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
|
|
h.once.Do(h.init)
|
|
|
|
dispAddr := addr.String()
|
|
if gen != states.CurrentGen {
|
|
dispAddr = fmt.Sprintf("%s (%s)", dispAddr, gen)
|
|
}
|
|
|
|
var operation string
|
|
var op uiResourceOp
|
|
idKey, idValue := format.ObjectValueIDOrName(priorState)
|
|
switch action {
|
|
case plans.Delete:
|
|
operation = "Destroying..."
|
|
op = uiResourceDestroy
|
|
case plans.Create:
|
|
operation = "Creating..."
|
|
op = uiResourceCreate
|
|
case plans.Update:
|
|
operation = "Modifying..."
|
|
op = uiResourceModify
|
|
default:
|
|
// We don't expect any other actions in here, so anything else is a
|
|
// bug in the caller but we'll ignore it in order to be robust.
|
|
h.ui.Output(fmt.Sprintf("(Unknown action %s for %s)", action, dispAddr))
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
attrBuf := new(bytes.Buffer)
|
|
|
|
// Get all the attributes that are changing, and sort them. Also
|
|
// determine the longest key so that we can align them all.
|
|
keyLen := 0
|
|
|
|
// FIXME: This is stubbed out in preparation for rewriting it to use
|
|
// a structural presentation rather than the old-style flatmap one.
|
|
// We just assume no attributes at all for now, pending new code to
|
|
// work with the two cty.Values we are given.
|
|
dAttrs := map[string]terraform.ResourceAttrDiff{}
|
|
keys := make([]string, 0, len(dAttrs))
|
|
for key, _ := range dAttrs {
|
|
keys = append(keys, key)
|
|
if len(key) > keyLen {
|
|
keyLen = len(key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
// Go through and output each attribute
|
|
for _, attrK := range keys {
|
|
attrDiff := dAttrs[attrK]
|
|
|
|
v := attrDiff.New
|
|
u := attrDiff.Old
|
|
if attrDiff.NewComputed {
|
|
v = "<computed>"
|
|
}
|
|
|
|
if attrDiff.Sensitive {
|
|
u = "<sensitive>"
|
|
v = "<sensitive>"
|
|
}
|
|
|
|
attrBuf.WriteString(fmt.Sprintf(
|
|
" %s:%s %#v => %#v\n",
|
|
attrK,
|
|
strings.Repeat(" ", keyLen-len(attrK)),
|
|
u,
|
|
v))
|
|
}
|
|
|
|
attrString := strings.TrimSpace(attrBuf.String())
|
|
if attrString != "" {
|
|
attrString = "\n " + attrString
|
|
}
|
|
|
|
var stateIdSuffix string
|
|
if idKey != "" && idValue != "" {
|
|
stateIdSuffix = fmt.Sprintf(" [%s=%s]", idKey, idValue)
|
|
} else {
|
|
// Make sure they are both empty so we can deal with this more
|
|
// easily in the other hook methods.
|
|
idKey = ""
|
|
idValue = ""
|
|
}
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][bold]%s: %s%s[reset]%s",
|
|
dispAddr,
|
|
operation,
|
|
stateIdSuffix,
|
|
attrString,
|
|
)))
|
|
|
|
key := addr.String()
|
|
uiState := uiResourceState{
|
|
DispAddr: key,
|
|
IDKey: idKey,
|
|
IDValue: idValue,
|
|
Op: op,
|
|
Start: time.Now().Round(time.Second),
|
|
DoneCh: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
h.l.Lock()
|
|
h.resources[key] = uiState
|
|
h.l.Unlock()
|
|
|
|
// Start goroutine that shows progress
|
|
go h.stillApplying(uiState)
|
|
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
func (h *UiHook) stillApplying(state uiResourceState) {
|
|
defer close(state.done)
|
|
for {
|
|
select {
|
|
case <-state.DoneCh:
|
|
return
|
|
|
|
case <-time.After(h.PeriodicUiTimer):
|
|
// Timer up, show status
|
|
}
|
|
|
|
var msg string
|
|
switch state.Op {
|
|
case uiResourceModify:
|
|
msg = "Still modifying..."
|
|
case uiResourceDestroy:
|
|
msg = "Still destroying..."
|
|
case uiResourceCreate:
|
|
msg = "Still creating..."
|
|
case uiResourceUnknown:
|
|
return
|
|
}
|
|
|
|
idSuffix := ""
|
|
if state.IDKey != "" {
|
|
idSuffix = fmt.Sprintf("%s=%s, ", state.IDKey, truncateId(state.IDValue, maxIdLen))
|
|
}
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][bold]%s: %s [%s%s elapsed][reset]",
|
|
state.DispAddr,
|
|
msg,
|
|
idSuffix,
|
|
time.Now().Round(time.Second).Sub(state.Start),
|
|
)))
|
|
}
|
|
}
|
|
|
|
func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) {
|
|
|
|
id := addr.String()
|
|
|
|
h.l.Lock()
|
|
state := h.resources[id]
|
|
if state.DoneCh != nil {
|
|
close(state.DoneCh)
|
|
}
|
|
|
|
delete(h.resources, id)
|
|
h.l.Unlock()
|
|
|
|
var stateIdSuffix string
|
|
if k, v := format.ObjectValueID(newState); k != "" && v != "" {
|
|
stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
|
|
}
|
|
|
|
var msg string
|
|
switch state.Op {
|
|
case uiResourceModify:
|
|
msg = "Modifications complete"
|
|
case uiResourceDestroy:
|
|
msg = "Destruction complete"
|
|
case uiResourceCreate:
|
|
msg = "Creation complete"
|
|
case uiResourceUnknown:
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
if applyerr != nil {
|
|
// Errors are collected and printed in ApplyCommand, no need to duplicate
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
colorized := h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][bold]%s: %s after %s%s[reset]",
|
|
addr, msg, time.Now().Round(time.Second).Sub(state.Start), stateIdSuffix))
|
|
|
|
h.ui.Output(colorized)
|
|
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
func (h *UiHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (terraform.HookAction, error) {
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][bold]%s: Provisioning with '%s'...[reset]",
|
|
addr, typeName,
|
|
)))
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) {
|
|
var buf bytes.Buffer
|
|
buf.WriteString(h.Colorize.Color("[reset]"))
|
|
|
|
prefix := fmt.Sprintf("%s (%s): ", addr, typeName)
|
|
s := bufio.NewScanner(strings.NewReader(msg))
|
|
s.Split(scanLines)
|
|
for s.Scan() {
|
|
line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
|
|
if line != "" {
|
|
buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
|
|
}
|
|
}
|
|
|
|
h.ui.Output(strings.TrimSpace(buf.String()))
|
|
}
|
|
|
|
func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
|
|
h.once.Do(h.init)
|
|
|
|
var stateIdSuffix string
|
|
if k, v := format.ObjectValueID(priorState); k != "" && v != "" {
|
|
stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
|
|
}
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][bold]%s: Refreshing state...%s",
|
|
addr, stateIdSuffix)))
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) {
|
|
h.once.Do(h.init)
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][bold]%s: Importing from ID %q...",
|
|
addr, importID,
|
|
)))
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (terraform.HookAction, error) {
|
|
h.once.Do(h.init)
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][bold][green]%s: Import complete!", addr)))
|
|
for _, s := range imported {
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
"[reset][green] Imported %s",
|
|
s.TypeName,
|
|
)))
|
|
}
|
|
|
|
return terraform.HookActionContinue, nil
|
|
}
|
|
|
|
func (h *UiHook) init() {
|
|
if h.Colorize == nil {
|
|
panic("colorize not given")
|
|
}
|
|
if h.PeriodicUiTimer == 0 {
|
|
h.PeriodicUiTimer = defaultPeriodicUiTimer
|
|
}
|
|
|
|
h.resources = make(map[string]uiResourceState)
|
|
|
|
// Wrap the ui so that it is safe for concurrency regardless of the
|
|
// underlying reader/writer that is in place.
|
|
h.ui = &cli.ConcurrentUi{Ui: h.Ui}
|
|
}
|
|
|
|
// scanLines is basically copied from the Go standard library except
|
|
// we've modified it to also fine `\r`.
|
|
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
if atEOF && len(data) == 0 {
|
|
return 0, nil, nil
|
|
}
|
|
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
|
// We have a full newline-terminated line.
|
|
return i + 1, dropCR(data[0:i]), nil
|
|
}
|
|
if i := bytes.IndexByte(data, '\r'); i >= 0 {
|
|
// We have a full newline-terminated line.
|
|
return i + 1, dropCR(data[0:i]), nil
|
|
}
|
|
// If we're at EOF, we have a final, non-terminated line. Return it.
|
|
if atEOF {
|
|
return len(data), dropCR(data), nil
|
|
}
|
|
// Request more data.
|
|
return 0, nil, nil
|
|
}
|
|
|
|
// dropCR drops a terminal \r from the data.
|
|
func dropCR(data []byte) []byte {
|
|
if len(data) > 0 && data[len(data)-1] == '\r' {
|
|
return data[0 : len(data)-1]
|
|
}
|
|
return data
|
|
}
|
|
|
|
func truncateId(id string, maxLen int) string {
|
|
totalLength := len(id)
|
|
if totalLength <= maxLen {
|
|
return id
|
|
}
|
|
if maxLen < 5 {
|
|
// We don't shorten to less than 5 chars
|
|
// as that would be pointless with ... (3 chars)
|
|
maxLen = 5
|
|
}
|
|
|
|
dots := "..."
|
|
partLen := maxLen / 2
|
|
|
|
leftIdx := partLen - 1
|
|
leftPart := id[0:leftIdx]
|
|
|
|
rightIdx := totalLength - partLen - 1
|
|
|
|
overlap := maxLen - (partLen*2 + len(dots))
|
|
if overlap < 0 {
|
|
rightIdx -= overlap
|
|
}
|
|
|
|
rightPart := id[rightIdx:]
|
|
|
|
return leftPart + dots + rightPart
|
|
}
|