diff --git a/packages/@aws-cdk/aws-amplify/README.md b/packages/@aws-cdk/aws-amplify/README.md index 059588e493241..c015bd38494e8 100644 --- a/packages/@aws-cdk/aws-amplify/README.md +++ b/packages/@aws-cdk/aws-amplify/README.md @@ -208,3 +208,13 @@ 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 `asset` property 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", { asset: asset }); +``` 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..9577bb3049986 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/handler.ts @@ -0,0 +1,136 @@ +// 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.find(summary => summary.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/branch.ts b/packages/@aws-cdk/aws-amplify/lib/branch.ts index 210f27d4831e2..05890ec2895a9 100644 --- a/packages/@aws-cdk/aws-amplify/lib/branch.ts +++ b/packages/@aws-cdk/aws-amplify/lib/branch.ts @@ -1,5 +1,19 @@ +import * as path from 'path'; import * as codebuild from '@aws-cdk/aws-codebuild'; -import { IResource, Lazy, Resource } from '@aws-cdk/core'; +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 { Asset } from '@aws-cdk/aws-s3-assets'; +import { + CustomResource, + IResource, + Lazy, + Resource, + Duration, + NestedStack, + Stack, +} from '@aws-cdk/core'; +import { Provider } from '@aws-cdk/custom-resources'; import { Construct } from 'constructs'; import { CfnBranch } from './amplify.generated'; import { IApp } from './app'; @@ -90,6 +104,16 @@ export interface BranchOptions { * @default - no stage */ readonly stage?: string; + + /** + * Asset for deployment. + * + * 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. + * + * @default - no asset + */ + readonly asset?: Asset } /** @@ -148,6 +172,19 @@ export class Branch extends Resource implements IBranch { this.arn = branch.attrArn; this.branchName = branch.attrBranchName; + + if (props.asset) { + new CustomResource(this, 'DeploymentResource', { + serviceToken: AmplifyAssetDeploymentProvider.getOrCreate(this), + resourceType: 'Custom::AmplifyAssetDeployment', + properties: { + AppId: props.app.appId, + BranchName: branchName, + S3ObjectKey: props.asset.s3ObjectKey, + S3BucketName: props.asset.s3BucketName, + }, + }); + } } /** @@ -161,3 +198,76 @@ export class Branch extends Resource implements IBranch { return this; } } + +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), + }, + ); + } +} diff --git a/packages/@aws-cdk/aws-amplify/package.json b/packages/@aws-cdk/aws-amplify/package.json index aa5e8cb02ca74..f5903f7a8ca74 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": "^27.0.3", - "@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/branch.test.ts b/packages/@aws-cdk/aws-amplify/test/branch.test.ts index 1638157eb4b14..ba8e517205beb 100644 --- a/packages/@aws-cdk/aws-amplify/test/branch.test.ts +++ b/packages/@aws-cdk/aws-amplify/test/branch.test.ts @@ -1,4 +1,6 @@ +import * as path from 'path'; import { Template } from '@aws-cdk/assertions'; +import { Asset } from '@aws-cdk/aws-s3-assets'; import { SecretValue, Stack } from '@aws-cdk/core'; import * as amplify from '../lib'; @@ -99,3 +101,64 @@ test('with env vars', () => { ], }); }); + +test('with asset deployment', () => { + // WHEN + const asset = new Asset(app, 'SampleAsset', { + path: path.join(__dirname, './test-asset'), + }); + app.addBranch('dev', { asset }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::AmplifyAssetDeployment', { + ServiceToken: { + 'Fn::GetAtt': [ + 'comamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackcomamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackResource89BDFEB2', + 'Outputs.comamazonawscdkcustomresourcesamplifyassetdeploymentprovideramplifyassetdeploymenthandlerproviderframeworkonEventA449D9A9Arn', + ], + }, + AppId: { + 'Fn::GetAtt': [ + 'AppF1B96344', + 'AppId', + ], + }, + BranchName: 'dev', + S3ObjectKey: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters8c89eadc6be22019c81ed6b9c7d9929ae10de55679fd8e0e9fd4c00f8edc1cdaS3VersionKey70C0B407', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters8c89eadc6be22019c81ed6b9c7d9929ae10de55679fd8e0e9fd4c00f8edc1cdaS3VersionKey70C0B407', + }, + ], + }, + ], + }, + ], + ], + }, + S3BucketName: { + Ref: 'AssetParameters8c89eadc6be22019c81ed6b9c7d9929ae10de55679fd8e0e9fd4c00f8edc1cdaS3Bucket83484C89', + }, + }); +}); 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..47b29583b6a21 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.expected.json @@ -0,0 +1,223 @@ +{ + "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\"" + }, + "AssetParametersff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56S3Bucket7A871D89": { + "Type": "String", + "Description": "S3 bucket for asset \"ff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56\"" + }, + "AssetParametersff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56S3VersionKeyAACF81DD": { + "Type": "String", + "Description": "S3 key for asset version \"ff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56\"" + }, + "AssetParametersff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56ArtifactHash2A4E644A": { + "Type": "String", + "Description": "Artifact hash for asset \"ff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56\"" + }, + "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\"" + }, + "AssetParametersa1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2ddS3Bucket456FC783": { + "Type": "String", + "Description": "S3 bucket for asset \"a1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2dd\"" + }, + "AssetParametersa1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2ddS3VersionKey4A933266": { + "Type": "String", + "Description": "S3 key for asset version \"a1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2dd\"" + }, + "AssetParametersa1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2ddArtifactHash7857C55E": { + "Type": "String", + "Description": "Artifact hash for asset \"a1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2dd\"" + } + }, + "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 + } + }, + "AppmainDeploymentResource442DE93D": { + "Type": "Custom::AmplifyAssetDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "comamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackcomamazonawscdkcustomresourcesamplifyassetdeploymentproviderNestedStackResource89BDFEB2", + "Outputs.cdkamplifyappassetdeploymentcomamazonawscdkcustomresourcesamplifyassetdeploymentprovideramplifyassetdeploymenthandlerproviderframeworkonEventC3C43E44Arn" + ] + }, + "AppId": { + "Fn::GetAtt": [ + "AppF1B96344", + "AppId" + ] + }, + "BranchName": "main", + "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": "AssetParametersa1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2ddS3Bucket456FC783" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersa1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2ddS3VersionKey4A933266" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersa1ec2b3c34d7ba5b1816474781916bb1c8a8086a266e6d7cf88a0720b114d2ddS3VersionKey4A933266" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetocdkamplifyappassetdeploymentAssetParametersff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56S3BucketA0EDA7B5Ref": { + "Ref": "AssetParametersff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56S3Bucket7A871D89" + }, + "referencetocdkamplifyappassetdeploymentAssetParametersff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56S3VersionKeyD32C918ARef": { + "Ref": "AssetParametersff9527128e3cc60cee11deb3d533504348f62709c853288178d757494fd92c56S3VersionKeyAACF81DD" + }, + "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..976b6c441af47 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/test/integ.app-asset-deployment.ts @@ -0,0 +1,23 @@ +/// !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'; + +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', {}); + amplifyApp.addBranch('main', { asset }); + } +} + +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..c6b3e17625253 --- /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 addAssetDeployment method! \ No newline at end of file