diff --git a/cmd/plan.go b/cmd/plan.go index c61ea982a..f658dfd24 100644 --- a/cmd/plan.go +++ b/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 = "" + } + + // 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 { diff --git a/pkg/diag/colors/colors.go b/pkg/diag/colors/colors.go new file mode 100644 index 000000000..bbe3d04dd --- /dev/null +++ b/pkg/diag/colors/colors.go @@ -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") +) diff --git a/pkg/diag/sink.go b/pkg/diag/sink.go index cbd98c156..acac2f849 100644 --- a/pkg/diag/sink.go +++ b/pkg/diag/sink.go @@ -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) } } diff --git a/pkg/graph/topsort.go b/pkg/graph/topsort.go index d290ed0dc..bf4f7b230 100644 --- a/pkg/graph/topsort.go +++ b/pkg/graph/topsort.go @@ -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. diff --git a/pkg/resource/diff.go b/pkg/resource/diff.go new file mode 100644 index 000000000..75e46550f --- /dev/null +++ b/pkg/resource/diff.go @@ -0,0 +1,3 @@ +// Copyright 2016 Marapongo, Inc. All rights reserved. + +package resource diff --git a/pkg/resource/moniker.go b/pkg/resource/moniker.go new file mode 100644 index 000000000..258f5b8d0 --- /dev/null +++ b/pkg/resource/moniker.go @@ -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 "" +} diff --git a/pkg/resource/plan.go b/pkg/resource/plan.go new file mode 100644 index 000000000..03f58535c --- /dev/null +++ b/pkg/resource/plan.go @@ -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 +} diff --git a/pkg/resource/plugin.go b/pkg/resource/plugin.go new file mode 100644 index 000000000..4ee7da57e --- /dev/null +++ b/pkg/resource/plugin.go @@ -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 { +} diff --git a/pkg/resource/provider.go b/pkg/resource/provider.go new file mode 100644 index 000000000..14b3f3d17 --- /dev/null +++ b/pkg/resource/provider.go @@ -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 +) diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go new file mode 100644 index 000000000..e6bdb8050 --- /dev/null +++ b/pkg/resource/resource.go @@ -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 +} diff --git a/pkg/resource/snapshot.go b/pkg/resource/snapshot.go new file mode 100644 index 000000000..68e94e71f --- /dev/null +++ b/pkg/resource/snapshot.go @@ -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 +} diff --git a/pkg/resource/stable.go b/pkg/resource/stable.go new file mode 100644 index 000000000..ed11e0e7e --- /dev/null +++ b/pkg/resource/stable.go @@ -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] +} diff --git a/sdk/proto/provider.proto b/sdk/proto/provider.proto index 6acf4a7b1..77d8f5dbe 100644 --- a/sdk/proto/provider.proto +++ b/sdk/proto/provider.proto @@ -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. }