Adds an integration test that runs the following commands on the AWS webserver example, failing if any command returns an error code: * lumijs * lumi env init * lumi config * lumi plan * lumi deploy * lumi destroy * lumi env rm Also ensures that plan and deploy failures propagate errors through to error codes at the CLI.
241 lines
8.8 KiB
Go
241 lines
8.8 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
|
|
}
|
|
deployLatest(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
|
|
}
|
|
|
|
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.
|
|
}
|
|
|
|
func deployLatest(cmd *cobra.Command, info *envCmdInfo, opts deployOptions) error {
|
|
if result := plan(cmd, info, opts); result != nil {
|
|
if opts.DryRun {
|
|
// If a dry run, just print the plan, don't actually carry out the deployment.
|
|
if err := printPlan(result, opts); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Otherwise, we will actually deploy the latest bits.
|
|
var header bytes.Buffer
|
|
printPrelude(&header, result, opts, false)
|
|
header.WriteString(fmt.Sprintf("%vDeploying changes:%v\n", colors.SpecUnimportant, colors.Reset))
|
|
fmt.Print(colors.Colorize(&header))
|
|
|
|
// Create an object to track progress and perform the actual operations.
|
|
start := time.Now()
|
|
progress := newProgress(opts.Summary)
|
|
summary, _, _, err := 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.Print(colors.Colorize(&footer))
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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.Print(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))
|
|
}
|
|
}
|
|
}
|