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:
parent
dbd1721ced
commit
276b6c253d
16
cmd/apply.go
16
cmd/apply.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
39
lib/aws/provider/awsctx/context.go
Normal file
39
lib/aws/provider/awsctx/context.go
Normal 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
|
||||
}
|
126
lib/aws/provider/ec2/instance.go
Normal file
126
lib/aws/provider/ec2/instance.go
Normal 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
|
||||
}
|
257
lib/aws/provider/ec2/security_group.go
Normal file
257
lib/aws/provider/ec2/security_group.go
Normal 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
56
lib/aws/provider/main.go
Normal 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)
|
||||
}
|
||||
}
|
74
lib/aws/provider/provider.go
Normal file
74
lib/aws/provider/provider.go
Normal 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)
|
||||
}
|
|
@ -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
319
pkg/resource/properties.go
Normal 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
|
||||
}
|
|
@ -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
130
pkg/resource/rpc.go
Normal 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()
|
||||
}
|
Loading…
Reference in a new issue