From 033c262918e72827659c0100438101771a9f7f26 Mon Sep 17 00:00:00 2001 From: Luke Hoban Date: Tue, 27 Jun 2017 16:50:54 -0700 Subject: [PATCH] Support for AWS SNS resources (#272) Support for AWS SNS Topic and Subscription resources. --- lib/aws/idl/sns/subscription.go | 33 ++++ lib/aws/idl/sns/topic.go | 23 --- lib/aws/pack/cloudwatch/alarm.ts | 5 - lib/aws/pack/sns/index.ts | 1 + lib/aws/pack/sns/subscription.ts | 63 +++++++ lib/aws/pack/sns/topic.ts | 27 --- lib/aws/provider/awsctx/context.go | 10 ++ lib/aws/provider/provider.go | 3 + lib/aws/provider/sns/sns_test.go | 70 ++++++++ lib/aws/provider/sns/subscription.go | 111 ++++++++++++ lib/aws/provider/sns/topic.go | 152 +++++++++++++++++ lib/aws/rpc/cloudwatch/alarm.go | 10 -- lib/aws/rpc/sns/subscription.go | 242 +++++++++++++++++++++++++++ lib/aws/rpc/sns/topic.go | 41 ----- 14 files changed, 685 insertions(+), 106 deletions(-) create mode 100644 lib/aws/idl/sns/subscription.go create mode 100644 lib/aws/pack/sns/subscription.ts create mode 100644 lib/aws/provider/sns/sns_test.go create mode 100644 lib/aws/provider/sns/subscription.go create mode 100644 lib/aws/provider/sns/topic.go create mode 100644 lib/aws/rpc/sns/subscription.go diff --git a/lib/aws/idl/sns/subscription.go b/lib/aws/idl/sns/subscription.go new file mode 100644 index 000000000..80e87c211 --- /dev/null +++ b/lib/aws/idl/sns/subscription.go @@ -0,0 +1,33 @@ +// Copyright 2016-2017, Pulumi Corporation. All rights reserved. + +package sns + +import ( + "github.com/pulumi/lumi/pkg/resource/idl" +) + +// An Amazon Simple Notification Service (Amazon SNS) topic subscription. For more information, see +// http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-subscription.html. +type Subscription struct { + idl.NamedResource + // A name for the topic. If you don't specify a name, a unique physical ID will be generated. + Topic *Topic `lumi:"topic,replaces"` + // The subscription's protocol. + Protocol Protocol `lumi:"protocol,replaces"` + // The subscription's endpoint (format depends on the protocol). + Endpoint string `lumi:"endpoint,replaces"` +} + +// The protocols supported by the Amazon Simple Notification Service (Amazon SNS). +type Protocol string + +const ( + HTTSubscription Protocol = "http" // delivery of JSON-encoded message via HTTP POST. + HTTPSSubscription Protocol = "https" // delivery of JSON-encoded message via HTTPS POST. + EmailSubscription Protocol = "email" // delivery of message via SMTP. + EmailJSONSubscription Protocol = "email-json" // delivery of JSON-encoded message via SMTP. + SMSSubscription Protocol = "sms" // delivery of message via SMS. + SQSSubscription Protocol = "sqs" // delivery of JSON-encoded message to an Amazon SQS queue. + ApplicationSubscription Protocol = "application" // delivery of JSON-encoded message to a mobile app or device. + LambdaSubscription Protocol = "lambda" // delivery of JSON-encoded message to an AWS Lambda function. +) diff --git a/lib/aws/idl/sns/topic.go b/lib/aws/idl/sns/topic.go index d97bce7f5..528c7d189 100644 --- a/lib/aws/idl/sns/topic.go +++ b/lib/aws/idl/sns/topic.go @@ -14,27 +14,4 @@ type Topic struct { TopicName *string `lumi:"topicName,replaces,optional"` // A developer-defined string that can be used to identify this SNS topic. DisplayName *string `lumi:"displayName,optional"` - // The SNS subscriptions (endpoints) for this topic. - Subscription *[]TopicSubscription `lumi:"subscription,optional"` } - -type TopicSubscription struct { - // The subscription's protocol. - Protocol TopicProtocol `lumi:"protocol"` - // The subscription's endpoint (format depends on the protocol). - Endpoint string `lumi:"endpoint"` -} - -// The protocols supported by the Amazon Simple Notification Service (Amazon SNS). -type TopicProtocol string - -const ( - HTTPTopic TopicProtocol = "http" // delivery of JSON-encoded message via HTTP POST. - HTTPSTopic TopicProtocol = "https" // delivery of JSON-encoded message via HTTPS POST. - EmailTopic TopicProtocol = "email" // delivery of message via SMTP. - EmailJSONTopic TopicProtocol = "email-json" // delivery of JSON-encoded message via SMTP. - SMSTopic TopicProtocol = "sms" // delivery of message via SMS. - SQSTopic TopicProtocol = "sqs" // delivery of JSON-encoded message to an Amazon SQS queue. - ApplicationTopic TopicProtocol = "application" // delivery of JSON-encoded message to a mobile app or device. - LambdaTopic TopicProtocol = "lambda" // delivery of JSON-encoded message to an AWS Lambda function. -) diff --git a/lib/aws/pack/cloudwatch/alarm.ts b/lib/aws/pack/cloudwatch/alarm.ts index fb8d0df1b..744157e42 100644 --- a/lib/aws/pack/cloudwatch/alarm.ts +++ b/lib/aws/pack/cloudwatch/alarm.ts @@ -4,8 +4,6 @@ /* tslint:disable:ordered-imports variable-name */ import * as lumi from "@lumi/lumi"; -import {TopicSubscription} from "../sns/topic"; - export let AverageStatistic: AlarmStatistic = "Average"; export let BitsMetric: AlarmMetric = "Bits"; export let BitsPerSecondMetric: AlarmMetric = "Bits/Second"; @@ -46,7 +44,6 @@ export let ThresholdLessThanOrEqualTo: AlarmComparisonOperator = "LessThanOrEqua export class ActionTarget extends lumi.NamedResource implements ActionTargetArgs { public readonly topicName?: string; public displayName?: string; - public subscription?: TopicSubscription[]; public static get(id: lumi.ID): ActionTarget { return undefined; // functionality provided by the runtime @@ -61,7 +58,6 @@ export class ActionTarget extends lumi.NamedResource implements ActionTargetArgs if (args !== undefined) { this.topicName = args.topicName; this.displayName = args.displayName; - this.subscription = args.subscription; } } } @@ -69,7 +65,6 @@ export class ActionTarget extends lumi.NamedResource implements ActionTargetArgs export interface ActionTargetArgs { readonly topicName?: string; displayName?: string; - subscription?: TopicSubscription[]; } export class Alarm extends lumi.NamedResource implements AlarmArgs { diff --git a/lib/aws/pack/sns/index.ts b/lib/aws/pack/sns/index.ts index 4316619af..ce44b5985 100644 --- a/lib/aws/pack/sns/index.ts +++ b/lib/aws/pack/sns/index.ts @@ -1,4 +1,5 @@ // Copyright 2016-2017, Pulumi Corporation. All rights reserved. +export * from "./subscription"; export * from "./topic"; diff --git a/lib/aws/pack/sns/subscription.ts b/lib/aws/pack/sns/subscription.ts new file mode 100644 index 000000000..720894b9a --- /dev/null +++ b/lib/aws/pack/sns/subscription.ts @@ -0,0 +1,63 @@ +// *** WARNING: this file was generated by the Lumi IDL Compiler (LUMIDL). *** +// *** Do not edit by hand unless you're certain you know what you are doing! *** + +/* tslint:disable:ordered-imports variable-name */ +import * as lumi from "@lumi/lumi"; + +import {Topic} from "./topic"; + +export let ApplicationSubscription: Protocol = "application"; +export let EmailJSONSubscription: Protocol = "email-json"; +export let EmailSubscription: Protocol = "email"; +export let HTTPSSubscription: Protocol = "https"; +export let HTTSubscription: Protocol = "http"; +export let LambdaSubscription: Protocol = "lambda"; +export let SMSSubscription: Protocol = "sms"; +export let SQSSubscription: Protocol = "sqs"; + +export type Protocol = + "application" | + "email-json" | + "email" | + "https" | + "http" | + "lambda" | + "sms" | + "sqs"; + +export class Subscription extends lumi.NamedResource implements SubscriptionArgs { + public readonly topic: Topic; + public readonly protocol: Protocol; + public readonly endpoint: string; + + public static get(id: lumi.ID): Subscription { + return undefined; // functionality provided by the runtime + } + + public static query(q: any): Subscription[] { + return undefined; // functionality provided by the runtime + } + + constructor(name: string, args: SubscriptionArgs) { + super(name); + if (args.topic === undefined) { + throw new Error("Missing required argument 'topic'"); + } + this.topic = args.topic; + if (args.protocol === undefined) { + throw new Error("Missing required argument 'protocol'"); + } + this.protocol = args.protocol; + if (args.endpoint === undefined) { + throw new Error("Missing required argument 'endpoint'"); + } + this.endpoint = args.endpoint; + } +} + +export interface SubscriptionArgs { + readonly topic: Topic; + readonly protocol: Protocol; + readonly endpoint: string; +} + diff --git a/lib/aws/pack/sns/topic.ts b/lib/aws/pack/sns/topic.ts index a9a756165..c240c92e6 100644 --- a/lib/aws/pack/sns/topic.ts +++ b/lib/aws/pack/sns/topic.ts @@ -4,19 +4,9 @@ /* tslint:disable:ordered-imports variable-name */ import * as lumi from "@lumi/lumi"; -export let ApplicationTopic: TopicProtocol = "application"; -export let EmailJSONTopic: TopicProtocol = "email-json"; -export let EmailTopic: TopicProtocol = "email"; -export let HTTPSTopic: TopicProtocol = "https"; -export let HTTPTopic: TopicProtocol = "http"; -export let LambdaTopic: TopicProtocol = "lambda"; -export let SMSTopic: TopicProtocol = "sms"; -export let SQSTopic: TopicProtocol = "sqs"; - export class Topic extends lumi.NamedResource implements TopicArgs { public readonly topicName?: string; public displayName?: string; - public subscription?: TopicSubscription[]; public static get(id: lumi.ID): Topic { return undefined; // functionality provided by the runtime @@ -31,7 +21,6 @@ export class Topic extends lumi.NamedResource implements TopicArgs { if (args !== undefined) { this.topicName = args.topicName; this.displayName = args.displayName; - this.subscription = args.subscription; } } } @@ -39,21 +28,5 @@ export class Topic extends lumi.NamedResource implements TopicArgs { export interface TopicArgs { readonly topicName?: string; displayName?: string; - subscription?: TopicSubscription[]; -} - -export type TopicProtocol = - "application" | - "email-json" | - "email" | - "https" | - "http" | - "lambda" | - "sms" | - "sqs"; - -export interface TopicSubscription { - protocol: TopicProtocol; - endpoint: string; } diff --git a/lib/aws/provider/awsctx/context.go b/lib/aws/provider/awsctx/context.go index 468e7267b..0b38959fb 100644 --- a/lib/aws/provider/awsctx/context.go +++ b/lib/aws/provider/awsctx/context.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/lambda" "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/sns" "github.com/golang/glog" "github.com/pkg/errors" "github.com/pulumi/lumi/pkg/resource/provider" @@ -38,6 +39,7 @@ type Context struct { iam *iam.IAM lambda *lambda.Lambda s3 *s3.S3 + sns *sns.SNS } const regionConfig = "aws:config:region" @@ -152,6 +154,14 @@ func (ctx *Context) S3() *s3.S3 { return ctx.s3 } +func (ctx *Context) SNS() *sns.SNS { + contract.Assert(ctx.sess != nil) + if ctx.sns == nil { + ctx.sns = sns.New(ctx.sess) + } + return ctx.sns +} + // Request manufactures a standard Golang context object for a request within this overall AWS context. func (ctx *Context) Request() context.Context { // IDEA: unify this with the gRPC context; this will be easier once gRPC moves to the standard Golang context. diff --git a/lib/aws/provider/provider.go b/lib/aws/provider/provider.go index ccbe27634..a031a53af 100644 --- a/lib/aws/provider/provider.go +++ b/lib/aws/provider/provider.go @@ -19,6 +19,7 @@ import ( "github.com/pulumi/lumi/lib/aws/provider/iam" "github.com/pulumi/lumi/lib/aws/provider/lambda" "github.com/pulumi/lumi/lib/aws/provider/s3" + "github.com/pulumi/lumi/lib/aws/provider/sns" ) // Provider implements the AWS resource provider's operations for all known AWS types. @@ -49,6 +50,8 @@ func NewProvider(host *provider.HostClient) (*Provider, error) { iam.RoleToken: iam.NewRoleProvider(ctx), s3.BucketToken: s3.NewBucketProvider(ctx), s3.ObjectToken: s3.NewObjectProvider(ctx), + sns.TopicToken: sns.NewTopicProvider(ctx), + sns.SubscriptionToken: sns.NewSubscriptionProvider(ctx), }, }, nil } diff --git a/lib/aws/provider/sns/sns_test.go b/lib/aws/provider/sns/sns_test.go new file mode 100644 index 000000000..362ddf548 --- /dev/null +++ b/lib/aws/provider/sns/sns_test.go @@ -0,0 +1,70 @@ +// Copyright 2016-2017, Pulumi Corporation. All rights reserved. + +package sns + +import ( + "fmt" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws" + awssns "github.com/aws/aws-sdk-go/service/sns" + "github.com/pulumi/lumi/lib/aws/provider/awsctx" + "github.com/pulumi/lumi/lib/aws/provider/testutil" + "github.com/pulumi/lumi/lib/aws/rpc/sns" + "github.com/pulumi/lumi/pkg/resource" + "github.com/stretchr/testify/assert" +) + +func Test(t *testing.T) { + t.Parallel() + + prefix := resource.NewUniqueHex("lumitest", 20, 20) + ctx := testutil.CreateContext(t) + defer func() { + err := cleanupTopics(prefix, ctx) + assert.Nil(t, err) + }() + + resources := map[string]testutil.Resource{ + "topic": {Provider: NewTopicProvider(ctx), Token: TopicToken}, + } + steps := []testutil.Step{ + { + testutil.ResourceGenerator{ + Name: "topic", + Creator: func(ctx testutil.Context) interface{} { + return &sns.Topic{ + Name: aws.String(prefix), + DisplayName: aws.String(prefix), + } + }, + }, + }, + } + + props := testutil.ProviderTest(t, resources, steps) + assert.NotNil(t, props) +} + +func cleanupTopics(prefix string, ctx *awsctx.Context) error { + fmt.Printf("Cleaning up topic with name:%v\n", prefix) + list, err := ctx.SNS().ListTopics(&awssns.ListTopicsInput{}) + if err != nil { + return err + } + cleaned := 0 + for _, topic := range list.Topics { + if strings.Contains(aws.StringValue(topic.TopicArn), prefix) { + if _, delerr := ctx.SNS().DeleteTopic(&awssns.DeleteTopicInput{ + TopicArn: topic.TopicArn, + }); delerr != nil { + fmt.Printf("Unable to cleanup topic %v: %v\n", topic.TopicArn, delerr) + return delerr + } + cleaned++ + } + } + fmt.Printf("Cleaned up %v topics\n", cleaned) + return nil +} diff --git a/lib/aws/provider/sns/subscription.go b/lib/aws/provider/sns/subscription.go new file mode 100644 index 000000000..830c5dbe4 --- /dev/null +++ b/lib/aws/provider/sns/subscription.go @@ -0,0 +1,111 @@ +// Copyright 2016-2017, Pulumi Corporation. All rights reserved. + +package sns + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + awssns "github.com/aws/aws-sdk-go/service/sns" + "github.com/pulumi/lumi/pkg/resource" + "github.com/pulumi/lumi/pkg/util/contract" + "github.com/pulumi/lumi/sdk/go/pkg/lumirpc" + "golang.org/x/net/context" + + "github.com/pulumi/lumi/lib/aws/provider/arn" + "github.com/pulumi/lumi/lib/aws/provider/awsctx" + "github.com/pulumi/lumi/lib/aws/rpc/sns" +) + +const SubscriptionToken = sns.SubscriptionToken + +// NewSubscriptionProvider creates a provider that handles SNS subscription operations. +func NewSubscriptionProvider(ctx *awsctx.Context) lumirpc.ResourceProviderServer { + ops := &subscriptionProvider{ctx} + return sns.NewSubscriptionProvider(ops) +} + +type subscriptionProvider struct { + ctx *awsctx.Context +} + +// Check validates that the given property bag is valid for a resource of the given type. +func (p *subscriptionProvider) Check(ctx context.Context, obj *sns.Subscription, property string) error { + return 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 *subscriptionProvider) Create(ctx context.Context, obj *sns.Subscription) (resource.ID, error) { + topicName, err := arn.ParseResourceName(obj.Topic) + if err != nil { + return "", err + } + fmt.Printf("Creating SNS Subscription on topic '%v'\n", topicName) + create := &awssns.SubscribeInput{ + TopicArn: aws.String(string(obj.Topic)), + Endpoint: aws.String(obj.Endpoint), + Protocol: aws.String(string(obj.Protocol)), + } + resp, err := p.ctx.SNS().Subscribe(create) + if err != nil { + return "", err + } + contract.Assert(resp != nil) + contract.Assert(resp.SubscriptionArn != nil) + return resource.ID(*resp.SubscriptionArn), nil +} + +// Get reads the instance state identified by ID, returning a populated resource object, or an error if not found. +func (p *subscriptionProvider) Get(ctx context.Context, id resource.ID) (*sns.Subscription, error) { + resp, err := p.ctx.SNS().GetSubscriptionAttributes(&awssns.GetSubscriptionAttributesInput{ + SubscriptionArn: aws.String(string(id)), + }) + if err != nil { + return nil, err + } + listResp, err := p.ctx.SNS().ListSubscriptionsByTopic(&awssns.ListSubscriptionsByTopicInput{ + TopicArn: resp.Attributes["TopicArn"], + }) + if err != nil { + return nil, err + } + var subscription *awssns.Subscription + for _, s := range listResp.Subscriptions { + if *s.SubscriptionArn == string(id) { + subscription = s + } + } + return &sns.Subscription{ + Topic: resource.ID(aws.StringValue(resp.Attributes["TopicArn"])), + Endpoint: aws.StringValue(subscription.Endpoint), + Protocol: sns.Protocol(aws.StringValue(subscription.Protocol)), + }, nil +} + +// InspectChange checks what impacts a hypothetical update will have on the resource's properties. +func (p *subscriptionProvider) InspectChange(ctx context.Context, id resource.ID, + old *sns.Subscription, new *sns.Subscription, diff *resource.ObjectDiff) ([]string, error) { + return nil, nil +} + +// 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 *subscriptionProvider) Update(ctx context.Context, id resource.ID, + old *sns.Subscription, new *sns.Subscription, diff *resource.ObjectDiff) error { + contract.Failf("No updatable properties on SNS Subscription") + return nil +} + +// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist. +func (p *subscriptionProvider) Delete(ctx context.Context, id resource.ID) error { + name, err := arn.ParseResourceName(id) + if err != nil { + return err + } + fmt.Printf("Deleting SNS Subscription '%v'\n", name) + _, err = p.ctx.SNS().Unsubscribe(&awssns.UnsubscribeInput{ + SubscriptionArn: id.StringPtr(), + }) + return err +} diff --git a/lib/aws/provider/sns/topic.go b/lib/aws/provider/sns/topic.go new file mode 100644 index 000000000..f4a6a06a7 --- /dev/null +++ b/lib/aws/provider/sns/topic.go @@ -0,0 +1,152 @@ +// Copyright 2016-2017, Pulumi Corporation. All rights reserved. + +package sns + +import ( + "crypto/sha1" + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + awssns "github.com/aws/aws-sdk-go/service/sns" + "github.com/pulumi/lumi/pkg/resource" + "github.com/pulumi/lumi/sdk/go/pkg/lumirpc" + "golang.org/x/net/context" + + "github.com/pulumi/lumi/lib/aws/provider/arn" + "github.com/pulumi/lumi/lib/aws/provider/awsctx" + "github.com/pulumi/lumi/lib/aws/rpc/sns" + "github.com/pulumi/lumi/pkg/util/contract" +) + +const TopicToken = sns.TopicToken + +// constants for the various topic limits. +const ( + minTopicName = 1 + maxTopicName = 256 + displayNameAttributeName = "DisplayName" +) + +var ( + topicNameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_\-]*$`) + topicNameDisallowedRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-]`) +) + +// NewTopicProvider creates a provider that handles SNS topic operations. +func NewTopicProvider(ctx *awsctx.Context) lumirpc.ResourceProviderServer { + ops := &topicProvider{ctx} + return sns.NewTopicProvider(ops) +} + +type topicProvider struct { + ctx *awsctx.Context +} + +// Check validates that the given property bag is valid for a resource of the given type. +func (p *topicProvider) Check(ctx context.Context, obj *sns.Topic, property string) error { + switch property { + case sns.Topic_TopicName: + if name := obj.TopicName; name != nil { + if matched := topicNameRegexp.MatchString(*name); !matched { + fmt.Printf("Failed to match regexp\n") + return fmt.Errorf("did not match regexp %v", topicNameRegexp) + } else if len(*name) < minTopicName { + return fmt.Errorf("less than minimum length of %v", minTopicName) + } else if len(*name) > maxTopicName { + return fmt.Errorf("exceeded maximum length of %v", maxTopicName) + } + } + } + return 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 *topicProvider) Create(ctx context.Context, obj *sns.Topic) (resource.ID, error) { + // If an explicit name is given, use it. Otherwise, auto-generate a name in part based on the resource name. + var name string + if obj.TopicName != nil { + name = *obj.TopicName + } else { + // SNS topic names have strict naming requirements. To use the Name property as a prefix, we + // need to convert it to a safe form first. + safeName := topicNameDisallowedRegexp.ReplaceAllString(*obj.Name, "-") + name = resource.NewUniqueHex(safeName+"-", maxTopicName, sha1.Size) + } + fmt.Printf("Creating SNS Topic '%v' with name '%v'\n", *obj.Name, name) + create := &awssns.CreateTopicInput{ + Name: aws.String(name), + } + resp, err := p.ctx.SNS().CreateTopic(create) + if err != nil { + return "", err + } + contract.Assert(resp != nil) + contract.Assert(resp.TopicArn != nil) + if obj.DisplayName != nil { + _, err := p.ctx.SNS().SetTopicAttributes(&awssns.SetTopicAttributesInput{ + TopicArn: resp.TopicArn, + AttributeName: aws.String(displayNameAttributeName), + AttributeValue: obj.DisplayName, + }) + if err != nil { + return "", err + } + } + return resource.ID(*resp.TopicArn), nil +} + +// Get reads the instance state identified by ID, returning a populated resource object, or an error if not found. +func (p *topicProvider) Get(ctx context.Context, id resource.ID) (*sns.Topic, error) { + name, err := arn.ParseResourceName(id) + if err != nil { + return nil, err + } + resp, err := p.ctx.SNS().GetTopicAttributes(&awssns.GetTopicAttributesInput{ + TopicArn: aws.String(string(id)), + }) + if err != nil { + return nil, err + } + return &sns.Topic{ + TopicName: &name, + DisplayName: resp.Attributes[displayNameAttributeName], + }, nil +} + +// InspectChange checks what impacts a hypothetical update will have on the resource's properties. +func (p *topicProvider) InspectChange(ctx context.Context, id resource.ID, + old *sns.Topic, new *sns.Topic, diff *resource.ObjectDiff) ([]string, error) { + return nil, nil +} + +// 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 *topicProvider) Update(ctx context.Context, id resource.ID, + old *sns.Topic, new *sns.Topic, diff *resource.ObjectDiff) error { + if diff.Changed(sns.Topic_DisplayName) { + _, err := p.ctx.SNS().SetTopicAttributes(&awssns.SetTopicAttributesInput{ + TopicArn: aws.String(string(id)), + AttributeName: aws.String(displayNameAttributeName), + AttributeValue: new.DisplayName, + }) + 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. +func (p *topicProvider) Delete(ctx context.Context, id resource.ID) error { + name, err := arn.ParseResourceName(id) + if err != nil { + return err + } + fmt.Printf("Deleting SNS Topic '%v'\n", name) + _, err = p.ctx.SNS().DeleteTopic(&awssns.DeleteTopicInput{ + TopicArn: id.StringPtr(), + }) + return err +} diff --git a/lib/aws/rpc/cloudwatch/alarm.go b/lib/aws/rpc/cloudwatch/alarm.go index dd035531c..2445ad001 100644 --- a/lib/aws/rpc/cloudwatch/alarm.go +++ b/lib/aws/rpc/cloudwatch/alarm.go @@ -16,8 +16,6 @@ import ( "github.com/pulumi/lumi/pkg/util/contract" "github.com/pulumi/lumi/pkg/util/mapper" "github.com/pulumi/lumi/sdk/go/pkg/lumirpc" - - __sns "github.com/pulumi/lumi/lib/aws/rpc/sns" ) /* RPC stubs for ActionTarget resource provider */ @@ -78,12 +76,6 @@ func (p *ActionTargetProvider) Check( resource.NewPropertyError("ActionTarget", "displayName", failure)) } } - if !unks["subscription"] { - if failure := p.ops.Check(ctx, obj, "subscription"); failure != nil { - failures = append(failures, - resource.NewPropertyError("ActionTarget", "subscription", failure)) - } - } if len(failures) > 0 { return plugin.NewCheckResponse(resource.NewErrors(failures)), nil } @@ -208,7 +200,6 @@ type ActionTarget struct { Name *string `lumi:"name,optional"` TopicName *string `lumi:"topicName,optional"` DisplayName *string `lumi:"displayName,optional"` - Subscription *[]__sns.TopicSubscription `lumi:"subscription,optional"` } // ActionTarget's properties have constants to make dealing with diffs and property bags easier. @@ -216,7 +207,6 @@ const ( ActionTarget_Name = "name" ActionTarget_TopicName = "topicName" ActionTarget_DisplayName = "displayName" - ActionTarget_Subscription = "subscription" ) /* RPC stubs for Alarm resource provider */ diff --git a/lib/aws/rpc/sns/subscription.go b/lib/aws/rpc/sns/subscription.go new file mode 100644 index 000000000..85d24752a --- /dev/null +++ b/lib/aws/rpc/sns/subscription.go @@ -0,0 +1,242 @@ +// *** WARNING: this file was generated by the Lumi IDL Compiler (LUMIDL). *** +// *** Do not edit by hand unless you're certain you know what you are doing! *** + +package sns + +import ( + "errors" + + pbempty "github.com/golang/protobuf/ptypes/empty" + pbstruct "github.com/golang/protobuf/ptypes/struct" + "golang.org/x/net/context" + + "github.com/pulumi/lumi/pkg/resource" + "github.com/pulumi/lumi/pkg/resource/plugin" + "github.com/pulumi/lumi/pkg/tokens" + "github.com/pulumi/lumi/pkg/util/contract" + "github.com/pulumi/lumi/pkg/util/mapper" + "github.com/pulumi/lumi/sdk/go/pkg/lumirpc" +) + +/* RPC stubs for Subscription resource provider */ + +// SubscriptionToken is the type token corresponding to the Subscription package type. +const SubscriptionToken = tokens.Type("aws:sns/subscription:Subscription") + +// SubscriptionProviderOps is a pluggable interface for Subscription-related management functionality. +type SubscriptionProviderOps interface { + Check(ctx context.Context, obj *Subscription, property string) error + Create(ctx context.Context, obj *Subscription) (resource.ID, error) + Get(ctx context.Context, id resource.ID) (*Subscription, error) + InspectChange(ctx context.Context, + id resource.ID, old *Subscription, new *Subscription, diff *resource.ObjectDiff) ([]string, error) + Update(ctx context.Context, + id resource.ID, old *Subscription, new *Subscription, diff *resource.ObjectDiff) error + Delete(ctx context.Context, id resource.ID) error +} + +// SubscriptionProvider is a dynamic gRPC-based plugin for managing Subscription resources. +type SubscriptionProvider struct { + ops SubscriptionProviderOps +} + +// NewSubscriptionProvider allocates a resource provider that delegates to a ops instance. +func NewSubscriptionProvider(ops SubscriptionProviderOps) lumirpc.ResourceProviderServer { + contract.Assert(ops != nil) + return &SubscriptionProvider{ops: ops} +} + +func (p *SubscriptionProvider) Check( + ctx context.Context, req *lumirpc.CheckRequest) (*lumirpc.CheckResponse, error) { + contract.Assert(req.GetType() == string(SubscriptionToken)) + obj, _, err := p.Unmarshal(req.GetProperties()) + if err != nil { + return plugin.NewCheckResponse(err), nil + } + var failures []error + unks := req.GetUnknowns() + if !unks["name"] { + if failure := p.ops.Check(ctx, obj, "name"); failure != nil { + failures = append(failures, + resource.NewPropertyError("Subscription", "name", failure)) + } + } + if !unks["topic"] { + if failure := p.ops.Check(ctx, obj, "topic"); failure != nil { + failures = append(failures, + resource.NewPropertyError("Subscription", "topic", failure)) + } + } + if !unks["protocol"] { + if failure := p.ops.Check(ctx, obj, "protocol"); failure != nil { + failures = append(failures, + resource.NewPropertyError("Subscription", "protocol", failure)) + } + } + if !unks["endpoint"] { + if failure := p.ops.Check(ctx, obj, "endpoint"); failure != nil { + failures = append(failures, + resource.NewPropertyError("Subscription", "endpoint", failure)) + } + } + if len(failures) > 0 { + return plugin.NewCheckResponse(resource.NewErrors(failures)), nil + } + return plugin.NewCheckResponse(nil), nil +} + +func (p *SubscriptionProvider) Name( + ctx context.Context, req *lumirpc.NameRequest) (*lumirpc.NameResponse, error) { + contract.Assert(req.GetType() == string(SubscriptionToken)) + obj, _, err := p.Unmarshal(req.GetProperties()) + if err != nil { + return nil, err + } + if obj.Name == nil || *obj.Name == "" { + if req.Unknowns[Subscription_Name] { + return nil, errors.New("Name property cannot be computed from unknown outputs") + } + return nil, errors.New("Name property cannot be empty") + } + return &lumirpc.NameResponse{Name: *obj.Name}, nil +} + +func (p *SubscriptionProvider) Create( + ctx context.Context, req *lumirpc.CreateRequest) (*lumirpc.CreateResponse, error) { + contract.Assert(req.GetType() == string(SubscriptionToken)) + obj, _, err := p.Unmarshal(req.GetProperties()) + if err != nil { + return nil, err + } + id, err := p.ops.Create(ctx, obj) + if err != nil { + return nil, err + } + return &lumirpc.CreateResponse{Id: string(id)}, nil +} + +func (p *SubscriptionProvider) Get( + ctx context.Context, req *lumirpc.GetRequest) (*lumirpc.GetResponse, error) { + contract.Assert(req.GetType() == string(SubscriptionToken)) + id := resource.ID(req.GetId()) + obj, err := p.ops.Get(ctx, id) + if err != nil { + return nil, err + } + return &lumirpc.GetResponse{ + Properties: plugin.MarshalProperties( + nil, resource.NewPropertyMap(obj), plugin.MarshalOptions{}), + }, nil +} + +func (p *SubscriptionProvider) InspectChange( + ctx context.Context, req *lumirpc.InspectChangeRequest) (*lumirpc.InspectChangeResponse, error) { + contract.Assert(req.GetType() == string(SubscriptionToken)) + id := resource.ID(req.GetId()) + old, oldprops, err := p.Unmarshal(req.GetOlds()) + if err != nil { + return nil, err + } + new, newprops, err := p.Unmarshal(req.GetNews()) + if err != nil { + return nil, err + } + var replaces []string + diff := oldprops.Diff(newprops) + if diff != nil { + if diff.Changed("name") { + replaces = append(replaces, "name") + } + if diff.Changed("topic") { + replaces = append(replaces, "topic") + } + if diff.Changed("protocol") { + replaces = append(replaces, "protocol") + } + if diff.Changed("endpoint") { + replaces = append(replaces, "endpoint") + } + } + more, err := p.ops.InspectChange(ctx, id, old, new, diff) + if err != nil { + return nil, err + } + return &lumirpc.InspectChangeResponse{ + Replaces: append(replaces, more...), + }, err +} + +func (p *SubscriptionProvider) Update( + ctx context.Context, req *lumirpc.UpdateRequest) (*pbempty.Empty, error) { + contract.Assert(req.GetType() == string(SubscriptionToken)) + id := resource.ID(req.GetId()) + old, oldprops, err := p.Unmarshal(req.GetOlds()) + if err != nil { + return nil, err + } + new, newprops, err := p.Unmarshal(req.GetNews()) + if err != nil { + return nil, err + } + diff := oldprops.Diff(newprops) + if err := p.ops.Update(ctx, id, old, new, diff); err != nil { + return nil, err + } + return &pbempty.Empty{}, nil +} + +func (p *SubscriptionProvider) Delete( + ctx context.Context, req *lumirpc.DeleteRequest) (*pbempty.Empty, error) { + contract.Assert(req.GetType() == string(SubscriptionToken)) + id := resource.ID(req.GetId()) + if err := p.ops.Delete(ctx, id); err != nil { + return nil, err + } + return &pbempty.Empty{}, nil +} + +func (p *SubscriptionProvider) Unmarshal( + v *pbstruct.Struct) (*Subscription, resource.PropertyMap, error) { + var obj Subscription + props := plugin.UnmarshalProperties(nil, v, plugin.MarshalOptions{RawResources: true}) + return &obj, props, mapper.MapIU(props.Mappable(), &obj) +} + +/* Marshalable Subscription structure(s) */ + +// Subscription is a marshalable representation of its corresponding IDL type. +type Subscription struct { + Name *string `lumi:"name,optional"` + Topic resource.ID `lumi:"topic"` + Protocol Protocol `lumi:"protocol"` + Endpoint string `lumi:"endpoint"` +} + +// Subscription's properties have constants to make dealing with diffs and property bags easier. +const ( + Subscription_Name = "name" + Subscription_Topic = "topic" + Subscription_Protocol = "protocol" + Subscription_Endpoint = "endpoint" +) + +/* Typedefs */ + +type ( + Protocol string +) + +/* Constants */ + +const ( + ApplicationSubscription Protocol = "application" + EmailJSONSubscription Protocol = "email-json" + EmailSubscription Protocol = "email" + HTTPSSubscription Protocol = "https" + HTTSubscription Protocol = "http" + LambdaSubscription Protocol = "lambda" + SMSSubscription Protocol = "sms" + SQSSubscription Protocol = "sqs" +) + + diff --git a/lib/aws/rpc/sns/topic.go b/lib/aws/rpc/sns/topic.go index 7d0a37180..37f7a1782 100644 --- a/lib/aws/rpc/sns/topic.go +++ b/lib/aws/rpc/sns/topic.go @@ -76,12 +76,6 @@ func (p *TopicProvider) Check( resource.NewPropertyError("Topic", "displayName", failure)) } } - if !unks["subscription"] { - if failure := p.ops.Check(ctx, obj, "subscription"); failure != nil { - failures = append(failures, - resource.NewPropertyError("Topic", "subscription", failure)) - } - } if len(failures) > 0 { return plugin.NewCheckResponse(resource.NewErrors(failures)), nil } @@ -206,7 +200,6 @@ type Topic struct { Name *string `lumi:"name,optional"` TopicName *string `lumi:"topicName,optional"` DisplayName *string `lumi:"displayName,optional"` - Subscription *[]TopicSubscription `lumi:"subscription,optional"` } // Topic's properties have constants to make dealing with diffs and property bags easier. @@ -214,40 +207,6 @@ const ( Topic_Name = "name" Topic_TopicName = "topicName" Topic_DisplayName = "displayName" - Topic_Subscription = "subscription" -) - -/* Marshalable TopicSubscription structure(s) */ - -// TopicSubscription is a marshalable representation of its corresponding IDL type. -type TopicSubscription struct { - Protocol TopicProtocol `lumi:"protocol"` - Endpoint string `lumi:"endpoint"` -} - -// TopicSubscription's properties have constants to make dealing with diffs and property bags easier. -const ( - TopicSubscription_Protocol = "protocol" - TopicSubscription_Endpoint = "endpoint" -) - -/* Typedefs */ - -type ( - TopicProtocol string -) - -/* Constants */ - -const ( - ApplicationTopic TopicProtocol = "application" - EmailJSONTopic TopicProtocol = "email-json" - EmailTopic TopicProtocol = "email" - HTTPSTopic TopicProtocol = "https" - HTTPTopic TopicProtocol = "http" - LambdaTopic TopicProtocol = "lambda" - SMSTopic TopicProtocol = "sms" - SQSTopic TopicProtocol = "sqs" )