Skip to content
This repository was archived by the owner on Jun 28, 2022. It is now read-only.

Commit

Permalink
fix: deploy static-app lambdaedge function limit
Browse files Browse the repository at this point in the history
  • Loading branch information
arantespp committed Mar 7, 2021
1 parent 0f06276 commit db03725
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 45 deletions.
3 changes: 3 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
"mime-types": "^2.1.29",
"npmlog": "^4.1.2",
"prettier": "^2.2.1",
"semver": "^7.3.4",
"simple-git": "^2.25.0",
"ts-loader": "^8.0.14",
"ts-node": "^9.1.1",
"uglify-js": "^3.13.0",
"webpack": "^5.15.0",
"yargs": "^16.2.0"
},
Expand All @@ -52,6 +54,7 @@
"@types/mime-types": "^2.1.0",
"@types/node": "^14.14.10",
"@types/npmlog": "^4.1.2",
"@types/semver": "^7.3.4",
"@types/yargs": "^16.0.0",
"faker": "^5.1.0",
"jest": "^26.6.3",
Expand Down
14 changes: 10 additions & 4 deletions packages/cli/src/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ import cli from './cli';

import { deployStaticApp } from './deploy/staticApp/staticApp';

const parse = (arg: any, context: any) => {
return cli().strict(false).parse(arg, context);
};

describe('handle merge config correctly', () => {
describe('Config merging errors when default values is present #16 https://github.com/ttoss/carlin/issues/16', () => {
test('deploy static-app --region should not be the default', async () => {
await cli().parse('deploy static-app', { environment: 'Production' });
await parse('deploy static-app', {
environment: 'Production',
});
expect(deployStaticApp).toHaveBeenCalledWith(
expect.objectContaining({ region }),
);
Expand All @@ -41,13 +47,13 @@ describe('handle merge config correctly', () => {
const options = {
region: faker.random.word(),
};
const argv = await cli().parse('print-args', options);
const argv = await parse('print-args', options);
expect(argv.environment).toBeUndefined();
expect(argv).toMatchObject(options);
});

test('argv must have the environment option', async () => {
const argv = await cli().parse('print-args', { environment: 'Production' });
const argv = await parse('print-args', { environment: 'Production' });
expect(argv.environment).toBe('Production');
expect(argv.optionEnv).toEqual(
optionsFromConfigFiles.environments.Production.optionEnv,
Expand All @@ -56,7 +62,7 @@ describe('handle merge config correctly', () => {

test('argv must have the CLI optionEnv', async () => {
const newOptionEnv = faker.random.word();
const argv = await cli().parse('print-args', {
const argv = await parse('print-args', {
environment: 'Production',
optionEnv: newOptionEnv,
});
Expand Down
16 changes: 15 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export const options = {
environments: {},
} as const;

/**
* All options my be passed as environment variables matching the prefix
* "CARLIN". See [Yargs reference](https://yargs.js.org/docs/#api-reference-envprefix).
* Example, we may use `carlin deploy --stack-name StackName` or
* `CARLIN_STACK_NAME=StackName carlin deploy`.
*/
const getEnv = () => {
return constantCase(NAME);
};

/**
* Transformed to method because finalConfig was failing the tests.
*/
Expand Down Expand Up @@ -68,8 +78,9 @@ const cli = () => {
};

return yargs
.strict()
.scriptName(NAME)
.env(constantCase(NAME))
.env(getEnv())
.options(addGroupToOptions(options, 'Common Options'))
.middleware(((argv: any, { parsed }: any) => {
const { environment, environments } = argv;
Expand Down Expand Up @@ -135,6 +146,9 @@ const cli = () => {
handler: (argv) => console.log(JSON.stringify(argv, null, 2)),
})
.command(deployCommand)
.epilogue(
'For more information, find our manual at https://carlin.ttoss.dev',
)
.help();
};

Expand Down
24 changes: 21 additions & 3 deletions packages/cli/src/deploy/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import path from 'path';

const logPrefix = 's3';

const s3 = new S3({ apiVersion: '2006-03-01' });
export const s3 = new S3({ apiVersion: '2006-03-01' });

export const getBucketKeyUrl = ({
bucket,
Expand Down Expand Up @@ -211,9 +211,27 @@ export const emptyS3Directory = async ({
await emptyS3Directory({ bucket, directory });
}

log.info(logPrefix, `${bucket}/${directory} is empty`);
log.info(logPrefix, `${bucket}/${directory} is empty.`);
} catch (err) {
log.error(logPrefix, `Cannot empty ${bucket}/${directory}`);
log.error(logPrefix, `Cannot empty ${bucket}/${directory}.`);
throw err;
}
};

export const deleteS3Directory = async ({
bucket,
directory = '',
}: {
bucket: string;
directory?: string;
}) => {
try {
log.info(logPrefix, `${bucket}/${directory} is being deleted...`);
await emptyS3Directory({ bucket, directory });
await s3.deleteObject({ Bucket: bucket, Key: directory }).promise();
log.info(logPrefix, `${bucket}/${directory} was deleted.`);
} catch (error) {
log.error(logPrefix, `Cannot delete ${bucket}/${directory}.`);
throw error;
}
};
2 changes: 1 addition & 1 deletion packages/cli/src/deploy/stackName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const limitStackName = (stackName: string) =>
*
* 1. The second part will be defined by, whichever is defined first:
* 1. environment,
* 1. branch name in param-case,
* 1. [branch name](https://carlin.ttoss.dev/docs/CLI#branchbranch_name) in param-case,
* 1. `undefined`.
*
* Example:
Expand Down
22 changes: 12 additions & 10 deletions packages/cli/src/deploy/staticApp/staticApp.template.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,21 +351,13 @@ describe('testing getLambdaEdgeOriginRequestZipFile', () => {
),
)(event);

const assertions = (response: any) => {
expect(response.headers).toEqual(expect.objectContaining(requestHeaders));
expect(response.headers).toEqual(
expect.objectContaining(securityHeaders),
);
expect(response.headers).toEqual(expect.objectContaining(cachingHeaders));
};

test('request (without body) should forward the headers', async () => {
const newRecord = JSON.parse(JSON.stringify(record));
const newUri = '/script.js';
newRecord.cf.request.uri = newUri;
const response = await handler({ Records: [newRecord] });
expect(response.body).toBeUndefined();
assertions(response);
expect(response.headers).toEqual(expect.objectContaining(requestHeaders));
});

test('response (with body) should forward the headers', async () => {
Expand All @@ -374,7 +366,17 @@ describe('testing getLambdaEdgeOriginRequestZipFile', () => {
newRecord.cf.request.uri = newUri;
const response = await handler({ Records: [newRecord] });
expect(response.body).toBeDefined();
assertions(response);
expect(response.headers).toEqual(
expect.objectContaining(securityHeaders),
);
expect(response.headers).toEqual(expect.objectContaining(cachingHeaders));
/**
* Lambda@Edge cannot forward these headers
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-header-restrictions.
*/
expect(response.headers).toEqual(
expect.not.objectContaining(requestHeaders),
);
});
});

Expand Down
24 changes: 8 additions & 16 deletions packages/cli/src/deploy/staticApp/staticApp.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getIamPath,
} from '../../utils';

import { formatCode } from '../../utils/formatCode';
import { formatCode, uglify } from '../../utils/formatCode';

import { getOriginShieldRegion } from './getOriginShieldRegion';

Expand Down Expand Up @@ -326,7 +326,7 @@ const s3 = new S3({ region: "${region}" });
exports.handler = async (event, context) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const headers = { };
${assignSecurityHeaders({ csp: fullCsp })}
Expand Down Expand Up @@ -376,19 +376,9 @@ exports.handler = async (event, context) => {
const nonce = crypto.randomBytes(16).toString('base64');
// https://developers.google.com/tag-manager/quickstart
const gtmScriptHead = \`
<script nonce="\${nonce}">(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;var n=d.querySelector('[nonce]');
n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${gtmId}');</script>
\`.replace(/\\n/g, '');
const gtmScriptHead = \`<script nonce="\${nonce}">(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;var n=d.querySelector('[nonce]');n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtmId}');</script>\`;
const gtmScriptBody = \`
<noscript><iframe nonce='\${nonce}' src='https://www.googletagmanager.com/ns.html?id=${gtmId}'${' '}
height='0' width='0' style='display:none;visibility:hidden'></iframe></noscript>
\`.replace(/\\n/g, '');
const gtmScriptBody = \`<noscript><iframe nonce='\${nonce}' src='https://www.googletagmanager.com/ns.html?id=${gtmId}' height='0' width='0' style='display:none;visibility:hidden'></iframe></noscript>\`;
const cspValue = headers['content-security-policy'][0]
.value
Expand Down Expand Up @@ -660,7 +650,9 @@ const getCloudFrontEdgeLambdas = ({
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: getLambdaEdgeOriginRequestZipFile({ gtmId, csp, region }),
ZipFile: uglify(
getLambdaEdgeOriginRequestZipFile({ gtmId, csp, region }),
),
},
Description: 'Lambda@Edge function serving as origin request.',
Handler: 'index.handler',
Expand All @@ -685,7 +677,7 @@ const getCloudFrontEdgeLambdas = ({
[LAMBDA_EDGE_ORIGIN_RESPONSE_LOGICAL_ID]: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: { ZipFile: getLambdaEdgeOriginResponseZipFile({ csp }) },
Code: { ZipFile: uglify(getLambdaEdgeOriginResponseZipFile({ csp })) },
Description: 'Lambda@Edge function serving as origin response.',
Handler: 'index.handler',
MemorySize: 128,
Expand Down
47 changes: 46 additions & 1 deletion packages/cli/src/deploy/staticApp/staticApp.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/* eslint-disable no-template-curly-in-string */
import { CloudFormation, CloudFront } from 'aws-sdk';
import log from 'npmlog';
import semver from 'semver';

import { getPackageVersion } from '../../utils';

import { cloudFormation, deploy } from '../cloudFormation.core';
import {
uploadDirectoryToS3,
emptyS3Directory,
deleteS3Directory,
getAllFilesInsideADirectory,
s3,
} from '../s3';
import { handleDeployError, handleDeployInitialization } from '../utils';

Expand Down Expand Up @@ -114,6 +117,41 @@ export const uploadBuiltAppToS3 = async ({
);
};

const removeOldVersions = async ({ bucket }: { bucket: string }) => {
try {
log.info(logPrefix, 'Removing old versions...');

const { CommonPrefixes = [] } = await s3
.listObjectsV2({ Bucket: bucket, Delimiter: '/' })
.promise();

const versions = CommonPrefixes?.map(({ Prefix }) =>
Prefix?.replace('/', ''),
)
.filter((version) => !!version)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.sort((a, b) => (semver.gt(a!, b!) ? -1 : 1));

/**
* Keep the 3 most recent versions.
*/
versions.shift();
versions.shift();
versions.shift();

await Promise.all(
versions.map((version) =>
deleteS3Directory({ bucket, directory: `${version}` }),
),
);
} catch (error) {
log.info(
logPrefix,
`Cannot remove older versions from "${bucket}" bucket.`,
);
}
};

export const invalidateCloudFront = async ({
outputs,
}: {
Expand Down Expand Up @@ -173,6 +211,7 @@ export const invalidateCloudFront = async ({
* the options, for instance, only S3, SPA, with hosted zone...
* 1. Create AWS resources using the templated created.
* 1. Upload static files to the host bucket S3.
* 1. Remove old deployment versions. Keep only the 3 most recent ones.
*/
export const deployStaticApp = async ({
acm,
Expand All @@ -185,6 +224,7 @@ export const deployStaticApp = async ({
hostedZoneName,
region,
skipUpload,
invalidate = false,
}: {
acm?: string;
aliases?: string[];
Expand All @@ -196,6 +236,7 @@ export const deployStaticApp = async ({
hostedZoneName?: string;
region: string;
skipUpload?: boolean;
invalidate?: boolean;
}) => {
try {
const { stackName } = await handleDeployInitialization({ logPrefix });
Expand All @@ -222,11 +263,15 @@ export const deployStaticApp = async ({
if (bucket) {
if (!skipUpload) {
await uploadBuiltAppToS3({ buildFolder, bucket, cloudfront });
await removeOldVersions({ bucket });
}

const { Outputs } = await deploy({ params, template });

await invalidateCloudFront({ outputs: Outputs });
if (invalidate) {
await invalidateCloudFront({ outputs: Outputs });
}

/**
* Stack doesn't exist. Deploy CloudFormation first, get the bucket name,
* and upload files to S3.
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/utils/formatCode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import prettier from 'prettier';
import UglifyJS from 'uglify-js';

export const formatCode = (code: string) => {
return prettier.format(code, { parser: 'babel' });
};

export const uglify = (code: string) => UglifyJS.minify(code).code;
Loading

0 comments on commit db03725

Please sign in to comment.