pulumi/pkg/backend/display/json.go

258 lines
11 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package display
import (
"encoding/json"
"fmt"
"time"
"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"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/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)
// massagePropertyValue takes a property value and strips out the secrets annotations from it. If showSecrets is
// not true any secret values are replaced with "[secret]".
func massagePropertyValue(v resource.PropertyValue, showSecrets bool) resource.PropertyValue {
switch {
case v.IsArray():
new := make([]resource.PropertyValue, len(v.ArrayValue()))
for i, e := range v.ArrayValue() {
new[i] = massagePropertyValue(e, showSecrets)
}
return resource.NewArrayProperty(new)
case v.IsObject():
new := make(resource.PropertyMap, len(v.ObjectValue()))
for k, e := range v.ObjectValue() {
new[k] = massagePropertyValue(e, showSecrets)
}
return resource.NewObjectProperty(new)
case v.IsSecret() && showSecrets:
return massagePropertyValue(v.SecretValue().Element, showSecrets)
case v.IsSecret():
return resource.NewStringProperty("[secret]")
default:
return v
}
}
// MassageSecrets takes a property map and returns a new map by transforming each value with massagePropertyValue
// This allows us to serialize the resulting map using our existing serialization logic we use for deployments, to
// produce sane output for stackOutputs. If we did not do this, SecretValues would be serialized as objects
// with the signature key and value.
func MassageSecrets(m resource.PropertyMap, showSecrets bool) resource.PropertyMap {
new := make(resource.PropertyMap, len(m))
for k, e := range m {
new[k] = massagePropertyValue(e, showSecrets)
}
return new
}
// stateForJSONOutput prepares some resource's state for JSON output. This includes filtering the output based
// on the supplied options, in addition to massaging secret fields.
func stateForJSONOutput(s *resource.State, opts Options) *resource.State {
var inputs resource.PropertyMap
var outputs resource.PropertyMap
if !isRootURN(s.URN) || !opts.SuppressOutputs {
// For now, replace any secret properties as the string [secret] and then serialize what we have.
inputs = MassageSecrets(s.Inputs, false)
outputs = MassageSecrets(s.Outputs, false)
} else {
// If we're suppressing outputs, don't show the root stack properties.
inputs = resource.PropertyMap{}
outputs = resource.PropertyMap{}
}
return resource.NewState(s.Type, s.URN, s.Custom, s.Delete, s.ID, inputs,
outputs, s.Parent, s.Protect, s.External, s.Dependencies, s.InitErrors, s.Provider,
s.PropertyDependencies, s.PendingReplacement, s.AdditionalSecretOutputs, s.Aliases, &s.CustomTimeouts,
s.ImportID)
}
// ShowJSONEvents renders engine events from a preview into a well-formed JSON document. Note that this does not
// emit events incrementally so that it can guarantee anything emitted to stdout is well-formed. This means that,
// if used interactively, the experience will lead to potentially very long pauses. If run in CI, it is up to the
// end user to ensure that output is periodically printed to prevent tools from thinking preview has hung.
func ShowJSONEvents(op string, action apitype.UpdateKind, events <-chan engine.Event, done chan<- bool, opts Options) {
// Ensure we close the done channel before exiting.
defer func() { close(done) }()
// Now loop and accumulate our digest until the event stream is closed, or we hit a cancellation.
var digest previewDigest
for e := range events {
// In the event of cancelation, break out of the loop immediately.
if e.Type == engine.CancelEvent {
break
}
// For all other events, use the payload to build up the JSON digest we'll emit later.
switch e.Type {
// Events ocurring early:
case engine.PreludeEvent:
// Capture the config map from the prelude. Note that all secrets will remain blinded for safety.
digest.Config = e.Payload().(engine.PreludeEventPayload).Config
// Events throughout the execution:
case engine.DiagEvent:
// Skip any ephemeral or debug messages, and elide all colorization.
p := e.Payload().(engine.DiagEventPayload)
if !p.Ephemeral && p.Severity != diag.Debug {
digest.Diagnostics = append(digest.Diagnostics, previewDiagnostic{
URN: p.URN,
Message: colors.Never.Colorize(p.Prefix + p.Message),
Severity: p.Severity,
})
}
case engine.StdoutColorEvent:
// Append stdout events as informational messages, and elide all colorization.
p := e.Payload().(engine.StdoutEventPayload)
digest.Diagnostics = append(digest.Diagnostics, previewDiagnostic{
Message: colors.Never.Colorize(p.Message),
Severity: diag.Info,
})
case engine.ResourcePreEvent:
// Create the detailed metadata for this step and the initial state of its resource. Later,
// if new outputs arrive, we'll search for and swap in those new values.
if m := e.Payload().(engine.ResourcePreEventPayload).Metadata; shouldShow(m, opts) || isRootStack(m) {
var detailedDiff map[string]propertyDiff
if m.DetailedDiff != nil {
detailedDiff = make(map[string]propertyDiff)
for k, v := range m.DetailedDiff {
detailedDiff[k] = propertyDiff{
Kind: v.Kind.String(),
InputDiff: v.InputDiff,
}
}
}
step := &previewStep{
Op: m.Op,
URN: m.URN,
Provider: m.Provider,
DiffReasons: m.Diffs,
ReplaceReasons: m.Keys,
DetailedDiff: detailedDiff,
}
if m.Old != nil {
oldState := stateForJSONOutput(m.Old.State, opts)
res, err := stack.SerializeResource(oldState, config.NewPanicCrypter(), false /* showSecrets */)
if err == nil {
step.OldState = &res
} else {
logging.V(7).Infof("not adding old state as there was an error serialzing: %s", err)
}
}
if m.New != nil {
newState := stateForJSONOutput(m.New.State, opts)
res, err := stack.SerializeResource(newState, config.NewPanicCrypter(), false /* showSecrets */)
if err == nil {
step.NewState = &res
} else {
logging.V(7).Infof("not adding new state as there was an error serialzing: %s", err)
}
}
digest.Steps = append(digest.Steps, step)
}
case engine.ResourceOutputsEvent, engine.ResourceOperationFailed:
// Because we are only JSON serializing previews, we don't need to worry about outputs
// resolving or operations failing. In the future, if we serialize actual deployments, we will
// need to come up with a scheme for matching the failure to the associated step.
// Events ocurring late:
case engine.PolicyViolationEvent:
// At this point in time, we don't handle policy events in JSON serialization
continue
case engine.SummaryEvent:
// At the end of the preview, a summary event indicates the final conclusions.
p := e.Payload().(engine.SummaryEventPayload)
digest.Duration = p.Duration
digest.ChangeSummary = p.ResourceChanges
digest.MaybeCorrupt = p.MaybeCorrupt
default:
contract.Failf("unknown event type '%s'", e.Type)
}
}
// Finally, go ahead and render the JSON to stdout.
out, err := json.MarshalIndent(&digest, "", " ")
contract.Assertf(err == nil, "unexpected JSON error: %v", err)
fmt.Println(string(out))
}
// previewDigest is a JSON-serializable overview of a preview operation.
type previewDigest struct {
// Config contains a map of configuration keys/values used during the preview. Any secrets will be blinded.
Config map[string]string `json:"config,omitempty"`
// Steps contains a detailed list of all resource step operations.
Steps []*previewStep `json:"steps,omitempty"`
// Diagnostics contains a record of all warnings/errors that took place during the preview. Note that
// ephemeral and debug messages are omitted from this list, as they are meant for display purposes only.
Diagnostics []previewDiagnostic `json:"diagnostics,omitempty"`
// Duration records the amount of time it took to perform the preview.
Duration time.Duration `json:"duration,omitempty"`
// ChangeSummary contains a map of count per operation (create, update, etc).
ChangeSummary engine.ResourceChanges `json:"changeSummary,omitempty"`
// MaybeCorrupt indicates whether one or more resources may be corrupt.
MaybeCorrupt bool `json:"maybeCorrupt,omitempty"`
}
// propertyDiff contains information about the difference in a single property value.
type propertyDiff struct {
// Kind is the kind of difference.
Kind string `json:"kind"`
// InputDiff is true if this is a difference between old and new inputs instead of old state and new inputs.
InputDiff bool `json:"inputDiff"`
}
// previewStep is a detailed overview of a step the engine intends to take.
type previewStep struct {
// Op is the kind of operation being performed.
Op deploy.StepOp `json:"op"`
// URN is the resource being affected by this operation.
URN resource.URN `json:"urn"`
// Provider is the provider that will perform this step.
Provider string `json:"provider,omitempty"`
// OldState is the old state for this resource, if appropriate given the operation type.
OldState *apitype.ResourceV3 `json:"oldState,omitempty"`
// NewState is the new state for this resource, if appropriate given the operation type.
NewState *apitype.ResourceV3 `json:"newState,omitempty"`
// DiffReasons is a list of keys that are causing a diff (for updating steps only).
DiffReasons []resource.PropertyKey `json:"diffReasons,omitempty"`
// ReplaceReasons is a list of keys that are causing replacement (for replacement steps only).
ReplaceReasons []resource.PropertyKey `json:"replaceReasons,omitempty"`
// DetailedDiff is a structured diff that indicates precise per-property differences.
DetailedDiff map[string]propertyDiff `json:"detailedDiff"`
}
// previewDiagnostic is a warning or error emitted during the execution of the preview.
type previewDiagnostic struct {
URN resource.URN `json:"urn,omitempty"`
Prefix string `json:"prefix,omitempty"`
Message string `json:"message,omitempty"`
Severity diag.Severity `json:"severity,omitempty"`
}