Include richer information in events so that final display can flexibly chose how to present it. (#1088)

This commit is contained in:
CyrusNajmabadi 2018-03-31 12:08:48 -07:00 committed by GitHub
parent e74ea464f3
commit 4b761f9fc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 351 additions and 174 deletions

View file

@ -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{

View file

@ -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.

View file

@ -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

View file

@ -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,
},
}
}

View file

@ -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
}

View file

@ -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.

View file

@ -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) {