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 ...
2018-04-10 20:22:39 +02:00
|
|
|
// 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 }
|
2018-04-18 20:12:02 +02:00
|
|
|
func (src *refreshSource) IsRefresh() bool { return true }
|
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 ...
2018-04-10 20:22:39 +02:00
|
|
|
|
|
|
|
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) {}
|