Implement name property in AWS provider/library

This commit is contained in:
joeduffy 2017-02-24 15:41:56 -08:00
parent c120f62964
commit 14e3f19437
18 changed files with 107 additions and 32 deletions

View file

@ -247,7 +247,7 @@ let awsInstanceType2Arch: { [name: string]: { Arch: string; } } = {
}
};
let securityGroup = new SecurityGroup({
let securityGroup = new SecurityGroup("group", {
groupDescription: "Enable SSH access",
securityGroupIngress: [{
ipProtocol: "tcp",
@ -257,9 +257,10 @@ let securityGroup = new SecurityGroup({
}]
});
let instance = new Instance({
let instance = new Instance("instance", {
instanceType: instanceType,
securityGroups: [securityGroup],
keyName: keyName,
imageId: awsRegionArch2AMI[region][awsInstanceType2Arch[instanceType].Arch]
});

View file

@ -8,12 +8,14 @@ export class Resource
extends mu.Resource
implements ResourceProperties {
public readonly name: string;
public readonly resource: string;
public readonly properties?: any;
public readonly dependsOn?: mu.Stack[];
constructor(args: ResourceProperties) {
super();
this.name = args.name;
this.resource = args.resource;
this.properties = args.properties;
this.dependsOn = args.dependsOn;
@ -21,6 +23,8 @@ export class Resource
}
export interface ResourceProperties {
// The resource name.
readonly name: string;
// The CF resource name.
readonly resource: string;
// An optional list of properties to map.

View file

@ -16,9 +16,10 @@ export class Instance
public securityGroups?: SecurityGroup[];
public keyName?: string;
constructor(args: InstanceProperties) {
constructor(name: string, args: InstanceProperties) {
super({
resource: "AWS::EC2::Instance",
name: name,
resource: "AWS::EC2::Instance",
properties: args,
});
this.imageId = args.imageId;

View file

@ -7,8 +7,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/internetGateway
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-internet-gateway.html
export class InternetGateway extends cloudformation.Resource {
constructor(args: InternetGatewayArgs) {
constructor(name: string, args: InternetGatewayArgs) {
super({
name: name,
resource: "AWS::EC2::InternetGateway",
properties: args,
});

View file

@ -10,8 +10,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/route
// @website: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route.html
export class Route extends cloudformation.Resource {
constructor(args: RouteArgs) {
constructor(name: string, args: RouteArgs) {
super({
name: name,
resource: "AWS::EC2::Route",
dependsOn: [
args.vpcGatewayAttachment,

View file

@ -9,8 +9,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/routeTable
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route-table.html
export class RouteTable extends cloudformation.Resource {
constructor(args: RouteTableArgs) {
constructor(name: string, args: RouteTableArgs) {
super({
name: name,
resource: "AWS::EC2::RouteTable",
properties: args,
});

View file

@ -17,8 +17,9 @@ export class SecurityGroup
public securityGroupEgress?: SecurityGroupRule[];
public securityGroupIngress?: SecurityGroupRule[];
constructor(args: SecurityGroupProperties) {
constructor(name: string, args: SecurityGroupProperties) {
super({
name: name,
resource: "AWS::EC2::SecurityGroup",
properties: args,
});

View file

@ -8,8 +8,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/securityGroupEgressRule
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-security-group-egress.html
export class SecurityGroupEgress extends cloudformation.Resource {
constructor(args: SecurityGroupEgressArgs) {
constructor(name: string, args: SecurityGroupEgressArgs) {
super({
name: name,
resource: "AWS::EC2::SecurityGroupEgress",
properties: args,
});

View file

@ -8,8 +8,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/securityGroupIngressRule
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-security-group-ingress.html
export class SecurityGroupIngress extends cloudformation.Resource {
constructor(args: SecurityGroupIngressArgs) {
constructor(name: string, args: SecurityGroupIngressArgs) {
super({
name: name,
resource: "AWS::EC2::SecurityGroupIngress",
properties: args,
});

View file

@ -8,8 +8,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/subnet
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html
export class Subnet extends cloudformation.Resource {
constructor(args: SubnetArgs) {
constructor(name: string, args: SubnetArgs) {
super({
name: name,
resource: "AWS::EC2::Subnet",
properties: args,
});

View file

@ -7,8 +7,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/vpc
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html
export class VPC extends cloudformation.Resource {
constructor(args: VPCArgs) {
constructor(name: string, args: VPCArgs) {
super({
name: name,
resource: "AWS::EC2::VPC",
properties: args,
});

View file

@ -9,8 +9,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/vpcGatewayAttachment
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc-gateway-attachment.html
export class VPCGatewayAttachment extends cloudformation.Resource {
constructor(args: VPCGatewayAttachmentArgs) {
constructor(name: string, args: VPCGatewayAttachmentArgs) {
super({
name: name,
resource: "AWS::EC2::VPCGatewayAttachment",
properties: args,
});

View file

@ -9,8 +9,9 @@ import * as cloudformation from '../cloudformation';
// @name: aws/ec2/vpcPeeringConnection
// @website: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpcpeeringconnection.html
export class VPCPeeringConnection extends cloudformation.Resource {
constructor(args: VPCPeeringConnectionArgs) {
constructor(name: string, args: VPCPeeringConnectionArgs) {
super({
name: name,
resource: "AWS::EC2::VPCPeeringConnection",
properties: args,
});

View file

@ -30,6 +30,13 @@ type instanceProvider struct {
ctx *awsctx.Context
}
// Name names a given resource. Sometimes this will be assigned by a developer, and so the provider
// simply fetches it from the property bag; other times, the provider will assign this based on its own algorithm.
// In any case, resources with the same name must be safe to use interchangeably with one another.
func (p *instanceProvider) Name(ctx context.Context, req *murpc.NameRequest) (*murpc.NameResponse, error) {
return nil, nil // use the AWS provider default name
}
// 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) {

View file

@ -31,6 +31,13 @@ type securityGroupProvider struct {
ctx *awsctx.Context
}
// Name names a given resource. Sometimes this will be assigned by a developer, and so the provider
// simply fetches it from the property bag; other times, the provider will assign this based on its own algorithm.
// In any case, resources with the same name must be safe to use interchangeably with one another.
func (p *securityGroupProvider) Name(ctx context.Context, req *murpc.NameRequest) (*murpc.NameResponse, error) {
return nil, nil // use the AWS provider default name
}
// 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) {

View file

@ -6,6 +6,7 @@ import (
"fmt"
pbempty "github.com/golang/protobuf/ptypes/empty"
"github.com/marapongo/mu/pkg/resource"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/sdk/go/pkg/murpc"
"golang.org/x/net/context"
@ -35,6 +36,36 @@ func NewProvider() (*Provider, error) {
var _ murpc.ResourceProviderServer = (*Provider)(nil)
const nameProperty string = "name" // the property used for naming AWS resources.
// Name names a given resource. Sometimes this will be assigned by a developer, and so the provider
// simply fetches it from the property bag; other times, the provider will assign this based on its own algorithm.
// In any case, resources with the same name must be safe to use interchangeably with one another.
func (p *Provider) Name(ctx context.Context, req *murpc.NameRequest) (*murpc.NameResponse, error) {
// First, see if the provider overrides the naming.
t := tokens.Type(req.GetType())
if prov, has := p.impls[t]; has {
if res, err := prov.Name(ctx, req); res != nil || err != nil {
return res, err
}
} else {
return nil, fmt.Errorf("Unrecognized resource type (Create): %v", t)
}
// If the provider didn't override, we can go ahead and default to the name property.
// TODO: eventually, we want to specialize some resources, like SecurityGroups, since they already have names.
if nameprop, has := req.GetProperties().Fields[nameProperty]; has {
name := resource.UnmarshalPropertyValue(nameprop)
if name.IsString() {
return &murpc.NameResponse{Name: name.StringValue()}, nil
} else {
return nil, fmt.Errorf(
"Resource '%v' had a name property '%v', but it wasn't a string", t, nameProperty)
}
}
return nil, fmt.Errorf("Resource '%v' was missing a name property '%v'", t, nameProperty)
}
// 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) {

View file

@ -143,8 +143,10 @@ func execPlugin(name string) (*os.Process, io.WriteCloser, io.ReadCloser, io.Rea
func (p *Plugin) Name(t tokens.Type, props PropertyMap) (tokens.QName, error) {
glog.V(7).Infof("Plugin[%v].Name(t=%v,#props=%v) executing", p.pkg, t, len(props))
req := &murpc.NameRequest{
Type: string(t),
Properties: MarshalProperties(p.ctx, props),
Type: string(t),
Properties: MarshalProperties(p.ctx, props, MarshalOptions{
SkipMonikers: true, // often used during moniker creation; IDs won't be ready.
}),
}
resp, err := p.client.Name(p.ctx.Request(), req)
@ -163,7 +165,7 @@ func (p *Plugin) Create(t tokens.Type, props PropertyMap) (ID, error, ResourceSt
glog.V(7).Infof("Plugin[%v].Create(t=%v,#props=%v) executing", p.pkg, t, len(props))
req := &murpc.CreateRequest{
Type: string(t),
Properties: MarshalProperties(p.ctx, props),
Properties: MarshalProperties(p.ctx, props, MarshalOptions{}),
}
resp, err := p.client.Create(p.ctx.Request(), req)
@ -211,8 +213,8 @@ func (p *Plugin) Update(id ID, t tokens.Type, olds PropertyMap, news PropertyMap
req := &murpc.UpdateRequest{
Id: string(id),
Type: string(t),
Olds: MarshalProperties(p.ctx, olds),
News: MarshalProperties(p.ctx, news),
Olds: MarshalProperties(p.ctx, olds, MarshalOptions{}),
News: MarshalProperties(p.ctx, news, MarshalOptions{}),
}
resp, err := p.client.Update(p.ctx.Request(), req)

View file

@ -12,61 +12,73 @@ import (
"github.com/marapongo/mu/pkg/util/contract"
)
// MarshalOptions controls the marshaling of RPC structures.
type MarshalOptions struct {
SkipMonikers bool // true to skip monikers (e.g., if they aren't ready yet).
}
// MarshalProperties marshals a resource's property map as a "JSON-like" protobuf structure. Any monikers are replaced
// with their resource IDs during marshaling; it is an error to marshal a moniker for a resource without an ID.
func MarshalProperties(ctx *Context, props PropertyMap) *structpb.Struct {
func MarshalProperties(ctx *Context, props PropertyMap, opts MarshalOptions) *structpb.Struct {
result := &structpb.Struct{
Fields: make(map[string]*structpb.Value),
}
for _, key := range StablePropertyKeys(props) {
result.Fields[string(key)] = MarshalPropertyValue(ctx, props[key])
if v, use := MarshalPropertyValue(ctx, props[key], opts); use {
result.Fields[string(key)] = v
}
}
return result
}
// MarshalPropertyValue marshals a single resource property value into its "JSON-like" value representation.
func MarshalPropertyValue(ctx *Context, v PropertyValue) *structpb.Value {
func MarshalPropertyValue(ctx *Context, v PropertyValue, opts MarshalOptions) (*structpb.Value, bool) {
if v.IsNull() {
return &structpb.Value{
Kind: &structpb.Value_NullValue{
structpb.NullValue_NULL_VALUE,
},
}
}, true
} else if v.IsBool() {
return &structpb.Value{
Kind: &structpb.Value_BoolValue{
v.BoolValue(),
},
}
}, true
} else if v.IsNumber() {
return &structpb.Value{
Kind: &structpb.Value_NumberValue{
v.NumberValue(),
},
}
}, true
} else if v.IsString() {
return &structpb.Value{
Kind: &structpb.Value_StringValue{
v.StringValue(),
},
}
}, true
} else if v.IsArray() {
var elems []*structpb.Value
for _, elem := range v.ArrayValue() {
elems = append(elems, MarshalPropertyValue(ctx, elem))
if elemv, use := MarshalPropertyValue(ctx, elem, opts); use {
elems = append(elems, elemv)
}
}
return &structpb.Value{
Kind: &structpb.Value_ListValue{
&structpb.ListValue{elems},
},
}
}, true
} else if v.IsObject() {
return &structpb.Value{
Kind: &structpb.Value_StructValue{
MarshalProperties(ctx, v.ObjectValue()),
MarshalProperties(ctx, v.ObjectValue(), opts),
},
}
}, true
} else if v.IsResource() {
if opts.SkipMonikers {
return nil, false
}
m := v.ResourceValue()
res, has := ctx.MksRes[m]
contract.Assertf(has, "Expected resource moniker '%v' to exist at marshal time", m)
@ -77,10 +89,10 @@ func MarshalPropertyValue(ctx *Context, v PropertyValue) *structpb.Value {
Kind: &structpb.Value_StringValue{
string(id),
},
}
}, true
} else {
contract.Failf("Unrecognized property value: %v (type=%v)", v.V, reflect.TypeOf(v.V))
return nil
return nil, true
}
}