This change introduces an OpSame planning step. The reason we need this is so that we can apply the necessary output properties, including the ID, even as we are simply walking the plan (i.e., when we aren't actually performing a deployment). This ensures that the object state evolves as required to let reads of output properties propagate in the ways necessary to reproduce past executions of the program.
244 lines
9 KiB
Go
244 lines
9 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"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/pulumi/lumi/pkg/compiler/errors"
|
|
"github.com/pulumi/lumi/pkg/diag"
|
|
"github.com/pulumi/lumi/pkg/diag/colors"
|
|
"github.com/pulumi/lumi/pkg/resource"
|
|
"github.com/pulumi/lumi/pkg/resource/deploy"
|
|
"github.com/pulumi/lumi/pkg/tokens"
|
|
"github.com/pulumi/lumi/pkg/util/cmdutil"
|
|
"github.com/pulumi/lumi/pkg/util/contract"
|
|
)
|
|
|
|
func newDeployCmd() *cobra.Command {
|
|
var analyzers []string
|
|
var dryRun bool
|
|
var env string
|
|
var showConfig bool
|
|
var showReplaceSteps bool
|
|
var showSames bool
|
|
var summary bool
|
|
var output string
|
|
var cmd = &cobra.Command{
|
|
Use: "deploy [<package>] [-- [<args>]]",
|
|
Aliases: []string{"up", "update"},
|
|
Short: "Deploy resource updates, creations, and deletions to an environment",
|
|
Long: "Deploy resource updates, creations, and deletions to an environment\n" +
|
|
"\n" +
|
|
"This command updates an existing environment whose state is represented by the\n" +
|
|
"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. This command results in a full snapshot of the\n" +
|
|
"environment's new resource state, so that it may be updated incrementally again later.\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
|
|
}
|
|
planAndDeploy(cmd, info, deployOptions{
|
|
Delete: false,
|
|
DryRun: dryRun,
|
|
Analyzers: analyzers,
|
|
ShowConfig: showConfig,
|
|
ShowReplaceSteps: showReplaceSteps,
|
|
ShowSames: showSames,
|
|
Summary: summary,
|
|
Output: output,
|
|
})
|
|
return nil
|
|
}),
|
|
}
|
|
|
|
cmd.PersistentFlags().StringSliceVar(
|
|
&analyzers, "analyzer", []string{},
|
|
"Run one or more analyzers as part of this deployment")
|
|
cmd.PersistentFlags().BoolVarP(
|
|
&dryRun, "dry-run", "n", false,
|
|
"Don't actually update resources, just print out the planned updates (synonym for plan)")
|
|
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(
|
|
&showSames, "show-sames", 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 checkpoint to a specific file, instead of overwriting the existing one")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func planAndDeploy(cmd *cobra.Command, info *envCmdInfo, opts deployOptions) {
|
|
if result := plan(cmd, info, opts); result != nil {
|
|
// Now based on whether a dry run was specified, or not, either print or perform the planned operations.
|
|
if opts.DryRun {
|
|
// If no output file was requested, or "-", print to stdout; else write to that file.
|
|
if opts.Output == "" || opts.Output == "-" {
|
|
printPlan(result, opts)
|
|
} else {
|
|
// FIXME: reenable this.
|
|
//saveEnv(info.Target, result.New, opts.Output, true /*overwrite*/)
|
|
}
|
|
} else {
|
|
// If show unchanged was requested, print them first, along with a header.
|
|
var header bytes.Buffer
|
|
printPrelude(&header, result, opts, false)
|
|
header.WriteString(fmt.Sprintf("%vDeploying changes:%v\n", colors.SpecUnimportant, colors.Reset))
|
|
fmt.Printf(colors.Colorize(&header))
|
|
|
|
// Create an object to track progress and perform the actual operations.
|
|
start := time.Now()
|
|
progress := newProgress(opts.Summary)
|
|
summary, _, _, _ := result.Plan.Apply(progress)
|
|
contract.Assert(summary != nil)
|
|
empty := (summary.Steps() == 0) // if no step is returned, it was empty.
|
|
|
|
// Print a summary.
|
|
var footer bytes.Buffer
|
|
|
|
if empty {
|
|
cmdutil.Diag().Infof(diag.Message("no resources need to be updated"))
|
|
} else {
|
|
// Print out the total number of steps performed (and their kinds), the duration, and any summary info.
|
|
printSummary(&footer, progress.Ops, opts.ShowReplaceSteps, false)
|
|
footer.WriteString(fmt.Sprintf("%vDeployment duration: %v%v\n",
|
|
colors.SpecUnimportant, time.Since(start), colors.Reset))
|
|
}
|
|
|
|
if progress.MaybeCorrupt {
|
|
footer.WriteString(fmt.Sprintf(
|
|
"%vA catastrophic error occurred; resources states may be unknown%v\n",
|
|
colors.SpecAttention, colors.Reset))
|
|
}
|
|
|
|
// Now save the updated snapshot to the specified output file, if any, or the standard location otherwise.
|
|
// Note that if a failure has occurred, the Apply routine above will have returned a safe checkpoint.
|
|
targ := result.Info.Target
|
|
saveEnv(targ, summary.Snap(), opts.Output, true /*overwrite*/)
|
|
|
|
fmt.Printf(colors.Colorize(&footer))
|
|
}
|
|
}
|
|
}
|
|
|
|
type deployOptions struct {
|
|
Create bool // true if we are creating resources.
|
|
Delete bool // true if we are deleting resources.
|
|
DryRun bool // true if we should just print the plan without performing it.
|
|
Analyzers []string // an optional set of analyzers to run as part of this deployment.
|
|
ShowConfig bool // true to show the configuration variables being used.
|
|
ShowReplaceSteps bool // true to show the replacement steps in the plan.
|
|
ShowSames bool // true to show the resources that aren't updated, in addition to those that are.
|
|
Summary bool // true if we should only summarize resources and operations.
|
|
DOT bool // true if we should print the DOT file for this plan.
|
|
Output string // the place to store the output, if any.
|
|
}
|
|
|
|
// deployProgress pretty-prints the plan application process as it goes.
|
|
type deployProgress struct {
|
|
Steps int
|
|
Ops map[deploy.StepOp]int
|
|
MaybeCorrupt bool
|
|
Summary bool
|
|
}
|
|
|
|
func newProgress(summary bool) *deployProgress {
|
|
return &deployProgress{
|
|
Steps: 0,
|
|
Ops: make(map[deploy.StepOp]int),
|
|
Summary: summary,
|
|
}
|
|
}
|
|
|
|
func (prog *deployProgress) Before(step *deploy.Step) {
|
|
stepop := step.Op()
|
|
if stepop == deploy.OpSame {
|
|
return
|
|
}
|
|
|
|
// Print the step.
|
|
stepnum := prog.Steps + 1
|
|
|
|
var extra string
|
|
if stepop == deploy.OpReplaceCreate || stepop == deploy.OpReplaceDelete {
|
|
extra = " (part of a replacement change)"
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
b.WriteString(fmt.Sprintf("Applying step #%v [%v]%v\n", stepnum, stepop, extra))
|
|
printStep(&b, step, prog.Summary, false, "")
|
|
fmt.Printf(colors.Colorize(&b))
|
|
}
|
|
|
|
func (prog *deployProgress) After(step *deploy.Step, status resource.Status, err error) {
|
|
stepop := step.Op()
|
|
if err != nil {
|
|
// Issue a true, bonafide error.
|
|
cmdutil.Diag().Errorf(errors.ErrorPlanApplyFailed, err)
|
|
|
|
// Print the state of the resource; we don't issue the error, because the deploy above will do that.
|
|
var b bytes.Buffer
|
|
stepnum := prog.Steps + 1
|
|
b.WriteString(fmt.Sprintf("Step #%v failed [%v]: ", stepnum, stepop))
|
|
switch status {
|
|
case resource.StatusOK:
|
|
b.WriteString(colors.SpecNote)
|
|
b.WriteString("provider successfully recovered from this failure")
|
|
case resource.StatusUnknown:
|
|
b.WriteString(colors.SpecAttention)
|
|
b.WriteString("this failure was catastrophic and the provider cannot guarantee recovery")
|
|
prog.MaybeCorrupt = true
|
|
default:
|
|
contract.Failf("Unrecognized resource state: %v", status)
|
|
}
|
|
b.WriteString(colors.Reset)
|
|
b.WriteString("\n")
|
|
fmt.Printf(colors.Colorize(&b))
|
|
} else if stepop != deploy.OpSame {
|
|
// Increment the counters.
|
|
prog.Steps++
|
|
prog.Ops[stepop]++
|
|
|
|
// Print out any output properties that got created as a result of this operation.
|
|
if step.Op() == deploy.OpCreate {
|
|
var b bytes.Buffer
|
|
printResourceOutputProperties(&b, step, "")
|
|
fmt.Printf(colors.Colorize(&b))
|
|
}
|
|
}
|
|
}
|