Bring back preview, swizzle some flags

This changes the CLI interface in a few ways:

* `pulumi preview` is back!  The alternative of saying
  `pulumi update --preview` just felt awkward, and it's a common
  operation to want to perform.  Let's just make it work.

* There are two flags consistent across all update commands,
  `update`, `refresh`, and `destroy`:

    - `--skip-preview` will skip the preview step.  Note that this
      does *not* skip the prompt to confirm that you'd like to proceed.
      Indeed, it will still prompt, with a little warning text about
      the fact that the preview has been skipped.

    * `--yes` will auto-approve the updates.

This lands us in a simpler and more intuitive spot for common scenarios.
This commit is contained in:
joeduffy 2018-05-05 11:57:09 -07:00
parent 6ad785d5c4
commit 7c7f6d3ed7
17 changed files with 362 additions and 331 deletions

View file

@ -4,7 +4,6 @@ package cmd
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -25,11 +24,12 @@ func newDestroyCmd() *cobra.Command {
var color colorFlag
var diffDisplay bool
var parallel int
var preview string
var showConfig bool
var showReplacementSteps bool
var showSames bool
var nonInteractive bool
var skipPreview bool
var yes bool
var cmd = &cobra.Command{
Use: "destroy",
@ -45,8 +45,12 @@ func newDestroyCmd() *cobra.Command {
"is generally irreversible and should be used with great care.",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
interactive := isInteractive(cmd)
behavior, err := previewFlagsToBehavior(interactive, preview)
interactive := isInteractive(nonInteractive)
if !interactive {
yes = true // auto-approve changes, since we cannot prompt.
}
opts, err := updateFlagsToOptions(interactive, skipPreview, yes)
if err != nil {
return err
}
@ -65,32 +69,22 @@ func newDestroyCmd() *cobra.Command {
return errors.Wrap(err, "gathering environment metadata")
}
if behavior.Interactive() {
prompt := fmt.Sprintf("This will permanently destroy all resources in the '%s' stack!", s.Name())
if !confirmPrompt(prompt, s.Name().String()) {
return errors.New("confirmation declined")
}
opts.Engine = engine.UpdateOptions{
Analyzers: analyzers,
Parallel: parallel,
Debug: debug,
}
opts.Display = backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
IsInteractive: interactive,
DiffDisplay: diffDisplay,
Debug: debug,
}
err = s.Destroy(
proj, root, m,
engine.UpdateOptions{
Analyzers: analyzers,
Parallel: parallel,
Debug: debug,
},
behavior,
backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
IsInteractive: interactive,
DiffDisplay: diffDisplay,
Debug: debug,
},
cancellationScopes,
)
err = s.Destroy(proj, root, m, opts, cancellationScopes)
if err == context.Canceled {
return errors.New("destroy cancelled")
}
@ -117,12 +111,11 @@ func newDestroyCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().BoolVar(
&nonInteractive, "non-interactive", false, "Disable interactive mode")
cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)")
cmd.PersistentFlags().StringVar(
&preview, "preview", "",
"Preview behavior. Choices are: only (dry-run), skip (no preview, just update), auto (auto-accept)")
cmd.PersistentFlags().BoolVar(
&showConfig, "show-config", false,
"Show configuration keys and variables")
@ -132,7 +125,12 @@ func newDestroyCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&showSames, "show-sames", false,
"Show resources that don't need to be updated because they haven't changed, alongside those that do")
cmd.PersistentFlags().BoolVar(&nonInteractive, "non-interactive", false, "Disable interactive mode")
cmd.PersistentFlags().BoolVar(
&skipPreview, "skip-preview", false,
"Do not perform a preview before performing the destroy")
cmd.PersistentFlags().BoolVar(
&yes, "yes", false,
"Automatically approve and perform the destroy after previewing it")
return cmd
}

View file

@ -3,21 +3,24 @@
package cmd
import (
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
)
func newPreviewCmd() *cobra.Command {
var debug bool
var message string
var stack string
// Flags for engine.UpdateOptions.
var analyzers []string
var color colorFlag
var diffDisplay bool
var nonInteractive bool
var parallel int
var showConfig bool
var showReplacementSteps bool
@ -27,12 +30,52 @@ func newPreviewCmd() *cobra.Command {
Use: "preview",
Aliases: []string{"pre"},
SuggestFor: []string{"build", "plan"},
Short: "Deprecated. Use 'pulumi update --preview' instead",
Long: "Deprecated. Use 'pulumi update --preview' instead",
Args: cmdutil.NoArgs,
Short: "Show a preview of updates to a stack's resources",
Long: "Show a preview of updates a stack's resources.\n" +
"\n" +
"This command displays a preview of the updates to an existing stack whose state is\n" +
"represented by an existing snapshot file. The new desired state is computed by running\n" +
"a Pulumi program, and extracting all resource allocations from its resulting object graph.\n" +
"These allocations are then compared against the existing state to determine what\n" +
"operations must take place to achieve the desired state. No changes to the stack will\n" +
"actually take place.\n" +
"\n" +
"The program to run is loaded from the project in the current directory. Use the `-C` or\n" +
"`--cwd` flag to use a different directory.",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
return errors.New(
"`pulumi preview` is deprecated; `pulumi update` now perform previews by default")
s, err := requireStack(stack, true)
if err != nil {
return err
}
proj, root, err := readProject()
if err != nil {
return err
}
m, err := getUpdateMetadata("", root)
if err != nil {
return errors.Wrap(err, "gathering environment metadata")
}
opts := backend.UpdateOptions{
Engine: engine.UpdateOptions{
Analyzers: analyzers,
Parallel: parallel,
Debug: debug,
},
Display: backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
IsInteractive: isInteractive(nonInteractive),
DiffDisplay: diffDisplay,
Debug: debug,
},
}
return s.Preview(proj, root, m, opts, cancellationScopes)
}),
}
@ -43,6 +86,10 @@ func newPreviewCmd() *cobra.Command {
&stack, "stack", "s", "",
"Choose a stack other than the currently selected one")
cmd.PersistentFlags().StringVarP(
&message, "message", "m", "",
"Optional message to associate with the preview operation")
// Flags for engine.UpdateOptions.
cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{},
@ -52,6 +99,8 @@ func newPreviewCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().BoolVar(
&nonInteractive, "non-interactive", false, "Disable interactive mode")
cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)")

View file

@ -23,11 +23,12 @@ func newRefreshCmd() *cobra.Command {
var color colorFlag
var diffDisplay bool
var parallel int
var preview string
var showConfig bool
var showReplacementSteps bool
var showSames bool
var nonInteractive bool
var skipPreview bool
var yes bool
var cmd = &cobra.Command{
Use: "refresh",
@ -43,8 +44,12 @@ func newRefreshCmd() *cobra.Command {
"`--cwd` flag to use a different directory.",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
interactive := isInteractive(cmd)
behavior, err := previewFlagsToBehavior(interactive, preview)
interactive := isInteractive(nonInteractive)
if !interactive {
yes = true // auto-approve changes, since we cannot prompt.
}
opts, err := updateFlagsToOptions(interactive, skipPreview, yes)
if err != nil {
return err
}
@ -64,25 +69,22 @@ func newRefreshCmd() *cobra.Command {
return errors.Wrap(err, "gathering environment metadata")
}
err = s.Refresh(
proj, root, m,
engine.UpdateOptions{
Analyzers: analyzers,
Parallel: parallel,
Debug: debug,
},
behavior,
backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
IsInteractive: interactive,
DiffDisplay: diffDisplay,
Debug: debug,
},
cancellationScopes,
)
opts.Engine = engine.UpdateOptions{
Analyzers: analyzers,
Parallel: parallel,
Debug: debug,
}
opts.Display = backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
IsInteractive: interactive,
DiffDisplay: diffDisplay,
Debug: debug,
}
err = s.Refresh(proj, root, m, opts, cancellationScopes)
if err == context.Canceled {
return errors.New("refresh cancelled")
}
@ -110,19 +112,23 @@ func newRefreshCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().BoolVar(
&nonInteractive, "non-interactive", false, "Disable interactive mode")
cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)")
cmd.PersistentFlags().StringVar(
&preview, "preview", "",
"Preview behavior. Choices are: only (dry-run), skip (no preview, just update), auto (auto-accept)")
cmd.PersistentFlags().BoolVar(
&showReplacementSteps, "show-replacement-steps", false,
"Show detailed resource replacement creates and deletes instead of 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().BoolVar(&nonInteractive, "non-interactive", false, "Disable interactive mode")
cmd.PersistentFlags().BoolVar(
&skipPreview, "skip-preview", false,
"Do not perform a preview before performing the refresh")
cmd.PersistentFlags().BoolVar(
&yes, "yes", false,
"Automatically approve and perform the refresh after previewing it")
return cmd
}

View file

@ -22,12 +22,13 @@ func newUpdateCmd() *cobra.Command {
var analyzers []string
var color colorFlag
var diffDisplay bool
var nonInteractive bool
var parallel int
var preview string
var showConfig bool
var showReplacementSteps bool
var showSames bool
var nonInteractive bool
var skipPreview bool
var yes bool
var cmd = &cobra.Command{
Use: "update",
@ -47,8 +48,12 @@ func newUpdateCmd() *cobra.Command {
"`--cwd` flag to use a different directory.",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
interactive := isInteractive(cmd)
behavior, err := previewFlagsToBehavior(interactive, preview)
interactive := isInteractive(nonInteractive)
if !interactive {
yes = true // auto-approve changes, since we cannot prompt.
}
opts, err := updateFlagsToOptions(interactive, skipPreview, yes)
if err != nil {
return err
}
@ -68,25 +73,22 @@ func newUpdateCmd() *cobra.Command {
return errors.Wrap(err, "gathering environment metadata")
}
err = s.Update(
proj, root, m,
engine.UpdateOptions{
Analyzers: analyzers,
Parallel: parallel,
Debug: debug,
},
behavior,
backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
IsInteractive: interactive,
DiffDisplay: diffDisplay,
Debug: debug,
},
cancellationScopes,
)
opts.Engine = engine.UpdateOptions{
Analyzers: analyzers,
Parallel: parallel,
Debug: debug,
}
opts.Display = backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
IsInteractive: interactive,
DiffDisplay: diffDisplay,
Debug: debug,
}
err = s.Update(proj, root, m, opts, cancellationScopes)
if err == context.Canceled {
return errors.New("update cancelled")
}
@ -114,9 +116,8 @@ func newUpdateCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().StringVar(
&preview, "preview", "",
"Preview behavior. Choices are: only (dry-run), skip (no preview, just update), auto (auto-accept)")
cmd.PersistentFlags().BoolVar(
&nonInteractive, "non-interactive", false, "Disable interactive mode")
cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)")
@ -129,7 +130,12 @@ func newUpdateCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&showSames, "show-sames", false,
"Show resources that don't need be updated because they haven't changed, alongside those that do")
cmd.PersistentFlags().BoolVar(&nonInteractive, "non-interactive", false, "Disable interactive mode")
cmd.PersistentFlags().BoolVar(
&skipPreview, "skip-preview", false,
"Do not perform a preview before performing the update")
cmd.PersistentFlags().BoolVar(
&yes, "yes", false,
"Automatically approve and perform the update after previewing it")
return cmd
}

View file

@ -17,7 +17,6 @@ import (
"github.com/golang/glog"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
survey "gopkg.in/AlecAivazis/survey.v1"
surveycore "gopkg.in/AlecAivazis/survey.v1/core"
@ -502,25 +501,20 @@ func (cancellationScopeSource) NewScope(events chan<- engine.Event, isPreview bo
// isInteractive returns true if the environment and command line options indicate we should
// do things interactively
func isInteractive(cmd *cobra.Command) bool {
nonInteractive, err := cmd.Flags().GetBool("non-interactive")
contract.IgnoreError(err)
func isInteractive(nonInteractive bool) bool {
return !nonInteractive && terminal.IsTerminal(int(os.Stdout.Fd())) && !isCI()
}
// previewFlagsToBehavior turns the CLI preview flag into a backend behavior enum.
func previewFlagsToBehavior(interactive bool, preview string) (backend.PreviewBehavior, error) {
behavior := backend.PreviewBehavior(preview)
switch behavior {
case backend.DefaultPreview, backend.OnlyPreview, backend.SkipPreview, backend.AutoPreview:
// ok
default:
return "", errors.Errorf("unrecognized preview behavior: '%s'", preview)
// updateFlagsToOptions ensures that the given update flags represent a valid combination. If so, an UpdateOptions
// is returned with a nil-error; otherwise, the non-nil error contains information about why the combination is invalid.
func updateFlagsToOptions(interactive, skipPreview, yes bool) (backend.UpdateOptions, error) {
if !interactive && !yes {
return backend.UpdateOptions{},
errors.New("--yes must be passed in non-interactive mode")
}
if !interactive && behavior.Interactive() {
return "", errors.New("--preview must be only, skip, or auto when run in non-interactive mode")
}
return behavior, nil
return backend.UpdateOptions{
AutoApprove: yes,
SkipPreview: skipPreview,
}, nil
}

View file

@ -47,9 +47,6 @@ func TestExamples(t *testing.T) {
"simple:config:x": "1",
"simple:config:y": "1",
},
Verbose: true,
DebugUpdates: true,
DebugLogLevel: 12,
},
{
Dir: path.Join(cwd, "dynamic-provider/multiple-turns"),
@ -97,7 +94,7 @@ func TestExamples(t *testing.T) {
for _, example := range examples {
ex := example.With(integration.ProgramTestOptions{
ReportStats: integration.NewS3Reporter("us-west-2", "eng.pulumi.com", "testreports"),
Tracing: "https://tracing.pulumi-engineering.com/collector/api/v1/spans",
Tracing: "https://tracing.pulumi-engineering.com/collector/api/v1/spans",
})
t.Run(example.Dir, func(t *testing.T) {
integration.ProgramTest(t, &ex)

View file

@ -52,18 +52,18 @@ type Backend interface {
// GetStackCrypter returns an encrypter/decrypter for the given stack's secret config values.
GetStackCrypter(stackRef StackReference) (config.Crypter, error)
// Preview shows what would be updated given the current workspace's contents.
Preview(stackRef StackReference, proj *workspace.Project, root string,
m UpdateMetadata, opts UpdateOptions, scopes CancellationScopeSource) error
// Update updates the target stack with the current workspace's contents (config and code).
Update(stackRef StackReference, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
m UpdateMetadata, opts UpdateOptions, scopes CancellationScopeSource) error
// Refresh refreshes the stack's state from the cloud provider.
Refresh(stackRef StackReference, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
m UpdateMetadata, opts UpdateOptions, scopes CancellationScopeSource) error
// Destroy destroys all of this stack's resources.
Destroy(stackRef StackReference, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
m UpdateMetadata, opts UpdateOptions, scopes CancellationScopeSource) error
// GetHistory returns all updates for the stack. The returned UpdateInfo slice will be in
// descending order (newest first).
@ -79,44 +79,17 @@ type Backend interface {
Logout() error
}
// PreviewBehavior controls how previews are performed during an update.
type PreviewBehavior string
// UpdateOptions is the full set of update options, including backend and engine options.
type UpdateOptions struct {
// Engine contains all of the engine-specific options.
Engine engine.UpdateOptions
// Display contains all of the backend display options.
Display DisplayOptions
const (
// DefaultPreview is the default behavior of a preview followed by an confirmation-prompt-guarded update.
DefaultPreview PreviewBehavior = ""
// OnlyPreview only runs a preview but does not perform an update.
OnlyPreview PreviewBehavior = "only"
// SkipPreview skips a preview, and prompt, altogether, and simply performs the update.
SkipPreview PreviewBehavior = "skip"
// AutoPreview automatically confirms a preview after performing it.
AutoPreview PreviewBehavior = "auto"
)
// Only returns true if the update only entails a preview operation.
func (preview PreviewBehavior) Only() bool {
return preview == OnlyPreview
}
// Skip returns true if the update should skip the preview altogether.
func (preview PreviewBehavior) Skip() bool {
return preview == SkipPreview
}
// Should returns true if the update options mandate that a preview occur.
func (preview PreviewBehavior) Should() bool {
return !preview.Skip()
}
// Interactive returns true if a preview behavior will require confirmation prompts.
func (preview PreviewBehavior) Interactive() bool {
return preview == DefaultPreview
}
// AutoAccept returns true if a preview's result should be automatically accepted. This can be useful in
// non-interactive situations where you'd like to still preview before performing an apply.
func (preview PreviewBehavior) AutoAccept() bool {
return preview == AutoPreview
// AutoApprove, when true, will automatically approve previews.
AutoApprove bool
// SkipPreview, when true, causes the preview step to be skipped.
SkipPreview bool
}
// CancellationScope provides a scoped source of cancellation and termination requests.

View file

@ -22,6 +22,8 @@ import (
"github.com/golang/glog"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
survey "gopkg.in/AlecAivazis/survey.v1"
surveycore "gopkg.in/AlecAivazis/survey.v1/core"
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/backend"
@ -40,9 +42,6 @@ import (
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/util/retry"
"github.com/pulumi/pulumi/pkg/workspace"
survey "gopkg.in/AlecAivazis/survey.v1"
surveycore "gopkg.in/AlecAivazis/survey.v1/core"
)
const (
@ -482,6 +481,7 @@ var (
previewText string
text string
}{
string(client.UpdateKindPreview): {"update of", "Previewing"},
string(client.UpdateKindUpdate): {"update of", "Updating"},
string(client.UpdateKindRefresh): {"refresh of", "Refreshing"},
string(client.UpdateKindDestroy): {"destroy of", "Destroying"},
@ -543,57 +543,51 @@ func createDiff(events []engine.Event, displayOpts backend.DisplayOptions) strin
func (b *cloudBackend) PreviewThenPrompt(
updateKind client.UpdateKind, stack backend.Stack, pkg *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
m backend.UpdateMetadata, opts backend.UpdateOptions, scopes backend.CancellationScopeSource) (bool, error) {
// create a channel to hear about the update events from the engine. this will be used so that
// we can build up the diff display in case the user asks to see the details of the diff
var eventsChannel chan engine.Event
eventsChannel := make(chan engine.Event)
defer func() {
close(eventsChannel)
}()
events := []engine.Event{}
go func() {
// pull the events from the channel and store them locally
for e := range eventsChannel {
if e.Type == engine.ResourcePreEvent ||
e.Type == engine.ResourceOutputsEvent ||
e.Type == engine.SummaryEvent {
// if we're previewing, we don't need to store the events as we're not going to prompt
// the user to get details of what's happening.
if !preview.Only() {
eventsChannel = make(chan engine.Event)
defer func() {
close(eventsChannel)
}()
go func() {
// pull the events from the channel and store them locally
for e := range eventsChannel {
if e.Type == engine.ResourcePreEvent ||
e.Type == engine.ResourceOutputsEvent ||
e.Type == engine.SummaryEvent {
events = append(events, e)
}
events = append(events, e)
}
}()
}
}
}()
// Perform the update operations, passing true for dryRun, so that we get a preview.
err := b.updateStack(
updateKind, stack, pkg, root, m,
opts, displayOpts, eventsChannel, true /*dryRun*/, scopes)
if err != nil || preview.Only() {
// if we're just previewing, then we can stop at this point.
return err
hasChanges := true
if !opts.SkipPreview {
changes, err := b.updateStack(
updateKind, stack, pkg, root, m, opts, eventsChannel, true /*dryRun*/, scopes)
if err != nil {
return false, err
}
hasChanges = changes.HasChanges()
}
// Ensure the user wants to proceed.
return confirmBeforeUpdating(stack, preview, events, displayOpts)
// If there are no changes, or we're auto-approving or just previewing, we can skip the confirmation prompt.
if !hasChanges || opts.AutoApprove || updateKind == client.UpdateKindPreview {
return hasChanges, nil
}
// Otherwise, ensure the user wants to proceed.
return hasChanges, confirmBeforeUpdating(updateKind, stack, events, opts)
}
// confirmBeforeUpdating asks the user whether to proceed. A nil error means yes.
func confirmBeforeUpdating(stack backend.Stack, preview backend.PreviewBehavior,
events []engine.Event, displayOpts backend.DisplayOptions) error {
// If auto-accept is true, we can exit right away.
if preview.AutoAccept() {
return nil
}
// Otherwise, we will ask the user for confirmation before proceeding.
func confirmBeforeUpdating(updateKind client.UpdateKind, stack backend.Stack,
events []engine.Event, opts backend.UpdateOptions) error {
for {
var response string
@ -601,25 +595,32 @@ func confirmBeforeUpdating(stack backend.Stack, preview backend.PreviewBehavior,
surveycore.QuestionIcon = ""
surveycore.SelectFocusIcon = colors.ColorizeText(colors.BrightGreen + ">" + colors.Reset)
options := []string{string(yes), string(no)}
choices := []string{string(yes), string(no)}
// if this is a managed stack, then we can get the details for the operation, as we will
// have been able to collect the details while the preview ran. For ppc stacks, we don't
// have that information since all the PPC does is forward stdout events to us.
if stack.(Stack).RunLocally() {
options = append(options, string(details))
if stack.(Stack).RunLocally() && !opts.SkipPreview {
choices = append(choices, string(details))
}
var previewWarning string
if opts.SkipPreview {
previewWarning = colors.SpecWarning + " without a preview" + colors.BrightWhite
}
if err := survey.AskOne(&survey.Select{
Message: colors.ColorizeText(colors.BrightWhite + "Do you want to proceed?" + colors.Reset),
Options: options,
Message: "\b" + colors.ColorizeText(
colors.BrightWhite+fmt.Sprintf("Do you want to perform this %s%s?",
updateKind, previewWarning)+colors.Reset),
Options: choices,
Default: string(no),
}, &response, nil); err != nil {
return err
return errors.Wrapf(err, "confirmation cancelled, not proceeding with the %s", updateKind)
}
if response == string(no) {
return errors.New("confirmation declined")
return errors.Errorf("confirmation declined, not proceeding with the %s", updateKind)
}
if response == string(yes) {
@ -627,7 +628,7 @@ func confirmBeforeUpdating(stack backend.Stack, preview backend.PreviewBehavior,
}
if response == string(details) {
diff := createDiff(events, displayOpts)
diff := createDiff(events, opts.Display)
_, err := os.Stdout.WriteString(diff + "\n\n")
contract.IgnoreError(err)
continue
@ -637,8 +638,7 @@ func confirmBeforeUpdating(stack backend.Stack, preview backend.PreviewBehavior,
func (b *cloudBackend) PreviewThenPromptThenExecute(
updateKind client.UpdateKind, stackRef backend.StackReference, pkg *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
m backend.UpdateMetadata, opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
// First get the stack.
stack, err := getStack(b, stackRef)
if err != nil {
@ -646,54 +646,45 @@ func (b *cloudBackend) PreviewThenPromptThenExecute(
}
if !stack.(Stack).RunLocally() && updateKind == client.UpdateKindDestroy {
// The service does not support preview of a destroy for a stack managed by a PPC. So behave as if (--force)
// had been passed (so we don't run the preview step)
opts.Force = true
// The service does not support preview of a destroy for a stack managed by a PPC. So skip the preview.
opts.SkipPreview = true
}
if preview.Should() {
// If we're not skipping the preview, then preview the operation to the user and ask them if
// they want to proceed.
err = b.PreviewThenPrompt(updateKind, stack, pkg, root, m, opts, preview, displayOpts, scopes)
if err != nil || preview.Only() {
return err
}
// Preview the operation to the user and ask them if they want to proceed.
hasChanges, err := b.PreviewThenPrompt(updateKind, stack, pkg, root, m, opts, scopes)
if err != nil || !hasChanges || updateKind == client.UpdateKindPreview {
return err
}
// Now do the real operation. We don't care about the events it issues, so just
// pass a nil channel along.
var unused chan engine.Event
// Now do the real operation. We don't care about the events it issues, so just pass a nil channel along.
_, err = b.updateStack(updateKind, stack, pkg, root, m, opts, nil, false /*dryRun*/, scopes)
return err
}
return b.updateStack(
updateKind, stack, pkg,
root, m, opts, displayOpts, unused, false /*dryRun*/, scopes)
func (b *cloudBackend) Preview(stackRef backend.StackReference, pkg *workspace.Project, root string,
m backend.UpdateMetadata, opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return b.PreviewThenPromptThenExecute(client.UpdateKindPreview, stackRef, pkg, root, m, opts, scopes)
}
func (b *cloudBackend) Update(stackRef backend.StackReference, pkg *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return b.PreviewThenPromptThenExecute(
client.UpdateKindUpdate, stackRef, pkg, root, m, opts, preview, displayOpts, scopes)
m backend.UpdateMetadata, opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return b.PreviewThenPromptThenExecute(client.UpdateKindUpdate, stackRef, pkg, root, m, opts, scopes)
}
func (b *cloudBackend) Refresh(stackRef backend.StackReference, pkg *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return b.PreviewThenPromptThenExecute(
client.UpdateKindRefresh, stackRef, pkg, root, m, opts, preview, displayOpts, scopes)
m backend.UpdateMetadata, opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return b.PreviewThenPromptThenExecute(client.UpdateKindRefresh, stackRef, pkg, root, m, opts, scopes)
}
func (b *cloudBackend) Destroy(stackRef backend.StackReference, pkg *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return b.PreviewThenPromptThenExecute(
client.UpdateKindDestroy, stackRef, pkg, root, m, opts, preview, displayOpts, scopes)
m backend.UpdateMetadata, opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return b.PreviewThenPromptThenExecute(client.UpdateKindDestroy, stackRef, pkg, root, m, opts, scopes)
}
func (b *cloudBackend) createAndStartUpdate(
action client.UpdateKind, stackRef backend.StackReference,
pkg *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, dryRun bool) (client.UpdateIdentifier, int, string, error) {
opts backend.UpdateOptions, dryRun bool) (client.UpdateIdentifier, int, string, error) {
stack, err := b.getCloudStackIdentifier(stackRef)
if err != nil {
@ -716,7 +707,7 @@ func (b *cloudBackend) createAndStartUpdate(
return getUpdateContents(context, pkg.UseDefaultIgnores(), showProgress)
}
update, err := b.client.CreateUpdate(
action, stack, pkg, workspaceStack.Config, main, metadata, opts, dryRun, getContents)
action, stack, pkg, workspaceStack.Config, main, metadata, opts.Engine, dryRun, getContents)
if err != nil {
return client.UpdateIdentifier{}, 0, "", err
}
@ -741,9 +732,9 @@ func (b *cloudBackend) createAndStartUpdate(
// updateStack performs a the provided type of update on a stack hosted in the Pulumi Cloud.
func (b *cloudBackend) updateStack(
action client.UpdateKind, stack backend.Stack, pkg *workspace.Project,
root string, m backend.UpdateMetadata, opts engine.UpdateOptions,
displayOpts backend.DisplayOptions, callerEventsOpt chan<- engine.Event, dryRun bool,
scopes backend.CancellationScopeSource) error {
root string, m backend.UpdateMetadata, opts backend.UpdateOptions,
callerEventsOpt chan<- engine.Event, dryRun bool,
scopes backend.CancellationScopeSource) (engine.ResourceChanges, error) {
// Print a banner so it's clear this is going to the cloud.
actionLabel := getActionLabel(string(action), dryRun)
@ -757,11 +748,10 @@ func (b *cloudBackend) updateStack(
var token string
var err error
if !stack.(Stack).RunLocally() || !dryRun {
update, version, token, err = b.createAndStartUpdate(
action, stack.Name(), pkg, root, m, opts, dryRun)
update, version, token, err = b.createAndStartUpdate(action, stack.Name(), pkg, root, m, opts, dryRun)
}
if err != nil {
return err
return nil, err
}
if version != 0 {
@ -779,19 +769,18 @@ func (b *cloudBackend) updateStack(
// If we are targeting a stack that uses local operations, run the appropriate engine action locally.
if stack.(Stack).RunLocally() {
return b.runEngineAction(
action, stack.Name(), pkg, root, opts, displayOpts,
update, token, callerEventsOpt, dryRun, scopes)
action, stack.Name(), pkg, root, opts, update, token, callerEventsOpt, dryRun, scopes)
}
// Otherwise, wait for the update to complete while rendering its events to stdout/stderr.
status, err := b.waitForUpdate(actionLabel, update, displayOpts)
status, err := b.waitForUpdate(actionLabel, update, opts.Display)
if err != nil {
return errors.Wrapf(err, "waiting for %s", action)
return nil, errors.Wrapf(err, "waiting for %s", action)
} else if status != apitype.StatusSucceeded {
return errors.Errorf("%s unsuccessful: status %v", action, status)
return nil, errors.Errorf("%s unsuccessful: status %v", action, status)
}
return nil
return nil, nil
}
// uploadArchive archives the current Pulumi program and uploads it to a signed URL. "current"
@ -821,13 +810,13 @@ func getUpdateContents(context string, useDefaultIgnores bool, progress bool) (i
func (b *cloudBackend) runEngineAction(
action client.UpdateKind, stackRef backend.StackReference, pkg *workspace.Project,
root string, opts engine.UpdateOptions, displayOpts backend.DisplayOptions,
update client.UpdateIdentifier, token string,
callerEventsOpt chan<- engine.Event, dryRun bool, scopes backend.CancellationScopeSource) error {
root string, opts backend.UpdateOptions, update client.UpdateIdentifier, token string,
callerEventsOpt chan<- engine.Event, dryRun bool,
scopes backend.CancellationScopeSource) (engine.ResourceChanges, error) {
u, err := b.newUpdate(stackRef, pkg, root, update, token)
if err != nil {
return err
return nil, err
}
persister := b.newSnapshotPersister(u.update, u.tokenSource)
@ -835,8 +824,7 @@ func (b *cloudBackend) runEngineAction(
displayEvents := make(chan engine.Event)
displayDone := make(chan bool)
go u.RecordAndDisplayEvents(
getActionLabel(string(action), dryRun), displayEvents, displayDone, displayOpts)
go u.RecordAndDisplayEvents(getActionLabel(string(action), dryRun), displayEvents, displayDone, opts.Display)
engineEvents := make(chan engine.Event)
@ -856,18 +844,21 @@ func (b *cloudBackend) runEngineAction(
// Depending on the action, kick off the relevant engine activity. Note that we don't immediately check and
// return error conditions, because we will do so below after waiting for the display channels to close.
var changes engine.ResourceChanges
engineCtx := &engine.Context{Cancel: scope.Context(), Events: engineEvents, SnapshotManager: manager}
switch action {
case client.UpdateKindPreview:
changes, err = engine.Preview(u, engineCtx, opts.Engine)
case client.UpdateKindUpdate:
if dryRun {
err = engine.Preview(u, engineCtx, opts)
changes, err = engine.Preview(u, engineCtx, opts.Engine)
} else {
_, err = engine.Update(u, engineCtx, opts, dryRun)
changes, err = engine.Update(u, engineCtx, opts.Engine, dryRun)
}
case client.UpdateKindRefresh:
_, err = engine.Refresh(u, engineCtx, opts, dryRun)
changes, err = engine.Refresh(u, engineCtx, opts.Engine, dryRun)
case client.UpdateKindDestroy:
_, err = engine.Destroy(u, engineCtx, opts, dryRun)
changes, err = engine.Destroy(u, engineCtx, opts.Engine, dryRun)
default:
contract.Failf("Unrecognized action type: %s", action)
}
@ -891,7 +882,7 @@ func (b *cloudBackend) runEngineAction(
}
}
return err
return changes, err
}
func (b *cloudBackend) CancelCurrentUpdate(stackRef backend.StackReference) error {

View file

@ -8,7 +8,6 @@ import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/config"
@ -117,22 +116,24 @@ func (s *cloudStack) Remove(force bool) (bool, error) {
return backend.RemoveStack(s, force)
}
func (s *cloudStack) Preview(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.PreviewStack(s, proj, root, m, opts, scopes)
}
func (s *cloudStack) Update(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return backend.UpdateStack(s, proj, root, m, opts, preview, displayOpts, scopes)
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.UpdateStack(s, proj, root, m, opts, scopes)
}
func (s *cloudStack) Refresh(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return backend.RefreshStack(s, proj, root, m, opts, preview, displayOpts, scopes)
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.RefreshStack(s, proj, root, m, opts, scopes)
}
func (s *cloudStack) Destroy(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return backend.DestroyStack(s, proj, root, m, opts, preview, displayOpts, scopes)
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.DestroyStack(s, proj, root, m, opts, scopes)
}
func (s *cloudStack) GetLogs(query operations.LogQuery) ([]operations.LogEntry, error) {

View file

@ -170,10 +170,22 @@ func (b *localBackend) GetStackCrypter(stackRef backend.StackReference) (config.
return symmetricCrypter(stackRef.StackName())
}
func (b *localBackend) Preview(
stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return b.performEngineOp("previewing", backend.PreviewUpdate,
stackRef.StackName(), proj, root, m, opts, scopes,
func(u engine.UpdateInfo, ctx *engine.Context,
opts engine.UpdateOptions, dryRun bool) (engine.ResourceChanges, error) {
contract.Assert(dryRun)
return engine.Preview(u, ctx, opts)
},
)
}
func (b *localBackend) Update(
stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
// The Pulumi Service will pick up changes to a stack's tags on each update. (e.g. changing the description
// in Pulumi.yaml.) While this isn't necessary for local updates, we do the validation here to keep
// parity with stacks managed by the Pulumi Service.
@ -186,49 +198,41 @@ func (b *localBackend) Update(
return errors.Wrap(err, "validating stack properties")
}
return b.performEngineOp("updating", backend.DeployUpdate,
stackName, proj, root, m, opts, preview, displayOpts, engine.Update, scopes)
stackName, proj, root, m, opts, scopes, engine.Update)
}
func (b *localBackend) Refresh(
stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return b.performEngineOp("refreshing", backend.RefreshUpdate,
stackRef.StackName(), proj, root, m, opts, preview, displayOpts, engine.Refresh, scopes)
stackRef.StackName(), proj, root, m, opts, scopes, engine.Refresh)
}
func (b *localBackend) Destroy(
stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return b.performEngineOp("destroying", backend.DestroyUpdate,
stackRef.StackName(), proj, root, m, opts, preview, displayOpts, engine.Destroy, scopes)
stackRef.StackName(), proj, root, m, opts, scopes, engine.Destroy)
}
type engineOpFunc func(engine.UpdateInfo, *engine.Context, engine.UpdateOptions, bool) (engine.ResourceChanges, error)
func (b *localBackend) performEngineOp(op string, kind backend.UpdateKind,
stackName tokens.QName, proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior, displayOpts backend.DisplayOptions,
performEngineOp engineOpFunc, scopes backend.CancellationScopeSource) error {
// At the moment, we don't support preview-confirm-update flows for local stacks.
if preview == backend.DefaultPreview {
return errors.Errorf("--preview or --skip-preview must be passed when %s a local stack", op)
}
opts backend.UpdateOptions, scopes backend.CancellationScopeSource, performEngineOp engineOpFunc) error {
update, err := b.newUpdate(stackName, proj, root)
if err != nil {
return err
}
events := make(chan engine.Event)
dryRun := preview.Only()
dryRun := (kind == backend.PreviewUpdate)
cancelScope := scopes.NewScope(events, dryRun)
defer cancelScope.Close()
done := make(chan bool)
go DisplayEvents(op, events, done, displayOpts)
go DisplayEvents(op, events, done, opts.Display)
// Create the management machinery.
persister := b.newSnapshotPersister(stackName)
@ -237,7 +241,7 @@ func (b *localBackend) performEngineOp(op string, kind backend.UpdateKind,
// Perform the update
start := time.Now().Unix()
changes, updateErr := performEngineOp(update, engineCtx, opts, dryRun)
changes, updateErr := performEngineOp(update, engineCtx, opts.Engine, dryRun)
end := time.Now().Unix()
<-done

View file

@ -5,7 +5,6 @@ package local
import (
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/resource/deploy"
@ -48,22 +47,24 @@ func (s *localStack) Remove(force bool) (bool, error) {
return backend.RemoveStack(s, force)
}
func (s *localStack) Preview(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.PreviewStack(s, proj, root, m, opts, scopes)
}
func (s *localStack) Update(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return backend.UpdateStack(s, proj, root, m, opts, preview, displayOpts, scopes)
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.UpdateStack(s, proj, root, m, opts, scopes)
}
func (s *localStack) Refresh(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return backend.RefreshStack(s, proj, root, m, opts, preview, displayOpts, scopes)
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.RefreshStack(s, proj, root, m, opts, scopes)
}
func (s *localStack) Destroy(proj *workspace.Project, root string, m backend.UpdateMetadata,
opts engine.UpdateOptions, preview backend.PreviewBehavior,
displayOpts backend.DisplayOptions, scopes backend.CancellationScopeSource) error {
return backend.DestroyStack(s, proj, root, m, opts, preview, displayOpts, scopes)
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) error {
return backend.DestroyStack(s, proj, root, m, opts, scopes)
}
func (s *localStack) GetLogs(query operations.LogQuery) ([]operations.LogEntry, error) {

View file

@ -8,7 +8,6 @@ import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/resource/deploy"
@ -22,18 +21,18 @@ type Stack interface {
Snapshot() *deploy.Snapshot // the latest deployment snapshot.
Backend() Backend // the backend this stack belongs to.
// Preview changes to this stack.
Preview(proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error
// Update this stack.
Update(proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
opts UpdateOptions, scopes CancellationScopeSource) error
// Refresh this stack's state from the cloud provider.
Refresh(proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
opts UpdateOptions, scopes CancellationScopeSource) error
// Destroy this stack's resources.
Destroy(proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
opts UpdateOptions, scopes CancellationScopeSource) error
Remove(force bool) (bool, error) // remove this stack.
GetLogs(query operations.LogQuery) ([]operations.LogEntry, error) // list log entries for this stack.
@ -46,25 +45,28 @@ func RemoveStack(s Stack, force bool) (bool, error) {
return s.Backend().RemoveStack(s.Name(), force)
}
// PreviewStack previews changes to this stack.
func PreviewStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Preview(s.Name(), proj, root, m, opts, scopes)
}
// UpdateStack updates the target stack with the current workspace's contents (config and code).
func UpdateStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error {
return s.Backend().Update(s.Name(), proj, root, m, opts, preview, displayOpts, scopes)
opts UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Update(s.Name(), proj, root, m, opts, scopes)
}
// RefreshStack refresh's the stack's state from the cloud provider.
func RefreshStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error {
return s.Backend().Refresh(s.Name(), proj, root, m, opts, preview, displayOpts, scopes)
opts UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Refresh(s.Name(), proj, root, m, opts, scopes)
}
// DestroyStack destroys all of this stack's resources.
func DestroyStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error {
return s.Backend().Destroy(s.Name(), proj, root, m, opts, preview, displayOpts, scopes)
opts UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Destroy(s.Name(), proj, root, m, opts, scopes)
}
// GetStackCrypter fetches the encrypter/decrypter for a stack.

View file

@ -9,7 +9,7 @@ import (
"github.com/pulumi/pulumi/pkg/util/contract"
)
func Preview(u UpdateInfo, ctx *Context, opts UpdateOptions) error {
func Preview(u UpdateInfo, ctx *Context, opts UpdateOptions) (ResourceChanges, error) {
contract.Require(u != nil, "u")
contract.Require(ctx != nil, "ctx")
@ -17,7 +17,7 @@ func Preview(u UpdateInfo, ctx *Context, opts UpdateOptions) error {
info, err := newPlanContext(u, "preview", ctx.ParentSpan)
if err != nil {
return err
return nil, err
}
defer info.Close()
@ -30,10 +30,10 @@ func Preview(u UpdateInfo, ctx *Context, opts UpdateOptions) error {
})
}
func preview(ctx *Context, info *planContext, opts planOptions) error {
func preview(ctx *Context, info *planContext, opts planOptions) (ResourceChanges, error) {
result, err := plan(info, opts, true /*dryRun*/)
if err != nil {
return err
return nil, err
}
if result != nil {
defer contract.IgnoreClose(result)
@ -41,16 +41,14 @@ func preview(ctx *Context, info *planContext, opts planOptions) error {
// Make the current working directory the same as the program's, and restore it upon exit.
done, err := result.Chdir()
if err != nil {
return err
return nil, err
}
defer done()
if _, err := printPlan(ctx, result, true /*dryRun*/); err != nil {
return err
}
return printPlan(ctx, result, true /*dryRun*/)
}
return nil
return nil, nil
}
type previewActions struct {

View file

@ -28,6 +28,17 @@ type UpdateOptions struct {
// ResourceChanges contains the aggregate resource changes by operation type.
type ResourceChanges map[deploy.StepOp]int
// HasChanges returns true if there are any non-same changes in the resulting summary.
func (changes ResourceChanges) HasChanges() bool {
var c int
for op, count := range changes {
if op != deploy.OpSame {
c += count
}
}
return c > 0
}
func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, error) {
contract.Require(u != nil, "update")
contract.Require(ctx != nil, "ctx")

View file

@ -526,7 +526,7 @@ func (pt *programTester) testLifeCycleInitialize(dir string) error {
func (pt *programTester) testLifeCycleDestroy(dir string) error {
// Destroy and remove the stack.
fprintf(pt.opts.Stdout, "Destroying stack\n")
destroy := []string{"destroy", "--non-interactive", "--force"}
destroy := []string{"destroy", "--non-interactive", "--skip-preview"}
if pt.opts.GetDebugUpdates() {
destroy = append(destroy, "-d")
}
@ -567,8 +567,8 @@ func (pt *programTester) testPreviewUpdateAndEdits(dir string) error {
}
func (pt *programTester) previewAndUpdate(dir string, name string, shouldFail bool) error {
preview := []string{"update", "--non-interactive", "--preview=only"}
update := []string{"update", "--non-interactive", "--preview=skip"}
preview := []string{"preview", "--non-interactive"}
update := []string{"update", "--non-interactive", "--skip-preview"}
if pt.opts.GetDebugUpdates() {
preview = append(preview, "-d")
update = append(update, "-d")

View file

@ -58,7 +58,7 @@ func TestProjectMain(t *testing.T) {
e.ImportDirectory("project_main_abs")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
e.RunCommand("pulumi", "stack", "init", "main-abs")
stdout, stderr := e.RunCommandExpectError("pulumi", "update", "--non-interactive", "--preview=skip")
stdout, stderr := e.RunCommandExpectError("pulumi", "update", "--non-interactive", "--skip-preview", "--yes")
assert.Equal(t, "", stdout)
assert.Contains(t, stderr, "project 'main' must be a relative path")
e.RunCommand("pulumi", "stack", "rm", "--yes")
@ -74,7 +74,7 @@ func TestProjectMain(t *testing.T) {
e.ImportDirectory("project_main_parent")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
e.RunCommand("pulumi", "stack", "init", "main-parent")
stdout, stderr := e.RunCommandExpectError("pulumi", "update", "--non-interactive", "--preview=skip")
stdout, stderr := e.RunCommandExpectError("pulumi", "update", "--non-interactive", "--skip-preview", "--yes")
assert.Equal(t, "", stdout)
assert.Contains(t, stderr, "project 'main' must be a subfolder")
e.RunCommand("pulumi", "stack", "rm", "--yes")

View file

@ -169,7 +169,7 @@ func TestStackBackups(t *testing.T) {
// Now run pulumi update.
before := time.Now().UnixNano()
e.RunCommand("pulumi", "update", "--non-interactive", "--preview=skip")
e.RunCommand("pulumi", "update", "--non-interactive", "--skip-preview", "--yes")
after := time.Now().UnixNano()
// Verify the backup directory contains a single backup.
@ -183,7 +183,7 @@ func TestStackBackups(t *testing.T) {
// Now run pulumi destroy.
before = time.Now().UnixNano()
e.RunCommand("pulumi", "destroy", "--non-interactive", "--preview=skip")
e.RunCommand("pulumi", "destroy", "--non-interactive", "--skip-preview", "--yes")
after = time.Now().UnixNano()
// Verify the backup directory has been updated with 1 additional backups.