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.
855 lines
32 KiB
Go
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
|
|
}
|