Skip to content
This repository has been archived by the owner on Mar 20, 2023. It is now read-only.

Commit

Permalink
feat(amplify): Add Amplify asset deployment resource
Browse files Browse the repository at this point in the history
This change adds a custom resource that allows users
to publish S3 assets to AWS Amplify.

fixes aws#16208
  • Loading branch information
samkio committed Oct 13, 2021
1 parent 7c73880 commit 622fa23
Show file tree
Hide file tree
Showing 12 changed files with 1,522 additions and 1 deletion.
16 changes: 16 additions & 0 deletions packages/@aws-cdk/aws-amplify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,19 @@ const amplifyApp = new amplify.App(this, 'MyApp', {
autoBranchDeletion: true, // Automatically disconnect a branch when you delete a branch from your repository
});
```

## 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,
});
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { IsCompleteResponse, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types';
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 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<OnEventResponse>;
protected abstract async onDelete(): Promise<OnEventResponse | void>;
protected abstract async onUpdate(): Promise<(OnEventResponse & AmplifyJobId) | void>;
protected abstract async isCreateComplete(): Promise<IsCompleteResponse>;
protected abstract async isDeleteComplete(): Promise<IsCompleteResponse>;
protected abstract async isUpdateComplete(): Promise<IsCompleteResponse>;
}
140 changes: 140 additions & 0 deletions packages/@aws-cdk/aws-amplify/lib/asset-deployment-handler/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { IsCompleteResponse, OnEventResponse } 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 } from 'aws-sdk';
import { 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<OnEventResponse> {
// 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<OnEventResponse> {
// We can't delete this resource as it's a deployment.
return {};
}

protected async isDeleteComplete(): Promise<IsCompleteResponse> {
// 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<IsCompleteResponse> {
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;
}
Original file line number Diff line number Diff line change
@@ -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<IsCompleteResponse> {
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}"`);
}
}
Loading

0 comments on commit 622fa23

Please sign in to comment.