Skip to content

Commit aea1372

Browse files
authored
feat(amplify-alpha): support custom response headers in monorepo structures (#31771)
### Issue # (if applicable) Closes #31758. ### Reason for this change The current custom response headers implementation does not support Amplify apps with monorepo structures, this is due to a difference in the YAML formats for these apps: https://docs.aws.amazon.com/amplify/latest/userguide/custom-header-YAML-format.html ### Description of changes An `appRoot` property has been added to `CustomResponseHeader`, which specifies the appRoot from the build spec to use for the output YAML. ### Description of how you validated changes I added unit tests and tested the implementation using a sample deployment. I'm happy to add integration tests if required. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent c6ea664 commit aea1372

12 files changed

+658
-10
lines changed

packages/@aws-cdk/aws-amplify-alpha/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,68 @@ const amplifyApp = new amplify.App(this, 'App', {
238238
});
239239
```
240240

241+
If the app uses a monorepo structure, define which appRoot from the build spec the custom response headers should apply to by using the `appRoot` property:
242+
243+
```ts
244+
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
245+
246+
const amplifyApp = new amplify.App(this, 'App', {
247+
sourceCodeProvider: new amplify.GitHubSourceCodeProvider({
248+
owner: '<user>',
249+
repository: '<repo>',
250+
oauthToken: SecretValue.secretsManager('my-github-token'),
251+
}),
252+
buildSpec: codebuild.BuildSpec.fromObjectToYaml({
253+
version: '1.0',
254+
applications: [
255+
{
256+
appRoot: 'frontend',
257+
frontend: {
258+
phases: {
259+
preBuild: {
260+
commands: ['npm install'],
261+
},
262+
build: {
263+
commands: ['npm run build'],
264+
},
265+
},
266+
},
267+
},
268+
{
269+
appRoot: 'backend',
270+
backend: {
271+
phases: {
272+
preBuild: {
273+
commands: ['npm install'],
274+
},
275+
build: {
276+
commands: ['npm run build'],
277+
},
278+
},
279+
},
280+
},
281+
],
282+
}),
283+
customResponseHeaders: [
284+
{
285+
appRoot: 'frontend',
286+
pattern: '*.json',
287+
headers: {
288+
'custom-header-name-1': 'custom-header-value-1',
289+
'custom-header-name-2': 'custom-header-value-2',
290+
},
291+
},
292+
{
293+
appRoot: 'backend',
294+
pattern: '/path/*',
295+
headers: {
296+
'custom-header-name-1': 'custom-header-value-2',
297+
},
298+
},
299+
],
300+
});
301+
```
302+
241303
## Configure server side rendering when hosting app
242304

243305
Setting the `platform` field on the Amplify `App` construct can be used to control whether the app will host only static assets or server side rendered assets in addition to static. By default, the value is set to `WEB` (static only), however, server side rendering can be turned on by setting to `WEB_COMPUTE` as follows:

packages/@aws-cdk/aws-amplify-alpha/lib/app.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
22
import * as iam from 'aws-cdk-lib/aws-iam';
33
import { IResource, Lazy, Resource, SecretValue, ValidationError } from 'aws-cdk-lib/core';
4-
import { Construct } from 'constructs';
4+
import { Construct, IConstruct } from 'constructs';
55
import { CfnApp } from 'aws-cdk-lib/aws-amplify';
66
import { BasicAuth } from './basic-auth';
77
import { Branch, BranchOptions } from './branch';
@@ -309,7 +309,7 @@ export class App extends Resource implements IApp, iam.IGrantable {
309309
name: props.appName || this.node.id,
310310
oauthToken: sourceCodeProviderOptions?.oauthToken?.unsafeUnwrap(), // Safe usage
311311
repository: sourceCodeProviderOptions?.repository,
312-
customHeaders: props.customResponseHeaders ? renderCustomResponseHeaders(props.customResponseHeaders) : undefined,
312+
customHeaders: props.customResponseHeaders ? renderCustomResponseHeaders(props.customResponseHeaders, this) : undefined,
313313
platform: appPlatform,
314314
});
315315

@@ -569,6 +569,12 @@ export class CustomRule {
569569
* Custom response header of an Amplify App.
570570
*/
571571
export interface CustomResponseHeader {
572+
/**
573+
* If the app uses a monorepo structure, the appRoot from the build spec to apply the custom headers to.
574+
* @default - The appRoot is omitted in the custom headers output.
575+
*/
576+
readonly appRoot?: string;
577+
572578
/**
573579
* These custom headers will be applied to all URL file paths that match this pattern.
574580
*/
@@ -580,17 +586,25 @@ export interface CustomResponseHeader {
580586
readonly headers: { [key: string]: string };
581587
}
582588

583-
function renderCustomResponseHeaders(customHeaders: CustomResponseHeader[]): string {
584-
const yaml = [
585-
'customHeaders:',
586-
];
589+
function renderCustomResponseHeaders(customHeaders: CustomResponseHeader[], scope: IConstruct): string {
590+
const hasAppRoot = customHeaders[0].appRoot !== undefined;
591+
const yaml = [hasAppRoot ? 'applications:' : 'customHeaders:'];
587592

588593
for (const customHeader of customHeaders) {
589-
yaml.push(` - pattern: "${customHeader.pattern}"`);
590-
yaml.push(' headers:');
594+
if ((customHeader.appRoot !== undefined) !== hasAppRoot) {
595+
throw new ValidationError('appRoot must be either be present or absent across all custom response headers', scope);
596+
}
597+
598+
const baseIndentation = ' '.repeat(hasAppRoot ? 6 : 2);
599+
if (hasAppRoot) {
600+
yaml.push(` - appRoot: ${customHeader.appRoot}`);
601+
yaml.push(' customHeaders:');
602+
}
603+
yaml.push(`${baseIndentation}- pattern: "${customHeader.pattern}"`);
604+
yaml.push(`${baseIndentation} headers:`);
591605
for (const [key, value] of Object.entries(customHeader.headers)) {
592-
yaml.push(` - key: "${key}"`);
593-
yaml.push(` value: "${value}"`);
606+
yaml.push(`${baseIndentation} - key: "${key}"`);
607+
yaml.push(`${baseIndentation} value: "${value}"`);
594608
}
595609
}
596610

packages/@aws-cdk/aws-amplify-alpha/test/app.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,111 @@ test('with custom headers', () => {
444444
});
445445
});
446446

447+
test('with custom headers in a monorepo structure', () => {
448+
// WHEN
449+
new amplify.App(stack, 'App', {
450+
sourceCodeProvider: new amplify.GitHubSourceCodeProvider({
451+
owner: 'aws',
452+
repository: 'aws-cdk',
453+
oauthToken: SecretValue.unsafePlainText('secret'),
454+
}),
455+
buildSpec: codebuild.BuildSpec.fromObjectToYaml({
456+
version: '1.0',
457+
applications: [
458+
{
459+
appRoot: 'frontend',
460+
frontend: {
461+
phases: {
462+
preBuild: {
463+
commands: ['npm install'],
464+
},
465+
build: {
466+
commands: ['npm run build'],
467+
},
468+
},
469+
},
470+
},
471+
{
472+
appRoot: 'backend',
473+
backend: {
474+
phases: {
475+
preBuild: {
476+
commands: ['npm install'],
477+
},
478+
build: {
479+
commands: ['npm run build'],
480+
},
481+
},
482+
},
483+
},
484+
],
485+
}),
486+
customResponseHeaders: [
487+
{
488+
appRoot: 'frontend',
489+
pattern: '*.json',
490+
headers: {
491+
'custom-header-name-1': 'custom-header-value-1',
492+
'custom-header-name-2': 'custom-header-value-2',
493+
},
494+
},
495+
{
496+
appRoot: 'backend',
497+
pattern: '/path/*',
498+
headers: {
499+
'custom-header-name-1': 'custom-header-value-2',
500+
'x-aws-url-suffix': `this-is-the-suffix-${stack.urlSuffix}`,
501+
},
502+
},
503+
],
504+
});
505+
506+
// THEN
507+
Template.fromStack(stack).hasResourceProperties('AWS::Amplify::App', {
508+
CustomHeaders: {
509+
'Fn::Join': [
510+
'',
511+
[
512+
'applications:\n - appRoot: frontend\n customHeaders:\n - pattern: "*.json"\n headers:\n - key: "custom-header-name-1"\n value: "custom-header-value-1"\n - key: "custom-header-name-2"\n value: "custom-header-value-2"\n - appRoot: backend\n customHeaders:\n - pattern: "/path/*"\n headers:\n - key: "custom-header-name-1"\n value: "custom-header-value-2"\n - key: "x-aws-url-suffix"\n value: "this-is-the-suffix-',
513+
{
514+
Ref: 'AWS::URLSuffix',
515+
},
516+
'"\n',
517+
],
518+
],
519+
},
520+
});
521+
});
522+
523+
test('error with inconsistent appRoot in custom headers', () => {
524+
// WHEN
525+
expect(() => {
526+
new amplify.App(stack, 'App', {
527+
sourceCodeProvider: new amplify.GitHubSourceCodeProvider({
528+
owner: 'aws',
529+
repository: 'aws-cdk',
530+
oauthToken: SecretValue.unsafePlainText('secret'),
531+
}),
532+
customResponseHeaders: [
533+
{
534+
pattern: '*.json',
535+
headers: {
536+
'custom-header-name-1': 'custom-header-value-1',
537+
'custom-header-name-2': 'custom-header-value-2',
538+
},
539+
},
540+
{
541+
appRoot: 'backend',
542+
pattern: '/path/*',
543+
headers: {
544+
'custom-header-name-1': 'custom-header-value-2',
545+
},
546+
},
547+
],
548+
});
549+
}).toThrow('appRoot must be either be present or absent across all custom response headers');
550+
});
551+
447552
test('create a statically hosted app by default', () => {
448553
// WHEN
449554
new amplify.App(stack, 'App', {});

packages/@aws-cdk/aws-amplify-alpha/test/integ.app-monorepo-custom-headers.js.snapshot/amplifyappmonorepocustomheadersintegDefaultTestDeployAssert582B40C4.assets.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/aws-amplify-alpha/test/integ.app-monorepo-custom-headers.js.snapshot/amplifyappmonorepocustomheadersintegDefaultTestDeployAssert582B40C4.template.json

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/aws-amplify-alpha/test/integ.app-monorepo-custom-headers.js.snapshot/cdk-amplify-monorepo-custom-headers.assets.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)