pulumi/pkg/resource/deploy/step.go
Justin Van Patten 7f27618e2d
Avoid replace on second update with import applied (#4403)
After importing some resources, and running a second update with the
import still applied, an unexpected replace would occur. This wouldn't
happen for the vast majority of resources, but for some it would.

It turns out that the resources that trigger this are ones that use a
different format of identifier for the import input than they do for the
ID property.

Before this change, we would trigger an import-replacement when an
existing resource's ID property didn't match the import property, which
would be the case for the small set of resources where the input
identifier is different than the ID property.

To avoid this, we now store the `importID` in the statefile, and
compare that to the import property instead of comparing the ID.
2020-04-15 18:52:40 -07:00

1066 lines
40 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/v2/resource/deploy/providers"
"github.com/pulumi/pulumi/sdk/v2/go/common/diag"
"github.com/pulumi/pulumi/sdk/v2/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/logging"
)
// 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.
// If this is a same-step for a resource being created but which was not --target'ed by the user
// (and thus was skipped).
skippedCreate bool
}
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,
}
}
// NewSkippedCreateStep produces a SameStep for a resource that was created but not targeted
// by the user (and thus was skipped). These act as no-op steps (hence 'same') since we are not
// actually creating the resource, but ensure that we complete resource-registration and convey the
// right information downstream. For example, we will not write these into the checkpoint file.
func NewSkippedCreateStep(plan *Plan, reg RegisterResourceEvent, new *resource.State) Step {
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)
// Make the old state here a direct copy of the new state
old := *new
return &SameStep{
plan: plan,
reg: reg,
old: &old,
new: new,
skippedCreate: true,
}
}
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}) }
return resource.StatusOK, complete, nil
}
func (s *SameStep) IsSkippedCreate() bool {
return s.skippedCreate
}
// 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, s.new.CustomTimeouts.Create)
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
}
} else {
s.new.Outputs = s.new.Inputs
}
// 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, s.old.CustomTimeouts.Delete); 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.
ignoreChanges []string // a list of property paths to ignore when updating.
}
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,
ignoreChanges []string) 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,
ignoreChanges: ignoreChanges,
}
}
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,
s.new.CustomTimeouts.Update, s.ignoreChanges)
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
}
} else {
s.new.Outputs = s.new.Inputs
}
// Finally, mark this operation as complete.
complete := func() { s.reg.Done(&RegisterResult{State: s.new}) }
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 == 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
}
}
// If there is no such resource, return an error indicating as such.
if result.Outputs == nil {
return resource.StatusOK, nil, errors.Errorf("resource '%s' does not exist", id)
}
s.new.Outputs = result.Outputs
if result.ID != "" {
s.new.ID = result.ID
}
}
// 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) }
}
resourceID := s.old.ID
// 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, resourceID, 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 {
// There is a chance that the ID has changed. We want to allow this change to happen
// it will have changed already in the outputs, but we need to persist this change
// at a state level because the Id
if refreshed.ID != "" && refreshed.ID != resourceID {
logging.V(7).Infof("Refreshing ID; oldId=%s, newId=%s", resourceID, refreshed.ID)
resourceID = refreshed.ID
}
s.new = resource.NewState(s.old.Type, s.old.URN, s.old.Custom, s.old.Delete, resourceID, 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,
&s.old.CustomTimeouts, s.old.ImportID)
} else {
s.new = nil
}
return rst, complete, err
}
type ImportStep struct {
plan *Plan // the current plan.
reg RegisterResourceEvent // the registration intent to convey a URN back to.
original *resource.State // the original resource, if this is an import-replace.
old *resource.State // the state of the resource fetched from the provider.
new *resource.State // the newly computed state of the resource after importing.
replacing bool // true if we are replacing a Pulumi-managed resource.
diffs []resource.PropertyKey // any keys that differed between the user's program and the actual state.
detailedDiff map[string]plugin.PropertyDiff // the structured property diff.
ignoreChanges []string // a list of property paths to ignore when updating.
}
func NewImportStep(plan *Plan, reg RegisterResourceEvent, new *resource.State, ignoreChanges []string) Step {
contract.Assert(new != nil)
contract.Assert(new.URN != "")
contract.Assert(new.ID != "")
contract.Assert(new.Custom)
contract.Assert(!new.Delete)
contract.Assert(!new.External)
return &ImportStep{
plan: plan,
reg: reg,
new: new,
ignoreChanges: ignoreChanges,
}
}
func NewImportReplacementStep(plan *Plan, reg RegisterResourceEvent, original, new *resource.State,
ignoreChanges []string) Step {
contract.Assert(original != nil)
contract.Assert(new != nil)
contract.Assert(new.URN != "")
contract.Assert(new.ID != "")
contract.Assert(new.Custom)
contract.Assert(!new.Delete)
contract.Assert(!new.External)
return &ImportStep{
plan: plan,
reg: reg,
original: original,
new: new,
replacing: true,
ignoreChanges: ignoreChanges,
}
}
func (s *ImportStep) Op() StepOp {
if s.replacing {
return OpImportReplacement
}
return OpImport
}
func (s *ImportStep) Plan() *Plan { return s.plan }
func (s *ImportStep) Type() tokens.Type { return s.new.Type }
func (s *ImportStep) Provider() string { return s.new.Provider }
func (s *ImportStep) URN() resource.URN { return s.new.URN }
func (s *ImportStep) Old() *resource.State { return s.old }
func (s *ImportStep) New() *resource.State { return s.new }
func (s *ImportStep) Res() *resource.State { return s.new }
func (s *ImportStep) Logical() bool { return !s.replacing }
func (s *ImportStep) Diffs() []resource.PropertyKey { return s.diffs }
func (s *ImportStep) DetailedDiff() map[string]plugin.PropertyDiff { return s.detailedDiff }
func (s *ImportStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
complete := func() { s.reg.Done(&RegisterResult{State: s.new}) }
// Read the current state of the resource to import. If the provider does not hand us back any inputs for the
// resource, it probably needs to be updated. If the resource does not exist at all, fail the import.
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, nil, err
}
read, rst, err := prov.Read(s.new.URN, s.new.ID, nil, nil)
if err != nil {
if initErr, isInitErr := err.(*plugin.InitError); isInitErr {
s.new.InitErrors = initErr.Reasons
} else {
return rst, nil, err
}
}
if read.Outputs == nil {
return rst, nil, errors.Errorf("resource '%v' does not exist", s.new.ID)
}
if read.Inputs == nil {
return resource.StatusOK, nil, errors.Errorf(
"provider does not support importing resources; please try updating the '%v' plugin",
s.new.URN.Type().Package())
}
if read.ID != "" {
s.new.ID = read.ID
}
s.new.Outputs = read.Outputs
// Magic up an old state so the frontend can display a proper diff. This state is the output of the just-executed
// `Read` combined with the resource identity and metadata from the desired state. This ensures that the only
// differences between the old and new states are between the inputs and outputs.
s.old = resource.NewState(s.new.Type, s.new.URN, s.new.Custom, false, s.new.ID, read.Inputs, read.Outputs,
s.new.Parent, s.new.Protect, false, s.new.Dependencies, s.new.InitErrors, s.new.Provider,
s.new.PropertyDependencies, false, nil, nil, &s.new.CustomTimeouts, s.new.ImportID)
// Check the user inputs using the provider inputs for defaults.
inputs, failures, err := prov.Check(s.new.URN, s.old.Inputs, s.new.Inputs, preview)
if err != nil {
return rst, nil, err
}
if issueCheckErrors(s.plan, s.new, s.new.URN, failures) {
return rst, nil, errors.New("one or more inputs failed to validate")
}
s.new.Inputs = inputs
// Diff the user inputs against the provider inputs. If there are any differences, fail the import.
diff, err := diffResource(s.new.URN, s.new.ID, s.old.Inputs, s.old.Outputs, s.new.Inputs, prov, preview,
s.ignoreChanges)
if err != nil {
return rst, nil, err
}
s.diffs, s.detailedDiff = diff.ChangedKeys, diff.DetailedDiff
if diff.Changes != plugin.DiffNone {
const message = "inputs to import do not match the existing resource"
if preview {
s.plan.ctx.Diag.Warningf(diag.StreamMessage(s.new.URN, message+"; importing this resource will fail", 0))
} else {
err = errors.New(message)
}
}
// If we were asked to replace an existing, non-External resource, pend the deletion here.
if err == nil && s.replacing {
s.original.Delete = true
}
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.
OpImport StepOp = "import" // import an existing resource.
OpImportReplacement StepOp = "import-replacement" // replace an existing resource with an imported 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,
OpImport,
OpImportReplacement,
}
// 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, OpImport:
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.SpecRead
case OpReadReplacement, OpImportReplacement:
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 "<<"
case OpImport:
return "= "
case OpImportReplacement:
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"
case OpImport, OpImportReplacement:
return "imported"
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 {
switch op {
case OpCreateReplacement, OpUpdate, OpReplace, OpReadReplacement, OpRefresh, OpImportReplacement:
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
}