package display import ( "context" "errors" "fmt" "math" "regexp" "time" "github.com/pulumi/pulumi/pkg/v3/display" "github.com/pulumi/pulumi/pkg/v3/engine" "github.com/pulumi/pulumi/pkg/v3/resource/stack" "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" "github.com/pulumi/pulumi/sdk/v3/go/common/diag" "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "github.com/pulumi/pulumi/sdk/v3/go/common/slice" "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) var matchAnsiControlCodes = regexp.MustCompile(`\x1b\[[0-9;]*[mK]`) // ConvertEngineEvent converts a raw engine.Event into an apitype.EngineEvent used in the Pulumi // REST API. Returns an error if the engine event is unknown or not in an expected format. // EngineEvent.{ Sequence, Timestamp } are expected to be set by the caller. // // IMPORTANT: Any resource secret data stored in the engine event will be encrypted using the // blinding encrypter, and unrecoverable. So this operation is inherently lossy. func ConvertEngineEvent(e engine.Event, showSecrets bool) (apitype.EngineEvent, error) { var apiEvent apitype.EngineEvent // Error to return if the payload doesn't match expected. eventTypePayloadMismatch := fmt.Errorf("unexpected payload for event type %v", e.Type) switch e.Type { case engine.CancelEvent: apiEvent.CancelEvent = &apitype.CancelEvent{} case engine.StdoutColorEvent: p, ok := e.Payload().(engine.StdoutEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } apiEvent.StdoutEvent = &apitype.StdoutEngineEvent{ Message: p.Message, Color: string(p.Color), } case engine.DiagEvent: p, ok := e.Payload().(engine.DiagEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } // Clean up ANSI control codes. cleanedMsg := matchAnsiControlCodes.ReplaceAllString(p.Message, "") apiEvent.DiagnosticEvent = &apitype.DiagnosticEvent{ URN: string(p.URN), Prefix: p.Prefix, Message: cleanedMsg, Color: string(p.Color), Severity: string(p.Severity), Ephemeral: p.Ephemeral, } case engine.PolicyViolationEvent: p, ok := e.Payload().(engine.PolicyViolationEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } apiEvent.PolicyEvent = &apitype.PolicyEvent{ ResourceURN: string(p.ResourceURN), Message: p.Message, Color: string(p.Color), PolicyName: p.PolicyName, PolicyPackName: p.PolicyPackName, PolicyPackVersion: p.PolicyPackVersion, PolicyPackVersionTag: p.PolicyPackVersion, EnforcementLevel: string(p.EnforcementLevel), } case engine.PolicyRemediationEvent: p, ok := e.Payload().(engine.PolicyRemediationEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } // Serialize properties, ignoring errors, as with other event types. ctx := context.TODO() encrypter := config.BlindingCrypter before, err := stack.SerializeProperties(ctx, p.Before, encrypter, showSecrets) contract.IgnoreError(err) after, err := stack.SerializeProperties(ctx, p.After, encrypter, showSecrets) contract.IgnoreError(err) apiEvent.PolicyRemediationEvent = &apitype.PolicyRemediationEvent{ ResourceURN: string(p.ResourceURN), Color: string(p.Color), PolicyName: p.PolicyName, PolicyPackName: p.PolicyPackName, PolicyPackVersion: p.PolicyPackVersion, PolicyPackVersionTag: p.PolicyPackVersion, Before: before, After: after, } case engine.PreludeEvent: p, ok := e.Payload().(engine.PreludeEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } // Convert the config bag. cfg := make(map[string]string) for k, v := range p.Config { cfg[k] = v } apiEvent.PreludeEvent = &apitype.PreludeEvent{ Config: cfg, } case engine.SummaryEvent: p, ok := e.Payload().(engine.SummaryEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } // Convert the resource changes. changes := make(map[apitype.OpType]int) for op, count := range p.ResourceChanges { changes[apitype.OpType(op)] = count } apiEvent.SummaryEvent = &apitype.SummaryEvent{ MaybeCorrupt: p.MaybeCorrupt, DurationSeconds: int(math.Ceil(p.Duration.Seconds())), ResourceChanges: changes, PolicyPacks: p.PolicyPacks, } case engine.ResourcePreEvent: p, ok := e.Payload().(engine.ResourcePreEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } apiEvent.ResourcePreEvent = &apitype.ResourcePreEvent{ Metadata: convertStepEventMetadata(p.Metadata, showSecrets), Planning: p.Planning, } case engine.ResourceOutputsEvent: p, ok := e.Payload().(engine.ResourceOutputsEventPayload) if !ok { return apiEvent, eventTypePayloadMismatch } apiEvent.ResOutputsEvent = &apitype.ResOutputsEvent{ Metadata: convertStepEventMetadata(p.Metadata, showSecrets), Planning: p.Planning, } case engine.ResourceOperationFailed: p, ok := e.Payload().(engine.ResourceOperationFailedPayload) if !ok { return apiEvent, eventTypePayloadMismatch } apiEvent.ResOpFailedEvent = &apitype.ResOpFailedEvent{ Metadata: convertStepEventMetadata(p.Metadata, showSecrets), Status: int(p.Status), Steps: p.Steps, } case engine.PolicyLoadEvent: apiEvent.PolicyLoadEvent = &apitype.PolicyLoadEvent{} default: return apiEvent, fmt.Errorf("unknown event type %q", e.Type) } return apiEvent, nil } func convertStepEventMetadata(md engine.StepEventMetadata, showSecrets bool) apitype.StepEventMetadata { keys := make([]string, len(md.Keys)) for i, v := range md.Keys { keys[i] = string(v) } diffs := slice.Prealloc[string](len(md.Diffs)) for _, v := range md.Diffs { diffs = append(diffs, string(v)) } var detailedDiff map[string]apitype.PropertyDiff if md.DetailedDiff != nil { detailedDiff = make(map[string]apitype.PropertyDiff) for k, v := range md.DetailedDiff { var d apitype.DiffKind switch v.Kind { case plugin.DiffAdd: d = apitype.DiffAdd case plugin.DiffAddReplace: d = apitype.DiffAddReplace case plugin.DiffDelete: d = apitype.DiffDelete case plugin.DiffDeleteReplace: d = apitype.DiffDeleteReplace case plugin.DiffUpdate: d = apitype.DiffUpdate case plugin.DiffUpdateReplace: d = apitype.DiffUpdateReplace default: contract.Failf("unrecognized diff kind %v", v) } detailedDiff[k] = apitype.PropertyDiff{ Kind: d, InputDiff: v.InputDiff, } } } return apitype.StepEventMetadata{ Op: apitype.OpType(md.Op), URN: string(md.URN), Type: string(md.Type), Old: convertStepEventStateMetadata(md.Old, showSecrets), New: convertStepEventStateMetadata(md.New, showSecrets), Keys: keys, Diffs: diffs, DetailedDiff: detailedDiff, Logical: md.Logical, Provider: md.Provider, } } // convertStepEventStateMetadata converts the internal StepEventStateMetadata to the API type // we send over the wire. // // IMPORTANT: Any secret values are encrypted using the blinding encrypter. So any secret data // in the resource state will be lost and unrecoverable. func convertStepEventStateMetadata(md *engine.StepEventStateMetadata, showSecrets bool, ) *apitype.StepEventStateMetadata { if md == nil { return nil } ctx := context.TODO() encrypter := config.BlindingCrypter inputs, err := stack.SerializeProperties(ctx, md.Inputs, encrypter, showSecrets) contract.IgnoreError(err) outputs, err := stack.SerializeProperties(ctx, md.Outputs, encrypter, showSecrets) contract.IgnoreError(err) return &apitype.StepEventStateMetadata{ Type: string(md.Type), URN: string(md.URN), Custom: md.Custom, Delete: md.Delete, ID: string(md.ID), Parent: string(md.Parent), Provider: md.Provider, Protect: md.Protect, RetainOnDelete: md.RetainOnDelete, Inputs: inputs, Outputs: outputs, InitErrors: md.InitErrors, } } // ConvertJSONEvent converts an apitype.EngineEvent from the Pulumi REST API into a raw engine.Event // Returns an error if the engine event is unknown or not in an expected format. // // IMPORTANT: Any resource secret data stored in the engine event will be encrypted using the // blinding encrypter, and unrecoverable. So this operation is inherently lossy. func ConvertJSONEvent(apiEvent apitype.EngineEvent) (engine.Event, error) { var event engine.Event switch { case apiEvent.CancelEvent != nil: event = engine.NewCancelEvent() case apiEvent.StdoutEvent != nil: p := apiEvent.StdoutEvent event = engine.NewEvent(engine.StdoutEventPayload{ Message: p.Message, Color: colors.Colorization(p.Color), }) case apiEvent.DiagnosticEvent != nil: p := apiEvent.DiagnosticEvent event = engine.NewEvent(engine.DiagEventPayload{ URN: resource.URN(p.URN), Prefix: p.Prefix, Message: p.Message, Color: colors.Colorization(p.Color), Severity: diag.Severity(p.Severity), Ephemeral: p.Ephemeral, }) apiEvent.DiagnosticEvent = &apitype.DiagnosticEvent{} case apiEvent.PolicyEvent != nil: p := apiEvent.PolicyEvent event = engine.NewEvent(engine.PolicyViolationEventPayload{ ResourceURN: resource.URN(p.ResourceURN), Message: p.Message, Color: colors.Colorization(p.Color), PolicyName: p.PolicyName, PolicyPackName: p.PolicyPackName, PolicyPackVersion: p.PolicyPackVersion, EnforcementLevel: apitype.EnforcementLevel(p.EnforcementLevel), }) case apiEvent.PolicyRemediationEvent != nil: p := apiEvent.PolicyRemediationEvent // Deserialize the before and after properties, ignoring serialization // errors as the other event types do (e.g., step events). crypter := config.BlindingCrypter before, err := stack.DeserializeProperties(p.Before, crypter, crypter) contract.IgnoreError(err) after, err := stack.DeserializeProperties(p.After, crypter, crypter) contract.IgnoreError(err) event = engine.NewEvent(engine.PolicyRemediationEventPayload{ ResourceURN: resource.URN(p.ResourceURN), Color: colors.Colorization(p.Color), PolicyName: p.PolicyName, PolicyPackName: p.PolicyPackName, PolicyPackVersion: p.PolicyPackVersion, Before: before, After: after, }) case apiEvent.PreludeEvent != nil: p := apiEvent.PreludeEvent // Convert the config bag. event = engine.NewEvent(engine.PreludeEventPayload{ Config: p.Config, }) case apiEvent.SummaryEvent != nil: p := apiEvent.SummaryEvent // Convert the resource changes. changes := display.ResourceChanges{} for op, count := range p.ResourceChanges { changes[display.StepOp(op)] = count } event = engine.NewEvent(engine.SummaryEventPayload{ MaybeCorrupt: p.MaybeCorrupt, Duration: time.Duration(p.DurationSeconds) * time.Second, ResourceChanges: changes, PolicyPacks: p.PolicyPacks, }) case apiEvent.ResourcePreEvent != nil: p := apiEvent.ResourcePreEvent event = engine.NewEvent(engine.ResourcePreEventPayload{ Metadata: convertJSONStepEventMetadata(p.Metadata), Planning: p.Planning, }) case apiEvent.ResOutputsEvent != nil: p := apiEvent.ResOutputsEvent event = engine.NewEvent(engine.ResourceOutputsEventPayload{ Metadata: convertJSONStepEventMetadata(p.Metadata), Planning: p.Planning, }) case apiEvent.ResOpFailedEvent != nil: p := apiEvent.ResOpFailedEvent event = engine.NewEvent(engine.ResourceOperationFailedPayload{ Metadata: convertJSONStepEventMetadata(p.Metadata), Status: resource.Status(p.Status), Steps: p.Steps, }) case apiEvent.PolicyLoadEvent != nil: event = engine.NewEvent(engine.PolicyLoadEventPayload{}) default: return event, errors.New("unknown event type") } return event, nil } func convertJSONStepEventMetadata(md apitype.StepEventMetadata) engine.StepEventMetadata { keys := make([]resource.PropertyKey, len(md.Keys)) for i, v := range md.Keys { keys[i] = resource.PropertyKey(v) } diffs := slice.Prealloc[resource.PropertyKey](len(md.Diffs)) for _, v := range md.Diffs { diffs = append(diffs, resource.PropertyKey(v)) } var detailedDiff map[string]plugin.PropertyDiff if md.DetailedDiff != nil { detailedDiff = make(map[string]plugin.PropertyDiff) for k, v := range md.DetailedDiff { var d plugin.DiffKind switch v.Kind { case apitype.DiffAdd: d = plugin.DiffAdd case apitype.DiffAddReplace: d = plugin.DiffAddReplace case apitype.DiffDelete: d = plugin.DiffDelete case apitype.DiffDeleteReplace: d = plugin.DiffDeleteReplace case apitype.DiffUpdate: d = plugin.DiffUpdate case apitype.DiffUpdateReplace: d = plugin.DiffUpdateReplace default: contract.Failf("unrecognized diff kind %v", v) } detailedDiff[k] = plugin.PropertyDiff{ Kind: d, InputDiff: v.InputDiff, } } } old, new := convertJSONStepEventStateMetadata(md.Old), convertJSONStepEventStateMetadata(md.New) res := old if new != nil { res = new } return engine.StepEventMetadata{ Op: display.StepOp(md.Op), URN: resource.URN(md.URN), Type: tokens.Type(md.Type), Old: old, New: new, Res: res, Keys: keys, Diffs: diffs, DetailedDiff: detailedDiff, Logical: md.Logical, Provider: md.Provider, } } // convertJSONStepEventStateMetadata converts the internal StepEventStateMetadata to the API type // we send over the wire. // // IMPORTANT: Any secret values are encrypted using the blinding encrypter. So any secret data // in the resource state will be lost and unrecoverable. func convertJSONStepEventStateMetadata(md *apitype.StepEventStateMetadata) *engine.StepEventStateMetadata { if md == nil { return nil } crypter := config.BlindingCrypter inputs, err := stack.DeserializeProperties(md.Inputs, crypter, crypter) contract.IgnoreError(err) outputs, err := stack.DeserializeProperties(md.Outputs, crypter, crypter) contract.IgnoreError(err) return &engine.StepEventStateMetadata{ Type: tokens.Type(md.Type), URN: resource.URN(md.URN), Custom: md.Custom, Delete: md.Delete, ID: resource.ID(md.ID), Parent: resource.URN(md.Parent), Provider: md.Provider, Protect: md.Protect, RetainOnDelete: md.RetainOnDelete, Inputs: inputs, Outputs: outputs, InitErrors: md.InitErrors, } }