2014-06-27 02:05:21 +02:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
2014-10-06 08:29:13 +02:00
|
|
|
"bufio"
|
2014-07-13 02:03:55 +02:00
|
|
|
"bytes"
|
2014-06-27 02:05:21 +02:00
|
|
|
"fmt"
|
2014-07-13 02:03:55 +02:00
|
|
|
"sort"
|
|
|
|
"strings"
|
2014-06-27 07:01:05 +02:00
|
|
|
"sync"
|
2016-04-13 19:54:11 +02:00
|
|
|
"time"
|
2014-10-12 18:59:08 +02:00
|
|
|
"unicode"
|
2014-06-27 02:05:21 +02:00
|
|
|
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
"github.com/mitchellh/cli"
|
2014-07-13 02:03:55 +02:00
|
|
|
"github.com/mitchellh/colorstring"
|
2014-06-27 02:05:21 +02:00
|
|
|
)
|
|
|
|
|
2017-03-13 21:09:25 +01:00
|
|
|
const defaultPeriodicUiTimer = 10 * time.Second
|
2017-03-01 23:16:22 +01:00
|
|
|
const maxIdLen = 20
|
2016-04-13 19:54:11 +02:00
|
|
|
|
2014-06-27 02:05:21 +02:00
|
|
|
type UiHook struct {
|
|
|
|
terraform.NilHook
|
|
|
|
|
2017-03-13 21:09:25 +01:00
|
|
|
Colorize *colorstring.Colorize
|
|
|
|
Ui cli.Ui
|
|
|
|
PeriodicUiTimer time.Duration
|
2014-06-27 07:01:05 +02:00
|
|
|
|
2014-07-27 06:20:31 +02:00
|
|
|
l sync.Mutex
|
|
|
|
once sync.Once
|
2016-04-13 19:54:11 +02:00
|
|
|
resources map[string]uiResourceState
|
2014-07-27 06:20:31 +02:00
|
|
|
ui cli.Ui
|
2014-06-27 02:05:21 +02:00
|
|
|
}
|
|
|
|
|
2016-04-13 19:54:11 +02:00
|
|
|
// uiResourceState tracks the state of a single resource
|
|
|
|
type uiResourceState struct {
|
2017-03-02 20:15:02 +01:00
|
|
|
Name string
|
|
|
|
ResourceId string
|
|
|
|
Op uiResourceOp
|
|
|
|
Start time.Time
|
|
|
|
|
|
|
|
DoneCh chan struct{} // To be used for cancellation
|
2016-04-13 19:54:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// uiResourceOp is an enum for operations on a resource
|
2014-07-27 06:20:31 +02:00
|
|
|
type uiResourceOp byte
|
|
|
|
|
|
|
|
const (
|
|
|
|
uiResourceUnknown uiResourceOp = iota
|
|
|
|
uiResourceCreate
|
|
|
|
uiResourceModify
|
|
|
|
uiResourceDestroy
|
|
|
|
)
|
|
|
|
|
2014-06-27 07:11:04 +02:00
|
|
|
func (h *UiHook) PreApply(
|
2014-09-25 23:52:06 +02:00
|
|
|
n *terraform.InstanceInfo,
|
2014-09-17 20:15:07 +02:00
|
|
|
s *terraform.InstanceState,
|
2014-09-18 01:33:24 +02:00
|
|
|
d *terraform.InstanceDiff) (terraform.HookAction, error) {
|
2014-06-27 07:11:04 +02:00
|
|
|
h.once.Do(h.init)
|
|
|
|
|
2014-09-25 23:52:06 +02:00
|
|
|
id := n.HumanId()
|
|
|
|
|
2014-07-27 06:20:31 +02:00
|
|
|
op := uiResourceModify
|
2014-07-13 02:03:55 +02:00
|
|
|
if d.Destroy {
|
2014-07-27 06:20:31 +02:00
|
|
|
op = uiResourceDestroy
|
2014-07-13 02:03:55 +02:00
|
|
|
} else if s.ID == "" {
|
2014-07-27 06:20:31 +02:00
|
|
|
op = uiResourceCreate
|
|
|
|
}
|
|
|
|
|
|
|
|
var operation string
|
|
|
|
switch op {
|
|
|
|
case uiResourceModify:
|
|
|
|
operation = "Modifying..."
|
|
|
|
case uiResourceDestroy:
|
|
|
|
operation = "Destroying..."
|
|
|
|
case uiResourceCreate:
|
2014-07-13 02:03:55 +02:00
|
|
|
operation = "Creating..."
|
2014-07-27 06:20:31 +02:00
|
|
|
case uiResourceUnknown:
|
|
|
|
return terraform.HookActionContinue, nil
|
2014-07-13 02:03:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2016-07-29 19:17:48 +02:00
|
|
|
|
|
|
|
dAttrs := d.CopyAttributes()
|
|
|
|
keys := make([]string, 0, len(dAttrs))
|
|
|
|
for key, _ := range dAttrs {
|
2014-07-13 02:03:55 +02:00
|
|
|
// Skip the ID since we do that specially
|
|
|
|
if key == "id" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
keys = append(keys, key)
|
|
|
|
if len(key) > keyLen {
|
|
|
|
keyLen = len(key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
// Go through and output each attribute
|
|
|
|
for _, attrK := range keys {
|
2016-07-29 19:17:48 +02:00
|
|
|
attrDiff, _ := d.GetAttribute(attrK)
|
2014-07-13 02:03:55 +02:00
|
|
|
|
|
|
|
v := attrDiff.New
|
2016-05-28 02:00:59 +02:00
|
|
|
u := attrDiff.Old
|
2014-07-13 02:03:55 +02:00
|
|
|
if attrDiff.NewComputed {
|
|
|
|
v = "<computed>"
|
|
|
|
}
|
|
|
|
|
2016-05-28 02:00:59 +02:00
|
|
|
if attrDiff.Sensitive {
|
|
|
|
u = "<sensitive>"
|
|
|
|
v = "<sensitive>"
|
|
|
|
}
|
|
|
|
|
2014-07-13 02:03:55 +02:00
|
|
|
attrBuf.WriteString(fmt.Sprintf(
|
|
|
|
" %s:%s %#v => %#v\n",
|
|
|
|
attrK,
|
|
|
|
strings.Repeat(" ", keyLen-len(attrK)),
|
2016-05-28 02:00:59 +02:00
|
|
|
u,
|
2014-07-13 02:03:55 +02:00
|
|
|
v))
|
|
|
|
}
|
|
|
|
|
2014-07-13 18:34:35 +02:00
|
|
|
attrString := strings.TrimSpace(attrBuf.String())
|
|
|
|
if attrString != "" {
|
|
|
|
attrString = "\n " + attrString
|
|
|
|
}
|
|
|
|
|
2017-03-02 20:15:02 +01:00
|
|
|
var stateId, stateIdSuffix string
|
2017-03-02 11:52:16 +01:00
|
|
|
if s != nil && s.ID != "" {
|
2017-03-02 20:15:02 +01:00
|
|
|
stateId = s.ID
|
2017-03-01 23:16:22 +01:00
|
|
|
stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen))
|
|
|
|
}
|
|
|
|
|
2014-07-13 02:03:55 +02:00
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
2017-03-01 23:16:22 +01:00
|
|
|
"[reset][bold]%s: %s%s[reset]%s",
|
2014-07-13 02:03:55 +02:00
|
|
|
id,
|
|
|
|
operation,
|
2017-03-01 23:16:22 +01:00
|
|
|
stateIdSuffix,
|
2014-07-13 18:34:35 +02:00
|
|
|
attrString)))
|
2014-07-13 02:03:55 +02:00
|
|
|
|
2017-03-02 20:15:02 +01:00
|
|
|
uiState := uiResourceState{
|
|
|
|
Name: id,
|
|
|
|
ResourceId: stateId,
|
|
|
|
Op: op,
|
|
|
|
Start: time.Now().Round(time.Second),
|
|
|
|
DoneCh: make(chan struct{}),
|
|
|
|
}
|
|
|
|
|
|
|
|
h.l.Lock()
|
|
|
|
h.resources[id] = uiState
|
|
|
|
h.l.Unlock()
|
|
|
|
|
|
|
|
// Start goroutine that shows progress
|
|
|
|
go h.stillApplying(uiState)
|
2016-04-13 19:54:11 +02:00
|
|
|
|
2014-06-27 07:11:04 +02:00
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
|
|
|
|
2017-03-02 20:15:02 +01:00
|
|
|
func (h *UiHook) stillApplying(state uiResourceState) {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-state.DoneCh:
|
|
|
|
return
|
2016-04-13 19:54:11 +02:00
|
|
|
|
2017-03-13 21:09:25 +01:00
|
|
|
case <-time.After(h.PeriodicUiTimer):
|
2017-03-02 20:15:02 +01:00
|
|
|
// Timer up, show status
|
|
|
|
}
|
2016-04-13 19:54:11 +02:00
|
|
|
|
2017-03-02 20:15:02 +01:00
|
|
|
var msg string
|
|
|
|
switch state.Op {
|
|
|
|
case uiResourceModify:
|
|
|
|
msg = "Still modifying..."
|
|
|
|
case uiResourceDestroy:
|
|
|
|
msg = "Still destroying..."
|
|
|
|
case uiResourceCreate:
|
|
|
|
msg = "Still creating..."
|
|
|
|
case uiResourceUnknown:
|
|
|
|
return
|
|
|
|
}
|
2017-03-01 23:16:22 +01:00
|
|
|
|
2017-03-02 20:15:02 +01:00
|
|
|
idSuffix := ""
|
|
|
|
if v := state.ResourceId; v != "" {
|
|
|
|
idSuffix = fmt.Sprintf("ID: %s, ", truncateId(v, maxIdLen))
|
|
|
|
}
|
2016-04-13 19:54:11 +02:00
|
|
|
|
2017-03-02 20:15:02 +01:00
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
|
|
"[reset][bold]%s: %s (%s%s elapsed)[reset]",
|
|
|
|
state.Name,
|
|
|
|
msg,
|
|
|
|
idSuffix,
|
|
|
|
time.Now().Round(time.Second).Sub(state.Start),
|
|
|
|
)))
|
|
|
|
}
|
2016-04-13 19:54:11 +02:00
|
|
|
}
|
|
|
|
|
2014-07-27 06:20:31 +02:00
|
|
|
func (h *UiHook) PostApply(
|
2014-09-25 23:52:06 +02:00
|
|
|
n *terraform.InstanceInfo,
|
2014-09-17 20:15:07 +02:00
|
|
|
s *terraform.InstanceState,
|
2014-07-27 06:20:31 +02:00
|
|
|
applyerr error) (terraform.HookAction, error) {
|
2017-03-02 19:41:18 +01:00
|
|
|
|
2014-09-25 23:52:06 +02:00
|
|
|
id := n.HumanId()
|
|
|
|
|
2014-07-27 06:20:31 +02:00
|
|
|
h.l.Lock()
|
2016-04-13 19:54:11 +02:00
|
|
|
state := h.resources[id]
|
2017-03-02 20:15:02 +01:00
|
|
|
if state.DoneCh != nil {
|
|
|
|
close(state.DoneCh)
|
|
|
|
}
|
|
|
|
|
2014-07-27 06:20:31 +02:00
|
|
|
delete(h.resources, id)
|
|
|
|
h.l.Unlock()
|
|
|
|
|
2017-03-01 23:16:22 +01:00
|
|
|
var stateIdSuffix string
|
2017-03-02 11:52:16 +01:00
|
|
|
if s != nil && s.ID != "" {
|
2017-03-01 23:16:22 +01:00
|
|
|
stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen))
|
|
|
|
}
|
|
|
|
|
2014-07-27 06:20:31 +02:00
|
|
|
var msg string
|
2016-04-13 19:54:11 +02:00
|
|
|
switch state.Op {
|
2014-07-27 06:20:31 +02:00
|
|
|
case uiResourceModify:
|
|
|
|
msg = "Modifications complete"
|
|
|
|
case uiResourceDestroy:
|
|
|
|
msg = "Destruction complete"
|
|
|
|
case uiResourceCreate:
|
|
|
|
msg = "Creation complete"
|
|
|
|
case uiResourceUnknown:
|
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if applyerr != nil {
|
2015-07-22 12:49:05 +02:00
|
|
|
// Errors are collected and printed in ApplyCommand, no need to duplicate
|
|
|
|
return terraform.HookActionContinue, nil
|
2014-07-27 06:20:31 +02:00
|
|
|
}
|
|
|
|
|
2017-03-02 19:41:18 +01:00
|
|
|
colorized := h.Colorize.Color(fmt.Sprintf(
|
2017-03-01 23:16:22 +01:00
|
|
|
"[reset][bold]%s: %s%s[reset]",
|
2017-03-02 19:41:18 +01:00
|
|
|
id, msg, stateIdSuffix))
|
|
|
|
|
|
|
|
h.ui.Output(colorized)
|
2014-07-27 06:20:31 +02:00
|
|
|
|
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
|
|
|
|
2014-06-27 02:18:46 +02:00
|
|
|
func (h *UiHook) PreDiff(
|
2014-09-25 23:52:06 +02:00
|
|
|
n *terraform.InstanceInfo,
|
|
|
|
s *terraform.InstanceState) (terraform.HookAction, error) {
|
2014-06-27 02:18:46 +02:00
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
|
|
|
|
2014-09-25 23:52:06 +02:00
|
|
|
func (h *UiHook) PreProvision(
|
|
|
|
n *terraform.InstanceInfo,
|
|
|
|
provId string) (terraform.HookAction, error) {
|
|
|
|
id := n.HumanId()
|
2014-07-27 18:11:53 +02:00
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
2016-11-29 18:52:58 +01:00
|
|
|
"[reset][bold]%s: Provisioning with '%s'...[reset]",
|
2014-07-27 18:11:53 +02:00
|
|
|
id, provId)))
|
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
|
|
|
|
2014-10-05 01:33:47 +02:00
|
|
|
func (h *UiHook) ProvisionOutput(
|
|
|
|
n *terraform.InstanceInfo,
|
|
|
|
provId string,
|
|
|
|
msg string) {
|
|
|
|
id := n.HumanId()
|
|
|
|
var buf bytes.Buffer
|
2014-10-06 08:29:13 +02:00
|
|
|
buf.WriteString(h.Colorize.Color("[reset]"))
|
|
|
|
|
|
|
|
prefix := fmt.Sprintf("%s (%s): ", id, provId)
|
|
|
|
s := bufio.NewScanner(strings.NewReader(msg))
|
2014-10-12 02:35:32 +02:00
|
|
|
s.Split(scanLines)
|
2014-10-06 08:29:13 +02:00
|
|
|
for s.Scan() {
|
2014-10-12 18:59:08 +02:00
|
|
|
line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
|
|
|
|
if line != "" {
|
2014-10-12 02:40:28 +02:00
|
|
|
buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
|
|
|
|
}
|
2014-10-06 08:29:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
h.ui.Output(strings.TrimSpace(buf.String()))
|
2014-10-05 01:33:47 +02:00
|
|
|
}
|
|
|
|
|
2014-06-27 02:05:21 +02:00
|
|
|
func (h *UiHook) PreRefresh(
|
2014-09-25 23:52:06 +02:00
|
|
|
n *terraform.InstanceInfo,
|
|
|
|
s *terraform.InstanceState) (terraform.HookAction, error) {
|
2014-06-27 07:01:05 +02:00
|
|
|
h.once.Do(h.init)
|
|
|
|
|
2014-09-25 23:52:06 +02:00
|
|
|
id := n.HumanId()
|
2016-05-08 08:51:55 +02:00
|
|
|
|
|
|
|
var stateIdSuffix string
|
|
|
|
// Data resources refresh before they have ids, whereas managed
|
|
|
|
// resources are only refreshed when they have ids.
|
|
|
|
if s.ID != "" {
|
2017-03-01 23:16:22 +01:00
|
|
|
stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen))
|
2016-05-08 08:51:55 +02:00
|
|
|
}
|
|
|
|
|
2014-07-13 02:17:03 +02:00
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
2016-05-08 08:51:55 +02:00
|
|
|
"[reset][bold]%s: Refreshing state...%s",
|
|
|
|
id, stateIdSuffix)))
|
2014-06-27 02:05:21 +02:00
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
2014-06-27 07:01:05 +02:00
|
|
|
|
2016-05-04 21:05:42 +02:00
|
|
|
func (h *UiHook) PreImportState(
|
2016-05-04 22:02:51 +02:00
|
|
|
n *terraform.InstanceInfo,
|
|
|
|
id string) (terraform.HookAction, error) {
|
2016-05-04 21:05:42 +02:00
|
|
|
h.once.Do(h.init)
|
|
|
|
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
|
|
"[reset][bold]%s: Importing from ID %q...",
|
2016-05-04 22:02:51 +02:00
|
|
|
n.HumanId(), id)))
|
2016-05-04 21:05:42 +02:00
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *UiHook) PostImportState(
|
|
|
|
n *terraform.InstanceInfo,
|
|
|
|
s []*terraform.InstanceState) (terraform.HookAction, error) {
|
|
|
|
h.once.Do(h.init)
|
|
|
|
|
|
|
|
id := n.HumanId()
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
|
|
"[reset][bold][green]%s: Import complete!", id)))
|
|
|
|
for _, s := range s {
|
|
|
|
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
|
|
|
|
"[reset][green] Imported %s (ID: %s)",
|
|
|
|
s.Ephemeral.Type, s.ID)))
|
|
|
|
}
|
|
|
|
|
|
|
|
return terraform.HookActionContinue, nil
|
|
|
|
}
|
|
|
|
|
2014-06-27 07:01:05 +02:00
|
|
|
func (h *UiHook) init() {
|
2014-07-13 02:03:55 +02:00
|
|
|
if h.Colorize == nil {
|
2014-07-13 05:37:30 +02:00
|
|
|
panic("colorize not given")
|
2014-07-13 02:03:55 +02:00
|
|
|
}
|
2017-03-13 21:09:25 +01:00
|
|
|
if h.PeriodicUiTimer == 0 {
|
|
|
|
h.PeriodicUiTimer = defaultPeriodicUiTimer
|
|
|
|
}
|
2014-07-13 02:03:55 +02:00
|
|
|
|
2016-04-13 19:54:11 +02:00
|
|
|
h.resources = make(map[string]uiResourceState)
|
2014-07-27 06:20:31 +02:00
|
|
|
|
2014-06-27 07:01:05 +02:00
|
|
|
// 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}
|
|
|
|
}
|
2014-10-12 02:35:32 +02:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2017-03-01 23:16:22 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|