diff --git a/packages/@aws-cdk/aws-amplify/README.md b/packages/@aws-cdk/aws-amplify/README.md index 059588e493241..ff644765e2a7f 100644 --- a/packages/@aws-cdk/aws-amplify/README.md +++ b/packages/@aws-cdk/aws-amplify/README.md @@ -206,5 +206,21 @@ const amplifyApp = new amplify.App(stack, 'App', { }, }, ], + }); +``` + +## Deploying Assets + +`sourceCodeProvider` is optional; when this is not specified the Amplify app can be deployed to using `.zip` packages. The `AmplifyAssetDeployment` construct can be used to deploy S3 assets to Amplify as part of the CDK: + +```ts +const asset = new assets.Asset(this, "SampleAsset", {}); +const amplifyApp = new amplify.App(this, 'MyApp', {}); +const branch = amplifyApp.addBranch("dev"); +new AmplifyAssetDeployment(this, "AmplifyAssetDeployment", { + app: amplifyApp, + branch: branch, + s3BucketName: asset.s3BucketName, + s3ObjectKey: asset.s3ObjectKey, }); ``` diff --git a/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/common.ts b/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/common.ts new file mode 100644 index 0000000000000..92c29e72c6768 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/common.ts @@ -0,0 +1,76 @@ +export interface AmplifyJobId { + /** + * If this field is included in an event passed to "IsComplete", it means we + * initiated an Amplify deployment that should be monitored using + * amplify:GetJob + */ + AmplifyJobId?: string; +} + +export type ResourceEvent = AWSLambda.CloudFormationCustomResourceEvent & AmplifyJobId; + +export interface IsCompleteResponse { + /** + * Indicates if the resource operation is complete or should we retry. + */ + readonly IsComplete: boolean; + + /** + * Additional/changes to resource attributes. + */ + readonly Data?: { [name: string]: any }; +}; + +export abstract class ResourceHandler { + protected readonly requestId: string; + protected readonly logicalResourceId: string; + protected readonly requestType: 'Create' | 'Update' | 'Delete'; + protected readonly physicalResourceId?: string; + protected readonly event: ResourceEvent; + + constructor(event: ResourceEvent) { + this.requestType = event.RequestType; + this.requestId = event.RequestId; + this.logicalResourceId = event.LogicalResourceId; + this.physicalResourceId = (event as any).PhysicalResourceId; + this.event = event; + } + + public onEvent() { + switch (this.requestType) { + case 'Create': + return this.onCreate(); + case 'Update': + return this.onUpdate(); + case 'Delete': + return this.onDelete(); + } + + throw new Error(`Invalid request type ${this.requestType}`); + } + + public isComplete() { + switch (this.requestType) { + case 'Create': + return this.isCreateComplete(); + case 'Update': + return this.isUpdateComplete(); + case 'Delete': + return this.isDeleteComplete(); + } + + throw new Error(`Invalid request type ${this.requestType}`); + } + + protected log(x: any) { + // eslint-disable-next-line no-console + console.log(JSON.stringify(x, undefined, 2)); + } + + protected abstract async onCreate(): Promise; + protected abstract async onDelete(): Promise; + protected abstract async onUpdate(): Promise; + protected abstract async isCreateComplete(): Promise; + protected abstract async isDeleteComplete(): Promise; + protected abstract async isUpdateComplete(): Promise; +} diff --git a/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/handler.ts b/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/handler.ts new file mode 100644 index 0000000000000..f1d615909ac27 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/handler.ts @@ -0,0 +1,137 @@ +// aws-sdk available at runtime for lambdas +// eslint-disable-next-line import/no-extraneous-dependencies +import { Amplify, S3 } from 'aws-sdk'; +import { AmplifyJobId, IsCompleteResponse, ResourceEvent, ResourceHandler } from './common'; + +export interface AmplifyAssetDeploymentProps { + AppId: string; + BranchName: string; + S3BucketName: string; + S3ObjectKey: string; + TimeoutSeconds: number; +} + +export class AmplifyAssetDeploymentHandler extends ResourceHandler { + private readonly props: AmplifyAssetDeploymentProps; + protected readonly amplify: Amplify; + protected readonly s3: S3; + + constructor(amplify: Amplify, s3: S3, event: ResourceEvent) { + super(event); + + this.props = parseProps(this.event.ResourceProperties); + this.amplify = amplify; + this.s3 = s3; + } + + // ------ + // CREATE + // ------ + + protected async onCreate(): Promise { + // eslint-disable-next-line no-console + console.log('deploying to Amplify with options:', JSON.stringify(this.props, undefined, 2)); + + // Verify no jobs are currently running. + const jobs = await this.amplify + .listJobs({ + appId: this.props.AppId, + branchName: this.props.BranchName, + maxResults: 1, + }) + .promise(); + + if ( + jobs.jobSummaries && + jobs.jobSummaries.length > 0 && + jobs.jobSummaries[0].status == 'PENDING' + ) { + return Promise.reject('Amplify job already running. Aborting deployment.'); + } + + // Create a pre-signed get URL of the asset so Amplify can retrieve it. + const assetUrl = this.s3.getSignedUrl('getObject', { + Bucket: this.props.S3BucketName, + Key: this.props.S3ObjectKey, + }); + + // Deploy the asset to Amplify. + const deployment = await this.amplify + .startDeployment({ + appId: this.props.AppId, + branchName: this.props.BranchName, + sourceUrl: assetUrl, + }) + .promise(); + + return { + AmplifyJobId: deployment.jobSummary.jobId, + }; + } + + protected async isCreateComplete() { + return this.isActive(this.event.AmplifyJobId); + } + + // ------ + // DELETE + // ------ + + protected async onDelete(): Promise { + // We can't delete this resource as it's a deployment. + return; + } + + protected async isDeleteComplete(): Promise { + // We can't delete this resource as it's a deployment. + return { + IsComplete: true, + }; + } + + // ------ + // UPDATE + // ------ + + protected async onUpdate() { + return this.onCreate(); + } + + protected async isUpdateComplete() { + return this.isActive(this.event.AmplifyJobId); + } + + private async isActive(jobId?: string): Promise { + if (!jobId) { + throw new Error('Unable to determine Amplify job status without job id'); + } + + const job = await this.amplify + .getJob({ + appId: this.props.AppId, + branchName: this.props.BranchName, + jobId: jobId, + }) + .promise(); + + if (job.job.summary.status === 'SUCCEED') { + return { + IsComplete: true, + Data: { + JobId: jobId, + Status: job.job.summary.status, + }, + }; + } if (job.job.summary.status === 'FAILED' || job.job.summary.status === 'CANCELLED') { + throw new Error(`Amplify job failed with status: ${job.job.summary.status}`); + } else { + return { + IsComplete: false, + }; + } + } +} + +function parseProps(props: any): AmplifyAssetDeploymentProps { + return props; +} diff --git a/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/index.ts b/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/index.ts new file mode 100644 index 0000000000000..ebb589bb3f847 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/index.ts @@ -0,0 +1,35 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { IsCompleteResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types'; +// aws-sdk available at runtime for lambdas +// eslint-disable-next-line import/no-extraneous-dependencies +import { Amplify, S3, config } from 'aws-sdk'; +import { ResourceEvent } from './common'; +import { AmplifyAssetDeploymentHandler } from './handler'; + +const AMPLIFY_ASSET_DEPLOYMENT_RESOURCE_TYPE = 'Custom::AmplifyAssetDeployment'; + +config.logger = console; + +const amplify = new Amplify(); +const s3 = new S3({ signatureVersion: 'v4' }); + +export async function onEvent(event: ResourceEvent) { + const provider = createResourceHandler(event); + return provider.onEvent(); +} + +export async function isComplete( + event: ResourceEvent, +): Promise { + const provider = createResourceHandler(event); + return provider.isComplete(); +} + +function createResourceHandler(event: ResourceEvent) { + switch (event.ResourceType) { + case AMPLIFY_ASSET_DEPLOYMENT_RESOURCE_TYPE: + return new AmplifyAssetDeploymentHandler(amplify, s3, event); + default: + throw new Error(`Unsupported resource type "${event.ResourceType}"`); + } +} diff --git a/packages/@aws-cdk/aws-amplify/lib/asset-deployment.ts b/packages/@aws-cdk/aws-amplify/lib/asset-deployment.ts new file mode 100644 index 0000000000000..aa5a3f8800256 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/lib/asset-deployment.ts @@ -0,0 +1,140 @@ +import * as path from 'path'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs'; +import { CustomResource, Duration, NestedStack, Stack } from '@aws-cdk/core'; + +import { Provider } from '@aws-cdk/custom-resources'; + +import { Construct } from 'constructs'; +import { IApp } from './app'; +import { IBranch } from './branch'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties for AmplifyAssetDeployment + */ +export interface AmplifyAssetDeploymentProps { + /** + * The Amplify app to deploy to. + */ + readonly app: IApp; + + /** + * The Amplify branch to deploy to. + */ + readonly branch: IBranch; + + /** + * The s3 bucket of the asset. + */ + readonly s3BucketName: string; + + /** + * The s3 object key of the asset. + */ + readonly s3ObjectKey: string; +} + +/** + * Allows deployment of S3 assets to Amplify via a custom resource. + * + * The Amplify app must not have a sourceCodeProvider configured as this resource uses Amplify's + * startDeployment API to initiate and deploy a S3 asset onto the App. + */ +export class AmplifyAssetDeployment extends CoreConstruct { + constructor( + scope: Construct, + id: string, + props: AmplifyAssetDeploymentProps, + ) { + super(scope, id); + + new CustomResource(this, 'Resource', { + serviceToken: AmplifyAssetDeploymentProvider.getOrCreate(this), + resourceType: 'Custom::AmplifyAssetDeployment', + properties: { + AppId: props.app.appId, + BranchName: props.branch.branchName, + S3ObjectKey: props.s3ObjectKey, + S3BucketName: props.s3BucketName, + }, + }); + } +} + +class AmplifyAssetDeploymentProvider extends NestedStack { + /** + * Returns the singleton provider. + */ + public static getOrCreate(scope: Construct) { + const providerId = + 'com.amazonaws.cdk.custom-resources.amplify-asset-deployment-provider'; + const stack = Stack.of(scope); + const group = + (stack.node.tryFindChild(providerId) as AmplifyAssetDeploymentProvider) ?? new AmplifyAssetDeploymentProvider(stack, providerId); + return group.provider.serviceToken; + } + + private readonly provider: Provider; + + constructor(scope: Construct, id: string) { + super(scope, id); + + const onEvent = new NodejsFunction( + this, + 'amplify-asset-deployment-on-event', + { + entry: path.join( + __dirname, + 'asset-deployment-handler/index.ts', + ), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'onEvent', + initialPolicy: [ + new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 's3:GetObject', + 's3:GetSignedUrl', + 'amplify:ListJobs', + 'amplify:StartDeployment', + ], + }), + ], + }, + ); + + const isComplete = new NodejsFunction( + this, + 'amplify-asset-deployment-is-complete', + { + entry: path.join( + __dirname, + 'asset-deployment-handler/index.ts', + ), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'isComplete', + initialPolicy: [ + new iam.PolicyStatement({ + resources: ['*'], + actions: ['amplify:GetJob*'], + }), + ], + }, + ); + + this.provider = new Provider( + this, + 'amplify-asset-deployment-handler-provider', + { + onEventHandler: onEvent, + isCompleteHandler: isComplete, + totalTimeout: Duration.minutes(5), + }, + ); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-amplify/lib/index.ts b/packages/@aws-cdk/aws-amplify/lib/index.ts index 5a89be4af844c..eda383d8578e8 100644 --- a/packages/@aws-cdk/aws-amplify/lib/index.ts +++ b/packages/@aws-cdk/aws-amplify/lib/index.ts @@ -3,6 +3,7 @@ export * from './branch'; export * from './domain'; export * from './basic-auth'; export * from './source-code-providers'; +export * from './asset-deployment'; // AWS::Amplify CloudFormation Resources: export * from './amplify.generated'; diff --git a/packages/@aws-cdk/aws-amplify/package.json b/packages/@aws-cdk/aws-amplify/package.json index 7ee41fd004517..3c1fe679b4a92 100644 --- a/packages/@aws-cdk/aws-amplify/package.json +++ b/packages/@aws-cdk/aws-amplify/package.json @@ -80,15 +80,20 @@ "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^26.0.24", - "@types/yaml": "1.9.6" + "@types/yaml": "1.9.6", + "aws-sdk": "^2.848.0" }, "dependencies": { "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-lambda-nodejs": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.3.69", "yaml": "1.10.2" }, @@ -100,8 +105,12 @@ "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-lambda-nodejs": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.3.69" }, "engines": { diff --git a/packages/@aws-cdk/aws-amplify/test/asset-deployment-handler/index.test.ts b/packages/@aws-cdk/aws-amplify/test/asset-deployment-handler/index.test.ts new file mode 100644 index 0000000000000..ad40018672e23 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/test/asset-deployment-handler/index.test.ts @@ -0,0 +1,741 @@ +const getSignedUrlResponse = jest.fn(); +const mockS3 = { + getSignedUrl: getSignedUrlResponse, +}; +const listJobsResponse = jest.fn(); +const listJobsRequest = jest.fn().mockImplementation(() => { + return { + promise: listJobsResponse, + }; +}); +const startDeploymentResponse = jest.fn(); +const startDeploymentRequest = jest.fn().mockImplementation(() => { + return { + promise: startDeploymentResponse, + }; +}); +const getJobResponse = jest.fn(); +const getJobRequest = jest.fn().mockImplementation(() => { + return { + promise: getJobResponse, + }; +}); +const mockAmplify = { + listJobs: listJobsRequest, + startDeployment: startDeploymentRequest, + getJob: getJobRequest, +}; + +jest.mock('aws-sdk', () => { + return { + S3: jest.fn(() => mockS3), + Amplify: jest.fn(() => mockAmplify), + config: { logger: '' }, + }; +}); + +import { + onEvent, + isComplete, +} from '../../lib/asset-deployment-handler'; + +describe('handler', () => { + + let oldConsoleLog: any; + + beforeAll(() => { + oldConsoleLog = global.console.log; + global.console.log = jest.fn(); + }); + + afterAll(() => { + global.console.log = oldConsoleLog; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('onEvent CREATE success', async () => { + // GIVEN + listJobsResponse.mockImplementation(() => { + return { + jobSummaries: [], + }; + }); + getSignedUrlResponse.mockImplementation(() => { + return 'signedUrlValue'; + }); + startDeploymentResponse.mockImplementation(() => { + return { + jobSummary: { jobId: 'jobIdValue' }, + }; + }); + + // WHEN + const response = await onEvent({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Create', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + }); + + // THEN + expect(response).toEqual({ + AmplifyJobId: 'jobIdValue', + }); + + expect(listJobsRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + maxResults: 1, + }); + expect(listJobsResponse).toBeCalled(); + expect(getSignedUrlResponse).toHaveBeenCalledWith('getObject', { + Bucket: 's3BucketNameValue', + Key: 's3ObjectKeyValue', + }); + expect(startDeploymentRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + sourceUrl: 'signedUrlValue', + }); + expect(startDeploymentResponse).toBeCalled(); + }); + + it('onEvent CREATE pending job', async () => { + // GIVEN + listJobsResponse.mockImplementation(() => { + return { + jobSummaries: [{ status: 'PENDING' }], + }; + }); + + // WHEN + await expect(() => onEvent({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Create', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + })).rejects.toMatch('Amplify job already running. Aborting deployment.'); + + expect(listJobsRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + maxResults: 1, + }); + expect(listJobsResponse).toBeCalled(); + expect(getSignedUrlResponse).not.toHaveBeenCalled(); + expect(startDeploymentRequest).not.toHaveBeenCalled(); + expect(startDeploymentResponse).not.toHaveBeenCalled(); + }); + + it('isComplete CREATE success', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'SUCCEED' } }, + }; + }); + + // WHEN + const response = await isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Create', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + }); + + // THEN + expect(response).toEqual({ + Data: { + JobId: 'amplifyJobIdValue', + Status: 'SUCCEED', + }, + IsComplete: true, + }); + + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete CREATE pending', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'PENDING' } }, + }; + }); + + // WHEN + const response = await isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Create', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + }); + + // THEN + expect(response).toEqual({ + IsComplete: false, + }); + + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete CREATE failed', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'FAILED' } }, + }; + }); + + // WHEN + await expect(() => isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Create', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + })).rejects.toThrow('Amplify job failed with status: FAILED'); + // THEN + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete CREATE cancelled', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'CANCELLED' } }, + }; + }); + + // WHEN + await expect(() => isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Create', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + })).rejects.toThrow('Amplify job failed with status: CANCELLED'); + + // THEN + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete CREATE no JobId', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'PENDING' } }, + }; + }); + + // WHEN + await expect(() => isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Create', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + })).rejects.toThrow('Unable to determine Amplify job status without job id'); + + // THEN + expect(getJobRequest).not.toHaveBeenCalled(); + expect(getJobResponse).not.toHaveBeenCalled(); + }); + + it('onEvent UPDATE success', async () => { + // GIVEN + listJobsResponse.mockImplementation(() => { + return { + jobSummaries: [], + }; + }); + getSignedUrlResponse.mockImplementation(() => { + return 'signedUrlValue'; + }); + startDeploymentResponse.mockImplementation(() => { + return { + jobSummary: { jobId: 'jobIdValue' }, + }; + }); + + // WHEN + const response = await onEvent({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: { ServiceToken: 'serviceTokenValue' }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + }); + + // THEN + expect(response).toEqual({ + AmplifyJobId: 'jobIdValue', + }); + + expect(listJobsRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + maxResults: 1, + }); + expect(listJobsResponse).toBeCalled(); + expect(getSignedUrlResponse).toHaveBeenCalledWith('getObject', { + Bucket: 's3BucketNameValue', + Key: 's3ObjectKeyValue', + }); + expect(startDeploymentRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + sourceUrl: 'signedUrlValue', + }); + expect(startDeploymentResponse).toBeCalled(); + }); + + it('onEvent UPDATE pending job', async () => { + // GIVEN + listJobsResponse.mockImplementation(() => { + return { + jobSummaries: [{ status: 'PENDING' }], + }; + }); + + // WHEN + await expect(() => onEvent({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: { ServiceToken: 'serviceTokenValue' }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + })).rejects.toMatch('Amplify job already running. Aborting deployment.'); + + // THEN + expect(listJobsRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + maxResults: 1, + }); + expect(listJobsResponse).toBeCalled(); + expect(getSignedUrlResponse).not.toHaveBeenCalled(); + expect(startDeploymentRequest).not.toHaveBeenCalled(); + expect(startDeploymentResponse).not.toHaveBeenCalled(); + }); + + it('isComplete UPDATE success', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'SUCCEED' } }, + }; + }); + + // WHEN + const response = await isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: {}, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + }); + + // THEN + expect(response).toEqual({ + Data: { + JobId: 'amplifyJobIdValue', + Status: 'SUCCEED', + }, + IsComplete: true, + }); + + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete UPDATE pending', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'PENDING' } }, + }; + }); + + // WHEN + const response = await isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: {}, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + }); + + // THEN + expect(response).toEqual({ + IsComplete: false, + }); + + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete UPDATE failed', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'FAILED' } }, + }; + }); + + // WHEN + await expect(() => isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: {}, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + })).rejects.toThrow('Amplify job failed with status: FAILED'); + // THEN + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete UPDATE cancelled', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'CANCELLED' } }, + }; + }); + + // WHEN + await expect(() => isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: {}, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + AmplifyJobId: 'amplifyJobIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + })).rejects.toThrow('Amplify job failed with status: CANCELLED'); + + // THEN + expect(getJobRequest).toHaveBeenCalledWith({ + appId: 'appIdValue', + branchName: 'branchNameValue', + jobId: 'amplifyJobIdValue', + }); + expect(getJobResponse).toBeCalled(); + }); + + it('isComplete UPDATE no JobId', async () => { + // GIVEN + getJobResponse.mockImplementation(() => { + return { + job: { summary: { status: 'PENDING' } }, + }; + }); + + // WHEN + await expect(() => isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: {}, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + })).rejects.toThrow('Unable to determine Amplify job status without job id'); + + // THEN + expect(getJobRequest).not.toHaveBeenCalled(); + expect(getJobResponse).not.toHaveBeenCalled(); + }); + + it('onEvent DELETE success', async () => { + // GIVEN + + // WHEN + await expect(() => onEvent({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Delete', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + })).resolves; + }); + + it('isComplete DELETE success', async () => { + // GIVEN + + // WHEN + const response = await isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Delete', + ResourceType: 'Custom::AmplifyAssetDeployment', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + }); + + // THEN + expect(response).toEqual({ + IsComplete: true, + }); + }); + + it('onEvent unsupported resource type', async () => { + // GIVEN + + // WHEN + await expect(() => onEvent({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::BadResourceType', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: {}, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + })).rejects.toThrow('Unsupported resource type "Custom::BadResourceType"'); + + + // THEN + expect(getJobRequest).not.toHaveBeenCalled(); + expect(getJobResponse).not.toHaveBeenCalled(); + }); + + it('isComplete unsupported resource type', async () => { + // GIVEN + + // WHEN + await expect(() => isComplete({ + ServiceToken: 'serviceTokenValue', + RequestType: 'Update', + ResourceType: 'Custom::BadResourceType', + ResourceProperties: { + ServiceToken: 'serviceTokenValue', + AppId: 'appIdValue', + BranchName: 'branchNameValue', + S3BucketName: 's3BucketNameValue', + S3ObjectKey: 's3ObjectKeyValue', + }, + OldResourceProperties: {}, + ResponseURL: 'responseUrlValue', + StackId: 'stackIdValue', + RequestId: 'requestIdValue', + LogicalResourceId: 'logicalResourceIdValue', + PhysicalResourceId: 'physicalResourceIdValue', + })).rejects.toThrow('Unsupported resource type "Custom::BadResourceType"'); + + // THEN + expect(getJobRequest).not.toHaveBeenCalled(); + expect(getJobResponse).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-amplify/test/asset-deployment.test.ts b/packages/@aws-cdk/aws-amplify/test/asset-deployment.test.ts new file mode 100644 index 0000000000000..63dae2ddfeffd --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/test/asset-deployment.test.ts @@ -0,0 +1,53 @@ +import { Template } from '@aws-cdk/assertions'; +import { SecretValue, Stack } from '@aws-cdk/core'; +import { App, GitHubSourceCodeProvider, IBranch } from '../lib'; +import { AmplifyAssetDeployment } from '../lib/asset-deployment'; + +let stack: Stack; +let app: App; +let branch: IBranch; +beforeEach(() => { + stack = new Stack(); + app = new App(stack, 'App', { + sourceCodeProvider: new GitHubSourceCodeProvider({ + owner: 'aws', + repository: 'aws-cdk', + oauthToken: SecretValue.plainText('secret'), + }), + }); + branch = app.addBranch('master'); +}); + +test('Creates custom resource', () => { + // WHEN + new AmplifyAssetDeployment(stack, 'Test', { + app: app, + branch: branch, + s3ObjectKey: 's3ObjectKeyValue', + s3BucketName: 's3BucketNameValue', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::AmplifyAssetDeployment', { + ServiceToken: { + 'Fn::GetAtt': [ + 'comamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackcomamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackResource89BDFEB2', + 'Outputs.comamazonawscdkcustomresourcesamplifyassetdeploymentprovideramplifyassetdeploymenthandlerproviderframeworkonEventA449D9A9Arn', + ], + }, + AppId: { + 'Fn::GetAtt': [ + 'AppF1B96344', + 'AppId', + ], + }, + BranchName: { + 'Fn::GetAtt': [ + 'Appmaster71597E87', + 'BranchName', + ], + }, + S3ObjectKey: 's3ObjectKeyValue', + S3BucketName: 's3BucketNameValue', + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.expected.json b/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.expected.json new file mode 100644 index 0000000000000..78c310ad423ed --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.expected.json @@ -0,0 +1,228 @@ +{ + "Parameters": { + "AssetParameters76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7S3Bucket3C55BA0F": { + "Type": "String", + "Description": "S3 bucket for asset \"76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7\"" + }, + "AssetParameters76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7S3VersionKeyE1E2D7D6": { + "Type": "String", + "Description": "S3 key for asset version \"76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7\"" + }, + "AssetParameters76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7ArtifactHashB1665559": { + "Type": "String", + "Description": "Artifact hash for asset \"76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7\"" + }, + "AssetParameters5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534S3BucketA04DEC97": { + "Type": "String", + "Description": "S3 bucket for asset \"5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534\"" + }, + "AssetParameters5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534S3VersionKey407A05FE": { + "Type": "String", + "Description": "S3 key for asset version \"5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534\"" + }, + "AssetParameters5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534ArtifactHashFB218162": { + "Type": "String", + "Description": "Artifact hash for asset \"5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { + "Type": "String", + "Description": "S3 bucket for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F": { + "Type": "String", + "Description": "S3 key for asset version \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1ArtifactHashA521A16F": { + "Type": "String", + "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5S3BucketA91B865D": { + "Type": "String", + "Description": "S3 bucket for asset \"ab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5\"" + }, + "AssetParametersab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5S3VersionKey2801B03E": { + "Type": "String", + "Description": "S3 key for asset version \"ab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5\"" + }, + "AssetParametersab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5ArtifactHash7A2C8ABD": { + "Type": "String", + "Description": "Artifact hash for asset \"ab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5\"" + } + }, + "Resources": { + "AppRole1AF9B530": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "amplify.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AppF1B96344": { + "Type": "AWS::Amplify::App", + "Properties": { + "Name": "App", + "BasicAuthConfig": { + "EnableBasicAuth": false + }, + "IAMServiceRole": { + "Fn::GetAtt": [ + "AppRole1AF9B530", + "Arn" + ] + } + } + }, + "AppmainF505BAED": { + "Type": "AWS::Amplify::Branch", + "Properties": { + "AppId": { + "Fn::GetAtt": [ + "AppF1B96344", + "AppId" + ] + }, + "BranchName": "main", + "EnableAutoBuild": true, + "EnablePullRequestPreview": true + } + }, + "AmplifyAssetDeploymentB1E434BC": { + "Type": "Custom::AmplifyAssetDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "comamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackcomamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackResource89BDFEB2", + "Outputs.cdkamplifyappassetdeploymentcomamazonawscdkcustomresourcesamplifyassetdeploymentprovideramplifyassetdeploymenthandlerproviderframeworkonEventC3C43E44Arn" + ] + }, + "AppId": { + "Fn::GetAtt": [ + "AppF1B96344", + "AppId" + ] + }, + "BranchName": { + "Fn::GetAtt": [ + "AppmainF505BAED", + "BranchName" + ] + }, + "S3ObjectKey": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7S3VersionKeyE1E2D7D6" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7S3VersionKeyE1E2D7D6" + } + ] + } + ] + } + ] + ] + }, + "S3BucketName": { + "Ref": "AssetParameters76c74dffba7c3eb9a040dc95633eac403472969bf8a18831ac1cf243971c5bf7S3Bucket3C55BA0F" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "comamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackcomamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackResource89BDFEB2": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5S3BucketA91B865D" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5S3VersionKey2801B03E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersab3f82d514665bfd00afd85c9f237c60cd485d85105e720d590337be1e1195d5S3VersionKey2801B03E" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetocdkamplifyappassetdeploymentAssetParameters5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534S3Bucket2B96434BRef": { + "Ref": "AssetParameters5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534S3BucketA04DEC97" + }, + "referencetocdkamplifyappassetdeploymentAssetParameters5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534S3VersionKey3BF1FA38Ref": { + "Ref": "AssetParameters5be1647636645f2d81be4ceb5a37c547518860999133c787b5b5156c8b776534S3VersionKey407A05FE" + }, + "referencetocdkamplifyappassetdeploymentAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketA5B3B03BRef": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetocdkamplifyappassetdeploymentAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey61CE3542Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" + } + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.ts b/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.ts new file mode 100644 index 0000000000000..f4473e8fb47d3 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.ts @@ -0,0 +1,30 @@ +/// !cdk-integ pragma:ignore-assets +import * as path from 'path'; +import { Asset } from '@aws-cdk/aws-s3-assets'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as amplify from '../lib'; +import { AmplifyAssetDeployment } from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const asset = new Asset(this, 'SampleAsset', { + path: path.join(__dirname, './test-asset'), + }); + + const amplifyApp = new amplify.App(this, 'App', {}); + const branch = amplifyApp.addBranch('main'); + new AmplifyAssetDeployment(this, 'AmplifyAssetDeployment', { + app: amplifyApp, + branch: branch, + s3BucketName: asset.s3BucketName, + s3ObjectKey: asset.s3ObjectKey, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-amplify-app-asset-deployment'); +app.synth(); diff --git a/packages/@aws-cdk/aws-amplify/test/test-asset/index.html b/packages/@aws-cdk/aws-amplify/test/test-asset/index.html new file mode 100644 index 0000000000000..df6334c42a684 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/test/test-asset/index.html @@ -0,0 +1 @@ +Hello world! I am deployed on AWS Amplify using the AmplifyAssetDeployment construct. \ No newline at end of file