2017-08-30 03:24:12 +02:00
|
|
|
// Copyright 2017, Pulumi Corporation. All rights reserved.
|
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
package engine
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
2017-12-12 21:31:09 +01:00
|
|
|
"os"
|
2017-10-22 22:39:21 +02:00
|
|
|
"reflect"
|
2017-11-20 20:39:49 +01:00
|
|
|
"regexp"
|
2017-08-23 01:56:15 +02:00
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2017-09-22 04:18:21 +02:00
|
|
|
"github.com/pulumi/pulumi/pkg/diag/colors"
|
|
|
|
"github.com/pulumi/pulumi/pkg/resource"
|
2017-12-05 02:10:40 +01:00
|
|
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
2017-09-22 04:18:21 +02:00
|
|
|
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
|
|
|
"github.com/pulumi/pulumi/pkg/resource/plugin"
|
|
|
|
"github.com/pulumi/pulumi/pkg/tokens"
|
|
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
2017-11-20 20:39:49 +01:00
|
|
|
"github.com/sergi/go-diff/diffmatchpatch"
|
2017-08-23 01:56:15 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan.
|
2018-01-08 23:20:51 +01:00
|
|
|
func plan(info *planContext, opts deployOptions) (*planResult, error) {
|
2017-08-23 01:56:15 +02:00
|
|
|
contract.Assert(info != nil)
|
2018-01-08 22:01:40 +01:00
|
|
|
contract.Assert(info.Update != nil)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2018-01-08 22:01:40 +01:00
|
|
|
// First, load the package metadata and the deployment target in preparation for executing the package's program
|
|
|
|
// and creating resources.
|
|
|
|
pkg, target := info.Update.GetPackage(), info.Update.GetTarget()
|
|
|
|
contract.Assert(pkg != nil)
|
|
|
|
contract.Assert(target != nil)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-11-16 16:49:07 +01:00
|
|
|
// If the package contains an override for the main entrypoint, use it.
|
2018-01-08 22:01:40 +01:00
|
|
|
pkginfo := &Pkginfo{Pkg: pkg, Root: info.Update.GetRoot()}
|
2017-11-16 16:49:07 +01:00
|
|
|
pwd, main, err := pkginfo.GetPwdMain()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-12-12 21:31:09 +01:00
|
|
|
// Create a context for plugins.
|
|
|
|
ctx, err := plugin.NewContext(opts.Diag, nil, pwd, info.TracingSpan)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
// If that succeeded, create a new source that will perform interpretation of the compiled program.
|
2017-09-22 04:18:21 +02:00
|
|
|
// TODO[pulumi/pulumi#88]: we are passing `nil` as the arguments map; we need to allow a way to pass these.
|
2017-08-30 03:24:12 +02:00
|
|
|
source := deploy.NewEvalSource(ctx, &deploy.EvalRunInfo{
|
2017-11-16 16:49:07 +01:00
|
|
|
Pkg: pkginfo.Pkg,
|
|
|
|
Pwd: pwd,
|
|
|
|
Program: main,
|
2018-01-08 22:01:40 +01:00
|
|
|
Target: target,
|
2017-08-30 03:24:12 +02:00
|
|
|
}, opts.Destroy, opts.DryRun)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
// If there are any analyzers in the project file, add them.
|
|
|
|
var analyzers []tokens.QName
|
2017-08-30 03:24:12 +02:00
|
|
|
if as := pkginfo.Pkg.Analyzers; as != nil {
|
2017-08-23 01:56:15 +02:00
|
|
|
for _, a := range *as {
|
|
|
|
analyzers = append(analyzers, a)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append any analyzers from the command line.
|
|
|
|
for _, a := range opts.Analyzers {
|
|
|
|
analyzers = append(analyzers, tokens.QName(a))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a plan; this API handles all interesting cases (create, update, delete).
|
2018-01-10 03:40:08 +01:00
|
|
|
plan := deploy.NewPlan(ctx, target, target.Snapshot, source, analyzers, opts.DryRun)
|
2017-08-23 01:56:15 +02:00
|
|
|
return &planResult{
|
2017-10-05 23:08:46 +02:00
|
|
|
Ctx: ctx,
|
|
|
|
Info: info,
|
|
|
|
Plan: plan,
|
|
|
|
Options: opts,
|
2017-08-23 01:56:15 +02:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type planResult struct {
|
2017-09-17 17:10:46 +02:00
|
|
|
Ctx *plugin.Context // the context containing plugins and their state.
|
2017-10-03 02:27:55 +02:00
|
|
|
Info *planContext // plan command information.
|
2017-09-17 17:10:46 +02:00
|
|
|
Plan *deploy.Plan // the plan created by this command.
|
|
|
|
Options deployOptions // the deployment options.
|
|
|
|
}
|
|
|
|
|
2017-12-12 21:31:09 +01:00
|
|
|
// Chdir changes the directory so that all operations from now on are relative to the project we are working with.
|
|
|
|
// It returns a function that, when run, restores the old working directory.
|
|
|
|
func (res *planResult) Chdir() (func(), error) {
|
|
|
|
if res.Ctx.Pwd == "" {
|
|
|
|
return func() {}, nil
|
|
|
|
}
|
|
|
|
oldpwd, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err = os.Chdir(res.Ctx.Pwd); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "could not change to the project working directory")
|
|
|
|
}
|
|
|
|
return func() {
|
|
|
|
// Restore the working directory after planning completes.
|
|
|
|
cderr := os.Chdir(oldpwd)
|
|
|
|
contract.IgnoreError(cderr)
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2017-10-02 23:26:51 +02:00
|
|
|
// Walk enumerates all steps in the plan, calling out to the provided action at each step. It returns four things: the
|
|
|
|
// resulting Snapshot, no matter whether an error occurs or not; an error, if something went wrong; the step that
|
|
|
|
// failed, if the error is non-nil; and finally the state of the resource modified in the failing step.
|
Bring back component outputs
This change brings back component outputs to the overall system again.
In doing so, it generally overhauls the way we do resource RPCs a bit:
* Instead of RegisterResource and CompleteResource, we call these
BeginRegisterResource and EndRegisterResource, which begins to model
these as effectively "asynchronous" resource requests. This should also
help with parallelism (https://github.com/pulumi/pulumi/issues/106).
* Flip the CLI/engine a little on its head. Rather than it driving the
planning and deployment process, we move more to a model where it
simply observes it. This is done by implementing an event handler
interface with three events: OnResourceStepPre, OnResourceStepPost,
and OnResourceComplete. The first two are invoked immediately before
and after any step operation, and the latter is invoked whenever a
EndRegisterResource comes in. The reason for the asymmetry here is
that the checkpointing logic in the deployment engine is largely
untouched (intentionally, as this is a sensitive part of the system),
and so the "begin"/"end" nature doesn't flow through faithfully.
* Also make the engine more event-oriented in its terminology and the
way it handles the incoming BeginRegisterResource and
EndRegisterResource events from the language host. This is the first
step down a long road of incrementally refactoring the engine to work
this way, a necessary prerequisite for parallelism.
2017-11-29 16:42:14 +01:00
|
|
|
func (res *planResult) Walk(events deploy.Events, preview bool) (deploy.PlanSummary,
|
|
|
|
deploy.Step, resource.Status, error) {
|
2017-09-17 17:10:46 +02:00
|
|
|
opts := deploy.Options{
|
Bring back component outputs
This change brings back component outputs to the overall system again.
In doing so, it generally overhauls the way we do resource RPCs a bit:
* Instead of RegisterResource and CompleteResource, we call these
BeginRegisterResource and EndRegisterResource, which begins to model
these as effectively "asynchronous" resource requests. This should also
help with parallelism (https://github.com/pulumi/pulumi/issues/106).
* Flip the CLI/engine a little on its head. Rather than it driving the
planning and deployment process, we move more to a model where it
simply observes it. This is done by implementing an event handler
interface with three events: OnResourceStepPre, OnResourceStepPost,
and OnResourceComplete. The first two are invoked immediately before
and after any step operation, and the latter is invoked whenever a
EndRegisterResource comes in. The reason for the asymmetry here is
that the checkpointing logic in the deployment engine is largely
untouched (intentionally, as this is a sensitive part of the system),
and so the "begin"/"end" nature doesn't flow through faithfully.
* Also make the engine more event-oriented in its terminology and the
way it handles the incoming BeginRegisterResource and
EndRegisterResource events from the language host. This is the first
step down a long road of incrementally refactoring the engine to work
this way, a necessary prerequisite for parallelism.
2017-11-29 16:42:14 +01:00
|
|
|
Events: events,
|
2017-09-17 17:10:46 +02:00
|
|
|
Parallel: res.Options.Parallel,
|
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-10-02 23:26:51 +02:00
|
|
|
// Fetch a plan iterator and keep walking it until we are done.
|
|
|
|
iter, err := res.Plan.Start(opts)
|
2017-08-23 01:56:15 +02:00
|
|
|
if err != nil {
|
2017-10-02 23:26:51 +02:00
|
|
|
return nil, nil, resource.StatusOK, err
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
step, err := iter.Next()
|
|
|
|
if err != nil {
|
Bring back component outputs
This change brings back component outputs to the overall system again.
In doing so, it generally overhauls the way we do resource RPCs a bit:
* Instead of RegisterResource and CompleteResource, we call these
BeginRegisterResource and EndRegisterResource, which begins to model
these as effectively "asynchronous" resource requests. This should also
help with parallelism (https://github.com/pulumi/pulumi/issues/106).
* Flip the CLI/engine a little on its head. Rather than it driving the
planning and deployment process, we move more to a model where it
simply observes it. This is done by implementing an event handler
interface with three events: OnResourceStepPre, OnResourceStepPost,
and OnResourceComplete. The first two are invoked immediately before
and after any step operation, and the latter is invoked whenever a
EndRegisterResource comes in. The reason for the asymmetry here is
that the checkpointing logic in the deployment engine is largely
untouched (intentionally, as this is a sensitive part of the system),
and so the "begin"/"end" nature doesn't flow through faithfully.
* Also make the engine more event-oriented in its terminology and the
way it handles the incoming BeginRegisterResource and
EndRegisterResource events from the language host. This is the first
step down a long road of incrementally refactoring the engine to work
this way, a necessary prerequisite for parallelism.
2017-11-29 16:42:14 +01:00
|
|
|
closeerr := iter.Close() // ignore close errors; the Next error trumps
|
|
|
|
contract.IgnoreError(closeerr)
|
2017-10-02 23:26:51 +02:00
|
|
|
return nil, nil, resource.StatusOK, err
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for step != nil {
|
2017-10-03 19:27:59 +02:00
|
|
|
// Perform any per-step actions.
|
Bring back component outputs
This change brings back component outputs to the overall system again.
In doing so, it generally overhauls the way we do resource RPCs a bit:
* Instead of RegisterResource and CompleteResource, we call these
BeginRegisterResource and EndRegisterResource, which begins to model
these as effectively "asynchronous" resource requests. This should also
help with parallelism (https://github.com/pulumi/pulumi/issues/106).
* Flip the CLI/engine a little on its head. Rather than it driving the
planning and deployment process, we move more to a model where it
simply observes it. This is done by implementing an event handler
interface with three events: OnResourceStepPre, OnResourceStepPost,
and OnResourceComplete. The first two are invoked immediately before
and after any step operation, and the latter is invoked whenever a
EndRegisterResource comes in. The reason for the asymmetry here is
that the checkpointing logic in the deployment engine is largely
untouched (intentionally, as this is a sensitive part of the system),
and so the "begin"/"end" nature doesn't flow through faithfully.
* Also make the engine more event-oriented in its terminology and the
way it handles the incoming BeginRegisterResource and
EndRegisterResource events from the language host. This is the first
step down a long road of incrementally refactoring the engine to work
this way, a necessary prerequisite for parallelism.
2017-11-29 16:42:14 +01:00
|
|
|
rst, err := iter.Apply(step, preview)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-10-02 23:26:51 +02:00
|
|
|
// If an error occurred, exit early.
|
|
|
|
if err != nil {
|
Bring back component outputs
This change brings back component outputs to the overall system again.
In doing so, it generally overhauls the way we do resource RPCs a bit:
* Instead of RegisterResource and CompleteResource, we call these
BeginRegisterResource and EndRegisterResource, which begins to model
these as effectively "asynchronous" resource requests. This should also
help with parallelism (https://github.com/pulumi/pulumi/issues/106).
* Flip the CLI/engine a little on its head. Rather than it driving the
planning and deployment process, we move more to a model where it
simply observes it. This is done by implementing an event handler
interface with three events: OnResourceStepPre, OnResourceStepPost,
and OnResourceComplete. The first two are invoked immediately before
and after any step operation, and the latter is invoked whenever a
EndRegisterResource comes in. The reason for the asymmetry here is
that the checkpointing logic in the deployment engine is largely
untouched (intentionally, as this is a sensitive part of the system),
and so the "begin"/"end" nature doesn't flow through faithfully.
* Also make the engine more event-oriented in its terminology and the
way it handles the incoming BeginRegisterResource and
EndRegisterResource events from the language host. This is the first
step down a long road of incrementally refactoring the engine to work
this way, a necessary prerequisite for parallelism.
2017-11-29 16:42:14 +01:00
|
|
|
closeerr := iter.Close() // ignore close errors; the action error trumps
|
|
|
|
contract.IgnoreError(closeerr)
|
2017-10-02 23:26:51 +02:00
|
|
|
return iter, step, rst, err
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-10-03 19:27:59 +02:00
|
|
|
contract.Assert(rst == resource.StatusOK)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-10-02 23:26:51 +02:00
|
|
|
step, err = iter.Next()
|
|
|
|
if err != nil {
|
Bring back component outputs
This change brings back component outputs to the overall system again.
In doing so, it generally overhauls the way we do resource RPCs a bit:
* Instead of RegisterResource and CompleteResource, we call these
BeginRegisterResource and EndRegisterResource, which begins to model
these as effectively "asynchronous" resource requests. This should also
help with parallelism (https://github.com/pulumi/pulumi/issues/106).
* Flip the CLI/engine a little on its head. Rather than it driving the
planning and deployment process, we move more to a model where it
simply observes it. This is done by implementing an event handler
interface with three events: OnResourceStepPre, OnResourceStepPost,
and OnResourceComplete. The first two are invoked immediately before
and after any step operation, and the latter is invoked whenever a
EndRegisterResource comes in. The reason for the asymmetry here is
that the checkpointing logic in the deployment engine is largely
untouched (intentionally, as this is a sensitive part of the system),
and so the "begin"/"end" nature doesn't flow through faithfully.
* Also make the engine more event-oriented in its terminology and the
way it handles the incoming BeginRegisterResource and
EndRegisterResource events from the language host. This is the first
step down a long road of incrementally refactoring the engine to work
this way, a necessary prerequisite for parallelism.
2017-11-29 16:42:14 +01:00
|
|
|
closeerr := iter.Close() // ignore close errors; the action error trumps
|
|
|
|
contract.IgnoreError(closeerr)
|
2017-10-02 23:26:51 +02:00
|
|
|
return iter, step, resource.StatusOK, err
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-10-02 23:26:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, return a summary and the resulting plan information.
|
|
|
|
return iter, nil, resource.StatusOK, iter.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (res *planResult) Close() error {
|
|
|
|
return res.Ctx.Close()
|
|
|
|
}
|
|
|
|
|
2018-01-20 21:15:28 +01:00
|
|
|
// printPlan prints the plan's result to the plan's Options.Events stream.
|
2018-01-25 03:22:41 +01:00
|
|
|
func printPlan(result *planResult) (ResourceChanges, error) {
|
2017-10-02 23:26:51 +02:00
|
|
|
// First print config/unchanged/etc. if necessary.
|
|
|
|
var prelude bytes.Buffer
|
|
|
|
printPrelude(&prelude, result, true)
|
|
|
|
|
|
|
|
// Now walk the plan's steps and and pretty-print them out.
|
|
|
|
prelude.WriteString(fmt.Sprintf("%vPreviewing changes:%v\n", colors.SpecUnimportant, colors.Reset))
|
2017-12-14 20:53:02 +01:00
|
|
|
result.Options.Events <- stdOutEventWithColor(&prelude, result.Options.Color)
|
2017-10-02 23:26:51 +02:00
|
|
|
|
2017-11-17 03:21:41 +01:00
|
|
|
actions := newPreviewActions(result.Options)
|
Bring back component outputs
This change brings back component outputs to the overall system again.
In doing so, it generally overhauls the way we do resource RPCs a bit:
* Instead of RegisterResource and CompleteResource, we call these
BeginRegisterResource and EndRegisterResource, which begins to model
these as effectively "asynchronous" resource requests. This should also
help with parallelism (https://github.com/pulumi/pulumi/issues/106).
* Flip the CLI/engine a little on its head. Rather than it driving the
planning and deployment process, we move more to a model where it
simply observes it. This is done by implementing an event handler
interface with three events: OnResourceStepPre, OnResourceStepPost,
and OnResourceComplete. The first two are invoked immediately before
and after any step operation, and the latter is invoked whenever a
EndRegisterResource comes in. The reason for the asymmetry here is
that the checkpointing logic in the deployment engine is largely
untouched (intentionally, as this is a sensitive part of the system),
and so the "begin"/"end" nature doesn't flow through faithfully.
* Also make the engine more event-oriented in its terminology and the
way it handles the incoming BeginRegisterResource and
EndRegisterResource events from the language host. This is the first
step down a long road of incrementally refactoring the engine to work
this way, a necessary prerequisite for parallelism.
2017-11-29 16:42:14 +01:00
|
|
|
_, _, _, err := result.Walk(actions, true)
|
2017-10-02 23:26:51 +02:00
|
|
|
if err != nil {
|
2018-01-25 03:22:41 +01:00
|
|
|
return nil, errors.Errorf("An error occurred while advancing the preview: %v", err)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
2017-10-05 23:08:46 +02:00
|
|
|
if !result.Options.Diag.Success() {
|
Improve output formatting
This change improves our output formatting by generally adding
fewer prefixes. As shown in pulumi/pulumi#359, we were being
excessively verbose in many places, including prefixing every
console.out with "langhost[nodejs].stdout: ", displaying full
stack traces for simple errors like missing configuration, etc.
Overall, this change includes the following:
* Don't prefix stdout and stderr output from the program, other
than the standard "info:" prefix. I experimented with various
schemes here, but they all felt gratuitous. Simply emitting
the output seems fine, especially as it's closer to what would
happen if you just ran the program under node.
* Do NOT make writes to stderr fail the plan/deploy. Previously
we assumed that any console.errors, for instance, meant that
the overall program should fail. This simply isn't how stderr
is treated generally and meant you couldn't use certain
logging techniques and libraries, among other things.
* Do make sure that stderr writes in the program end up going to
stderr in the Pulumi CLI output, however, so that redirection
works as it should. This required a new Infoerr log level.
* Make a small fix to the planning logic so we don't attempt to
print the summary if an error occurs.
* Finally, add a new error type, RunError, that when thrown and
uncaught does not result in a full stack trace being printed.
Anyone can use this, however, we currently use it for config
errors so that we can terminate with a pretty error message,
rather than the monstrosity shown in pulumi/pulumi#359.
2017-09-23 14:20:11 +02:00
|
|
|
// If any error occurred while walking the plan, be sure to let the developer know. Otherwise,
|
|
|
|
// although error messages may have spewed to the output, the final lines of the plan may look fine.
|
2018-01-25 03:22:41 +01:00
|
|
|
return nil, errors.New("One or more errors occurred during this preview")
|
Improve output formatting
This change improves our output formatting by generally adding
fewer prefixes. As shown in pulumi/pulumi#359, we were being
excessively verbose in many places, including prefixing every
console.out with "langhost[nodejs].stdout: ", displaying full
stack traces for simple errors like missing configuration, etc.
Overall, this change includes the following:
* Don't prefix stdout and stderr output from the program, other
than the standard "info:" prefix. I experimented with various
schemes here, but they all felt gratuitous. Simply emitting
the output seems fine, especially as it's closer to what would
happen if you just ran the program under node.
* Do NOT make writes to stderr fail the plan/deploy. Previously
we assumed that any console.errors, for instance, meant that
the overall program should fail. This simply isn't how stderr
is treated generally and meant you couldn't use certain
logging techniques and libraries, among other things.
* Do make sure that stderr writes in the program end up going to
stderr in the Pulumi CLI output, however, so that redirection
works as it should. This required a new Infoerr log level.
* Make a small fix to the planning logic so we don't attempt to
print the summary if an error occurs.
* Finally, add a new error type, RunError, that when thrown and
uncaught does not result in a full stack trace being printed.
Anyone can use this, however, we currently use it for config
errors so that we can terminate with a pretty error message,
rather than the monstrosity shown in pulumi/pulumi#359.
2017-09-23 14:20:11 +02:00
|
|
|
}
|
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
// Print a summary of operation counts.
|
2017-12-12 21:30:40 +01:00
|
|
|
var summary bytes.Buffer
|
2018-01-25 03:22:41 +01:00
|
|
|
changes := ResourceChanges(actions.Ops)
|
|
|
|
printChangeSummary(&summary, changes, true)
|
2017-12-14 20:53:02 +01:00
|
|
|
result.Options.Events <- stdOutEventWithColor(&summary, result.Options.Color)
|
2018-01-25 03:22:41 +01:00
|
|
|
return changes, nil
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// shouldShow returns true if a step should show in the output.
|
2017-11-17 03:21:41 +01:00
|
|
|
func shouldShow(seen map[resource.URN]deploy.Step, step deploy.Step, opts deployOptions) bool {
|
|
|
|
// Ensure we've marked this step as observed.
|
|
|
|
seen[step.URN()] = step
|
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
// For certain operations, whether they are tracked is controlled by flags (to cut down on superfluous output).
|
2017-08-30 03:24:12 +02:00
|
|
|
if step.Op() == deploy.OpSame {
|
Implement resource protection (#751)
This change implements resource protection, as per pulumi/pulumi#689.
The overall idea is that a resource can be marked as "protect: true",
which will prevent deletion of that resource for any reason whatsoever
(straight deletion, replacement, etc). This is expressed in the
program. To "unprotect" a resource, one must perform an update setting
"protect: false", and then afterwards, they can delete the resource.
For example:
let res = new MyResource("precious", { .. }, { protect: true });
Afterwards, the resource will display in the CLI with a lock icon, and
any attempts to remove it will fail in the usual ways (in planning or,
worst case, during an actual update).
This was done by adding a new ResourceOptions bag parameter to the
base Resource types. This is unfortunately a breaking change, but now
is the right time to take this one. We had been adding new settings
one by one -- like parent and dependsOn -- and this new approach will
set us up to add any number of additional settings down the road,
without needing to worry about breaking anything ever again.
This is related to protected stacks, as described in
pulumi/pulumi-service#399. Most likely this will serve as a foundational
building block that enables the coarser grained policy management.
2017-12-20 23:31:07 +01:00
|
|
|
// If the op is the same, it is possible that the resource's metadata changed. In that case, still show it.
|
|
|
|
if step.Old().Protect != step.New().Protect {
|
|
|
|
return true
|
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
return opts.ShowSames
|
|
|
|
} else if step.Op() == deploy.OpCreateReplacement || step.Op() == deploy.OpDeleteReplaced {
|
|
|
|
return opts.ShowReplacementSteps
|
|
|
|
} else if step.Op() == deploy.OpReplace {
|
|
|
|
return !opts.ShowReplacementSteps
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2017-12-07 22:33:16 +01:00
|
|
|
// isRootStack returns true if the step pertains to the rootmost stack component.
|
|
|
|
func isRootStack(step deploy.Step) bool {
|
|
|
|
return step.URN().Type() == resource.RootStackType
|
|
|
|
}
|
|
|
|
|
2017-09-17 17:10:46 +02:00
|
|
|
func printPrelude(b *bytes.Buffer, result *planResult, planning bool) {
|
2017-08-23 01:56:15 +02:00
|
|
|
// If there are configuration variables, show them.
|
2017-09-17 17:10:46 +02:00
|
|
|
if result.Options.ShowConfig {
|
2018-01-08 22:01:40 +01:00
|
|
|
printConfig(b, result.Info.Update.GetTarget().Config)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-05 02:10:40 +01:00
|
|
|
func printConfig(b *bytes.Buffer, cfg config.Map) {
|
2017-08-23 01:56:15 +02:00
|
|
|
b.WriteString(fmt.Sprintf("%vConfiguration:%v\n", colors.SpecUnimportant, colors.Reset))
|
2017-12-05 02:10:40 +01:00
|
|
|
if cfg != nil {
|
2017-08-30 03:24:12 +02:00
|
|
|
var keys []string
|
2017-12-05 02:10:40 +01:00
|
|
|
for key := range cfg {
|
2017-08-31 23:31:33 +02:00
|
|
|
keys = append(keys, string(key))
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-08-30 03:24:12 +02:00
|
|
|
sort.Strings(keys)
|
|
|
|
for _, key := range keys {
|
2017-12-05 02:10:40 +01:00
|
|
|
v, err := cfg[tokens.ModuleMember(key)].Value(config.NewBlindingDecrypter())
|
2018-01-08 22:12:50 +01:00
|
|
|
contract.AssertNoError(err)
|
2017-12-05 02:10:40 +01:00
|
|
|
b.WriteString(fmt.Sprintf(" %v: %v\n", key, v))
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-20 21:07:03 +01:00
|
|
|
// printChangeSummary writes summary informatiom about the resoures changed to the provided buffer.
|
|
|
|
// Returns the total number of resources changed regardless of operation type.
|
|
|
|
func printChangeSummary(b *bytes.Buffer, changes ResourceChanges, preview bool) int {
|
|
|
|
changeCount := 0
|
|
|
|
for op, c := range changes {
|
2017-08-23 01:56:15 +02:00
|
|
|
if op != deploy.OpSame {
|
2018-01-20 21:07:03 +01:00
|
|
|
changeCount += c
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
var kind string
|
2017-09-23 02:23:40 +02:00
|
|
|
if preview {
|
|
|
|
kind = "previewed"
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-09-23 02:23:40 +02:00
|
|
|
kind = "performed"
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var changesLabel string
|
2018-01-20 21:07:03 +01:00
|
|
|
if changeCount == 0 {
|
2017-08-23 01:56:15 +02:00
|
|
|
kind = "required"
|
|
|
|
changesLabel = "no"
|
|
|
|
} else {
|
2018-01-20 21:07:03 +01:00
|
|
|
changesLabel = strconv.Itoa(changeCount)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
2018-01-20 21:07:03 +01:00
|
|
|
if changeCount > 0 || changes[deploy.OpSame] > 0 {
|
2017-11-19 17:07:59 +01:00
|
|
|
kind += ":"
|
|
|
|
}
|
|
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("%vinfo%v: %v %v %v\n",
|
2018-01-20 21:07:03 +01:00
|
|
|
colors.SpecInfo, colors.Reset, changesLabel, plural("change", changeCount), kind))
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
var planTo string
|
2017-09-23 02:23:40 +02:00
|
|
|
if preview {
|
2017-08-23 01:56:15 +02:00
|
|
|
planTo = "to "
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now summarize all of the changes; we print sames a little differently.
|
|
|
|
for _, op := range deploy.StepOps {
|
|
|
|
if op != deploy.OpSame {
|
2018-01-20 21:07:03 +01:00
|
|
|
if c := changes[op]; c > 0 {
|
2017-09-28 01:58:34 +02:00
|
|
|
opDescription := string(op)
|
|
|
|
if !preview {
|
|
|
|
opDescription = op.PastTense()
|
|
|
|
}
|
|
|
|
b.WriteString(fmt.Sprintf(" %v%v %v %v%v%v\n",
|
|
|
|
op.Prefix(), c, plural("resource", c), planTo, opDescription, colors.Reset))
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-01-20 21:07:03 +01:00
|
|
|
if c := changes[deploy.OpSame]; c > 0 {
|
2017-08-23 01:56:15 +02:00
|
|
|
b.WriteString(fmt.Sprintf(" %v %v unchanged\n", c, plural("resource", c)))
|
|
|
|
}
|
|
|
|
|
2018-01-20 21:07:03 +01:00
|
|
|
return changeCount
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func plural(s string, c int) string {
|
|
|
|
if c != 1 {
|
|
|
|
s += "s"
|
|
|
|
}
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
// stepParentIndent computes a step's parent indentation. If print is true, it also prints parents as it goes.
|
|
|
|
func stepParentIndent(b *bytes.Buffer, step deploy.Step,
|
|
|
|
seen map[resource.URN]deploy.Step, shown map[resource.URN]bool, planning bool, indent int, print bool) int {
|
2017-11-17 03:21:41 +01:00
|
|
|
for p := step.Res().Parent; p != ""; {
|
|
|
|
par := seen[p]
|
|
|
|
if par == nil {
|
|
|
|
// 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
|
|
|
|
}
|
2017-11-26 00:54:01 +01:00
|
|
|
if print && !shown[p] {
|
2017-11-17 03:21:41 +01:00
|
|
|
// If the parent isn't yet shown, print it now as a summary.
|
|
|
|
printStep(b, par, seen, shown, true, false, planning, indent)
|
|
|
|
}
|
|
|
|
indent++
|
|
|
|
p = par.Res().Parent
|
|
|
|
}
|
2017-11-26 00:54:01 +01:00
|
|
|
return indent
|
|
|
|
}
|
|
|
|
|
|
|
|
func printStep(b *bytes.Buffer, step deploy.Step, seen map[resource.URN]deploy.Step, shown map[resource.URN]bool,
|
|
|
|
summary bool, detailed bool, planning bool, indent int) {
|
|
|
|
op := step.Op()
|
|
|
|
|
|
|
|
// First, indent to the same level as this resource has parents, and toggle the level of detail accordingly.
|
|
|
|
// TODO[pulumi/pulumi#340]: this isn't entirely correct. Conventionally, all children are created adjacent to
|
|
|
|
// their parents, so this often does the right thing, but not always. For instance, we can have interleaved
|
|
|
|
// infrastructure that gets emitted in the middle of the flow, making things look like they are parented
|
|
|
|
// incorrectly. The real solution here is to have a more first class way of structuring the output.
|
|
|
|
indent = stepParentIndent(b, step, seen, shown, planning, indent, true)
|
2017-11-17 03:21:41 +01:00
|
|
|
|
|
|
|
// Print the indentation.
|
2017-11-26 00:54:01 +01:00
|
|
|
b.WriteString(getIndentationString(indent, op, false))
|
|
|
|
|
|
|
|
// First, print out the operation's prefix.
|
|
|
|
b.WriteString(op.Prefix())
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
// Next, print the resource type (since it is easy on the eyes and can be quickly identified).
|
|
|
|
printStepHeader(b, step)
|
|
|
|
|
|
|
|
// Next print the resource URN, properties, etc.
|
2017-11-17 03:21:41 +01:00
|
|
|
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()
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-11-17 03:21:41 +01:00
|
|
|
printResourceProperties(b, step.URN(), step.Old(), step.New(), replaces, summary, detailed, planning, indent, op)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-11-17 03:21:41 +01:00
|
|
|
// Reset the color and mark this as shown -- we're done.
|
2017-08-23 01:56:15 +02:00
|
|
|
b.WriteString(colors.Reset)
|
2017-11-17 03:21:41 +01:00
|
|
|
shown[step.URN()] = true
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func printStepHeader(b *bytes.Buffer, step deploy.Step) {
|
Implement resource protection (#751)
This change implements resource protection, as per pulumi/pulumi#689.
The overall idea is that a resource can be marked as "protect: true",
which will prevent deletion of that resource for any reason whatsoever
(straight deletion, replacement, etc). This is expressed in the
program. To "unprotect" a resource, one must perform an update setting
"protect: false", and then afterwards, they can delete the resource.
For example:
let res = new MyResource("precious", { .. }, { protect: true });
Afterwards, the resource will display in the CLI with a lock icon, and
any attempts to remove it will fail in the usual ways (in planning or,
worst case, during an actual update).
This was done by adding a new ResourceOptions bag parameter to the
base Resource types. This is unfortunately a breaking change, but now
is the right time to take this one. We had been adding new settings
one by one -- like parent and dependsOn -- and this new approach will
set us up to add any number of additional settings down the road,
without needing to worry about breaking anything ever again.
This is related to protected stacks, as described in
pulumi/pulumi-service#399. Most likely this will serve as a foundational
building block that enables the coarser grained policy management.
2017-12-20 23:31:07 +01:00
|
|
|
var extra string
|
|
|
|
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 = " 🔓"
|
|
|
|
} else if (new != nil && new.Protect) || (old != nil && old.Protect) {
|
|
|
|
// show a locked symbol, since we are either newly protecting this resource, or retaining protection.
|
|
|
|
extra = " 🔒"
|
|
|
|
}
|
|
|
|
b.WriteString(fmt.Sprintf("%s: (%s)%s\n", string(step.Type()), step.Op(), extra))
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
func getIndentationString(indent int, op deploy.StepOp, prefix bool) string {
|
|
|
|
var result string
|
2017-11-20 20:39:49 +01:00
|
|
|
for i := 0; i < indent; i++ {
|
|
|
|
result += " "
|
|
|
|
}
|
|
|
|
|
2017-11-29 19:06:51 +01:00
|
|
|
if result == "" {
|
|
|
|
contract.Assertf(!prefix, "Expected indention for a prefixed line")
|
2017-11-20 20:39:49 +01:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2017-11-29 19:06:51 +01:00
|
|
|
var rp string
|
|
|
|
if prefix {
|
|
|
|
rp = op.RawPrefix()
|
|
|
|
} else {
|
|
|
|
rp = " "
|
|
|
|
}
|
|
|
|
contract.Assert(len(rp) == 2)
|
|
|
|
contract.Assert(len(result) >= 2)
|
|
|
|
return result[:len(result)-2] + rp
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
2017-11-29 19:06:51 +01:00
|
|
|
func writeWithIndent(b *bytes.Buffer, indent int, op deploy.StepOp, prefix bool, format string, a ...interface{}) {
|
|
|
|
b.WriteString(op.Color())
|
|
|
|
b.WriteString(getIndentationString(indent, op, prefix))
|
2017-11-20 20:39:49 +01:00
|
|
|
b.WriteString(fmt.Sprintf(format, a...))
|
|
|
|
b.WriteString(colors.Reset)
|
|
|
|
}
|
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
func writeWithIndentNoPrefix(b *bytes.Buffer, indent int, op deploy.StepOp, format string, a ...interface{}) {
|
2017-11-29 19:06:51 +01:00
|
|
|
writeWithIndent(b, indent, op, false, format, a...)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func write(b *bytes.Buffer, op deploy.StepOp, format string, a ...interface{}) {
|
2017-11-29 19:06:51 +01:00
|
|
|
writeWithIndentNoPrefix(b, 0, op, format, a...)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func writeVerbatim(b *bytes.Buffer, op deploy.StepOp, value string) {
|
2017-11-29 19:06:51 +01:00
|
|
|
writeWithIndentNoPrefix(b, 0, op, "%s", value)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func printResourceProperties(
|
|
|
|
b *bytes.Buffer, urn resource.URN, old *resource.State, new *resource.State,
|
2017-11-17 03:21:41 +01:00
|
|
|
replaces []resource.PropertyKey, summary bool, detailed bool, planning bool, indent int, op deploy.StepOp) {
|
2017-11-20 20:39:49 +01:00
|
|
|
|
|
|
|
indent++
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
// For these simple properties, print them as 'same' if they're just an update or replace.
|
2017-11-26 00:54:01 +01:00
|
|
|
simplePropOp := considerSameIfNotCreateOrDelete(op)
|
2017-11-20 20:39:49 +01:00
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
// Print out the URN and, if present, the ID, as "pseudo-properties".
|
|
|
|
var id resource.ID
|
2017-08-23 01:56:15 +02:00
|
|
|
if old != nil {
|
|
|
|
id = old.ID
|
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
// Always print the ID and URN.
|
2017-08-23 01:56:15 +02:00
|
|
|
if id != "" {
|
2017-11-26 00:54:01 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, simplePropOp, "[id=%s]\n", string(id))
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
if urn != "" {
|
2017-11-26 00:54:01 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, simplePropOp, "[urn=%s]\n", urn)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
// If not summarizing, print all of the properties associated with this resource.
|
2017-08-23 01:56:15 +02:00
|
|
|
if !summary {
|
|
|
|
if old == nil && new != nil {
|
2017-12-03 01:34:16 +01:00
|
|
|
printObject(b, new.Inputs, planning, indent, op, false)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if new == nil && old != nil {
|
2017-12-03 01:34:16 +01:00
|
|
|
printObject(b, old.Inputs, planning, indent, op, false)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-12-03 01:34:16 +01:00
|
|
|
printOldNewDiffs(b, old.Inputs, new.Inputs, replaces, detailed, planning, indent, op)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func maxKey(keys []resource.PropertyKey) int {
|
|
|
|
maxkey := 0
|
|
|
|
for _, k := range keys {
|
|
|
|
if len(k) > maxkey {
|
|
|
|
maxkey = len(k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return maxkey
|
|
|
|
}
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
func printObject(
|
|
|
|
b *bytes.Buffer, props resource.PropertyMap, planning bool,
|
2017-11-26 00:54:01 +01:00
|
|
|
indent int, op deploy.StepOp, prefix bool) {
|
2017-11-20 20:39:49 +01:00
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
// Compute the maximum with of property keys so we can justify everything.
|
|
|
|
keys := props.StableKeys()
|
|
|
|
maxkey := maxKey(keys)
|
|
|
|
|
|
|
|
// Now print out the values intelligently based on the type.
|
|
|
|
for _, k := range keys {
|
|
|
|
if v := props[k]; shouldPrintPropertyValue(v, planning) {
|
2017-11-26 00:54:01 +01:00
|
|
|
printPropertyTitle(b, string(k), maxkey, indent, op, prefix)
|
|
|
|
printPropertyValue(b, v, planning, indent, op, prefix)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// printResourceOutputProperties 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.
|
2017-11-26 00:54:01 +01:00
|
|
|
func printResourceOutputProperties(b *bytes.Buffer, step deploy.Step,
|
2017-12-16 16:33:58 +01:00
|
|
|
seen map[resource.URN]deploy.Step, shown map[resource.URN]bool, planning bool, indent int) {
|
2017-08-23 01:56:15 +02:00
|
|
|
// Only certain kinds of steps have output properties associated with them.
|
2017-12-07 22:33:16 +01:00
|
|
|
new := step.New()
|
|
|
|
if new == nil || new.Outputs == nil {
|
2017-08-23 01:56:15 +02:00
|
|
|
return
|
|
|
|
}
|
2017-12-07 22:33:16 +01:00
|
|
|
op := considerSameIfNotCreateOrDelete(step.Op())
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
// Now compute the indentation level, in part based on the parents.
|
2017-11-29 19:06:51 +01:00
|
|
|
indent++ // indent for the resource.
|
2017-11-26 00:54:01 +01:00
|
|
|
indent = stepParentIndent(b, step, seen, shown, false, indent, false)
|
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
// First fetch all the relevant property maps that we may consult.
|
2017-12-07 22:33:16 +01:00
|
|
|
ins := new.Inputs
|
|
|
|
outs := new.Outputs
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
// Now sort the keys and enumerate each output property in a deterministic order.
|
|
|
|
firstout := true
|
2017-12-07 22:33:16 +01:00
|
|
|
keys := outs.StableKeys()
|
2017-08-23 01:56:15 +02:00
|
|
|
maxkey := maxKey(keys)
|
|
|
|
for _, k := range keys {
|
2017-12-07 22:33:16 +01:00
|
|
|
out := outs[k]
|
|
|
|
// Print this property if it is printable and either ins doesn't have it or it's different.
|
|
|
|
if shouldPrintPropertyValue(out, true) {
|
2017-08-23 01:56:15 +02:00
|
|
|
var print bool
|
2017-12-07 22:33:16 +01:00
|
|
|
if in, has := ins[k]; has {
|
|
|
|
print = (out.Diff(in) != nil)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-12-07 22:33:16 +01:00
|
|
|
print = true
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if print {
|
|
|
|
if firstout {
|
2017-11-26 00:54:01 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "---outputs:---\n")
|
2017-08-23 01:56:15 +02:00
|
|
|
firstout = false
|
|
|
|
}
|
2017-11-26 00:54:01 +01:00
|
|
|
printPropertyTitle(b, string(k), maxkey, indent, op, false)
|
2017-12-16 16:33:58 +01:00
|
|
|
printPropertyValue(b, out, planning, indent, op, false)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func considerSameIfNotCreateOrDelete(op deploy.StepOp) deploy.StepOp {
|
|
|
|
if op == deploy.OpCreate || op == deploy.OpDelete || op == deploy.OpDeleteReplaced {
|
|
|
|
return op
|
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
return deploy.OpSame
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func shouldPrintPropertyValue(v resource.PropertyValue, outs bool) bool {
|
|
|
|
if v.IsNull() {
|
2017-11-17 03:21:41 +01:00
|
|
|
return false // don't print nulls (they just clutter up the output).
|
|
|
|
}
|
|
|
|
if v.IsString() && v.StringValue() == "" {
|
|
|
|
return false // don't print empty strings either.
|
|
|
|
}
|
|
|
|
if v.IsArray() && len(v.ArrayValue()) == 0 {
|
|
|
|
return false // skip empty arrays, since they are often uninteresting default values.
|
|
|
|
}
|
|
|
|
if v.IsObject() && len(v.ObjectValue()) == 0 {
|
|
|
|
return false // skip objects with no properties, since they are also uninteresting.
|
|
|
|
}
|
|
|
|
if v.IsObject() && len(v.ObjectValue()) == 0 {
|
|
|
|
return false // skip objects with no properties, since they are also uninteresting.
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
if v.IsOutput() && !outs {
|
|
|
|
// also don't show output properties until the outs parameter tells us to.
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
func printPropertyTitle(b *bytes.Buffer, name string, align int, indent int, op deploy.StepOp, prefix bool) {
|
2017-11-29 19:06:51 +01:00
|
|
|
writeWithIndent(b, indent, op, prefix, "%-"+strconv.Itoa(align)+"s: ", name)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
func printPropertyValue(
|
|
|
|
b *bytes.Buffer, v resource.PropertyValue, planning bool,
|
2017-11-26 00:54:01 +01:00
|
|
|
indent int, op deploy.StepOp, prefix bool) {
|
2017-11-20 20:39:49 +01:00
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
if v.IsNull() {
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "<null>")
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if v.IsBool() {
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "%t", v.BoolValue())
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if v.IsNumber() {
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "%v", v.NumberValue())
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if v.IsString() {
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "%q", v.StringValue())
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if v.IsArray() {
|
|
|
|
arr := v.ArrayValue()
|
|
|
|
if len(arr) == 0 {
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "[]")
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "[\n")
|
2017-08-23 01:56:15 +02:00
|
|
|
for i, elem := range arr {
|
2017-11-29 19:06:51 +01:00
|
|
|
writeWithIndent(b, indent, op, prefix, " [%d]: ", i)
|
2017-11-26 00:54:01 +01:00
|
|
|
printPropertyValue(b, elem, planning, indent+1, op, prefix)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-12-18 23:51:23 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "]")
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
} else if v.IsAsset() {
|
|
|
|
a := v.AssetValue()
|
|
|
|
if text, has := a.GetText(); has {
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "asset(text:%s) {\n", shortHash(a.Hash))
|
|
|
|
|
|
|
|
massaged := massageText(text)
|
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
// pretty print the text, line by line, with proper breaks.
|
2017-11-20 20:39:49 +01:00
|
|
|
lines := strings.Split(massaged, "\n")
|
2017-08-23 01:56:15 +02:00
|
|
|
for _, line := range lines {
|
2017-11-26 00:54:01 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, " %s\n", line)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-12-18 23:51:23 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "}")
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if path, has := a.GetPath(); has {
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "asset(file:%s) { %s }", shortHash(a.Hash), path)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
|
|
|
contract.Assert(a.IsURI())
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "asset(uri:%s) { %s }", shortHash(a.Hash), a.URI)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
} else if v.IsArchive() {
|
|
|
|
a := v.ArchiveValue()
|
|
|
|
if assets, has := a.GetAssets(); has {
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "archive(assets:%s) {\n", shortHash(a.Hash))
|
2017-08-23 01:56:15 +02:00
|
|
|
var names []string
|
|
|
|
for name := range assets {
|
|
|
|
names = append(names, name)
|
|
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
2017-11-26 00:54:01 +01:00
|
|
|
printAssetOrArchive(b, assets[name], name, planning, indent, op, prefix)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-12-18 23:51:23 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "}")
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if path, has := a.GetPath(); has {
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "archive(file:%s) { %s }", shortHash(a.Hash), path)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
|
|
|
contract.Assert(a.IsURI())
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "archive(uri:%s) { %v }", shortHash(a.Hash), a.URI)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
} else if v.IsComputed() || v.IsOutput() {
|
2017-12-12 23:32:27 +01:00
|
|
|
// We render computed and output values differently depending on whether or not we are planning or deploying:
|
|
|
|
// in the former case, we display `computed<type>` or `output<type>`; in the former we display `undefined`.
|
|
|
|
// This is because we currently cannot distinguish between user-supplied undefined values and input properties
|
|
|
|
// that are undefined because they were sourced from undefined values in other resources' output properties.
|
|
|
|
// Once we have richer information about the dataflow between resources, we should be able to do a better job
|
|
|
|
// here (pulumi/pulumi#234).
|
2017-12-12 23:19:29 +01:00
|
|
|
if planning {
|
|
|
|
writeVerbatim(b, op, v.TypeString())
|
|
|
|
} else {
|
|
|
|
write(b, op, "undefined")
|
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
|
|
|
contract.Assert(v.IsObject())
|
|
|
|
obj := v.ObjectValue()
|
|
|
|
if len(obj) == 0 {
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "{}")
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "{\n")
|
2017-11-26 00:54:01 +01:00
|
|
|
printObject(b, obj, planning, indent+1, op, prefix)
|
2017-12-18 23:51:23 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "}")
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
func printAssetOrArchive(
|
|
|
|
b *bytes.Buffer, v interface{}, name string, planning bool,
|
2017-11-26 00:54:01 +01:00
|
|
|
indent int, op deploy.StepOp, prefix bool) {
|
2017-11-29 19:06:51 +01:00
|
|
|
writeWithIndent(b, indent, op, prefix, " \"%v\": ", name)
|
2017-11-26 00:54:01 +01:00
|
|
|
printPropertyValue(b, assetOrArchiveToPropertyValue(v), planning, indent+1, op, prefix)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func assetOrArchiveToPropertyValue(v interface{}) resource.PropertyValue {
|
|
|
|
switch t := v.(type) {
|
|
|
|
case *resource.Asset:
|
|
|
|
return resource.NewAssetProperty(t)
|
|
|
|
case *resource.Archive:
|
|
|
|
return resource.NewArchiveProperty(t)
|
|
|
|
default:
|
|
|
|
contract.Failf("Unexpected archive element '%v'", reflect.TypeOf(t))
|
|
|
|
return resource.PropertyValue{V: nil}
|
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
|
2017-10-22 22:39:21 +02:00
|
|
|
func shortHash(hash string) string {
|
|
|
|
if len(hash) > 7 {
|
|
|
|
return hash[:7]
|
|
|
|
}
|
|
|
|
return hash
|
|
|
|
}
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
func printOldNewDiffs(
|
|
|
|
b *bytes.Buffer, olds resource.PropertyMap, news resource.PropertyMap,
|
2017-11-17 03:21:41 +01:00
|
|
|
replaces []resource.PropertyKey, detailed bool, planning bool, indent int, op deploy.StepOp) {
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
// Get the full diff structure between the two, and print it (recursively).
|
|
|
|
if diff := olds.Diff(news); diff != nil {
|
2017-11-17 03:21:41 +01:00
|
|
|
printObjectDiff(b, *diff, detailed, replaces, false, planning, indent)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-11-26 00:54:01 +01:00
|
|
|
printObject(b, news, planning, indent, op, true)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-17 03:21:41 +01:00
|
|
|
func printObjectDiff(b *bytes.Buffer, diff resource.ObjectDiff, detailed bool,
|
|
|
|
replaces []resource.PropertyKey, causedReplace bool, planning bool, indent int) {
|
2017-11-20 20:39:49 +01:00
|
|
|
|
|
|
|
contract.Assert(indent > 0)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
// Compute the maximum with of property keys so we can justify everything.
|
|
|
|
keys := diff.Keys()
|
|
|
|
maxkey := maxKey(keys)
|
|
|
|
|
|
|
|
// If a list of what causes a resource to get replaced exist, create a handy map.
|
|
|
|
var replaceMap map[resource.PropertyKey]bool
|
|
|
|
if len(replaces) > 0 {
|
|
|
|
replaceMap = make(map[resource.PropertyKey]bool)
|
|
|
|
for _, k := range replaces {
|
|
|
|
replaceMap[k] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// To print an object diff, enumerate the keys in stable order, and print each property independently.
|
|
|
|
for _, k := range keys {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc := func(top deploy.StepOp, prefix bool) {
|
|
|
|
printPropertyTitle(b, string(k), maxkey, indent, top, prefix)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
if add, isadd := diff.Adds[k]; isadd {
|
|
|
|
if shouldPrintPropertyValue(add, planning) {
|
2017-11-26 00:54:01 +01:00
|
|
|
printAdd(b, add, titleFunc, true, planning, indent)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
} else if delete, isdelete := diff.Deletes[k]; isdelete {
|
|
|
|
if shouldPrintPropertyValue(delete, planning) {
|
2017-11-26 00:54:01 +01:00
|
|
|
printDelete(b, delete, titleFunc, true, planning, indent)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
} else if update, isupdate := diff.Updates[k]; isupdate {
|
|
|
|
if !causedReplace && replaceMap != nil {
|
|
|
|
causedReplace = replaceMap[k]
|
|
|
|
}
|
2017-11-26 00:54:01 +01:00
|
|
|
printPropertyValueDiff(b, titleFunc, update, detailed, causedReplace, planning, indent)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if same := diff.Sames[k]; shouldPrintPropertyValue(same, planning) {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(deploy.OpSame, false)
|
|
|
|
printPropertyValue(b, diff.Sames[k], planning, indent, deploy.OpSame, false)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-26 00:54:01 +01:00
|
|
|
func printPropertyValueDiff(b *bytes.Buffer, titleFunc func(deploy.StepOp, bool),
|
|
|
|
diff resource.ValueDiff, detailed bool, causedReplace bool, planning bool, indent int) {
|
2017-11-20 20:39:49 +01:00
|
|
|
|
|
|
|
op := deploy.OpUpdate
|
|
|
|
contract.Assert(indent > 0)
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
if diff.Array != nil {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(op, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "[\n")
|
2017-08-23 01:56:15 +02:00
|
|
|
|
|
|
|
a := diff.Array
|
|
|
|
for i := 0; i < a.Len(); i++ {
|
2017-11-26 00:54:01 +01:00
|
|
|
elemTitleFunc := func(eop deploy.StepOp, eprefix bool) {
|
2017-11-29 19:06:51 +01:00
|
|
|
writeWithIndent(b, indent+1, eop, eprefix, "[%d]: ", i)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
if add, isadd := a.Adds[i]; isadd {
|
2017-11-29 20:27:32 +01:00
|
|
|
printAdd(b, add, elemTitleFunc, true, planning, indent+2)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if delete, isdelete := a.Deletes[i]; isdelete {
|
2017-11-29 20:27:32 +01:00
|
|
|
printDelete(b, delete, elemTitleFunc, true, planning, indent+2)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if update, isupdate := a.Updates[i]; isupdate {
|
2017-11-29 20:27:32 +01:00
|
|
|
printPropertyValueDiff(b, elemTitleFunc, update, detailed, causedReplace, planning, indent+2)
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-11-26 00:54:01 +01:00
|
|
|
elemTitleFunc(deploy.OpSame, false)
|
2017-11-29 20:27:32 +01:00
|
|
|
printPropertyValue(b, a.Sames[i], planning, indent+2, deploy.OpSame, false)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
2017-11-29 20:27:32 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "]\n")
|
2017-08-23 01:56:15 +02:00
|
|
|
} else if diff.Object != nil {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(op, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
writeVerbatim(b, op, "{\n")
|
2017-11-17 03:21:41 +01:00
|
|
|
printObjectDiff(b, *diff.Object, detailed, nil, causedReplace, planning, indent+1)
|
2017-11-29 20:27:32 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "}\n")
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-11-20 20:39:49 +01:00
|
|
|
shouldPrintOld := shouldPrintPropertyValue(diff.Old, false)
|
|
|
|
shouldPrintNew := shouldPrintPropertyValue(diff.New, false)
|
|
|
|
|
|
|
|
if diff.Old.IsArchive() &&
|
|
|
|
diff.New.IsArchive() &&
|
|
|
|
!causedReplace &&
|
|
|
|
shouldPrintOld &&
|
|
|
|
shouldPrintNew {
|
2017-11-26 00:54:01 +01:00
|
|
|
printArchiveDiff(b, titleFunc, diff.Old.ArchiveValue(), diff.New.ArchiveValue(), planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-08-23 01:56:15 +02:00
|
|
|
// If we ended up here, the two values either differ by type, or they have different primitive values. We will
|
|
|
|
// simply emit a deletion line followed by an addition line.
|
2017-11-20 20:39:49 +01:00
|
|
|
if shouldPrintOld {
|
2017-11-26 00:54:01 +01:00
|
|
|
printDelete(b, diff.Old, titleFunc, causedReplace, planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
if shouldPrintNew {
|
2017-11-26 00:54:01 +01:00
|
|
|
printAdd(b, diff.New, titleFunc, causedReplace, planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func printDelete(
|
2017-11-26 00:54:01 +01:00
|
|
|
b *bytes.Buffer, v resource.PropertyValue, title func(deploy.StepOp, bool),
|
2017-11-20 20:39:49 +01:00
|
|
|
causedReplace bool, planning bool, indent int) {
|
2017-11-26 00:54:01 +01:00
|
|
|
op := deploy.OpDelete
|
|
|
|
title(op, true)
|
|
|
|
printPropertyValue(b, v, planning, indent, op, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func printAdd(
|
2017-11-26 00:54:01 +01:00
|
|
|
b *bytes.Buffer, v resource.PropertyValue, title func(deploy.StepOp, bool),
|
2017-11-20 20:39:49 +01:00
|
|
|
causedReplace bool, planning bool, indent int) {
|
2017-11-26 00:54:01 +01:00
|
|
|
op := deploy.OpCreate
|
|
|
|
title(op, true)
|
|
|
|
printPropertyValue(b, v, planning, indent, op, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func printArchiveDiff(
|
2017-11-26 00:54:01 +01:00
|
|
|
b *bytes.Buffer, titleFunc func(deploy.StepOp, bool),
|
2017-11-20 20:39:49 +01:00
|
|
|
oldArchive *resource.Archive, newArchive *resource.Archive,
|
|
|
|
planning bool, indent int) {
|
|
|
|
|
|
|
|
// TODO: this could be called recursively from itself. In the recursive case, we might have an
|
|
|
|
// archive that actually hasn't changed. Check for that, and terminate the diff printing.
|
|
|
|
|
|
|
|
op := deploy.OpUpdate
|
|
|
|
|
|
|
|
hashChange := getTextChangeString(shortHash(oldArchive.Hash), shortHash(newArchive.Hash))
|
|
|
|
|
|
|
|
if oldPath, has := oldArchive.GetPath(); has {
|
|
|
|
if newPath, has := newArchive.GetPath(); has {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(op, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "archive(file:%s) { %s }\n", hashChange, getTextChangeString(oldPath, newPath))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else if oldURI, has := oldArchive.GetURI(); has {
|
|
|
|
if newURI, has := newArchive.GetURI(); has {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(op, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "archive(uri:%s) { %s }\n", hashChange, getTextChangeString(oldURI, newURI))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
contract.Assert(oldArchive.IsAssets())
|
|
|
|
oldAssets, _ := oldArchive.GetAssets()
|
|
|
|
|
|
|
|
if newAssets, has := newArchive.GetAssets(); has {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(op, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "archive(assets:%s) {\n", hashChange)
|
|
|
|
printAssetsDiff(b, oldAssets, newAssets, planning, indent+1)
|
2017-12-18 23:51:23 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, deploy.OpUpdate, "}\n")
|
2017-11-20 20:39:49 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Type of archive changed, print this out as an remove and an add.
|
|
|
|
printDelete(
|
|
|
|
b, assetOrArchiveToPropertyValue(oldArchive),
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc, false /*causedReplace*/, planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
printAdd(
|
|
|
|
b, assetOrArchiveToPropertyValue(newArchive),
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc, false /*causedReplace*/, planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func printAssetsDiff(
|
|
|
|
b *bytes.Buffer,
|
|
|
|
oldAssets map[string]interface{}, newAssets map[string]interface{},
|
|
|
|
planning bool, indent int) {
|
|
|
|
|
|
|
|
// Diffing assets proceeds by getting the sorted list of asset names from both the old and
|
|
|
|
// new assets, and then stepwise processing each. For any asset in old that isn't in new,
|
|
|
|
// we print this out as a delete. For any asset in new that isn't in old, we print this out
|
|
|
|
// as an add. For any asset in both we print out of it is unchanged or not. If so, we
|
|
|
|
// recurse on that data to print out how it changed.
|
|
|
|
|
|
|
|
var oldNames []string
|
|
|
|
var newNames []string
|
|
|
|
|
|
|
|
for name := range oldAssets {
|
|
|
|
oldNames = append(oldNames, name)
|
|
|
|
}
|
|
|
|
|
|
|
|
for name := range newAssets {
|
|
|
|
newNames = append(newNames, name)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(oldNames)
|
|
|
|
sort.Strings(newNames)
|
|
|
|
|
|
|
|
i := 0
|
|
|
|
j := 0
|
|
|
|
|
|
|
|
var keys []resource.PropertyKey
|
|
|
|
for _, name := range oldNames {
|
|
|
|
keys = append(keys, "\""+resource.PropertyKey(name)+"\"")
|
|
|
|
}
|
|
|
|
for _, name := range newNames {
|
|
|
|
keys = append(keys, "\""+resource.PropertyKey(name)+"\"")
|
|
|
|
}
|
|
|
|
|
|
|
|
maxkey := maxKey(keys)
|
|
|
|
|
|
|
|
for i < len(oldNames) || j < len(newNames) {
|
|
|
|
deleteOld := false
|
|
|
|
addNew := false
|
|
|
|
if i < len(oldNames) && j < len(newNames) {
|
|
|
|
oldName := oldNames[i]
|
|
|
|
newName := newNames[j]
|
|
|
|
|
|
|
|
if oldName == newName {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc := func(top deploy.StepOp, tprefix bool) {
|
|
|
|
printPropertyTitle(b, "\""+oldName+"\"", maxkey, indent, top, tprefix)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
oldAsset := oldAssets[oldName]
|
|
|
|
newAsset := newAssets[newName]
|
|
|
|
|
|
|
|
switch t := oldAsset.(type) {
|
|
|
|
case *resource.Archive:
|
2017-11-26 00:54:01 +01:00
|
|
|
printArchiveDiff(b, titleFunc, t, newAsset.(*resource.Archive), planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
case *resource.Asset:
|
2017-11-26 00:54:01 +01:00
|
|
|
printAssetDiff(b, titleFunc, t, newAsset.(*resource.Asset), planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
i++
|
|
|
|
j++
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if oldName < newName {
|
|
|
|
deleteOld = true
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-11-20 20:39:49 +01:00
|
|
|
addNew = true
|
|
|
|
}
|
|
|
|
} else if i < len(oldNames) {
|
|
|
|
deleteOld = true
|
|
|
|
} else {
|
|
|
|
addNew = true
|
|
|
|
}
|
|
|
|
|
|
|
|
newIndent := indent + 1
|
|
|
|
if deleteOld {
|
|
|
|
oldName := oldNames[i]
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc := func(top deploy.StepOp, tprefix bool) {
|
|
|
|
printPropertyTitle(b, "\""+oldName+"\"", maxkey, indent, top, tprefix)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
2017-11-26 00:54:01 +01:00
|
|
|
printDelete(b, assetOrArchiveToPropertyValue(oldAssets[oldName]), titleFunc, false, planning, newIndent)
|
2017-11-20 20:39:49 +01:00
|
|
|
i++
|
|
|
|
continue
|
|
|
|
} else {
|
|
|
|
contract.Assert(addNew)
|
|
|
|
newName := newNames[j]
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc := func(top deploy.StepOp, tprefix bool) {
|
|
|
|
printPropertyTitle(b, "\""+newName+"\"", maxkey, indent, top, tprefix)
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-11-26 00:54:01 +01:00
|
|
|
printAdd(b, assetOrArchiveToPropertyValue(newAssets[newName]), titleFunc, false, planning, newIndent)
|
2017-11-20 20:39:49 +01:00
|
|
|
j++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeAssetHeader(asset *resource.Asset) string {
|
|
|
|
var assetType string
|
|
|
|
var contents string
|
|
|
|
|
|
|
|
if path, has := asset.GetPath(); has {
|
|
|
|
assetType = "file"
|
|
|
|
contents = path
|
|
|
|
} else if uri, has := asset.GetURI(); has {
|
|
|
|
assetType = "uri"
|
|
|
|
contents = uri
|
|
|
|
} else {
|
|
|
|
assetType = "text"
|
|
|
|
contents = "..."
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("asset(%s:%s) { %s }\n", assetType, shortHash(asset.Hash), contents)
|
|
|
|
}
|
|
|
|
|
|
|
|
func printAssetDiff(
|
2017-11-26 00:54:01 +01:00
|
|
|
b *bytes.Buffer, titleFunc func(deploy.StepOp, bool),
|
2017-11-20 20:39:49 +01:00
|
|
|
oldAsset *resource.Asset, newAsset *resource.Asset,
|
|
|
|
planning bool, indent int) {
|
|
|
|
|
|
|
|
op := deploy.OpUpdate
|
|
|
|
|
|
|
|
// If the assets aren't changed, just print out: = assetName: type(hash)
|
|
|
|
if oldAsset.Hash == newAsset.Hash {
|
|
|
|
op = deploy.OpSame
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(op, false)
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, makeAssetHeader(oldAsset))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the asset changed, print out: ~ assetName: type(hash->hash) details...
|
|
|
|
|
|
|
|
hashChange := getTextChangeString(shortHash(oldAsset.Hash), shortHash(newAsset.Hash))
|
|
|
|
|
|
|
|
if oldText, has := oldAsset.GetText(); has {
|
|
|
|
if newText, has := newAsset.GetText(); has {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(deploy.OpUpdate, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "asset(text:%s) {\n", hashChange)
|
|
|
|
|
|
|
|
massagedOldText := massageText(oldText)
|
|
|
|
massagedNewText := massageText(newText)
|
|
|
|
|
|
|
|
differ := diffmatchpatch.New()
|
|
|
|
differ.DiffTimeout = 0
|
|
|
|
|
|
|
|
hashed1, hashed2, lineArray := differ.DiffLinesToChars(massagedOldText, massagedNewText)
|
|
|
|
diffs1 := differ.DiffMain(hashed1, hashed2, false)
|
|
|
|
diffs2 := differ.DiffCharsToLines(diffs1, lineArray)
|
|
|
|
|
|
|
|
b.WriteString(diffToPrettyString(diffs2, indent+1))
|
|
|
|
|
2017-12-18 23:51:23 +01:00
|
|
|
writeWithIndentNoPrefix(b, indent, op, "}\n")
|
2017-11-20 20:39:49 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
} else if oldPath, has := oldAsset.GetPath(); has {
|
|
|
|
if newPath, has := newAsset.GetPath(); has {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(deploy.OpUpdate, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "asset(file:%s) { %s }\n", hashChange, getTextChangeString(oldPath, newPath))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
contract.Assert(oldAsset.IsURI())
|
|
|
|
|
|
|
|
oldURI, _ := oldAsset.GetURI()
|
|
|
|
if newURI, has := newAsset.GetURI(); has {
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc(deploy.OpUpdate, true)
|
2017-11-20 20:39:49 +01:00
|
|
|
write(b, op, "asset(uri:%s) { %s }\n", hashChange, getTextChangeString(oldURI, newURI))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Type of asset changed, print this out as an remove and an add.
|
|
|
|
printDelete(
|
|
|
|
b, assetOrArchiveToPropertyValue(oldAsset),
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc, false /*causedReplace*/, planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
printAdd(
|
|
|
|
b, assetOrArchiveToPropertyValue(newAsset),
|
2017-11-26 00:54:01 +01:00
|
|
|
titleFunc, false /*causedReplace*/, planning, indent)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func getTextChangeString(old string, new string) string {
|
|
|
|
if old == new {
|
|
|
|
return old
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s->%s", old, new)
|
|
|
|
}
|
|
|
|
|
2017-12-14 23:40:04 +01:00
|
|
|
var (
|
|
|
|
shaRegexp = regexp.MustCompile("__[a-zA-Z0-9]{40}")
|
2017-12-14 23:55:27 +01:00
|
|
|
withRegexp = regexp.MustCompile(` with\({ .* }\) {`)
|
2017-12-14 23:40:04 +01:00
|
|
|
environmentRegexp = regexp.MustCompile(` }\).apply\(.*\).apply\(this, arguments\);`)
|
2017-12-15 00:41:53 +01:00
|
|
|
preambleRegexp = regexp.MustCompile(
|
|
|
|
`function __shaHash\(\) {\n return \(function\(\) {\n with \(__closure\) {\n\nreturn \(`)
|
|
|
|
postambleRegexp = regexp.MustCompile(
|
|
|
|
`\)\n\n }\n }\).apply\(__environment\).apply\(this, arguments\);\n}`)
|
2017-12-14 23:40:04 +01:00
|
|
|
)
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
// 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) string {
|
|
|
|
|
|
|
|
// Only do this for strings that match our serialized function pattern.
|
2017-12-14 23:40:04 +01:00
|
|
|
if !shaRegexp.MatchString(text) ||
|
|
|
|
!withRegexp.MatchString(text) ||
|
|
|
|
!environmentRegexp.MatchString(text) {
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
return text
|
|
|
|
}
|
|
|
|
|
2017-12-14 23:55:27 +01:00
|
|
|
replaceNewlines := func() {
|
|
|
|
for {
|
|
|
|
newText := strings.Replace(text, "\n\n\n", "\n\n", -1)
|
|
|
|
if len(newText) == len(text) {
|
|
|
|
break
|
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
|
2017-12-14 23:55:27 +01:00
|
|
|
text = newText
|
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
2017-12-14 23:55:27 +01:00
|
|
|
replaceNewlines()
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
text = shaRegexp.ReplaceAllString(text, "__shaHash")
|
2017-12-14 23:40:04 +01:00
|
|
|
text = withRegexp.ReplaceAllString(text, " with (__closure) {")
|
|
|
|
text = environmentRegexp.ReplaceAllString(text, " }).apply(__environment).apply(this, arguments);")
|
2017-11-20 20:39:49 +01:00
|
|
|
|
2017-12-14 23:55:27 +01:00
|
|
|
text = preambleRegexp.ReplaceAllString(text, "")
|
|
|
|
text = postambleRegexp.ReplaceAllString(text, "")
|
|
|
|
|
|
|
|
replaceNewlines()
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
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
|
|
|
|
// those add/removes. Because the unchanged portions may be very large, it only included around 3
|
|
|
|
// lines before/after the change.
|
|
|
|
func diffToPrettyString(diffs []diffmatchpatch.Diff, indent int) string {
|
|
|
|
var buff bytes.Buffer
|
|
|
|
|
|
|
|
writeDiff := func(op deploy.StepOp, text string) {
|
2017-11-30 17:03:32 +01:00
|
|
|
var prefix bool
|
|
|
|
if op == deploy.OpCreate || op == deploy.OpDelete {
|
|
|
|
prefix = true
|
|
|
|
}
|
|
|
|
writeWithIndent(&buff, indent, op, prefix, "%s", text)
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for index, diff := range diffs {
|
|
|
|
text := diff.Text
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
printLines := func(op deploy.StepOp, startInclusive int, endExclusive int) {
|
|
|
|
for i := startInclusive; i < endExclusive; i++ {
|
2017-11-30 17:03:32 +01:00
|
|
|
if strings.TrimSpace(lines[i]) != "" {
|
|
|
|
writeDiff(op, lines[i])
|
|
|
|
buff.WriteString("\n")
|
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch diff.Type {
|
|
|
|
case diffmatchpatch.DiffInsert:
|
|
|
|
printLines(deploy.OpCreate, 0, len(lines))
|
|
|
|
case diffmatchpatch.DiffDelete:
|
|
|
|
printLines(deploy.OpDelete, 0, len(lines))
|
|
|
|
case diffmatchpatch.DiffEqual:
|
|
|
|
var trimmedLines []string
|
|
|
|
for _, line := range lines {
|
|
|
|
if strings.TrimSpace(line) != "" {
|
|
|
|
trimmedLines = append(trimmedLines, line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
lines = trimmedLines
|
|
|
|
|
2017-12-14 23:55:27 +01:00
|
|
|
const contextLines = 2
|
|
|
|
|
2017-11-20 20:39:49 +01:00
|
|
|
// Show the unchanged text in white.
|
|
|
|
if index == 0 {
|
|
|
|
// First chunk of the file.
|
2017-12-14 23:55:27 +01:00
|
|
|
if len(lines) > contextLines+1 {
|
2017-11-20 20:39:49 +01:00
|
|
|
writeDiff(deploy.OpSame, "...\n")
|
2017-12-14 23:55:27 +01:00
|
|
|
printLines(deploy.OpSame, len(lines)-contextLines, len(lines))
|
2017-11-20 20:39:49 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
} else if index == len(diffs)-1 {
|
2017-12-14 23:55:27 +01:00
|
|
|
if len(lines) > contextLines+1 {
|
|
|
|
printLines(deploy.OpSame, 0, contextLines)
|
2017-11-20 20:39:49 +01:00
|
|
|
writeDiff(deploy.OpSame, "...\n")
|
|
|
|
continue
|
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
} else {
|
2017-12-14 23:55:27 +01:00
|
|
|
if len(lines) > (2*contextLines + 1) {
|
|
|
|
printLines(deploy.OpSame, 0, contextLines)
|
2017-11-20 20:39:49 +01:00
|
|
|
writeDiff(deploy.OpSame, "...\n")
|
2017-12-14 23:55:27 +01:00
|
|
|
printLines(deploy.OpSame, len(lines)-contextLines, len(lines))
|
2017-11-20 20:39:49 +01:00
|
|
|
continue
|
|
|
|
}
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
|
|
|
|
printLines(deploy.OpSame, 0, len(lines))
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|
|
|
|
}
|
2017-11-20 20:39:49 +01:00
|
|
|
|
|
|
|
return buff.String()
|
2017-08-23 01:56:15 +02:00
|
|
|
}
|