Restructure everything and automate deployment. Fix nasty Javascript.

This commit is contained in:
Empathic Qubit 2020-06-18 20:14:57 -04:00
parent c718808aee
commit 1de3a78cc2
22 changed files with 2081 additions and 1061 deletions

3
.gitignore vendored
View file

@ -27,5 +27,6 @@ build/Release
node_modules node_modules
# Other local things to ignore # Other local things to ignore
cloudformation-helpers.zip
.DS_Store .DS_Store
/out

View file

@ -1,134 +0,0 @@
var Promise = require('bluebird'),
AWS = require('aws-sdk'),
base = require('lib/base'),
apiGateway = Promise.promisifyAll(new AWS.APIGateway());
// Exposes the SNS.subscribe API method
function CreateApi(event, context) {
base.Handler.call(this, event, context);
}
CreateApi.prototype = Object.create(base.Handler.prototype);
CreateApi.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
var rootObject = this;
return apiGateway.createRestApiAsync({
name: p.name,
description: p.description
})
.then(function(apiData) {
return rootObject.setReferenceData({ restApiId: apiData.id }) // Set this immediately, in case later calls fail
.then(function() {
return apiGateway.getResourcesAsync({
restApiId: apiData.id
})
.then(function(resourceData) {
return setupEndpoints(p.endpoints, resourceData.items[0].id, apiData.id)
.then(function(endpointsData) {
return apiGateway.createDeploymentAsync({
restApiId: apiData.id,
stageName: p.version
})
.then(function(deploymentData) {
// Total hack: there are limits to the number of API Gateway API calls you can make. So this function
// will fail if a CloudFormation template includes two or more APIs. Attempting to avoid this by blocking.
var until = new Date();
until.setSeconds(until.getSeconds() + 60);
while (new Date() < until) {
// Wait...
}
// AWS.config.region is a bit of a hack, but I can't figure out how else to dynamically
// detect the region of the API - seems to be nothing in API Gateway or AWS Lambda context.
// Could possibly get it from the CloudFormation stack, but that seems wrong.
return {
baseUrl: "https://" + apiData.id + ".execute-api." + AWS.config.region + ".amazonaws.com/" + p.version,
restApiId: apiData.id
};
});
});
});
});
});
}
CreateApi.prototype.handleDelete = function(referenceData) {
return Promise.try(function() {
if (referenceData && referenceData.restApiId) {
// Can simply delete the entire API - don't need to delete each individual component
return apiGateway.deleteRestApiAsync({
restApiId: referenceData.restApiId
});
}
})
}
function setupEndpoints(config, parentResourceId, restApiId) {
return Promise.map(
Object.keys(config),
function(key) {
switch (key.toUpperCase()) {
case 'GET':
case 'HEAD':
case 'DELETE':
case 'OPTIONS':
case 'PATCH':
case 'POST':
case 'PUT':
var params = config[key];
params["httpMethod"] = key.toUpperCase()
params["resourceId"] = parentResourceId
params["restApiId"] = restApiId
params["apiKeyRequired"] = params["apiKeyRequired"] == "true" // Passing through CloudFormation, booleans become strings :(
var integration = params["integration"]
delete params.integration
return apiGateway.putMethodAsync(params)
.then(function() {
return Promise.try(function() {
if (integration) {
var contentType = integration["contentType"]
if (!contentType) {
throw "Integration config must include response contentType."
}
delete integration.contentType
integration["httpMethod"] = key.toUpperCase()
integration["resourceId"] = parentResourceId
integration["restApiId"] = restApiId
return apiGateway.putIntegrationAsync(integration)
.then(function(integrationData) {
var responseContentTypes = {}
responseContentTypes[contentType] = "Empty"
return apiGateway.putMethodResponseAsync({
httpMethod: key.toUpperCase(),
resourceId: parentResourceId,
restApiId: restApiId,
statusCode: '200',
responseModels: responseContentTypes
})
.then(function(methodResponseData) {
responseContentTypes[contentType] = ""
return apiGateway.putIntegrationResponseAsync({
httpMethod: key.toUpperCase(),
resourceId: parentResourceId,
restApiId: restApiId,
statusCode: '200',
responseTemplates: responseContentTypes
});
});
});
}
});
});
default:
return apiGateway.createResourceAsync({
parentId: parentResourceId,
pathPart: key,
restApiId: restApiId,
})
.then(function(resourceData) {
return setupEndpoints(config[key], resourceData.id, restApiId);
});
}
}
);
}
exports.createApi = function(event, context) {
handler = new CreateApi(event, context);
handler.handle();
}

View file

@ -1,73 +0,0 @@
var Promise = require('bluebird'),
AWS = require('aws-sdk'),
base = require('lib/base'),
helpers = require('lib/helpers'),
dynamoDB = Promise.promisifyAll(new AWS.DynamoDB());
// Exposes the SNS.subscribe API method
function PutItems(event, context) {
base.Handler.call(this, event, context);
}
PutItems.prototype = Object.create(base.Handler.prototype);
PutItems.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
return dynamoDB.describeTableAsync({
TableName: p.TableName
})
.then(function(tableData) {
return Promise
.map(
p.Items,
function(item) {
return dynamoDB.putItemAsync({
TableName: p.TableName,
Item: helpers.formatForDynamo(item, true)
})
.then(function() {
var key = {};
tableData.Table.KeySchema.forEach(function(keyMember) {
key[keyMember.AttributeName] = item[keyMember.AttributeName]
});
return {
TableName: p.TableName,
Key: key
};
});
}
)
.then(function(itemsInserted) {
return {
ItemsInserted: itemsInserted
}
});
});
}
PutItems.prototype.handleDelete = function(referenceData) {
return Promise.try(function() {
if (referenceData) {
return Promise
.map(
referenceData.ItemsInserted,
function(item) {
return dynamoDB
.deleteItemAsync({
TableName: item.TableName,
Key: helpers.formatForDynamo(item.Key, true)
})
.then(function(data) {
return item.Key;
});
}
)
.then(function(itemsDeleted) {
return {
ItemsDeleted: itemsDeleted
}
});
}
});
}
exports.putItems = function(event, context) {
handler = new PutItems(event, context);
handler.handle();
}

View file

@ -1,61 +0,0 @@
var Promise = require('bluebird'),
AWS = require('aws-sdk'),
base = require('lib/base'),
kinesis = Promise.promisifyAll(new AWS.Kinesis());
// Exposes the SNS.subscribe API method
function CreateStream(event, context) {
base.Handler.call(this, event, context);
}
CreateStream.prototype = Object.create(base.Handler.prototype);
CreateStream.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
delete p.ServiceToken;
p.ShardCount = parseInt(p.ShardCount);
return kinesis.createStreamAsync(p)
.then(function() {
return waitWhileStatus(p.StreamName, "CREATING");
})
.then(function(arn) {
return {
StreamName: p.StreamName,
Arn: arn
}
});
}
CreateStream.prototype.handleDelete = function(referenceData) {
var p = this.event.ResourceProperties;
return kinesis.deleteStreamAsync({StreamName: p.StreamName})
.then(function() {
return waitWhileStatus(p.StreamName, "DELETING")
})
.catch(function(err) {
return err;
});
}
exports.createStream = function(event, context) {
handler = new CreateStream(event, context);
handler.handle();
}
// Watch until the given status is no longer the status of the stream.
function waitWhileStatus(streamName, status) {
return Promise.try(function() {
var validStatuses = ["CREATING", "DELETING", "ACTIVE", "UPDATING"]
if (validStatuses.indexOf(status) >= 0) {
return kinesis.describeStreamAsync({StreamName: streamName})
.then(function(data) {
console.log("Current status for [" + streamName +"]: " + data.StreamDescription.StreamStatus);
if (data.StreamDescription.StreamStatus == status) {
return Promise.delay(2000)
.then(function() {
return waitWhileStatus(streamName, status);
});
} else {
return data.StreamDescription.StreamARN;
}
});
} else {
throw "status [" + status + "] not one of [" + validStatuses.join(", ") + "]";
}
});
}

View file

@ -1,80 +0,0 @@
var Promise = require('bluebird'),
AWS = require('aws-sdk'),
base = require('lib/base'),
helpers = require('lib/helpers'),
s3 = Promise.promisifyAll(new AWS.S3());
// Exposes the SNS.subscribe API method
function PutObject(event, context) {
base.Handler.call(this, event, context);
}
PutObject.prototype = Object.create(base.Handler.prototype);
PutObject.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
delete p.ServiceToken;
return s3.putObjectAsync(p);
}
PutObject.prototype.handleDelete = function(referenceData) {
var p = this.event.ResourceProperties;
return Promise.try(function() {
if (p.Key.endsWith("/")) {
s3.listObjectsAsync({
Bucket: p.Bucket,
Prefix: p.Key
})
.then(function(subObjects) {
return Promise
.map(
subObjects.Contents,
function(item) {
return s3.deleteObjectAsync({
Bucket: p.Bucket,
Key: item.Key
})
}
)
})
}
})
.then(function() {
return s3.deleteObjectAsync({
Bucket: p.Bucket,
Key: p.Key
});
});
}
exports.putObject = function(event, context) {
handler = new PutObject(event, context);
handler.handle();
}
// Exposes the S3.putBucketPolicy API method
function PutBucketPolicy(event, context) {
base.Handler.call(this, event, context);
}
PutBucketPolicy.prototype = Object.create(base.Handler.prototype);
PutBucketPolicy.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
delete p.ServiceToken;
return s3.putBucketPolicyAsync(p)
.then(function() {
return {
BucketName : p.Bucket
}
});
}
PutBucketPolicy.prototype.handleDelete = function(referencedData) {
return Promise.try(function() {
if(referencedData) {
return s3.deleteBucketPolicyAsync({
Bucket : referencedData.BucketName
});
}
});
}
exports.putBucketPolicy = function(event, context) {
console.log(JSON.stringify(event));
handler = new PutBucketPolicy(event, context);
handler.handle();
}

View file

@ -1,39 +0,0 @@
var Promise = require('bluebird'),
AWS = require('aws-sdk'),
base = require('lib/base'),
helpers = require('lib/helpers'),
ses = Promise.promisifyAll(new AWS.SES());
// Exposes the SES.createReceiptRule API method
function CreateReceiptRule(event, context) {
base.Handler.call(this, event, context);
}
CreateReceiptRule.prototype = Object.create(base.Handler.prototype);
CreateReceiptRule.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
delete p.ServiceToken;
p.Rule.Enabled = ("true" === p.Rule.Enabled );
p.Rule.ScanEnabled = ("true" === p.Rule.ScanEnabled );
return ses.createReceiptRuleAsync(p)
.then(function() {
return {
RuleSetName : p.RuleSetName,
RuleName : p.Rule.Name
}
});
}
CreateReceiptRule.prototype.handleDelete = function(referenceData) {
return Promise.try(function() {
if (referenceData) {
return ses.deleteReceiptRuleAsync({
RuleSetName : referenceData.RuleSetName,
RuleName : referenceData.RuleName
});
}
});
}
exports.createReceiptRule = function(event, context) {
console.log(JSON.stringify(event));
handler = new CreateReceiptRule(event, context);
handler.handle();
}

View file

@ -1,31 +0,0 @@
var Promise = require('bluebird'),
AWS = require('aws-sdk'),
base = require('lib/base'),
sns = Promise.promisifyAll(new AWS.SNS());
// Exposes the SNS.subscribe API method
function Subscribe(event, context) {
base.Handler.call(this, event, context);
}
Subscribe.prototype = Object.create(base.Handler.prototype);
Subscribe.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
return sns.subscribeAsync({
Endpoint: p.Endpoint,
Protocol: p.Protocol,
TopicArn: p.TopicArn
});
}
Subscribe.prototype.handleDelete = function(referenceData) {
return Promise.try(function() {
if (referenceData && referenceData.SubscriptionArn) {
return sns.unsubscribeAsync({
SubscriptionArn: referenceData.SubscriptionArn
});
}
});
}
exports.subscribe = function(event, context) {
handler = new Subscribe(event, context);
handler.handle();
}

View file

@ -1,533 +0,0 @@
{
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"ReferenceDB": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"AttributeDefinitions": [
{
"AttributeName": "key",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "key",
"KeyType": "HASH"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
},
"TableName": { "Fn::Join": [ "-", [ { "Ref" : "AWS::StackName" }, "reference" ] ] }
}
},
"RoleBasePolicy": {
"Type": "AWS::IAM::ManagedPolicy",
"Properties": {
"Description" : { "Fn::Join": [ " ", [ "Base policy for all Lambda function roles in", { "Ref" : "AWS::StackName" }, "." ] ] },
"PolicyDocument" : {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Scan"
],
"Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" } , ":table/", { "Ref": "ReferenceDB" } ] ] }
}
]
}
}
},
"ApiGatewayCreateApiFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "ApiGatewayWriter",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"apigateway:*"
],
"Resource": "*"
}
]
}
}
]
}
},
"ApiGatewayCreateApiFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Fn::Join": [ ".", [ "com.gilt.public.backoffice", { "Ref" : "AWS::Region" } ] ] },
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to create a full API in Api Gateway.",
"Handler": "aws/apiGateway.createApi",
"Role": {"Fn::GetAtt" : [ "ApiGatewayCreateApiFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 30
},
"DependsOn": [
"ApiGatewayCreateApiFunctionRole"
]
},
"CloudWatchLogsPutMetricFilterFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "LogFilterCreator",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:DeleteMetricFilter",
"logs:PutMetricFilter"
],
"Resource": "*"
}
]
}
}
]
}
},
"CloudWatchLogsPutMetricFilterFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Fn::Join": [ ".", [ "com.gilt.public.backoffice", { "Ref" : "AWS::Region" } ] ] },
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to populate a DynamoDB database from CloudFormation",
"Handler": "aws/cloudWatchLogs.putMetricFilter",
"Role": {"Fn::GetAtt" : [ "CloudWatchLogsPutMetricFilterFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 30
},
"DependsOn": [
"CloudWatchLogsPutMetricFilterFunctionRole"
]
},
"DynamoDBPutItemsFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "DBWriter",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
"dynamodb:PutItem"
],
"Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" } , ":table/*" ] ] }
}
]
}
}
]
}
},
"DynamoDBPutItemsFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Fn::Join": [ ".", [ "com.gilt.public.backoffice", { "Ref" : "AWS::Region" } ] ] },
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to populate a DynamoDB database from CloudFormation",
"Handler": "aws/dynamo.putItems",
"Role": {"Fn::GetAtt" : [ "DynamoDBPutItemsFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 30
},
"DependsOn": [
"DynamoDBPutItemsFunctionRole"
]
},
"KinesisCreateStreamFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "KinesisCreator",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kinesis:CreateStream",
"kinesis:DeleteStream",
"kinesis:DescribeStream"
],
"Resource": "*"
}
]
}
}
]
}
},
"KinesisCreateStreamFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Fn::Join": [ ".", [ "com.gilt.public.backoffice", { "Ref" : "AWS::Region" } ] ] },
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to create a Kinesis stream",
"Handler": "aws/kinesis.createStream",
"Role": {"Fn::GetAtt" : [ "KinesisCreateStreamFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 180
},
"DependsOn": [
"KinesisCreateStreamFunctionRole"
]
},
"S3PutObjectFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "S3Writer",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:DeleteObject",
"s3:ListBucket",
"s3:PutObject"
],
"Resource": "*"
}
]
}
}
]
}
},
"S3PutObjectFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Fn::Join": [ ".", [ "com.gilt.public.backoffice", { "Ref" : "AWS::Region" } ] ] },
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to put objects into S3.",
"Handler": "aws/s3.putObject",
"Role": {"Fn::GetAtt" : [ "S3PutObjectFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 30
},
"DependsOn": [
"S3PutObjectFunctionRole"
]
},
"S3PutBucketPolicyFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "S3PolicyWriter",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy"
],
"Resource": "*"
}
]
}
}
]
}
},
"S3PutBucketPolicyFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": "com.gilt.public.backoffice",
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to put S3 bucket policy.",
"Handler": "aws/s3.putBucketPolicy",
"Role": {"Fn::GetAtt" : [ "S3PutBucketPolicyFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 30
},
"DependsOn": [
"S3PutBucketPolicyFunctionRole"
]
},
"SnsSubscribeFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "SNSSubscriber",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sns:subscribe",
"sns:unsubscribe"
],
"Resource": "*"
}
]
}
}
]
}
},
"SnsSubscribeFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Fn::Join": [ ".", [ "com.gilt.public.backoffice", { "Ref" : "AWS::Region" } ] ] },
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to subscribe to existing SNS topics.",
"Handler": "aws/sns.subscribe",
"Role": {"Fn::GetAtt" : [ "SnsSubscribeFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 30
},
"DependsOn": [
"SnsSubscribeFunctionRole"
]
},
"SesCreateReceiptRuleFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "SESReceiptRuleModifier",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ses:CreateReceiptRule",
"ses:DeleteReceiptRule"
],
"Resource": "*"
}
]
}
}
]
}
},
"SesCreateReceiptRuleFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": "com.gilt.public.backoffice",
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to create SES receipt rules.",
"Handler": "aws/ses.createReceiptRule",
"Role": {"Fn::GetAtt" : [ "SesCreateReceiptRuleFunctionRole", "Arn" ] },
"Runtime": "nodejs4.3",
"Timeout": 30
},
"DependsOn": [
"SesCreateReceiptRuleFunctionRole"
]
}
},
"Outputs": {
"ApiGatewayCreateApiFunctionArn": {
"Description": "The ARN of the ApiGatewayCreateApiFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["ApiGatewayCreateApiFunction", "Arn"] }
},
"CloudWatchLogsPutMetricFilterFunctionArn": {
"Description": "The ARN of the CloudWatchLogsPutMetricFilterFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["CloudWatchLogsPutMetricFilterFunction", "Arn"] }
},
"DynamoDBPutItemsFunctionArn": {
"Description": "The ARN of the DynamoDBPutItemsFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["DynamoDBPutItemsFunction", "Arn"] }
},
"KinesisCreateStreamFunctionArn": {
"Description": "The ARN of the KinesisCreateStreamFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["KinesisCreateStreamFunction", "Arn"] }
},
"SnsSubscribeFunctionArn": {
"Description": "The ARN of the SnsSubscribeFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["SnsSubscribeFunction", "Arn"] }
},
"S3PutObjectFunctionArn": {
"Description": "The ARN of the S3PutObjectFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["S3PutObjectFunction", "Arn"] }
},
"S3PutBucketPolicyFunctionArn": {
"Description": "The ARN of the S3PutBucketPolicyFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["S3PutBucketPolicyFunction", "Arn"] }
},
"SesCreateReceiptRuleFunctionArn": {
"Description": "The ARN of the SesCreateReceiptRuleFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["SesCreateReceiptRuleFunction", "Arn"] }
}
}
}

View file

@ -0,0 +1,345 @@
AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::Serverless-2016-10-31'
Resources:
ReferenceDB:
Type: 'AWS::DynamoDB::Table'
Properties:
AttributeDefinitions:
- AttributeName: key
AttributeType: S
KeySchema:
- AttributeName: key
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: !Sub '${AWS::StackName}-reference'
RoleBasePolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
Description: !Sub 'Base policy for all Lambda function roles in ${AWS::StackName}.'
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: 'arn:aws:logs:*:*:*'
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Scan
Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ReferenceDB}'
ApiGatewayCreateApiFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: ApiGatewayWriter
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'apigateway:*'
Resource: '*'
ApiGatewayCreateApiFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to create a full API in Api Gateway.
Handler: aws/apiGateway.createApi
Runtime: nodejs12.x
Role: !GetAtt 'ApiGatewayCreateApiFunctionRole.Arn'
Timeout: 30
DependsOn:
- ApiGatewayCreateApiFunctionRole
CloudWatchLogsPutMetricFilterFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: LogFilterCreator
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'logs:DeleteMetricFilter'
- 'logs:PutMetricFilter'
Resource: '*'
CloudWatchLogsPutMetricFilterFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to populate a DynamoDB database from CloudFormation
Handler: aws/cloudWatchLogs.putMetricFilter
Runtime: nodejs12.x
Role: !GetAtt 'CloudWatchLogsPutMetricFilterFunctionRole.Arn'
Timeout: 30
DependsOn:
- CloudWatchLogsPutMetricFilterFunctionRole
DynamoDBPutItemsFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: DBWriter
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'dynamodb:DeleteItem'
- 'dynamodb:DescribeTable'
- 'dynamodb:PutItem'
Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*'
DynamoDBPutItemsFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to populate a DynamoDB database from CloudFormation
Handler: aws/dynamo.putItems
Runtime: nodejs12.x
Role: !GetAtt 'DynamoDBPutItemsFunctionRole.Arn'
Timeout: 30
DependsOn:
- DynamoDBPutItemsFunctionRole
KinesisCreateStreamFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: KinesisCreator
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'kinesis:CreateStream'
- 'kinesis:DeleteStream'
- 'kinesis:DescribeStream'
Resource: '*'
KinesisCreateStreamFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to create a Kinesis stream
Handler: aws/kinesis.createStream
Runtime: nodejs12.x
Role: !GetAtt 'KinesisCreateStreamFunctionRole.Arn'
Timeout: 180
DependsOn:
- KinesisCreateStreamFunctionRole
S3PutObjectFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: S3Writer
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 's3:DeleteObject'
- 's3:ListBucket'
- 's3:PutObject'
Resource: '*'
S3PutObjectFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to put objects into S3.
Handler: aws/s3.putObject
Runtime: nodejs12.x
Role: !GetAtt 'S3PutObjectFunctionRole.Arn'
Timeout: 30
DependsOn:
- S3PutObjectFunctionRole
S3PutBucketPolicyFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: S3PolicyWriter
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 's3:ListBucket'
- 's3:PutBucketPolicy'
- 's3:DeleteBucketPolicy'
Resource: '*'
S3PutBucketPolicyFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to put S3 bucket policy.
Handler: aws/s3.putBucketPolicy
Runtime: nodejs12.x
Role: !GetAtt 'S3PutBucketPolicyFunctionRole.Arn'
Timeout: 30
DependsOn:
- S3PutBucketPolicyFunctionRole
SnsSubscribeFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: SNSSubscriber
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'sns:subscribe'
- 'sns:unsubscribe'
Resource: '*'
SnsSubscribeFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to subscribe to existing SNS topics
Handler: aws/sns.subscribe
Runtime: nodejs12.x
Role: !GetAtt 'SnsSubscribeFunctionRole.Arn'
Timeout: 30
DependsOn:
- SnsSubscribeFunctionRole
SesCreateReceiptRuleFunctionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref 'RoleBasePolicy'
Policies:
- PolicyName: SESReceiptRuleModifier
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'ses:CreateReceiptRule'
- 'ses:DeleteReceiptRule'
Resource: '*'
SesCreateReceiptRuleFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: out/cloudformation-helpers
Description: Used to create SES receipt rules
Handler: aws/ses.createReceiptRule
Runtime: nodejs12.x
Role: !GetAtt 'SesCreateReceiptRuleFunctionRole.Arn'
Timeout: 30
DependsOn:
- SesCreateReceiptRuleFunctionRole
Outputs:
ApiGatewayCreateApiFunctionArn:
Description: The ARN of the ApiGatewayCreateApiFunction, for use in other CloudFormation templates
Value: !GetAtt 'ApiGatewayCreateApiFunction.Arn'
CloudWatchLogsPutMetricFilterFunctionArn:
Description: The ARN of the CloudWatchLogsPutMetricFilterFunction, for use in other CloudFormation templates
Value: !GetAtt 'CloudWatchLogsPutMetricFilterFunction.Arn'
DynamoDBPutItemsFunctionArn:
Description: The ARN of the DynamoDBPutItemsFunction, for use in other CloudFormation templates.
Value: !GetAtt 'DynamoDBPutItemsFunction.Arn'
KinesisCreateStreamFunctionArn:
Description: The ARN of the KinesisCreateStreamFunction, for use in other CloudFormation templates
Value: !GetAtt 'KinesisCreateStreamFunction.Arn'
SnsSubscribeFunctionArn:
Description: The ARN of the SnsSubscribeFunction, for use in other CloudFormation templates.
Value: !GetAtt 'SnsSubscribeFunction.Arn'
S3PutObjectFunctionArn:
Description: The ARN of the S3PutObjectFunction, for use in other CloudFormation templates.
Value: !GetAtt 'S3PutObjectFunction.Arn'
S3PutBucketPolicyFunctionArn:
Description: The ARN of the S3PutBucketPolicyFunction, for use in other CloudFormation templates.
Value: !GetAtt 'S3PutBucketPolicyFunction.Arn'
SesCreateReceiptRuleFunctionArn:
Description: The ARN of the SesCreateReceiptRuleFunction, for use in other CloudFormation templates.
Value: !GetAtt 'SesCreateReceiptRuleFunction.Arn'

View file

@ -1,20 +0,0 @@
{
"name": "cloudformation-helpers",
"version": "0.0.0",
"description": "A set of helper methods to fill in the gaps in existing CloudFormation support.",
"repository": {
"type": "git",
"url": "https://github.com/gilt/cloudformation-helpers"
},
"keywords": [
"cloudformation"
],
"author": "Ryan Martin",
"license": "Apache 2.0",
"bugs": {
"url": "https://github.com/gilt/cloudformation-helpers/issues"
},
"dependencies": {
"bluebird": "3.x"
}
}

20
src/.eslintrc.js Normal file
View file

@ -0,0 +1,20 @@
module.exports =
{
"env": {
"node": true,
"es6": true,
},
"globals": {
"handler": "readonly",
},
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module",
},
"rules": {
"no-undef": "error",
"no-unused-vars": "error",
"no-var": "error",
"semi": ["error", "always" ],
}
};

114
src/aws/apiGateway.js Normal file
View file

@ -0,0 +1,114 @@
const
utilPromisifyAll = require('util-promisifyall'),
AWS = require('aws-sdk'),
base = require('lib/base'),
apiGateway = utilPromisifyAll(new AWS.APIGateway());
// Exposes the SNS.subscribe API method
function CreateApi(event, context) {
base.Handler.call(this, event, context);
}
CreateApi.prototype = Object.create(base.Handler.prototype);
CreateApi.prototype.handleCreate = async function() {
const p = this.event.ResourceProperties;
const apiData = await apiGateway.createRestApiAsync({
name: p.name,
description: p.description
});
await this.setReferenceData({ restApiId: apiData.id }); // Set this immediately, in case later calls fail
const resourceData = await apiGateway.getResourcesAsync({
restApiId: apiData.id
});
await setupEndpoints(p.endpoints, resourceData.items[0].id, apiData.id);
await apiGateway.createDeploymentAsync({
restApiId: apiData.id,
stageName: p.version
});
// Total hack: there are limits to the number of API Gateway API calls you can make. So this function
// will fail if a CloudFormation template includes two or more APIs. Attempting to avoid this by blocking.
const until = new Date();
until.setSeconds(until.getSeconds() + 60);
while (new Date() < until) {
// Wait...
}
// AWS.config.region is a bit of a hack, but I can't figure out how else to dynamically
// detect the region of the API - seems to be nothing in API Gateway or AWS Lambda context.
// Could possibly get it from the CloudFormation stack, but that seems wrong.
return {
baseUrl: "https://" + apiData.id + ".execute-api." + AWS.config.region + ".amazonaws.com/" + p.version,
restApiId: apiData.id
};
};
CreateApi.prototype.handleDelete = async function(referenceData) {
if (referenceData && referenceData.restApiId) {
// Can simply delete the entire API - don't need to delete each individual component
return await apiGateway.deleteRestApiAsync({
restApiId: referenceData.restApiId
});
}
};
async function setupEndpoints(config, parentResourceId, restApiId) {
return await Promise.all(
Object.keys(config).map(async key => {
switch (key.toUpperCase()) {
case 'GET':
case 'HEAD':
case 'DELETE':
case 'OPTIONS':
case 'PATCH':
case 'POST':
case 'PUT':
const params = config[key];
params["httpMethod"] = key.toUpperCase();
params["resourceId"] = parentResourceId;
params["restApiId"] = restApiId;
params["apiKeyRequired"] = params["apiKeyRequired"] == "true"; // Passing through CloudFormation, booleans become strings :/
const integration = params["integration"];
delete params.integration;
await apiGateway.putMethodAsync(params);
if (integration) {
const contentType = integration["contentType"];
if (!contentType) {
throw "Integration config must include response contentType.";
}
delete integration.contentType;
integration["httpMethod"] = key.toUpperCase();
integration["resourceId"] = parentResourceId;
integration["restApiId"] = restApiId;
apiGateway.putIntegrationAsync(integration);
const responseContentTypes = {};
responseContentTypes[contentType] = "Empty";
await apiGateway.putMethodResponseAsync({
httpMethod: key.toUpperCase(),
resourceId: parentResourceId,
restApiId: restApiId,
statusCode: '200',
responseModels: responseContentTypes
});
responseContentTypes[contentType] = "";
return await apiGateway.putIntegrationResponseAsync({
httpMethod: key.toUpperCase(),
resourceId: parentResourceId,
restApiId: restApiId,
statusCode: '200',
responseTemplates: responseContentTypes
});
}
else {
return;
}
default:
const resourceData = await apiGateway.createResourceAsync({
parentId: parentResourceId,
pathPart: key,
restApiId: restApiId,
});
return await setupEndpoints(config[key], resourceData.id, restApiId);
}
})
);
}
exports.createApi = function(event, context) {
handler = new CreateApi(event, context);
handler.handle();
};

64
src/aws/dynamo.js Normal file
View file

@ -0,0 +1,64 @@
const
utilPromisifyAll = require('util-promisifyall'),
AWS = require('aws-sdk'),
base = require('lib/base'),
helpers = require('lib/helpers'),
dynamoDB = utilPromisifyAll(new AWS.DynamoDB());
// Exposes the SNS.subscribe API method
function PutItems(event, context) {
base.Handler.call(this, event, context);
}
PutItems.prototype = Object.create(base.Handler.prototype);
PutItems.prototype.handleCreate = async function() {
const p = this.event.ResourceProperties;
const tableData = await dynamoDB.describeTableAsync({
TableName: p.TableName
});
const itemsInserted = await Promise.all(
p.Items.map(async item => {
await dynamoDB.putItemAsync({
TableName: p.TableName,
Item: helpers.formatForDynamo(item, true)
});
const key = {};
tableData.Table.KeySchema.forEach(function(keyMember) {
key[keyMember.AttributeName] = item[keyMember.AttributeName];
});
return {
TableName: p.TableName,
Key: key
};
})
);
return {
ItemsInserted: itemsInserted
};
};
PutItems.prototype.handleDelete = async function(referenceData) {
if (referenceData) {
const itemsDeleted = await Promise.all(
referenceData.itemsInserted.map(async item => {
await dynamoDB.deleteItemAsync({
TableName: item.TableName,
Key: helpers.formatForDynamo(item.Key, true)
});
return item.Key;
})
);
return {
ItemsDeleted: itemsDeleted
};
}
};
exports.putItems = function(event, context) {
handler = new PutItems(event, context);
handler.handle();
};

54
src/aws/kinesis.js Normal file
View file

@ -0,0 +1,54 @@
const
utilPromisifyAll = require('util-promisifyall'),
AWS = require('aws-sdk'),
base = require('lib/base'),
kinesis = utilPromisifyAll(new AWS.Kinesis());
// Exposes the SNS.subscribe API method
function CreateStream(event, context) {
base.Handler.call(this, event, context);
}
CreateStream.prototype = Object.create(base.Handler.prototype);
CreateStream.prototype.handleCreate = async function() {
const p = this.event.ResourceProperties;
delete p.ServiceToken;
p.ShardCount = parseInt(p.ShardCount);
await kinesis.createStreamAsync(p);
const arn = await waitWhileStatus(p.StreamName, "CREATING");
return {
StreamName: p.StreamName,
Arn: arn
};
};
// eslint-disable-next-line no-unused-vars
CreateStream.prototype.handleDelete = async function(referenceData) {
try {
const p = this.event.ResourceProperties;
await kinesis.deleteStreamAsync({StreamName: p.StreamName});
return await waitWhileStatus(p.StreamName, "DELETING");
}
catch(err) {
return err;
}
};
exports.createStream = function(event, context) {
handler = new CreateStream(event, context);
handler.handle();
};
// Watch until the given status is no longer the status of the stream.
async function waitWhileStatus(streamName, status) {
const validStatuses = ["CREATING", "DELETING", "ACTIVE", "UPDATING"];
if (validStatuses.indexOf(status) >= 0) {
const data = await kinesis.describeStreamAsync({StreamName: streamName});
console.log("Current status for [" + streamName +"]: " + data.StreamDescription.StreamStatus);
if (data.StreamDescription.StreamStatus == status) {
await new Promise((resolve) => setTimeout(resolve, 2000));
return await waitWhileStatus(streamName, status);
} else {
return data.StreamDescription.StreamARN;
}
} else {
throw "status [" + status + "] not one of [" + validStatuses.join(", ") + "]";
}
}

72
src/aws/s3.js Normal file
View file

@ -0,0 +1,72 @@
const
utilPromisifyAll = require('util-promisifyall'),
AWS = require('aws-sdk'),
base = require('lib/base'),
s3 = utilPromisifyAll(new AWS.S3());
// Exposes the SNS.subscribe API method
function PutObject(event, context) {
base.Handler.call(this, event, context);
}
PutObject.prototype = Object.create(base.Handler.prototype);
PutObject.prototype.handleCreate = async function() {
const p = this.event.ResourceProperties;
delete p.ServiceToken;
return await s3.putObjectAsync(p);
};
// eslint-disable-next-line no-unused-vars
PutObject.prototype.handleDelete = async function(referenceData) {
const p = this.event.ResourceProperties;
if (p.Key.endsWith("/")) {
const subObjects = await s3.listObjectsAsync({
Bucket: p.Bucket,
Prefix: p.Key
});
await Promise.all(
subObjects.Contents.map(async item => {
return await s3.deleteObjectAsync({
Bucket: p.Bucket,
Key: item.Key
});
})
);
}
return s3.deleteObjectAsync({
Bucket: p.Bucket,
Key: p.Key
});
};
exports.putObject = function(event, context) {
handler = new PutObject(event, context);
handler.handle();
};
// Exposes the S3.putBucketPolicy API method
function PutBucketPolicy(event, context) {
base.Handler.call(this, event, context);
}
PutBucketPolicy.prototype = Object.create(base.Handler.prototype);
PutBucketPolicy.prototype.handleCreate = async function() {
const p = this.event.ResourceProperties;
delete p.ServiceToken;
await s3.putBucketPolicyAsync(p);
return {
BucketName : p.Bucket
};
};
PutBucketPolicy.prototype.handleDelete = async function(referencedData) {
if(referencedData) {
return await s3.deleteBucketPolicyAsync({
Bucket : referencedData.BucketName
});
}
};
exports.putBucketPolicy = function(event, context) {
console.log(JSON.stringify(event));
handler = new PutBucketPolicy(event, context);
handler.handle();
};

36
src/aws/ses.js Normal file
View file

@ -0,0 +1,36 @@
const
utilPromisifyAll = require('util-promisifyall'),
AWS = require('aws-sdk'),
base = require('lib/base'),
ses = utilPromisifyAll(new AWS.SES());
// Exposes the SES.createReceiptRule API method
function CreateReceiptRule(event, context) {
base.Handler.call(this, event, context);
}
CreateReceiptRule.prototype = Object.create(base.Handler.prototype);
CreateReceiptRule.prototype.handleCreate = async function() {
const p = this.event.ResourceProperties;
delete p.ServiceToken;
p.Rule.Enabled = ("true" === p.Rule.Enabled );
p.Rule.ScanEnabled = ("true" === p.Rule.ScanEnabled );
await ses.createReceiptRuleAsync(p);
return {
RuleSetName : p.RuleSetName,
RuleName : p.Rule.Name
};
};
CreateReceiptRule.prototype.handleDelete = async function(referenceData) {
if (referenceData) {
return await ses.deleteReceiptRuleAsync({
RuleSetName : referenceData.RuleSetName,
RuleName : referenceData.RuleName
});
}
};
exports.createReceiptRule = function(event, context) {
console.log(JSON.stringify(event));
handler = new CreateReceiptRule(event, context);
handler.handle();
};

30
src/aws/sns.js Normal file
View file

@ -0,0 +1,30 @@
const
utilPromisifyAll = require('util-promisifyall'),
AWS = require('aws-sdk'),
base = require('lib/base'),
sns = utilPromisifyAll(new AWS.SNS());
// Exposes the SNS.subscribe API method
function Subscribe(event, context) {
base.Handler.call(this, event, context);
}
Subscribe.prototype = Object.create(base.Handler.prototype);
Subscribe.prototype.handleCreate = async function() {
const p = this.event.ResourceProperties;
return await sns.subscribeAsync({
Endpoint: p.Endpoint,
Protocol: p.Protocol,
TopicArn: p.TopicArn
});
};
Subscribe.prototype.handleDelete = async function(referenceData) {
if (referenceData && referenceData.SubscriptionArn) {
return await sns.unsubscribeAsync({
SubscriptionArn: referenceData.SubscriptionArn
});
}
};
exports.subscribe = function(event, context) {
handler = new Subscribe(event, context);
handler.handle();
};

View file

@ -1,49 +1,45 @@
// Implement this class for every new handler. // Implement this class for every new handler.
var Promise = require('bluebird'), const
helpers = require('lib/helpers'), utilPromisifyAll = require('util-promisifyall'),
response = require('lib/cfn-response'), helpers = require('lib/helpers'),
AWS = require('aws-sdk'), response = require('lib/cfn-response'),
dynamoDB = Promise.promisifyAll(new AWS.DynamoDB()); AWS = require('aws-sdk'),
dynamoDB = utilPromisifyAll(new AWS.DynamoDB());
exports.Handler = function(event, context) { exports.Handler = function(event, context) {
this.event = event; this.event = event;
this.context = context; this.context = context;
} };
exports.Handler.prototype.handle = function() { exports.Handler.prototype.handle = async function() {
var outer = this; try {
Promise.try(function() { let data;
switch (outer.event.RequestType) { let referenceData;
switch (this.event.RequestType) {
case 'Create': case 'Create':
return outer.handleCreate() const created = await this.handleCreate();
.then(function(data) { await this.setReferenceData(created);
return outer.setReferenceData(data) data = created;
.then(function() { break;
return data;
});
});
case 'Delete': case 'Delete':
return outer.getReferenceData() referenceData = await this.getReferenceData();
.then(function(data) { data = await this.handleDelete(referenceData);
return outer.handleDelete(data); break;
});
case 'Update': case 'Update':
return outer.getReferenceData() await this.getReferenceData();
.then(function(data) { data = await this.handleUpdate();
return outer.handleUpdate(); break;
});
default: default:
throw "Unrecognized RequestType [" + outer.event.RequestType + "]"; throw "Unrecognized RequestType [" + this.event.RequestType + "]";
} }
})
.then(function(data) { response.send(this.event, this.context, response.SUCCESS, data);
response.send(outer.event, outer.context, response.SUCCESS, data); }
}) catch(err) {
.catch(function(err) { this.error(err);
outer.error(err); }
}); };
}
/* /*
When implemented, these should all return a Promise, which will then be completed by the handle() When implemented, these should all return a Promise, which will then be completed by the handle()
@ -54,59 +50,56 @@ exports.Handler.prototype.handle = function() {
*/ */
exports.Handler.prototype.handleCreate = function() { exports.Handler.prototype.handleCreate = function() {
throw "create method not implemented"; throw "create method not implemented";
} };
// eslint-disable-next-line no-unused-vars
exports.Handler.prototype.handleDelete = function(referenceData) { exports.Handler.prototype.handleDelete = function(referenceData) {
throw "delete method not implemented"; throw "delete method not implemented";
} };
exports.Handler.prototype.handleUpdate = function(referenceData) { exports.Handler.prototype.handleUpdate = async function(referenceData) {
var self = this; await this.handleDelete(referenceData);
return this.handleDelete(referenceData) return await this.handleCreate();
.then(function() { };
return self.handleCreate();
});
}
exports.Handler.prototype.error = function(message) { exports.Handler.prototype.error = function(message) {
console.error(message); console.error(message);
response.send(this.event, this.context, response.FAILED, { Error: message }); response.send(this.event, this.context, response.FAILED, { Error: message });
throw message; throw message;
} };
exports.Handler.prototype.getStackName = function() { exports.Handler.prototype.getStackName = function() {
var functionName = this.context.functionName; const functionName = this.context.functionName;
// Assume functionName is: stackName-resourceLogicalId-randomString. // Assume functionName is: stackName-resourceLogicalId-randomString.
// Per http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html, // Per http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html,
// resourceLogicalId cannot include a '-'; randomString seems to also be alphanumeric. // resourceLogicalId cannot include a '-'; randomString seems to also be alphanumeric.
// Thus it seems safe to search for the two dashes in order to find the stackName. // Thus it seems safe to search for the two dashes in order to find the stackName.
var i = functionName.lastIndexOf("-", functionName.lastIndexOf("-") - 1); const i = functionName.lastIndexOf("-", functionName.lastIndexOf("-") - 1);
if (i >= 0) if (i >= 0)
return functionName.substr(0, i); return functionName.substr(0, i);
else else
return functionName; return functionName;
} };
exports.Handler.prototype.getReferenceData = function() { exports.Handler.prototype.getReferenceData = async function() {
return dynamoDB.getItemAsync( const data = await dynamoDB.getItemAsync(
{ {
TableName: this.getStackName() + "-reference", TableName: this.getStackName() + "-reference",
Key: helpers.formatForDynamo({ Key: helpers.formatForDynamo({
key: this.event.StackId + this.event.LogicalResourceId key: this.event.StackId + this.event.LogicalResourceId
}, true) }, true)
} }
) );
.then(function(data) {
data = helpers.formatFromDynamo(data);
if (data && data.Item && data.Item.value)
return data.Item.value;
else
return null;
})
}
exports.Handler.prototype.setReferenceData = function(data) { const formattedData = helpers.formatFromDynamo(data);
return dynamoDB.putItemAsync( if (formattedData && formattedData.Item && formattedData.Item.value)
return formattedData.Item.value;
else
return null;
};
exports.Handler.prototype.setReferenceData = async function(data) {
return await dynamoDB.putItemAsync(
{ {
TableName: this.getStackName() + "-reference", TableName: this.getStackName() + "-reference",
Item: helpers.formatForDynamo({ Item: helpers.formatForDynamo({
@ -115,4 +108,4 @@ exports.Handler.prototype.setReferenceData = function(data) {
}, true) }, true)
} }
); );
} };

View file

@ -10,7 +10,7 @@ exports.FAILED = "FAILED";
exports.send = function(event, context, responseStatus, responseData, physicalResourceId) { exports.send = function(event, context, responseStatus, responseData, physicalResourceId) {
var responseBody = JSON.stringify({ const responseBody = JSON.stringify({
Status: responseStatus, Status: responseStatus,
Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: physicalResourceId || context.logStreamName, PhysicalResourceId: physicalResourceId || context.logStreamName,
@ -22,15 +22,15 @@ exports.send = function(event, context, responseStatus, responseData, physicalRe
console.log("Response body:\n", responseBody); console.log("Response body:\n", responseBody);
var https = require("https"); const https = require("https");
var url = require("url"); const url = require("url");
// This script comes from http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule // This script comes from http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule
// The only change is this 'if' statement around this block of code, so it doesn't // The only change is this 'if' statement around this block of code, so it doesn't
// fail when the ResonseURL is missing (i.e. during manual testing). // fail when the ResonseURL is missing (i.e. during manual testing).
if (event.ResponseURL) { if (event.ResponseURL) {
var parsedUrl = url.parse(event.ResponseURL); const parsedUrl = url.parse(event.ResponseURL);
var options = { const options = {
hostname: parsedUrl.hostname, hostname: parsedUrl.hostname,
port: 443, port: 443,
path: parsedUrl.path, path: parsedUrl.path,
@ -41,7 +41,7 @@ exports.send = function(event, context, responseStatus, responseData, physicalRe
} }
}; };
var request = https.request(options, function(response) { const request = https.request(options, function(response) {
console.log("Status code: " + response.statusCode); console.log("Status code: " + response.statusCode);
console.log("Status message: " + response.statusMessage); console.log("Status message: " + response.statusMessage);
context.done(); context.done();
@ -55,4 +55,4 @@ exports.send = function(event, context, responseStatus, responseData, physicalRe
request.write(responseBody); request.write(responseBody);
request.end(); request.end();
} }
} };

View file

@ -1,5 +1,3 @@
var Promise = require('bluebird');
// Translates from raw JSON into DynamoDB-formatted JSON. This is more than a // Translates from raw JSON into DynamoDB-formatted JSON. This is more than a
// convenience thing: the original iteration of this accepted DyanamoDB-JSON Items, // convenience thing: the original iteration of this accepted DyanamoDB-JSON Items,
// to avoid complications in translation. But when CloudFormation passes the parameters // to avoid complications in translation. But when CloudFormation passes the parameters
@ -8,44 +6,44 @@ var Promise = require('bluebird');
// it is 'true' or 'false') as a 'BOOL' value. So some translation was needed, and // it is 'true' or 'false') as a 'BOOL' value. So some translation was needed, and
// it seemed best to then simplify things for the client by accepting raw JSON. // it seemed best to then simplify things for the client by accepting raw JSON.
exports.formatForDynamo = function(value, topLevel) { exports.formatForDynamo = function(value, topLevel) {
var result = undefined; let result = undefined;
if (value == 'true' || value == 'false') { if (value == 'true' || value == 'false') {
result = {'BOOL': value == 'true'} result = {'BOOL': value == 'true'};
} else if (!isNaN(value) && value.trim() != '') { } else if (!isNaN(value) && value.trim() != '') {
result = {'N': value} result = {'N': value};
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
var arr = []; const arr = [];
for (var i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
arr.push(exports.formatForDynamo(value[i], false)); arr.push(exports.formatForDynamo(value[i], false));
} }
result = {'L': arr}; result = {'L': arr};
} else if (typeof value === "object") { } else if (typeof value === "object") {
var map = {}; const map = {};
Object.keys(value).forEach(function(key) { Object.keys(value).forEach(function(key) {
map[key] = exports.formatForDynamo(value[key], false) map[key] = exports.formatForDynamo(value[key], false);
}); });
if (topLevel) result = map; if (topLevel) result = map;
else result = {'M': map} else result = {'M': map};
} else { } else {
result = {'S': value} result = {'S': value};
} }
return result; return result;
} };
exports.formatFromDynamo = function(value) { exports.formatFromDynamo = function(value) {
var result = undefined; let result = undefined;
if (typeof value === "string" || typeof value === 'boolean' || typeof value === 'number') { if (typeof value === "string" || typeof value === 'boolean' || typeof value === 'number') {
result = value; result = value;
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
var arr = []; const arr = [];
for (var i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
arr.push(exports.formatFromDynamo(value[i])); arr.push(exports.formatFromDynamo(value[i]));
} }
result = arr; result = arr;
} else if (typeof value === "object") { } else if (typeof value === "object") {
var map = {}; const map = {};
Object.keys(value).forEach(function(key) { Object.keys(value).forEach(function(key) {
var v = exports.formatFromDynamo(value[key]); const v = exports.formatFromDynamo(value[key]);
switch (key) { switch (key) {
case 'B': case 'B':
throw "Unsupported Mongo type [B]"; throw "Unsupported Mongo type [B]";
@ -65,7 +63,7 @@ exports.formatFromDynamo = function(value) {
break; break;
case 'NS': case 'NS':
result = []; result = [];
for (var i = 0; i < v.length; i++) { for (let i = 0; i < v.length; i++) {
result.push(Number(value[i])); result.push(Number(value[i]));
}; };
break; break;
@ -85,16 +83,16 @@ exports.formatFromDynamo = function(value) {
throw "Unrecognized type [" + (typeof value) + "]"; throw "Unrecognized type [" + (typeof value) + "]";
} }
return result; return result;
} };
if (!String.prototype.endsWith) { if (!String.prototype.endsWith) {
String.prototype.endsWith = function(searchString, position) { String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString(); const subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length; position = subjectString.length;
} }
position -= searchString.length; position -= searchString.length;
var lastIndex = subjectString.indexOf(searchString, position); const lastIndex = subjectString.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position; return lastIndex !== -1 && lastIndex === position;
}; };
} }

33
src/package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "cloudformation-helpers",
"version": "0.0.0",
"description": "A set of helper methods to fill in the gaps in existing CloudFormation support.",
"repository": {
"type": "git",
"url": "https://github.com/empathicqubit/cloudformation-helpers"
},
"keywords": [
"cloudformation"
],
"author": "Ryan Martin, EmpathicQubit",
"license": "Apache 2.0",
"bugs": {
"url": "https://github.com/empathicquibt/cloudformation-helpers/issues"
},
"scripts": {
"package": "shx mkdir -p ../out && shx rm -rf ../out/cloudformation-helpers && shx cp -r ../src ../out/cloudformation-helpers && cd ../out/cloudformation-helpers && yarn install --production",
"lint": "eslint .",
"deploy": "cd .. && cross-env-shell \"aws cloudformation package --s3-bucket $CF_S3_BUCKET --template-file create_cloudformation_helper_functions.template.yml --output-template-file out/create_cloudformation_helper_functions.template.yml\" && cross-env-shell \"aws cloudformation deploy --template-file out/create_cloudformation_helper_functions.template.yml --capabilities CAPABILITY_NAMED_IAM --stack-name $CF_STACK_NAME\"",
"publish": "npm-run-all lint package deploy"
},
"private": true,
"dependencies": {
"util-promisifyall": "^1.0.6"
},
"devDependencies": {
"cross-env": "^7.0.2",
"eslint": "^7.2.0",
"npm-run-all": "^4.1.5",
"shx": "^0.3.2"
}
}

1231
src/yarn.lock Normal file

File diff suppressed because it is too large Load diff