pulumi/pkg/resource/plugin/rpc.go

294 lines
11 KiB
Go
Raw Normal View History

2017-06-26 23:46:34 +02:00
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package plugin
import (
"reflect"
"sort"
"github.com/golang/glog"
structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/pulumi/pulumi-fabric/pkg/resource"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
// MarshalOptions controls the marshaling of RPC structures.
type MarshalOptions struct {
SkipNulls bool // true to skip nulls altogether in the resulting map.
DisallowUnknowns bool // true if we are disallowing unknown values (results in assertion failures).
}
const (
// UnknownBoolValue is a sentinel indicating that a bool property's value is not known, because it depends on
// a computation with values whose values themselves are not yet known (e.g., dependent upon an output property).
UnknownBoolValue = "1c4a061d-8072-4f0a-a4cb-0ff528b18fe7"
// UnknownNumberValue is a sentinel indicating that a number property's value is not known, because it depends on
// a computation with values whose values themselves are not yet known (e.g., dependent upon an output property).
UnknownNumberValue = "3eeb2bf0-c639-47a8-9e75-3b44932eb421"
// UnknownStringValue is a sentinel indicating that a string property's value is not known, because it depends on
// a computation with values whose values themselves are not yet known (e.g., dependent upon an output property).
UnknownStringValue = "04da6b54-80e4-46f7-96ec-b56ff0331ba9"
// UnknownArrayValue is a sentinel indicating that an array property's value is not known, because it depends on
// a computation with values whose values themselves are not yet known (e.g., dependent upon an output property).
UnknownArrayValue = "6a19a0b0-7e62-4c92-b797-7f8e31da9cc2"
// UnknownAssetValue is a sentinel indicating that an asset property's value is not known, because it depends on
// a computation with values whose values themselves are not yet known (e.g., dependent upon an output property).
UnknownAssetValue = "030794c1-ac77-496b-92df-f27374a8bd58"
// UnknownArchiveValue is a sentinel indicating that an archive property's value is not known, because it depends
// on a computation with values whose values themselves are not yet known (e.g., dependent upon an output property).
UnknownArchiveValue = "e48ece36-62e2-4504-bad9-02848725956a"
// UnknownObjectValue is a sentinel indicating that an archive property's value is not known, because it depends
// on a computation with values whose values themselves are not yet known (e.g., dependent upon an output property).
UnknownObjectValue = "dd056dcd-154b-4c76-9bd3-c8f88648b5ff"
)
// MarshalProperties marshals a resource's property map as a "JSON-like" protobuf structure.
func MarshalProperties(props resource.PropertyMap, opts MarshalOptions) *structpb.Struct {
fields := make(map[string]*structpb.Value)
for _, key := range props.StableKeys() {
v := props[key]
glog.V(9).Infof("Marshaling property for RPC: %v=%v", key, v)
if v.IsOutput() {
glog.V(9).Infof("Skipping output property %v", key)
} else if opts.SkipNulls && v.IsNull() {
glog.V(9).Infof("Skipping null property %v (as requested)", key)
} else {
fields[string(key)] = MarshalPropertyValue(v, opts)
}
}
return &structpb.Struct{
Fields: fields,
}
Initial support for output properties (1 of 3) This change includes approximately 1/3rd of the change necessary to support output properties, as per pulumi/lumi#90. In short, the runtime now has a new hidden type, Latent<T>, which represents a "speculative" value, whose eventual type will be T, that we can use during evaluation in various ways. Namely, operations against Latent<T>s generally produce new Latent<U>s. During planning, any Latent<T>s that end up in resource properties are transformed into "unknown" property values. An unknown property value is legal only during planning-time activities, such as Check, Name, and InspectChange. As a result, those RPC interfaces have been updated to include lookaside maps indicating which properties have unknown values. My intent is to add some helper functions to make dealing with this circumstance more correct-by-construction. For now, using an unresolved Latent<T> in a conditional will lead to an error. See pulumi/lumi#67. Speculating beyond these -- by supporting iterative planning and application -- is something we want to support eventually, but it makes sense to do that as an additive change beyond this initial support. That is a missing 1/3. Finally, the other missing 1/3rd which will happen much sooner than the rest is restructuing plan application so that it will correctly observe resolution of Latent<T> values. Right now, the evaluation happens in one single pass, prior to the application, and so Latent<T>s never actually get witnessed in a resolved state.
2017-05-24 02:32:59 +02:00
}
// MarshalPropertyValue marshals a single resource property value into its "JSON-like" value representation.
func MarshalPropertyValue(v resource.PropertyValue, opts MarshalOptions) *structpb.Value {
if v.IsNull() {
return MarshalNull(opts)
} else if v.IsBool() {
return &structpb.Value{
Kind: &structpb.Value_BoolValue{
2017-02-28 20:02:33 +01:00
BoolValue: v.BoolValue(),
},
}
} else if v.IsNumber() {
return &structpb.Value{
Kind: &structpb.Value_NumberValue{
2017-02-28 20:02:33 +01:00
NumberValue: v.NumberValue(),
},
}
} else if v.IsString() {
return MarshalString(v.StringValue(), opts)
} else if v.IsArray() {
var elems []*structpb.Value
for _, elem := range v.ArrayValue() {
elems = append(elems, MarshalPropertyValue(elem, opts))
}
return &structpb.Value{
Kind: &structpb.Value_ListValue{
2017-02-28 20:02:33 +01:00
ListValue: &structpb.ListValue{Values: elems},
},
}
} else if v.IsAsset() {
return MarshalAsset(v.AssetValue(), opts)
} else if v.IsArchive() {
return MarshalArchive(v.ArchiveValue(), opts)
} else if v.IsObject() {
obj := MarshalProperties(v.ObjectValue(), opts)
return MarshalStruct(obj, opts)
} else if v.IsComputed() {
return marshalUnknownProperty(v.ComputedValue().Element, opts)
} else if v.IsOutput() {
// Note that at the moment we don't differentiate between computed and output properties on the wire. As
// a result, they will show up as computed on the other end. This distinction isn't currently interesting.
return marshalUnknownProperty(v.OutputValue().Element, opts)
}
contract.Failf("Unrecognized property value: %v (type=%v)", v.V, reflect.TypeOf(v.V))
return nil
}
// marshalUnknownProperty marshals an unknown property in a way that lets us recover its type on the other end.
func marshalUnknownProperty(elem resource.PropertyValue, opts MarshalOptions) *structpb.Value {
contract.Assertf(!opts.DisallowUnknowns, "Unexpected unknown value when opts.DisallowUnknowns")
// Normal cases, these get sentinels.
if elem.IsBool() {
return MarshalString(UnknownBoolValue, opts)
} else if elem.IsNumber() {
return MarshalString(UnknownNumberValue, opts)
} else if elem.IsString() {
return MarshalString(UnknownStringValue, opts)
} else if elem.IsArray() {
return MarshalString(UnknownArrayValue, opts)
} else if elem.IsAsset() {
return MarshalString(UnknownAssetValue, opts)
} else if elem.IsArchive() {
return MarshalString(UnknownArchiveValue, opts)
} else if elem.IsObject() {
return MarshalString(UnknownObjectValue, opts)
}
// If for some reason we end up with a recursive computed/output, just keep digging.
if elem.IsComputed() {
return marshalUnknownProperty(elem.ComputedValue().Element, opts)
} else if elem.IsOutput() {
return marshalUnknownProperty(elem.OutputValue().Element, opts)
}
// Finally, if a null, we can guess its value! (the one and only...)
if elem.IsNull() {
return MarshalNull(opts)
}
contract.Failf("Unexpected output/computed property element: %v", elem)
return nil
}
// UnmarshalProperties unmarshals a "JSON-like" protobuf structure into a new resource property map.
func UnmarshalProperties(props *structpb.Struct, opts MarshalOptions) resource.PropertyMap {
result := make(resource.PropertyMap)
// First sort the keys so we enumerate them in order (in case errors happen, we want determinism).
var keys []string
2017-07-15 03:08:33 +02:00
if props != nil {
for k := range props.Fields {
keys = append(keys, k)
}
sort.Strings(keys)
}
// And now unmarshal every field it into the map.
for _, key := range keys {
pk := resource.PropertyKey(key)
v := UnmarshalPropertyValue(props.Fields[key], opts)
glog.V(9).Infof("Unmarshaling property for RPC: %v=%v", key, v)
if opts.SkipNulls && v.IsNull() {
glog.V(9).Infof("Skipping unmarshaling of %v (it is null)", key)
} else {
result[pk] = v
}
}
return result
}
// UnmarshalPropertyValue unmarshals a single "JSON-like" value into a new property value.
func UnmarshalPropertyValue(v *structpb.Value, opts MarshalOptions) resource.PropertyValue {
contract.Assert(v != nil)
switch v.Kind.(type) {
case *structpb.Value_NullValue:
return resource.NewNullProperty()
case *structpb.Value_BoolValue:
return resource.NewBoolProperty(v.GetBoolValue())
case *structpb.Value_NumberValue:
return resource.NewNumberProperty(v.GetNumberValue())
case *structpb.Value_StringValue:
// If it's a string, it could be an unknown property, or just a regular string.
s := v.GetStringValue()
if unk, isunk := unmarshalUnknownPropertyValue(s, opts); isunk {
return unk
}
return resource.NewStringProperty(s)
case *structpb.Value_ListValue:
// If there's already an array, prefer to swap elements within it.
var elems []resource.PropertyValue
lst := v.GetListValue()
for i, elem := range lst.GetValues() {
if i == len(elems) {
elems = append(elems, resource.PropertyValue{})
}
contract.Assert(len(elems) > i)
elems[i] = UnmarshalPropertyValue(elem, opts)
}
return resource.NewArrayProperty(elems)
case *structpb.Value_StructValue:
// Start by unmarshaling.
obj := UnmarshalProperties(v.GetStructValue(), opts)
// Before returning it as an object, check to see if it's a known recoverable type.
objmap := obj.Mappable()
if asset, isasset := resource.DeserializeAsset(objmap); isasset {
return resource.NewAssetProperty(asset)
} else if archive, isarchive := resource.DeserializeArchive(objmap); isarchive {
return resource.NewArchiveProperty(archive)
}
return resource.NewObjectProperty(obj)
default:
contract.Failf("Unrecognized structpb value kind: %v", reflect.TypeOf(v.Kind))
return resource.NewNullProperty()
}
}
func unmarshalUnknownPropertyValue(s string, opts MarshalOptions) (resource.PropertyValue, bool) {
var elem resource.PropertyValue
var unknown bool
switch s {
case UnknownBoolValue:
elem, unknown = resource.NewBoolProperty(false), true
case UnknownNumberValue:
elem, unknown = resource.NewNumberProperty(0), true
case UnknownStringValue:
elem, unknown = resource.NewStringProperty(""), true
case UnknownArrayValue:
elem, unknown = resource.NewArrayProperty([]resource.PropertyValue{}), true
case UnknownAssetValue:
elem, unknown = resource.NewAssetProperty(resource.Asset{}), true
case UnknownArchiveValue:
elem, unknown = resource.NewArchiveProperty(resource.Archive{}), true
case UnknownObjectValue:
elem, unknown = resource.NewObjectProperty(make(resource.PropertyMap)), true
}
if unknown {
comp := resource.Computed{Element: elem}
return resource.NewComputedProperty(comp), true
}
return resource.PropertyValue{}, false
}
// MarshalNull marshals a nil to its protobuf form.
func MarshalNull(opts MarshalOptions) *structpb.Value {
return &structpb.Value{
Kind: &structpb.Value_NullValue{
NullValue: structpb.NullValue_NULL_VALUE,
},
}
}
// MarshalString marshals a string to its protobuf form.
func MarshalString(s string, opts MarshalOptions) *structpb.Value {
return &structpb.Value{
Kind: &structpb.Value_StringValue{
StringValue: s,
},
}
}
// MarshalStruct marshals a struct for use in a protobuf field where a value is expected.
func MarshalStruct(obj *structpb.Struct, opts MarshalOptions) *structpb.Value {
return &structpb.Value{
Kind: &structpb.Value_StructValue{
StructValue: obj,
},
}
}
// MarshalAsset marshals an asset into its wire form for resource provider plugins.
func MarshalAsset(v resource.Asset, opts MarshalOptions) *structpb.Value {
// To marshal an asset, we need to first serialize it, and then marshal that.
sera := v.Serialize()
serap := resource.NewPropertyMapFromMap(sera)
return MarshalPropertyValue(resource.NewObjectProperty(serap), opts)
}
// MarshalArchive marshals an archive into its wire form for resource provider plugins.
func MarshalArchive(v resource.Archive, opts MarshalOptions) *structpb.Value {
// To marshal an archive, we need to first serialize it, and then marshal that.
sera := v.Serialize()
serap := resource.NewPropertyMapFromMap(sera)
return MarshalPropertyValue(resource.NewObjectProperty(serap), opts)
}