Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloudformation): aws custom resource #1850

Merged
merged 31 commits into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4a8e4b6
feat(cloudformation): aws sdk js custom resource
jogold Feb 23, 2019
5ab6588
Log AWS SDK version
jogold Feb 25, 2019
15a6ef8
Add tests
jogold Feb 25, 2019
34a3ebc
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 1, 2019
ad86457
Return API response as output attributes
jogold Mar 4, 2019
84c73ab
Remove default on onUpdate
jogold Mar 4, 2019
5ba7765
Add options to specify physical resource id
jogold Mar 4, 2019
6f1c0c6
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 26, 2019
5811c94
Rename to AwsCustomResource
jogold Mar 26, 2019
941913b
Add readonly
jogold Mar 28, 2019
552a5e7
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 28, 2019
ae446b9
Write provider in ts
jogold Mar 28, 2019
2fb5163
Support path for physical resource id
jogold Mar 28, 2019
04c8df5
Typo
jogold Mar 28, 2019
8ddf39f
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 28, 2019
82d1ea1
Fix bad pkglint version after merge
jogold Mar 28, 2019
30d3226
Merge branch 'master' into aws-sdk-js-resource
jogold Apr 2, 2019
a26be81
Add option to catch API errors
jogold Apr 2, 2019
7e9b21c
Merge branch 'aws-sdk-js-resource' of github.com:jogold/aws-cdk into …
jogold Apr 2, 2019
b5fc424
Restore package-lock.json in aws-codepipeline-actions
jogold Apr 2, 2019
25691a0
Merge branch 'master' into aws-sdk-js-resource
jogold May 13, 2019
1b7f392
CustomResourceProvider
jogold May 13, 2019
598f605
exclude construct-ctor-props-optional
jogold May 13, 2019
290d77c
remove duplicate statements check
jogold May 13, 2019
8981600
add option to lock api version
jogold May 13, 2019
c423f35
JSDoc
jogold May 13, 2019
d7b66cc
fix booleans in parameters
jogold May 14, 2019
e8813f5
update integration test
jogold May 14, 2019
1b204d8
update README
jogold May 14, 2019
700fa4d
Merge branch 'master' into aws-sdk-js-resource
jogold May 27, 2019
e02480f
update integ test
jogold May 27, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!*.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const AWS = require('aws-sdk');

/**
* Flattens a nested object. Keys are path.
*/
function flatten(object) {
return Object.assign(
{},
...function _flatten(child, path = []) {
return [].concat(...Object.keys(child).map(key => typeof child[key] === 'object'
? _flatten(child[key], path.concat([key]))
: ({ [path.concat([key]).join('.')] : child[key] })
));
}(object)
);
}

exports.handler = async function(event, context) {
try {
console.log(JSON.stringify(event));
console.log('AWS SDK VERSION: ' + AWS.VERSION);

let data = {};

const physicalResourceId = event.ResourceProperties.PhysicalResourceId;

if (event.ResourceProperties[event.RequestType]) {
const { service, action, parameters = {} } = event.ResourceProperties[event.RequestType];

const awsService = new AWS[service]();

const response = await awsService[action](parameters).promise();

data = flatten(response);
}

await respond('SUCCESS', 'OK', physicalResourceId, data);
} catch (e) {
console.log(e);
await respond('FAILED', e.message, context.logStreamName, {});
}

function respond(responseStatus, reason, physicalResourceId, data) {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: reason,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: false,
Data: data
});

console.log('Responding', JSON.stringify(responseBody));

const parsedUrl = require('url').parse(event.ResponseURL);
const requestOptions = {
hostname: parsedUrl.hostname,
path: parsedUrl.path,
method: 'PUT',
headers: { 'content-type': '', 'content-length': responseBody.length }
};

return new Promise((resolve, reject) => {
try {
const request = require('https').request(requestOptions, resolve);
request.on('error', reject);
request.write(responseBody);
request.end();
} catch (e) {
reject(e);
}
});
}
}
175 changes: 175 additions & 0 deletions packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import iam = require('@aws-cdk/aws-iam');
import lambda = require('@aws-cdk/aws-lambda');
import cdk = require('@aws-cdk/cdk');
import metadata = require('aws-sdk/apis/metadata.json');
import path = require('path');
import { CustomResource } from './custom-resource';

/**
* AWS SDK service metadata.
*/
export type AwsSdkMetadata = {[key: string]: any};

const awsSdkMetadata: AwsSdkMetadata = metadata;

/**
* An AWS SDK call.
*/
export interface AwsSdkCall {
/**
* The service to call
*/
readonly service: string;

/**
* The service action to call
*/
readonly action: string;

/**
* The parameters for the service action
*/
readonly parameters?: any;
}

export interface AwsCustomResourceProps {
/**
* The physical resource id of the custom resource.
*/
readonly physicalResourceId: string;

/**
* The AWS SDK call to make when the resource is created.
* At least onCreate, onUpdate or onDelete must be specified.
*
* @default the call when the resource is updated
*/
readonly onCreate?: AwsSdkCall;

/**
* The AWS SDK call to make when the resource is updated
*
* @default no call
*/
readonly onUpdate?: AwsSdkCall;

/**
* THe AWS SDK call to make when the resource is deleted
*
* @default no call
*/
readonly onDelete?: AwsSdkCall;

/**
* The IAM policy statements to allow the different calls. Use only if
* resource restriction is needed.
*
* @default Allow onCreate, onUpdate and onDelete calls on all resources ('*')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the default is to extract the permissions from the calls

*/
readonly policyStatements?: iam.PolicyStatement[];
}

export class AwsCustomResource extends cdk.Construct {
/**
* The physical resource id of the custom resource.
*/
public readonly physicalResourceId: string;

/**
* The AWS SDK call made when the resource is created.
*/
public readonly onCreate?: AwsSdkCall;

/**
* The AWS SDK call made when the resource is udpated.
*/
public readonly onUpdate?: AwsSdkCall;

/**
* The AWS SDK call made when the resource is deleted.
*/
public readonly onDelete?: AwsSdkCall;

/**
* The IAM policy statements used by the lambda provider.
*/
public readonly policyStatements: iam.PolicyStatement[];

/**
* The custom resource making the AWS SDK calls.
*/
private readonly customResource: CustomResource;

constructor(scope: cdk.Construct, id: string, props: AwsCustomResourceProps) {
super(scope, id);

if (!props.onCreate && !props.onUpdate && !props.onDelete) {
throw new Error('At least `onCreate`, `onUpdate` or `onDelete` must be specified.');
}

this.onCreate = props.onCreate || props.onUpdate;
this.onUpdate = props.onUpdate;
this.onDelete = props.onDelete;
this.physicalResourceId = props.physicalResourceId;

const fn = new lambda.SingletonFunction(this, 'Function', {
code: lambda.Code.asset(path.join(__dirname, 'aws-custom-resource-provider')),
runtime: lambda.Runtime.NodeJS810,
handler: 'index.handler',
uuid: '679f53fa-c002-430c-b0da-5b7982bd2287'
});

if (props.policyStatements) {
props.policyStatements.forEach(statement => {
fn.addToRolePolicy(statement);
});
this.policyStatements = props.policyStatements;
} else { // Derive statements from AWS SDK calls
this.policyStatements = [];

[this.onCreate, this.onUpdate, this.onDelete].forEach(call => {
if (call) {
const statement = new iam.PolicyStatement()
.addAction(awsSdkToIamAction(call.service, call.action))
.addAllResources();
fn.addToRolePolicy(statement); // TODO: remove duplicates?
this.policyStatements.push(statement);
}
});
}

this.customResource = new CustomResource(this, 'Resource', {
resourceType: 'Custom::AWS',
lambdaProvider: fn,
properties: {
physicalResourceId: this.physicalResourceId,
create: this.onCreate,
update: this.onUpdate,
delete: this.onDelete
}
});
}

/**
* Returns response data for the AWS SDK call.
* Example for S3 / listBucket : 'Buckets.0.Name'
*
* @param dataPath the path to the data
*/
public getData(dataPath: string) {
return this.customResource.getAtt(dataPath);
}
}

/**
* Transform SDK service/action to IAM action using metadata from aws-sdk module.
* Example: CloudWatchLogs with putRetentionPolicy => logs:PutRetentionPolicy
*
* TODO: is this mapping correct for all services?
*/
function awsSdkToIamAction(service: string, action: string): string {
const srv = service.toLowerCase();
const iamService = awsSdkMetadata[srv].prefix || srv;
const iamAction = action.charAt(0).toUpperCase() + action.slice(1);
return `${iamService}:${iamAction}`;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudformation/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './custom-resource';
export * from './pipeline-actions';
export * from './aws-custom-resource';

// AWS::CloudFormation CloudFormation Resources:
export * from './cloudformation.generated';
96 changes: 95 additions & 1 deletion packages/@aws-cdk/aws-cloudformation/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions packages/@aws-cdk/aws-cloudformation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@
"@aws-cdk/aws-iam": "^0.26.0",
"@aws-cdk/aws-lambda": "^0.26.0",
"@aws-cdk/aws-sns": "^0.26.0",
"@aws-cdk/cdk": "^0.26.0"
"@aws-cdk/cdk": "^0.26.0",
"aws-sdk": "^2.409.0"
},
"bundledDependencies": [
"aws-sdk"
],
"homepage": "https://github.com/awslabs/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-codepipeline-api": "^0.26.0",
Expand All @@ -92,4 +96,4 @@
"construct-ctor:@aws-cdk/aws-cloudformation.PipelineCloudFormationDeployAction.<initializer>"
]
}
}
}
Loading