// Copyright 2018, Pulumi Corporation. All rights reserved. package engine import ( "bytes" "reflect" "regexp" "time" "github.com/pulumi/pulumi/pkg/diag" "github.com/pulumi/pulumi/pkg/diag/colors" "github.com/pulumi/pulumi/pkg/resource" "github.com/pulumi/pulumi/pkg/resource/config" "github.com/pulumi/pulumi/pkg/resource/deploy" "github.com/pulumi/pulumi/pkg/tokens" "github.com/pulumi/pulumi/pkg/util/contract" ) // Event represents an event generated by the engine during an operation. The underlying // type for the `Payload` field will differ depending on the value of the `Type` field type Event struct { Type EventType Payload interface{} } // EventType is the kind of event being emitted. type EventType string const ( CancelEvent EventType = "cancel" StdoutColorEvent EventType = "stdoutcolor" DiagEvent EventType = "diag" PreludeEvent EventType = "prelude" SummaryEvent EventType = "summary" ResourcePreEvent EventType = "resource-pre" ResourceOutputsEvent EventType = "resource-outputs" ResourceOperationFailed EventType = "resource-operationfailed" ) func cancelEvent() Event { return Event{Type: CancelEvent} } // DiagEventPayload is the payload for an event with type `diag` type DiagEventPayload struct { URN resource.URN Message string Color colors.Colorization Severity diag.Severity } type StdoutEventPayload struct { Message string Color colors.Colorization } type PreludeEventPayload struct { IsPreview bool // true if this prelude is for a plan operation Config map[string]string // the keys and values for config. For encrypted config, the values may be blinded } type SummaryEventPayload struct { IsPreview bool // true if this summary is for a plan operation MaybeCorrupt bool // true if one or more resources may be corrupt Duration time.Duration // the duration of the entire update operation (zero values for previews) ResourceChanges ResourceChanges // count of changed resources, useful for reporting } type ResourceOperationFailedPayload struct { Metadata StepEventMetadata Status resource.Status Steps int } type ResourceOutputsEventPayload struct { Metadata StepEventMetadata Planning bool Debug bool } type ResourcePreEventPayload struct { Metadata StepEventMetadata Planning bool Debug bool } 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 { // 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 { var f filter = &nopFilter{} target := update.GetTarget() if target.Config.HasSecureValue() { var b bytes.Buffer for _, v := range target.Config { if !v.Secure() { continue } secret, err := v.Value(target.Decrypter) contract.AssertNoError(err) // For short secrets, don't actually add them to the filter, this is a trade-off we make to prevent // displaying `[secret]`. Travis does a similar thing, for example. if len(secret) < 3 { continue } if b.Len() > 0 { b.WriteRune('|') } b.WriteString(regexp.QuoteMeta(secret)) } if b.Len() > 0 { f = ®exFilter{re: regexp.MustCompile(b.String())} } } return eventEmitter{ Chan: events, Filter: f, } } type eventEmitter struct { Chan chan<- Event Filter filter } 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(), 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, filter filter, debug bool) *StepEventStateMetadata { if state == nil { return nil } return &StepEventStateMetadata{ Type: state.Type, URN: state.URN, Custom: state.Custom, Delete: state.Delete, 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 = "" } } 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 } type nopFilter struct { } func (f *nopFilter) Filter(s string) string { return s } type regexFilter struct { re *regexp.Regexp } 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, debug bool) { contract.Requiref(e != nil, "e", "!= nil") e.Chan <- Event{ Type: ResourceOperationFailed, Payload: ResourceOperationFailedPayload{ Metadata: makeStepEventMetadata(step, e.Filter, debug), Status: status, Steps: steps, }, } } 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, e.Filter, debug), Planning: planning, Debug: debug, }, } } 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, e.Filter, debug), Planning: planning, Debug: debug, }, } } func (e *eventEmitter) preludeEvent(isPreview bool, cfg config.Map) { contract.Requiref(e != nil, "e", "!= nil") configStringMap := make(map[string]string, len(cfg)) for k, v := range cfg { keyString := k.String() valueString, err := v.Value(config.NewBlindingDecrypter()) contract.AssertNoError(err) configStringMap[keyString] = valueString } e.Chan <- Event{ Type: PreludeEvent, Payload: PreludeEventPayload{ IsPreview: isPreview, Config: configStringMap, }, } } func (e *eventEmitter) previewSummaryEvent(resourceChanges ResourceChanges) { contract.Requiref(e != nil, "e", "!= nil") e.Chan <- Event{ Type: SummaryEvent, Payload: SummaryEventPayload{ IsPreview: true, MaybeCorrupt: false, Duration: 0, ResourceChanges: resourceChanges, }, } } func (e *eventEmitter) updateSummaryEvent(maybeCorrupt bool, duration time.Duration, resourceChanges ResourceChanges) { contract.Requiref(e != nil, "e", "!= nil") e.Chan <- Event{ Type: SummaryEvent, Payload: SummaryEventPayload{ IsPreview: false, MaybeCorrupt: maybeCorrupt, Duration: duration, ResourceChanges: resourceChanges, }, } } func diagEvent(e *eventEmitter, urn resource.URN, msg string, sev diag.Severity) { contract.Requiref(e != nil, "e", "!= nil") e.Chan <- Event{ Type: DiagEvent, Payload: DiagEventPayload{ URN: urn, Message: e.Filter.Filter(msg), Color: colors.Raw, Severity: sev, }, } } func (e *eventEmitter) diagDebugEvent(urn resource.URN, msg string) { diagEvent(e, urn, msg, diag.Debug) } func (e *eventEmitter) diagInfoEvent(urn resource.URN, msg string) { diagEvent(e, urn, msg, diag.Info) } func (e *eventEmitter) diagInfoerrEvent(urn resource.URN, msg string) { diagEvent(e, urn, msg, diag.Infoerr) } func (e *eventEmitter) diagErrorEvent(urn resource.URN, msg string) { diagEvent(e, urn, msg, diag.Error) } func (e *eventEmitter) diagWarningEvent(urn resource.URN, msg string) { diagEvent(e, urn, msg, diag.Warning) }