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:
parent
28a13a28cb
commit
d9ee2429da
155
cmd/plan.go
155
cmd/plan.go
|
@ -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
46
pkg/diag/colors/colors.go
Normal 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")
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
3
pkg/resource/diff.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Copyright 2016 Marapongo, Inc. All rights reserved.
|
||||
|
||||
package resource
|
32
pkg/resource/moniker.go
Normal file
32
pkg/resource/moniker.go
Normal 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
129
pkg/resource/plan.go
Normal 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
9
pkg/resource/plugin.go
Normal 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
54
pkg/resource/provider.go
Normal 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
171
pkg/resource/resource.go
Normal 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
76
pkg/resource/snapshot.go
Normal 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
30
pkg/resource/stable.go
Normal 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]
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue