ec2b964daa
This scrubs about 80% of our TODOs, as part of pulumi/lumi#212. The remaining 20% will come shortly.
737 lines
27 KiB
Go
737 lines
27 KiB
Go
// Licensed to Pulumi Corporation ("Pulumi") under one or more
|
|
// contributor license agreements. See the NOTICE file distributed with
|
|
// this work for additional information regarding copyright ownership.
|
|
// Pulumi licenses this file to You 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 resource
|
|
|
|
import (
|
|
"github.com/golang/glog"
|
|
|
|
"github.com/pulumi/lumi/pkg/compiler/core"
|
|
"github.com/pulumi/lumi/pkg/compiler/errors"
|
|
"github.com/pulumi/lumi/pkg/diag/colors"
|
|
"github.com/pulumi/lumi/pkg/graph"
|
|
"github.com/pulumi/lumi/pkg/pack"
|
|
"github.com/pulumi/lumi/pkg/tokens"
|
|
"github.com/pulumi/lumi/pkg/util/contract"
|
|
)
|
|
|
|
// TODO[pulumi/lumi#106]: plan parallelism.
|
|
|
|
// Plan is the output of analyzing resource graphs and contains the steps necessary to perform an infrastructure
|
|
// deployment. A plan can be generated out of whole cloth from a resource graph -- in the case of new deployments --
|
|
// however, it can alternatively be generated by diffing two resource graphs -- in the case of updates to existing
|
|
// environments (presumably more common). The plan contains step objects that can be used to drive a deployment.
|
|
type Plan interface {
|
|
Empty() bool // true if the plan is empty.
|
|
Steps() Step // the first step to perform, linked to the rest.
|
|
Replaces() map[URN][]PropertyKey // resources being replaced and their properties.
|
|
Unchanged() map[Resource]Resource // the resources untouched by this plan (map from old to new).
|
|
Apply(prog Progress) (Snapshot, Step, State, error) // performs the operations specified in this plan.
|
|
}
|
|
|
|
// Progress can be used for progress reporting.
|
|
type Progress interface {
|
|
Before(step Step)
|
|
After(step Step, state State, err error)
|
|
}
|
|
|
|
// Step is a specification for a deployment operation.
|
|
type Step interface {
|
|
Plan() Plan // the plan this step belongs to.
|
|
Op() StepOp // the operation that will be performed.
|
|
Logical() bool // true if this is a logical step, rather than a physical one.
|
|
Old() Resource // the old resource state, if any, before performing this step.
|
|
New() Resource // the new resource state, if any, after performing this step.
|
|
NewProps() PropertyMap // the projected new resource state, factoring in dependency updates.
|
|
Next() Step // the next step to perform, or nil if none.
|
|
Apply() (State, error) // performs the operation specified by this step.
|
|
}
|
|
|
|
// StepOp represents the kind of operation performed by this step.
|
|
type StepOp string
|
|
|
|
const (
|
|
OpCreate StepOp = "create" // creating a new resource.
|
|
OpRead StepOp = "read" // reading from an existing 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 (logically).
|
|
OpReplaceCreate StepOp = "replace-create" // the fine-grained replacement step to create the new resource.
|
|
OpReplaceDelete StepOp = "replace-delete" // the fine-grained replacement step to delete the old resource.
|
|
)
|
|
|
|
// Color returns a suggested color for lines of this op type.
|
|
func (op StepOp) Color() string {
|
|
switch op {
|
|
case OpCreate, OpReplaceCreate:
|
|
return colors.SpecAdded
|
|
case OpDelete, OpReplaceDelete:
|
|
return colors.SpecDeleted
|
|
case OpUpdate:
|
|
return colors.SpecChanged
|
|
case OpReplace:
|
|
return colors.SpecReplaced
|
|
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 {
|
|
switch op {
|
|
case OpCreate:
|
|
return op.Color() + "+ "
|
|
case OpDelete:
|
|
return op.Color() + "- "
|
|
case OpUpdate:
|
|
return op.Color() + " "
|
|
case OpReplace:
|
|
return op.Color() + "-+"
|
|
case OpReplaceCreate:
|
|
return op.Color() + "~+"
|
|
case OpReplaceDelete:
|
|
return op.Color() + "~-"
|
|
default:
|
|
contract.Failf("Unrecognized resource step op: %v", op)
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Suffix returns a suggested suffix for lines of this op type.
|
|
func (op StepOp) Suffix() string {
|
|
if op == OpUpdate || op == OpReplace {
|
|
return colors.Reset // updates and replacements colorize individual lines
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// StepOps returns the full set of step operation types.
|
|
func StepOps() []StepOp {
|
|
return []StepOp{
|
|
OpCreate,
|
|
OpUpdate,
|
|
OpDelete,
|
|
OpReplace,
|
|
OpReplaceCreate,
|
|
OpReplaceDelete,
|
|
}
|
|
}
|
|
|
|
// NewPlan analyzes a resource graph new compared to an optional old resource graph old, and creates a plan
|
|
// that will carry out operations necessary to bring the old resource graph in line with the new one. It is possible
|
|
// for old, new, or both to be nil; combinations of these can be used to create different kinds of plans: (1) a creation
|
|
// plan from a new snapshot when old doesn't exist (nil), (2) an update plan when both old and new exist, and (3) a
|
|
// deletion plan when old exists, but not new, and (4) an "empty plan" when both are nil.
|
|
func NewPlan(ctx *Context, old Snapshot, new Snapshot, analyzers []tokens.QName) (Plan, error) {
|
|
return newPlan(ctx, old, new, analyzers)
|
|
}
|
|
|
|
type plan struct {
|
|
ctx *Context // this plan's context.
|
|
ns tokens.QName // the namespace target being deployed into.
|
|
pkg tokens.Package // the package from which this snapshot came.
|
|
args core.Args // the arguments used to compile this package.
|
|
first *step // the first step to take.
|
|
replaces map[URN][]PropertyKey // resources being replaced and their properties.
|
|
unchanged map[Resource]Resource // the resources that are remaining the same without modification.
|
|
}
|
|
|
|
var _ Plan = (*plan)(nil)
|
|
|
|
func (p *plan) Replaces() map[URN][]PropertyKey { return p.replaces }
|
|
func (p *plan) Unchanged() map[Resource]Resource { return p.unchanged }
|
|
func (p *plan) Empty() bool { return p.Steps() == nil }
|
|
|
|
func (p *plan) Steps() Step {
|
|
if p.first == nil {
|
|
return nil
|
|
}
|
|
return p.first
|
|
}
|
|
|
|
// Provider fetches the provider for a given resource, possibly lazily allocating the plugins for it. If a provider
|
|
// could not be found, or an error occurred while creating it, a non-nil error is returned.
|
|
func (p *plan) Provider(res Resource) (Provider, error) {
|
|
t := res.Type()
|
|
pkg := t.Package()
|
|
return p.ctx.Host.Provider(pkg)
|
|
}
|
|
|
|
// Apply performs all steps in the plan, calling out to the progress reporting functions as desired. It returns four
|
|
// things: the resulting Snapshot, no matter whether an error occurs or not; an error, if something went wrong; the step
|
|
// that failed, if the error is non-nil; and finally the state of the resource modified in the failing step.
|
|
func (p *plan) Apply(prog Progress) (Snapshot, Step, State, error) {
|
|
// First go ahead and propagate IDs for unchanged resources.
|
|
for old, new := range p.unchanged {
|
|
contract.Assert(old.HasID())
|
|
contract.Assert(!new.HasID())
|
|
id := old.ID()
|
|
new.SetID(id)
|
|
new.SetOutputsFrom(old)
|
|
p.ctx.IDURN[id] = new.URN()
|
|
}
|
|
|
|
// Next, walk the plan linked list and apply each step.
|
|
var res []Resource
|
|
var rst State
|
|
var err error
|
|
|
|
stepno := 1
|
|
step := p.Steps()
|
|
for step != nil {
|
|
if prog != nil {
|
|
prog.Before(step)
|
|
}
|
|
|
|
rst, err = step.Apply()
|
|
if prog != nil {
|
|
prog.After(step, rst, err)
|
|
}
|
|
|
|
// If an error occurred, append the old step to the list (and all subsequent steps). Else, the new one.
|
|
if err != nil {
|
|
old := step.Old()
|
|
glog.V(7).Infof("Plan step #%v failed [%v]; hasold = %v: %v", stepno, step.Op(), old != nil, err)
|
|
if old != nil {
|
|
res = append(res, old)
|
|
}
|
|
rest := step.Next()
|
|
for rest != nil {
|
|
restres := rest.Old()
|
|
glog.V(7).Infof("Plan step #%v rest.old=%v", restres != nil)
|
|
if restres != nil && !step.Logical() {
|
|
res = append(res, restres) // track all remaining physical resources
|
|
}
|
|
rest = rest.Next()
|
|
}
|
|
break
|
|
} else {
|
|
new := step.New()
|
|
glog.V(7).Infof("Plan step #%v succeeded [%v]; hasnew = %v", stepno, step.Op(), new != nil)
|
|
if new != nil && !step.Logical() {
|
|
res = append(res, new) // track all new physical resources
|
|
}
|
|
}
|
|
|
|
step = step.Next()
|
|
stepno++
|
|
}
|
|
|
|
// Append all the resources that aren't getting modified.
|
|
glog.V(7).Infof("Adding %v unchanged resource(s) to checkpoint", len(p.unchanged))
|
|
for _, unres := range p.unchanged {
|
|
res = append(res, unres)
|
|
}
|
|
|
|
// Finally, produce a new snapshot and return the resulting information.
|
|
return p.checkpoint(res), step, rst, err
|
|
}
|
|
|
|
// checkpoint takes the outputs from a plan application and returns it so that it's suitable for persistence.
|
|
func (p *plan) checkpoint(resources []Resource) Snapshot {
|
|
// Produce a resource graph and then topsort it. Store the result of that.
|
|
g := newResourceGraph(resources)
|
|
topverts, err := graph.Topsort(g)
|
|
contract.Assertf(err == nil, "Fatal inability to topsort plan's output resources; checkpoint impossible")
|
|
var tops []Resource
|
|
for _, topvert := range topverts {
|
|
tops = append(tops, topvert.Data().(Resource))
|
|
}
|
|
glog.V(7).Infof("Checkpointing plan application: %v total resources", len(tops))
|
|
return NewSnapshot(p.ctx, p.ns, p.pkg, p.args, tops)
|
|
}
|
|
|
|
// newPlan handles all three cases: (1) a creation plan from a new snapshot when old doesn't exist (nil), (2) an update
|
|
// plan when both old and new exist, and (3) a deletion plan when old exists, but not new.
|
|
func newPlan(ctx *Context, old Snapshot, new Snapshot, analyzers []tokens.QName) (*plan, error) {
|
|
// Create a new plan builder and then proceed to do some building.
|
|
pb := newPlanBuilder(ctx, old, new)
|
|
if glog.V(7) {
|
|
glog.V(7).Infof("Creating plan with #old=%v #new=%v #analyzers=%v\n",
|
|
len(pb.OldRes), len(pb.NewRes), len(analyzers))
|
|
}
|
|
|
|
// Give analyzers a chance to inspect the overall deployment.
|
|
for _, aname := range analyzers {
|
|
analyzer, err := ctx.Host.Analyzer(aname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO[pulumi/lumi#53]: we want to use the full package URL, including its SHA1 hash/version/etc.
|
|
failures, err := analyzer.Analyze(pack.PackageURL{Name: new.Pkg().Name()})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, failure := range failures {
|
|
ctx.Diag.Errorf(errors.ErrorAnalyzeFailure, aname, failure.Reason)
|
|
}
|
|
}
|
|
|
|
// Initialize the builder's maps used by everything below (olds, news, dependencies).
|
|
for _, old := range pb.OldRes {
|
|
m := old.URN()
|
|
pb.Olds[m] = old
|
|
contract.Assert(old.HasID())
|
|
// Keep track of which dependents exist for all resources.
|
|
for dep := range old.Inputs().AllResources() {
|
|
pb.Depends[dep] = append(pb.Depends[dep], m)
|
|
}
|
|
}
|
|
for _, new := range pb.NewRes {
|
|
pb.News[new.URN()] = new
|
|
}
|
|
|
|
// Do a quick pass over the new resources and make sure properties pass muster.
|
|
for _, new := range pb.NewRes {
|
|
// First ensure that the provider is okay with this resource.
|
|
prov, err := pb.P.Provider(new)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
failures, err := prov.Check(new)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, failure := range failures {
|
|
ctx.Diag.Errorf(errors.ErrorResourcePropertyValueInvalid, new.URN(), failure.Property, failure.Reason)
|
|
}
|
|
|
|
// Next, give each analyzer -- if any -- a chance to inspect the reosurce too.
|
|
for _, aname := range analyzers {
|
|
analyzer, err := ctx.Host.Analyzer(aname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
failures, err := analyzer.AnalyzeResource(new)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, failure := range failures {
|
|
ctx.Diag.Errorf(errors.ErrorAnalyzeResourceFailure, aname, new.URN(), failure.Property, failure.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Here's the real meat of the process: diffing the snapshots, looking for:
|
|
//
|
|
// - Anything in old but not new is a delete
|
|
// - Anything in new but not old is a create
|
|
// - For those things in both new and old, any changed properties imply an update
|
|
//
|
|
// Any property changes that require replacement are applied, recursively, in a cascading manner.
|
|
|
|
// First, those things in old but not new, and add them to the delete queue.
|
|
for _, old := range pb.OldRes {
|
|
m := old.URN()
|
|
if _, hasnew := pb.News[m]; !hasnew {
|
|
step := newDeleteStep(pb.P, old)
|
|
pb.Deletes[m] = newPlanVertex(step)
|
|
glog.V(7).Infof("Update plan decided to delete '%v'", m)
|
|
}
|
|
}
|
|
|
|
// Next, creates and updates: creates are those in new but not old, and updates are those in both.
|
|
for _, new := range pb.NewRes {
|
|
m := new.URN()
|
|
if old, hasold := pb.Olds[m]; hasold {
|
|
contract.Assert(old.Type() == new.Type())
|
|
|
|
// The resource exists in both new and old; it could be an update. This resource is an update if one of
|
|
// these two conditions exist: 1) either the old and new properties don't match or 2) the update impact
|
|
// is assessed as having to replace the resource, in which case the ID will change. This might have a
|
|
// cascading impact on subsequent updates too, since those IDs must trigger recreations, etc.
|
|
computed := new.Inputs().ReplaceResources(func(r URN) URN {
|
|
if pb.Replace(r) {
|
|
// If the resource is being replaced, simply mangle the URN so that it's different; this value
|
|
// won't actually be used for anything other than the diffing algorithms below.
|
|
// TODO[pulumi/lumi#90]: replace this entirely by computed properties and corresponding diffing.
|
|
r = r.Replace()
|
|
glog.V(7).Infof("Patched resource '%v's URN property: %v", m, r)
|
|
}
|
|
return r
|
|
})
|
|
|
|
if !old.Inputs().DeepEquals(computed) {
|
|
// See if this update has the effect of deleting and recreating the resource. If so, we need to make
|
|
// sure to insert the right replacement steps into the graph (a create, replace, and delete).
|
|
// TODO[pulumi/lumi#90]: this should generalize to any property changes.
|
|
prov, err := pb.P.Provider(old)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replacements, _, err := prov.InspectChange(old, new, computed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Now create a step and vertex of the right kind.
|
|
if len(replacements) > 0 {
|
|
// To perform a replacement, create a creation, deletion, and add the appropriate edges. Namely:
|
|
//
|
|
// - Replacement depends on creation
|
|
// - Deletion depends on replacement
|
|
// - Existing dependencies depend on replacement (ensured through usual update logic)
|
|
// - Deletion depends on updating all existing dependencies (ensured through usual update logic)
|
|
//
|
|
// This ensures the right sequencing, with the replacement node acting as a juncture in the graph.
|
|
replkeys := make([]PropertyKey, len(replacements))
|
|
for i, repl := range replacements {
|
|
replkeys[i] = PropertyKey(repl)
|
|
}
|
|
pb.Replaces[m] = replkeys
|
|
create := newReplaceCreateStep(pb.P, new)
|
|
pb.Creates[m] = newPlanVertex(create)
|
|
replace := newReplaceStep(pb.P, old, new, computed)
|
|
pb.Updates[m] = newPlanVertex(replace)
|
|
pb.Updates[m].connectTo(pb.Creates[m]) // replacement depends on creation
|
|
delete := newReplaceDeleteStep(pb.P, old)
|
|
pb.Deletes[m] = newPlanVertex(delete)
|
|
pb.Deletes[m].connectTo(pb.Updates[m]) // deletion depends on replacement
|
|
glog.V(7).Infof("Update plan decided to update '%v'; necessitates a replacement", m)
|
|
} else {
|
|
// An update is simple: just create a single update step and associated node in the graph.
|
|
step := newUpdateStep(pb.P, old, new, computed)
|
|
pb.Updates[m] = newPlanVertex(step)
|
|
glog.V(7).Infof("Update plan decided to update '%v'", m)
|
|
}
|
|
} else {
|
|
pb.Unchanged[old] = new
|
|
glog.V(7).Infof("Update plan decided not to update '%v'", m)
|
|
}
|
|
} else {
|
|
// The resource isn't in the old map, so it must be a resource creation.
|
|
step := newCreateStep(pb.P, new)
|
|
pb.Creates[m] = newPlanVertex(step)
|
|
glog.V(7).Infof("Update plan decided to create '%v'", m)
|
|
}
|
|
}
|
|
|
|
// Finally, we need to sequence the overall set of changes to create the final plan. To do this, we create a DAG
|
|
// of the above operations, so that inherent dependencies between operations are respected; specifically:
|
|
//
|
|
// - Deleting a resource depends on deletes of dependents and updates whose olds refer to it
|
|
// - Creating a resource depends on creates of dependencies
|
|
// - Updating a resource depends on creates or updates of news
|
|
//
|
|
// Clearly we must prohibit cycles in this overall graph of resource operations (hence the DAG part). To ensure
|
|
// this ordering, we will produce a plan graph whose vertices are operations and whose edges encode dependencies.
|
|
for _, old := range pb.OldRes {
|
|
m := old.URN()
|
|
if delete, isdelete := pb.Deletes[m]; isdelete {
|
|
pb.ConnectDelete(m, delete) // connect this delete so it happens before dependencies.
|
|
} else if update, isupdate := pb.Updates[m]; isupdate {
|
|
pb.ConnectUpdate(m, update) // connect this delete so it happens after dependencies are created/updated.
|
|
}
|
|
}
|
|
for _, new := range pb.NewRes {
|
|
m := new.URN()
|
|
if create, iscreate := pb.Creates[m]; iscreate {
|
|
pb.ConnectCreate(m, create) // connect this create so it happens after dependencies are created/updated.
|
|
}
|
|
}
|
|
|
|
// Finally, finish the creation of the plan, and return it.
|
|
return pb.Plan()
|
|
}
|
|
|
|
// planBuilder records a lot of the necessary information during the creation of a plan.
|
|
type planBuilder struct {
|
|
P *plan // the plan under construction.
|
|
Olds map[URN]Resource // a map of URN to old resource.
|
|
OldRes []Resource // a flat list of old resources (in topological order).
|
|
News map[URN]Resource // a map of URN to new resource.
|
|
NewRes []Resource // a flat list of new resources (in topological order).
|
|
Depends map[URN][]URN // a map of URN to all existing (old) dependencies.
|
|
Creates map[URN]*planVertex // a map of pending creates to their associated vertex.
|
|
Updates map[URN]*planVertex // a map of pending updates to their associated vertex.
|
|
Deletes map[URN]*planVertex // a map of pending deletes to their associated vertex.
|
|
Replaces map[URN][]PropertyKey // a map of URNs scheduled for replacement to properties being replaced.
|
|
Unchanged map[Resource]Resource // a map of unchanged resources to their ID-stamped state.
|
|
}
|
|
|
|
// newPlanBuilder initializes a fresh plan state instance, ready to use for planning.
|
|
func newPlanBuilder(ctx *Context, old Snapshot, new Snapshot) *planBuilder {
|
|
// These variables are read from either snapshot (preferred new, since it may have updated args).
|
|
var ns tokens.QName
|
|
var pkg tokens.Package
|
|
var args core.Args
|
|
|
|
// Now extract the resources and settings from the old and/or new snapshots.
|
|
var oldres []Resource
|
|
if old != nil {
|
|
oldres = old.Resources()
|
|
if new == nil {
|
|
ns = old.Namespace()
|
|
pkg = old.Pkg()
|
|
args = old.Args()
|
|
}
|
|
}
|
|
var newres []Resource
|
|
if new != nil {
|
|
newres = new.Resources()
|
|
ns = new.Namespace()
|
|
pkg = new.Pkg()
|
|
args = new.Args()
|
|
}
|
|
|
|
// Create a new, unfinished plan; it will be completed later on after the builder is done.
|
|
p := &plan{
|
|
ctx: ctx,
|
|
ns: ns,
|
|
pkg: pkg,
|
|
args: args,
|
|
}
|
|
|
|
return &planBuilder{
|
|
P: p,
|
|
Olds: make(map[URN]Resource),
|
|
OldRes: oldres,
|
|
News: make(map[URN]Resource),
|
|
NewRes: newres,
|
|
Depends: make(map[URN][]URN),
|
|
Creates: make(map[URN]*planVertex),
|
|
Updates: make(map[URN]*planVertex),
|
|
Deletes: make(map[URN]*planVertex),
|
|
Replaces: make(map[URN][]PropertyKey),
|
|
Unchanged: make(map[Resource]Resource),
|
|
}
|
|
}
|
|
|
|
func (pb *planBuilder) Replace(m URN) bool {
|
|
return len(pb.Replaces[m]) > 0
|
|
}
|
|
|
|
func (pb *planBuilder) ConnectCreate(m URN, v *planVertex) {
|
|
pb.connectCreateUpdate(m, v, false)
|
|
}
|
|
|
|
func (pb *planBuilder) ConnectUpdate(m URN, v *planVertex) {
|
|
pb.connectCreateUpdate(m, v, true)
|
|
}
|
|
|
|
func (pb *planBuilder) connectCreateUpdate(m URN, v *planVertex, update bool) {
|
|
var label string
|
|
if update {
|
|
label = "Updating"
|
|
} else {
|
|
label = "Creating"
|
|
}
|
|
|
|
// Add edges to:
|
|
// - new resources this step depends on
|
|
// - updated resources that this step depends on
|
|
new := v.Step().New()
|
|
for dep := range new.Inputs().AllResources() {
|
|
tov, has := pb.Creates[dep] // see if we're creating the dependency.
|
|
if !has {
|
|
tov, has = pb.Updates[dep] // see if the dependency is being updated.
|
|
}
|
|
if has {
|
|
contract.Assert(tov != nil)
|
|
v.connectTo(tov)
|
|
glog.V(7).Infof("%v '%v' depends on resource '%v'; edge created", label, m, dep)
|
|
} else {
|
|
// A missing entry is ok; it means the resource isn't changing.
|
|
old := pb.Olds[dep]
|
|
contract.Assertf(old != nil, "Expected '%v' to be an existing resource", dep)
|
|
contract.Assertf(pb.Unchanged[old] != nil, "Expected '%v' to be unchanged", dep)
|
|
glog.V(7).Infof("%v '%v' depends on resource '%v'; unchanged, so ignoring", label, m, dep)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (pb *planBuilder) ConnectDelete(m URN, v *planVertex) {
|
|
// Add edges to:
|
|
// - any dependents that used to refer to this (and are necessarily being deleted or updated)
|
|
for _, dep := range pb.Depends[m] {
|
|
tov, has := pb.Deletes[dep] // see if dependents are being deleted
|
|
if !has {
|
|
tov, has = pb.Updates[dep] // else, they should be updated, otherwise there is a problem
|
|
}
|
|
contract.Assertf(has, "Resource '%v' depends on '%v' (scheduled for deletion)", m, dep)
|
|
contract.Assert(tov != nil)
|
|
v.connectTo(tov)
|
|
glog.V(7).Infof("Deletion '%v' depends on resource '%v'; edge created", m, dep)
|
|
}
|
|
}
|
|
|
|
// Plan finishes the plan building and returns the resulting, completed plan (or non-nil error if it fails).
|
|
func (pb *planBuilder) Plan() (*plan, error) {
|
|
// For all plan vertices with no ins, make them root nodes.
|
|
var roots []*planEdge
|
|
for _, vs := range []map[URN]*planVertex{pb.Creates, pb.Updates, pb.Deletes} {
|
|
for _, v := range vs {
|
|
if len(v.Ins()) == 0 {
|
|
roots = append(roots, &planEdge{to: v})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now topologically sort the steps in the order they must execute, thread the plan together, and return it.
|
|
g := newPlanGraph(roots)
|
|
topdag, err := graph.Topsort(g)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var prev *step
|
|
for _, v := range topdag {
|
|
insertStep(&prev, v.Data().(*step))
|
|
}
|
|
|
|
// Remember extra information useful for plan consumers.
|
|
pb.P.replaces = pb.Replaces
|
|
pb.P.unchanged = pb.Unchanged
|
|
|
|
return pb.P, nil
|
|
}
|
|
|
|
type step struct {
|
|
p *plan // this step's plan.
|
|
op StepOp // the operation to perform.
|
|
old Resource // the state of the resource before this step.
|
|
new Resource // the state of the resource after this step.
|
|
newprops PropertyMap // the resource's properties, factoring in dependency updates.
|
|
next *step // the next step after this one in the plan.
|
|
}
|
|
|
|
var _ Step = (*step)(nil)
|
|
|
|
func (s *step) Plan() Plan { return s.p }
|
|
func (s *step) Op() StepOp { return s.op }
|
|
func (s *step) Logical() bool { return s.op == OpReplace }
|
|
func (s *step) Old() Resource { return s.old }
|
|
func (s *step) New() Resource { return s.new }
|
|
func (s *step) NewProps() PropertyMap { return s.newprops }
|
|
func (s *step) Next() Step {
|
|
if s.next == nil {
|
|
return nil
|
|
}
|
|
return s.next
|
|
}
|
|
|
|
func (s *step) Provider() (Provider, error) {
|
|
contract.Assert(s.old == nil || s.new == nil || s.old.Type() == s.new.Type())
|
|
if s.old != nil {
|
|
return s.p.Provider(s.old)
|
|
}
|
|
contract.Assert(s.new != nil)
|
|
return s.p.Provider(s.new)
|
|
}
|
|
|
|
func newCreateStep(p *plan, new Resource) *step {
|
|
return &step{p: p, op: OpCreate, new: new, newprops: new.Inputs()}
|
|
}
|
|
|
|
func newDeleteStep(p *plan, old Resource) *step {
|
|
return &step{p: p, op: OpDelete, old: old, newprops: nil}
|
|
}
|
|
|
|
func newUpdateStep(p *plan, old Resource, new Resource, newprops PropertyMap) *step {
|
|
return &step{p: p, op: OpUpdate, old: old, new: new, newprops: newprops}
|
|
}
|
|
|
|
func newReplaceStep(p *plan, old Resource, new Resource, newprops PropertyMap) *step {
|
|
return &step{p: p, op: OpReplace, old: old, new: new, newprops: newprops}
|
|
}
|
|
|
|
func newReplaceCreateStep(p *plan, new Resource) *step {
|
|
return &step{p: p, op: OpReplaceCreate, new: new, newprops: new.Inputs()}
|
|
}
|
|
|
|
func newReplaceDeleteStep(p *plan, old Resource) *step {
|
|
return &step{p: p, op: OpReplaceDelete, old: old, newprops: nil}
|
|
}
|
|
|
|
func insertStep(prev **step, step *step) {
|
|
contract.Assert(prev != nil)
|
|
if *prev == nil {
|
|
contract.Assert(step.p.first == nil)
|
|
step.p.first = step
|
|
*prev = step
|
|
} else {
|
|
(*prev).next = step
|
|
*prev = step
|
|
}
|
|
}
|
|
|
|
func (s *step) Apply() (State, error) {
|
|
// Fetch the provider.
|
|
prov, err := s.Provider()
|
|
if err != nil {
|
|
return StateOK, err
|
|
}
|
|
|
|
// Now simply perform the operation of the right kind.
|
|
switch s.op {
|
|
case OpCreate, OpReplaceCreate:
|
|
// Invoke the Create RPC function for this provider:
|
|
contract.Assert(s.old == nil)
|
|
contract.Assert(s.new != nil)
|
|
contract.Assertf(!s.new.HasID(), "Resources being created must not have IDs already")
|
|
rst, err := prov.Create(s.new)
|
|
if err != nil {
|
|
return rst, err
|
|
}
|
|
|
|
// Get the ID, read the resource state back (to fetch outputs), and store them.
|
|
id := s.new.ID()
|
|
contract.Assert(id != "")
|
|
s.p.ctx.IDURN[id] = s.new.URN()
|
|
if err := prov.Get(s.new); err != nil {
|
|
return StateUnknown, err
|
|
}
|
|
|
|
case OpDelete, OpReplaceDelete:
|
|
// Invoke the Delete RPC function for this provider:
|
|
contract.Assert(s.old != nil)
|
|
contract.Assert(s.new == nil)
|
|
contract.Assertf(s.old.HasID(), "Resources being deleted must have IDs")
|
|
if rst, err := prov.Delete(s.old); err != nil {
|
|
return rst, err
|
|
}
|
|
|
|
case OpUpdate:
|
|
// Invoke the Update RPC function for this provider:
|
|
contract.Assert(s.old != nil)
|
|
contract.Assert(s.new != nil)
|
|
contract.Assert(s.old.Type() == s.new.Type())
|
|
contract.Assertf(s.old.HasID(), "Resources being updated must have IDs")
|
|
id := s.old.ID()
|
|
if rst, err := prov.Update(s.old, s.new); err != nil {
|
|
return rst, err
|
|
}
|
|
contract.Assert(s.new.ID() == id)
|
|
|
|
// Now read the resource state back in case the update triggered cascading updates to other properties.
|
|
if err := prov.Get(s.new); err != nil {
|
|
return StateUnknown, err
|
|
}
|
|
|
|
case OpReplace:
|
|
contract.Assert(s.old != nil)
|
|
contract.Assert(s.new != nil)
|
|
contract.Assert(s.old.Type() == s.new.Type())
|
|
contract.Assertf(s.old.HasID(), "Resources being replaced must have IDs")
|
|
|
|
// There is nothing to do for OpReplace nodes; they are here to represent logical steps in the graph, and mostly
|
|
// for visualization purposes; there will be true OpReplaceCreate and OpReplaceDelete nodes in the graph.
|
|
|
|
default:
|
|
contract.Failf("Unexpected step operation: %v", s.op)
|
|
}
|
|
|
|
return StateOK, nil
|
|
}
|