Merge pull request #194 from pulumi/providertest
Test coverage for more AWS providers
This commit is contained in:
commit
bdcda5717a
|
@ -31,6 +31,8 @@ type Instance struct {
|
|||
SecurityGroups *[]*SecurityGroup `lumi:"securityGroups,optional,replaces"`
|
||||
// Provides the name of the Amazon EC2 key pair.
|
||||
KeyName *string `lumi:"keyName,optional"`
|
||||
// Provides a list of tags to attach to the instance.
|
||||
Tags *[]Tag `lumi:"tags,optional"`
|
||||
|
||||
// Output properties:
|
||||
|
||||
|
@ -46,6 +48,12 @@ type Instance struct {
|
|||
PublicIP *string `lumi:"publicIP,out,optional"`
|
||||
}
|
||||
|
||||
// A Tag applied to an EC2 instance.
|
||||
type Tag struct {
|
||||
Key string `lumi:"key"`
|
||||
Value string `lumi:"value"`
|
||||
}
|
||||
|
||||
// InstanceType is an enum type with all the names of instance types available in EC2.
|
||||
type InstanceType string
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ export class Instance extends lumi.Resource implements InstanceArgs {
|
|||
public instanceType?: InstanceType;
|
||||
public readonly securityGroups?: SecurityGroup[];
|
||||
public keyName?: string;
|
||||
public tags?: Tag[];
|
||||
@lumi.out public availabilityZone: string;
|
||||
@lumi.out public privateDNSName?: string;
|
||||
@lumi.out public publicDNSName?: string;
|
||||
|
@ -88,6 +89,7 @@ export class Instance extends lumi.Resource implements InstanceArgs {
|
|||
this.instanceType = args.instanceType;
|
||||
this.securityGroups = args.securityGroups;
|
||||
this.keyName = args.keyName;
|
||||
this.tags = args.tags;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,6 +98,7 @@ export interface InstanceArgs {
|
|||
instanceType?: InstanceType;
|
||||
readonly securityGroups?: SecurityGroup[];
|
||||
keyName?: string;
|
||||
tags?: Tag[];
|
||||
}
|
||||
|
||||
export type InstanceType =
|
||||
|
@ -157,4 +160,9 @@ export type InstanceType =
|
|||
"x1.16xlarge" |
|
||||
"x1.32xlarge";
|
||||
|
||||
export interface Tag {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,45 +4,95 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
awsdynamodb "github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
structpb "github.com/golang/protobuf/ptypes/struct"
|
||||
"github.com/pulumi/lumi/lib/aws/provider/awsctx"
|
||||
"github.com/pulumi/lumi/lib/aws/provider/testutil"
|
||||
"github.com/pulumi/lumi/lib/aws/rpc/dynamodb"
|
||||
"github.com/pulumi/lumi/pkg/resource"
|
||||
"github.com/pulumi/lumi/sdk/go/pkg/lumirpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const TABLENAMEPREFIX = "lumitest"
|
||||
const RESOURCEPREFIX = "lumitest"
|
||||
|
||||
func marshal(table dynamodb.Table) (*structpb.Struct, error) {
|
||||
byts, err := json.Marshal(table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var obj map[string]interface{}
|
||||
err = json.Unmarshal(byts, &obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
props := resource.NewPropertyMapFromMap(obj)
|
||||
return resource.MarshalProperties(nil, props, resource.MarshalOptions{}), nil
|
||||
func Test(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, err := awsctx.New()
|
||||
assert.Nil(t, err, "expected no error getting AWS context")
|
||||
|
||||
cleanup(ctx)
|
||||
|
||||
testutil.ProviderTestSimple(t, NewTableProvider(ctx), TableToken, []interface{}{
|
||||
&dynamodb.Table{
|
||||
Name: aws.String(RESOURCEPREFIX),
|
||||
Attributes: []dynamodb.Attribute{
|
||||
{Name: "Album", Type: "S"},
|
||||
{Name: "Artist", Type: "S"},
|
||||
{Name: "Sales", Type: "N"},
|
||||
},
|
||||
HashKey: "Album",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 2,
|
||||
WriteCapacity: 2,
|
||||
GlobalSecondaryIndexes: &[]dynamodb.GlobalSecondaryIndex{
|
||||
{
|
||||
IndexName: "myGSI",
|
||||
HashKey: "Sales",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
NonKeyAttributes: []string{"Album"},
|
||||
ProjectionType: "INCLUDE",
|
||||
},
|
||||
},
|
||||
},
|
||||
&dynamodb.Table{
|
||||
Name: aws.String(RESOURCEPREFIX),
|
||||
Attributes: []dynamodb.Attribute{
|
||||
{Name: "Album", Type: "S"},
|
||||
{Name: "Artist", Type: "S"},
|
||||
{Name: "NumberOfSongs", Type: "N"},
|
||||
{Name: "Sales", Type: "N"},
|
||||
},
|
||||
HashKey: "Album",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
GlobalSecondaryIndexes: &[]dynamodb.GlobalSecondaryIndex{
|
||||
{
|
||||
IndexName: "myGSI",
|
||||
HashKey: "Sales",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
NonKeyAttributes: []string{"Album"},
|
||||
ProjectionType: "INCLUDE",
|
||||
},
|
||||
{
|
||||
IndexName: "myGSI2",
|
||||
HashKey: "NumberOfSongs",
|
||||
RangeKey: aws.String("Sales"),
|
||||
NonKeyAttributes: []string{"Album", "Artist"},
|
||||
ProjectionType: "INCLUDE",
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func cleanup(ctx *awsctx.Context) {
|
||||
fmt.Printf("Cleaning up tables with prefix: %v\n", TABLENAMEPREFIX)
|
||||
fmt.Printf("Cleaning up tables with prefix: %v\n", RESOURCEPREFIX)
|
||||
list, err := ctx.DynamoDB().ListTables(&awsdynamodb.ListTablesInput{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cleaned := 0
|
||||
for _, table := range list.TableNames {
|
||||
if strings.HasPrefix(aws.StringValue(table), TABLENAMEPREFIX) {
|
||||
if strings.HasPrefix(aws.StringValue(table), RESOURCEPREFIX) {
|
||||
ctx.DynamoDB().DeleteTable(&awsdynamodb.DeleteTableInput{
|
||||
TableName: table,
|
||||
})
|
||||
|
@ -51,135 +101,3 @@ func cleanup(ctx *awsctx.Context) {
|
|||
}
|
||||
fmt.Printf("Cleaned up %v tables\n", cleaned)
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a TableProvider
|
||||
ctx, err := awsctx.New()
|
||||
assert.Nil(t, err, "expected no error getting AWS context")
|
||||
tableProvider := NewTableProvider(ctx)
|
||||
|
||||
defer cleanup(ctx)
|
||||
|
||||
// Table to create
|
||||
tablename := TABLENAMEPREFIX
|
||||
table := dynamodb.Table{
|
||||
Name: &tablename,
|
||||
Attributes: []dynamodb.Attribute{
|
||||
{Name: "Album", Type: "S"},
|
||||
{Name: "Artist", Type: "S"},
|
||||
{Name: "Sales", Type: "N"},
|
||||
},
|
||||
HashKey: "Album",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 2,
|
||||
WriteCapacity: 2,
|
||||
GlobalSecondaryIndexes: &[]dynamodb.GlobalSecondaryIndex{
|
||||
{
|
||||
IndexName: "myGSI",
|
||||
HashKey: "Sales",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
NonKeyAttributes: []string{"Album"},
|
||||
ProjectionType: "INCLUDE",
|
||||
},
|
||||
},
|
||||
}
|
||||
props, err := marshal(table)
|
||||
if !assert.NoError(t, err, "expected no error marshaling object to protobuf") {
|
||||
return
|
||||
}
|
||||
checkResp, err := tableProvider.Check(nil, &lumirpc.CheckRequest{
|
||||
Type: string(TableToken),
|
||||
Properties: props,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error checking table") {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, 0, len(checkResp.Failures), "expected no check failures")
|
||||
|
||||
// Invoke Create request
|
||||
resp, err := tableProvider.Create(nil, &lumirpc.CreateRequest{
|
||||
Type: string(TableToken),
|
||||
Properties: props,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error creating resource") {
|
||||
return
|
||||
}
|
||||
if !assert.NotNil(t, resp, "expected a non-nil response") {
|
||||
return
|
||||
}
|
||||
|
||||
id := resp.Id
|
||||
assert.Contains(t, id, "lumitest", "expected resource ID to contain `lumitest`")
|
||||
|
||||
// Table for update
|
||||
tablename2 := "lumitest"
|
||||
table2 := dynamodb.Table{
|
||||
Name: &tablename2,
|
||||
Attributes: []dynamodb.Attribute{
|
||||
{Name: "Album", Type: "S"},
|
||||
{Name: "Artist", Type: "S"},
|
||||
{Name: "NumberOfSongs", Type: "N"},
|
||||
{Name: "Sales", Type: "N"},
|
||||
},
|
||||
HashKey: "Album",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
GlobalSecondaryIndexes: &[]dynamodb.GlobalSecondaryIndex{
|
||||
{
|
||||
IndexName: "myGSI",
|
||||
HashKey: "Sales",
|
||||
RangeKey: aws.String("Artist"),
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
NonKeyAttributes: []string{"Album"},
|
||||
ProjectionType: "INCLUDE",
|
||||
},
|
||||
{
|
||||
IndexName: "myGSI2",
|
||||
HashKey: "NumberOfSongs",
|
||||
RangeKey: aws.String("Sales"),
|
||||
NonKeyAttributes: []string{"Album", "Artist"},
|
||||
ProjectionType: "INCLUDE",
|
||||
ReadCapacity: 1,
|
||||
WriteCapacity: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
props2, err := marshal(table2)
|
||||
if !assert.NoError(t, err, "expected no error marshaling object to protobuf") {
|
||||
return
|
||||
}
|
||||
checkResp, err = tableProvider.Check(nil, &lumirpc.CheckRequest{
|
||||
Type: string(TableToken),
|
||||
Properties: props2,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error checking table") {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, 0, len(checkResp.Failures), "expected no check failures")
|
||||
|
||||
// Invoke Update request
|
||||
_, err = tableProvider.Update(nil, &lumirpc.UpdateRequest{
|
||||
Type: string(TableToken),
|
||||
Id: id,
|
||||
Olds: props,
|
||||
News: props2,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error creating resource") {
|
||||
return
|
||||
}
|
||||
|
||||
// Invoke the Delete request
|
||||
_, err = tableProvider.Delete(nil, &lumirpc.DeleteRequest{
|
||||
Type: string(TableToken),
|
||||
Id: id,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error deleting resource") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,13 +89,28 @@ func (p *instanceProvider) Create(ctx context.Context, obj *ec2.Instance) (resou
|
|||
its := string(*obj.InstanceType)
|
||||
instanceType = &its
|
||||
}
|
||||
var tagSpecifications []*awsec2.TagSpecification
|
||||
if obj.Tags != nil {
|
||||
var tags []*awsec2.Tag
|
||||
for _, tag := range *obj.Tags {
|
||||
tags = append(tags, &awsec2.Tag{
|
||||
Key: aws.String(tag.Key),
|
||||
Value: aws.String(tag.Value),
|
||||
})
|
||||
}
|
||||
tagSpecifications = []*awsec2.TagSpecification{{
|
||||
ResourceType: aws.String("instance"),
|
||||
Tags: tags,
|
||||
}}
|
||||
}
|
||||
create := &awsec2.RunInstancesInput{
|
||||
ImageId: aws.String(obj.ImageID),
|
||||
InstanceType: instanceType,
|
||||
SecurityGroupIds: secgrpIDs,
|
||||
KeyName: obj.KeyName,
|
||||
MinCount: aws.Int64(int64(1)),
|
||||
MaxCount: aws.Int64(int64(1)),
|
||||
ImageId: aws.String(obj.ImageID),
|
||||
InstanceType: instanceType,
|
||||
SecurityGroupIds: secgrpIDs,
|
||||
KeyName: obj.KeyName,
|
||||
MinCount: aws.Int64(int64(1)),
|
||||
MaxCount: aws.Int64(int64(1)),
|
||||
TagSpecifications: tagSpecifications,
|
||||
}
|
||||
|
||||
// Now go ahead and perform the action.
|
||||
|
@ -159,6 +174,18 @@ func (p *instanceProvider) Get(ctx context.Context, id resource.ID) (*ec2.Instan
|
|||
secgrpIDs = &ids
|
||||
}
|
||||
|
||||
var instanceTags *[]ec2.Tag
|
||||
if len(inst.Tags) > 0 {
|
||||
var tags []ec2.Tag
|
||||
for _, tag := range inst.Tags {
|
||||
tags = append(tags, ec2.Tag{
|
||||
Key: aws.StringValue(tag.Key),
|
||||
Value: aws.StringValue(tag.Value),
|
||||
})
|
||||
}
|
||||
instanceTags = &tags
|
||||
}
|
||||
|
||||
instanceType := ec2.InstanceType(aws.StringValue(inst.InstanceType))
|
||||
return &ec2.Instance{
|
||||
ImageID: aws.StringValue(inst.ImageId),
|
||||
|
@ -170,6 +197,7 @@ func (p *instanceProvider) Get(ctx context.Context, id resource.ID) (*ec2.Instan
|
|||
PublicDNSName: inst.PublicDnsName,
|
||||
PrivateIP: inst.PrivateIpAddress,
|
||||
PublicIP: inst.PublicIpAddress,
|
||||
Tags: instanceTags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -184,7 +212,50 @@ func (p *instanceProvider) InspectChange(ctx context.Context, id resource.ID,
|
|||
// 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, id resource.ID,
|
||||
old *ec2.Instance, new *ec2.Instance, diff *resource.ObjectDiff) error {
|
||||
return errors.New("No known updatable instance properties")
|
||||
iid, err := arn.ParseResourceName(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if diff.Changed(ec2.Instance_Tags) {
|
||||
newTagSet := newTagHashSet(new.Tags)
|
||||
oldTagSet := newTagHashSet(old.Tags)
|
||||
d := oldTagSet.Diff(newTagSet)
|
||||
var addOrUpdateTags []*awsec2.Tag
|
||||
for _, o := range d.AddOrUpdates() {
|
||||
option := o.(tagHash).item
|
||||
addOrUpdateTags = append(addOrUpdateTags, &awsec2.Tag{
|
||||
Key: aws.String(option.Key),
|
||||
Value: aws.String(option.Value),
|
||||
})
|
||||
}
|
||||
if len(addOrUpdateTags) > 0 {
|
||||
_, err := p.ctx.EC2().CreateTags(&awsec2.CreateTagsInput{
|
||||
Resources: []*string{aws.String(iid)},
|
||||
Tags: addOrUpdateTags,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var deleteTags []*awsec2.Tag
|
||||
for _, o := range d.Deletes() {
|
||||
option := o.(tagHash).item
|
||||
deleteTags = append(deleteTags, &awsec2.Tag{
|
||||
Key: aws.String(option.Key),
|
||||
Value: nil,
|
||||
})
|
||||
}
|
||||
if len(deleteTags) > 0 {
|
||||
_, err = p.ctx.EC2().DeleteTags(&awsec2.DeleteTagsInput{
|
||||
Resources: []*string{aws.String(iid)},
|
||||
Tags: deleteTags,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
|
||||
|
@ -203,3 +274,26 @@ func (p *instanceProvider) Delete(ctx context.Context, id resource.ID) error {
|
|||
return p.ctx.EC2().WaitUntilInstanceTerminated(
|
||||
&awsec2.DescribeInstancesInput{InstanceIds: []*string{aws.String(iid)}})
|
||||
}
|
||||
|
||||
type tagHash struct {
|
||||
item ec2.Tag
|
||||
}
|
||||
|
||||
var _ awsctx.Hashable = tagHash{}
|
||||
|
||||
func (option tagHash) HashKey() awsctx.Hash {
|
||||
return awsctx.Hash(option.item.Key)
|
||||
}
|
||||
func (option tagHash) HashValue() awsctx.Hash {
|
||||
return awsctx.Hash(option.item.Key + ":" + option.item.Value)
|
||||
}
|
||||
func newTagHashSet(options *[]ec2.Tag) *awsctx.HashSet {
|
||||
set := awsctx.NewHashSet()
|
||||
if options == nil {
|
||||
return set
|
||||
}
|
||||
for _, option := range *options {
|
||||
set.Add(tagHash{option})
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
|
102
lib/aws/provider/ec2/instance_test.go
Normal file
102
lib/aws/provider/ec2/instance_test.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package ec2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
awsec2 "github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/pulumi/lumi/lib/aws/provider/awsctx"
|
||||
"github.com/pulumi/lumi/lib/aws/provider/testutil"
|
||||
"github.com/pulumi/lumi/lib/aws/rpc/ec2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const RESOURCEPREFIX = "lumitest"
|
||||
|
||||
var amis = map[string]string{
|
||||
"us-east-1": "ami-6869aa05",
|
||||
"us-west-2": "ami-7172b611",
|
||||
"us-west-1": "ami-31490d51",
|
||||
"eu-west-1": "ami-f9dd458a",
|
||||
"eu-west-2": "ami-886369ec",
|
||||
"eu-central-1": "ami-ea26ce85",
|
||||
"ap-northeast-1": "ami-374db956",
|
||||
"ap-northeast-2": "ami-2b408b45",
|
||||
"ap-southeast-1": "ami-a59b49c6",
|
||||
"ap-southeast-2": "ami-dc361ebf",
|
||||
"ap-south-1": "ami-ffbdd790",
|
||||
"us-east-2": "ami-f6035893",
|
||||
"ca-central-1": "ami-730ebd17",
|
||||
"sa-east-1": "ami-6dd04501",
|
||||
"cn-north-1": "ami-8e6aa0e3",
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, err := awsctx.New()
|
||||
assert.Nil(t, err, "expected no error getting AWS context")
|
||||
|
||||
cleanup(ctx)
|
||||
|
||||
instanceType := ec2.InstanceType("t2.nano")
|
||||
|
||||
testutil.ProviderTestSimple(t, NewInstanceProvider(ctx), InstanceToken, []interface{}{
|
||||
&ec2.Instance{
|
||||
Name: aws.String(RESOURCEPREFIX),
|
||||
InstanceType: &instanceType,
|
||||
ImageID: amis[ctx.Region()],
|
||||
Tags: &[]ec2.Tag{{
|
||||
Key: RESOURCEPREFIX,
|
||||
Value: RESOURCEPREFIX,
|
||||
}},
|
||||
},
|
||||
&ec2.Instance{
|
||||
Name: aws.String(RESOURCEPREFIX),
|
||||
InstanceType: &instanceType,
|
||||
ImageID: amis[ctx.Region()],
|
||||
Tags: &[]ec2.Tag{{
|
||||
Key: RESOURCEPREFIX,
|
||||
Value: RESOURCEPREFIX,
|
||||
}, {
|
||||
Key: "Hello",
|
||||
Value: "World",
|
||||
}},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func cleanup(ctx *awsctx.Context) {
|
||||
fmt.Printf("Cleaning up instances with tag:%v=%v\n", RESOURCEPREFIX, RESOURCEPREFIX)
|
||||
list, err := ctx.EC2().DescribeInstances(&awsec2.DescribeInstancesInput{
|
||||
Filters: []*awsec2.Filter{{
|
||||
Name: aws.String("tag:" + RESOURCEPREFIX),
|
||||
Values: []*string{aws.String(RESOURCEPREFIX)},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cleaned := 0
|
||||
instanceIds := []*string{}
|
||||
for _, reservation := range list.Reservations {
|
||||
for _, instance := range reservation.Instances {
|
||||
if aws.StringValue(instance.State.Name) != awsec2.InstanceStateNameTerminated {
|
||||
instanceIds = append(instanceIds, instance.InstanceId)
|
||||
cleaned++
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(instanceIds) > 0 {
|
||||
_, err = ctx.EC2().TerminateInstances(&awsec2.TerminateInstancesInput{
|
||||
InstanceIds: instanceIds,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed cleaning up %v tables: %v\n", cleaned, err)
|
||||
} else {
|
||||
fmt.Printf("Cleaned up %v tables\n", cleaned)
|
||||
}
|
||||
}
|
||||
}
|
147
lib/aws/provider/lambda/function_test.go
Normal file
147
lib/aws/provider/lambda/function_test.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package lambda
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
awsiam "github.com/aws/aws-sdk-go/service/iam"
|
||||
awslambda "github.com/aws/aws-sdk-go/service/lambda"
|
||||
"github.com/pulumi/lumi/lib/aws/provider/awsctx"
|
||||
iamprovider "github.com/pulumi/lumi/lib/aws/provider/iam"
|
||||
"github.com/pulumi/lumi/lib/aws/provider/testutil"
|
||||
rpc "github.com/pulumi/lumi/lib/aws/rpc"
|
||||
"github.com/pulumi/lumi/lib/aws/rpc/iam"
|
||||
"github.com/pulumi/lumi/lib/aws/rpc/lambda"
|
||||
"github.com/pulumi/lumi/pkg/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const RESOURCEPREFIX = "lumitest"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, err := awsctx.New()
|
||||
assert.Nil(t, err, "expected no error getting AWS context")
|
||||
|
||||
cleanupFunctions(ctx)
|
||||
cleanupRoles(ctx)
|
||||
|
||||
functionProvider := NewFunctionProvider(ctx)
|
||||
roleProvider := iamprovider.NewRoleProvider(ctx)
|
||||
|
||||
resources := map[string]testutil.Resource{
|
||||
"role": {Provider: roleProvider, Token: iam.RoleToken},
|
||||
"f": {Provider: functionProvider, Token: FunctionToken},
|
||||
}
|
||||
steps := []testutil.Step{
|
||||
testutil.Step{
|
||||
testutil.ResourceGenerator{
|
||||
Name: "role",
|
||||
Creator: func(ctx testutil.Context) interface{} {
|
||||
return &iam.Role{
|
||||
Name: aws.String(RESOURCEPREFIX),
|
||||
ManagedPolicyARNs: &[]rpc.ARN{
|
||||
rpc.ARN("arn:aws:iam::aws:policy/AWSLambdaFullAccess"),
|
||||
},
|
||||
AssumeRolePolicyDocument: map[string]interface{}{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": []map[string]interface{}{
|
||||
{
|
||||
"Action": "sts:AssumeRole",
|
||||
"Principal": map[string]interface{}{
|
||||
"Service": "lambda.amazonaws.com",
|
||||
},
|
||||
"Effect": "Allow",
|
||||
"Sid": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
testutil.ResourceGenerator{
|
||||
Name: "f",
|
||||
Creator: func(ctx testutil.Context) interface{} {
|
||||
return &lambda.Function{
|
||||
Name: aws.String(RESOURCEPREFIX),
|
||||
Code: resource.Archive{
|
||||
Assets: &map[string]*resource.Asset{
|
||||
"index.js": &resource.Asset{
|
||||
Text: aws.String("exports.handler = (ev, ctx, cb) => { console.log(ev); console.log(ctx); }"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: "index.handler",
|
||||
Runtime: lambda.NodeJS6d10Runtime,
|
||||
Role: ctx.GetResourceID("role"),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testutil.ProviderTest(t, resources, steps)
|
||||
|
||||
}
|
||||
|
||||
func cleanupFunctions(ctx *awsctx.Context) {
|
||||
fmt.Printf("Cleaning up function with name:%v\n", RESOURCEPREFIX)
|
||||
list, err := ctx.Lambda().ListFunctions(&awslambda.ListFunctionsInput{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cleaned := 0
|
||||
for _, fnc := range list.Functions {
|
||||
if strings.HasPrefix(aws.StringValue(fnc.FunctionName), RESOURCEPREFIX) {
|
||||
_, err := ctx.Lambda().DeleteFunction(&awslambda.DeleteFunctionInput{
|
||||
FunctionName: fnc.FunctionName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to cleanip function %v: %v\n", fnc.FunctionName, err)
|
||||
} else {
|
||||
cleaned++
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("Cleaned up %v functions\n", cleaned)
|
||||
}
|
||||
|
||||
func cleanupRoles(ctx *awsctx.Context) {
|
||||
fmt.Printf("Cleaning up roles with name:%v\n", RESOURCEPREFIX)
|
||||
list, err := ctx.IAM().ListRoles(&awsiam.ListRolesInput{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cleaned := 0
|
||||
for _, role := range list.Roles {
|
||||
if strings.HasPrefix(aws.StringValue(role.RoleName), RESOURCEPREFIX) {
|
||||
policies, err := ctx.IAM().ListAttachedRolePolicies(&awsiam.ListAttachedRolePoliciesInput{
|
||||
RoleName: role.RoleName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to cleanup role %v: %v\n", role.RoleName, err)
|
||||
continue
|
||||
}
|
||||
if policies != nil {
|
||||
for _, policy := range policies.AttachedPolicies {
|
||||
ctx.IAM().DetachRolePolicy(&awsiam.DetachRolePolicyInput{
|
||||
RoleName: role.RoleName,
|
||||
PolicyArn: policy.PolicyArn,
|
||||
})
|
||||
}
|
||||
}
|
||||
_, err = ctx.IAM().DeleteRole(&awsiam.DeleteRoleInput{
|
||||
RoleName: role.RoleName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to cleanup role %v: %v\n", role.RoleName, err)
|
||||
} else {
|
||||
cleaned++
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("Cleaned up %v roles\n", cleaned)
|
||||
}
|
169
lib/aws/provider/testutil/provider.go
Normal file
169
lib/aws/provider/testutil/provider.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
structpb "github.com/golang/protobuf/ptypes/struct"
|
||||
"github.com/pulumi/lumi/pkg/resource"
|
||||
"github.com/pulumi/lumi/pkg/tokens"
|
||||
"github.com/pulumi/lumi/sdk/go/pkg/lumirpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
GetResourceID(name string) resource.ID
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Provider lumirpc.ResourceProviderServer
|
||||
Token tokens.Type
|
||||
}
|
||||
|
||||
type ResourceGenerator struct {
|
||||
Name string
|
||||
Creator func(ctx Context) interface{}
|
||||
}
|
||||
|
||||
type Step []ResourceGenerator
|
||||
|
||||
// ProviderTest walks through Create, Update and Delete operations for a collection of resources. The provided
|
||||
// resources map must contain the provider and tokens for each named resource to be created during the test.
|
||||
// Each step of the test can provide values for any subset of the named resources, causeing those resources to
|
||||
// be created or updated as needed. After walking through each step, all of the created resources are deleted.
|
||||
// Check operations are performed on all provided resource inputs during the test.
|
||||
// performs Check operations on each provided resource.
|
||||
func ProviderTest(t *testing.T, resources map[string]Resource, steps []Step) {
|
||||
|
||||
p := &providerTest{
|
||||
resources: resources,
|
||||
ids: map[string]resource.ID{},
|
||||
props: map[string]*structpb.Struct{},
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
for _, res := range step {
|
||||
provider := resources[res.Name].Provider
|
||||
token := resources[res.Name].Token
|
||||
if id, ok := p.ids[res.Name]; !ok {
|
||||
id, props := createResource(t, res.Creator(p), provider, token)
|
||||
p.ids[res.Name] = resource.ID(id)
|
||||
p.props[res.Name] = props
|
||||
if id == "" {
|
||||
t.Fatal("expected to succesfully create resource")
|
||||
}
|
||||
} else {
|
||||
oldProps := p.props[res.Name]
|
||||
ok, props := updateResource(t, string(id), oldProps, res.Creator(p), provider, token)
|
||||
if !ok {
|
||||
t.Fatal("expected to succesfully update resource")
|
||||
}
|
||||
p.props[res.Name] = props
|
||||
}
|
||||
}
|
||||
}
|
||||
for name, id := range p.ids {
|
||||
provider := resources[name].Provider
|
||||
token := resources[name].Token
|
||||
ok := deleteResource(t, string(id), provider, token)
|
||||
if !ok {
|
||||
t.Fatal("expected to succesfully delete resource")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderTestSimple takes a resource provider and array of resource steps and performs a Create, as many Udpates
|
||||
// as neeed, and finally a Delete operation on a single resouce of the given type to walk the resource through the
|
||||
// resource lifecycle. It also performs Check operations on each input state of the resource.
|
||||
func ProviderTestSimple(t *testing.T, provider lumirpc.ResourceProviderServer, token tokens.Type, steps []interface{}) {
|
||||
resources := map[string]Resource{
|
||||
"testResource": Resource{
|
||||
Provider: provider,
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
detailedSteps := []Step{}
|
||||
for _, step := range steps {
|
||||
curStep := step
|
||||
detailedSteps = append(detailedSteps, []ResourceGenerator{
|
||||
{
|
||||
Name: "testResource",
|
||||
Creator: func(ctx Context) interface{} {
|
||||
return curStep
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
ProviderTest(t, resources, detailedSteps)
|
||||
}
|
||||
|
||||
type providerTest struct {
|
||||
resources map[string]Resource
|
||||
ids map[string]resource.ID
|
||||
props map[string]*structpb.Struct
|
||||
}
|
||||
|
||||
func (p *providerTest) GetResourceID(name string) resource.ID {
|
||||
if id, ok := p.ids[name]; ok {
|
||||
return id
|
||||
}
|
||||
return resource.ID("")
|
||||
}
|
||||
|
||||
var _ Context = &providerTest{}
|
||||
|
||||
func createResource(t *testing.T, res interface{}, provider lumirpc.ResourceProviderServer, token tokens.Type) (string, *structpb.Struct) {
|
||||
props := resource.MarshalProperties(nil, resource.NewPropertyMap(res), resource.MarshalOptions{})
|
||||
checkResp, err := provider.Check(nil, &lumirpc.CheckRequest{
|
||||
Type: string(token),
|
||||
Properties: props,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error checking table") {
|
||||
return "", nil
|
||||
}
|
||||
assert.Equal(t, 0, len(checkResp.Failures), "expected no check failures")
|
||||
resp, err := provider.Create(nil, &lumirpc.CreateRequest{
|
||||
Type: string(token),
|
||||
Properties: props,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error creating resource") {
|
||||
return "", nil
|
||||
}
|
||||
if !assert.NotNil(t, resp, "expected a non-nil response") {
|
||||
return "", nil
|
||||
}
|
||||
id := resp.Id
|
||||
return id, props
|
||||
}
|
||||
|
||||
func updateResource(t *testing.T, id string, lastProps *structpb.Struct, res interface{}, provider lumirpc.ResourceProviderServer, token tokens.Type) (bool, *structpb.Struct) {
|
||||
newProps := resource.MarshalProperties(nil, resource.NewPropertyMap(res), resource.MarshalOptions{})
|
||||
checkResp, err := provider.Check(nil, &lumirpc.CheckRequest{
|
||||
Type: string(token),
|
||||
Properties: newProps,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error checking resource") {
|
||||
return false, nil
|
||||
}
|
||||
assert.Equal(t, 0, len(checkResp.Failures), "expected no check failures")
|
||||
_, err = provider.Update(nil, &lumirpc.UpdateRequest{
|
||||
Type: string(token),
|
||||
Id: id,
|
||||
Olds: lastProps,
|
||||
News: newProps,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error creating resource") {
|
||||
return false, nil
|
||||
}
|
||||
return true, newProps
|
||||
}
|
||||
|
||||
func deleteResource(t *testing.T, id string, provider lumirpc.ResourceProviderServer, token tokens.Type) bool {
|
||||
_, err := provider.Delete(nil, &lumirpc.DeleteRequest{
|
||||
Type: string(token),
|
||||
Id: id,
|
||||
})
|
||||
if !assert.NoError(t, err, "expected no error deleting resource") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -182,6 +182,7 @@ type Instance struct {
|
|||
InstanceType *InstanceType `json:"instanceType,omitempty"`
|
||||
SecurityGroups *[]resource.ID `json:"securityGroups,omitempty"`
|
||||
KeyName *string `json:"keyName,omitempty"`
|
||||
Tags *[]Tag `json:"tags,omitempty"`
|
||||
AvailabilityZone string `json:"availabilityZone,omitempty"`
|
||||
PrivateDNSName *string `json:"privateDNSName,omitempty"`
|
||||
PublicDNSName *string `json:"publicDNSName,omitempty"`
|
||||
|
@ -196,6 +197,7 @@ const (
|
|||
Instance_InstanceType = "instanceType"
|
||||
Instance_SecurityGroups = "securityGroups"
|
||||
Instance_KeyName = "keyName"
|
||||
Instance_Tags = "tags"
|
||||
Instance_AvailabilityZone = "availabilityZone"
|
||||
Instance_PrivateDNSName = "privateDNSName"
|
||||
Instance_PublicDNSName = "publicDNSName"
|
||||
|
@ -203,6 +205,20 @@ const (
|
|||
Instance_PublicIP = "publicIP"
|
||||
)
|
||||
|
||||
/* Marshalable Tag structure(s) */
|
||||
|
||||
// Tag is a marshalable representation of its corresponding IDL type.
|
||||
type Tag struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Tag's properties have constants to make dealing with diffs and property bags easier.
|
||||
const (
|
||||
Tag_Key = "key"
|
||||
Tag_Value = "value"
|
||||
)
|
||||
|
||||
/* Typedefs */
|
||||
|
||||
type (
|
||||
|
|
Loading…
Reference in a new issue