Skip to content

Commit

Permalink
feat(custom-resources): physical resource id union type (#6518)
Browse files Browse the repository at this point in the history
* added additional stack to 'ls' test so it doesn't fail

* combine physicalResourceId and physicalResourceIdPath to a union type

* fix tests according to new api

* added doc strings and rename argument

* fix tests

* fixing some tests

* enhance 'fromResponsePath' docstring

* fix references to physicalResourceId in README

* fix integ expected template and rename fromResponsePath to fromResponse

* Rephrase docstring for `fromResponse`

Co-Authored-By: Elad Ben-Israel <[email protected]>

* Rephrase docstring for `of`

Co-Authored-By: Elad Ben-Israel <[email protected]>

* fix README reference to fromResponsePath

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Elad Ben-Israel <[email protected]>
  • Loading branch information
3 people committed Mar 9, 2020
1 parent 26a56b0 commit aabd36d
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 80 deletions.
26 changes: 13 additions & 13 deletions packages/@aws-cdk/custom-resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The following example is a skeleton for a Python implementation of `onEvent`:

```py
def on_event(event, context):
print(event)
print(event)
request_type = event['RequestType']
if request_type == 'Create': return on_create(event)
if request_type == 'Update': return on_update(event)
Expand All @@ -76,9 +76,9 @@ def on_create(event):

# add your create code here...
physical_id = ...

return { 'PhysicalResourceId': physical_id }

def on_update(event):
physical_id = event["PhysicalResourceId"]
props = event["ResourceProperties"]
Expand All @@ -104,8 +104,8 @@ def is_complete(event, context):
request_type = event["RequestType"]

# check if resource is stable based on request_type
is_ready = ...
is_ready = ...

return { 'IsComplete': is_ready }
```

Expand All @@ -121,7 +121,7 @@ If `onEvent` returns successfully, the framework will submit a "SUCCESS" respons
to AWS CloudFormation for this resource operation. If the provider is
[asynchronous](#asynchronous-providers-iscomplete) (`isCompleteHandler` is
defined), the framework will only submit a response based on the result of
`isComplete`.
`isComplete`.

If `onEvent` throws an error, the framework will submit a "FAILED" response to
AWS CloudFormation.
Expand Down Expand Up @@ -153,10 +153,10 @@ The return value from `onEvent` must be a JSON object with the following fields:

It is not uncommon for the provisioning of resources to be an asynchronous
operation, which means that the operation does not immediately finish, and we
need to "wait" until the resource stabilizes.
need to "wait" until the resource stabilizes.

The provider framework makes it easy to implement "waiters" by allowing users to
specify an additional AWS Lambda function in `isCompleteHandler`.
specify an additional AWS Lambda function in `isCompleteHandler`.

The framework will repeatedly invoke the handler every `queryInterval`. When
`isComplete` returns with `IsComplete: true`, the framework will submit a
Expand Down Expand Up @@ -230,7 +230,7 @@ lifecycle events:
### Execution Policy

Similarly to any AWS Lambda function, if the user-defined handlers require
access to AWS resources, you will have to define these permissions
access to AWS resources, you will have to define these permissions
by calling "grant" methods such as `myBucket.grantRead(myHandler)`), using `myHandler.addToRolePolicy`
or specifying an `initialPolicy` when defining the function.

Expand Down Expand Up @@ -353,7 +353,7 @@ const awsCustom1 = new AwsCustomResource(this, 'API1', {
onCreate: {
service: '...',
action: '...',
physicalResourceId: '...'
physicalResourceId: PhysicalResourceId.of('...')
}
});

Expand All @@ -364,7 +364,7 @@ const awsCustom2 = new AwsCustomResource(this, 'API2', {
parameters: {
text: awsCustom1.getDataString('Items.0.text')
},
physicalResourceId: '...'
physicalResourceId: PhysicalResourceId.of('...')
}
})
```
Expand All @@ -381,7 +381,7 @@ const verifyDomainIdentity = new AwsCustomResource(this, 'VerifyDomainIdentity',
parameters: {
Domain: 'example.com'
},
physicalResourceIdPath: 'VerificationToken' // Use the token returned by the call as physical id
physicalResourceId: PhysicalResourceId.fromResponse('VerificationToken') // Use the token returned by the call as physical id
}
});

Expand All @@ -403,7 +403,7 @@ const getParameter = new AwsCustomResource(this, 'GetParameter', {
Name: 'my-parameter',
WithDecryption: true
},
physicalResourceId: Date.now().toString() // Update physical id to always fetch the latest version
physicalResourceId: PhysicalResourceId.of(Date.now().toString()) // Update physical id to always fetch the latest version
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ export type AwsSdkMetadata = {[key: string]: any};

const awsSdkMetadata: AwsSdkMetadata = metadata;

/**
* Physical ID of the custom resource.
*/
export class PhysicalResourceId {

/**
* Extract the physical resource id from the path (dot notation) to the data in the API call response.
*/
public static fromResponse(responsePath: string): PhysicalResourceId {
return new PhysicalResourceId(responsePath, undefined);
}

/**
* Explicit physical resource id.
*/
public static of(id: string): PhysicalResourceId {
return new PhysicalResourceId(undefined, id);
}

/**
* @param responsePath Path to a response data element to be used as the physical id.
* @param id Literal string to be used as the physical id.
*/
private constructor(public readonly responsePath?: string, public readonly id?: string) { }
}

/**
* An AWS SDK call.
*/
Expand All @@ -42,22 +68,12 @@ export interface AwsSdkCall {
readonly parameters?: any;

/**
* The path to the data in the API call response to use as the physical
* resource id. Either `physicalResourceId` or `physicalResourceIdPath`
* must be specified for onCreate or onUpdate calls.
*
* @default - no path
*/
readonly physicalResourceIdPath?: string;

/**
* The physical resource id of the custom resource for this call. Either
* `physicalResourceId` or `physicalResourceIdPath` must be specified for
* onCreate or onUpdate calls.
* The physical resource id of the custom resource for this call.
* Mandatory for onCreate or onUpdate calls.
*
* @default - no physical resource id
*/
readonly physicalResourceId?: string;
readonly physicalResourceId?: PhysicalResourceId;

/**
* The regex pattern to use to catch API errors. The `code` property of the
Expand Down Expand Up @@ -174,8 +190,8 @@ export class AwsCustomResource extends cdk.Construct implements iam.IGrantable {
}

for (const call of [props.onCreate, props.onUpdate]) {
if (call && !call.physicalResourceId && !call.physicalResourceIdPath) {
throw new Error('Either `physicalResourceId` or `physicalResourceIdPath` must be specified for onCreate and onUpdate calls.');
if (call && !call.physicalResourceId) {
throw new Error('`physicalResourceId` must be specified for onCreate and onUpdate calls.');
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,14 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
let physicalResourceId: string;
switch (event.RequestType) {
case 'Create':
physicalResourceId = event.ResourceProperties.Create?.physicalResourceId ??
event.ResourceProperties.Update?.physicalResourceId ??
event.ResourceProperties.Delete?.physicalResourceId ??
physicalResourceId = event.ResourceProperties.Create?.physicalResourceId?.id ??
event.ResourceProperties.Update?.physicalResourceId?.id ??
event.ResourceProperties.Delete?.physicalResourceId?.id ??
event.LogicalResourceId;
break;
case 'Update':
case 'Delete':
physicalResourceId = event.ResourceProperties[event.RequestType]?.physicalResourceId ?? event.PhysicalResourceId;
physicalResourceId = event.ResourceProperties[event.RequestType]?.physicalResourceId?.id ?? event.PhysicalResourceId;
break;
}

Expand Down Expand Up @@ -127,8 +127,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
}
}

if (call.physicalResourceIdPath) {
physicalResourceId = flatData[call.physicalResourceIdPath];
if (call.physicalResourceId?.responsePath) {
physicalResourceId = flatData[call.physicalResourceId.responsePath];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as AWS from 'aws-sdk-mock';
import * as fs from 'fs-extra';
import * as nock from 'nock';
import * as sinon from 'sinon';
import { AwsSdkCall } from '../../lib';
import { AwsSdkCall, PhysicalResourceId } from '../../lib';
import { flatten, handler } from '../../lib/aws-custom-resource/runtime';

AWS.setSDK(require.resolve('aws-sdk'));
Expand Down Expand Up @@ -62,7 +62,7 @@ test('create event with physical resource id path', async () => {
parameters: {
Bucket: 'my-bucket'
},
physicalResourceIdPath: 'Contents.1.ETag'
physicalResourceId: PhysicalResourceId.fromResponse('Contents.1.ETag')
} as AwsSdkCall
}
};
Expand Down Expand Up @@ -101,7 +101,7 @@ test('update event with physical resource id', async () => {
Message: 'hello',
TopicArn: 'topicarn'
},
physicalResourceId: 'topicarn'
physicalResourceId: PhysicalResourceId.of('topicarn')
} as AwsSdkCall
}
};
Expand Down Expand Up @@ -133,7 +133,7 @@ test('delete event', async () => {
parameters: {
Bucket: 'my-bucket'
},
physicalResourceIdPath: 'Contents.1.ETag'
physicalResourceId: PhysicalResourceId.fromResponse('Contents.1.ETag')
} as AwsSdkCall
}
};
Expand Down Expand Up @@ -236,7 +236,7 @@ test('catch errors', async () => {
parameters: {
Bucket: 'my-bucket'
},
physicalResourceId: 'physicalResourceId',
physicalResourceId: PhysicalResourceId.of('physicalResourceId'),
catchErrorPattern: 'NoSuchBucket'
} as AwsSdkCall
}
Expand Down Expand Up @@ -283,7 +283,7 @@ test('decodes booleans', async () => {
},
}
},
physicalResourceId: 'put-item'
physicalResourceId: PhysicalResourceId.of('put-item')
} as AwsSdkCall
}
};
Expand Down Expand Up @@ -342,7 +342,7 @@ test('restrict output path', async () => {
parameters: {
Bucket: 'my-bucket'
},
physicalResourceId: 'id',
physicalResourceId: PhysicalResourceId.of('id'),
outputPath: 'Contents.0'
} as AwsSdkCall
}
Expand Down Expand Up @@ -379,7 +379,7 @@ test('can specify apiVersion and region', async () => {
},
apiVersion: '2010-03-31',
region: 'eu-west-1',
physicalResourceId: 'id',
physicalResourceId: PhysicalResourceId.of('id'),
} as AwsSdkCall
}
};
Expand Down Expand Up @@ -433,7 +433,7 @@ test('installs the latest SDK', async () => {
Message: 'message',
TopicArn: 'topic'
},
physicalResourceId: 'id',
physicalResourceId: PhysicalResourceId.of('id'),
} as AwsSdkCall
}
};
Expand Down
Loading

0 comments on commit aabd36d

Please sign in to comment.