diff --git a/cmd/cancel.go b/cmd/cancel.go new file mode 100644 index 000000000..2dd922f11 --- /dev/null +++ b/cmd/cancel.go @@ -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 []", + 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 +} diff --git a/cmd/pulumi.go b/cmd/pulumi.go index 7909edfd9..86660daf7 100644 --- a/cmd/pulumi.go +++ b/cmd/pulumi.go @@ -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()) diff --git a/pkg/backend/cloud/backend.go b/pkg/backend/cloud/backend.go index b4f0f6f0e..7808c6192 100644 --- a/pkg/backend/cloud/backend.go +++ b/pkg/backend/cloud/backend.go @@ -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 { diff --git a/pkg/backend/cloud/client/client.go b/pkg/backend/cloud/client/client.go index 4ed71b42f..724bdf261 100644 --- a/pkg/backend/cloud/client/client.go +++ b/pkg/backend/cloud/client/client.go @@ -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{