Begin resource modeling and planning

This change introduces a new package, pkg/resource, that will form
the foundation for actually performing deployment plans and applications.

It contains the following key abstractions:

* resource.Provider is a wrapper around the CRUD operations exposed by
  underlying resource plugins.  It will eventually defer to resource.Plugin,
  which itself defers -- over an RPC interface -- to the actual plugin, one
  per package exposing resources.  The provider will also understand how to
  load, cache, and overall manage the lifetime of each plugin.

* resource.Resource is the actual resource object.  This is created from
  the overall evaluation object graph, but is simplified.  It contains only
  serializable properties, for example.  Inter-resource references are
  translated into serializable monikers as part of creating the resource.

* resource.Moniker is a serializable string that uniquely identifies
  a resource in the Mu system.  This is in contrast to resource IDs, which
  are generated by resource providers and generally opaque to the Mu
  system.  See marapongo/mu#69 for more information about monikers and some
  of their challenges (namely, designing a stable algorithm).

* resource.Snapshot is a "snapshot" taken from a graph of resources.  This
  is a transitive closure of state representing one possible configuration
  of a given environment.  This is what plans are created from.  Eventually,
  two snapshots will be diffable, in order to perform incremental updates.
  One way of thinking about this is that a snapshot of the old world's state
  is advanced, one step at a time, until it reaches a desired snapshot of
  the new world's state.

* resource.Plan is a plan for carrying out desired CRUD operations on a target
  environment.  Each plan consists of zero-to-many Steps, each of which has
  a CRUD operation type, a resource target, and a next step.  This is an
  enumerator because it is possible the plan will evolve -- and introduce new
  steps -- as it is carried out (hence, the Next() method).  At the moment, this
  is linearized; eventually, we want to make this more "graph-like" so that we
  can exploit available parallelism within the dependencies.

There are tons of TODOs remaining.  However, the `mu plan` command is functioning
with these new changes -- including colorization FTW -- so I'm landing it now.

This is part of marapongo/mu#38 and marapongo/mu#41.
This commit is contained in:
joeduffy 2017-02-17 12:31:48 -08:00
parent 28a13a28cb
commit d9ee2429da
13 changed files with 674 additions and 87 deletions

View file

@ -3,6 +3,7 @@
package cmd
import (
"bytes"
"fmt"
"os"
"strconv"
@ -10,10 +11,10 @@ import (
"github.com/spf13/cobra"
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/compiler/types"
"github.com/marapongo/mu/pkg/compiler/types/predef"
"github.com/marapongo/mu/pkg/diag/colors"
"github.com/marapongo/mu/pkg/eval/rt"
"github.com/marapongo/mu/pkg/graph"
"github.com/marapongo/mu/pkg/resource"
"github.com/marapongo/mu/pkg/util/contract"
)
func newPlanCmd() *cobra.Command {
@ -31,9 +32,16 @@ func newPlanCmd() *cobra.Command {
"By default, a blueprint package is loaded from the current directory. Optionally,\n" +
"a path to a blueprint elsewhere can be provided as the [blueprint] argument.",
Run: func(cmd *cobra.Command, args []string) {
// Perform the compilation and, if non-nil is returned, output the plan.
// Perform the compilation and, if non-nil is returned, create a plan and print it.
if mugl := compile(cmd, args); mugl != nil {
printPlan(mugl)
// TODO: fetch the old plan for purposes of diffing.
rs, err := resource.NewSnapshot(mugl) // create a resource snapshot from the object graph.
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: %v\n", err)
os.Exit(-1)
}
plan := resource.NewPlan(rs, nil) // generate a plan for creating the resources from scratch.
printPlan(plan)
}
},
}
@ -41,70 +49,115 @@ func newPlanCmd() *cobra.Command {
return cmd
}
func printPlan(mugl graph.Graph) {
// Sort the graph output so that it's a DAG.
// TODO: consider pruning out all non-resources so there is less to sort.
sorted, err := graph.TopSort(mugl)
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: %v\n", err)
os.Exit(-1)
}
func printPlan(plan resource.Plan) {
// Now walk the plan's steps and and pretty-print them out.
step := plan.Steps()
for step != nil {
var b bytes.Buffer
// Now walk the elements and (for now), just print out which resources will be created.
for _, vert := range sorted {
o := vert.Obj()
t := o.Type()
if types.HasBaseName(t, predef.MuResourceClass) {
// Print the resource type.
fmt.Printf("+ %v:\n", t)
// Print this step information (resource and all its properties).
printStep(&b, step, "")
// Print all of the properties associated with this resource.
printProperties(o, " ")
}
// Now go ahead and emit the output to the console, and move on to the next step in the plan.
// TODO: it would be nice if, in the output, we showed the dependencies a la `git log --graph`.
s := colors.Colorize(b.String())
fmt.Printf(s)
step = step.Next()
}
}
func printProperties(obj *rt.Object, indent string) {
var keys []rt.PropertyKey
props := obj.PropertyValues()
func printStep(b *bytes.Buffer, step resource.Step, indent string) {
// First print out the operation.
switch step.Op() {
case resource.OpCreate:
b.WriteString(colors.Green)
b.WriteString("+ ")
case resource.OpDelete:
b.WriteString(colors.Red)
b.WriteString("- ")
default:
b.WriteString(" ")
}
// Next print the resource moniker, properties, etc.
printResource(b, step.Resource(), indent)
// Finally make sure to reset the color.
b.WriteString(colors.Reset)
}
func printResource(b *bytes.Buffer, res resource.Resource, indent string) {
// Create a resource "banner", first using the moniker.
banner := string(res.Moniker())
// If there is an ID and/or type, add those to the banner too.
id := res.ID()
t := res.Type()
var idty string
if id != "" {
idty = "<id=" + string(id)
}
if t != "" {
if idty == "" {
idty = "<"
} else {
idty += ", "
}
idty += "type=" + string(t)
}
if idty != "" {
banner += " " + idty + ">"
}
// Print the resource moniker.
b.WriteString(fmt.Sprintf("%s:\n", banner))
// Print all of the properties associated with this resource.
printObject(b, res.Properties(), indent+" ")
}
func printObject(b *bytes.Buffer, props resource.PropertyMap, indent string) {
// Compute the maximum with of property keys so we can justify everything.
keys := resource.StablePropertyKeys(props)
maxkey := 0
for _, k := range rt.StablePropertyKeys(props) {
if isPrintableProperty(props[k]) {
keys = append(keys, k)
if len(k) > maxkey {
maxkey = len(k)
}
for _, k := range keys {
if len(k) > maxkey {
maxkey = len(k)
}
}
// Now print out the values intelligently based on the type.
for _, k := range keys {
fmt.Printf("%v%-"+strconv.Itoa(maxkey)+"s: ", indent, k)
printProperty(props[k].Obj(), indent)
b.WriteString(fmt.Sprintf("%s%-"+strconv.Itoa(maxkey)+"s: ", indent, k))
printProperty(b, props[k], indent)
}
}
func printProperty(obj *rt.Object, indent string) {
switch obj.Type() {
case types.Bool, types.Number, types.String:
fmt.Printf("%v\n", obj)
default:
switch obj.Type().(type) {
case *symbols.ArrayType:
fmt.Printf("[\n")
for i, elem := range *obj.ArrayValue() {
fmt.Printf("%v [%d]: ", indent, i)
printProperty(elem.Obj(), fmt.Sprintf(indent+" "))
}
fmt.Printf("%s]\n", indent)
default:
fmt.Printf("<%s> {\n", obj.Type())
printProperties(obj, indent+" ")
fmt.Printf("%s}\n", indent)
func printProperty(b *bytes.Buffer, v resource.PropertyValue, indent string) {
if v.IsBool() {
b.WriteString(fmt.Sprintf("%t", v.BoolValue()))
} else if v.IsNumber() {
b.WriteString(fmt.Sprintf("%v", v.NumberValue()))
} else if v.IsString() {
b.WriteString(fmt.Sprintf("\"%s\"", v.StringValue()))
} else if v.IsResource() {
b.WriteString(fmt.Sprintf("-> *%s", v.ResourceValue()))
} else if v.IsArray() {
b.WriteString(fmt.Sprintf("[\n"))
for i, elem := range v.ArrayValue() {
prefix := fmt.Sprintf("%s [%d]: ", indent, i)
b.WriteString(prefix)
printProperty(b, elem, fmt.Sprintf("%-"+strconv.Itoa(len(prefix))+"s", ""))
}
b.WriteString(fmt.Sprintf("%s]", indent))
} else {
contract.Assert(v.IsObject())
b.WriteString("{\n")
printObject(b, v.ObjectValue(), indent+" ")
b.WriteString(fmt.Sprintf("%s}", indent))
}
b.WriteString("\n")
}
func isPrintableProperty(prop *rt.Pointer) bool {

46
pkg/diag/colors/colors.go Normal file
View file

@ -0,0 +1,46 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package colors
import (
"github.com/reconquest/loreley"
"github.com/marapongo/mu/pkg/util/contract"
)
const colorLeft = "<{%"
const colorRight = "%}>"
func init() {
// Change the Loreley delimiters from { and }, to something more complex, to avoid accidental collisions.
loreley.DelimLeft = colorLeft
loreley.DelimRight = colorRight
}
func Command(s string) string {
return colorLeft + s + colorRight
}
func Colorize(s string) string {
c, err := loreley.CompileAndExecuteToString(s, nil, nil)
contract.Assertf(err == nil, "Expected no errors during string colorization; str=%v, err=%v", s, err)
return c
}
var (
Black = Command("fg 0")
Red = Command("fg 1")
Green = Command("fg 2")
Yellow = Command("fg 3")
Magenta = Command("fg 4")
Cyan = Command("fg 5")
White = Command("fg 7")
BrightBlack = Command("fg 8")
BrightRed = Command("fg 9")
BrightGreen = Command("fg 10")
BrightYellow = Command("fg 11")
BrightMagenta = Command("fg 12")
BrightCyan = Command("fg 13")
BrightWhite = Command("fg 14")
Reset = Command("reset")
)

View file

@ -11,8 +11,8 @@ import (
"strconv"
"github.com/golang/glog"
"github.com/reconquest/loreley"
"github.com/marapongo/mu/pkg/diag/colors"
"github.com/marapongo/mu/pkg/util/contract"
)
@ -106,19 +106,6 @@ func (d *defaultSink) Warningf(diag *Diag, args ...interface{}) {
d.warnings++
}
const colorLeft = "<{%"
const colorRight = "%}>"
func init() {
// Change the Loreley delimiters from { and }, to something more complex, to avoid accidental collisions.
loreley.DelimLeft = colorLeft
loreley.DelimRight = colorRight
}
func colorString(s string) string {
return colorLeft + s + colorRight
}
func (d *defaultSink) Stringify(diag *Diag, cat Category, args ...interface{}) string {
var buffer bytes.Buffer
@ -132,9 +119,9 @@ func (d *defaultSink) Stringify(diag *Diag, cat Category, args ...interface{}) s
if d.opts.Colors {
switch cat {
case Error:
buffer.WriteString(colorString("fg 1")) // red
buffer.WriteString(colors.Red)
case Warning:
buffer.WriteString(colorString("fg 11")) // bright yellow
buffer.WriteString(colors.BrightYellow)
default:
contract.Failf("Unrecognized diagnostic category: %v", cat)
}
@ -151,18 +138,18 @@ func (d *defaultSink) Stringify(diag *Diag, cat Category, args ...interface{}) s
buffer.WriteString(": ")
if d.opts.Colors {
buffer.WriteString(colorString("reset"))
buffer.WriteString(colors.Reset)
}
// Finally, actually print the message itself.
if d.opts.Colors {
buffer.WriteString(colorString("fg 7")) // white
buffer.WriteString(colors.White)
}
buffer.WriteString(fmt.Sprintf(diag.Message, args...))
if d.opts.Colors {
buffer.WriteString(colorString("reset"))
buffer.WriteString(colors.Reset)
}
buffer.WriteRune('\n')
@ -174,9 +161,7 @@ func (d *defaultSink) Stringify(diag *Diag, cat Category, args ...interface{}) s
// If colorization was requested, compile and execute the directives now.
if d.opts.Colors {
var err error
s, err = loreley.CompileAndExecuteToString(s, nil, nil)
contract.Assertf(err == nil, "Expected no errors during string format operation; str=%v, err=%v", s, err)
s = colors.Colorize(s)
}
return s
@ -187,7 +172,7 @@ func (d *defaultSink) StringifyLocation(doc *Document, loc *Location) string {
if doc != nil {
if d.opts.Colors {
buffer.WriteString(colorString("fg 6")) // cyan
buffer.WriteString(colors.Cyan)
}
file := doc.File
@ -212,16 +197,14 @@ func (d *defaultSink) StringifyLocation(doc *Document, loc *Location) string {
var s string
if doc != nil || loc != nil {
if d.opts.Colors {
buffer.WriteString(colorString("reset"))
buffer.WriteString(colors.Reset)
}
s = buffer.String()
// If colorization was requested, compile and execute the directives now.
if d.opts.Colors {
var err error
s, err = loreley.CompileAndExecuteToString(s, nil, nil)
contract.Assertf(err == nil, "Expected no errors during string format operation; str=%v, err=%v", s, err)
s = colors.Colorize(s)
}
}

View file

@ -6,9 +6,9 @@ import (
"errors"
)
// TopSort topologically sorts the graph, yielding an array of nodes that are in dependency order, using a simple
// Topsort topologically sorts the graph, yielding an array of nodes that are in dependency order, using a simple
// DFS-based algorithm. The graph must be acyclic, otherwise this function will return an error.
func TopSort(g Graph) ([]Vertex, error) {
func Topsort(g Graph) ([]Vertex, error) {
var sorted []Vertex // will hold the sorted vertices.
visiting := make(map[Vertex]bool) // temporary entries to detect cycles.
visited := make(map[Vertex]bool) // entries to avoid visiting the same node twice.

3
pkg/resource/diff.go Normal file
View file

@ -0,0 +1,3 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource

32
pkg/resource/moniker.go Normal file
View file

@ -0,0 +1,32 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"github.com/marapongo/mu/pkg/graph"
"github.com/marapongo/mu/pkg/util/contract"
)
// Moniker is a friendly, but unique, name for a resource, most often auto-assigned by the Mu system. (In theory, we
// could support manually assigned monikers in the future, to help with the "stable moniker" problem outlined below).
// These monikers are used to perform graph diffing and resolution of resource object to the underlying provider ID.
type Moniker string
// NewMoniker creates a unique moniker for the given vertex v inside of the given graph g.
func NewMoniker(g graph.Graph, v graph.Vertex) Moniker {
// The algorithm for generating a moniker is quite simple at the moment.
//
// We begain by walk the in-edges to the vertex v, recursively until we hit a root node in g. The edge path
// traversed is then inspected for labels and those labels are concatenated with the vertex object's type name to
// form a moniker -- or string-based "path" -- from the root of the graph to the vertex within it.
//
// Note that it is possible there are multiple paths to the object. In that case, we pick the shortest one. If
// there are still more than one that are of equal length, we pick the first lexicographically ordered one.
//
// It's worth pointing out the "stable moniker" problem. Because the moniker is sensitive to the object's path, any
// changes in this path will alter the moniker. This can introduce difficulties in diffing two resource graphs. As
// a result, I suspect we will go through many iterations of this algortihm, and will need to provider facilities
// for developers to "rename" existing resources manually and/or even give resources IDs, monikers, etc. manually.
contract.Failf("Moniker creation not yet implemented")
return ""
}

129
pkg/resource/plan.go Normal file
View file

@ -0,0 +1,129 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"github.com/marapongo/mu/pkg/util/contract"
)
// TODO: cancellation.
// TODO: progress reporting.
// TODO: concurrency.
// TODO: handle output dependencies
// TODO: plan application.
// 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 {
Steps() Step // the first step to perform, linked to the rest.
Apply() error // performs the operations specified in this plan.
}
// Step is a specification for a deployment operation.
type Step interface {
Op() StepOp // the operation that will be performed.
Resource() Resource // the resource affected by performing this step.
Next() Step // the next step to perform, or nil if none.
Apply() error // performs the operation specified by this step.
}
// StepOp represents the kind of operation performed by this step.
type StepOp int
const (
OpCreate StepOp = iota
OpRead
OpUpdate
OpDelete
)
// NewPlan analyzes a resource graph rg compared to an optional old resource graph oldRg, and creates a plan that will
// carry out operations necessary to bring the old resource graph in line with the new one. It is ok for oldRg to be
// nil, in which case the plan will represent the steps necessary for a brand new deployment from whole cloth.
func NewPlan(s Snapshot, old Snapshot) Plan {
contract.Requiref(s != nil, "s", "!= nil")
// If old is non-nil, we need to diff the two snapshots to figure out the steps to take.
if old != nil {
return newUpdatePlan(s, old)
}
// If old is nil, on the other hand, our job is easier: we simply create all resources from scratch.
return newCreatePlan(s)
}
type plan struct {
first *step // the first step to take
}
var _ Plan = (*plan)(nil)
func (p *plan) Steps() Step { return p.first }
func (p *plan) Apply() error {
contract.Failf("Apply is not yet implemented")
return nil
}
func newUpdatePlan(s Snapshot, old Snapshot) *plan {
// First diff the snapshots; in a nutshell:
//
// - Anything in s but not old is a create
// - Anything in old but not s is a delete
// - Anything in both s and old is inspected:
// . Any changed properties imply an update
// . Otherwise, we may need to read the resource
//
// There are some caveats:
//
// - Any changes in dependencies are possibly interesting
// - Any changes in moniker are interesting (see note on stability in monikers.go)
//
contract.Failf("Update plans not yet implemented")
return nil
}
func newCreatePlan(s Snapshot) *plan {
// There is no old snapshot, so we are creating a plan from scratch. Everything is a create.
var head *step
var tail *step
for _, res := range s.Topsort() {
step := &step{
op: OpCreate,
res: res,
}
if tail == nil {
contract.Assert(head == nil)
head = step
tail = step
} else {
tail.next = step
tail = step
}
}
return &plan{first: head}
}
type step struct {
op StepOp // the operation to perform.
res Resource // the target of the operation.
next *step // the next step after this one in the plan.
}
var _ Step = (*step)(nil)
func (s *step) Op() StepOp { return s.op }
func (s *step) Resource() Resource { return s.res }
func (s *step) Next() Step {
if s.next == nil {
return nil
}
return s.next
}
func (s *step) Apply() error {
contract.Failf("Apply is not yet implemented")
return nil
}

9
pkg/resource/plugin.go Normal file
View file

@ -0,0 +1,9 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
// TODO: implement plugins.
// Plugin reflects a resource plugin, loaded dynamically for a single package.
type Plugin struct {
}

54
pkg/resource/provider.go Normal file
View file

@ -0,0 +1,54 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
// Provider presents a simple interface for orchestrating resource create, reead, update, and delete operations. Each
// provider understands how to handle all of the resource types within a single package.
//
// This interface hides some of the messiness of the underlying machinery, since providers are behind an RPC boundary.
//
// It is important to note that provider operations are not transactional. (Some providers might decide to offer
// transactional semantics, but such a provider is a rare treat.) As a result, failures in the operations below can
// range from benign to catastrophic (possibly leaving behind a corrupt resource). It is up to the provider to make a
// best effort to ensure catastrophies do not occur. The errors returned from mutating operations indicate both the
// underlying error condition in addition to a bit indicating whether the operation was successfully rolled back.
type Provider interface {
Create(res *Resource) (ID, error, ResourceState)
Read(id ID, t Type, props PropertyMap) (*Resource, error)
Update(old *Resource, new *Resource) (ID, error, ResourceState)
Delete(res *Resource)
}
type provider struct {
plugin *Plugin
}
// Create allocates a new instance of the provided resource and returns its unique ID afterwards.
func (p *provider) Create(res *Resource) (ID, error, ResourceState) {
return "", nil, StateOK // TODO: implement this.
}
// Read reads the instance state identified by id/t, and returns resource object (or nil if not found).
func (p *provider) Read(id ID, t Type, props PropertyMap) (*Resource, error) {
return nil, nil // TODO: implement this.
}
// 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 *provider) Update(old *Resource, new *Resource) (ID, error, ResourceState) {
return "", nil, StateOK // TODO: implement this.
}
// Delete tears down an existing resource with the given ID.
func (p *provider) Delete(id ID, t Type) (error, ResourceState) {
return nil, StateOK // TODO: implement this.
}
// ResourceState is returned when an error has occurred during a resource provider operation. It indicates whether the
// operation could be rolled back cleanly (OK). If not, it means the resource was left in an indeterminate state.
type ResourceState int
const (
StateOK ResourceState = iota
StateUnknown
)

171
pkg/resource/resource.go Normal file
View file

@ -0,0 +1,171 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"reflect"
"github.com/golang/glog"
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/compiler/types"
"github.com/marapongo/mu/pkg/compiler/types/predef"
"github.com/marapongo/mu/pkg/eval/rt"
"github.com/marapongo/mu/pkg/graph"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/pkg/util/contract"
)
// ID is a unique resource identifier; it is managed by the provider and is mostly opaque to Mu.
type ID string
// Type is a resource type identifier.
type Type tokens.Type
// Resource is an instance of a resource with an ID, type, and bag of state.
type Resource interface {
ID() ID // the resource's unique ID, assigned by the resource provider (or blank if uncreated).
Moniker() Moniker // the resource's object moniker, a human-friendly, unique name for the resource.
Type() Type // the resource's type.
Properties() PropertyMap // the resource's property map.
}
type PropertyMap map[PropertyKey]PropertyValue
type PropertyKey tokens.Name // the name of a property.
// PropertyValue is the value of a property, limited to a select few types (see below).
type PropertyValue struct {
V interface{}
}
func NewPropertyBool(v bool) PropertyValue { return PropertyValue{v} }
func NewPropertyNumber(v float64) PropertyValue { return PropertyValue{v} }
func NewPropertyString(v string) PropertyValue { return PropertyValue{v} }
func NewPropertyArray(v []PropertyValue) PropertyValue { return PropertyValue{v} }
func NewPropertyObject(v PropertyMap) PropertyValue { return PropertyValue{v} }
func NewPropertyResource(v Moniker) PropertyValue { return PropertyValue{v} }
func (v PropertyValue) BoolValue() bool { return v.V.(bool) }
func (v PropertyValue) NumberValue() float64 { return v.V.(float64) }
func (v PropertyValue) StringValue() string { return v.V.(string) }
func (v PropertyValue) ArrayValue() []PropertyValue { return v.V.([]PropertyValue) }
func (v PropertyValue) ObjectValue() PropertyMap { return v.V.(PropertyMap) }
func (v PropertyValue) ResourceValue() Moniker { return v.V.(Moniker) }
func (b PropertyValue) IsBool() bool {
_, is := b.V.(bool)
return is
}
func (b PropertyValue) IsNumber() bool {
_, is := b.V.(float64)
return is
}
func (b PropertyValue) IsString() bool {
_, is := b.V.(string)
return is
}
func (b PropertyValue) IsArray() bool {
_, is := b.V.([]PropertyValue)
return is
}
func (b PropertyValue) IsObject() bool {
_, is := b.V.(PropertyMap)
return is
}
func (b PropertyValue) IsResource() bool {
_, is := b.V.(Moniker)
return is
}
func IsResourceType(t symbols.Type) bool { return types.HasBaseName(t, predef.MuResourceClass) }
func IsResourceVertex(v graph.Vertex) bool { return IsResourceType(v.Obj().Type()) }
type resource struct {
id ID // the resource's unique ID, assigned by the resource provider (or blank if uncreated).
moniker Moniker // the resource's object moniker, a human-friendly, unique name for the resource.
t Type // the resource's type.
properties PropertyMap // the resource's property map.
}
func (r *resource) ID() ID { return r.id }
func (r *resource) Moniker() Moniker { return r.moniker }
func (r *resource) Type() Type { return r.t }
func (r *resource) Properties() PropertyMap { return r.properties }
func NewResource(g graph.Graph, v graph.Vertex) Resource {
obj := v.Obj()
t := obj.Type().Token()
// First create a moniker for this resource.
// TODO: use a real moniker by way of the NewMoniker(g, v) function.
m := Moniker(t)
// Now do a deep copy of the resource properties. This ensures property serializability.
props := cloneObject(obj)
// Finally allocate and return the resource object; note that ID is left blank until the provider assignes one.
return &resource{
moniker: m,
t: Type(t),
properties: props,
}
}
// cloneObject creates a property map out of a runtime object. The result is fully serializable in the sense that it
// can be stored in a JSON or YAML file, serialized over an RPC interface, etc. In particular, any references to other
// resources are replaced with their moniker equivalents, which the runtime understands.
func cloneObject(obj *rt.Object) PropertyMap {
contract.Assert(obj != nil)
src := obj.PropertyValues()
dest := make(PropertyMap)
for _, k := range rt.StablePropertyKeys(src) {
// TODO: detect cycles.
if v, ok := cloneObjectValue(src[k].Obj()); ok {
dest[PropertyKey(k)] = v
}
}
return dest
}
// cloneObjectValue creates a single property value out of a runtime object. It returns false if the property could not
// be stored in a property (e.g., it is a function or other unrecognized or unserializable runtime object).
func cloneObjectValue(obj *rt.Object) (PropertyValue, bool) {
t := obj.Type()
if IsResourceType(t) {
// TODO: we need to somehow recover a moniker for this dependency.
return NewPropertyResource(Moniker(t.Token())), true
}
switch t {
case types.Bool:
return NewPropertyBool(obj.BoolValue()), true
case types.Number:
return NewPropertyNumber(obj.NumberValue()), true
case types.String:
return NewPropertyString(obj.StringValue()), true
case types.Object, types.Dynamic:
obj := cloneObject(obj) // an object literal, clone it
return NewPropertyObject(obj), true
}
switch t.(type) {
case *symbols.ArrayType:
var result []PropertyValue
for _, e := range *obj.ArrayValue() {
if v, ok := cloneObjectValue(e.Obj()); ok {
result = append(result, v)
}
}
return NewPropertyArray(result), true
case *symbols.Class:
obj := cloneObject(obj) // a class, just deep clone it
return NewPropertyObject(obj), true
}
// TODO: handle symbols.MapType.
// TODO: it's unclear if we should do something more drastic here. There will always be unrecognized property
// kinds because objects contain things like constructors, methods, etc. But we may want to ratchet this a bit.
glog.V(5).Infof("Ignoring object value of type '%v': unrecognized kind %v", t, reflect.TypeOf(t))
return PropertyValue{}, false
}

76
pkg/resource/snapshot.go Normal file
View file

@ -0,0 +1,76 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"github.com/marapongo/mu/pkg/graph"
"github.com/marapongo/mu/pkg/util/contract"
)
// Snapshot is a view of a collection of resources in an environment at a point in time. It describes resources; their
// IDs, names, and properties; their dependencies; and more. A snapshot is a diffable entity and can be used to create
// or apply an infrastructure deployment plan in order to make reality match the snapshot state.
type Snapshot interface {
Objects() graph.Graph // the raw underlying object graph.
Resources() graph.Graph // a graph containing just resources and their dependencies.
Topsort() []Resource // a topologically sorted list of resources (based on dependencies).
ResourceByID(id ID, t Type) Resource // looks up a resource by ID and type.
ResourceByMoniker(m Moniker) Resource // looks up a resource by its moniker.
}
// NewSnapshot takes an object graph and produces a resource snapshot from it. It understands how to name resources
// based on their position within the graph and how to identify and record dependencies. This function can fail
// dynamically if the input graph did not satisfy the preconditions for resource graphs (like that it is a DAG).
func NewSnapshot(g graph.Graph) (Snapshot, error) {
// TODO: create the resource graph.
tops, err := topsort(g)
if err != nil {
return nil, err
}
return &snapshot{g: g, tops: tops}, nil
}
type snapshot struct {
g graph.Graph // the underlying MuGL object graph.
res graph.Graph // the MuGL graph containing just resources.
tops []Resource // the topologically sorted linearized list of resources.
}
func (s *snapshot) Objects() graph.Graph { return s.g }
func (s *snapshot) Resources() graph.Graph { return s.res }
func (s *snapshot) Topsort() []Resource { return s.tops }
func topsort(g graph.Graph) ([]Resource, error) {
var resources []Resource
// TODO: we want this to return a *graph*, not a linearized list, so that we can parallelize.
// TODO: this should actually operate on the resource graph, once it exists. This has two advantages: first, there
// will be less to sort; second, we won't need an extra pass just to prune out the result.
// TODO: as soon as we create the resource graph, we need to memoize monikers so that we can do lookups.
// Sort the graph output so that it's a DAG; if it's got cycles, this can fail.
sorted, err := graph.Topsort(g)
if err != nil {
return resources, err
}
// Now walk the list and prune out anything that isn't a resource.
for _, v := range sorted {
if IsResourceVertex(v) {
res := NewResource(g, v)
resources = append(resources, res)
}
}
return resources, nil
}
func (s *snapshot) ResourceByID(id ID, t Type) Resource {
contract.Failf("TODO: not yet implemented")
return nil
}
func (s *snapshot) ResourceByMoniker(m Moniker) Resource {
contract.Failf("TODO: not yet implemented")
return nil
}

30
pkg/resource/stable.go Normal file
View file

@ -0,0 +1,30 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"sort"
)
func StablePropertyKeys(props PropertyMap) []PropertyKey {
sorted := make(propertyKeys, 0, len(props))
for prop := range props {
sorted = append(sorted, prop)
}
sort.Sort(sorted)
return sorted
}
type propertyKeys []PropertyKey
func (s propertyKeys) Len() int {
return len(s)
}
func (s propertyKeys) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s propertyKeys) Less(i, j int) bool {
return s[i] < s[j]
}

View file

@ -15,7 +15,7 @@ service ResourceProvider {
// Create allocates a new instance of the provided resource and returns its unique ID afterwards. (The input ID
// must be blank.) If this call fails, the resource must not have been created (i.e., it is "transacational").
rpc Create(CreateRequest) returns (CreateResponse) {}
// Read read the instance state identifier by ID, returning a populated resource object, or an error if not found.
// 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.
@ -36,7 +36,7 @@ message CreateResponse {
message ReadRequest {
string id = 1; // the ID of the resource to read.
string type = 2; // the type token of the resource.
google.protobuf.Struct properties = 3; // an optional list of properties to read (if empty, all).
google.protobuf.Struct properties = 1; // an optional list of properties to read (if empty, all).
}
message ReadResponse {
@ -45,16 +45,17 @@ message ReadResponse {
message UpdateRequest {
string id = 1; // the ID of the resource to update.
string type = 2; // the type token of the resource.
google.protobuf.Struct olds = 3; // the old values of properties to update.
google.protobuf.Struct news = 4; // the new values of properties to update.
string type = 2; // the type token of the resource to update.
google.protobuf.Struct olds = 2; // the old values of properties to update.
google.protobuf.Struct news = 3; // the new values of properties to update.
}
message UpdateResponse {
string id = 1;
string id = 1; // the new ID for the resource, if it had to be recreated.
}
message DeleteRequest {
string id = 1; // the ID of the resource to delete.
string id = 1; // the ID of the resource to delete.
string type = 2; // the type token of the resource to update.
}