pulumi/cmd/lumi/plan.go
joeduffy 47e242f9a7 Rearrange some deployment logic
This change prepares for integrating more planning and deployment logic
closer to the runtime itself.  For historical reasons, we ended up with these
in the env.go file which really has nothing to do with deployments anymore.
2017-06-01 08:36:43 -07:00

673 lines
22 KiB
Go

// Licensed to Pulumi Corporation ("Pulumi") under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// Pulumi licenses this file to You 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 main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
goerr "github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/lumi/pkg/compiler"
"github.com/pulumi/lumi/pkg/compiler/core"
"github.com/pulumi/lumi/pkg/compiler/errors"
"github.com/pulumi/lumi/pkg/compiler/symbols"
"github.com/pulumi/lumi/pkg/diag"
"github.com/pulumi/lumi/pkg/diag/colors"
"github.com/pulumi/lumi/pkg/eval/heapstate"
"github.com/pulumi/lumi/pkg/eval/rt"
"github.com/pulumi/lumi/pkg/graph/dotconv"
"github.com/pulumi/lumi/pkg/pack"
"github.com/pulumi/lumi/pkg/resource"
"github.com/pulumi/lumi/pkg/tokens"
"github.com/pulumi/lumi/pkg/util/cmdutil"
"github.com/pulumi/lumi/pkg/util/contract"
)
func newPlanCmd() *cobra.Command {
var analyzers []string
var dotOutput bool
var env string
var showConfig bool
var showReplaceSteps bool
var showUnchanged bool
var summary bool
var output string
var cmd = &cobra.Command{
Use: "plan [<package>] [-- [<args>]]",
Aliases: []string{"dryrun"},
Short: "Show a plan to update, create, and delete an environment's resources",
Long: "Show a plan to update, create, and delete an environment's resources\n" +
"\n" +
"This command displays a plan to update an existing environment whose state is represented by\n" +
"an existing snapshot file. The new desired state is computed by compiling and evaluating an\n" +
"executable package, and extracting all resource allocations from its resulting object graph.\n" +
"This graph is compared against the existing state to determine what operations must take\n" +
"place to achieve the desired state. No changes to the environment will actually take place.\n" +
"\n" +
"By default, the package to execute is loaded from the current directory. Optionally, an\n" +
"explicit path can be provided using the [package] argument.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
info, err := initEnvCmdName(tokens.QName(env), args)
if err != nil {
return err
}
defer info.Close()
deploy(cmd, info, deployOptions{
Delete: false,
DryRun: true,
Analyzers: analyzers,
ShowConfig: showConfig,
ShowReplaceSteps: showReplaceSteps,
ShowUnchanged: showUnchanged,
Summary: summary,
DOT: dotOutput,
Output: output,
})
return nil
}),
}
cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{},
"Run one or more analyzers as part of this deployment")
cmd.PersistentFlags().BoolVar(
&dotOutput, "dot", false,
"Output the plan as a DOT digraph (graph description language)")
cmd.PersistentFlags().StringVarP(
&env, "env", "e", "",
"Choose an environment other than the currently selected one")
cmd.PersistentFlags().BoolVar(
&showConfig, "show-config", false,
"Show configuration keys and variables")
cmd.PersistentFlags().BoolVar(
&showReplaceSteps, "show-replace-steps", false,
"Show detailed resource replacement creates and deletes; normally shows as a single step")
cmd.PersistentFlags().BoolVar(
&showUnchanged, "show-unchanged", false,
"Show resources that needn't be updated because they haven't changed, alongside those that do")
cmd.PersistentFlags().BoolVarP(
&summary, "summary", "s", false,
"Only display summarization of resources and plan operations")
cmd.PersistentFlags().StringVarP(
&output, "output", "o", "",
"Serialize the resulting plan to a file instead of simply printing it")
return cmd
}
// plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan.
func plan(cmd *cobra.Command, info *envCmdInfo, opts deployOptions) *planResult {
// If deleting, there is no need to create a new snapshot; otherwise, we will need to compile the package.
var new resource.Snapshot
var result *compileResult
var analyzers []tokens.QName
if !opts.Delete {
// First, compile; if that yields errors or an empty heap, exit early.
if result = compile(cmd, info.Args, info.Env.Config); result == nil || result.Heap == nil {
return nil
}
// Next, if a DOT output is requested, generate it and quite right now.
// TODO: generate this DOT from the snapshot/diff, not the raw object graph.
if opts.DOT {
// Convert the output to a DOT file.
if err := dotconv.Print(result.Heap.G, os.Stdout); err != nil {
cmdutil.Sink().Errorf(errors.ErrorIO,
goerr.Errorf("failed to write DOT file to output: %v", err))
}
return nil
}
// Create a resource snapshot from the compiled/evaluated object graph.
var err error
new, err = resource.NewGraphSnapshot(
info.Ctx, info.Env.Name, result.Pkg.Tok, result.C.Ctx().Opts.Args, result.Heap, info.Old)
if err != nil {
result.C.Diag().Errorf(errors.ErrorCantCreateSnapshot, err)
return nil
} else if !info.Ctx.Diag.Success() {
return nil
}
// If there are any analyzers to run, queue them up.
for _, a := range opts.Analyzers {
analyzers = append(analyzers, tokens.QName(a)) // from the command line.
}
if as := result.Pkg.Node.Analyzers; as != nil {
for _, a := range *as {
analyzers = append(analyzers, a) // from the project file.
}
}
}
// Generate a plan; this API handles all interesting cases (create, update, delete).
plan, err := resource.NewPlan(info.Ctx, info.Old, new, analyzers)
if err != nil {
result.C.Diag().Errorf(errors.ErrorCantCreateSnapshot, err)
return nil
}
if !info.Ctx.Diag.Success() {
return nil
}
return &planResult{
compileResult: result,
Info: info,
New: new,
Plan: plan,
}
}
type planResult struct {
*compileResult
Info *envCmdInfo // plan command information.
Old resource.Snapshot // the existing snapshot (if any).
New resource.Snapshot // the new snapshot for this plan (if any).
Plan resource.Plan // the plan created by this command.
}
func checkEmpty(d diag.Sink, plan resource.Plan) bool {
// If we are doing an empty update, say so.
if plan.Empty() {
d.Infof(diag.Message("no resources need to be updated"))
return true
}
return false
}
func prepareCompiler(cmd *cobra.Command, args []string) (compiler.Compiler, *pack.Package) {
// If there's a --, we need to separate out the command args from the stack args.
flags := cmd.Flags()
dashdash := flags.ArgsLenAtDash()
var packArgs []string
if dashdash != -1 {
packArgs = args[dashdash:]
args = args[0:dashdash]
}
// Create a compiler options object and map any flags and arguments to settings on it.
opts := core.DefaultOptions()
opts.Args = dashdashArgsToMap(packArgs)
// In the case of an argument, load that specific package and new up a compiler based on its base path.
// Otherwise, use the default workspace and package logic (which consults the current working directory).
var comp compiler.Compiler
var pkg *pack.Package
if len(args) == 0 {
var err error
comp, err = compiler.Newwd(opts)
if err != nil {
// Create a temporary diagnostics sink so that we can issue an error and bail out.
cmdutil.Sink().Errorf(errors.ErrorCantCreateCompiler, err)
}
} else {
fn := args[0]
if pkg = cmdutil.ReadPackageFromArg(fn); pkg != nil {
var err error
if fn == "-" {
comp, err = compiler.Newwd(opts)
} else {
comp, err = compiler.New(filepath.Dir(fn), opts)
}
if err != nil {
cmdutil.Sink().Errorf(errors.ErrorCantReadPackage, fn, err)
}
}
}
return comp, pkg
}
// compile just uses the standard logic to parse arguments, options, and to locate/compile a package. It returns the
// LumiGL graph that is produced, or nil if an error occurred (in which case, we would expect non-0 errors).
func compile(cmd *cobra.Command, args []string, config resource.ConfigMap) *compileResult {
// Prepare the compiler info and, provided it succeeds, perform the compilation.
if comp, pkg := prepareCompiler(cmd, args); comp != nil {
// Create the preexec hook if the config map is non-nil.
var preexec compiler.Preexec
configVars := make(map[tokens.Token]*rt.Object)
if config != nil {
preexec = config.ConfigApplier(configVars)
}
// Now perform the compilation and extract the heap snapshot.
var heap *heapstate.Heap
var pkgsym *symbols.Package
if pkg == nil {
pkgsym, heap = comp.Compile(preexec)
} else {
pkgsym, heap = comp.CompilePackage(pkg, preexec)
}
return &compileResult{
C: comp,
Pkg: pkgsym,
Heap: heap,
ConfigVars: configVars,
}
}
return nil
}
type compileResult struct {
C compiler.Compiler
Pkg *symbols.Package
Heap *heapstate.Heap
ConfigVars map[tokens.Token]*rt.Object
}
// verify creates a compiler, much like compile, but only performs binding and verification on it. If verification
// succeeds, the return value is true; if verification fails, errors will have been output, and the return is false.
func verify(cmd *cobra.Command, args []string) bool {
// Prepare the compiler info and, provided it succeeds, perform the verification.
if comp, pkg := prepareCompiler(cmd, args); comp != nil {
// Now perform the compilation and extract the heap snapshot.
if pkg == nil {
return comp.Verify()
}
return comp.VerifyPackage(pkg)
}
return false
}
func printPlan(d diag.Sink, result *planResult, opts deployOptions) {
// First print config/unchanged/etc. if necessary.
var prelude bytes.Buffer
printPrelude(&prelude, result, opts)
// Now walk the plan's steps and and pretty-print them out.
prelude.WriteString(fmt.Sprintf("%vPlanned changes:%v\n", colors.SpecUnimportant, colors.Reset))
fmt.Printf(colors.Colorize(&prelude))
// Print a nice message if the update is an empty one.
if empty := checkEmpty(d, result.Plan); !empty {
var summary bytes.Buffer
step := result.Plan.Steps()
counts := make(map[resource.StepOp]int)
for step != nil {
op := step.Op()
// Print this step information (resource and all its properties).
// TODO: it would be nice if, in the output, we showed the dependencies a la `git log --graph`.
if opts.ShowReplaceSteps || (op != resource.OpReplaceCreate && op != resource.OpReplaceDelete) {
printStep(&summary, step, opts.Summary, "")
}
counts[step.Op()]++
step = step.Next()
}
// Print a summary of operation counts.
printSummary(&summary, counts, opts.ShowReplaceSteps, true)
fmt.Printf(colors.Colorize(&summary))
}
}
func printPrelude(b *bytes.Buffer, result *planResult, opts deployOptions) {
// If there are configuration variables, show them.
if opts.ShowConfig {
printConfig(b, result.compileResult)
}
// If show-sames was requested, walk the sames and print them.
if opts.ShowUnchanged {
printUnchanged(b, result.Plan, opts.Summary)
}
}
func printConfig(b *bytes.Buffer, result *compileResult) {
b.WriteString(fmt.Sprintf("%vConfiguration:%v\n", colors.SpecUnimportant, colors.Reset))
if result != nil && result.ConfigVars != nil {
var toks []string
for tok := range result.ConfigVars {
toks = append(toks, string(tok))
}
sort.Strings(toks)
for _, tok := range toks {
b.WriteString(fmt.Sprintf("%v%v: %v\n", detailsIndent, tok, result.ConfigVars[tokens.Token(tok)]))
}
}
}
func printSummary(b *bytes.Buffer, counts map[resource.StepOp]int, showReplaceSteps bool, plan bool) {
total := 0
for op, c := range counts {
if !showReplaceSteps && (op == resource.OpReplaceCreate || op == resource.OpReplaceDelete) {
continue // skip counting replacement steps unless explicitly requested.
}
total += c
}
var planned string
if plan {
planned = "planned "
}
var colon string
if total != 0 {
colon = ":"
}
b.WriteString(fmt.Sprintf("%v total %v%v%v\n", total, planned, plural("change", total), colon))
var planTo string
var pastTense string
if plan {
planTo = "to "
} else {
pastTense = "d"
}
for _, op := range resource.StepOps() {
if !showReplaceSteps && (op == resource.OpReplaceCreate || op == resource.OpReplaceDelete) {
// Unless the user requested it, don't show the fine-grained replacement steps; just the logical ones.
continue
}
if c := counts[op]; c > 0 {
b.WriteString(fmt.Sprintf(" %v%v %v %v%v%v%v\n",
op.Prefix(), c, plural("resource", c), planTo, op, pastTense, colors.Reset))
}
}
}
func plural(s string, c int) string {
if c != 1 {
s += "s"
}
return s
}
const detailsIndent = " " // 4 spaces, plus 2 for "+ ", "- ", and " " leaders
func printUnchanged(b *bytes.Buffer, plan resource.Plan, summary bool) {
b.WriteString(fmt.Sprintf("%vUnchanged resources:%v\n", colors.SpecUnimportant, colors.Reset))
for _, res := range plan.Unchanged() {
b.WriteString(" ") // simulate the 2 spaces for +, -, etc.
printResourceHeader(b, res, nil, "")
printResourceProperties(b, res, nil, nil, nil, summary, "")
}
}
func printStep(b *bytes.Buffer, step resource.Step, summary bool, indent string) {
// First print out the operation's prefix.
b.WriteString(step.Op().Prefix())
// Next print the resource URN, properties, etc.
printResourceHeader(b, step.Old(), step.New(), indent)
b.WriteString(step.Op().Suffix())
var replaces []resource.PropertyKey
if step.Old() != nil {
m := step.Old().URN()
replaceMap := step.Plan().Replaces()
replaces = replaceMap[m]
}
printResourceProperties(b, step.Old(), step.New(), step.NewProps(), replaces, summary, indent)
// Finally make sure to reset the color.
b.WriteString(colors.Reset)
}
func printResourceHeader(b *bytes.Buffer, old resource.Resource, new resource.Resource, indent string) {
var t tokens.Type
if old == nil {
t = new.Type()
} else {
t = old.Type()
}
// The primary header is the resource type (since it is easy on the eyes).
b.WriteString(fmt.Sprintf("%s:\n", string(t)))
}
func printResourceProperties(b *bytes.Buffer, old resource.Resource, new resource.Resource,
computed resource.PropertyMap, replaces []resource.PropertyKey, summary bool, indent string) {
indent += detailsIndent
// Print out the URN and, if present, the ID, as "pseudo-properties".
var id resource.ID
var URN resource.URN
if old == nil {
id = new.ID()
URN = new.URN()
} else {
id = old.ID()
URN = old.URN()
}
if id != "" {
b.WriteString(fmt.Sprintf("%s[id=%s]\n", indent, string(id)))
}
b.WriteString(fmt.Sprintf("%s[urn=%s]\n", indent, URN.Name()))
if !summary {
// Print all of the properties associated with this resource.
if old == nil && new != nil {
printObject(b, new.Properties(), indent)
} else if new == nil && old != nil {
printObject(b, old.Properties(), indent)
} else {
contract.Assert(computed != nil) // use computed properties for diffs.
printOldNewDiffs(b, old.Properties(), computed, replaces, indent)
}
}
}
func printObject(b *bytes.Buffer, props resource.PropertyMap, indent string) {
// Compute the maximum with of property keys so we can justify everything.
keys := resource.StablePropertyKeys(props)
maxkey := 0
for _, k := range keys {
if len(k) > maxkey {
maxkey = len(k)
}
}
// Now print out the values intelligently based on the type.
for _, k := range keys {
if v := props[k]; shouldPrintPropertyValue(v) {
printPropertyTitle(b, k, maxkey, indent)
printPropertyValue(b, v, indent)
}
}
}
func shouldPrintPropertyValue(v resource.PropertyValue) bool {
return !v.IsNull() // by default, don't print nulls (they just clutter up the output)
}
func printPropertyTitle(b *bytes.Buffer, k resource.PropertyKey, align int, indent string) {
b.WriteString(fmt.Sprintf("%s%-"+strconv.Itoa(align)+"s: ", indent, k))
}
func printPropertyValue(b *bytes.Buffer, v resource.PropertyValue, indent string) {
if v.IsNull() {
b.WriteString("<null>")
} else if v.IsBool() {
b.WriteString(fmt.Sprintf("%t", v.BoolValue()))
} else if v.IsNumber() {
b.WriteString(fmt.Sprintf("%v", v.NumberValue()))
} else if v.IsString() {
b.WriteString(fmt.Sprintf("%q", v.StringValue()))
} else if v.IsResource() {
b.WriteString(fmt.Sprintf("&%s", v.ResourceValue()))
} else if v.IsArray() {
b.WriteString(fmt.Sprintf("[\n"))
for i, elem := range v.ArrayValue() {
newIndent := printArrayElemHeader(b, i, indent)
printPropertyValue(b, elem, newIndent)
}
b.WriteString(fmt.Sprintf("%s]", indent))
} else if v.IsComputed() || v.IsOutput() {
b.WriteString(v.TypeString())
} else {
contract.Assert(v.IsObject())
b.WriteString("{\n")
printObject(b, v.ObjectValue(), indent+" ")
b.WriteString(fmt.Sprintf("%s}", indent))
}
b.WriteString("\n")
}
func getArrayElemHeader(b *bytes.Buffer, i int, indent string) (string, string) {
prefix := fmt.Sprintf(" %s[%d]: ", indent, i)
return prefix, fmt.Sprintf("%-"+strconv.Itoa(len(prefix))+"s", "")
}
func printArrayElemHeader(b *bytes.Buffer, i int, indent string) string {
prefix, newIndent := getArrayElemHeader(b, i, indent)
b.WriteString(prefix)
return newIndent
}
func printOldNewDiffs(b *bytes.Buffer, olds resource.PropertyMap, news resource.PropertyMap,
replaces []resource.PropertyKey, indent string) {
// Get the full diff structure between the two, and print it (recursively).
if diff := olds.Diff(news); diff != nil {
printObjectDiff(b, *diff, replaces, false, indent)
} else {
printObject(b, news, indent)
}
}
func printObjectDiff(b *bytes.Buffer, diff resource.ObjectDiff,
replaces []resource.PropertyKey, causedReplace bool, indent string) {
contract.Assert(len(indent) > 2)
// Compute the maximum with of property keys so we can justify everything.
keys := diff.Keys()
maxkey := 0
for _, k := range keys {
if len(k) > maxkey {
maxkey = len(k)
}
}
// 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 {
title := func(id string) { printPropertyTitle(b, k, maxkey, id) }
if add, isadd := diff.Adds[k]; isadd {
if shouldPrintPropertyValue(add) {
b.WriteString(colors.SpecAdded)
title(addIndent(indent))
printPropertyValue(b, add, addIndent(indent))
b.WriteString(colors.Reset)
}
} else if delete, isdelete := diff.Deletes[k]; isdelete {
if shouldPrintPropertyValue(delete) {
b.WriteString(colors.SpecDeleted)
title(deleteIndent(indent))
printPropertyValue(b, delete, deleteIndent(indent))
b.WriteString(colors.Reset)
}
} else if update, isupdate := diff.Updates[k]; isupdate {
if !causedReplace && replaceMap != nil {
causedReplace = replaceMap[k]
}
printPropertyValueDiff(b, title, update, causedReplace, indent)
} else if same := diff.Sames[k]; shouldPrintPropertyValue(same) {
title(indent)
printPropertyValue(b, diff.Sames[k], indent)
}
}
}
func printPropertyValueDiff(b *bytes.Buffer, title func(string), diff resource.ValueDiff,
causedReplace bool, indent string) {
contract.Assert(len(indent) > 2)
if diff.Array != nil {
title(indent)
b.WriteString("[\n")
a := diff.Array
for i := 0; i < a.Len(); i++ {
_, newIndent := getArrayElemHeader(b, i, indent)
title := func(id string) { printArrayElemHeader(b, i, id) }
if add, isadd := a.Adds[i]; isadd {
b.WriteString(resource.OpCreate.Color())
title(addIndent(indent))
printPropertyValue(b, add, addIndent(newIndent))
b.WriteString(colors.Reset)
} else if delete, isdelete := a.Deletes[i]; isdelete {
b.WriteString(resource.OpDelete.Color())
title(deleteIndent(indent))
printPropertyValue(b, delete, deleteIndent(newIndent))
b.WriteString(colors.Reset)
} else if update, isupdate := a.Updates[i]; isupdate {
title(indent)
printPropertyValueDiff(b, func(string) {}, update, causedReplace, newIndent)
} else {
title(indent)
printPropertyValue(b, a.Sames[i], newIndent)
}
}
b.WriteString(fmt.Sprintf("%s]\n", indent))
} else if diff.Object != nil {
title(indent)
b.WriteString("{\n")
printObjectDiff(b, *diff.Object, nil, causedReplace, indent+" ")
b.WriteString(fmt.Sprintf("%s}\n", indent))
} else if diff.Old.IsResource() && diff.New.IsResource() && diff.New.ResourceValue().Replacement() {
// If the old and new are both resources, and the new is a replacement, show this in a special way (+-).
b.WriteString(resource.OpReplace.Color())
title(updateIndent(indent))
printPropertyValue(b, diff.Old, updateIndent(indent))
b.WriteString(colors.Reset)
} else {
// 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.
if shouldPrintPropertyValue(diff.Old) {
var color string
if causedReplace {
color = resource.OpDelete.Color() // this property triggered replacement; color as a delete
} else {
color = resource.OpUpdate.Color()
}
b.WriteString(color)
title(deleteIndent(indent))
printPropertyValue(b, diff.Old, deleteIndent(indent))
b.WriteString(colors.Reset)
}
if shouldPrintPropertyValue(diff.New) {
var color string
if causedReplace {
color = resource.OpCreate.Color() // this property triggered replacement; color as a create
} else {
color = resource.OpUpdate.Color()
}
b.WriteString(color)
title(addIndent(indent))
printPropertyValue(b, diff.New, addIndent(indent))
b.WriteString(colors.Reset)
}
}
}
func addIndent(indent string) string { return indent[:len(indent)-2] + "+ " }
func deleteIndent(indent string) string { return indent[:len(indent)-2] + "- " }
func updateIndent(indent string) string { return indent[:len(indent)-2] + "+-" }