Skip to content

Commit

Permalink
feat: honour cdn-site-hosting source deployment order (#53)
Browse files Browse the repository at this point in the history
* feat(cdn-site-hosting): honour source deployment order

When deploying assets to S3 via CDK, ensure the order of the sources given in config is honoured.

This allows routed SPAs to explicitly deploy the websiteIndexDocument last, after all static assets have been deployed.

* refactor: move validation to validateProps function

* fix: typo

* test: s3 deployments are configured sequentially
  • Loading branch information
Camille Fenton authored May 20, 2022
1 parent 7b3a98d commit 912b6da
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 19 deletions.
48 changes: 29 additions & 19 deletions lib/cdn-site-hosting/cdn-site-hosting-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ export class CdnSiteHostingConstruct extends cdk.Construct {
) {
super(scope, id);

if (props.websiteIndexDocument.startsWith("/")) {
throw Error("leading slashes are not allowed in websiteIndexDocument");
}

validateProps(props);

const siteDomain = getSiteDomain(props);
Expand Down Expand Up @@ -109,7 +105,7 @@ export class CdnSiteHostingConstruct extends cdk.Construct {
props.sourcesWithDeploymentOptions.length === 1;

// multiple sources with granular cache and invalidation control
props.sourcesWithDeploymentOptions.forEach(
const deployments = props.sourcesWithDeploymentOptions.map(
(
{ name, sources, distributionPathsToInvalidate, cacheControl },
index
Expand All @@ -120,20 +116,30 @@ export class CdnSiteHostingConstruct extends cdk.Construct {

const nameOrIndex = name ? name : `${index}`;

new s3deploy.BucketDeployment(this, `CustomDeploy${nameOrIndex}`, {
cacheControl,
sources: sources,
prune: isSingleDeploymentStep,
destinationBucket: this.s3Bucket,
distribution: isInvalidationRequired
? this.cloudfrontWebDistribution
: undefined,
distributionPaths: isInvalidationRequired
? distributionPathsToInvalidate
: undefined,
});
return new s3deploy.BucketDeployment(
this,
`CustomDeploy${nameOrIndex}`,
{
cacheControl,
sources: sources,
prune: isSingleDeploymentStep,
destinationBucket: this.s3Bucket,
distribution: isInvalidationRequired
? this.cloudfrontWebDistribution
: undefined,
distributionPaths: isInvalidationRequired
? distributionPathsToInvalidate
: undefined,
}
);
}
);

deployments.forEach((deployment, deploymentIndex) => {
if (deploymentIndex > 0) {
deployment.node.addDependency(deployments[deploymentIndex - 1]);
}
});
} else if (props.sources) {
// multiple sources, with default cache-control and wholesale invalidation
new s3deploy.BucketDeployment(this, "DeployAndInvalidate", {
Expand All @@ -147,9 +153,9 @@ export class CdnSiteHostingConstruct extends cdk.Construct {
}

function validateProps(props: CdnSiteHostingConstructProps): void {
const { sources, sourcesWithDeploymentOptions } = props;
const { sources, sourcesWithDeploymentOptions, websiteIndexDocument } = props;

// validate source specfications
// validate source specifications
if (!sources && !sourcesWithDeploymentOptions) {
throw new Error(
"Either `sources` or `sourcesWithDeploymentOptions` must be specified"
Expand All @@ -173,4 +179,8 @@ function validateProps(props: CdnSiteHostingConstructProps): void {
} else if (sources && sources.length === 0) {
throw new Error("If specified, `sources` cannot be empty");
}

if (websiteIndexDocument.startsWith("/")) {
throw Error("leading slashes are not allowed in websiteIndexDocument");
}
}
56 changes: 56 additions & 0 deletions test/infra/cdn-site-hosting/cdn-site-hosting-construct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as cdk from "@aws-cdk/core";
import { Environment, RemovalPolicy, Stack } from "@aws-cdk/core";
import * as s3deploy from "@aws-cdk/aws-s3-deployment";
import { CdnSiteHostingConstruct } from "../../../lib/cdn-site-hosting";
import { Template } from "@aws-cdk/assertions";

// hosted-zone requires an environment be attached to the Stack
const testEnv: Environment = {
Expand Down Expand Up @@ -251,4 +252,59 @@ describe("CdnSiteHostingConstruct", () => {
);
});
});

describe("When sourcesWithDeploymentOptions is provided", () => {
let stack: Stack;

beforeAll(() => {
const app = new cdk.App();
stack = new cdk.Stack(app, "TestRoutedSPAStack", { env: testEnv });
new CdnSiteHostingConstruct(stack, "MyTestConstruct", {
certificateArn: fakeCertificateArn,
siteSubDomain: fakeSiteSubDomain,
domainName: fakeDomain,
removalPolicy: RemovalPolicy.DESTROY,
isRoutedSpa: true,
sourcesWithDeploymentOptions: [
{
name: "source1",
sources: [s3deploy.Source.asset("./", { exclude: ["index.html"] })],
},
{
name: "source2",
sources: [
s3deploy.Source.asset("./", { exclude: ["*", "!index.html"] }),
],
},
],
websiteIndexDocument: "index.html",
});
});

test("provisions a single S3 bucket with website hosting configured", () => {
expectCDK(stack).to(countResources("AWS::S3::Bucket", 1));
expectCDK(stack).to(
haveResource("AWS::S3::Bucket", {
BucketName: fakeFqdn,
WebsiteConfiguration: {
IndexDocument: "index.html",
ErrorDocument: "index.html",
},
})
);
});

test("configures all S3 deployments sequentially, with each deployment depending on the previous one", () => {
const template = Template.fromStack(stack);
const deployments = Object.entries(
template.findResources("Custom::CDKBucketDeployment")
);
expect(deployments.length).toBe(2);
const [[firstDeploymentId, firstDeployment], [, secondDeployment]] =
deployments;
expect(firstDeployment.DependsOn).toBeUndefined();
expect(secondDeployment.DependsOn).toBeDefined();
expect(secondDeployment.DependsOn).toContain(firstDeploymentId);
});
});
});

0 comments on commit 912b6da

Please sign in to comment.