Implement a basic AWS resource provider

This commit includes a basic AWS resource provider.  Mostly it is just
scaffolding, however, it also includes prototype implementations for EC2
instance and security group resource creation operations.
This commit is contained in:
joeduffy 2017-02-20 11:18:47 -08:00
parent dbd1721ced
commit 276b6c253d
12 changed files with 1028 additions and 187 deletions

View file

@ -16,6 +16,7 @@ import (
func newApplyCmd() *cobra.Command {
var delete bool
var detailed bool
var cmd = &cobra.Command{
Use: "apply [blueprint] [-- [args]]",
Short: "Apply a deployment plan from a Mu blueprint",
@ -33,7 +34,8 @@ func newApplyCmd() *cobra.Command {
"a path to a blueprint elsewhere can be provided as the [blueprint] argument.",
Run: func(cmd *cobra.Command, args []string) {
if comp, plan := plan(cmd, args, delete); plan != nil {
if err, _, _ := plan.Apply(&applyProgress{}); err != nil {
progress := newProgress(detailed)
if err, _, _ := plan.Apply(progress); err != nil {
// TODO: we want richer diagnostics in the event that a plan apply fails. For instance, we want to
// know precisely what step failed, we want to know whether it was catastrophic, etc. We also
// probably want to plumb diag.Sink through apply so it can issue its own rich diagnostics.
@ -47,20 +49,28 @@ func newApplyCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&delete, "delete", false,
"Delete the entirety of the blueprint's resources")
cmd.PersistentFlags().BoolVar(
&detailed, "detailed", false,
"Display detailed output during the application of changes")
return cmd
}
// applyProgress pretty-prints the plan application process as it goes.
type applyProgress struct {
c int
c int
detailed bool
}
func newProgress(detailed bool) *applyProgress {
return &applyProgress{detailed: detailed}
}
func (prog *applyProgress) Before(step resource.Step) {
var b bytes.Buffer
prog.c++
b.WriteString(fmt.Sprintf("Applying step #%v\n", prog.c))
printStep(&b, step, true, " ")
printStep(&b, step, !prog.detailed, " ")
s := colors.Colorize(b.String())
fmt.Printf(s)
}

View file

@ -17,12 +17,12 @@ export class Instance extends cloudformation.Resource {
}
export interface InstanceArgs extends cloudformation.TagArgs {
// The instance type, such as t2.micro. The default type is "m3.medium".
instanceType: string;
// A list that contains the Amazon EC2 security groups to assign to the Amazon EC2 instance.
securityGroups: SecurityGroup[];
// Provides the name of the Amazon EC2 key pair.
keyName: string;
// Provides the unique ID of the Amazon Machine Image (AMI) that was assigned during registration.
imageId: string;
// The instance type, such as t2.micro. The default type is "m3.medium".
instanceType?: string;
// A list that contains the Amazon EC2 security groups to assign to the Amazon EC2 instance.
securityGroups?: SecurityGroup[];
// Provides the name of the Amazon EC2 key pair.
keyName?: string;
}

View file

@ -4,7 +4,10 @@
set -e # bail on errors
echo Compiling:
mujs # compile the package
mujs # compile the MuPackage
pushd provider/ && # compile the resource provider
go build -o ../bin/mu-ressrv-aws &&
popd
echo Sharing NPM links:
yarn link # let NPM references resolve easily.

View file

@ -0,0 +1,39 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package awsctx
import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/marapongo/mu/pkg/util/contract"
)
// Context represents state shared amongst all parties in this process. In particular, it wraps an AWS session
// object and offers convenient wrappers for creating connections to the various sub-services (EC2, S3, etc).
type Context struct {
sess *session.Session
ec2 *ec2.EC2
}
func New() (*Context, error) {
// Create an AWS session; note that this is safe to share among many operations.
// TODO: consider verifying credentials, region, etc. here.
// TODO: currently we just inherit the standard AWS SDK credentials logic; eventually we will want more
// flexibility, I assume, including possibly reading from configuration dynamically.
sess, err := session.NewSession()
if err != nil {
return nil, err
}
// Allocate a new global context with this session; note that all other connections are lazily allocated.
return &Context{
sess: sess,
}, nil
}
func (ctx *Context) EC2() *ec2.EC2 {
contract.Assert(ctx.sess != nil)
if ctx.ec2 == nil {
ctx.ec2 = ec2.New(ctx.sess)
}
return ctx.ec2
}

View file

@ -0,0 +1,126 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package ec2
import (
"errors"
"github.com/aws/aws-sdk-go/service/ec2"
pbempty "github.com/golang/protobuf/ptypes/empty"
"github.com/marapongo/mu/pkg/resource"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/pkg/util/contract"
"github.com/marapongo/mu/sdk/go/pkg/murpc"
"golang.org/x/net/context"
"github.com/marapongo/mu/lib/aws/provider/awsctx"
)
const Instance = tokens.Type("aws:ec2/instance:Instance")
// NewInstanceProvider creates a provider that handles EC2 instance operations.
func NewInstanceProvider(ctx *awsctx.Context) murpc.ResourceProviderServer {
return &instanceProvider{}
}
type instanceProvider struct {
ctx *awsctx.Context
}
// Create allocates a new instance of the provided resource and returns its unique ID afterwards. (The input ID
// must be blank.) If this call fails, the resource must not have been created (i.e., it is "transacational").
func (p *instanceProvider) Create(ctx context.Context, req *murpc.CreateRequest) (*murpc.CreateResponse, error) {
contract.Assert(req.GetType() == string(Instance))
props := resource.UnmarshalProperties(req.GetProperties())
// Read in the properties given by the request, validating as we go; if any fail, reject the request.
// TODO: this is a good example of a "benign" (StateOK) error; handle it accordingly.
inst, err := newInstance(props, true)
if err != nil {
return nil, err
}
// Create the create instances request object.
var secgrpIDs []*string
if inst.SecurityGroupIDs != nil {
for _, sid := range *inst.SecurityGroupIDs {
secgrpIDs = append(secgrpIDs, &sid)
}
}
create := &ec2.RunInstancesInput{
ImageId: &inst.ImageID,
InstanceType: inst.InstanceType,
SecurityGroupIds: secgrpIDs,
KeyName: inst.KeyName,
}
// Now go ahead and perform the action.
out, err := p.ctx.EC2().RunInstances(create)
if err != nil {
return nil, err
}
contract.Assert(out != nil)
contract.Assert(len(out.Instances) == 1)
contract.Assert(out.Instances[0] != nil)
contract.Assert(out.Instances[0].InstanceId != nil)
// TODO: memoize the ID.
// TODO: wait for the instance to finish spinning up.
return &murpc.CreateResponse{
Id: *out.Instances[0].InstanceId,
}, nil
}
// Read reads the instance state identified by ID, returning a populated resource object, or an error if not found.
func (p *instanceProvider) Read(ctx context.Context, req *murpc.ReadRequest) (*murpc.ReadResponse, error) {
contract.Assert(req.GetType() == string(Instance))
return nil, errors.New("Not yet implemented")
}
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
func (p *instanceProvider) Update(ctx context.Context, req *murpc.UpdateRequest) (*murpc.UpdateResponse, error) {
contract.Assert(req.GetType() == string(Instance))
return nil, errors.New("Not yet implemented")
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
func (p *instanceProvider) Delete(ctx context.Context, req *murpc.DeleteRequest) (*pbempty.Empty, error) {
contract.Assert(req.GetType() == string(Instance))
return nil, errors.New("Not yet implemented")
}
// instance represents the state associated with an instance.
type instance struct {
ImageID string
InstanceType *string
SecurityGroupIDs *[]string
KeyName *string
}
// newInstance creates a new instance bag of state, validating required properties if asked to do so.
func newInstance(m resource.PropertyMap, req bool) (*instance, error) {
imageID, err := m.ReqStringOrErr("imageId")
if err != nil && (req || !resource.IsReqError(err)) {
return nil, err
}
instanceType, err := m.OptStringOrErr("instanceType")
if err != nil {
return nil, err
}
securityGroupIDs, err := m.OptStringArrayOrErr("securityGroups")
if err != nil {
return nil, err
}
keyName, err := m.OptStringOrErr("keyName")
if err != nil {
return nil, err
}
return &instance{
ImageID: imageID,
InstanceType: instanceType,
SecurityGroupIDs: securityGroupIDs,
KeyName: keyName,
}, nil
}

View file

@ -0,0 +1,257 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package ec2
import (
"errors"
"github.com/aws/aws-sdk-go/service/ec2"
pbempty "github.com/golang/protobuf/ptypes/empty"
"github.com/marapongo/mu/pkg/resource"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/pkg/util/contract"
"github.com/marapongo/mu/sdk/go/pkg/murpc"
"golang.org/x/net/context"
"github.com/marapongo/mu/lib/aws/provider/awsctx"
)
const SecurityGroup = tokens.Type("aws:ec2/securityGroup:SecurityGroup")
// NewSecurityGroupProvider creates a provider that handles EC2 security group operations.
func NewSecurityGroupProvider(ctx *awsctx.Context) murpc.ResourceProviderServer {
return &securityGroupProvider{ctx}
}
type securityGroupProvider struct {
ctx *awsctx.Context
}
// Create allocates a new instance of the provided resource and returns its unique ID afterwards. (The input ID
// must be blank.) If this call fails, the resource must not have been created (i.e., it is "transacational").
func (p *securityGroupProvider) Create(ctx context.Context, req *murpc.CreateRequest) (*murpc.CreateResponse, error) {
contract.Assert(req.GetType() == string(SecurityGroup))
props := resource.UnmarshalProperties(req.GetProperties())
// Read in the properties given by the request, validating as we go; if any fail, reject the request.
// TODO: this is a good example of a "benign" (StateOK) error; handle it accordingly.
secgrp, err := newSecurityGroup(props, true)
if err != nil {
return nil, err
}
// Make the security group creation parameters.
// TODO: the name needs to be figured out; CloudFormation doesn't expose it, presumably due to its requirement to
// be unique. I think we can use the moniker here, but that isn't necessarily stable. UUID?
create := &ec2.CreateSecurityGroupInput{
GroupName: &secgrp.Description,
Description: &secgrp.Description,
VpcId: secgrp.VPCID,
}
// Now go ahead and perform the action.
result, err := p.ctx.EC2().CreateSecurityGroup(create)
if err != nil {
return nil, err
}
contract.Assert(result != nil)
contract.Assert(result.GroupId != nil)
// TODO: memoize the ID.
// TODO: wait for the group to finish spinning up.
// TODO: create the ingress/egress rules.
return &murpc.CreateResponse{
Id: *result.GroupId,
}, nil
}
// Read reads the instance state identified by ID, returning a populated resource object, or an error if not found.
func (p *securityGroupProvider) Read(ctx context.Context, req *murpc.ReadRequest) (*murpc.ReadResponse, error) {
contract.Assert(req.GetType() == string(SecurityGroup))
return nil, errors.New("Not yet implemented")
}
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
func (p *securityGroupProvider) Update(ctx context.Context, req *murpc.UpdateRequest) (*murpc.UpdateResponse, error) {
contract.Assert(req.GetType() == string(SecurityGroup))
return nil, errors.New("Not yet implemented")
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
func (p *securityGroupProvider) Delete(ctx context.Context, req *murpc.DeleteRequest) (*pbempty.Empty, error) {
contract.Assert(req.GetType() == string(SecurityGroup))
return nil, errors.New("Not yet implemented")
}
// securityGroup represents the state associated with a security group.
type securityGroup struct {
Description string // description of the security group.
VPCID *string // the VPC in which this security group resides.
Egress *[]securityGroupEgressRule // a list of security group egress rules.
Ingress *[]securityGroupIngressRule // a list of security group ingress rules.
}
// newSecurityGroup creates a new instance bag of state, validating required properties if asked to do so.
func newSecurityGroup(m resource.PropertyMap, req bool) (*securityGroup, error) {
description, err := m.ReqStringOrErr("groupDescription")
if err != nil && (req || !resource.IsReqError(err)) {
return nil, err
}
// TODO: validate other aspects of the parameters; for instance, ensure that the description is < 255 characters,
// etc. Furthermore, consider doing this in a pass before performing *any* actions (and during planning), so
// that we can hoist failures to before even trying to execute a plan (when such errors are more costly).
vpcID, err := m.OptStringOrErr("vpc")
if err != nil {
return nil, err
}
var egress *[]securityGroupEgressRule
egressArray, err := m.OptObjectArrayOrErr("securityGroupEgress")
if err != nil {
return nil, err
} else {
var rules []securityGroupEgressRule
for _, rule := range *egressArray {
sger, err := newSecurityGroupEgressRule(rule, req)
if err != nil {
return nil, err
}
rules = append(rules, *sger)
}
egress = &rules
}
var ingress *[]securityGroupIngressRule
ingressArray, err := m.OptObjectArrayOrErr("securityGroupIngress")
if err != nil {
return nil, err
} else {
var rules []securityGroupIngressRule
for _, rule := range *ingressArray {
sgir, err := newSecurityGroupIngressRule(rule, req)
if err != nil {
return nil, err
}
rules = append(rules, *sgir)
}
ingress = &rules
}
return &securityGroup{
Description: description,
VPCID: vpcID,
Egress: egress,
Ingress: ingress,
}, nil
}
// securityGroupRule represents the state associated with a security group rule.
type securityGroupRule struct {
IPProtocol string // an IP protocol name or number.
CIDRIP *string // specifies a CIDR range.
FromPort *int64 // the start of port range for the TCP/UDP protocols, or an ICMP type number.
ToPort *int64 // the end of port range for the TCP/UDP protocols, or an ICMP code.
}
// newSecurityGroupRule creates a new instance bag of state, validating required properties if asked to do so.
func newSecurityGroupRule(m resource.PropertyMap, req bool) (*securityGroupRule, error) {
ipProtocol, err := m.ReqStringOrErr("ipProtocol")
if err != nil && (req || !resource.IsReqError(err)) {
return nil, err
}
cidrIP, err := m.OptStringOrErr("cidrIp")
if err != nil {
return nil, err
}
var fromPort *int64
fromPortF, err := m.OptNumberOrErr("fromPort")
if err != nil {
return nil, err
} else {
fromPortI := int64(*fromPortF)
fromPort = &fromPortI
}
var toPort *int64
toPortF, err := m.OptNumberOrErr("toPort")
if err != nil {
return nil, err
} else {
toPortI := int64(*toPortF)
toPort = &toPortI
}
return &securityGroupRule{
IPProtocol: ipProtocol,
CIDRIP: cidrIP,
FromPort: fromPort,
ToPort: toPort,
}, nil
}
// securityGroupEgressRule represents the state associated with a security group egress rule.
type securityGroupEgressRule struct {
securityGroupRule
DestinationPrefixListID *string // the AWS service prefix of an Amazon VPC endpoint.
DestinationSecurityGroupID *string // specifies the destination Amazon VPC security group.
}
// newSecurityEgressGroupRule creates a new instance bag of state, validating required properties if asked to do so.
func newSecurityGroupEgressRule(m resource.PropertyMap, req bool) (*securityGroupEgressRule, error) {
rule, err := newSecurityGroupRule(m, req)
if err != nil && (req || !resource.IsReqError(err)) {
return nil, err
}
destPrefixListID, err := m.OptStringOrErr("destinationPrefixListId")
if err != nil {
return nil, err
}
destSecurityGroupID, err := m.OptStringOrErr("destinationSecurityGroup")
if err != nil {
return nil, err
}
return &securityGroupEgressRule{
securityGroupRule: *rule,
DestinationPrefixListID: destPrefixListID,
DestinationSecurityGroupID: destSecurityGroupID,
}, nil
}
// securityGroupIngressRule represents the state associated with a security group ingress rule.
type securityGroupIngressRule struct {
securityGroupRule
SourceSecurityGroupID *string // the ID of a security group to allow access (for VPC groups only).
SourceSecurityGroupName *string // the name of a security group to allow access (for non-VPC groups only).
SourceSecurityGroupOwnerID *string // the account ID of the owner of the group sepcified by the name, if any.
}
// newSecurityIngressGroupRule creates a new instance bag of state, validating required properties if asked to do so.
func newSecurityGroupIngressRule(m resource.PropertyMap, req bool) (*securityGroupIngressRule, error) {
rule, err := newSecurityGroupRule(m, req)
if err != nil && (req || !resource.IsReqError(err)) {
return nil, err
}
srcSecurityGroupID, err := m.OptStringOrErr("sourceSecurityGroup")
if err != nil {
return nil, err
}
srcSecurityGroupName, err := m.OptStringOrErr("sourceSecurityGroupName")
if err != nil {
return nil, err
}
srcSecurityGroupOwnerID, err := m.OptStringOrErr("sourceSecurityGroupOwnerId")
if err != nil {
return nil, err
}
return &securityGroupIngressRule{
securityGroupRule: *rule,
SourceSecurityGroupID: srcSecurityGroupID,
SourceSecurityGroupName: srcSecurityGroupName,
SourceSecurityGroupOwnerID: srcSecurityGroupOwnerID,
}, nil
}

56
lib/aws/provider/main.go Normal file
View file

@ -0,0 +1,56 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package main
import (
"fmt"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"github.com/marapongo/mu/sdk/go/pkg/murpc"
)
func main() {
// Listen on a TCP port, but let the kernel choose a free port for us.
lis, err := net.Listen("tcp", ":0")
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: failed to listen on TCP port ':0': %v\n", err)
os.Exit(-1)
}
// Now new up a gRPC server and register the resource provider implementation.
srv := grpc.NewServer()
prov, err := NewProvider()
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: failed to create AWS resource provider: %v\n", err)
os.Exit(-1)
}
murpc.RegisterResourceProviderServer(srv, prov)
reflection.Register(srv)
// The resource provider protocol requires that we now write out the port we have chosen to listen on. To do
// that, we must retrieve the port chosen by the kernel, by accessing the underlying TCP listener/address.
tcpl := lis.(*net.TCPListener)
tcpa := tcpl.Addr().(*net.TCPAddr)
fmt.Printf("%v\n", strconv.Itoa(tcpa.Port))
// Now register some signals to gracefully terminate the program upon request.
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
go func() {
<-sigs
srv.Stop()
}()
// Finally, serve; this returns only once the server shuts down (e.g., due to a signal).
if err := srv.Serve(lis); err != nil {
fmt.Fprintf(os.Stderr, "fatal: stopped serving: %v\n", err)
os.Exit(-1)
}
}

View file

@ -0,0 +1,74 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package main
import (
"fmt"
pbempty "github.com/golang/protobuf/ptypes/empty"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/sdk/go/pkg/murpc"
"golang.org/x/net/context"
"github.com/marapongo/mu/lib/aws/provider/awsctx"
"github.com/marapongo/mu/lib/aws/provider/ec2"
)
// provider implements the AWS resource provider's operations for all known AWS types.
type Provider struct {
impls map[tokens.Type]murpc.ResourceProviderServer
}
// NewProvider creates a new provider instance with server objects registered for every resource type.
func NewProvider() (*Provider, error) {
ctx, err := awsctx.New()
if err != nil {
return nil, err
}
return &Provider{
impls: map[tokens.Type]murpc.ResourceProviderServer{
ec2.Instance: ec2.NewInstanceProvider(ctx),
ec2.SecurityGroup: ec2.NewSecurityGroupProvider(ctx),
},
}, nil
}
var _ murpc.ResourceProviderServer = (*Provider)(nil)
// Create allocates a new instance of the provided resource and returns its unique ID afterwards. (The input ID
// must be blank.) If this call fails, the resource must not have been created (i.e., it is "transacational").
func (p *Provider) Create(ctx context.Context, req *murpc.CreateRequest) (*murpc.CreateResponse, error) {
t := tokens.Type(req.GetType())
if prov, has := p.impls[t]; has {
return prov.Create(ctx, req)
}
return nil, fmt.Errorf("Unrecognized resource type (Create): %v", t)
}
// Read reads the instance state identified by ID, returning a populated resource object, or an error if not found.
func (p *Provider) Read(ctx context.Context, req *murpc.ReadRequest) (*murpc.ReadResponse, error) {
t := tokens.Type(req.GetType())
if prov, has := p.impls[t]; has {
return prov.Read(ctx, req)
}
return nil, fmt.Errorf("Unrecognized resource type (Read): %v", t)
}
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
func (p *Provider) Update(ctx context.Context, req *murpc.UpdateRequest) (*murpc.UpdateResponse, error) {
t := tokens.Type(req.GetType())
if prov, has := p.impls[t]; has {
return prov.Update(ctx, req)
}
return nil, fmt.Errorf("Unrecognized resource type (Update): %v", t)
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
func (p *Provider) Delete(ctx context.Context, req *murpc.DeleteRequest) (*pbempty.Empty, error) {
t := tokens.Type(req.GetType())
if prov, has := p.impls[t]; has {
return prov.Delete(ctx, req)
}
return nil, fmt.Errorf("Unrecognized resource type (Delete): %v", t)
}

View file

@ -9,12 +9,9 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
structpb "github.com/golang/protobuf/ptypes/struct"
"google.golang.org/grpc"
"github.com/marapongo/mu/pkg/tokens"
@ -119,7 +116,7 @@ func (p *Plugin) Create(res Resource) (ID, error, ResourceState) {
t := string(res.Type())
req := &murpc.CreateRequest{
Type: t,
Properties: marshalProperties(res.Properties()),
Properties: MarshalProperties(res.Properties()),
}
resp, err := p.client.Create(p.ctx.Request(), req)
@ -148,7 +145,7 @@ func (p *Plugin) Read(id ID, t tokens.Type) (PropertyMap, error) {
return nil, err
}
return unmarshalProperties(resp.GetProperties()), nil
return UnmarshalProperties(resp.GetProperties()), nil
}
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
@ -164,8 +161,8 @@ func (p *Plugin) Update(old Resource, new Resource) (ID, error, ResourceState) {
req := &murpc.UpdateRequest{
Id: string(old.ID()),
Type: string(old.Type()),
Olds: marshalProperties(old.Properties()),
News: marshalProperties(new.Properties()),
Olds: MarshalProperties(old.Properties()),
News: MarshalProperties(new.Properties()),
}
resp, err := p.client.Update(p.ctx.Request(), req)
@ -202,121 +199,3 @@ func (p *Plugin) Close() error {
}
return cerr
}
// marshalProperties marshals a resource's property map as a "JSON-like" protobuf structure.
func marshalProperties(props PropertyMap) *structpb.Struct {
result := &structpb.Struct{
Fields: make(map[string]*structpb.Value),
}
for _, key := range StablePropertyKeys(props) {
result.Fields[string(key)] = marshalPropertyValue(props[key])
}
return result
}
// marshalPropertyValue marshals a single resource property value into its "JSON-like" value representation.
func marshalPropertyValue(v PropertyValue) *structpb.Value {
if v.IsNull() {
return &structpb.Value{
Kind: &structpb.Value_NullValue{
structpb.NullValue_NULL_VALUE,
},
}
} else if v.IsBool() {
return &structpb.Value{
Kind: &structpb.Value_BoolValue{
v.BoolValue(),
},
}
} else if v.IsNumber() {
return &structpb.Value{
Kind: &structpb.Value_NumberValue{
v.NumberValue(),
},
}
} else if v.IsString() {
return &structpb.Value{
Kind: &structpb.Value_StringValue{
v.StringValue(),
},
}
} else if v.IsArray() {
var elems []*structpb.Value
for _, elem := range v.ArrayValue() {
elems = append(elems, marshalPropertyValue(elem))
}
return &structpb.Value{
Kind: &structpb.Value_ListValue{
&structpb.ListValue{elems},
},
}
} else if v.IsObject() {
return &structpb.Value{
Kind: &structpb.Value_StructValue{
marshalProperties(v.ObjectValue()),
},
}
} else if v.IsResource() {
// TODO: consider a tag so that the other end knows they are monikers. These just look like strings.
return &structpb.Value{
Kind: &structpb.Value_StringValue{
string(v.ResourceValue()),
},
}
} else {
contract.Failf("Unrecognized property value: %v (type=%v)", v.V, reflect.TypeOf(v.V))
return nil
}
}
// unmarshalProperties unmarshals a "JSON-like" protobuf structure into a resource property map.
func unmarshalProperties(props *structpb.Struct) PropertyMap {
result := make(PropertyMap)
if props == nil {
return result
}
// First sort the keys so we enumerate them in order (in case errors happen, we want determinism).
var keys []string
for k := range props.Fields {
keys = append(keys, k)
}
sort.Strings(keys)
// And now unmarshal every field it into the map.
for _, k := range keys {
result[PropertyKey(k)] = unmarshalPropertyValue(props.Fields[k])
}
return result
}
// unmarshalPropertyValue unmarshals a single "JSON-like" value into its property form.
func unmarshalPropertyValue(v *structpb.Value) PropertyValue {
if v != nil {
switch v.Kind.(type) {
case *structpb.Value_NullValue:
return NewPropertyNull()
case *structpb.Value_BoolValue:
return NewPropertyBool(v.GetBoolValue())
case *structpb.Value_NumberValue:
return NewPropertyNumber(v.GetNumberValue())
case *structpb.Value_StringValue:
// TODO: we have no way of determining that this is a moniker; consider tagging.
return NewPropertyString(v.GetStringValue())
case *structpb.Value_ListValue:
var elems []PropertyValue
lst := v.GetListValue()
for _, elem := range lst.GetValues() {
elems = append(elems, unmarshalPropertyValue(elem))
}
return NewPropertyArray(elems)
case *structpb.Value_StructValue:
props := unmarshalProperties(v.GetStructValue())
return NewPropertyObject(props)
default:
contract.Failf("Unrecognized structpb value kind: %v", reflect.TypeOf(v.Kind))
}
}
return NewPropertyNull()
}

319
pkg/resource/properties.go Normal file
View file

@ -0,0 +1,319 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"fmt"
"reflect"
"github.com/marapongo/mu/pkg/tokens"
)
// PropertyKey is the name of a property.
type PropertyKey tokens.Name
// PropertyMap is a simple map keyed by property name with "JSON-like" values.
type PropertyMap map[PropertyKey]PropertyValue
// PropertyValue is the value of a property, limited to a select few types (see below).
type PropertyValue struct {
V interface{}
}
type ReqError struct {
K PropertyKey
}
func IsReqError(err error) bool {
_, isreq := err.(*ReqError)
return isreq
}
func (err *ReqError) Error() string {
return fmt.Sprintf("required property '%v' is missing", err.K)
}
// BoolOrErr checks that the given property has the type bool, issuing an error if not; req indicates if required.
func (m PropertyMap) BoolOrErr(k PropertyKey, req bool) (*bool, error) {
if v, has := m[k]; has {
if !v.IsBool() {
return nil, fmt.Errorf("property '%v' is not a bool (%v)", k, reflect.TypeOf(v.V))
}
b := v.BoolValue()
return &b, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// NumberOrErr checks that the given property has the type float64, issuing an error if not; req indicates if required.
func (m PropertyMap) NumberOrErr(k PropertyKey, req bool) (*float64, error) {
if v, has := m[k]; has {
if !v.IsNumber() {
return nil, fmt.Errorf("property '%v' is not a number (%v)", k, reflect.TypeOf(v.V))
}
n := v.NumberValue()
return &n, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// StringOrErr checks that the given property has the type string, issuing an error if not; req indicates if required.
func (m PropertyMap) StringOrErr(k PropertyKey, req bool) (*string, error) {
if v, has := m[k]; has {
if !v.IsString() {
return nil, fmt.Errorf("property '%v' is not a string (%v)", k, reflect.TypeOf(v.V))
}
s := v.StringValue()
return &s, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// ArrayOrErr checks that the given property has the type array, issuing an error if not; req indicates if required.
func (m PropertyMap) ArrayOrErr(k PropertyKey, req bool) (*[]PropertyValue, error) {
if v, has := m[k]; has {
if !v.IsArray() {
return nil, fmt.Errorf("property '%v' is not an array (%v)", k, reflect.TypeOf(v.V))
}
a := v.ArrayValue()
return &a, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// ObjectArrayOrErr ensures a property is an array of objects, issuing an error if not; req indicates if required.
func (m PropertyMap) ObjectArrayOrErr(k PropertyKey, req bool) (*[]PropertyMap, error) {
if v, has := m[k]; has {
if !v.IsArray() {
return nil, fmt.Errorf("property '%v' is not an array (%v)", k, reflect.TypeOf(v.V))
}
a := v.ArrayValue()
var objs []PropertyMap
for i, e := range a {
if e.IsObject() {
objs = append(objs, e.ObjectValue())
} else {
return nil, fmt.Errorf(
"property '%v' array element %v is not an object (%v)", k, i, reflect.TypeOf(e))
}
}
return &objs, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// StringArrayOrErr ensures a property is an array of strings, issuing an error if not; req indicates if required.
func (m PropertyMap) StringArrayOrErr(k PropertyKey, req bool) (*[]string, error) {
if v, has := m[k]; has {
if !v.IsArray() {
return nil, fmt.Errorf("property '%v' is not an array (%v)", k, reflect.TypeOf(v.V))
}
a := v.ArrayValue()
var strs []string
for i, e := range a {
if e.IsString() {
strs = append(strs, e.StringValue())
} else {
return nil, fmt.Errorf(
"property '%v' array element %v is not a string (%v)", k, i, reflect.TypeOf(e))
}
}
return &strs, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// ObjectOrErr checks that the given property is an object, issuing an error if not; req indicates if required.
func (m PropertyMap) ObjectOrErr(k PropertyKey, req bool) (*PropertyMap, error) {
if v, has := m[k]; has {
if !v.IsObject() {
return nil, fmt.Errorf("property '%v' is not an object (%v)", k, reflect.TypeOf(v.V))
}
o := v.ObjectValue()
return &o, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// ResourceOrErr checks that the given property is a resource, issuing an error if not; req indicates if required.
func (m PropertyMap) ResourceOrErr(k PropertyKey, req bool) (*Moniker, error) {
if v, has := m[k]; has {
if !v.IsResource() {
return nil, fmt.Errorf("property '%v' is not an object (%v)", k, reflect.TypeOf(v.V))
}
m := v.ResourceValue()
return &m, nil
} else if req {
return nil, &ReqError{k}
}
return nil, nil
}
// ReqBoolOrErr checks that the given property exists and has the type bool.
func (m PropertyMap) ReqBoolOrErr(k PropertyKey) (bool, error) {
b, err := m.BoolOrErr(k, true)
if err != nil {
return false, err
}
return *b, nil
}
// ReqNumberOrErr checks that the given property exists and has the type float64.
func (m PropertyMap) ReqNumberOrErr(k PropertyKey) (float64, error) {
n, err := m.NumberOrErr(k, true)
if err != nil {
return 0, err
}
return *n, nil
}
// ReqStringOrErr checks that the given property exists and has the type string.
func (m PropertyMap) ReqStringOrErr(k PropertyKey) (string, error) {
s, err := m.StringOrErr(k, true)
if err != nil {
return "", err
}
return *s, nil
}
// ReqArrayOrErr checks that the given property exists and has the type array.
func (m PropertyMap) ReqArrayOrErr(k PropertyKey) ([]PropertyValue, error) {
a, err := m.ArrayOrErr(k, true)
if err != nil {
return nil, err
}
return *a, nil
}
// ReqObjectArrayOrErr checks that the given property exists and has the type array of objects.
func (m PropertyMap) ReqObjectArrayOrErr(k PropertyKey) ([]PropertyMap, error) {
a, err := m.ObjectArrayOrErr(k, true)
if err != nil {
return nil, err
}
return *a, nil
}
// ReqStringArrayOrErr checks that the given property exists and has the type array of objects.
func (m PropertyMap) ReqStringArrayOrErr(k PropertyKey) ([]string, error) {
a, err := m.StringArrayOrErr(k, true)
if err != nil {
return nil, err
}
return *a, nil
}
// ReqObjectOrErr checks that the given property exists and has the type object.
func (m PropertyMap) ReqObjectOrErr(k PropertyKey) (PropertyMap, error) {
o, err := m.ObjectOrErr(k, true)
if err != nil {
return nil, err
}
return *o, nil
}
// ReqResourceOrErr checks that the given property exists and has the type moniker.
func (m PropertyMap) ReqResourceOrErr(k PropertyKey) (Moniker, error) {
r, err := m.ResourceOrErr(k, true)
if err != nil {
return Moniker(""), err
}
return *r, nil
}
// OptBoolOrErr checks that the given property has the type bool, if it exists.
func (m PropertyMap) OptBoolOrErr(k PropertyKey) (*bool, error) {
return m.BoolOrErr(k, false)
}
// OptNumberOrErr checks that the given property has the type float64, if it exists.
func (m PropertyMap) OptNumberOrErr(k PropertyKey) (*float64, error) {
return m.NumberOrErr(k, false)
}
// OptStringOrErr checks that the given property has the type string, if it exists.
func (m PropertyMap) OptStringOrErr(k PropertyKey) (*string, error) {
return m.StringOrErr(k, false)
}
// OptArrayOrErr checks that the given property has the type array, if it exists.
func (m PropertyMap) OptArrayOrErr(k PropertyKey) (*[]PropertyValue, error) {
return m.ArrayOrErr(k, false)
}
// OptObjectArrayOrErr checks that the given property has the type array of objects, if it exists.
func (m PropertyMap) OptObjectArrayOrErr(k PropertyKey) (*[]PropertyMap, error) {
return m.ObjectArrayOrErr(k, false)
}
// OptStringArrayOrErr checks that the given property has the type array of objects, if it exists.
func (m PropertyMap) OptStringArrayOrErr(k PropertyKey) (*[]string, error) {
return m.StringArrayOrErr(k, false)
}
// OptObjectOrErr checks that the given property has the type object, if it exists.
func (m PropertyMap) OptObjectOrErr(k PropertyKey) (*PropertyMap, error) {
return m.ObjectOrErr(k, false)
}
// OptResourceOrErr checks that the given property has the type moniker, if it exists.
func (m PropertyMap) OptResourceOrErr(k PropertyKey) (*Moniker, error) {
return m.ResourceOrErr(k, false)
}
func NewPropertyNull() PropertyValue { return PropertyValue{nil} }
func NewPropertyBool(v bool) PropertyValue { return PropertyValue{v} }
func NewPropertyNumber(v float64) PropertyValue { return PropertyValue{v} }
func NewPropertyString(v string) PropertyValue { return PropertyValue{v} }
func NewPropertyArray(v []PropertyValue) PropertyValue { return PropertyValue{v} }
func NewPropertyObject(v PropertyMap) PropertyValue { return PropertyValue{v} }
func NewPropertyResource(v Moniker) PropertyValue { return PropertyValue{v} }
func (v PropertyValue) BoolValue() bool { return v.V.(bool) }
func (v PropertyValue) NumberValue() float64 { return v.V.(float64) }
func (v PropertyValue) StringValue() string { return v.V.(string) }
func (v PropertyValue) ArrayValue() []PropertyValue { return v.V.([]PropertyValue) }
func (v PropertyValue) ObjectValue() PropertyMap { return v.V.(PropertyMap) }
func (v PropertyValue) ResourceValue() Moniker { return v.V.(Moniker) }
func (b PropertyValue) IsNull() bool {
return b.V == nil
}
func (b PropertyValue) IsBool() bool {
_, is := b.V.(bool)
return is
}
func (b PropertyValue) IsNumber() bool {
_, is := b.V.(float64)
return is
}
func (b PropertyValue) IsString() bool {
_, is := b.V.(string)
return is
}
func (b PropertyValue) IsArray() bool {
_, is := b.V.([]PropertyValue)
return is
}
func (b PropertyValue) IsObject() bool {
_, is := b.V.(PropertyMap)
return is
}
func (b PropertyValue) IsResource() bool {
_, is := b.V.(Moniker)
return is
}

View file

@ -37,58 +37,6 @@ const (
StateUnknown
)
type PropertyMap map[PropertyKey]PropertyValue
type PropertyKey tokens.Name // the name of a property.
// PropertyValue is the value of a property, limited to a select few types (see below).
type PropertyValue struct {
V interface{}
}
func NewPropertyNull() PropertyValue { return PropertyValue{nil} }
func NewPropertyBool(v bool) PropertyValue { return PropertyValue{v} }
func NewPropertyNumber(v float64) PropertyValue { return PropertyValue{v} }
func NewPropertyString(v string) PropertyValue { return PropertyValue{v} }
func NewPropertyArray(v []PropertyValue) PropertyValue { return PropertyValue{v} }
func NewPropertyObject(v PropertyMap) PropertyValue { return PropertyValue{v} }
func NewPropertyResource(v Moniker) PropertyValue { return PropertyValue{v} }
func (v PropertyValue) BoolValue() bool { return v.V.(bool) }
func (v PropertyValue) NumberValue() float64 { return v.V.(float64) }
func (v PropertyValue) StringValue() string { return v.V.(string) }
func (v PropertyValue) ArrayValue() []PropertyValue { return v.V.([]PropertyValue) }
func (v PropertyValue) ObjectValue() PropertyMap { return v.V.(PropertyMap) }
func (v PropertyValue) ResourceValue() Moniker { return v.V.(Moniker) }
func (b PropertyValue) IsNull() bool {
return b.V == nil
}
func (b PropertyValue) IsBool() bool {
_, is := b.V.(bool)
return is
}
func (b PropertyValue) IsNumber() bool {
_, is := b.V.(float64)
return is
}
func (b PropertyValue) IsString() bool {
_, is := b.V.(string)
return is
}
func (b PropertyValue) IsArray() bool {
_, is := b.V.([]PropertyValue)
return is
}
func (b PropertyValue) IsObject() bool {
_, is := b.V.(PropertyMap)
return is
}
func (b PropertyValue) IsResource() bool {
_, is := b.V.(Moniker)
return is
}
func IsResourceType(t symbols.Type) bool { return types.HasBaseName(t, predef.MuResourceClass) }
func IsResourceVertex(v graph.Vertex) bool { return IsResourceType(v.Obj().Type()) }

130
pkg/resource/rpc.go Normal file
View file

@ -0,0 +1,130 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package resource
import (
"reflect"
"sort"
structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/marapongo/mu/pkg/util/contract"
)
// MarshalProperties marshals a resource's property map as a "JSON-like" protobuf structure.
func MarshalProperties(props PropertyMap) *structpb.Struct {
result := &structpb.Struct{
Fields: make(map[string]*structpb.Value),
}
for _, key := range StablePropertyKeys(props) {
result.Fields[string(key)] = MarshalPropertyValue(props[key])
}
return result
}
// MarshalPropertyValue marshals a single resource property value into its "JSON-like" value representation.
func MarshalPropertyValue(v PropertyValue) *structpb.Value {
if v.IsNull() {
return &structpb.Value{
Kind: &structpb.Value_NullValue{
structpb.NullValue_NULL_VALUE,
},
}
} else if v.IsBool() {
return &structpb.Value{
Kind: &structpb.Value_BoolValue{
v.BoolValue(),
},
}
} else if v.IsNumber() {
return &structpb.Value{
Kind: &structpb.Value_NumberValue{
v.NumberValue(),
},
}
} else if v.IsString() {
return &structpb.Value{
Kind: &structpb.Value_StringValue{
v.StringValue(),
},
}
} else if v.IsArray() {
var elems []*structpb.Value
for _, elem := range v.ArrayValue() {
elems = append(elems, MarshalPropertyValue(elem))
}
return &structpb.Value{
Kind: &structpb.Value_ListValue{
&structpb.ListValue{elems},
},
}
} else if v.IsObject() {
return &structpb.Value{
Kind: &structpb.Value_StructValue{
MarshalProperties(v.ObjectValue()),
},
}
} else if v.IsResource() {
// TODO: consider a tag so that the other end knows they are monikers. These just look like strings.
return &structpb.Value{
Kind: &structpb.Value_StringValue{
string(v.ResourceValue()),
},
}
} else {
contract.Failf("Unrecognized property value: %v (type=%v)", v.V, reflect.TypeOf(v.V))
return nil
}
}
// UnmarshalProperties unmarshals a "JSON-like" protobuf structure into a resource property map.
func UnmarshalProperties(props *structpb.Struct) PropertyMap {
result := make(PropertyMap)
if props == nil {
return result
}
// First sort the keys so we enumerate them in order (in case errors happen, we want determinism).
var keys []string
for k := range props.Fields {
keys = append(keys, k)
}
sort.Strings(keys)
// And now unmarshal every field it into the map.
for _, k := range keys {
result[PropertyKey(k)] = UnmarshalPropertyValue(props.Fields[k])
}
return result
}
// UnmarshalPropertyValue unmarshals a single "JSON-like" value into its property form.
func UnmarshalPropertyValue(v *structpb.Value) PropertyValue {
if v != nil {
switch v.Kind.(type) {
case *structpb.Value_NullValue:
return NewPropertyNull()
case *structpb.Value_BoolValue:
return NewPropertyBool(v.GetBoolValue())
case *structpb.Value_NumberValue:
return NewPropertyNumber(v.GetNumberValue())
case *structpb.Value_StringValue:
// TODO: we have no way of determining that this is a moniker; consider tagging.
return NewPropertyString(v.GetStringValue())
case *structpb.Value_ListValue:
var elems []PropertyValue
lst := v.GetListValue()
for _, elem := range lst.GetValues() {
elems = append(elems, UnmarshalPropertyValue(elem))
}
return NewPropertyArray(elems)
case *structpb.Value_StructValue:
props := UnmarshalProperties(v.GetStructValue())
return NewPropertyObject(props)
default:
contract.Failf("Unrecognized structpb value kind: %v", reflect.TypeOf(v.Kind))
}
}
return NewPropertyNull()
}