pulumi/pkg/resource/resource.go
2017-05-18 14:51:52 -07:00

220 lines
8.2 KiB
Go

// Licensed to Pulumi Corporation ("Pulumi") under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// Pulumi licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resource
import (
"crypto/rand"
"encoding/hex"
"reflect"
"github.com/golang/glog"
"github.com/pulumi/lumi/pkg/compiler/symbols"
"github.com/pulumi/lumi/pkg/compiler/types"
"github.com/pulumi/lumi/pkg/compiler/types/predef"
"github.com/pulumi/lumi/pkg/eval/heapstate"
"github.com/pulumi/lumi/pkg/eval/rt"
"github.com/pulumi/lumi/pkg/tokens"
"github.com/pulumi/lumi/pkg/util/contract"
)
// ID is a unique resource identifier; it is managed by the provider and is mostly opaque to Lumi.
type ID string
func (id ID) String() string { return string(id) }
func (id *ID) StringPtr() *string {
if id == nil {
return nil
}
ids := (*id).String()
return &ids
}
func IDStrings(ids []ID) []string {
ss := make([]string, len(ids))
for i, id := range ids {
ss[i] = id.String()
}
return ss
}
// 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 provider (or blank if uncreated).
URN() URN // the resource's object urn, 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.
SetPropertiesFrom(m PropertyMap) // copies properties from the given map to the resource.
HasURN() bool // returns true if the resource has been assigned URN.
SetURN(m URN) // assignes a URN to this resource, for those under creation.
}
// State 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 State int
const (
StateOK State = iota
StateUnknown
)
func IsResourceType(t symbols.Type) bool { return types.HasBaseName(t, predef.LumiStdlibResourceClass) }
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).
urn URN // the resource's object urn, 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) URN() URN { return r.urn }
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) SetPropertiesFrom(m PropertyMap) {
for k, v := range m {
r.properties[k] = v
}
}
func (r *resource) HasURN() bool { return (string(r.urn) != "") }
func (r *resource) SetURN(m URN) {
contract.Requiref(!r.HasURN(), "urn", "empty")
r.urn = m
}
// NewResource creates a new resource from the information provided.
func NewResource(id ID, urn URN, t tokens.Type, properties PropertyMap) Resource {
return &resource{
id: id,
urn: urn,
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 urn. This must already exist.
urn, hasm := ctx.ObjURN[obj]
contract.Assertf(!hasm, "Object already assigned a urn '%v'; double allocation detected", urn)
// 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 urn 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 src.Stable() {
// TODO: detect cycles.
obj := src.Get(k)
if v, ok := cloneObjectValue(ctx, 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 urn from the resource map.
urn, hasm := ctx.ObjURN[obj]
contract.Assertf(hasm, "Missing object reference; possible out of order dependency walk")
return NewPropertyResource(urn), 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
}
// NewUniqueHex generates a new "random" hex string for use by resource providers. It has the given optional prefix and
// the total length is capped to the maxlen. Note that capping to maxlen necessarily increases the risk of collisions.
func NewUniqueHex(prefix string, randlen, maxlen int) string {
bs := make([]byte, randlen)
n, err := rand.Read(bs)
contract.Assert(err == nil)
contract.Assert(n == len(bs))
str := prefix + hex.EncodeToString(bs)
if len(str) > maxlen {
str = str[:maxlen]
}
return str
}
// NewUniqueHexID generates a new "random" hex ID for use by resource providers. It has the given optional prefix and
// the total length is capped to the maxlen. Note that capping to maxlen necessarily increases the risk of collisions.
func NewUniqueHexID(prefix string, randlen, maxlen int) ID {
return ID(NewUniqueHex(prefix, randlen, maxlen))
}