2017-03-15 03:26:14 +01:00
|
|
|
// Copyright 2017 Pulumi, Inc. All rights reserved.
|
2017-02-22 03:31:43 +01:00
|
|
|
|
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
2017-02-23 02:33:08 +01:00
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2017-02-22 23:32:03 +01:00
|
|
|
"reflect"
|
2017-02-26 20:20:14 +01:00
|
|
|
"time"
|
2017-02-22 23:32:03 +01:00
|
|
|
|
2017-02-25 16:25:33 +01:00
|
|
|
"github.com/pulumi/coconut/pkg/compiler/core"
|
|
|
|
"github.com/pulumi/coconut/pkg/tokens"
|
|
|
|
"github.com/pulumi/coconut/pkg/util/contract"
|
2017-02-22 03:31:43 +01:00
|
|
|
)
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
// DeploymentRecord is a serializable, flattened CocoGL graph structure, representing a deployment. It is similar
|
2017-02-25 16:25:33 +01:00
|
|
|
// to the actual Snapshot interface, except that it flattens and rearranges a few data structures for serializability.
|
2017-02-26 20:20:14 +01:00
|
|
|
// Over time, we also expect this to gather more information about deployments themselves.
|
|
|
|
type DeploymentRecord struct {
|
2017-04-08 16:44:02 +02:00
|
|
|
Time time.Time `json:"time"` // the time of the deployment.
|
|
|
|
Reftag *string `json:"reftag,omitempty"` // the ref alias, if any (`#ref` by default).
|
|
|
|
Package tokens.Package `json:"package"` // the package deployed by this record.
|
|
|
|
Args *core.Args `json:"args,omitempty"` // the blueprint args for graph creation.
|
|
|
|
Resources *DeploymentMap `json:"resources,omitempty"` // a map of URNs to resource vertices.
|
2017-02-22 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
// DefaultDeploymentReftag is the default ref tag for intra-graph edges.
|
|
|
|
const DefaultDeploymentReftag = "#ref"
|
2017-02-22 03:31:43 +01:00
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
// Deployment is a serializable vertex within a CocoGL graph, specifically for resource snapshots.
|
|
|
|
type Deployment struct {
|
2017-02-26 20:20:14 +01:00
|
|
|
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 *DeployedPropertyMap `json:"properties,omitempty"` // an untyped bag of properties.
|
2017-02-22 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
// DeployedPropertyMap is a property map from resource key to the underlying property value.
|
|
|
|
type DeployedPropertyMap map[string]interface{}
|
2017-02-22 03:31:43 +01:00
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
func serializeDeploymentRecord(snap Snapshot, reftag string) *DeploymentRecord {
|
|
|
|
// Initialize the reftag if needed, and only serialize if overridden.
|
2017-02-22 03:31:43 +01:00
|
|
|
var refp *string
|
|
|
|
if reftag == "" {
|
2017-02-26 20:20:14 +01:00
|
|
|
reftag = DefaultDeploymentReftag
|
2017-02-22 03:31:43 +01:00
|
|
|
} else {
|
|
|
|
refp = &reftag
|
|
|
|
}
|
|
|
|
|
|
|
|
// Serialize all vertices and only include a vertex section if non-empty.
|
2017-04-08 16:44:02 +02:00
|
|
|
var resm *DeploymentMap
|
2017-02-23 02:33:08 +01:00
|
|
|
if snapres := snap.Resources(); len(snapres) > 0 {
|
2017-04-08 16:44:02 +02:00
|
|
|
resm = NewDeploymentMap()
|
2017-02-23 02:33:08 +01:00
|
|
|
for _, res := range snap.Resources() {
|
2017-03-03 02:10:10 +01:00
|
|
|
m := res.URN()
|
|
|
|
contract.Assertf(string(m) != "", "Unexpected empty resource URN")
|
|
|
|
contract.Assertf(!resm.Has(m), "Unexpected duplicate resource URN '%v'", m)
|
2017-04-08 16:44:02 +02:00
|
|
|
resm.Add(m, serializeDeployment(res, reftag))
|
2017-02-23 02:33:08 +01:00
|
|
|
}
|
2017-02-22 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
// Initialize the args pointer, but only serialize if the args are non-empty.
|
2017-02-22 03:31:43 +01:00
|
|
|
var argsp *core.Args
|
|
|
|
if args := snap.Args(); len(args) > 0 {
|
|
|
|
argsp = &args
|
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
return &DeploymentRecord{
|
|
|
|
Time: time.Now(),
|
|
|
|
Reftag: refp,
|
|
|
|
Package: snap.Pkg(),
|
2017-02-22 23:32:03 +01:00
|
|
|
Args: argsp,
|
2017-02-23 02:33:08 +01:00
|
|
|
Resources: resm,
|
2017-02-22 03:31:43 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
// serializeDeployment turns a resource into a CocoGL data structure suitable for serialization.
|
|
|
|
func serializeDeployment(res Resource, reftag string) *Deployment {
|
2017-02-22 03:31:43 +01:00
|
|
|
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.
|
2017-02-26 20:20:14 +01:00
|
|
|
var props *DeployedPropertyMap
|
|
|
|
if result, use := serializeProperties(res.Properties(), reftag); use {
|
2017-02-22 23:32:03 +01:00
|
|
|
props = &result
|
2017-02-22 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
return &Deployment{
|
2017-02-22 03:31:43 +01:00
|
|
|
ID: idp,
|
|
|
|
Type: res.Type(),
|
|
|
|
Properties: props,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
// serializeProperties serializes a resource property bag so that it's suitable for serialization.
|
|
|
|
func serializeProperties(props PropertyMap, reftag string) (DeployedPropertyMap, bool) {
|
|
|
|
dst := make(DeployedPropertyMap)
|
2017-02-22 23:32:03 +01:00
|
|
|
for _, k := range StablePropertyKeys(props) {
|
2017-02-26 20:20:14 +01:00
|
|
|
if v, use := serializeProperty(props[k], reftag); use {
|
2017-02-22 23:32:03 +01:00
|
|
|
dst[string(k)] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(dst) > 0 {
|
|
|
|
return dst, true
|
|
|
|
}
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
// serializeProperty serializes a resource property value so that it's suitable for serialization.
|
|
|
|
func serializeProperty(prop PropertyValue, reftag string) (interface{}, bool) {
|
2017-02-22 03:31:43 +01:00
|
|
|
// 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() {
|
2017-02-26 20:20:14 +01:00
|
|
|
if v, use := serializeProperty(elem, reftag); use {
|
2017-02-22 03:31:43 +01:00
|
|
|
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() {
|
2017-02-26 20:20:14 +01:00
|
|
|
return serializeProperties(prop.ObjectValue(), reftag)
|
2017-02-22 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2017-03-03 02:10:10 +01:00
|
|
|
// Morph resources into their equivalent `{ "#ref": "<URN>" }` form.
|
2017-02-22 03:31:43 +01:00
|
|
|
if prop.IsResource() {
|
|
|
|
return map[string]string{
|
|
|
|
reftag: string(prop.ResourceValue()),
|
|
|
|
}, true
|
|
|
|
}
|
|
|
|
|
|
|
|
// All others are returned as-is.
|
|
|
|
return prop.V, true
|
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
func deserializeProperties(props DeployedPropertyMap, reftag string) PropertyMap {
|
2017-02-22 23:32:03 +01:00
|
|
|
result := make(PropertyMap)
|
|
|
|
for k, prop := range props {
|
2017-02-26 20:20:14 +01:00
|
|
|
result[PropertyKey(k)] = deserializeProperty(prop, reftag)
|
2017-02-22 23:32:03 +01:00
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2017-02-26 20:20:14 +01:00
|
|
|
func deserializeProperty(v interface{}, reftag string) PropertyValue {
|
2017-02-22 23:32:03 +01:00
|
|
|
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 {
|
2017-02-26 20:20:14 +01:00
|
|
|
arr = append(arr, deserializeProperty(elem, reftag))
|
2017-02-22 23:32:03 +01:00
|
|
|
}
|
|
|
|
return NewPropertyArray(arr)
|
|
|
|
case map[string]interface{}:
|
2017-03-03 02:10:10 +01:00
|
|
|
// If the map has a single entry and it is the reftag, this is a URN.
|
2017-02-22 23:32:03 +01:00
|
|
|
if len(w) == 1 {
|
|
|
|
if tag, has := w[reftag]; has {
|
|
|
|
if tagstr, isstring := tag.(string); isstring {
|
2017-03-03 02:10:10 +01:00
|
|
|
return NewPropertyResource(URN(tagstr))
|
2017-02-22 23:32:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, this is an arbitrary object value.
|
2017-02-26 20:20:14 +01:00
|
|
|
obj := deserializeProperties(DeployedPropertyMap(w), reftag)
|
2017-02-22 23:32:03 +01:00
|
|
|
return NewPropertyObject(obj)
|
|
|
|
default:
|
|
|
|
contract.Failf("Unrecognized property type: %v", reflect.ValueOf(v))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return NewPropertyNull()
|
2017-02-22 03:31:43 +01:00
|
|
|
}
|
2017-02-23 02:33:08 +01:00
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
// DeploymentMap is a map of URN to resource, that also preserves a stable order of its keys. This ensures
|
2017-02-23 02:33:08 +01:00
|
|
|
// 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.
|
2017-04-08 16:44:02 +02:00
|
|
|
type DeploymentMap struct {
|
|
|
|
m map[URN]*Deployment
|
2017-03-03 02:10:10 +01:00
|
|
|
keys []URN
|
2017-02-23 02:33:08 +01:00
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func NewDeploymentMap() *DeploymentMap {
|
|
|
|
return &DeploymentMap{m: make(map[URN]*Deployment)}
|
2017-02-23 02:33:08 +01:00
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Keys() []URN { return m.keys }
|
|
|
|
func (m *DeploymentMap) Len() int { return len(m.keys) }
|
2017-02-23 02:33:08 +01:00
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Add(k URN, v *Deployment) {
|
2017-02-23 02:33:08 +01:00
|
|
|
_, has := m.m[k]
|
|
|
|
contract.Assertf(!has, "Unexpected duplicate key '%v' added to map")
|
|
|
|
m.m[k] = v
|
|
|
|
m.keys = append(m.keys, k)
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Delete(k URN) {
|
2017-02-23 02:33:08 +01:00
|
|
|
_, 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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Get(k URN) (*Deployment, bool) {
|
2017-02-23 02:33:08 +01:00
|
|
|
v, has := m.m[k]
|
|
|
|
return v, has
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Has(k URN) bool {
|
2017-02-23 02:33:08 +01:00
|
|
|
_, has := m.m[k]
|
|
|
|
return has
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Must(k URN) *Deployment {
|
2017-02-23 02:33:08 +01:00
|
|
|
v, has := m.m[k]
|
|
|
|
contract.Assertf(has, "Expected key '%v' to exist in this map", k)
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Set(k URN, v *Deployment) {
|
2017-02-23 02:33:08 +01:00
|
|
|
_, has := m.m[k]
|
|
|
|
contract.Assertf(has, "Expected key '%v' to exist in this map for setting an element", k)
|
|
|
|
m.m[k] = v
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) SetOrAdd(k URN, v *Deployment) {
|
2017-02-23 02:33:08 +01:00
|
|
|
if _, has := m.m[k]; has {
|
|
|
|
m.Set(k, v)
|
|
|
|
} else {
|
|
|
|
m.Add(k, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
type DeploymentKeyValue struct {
|
2017-03-03 02:10:10 +01:00
|
|
|
Key URN
|
2017-04-08 16:44:02 +02:00
|
|
|
Value *Deployment
|
2017-02-23 02:33:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Iter can be used to conveniently range over a map's contents stably.
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) Iter() []DeploymentKeyValue {
|
|
|
|
var kvps []DeploymentKeyValue
|
2017-02-23 02:33:08 +01:00
|
|
|
for _, k := range m.Keys() {
|
2017-04-08 16:44:02 +02:00
|
|
|
kvps = append(kvps, DeploymentKeyValue{k, m.Must(k)})
|
2017-02-23 02:33:08 +01:00
|
|
|
}
|
|
|
|
return kvps
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) MarshalJSON() ([]byte, error) {
|
2017-02-23 02:33:08 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
func (m *DeploymentMap) UnmarshalJSON(b []byte) error {
|
2017-02-23 02:33:08 +01:00
|
|
|
contract.Assert(m.m == nil)
|
2017-04-08 16:44:02 +02:00
|
|
|
m.m = make(map[URN]*Deployment)
|
2017-02-23 02:33:08 +01:00
|
|
|
|
|
|
|
// 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) == '{')
|
|
|
|
|
2017-04-08 16:44:02 +02:00
|
|
|
// Parse out every resource key (URN) and element (*Deployment):
|
2017-02-23 02:33:08 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-03-03 02:10:10 +01:00
|
|
|
k := URN(token.(string))
|
2017-02-23 02:33:08 +01:00
|
|
|
contract.Assert(dec.More())
|
2017-04-08 16:44:02 +02:00
|
|
|
var v *Deployment
|
2017-02-23 02:33:08 +01:00
|
|
|
if err := dec.Decode(&v); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
contract.Assert(!m.Has(k))
|
|
|
|
m.Add(k, v)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|