Include richer information in events so that final display can flexibly chose how to present it. (#1088)
This commit is contained in:
parent
e74ea464f3
commit
4b761f9fc1
|
@ -14,6 +14,7 @@ import (
|
|||
"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/resource/stack"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
|
@ -146,7 +147,9 @@ func (u *cloudUpdate) Complete(status apitype.UpdateStatus) error {
|
|||
return u.backend.client.CompleteUpdate(u.update, status, token)
|
||||
}
|
||||
|
||||
func (u *cloudUpdate) recordEvent(event engine.Event, debug bool, opts backend.DisplayOptions) error {
|
||||
func (u *cloudUpdate) recordEvent(
|
||||
event engine.Event, seen map[resource.URN]engine.StepEventMetadata, debug bool, opts backend.DisplayOptions) error {
|
||||
|
||||
// If we don't have a token source, we can't perform any mutations.
|
||||
if u.tokenSource == nil {
|
||||
return nil
|
||||
|
@ -162,7 +165,7 @@ func (u *cloudUpdate) recordEvent(event engine.Event, debug bool, opts backend.D
|
|||
|
||||
// Ensure we render events with raw colorization tags.
|
||||
opts.Color = colors.Raw
|
||||
msg := local.RenderEvent(event, debug, opts)
|
||||
msg := local.RenderEvent(event, seen, debug, opts)
|
||||
if msg == "" {
|
||||
return nil
|
||||
}
|
||||
|
@ -183,12 +186,14 @@ func (u *cloudUpdate) RecordAndDisplayEvents(action string,
|
|||
displayEvents := make(chan engine.Event)
|
||||
go local.DisplayEvents(action, displayEvents, done, debug, opts)
|
||||
|
||||
seen := make(map[resource.URN]engine.StepEventMetadata)
|
||||
|
||||
for e := range events {
|
||||
// First echo the event to the local display.
|
||||
displayEvents <- e
|
||||
|
||||
// Then render and record the event for posterity.
|
||||
if err := u.recordEvent(e, debug, opts); err != nil {
|
||||
if err := u.recordEvent(e, seen, debug, opts); err != nil {
|
||||
diagEvent := engine.Event{
|
||||
Type: engine.DiagEvent,
|
||||
Payload: engine.DiagEventPayload{
|
||||
|
|
|
@ -34,6 +34,8 @@ func DisplayEvents(action string,
|
|||
done <- true
|
||||
}()
|
||||
|
||||
seen := make(map[resource.URN]engine.StepEventMetadata)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
|
@ -49,7 +51,7 @@ func DisplayEvents(action string,
|
|||
}
|
||||
}
|
||||
|
||||
msg := RenderEvent(event, debug, opts)
|
||||
msg := RenderEvent(event, seen, debug, opts)
|
||||
if msg != "" && out != nil {
|
||||
fprintIgnoreError(out, msg)
|
||||
}
|
||||
|
@ -61,7 +63,9 @@ func DisplayEvents(action string,
|
|||
}
|
||||
}
|
||||
|
||||
func RenderEvent(event engine.Event, debug bool, opts backend.DisplayOptions) string {
|
||||
func RenderEvent(
|
||||
event engine.Event, seen map[resource.URN]engine.StepEventMetadata, debug bool, opts backend.DisplayOptions) string {
|
||||
|
||||
switch event.Type {
|
||||
case engine.CancelEvent:
|
||||
return ""
|
||||
|
@ -72,24 +76,30 @@ func RenderEvent(event engine.Event, debug bool, opts backend.DisplayOptions) st
|
|||
case engine.ResourceOperationFailed:
|
||||
return RenderResourceOperationFailedEvent(event.Payload.(engine.ResourceOperationFailedPayload), opts)
|
||||
case engine.ResourceOutputsEvent:
|
||||
return RenderResourceOutputsEvent(event.Payload.(engine.ResourceOutputsEventPayload), opts)
|
||||
return RenderResourceOutputsEvent(event.Payload.(engine.ResourceOutputsEventPayload), seen, opts)
|
||||
case engine.ResourcePreEvent:
|
||||
return RenderResourcePreEvent(event.Payload.(engine.ResourcePreEventPayload), opts)
|
||||
return RenderResourcePreEvent(event.Payload.(engine.ResourcePreEventPayload), seen, opts)
|
||||
case engine.StdoutColorEvent:
|
||||
payload := event.Payload.(engine.StdoutEventPayload)
|
||||
return opts.Color.Colorize(payload.Message)
|
||||
return RenderStdoutColorEvent(event.Payload.(engine.StdoutEventPayload), opts)
|
||||
case engine.DiagEvent:
|
||||
payload := event.Payload.(engine.DiagEventPayload)
|
||||
if payload.Severity == diag.Debug && !debug {
|
||||
return ""
|
||||
}
|
||||
return opts.Color.Colorize(payload.Message)
|
||||
return RenderDiagEvent(event.Payload.(engine.DiagEventPayload), debug, opts)
|
||||
default:
|
||||
contract.Failf("unknown event type '%s'", event.Type)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func RenderDiagEvent(payload engine.DiagEventPayload, debug bool, opts backend.DisplayOptions) string {
|
||||
if payload.Severity == diag.Debug && !debug {
|
||||
return ""
|
||||
}
|
||||
return opts.Color.Colorize(payload.Message)
|
||||
}
|
||||
|
||||
func RenderStdoutColorEvent(payload engine.StdoutEventPayload, opts backend.DisplayOptions) string {
|
||||
return opts.Color.Colorize(payload.Message)
|
||||
}
|
||||
|
||||
func RenderSummaryEvent(event engine.SummaryEventPayload, opts backend.DisplayOptions) string {
|
||||
changes := event.ResourceChanges
|
||||
|
||||
|
@ -183,7 +193,7 @@ func RenderPreludeEvent(event engine.PreludeEventPayload, opts backend.DisplayOp
|
|||
}
|
||||
|
||||
func RenderResourceOperationFailedEvent(
|
||||
event engine.ResourceOperationFailedPayload, opts backend.DisplayOptions) string {
|
||||
payload engine.ResourceOperationFailedPayload, opts backend.DisplayOptions) string {
|
||||
|
||||
// It's not actually useful or interesting to print out any details about
|
||||
// the resource state here, because we always assume that the resource state
|
||||
|
@ -195,14 +205,24 @@ func RenderResourceOperationFailedEvent(
|
|||
return ""
|
||||
}
|
||||
|
||||
func RenderResourcePreEvent(event engine.ResourcePreEventPayload, opts backend.DisplayOptions) string {
|
||||
func RenderResourcePreEvent(
|
||||
payload engine.ResourcePreEventPayload,
|
||||
seen map[resource.URN]engine.StepEventMetadata,
|
||||
opts backend.DisplayOptions) string {
|
||||
|
||||
seen[payload.Metadata.URN] = payload.Metadata
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
if shouldShow(event.Metadata, opts) || isRootStack(event.Metadata) {
|
||||
fprintIgnoreError(out, opts.Color.Colorize(event.Summary))
|
||||
if shouldShow(payload.Metadata, opts) || isRootStack(payload.Metadata) {
|
||||
indent := engine.GetIndent(payload.Metadata, seen)
|
||||
summary := engine.GetResourcePropertiesSummary(payload.Metadata, indent)
|
||||
details := engine.GetResourcePropertiesDetails(payload.Metadata, indent, payload.Planning, payload.Debug)
|
||||
|
||||
fprintIgnoreError(out, opts.Color.Colorize(summary))
|
||||
|
||||
if !opts.Summary {
|
||||
fprintIgnoreError(out, opts.Color.Colorize(event.Details))
|
||||
fprintIgnoreError(out, opts.Color.Colorize(details))
|
||||
}
|
||||
|
||||
fprintIgnoreError(out, opts.Color.Colorize(colors.Reset))
|
||||
|
@ -211,21 +231,30 @@ func RenderResourcePreEvent(event engine.ResourcePreEventPayload, opts backend.D
|
|||
return out.String()
|
||||
}
|
||||
|
||||
func RenderResourceOutputsEvent(event engine.ResourceOutputsEventPayload, opts backend.DisplayOptions) string {
|
||||
func RenderResourceOutputsEvent(
|
||||
payload engine.ResourceOutputsEventPayload,
|
||||
seen map[resource.URN]engine.StepEventMetadata,
|
||||
opts backend.DisplayOptions) string {
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
if (shouldShow(event.Metadata, opts) || isRootStack(event.Metadata)) && !opts.Summary {
|
||||
fprintIgnoreError(out, opts.Color.Colorize(event.Text))
|
||||
|
||||
if (shouldShow(payload.Metadata, opts) || isRootStack(payload.Metadata)) && !opts.Summary {
|
||||
indent := engine.GetIndent(payload.Metadata, seen)
|
||||
text := engine.GetResourceOutputsPropertiesString(payload.Metadata, indent, payload.Planning, payload.Debug)
|
||||
|
||||
fprintIgnoreError(out, opts.Color.Colorize(text))
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// isRootStack returns true if the step pertains to the rootmost stack component.
|
||||
func isRootStack(step engine.StepEventMetdata) bool {
|
||||
func isRootStack(step engine.StepEventMetadata) bool {
|
||||
return step.URN.Type() == resource.RootStackType
|
||||
}
|
||||
|
||||
// shouldShow returns true if a step should show in the output.
|
||||
func shouldShow(step engine.StepEventMetdata, opts backend.DisplayOptions) bool {
|
||||
func shouldShow(step engine.StepEventMetadata, opts backend.DisplayOptions) bool {
|
||||
// For certain operations, whether they are tracked is controlled by flags (to cut down on superfluous output).
|
||||
if step.Op == deploy.OpSame {
|
||||
// If the op is the same, it is possible that the resource's metadata changed. In that case, still show it.
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -19,27 +18,27 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
)
|
||||
|
||||
// getIndent computes a step's parent indentation.
|
||||
func getIndent(step deploy.Step, seen map[resource.URN]deploy.Step) int {
|
||||
// GetIndent computes a step's parent indentation.
|
||||
func GetIndent(step StepEventMetadata, seen map[resource.URN]StepEventMetadata) int {
|
||||
indent := 0
|
||||
for p := step.Res().Parent; p != ""; {
|
||||
par := seen[p]
|
||||
if par == nil {
|
||||
for p := step.Res.Parent; p != ""; {
|
||||
if par, has := seen[p]; !has {
|
||||
// This can happen during deletes, since we delete children before parents.
|
||||
// TODO[pulumi/pulumi#340]: we need to figure out how best to display this sequence; at the very
|
||||
// least, it would be ideal to preserve the indentation.
|
||||
break
|
||||
} else {
|
||||
indent++
|
||||
p = par.Res.Parent
|
||||
}
|
||||
indent++
|
||||
p = par.Res().Parent
|
||||
}
|
||||
return indent
|
||||
}
|
||||
|
||||
func printStepHeader(b *bytes.Buffer, step deploy.Step) {
|
||||
func printStepHeader(b *bytes.Buffer, step StepEventMetadata) {
|
||||
var extra string
|
||||
old := step.Old()
|
||||
new := step.New()
|
||||
old := step.Old
|
||||
new := step.New
|
||||
if new != nil && !new.Protect && old != nil && old.Protect {
|
||||
// show an unlocked symbol, since we are unprotecting a resource.
|
||||
extra = " 🔓"
|
||||
|
@ -47,7 +46,7 @@ func printStepHeader(b *bytes.Buffer, step deploy.Step) {
|
|||
// show a locked symbol, since we are either newly protecting this resource, or retaining protection.
|
||||
extra = " 🔒"
|
||||
}
|
||||
writeString(b, fmt.Sprintf("%s: (%s)%s\n", string(step.Type()), step.Op(), extra))
|
||||
writeString(b, fmt.Sprintf("%s: (%s)%s\n", string(step.Type), step.Op, extra))
|
||||
}
|
||||
|
||||
func getIndentationString(indent int, op deploy.StepOp, prefix bool) string {
|
||||
|
@ -96,12 +95,12 @@ func writeVerbatim(b *bytes.Buffer, op deploy.StepOp, value string) {
|
|||
writeWithIndentNoPrefix(b, 0, op, "%s", value)
|
||||
}
|
||||
|
||||
func getResourcePropertiesSummary(step deploy.Step, indent int) string {
|
||||
func GetResourcePropertiesSummary(step StepEventMetadata, indent int) string {
|
||||
var b bytes.Buffer
|
||||
|
||||
op := step.Op()
|
||||
urn := step.URN()
|
||||
old := step.Old()
|
||||
op := step.Op
|
||||
urn := step.URN
|
||||
old := step.Old
|
||||
|
||||
// Print the indentation.
|
||||
writeString(&b, getIndentationString(indent, op, false))
|
||||
|
@ -132,28 +131,26 @@ func getResourcePropertiesSummary(step deploy.Step, indent int) string {
|
|||
return b.String()
|
||||
}
|
||||
|
||||
func getResourcePropertiesDetails(step deploy.Step, indent int, planning bool, debug bool) string {
|
||||
func GetResourcePropertiesDetails(step StepEventMetadata, indent int, planning bool, debug bool) string {
|
||||
var b bytes.Buffer
|
||||
|
||||
// indent everything an additional level, like other properties.
|
||||
indent++
|
||||
|
||||
var replaces []resource.PropertyKey
|
||||
if step.Op() == deploy.OpCreateReplacement {
|
||||
replaces = step.(*deploy.CreateStep).Keys()
|
||||
} else if step.Op() == deploy.OpReplace {
|
||||
replaces = step.(*deploy.ReplaceStep).Keys()
|
||||
if step.Op == deploy.OpCreateReplacement || step.Op == deploy.OpReplace {
|
||||
replaces = step.Keys
|
||||
}
|
||||
|
||||
old := step.Old()
|
||||
new := step.New()
|
||||
old := step.Old
|
||||
new := step.New
|
||||
|
||||
if old == nil && new != nil {
|
||||
printObject(&b, new.Inputs, planning, indent, step.Op(), false, debug)
|
||||
printObject(&b, new.Inputs, planning, indent, step.Op, false, debug)
|
||||
} else if new == nil && old != nil {
|
||||
printObject(&b, old.Inputs, planning, indent, step.Op(), false, debug)
|
||||
printObject(&b, old.Inputs, planning, indent, step.Op, false, debug)
|
||||
} else {
|
||||
printOldNewDiffs(&b, old.Inputs, new.Inputs, replaces, planning, indent, step.Op(), debug)
|
||||
printOldNewDiffs(&b, old.Inputs, new.Inputs, replaces, planning, indent, step.Op, debug)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
|
@ -186,17 +183,17 @@ func printObject(
|
|||
}
|
||||
}
|
||||
|
||||
// printResourceOutputProperties prints only those properties that either differ from the input properties or, if
|
||||
// GetResourceOutputsPropertiesString prints only those properties that either differ from the input properties or, if
|
||||
// there is an old snapshot of the resource, differ from the prior old snapshot's output properties.
|
||||
func getResourceOutputsPropertiesString(step deploy.Step, indent int, planning bool, debug bool) string {
|
||||
func GetResourceOutputsPropertiesString(step StepEventMetadata, indent int, planning bool, debug bool) string {
|
||||
var b bytes.Buffer
|
||||
|
||||
// Only certain kinds of steps have output properties associated with them.
|
||||
new := step.New()
|
||||
new := step.New
|
||||
if new == nil || new.Outputs == nil {
|
||||
return ""
|
||||
}
|
||||
op := considerSameIfNotCreateOrDelete(step.Op())
|
||||
op := considerSameIfNotCreateOrDelete(step.Op)
|
||||
|
||||
// First fetch all the relevant property maps that we may consult.
|
||||
ins := new.Inputs
|
||||
|
@ -292,10 +289,12 @@ func printPropertyValue(
|
|||
}
|
||||
} else if v.IsAsset() {
|
||||
a := v.AssetValue()
|
||||
if text, has := a.GetText(); has {
|
||||
if a.IsText() {
|
||||
write(b, op, "asset(text:%s) {\n", shortHash(a.Hash))
|
||||
|
||||
massaged := massageText(text, debug)
|
||||
a = resource.MassageIfUserProgramCodeAsset(a, debug)
|
||||
|
||||
massaged := a.Text
|
||||
|
||||
// pretty print the text, line by line, with proper breaks.
|
||||
lines := strings.Split(massaged, "\n")
|
||||
|
@ -687,13 +686,13 @@ func printAssetDiff(
|
|||
|
||||
hashChange := getTextChangeString(shortHash(oldAsset.Hash), shortHash(newAsset.Hash))
|
||||
|
||||
if oldText, has := oldAsset.GetText(); has {
|
||||
if newText, has := newAsset.GetText(); has {
|
||||
if oldAsset.IsText() {
|
||||
if newAsset.IsText() {
|
||||
titleFunc(deploy.OpUpdate, true)
|
||||
write(b, op, "asset(text:%s) {\n", hashChange)
|
||||
|
||||
massagedOldText := massageText(oldText, debug)
|
||||
massagedNewText := massageText(newText, debug)
|
||||
massagedOldText := resource.MassageIfUserProgramCodeAsset(oldAsset, debug).Text
|
||||
massagedNewText := resource.MassageIfUserProgramCodeAsset(newAsset, debug).Text
|
||||
|
||||
differ := diffmatchpatch.New()
|
||||
differ.DiffTimeout = 0
|
||||
|
@ -741,69 +740,6 @@ func getTextChangeString(old string, new string) string {
|
|||
return fmt.Sprintf("%s->%s", old, new)
|
||||
}
|
||||
|
||||
var (
|
||||
functionRegexp = regexp.MustCompile(`function __.*`)
|
||||
withRegexp = regexp.MustCompile(` with\({ .* }\) {`)
|
||||
environmentRegexp = regexp.MustCompile(` }\).apply\(.*\).apply\(this, arguments\);`)
|
||||
preambleRegexp = regexp.MustCompile(
|
||||
`function __.*\(\) {\n return \(function\(\) {\n with \(__closure\) {\n\nreturn `)
|
||||
postambleRegexp = regexp.MustCompile(
|
||||
`;\n\n }\n }\).apply\(__environment\).apply\(this, arguments\);\n}`)
|
||||
)
|
||||
|
||||
// massageText takes the text for a function and cleans it up a bit to make the user visible diffs
|
||||
// less noisy. Specifically:
|
||||
// 1. it tries to condense things by changling multiple blank lines into a single blank line.
|
||||
// 2. it normalizs the sha hashes we emit so that changes to them don't appear in the diff.
|
||||
// 3. it elides the with-capture headers, as changes there are not generally meaningful.
|
||||
//
|
||||
// TODO(https://github.com/pulumi/pulumi/issues/592) this is baking in a lot of knowledge about
|
||||
// pulumi serialized functions. We should try to move to an alternative mode that isn't so brittle.
|
||||
// Options include:
|
||||
// 1. Have a documented delimeter format that plan.go will look for. Have the function serializer
|
||||
// emit those delimeters around code that should be ignored.
|
||||
// 2. Have our resource generation code supply not just the resource, but the "user presentable"
|
||||
// resource that cuts out a lot of cruft. We could then just diff that content here.
|
||||
func massageText(text string, debug bool) string {
|
||||
if debug {
|
||||
return text
|
||||
}
|
||||
|
||||
// Only do this for strings that match our serialized function pattern.
|
||||
if !functionRegexp.MatchString(text) ||
|
||||
!withRegexp.MatchString(text) ||
|
||||
!environmentRegexp.MatchString(text) {
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
replaceNewlines := func() {
|
||||
for {
|
||||
newText := strings.Replace(text, "\n\n\n", "\n\n", -1)
|
||||
if len(newText) == len(text) {
|
||||
break
|
||||
}
|
||||
|
||||
text = newText
|
||||
}
|
||||
}
|
||||
|
||||
replaceNewlines()
|
||||
|
||||
firstFunc := functionRegexp.FindStringIndex(text)
|
||||
text = text[firstFunc[0]:]
|
||||
|
||||
text = withRegexp.ReplaceAllString(text, " with (__closure) {")
|
||||
text = environmentRegexp.ReplaceAllString(text, " }).apply(__environment).apply(this, arguments);")
|
||||
|
||||
text = preambleRegexp.ReplaceAllString(text, "")
|
||||
text = postambleRegexp.ReplaceAllString(text, "")
|
||||
|
||||
replaceNewlines()
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// diffToPrettyString takes the full diff produed by diffmatchpatch and condenses it into something
|
||||
// useful we can print to the console. Specifically, while it includes any adds/removes in
|
||||
// green/red, it will also show portions of the unchanged text to help give surrounding context to
|
||||
|
|
|
@ -4,6 +4,7 @@ package engine
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
|
@ -66,42 +67,59 @@ type SummaryEventPayload struct {
|
|||
}
|
||||
|
||||
type ResourceOperationFailedPayload struct {
|
||||
Metadata StepEventMetdata
|
||||
Metadata StepEventMetadata
|
||||
Status resource.Status
|
||||
Steps int
|
||||
}
|
||||
|
||||
type ResourceOutputsEventPayload struct {
|
||||
Metadata StepEventMetdata
|
||||
Indent int
|
||||
Text string
|
||||
Metadata StepEventMetadata
|
||||
Planning bool
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type ResourcePreEventPayload struct {
|
||||
Metadata StepEventMetdata
|
||||
Indent int
|
||||
Summary string
|
||||
Details string
|
||||
Metadata StepEventMetadata
|
||||
Planning bool
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type StepEventMetdata struct {
|
||||
type StepEventMetadata struct {
|
||||
Op deploy.StepOp // the operation performed by this step.
|
||||
URN resource.URN // the resource URN (for before and after).
|
||||
Type tokens.Type // the type affected by this step.
|
||||
Old *StepEventStateMetadata // the state of the resource before performing this step.
|
||||
New *StepEventStateMetadata // the state of the resource after performing this step.
|
||||
Res *StepEventStateMetadata // the latest state for the resource that is known (worst case, old).
|
||||
Keys []resource.PropertyKey // the keys causing replacement (only for CreateStep and ReplaceStep).
|
||||
Logical bool // true if this step represents a logical operation in the program.
|
||||
}
|
||||
|
||||
type StepEventStateMetadata struct {
|
||||
Type tokens.Type // the resource's type.
|
||||
URN resource.URN // the resource's object urn, a human-friendly, unique name for the resource.
|
||||
Custom bool // true if the resource is custom, managed by a plugin.
|
||||
Delete bool // true if this resource is pending deletion due to a replacement.
|
||||
ID resource.ID // the resource's unique ID, assigned by the resource provider (or blank if none/uncreated).
|
||||
Parent resource.URN // an optional parent URN that this resource belongs to.
|
||||
Protect bool // true to "protect" this resource (protected resources cannot be deleted).
|
||||
// the resource's type.
|
||||
Type tokens.Type
|
||||
// the resource's object urn, a human-friendly, unique name for the resource.
|
||||
URN resource.URN
|
||||
// true if the resource is custom, managed by a plugin.
|
||||
Custom bool
|
||||
// true if this resource is pending deletion due to a replacement.
|
||||
Delete bool
|
||||
// the resource's unique ID, assigned by the resource provider (or blank if none/uncreated).
|
||||
ID resource.ID
|
||||
// an optional parent URN that this resource belongs to.
|
||||
Parent resource.URN
|
||||
// true to "protect" this resource (protected resources cannot be deleted).
|
||||
Protect bool
|
||||
// the resource's input properties (as specified by the program). Note: because this will cross
|
||||
// over rpc boundaries it will be slightly different than the Inputs found in resource_state.
|
||||
// Specifically, secrets will have been filtered out, and large values (like assets) will be
|
||||
// have a simple hash-based representation. This allows clients to display this information
|
||||
// properly, without worrying about leaking sensitive data, and without having to transmit huge
|
||||
// amounts of data.
|
||||
Inputs resource.PropertyMap
|
||||
// the resource's complete output state (as returned by the resource provider). See "Inputs"
|
||||
// for additional details about how data will be transformed before going into this map.
|
||||
Outputs resource.PropertyMap
|
||||
}
|
||||
|
||||
func makeEventEmitter(events chan<- Event, update UpdateInfo) eventEmitter {
|
||||
|
@ -144,19 +162,28 @@ type eventEmitter struct {
|
|||
Filter filter
|
||||
}
|
||||
|
||||
func makeStepEventMetadata(step deploy.Step) StepEventMetdata {
|
||||
return StepEventMetdata{
|
||||
func makeStepEventMetadata(step deploy.Step, filter filter, debug bool) StepEventMetadata {
|
||||
var keys []resource.PropertyKey
|
||||
|
||||
if step.Op() == deploy.OpCreateReplacement {
|
||||
keys = step.(*deploy.CreateStep).Keys()
|
||||
} else if step.Op() == deploy.OpReplace {
|
||||
keys = step.(*deploy.ReplaceStep).Keys()
|
||||
}
|
||||
|
||||
return StepEventMetadata{
|
||||
Op: step.Op(),
|
||||
URN: step.URN(),
|
||||
Type: step.Type(),
|
||||
Old: makeStepEventStateMetadata(step.Old()),
|
||||
New: makeStepEventStateMetadata(step.New()),
|
||||
Res: makeStepEventStateMetadata(step.Res()),
|
||||
Keys: keys,
|
||||
Old: makeStepEventStateMetadata(step.Old(), filter, debug),
|
||||
New: makeStepEventStateMetadata(step.New(), filter, debug),
|
||||
Res: makeStepEventStateMetadata(step.Res(), filter, debug),
|
||||
Logical: step.Logical(),
|
||||
}
|
||||
}
|
||||
|
||||
func makeStepEventStateMetadata(state *resource.State) *StepEventStateMetadata {
|
||||
func makeStepEventStateMetadata(state *resource.State, filter filter, debug bool) *StepEventStateMetadata {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -169,9 +196,120 @@ func makeStepEventStateMetadata(state *resource.State) *StepEventStateMetadata {
|
|||
ID: state.ID,
|
||||
Parent: state.Parent,
|
||||
Protect: state.Protect,
|
||||
Inputs: filterPropertyMap(state.Inputs, filter, debug),
|
||||
Outputs: filterPropertyMap(state.Outputs, filter, debug),
|
||||
}
|
||||
}
|
||||
|
||||
func filterPropertyMap(propertyMap resource.PropertyMap, filter filter, debug bool) resource.PropertyMap {
|
||||
mappable := propertyMap.Mappable()
|
||||
|
||||
var filterValue func(v interface{}) interface{}
|
||||
|
||||
filterPropertyValue := func(pv resource.PropertyValue) resource.PropertyValue {
|
||||
return resource.NewPropertyValue(filterValue(pv.Mappable()))
|
||||
}
|
||||
|
||||
// filter values walks unwrapped (i.e. non-PropertyValue) values and applies the filter function
|
||||
// to them recursively. The only thing the filter actually applies to is strings.
|
||||
//
|
||||
// The return value of this function should have the same type as the input value.
|
||||
filterValue = func(v interface{}) interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Else, check for some known primitive types.
|
||||
switch t := v.(type) {
|
||||
case bool, int, uint, int32, uint32,
|
||||
int64, uint64, float32, float64:
|
||||
// simple types. map over as is.
|
||||
return v
|
||||
case string:
|
||||
// have to ensure we filter out secrets.
|
||||
return filter.Filter(t)
|
||||
case *resource.Asset:
|
||||
text := t.Text
|
||||
if text != "" {
|
||||
// we don't want to include the full text of an asset as we serialize it over as
|
||||
// events. They represent user files and are thus are unbounded in size. Instead,
|
||||
// we only include the text if it represents a user's serialized program code, as
|
||||
// that is something we want the receiver to see to display as part of
|
||||
// progress/diffs/etc.
|
||||
if t.IsUserProgramCode() {
|
||||
// also make sure we filter this in case there are any secrets in the code.
|
||||
text = filter.Filter(resource.MassageIfUserProgramCodeAsset(t, debug).Text)
|
||||
} else {
|
||||
// We need to have some string here so that we preserve that this is a
|
||||
// text-asset
|
||||
text = "<stripped>"
|
||||
}
|
||||
}
|
||||
|
||||
return &resource.Asset{
|
||||
Sig: t.Sig,
|
||||
Hash: t.Hash,
|
||||
Text: text,
|
||||
Path: t.Path,
|
||||
URI: t.URI,
|
||||
}
|
||||
case *resource.Archive:
|
||||
return &resource.Archive{
|
||||
Sig: t.Sig,
|
||||
Hash: t.Hash,
|
||||
Path: t.Path,
|
||||
URI: t.URI,
|
||||
Assets: filterValue(t.Assets).(map[string]interface{}),
|
||||
}
|
||||
case resource.Computed:
|
||||
return resource.Computed{
|
||||
Element: filterPropertyValue(t.Element),
|
||||
}
|
||||
case resource.Output:
|
||||
return resource.Output{
|
||||
Element: filterPropertyValue(t.Element),
|
||||
}
|
||||
}
|
||||
|
||||
// Next, see if it's an array, slice, pointer or struct, and handle each accordingly.
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rk := rv.Type().Kind(); rk {
|
||||
case reflect.Array, reflect.Slice:
|
||||
// If an array or slice, just create an array out of it.
|
||||
var arr []interface{}
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
arr = append(arr, filterValue(rv.Index(i).Interface()))
|
||||
}
|
||||
return arr
|
||||
case reflect.Ptr:
|
||||
if rv.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
v1 := filterValue(rv.Elem().Interface())
|
||||
return &v1
|
||||
case reflect.Map:
|
||||
obj := make(map[string]interface{})
|
||||
for _, key := range rv.MapKeys() {
|
||||
k := key.Interface().(string)
|
||||
v := rv.MapIndex(key).Interface()
|
||||
obj[k] = filterValue(v)
|
||||
}
|
||||
return obj
|
||||
default:
|
||||
contract.Failf("Unrecognized value type: type=%v kind=%v", rv.Type(), rk)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return resource.NewPropertyMapFromMapRepl(
|
||||
mappable, nil, /*replk*/
|
||||
func(v interface{}) (resource.PropertyValue, bool) {
|
||||
return resource.NewPropertyValue(filterValue(v)), true
|
||||
})
|
||||
}
|
||||
|
||||
type filter interface {
|
||||
Filter(s string) string
|
||||
}
|
||||
|
@ -191,42 +329,47 @@ func (f *regexFilter) Filter(s string) string {
|
|||
return f.re.ReplaceAllLiteralString(s, "[secret]")
|
||||
}
|
||||
|
||||
func (e *eventEmitter) resourceOperationFailedEvent(step deploy.Step, status resource.Status, steps int) {
|
||||
func (e *eventEmitter) resourceOperationFailedEvent(
|
||||
step deploy.Step, status resource.Status, steps int, debug bool) {
|
||||
|
||||
contract.Requiref(e != nil, "e", "!= nil")
|
||||
|
||||
e.Chan <- Event{
|
||||
Type: ResourceOperationFailed,
|
||||
Payload: ResourceOperationFailedPayload{
|
||||
Metadata: makeStepEventMetadata(step),
|
||||
Metadata: makeStepEventMetadata(step, e.Filter, debug),
|
||||
Status: status,
|
||||
Steps: steps,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *eventEmitter) resourceOutputsEvent(step deploy.Step, indent int, text string) {
|
||||
func (e *eventEmitter) resourceOutputsEvent(
|
||||
step deploy.Step, planning bool, debug bool) {
|
||||
|
||||
contract.Requiref(e != nil, "e", "!= nil")
|
||||
|
||||
e.Chan <- Event{
|
||||
Type: ResourceOutputsEvent,
|
||||
Payload: ResourceOutputsEventPayload{
|
||||
Metadata: makeStepEventMetadata(step),
|
||||
Indent: indent,
|
||||
Text: e.Filter.Filter(text),
|
||||
Metadata: makeStepEventMetadata(step, e.Filter, debug),
|
||||
Planning: planning,
|
||||
Debug: debug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *eventEmitter) resourcePreEvent(step deploy.Step, indent int, summary string, details string) {
|
||||
func (e *eventEmitter) resourcePreEvent(
|
||||
step deploy.Step, planning bool, debug bool) {
|
||||
|
||||
contract.Requiref(e != nil, "e", "!= nil")
|
||||
|
||||
e.Chan <- Event{
|
||||
Type: ResourcePreEvent,
|
||||
Payload: ResourcePreEventPayload{
|
||||
Metadata: makeStepEventMetadata(step),
|
||||
Indent: indent,
|
||||
Summary: e.Filter.Filter(summary),
|
||||
Details: e.Filter.Filter(details),
|
||||
Metadata: makeStepEventMetadata(step, e.Filter, debug),
|
||||
Planning: planning,
|
||||
Debug: debug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,10 +79,7 @@ func newPreviewActions(opts planOptions) *previewActions {
|
|||
func (acts *previewActions) OnResourceStepPre(step deploy.Step) (interface{}, error) {
|
||||
acts.Seen[step.URN()] = step
|
||||
|
||||
indent := getIndent(step, acts.Seen)
|
||||
summary := getResourcePropertiesSummary(step, indent)
|
||||
details := getResourcePropertiesDetails(step, indent, true, acts.Opts.Debug)
|
||||
acts.Opts.Events.resourcePreEvent(step, indent, summary, details)
|
||||
acts.Opts.Events.resourcePreEvent(step, true /*planning*/, acts.Opts.Debug)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -106,9 +103,7 @@ func (acts *previewActions) OnResourceStepPost(ctx interface{},
|
|||
func (acts *previewActions) OnResourceOutputs(step deploy.Step) error {
|
||||
assertSeen(acts.Seen, step)
|
||||
|
||||
indent := getIndent(step, acts.Seen)
|
||||
text := getResourceOutputsPropertiesString(step, indent, true, acts.Opts.Debug)
|
||||
acts.Opts.Events.resourceOutputsEvent(step, indent, text)
|
||||
acts.Opts.Events.resourceOutputsEvent(step, true /*planning*/, acts.Opts.Debug)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -151,10 +151,7 @@ func (acts *updateActions) OnResourceStepPre(step deploy.Step) (interface{}, err
|
|||
// Ensure we've marked this step as observed.
|
||||
acts.Seen[step.URN()] = step
|
||||
|
||||
indent := getIndent(step, acts.Seen)
|
||||
summary := getResourcePropertiesSummary(step, indent)
|
||||
details := getResourcePropertiesDetails(step, indent, false, acts.Opts.Debug)
|
||||
acts.Opts.Events.resourcePreEvent(step, indent, summary, details)
|
||||
acts.Opts.Events.resourcePreEvent(step, false /*planning*/, acts.Opts.Debug)
|
||||
|
||||
// Inform the snapshot service that we are about to perform a step.
|
||||
return acts.Update.BeginMutation()
|
||||
|
@ -162,6 +159,7 @@ func (acts *updateActions) OnResourceStepPre(step deploy.Step) (interface{}, err
|
|||
|
||||
func (acts *updateActions) OnResourceStepPost(ctx interface{},
|
||||
step deploy.Step, status resource.Status, err error) error {
|
||||
|
||||
assertSeen(acts.Seen, step)
|
||||
|
||||
// Report the result of the step.
|
||||
|
@ -173,7 +171,7 @@ func (acts *updateActions) OnResourceStepPost(ctx interface{},
|
|||
|
||||
// Issue a true, bonafide error.
|
||||
acts.Opts.Diag.Errorf(diag.ErrorPlanApplyFailed, err)
|
||||
acts.Opts.Events.resourceOperationFailedEvent(step, status, acts.Steps)
|
||||
acts.Opts.Events.resourceOperationFailedEvent(step, status, acts.Steps, acts.Opts.Debug)
|
||||
} else {
|
||||
if step.Logical() {
|
||||
// Increment the counters.
|
||||
|
@ -182,9 +180,7 @@ func (acts *updateActions) OnResourceStepPost(ctx interface{},
|
|||
}
|
||||
|
||||
// Also show outputs here, since there might be some from the initial registration.
|
||||
indent := getIndent(step, acts.Seen)
|
||||
text := getResourceOutputsPropertiesString(step, indent, false, acts.Opts.Debug)
|
||||
acts.Opts.Events.resourceOutputsEvent(step, indent, text)
|
||||
acts.Opts.Events.resourceOutputsEvent(step, false /*planning*/, acts.Opts.Debug)
|
||||
}
|
||||
|
||||
// Write out the current snapshot. Note that even if a failure has occurred, we should still have a
|
||||
|
@ -195,9 +191,7 @@ func (acts *updateActions) OnResourceStepPost(ctx interface{},
|
|||
func (acts *updateActions) OnResourceOutputs(step deploy.Step) error {
|
||||
assertSeen(acts.Seen, step)
|
||||
|
||||
indent := getIndent(step, acts.Seen)
|
||||
text := getResourceOutputsPropertiesString(step, indent, false, acts.Opts.Debug)
|
||||
acts.Opts.Events.resourceOutputsEvent(step, indent, text)
|
||||
acts.Opts.Events.resourceOutputsEvent(step, false /*planning*/, acts.Opts.Debug)
|
||||
|
||||
// There's a chance there are new outputs that weren't written out last time.
|
||||
// We need to perform another snapshot write to ensure they get written out.
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
@ -91,6 +92,80 @@ func (a *Asset) GetURI() (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
var (
|
||||
functionRegexp = regexp.MustCompile(`function __.*`)
|
||||
withRegexp = regexp.MustCompile(` with\({ .* }\) {`)
|
||||
environmentRegexp = regexp.MustCompile(` }\).apply\(.*\).apply\(this, arguments\);`)
|
||||
preambleRegexp = regexp.MustCompile(
|
||||
`function __.*\(\) {\n return \(function\(\) {\n with \(__closure\) {\n\nreturn `)
|
||||
postambleRegexp = regexp.MustCompile(
|
||||
`;\n\n }\n }\).apply\(__environment\).apply\(this, arguments\);\n}`)
|
||||
)
|
||||
|
||||
// IsUserProgramCode checks to see if this is the special asset containing the users's code
|
||||
func (a *Asset) IsUserProgramCode() bool {
|
||||
if !a.IsText() {
|
||||
return false
|
||||
}
|
||||
|
||||
text := a.Text
|
||||
|
||||
return functionRegexp.MatchString(text) &&
|
||||
withRegexp.MatchString(text) &&
|
||||
environmentRegexp.MatchString(text)
|
||||
}
|
||||
|
||||
// MassageIfUserProgramCodeAsset takes the text for a function and cleans it up a bit to make the
|
||||
// user visible diffs less noisy. Specifically:
|
||||
// 1. it tries to condense things by changling multiple blank lines into a single blank line.
|
||||
// 2. it normalizs the sha hashes we emit so that changes to them don't appear in the diff.
|
||||
// 3. it elides the with-capture headers, as changes there are not generally meaningful.
|
||||
//
|
||||
// TODO(https://github.com/pulumi/pulumi/issues/592) this is baking in a lot of knowledge about
|
||||
// pulumi serialized functions. We should try to move to an alternative mode that isn't so brittle.
|
||||
// Options include:
|
||||
// 1. Have a documented delimeter format that plan.go will look for. Have the function serializer
|
||||
// emit those delimeters around code that should be ignored.
|
||||
// 2. Have our resource generation code supply not just the resource, but the "user presentable"
|
||||
// resource that cuts out a lot of cruft. We could then just diff that content here.
|
||||
func MassageIfUserProgramCodeAsset(asset *Asset, debug bool) *Asset {
|
||||
if debug {
|
||||
return asset
|
||||
}
|
||||
|
||||
// Only do this for strings that match our serialized function pattern.
|
||||
if !asset.IsUserProgramCode() {
|
||||
return asset
|
||||
}
|
||||
|
||||
text := asset.Text
|
||||
replaceNewlines := func() {
|
||||
for {
|
||||
newText := strings.Replace(text, "\n\n\n", "\n\n", -1)
|
||||
if len(newText) == len(text) {
|
||||
break
|
||||
}
|
||||
|
||||
text = newText
|
||||
}
|
||||
}
|
||||
|
||||
replaceNewlines()
|
||||
|
||||
firstFunc := functionRegexp.FindStringIndex(text)
|
||||
text = text[firstFunc[0]:]
|
||||
|
||||
text = withRegexp.ReplaceAllString(text, " with (__closure) {")
|
||||
text = environmentRegexp.ReplaceAllString(text, " }).apply(__environment).apply(this, arguments);")
|
||||
|
||||
text = preambleRegexp.ReplaceAllString(text, "")
|
||||
text = postambleRegexp.ReplaceAllString(text, "")
|
||||
|
||||
replaceNewlines()
|
||||
|
||||
return &Asset{Text: text}
|
||||
}
|
||||
|
||||
// GetURIURL returns the underlying URI as a parsed URL, provided it is one. If there was an error parsing the URI, it
|
||||
// will be returned as a non-nil error object.
|
||||
func (a *Asset) GetURIURL() (*url.URL, bool, error) {
|
||||
|
|
Loading…
Reference in a new issue