pulumi/pkg/resource/mugl.go
joeduffy c120f62964 Redo object monikers
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.
2017-02-24 14:50:02 -08:00

383 lines
10 KiB
Go

// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"bytes"
"encoding/json"
"reflect"
"github.com/marapongo/mu/pkg/compiler/core"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/pkg/util/contract"
)
// MuglSnapshot is a serializable, flattened MuGL graph structure, specifically for snapshots. It is very similar to
// the actual Snapshot interface, except that it flattens and rearranges a few data structures for serializability.
type MuglSnapshot struct {
Target Namespace `json:"target"` // the target environment name.
Package tokens.PackageName `json:"package"` // the package which created this graph.
Args *core.Args `json:"args,omitempty"` // the blueprint args for graph creation.
Refs *string `json:"refs,omitempty"` // the ref alias, if any (`#ref` by default).
Resources *MuglResourceMap `json:"resources,omitempty"` // a map of monikers to resource vertices.
}
// DefaultSnapshotReftag is the default ref tag for intra-graph edges.
const DefaultSnapshotReftag = "#ref"
// MuglResource is a serializable vertex within a MuGL graph, specifically for resource snapshots.
type MuglResource struct {
ID *ID `json:"id,omitempty"` // the provider ID for this resource, if any.
Type tokens.Type `json:"type"` // this resource's full type token.
Properties *MuglPropertyMap `json:"properties,omitempty"` // an untyped bag of properties.
}
// MuglPropertyMap is a property map from resource key to the underlying property value.
type MuglPropertyMap map[string]interface{}
// SerializeSnapshot turns a snapshot into a MuGL data structure suitable for serialization.
func SerializeSnapshot(snap Snapshot, reftag string) *MuglSnapshot {
contract.Assert(snap != nil)
// Initialize the reftag if needed, and only serialize it if overridden.
var refp *string
if reftag == "" {
reftag = DefaultSnapshotReftag
} else {
refp = &reftag
}
// Serialize all vertices and only include a vertex section if non-empty.
var resm *MuglResourceMap
if snapres := snap.Resources(); len(snapres) > 0 {
resm = NewMuglResourceMap()
for _, res := range snap.Resources() {
m := res.Moniker()
contract.Assertf(string(m) != "", "Unexpected empty resource moniker")
contract.Assertf(!resm.Has(m), "Unexpected duplicate resource moniker '%v'", m)
resm.Add(m, SerializeResource(res, reftag))
}
}
// Only include the arguments in the output if non-emtpy.
var argsp *core.Args
if args := snap.Args(); len(args) > 0 {
argsp = &args
}
return &MuglSnapshot{
Target: snap.Ns(),
Package: snap.Pkg(), // TODO: eventually, this should carry version metadata too.
Args: argsp,
Refs: refp,
Resources: resm,
}
}
// SerializeResource turns a resource into a MuGL data structure suitable for serialization.
func SerializeResource(res Resource, reftag string) *MuglResource {
contract.Assert(res != nil)
// Only serialize the ID if it is non-empty.
var idp *ID
if id := res.ID(); id != ID("") {
idp = &id
}
// Serialize all properties recursively, and add them if non-empty.
var props *MuglPropertyMap
if result, use := SerializeProperties(res.Properties(), reftag); use {
props = &result
}
return &MuglResource{
ID: idp,
Type: res.Type(),
Properties: props,
}
}
// SerializeProperties serializes a resource property bag so that it's suitable for serialization.
func SerializeProperties(props PropertyMap, reftag string) (MuglPropertyMap, bool) {
dst := make(MuglPropertyMap)
for _, k := range StablePropertyKeys(props) {
if v, use := SerializeProperty(props[k], reftag); use {
dst[string(k)] = v
}
}
if len(dst) > 0 {
return dst, true
}
return nil, false
}
// SerializeProperty serializes a resource property value so that it's suitable for serialization.
func SerializeProperty(prop PropertyValue, reftag string) (interface{}, bool) {
// Skip nulls.
if prop.IsNull() {
return nil, false
}
// For arrays, make sure to recurse.
if prop.IsArray() {
var arr []interface{}
for _, elem := range prop.ArrayValue() {
if v, use := SerializeProperty(elem, reftag); use {
arr = append(arr, v)
}
}
if len(arr) > 0 {
return arr, true
}
return nil, false
}
// Also for objects, recurse and use naked properties.
if prop.IsObject() {
return SerializeProperties(prop.ObjectValue(), reftag)
}
// Morph resources into their equivalent `{ "#ref": "<moniker>" }` form.
if prop.IsResource() {
return map[string]string{
reftag: string(prop.ResourceValue()),
}, true
}
// All others are returned as-is.
return prop.V, true
}
// DeserializeSnapshot takes a serialized MuGL snapshot data structure and returns its associated snapshot.
func DeserializeSnapshot(ctx *Context, mugl *MuglSnapshot) Snapshot {
// Determine the reftag to use.
var reftag string
if mugl.Refs == nil {
reftag = DefaultSnapshotReftag
} else {
reftag = *mugl.Refs
}
// For every serialized resource vertex, create a MuglResource out of it.
var resources []Resource
if mugl.Resources != nil {
// TODO: we need to enumerate resources in the specific order in which they were emitted.
for _, kvp := range mugl.Resources.Iter() {
// Deserialize the resources, if they exist.
res := kvp.Value
var props PropertyMap
if res.Properties == nil {
props = make(PropertyMap)
} else {
props = DeserializeProperties(*res.Properties, reftag)
}
// And now just produce a resource object using the information available.
var id ID
if res.ID != nil {
id = *res.ID
}
resources = append(resources, NewResource(id, kvp.Key, res.Type, props))
}
}
var args core.Args
if mugl.Args != nil {
args = *mugl.Args
}
return NewSnapshot(ctx, mugl.Target, mugl.Package, args, resources)
}
func DeserializeProperties(props MuglPropertyMap, reftag string) PropertyMap {
result := make(PropertyMap)
for k, prop := range props {
result[PropertyKey(k)] = DeserializeProperty(prop, reftag)
}
return result
}
func DeserializeProperty(v interface{}, reftag string) PropertyValue {
if v != nil {
switch w := v.(type) {
case bool:
return NewPropertyBool(w)
case float64:
return NewPropertyNumber(w)
case string:
return NewPropertyString(w)
case []interface{}:
var arr []PropertyValue
for _, elem := range w {
arr = append(arr, DeserializeProperty(elem, reftag))
}
return NewPropertyArray(arr)
case map[string]interface{}:
// If the map has a single entry and it is the reftag, this is a moniker.
if len(w) == 1 {
if tag, has := w[reftag]; has {
if tagstr, isstring := tag.(string); isstring {
return NewPropertyResource(Moniker(tagstr))
}
}
}
// Otherwise, this is an arbitrary object value.
obj := DeserializeProperties(MuglPropertyMap(w), reftag)
return NewPropertyObject(obj)
default:
contract.Failf("Unrecognized property type: %v", reflect.ValueOf(v))
}
}
return NewPropertyNull()
}
// MuglResourceMap is a map of moniker to resource, that also preserves a stable order of its keys. This ensures
// enumerations are ordered deterministically, versus Go's built-in map type whose enumeration is randomized.
// Additionally, because of this stable ordering, marshaling to and from JSON also preserves the order of keys.
type MuglResourceMap struct {
m map[Moniker]*MuglResource
keys []Moniker
}
func NewMuglResourceMap() *MuglResourceMap {
return &MuglResourceMap{m: make(map[Moniker]*MuglResource)}
}
func (m *MuglResourceMap) Keys() []Moniker { return m.keys }
func (m *MuglResourceMap) Len() int { return len(m.keys) }
func (m *MuglResourceMap) Add(k Moniker, v *MuglResource) {
_, has := m.m[k]
contract.Assertf(!has, "Unexpected duplicate key '%v' added to map")
m.m[k] = v
m.keys = append(m.keys, k)
}
func (m *MuglResourceMap) Delete(k Moniker) {
_, has := m.m[k]
contract.Assertf(has, "Unexpected delete of non-existent key key '%v'")
delete(m.m, k)
for i, ek := range m.keys {
if ek == k {
newk := m.keys[:i]
m.keys = append(newk, m.keys[i+1:]...)
break
}
contract.Assertf(i != len(m.keys)-1, "Expected to find deleted key '%v' in map's keys")
}
}
func (m *MuglResourceMap) Get(k Moniker) (*MuglResource, bool) {
v, has := m.m[k]
return v, has
}
func (m *MuglResourceMap) Has(k Moniker) bool {
_, has := m.m[k]
return has
}
func (m *MuglResourceMap) Must(k Moniker) *MuglResource {
v, has := m.m[k]
contract.Assertf(has, "Expected key '%v' to exist in this map", k)
return v
}
func (m *MuglResourceMap) Set(k Moniker, v *MuglResource) {
_, has := m.m[k]
contract.Assertf(has, "Expected key '%v' to exist in this map for setting an element", k)
m.m[k] = v
}
func (m *MuglResourceMap) SetOrAdd(k Moniker, v *MuglResource) {
if _, has := m.m[k]; has {
m.Set(k, v)
} else {
m.Add(k, v)
}
}
type MuglResourceKeyValue struct {
Key Moniker
Value *MuglResource
}
// Iter can be used to conveniently range over a map's contents stably.
func (m *MuglResourceMap) Iter() []MuglResourceKeyValue {
var kvps []MuglResourceKeyValue
for _, k := range m.Keys() {
kvps = append(kvps, MuglResourceKeyValue{k, m.Must(k)})
}
return kvps
}
func (m *MuglResourceMap) MarshalJSON() ([]byte, error) {
var b bytes.Buffer
b.WriteString("{")
for i, k := range m.Keys() {
if i != 0 {
b.WriteString(",")
}
kb, err := json.Marshal(k)
if err != nil {
return nil, err
}
b.Write(kb)
b.WriteString(":")
vb, err := json.Marshal(m.Must(k))
if err != nil {
return nil, err
}
b.Write(vb)
}
b.WriteString("}")
return b.Bytes(), nil
}
func (m *MuglResourceMap) UnmarshalJSON(b []byte) error {
contract.Assert(m.m == nil)
m.m = make(map[Moniker]*MuglResource)
// Do a pass and read keys and values in the right order.
rdr := bytes.NewReader(b)
dec := json.NewDecoder(rdr)
// First, eat the open object curly '{':
contract.Assert(dec.More())
opencurly, err := dec.Token()
if err != nil {
return err
}
contract.Assert(opencurly.(json.Delim) == '{')
// Parse out every resource key (Moniker) and element (*MuglResource):
for dec.More() {
// See if we've reached the closing '}'; if yes, chew on it and break.
token, err := dec.Token()
if err != nil {
return err
}
if closecurly, isclose := token.(json.Delim); isclose {
contract.Assert(closecurly == '}')
break
}
k := Moniker(token.(string))
contract.Assert(dec.More())
var v *MuglResource
if err := dec.Decode(&v); err != nil {
return err
}
contract.Assert(!m.Has(k))
m.Add(k, v)
}
return nil
}