pulumi/pkg/resource/deploy/step.go
Pat Gavlin 6e5c4a38d8
Defer all diffs to resource providers. (#2849)
Thse changes make a subtle but critical adjustment to the process the
Pulumi engine uses to determine whether or not a difference exists
between a resource's actual and desired states, and adjusts the way this
difference is calculated and displayed accordingly.

Today, the Pulumi engine get the first chance to decide whether or not
there is a difference between a resource's actual and desired states. It
does this by comparing the current set of inputs for a resource (i.e.
the inputs from the running Pulumi program) with the last set of inputs
used to update the resource. If there is no difference between the old
and new inputs, the engine decides that no change is necessary without
consulting the resource's provider. Only if there are changes does the
engine consult the resource's provider for more information about the
difference. This can be problematic for a number of reasons:

- Not all providers do input-input comparison; some do input-state
  comparison
- Not all providers are able to update the last deployed set of inputs
  when performing a refresh
- Some providers--either intentionally or due to bugs--may see changes
  in resources whose inputs have not changed

All of these situations are confusing at the very least, and the first
is problematic with respect to correctness. Furthermore, the display
code only renders diffs it observes rather than rendering the diffs
observed by the provider, which can obscure the actual changes detected
at runtime.

These changes address both of these issues:
- Rather than comparing the current inputs against the last inputs
  before calling a resource provider's Diff function, the engine calls
  the Diff function in all cases.
- Providers may now return a list of properties that differ between the
  requested and actual state and the way in which they differ. This
  information will then be used by the CLI to render the diff
  appropriately. A provider may also indicate that a particular diff is
  between old and new inputs rather than old state and new inputs.

Fixes #2453.
2019-07-01 12:34:19 -07:00

855 lines
32 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deploy
import (
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// StepCompleteFunc is the type of functions returned from Step.Apply. These functions are to be called
// when the engine has fully retired a step.
type StepCompleteFunc func()
// Step is a specification for a deployment operation.
type Step interface {
// Apply applies or previews this step. It returns the status of the resource after the step application,
// a function to call to signal that this step has fully completed, and an error, if one occurred while applying
// the step.
//
// The returned StepCompleteFunc, if not nil, must be called after committing the results of this step into
// the state of the deployment.
Apply(preview bool) (resource.Status, StepCompleteFunc, error) // applies or previews this step.
Op() StepOp // the operation performed by this step.
URN() resource.URN // the resource URN (for before and after).
Type() tokens.Type // the type affected by this step.
Provider() string // the provider reference for this step.
Old() *resource.State // the state of the resource before performing this step.
New() *resource.State // the state of the resource after performing this step.
Res() *resource.State // the latest state for the resource that is known (worst case, old).
Logical() bool // true if this step represents a logical operation in the program.
Plan() *Plan // the owning plan.
}
// SameStep is a mutating step that does nothing.
type SameStep struct {
plan *Plan // the current plan.
reg RegisterResourceEvent // the registration intent to convey a URN back to.
old *resource.State // the state of the resource before this step.
new *resource.State // the state of the resource after this step.
}
var _ Step = (*SameStep)(nil)
func NewSameStep(plan *Plan, reg RegisterResourceEvent, old *resource.State, new *resource.State) Step {
contract.Assert(old != nil)
contract.Assert(old.URN != "")
contract.Assert(old.ID != "" || !old.Custom)
contract.Assert(!old.Custom || old.Provider != "" || providers.IsProviderType(old.Type))
contract.Assert(!old.Delete)
contract.Assert(new != nil)
contract.Assert(new.URN != "")
contract.Assert(new.ID == "")
contract.Assert(!new.Custom || new.Provider != "" || providers.IsProviderType(new.Type))
contract.Assert(!new.Delete)
return &SameStep{
plan: plan,
reg: reg,
old: old,
new: new,
}
}
func (s *SameStep) Op() StepOp { return OpSame }
func (s *SameStep) Plan() *Plan { return s.plan }
func (s *SameStep) Type() tokens.Type { return s.new.Type }
func (s *SameStep) Provider() string { return s.new.Provider }
func (s *SameStep) URN() resource.URN { return s.new.URN }
func (s *SameStep) Old() *resource.State { return s.old }
func (s *SameStep) New() *resource.State { return s.new }
func (s *SameStep) Res() *resource.State { return s.new }
func (s *SameStep) Logical() bool { return true }
func (s *SameStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
// Retain the ID, and outputs:
s.new.ID = s.old.ID
s.new.Outputs = s.old.Outputs
complete := func() { s.reg.Done(&RegisterResult{State: s.new, Stable: true}) }
return resource.StatusOK, complete, nil
}
// CreateStep is a mutating step that creates an entirely new resource.
type CreateStep struct {
plan *Plan // the current plan.
reg RegisterResourceEvent // the registration intent to convey a URN back to.
old *resource.State // the state of the existing resource (only for replacements).
new *resource.State // the state of the resource after this step.
keys []resource.PropertyKey // the keys causing replacement (only for replacements).
diffs []resource.PropertyKey // the keys causing a diff (only for replacements).
detailedDiff map[string]plugin.PropertyDiff // the structured property diff (only for replacements).
replacing bool // true if this is a create due to a replacement.
pendingDelete bool // true if this replacement should create a pending delete.
}
var _ Step = (*CreateStep)(nil)
func NewCreateStep(plan *Plan, reg RegisterResourceEvent, new *resource.State) Step {
contract.Assert(reg != nil)
contract.Assert(new != nil)
contract.Assert(new.URN != "")
contract.Assert(new.ID == "")
contract.Assert(!new.Custom || new.Provider != "" || providers.IsProviderType(new.Type))
contract.Assert(!new.Delete)
contract.Assert(!new.External)
return &CreateStep{
plan: plan,
reg: reg,
new: new,
}
}
func NewCreateReplacementStep(plan *Plan, reg RegisterResourceEvent, old, new *resource.State,
keys, diffs []resource.PropertyKey, detailedDiff map[string]plugin.PropertyDiff, pendingDelete bool) Step {
contract.Assert(reg != nil)
contract.Assert(old != nil)
contract.Assert(old.URN != "")
contract.Assert(old.ID != "" || !old.Custom)
contract.Assert(!old.Delete)
contract.Assert(new != nil)
contract.Assert(new.URN != "")
contract.Assert(new.ID == "")
contract.Assert(!new.Custom || new.Provider != "" || providers.IsProviderType(new.Type))
contract.Assert(!new.Delete)
contract.Assert(!new.External)
return &CreateStep{
plan: plan,
reg: reg,
old: old,
new: new,
keys: keys,
diffs: diffs,
detailedDiff: detailedDiff,
replacing: true,
pendingDelete: pendingDelete,
}
}
func (s *CreateStep) Op() StepOp {
if s.replacing {
return OpCreateReplacement
}
return OpCreate
}
func (s *CreateStep) Plan() *Plan { return s.plan }
func (s *CreateStep) Type() tokens.Type { return s.new.Type }
func (s *CreateStep) Provider() string { return s.new.Provider }
func (s *CreateStep) URN() resource.URN { return s.new.URN }
func (s *CreateStep) Old() *resource.State { return s.old }
func (s *CreateStep) New() *resource.State { return s.new }
func (s *CreateStep) Res() *resource.State { return s.new }
func (s *CreateStep) Keys() []resource.PropertyKey { return s.keys }
func (s *CreateStep) Diffs() []resource.PropertyKey { return s.diffs }
func (s *CreateStep) DetailedDiff() map[string]plugin.PropertyDiff { return s.detailedDiff }
func (s *CreateStep) Logical() bool { return !s.replacing }
func (s *CreateStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
var resourceError error
resourceStatus := resource.StatusOK
if !preview {
if s.new.Custom {
// Invoke the Create RPC function for this provider:
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, nil, err
}
id, outs, rst, err := prov.Create(s.URN(), s.new.Inputs)
if err != nil {
if rst != resource.StatusPartialFailure {
return rst, nil, err
}
resourceError = err
resourceStatus = rst
if initErr, isInitErr := err.(*plugin.InitError); isInitErr {
s.new.InitErrors = initErr.Reasons
}
}
contract.Assert(id != "")
// Copy any of the default and output properties on the live object state.
s.new.ID = id
s.new.Outputs = outs
}
}
// Mark the old resource as pending deletion if necessary.
if s.replacing && s.pendingDelete {
s.old.Delete = true
}
complete := func() { s.reg.Done(&RegisterResult{State: s.new}) }
if resourceError == nil {
return resourceStatus, complete, nil
}
return resourceStatus, complete, resourceError
}
// DeleteStep is a mutating step that deletes an existing resource. If `old` is marked "External",
// DeleteStep is a no-op.
type DeleteStep struct {
plan *Plan // the current plan.
old *resource.State // the state of the existing resource.
replacing bool // true if part of a replacement.
}
var _ Step = (*DeleteStep)(nil)
func NewDeleteStep(plan *Plan, old *resource.State) Step {
contract.Assert(old != nil)
contract.Assert(old.URN != "")
contract.Assert(old.ID != "" || !old.Custom)
contract.Assert(!old.Custom || old.Provider != "" || providers.IsProviderType(old.Type))
return &DeleteStep{
plan: plan,
old: old,
}
}
func NewDeleteReplacementStep(plan *Plan, old *resource.State, pendingReplace bool) Step {
contract.Assert(old != nil)
contract.Assert(old.URN != "")
contract.Assert(old.ID != "" || !old.Custom)
contract.Assert(!old.Custom || old.Provider != "" || providers.IsProviderType(old.Type))
// There are two cases in which we create a delete-replacment step:
//
// 1. When creating the delete steps that occur due to a delete-before-replace
// 2. When creating the delete step that occurs due to a delete-after-replace
//
// In the former case, the persistence layer may require that the resource remain in the
// checkpoint file for purposes of checkpoint integrity. We communicate this case by means
// of the `PendingReplacement` field on `resource.State`, which we set here.
//
// In the latter case, the resource must be deleted, but the deletion may not occur if an earlier step fails.
// The engine requires that the fact that the old resource must be deleted is persisted in the checkpoint so
// that it can issue a deletion of this resource on the next update to this stack.
contract.Assert(pendingReplace != old.Delete)
old.PendingReplacement = pendingReplace
return &DeleteStep{
plan: plan,
old: old,
replacing: true,
}
}
func (s *DeleteStep) Op() StepOp {
if s.old.External {
if s.replacing {
return OpDiscardReplaced
}
return OpReadDiscard
}
if s.replacing {
return OpDeleteReplaced
}
return OpDelete
}
func (s *DeleteStep) Plan() *Plan { return s.plan }
func (s *DeleteStep) Type() tokens.Type { return s.old.Type }
func (s *DeleteStep) Provider() string { return s.old.Provider }
func (s *DeleteStep) URN() resource.URN { return s.old.URN }
func (s *DeleteStep) Old() *resource.State { return s.old }
func (s *DeleteStep) New() *resource.State { return nil }
func (s *DeleteStep) Res() *resource.State { return s.old }
func (s *DeleteStep) Logical() bool { return !s.replacing }
func (s *DeleteStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
// Refuse to delete protected resources.
if s.old.Protect {
return resource.StatusOK, nil,
errors.Errorf("refusing to delete protected resource '%s'", s.old.URN)
}
// Deleting an External resource is a no-op, since Pulumi does not own the lifecycle.
if !preview && !s.old.External {
if s.old.Custom {
// Invoke the Delete RPC function for this provider:
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, nil, err
}
if rst, err := prov.Delete(s.URN(), s.old.ID, s.old.Outputs); err != nil {
return rst, nil, err
}
}
}
return resource.StatusOK, func() {}, nil
}
type RemovePendingReplaceStep struct {
plan *Plan // the current plan.
old *resource.State // the state of the existing resource.
}
func NewRemovePendingReplaceStep(plan *Plan, old *resource.State) Step {
contract.Assert(old != nil)
contract.Assert(old.PendingReplacement)
return &RemovePendingReplaceStep{
plan: plan,
old: old,
}
}
func (s *RemovePendingReplaceStep) Op() StepOp {
return OpRemovePendingReplace
}
func (s *RemovePendingReplaceStep) Plan() *Plan { return s.plan }
func (s *RemovePendingReplaceStep) Type() tokens.Type { return s.old.Type }
func (s *RemovePendingReplaceStep) Provider() string { return s.old.Provider }
func (s *RemovePendingReplaceStep) URN() resource.URN { return s.old.URN }
func (s *RemovePendingReplaceStep) Old() *resource.State { return s.old }
func (s *RemovePendingReplaceStep) New() *resource.State { return nil }
func (s *RemovePendingReplaceStep) Res() *resource.State { return s.old }
func (s *RemovePendingReplaceStep) Logical() bool { return false }
func (s *RemovePendingReplaceStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
return resource.StatusOK, nil, nil
}
// UpdateStep is a mutating step that updates an existing resource's state.
type UpdateStep struct {
plan *Plan // the current plan.
reg RegisterResourceEvent // the registration intent to convey a URN back to.
old *resource.State // the state of the existing resource.
new *resource.State // the newly computed state of the resource after updating.
stables []resource.PropertyKey // an optional list of properties that won't change during this update.
diffs []resource.PropertyKey // the keys causing a diff.
detailedDiff map[string]plugin.PropertyDiff // the structured diff.
}
var _ Step = (*UpdateStep)(nil)
func NewUpdateStep(plan *Plan, reg RegisterResourceEvent, old *resource.State,
new *resource.State, stables, diffs []resource.PropertyKey, detailedDiff map[string]plugin.PropertyDiff) Step {
contract.Assert(old != nil)
contract.Assert(old.URN != "")
contract.Assert(old.ID != "" || !old.Custom)
contract.Assert(!old.Custom || old.Provider != "" || providers.IsProviderType(old.Type))
contract.Assert(!old.Delete)
contract.Assert(new != nil)
contract.Assert(new.URN != "")
contract.Assert(new.ID == "")
contract.Assert(!new.Custom || new.Provider != "" || providers.IsProviderType(new.Type))
contract.Assert(!new.Delete)
contract.Assert(!new.External)
contract.Assert(!old.External)
return &UpdateStep{
plan: plan,
reg: reg,
old: old,
new: new,
stables: stables,
diffs: diffs,
detailedDiff: detailedDiff,
}
}
func (s *UpdateStep) Op() StepOp { return OpUpdate }
func (s *UpdateStep) Plan() *Plan { return s.plan }
func (s *UpdateStep) Type() tokens.Type { return s.new.Type }
func (s *UpdateStep) Provider() string { return s.new.Provider }
func (s *UpdateStep) URN() resource.URN { return s.new.URN }
func (s *UpdateStep) Old() *resource.State { return s.old }
func (s *UpdateStep) New() *resource.State { return s.new }
func (s *UpdateStep) Res() *resource.State { return s.new }
func (s *UpdateStep) Logical() bool { return true }
func (s *UpdateStep) Diffs() []resource.PropertyKey { return s.diffs }
func (s *UpdateStep) DetailedDiff() map[string]plugin.PropertyDiff { return s.detailedDiff }
func (s *UpdateStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
// Always propagate the ID, even in previews and refreshes.
s.new.ID = s.old.ID
var resourceError error
resourceStatus := resource.StatusOK
if !preview {
if s.new.Custom {
// Invoke the Update RPC function for this provider:
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, nil, err
}
// Update to the combination of the old "all" state, but overwritten with new inputs.
outs, rst, upderr := prov.Update(s.URN(), s.old.ID, s.old.Outputs, s.new.Inputs)
if upderr != nil {
if rst != resource.StatusPartialFailure {
return rst, nil, upderr
}
resourceError = upderr
resourceStatus = rst
if initErr, isInitErr := upderr.(*plugin.InitError); isInitErr {
s.new.InitErrors = initErr.Reasons
}
}
// Now copy any output state back in case the update triggered cascading updates to other properties.
s.new.Outputs = outs
}
}
// Finally, mark this operation as complete.
complete := func() { s.reg.Done(&RegisterResult{State: s.new, Stables: s.stables}) }
if resourceError == nil {
return resourceStatus, complete, nil
}
return resourceStatus, complete, resourceError
}
// ReplaceStep is a logical step indicating a resource will be replaced. This is comprised of three physical steps:
// a creation of the new resource, any number of intervening updates of dependents to the new resource, and then
// a deletion of the now-replaced old resource. This logical step is primarily here for tools and visualization.
type ReplaceStep struct {
plan *Plan // the current plan.
old *resource.State // the state of the existing resource.
new *resource.State // the new state snapshot.
keys []resource.PropertyKey // the keys causing replacement.
diffs []resource.PropertyKey // the keys causing a diff.
detailedDiff map[string]plugin.PropertyDiff // the structured property diff.
pendingDelete bool // true if a pending deletion should happen.
}
var _ Step = (*ReplaceStep)(nil)
func NewReplaceStep(plan *Plan, old *resource.State, new *resource.State,
keys, diffs []resource.PropertyKey, detailedDiff map[string]plugin.PropertyDiff, pendingDelete bool) Step {
contract.Assert(old != nil)
contract.Assert(old.URN != "")
contract.Assert(old.ID != "" || !old.Custom)
contract.Assert(!old.Delete)
contract.Assert(new != nil)
contract.Assert(new.URN != "")
// contract.Assert(new.ID == "")
contract.Assert(!new.Delete)
return &ReplaceStep{
plan: plan,
old: old,
new: new,
keys: keys,
diffs: diffs,
detailedDiff: detailedDiff,
pendingDelete: pendingDelete,
}
}
func (s *ReplaceStep) Op() StepOp { return OpReplace }
func (s *ReplaceStep) Plan() *Plan { return s.plan }
func (s *ReplaceStep) Type() tokens.Type { return s.new.Type }
func (s *ReplaceStep) Provider() string { return s.new.Provider }
func (s *ReplaceStep) URN() resource.URN { return s.new.URN }
func (s *ReplaceStep) Old() *resource.State { return s.old }
func (s *ReplaceStep) New() *resource.State { return s.new }
func (s *ReplaceStep) Res() *resource.State { return s.new }
func (s *ReplaceStep) Keys() []resource.PropertyKey { return s.keys }
func (s *ReplaceStep) Diffs() []resource.PropertyKey { return s.diffs }
func (s *ReplaceStep) DetailedDiff() map[string]plugin.PropertyDiff { return s.detailedDiff }
func (s *ReplaceStep) Logical() bool { return true }
func (s *ReplaceStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
// If this is a pending delete, we should have marked the old resource for deletion in the CreateReplacement step.
contract.Assert(!s.pendingDelete || s.old.Delete)
return resource.StatusOK, func() {}, nil
}
// ReadStep is a step indicating that an existing resources will be "read" and projected into the Pulumi object
// model. Resources that are read are marked with the "External" bit which indicates to the engine that it does
// not own this resource's lifeycle.
//
// A resource with a given URN can transition freely between an "external" state and a non-external state. If
// a URN that was previously marked "External" (i.e. was the target of a ReadStep in a previous plan) is the
// target of a RegisterResource in the next plan, a CreateReplacement step will be issued to indicate the transition
// from external to owned. If a URN that was previously not marked "External" is the target of a ReadResource in the
// next plan, a ReadReplacement step will be issued to indicate the transition from owned to external.
type ReadStep struct {
plan *Plan // the plan that produced this read
event ReadResourceEvent // the event that should be signaled upon completion
old *resource.State // the old resource state, if one exists for this urn
new *resource.State // the new resource state, to be used to query the provider
replacing bool // whether or not the new resource is replacing the old resource
}
// NewReadStep creates a new Read step.
func NewReadStep(plan *Plan, event ReadResourceEvent, old *resource.State, new *resource.State) Step {
contract.Assert(new != nil)
contract.Assertf(new.External, "target of Read step must be marked External")
contract.Assertf(new.Custom, "target of Read step must be Custom")
// If Old was given, it's either an external resource or its ID is equal to the
// ID that we are preparing to read.
if old != nil {
contract.Assert(old.ID == new.ID || old.External)
}
return &ReadStep{
plan: plan,
event: event,
old: old,
new: new,
replacing: false,
}
}
// NewReadReplacementStep creates a new Read step with the `replacing` flag set. When executed,
// it will pend deletion of the "old" resource, which must not be an external resource.
func NewReadReplacementStep(plan *Plan, event ReadResourceEvent, old *resource.State, new *resource.State) Step {
contract.Assert(new != nil)
contract.Assertf(new.External, "target of ReadReplacement step must be marked External")
contract.Assertf(new.Custom, "target of ReadReplacement step must be Custom")
contract.Assert(old != nil)
contract.Assertf(!old.External, "old target of ReadReplacement step must not be External")
return &ReadStep{
plan: plan,
event: event,
old: old,
new: new,
replacing: true,
}
}
func (s *ReadStep) Op() StepOp {
if s.replacing {
return OpReadReplacement
}
return OpRead
}
func (s *ReadStep) Plan() *Plan { return s.plan }
func (s *ReadStep) Type() tokens.Type { return s.new.Type }
func (s *ReadStep) Provider() string { return s.new.Provider }
func (s *ReadStep) URN() resource.URN { return s.new.URN }
func (s *ReadStep) Old() *resource.State { return s.old }
func (s *ReadStep) New() *resource.State { return s.new }
func (s *ReadStep) Res() *resource.State { return s.new }
func (s *ReadStep) Logical() bool { return !s.replacing }
func (s *ReadStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
urn := s.new.URN
id := s.new.ID
var resourceError error
resourceStatus := resource.StatusOK
// Unlike most steps, Read steps run during previews. The only time
// we can't run is if the ID we are given is unknown.
if id == "" || id == plugin.UnknownStringValue {
s.new.Outputs = resource.PropertyMap{}
} else {
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, nil, err
}
result, rst, err := prov.Read(urn, id, nil, s.new.Inputs)
if err != nil {
if rst != resource.StatusPartialFailure {
return rst, nil, err
}
resourceError = err
resourceStatus = rst
if initErr, isInitErr := err.(*plugin.InitError); isInitErr {
s.new.InitErrors = initErr.Reasons
}
}
s.new.Outputs = result.Outputs
}
// If we were asked to replace an existing, non-External resource, pend the
// deletion here.
if s.replacing {
s.old.Delete = true
}
complete := func() { s.event.Done(&ReadResult{State: s.new}) }
if resourceError == nil {
return resourceStatus, complete, nil
}
return resourceStatus, complete, resourceError
}
// RefreshStep is a step used to track the progress of a refresh operation. A refresh operation updates the an existing
// resource by reading its current state from its provider plugin. These steps are not issued by the step generator;
// instead, they are issued by the plan executor as the optional first step in plan execution.
type RefreshStep struct {
plan *Plan // the plan that produced this refresh
old *resource.State // the old resource state, if one exists for this urn
new *resource.State // the new resource state, to be used to query the provider
done chan<- bool // the channel to use to signal completion, if any
}
// NewRefreshStep creates a new Refresh step.
func NewRefreshStep(plan *Plan, old *resource.State, done chan<- bool) Step {
contract.Assert(old != nil)
// NOTE: we set the new state to the old state by default so that we don't interpret step failures as deletes.
return &RefreshStep{
plan: plan,
old: old,
new: old,
done: done,
}
}
func (s *RefreshStep) Op() StepOp { return OpRefresh }
func (s *RefreshStep) Plan() *Plan { return s.plan }
func (s *RefreshStep) Type() tokens.Type { return s.old.Type }
func (s *RefreshStep) Provider() string { return s.old.Provider }
func (s *RefreshStep) URN() resource.URN { return s.old.URN }
func (s *RefreshStep) Old() *resource.State { return s.old }
func (s *RefreshStep) New() *resource.State { return s.new }
func (s *RefreshStep) Res() *resource.State { return s.old }
func (s *RefreshStep) Logical() bool { return false }
// ResultOp returns the operation that corresponds to the change to this resource after reading its current state, if
// any.
func (s *RefreshStep) ResultOp() StepOp {
if s.new == nil {
return OpDelete
}
if s.new == s.old || s.old.Outputs.Diff(s.new.Outputs) == nil {
return OpSame
}
return OpUpdate
}
func (s *RefreshStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
var complete func()
if s.done != nil {
complete = func() { close(s.done) }
}
// Component, provider, and pending-replace resources never change with a refresh; just return the current state.
if !s.old.Custom || providers.IsProviderType(s.old.Type) || s.old.PendingReplacement {
return resource.StatusOK, complete, nil
}
// For a custom resource, fetch the resource's provider and read the resource's current state.
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, nil, err
}
var initErrors []string
refreshed, rst, err := prov.Read(s.old.URN, s.old.ID, s.old.Inputs, s.old.Outputs)
if err != nil {
if rst != resource.StatusPartialFailure {
return rst, nil, err
}
if initErr, isInitErr := err.(*plugin.InitError); isInitErr {
initErrors = initErr.Reasons
// Partial failure SHOULD NOT cause refresh to fail. Instead:
//
// 1. Warn instead that during refresh we noticed the resource has become unhealthy.
// 2. Make sure the initialization errors are persisted in the state, so that the next
// `pulumi up` will surface them to the user.
err = nil
msg := fmt.Sprintf("Refreshed resource is in an unhealthy state:\n* %s", strings.Join(initErrors, "\n* "))
s.Plan().Diag().Warningf(diag.RawMessage(s.URN(), msg))
}
}
outputs := refreshed.Outputs
// If the provider specified new inputs for this resource, pick them up now. Otherwise, retain the current inputs.
inputs := s.old.Inputs
if refreshed.Inputs != nil {
inputs = refreshed.Inputs
}
if outputs != nil {
s.new = resource.NewState(s.old.Type, s.old.URN, s.old.Custom, s.old.Delete, s.old.ID, inputs, outputs,
s.old.Parent, s.old.Protect, s.old.External, s.old.Dependencies, initErrors, s.old.Provider,
s.old.PropertyDependencies, s.old.PendingReplacement, s.old.AdditionalSecretOutputs, s.old.Aliases)
} else {
s.new = nil
}
return rst, complete, err
}
// StepOp represents the kind of operation performed by a step. It evaluates to its string label.
type StepOp string
const (
OpSame StepOp = "same" // nothing to do.
OpCreate StepOp = "create" // creating a new resource.
OpUpdate StepOp = "update" // updating an existing resource.
OpDelete StepOp = "delete" // deleting an existing resource.
OpReplace StepOp = "replace" // replacing a resource with a new one.
OpCreateReplacement StepOp = "create-replacement" // creating a new resource for a replacement.
OpDeleteReplaced StepOp = "delete-replaced" // deleting an existing resource after replacement.
OpRead StepOp = "read" // reading an existing resource.
OpReadReplacement StepOp = "read-replacement" // reading an existing resource for a replacement.
OpRefresh StepOp = "refresh" // refreshing an existing resource.
OpReadDiscard StepOp = "discard" // removing a resource that was read.
OpDiscardReplaced StepOp = "discard-replaced" // discarding a read resource that was replaced.
OpRemovePendingReplace StepOp = "remove-pending-replace" // removing a pending replace resource.
)
// StepOps contains the full set of step operation types.
var StepOps = []StepOp{
OpSame,
OpCreate,
OpUpdate,
OpDelete,
OpReplace,
OpCreateReplacement,
OpDeleteReplaced,
OpRead,
OpReadReplacement,
OpRefresh,
OpReadDiscard,
OpDiscardReplaced,
OpRemovePendingReplace,
}
// Color returns a suggested color for lines of this op type.
func (op StepOp) Color() string {
switch op {
case OpSame:
return colors.SpecUnimportant
case OpCreate:
return colors.SpecCreate
case OpDelete:
return colors.SpecDelete
case OpUpdate:
return colors.SpecUpdate
case OpReplace:
return colors.SpecReplace
case OpCreateReplacement:
return colors.SpecCreateReplacement
case OpDeleteReplaced:
return colors.SpecDeleteReplaced
case OpRead:
return colors.SpecCreate
case OpReadReplacement:
return colors.SpecReplace
case OpRefresh:
return colors.SpecUpdate
case OpReadDiscard, OpDiscardReplaced:
return colors.SpecDelete
default:
contract.Failf("Unrecognized resource step op: '%v'", op)
return ""
}
}
// Prefix returns a suggested prefix for lines of this op type.
func (op StepOp) Prefix() string {
return op.Color() + op.RawPrefix()
}
// RawPrefix returns the uncolorized prefix text.
func (op StepOp) RawPrefix() string {
switch op {
case OpSame:
return " "
case OpCreate:
return "+ "
case OpDelete:
return "- "
case OpUpdate:
return "~ "
case OpReplace:
return "+-"
case OpCreateReplacement:
return "++"
case OpDeleteReplaced:
return "--"
case OpRead:
return "> "
case OpReadReplacement:
return ">>"
case OpRefresh:
return "~ "
case OpReadDiscard:
return "< "
case OpDiscardReplaced:
return "<<"
default:
contract.Failf("Unrecognized resource step op: %v", op)
return ""
}
}
func (op StepOp) PastTense() string {
switch op {
case OpSame, OpCreate, OpDelete, OpReplace, OpCreateReplacement, OpDeleteReplaced, OpUpdate, OpReadReplacement:
return string(op) + "d"
case OpRefresh:
return "refreshed"
case OpRead:
return "read"
case OpReadDiscard, OpDiscardReplaced:
return "discarded"
default:
contract.Failf("Unexpected resource step op: %v", op)
return ""
}
}
// Suffix returns a suggested suffix for lines of this op type.
func (op StepOp) Suffix() string {
if op == OpCreateReplacement || op == OpUpdate || op == OpReplace || op == OpReadReplacement || op == OpRefresh {
return colors.Reset // updates and replacements colorize individual lines; get has none
}
return ""
}
// getProvider fetches the provider for the given step.
func getProvider(s Step) (plugin.Provider, error) {
if providers.IsProviderType(s.Type()) {
return s.Plan().providers, nil
}
ref, err := providers.ParseReference(s.Provider())
if err != nil {
return nil, errors.Errorf("bad provider reference '%v' for resource %v: %v", s.Provider(), s.URN(), err)
}
provider, ok := s.Plan().GetProvider(ref)
if !ok {
return nil, errors.Errorf("unknown provider '%v' for resource %v", s.Provider(), s.URN())
}
return provider, nil
}