pulumi/pkg/backend/display/json.go

258 lines
11 KiB
Go
Raw Normal View History

// 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) {
Defer all diffs to resource providers. (#2849) Thse changes make a subtle but critical adjustment to the process the Pulumi engine uses to determine whether or not a difference exists between a resource's actual and desired states, and adjusts the way this difference is calculated and displayed accordingly. Today, the Pulumi engine get the first chance to decide whether or not there is a difference between a resource's actual and desired states. It does this by comparing the current set of inputs for a resource (i.e. the inputs from the running Pulumi program) with the last set of inputs used to update the resource. If there is no difference between the old and new inputs, the engine decides that no change is necessary without consulting the resource's provider. Only if there are changes does the engine consult the resource's provider for more information about the difference. This can be problematic for a number of reasons: - Not all providers do input-input comparison; some do input-state comparison - Not all providers are able to update the last deployed set of inputs when performing a refresh - Some providers--either intentionally or due to bugs--may see changes in resources whose inputs have not changed All of these situations are confusing at the very least, and the first is problematic with respect to correctness. Furthermore, the display code only renders diffs it observes rather than rendering the diffs observed by the provider, which can obscure the actual changes detected at runtime. These changes address both of these issues: - Rather than comparing the current inputs against the last inputs before calling a resource provider's Diff function, the engine calls the Diff function in all cases. - Providers may now return a list of properties that differ between the requested and actual state and the way in which they differ. This information will then be used by the CLI to render the diff appropriately. A provider may also indicate that a particular diff is between old and new inputs rather than old state and new inputs. Fixes #2453.
2019-07-01 21:34:19 +02:00
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,
Defer all diffs to resource providers. (#2849) Thse changes make a subtle but critical adjustment to the process the Pulumi engine uses to determine whether or not a difference exists between a resource's actual and desired states, and adjusts the way this difference is calculated and displayed accordingly. Today, the Pulumi engine get the first chance to decide whether or not there is a difference between a resource's actual and desired states. It does this by comparing the current set of inputs for a resource (i.e. the inputs from the running Pulumi program) with the last set of inputs used to update the resource. If there is no difference between the old and new inputs, the engine decides that no change is necessary without consulting the resource's provider. Only if there are changes does the engine consult the resource's provider for more information about the difference. This can be problematic for a number of reasons: - Not all providers do input-input comparison; some do input-state comparison - Not all providers are able to update the last deployed set of inputs when performing a refresh - Some providers--either intentionally or due to bugs--may see changes in resources whose inputs have not changed All of these situations are confusing at the very least, and the first is problematic with respect to correctness. Furthermore, the display code only renders diffs it observes rather than rendering the diffs observed by the provider, which can obscure the actual changes detected at runtime. These changes address both of these issues: - Rather than comparing the current inputs against the last inputs before calling a resource provider's Diff function, the engine calls the Diff function in all cases. - Providers may now return a list of properties that differ between the requested and actual state and the way in which they differ. This information will then be used by the CLI to render the diff appropriately. A provider may also indicate that a particular diff is between old and new inputs rather than old state and new inputs. Fixes #2453.
2019-07-01 21:34:19 +02:00
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"`
}
Defer all diffs to resource providers. (#2849) Thse changes make a subtle but critical adjustment to the process the Pulumi engine uses to determine whether or not a difference exists between a resource's actual and desired states, and adjusts the way this difference is calculated and displayed accordingly. Today, the Pulumi engine get the first chance to decide whether or not there is a difference between a resource's actual and desired states. It does this by comparing the current set of inputs for a resource (i.e. the inputs from the running Pulumi program) with the last set of inputs used to update the resource. If there is no difference between the old and new inputs, the engine decides that no change is necessary without consulting the resource's provider. Only if there are changes does the engine consult the resource's provider for more information about the difference. This can be problematic for a number of reasons: - Not all providers do input-input comparison; some do input-state comparison - Not all providers are able to update the last deployed set of inputs when performing a refresh - Some providers--either intentionally or due to bugs--may see changes in resources whose inputs have not changed All of these situations are confusing at the very least, and the first is problematic with respect to correctness. Furthermore, the display code only renders diffs it observes rather than rendering the diffs observed by the provider, which can obscure the actual changes detected at runtime. These changes address both of these issues: - Rather than comparing the current inputs against the last inputs before calling a resource provider's Diff function, the engine calls the Diff function in all cases. - Providers may now return a list of properties that differ between the requested and actual state and the way in which they differ. This information will then be used by the CLI to render the diff appropriately. A provider may also indicate that a particular diff is between old and new inputs rather than old state and new inputs. Fixes #2453.
2019-07-01 21:34:19 +02:00
// 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"`
Defer all diffs to resource providers. (#2849) Thse changes make a subtle but critical adjustment to the process the Pulumi engine uses to determine whether or not a difference exists between a resource's actual and desired states, and adjusts the way this difference is calculated and displayed accordingly. Today, the Pulumi engine get the first chance to decide whether or not there is a difference between a resource's actual and desired states. It does this by comparing the current set of inputs for a resource (i.e. the inputs from the running Pulumi program) with the last set of inputs used to update the resource. If there is no difference between the old and new inputs, the engine decides that no change is necessary without consulting the resource's provider. Only if there are changes does the engine consult the resource's provider for more information about the difference. This can be problematic for a number of reasons: - Not all providers do input-input comparison; some do input-state comparison - Not all providers are able to update the last deployed set of inputs when performing a refresh - Some providers--either intentionally or due to bugs--may see changes in resources whose inputs have not changed All of these situations are confusing at the very least, and the first is problematic with respect to correctness. Furthermore, the display code only renders diffs it observes rather than rendering the diffs observed by the provider, which can obscure the actual changes detected at runtime. These changes address both of these issues: - Rather than comparing the current inputs against the last inputs before calling a resource provider's Diff function, the engine calls the Diff function in all cases. - Providers may now return a list of properties that differ between the requested and actual state and the way in which they differ. This information will then be used by the CLI to render the diff appropriately. A provider may also indicate that a particular diff is between old and new inputs rather than old state and new inputs. Fixes #2453.
2019-07-01 21:34:19 +02:00
// 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"`
}