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.
var analyzers []string
var color colorFlag
var diffDisplay bool
var parallel int
var force bool
var preview bool
var showConfig bool
var showReplacementSteps bool
var showSames bool
var diffDisplay bool
var cmd = &cobra.Command{
Use: "destroy",
@ -108,6 +108,11 @@ func newDestroyCmd() *cobra.Command {
cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{},
"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(
&parallel, "parallel", "p", 0,
"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(
&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(
&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
}

View file

@ -17,11 +17,11 @@ func newPreviewCmd() *cobra.Command {
// Flags for engine.UpdateOptions.
var analyzers []string
var color colorFlag
var diffDisplay bool
var parallel int
var showConfig bool
var showReplacementSteps bool
var showSames bool
var diffDisplay bool
var cmd = &cobra.Command{
Use: "preview",
@ -43,11 +43,14 @@ func newPreviewCmd() *cobra.Command {
"Choose a stack other than the currently selected one")
// Flags for engine.UpdateOptions.
cmd.PersistentFlags().VarP(
&color, "color", "c", "Colorize output. Choices are: always, never, raw, auto")
cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{},
"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(
&parallel, "parallel", "p", 0,
"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(
&showSames, "show-sames", false,
"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
}

View file

@ -86,6 +86,7 @@ func NewPulumiCmd() *cobra.Command {
cmd.AddCommand(newNewCmd())
cmd.AddCommand(newPluginCmd())
cmd.AddCommand(newPreviewCmd())
cmd.AddCommand(newRefreshCmd())
cmd.AddCommand(newStackCmd())
cmd.AddCommand(newUpdateCmd())
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 {
var debug bool
var stack string
var message string
var stack string
// Flags for engine.UpdateOptions.
var analyzers []string
var color colorFlag
var diffDisplay bool
var parallel int
var preview bool
var force bool
var showConfig bool
var showReplacementSteps bool
var showSames bool
var diffDisplay bool
var cmd = &cobra.Command{
Use: "update",
@ -105,6 +104,11 @@ func newUpdateCmd() *cobra.Command {
cmd.PersistentFlags().StringSliceVar(
&analyzers, "analyzer", []string{},
"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(
&parallel, "parallel", "p", 0,
"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(
&showSames, "show-sames", false,
"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
}

View file

@ -35,7 +35,9 @@ type Backend interface {
// Update updates the target stack with the current workspace's contents (config and code).
Update(stackName tokens.QName, proj *workspace.Project, root string,
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(stackName tokens.QName, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error

View file

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

View file

@ -26,7 +26,9 @@ type UpdateKind string
const (
UpdateKindUpdate UpdateKind = "update"
UpdateKindRefresh UpdateKind = "refresh"
UpdateKindDestroy UpdateKind = "destroy"
UpdateKindImport UpdateKind = "import"
)
// 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 {
endpoint = "update"
}
case UpdateKindRefresh:
contract.Failf("Refresh not yet supported for managed stacks [pulumi/pulumi#1081]")
case UpdateKindDestroy:
endpoint = "destroy"
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)
}
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,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
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")
}
if !opts.Force && !opts.Preview {
return errors.New("--update or --preview must be passed when updating a local stack")
}
return b.performEngineOp(
"updating", backend.DeployUpdate,
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(
stackName tokens.QName, proj *workspace.Project, root string,
func (b *localBackend) Refresh(stackName tokens.QName, proj *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
if !opts.Force && !opts.Preview {
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)
return b.performEngineOp("refreshing", backend.RefreshUpdate,
stackName, proj, root, m, opts, displayOpts, opts.Preview, engine.Refresh)
}
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 func(engine.UpdateInfo, chan<- engine.Event, engine.UpdateOptions, bool) (
engine.ResourceChanges, error)) error {
func (b *localBackend) Destroy(stackName tokens.QName, proj *workspace.Project, root string,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return b.performEngineOp("destroying", backend.DestroyUpdate,
stackName, proj, root, m, opts, displayOpts, opts.Preview, engine.Destroy)
}
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)
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)
}
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,
m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return backend.DestroyStack(s, proj, root, m, opts, displayOpts)

View file

@ -26,6 +26,9 @@ type Stack interface {
// Update this stack.
Update(proj *workspace.Project, root string,
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(proj *workspace.Project, root string,
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)
}
// 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.
func DestroyStack(s Stack, proj *workspace.Project, root string,
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error {

View file

@ -25,6 +25,8 @@ const (
DeployUpdate UpdateKind = "update"
// PreviewUpdate is a preview of an update, without impacting resources.
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 UpdateKind = "destroy"
)

View file

@ -143,11 +143,13 @@ func GetResourcePropertiesDetails(
replaces = step.Keys
}
old := step.Old
new := step.New
old, new := step.Old, step.New
if old == nil && new != nil {
printObject(&b, new.Inputs, planning, indent, step.Op, false, debug)
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)
}
} else if new == nil && old != nil {
// 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,
@ -155,6 +157,8 @@ func GetResourcePropertiesDetails(
if !summary {
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 {
printOldNewDiffs(&b, old.Inputs, new.Inputs, replaces, planning, indent, step.Op, summary, debug)
}

View file

@ -68,9 +68,10 @@ type planOptions struct {
// creates resources to compare against the current checkpoint state (e.g., by evaluating a program, etc).
SourceFunc planSourceFunc
DOT bool // true if we should print the DOT file for this plan.
Events eventEmitter // the channel to write events from the engine to.
Diag diag.Sink // the sink to use for diag'ing.
SkipOutputs bool // true if we we should skip printing outputs separately.
DOT bool // true if we should print the DOT file for this plan.
Events eventEmitter // the channel to write events from the engine to.
Diag diag.Sink // the sink to use for diag'ing.
}
// planSourceFunc is a callback that will be used to prepare for, and evaluate, the "new" state for a stack.

View file

@ -54,9 +54,10 @@ func preview(ctx *planContext, opts planOptions) error {
}
type previewActions struct {
Ops map[deploy.StepOp]int
Opts planOptions
Seen map[resource.URN]deploy.Step
Refresh bool
Ops map[deploy.StepOp]int
Opts planOptions
Seen map[resource.URN]deploy.Step
}
func newPreviewActions(opts planOptions) *previewActions {
@ -69,9 +70,7 @@ func newPreviewActions(opts planOptions) *previewActions {
func (acts *previewActions) OnResourceStepPre(step deploy.Step) (interface{}, error) {
acts.Seen[step.URN()] = step
acts.Opts.Events.resourcePreEvent(step, true /*planning*/, acts.Opts.Debug)
return nil, nil
}
@ -96,7 +95,10 @@ func (acts *previewActions) OnResourceStepPost(ctx interface{},
func (acts *previewActions) OnResourceOutputs(step deploy.Step) error {
assertSeen(acts.Seen, step)
acts.Opts.Events.resourceOutputsEvent(step, true /*planning*/, acts.Opts.Debug)
// 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)
}
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) Olds() map[resource.URN]*resource.State { return p.olds }
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 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.
// Use the resource goal state name to produce a globally unique URN.
res := e.Goal()
goal := e.Goal()
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.
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] {
invalid = true
// TODO[pulumi/pulumi-framework#19]: improve this error message!
@ -244,42 +245,59 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
}
iter.urns[urn] = true
// Produce a new state object that we'll build up as operations are performed. It begins with empty outputs.
// 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.
// Check for an old resource so that we can figure out if this is a create, delete, etc., and/or to diff.
old, hasOld := iter.p.Olds()[urn]
var olds resource.PropertyMap
var oldState resource.PropertyMap
var oldInputs resource.PropertyMap
var oldOutputs resource.PropertyMap
if hasOld {
olds = old.Inputs
oldState = old.All()
oldInputs = old.Inputs
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.
var prov plugin.Provider
var err error
if res.Custom {
if prov, err = iter.Provider(res.Type); err != nil {
if goal.Custom {
if prov, err = iter.Provider(t); err != nil {
return nil, err
}
}
// We only allow unknown property values to be exposed to the provider if we are performing a preview.
allowUnknowns := iter.p.preview
// We only allow unknown property values to be exposed to the provider if we are performing an update preview.
allowUnknowns := iter.p.preview && !refresh
// Ensure the provider is okay with this resource and fetch the inputs to pass to subsequent methods.
news, inputs := new.Inputs, new.Inputs
if prov != nil {
// If this isn't a refresh, ensure the provider is okay with this resource and fetch the inputs to pass to
// subsequent methods. If these are not inputs, we are just going to blindly store the outputs, so skip this.
if prov != nil && !refresh {
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 {
return nil, err
} else if iter.issueCheckErrors(new, urn, failures) {
invalid = true
}
props = 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)
}
var failures []plugin.AnalyzeFailure
failures, err = analyzer.Analyze(new.Type, inputs)
failures, err = analyzer.Analyze(new.Type, props)
if err != nil {
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.
var diff plugin.DiffResult
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
}
}
@ -342,7 +360,11 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
hasChanges = false
case plugin.DiffUnknown:
// 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:
return nil, errors.Errorf(
"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
// 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
inputs, failures, err = prov.Check(urn, nil, news, allowUnknowns)
inputs, failures, err = prov.Check(urn, nil, goal.Properties, allowUnknowns)
if err != nil {
return nil, err
} else if iter.issueCheckErrors(new, urn, failures) {
@ -368,7 +390,7 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
if glog.V(7) {
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:
@ -403,7 +425,7 @@ func (iter *PlanIterator) makeRegisterResouceSteps(e RegisterResourceEvent) ([]S
// If we fell through, it's an update.
iter.updates[urn] = true
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
}

View file

@ -82,17 +82,10 @@ type errorSource struct {
duringIterate bool // if true, the error happens in Iterate; else, Next.
}
func (src *errorSource) Close() error {
return nil // nothing to do.
}
func (src *errorSource) Project() tokens.PackageName {
return ""
}
func (src *errorSource) Info() interface{} {
return nil
}
func (src *errorSource) Close() error { return nil }
func (src *errorSource) Project() tokens.PackageName { return "" }
func (src *errorSource) Info() interface{} { return nil }
func (src *errorSource) Refresh() bool { return false }
func (src *errorSource) Iterate(opts Options) (SourceIterator, error) {
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.
type Source interface {
io.Closer
// Project returns the package name of the Pulumi project we are obtaining resources from.
Project() tokens.PackageName
// Info returns a serializable payload that can be used to stamp snapshots for future reconciliation.
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(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.
type SourceIterator interface {
io.Closer
// Next returns the next event from the source.
Next() (SourceEvent, error)
}

View file

@ -60,9 +60,8 @@ func (src *evalSource) Stack() tokens.QName {
return src.runinfo.Target.Name
}
func (src *evalSource) Info() interface{} {
return src.runinfo
}
func (src *evalSource) Info() interface{} { 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.
func (src *evalSource) Iterate(opts Options) (SourceIterator, error) {

View file

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

View file

@ -14,17 +14,10 @@ var NullSource Source = &nullSource{}
type nullSource struct {
}
func (src *nullSource) Close() error {
return nil // nothing to do.
}
func (src *nullSource) Project() tokens.PackageName {
return ""
}
func (src *nullSource) Info() interface{} {
return nil
}
func (src *nullSource) Close() error { return nil }
func (src *nullSource) Project() tokens.PackageName { return "" }
func (src *nullSource) Info() interface{} { return nil }
func (src *nullSource) Refresh() bool { return false }
func (src *nullSource) Iterate(opts Options) (SourceIterator, error) {
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) {
if !preview {
if s.new.Custom {
if s.new.Custom && !s.iter.p.Refresh() {
// Invoke the Create RPC function for this provider:
prov, err := getProvider(s)
if err != nil {
@ -195,6 +195,7 @@ func NewDeleteStep(iter *PlanIterator, old *resource.State) Step {
old: old,
}
}
func NewDeleteReplacementStep(iter *PlanIterator, old *resource.State, pendingDelete bool) Step {
contract.Assert(old != nil)
contract.Assert(old.URN != "")
@ -230,7 +231,7 @@ func (s *DeleteStep) Apply(preview bool) (resource.Status, error) {
}
if !preview {
if s.old.Custom {
if s.old.Custom && !s.iter.p.Refresh() {
// Invoke the Delete RPC function for this provider:
prov, err := getProvider(s)
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) Apply(preview bool) (resource.Status, error) {
if preview {
// 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.ID = s.old.ID
} else {
if s.new.Custom {
// Always propagate the URN and ID, even in previews and refreshes.
s.new.URN = s.old.URN
s.new.ID = s.old.ID
if !preview {
if s.new.Custom && !s.iter.p.Refresh() {
// Invoke the Update RPC function for this provider:
prov, err := getProvider(s)
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.
s.new.ID = s.old.ID
s.new.Outputs = outs
}

View file

@ -6,7 +6,8 @@ import (
"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 tokens.Type // the type of resource.
Name tokens.QName // the name for the resource's URN.