This change overhauls the way we do object monikers. The old mechanism, generating monikers using graph paths, was far too brittle and prone to collisions. The new approach mixes some amount of "automatic scoping" plus some "explicit naming." Although there is some explicitness, this is arguably a good thing, as the monikers will be relatable back to the source more readily by developers inspecting the graph and resource state. Each moniker has four parts: <Namespace>::<AllocModule>::<Type>::<Name> wherein each element is the following: <Namespace> The namespace being deployed into <AllocModule> The module in which the object was allocated <Type> The type of the resource <Name> The assigned name of the resource The <Namespace> is essentially the deployment target -- so "prod", "stage", etc -- although it is more general purpose to allow for future namespacing within a target (e.g., "prod/customer1", etc); for now this is rudimentary, however, see marapongo/mu#94. The <AllocModule> is the token for the code that contained the 'new' that led to this object being created. In the future, we may wish to extend this to also track the module under evaluation. (This is a nice aspect of monikers; they can become arbitrarily complex, so long as they are precise, and not prone to false positives/negatives.) The <Name> warrants more discussion. The resource provider is consulted via a new gRPC method, Name, that fetches the name. How the provider does this is entirely up to it. For some resource types, the resource may have properties that developers must set (e.g., `new Bucket("foo")`); for other providers, perhaps the resource intrinsically has a property that explicitly and uniquely qualifies the object (e.g., AWS SecurityGroups, via `new SecurityGroup({groupName: "my-sg"}`); and finally, it's conceivable that a provider might auto-generate the name (e.g., such as an AWS Lambda whose name could simply be a hash of the source code contents). This should overall produce better results with respect to moniker collisions, ability to match resources, and the usability of the system.
161 lines
6.2 KiB
Go
161 lines
6.2 KiB
Go
// 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/heapstate"
|
|
"github.com/marapongo/mu/pkg/eval/rt"
|
|
"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
|
|
|
|
// 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() tokens.Type // the resource's type.
|
|
Properties() PropertyMap // the resource's property map.
|
|
HasID() bool // returns true if the resource has been assigned an ID.
|
|
SetID(id ID) // assignes an ID to this resource, for those under creation.
|
|
HasMoniker() bool // returns true if the resource has been assigned moniker.
|
|
SetMoniker(m Moniker) // assignes a moniker to this resource, for those under creation.
|
|
}
|
|
|
|
// 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
|
|
)
|
|
|
|
func IsResourceType(t symbols.Type) bool { return types.HasBaseName(t, predef.MuResourceClass) }
|
|
func IsResourceVertex(v *heapstate.ObjectVertex) 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 tokens.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() tokens.Type { return r.t }
|
|
func (r *resource) Properties() PropertyMap { return r.properties }
|
|
|
|
func (r *resource) HasID() bool { return (string(r.id) != "") }
|
|
func (r *resource) SetID(id ID) {
|
|
contract.Requiref(!r.HasID(), "id", "empty")
|
|
r.id = id
|
|
}
|
|
|
|
func (r *resource) HasMoniker() bool { return (string(r.moniker) != "") }
|
|
func (r *resource) SetMoniker(m Moniker) {
|
|
contract.Requiref(!r.HasMoniker(), "moniker", "empty")
|
|
r.moniker = m
|
|
}
|
|
|
|
// NewResource creates a new resource from the information provided.
|
|
func NewResource(id ID, moniker Moniker, t tokens.Type, properties PropertyMap) Resource {
|
|
return &resource{
|
|
id: id,
|
|
moniker: moniker,
|
|
t: t,
|
|
properties: properties,
|
|
}
|
|
}
|
|
|
|
// NewObjectResource creates a new resource object out of the runtime object provided. The context is used to resolve
|
|
// dependencies between resources and must contain all references that could be encountered.
|
|
func NewObjectResource(ctx *Context, obj *rt.Object) Resource {
|
|
t := obj.Type()
|
|
contract.Assert(IsResourceType(t))
|
|
|
|
// Extract the moniker. This must already exist.
|
|
m, hasm := ctx.ObjMks[obj]
|
|
contract.Assertf(!hasm, "Object already assigned a moniker '%v'; double allocation detected", m)
|
|
|
|
// Do a deep copy of the resource properties. This ensures property serializability.
|
|
props := cloneObject(ctx, obj)
|
|
|
|
// Finally allocate and return the resource object; note that ID is left blank until the provider assignes one.
|
|
return &resource{
|
|
t: t.TypeToken(),
|
|
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(ctx *Context, 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(ctx, 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(ctx *Context, obj *rt.Object) (PropertyValue, bool) {
|
|
t := obj.Type()
|
|
if IsResourceType(t) {
|
|
// For resources, simply look up the moniker from the resource map.
|
|
m, hasm := ctx.ObjMks[obj]
|
|
contract.Assertf(hasm, "Missing object reference; possible out of order dependency walk")
|
|
return NewPropertyResource(m), true
|
|
}
|
|
|
|
switch t {
|
|
case types.Null:
|
|
return NewPropertyNull(), true
|
|
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(ctx, 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(ctx, e.Obj()); ok {
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
return NewPropertyArray(result), true
|
|
case *symbols.Class:
|
|
obj := cloneObject(ctx, 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
|
|
}
|