pulumi/pkg/resource/resource_object.go
joeduffy 26cf93f759 Implement get functions on all resources
This change implements the `get` function for resources.  Per pulumi/lumi#83,
this allows Lumi scripts to actually read from the target environment.

For example, we can now look up a SecurityGroup from its ARN:

    let group = aws.ec2.SecurityGroup.get(
        "arn:aws:ec2:us-west-2:153052954103:security-group:sg-02150d79");

The returned object is a fully functional resource object.  So, we can then
link it up with an EC2 instance, for example, in the usual ways:

    let instance = new aws.ec2.Instance(..., {
        securityGroups: [ group ],
    });

This didn't require any changes to the RPC or provider model, since we
already implement the Get function.

There are a few loose ends; two are short term:

    1) URNs are not rehydrated.
    2) Query is not yet implemented.

One is mid-term:

    3) We probably want a URN-based lookup function.  But we will likely
       wait until we tackle pulumi/lumi#109 before adding this.

And one is long term (and subtle):

    4) These amount to I/O and are not repeatable!  A change in the target
       environment may cause a script to generate a different plan
       intermittently.  Most likely we want to apply a different kind of
       deployment "policy" for such scripts.  These are inching towards the
       scripting model of pulumi/lumi#121, which is an entirely different
       beast than the repeatable immutable infrastructure deployments.

Finally, it is worth noting that with this, we have some of the fundamental
underpinnings required to finally tackle "inference" (pulumi/lumi#142).
2017-06-19 17:29:02 -07:00

324 lines
11 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 (
"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/rt"
"github.com/pulumi/lumi/pkg/tokens"
"github.com/pulumi/lumi/pkg/util/contract"
)
// IsResourceObject returns true if the given runtime object is a
func IsResourceObject(obj *rt.Object) bool {
return obj != nil && predef.IsResourceType(obj.Type())
}
// Object is a live resource object, connected to state that may change due to evaluation.
type Object struct {
obj *rt.Object // the resource's live object reference.
}
var _ Resource = (*Object)(nil)
// NewObject 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 NewObject(obj *rt.Object) *Object {
contract.Assertf(IsResourceObject(obj), "Expected a resource type")
return &Object{obj: obj}
}
// NewEmptyObject allocates an empty resource object of a given type.
func NewEmptyObject(t symbols.Type) *Object {
contract.Assert(predef.IsResourceType(t))
return &Object{
obj: rt.NewObject(t, nil, nil, nil),
}
}
func (r *Object) Obj() *rt.Object { return r.obj }
func (r *Object) Type() tokens.Type { return r.obj.Type().TypeToken() }
// ID fetches the object's ID.
func (r *Object) ID() ID {
if idobj := getIDObject(r.Obj()); idobj != nil && idobj.IsString() {
return ID(idobj.StringValue())
}
return ID("")
}
// HasID returns true if the object already has an ID assigned to it.
func (r *Object) HasID() bool {
return r.ID() != ""
}
// HasComputedID returns true if the object has an ID, but is computed and its value is not known yet.
func (r *Object) HasComputedID() bool {
idobj := getIDObject(r.Obj())
return idobj != nil && idobj.IsComputed()
}
// SetID assigns an ID to the target object. This must only happen once.
func (r *Object) SetID(id ID) {
prop := r.obj.GetPropertyAddr(IDProperty, true, true)
contract.Assertf(prop.Obj().IsNull() || prop.Obj().IsComputed(), "Unexpected double set on ID; previous=%v", prop)
prop.Set(rt.NewStringObject(id.String()))
}
// getIDObject fetches the ID off the target object, dynamically, given its runtime value.
func getIDObject(obj *rt.Object) *rt.Object {
contract.Assert(IsResourceObject(obj))
if idprop := obj.GetPropertyAddr(IDProperty, false, false); idprop != nil {
id := idprop.Obj()
contract.Assert(id != nil)
contract.Assert(id.IsString() || id.IsComputed())
return id
}
return nil
}
const (
// URNProperty is the special URN property name.
URNProperty = rt.PropertyKey("urn")
// URNPropertyKey is the special URN property name for resource maps.
URNPropertyKey = PropertyKey("urn")
)
// URN fetches the object's URN.
func (r *Object) URN() URN {
if urnobj := getURNObject(r.Obj()); urnobj != nil && urnobj.IsString() {
return URN(urnobj.StringValue())
}
return URN("")
}
// HasURN returns true if the object has a URN assigned.
func (r *Object) HasURN() bool {
return r.URN() != ""
}
// SetURN assignes a URN to the target object. This must only happen once.
func (r *Object) SetURN(urn URN) {
prop := r.obj.GetPropertyAddr(URNProperty, true, true)
contract.Assertf(prop.Obj().IsNull() || prop.Obj().IsComputed(), "Unexpected double set on URN; previous=%v", prop)
prop.Set(rt.NewStringObject(string(urn)))
}
// getURNObject fetches the URN off the target object, dynamically, given its runtime value.
func getURNObject(obj *rt.Object) *rt.Object {
contract.Assert(IsResourceObject(obj))
if urnprop := obj.GetPropertyAddr(URNProperty, false, false); urnprop != nil {
urn := urnprop.Obj()
contract.Assert(urn != nil)
contract.Assert(urn.IsString() || urn.IsComputed())
return urn
}
return nil
}
// Update updates the target object URN, ID, and resource property map. This mutates the live object connected to this
// resource and also archives the resource object's present state in the form of a state snapshot.
func (r *Object) Update(urn URN, id ID, outputs PropertyMap) *State {
// First take a snapshot of the properties.
inputs := r.CopyProperties()
// Now assign the URN, ID, and copy everything in the property map, overwriting what exists.
r.SetURN(urn)
r.SetID(id)
r.SetProperties(outputs)
// Finally, return a state snapshot of the underlying object state.
return NewState(r.Type(), r.URN(), id, inputs, outputs)
}
// CopyProperties creates a property map out of a resource's runtime object. This is a snapshot and is completely
// disconnected from the object itself, such that any subsequent object updates will not be observed.
func (r *Object) CopyProperties() PropertyMap {
resobj := r.Obj()
return copyObject(resobj, resobj)
}
// SetProperties copies from a resource property map to the runtime object, overwriting properties as it goes.
func (r *Object) SetProperties(props PropertyMap) {
if props != nil {
setRuntimeProperties(r.Obj(), props)
}
}
func copyObject(resobj *rt.Object, obj *rt.Object) PropertyMap {
contract.Assert(obj != nil)
props := obj.PropertyValues()
return copyObjectProperties(resobj, props)
}
// copyObjectProperty 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 copyObjectProperty(resobj *rt.Object, obj *rt.Object) PropertyValue {
t := obj.Type()
if predef.IsResourceType(t) {
// Resource references expand to that resource's ID.
idobj := getIDObject(obj)
if idobj != nil && idobj.IsString() {
return NewStringProperty(idobj.StringValue())
}
// If an ID hasn't yet been assigned, we must be planning, and so this is a computed property.
return MakeComputed(NewStringProperty(""))
}
// Serialize simple primitive types with their primitive equivalents.
switch t {
case types.Null:
return NewNullProperty()
case types.Bool:
return NewBoolProperty(obj.BoolValue())
case types.Number:
return NewNumberProperty(obj.NumberValue())
case types.String:
return NewStringProperty(obj.StringValue())
case types.Object, types.Dynamic:
result := copyObject(nil, obj) // an object literal, clone it
return NewObjectProperty(result)
}
// Serialize arrays, maps, and object instances in the obvious way.
switch t.(type) {
case *symbols.ArrayType:
// Make a new array, clone each element, and return the result.
var result []PropertyValue
for _, e := range *obj.ArrayValue() {
result = append(result, copyObjectProperty(nil, e.Obj()))
}
return NewArrayProperty(result)
case *symbols.MapType:
// Make a new map, clone each property value, and return the result.
props := obj.PropertyValues()
result := copyObjectProperties(nil, props)
return NewObjectProperty(result)
case *symbols.Class:
// Make a new object that contains a deep clone of the source.
result := copyObject(nil, obj)
return NewObjectProperty(result)
}
// If a computed value, we can propagate an unknown value, but only for certain cases.
if t.Computed() {
// See if this is an output property. An output property is a property that is set directly on the resource
// object that is computed from precisely a single dependency and no expression. Otherwise, it is computed.
var makeProperty func(PropertyValue) PropertyValue
if isOutputObject(resobj, obj) {
makeProperty = MakeOutput
} else {
makeProperty = MakeComputed
}
// Now just wrap the underlying object appropriately.
elem := t.(*symbols.ComputedType).Element
switch elem {
case types.Null:
return makeProperty(NewNullProperty())
case types.Bool:
return makeProperty(NewBoolProperty(false))
case types.Number:
return makeProperty(NewNumberProperty(0))
case types.String:
return makeProperty(NewStringProperty(""))
case types.Object, types.Dynamic:
return makeProperty(NewObjectProperty(make(PropertyMap)))
}
switch elem.(type) {
case *symbols.ArrayType:
return makeProperty(NewArrayProperty(nil))
case *symbols.Class:
return makeProperty(NewObjectProperty(make(PropertyMap)))
}
}
// We can safely skip serializing functions, however, anything else is unexpected at this point.
_, isfunc := t.(*symbols.FunctionType)
contract.Assertf(isfunc, "Unrecognized resource property object type '%v' (%v)", t, reflect.TypeOf(t))
return PropertyValue{}
}
// copyObjectProperties copies a resource's properties.
func copyObjectProperties(resobj *rt.Object, props *rt.PropertyMap) PropertyMap {
// Walk the object's properties and serialize them in a stable order.
result := make(PropertyMap)
for _, k := range props.Stable() {
result[PropertyKey(k)] = copyObjectProperty(resobj, props.Get(k))
}
return result
}
// isOutputObject returns true if the object obj is a computed output property for resource object resobj.
func isOutputObject(resobj *rt.Object, obj *rt.Object) bool {
if obj.IsComputed() {
v := obj.ComputedValue()
return !v.Expr && len(v.Sources) == 1 && v.Sources[0] == resobj
}
return false
}
// setRuntimeProperties translates from a resource property map into the equivalent runtime objects, and stores them on
// the given runtime object.
func setRuntimeProperties(obj *rt.Object, props PropertyMap) {
for k, v := range props {
prop := obj.GetPropertyAddr(rt.PropertyKey(k), true, true)
// TODO: we are only setting if IsNull or IsComputed, to avoid certain shortcomings in our serialization format
// today. For example, if a resource ID appears, we must map it back to the runtime object.
pobj := prop.Obj()
if pobj.IsNull() || isOutputObject(obj, pobj) {
glog.V(9).Infof("Setting resource object property: %v=%v", k, v)
val := createRuntimeProperty(v)
prop.Set(val)
} else {
glog.V(9).Infof("Skipping resource object property: %v=%v; existing=%v", k, v, pobj)
}
}
}
// createRuntimeProperty translates a property value into a runtime object.
func createRuntimeProperty(v PropertyValue) *rt.Object {
if v.IsNull() {
return rt.Null
} else if v.IsBool() {
return rt.Bools[v.BoolValue()]
} else if v.IsNumber() {
return rt.NewNumberObject(v.NumberValue())
} else if v.IsString() {
return rt.NewStringObject(v.StringValue())
} else if v.IsArray() {
src := v.ArrayValue()
arr := make([]*rt.Pointer, len(src))
for i, elem := range src {
ve := createRuntimeProperty(elem)
arr[i] = rt.NewPointer(ve, false, nil, nil)
}
return rt.NewArrayObject(types.Dynamic, &arr)
}
contract.Assertf(v.IsObject(), "Expected an object, not a computed/output value")
obj := rt.NewObject(types.Dynamic, nil, nil, nil)
setRuntimeProperties(obj, v.ObjectValue())
return obj
}