Thse changes make a subtle but critical adjustment to the process the Pulumi engine uses to determine whether or not a difference exists between a resource's actual and desired states, and adjusts the way this difference is calculated and displayed accordingly. Today, the Pulumi engine get the first chance to decide whether or not there is a difference between a resource's actual and desired states. It does this by comparing the current set of inputs for a resource (i.e. the inputs from the running Pulumi program) with the last set of inputs used to update the resource. If there is no difference between the old and new inputs, the engine decides that no change is necessary without consulting the resource's provider. Only if there are changes does the engine consult the resource's provider for more information about the difference. This can be problematic for a number of reasons: - Not all providers do input-input comparison; some do input-state comparison - Not all providers are able to update the last deployed set of inputs when performing a refresh - Some providers--either intentionally or due to bugs--may see changes in resources whose inputs have not changed All of these situations are confusing at the very least, and the first is problematic with respect to correctness. Furthermore, the display code only renders diffs it observes rather than rendering the diffs observed by the provider, which can obscure the actual changes detected at runtime. These changes address both of these issues: - Rather than comparing the current inputs against the last inputs before calling a resource provider's Diff function, the engine calls the Diff function in all cases. - Providers may now return a list of properties that differ between the requested and actual state and the way in which they differ. This information will then be used by the CLI to render the diff appropriately. A provider may also indicate that a particular diff is between old and new inputs rather than old state and new inputs. Fixes #2453.
534 lines
14 KiB
Go
534 lines
14 KiB
Go
// Copyright 2016-2018, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package display
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/dustin/go-humanize/english"
|
|
"github.com/pulumi/pulumi/pkg/apitype"
|
|
"github.com/pulumi/pulumi/pkg/diag"
|
|
"github.com/pulumi/pulumi/pkg/diag/colors"
|
|
"github.com/pulumi/pulumi/pkg/engine"
|
|
"github.com/pulumi/pulumi/pkg/resource"
|
|
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
|
)
|
|
|
|
type Row interface {
|
|
DisplayOrderIndex() int
|
|
SetDisplayOrderIndex(index int)
|
|
|
|
ColorizedColumns() []string
|
|
ColorizedSuffix() string
|
|
|
|
HideRowIfUnnecessary() bool
|
|
SetHideRowIfUnnecessary(value bool)
|
|
}
|
|
|
|
type ResourceRow interface {
|
|
Row
|
|
|
|
Step() engine.StepEventMetadata
|
|
SetStep(step engine.StepEventMetadata)
|
|
AddOutputStep(step engine.StepEventMetadata)
|
|
|
|
// The tick we were on when we created this row. Purely used for generating an
|
|
// ellipses to show progress for in-flight resources.
|
|
Tick() int
|
|
|
|
IsDone() bool
|
|
|
|
SetFailed()
|
|
|
|
DiagInfo() *DiagInfo
|
|
RecordDiagEvent(diagEvent engine.Event)
|
|
RecordPolicyViolationEvent(diagEvent engine.Event)
|
|
}
|
|
|
|
// Implementation of a Row, used for the header of the grid.
|
|
type headerRowData struct {
|
|
display *ProgressDisplay
|
|
columns []string
|
|
}
|
|
|
|
func (data *headerRowData) HideRowIfUnnecessary() bool {
|
|
return false
|
|
}
|
|
|
|
func (data *headerRowData) SetHideRowIfUnnecessary(value bool) {
|
|
}
|
|
|
|
func (data *headerRowData) DisplayOrderIndex() int {
|
|
// sort the header before all other rows
|
|
return -1
|
|
}
|
|
|
|
func (data *headerRowData) SetDisplayOrderIndex(time int) {
|
|
// Nothing to do here. Header is always at the same index.
|
|
}
|
|
|
|
func (data *headerRowData) ColorizedColumns() []string {
|
|
if len(data.columns) == 0 {
|
|
blue := func(msg string) string {
|
|
return colors.Underline + colors.BrightBlue + msg + colors.Reset
|
|
}
|
|
|
|
header := func(msg string) string {
|
|
return blue(msg)
|
|
}
|
|
|
|
var statusColumn string
|
|
if data.display.isPreview {
|
|
statusColumn = header("Plan")
|
|
} else {
|
|
statusColumn = header("Status")
|
|
}
|
|
data.columns = []string{"", header("Type"), header("Name"), statusColumn, header("Info")}
|
|
}
|
|
|
|
return data.columns
|
|
}
|
|
|
|
func (data *headerRowData) ColorizedSuffix() string {
|
|
return ""
|
|
}
|
|
|
|
// Implementation of a row used for all the resource rows in the grid.
|
|
type resourceRowData struct {
|
|
displayOrderIndex int
|
|
|
|
display *ProgressDisplay
|
|
|
|
// The change that the engine wants apply to that resource.
|
|
step engine.StepEventMetadata
|
|
outputSteps []engine.StepEventMetadata
|
|
|
|
// True if we should diff outputs instead of inputs for this row.
|
|
diffOutputs bool
|
|
|
|
// The tick we were on when we created this row. Purely used for generating an
|
|
// ellipses to show progress for in-flight resources.
|
|
tick int
|
|
|
|
// If we failed this operation for any reason.
|
|
failed bool
|
|
|
|
diagInfo *DiagInfo
|
|
|
|
// If this row should be hidden by default. We will hide unless we have any child nodes
|
|
// we need to show.
|
|
hideRowIfUnnecessary bool
|
|
}
|
|
|
|
func (data *resourceRowData) DisplayOrderIndex() int {
|
|
// sort the header before all other rows
|
|
return data.displayOrderIndex
|
|
}
|
|
|
|
func (data *resourceRowData) SetDisplayOrderIndex(index int) {
|
|
// only set this if it's the first time.
|
|
if data.displayOrderIndex == 0 {
|
|
data.displayOrderIndex = index
|
|
}
|
|
}
|
|
|
|
func (data *resourceRowData) HideRowIfUnnecessary() bool {
|
|
return data.hideRowIfUnnecessary
|
|
}
|
|
|
|
func (data *resourceRowData) SetHideRowIfUnnecessary(value bool) {
|
|
data.hideRowIfUnnecessary = value
|
|
}
|
|
|
|
func (data *resourceRowData) Step() engine.StepEventMetadata {
|
|
return data.step
|
|
}
|
|
|
|
func (data *resourceRowData) SetStep(step engine.StepEventMetadata) {
|
|
data.step = step
|
|
if step.Op == deploy.OpRefresh {
|
|
data.diffOutputs = true
|
|
}
|
|
}
|
|
|
|
func (data *resourceRowData) AddOutputStep(step engine.StepEventMetadata) {
|
|
data.outputSteps = append(data.outputSteps, step)
|
|
}
|
|
|
|
func (data *resourceRowData) Tick() int {
|
|
return data.tick
|
|
}
|
|
|
|
func (data *resourceRowData) Failed() bool {
|
|
return data.failed
|
|
}
|
|
|
|
func (data *resourceRowData) SetFailed() {
|
|
data.failed = true
|
|
}
|
|
|
|
func (data *resourceRowData) DiagInfo() *DiagInfo {
|
|
return data.diagInfo
|
|
}
|
|
|
|
func (data *resourceRowData) RecordDiagEvent(event engine.Event) {
|
|
payload := event.Payload.(engine.DiagEventPayload)
|
|
data.recordDiagEventPayload(payload)
|
|
}
|
|
|
|
func (data *resourceRowData) recordDiagEventPayload(payload engine.DiagEventPayload) {
|
|
diagInfo := data.diagInfo
|
|
diagInfo.LastDiag = &payload
|
|
|
|
switch payload.Severity {
|
|
case diag.Error:
|
|
diagInfo.LastError = &payload
|
|
case diag.Warning:
|
|
diagInfo.LastWarning = &payload
|
|
case diag.Infoerr:
|
|
diagInfo.LastInfoError = &payload
|
|
case diag.Info:
|
|
diagInfo.LastInfo = &payload
|
|
case diag.Debug:
|
|
diagInfo.LastDebug = &payload
|
|
}
|
|
|
|
if diagInfo.StreamIDToDiagPayloads == nil {
|
|
diagInfo.StreamIDToDiagPayloads = make(map[int32][]engine.DiagEventPayload)
|
|
}
|
|
|
|
payloads := diagInfo.StreamIDToDiagPayloads[payload.StreamID]
|
|
payloads = append(payloads, payload)
|
|
diagInfo.StreamIDToDiagPayloads[payload.StreamID] = payloads
|
|
|
|
if !payload.Ephemeral {
|
|
switch payload.Severity {
|
|
case diag.Error:
|
|
diagInfo.ErrorCount++
|
|
case diag.Warning:
|
|
diagInfo.WarningCount++
|
|
case diag.Infoerr:
|
|
diagInfo.InfoCount++
|
|
case diag.Info:
|
|
diagInfo.InfoCount++
|
|
case diag.Debug:
|
|
diagInfo.DebugCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
func (data *resourceRowData) RecordPolicyViolationEvent(event engine.Event) {
|
|
|
|
//
|
|
// NOTE: The display code only understands DiagEvents. We convert the policy violation events
|
|
// accordingly so they can be displayed.
|
|
//
|
|
|
|
pePayload := event.Payload.(engine.PolicyViolationEventPayload)
|
|
|
|
payload := engine.DiagEventPayload{
|
|
URN: pePayload.ResourceURN,
|
|
Prefix: pePayload.Prefix,
|
|
Message: pePayload.Message,
|
|
Color: pePayload.Color}
|
|
|
|
switch pePayload.EnforcementLevel {
|
|
case apitype.Mandatory:
|
|
payload.Severity = diag.Error
|
|
case apitype.Warning:
|
|
payload.Severity = diag.Warning
|
|
default:
|
|
contract.Failf("Unknown enforcement level %q", pePayload.EnforcementLevel)
|
|
}
|
|
|
|
data.recordDiagEventPayload(payload)
|
|
}
|
|
|
|
type column int
|
|
|
|
const (
|
|
opColumn column = 0
|
|
typeColumn column = 1
|
|
nameColumn column = 2
|
|
statusColumn column = 3
|
|
infoColumn column = 4
|
|
)
|
|
|
|
func (data *resourceRowData) IsDone() bool {
|
|
if data.failed {
|
|
// consider a failed resource 'done'.
|
|
return true
|
|
}
|
|
|
|
if data.display.done {
|
|
// if the display is done, then we're definitely done.
|
|
return true
|
|
}
|
|
|
|
if isRootStack(data.step) {
|
|
// the root stack only becomes 'done' once the program has completed (i.e. the condition
|
|
// checked just above this). If the program is not finished, then always show the root
|
|
// stack as not done so the user sees "running..." presented for it.
|
|
return false
|
|
}
|
|
|
|
// We're done if we have the output-step for whatever step operation we're performing
|
|
return data.ContainsOutputsStep(data.step.Op)
|
|
}
|
|
|
|
func (data *resourceRowData) ContainsOutputsStep(op deploy.StepOp) bool {
|
|
for _, s := range data.outputSteps {
|
|
if s.Op == op {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (data *resourceRowData) ColorizedSuffix() string {
|
|
if !data.IsDone() && data.display.isTerminal {
|
|
op := data.display.getStepOp(data.step)
|
|
if op != deploy.OpSame || isRootURN(data.step.URN) {
|
|
suffixes := data.display.suffixesArray
|
|
ellipses := suffixes[(data.tick+data.display.currentTick)%len(suffixes)]
|
|
|
|
return op.Color() + ellipses + colors.Reset
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (data *resourceRowData) ColorizedColumns() []string {
|
|
step := data.step
|
|
|
|
urn := data.step.URN
|
|
if urn == "" {
|
|
// If we don't have a URN yet, mock parent it to the global stack.
|
|
urn = resource.DefaultRootStackURN(data.display.stack, data.display.proj)
|
|
}
|
|
name := string(urn.Name())
|
|
typ := simplifyTypeName(urn.Type())
|
|
|
|
columns := make([]string, 5)
|
|
columns[opColumn] = data.display.getStepOpLabel(step)
|
|
columns[typeColumn] = typ
|
|
columns[nameColumn] = name
|
|
|
|
diagInfo := data.diagInfo
|
|
|
|
if data.IsDone() {
|
|
failed := data.failed || diagInfo.ErrorCount > 0
|
|
columns[statusColumn] = data.display.getStepDoneDescription(step, failed)
|
|
} else {
|
|
columns[statusColumn] = data.display.getStepInProgressDescription(step)
|
|
}
|
|
|
|
columns[infoColumn] = data.getInfoColumn()
|
|
return columns
|
|
}
|
|
|
|
func (data *resourceRowData) getInfoColumn() string {
|
|
step := data.step
|
|
if step.Op == deploy.OpCreateReplacement || step.Op == deploy.OpDeleteReplaced {
|
|
// if we're doing a replacement, see if we can find a replace step that contains useful
|
|
// information to display.
|
|
for _, outputStep := range data.outputSteps {
|
|
if outputStep.Op == deploy.OpReplace {
|
|
step = outputStep
|
|
}
|
|
}
|
|
}
|
|
|
|
var diagMsg string
|
|
appendDiagMessage := func(msg string) {
|
|
if diagMsg != "" {
|
|
diagMsg += "; "
|
|
}
|
|
|
|
diagMsg += msg
|
|
}
|
|
|
|
changes := data.getDiffInfo(step)
|
|
if colors.Never.Colorize(changes) != "" {
|
|
appendDiagMessage("[" + changes + "]")
|
|
}
|
|
|
|
diagInfo := data.diagInfo
|
|
if data.display.done {
|
|
// If we are done, show a summary of how many messages were printed.
|
|
if c := diagInfo.ErrorCount; c > 0 {
|
|
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
|
|
c, colors.SpecError, english.PluralWord(c, "error", ""), colors.Reset))
|
|
}
|
|
if c := diagInfo.WarningCount; c > 0 {
|
|
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
|
|
c, colors.SpecWarning, english.PluralWord(c, "warning", ""), colors.Reset))
|
|
}
|
|
if c := diagInfo.InfoCount; c > 0 {
|
|
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
|
|
c, colors.SpecInfo, english.PluralWord(c, "message", ""), colors.Reset))
|
|
}
|
|
if c := diagInfo.DebugCount; c > 0 {
|
|
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
|
|
c, colors.SpecDebug, english.PluralWord(c, "debug", ""), colors.Reset))
|
|
}
|
|
} else {
|
|
// If we're not totally done, and we're in the tree-view, just print out the worst diagnostic next to the
|
|
// status message. This is helpful for long running tasks to know what's going on. However, once done, we
|
|
// print the diagnostics at the bottom, so we don't need to show this.
|
|
//
|
|
// if we're not in the tree-view (i.e. non-interactive mode), then we want to print out whatever the last
|
|
// diagnostics was that we got. This way, as we're hearing about diagnostic events, we're always printing
|
|
// out the last one.
|
|
|
|
var diagnostic *engine.DiagEventPayload
|
|
if data.display.isTerminal {
|
|
diagnostic = data.diagInfo.LastDiag
|
|
} else {
|
|
diagnostic = getWorstDiagnostic(data.diagInfo)
|
|
}
|
|
|
|
if diagnostic != nil {
|
|
eventMsg := data.display.renderProgressDiagEvent(*diagnostic, true /*includePrefix:*/)
|
|
if eventMsg != "" {
|
|
appendDiagMessage(eventMsg)
|
|
}
|
|
}
|
|
}
|
|
|
|
newLineIndex := strings.Index(diagMsg, "\n")
|
|
if newLineIndex >= 0 {
|
|
diagMsg = diagMsg[0:newLineIndex]
|
|
}
|
|
|
|
return diagMsg
|
|
}
|
|
|
|
func (data *resourceRowData) getDiffInfo(step engine.StepEventMetadata) string {
|
|
changesBuf := &bytes.Buffer{}
|
|
if step.Old != nil && step.New != nil {
|
|
var diff *resource.ObjectDiff
|
|
if step.DetailedDiff != nil {
|
|
diff = translateDetailedDiff(step)
|
|
} else if data.diffOutputs {
|
|
if step.Old.Outputs != nil && step.New.Outputs != nil {
|
|
diff = step.Old.Outputs.Diff(step.New.Outputs)
|
|
}
|
|
} else if step.Old.Inputs != nil && step.New.Inputs != nil {
|
|
diff = step.Old.Inputs.Diff(step.New.Inputs)
|
|
}
|
|
|
|
// Show a diff if either `provider` or `protect` changed; they might not show a diff via inputs or outputs, but
|
|
// it is still useful to show that these changed in output.
|
|
recordMetadataDiff := func(name string, old, new resource.PropertyValue) {
|
|
if old != new {
|
|
if diff == nil {
|
|
diff = &resource.ObjectDiff{
|
|
Adds: make(resource.PropertyMap),
|
|
Deletes: make(resource.PropertyMap),
|
|
Sames: make(resource.PropertyMap),
|
|
Updates: make(map[resource.PropertyKey]resource.ValueDiff),
|
|
}
|
|
}
|
|
|
|
diff.Updates[resource.PropertyKey(name)] = resource.ValueDiff{Old: old, New: new}
|
|
}
|
|
}
|
|
|
|
recordMetadataDiff("provider",
|
|
resource.NewStringProperty(step.Old.Provider), resource.NewStringProperty(step.New.Provider))
|
|
recordMetadataDiff("protect",
|
|
resource.NewBoolProperty(step.Old.Protect), resource.NewBoolProperty(step.New.Protect))
|
|
|
|
if diff != nil {
|
|
writeString(changesBuf, "diff: ")
|
|
|
|
updates := make(resource.PropertyMap)
|
|
for k := range diff.Updates {
|
|
updates[k] = resource.PropertyValue{}
|
|
}
|
|
|
|
filteredKeys := func(m resource.PropertyMap) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, string(k))
|
|
}
|
|
return keys
|
|
}
|
|
if include := step.Diffs; include != nil {
|
|
includeSet := make(map[resource.PropertyKey]bool)
|
|
for _, k := range include {
|
|
includeSet[k] = true
|
|
}
|
|
filteredKeys = func(m resource.PropertyMap) []string {
|
|
var filteredKeys []string
|
|
for k := range m {
|
|
if includeSet[k] {
|
|
filteredKeys = append(filteredKeys, string(k))
|
|
}
|
|
}
|
|
return filteredKeys
|
|
}
|
|
}
|
|
|
|
writePropertyKeys(changesBuf, filteredKeys(diff.Adds), deploy.OpCreate)
|
|
writePropertyKeys(changesBuf, filteredKeys(diff.Deletes), deploy.OpDelete)
|
|
writePropertyKeys(changesBuf, filteredKeys(updates), deploy.OpUpdate)
|
|
}
|
|
}
|
|
|
|
fprintIgnoreError(changesBuf, colors.Reset)
|
|
return changesBuf.String()
|
|
}
|
|
|
|
func writePropertyKeys(b io.StringWriter, keys []string, op deploy.StepOp) {
|
|
if len(keys) > 0 {
|
|
writeString(b, strings.Trim(op.Prefix(), " "))
|
|
|
|
sort.Strings(keys)
|
|
|
|
for index, k := range keys {
|
|
if index != 0 {
|
|
writeString(b, ",")
|
|
}
|
|
writeString(b, k)
|
|
}
|
|
|
|
writeString(b, colors.Reset)
|
|
}
|
|
}
|
|
|
|
// Returns the worst diagnostic we've seen. Used to produce a diagnostic string to go along with
|
|
// any resource if it has had any issues.
|
|
func getWorstDiagnostic(diagInfo *DiagInfo) *engine.DiagEventPayload {
|
|
if diagInfo.LastError != nil {
|
|
return diagInfo.LastError
|
|
} else if diagInfo.LastWarning != nil {
|
|
return diagInfo.LastWarning
|
|
} else if diagInfo.LastInfoError != nil {
|
|
return diagInfo.LastInfoError
|
|
} else if diagInfo.LastInfo != nil {
|
|
return diagInfo.LastInfo
|
|
}
|
|
return diagInfo.LastDebug
|
|
}
|