Add a pulumi cancel command. (#1230)

This command cancels a stack's currently running update, if any. It can
be used to recover from the scenario in which an update is aborted
without marking the running update as complete. Once an update has been
cancelled, it is likely that the affected stack will need to be repaired
via an pair of export/import commands before future updates can succeed.

This is part of #1077.
This commit is contained in:
Pat Gavlin 2018-04-19 10:09:32 -07:00 committed by GitHub
parent 28806ac9f3
commit d1c547524d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 101 additions and 0 deletions

72
cmd/cancel.go Normal file
View file

@ -0,0 +1,72 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
package cmd
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/backend/cloud"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
)
func newCancelCmd() *cobra.Command {
var yes bool
var cmd = &cobra.Command{
Use: "cancel [<stack-name>]",
Args: cmdutil.MaximumNArgs(1),
Short: "Cancel a stack's currently running update, if any",
Long: "Cancel a stack's currently running, if any.\n" +
"\n" +
"This command cancels the update currently being applied to a stack if any exists.\n" +
"Note that this operation is _very dangerous_, and may leave the stack in an\n" +
"inconsistent state if a resource operation was pending when the update was canceled.\n" +
"\n" +
"After this command completes successfully, the stack will be ready for further\n" +
"updates.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
// Use the stack provided or, if missing, default to the current one.
var stack tokens.QName
if len(args) > 0 {
stack = tokens.QName(args[0])
}
s, err := requireStack(stack, false)
if err != nil {
return err
}
// Ensure that we are targeting the Pulumi cloud.
backend, ok := s.Backend().(cloud.Backend)
if !ok {
return errors.New("the `cancel` command is not supported for local stacks")
}
// Ensure the user really wants to do this.
prompt := fmt.Sprintf("This will irreversably cancel the currently running update for '%s'!", s.Name())
if !yes && !confirmPrompt(prompt, string(s.Name())) {
return errors.New("confirmation declined")
}
// Cancel the update.
if err := backend.CancelCurrentUpdate(s.Name()); err != nil {
return err
}
msg := fmt.Sprintf("%sThe currently running update for '%s' has been canceled!%s", colors.SpecAttention, s.Name(),
colors.Reset)
fmt.Println(colors.ColorizeText(msg))
return nil
}),
}
cmd.PersistentFlags().BoolVar(
&yes, "yes", false,
"Skip confirmation prompts, and proceed with cancellation anyway")
return cmd
}

View file

@ -78,6 +78,7 @@ func NewPulumiCmd() *cobra.Command {
cmd.PersistentFlags().IntVarP(
&verbose, "verbose", "v", 0, "Enable verbose logging (e.g., v=3); anything >3 is very verbose")
cmd.AddCommand(newCancelCmd())
cmd.AddCommand(newConfigCmd())
cmd.AddCommand(newDestroyCmd())
cmd.AddCommand(newHistoryCmd())

View file

@ -119,6 +119,8 @@ type Backend interface {
DownloadPlugin(info workspace.PluginInfo, progress bool) (io.ReadCloser, error)
DownloadTemplate(name string, progress bool) (io.ReadCloser, error)
ListTemplates() ([]workspace.Template, error)
CancelCurrentUpdate(stackName tokens.QName) error
}
type cloudBackend struct {
@ -788,6 +790,27 @@ func (b *cloudBackend) runEngineAction(
return err
}
func (b *cloudBackend) CancelCurrentUpdate(stackName tokens.QName) error {
stackID, err := getCloudStackIdentifier(stackName)
if err != nil {
return err
}
stack, err := b.client.GetStack(stackID)
if err != nil {
return err
}
// Compute the update identifier and attempt to cancel the update.
//
// NOTE: the update kind is not relevant; the same endpoint will work for updates of all kinds.
updateID := client.UpdateIdentifier{
StackIdentifier: stackID,
UpdateKind: client.UpdateKindUpdate,
UpdateID: stack.ActiveUpdate,
}
return b.client.CancelUpdate(updateID)
}
func (b *cloudBackend) GetHistory(stackName tokens.QName) ([]backend.UpdateInfo, error) {
stack, err := getCloudStackIdentifier(stackName)
if err != nil {

View file

@ -430,6 +430,11 @@ func (pc *Client) PatchUpdateCheckpoint(update UpdateIdentifier, deployment *api
return pc.updateRESTCall("PATCH", getUpdatePath(update, "checkpoint"), nil, req, nil, updateAccessToken(token))
}
// CancelUpdate cancels the indicated update.
func (pc *Client) CancelUpdate(update UpdateIdentifier) error {
return pc.restCall("POST", getUpdatePath(update, "cancel"), nil, nil, nil)
}
// CompleteUpdate completes the indicated update with the given status.
func (pc *Client) CompleteUpdate(update UpdateIdentifier, status apitype.UpdateStatus, token string) error {
req := apitype.CompleteUpdateRequest{