Implement a refresh command

This change implements a `pulumi refresh` command.  It operates a bit
like `pulumi update`, and friends, in that it supports `--preview` and
`--diff`, along with the usual flags, and will update your checkpoint.

It works through substitution of the deploy.Source abstraction, which
generates a sequence of resource registration events.  This new
deploy.RefreshSource takes in a prior checkpoint and will walk it,
refreshing the state via the associated resource providers by invoking
Read for each resource encountered, and merging the resulting state with
the prior checkpoint, to yield a new resource.Goal state.  This state is
then fed through the engine in the usual ways with a few minor caveats:
namely, although the engine must generate steps for the logical
operations (permitting us to get nice summaries, progress, and diffs),
it mustn't actually carry them out because the state being imported
already reflects reality (a deleted resource has *already* been deleted,
so of course the engine need not perform the deletion).  The diffing
logic also needs to know how to treat the case of refresh slightly
differently, because we are going to be diffing outputs and not inputs.

Note that support for managed stacks is not yet complete, since that
requires updates to the service to support a refresh endpoint.  That
will be coming soon ...
This commit is contained in:
joeduffy 2018-04-10 11:22:39 -07:00
parent a1626aea36
commit b77403b4bb
28 changed files with 465 additions and 145 deletions

View file

@ -25,13 +25,13 @@ func newDestroyCmd() *cobra.Command {
// Flags for engine.UpdateOptions. // Flags for engine.UpdateOptions.
var analyzers []string var analyzers []string
var color colorFlag var color colorFlag
var diffDisplay bool
var parallel int var parallel int
var force bool var force bool
var preview bool var preview bool
var showConfig bool var showConfig bool
var showReplacementSteps bool var showReplacementSteps bool
var showSames bool var showSames bool
var diffDisplay bool
var cmd = &cobra.Command{ var cmd = &cobra.Command{
Use: "destroy", Use: "destroy",
@ -108,6 +108,11 @@ func newDestroyCmd() *cobra.Command {
cmd.PersistentFlags().StringSliceVar( cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{}, &analyzers, "analyzer", []string{},
"Run one or more analyzers as part of this update") "Run one or more analyzers as part of this update")
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().IntVarP( cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0, &parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)") "Allow P resource operations to run in parallel at once (<=1 for no parallelism)")
@ -126,11 +131,6 @@ func newDestroyCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar( cmd.PersistentFlags().BoolVar(
&showSames, "show-sames", false, &showSames, "show-sames", false,
"Show resources that don't need to be updated because they haven't changed, alongside those that do") "Show resources that don't need to be updated because they haven't changed, alongside those that do")
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
return cmd return cmd
} }

View file

@ -17,11 +17,11 @@ func newPreviewCmd() *cobra.Command {
// Flags for engine.UpdateOptions. // Flags for engine.UpdateOptions.
var analyzers []string var analyzers []string
var color colorFlag var color colorFlag
var diffDisplay bool
var parallel int var parallel int
var showConfig bool var showConfig bool
var showReplacementSteps bool var showReplacementSteps bool
var showSames bool var showSames bool
var diffDisplay bool
var cmd = &cobra.Command{ var cmd = &cobra.Command{
Use: "preview", Use: "preview",
@ -43,11 +43,14 @@ func newPreviewCmd() *cobra.Command {
"Choose a stack other than the currently selected one") "Choose a stack other than the currently selected one")
// Flags for engine.UpdateOptions. // Flags for engine.UpdateOptions.
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
cmd.PersistentFlags().StringSliceVar( cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{}, &analyzers, "analyzer", []string{},
"Run one or more analyzers as part of this update") "Run one or more analyzers as part of this update")
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().IntVarP( cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0, &parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)") "Allow P resource operations to run in parallel at once (<=1 for no parallelism)")
@ -60,9 +63,6 @@ func newPreviewCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar( cmd.PersistentFlags().BoolVar(
&showSames, "show-sames", false, &showSames, "show-sames", false,
"Show resources that needn't be updated because they haven't changed, alongside those that do") "Show resources that needn't be updated because they haven't changed, alongside those that do")
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
return cmd return cmd
} }

View file

@ -86,6 +86,7 @@ func NewPulumiCmd() *cobra.Command {
cmd.AddCommand(newNewCmd()) cmd.AddCommand(newNewCmd())
cmd.AddCommand(newPluginCmd()) cmd.AddCommand(newPluginCmd())
cmd.AddCommand(newPreviewCmd()) cmd.AddCommand(newPreviewCmd())
cmd.AddCommand(newRefreshCmd())
cmd.AddCommand(newStackCmd()) cmd.AddCommand(newStackCmd())
cmd.AddCommand(newUpdateCmd()) cmd.AddCommand(newUpdateCmd())
cmd.AddCommand(newVersionCmd()) cmd.AddCommand(newVersionCmd())

125
cmd/refresh.go Normal file
View file

@ -0,0 +1,125 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
package cmd
import (
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
)
func newRefreshCmd() *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 force bool
var parallel int
var preview bool
var showConfig bool
var showReplacementSteps bool
var showSames bool
var cmd = &cobra.Command{
Use: "refresh",
Short: "Refresh the resources in a stack",
Long: "Refresh the resources in a stack.\n" +
"\n" +
"This command compares the current stack's resource state with the state known to exist in\n" +
"the actual cloud provider. Any such changes are adopted into the current stack. Note that if\n" +
"the program text isn't updated accordingly, subsequent updates may still appear to be out of\n" +
"synch with respect to the cloud provider's source of truth.\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 {
if !force && !preview && !terminal.IsTerminal(int(os.Stdout.Fd())) {
return errors.New("'update' must be run interactively or be passed the --force or --preview flag")
}
if force && preview {
return errors.New("--force and --preview cannot both be specified")
}
s, err := requireStack(tokens.QName(stack), true)
if err != nil {
return err
}
proj, root, err := readProject()
if err != nil {
return err
}
m, err := getUpdateMetadata(message, root)
if err != nil {
return errors.Wrap(err, "gathering environment metadata")
}
return s.Refresh(proj, root, m, engine.UpdateOptions{
Analyzers: analyzers,
Force: force,
Preview: preview,
Parallel: parallel,
Debug: debug,
}, backend.DisplayOptions{
Color: color.Colorization(),
ShowConfig: showConfig,
ShowReplacementSteps: showReplacementSteps,
ShowSameResources: showSames,
DiffDisplay: diffDisplay,
Debug: debug,
})
}),
}
cmd.PersistentFlags().BoolVarP(
&debug, "debug", "d", false,
"Print detailed debugging output during resource operations")
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"Choose a stack other than the currently selected one")
cmd.PersistentFlags().StringVarP(
&message, "message", "m", "",
"Optional message to associate with the update operation")
// Flags for engine.UpdateOptions.
cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", nil,
"Run one or more analyzers as part of this update")
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().BoolVarP(
&force, "force", "f", false,
"Skip confirmation prompts and preview, and proceed with the update automatically")
cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)")
cmd.PersistentFlags().BoolVarP(
&preview, "preview", "n", false,
"Don't create/delete resources; just preview the planned operations")
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")
return cmd
}

View file

@ -17,20 +17,19 @@ import (
func newUpdateCmd() *cobra.Command { func newUpdateCmd() *cobra.Command {
var debug bool var debug bool
var stack string
var message string var message string
var stack string
// Flags for engine.UpdateOptions. // Flags for engine.UpdateOptions.
var analyzers []string var analyzers []string
var color colorFlag var color colorFlag
var diffDisplay bool
var parallel int var parallel int
var preview bool var preview bool
var force bool var force bool
var showConfig bool var showConfig bool
var showReplacementSteps bool var showReplacementSteps bool
var showSames bool var showSames bool
var diffDisplay bool
var cmd = &cobra.Command{ var cmd = &cobra.Command{
Use: "update", Use: "update",
@ -105,6 +104,11 @@ func newUpdateCmd() *cobra.Command {
cmd.PersistentFlags().StringSliceVar( cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{}, &analyzers, "analyzer", []string{},
"Run one or more analyzers as part of this update") "Run one or more analyzers as part of this update")
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().IntVarP( cmd.PersistentFlags().IntVarP(
&parallel, "parallel", "p", 0, &parallel, "parallel", "p", 0,
"Allow P resource operations to run in parallel at once (<=1 for no parallelism)") "Allow P resource operations to run in parallel at once (<=1 for no parallelism)")
@ -123,11 +127,6 @@ func newUpdateCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar( cmd.PersistentFlags().BoolVar(
&showSames, "show-sames", false, &showSames, "show-sames", false,
"Show resources that don't need be updated because they haven't changed, alongside those that do") "Show resources that don't need be updated because they haven't changed, alongside those that do")
cmd.PersistentFlags().BoolVar(
&diffDisplay, "diff", false,
"Display operation as a rich diff showing the overall change")
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
return cmd return cmd
} }

View file

@ -35,7 +35,9 @@ type Backend interface {
// Update updates the target stack with the current workspace's contents (config and code). // Update updates the target stack with the current workspace's contents (config and code).
Update(stackName tokens.QName, proj *workspace.Project, root string, Update(stackName tokens.QName, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error
// Refresh refreshes the stack's state from the cloud provider.
Refresh(stackName tokens.QName, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error
// Destroy destroys all of this stack's resources. // Destroy destroys all of this stack's resources.
Destroy(stackName tokens.QName, proj *workspace.Project, root string, Destroy(stackName tokens.QName, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error

View file

@ -409,9 +409,11 @@ func getActionLabel(key string, dryRun bool) string {
switch key { switch key {
case string(client.UpdateKindUpdate): case string(client.UpdateKindUpdate):
return "Updating" return "Updating"
case string(client.UpdateKindRefresh):
return "Refreshing"
case string(client.UpdateKindDestroy): case string(client.UpdateKindDestroy):
return "Destroying" return "Destroying"
case "import": case string(client.UpdateKindImport):
return "Importing" return "Importing"
} }
@ -575,20 +577,19 @@ func (b *cloudBackend) PreviewThenPromptThenExecute(
root, m, opts, displayOpts, unused, false /*dryRun*/) root, m, opts, displayOpts, unused, false /*dryRun*/)
} }
func (b *cloudBackend) Update( func (b *cloudBackend) Update(stackName tokens.QName, pkg *workspace.Project, root string,
stackName tokens.QName, pkg *workspace.Project, root string, m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
m backend.UpdateMetadata, opts engine.UpdateOptions, return b.PreviewThenPromptThenExecute(client.UpdateKindUpdate, stackName, pkg, root, m, opts, displayOpts)
displayOpts backend.DisplayOptions) error { }
return b.PreviewThenPromptThenExecute( func (b *cloudBackend) Refresh(stackName tokens.QName, pkg *workspace.Project, root string,
client.UpdateKindUpdate, stackName, pkg, root, m, opts, displayOpts) m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return b.PreviewThenPromptThenExecute(client.UpdateKindRefresh, stackName, pkg, root, m, opts, displayOpts)
} }
func (b *cloudBackend) Destroy(stackName tokens.QName, pkg *workspace.Project, root string, func (b *cloudBackend) Destroy(stackName tokens.QName, pkg *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error { m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return b.PreviewThenPromptThenExecute(client.UpdateKindDestroy, stackName, pkg, root, m, opts, displayOpts)
return b.PreviewThenPromptThenExecute(
client.UpdateKindDestroy, stackName, pkg, root, m, opts, displayOpts)
} }
func (b *cloudBackend) createAndStartUpdate( func (b *cloudBackend) createAndStartUpdate(
@ -749,6 +750,8 @@ 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.
switch action { switch action {
case client.UpdateKindUpdate: case client.UpdateKindUpdate:
if dryRun { if dryRun {
@ -756,8 +759,12 @@ func (b *cloudBackend) runEngineAction(
} else { } else {
_, err = engine.Update(u, engineEvents, opts, dryRun) _, err = engine.Update(u, engineEvents, opts, dryRun)
} }
case client.UpdateKindRefresh:
_, err = engine.Refresh(u, engineEvents, opts, dryRun)
case client.UpdateKindDestroy: case client.UpdateKindDestroy:
_, err = engine.Destroy(u, engineEvents, opts, dryRun) _, err = engine.Destroy(u, engineEvents, opts, dryRun)
default:
contract.Failf("Unrecognized action type: %s", action)
} }
// Wait for the display to finish showing all the events. // Wait for the display to finish showing all the events.
@ -1017,8 +1024,8 @@ func displayEvents(
return return
} }
payload := event.Payload.(apitype.UpdateEvent)
// Pluck out the string. // Pluck out the string.
payload := event.Payload.(apitype.UpdateEvent)
if raw, ok := payload.Fields["text"]; ok && raw != nil { if raw, ok := payload.Fields["text"]; ok && raw != nil {
if text, ok := raw.(string); ok { if text, ok := raw.(string); ok {
text = opts.Color.Colorize(text) text = opts.Color.Colorize(text)

View file

@ -26,7 +26,9 @@ type UpdateKind string
const ( const (
UpdateKindUpdate UpdateKind = "update" UpdateKindUpdate UpdateKind = "update"
UpdateKindRefresh UpdateKind = "refresh"
UpdateKindDestroy UpdateKind = "destroy" UpdateKindDestroy UpdateKind = "destroy"
UpdateKindImport UpdateKind = "import"
) )
// ProjectIdentifier is the set of data needed to identify a Pulumi Cloud project. This the // ProjectIdentifier is the set of data needed to identify a Pulumi Cloud project. This the

View file

@ -314,6 +314,8 @@ func (pc *Client) CreateUpdate(
} else { } else {
endpoint = "update" endpoint = "update"
} }
case UpdateKindRefresh:
contract.Failf("Refresh not yet supported for managed stacks [pulumi/pulumi#1081]")
case UpdateKindDestroy: case UpdateKindDestroy:
endpoint = "destroy" endpoint = "destroy"
default: default:

View file

@ -91,6 +91,11 @@ func (s *cloudStack) Update(proj *workspace.Project, root string,
return backend.UpdateStack(s, proj, root, m, opts, displayOpts) return backend.UpdateStack(s, proj, root, m, opts, displayOpts)
} }
func (s *cloudStack) Refresh(proj *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return backend.RefreshStack(s, proj, root, m, opts, displayOpts)
}
func (s *cloudStack) Destroy(proj *workspace.Project, root string, func (s *cloudStack) Destroy(proj *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error { m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return backend.DestroyStack(s, proj, root, m, opts, displayOpts) return backend.DestroyStack(s, proj, root, m, opts, displayOpts)

View file

@ -152,36 +152,31 @@ func (b *localBackend) Update(
return errors.Wrap(err, "validating stack properties") return errors.Wrap(err, "validating stack properties")
} }
if !opts.Force && !opts.Preview { return b.performEngineOp("updating", backend.DeployUpdate,
return errors.New("--update or --preview must be passed when updating a local stack") stackName, proj, root, m, opts, displayOpts, opts.Preview, engine.Update)
}
return b.performEngineOp(
"updating", backend.DeployUpdate,
stackName, proj, root, m, opts, displayOpts,
opts.Preview, engine.Update)
} }
func (b *localBackend) Destroy( func (b *localBackend) Refresh(stackName tokens.QName, proj *workspace.Project, root string,
stackName tokens.QName, proj *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error { m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return b.performEngineOp("refreshing", backend.RefreshUpdate,
if !opts.Force && !opts.Preview { stackName, proj, root, m, opts, displayOpts, opts.Preview, engine.Refresh)
return errors.New("--update or --preview must be passed when destroying a local stacks")
}
return b.performEngineOp(
"destroying", backend.DestroyUpdate,
stackName, proj, root, m, opts, displayOpts,
opts.Preview, engine.Destroy)
} }
func (b *localBackend) performEngineOp( func (b *localBackend) Destroy(stackName tokens.QName, proj *workspace.Project, root string,
op string, kind backend.UpdateKind, stackName tokens.QName, proj *workspace.Project, m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
root string, m backend.UpdateMetadata, opts engine.UpdateOptions, return b.performEngineOp("destroying", backend.DestroyUpdate,
displayOpts backend.DisplayOptions, dryRun bool, stackName, proj, root, m, opts, displayOpts, opts.Preview, engine.Destroy)
performEngineOp func(engine.UpdateInfo, chan<- engine.Event, engine.UpdateOptions, bool) ( }
engine.ResourceChanges, error)) error {
type engineOpFunc func(
engine.UpdateInfo, chan<- engine.Event, 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, displayOpts backend.DisplayOptions, dryRun bool, performEngineOp engineOpFunc) error {
if !opts.Force && !dryRun {
return errors.Errorf("--force or --preview must be passed when %s a local stack", op)
}
update, err := b.newUpdate(stackName, proj, root) update, err := b.newUpdate(stackName, proj, root)
if err != nil { if err != nil {

View file

@ -54,6 +54,11 @@ func (s *localStack) Update(proj *workspace.Project, root string,
return backend.UpdateStack(s, proj, root, m, opts, displayOpts) return backend.UpdateStack(s, proj, root, m, opts, displayOpts)
} }
func (s *localStack) Refresh(proj *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return backend.RefreshStack(s, proj, root, m, opts, displayOpts)
}
func (s *localStack) Destroy(proj *workspace.Project, root string, func (s *localStack) Destroy(proj *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error { m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return backend.DestroyStack(s, proj, root, m, opts, displayOpts) return backend.DestroyStack(s, proj, root, m, opts, displayOpts)

View file

@ -26,6 +26,9 @@ type Stack interface {
// Update this stack. // Update this stack.
Update(proj *workspace.Project, root string, Update(proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error
// Refresh this stack's state from the cloud provider.
Refresh(proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error
// Destroy this stack's resources. // Destroy this stack's resources.
Destroy(proj *workspace.Project, root string, Destroy(proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error
@ -47,6 +50,12 @@ func UpdateStack(s Stack, proj *workspace.Project, root string,
return s.Backend().Update(s.Name(), proj, root, m, opts, displayOpts) return s.Backend().Update(s.Name(), proj, root, m, opts, displayOpts)
} }
// 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, displayOpts DisplayOptions) error {
return s.Backend().Refresh(s.Name(), proj, root, m, opts, displayOpts)
}
// DestroyStack destroys all of this stack's resources. // DestroyStack destroys all of this stack's resources.
func DestroyStack(s Stack, proj *workspace.Project, root string, func DestroyStack(s Stack, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error { m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error {

View file

@ -25,6 +25,8 @@ const (
DeployUpdate UpdateKind = "update" DeployUpdate UpdateKind = "update"
// PreviewUpdate is a preview of an update, without impacting resources. // PreviewUpdate is a preview of an update, without impacting resources.
PreviewUpdate UpdateKind = "preview" PreviewUpdate UpdateKind = "preview"
// RefreshUpdate is an update that adopts a cloud's existing resource state.
RefreshUpdate UpdateKind = "refresh"
// DestroyUpdate is an update which removes all resources. // DestroyUpdate is an update which removes all resources.
DestroyUpdate UpdateKind = "destroy" DestroyUpdate UpdateKind = "destroy"
) )

View file

@ -143,11 +143,13 @@ func GetResourcePropertiesDetails(
replaces = step.Keys replaces = step.Keys
} }
old := step.Old old, new := step.Old, step.New
new := step.New
if old == nil && new != nil { if old == nil && new != nil {
if len(new.Outputs) > 0 {
printObject(&b, new.Outputs, planning, indent, step.Op, false, debug)
} else {
printObject(&b, new.Inputs, planning, indent, step.Op, false, debug) printObject(&b, new.Inputs, planning, indent, step.Op, false, debug)
}
} else if new == nil && old != nil { } else if new == nil && old != nil {
// in summary view, we don't have to print out the entire object that is getting deleted. // in summary view, we don't have to print out the entire object that is getting deleted.
// note, the caller will have already printed out the type/name/id/urn of the resource, // note, the caller will have already printed out the type/name/id/urn of the resource,
@ -155,6 +157,8 @@ func GetResourcePropertiesDetails(
if !summary { if !summary {
printObject(&b, old.Inputs, planning, indent, step.Op, false, debug) printObject(&b, old.Inputs, planning, indent, step.Op, false, debug)
} }
} else if len(new.Outputs) > 0 {
printOldNewDiffs(&b, old.Outputs, new.Outputs, replaces, planning, indent, step.Op, summary, debug)
} else { } else {
printOldNewDiffs(&b, old.Inputs, new.Inputs, replaces, planning, indent, step.Op, summary, debug) printOldNewDiffs(&b, old.Inputs, new.Inputs, replaces, planning, indent, step.Op, summary, debug)
} }

View file

@ -68,6 +68,7 @@ type planOptions struct {
// creates resources to compare against the current checkpoint state (e.g., by evaluating a program, etc). // creates resources to compare against the current checkpoint state (e.g., by evaluating a program, etc).
SourceFunc planSourceFunc SourceFunc planSourceFunc
SkipOutputs bool // true if we we should skip printing outputs separately.
DOT bool // true if we should print the DOT file for this plan. DOT bool // true if we should print the DOT file for this plan.
Events eventEmitter // the channel to write events from the engine to. Events eventEmitter // the channel to write events from the engine to.
Diag diag.Sink // the sink to use for diag'ing. Diag diag.Sink // the sink to use for diag'ing.

View file

@ -54,6 +54,7 @@ func preview(ctx *planContext, opts planOptions) error {
} }
type previewActions struct { type previewActions struct {
Refresh bool
Ops map[deploy.StepOp]int Ops map[deploy.StepOp]int
Opts planOptions Opts planOptions
Seen map[resource.URN]deploy.Step Seen map[resource.URN]deploy.Step
@ -69,9 +70,7 @@ func newPreviewActions(opts planOptions) *previewActions {
func (acts *previewActions) OnResourceStepPre(step deploy.Step) (interface{}, error) { func (acts *previewActions) OnResourceStepPre(step deploy.Step) (interface{}, error) {
acts.Seen[step.URN()] = step acts.Seen[step.URN()] = step
acts.Opts.Events.resourcePreEvent(step, true /*planning*/, acts.Opts.Debug) acts.Opts.Events.resourcePreEvent(step, true /*planning*/, acts.Opts.Debug)
return nil, nil return nil, nil
} }
@ -96,7 +95,10 @@ func (acts *previewActions) OnResourceStepPost(ctx interface{},
func (acts *previewActions) OnResourceOutputs(step deploy.Step) error { func (acts *previewActions) OnResourceOutputs(step deploy.Step) error {
assertSeen(acts.Seen, step) assertSeen(acts.Seen, step)
// Print the resource outputs separately, unless this is a refresh in which case they are already printed.
if !acts.Opts.SkipOutputs {
acts.Opts.Events.resourceOutputsEvent(step, true /*planning*/, acts.Opts.Debug) acts.Opts.Events.resourceOutputsEvent(step, true /*planning*/, acts.Opts.Debug)
}
return nil return nil
} }

46
pkg/engine/refresh.go Normal file
View file

@ -0,0 +1,46 @@
// Copyright 2018, Pulumi Corporation. All rights reserved.
package engine
import (
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/workspace"
)
func Refresh(u UpdateInfo, events chan<- Event, opts UpdateOptions, dryRun bool) (ResourceChanges, error) {
contract.Require(u != nil, "u")
defer func() { events <- cancelEvent() }()
ctx, err := newPlanContext(u)
if err != nil {
return nil, err
}
defer ctx.Close()
emitter := makeEventEmitter(events, u)
return update(ctx, planOptions{
UpdateOptions: opts,
SkipOutputs: true, // refresh is exclusively about outputs
SourceFunc: newRefreshSource,
Events: emitter,
Diag: newEventSink(emitter),
}, dryRun)
}
func newRefreshSource(opts planOptions, proj *workspace.Project, pwd, main string,
target *deploy.Target, plugctx *plugin.Context, dryRun bool) (deploy.Source, error) {
// First, consult the manifest for the plugins we will need to ask to refresh the state.
if target != nil && target.Snapshot != nil {
if err := plugctx.Host.EnsurePlugins(target.Snapshot.Manifest.Plugins); err != nil {
return nil, err
}
}
// Now create a refresh source. This source simply loads up the current checkpoint state, enumerates it,
// and refreshes each state with the current cloud provider's view of it.
return deploy.NewRefreshSource(plugctx, proj, target, dryRun), nil
}

View file

@ -74,6 +74,7 @@ func (p *Plan) Diag() diag.Sink { return p.ctx.Diag }
func (p *Plan) Prev() *Snapshot { return p.prev } func (p *Plan) Prev() *Snapshot { return p.prev }
func (p *Plan) Olds() map[resource.URN]*resource.State { return p.olds } func (p *Plan) Olds() map[resource.URN]*resource.State { return p.olds }
func (p *Plan) Source() Source { return p.source } func (p *Plan) Source() Source { return p.source }
func (p *Plan) Refresh() bool { return p.source.Refresh() }
// Provider fetches the provider for a given resource type, possibly lazily allocating the plugins for it. If a // Provider fetches the provider for a given resource type, possibly lazily allocating the plugins for it. If a
// provider could not be found, or an error occurred while creating it, a non-nil error is returned. // provider could not be found, or an error occurred while creating it, a non-nil error is returned.

View file

@ -229,14 +229,15 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
var invalid bool // will be set to true if this object fails validation. var invalid bool // will be set to true if this object fails validation.
// Use the resource goal state name to produce a globally unique URN. // Use the resource goal state name to produce a globally unique URN.
res := e.Goal() goal := e.Goal()
parentType := tokens.Type("") parentType := tokens.Type("")
if res.Parent != "" && res.Parent.Type() != resource.RootStackType { if p := goal.Parent; p != "" && p.Type() != resource.RootStackType {
// Skip empty parents and don't use the root stack type; otherwise, use the full qualified type. // Skip empty parents and don't use the root stack type; otherwise, use the full qualified type.
parentType = res.Parent.QualifiedType() parentType = p.QualifiedType()
} }
urn := resource.NewURN(iter.p.Target().Name, iter.p.source.Project(), parentType, res.Type, res.Name) t := goal.Type
urn := resource.NewURN(iter.p.Target().Name, iter.p.source.Project(), parentType, t, goal.Name)
if iter.urns[urn] { if iter.urns[urn] {
invalid = true invalid = true
// TODO[pulumi/pulumi-framework#19]: improve this error message! // TODO[pulumi/pulumi-framework#19]: improve this error message!
@ -244,42 +245,59 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
} }
iter.urns[urn] = true iter.urns[urn] = true
// Produce a new state object that we'll build up as operations are performed. It begins with empty outputs. // Check for an old resource so that we can figure out if this is a create, delete, etc., and/or to diff.
// Ultimately, this is what will get serialized into the checkpoint file.
new := resource.NewState(res.Type, urn, res.Custom, false, "", res.Properties, nil,
res.Parent, res.Protect, res.Dependencies)
// Check for an old resource before going any further.
old, hasOld := iter.p.Olds()[urn] old, hasOld := iter.p.Olds()[urn]
var olds resource.PropertyMap var oldInputs resource.PropertyMap
var oldState resource.PropertyMap var oldOutputs resource.PropertyMap
if hasOld { if hasOld {
olds = old.Inputs oldInputs = old.Inputs
oldState = old.All() oldOutputs = old.Outputs
} }
// See if we're performing a refresh update, which takes slightly different code-paths.
refresh := iter.p.Refresh()
// Produce a new state object that we'll build up as operations are performed. Ultimately, this is what will
// get serialized into the checkpoint file. Normally there are no outputs, unless this is a refresh.
props := goal.Properties
var inputs resource.PropertyMap
var outputs resource.PropertyMap
if refresh {
// In the case of a refresh, we will preserve the old inputs (since we won't have any new ones). Note
// that this can lead to a state in which inputs could not have possibly produced the outputs, but this
// will need to be reconciled manually by the programmer updating the program accordingly.
inputs = oldInputs
outputs = props
} else {
// In the case of non-refreshes, outputs remain empty (they will be computed), but inputs are present.
inputs = props
}
new := resource.NewState(t, urn, goal.Custom, false, "",
inputs, outputs, goal.Parent, goal.Protect, goal.Dependencies)
// Fetch the provider for this resource type, assuming it isn't just a logical one. // Fetch the provider for this resource type, assuming it isn't just a logical one.
var prov plugin.Provider var prov plugin.Provider
var err error var err error
if res.Custom { if goal.Custom {
if prov, err = iter.Provider(res.Type); err != nil { if prov, err = iter.Provider(t); err != nil {
return nil, err return nil, err
} }
} }
// We only allow unknown property values to be exposed to the provider if we are performing a preview. // We only allow unknown property values to be exposed to the provider if we are performing an update preview.
allowUnknowns := iter.p.preview allowUnknowns := iter.p.preview && !refresh
// Ensure the provider is okay with this resource and fetch the inputs to pass to subsequent methods. // If this isn't a refresh, ensure the provider is okay with this resource and fetch the inputs to pass to
news, inputs := new.Inputs, new.Inputs // subsequent methods. If these are not inputs, we are just going to blindly store the outputs, so skip this.
if prov != nil { if prov != nil && !refresh {
var failures []plugin.CheckFailure var failures []plugin.CheckFailure
inputs, failures, err = prov.Check(urn, olds, news, allowUnknowns) inputs, failures, err = prov.Check(urn, oldInputs, inputs, allowUnknowns)
if err != nil { if err != nil {
return nil, err return nil, err
} else if iter.issueCheckErrors(new, urn, failures) { } else if iter.issueCheckErrors(new, urn, failures) {
invalid = true invalid = true
} }
props = inputs
new.Inputs = inputs new.Inputs = inputs
} }
@ -293,7 +311,7 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
return nil, errors.Errorf("analyzer '%v' could not be loaded from your $PATH", a) return nil, errors.Errorf("analyzer '%v' could not be loaded from your $PATH", a)
} }
var failures []plugin.AnalyzeFailure var failures []plugin.AnalyzeFailure
failures, err = analyzer.Analyze(new.Type, inputs) failures, err = analyzer.Analyze(new.Type, props)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -327,7 +345,7 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
// cascading impact on subsequent updates too, since those IDs must trigger recreations, etc. // cascading impact on subsequent updates too, since those IDs must trigger recreations, etc.
var diff plugin.DiffResult var diff plugin.DiffResult
if prov != nil { if prov != nil {
if diff, err = prov.Diff(urn, old.ID, oldState, inputs, allowUnknowns); err != nil { if diff, err = prov.Diff(urn, old.ID, oldOutputs, props, allowUnknowns); err != nil {
return nil, err return nil, err
} }
} }
@ -342,7 +360,11 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
hasChanges = false hasChanges = false
case plugin.DiffUnknown: case plugin.DiffUnknown:
// This is legacy behavior; just use the DeepEquals function to diff on the Pulumi side. // This is legacy behavior; just use the DeepEquals function to diff on the Pulumi side.
hasChanges = !olds.DeepEquals(inputs) if refresh {
hasChanges = !oldOutputs.DeepEquals(outputs)
} else {
hasChanges = !oldInputs.DeepEquals(inputs)
}
default: default:
return nil, errors.Errorf( return nil, errors.Errorf(
"resource provider for %s replied with unrecognized diff state: %d", urn, diff.Changes) "resource provider for %s replied with unrecognized diff state: %d", urn, diff.Changes)
@ -355,9 +377,9 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
// If we are going to perform a replacement, we need to recompute the default values. The above logic // If we are going to perform a replacement, we need to recompute the default values. The above logic
// had assumed that we were going to carry them over from the old resource, which is no longer true. // had assumed that we were going to carry them over from the old resource, which is no longer true.
if prov != nil { if prov != nil && !refresh {
var failures []plugin.CheckFailure var failures []plugin.CheckFailure
inputs, failures, err = prov.Check(urn, nil, news, allowUnknowns) inputs, failures, err = prov.Check(urn, nil, goal.Properties, allowUnknowns)
if err != nil { if err != nil {
return nil, err return nil, err
} else if iter.issueCheckErrors(new, urn, failures) { } else if iter.issueCheckErrors(new, urn, failures) {
@ -368,7 +390,7 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
if glog.V(7) { if glog.V(7) {
glog.V(7).Infof("Planner decided to replace '%v' (oldprops=%v inputs=%v)", glog.V(7).Infof("Planner decided to replace '%v' (oldprops=%v inputs=%v)",
urn, olds, new.Inputs) urn, oldInputs, new.Inputs)
} }
// We have two approaches to performing replacements: // We have two approaches to performing replacements:
@ -403,7 +425,7 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
// If we fell through, it's an update. // If we fell through, it's an update.
iter.updates[urn] = true iter.updates[urn] = true
if glog.V(7) { if glog.V(7) {
glog.V(7).Infof("Planner decided to update '%v' (oldprops=%v inputs=%v", urn, olds, new.Inputs) glog.V(7).Infof("Planner decided to update '%v' (oldprops=%v inputs=%v", urn, oldInputs, new.Inputs)
} }
return []Step{NewUpdateStep(iter, e, old, new, diff.StableKeys)}, nil return []Step{NewUpdateStep(iter, e, old, new, diff.StableKeys)}, nil
} }

View file

@ -82,17 +82,10 @@ type errorSource struct {
duringIterate bool // if true, the error happens in Iterate; else, Next. duringIterate bool // if true, the error happens in Iterate; else, Next.
} }
func (src *errorSource) Close() error { func (src *errorSource) Close() error { return nil }
return nil // nothing to do. func (src *errorSource) Project() tokens.PackageName { return "" }
} func (src *errorSource) Info() interface{} { return nil }
func (src *errorSource) Refresh() bool { return false }
func (src *errorSource) Project() tokens.PackageName {
return ""
}
func (src *errorSource) Info() interface{} {
return nil
}
func (src *errorSource) Iterate(opts Options) (SourceIterator, error) { func (src *errorSource) Iterate(opts Options) (SourceIterator, error) {
if src.duringIterate { if src.duringIterate {

View file

@ -12,10 +12,15 @@ import (
// A Source can generate a new set of resources that the planner will process accordingly. // A Source can generate a new set of resources that the planner will process accordingly.
type Source interface { type Source interface {
io.Closer io.Closer
// Project returns the package name of the Pulumi project we are obtaining resources from. // Project returns the package name of the Pulumi project we are obtaining resources from.
Project() tokens.PackageName Project() tokens.PackageName
// Info returns a serializable payload that can be used to stamp snapshots for future reconciliation. // Info returns a serializable payload that can be used to stamp snapshots for future reconciliation.
Info() interface{} Info() interface{}
// Refresh indicates whether this source returns events source from existing state (true), and hence can simply be
// assumed to reflect existing state, or whether the events should acted upon (false).
Refresh() bool
// Iterate begins iterating the source. Error is non-nil upon failure; otherwise, a valid iterator is returned. // Iterate begins iterating the source. Error is non-nil upon failure; otherwise, a valid iterator is returned.
Iterate(opts Options) (SourceIterator, error) Iterate(opts Options) (SourceIterator, error)
} }
@ -23,6 +28,7 @@ type Source interface {
// A SourceIterator enumerates the list of resources that a source has to offer and tracks associated state. // A SourceIterator enumerates the list of resources that a source has to offer and tracks associated state.
type SourceIterator interface { type SourceIterator interface {
io.Closer io.Closer
// Next returns the next event from the source. // Next returns the next event from the source.
Next() (SourceEvent, error) Next() (SourceEvent, error)
} }

View file

@ -60,9 +60,8 @@ func (src *evalSource) Stack() tokens.QName {
return src.runinfo.Target.Name return src.runinfo.Target.Name
} }
func (src *evalSource) Info() interface{} { func (src *evalSource) Info() interface{} { return src.runinfo }
return src.runinfo func (src *evalSource) Refresh() bool { return false }
}
// Iterate will spawn an evaluator coroutine and prepare to interact with it on subsequent calls to Next. // Iterate will spawn an evaluator coroutine and prepare to interact with it on subsequent calls to Next.
func (src *evalSource) Iterate(opts Options) (SourceIterator, error) { func (src *evalSource) Iterate(opts Options) (SourceIterator, error) {

View file

@ -17,17 +17,10 @@ type fixedSource struct {
steps []SourceEvent steps []SourceEvent
} }
func (src *fixedSource) Close() error { func (src *fixedSource) Close() error { return nil }
return nil // nothing to do. func (src *fixedSource) Project() tokens.PackageName { return src.ctx }
} func (src *fixedSource) Info() interface{} { return nil }
func (src *fixedSource) Refresh() bool { return false }
func (src *fixedSource) Project() tokens.PackageName {
return src.ctx
}
func (src *fixedSource) Info() interface{} {
return nil
}
func (src *fixedSource) Iterate(opts Options) (SourceIterator, error) { func (src *fixedSource) Iterate(opts Options) (SourceIterator, error) {
return &fixedSourceIterator{ return &fixedSourceIterator{

View file

@ -14,17 +14,10 @@ var NullSource Source = &nullSource{}
type nullSource struct { type nullSource struct {
} }
func (src *nullSource) Close() error { func (src *nullSource) Close() error { return nil }
return nil // nothing to do. func (src *nullSource) Project() tokens.PackageName { return "" }
} func (src *nullSource) Info() interface{} { return nil }
func (src *nullSource) Refresh() bool { return false }
func (src *nullSource) Project() tokens.PackageName {
return ""
}
func (src *nullSource) Info() interface{} {
return nil
}
func (src *nullSource) Iterate(opts Options) (SourceIterator, error) { func (src *nullSource) Iterate(opts Options) (SourceIterator, error) {
return &nullSourceIterator{}, nil return &nullSourceIterator{}, nil

View file

@ -0,0 +1,105 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
package deploy
import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/workspace"
)
// NewRefreshSource returns a new source that generates events based on reading an existing checkpoint state,
// combined with refreshing its associated resource state from the cloud provider.
func NewRefreshSource(plugctx *plugin.Context, proj *workspace.Project, target *Target, dryRun bool) Source {
return &refreshSource{
plugctx: plugctx,
proj: proj,
target: target,
dryRun: dryRun,
}
}
// A refreshSource refreshes resource state from the cloud provider.
type refreshSource struct {
plugctx *plugin.Context
proj *workspace.Project
target *Target
dryRun bool
}
func (src *refreshSource) Close() error { return nil }
func (src *refreshSource) Project() tokens.PackageName { return src.proj.Name }
func (src *refreshSource) Info() interface{} { return nil }
func (src *refreshSource) Refresh() bool { return true }
func (src *refreshSource) Iterate(opts Options) (SourceIterator, error) {
var states []*resource.State
if snap := src.target.Snapshot; snap != nil {
states = snap.Resources
}
return &refreshSourceIterator{
plugctx: src.plugctx,
states: states,
current: -1,
}, nil
}
// refreshSourceIterator returns state from an existing snapshot, augmented by consulting the resource provider.
type refreshSourceIterator struct {
plugctx *plugin.Context
states []*resource.State
current int
}
func (iter *refreshSourceIterator) Close() error {
return nil // nothing to do.
}
func (iter *refreshSourceIterator) Next() (SourceEvent, error) {
for {
iter.current++
if iter.current >= len(iter.states) {
return nil, nil
}
goal, err := iter.newRefreshGoal(iter.states[iter.current])
if err != nil {
return nil, err
} else if goal != nil {
return &refreshSourceEvent{goal: goal}, nil
}
// If the goal was nil, it means the resource was deleted, and we should keep going.
}
}
// newRefreshGoal refreshes the state, if appropriate, and returns a new goal state.
func (iter *refreshSourceIterator) newRefreshGoal(s *resource.State) (*resource.Goal, error) {
// If this is a custom resource, go ahead and load up its plugin, and ask it to refresh the state.
if s.Custom {
provider, err := iter.plugctx.Host.Provider(s.Type.Package(), nil)
if err != nil {
return nil, errors.Wrapf(err, "fetching provider to refresh %s", s.URN)
}
refreshed, err := provider.Read(s.URN, s.ID, s.Outputs)
if err != nil {
return nil, errors.Wrapf(err, "refreshing %s's state", s.URN)
} else if refreshed == nil {
return nil, nil // the resource was deleted.
}
s = resource.NewState(
s.Type, s.URN, s.Custom, s.Delete, s.ID, s.Inputs, refreshed, s.Parent, s.Protect, s.Dependencies)
}
// Now just return the actual state as the goal state.
return resource.NewGoal(s.Type, s.URN.Name(), s.Custom, s.Outputs, s.Parent, s.Protect, s.Dependencies), nil
}
type refreshSourceEvent struct {
goal *resource.Goal
}
func (rse *refreshSourceEvent) event() {}
func (rse *refreshSourceEvent) Goal() *resource.Goal { return rse.goal }
func (rse *refreshSourceEvent) Done(result *RegisterResult) {}

View file

@ -147,7 +147,7 @@ func (s *CreateStep) Logical() bool { return !s.replacing }
func (s *CreateStep) Apply(preview bool) (resource.Status, error) { func (s *CreateStep) Apply(preview bool) (resource.Status, error) {
if !preview { if !preview {
if s.new.Custom { if s.new.Custom && !s.iter.p.Refresh() {
// Invoke the Create RPC function for this provider: // Invoke the Create RPC function for this provider:
prov, err := getProvider(s) prov, err := getProvider(s)
if err != nil { if err != nil {
@ -195,6 +195,7 @@ func NewDeleteStep(iter *PlanIterator, old *resource.State) Step {
old: old, old: old,
} }
} }
func NewDeleteReplacementStep(iter *PlanIterator, old *resource.State, pendingDelete bool) Step { func NewDeleteReplacementStep(iter *PlanIterator, old *resource.State, pendingDelete bool) Step {
contract.Assert(old != nil) contract.Assert(old != nil)
contract.Assert(old.URN != "") contract.Assert(old.URN != "")
@ -230,7 +231,7 @@ func (s *DeleteStep) Apply(preview bool) (resource.Status, error) {
} }
if !preview { if !preview {
if s.old.Custom { if s.old.Custom && !s.iter.p.Refresh() {
// Invoke the Delete RPC function for this provider: // Invoke the Delete RPC function for this provider:
prov, err := getProvider(s) prov, err := getProvider(s)
if err != nil { if err != nil {
@ -289,12 +290,12 @@ func (s *UpdateStep) Res() *resource.State { return s.new }
func (s *UpdateStep) Logical() bool { return true } func (s *UpdateStep) Logical() bool { return true }
func (s *UpdateStep) Apply(preview bool) (resource.Status, error) { func (s *UpdateStep) Apply(preview bool) (resource.Status, error) {
if preview { // Always propagate the URN and ID, even in previews and refreshes.
// In the case of an update, the URN and ID are the same, however, the outputs remain unknown.
s.new.URN = s.old.URN s.new.URN = s.old.URN
s.new.ID = s.old.ID s.new.ID = s.old.ID
} else {
if s.new.Custom { if !preview {
if s.new.Custom && !s.iter.p.Refresh() {
// Invoke the Update RPC function for this provider: // Invoke the Update RPC function for this provider:
prov, err := getProvider(s) prov, err := getProvider(s)
if err != nil { if err != nil {
@ -308,7 +309,6 @@ func (s *UpdateStep) Apply(preview bool) (resource.Status, error) {
} }
// Now copy any output state back in case the update triggered cascading updates to other properties. // Now copy any output state back in case the update triggered cascading updates to other properties.
s.new.ID = s.old.ID
s.new.Outputs = outs s.new.Outputs = outs
} }

View file

@ -6,7 +6,8 @@ import (
"github.com/pulumi/pulumi/pkg/tokens" "github.com/pulumi/pulumi/pkg/tokens"
) )
// Goal is a desired state for a resource object. // Goal is a desired state for a resource object. Normally it represents a subset of the resource's state expressed by
// a program, however if Output is true, it represents a more complete, post-deployment view of the state.
type Goal struct { type Goal struct {
Type tokens.Type // the type of resource. Type tokens.Type // the type of resource.
Name tokens.QName // the name for the resource's URN. Name tokens.QName // the name for the resource's URN.