pulumi/pkg/resource/deploy/step.go
Alex Clemmer f037c7d143 Checkpoint resource initialization errors
When a resource fails to initialize (i.e., it is successfully created,
but fails to transition to a fully-initialized state), and a user
subsequently runs `pulumi update` without changing that resource, our
CLI will fail to warn the user that this resource is not initialized.

This commit begins the process of allowing our CLI to report this by
storing a list of initialization errors in the checkpoint.
2018-07-20 17:59:06 -07:00

493 lines
16 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 (
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// Step is a specification for a deployment operation.
type Step interface {
Apply(preview bool) (resource.Status, 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.
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.Delete)
contract.Assert(new != nil)
contract.Assert(new.URN != "")
contract.Assert(new.ID == "")
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.old.Type }
func (s *SameStep) URN() resource.URN { return s.old.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, error) {
// Retain the URN, ID, and outputs:
s.new.URN = s.old.URN
s.new.ID = s.old.ID
s.new.Outputs = s.old.Outputs
s.reg.Done(&RegisterResult{State: s.new, Stable: true})
return resource.StatusOK, 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).
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.Delete)
return &CreateStep{
plan: plan,
reg: reg,
new: new,
}
}
func NewCreateReplacementStep(plan *Plan, reg RegisterResourceEvent,
old *resource.State, new *resource.State, keys []resource.PropertyKey, 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.Delete)
contract.Assert(old.Type == new.Type)
return &CreateStep{
plan: plan,
reg: reg,
old: old,
new: new,
keys: keys,
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) 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) Logical() bool { return !s.replacing }
func (s *CreateStep) Apply(preview bool) (resource.Status, error) {
var resourceError error
resourceStatus := resource.StatusOK
if !preview {
if s.new.Custom && !s.plan.IsRefresh() {
// Invoke the Create RPC function for this provider:
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, err
}
id, outs, rst, err := prov.Create(s.URN(), s.new.Inputs)
if err != nil {
if rst == resource.StatusUnknown {
return rst, err
}
contract.Assert(rst == resource.StatusPartialFailure)
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
}
s.reg.Done(&RegisterResult{State: s.new})
if resourceError == nil {
return resourceStatus, nil
}
return resourceStatus, resourceError
}
// DeleteStep is a mutating step that deletes an existing resource.
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.Delete)
return &DeleteStep{
plan: plan,
old: old,
}
}
func NewDeleteReplacementStep(plan *Plan, old *resource.State, pendingDelete bool) Step {
contract.Assert(old != nil)
contract.Assert(old.URN != "")
contract.Assert(old.ID != "" || !old.Custom)
contract.Assert(!pendingDelete || old.Delete)
return &DeleteStep{
plan: plan,
old: old,
replacing: true,
}
}
func (s *DeleteStep) Op() StepOp {
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) 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, error) {
// Refuse to delete protected resources.
if s.old.Protect {
return resource.StatusOK,
errors.Errorf("refusing to delete protected resource '%s'", s.old.URN)
}
if !preview {
if s.old.Custom && !s.plan.IsRefresh() {
// Invoke the Delete RPC function for this provider:
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, err
}
if rst, err := prov.Delete(s.URN(), s.old.ID, s.old.All()); err != nil {
return rst, err
}
}
}
return resource.StatusOK, 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.
}
var _ Step = (*UpdateStep)(nil)
func NewUpdateStep(plan *Plan, reg RegisterResourceEvent, old *resource.State,
new *resource.State, stables []resource.PropertyKey) 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)
contract.Assert(old.Type == new.Type)
return &UpdateStep{
plan: plan,
reg: reg,
old: old,
new: new,
stables: stables,
}
}
func (s *UpdateStep) Op() StepOp { return OpUpdate }
func (s *UpdateStep) Plan() *Plan { return s.plan }
func (s *UpdateStep) Type() tokens.Type { return s.old.Type }
func (s *UpdateStep) URN() resource.URN { return s.old.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) Apply(preview bool) (resource.Status, error) {
// Always propagate the URN and ID, even in previews and refreshes.
s.new.URN = s.old.URN
s.new.ID = s.old.ID
var resourceError error
resourceStatus := resource.StatusOK
if !preview {
if s.new.Custom && !s.plan.IsRefresh() {
// Invoke the Update RPC function for this provider:
prov, err := getProvider(s)
if err != nil {
return resource.StatusOK, err
}
// Update to the combination of the old "all" state (including outputs), but overwritten with new inputs.
outs, rst, upderr := prov.Update(s.URN(), s.old.ID, s.old.All(), s.new.Inputs)
if upderr != nil {
if rst == resource.StatusUnknown {
return rst, upderr
}
contract.Assert(rst == resource.StatusPartialFailure)
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.
s.reg.Done(&RegisterResult{State: s.new, Stables: s.stables})
if resourceError == nil {
return resourceStatus, nil
}
return resourceStatus, 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.
pendingDelete bool // true if a pending deletion should happen.
}
var _ Step = (*ReplaceStep)(nil)
func NewReplaceStep(plan *Plan, old *resource.State, new *resource.State,
keys []resource.PropertyKey, 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,
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.old.Type }
func (s *ReplaceStep) URN() resource.URN { return s.old.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) Logical() bool { return true }
func (s *ReplaceStep) Apply(preview bool) (resource.Status, 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, nil
}
// 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.
)
// StepOps contains the full set of step operation types.
var StepOps = []StepOp{
OpSame,
OpCreate,
OpUpdate,
OpDelete,
OpReplace,
OpCreateReplacement,
OpDeleteReplaced,
}
// 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
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 "--"
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:
return string(op) + "d"
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 {
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) {
return s.Plan().Provider(s.Type().Package())
}