-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
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 5ab6588
Log AWS SDK version
jogold 15a6ef8
Add tests
jogold 34a3ebc
Merge branch 'master' into aws-sdk-js-resource
jogold ad86457
Return API response as output attributes
jogold 84c73ab
Remove default on onUpdate
jogold 5ba7765
Add options to specify physical resource id
jogold 6f1c0c6
Merge branch 'master' into aws-sdk-js-resource
jogold 5811c94
Rename to AwsCustomResource
jogold 941913b
Add readonly
jogold 552a5e7
Merge branch 'master' into aws-sdk-js-resource
jogold ae446b9
Write provider in ts
jogold 2fb5163
Support path for physical resource id
jogold 04c8df5
Typo
jogold 8ddf39f
Merge branch 'master' into aws-sdk-js-resource
jogold 82d1ea1
Fix bad pkglint version after merge
jogold 30d3226
Merge branch 'master' into aws-sdk-js-resource
jogold a26be81
Add option to catch API errors
jogold 7e9b21c
Merge branch 'aws-sdk-js-resource' of github.com:jogold/aws-cdk into …
jogold b5fc424
Restore package-lock.json in aws-codepipeline-actions
jogold 25691a0
Merge branch 'master' into aws-sdk-js-resource
jogold 1b7f392
CustomResourceProvider
jogold 598f605
exclude construct-ctor-props-optional
jogold 290d77c
remove duplicate statements check
jogold 8981600
add option to lock api version
jogold c423f35
JSDoc
jogold d7b66cc
fix booleans in parameters
jogold e8813f5
update integration test
jogold 1b204d8
update README
jogold 700fa4d
Merge branch 'master' into aws-sdk-js-resource
jogold e02480f
update integ test
jogold File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/.gitignore
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
!*.js |
76 changes: 76 additions & 0 deletions
76
packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
175
packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ('*') | ||
*/ | ||
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}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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