Make replacement first class

This change, part of pulumi/coconut#105, rearranges support for
resource replacement.  The old model didn't properly account for
the cascading updates and possible replacement of dependencies.

Namely, we need to model a replacement as a creation followed by
a deletion, inserted into the overall DAG correctly so that any
resources that must be updated are updated after the creation but
prior to the deletion.  This is done by inserting *three* nodes
into the graph per replacement: a physical creation step, a
physical deletion step, and a logical replacement step.  The logical
step simply makes it nicer in the output (the plan output shows
a single "replacement" rather than the fine-grained outputs, unless
they are requested with --show-replace-steps).  It also makes it
easier to fold all of the edges into a single linchpin node.

As part of this, the update step no longer gets to choose whether
to recreate the resource.  Instead, the engine takes care of
orchestrating the replacement through actual create and delete calls.
This commit is contained in:
joeduffy 2017-03-02 09:52:08 -08:00
parent 8698e5abeb
commit bd613a33e6
11 changed files with 429 additions and 473 deletions

View file

@ -294,7 +294,7 @@ func apply(cmd *cobra.Command, info *huskCmdInfo, opts applyOptions) {
var summary bytes.Buffer
if !empty {
// Print out the total number of steps performed (and their kinds), the duration, and any summary info.
printSummary(&summary, progress.Ops, false)
printSummary(&summary, progress.Ops, opts.ShowReplaceSteps, false)
summary.WriteString(fmt.Sprintf("%vDeployment duration: %v%v\n",
colors.SpecUnimportant, time.Since(start), colors.Reset))
}
@ -445,13 +445,14 @@ func saveHusk(husk *resource.Husk, snap resource.Snapshot, file string, existok
}
type applyOptions struct {
Create bool // true if we are creating resources.
Delete bool // true if we are deleting resources.
DryRun bool // true if we should just print the plan without performing it.
ShowConfig bool // true to show the configuration variables being used.
ShowUnchanged bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
Output string // the place to store the output, if any.
Create bool // true if we are creating resources.
Delete bool // true if we are deleting resources.
DryRun bool // true if we should just print the plan without performing it.
ShowConfig bool // true to show the configuration variables being used.
ShowReplaceSteps bool // true to show the replacement steps in the plan.
ShowUnchanged bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
Output string // the place to store the output, if any.
}
// applyProgress pretty-prints the plan application process as it goes.
@ -472,9 +473,16 @@ func newProgress(summary bool) *applyProgress {
func (prog *applyProgress) Before(step resource.Step) {
// Print the step.
var b bytes.Buffer
stepop := step.Op()
stepnum := prog.Steps + 1
b.WriteString(fmt.Sprintf("Applying step #%v [%v]\n", stepnum, step.Op()))
var extra string
if stepop == resource.OpReplaceCreate || stepop == resource.OpReplaceDelete {
extra = " (part of a replacement step)"
}
var b bytes.Buffer
b.WriteString(fmt.Sprintf("Applying step #%v [%v]%v\n", stepnum, stepop, extra))
printStep(&b, step, prog.Summary, " ")
fmt.Printf(colors.Colorize(&b))
}
@ -521,15 +529,18 @@ func printPlan(d diag.Sink, result *planResult, opts applyOptions) {
step := result.Plan.Steps()
counts := make(map[resource.StepOp]int)
for step != nil {
op := step.Op()
// Print this step information (resource and all its properties).
// TODO: it would be nice if, in the output, we showed the dependencies a la `git log --graph`.
printStep(&summary, step, opts.Summary, "")
if opts.ShowReplaceSteps || (op != resource.OpReplaceCreate && op != resource.OpReplaceDelete) {
printStep(&summary, step, opts.Summary, "")
}
counts[step.Op()]++
step = step.Next()
}
// Print a summary of operation counts.
printSummary(&summary, counts, true)
printSummary(&summary, counts, opts.ShowReplaceSteps, true)
fmt.Printf(colors.Colorize(&summary))
}
}
@ -560,7 +571,7 @@ func printConfig(b *bytes.Buffer, result *compileResult) {
}
}
func printSummary(b *bytes.Buffer, counts map[resource.StepOp]int, plan bool) {
func printSummary(b *bytes.Buffer, counts map[resource.StepOp]int, showReplaceSteps bool, plan bool) {
total := 0
for _, c := range counts {
total += c
@ -585,6 +596,10 @@ func printSummary(b *bytes.Buffer, counts map[resource.StepOp]int, plan bool) {
}
for _, op := range resource.StepOps() {
if !showReplaceSteps && (op == resource.OpReplaceCreate || op == resource.OpReplaceDelete) {
// Unless the user requested it, don't show the fine-grained replacement steps; just the logical ones.
continue
}
if c := counts[op]; c > 0 {
b.WriteString(fmt.Sprintf(" %v%v %v %v%v%v%v\n",
op.Prefix(), c, plural("resource", c), planTo, op, pastTense, colors.Reset))
@ -617,7 +632,7 @@ func printStep(b *bytes.Buffer, step resource.Step, summary bool, indent string)
// Next print the resource moniker, properties, etc.
printResourceHeader(b, step.Old(), step.New(), indent)
b.WriteString(step.Op().Suffix())
printResourceProperties(b, step.Old(), step.New(), step.Computed(), summary, indent)
printResourceProperties(b, step.Old(), step.New(), step.NewProps(), summary, indent)
// Finally make sure to reset the color.
b.WriteString(colors.Reset)
@ -790,12 +805,12 @@ func printPropertyValueDiff(b *bytes.Buffer, title func(string), diff resource.V
_, newIndent := getArrayElemHeader(b, i, indent)
title := func(id string) { printArrayElemHeader(b, i, id) }
if add, isadd := a.Adds[i]; isadd {
b.WriteString(colors.SpecAdded)
b.WriteString(resource.OpCreate.Color())
title(addIndent(indent))
printPropertyValue(b, add, addIndent(newIndent))
b.WriteString(colors.Reset)
} else if delete, isdelete := a.Deletes[i]; isdelete {
b.WriteString(colors.SpecDeleted)
b.WriteString(resource.OpDelete.Color())
title(deleteIndent(indent))
printPropertyValue(b, delete, deleteIndent(newIndent))
b.WriteString(colors.Reset)
@ -812,23 +827,30 @@ func printPropertyValueDiff(b *bytes.Buffer, title func(string), diff resource.V
b.WriteString("{\n")
printObjectDiff(b, *diff.Object, indent+" ")
b.WriteString(fmt.Sprintf("%s}\n", indent))
} else if diff.Old.IsResource() && diff.New.IsResource() && diff.New.ResourceValue().Replacement() {
// If the old and new are both resources, and the new is a replacement, show this in a special way (+-).
b.WriteString(resource.OpReplace.Color())
title(updateIndent(indent))
printPropertyValue(b, diff.Old, updateIndent(indent))
b.WriteString(colors.Reset)
} else {
// If we ended up here, the two values either differ by type, or they have different primitive values. We will
// simply emit a deletion line followed by an addition line.
if shouldPrintPropertyValue(diff.Old) {
b.WriteString(colors.SpecChanged)
b.WriteString(resource.OpUpdate.Color())
title(deleteIndent(indent))
printPropertyValue(b, diff.Old, deleteIndent(indent))
b.WriteString(fmt.Sprintf("%s", colors.Reset))
b.WriteString(colors.Reset)
}
if shouldPrintPropertyValue(diff.New) {
b.WriteString(colors.SpecChanged)
b.WriteString(resource.OpUpdate.Color())
title(addIndent(indent))
printPropertyValue(b, diff.New, addIndent(indent))
b.WriteString(fmt.Sprintf("%s", colors.Reset))
b.WriteString(colors.Reset)
}
}
}
func addIndent(indent string) string { return indent[:len(indent)-2] + "+ " }
func deleteIndent(indent string) string { return indent[:len(indent)-2] + "- " }
func updateIndent(indent string) string { return indent[:len(indent)-2] + "+-" }

View file

@ -9,6 +9,7 @@ import (
func newHuskDeployCmd() *cobra.Command {
var dryRun bool
var showConfig bool
var showReplaceSteps bool
var showUnchanged bool
var summary bool
var output string
@ -29,12 +30,13 @@ func newHuskDeployCmd() *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
info := initHuskCmd(cmd, args)
apply(cmd, info, applyOptions{
Delete: false,
DryRun: dryRun,
ShowConfig: showConfig,
ShowUnchanged: showUnchanged,
Summary: summary,
Output: output,
Delete: false,
DryRun: dryRun,
ShowConfig: showConfig,
ShowReplaceSteps: showReplaceSteps,
ShowUnchanged: showUnchanged,
Summary: summary,
Output: output,
})
},
}
@ -45,6 +47,9 @@ func newHuskDeployCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&showConfig, "show-config", false,
"Show configuration keys and variables")
cmd.PersistentFlags().BoolVar(
&showReplaceSteps, "show-replace-steps", false,
"Show detailed resource replacement creates and deletes; normally shows as a single step")
cmd.PersistentFlags().BoolVar(
&showUnchanged, "show-unchanged", false,
"Show resources that needn't be updated because they haven't changed, alongside those that do")

View file

@ -117,6 +117,7 @@ func (p *instanceProvider) UpdateImpact(
}
// Now check the diff for updates to any fields (none of them are updateable).
// TODO: we should permit changes to security groups for non-EC2-classic VMs that are in VPCs.
diff := olds.Diff(news)
replace := diff.Diff(instanceImageID) || diff.Diff(instanceType) ||
diff.Diff(instanceSecurityGroups) || diff.Diff(instanceKeyName)

View file

@ -3,7 +3,10 @@
package resource
import (
"strings"
"github.com/pulumi/coconut/pkg/tokens"
"github.com/pulumi/coconut/pkg/util/contract"
)
// Moniker is a friendly, but unique, name for a resource, most often auto-assigned by Coconut. These monikers
@ -31,10 +34,32 @@ const MonikerDelimiter = "::" // the delimiter between elements of the moniker.
// NewMoniker creates a unique moniker for the given object.
func NewMoniker(ns tokens.QName, alloc tokens.Module, t tokens.Type, name tokens.QName) Moniker {
return Moniker(
m := Moniker(
string(ns) +
MonikerDelimiter + string(alloc) +
MonikerDelimiter + string(t) +
MonikerDelimiter + string(name),
)
contract.Assert(!m.Replacement())
return m
}
// replaceMonikerSuffix is the suffix for monikers referring to resources that are being replaced.
const replaceMonikerSuffix = Moniker("#<new-id(replace)>")
// Replace returns a new, modified replacement moniker (used to tag resources that are meant to be replaced).
func (m Moniker) Replace() Moniker {
contract.Assert(!m.Replacement())
return m + replaceMonikerSuffix
}
// Unreplace returns the underlying replacement's moniker.
func (m Moniker) Unreplace() Moniker {
contract.Assert(m.Replacement())
return m[:len(m)-len(replaceMonikerSuffix)]
}
// Replacement returns true if this moniker refers to a resource that is meant to be replaced.
func (m Moniker) Replacement() bool {
return strings.HasSuffix(string(m), string(replaceMonikerSuffix))
}

View file

@ -3,8 +3,6 @@
package resource
import (
"fmt"
"github.com/golang/glog"
"github.com/pulumi/coconut/pkg/compiler/core"
@ -39,7 +37,7 @@ type Step interface {
Op() StepOp // the operation that will be performed.
Old() Resource // the old resource state, if any, before performing this step.
New() Resource // the new resource state, if any, after performing this step.
Computed() PropertyMap // the projected new resource state, factoring in dependency updates.
NewProps() PropertyMap // the projected new resource state, factoring in dependency updates.
Next() Step // the next step to perform, or nil if none.
Apply() (error, ResourceState) // performs the operation specified by this step.
}
@ -48,19 +46,21 @@ type Step interface {
type StepOp string
const (
OpCreate StepOp = "create"
OpRead = "read"
OpUpdate = "update"
OpReplace = "replace"
OpDelete = "delete"
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:
case OpCreate, OpReplaceCreate:
return colors.SpecAdded
case OpDelete:
case OpDelete, OpReplaceDelete:
return colors.SpecDeleted
case OpUpdate:
return colors.SpecChanged
@ -83,6 +83,10 @@ func (op StepOp) Prefix() string {
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 ""
@ -102,8 +106,10 @@ func StepOps() []StepOp {
return []StepOp{
OpCreate,
OpUpdate,
OpReplace,
OpDelete,
OpReplace,
OpReplaceCreate,
OpReplaceDelete,
}
}
@ -229,6 +235,156 @@ func (p *plan) checkpoint(resources []Resource) Snapshot {
// 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) (*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\n", len(pb.OldRes), len(pb.NewRes))
}
// First diff the snapshots; in a nutshell:
//
// - 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.
for _, old := range pb.OldRes {
m := old.Moniker()
pb.Olds[m] = old
contract.Assert(old.HasID())
// Keep track of which dependents exist for all resources.
for dep := range old.Properties().AllResources() {
pb.Depends[dep] = append(pb.Depends[dep], m)
}
}
for _, new := range pb.NewRes {
pb.News[new.Moniker()] = new
}
// Find those things in old but not new, and add them to the delete queue.
for _, old := range pb.OldRes {
m := old.Moniker()
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)
}
}
// Find creates and updates: creates are those in new but not old, and updates are those in both.
for _, new := range pb.NewRes {
m := new.Moniker()
if old, hasold := pb.Olds[m]; hasold {
// 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.
contract.Assert(old.Type() == new.Type())
computed := new.Properties().ReplaceResources(func(r Moniker) Moniker {
if pb.Replaces[r] {
// If the resource is being replaced, simply mangle the moniker so that it's different; this value
// won't actually be used for anything other than the diffing algorithms below.
r = r.Replace()
glog.V(7).Infof("Patched resource '%v's moniker property: %v", m, r)
}
return r
})
if !old.Properties().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/coconut#90]: this should generalize to any property changes.
prov, err := pb.P.Provider(old)
if err != nil {
return nil, err
}
replace, _, err := prov.UpdateImpact(old.ID(), old.Type(), old.Properties(), computed)
if err != nil {
return nil, err
}
// Now create a step and vertex of the right kind.
if replace {
// 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.
pb.Replaces[m] = true
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.Moniker()
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.Moniker()
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[Moniker]Resource // a map of moniker to old resource.
OldRes []Resource // a flat list of old resources (in topological order).
News map[Moniker]Resource // a map of moniker to new resource.
NewRes []Resource // a flat list of new resources (in topological order).
Depends map[Moniker][]Moniker // a map of moniker to all existing (old) dependencies.
Creates map[Moniker]*planVertex // a map of pending creates to their associated vertex.
Updates map[Moniker]*planVertex // a map of pending updates to their associated vertex.
Deletes map[Moniker]*planVertex // a map of pending deletes to their associated vertex.
Replaces map[Moniker]bool // a set of monikers that are scheduled for replacement.
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
@ -252,193 +408,38 @@ func newPlan(ctx *Context, old Snapshot, new Snapshot) (*plan, error) {
args = new.Args()
}
if glog.V(7) {
glog.V(7).Infof("Creating plan with #old=%v #new=%v\n", len(oldres), len(newres))
}
// First diff the snapshots; in a nutshell:
//
// - 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.
olds := make(map[Moniker]Resource)
olddepends := make(map[Moniker][]Moniker)
for _, res := range oldres {
m := res.Moniker()
olds[m] = res
contract.Assert(res.HasID())
// Keep track of which dependents exist for all resources.
for ref := range res.Properties().AllResources() {
olddepends[ref] = append(olddepends[ref], m)
}
}
news := make(map[Moniker]Resource)
for _, res := range newres {
news[res.Moniker()] = res
}
// Keep track of vertices for our later graph operations.
// 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,
unchanged: make(map[Resource]Resource),
}
vs := make(map[Moniker]*planVertex)
// Find those things in old but not new, and add them to the delete queue.
deletes := make(map[Resource]bool)
for _, res := range oldres {
m := res.Moniker()
if _, has := news[m]; !has {
deletes[res] = true
step := newDeleteStep(p, res)
vs[m] = newPlanVertex(step)
glog.V(7).Infof("Update plan decided to delete '%v'", m)
}
ctx: ctx,
ns: ns,
pkg: pkg,
args: args,
}
// Find creates and updates: creates are those in new but not old, and updates are those in both.
creates := make(map[Resource]bool)
updates := make(map[Resource]Resource)
replaces := make(map[Moniker]bool)
for _, res := range newres {
m := res.Moniker()
if oldres, has := olds[m]; has {
// 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.
contract.Assert(oldres.Type() == res.Type())
computed := res.Properties().ReplaceResources(func(r Moniker) Moniker {
if replaces[r] {
// If the resource is being replaced, simply mangle the moniker so that it's different; this value
// won't actually be used for anything other than the diffing algorithms below.
r += Moniker("#<new-id(replace)>")
glog.V(7).Infof("Patched resource '%v's moniker property: %v", m, r)
}
return r
})
if !oldres.Properties().DeepEquals(computed) {
updates[oldres] = res
glog.V(7).Infof("Update plan decided to update '%v'", m)
// If this update's impact will replace the resource, make sure to remember this fact.
// TODO[pulumi/coconut#90]: this should generalize to any property changes.
prov, err := p.Provider(oldres)
if err != nil {
return nil, err
}
replace, _, err := prov.UpdateImpact(oldres.ID(), oldres.Type(), oldres.Properties(), computed)
if err != nil {
return nil, err
}
// Now create a step and vertex of the right kind.
var step *step
if replace {
glog.V(7).Infof("Update to resource '%v' necessitates a replacement", m)
step = newReplaceStep(p, oldres, res, computed)
replaces[m] = true
} else {
step = newUpdateStep(p, oldres, res, computed)
}
vs[m] = newPlanVertex(step)
} else {
p.unchanged[oldres] = res
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.
creates[res] = true
step := newCreateStep(p, res)
vs[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 _, res := range oldres {
m := res.Moniker()
if deletes[res] {
// Add edges to:
// - any dependents that used to refer to this
fromv := vs[m]
contract.Assert(fromv != nil)
for _, ref := range olddepends[m] {
if tov, has := vs[ref]; has {
contract.Assert(tov != nil)
fromv.connectTo(tov)
glog.V(7).Infof("Deletion '%v' depends on resource '%v'; edge created", m, ref)
} else {
old := olds[ref]
contract.Assert(old != nil)
contract.Assert(!deletes[old])
contract.Assert(updates[old] == nil)
glog.V(7).Infof("Deletion '%v' depends on resource '%v'; unchanged, so ignoring", m, ref)
}
}
} else if to := updates[res]; to != nil {
// Add edge to:
// - creates news
// - updates news
// TODO[pulumi/coconut#90]: we need to track "cascading updates".
fromv := vs[m]
contract.Assert(fromv != nil)
for ref := range to.Properties().AllResources() {
if tov, has := vs[ref]; has {
contract.Assert(tov != nil)
fromv.connectTo(tov)
glog.V(7).Infof("Updating '%v' depends on resource '%v'; edge created", m, ref)
} else {
old := olds[ref]
contract.Assert(old != nil)
contract.Assert(!deletes[old])
contract.Assert(updates[old] == nil)
glog.V(7).Infof("Updating '%v' depends on resource '%v'; unchanged, so ignoring", m, ref)
}
}
}
}
for _, res := range newres {
if creates[res] {
// add edge to:
// - creates news
m := res.Moniker()
fromv := vs[m]
contract.Assert(fromv != nil)
for ref := range res.Properties().AllResources() {
if tov, has := vs[ref]; has {
contract.Assert(tov != nil)
fromv.connectTo(tov)
glog.V(7).Infof("Creating '%v' depends on resource '%v'; edge created", m, ref)
} else {
old := olds[ref]
new := news[ref]
contract.Assert(new == nil || !creates[new])
contract.Assert(old == nil || updates[old] == nil)
glog.V(7).Infof("Creating '%v' depends on resource '%v'; unchanged, so ignoring", m, ref)
}
}
}
return &planBuilder{
P: p,
Olds: make(map[Moniker]Resource),
OldRes: oldres,
News: make(map[Moniker]Resource),
NewRes: newres,
Depends: make(map[Moniker][]Moniker),
Creates: make(map[Moniker]*planVertex),
Updates: make(map[Moniker]*planVertex),
Deletes: make(map[Moniker]*planVertex),
Replaces: make(map[Moniker]bool),
Unchanged: make(map[Resource]Resource),
}
}
// 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 _, v := range vs {
if len(v.Ins()) == 0 {
roots = append(roots, &planEdge{to: v})
for _, vs := range []map[Moniker]*planVertex{pb.Creates, pb.Updates, pb.Deletes} {
for _, v := range vs {
if len(v.Ins()) == 0 {
roots = append(roots, &planEdge{to: v})
}
}
}
@ -452,7 +453,64 @@ func newPlan(ctx *Context, old Snapshot, new Snapshot) (*plan, error) {
for _, v := range topdag {
insertStep(&prev, v.Data().(*step))
}
return p, nil
// Remember the unchanged nodes.
pb.P.unchanged = pb.Unchanged
return pb.P, nil
}
func (pb *planBuilder) ConnectCreate(m Moniker, v *planVertex) {
pb.connectCreateUpdate(m, v, false)
}
func (pb *planBuilder) ConnectUpdate(m Moniker, v *planVertex) {
pb.connectCreateUpdate(m, v, true)
}
func (pb *planBuilder) connectCreateUpdate(m Moniker, 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.Properties().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.
contract.Assert(pb.Unchanged[new] != nil)
contract.Assert(update == (pb.Olds[dep] != nil))
glog.V(7).Infof("%v '%v' depends on resource '%v'; unchanged, so ignoring", label, m, dep)
}
}
}
func (pb *planBuilder) ConnectDelete(m Moniker, 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)
}
}
type step struct {
@ -460,7 +518,7 @@ type step struct {
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.
computed PropertyMap // the computed impact dependency changes will have on this resource.
newprops PropertyMap // the resource's properties, factoring in dependency updates.
next *step // the next step after this one in the plan.
}
@ -469,28 +527,44 @@ var _ Step = (*step)(nil)
func (s *step) Op() StepOp { return s.op }
func (s *step) Old() Resource { return s.old }
func (s *step) New() Resource { return s.new }
func (s *step) Computed() PropertyMap { return s.computed }
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, computed: new.Properties()}
return &step{p: p, op: OpCreate, new: new, newprops: new.Properties()}
}
func newDeleteStep(p *plan, old Resource) *step {
return &step{p: p, op: OpDelete, old: old, computed: nil}
return &step{p: p, op: OpDelete, old: old, newprops: nil}
}
func newUpdateStep(p *plan, old Resource, new Resource, computed PropertyMap) *step {
return &step{p: p, op: OpUpdate, old: old, new: new, computed: computed}
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, computed PropertyMap) *step {
return &step{p: p, op: OpReplace, old: old, new: new, computed: computed}
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.Properties()}
}
func newReplaceDeleteStep(p *plan, old Resource) *step {
return &step{p: p, op: OpReplaceDelete, old: old, newprops: nil}
}
func insertStep(prev **step, step *step) {
@ -506,58 +580,57 @@ func insertStep(prev **step, step *step) {
}
func (s *step) Apply() (error, ResourceState) {
// Fetch the provider.
prov, err := s.Provider()
if err != nil {
return err, StateOK
}
// Now simply perform the operation of the right kind.
switch s.op {
case OpCreate:
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")
prov, err := s.p.Provider(s.new)
if err != nil {
return err, StateOK
}
id, err, rst := prov.Create(s.new.Type(), s.new.Properties())
if err != nil {
return err, rst
}
s.new.SetID(id)
case OpDelete:
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")
prov, err := s.p.Provider(s.old)
if err != nil {
return err, StateOK
}
if err, rst := prov.Delete(s.old.ID(), s.old.Type()); err != nil {
return err, rst
}
case OpUpdate, OpReplace:
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")
prov, err := s.p.Provider(s.old)
if err != nil {
return err, StateOK
}
id, err, rst := prov.Update(s.old.ID(), s.old.Type(), s.old.Properties(), s.new.Properties())
if err != nil {
id := s.old.ID()
if err, rst := prov.Update(id, s.old.Type(), s.old.Properties(), s.new.Properties()); err != nil {
return err, rst
} else if id != ID("") {
// An update might need to recreate the resource, in which case the ID must change.
if s.op != OpReplace {
// If we didn't expect to replace this resource, but the provided did so anyway, we need to halt
// things. Dependent nodes aren't scheduled for updates, and so state has become out of synch.
// TODO: this isn't drastic enough; we need to walk and taint all dependencies (or something similar).
return fmt.Errorf("unexpected resource '%v' ID change (old %v, new %v)",
s.old.Type(), s.old.ID(), id), StateUnknown
}
s.new.SetID(id)
} else {
// Otherwise, propagate the old ID on the new resource, so that the resulting snapshot is correct.
s.new.SetID(s.old.ID())
}
// Propagate the old ID on the new resource, so that the resulting snapshot is correct.
s.new.SetID(id)
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)
}

View file

@ -204,9 +204,8 @@ func (p *Plugin) Read(id ID, t tokens.Type) (PropertyMap, error) {
return props, nil
}
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
func (p *Plugin) Update(id ID, t tokens.Type, olds PropertyMap, news PropertyMap) (ID, error, ResourceState) {
// Update updates an existing resource with new values.
func (p *Plugin) Update(id ID, t tokens.Type, olds PropertyMap, news PropertyMap) (error, ResourceState) {
contract.Requiref(id != "", "id", "not empty")
contract.Requiref(t != "", "t", "not empty")
@ -221,15 +220,14 @@ func (p *Plugin) Update(id ID, t tokens.Type, olds PropertyMap, news PropertyMap
News: MarshalProperties(p.ctx, news, MarshalOptions{}),
}
resp, err := p.client.Update(p.ctx.Request(), req)
_, err := p.client.Update(p.ctx.Request(), req)
if err != nil {
glog.V(7).Infof("Plugin[%v].Update(id=%v,t=%v,...) failed: %v", p.pkg, id, t, err)
return ID(""), err, StateUnknown
return err, StateUnknown
}
nid := resp.GetId()
glog.V(7).Infof("Plugin[%v].Update(id=%v,t=%v,...) success: nid=%v", p.pkg, id, t, nid)
return ID(nid), nil, StateOK
glog.V(7).Infof("Plugin[%v].Update(id=%v,t=%v,...) success", p.pkg, id, t)
return nil, StateOK
}
// UpdateImpact checks what impacts a hypothetical update will have on the resource's properties.

View file

@ -29,9 +29,8 @@ type Provider interface {
Create(t tokens.Type, props PropertyMap) (ID, error, ResourceState)
// Read reads the instance state identified by id/t, and returns a bag of properties.
Read(id ID, t tokens.Type) (PropertyMap, error)
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
Update(id ID, t tokens.Type, olds PropertyMap, news PropertyMap) (ID, error, ResourceState)
// Update updates an existing resource with new values.
Update(id ID, t tokens.Type, olds PropertyMap, news PropertyMap) (error, ResourceState)
// UpdateImpact checks what impacts a hypothetical update will have on the resource's properties.
UpdateImpact(id ID, t tokens.Type, olds PropertyMap, news PropertyMap) (bool, PropertyMap, error)
// Delete tears down an existing resource.

View file

@ -18,7 +18,6 @@ It has these top-level messages:
ReadRequest
ReadResponse
UpdateRequest
UpdateResponse
UpdateImpactResponse
DeleteRequest
*/

View file

@ -180,22 +180,6 @@ func (m *UpdateRequest) GetNews() *google_protobuf1.Struct {
return nil
}
type UpdateResponse struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
}
func (m *UpdateResponse) Reset() { *m = UpdateResponse{} }
func (m *UpdateResponse) String() string { return proto.CompactTextString(m) }
func (*UpdateResponse) ProtoMessage() {}
func (*UpdateResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{7} }
func (m *UpdateResponse) GetId() string {
if m != nil {
return m.Id
}
return ""
}
type UpdateImpactResponse struct {
Replace bool `protobuf:"varint,1,opt,name=replace" json:"replace,omitempty"`
Impacts *google_protobuf1.Struct `protobuf:"bytes,2,opt,name=impacts" json:"impacts,omitempty"`
@ -204,7 +188,7 @@ type UpdateImpactResponse struct {
func (m *UpdateImpactResponse) Reset() { *m = UpdateImpactResponse{} }
func (m *UpdateImpactResponse) String() string { return proto.CompactTextString(m) }
func (*UpdateImpactResponse) ProtoMessage() {}
func (*UpdateImpactResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{8} }
func (*UpdateImpactResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{7} }
func (m *UpdateImpactResponse) GetReplace() bool {
if m != nil {
@ -228,7 +212,7 @@ type DeleteRequest struct {
func (m *DeleteRequest) Reset() { *m = DeleteRequest{} }
func (m *DeleteRequest) String() string { return proto.CompactTextString(m) }
func (*DeleteRequest) ProtoMessage() {}
func (*DeleteRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{9} }
func (*DeleteRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{8} }
func (m *DeleteRequest) GetId() string {
if m != nil {
@ -252,7 +236,6 @@ func init() {
proto.RegisterType((*ReadRequest)(nil), "cocorpc.ReadRequest")
proto.RegisterType((*ReadResponse)(nil), "cocorpc.ReadResponse")
proto.RegisterType((*UpdateRequest)(nil), "cocorpc.UpdateRequest")
proto.RegisterType((*UpdateResponse)(nil), "cocorpc.UpdateResponse")
proto.RegisterType((*UpdateImpactResponse)(nil), "cocorpc.UpdateImpactResponse")
proto.RegisterType((*DeleteRequest)(nil), "cocorpc.DeleteRequest")
}
@ -277,9 +260,8 @@ type ResourceProviderClient interface {
Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error)
// Read reads the instance state identified by ID, returning a populated resource object, or an error if not found.
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*UpdateResponse, error)
// Update updates an existing resource with new values.
Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*google_protobuf.Empty, error)
// UpdateImpact checks what impacts a hypothetical update will have on the resource's properties.
UpdateImpact(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*UpdateImpactResponse, error)
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
@ -321,8 +303,8 @@ func (c *resourceProviderClient) Read(ctx context.Context, in *ReadRequest, opts
return out, nil
}
func (c *resourceProviderClient) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*UpdateResponse, error) {
out := new(UpdateResponse)
func (c *resourceProviderClient) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*google_protobuf.Empty, error) {
out := new(google_protobuf.Empty)
err := grpc.Invoke(ctx, "/cocorpc.ResourceProvider/Update", in, out, c.cc, opts...)
if err != nil {
return nil, err
@ -360,9 +342,8 @@ type ResourceProviderServer interface {
Create(context.Context, *CreateRequest) (*CreateResponse, error)
// Read reads the instance state identified by ID, returning a populated resource object, or an error if not found.
Read(context.Context, *ReadRequest) (*ReadResponse, error)
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
Update(context.Context, *UpdateRequest) (*UpdateResponse, error)
// Update updates an existing resource with new values.
Update(context.Context, *UpdateRequest) (*google_protobuf.Empty, error)
// UpdateImpact checks what impacts a hypothetical update will have on the resource's properties.
UpdateImpact(context.Context, *UpdateRequest) (*UpdateImpactResponse, error)
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
@ -517,33 +498,32 @@ var _ResourceProvider_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("provider.proto", fileDescriptor1) }
var fileDescriptor1 = []byte{
// 436 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xb4, 0x54, 0x5d, 0x8b, 0xd4, 0x30,
0x14, 0x6d, 0x6b, 0x99, 0xd1, 0x3b, 0x1f, 0x48, 0x58, 0x77, 0x4b, 0x55, 0x18, 0xf2, 0xb4, 0x20,
0x74, 0xd9, 0x5d, 0x16, 0x41, 0x1f, 0x55, 0x16, 0x5f, 0x44, 0x2a, 0xbe, 0x88, 0x2f, 0xdd, 0xf4,
0x3a, 0x14, 0xda, 0x49, 0x4c, 0x52, 0x65, 0x7e, 0x84, 0xbf, 0x59, 0x49, 0xd3, 0xd6, 0xb4, 0xc3,
0x38, 0xe3, 0xc3, 0xbe, 0xa5, 0x37, 0x27, 0xe7, 0xdc, 0xdc, 0x73, 0x52, 0x58, 0x0a, 0xc9, 0x7f,
0x14, 0x39, 0xca, 0x44, 0x48, 0xae, 0x39, 0x99, 0x32, 0xce, 0xb8, 0x14, 0x2c, 0x7e, 0xba, 0xe6,
0x7c, 0x5d, 0xe2, 0x45, 0x53, 0xbe, 0xab, 0xbf, 0x5d, 0x60, 0x25, 0xf4, 0xd6, 0xa2, 0xe2, 0x67,
0xe3, 0x4d, 0xa5, 0x65, 0xcd, 0xb4, 0xdd, 0xa5, 0x5f, 0x60, 0xf6, 0x21, 0xab, 0x30, 0xc5, 0xef,
0x35, 0x2a, 0x4d, 0x08, 0x84, 0x7a, 0x2b, 0x30, 0xf2, 0x57, 0xfe, 0xf9, 0xa3, 0xb4, 0x59, 0x93,
0x97, 0x00, 0x42, 0x72, 0x81, 0x52, 0x17, 0xa8, 0xa2, 0x60, 0xe5, 0x9f, 0xcf, 0xae, 0xce, 0x12,
0xcb, 0x9a, 0x74, 0xac, 0xc9, 0xa7, 0x86, 0x35, 0x75, 0xa0, 0x94, 0xc2, 0xdc, 0x72, 0x2b, 0xc1,
0x37, 0x0a, 0x0d, 0xf9, 0x26, 0xab, 0x7a, 0x72, 0xb3, 0xa6, 0x5f, 0x61, 0xf1, 0x46, 0x62, 0xa6,
0xef, 0xa7, 0x83, 0x15, 0x2c, 0x3b, 0xf6, 0xb6, 0x87, 0x25, 0x04, 0x45, 0xde, 0x92, 0x07, 0x45,
0x4e, 0x2f, 0x61, 0x96, 0x62, 0x96, 0x77, 0xea, 0xa3, 0xed, 0xbe, 0x9b, 0xe0, 0x6f, 0x37, 0xf4,
0x16, 0xe6, 0xf6, 0x48, 0x4b, 0x39, 0xec, 0xce, 0x3f, 0xbe, 0xbb, 0x5f, 0x3e, 0x2c, 0x3e, 0x8b,
0xdc, 0xb9, 0xfc, 0x11, 0xf2, 0xe4, 0x05, 0x84, 0xbc, 0xcc, 0x55, 0xf4, 0xe0, 0xdf, 0x42, 0x0d,
0xc8, 0x80, 0x37, 0xf8, 0x53, 0x45, 0xe1, 0x01, 0xb0, 0x01, 0x99, 0x69, 0x75, 0xed, 0xec, 0x99,
0x16, 0x83, 0x13, 0x8b, 0x78, 0x5f, 0x89, 0x8c, 0xe9, 0x1e, 0x17, 0xc1, 0x54, 0xa2, 0x28, 0x33,
0x66, 0x7d, 0x7b, 0x98, 0x76, 0x9f, 0xe4, 0x12, 0xa6, 0x45, 0x83, 0x3d, 0xe8, 0x5b, 0x87, 0xa3,
0xd7, 0xb0, 0x78, 0x8b, 0x25, 0xfe, 0xd7, 0x54, 0xae, 0x7e, 0x07, 0xf0, 0x38, 0x45, 0xc5, 0x6b,
0xc9, 0xf0, 0x63, 0xfb, 0x4c, 0xc8, 0x0d, 0x84, 0x26, 0x80, 0xe4, 0x24, 0x69, 0x5f, 0x4a, 0xe2,
0x64, 0x3d, 0x7e, 0x32, 0xaa, 0xda, 0xbb, 0x50, 0x8f, 0xbc, 0x86, 0x89, 0x4d, 0x0d, 0x39, 0xed,
0x21, 0x83, 0x90, 0xc6, 0x67, 0x3b, 0xf5, 0xfe, 0xf0, 0x0d, 0x84, 0x26, 0x1d, 0x8e, 0xa6, 0x93,
0x2f, 0x47, 0xd3, 0x8d, 0x90, 0xd5, 0xb4, 0x93, 0x75, 0x34, 0x07, 0xd9, 0x70, 0x34, 0x87, 0x26,
0x51, 0x8f, 0xdc, 0xc2, 0xdc, 0xb5, 0x65, 0x2f, 0xc5, 0xf3, 0x51, 0x7d, 0xe8, 0x22, 0xf5, 0xc8,
0x2b, 0x98, 0xd8, 0xd1, 0x3b, 0x14, 0x03, 0x2f, 0xe2, 0xd3, 0x1d, 0xfb, 0xde, 0x99, 0x7f, 0x0d,
0xf5, 0xee, 0x26, 0x4d, 0xe5, 0xfa, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x5d, 0x6e, 0xc5, 0x48,
0xa6, 0x04, 0x00, 0x00,
// 426 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xb4, 0x53, 0x4d, 0xab, 0xd3, 0x40,
0x14, 0x6d, 0x62, 0x68, 0xf5, 0xf6, 0x03, 0x19, 0x9e, 0xef, 0x85, 0xa8, 0x50, 0x66, 0xf5, 0x40,
0xc8, 0xa3, 0x2d, 0x45, 0xd0, 0xa5, 0x4a, 0x71, 0x23, 0x12, 0x71, 0x23, 0x6e, 0xd2, 0xc9, 0xb5,
0x04, 0x92, 0xce, 0x38, 0x33, 0x51, 0xfa, 0x23, 0xfc, 0xcb, 0x22, 0xc9, 0x24, 0x71, 0x92, 0x52,
0x5b, 0x17, 0x6f, 0x97, 0xdc, 0x39, 0x73, 0xee, 0xb9, 0x73, 0xce, 0x85, 0x99, 0x90, 0xfc, 0x47,
0x9a, 0xa0, 0x0c, 0x85, 0xe4, 0x9a, 0x93, 0x11, 0xe3, 0x8c, 0x4b, 0xc1, 0x82, 0xa7, 0x3b, 0xce,
0x77, 0x19, 0xde, 0x55, 0xe5, 0x6d, 0xf1, 0xed, 0x0e, 0x73, 0xa1, 0x0f, 0x06, 0x15, 0x3c, 0xeb,
0x1f, 0x2a, 0x2d, 0x0b, 0xa6, 0xcd, 0x29, 0xfd, 0x02, 0xe3, 0x0f, 0x71, 0x8e, 0x11, 0x7e, 0x2f,
0x50, 0x69, 0x42, 0xc0, 0xd3, 0x07, 0x81, 0xbe, 0x33, 0x77, 0x6e, 0x1f, 0x45, 0xd5, 0x37, 0x79,
0x09, 0x20, 0x24, 0x17, 0x28, 0x75, 0x8a, 0xca, 0x77, 0xe7, 0xce, 0xed, 0x78, 0x79, 0x13, 0x1a,
0xd6, 0xb0, 0x61, 0x0d, 0x3f, 0x55, 0xac, 0x91, 0x05, 0xa5, 0x14, 0x26, 0x86, 0x5b, 0x09, 0xbe,
0x57, 0x58, 0x92, 0xef, 0xe3, 0xbc, 0x25, 0x2f, 0xbf, 0xe9, 0x57, 0x98, 0xbe, 0x91, 0x18, 0xeb,
0xfb, 0x51, 0x30, 0x87, 0x59, 0xc3, 0x5e, 0x6b, 0x98, 0x81, 0x9b, 0x26, 0x35, 0xb9, 0x9b, 0x26,
0x74, 0x01, 0xe3, 0x08, 0xe3, 0xa4, 0xe9, 0xde, 0x3b, 0x6e, 0xd5, 0xb8, 0x7f, 0xd5, 0xd0, 0x0d,
0x4c, 0xcc, 0x95, 0x9a, 0xb2, 0xab, 0xce, 0xb9, 0x5c, 0xdd, 0x2f, 0x07, 0xa6, 0x9f, 0x45, 0x62,
0x0d, 0x7f, 0x41, 0x7b, 0xf2, 0x02, 0x3c, 0x9e, 0x25, 0xca, 0x7f, 0xf0, 0xef, 0x46, 0x15, 0xa8,
0x04, 0xef, 0xf1, 0xa7, 0xf2, 0xbd, 0x33, 0xe0, 0x12, 0x44, 0x19, 0x5c, 0x19, 0x39, 0xef, 0x73,
0x11, 0x33, 0xdd, 0x0e, 0xe8, 0xc3, 0x48, 0xa2, 0xc8, 0x62, 0x66, 0x5c, 0x79, 0x18, 0x35, 0xbf,
0x64, 0x01, 0xa3, 0xb4, 0xc2, 0x9e, 0x75, 0xa5, 0xc1, 0xd1, 0x15, 0x4c, 0xdf, 0x62, 0x86, 0xff,
0x35, 0xf3, 0xf2, 0xb7, 0x0b, 0x8f, 0x23, 0x54, 0xbc, 0x90, 0x0c, 0x3f, 0xd6, 0x4b, 0x40, 0xd6,
0xe0, 0x95, 0xf1, 0x22, 0x57, 0x61, 0xbd, 0x07, 0xa1, 0x95, 0xe4, 0xe0, 0x49, 0xaf, 0x6a, 0x66,
0xa1, 0x03, 0xf2, 0x1a, 0x86, 0x26, 0x13, 0xe4, 0xba, 0x85, 0x74, 0x22, 0x18, 0xdc, 0x1c, 0xd5,
0xdb, 0xcb, 0x6b, 0xf0, 0x4a, 0xef, 0xad, 0x9e, 0x56, 0x7a, 0xac, 0x9e, 0x76, 0x40, 0xe8, 0x80,
0xbc, 0x82, 0xa1, 0x79, 0x59, 0xab, 0x67, 0xc7, 0xf9, 0xe0, 0xfa, 0xe8, 0xe1, 0xde, 0x95, 0x3b,
0x4c, 0x07, 0x64, 0x03, 0x13, 0xdb, 0x95, 0x93, 0x0c, 0xcf, 0x7b, 0xf5, 0xae, 0x89, 0x46, 0x84,
0x79, 0x79, 0x8b, 0xa2, 0x63, 0xc5, 0x69, 0x11, 0xdb, 0x61, 0x55, 0x59, 0xfd, 0x09, 0x00, 0x00,
0xff, 0xff, 0xf3, 0xeb, 0x1f, 0x7c, 0x83, 0x04, 0x00, 0x00,
}

View file

@ -20,7 +20,6 @@ goog.exportSymbol('proto.cocorpc.ReadRequest', null, global);
goog.exportSymbol('proto.cocorpc.ReadResponse', null, global);
goog.exportSymbol('proto.cocorpc.UpdateImpactResponse', null, global);
goog.exportSymbol('proto.cocorpc.UpdateRequest', null, global);
goog.exportSymbol('proto.cocorpc.UpdateResponse', null, global);
/**
* Generated by JsPbCodeGenerator.
@ -1249,146 +1248,6 @@ proto.cocorpc.UpdateRequest.prototype.hasNews = function() {
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cocorpc.UpdateResponse = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cocorpc.UpdateResponse, jspb.Message);
if (goog.DEBUG && !COMPILED) {
proto.cocorpc.UpdateResponse.displayName = 'proto.cocorpc.UpdateResponse';
}
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto suitable for use in Soy templates.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS.
* @param {boolean=} opt_includeInstance Whether to include the JSPB instance
* for transitional soy proto support: http://goto/soy-param-migration
* @return {!Object}
*/
proto.cocorpc.UpdateResponse.prototype.toObject = function(opt_includeInstance) {
return proto.cocorpc.UpdateResponse.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Whether to include the JSPB
* instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cocorpc.UpdateResponse} msg The msg instance to transform.
* @return {!Object}
*/
proto.cocorpc.UpdateResponse.toObject = function(includeInstance, msg) {
var f, obj = {
id: jspb.Message.getFieldWithDefault(msg, 1, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cocorpc.UpdateResponse}
*/
proto.cocorpc.UpdateResponse.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cocorpc.UpdateResponse;
return proto.cocorpc.UpdateResponse.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cocorpc.UpdateResponse} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cocorpc.UpdateResponse}
*/
proto.cocorpc.UpdateResponse.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setId(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cocorpc.UpdateResponse.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cocorpc.UpdateResponse.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cocorpc.UpdateResponse} message
* @param {!jspb.BinaryWriter} writer
*/
proto.cocorpc.UpdateResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getId();
if (f.length > 0) {
writer.writeString(
1,
f
);
}
};
/**
* optional string id = 1;
* @return {string}
*/
proto.cocorpc.UpdateResponse.prototype.getId = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/** @param {string} value */
proto.cocorpc.UpdateResponse.prototype.setId = function(value) {
jspb.Message.setField(this, 1, value);
};
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a

View file

@ -21,9 +21,8 @@ service ResourceProvider {
rpc Create(CreateRequest) returns (CreateResponse) {}
// Read reads the instance state identified by ID, returning a populated resource object, or an error if not found.
rpc Read(ReadRequest) returns (ReadResponse) {}
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
rpc Update(UpdateRequest) returns (UpdateResponse) {}
// Update updates an existing resource with new values.
rpc Update(UpdateRequest) returns (google.protobuf.Empty) {}
// UpdateImpact checks what impacts a hypothetical update will have on the resource's properties.
rpc UpdateImpact(UpdateRequest) returns (UpdateImpactResponse) {}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
@ -64,10 +63,6 @@ message UpdateRequest {
google.protobuf.Struct news = 4; // the new values of properties to update.
}
message UpdateResponse {
string id = 1; // the new ID for the resource, if it had to be recreated.
}
message UpdateImpactResponse {
bool replace = 1; // true if this update triggers replacement of the resource, false otherwise.
google.protobuf.Struct impacts = 2; // the full set of properties that will be altered by this operation.