@inizioevoke/evosynth 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +12 -0
- package/dist/constructs/certificatemanager.d.ts +12 -0
- package/dist/constructs/certificatemanager.js +19 -0
- package/dist/constructs/cloudfront.d.ts +55 -0
- package/dist/constructs/cloudfront.js +84 -0
- package/dist/constructs/codebuild.d.ts +31 -0
- package/dist/constructs/codebuild.js +95 -0
- package/dist/constructs/codepipeline.d.ts +30 -0
- package/dist/constructs/codepipeline.js +63 -0
- package/dist/constructs/route53.d.ts +14 -0
- package/dist/constructs/route53.js +24 -0
- package/dist/constructs/s3.d.ts +11 -0
- package/dist/constructs/s3.js +17 -0
- package/dist/constructs/ssm.d.ts +9 -0
- package/dist/constructs/ssm.js +17 -0
- package/dist/constructs/waf.d.ts +21 -0
- package/dist/constructs/waf.js +89 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +26 -0
- package/dist/lib/const.d.ts +1 -0
- package/dist/lib/const.js +2 -0
- package/dist/lib/csv-redirects.d.ts +3 -0
- package/dist/lib/csv-redirects.js +124 -0
- package/dist/lib/tags.d.ts +19 -0
- package/dist/lib/tags.js +47 -0
- package/dist/lib/types.d.ts +14 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +7 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +5 -0
- package/dist/stacks/web-static-serverless/codepipeline-stack.d.ts +17 -0
- package/dist/stacks/web-static-serverless/codepipeline-stack.js +28 -0
- package/dist/stacks/web-static-serverless/web-global-stack.d.ts +43 -0
- package/dist/stacks/web-static-serverless/web-global-stack.js +142 -0
- package/dist/stages/str8r-clm-stage.d.ts +17 -0
- package/dist/stages/str8r-clm-stage.js +41 -0
- package/dist/stages/web-static-serverless-stage.d.ts +19 -0
- package/dist/stages/web-static-serverless-stage.js +51 -0
- package/dist/templates/cffn-default-doc-basic-auth.js +155 -0
- package/dist/templates/cffn-default-doc.js +132 -0
- package/inizioevoke-evosynth.code-workspace +8 -0
- package/package.json +24 -0
- package/readme.md +114 -0
- package/samples/app.ts +103 -0
- package/samples/codebuild/buildspec.yml +24 -0
- package/samples/config/redirects.csv +6 -0
- package/src/constructs/certificatemanager.ts +28 -0
- package/src/constructs/cloudfront.ts +148 -0
- package/src/constructs/codebuild.ts +124 -0
- package/src/constructs/codepipeline.ts +93 -0
- package/src/constructs/route53.ts +34 -0
- package/src/constructs/s3.ts +25 -0
- package/src/constructs/ssm.ts +24 -0
- package/src/constructs/waf.ts +118 -0
- package/src/index.ts +35 -0
- package/src/lib/const.ts +1 -0
- package/src/lib/csv-redirects.ts +139 -0
- package/src/lib/tags.ts +55 -0
- package/src/lib/types.ts +17 -0
- package/src/lib/utils.ts +7 -0
- package/src/lib/version.ts +6 -0
- package/src/stacks/web-static-serverless/codepipeline-stack.ts +42 -0
- package/src/stacks/web-static-serverless/web-global-stack.ts +184 -0
- package/src/stages/str8r-clm-stage.ts +57 -0
- package/src/stages/web-static-serverless-stage.ts +69 -0
- package/src/templates/cffn-default-doc-basic-auth.js +155 -0
- package/src/templates/cffn-default-doc.js +132 -0
- package/tasks/clean.js +7 -0
- package/tasks/copy-templates.js +13 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { Bucket } from 'aws-cdk-lib/aws-s3';
|
|
4
|
+
import { tagResource } from '../lib/tags.js';
|
|
5
|
+
import { ResourceProps } from "../lib/types.js";
|
|
6
|
+
|
|
7
|
+
export interface S3BucketProps extends ResourceProps {
|
|
8
|
+
/** The name of the S3 bucket */
|
|
9
|
+
bucketName?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class S3Bucket extends Construct {
|
|
13
|
+
public readonly bucket: Bucket;
|
|
14
|
+
|
|
15
|
+
constructor(scope: Construct, id: string, props: S3BucketProps) {
|
|
16
|
+
super(scope, id);
|
|
17
|
+
|
|
18
|
+
this.bucket = new Bucket(this, 'S3Bucket', {
|
|
19
|
+
bucketName: props.bucketName,
|
|
20
|
+
removalPolicy: props.destroy !== undefined ? props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN : undefined,
|
|
21
|
+
autoDeleteObjects: props.destroy === true ? true : undefined
|
|
22
|
+
});
|
|
23
|
+
tagResource(this.bucket, props.tags);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
|
|
4
|
+
|
|
5
|
+
interface CreateStringParameterProps {
|
|
6
|
+
parameterName: string;
|
|
7
|
+
stringValue: string;
|
|
8
|
+
overwrite?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export function createStringParameter(scope: Construct, id: string, { parameterName, stringValue, overwrite = false }: CreateStringParameterProps): StringParameter | undefined {
|
|
11
|
+
let param: StringParameter | undefined = undefined;
|
|
12
|
+
let flag = overwrite;
|
|
13
|
+
if (!flag) {
|
|
14
|
+
const existingValue = StringParameter.valueFromLookup(scope, parameterName, 'a8a79532-4163-46b9-a907-429dad04e93b');
|
|
15
|
+
flag = existingValue === 'a8a79532-4163-46b9-a907-429dad04e93b';
|
|
16
|
+
}
|
|
17
|
+
if (flag) {
|
|
18
|
+
param = new StringParameter(scope, id, {
|
|
19
|
+
parameterName,
|
|
20
|
+
stringValue
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return param;
|
|
24
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
|
|
4
|
+
import { tagResource } from '../lib/tags.js';
|
|
5
|
+
import { cleanResourceName } from '../lib/utils.js';
|
|
6
|
+
import type { ResourceProps } from '../lib/types.js';
|
|
7
|
+
|
|
8
|
+
export type WebAclManagedRule =
|
|
9
|
+
'AWSManagedRulesAmazonIpReputationList' |
|
|
10
|
+
'AWSManagedRulesAnonymousIpList' |
|
|
11
|
+
'AWSManagedRulesAntiDDoSRuleSet' |
|
|
12
|
+
'AWSManagedRulesBotControlRuleSet' |
|
|
13
|
+
'AWSManagedRulesCommonRuleSet' |
|
|
14
|
+
'AWSManagedRulesKnownBadInputsRuleSet' |
|
|
15
|
+
'AWSManagedRulesSQLiRuleSet';
|
|
16
|
+
|
|
17
|
+
export interface CloudFrontWebAclProps extends ResourceProps {
|
|
18
|
+
name: string;
|
|
19
|
+
managedRules?: (WebAclManagedRule & string)[];
|
|
20
|
+
}
|
|
21
|
+
export class CloudFrontWebAcl extends Construct {
|
|
22
|
+
public readonly acl: CfnWebACL;
|
|
23
|
+
|
|
24
|
+
constructor(scope: Construct, id: string, props: CloudFrontWebAclProps) {
|
|
25
|
+
super(scope, id);
|
|
26
|
+
|
|
27
|
+
props.name = cleanResourceName(props.name);
|
|
28
|
+
|
|
29
|
+
if (!props.managedRules) {
|
|
30
|
+
props.managedRules = [
|
|
31
|
+
'AWSManagedRulesAmazonIpReputationList',
|
|
32
|
+
'AWSManagedRulesAnonymousIpList',
|
|
33
|
+
'AWSManagedRulesCommonRuleSet',
|
|
34
|
+
'AWSManagedRulesKnownBadInputsRuleSet',
|
|
35
|
+
'AWSManagedRulesBotControlRuleSet'
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.acl = new CfnWebACL(this, 'CloudFrontWebAcl', {
|
|
40
|
+
scope: 'CLOUDFRONT',
|
|
41
|
+
name: props.name,
|
|
42
|
+
defaultAction: { allow: {} },
|
|
43
|
+
visibilityConfig: {
|
|
44
|
+
cloudWatchMetricsEnabled: true,
|
|
45
|
+
sampledRequestsEnabled: true,
|
|
46
|
+
metricName: `${props.name}-metrics`
|
|
47
|
+
},
|
|
48
|
+
rules: transformManagedRules(props.managedRules)
|
|
49
|
+
});
|
|
50
|
+
tagResource(this.acl, props.tags);
|
|
51
|
+
if (typeof props.destroy == 'boolean') {
|
|
52
|
+
this.acl.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ApiGatewayWebAclProps extends ResourceProps {
|
|
58
|
+
name: string;
|
|
59
|
+
aclScope?: 'CLOUDFRONT' | 'REGIONAL';
|
|
60
|
+
managedRules?: (WebAclManagedRule & string)[];
|
|
61
|
+
}
|
|
62
|
+
export class ApiGatewayWebAcl extends Construct {
|
|
63
|
+
public readonly acl: CfnWebACL;
|
|
64
|
+
constructor(scope: Construct, id: string, props: ApiGatewayWebAclProps) {
|
|
65
|
+
super(scope, id);
|
|
66
|
+
|
|
67
|
+
props.name = cleanResourceName(props.name);
|
|
68
|
+
|
|
69
|
+
if (!props.managedRules) {
|
|
70
|
+
props.managedRules = [
|
|
71
|
+
'AWSManagedRulesAmazonIpReputationList',
|
|
72
|
+
'AWSManagedRulesAnonymousIpList',
|
|
73
|
+
'AWSManagedRulesCommonRuleSet',
|
|
74
|
+
'AWSManagedRulesSQLiRuleSet',
|
|
75
|
+
'AWSManagedRulesKnownBadInputsRuleSet',
|
|
76
|
+
'AWSManagedRulesBotControlRuleSet'
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.acl = new CfnWebACL(this, 'ApiGatewayWebAcl', {
|
|
81
|
+
scope: props.aclScope ?? 'REGIONAL',
|
|
82
|
+
name: props.name,
|
|
83
|
+
defaultAction: { allow: {} },
|
|
84
|
+
visibilityConfig: {
|
|
85
|
+
cloudWatchMetricsEnabled: true,
|
|
86
|
+
sampledRequestsEnabled: true,
|
|
87
|
+
metricName: `${props.name}-metrics`
|
|
88
|
+
},
|
|
89
|
+
rules: transformManagedRules(props.managedRules)
|
|
90
|
+
});
|
|
91
|
+
tagResource(this.acl, props.tags);
|
|
92
|
+
if (typeof props.destroy == 'boolean') {
|
|
93
|
+
this.acl.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
function transformManagedRules(rules: (WebAclManagedRule | string)[]): CfnWebACL.RuleProperty[] {
|
|
100
|
+
return rules.map((ruleName, i) => {
|
|
101
|
+
return {
|
|
102
|
+
name: `AWS-${ruleName}`,
|
|
103
|
+
priority: i,
|
|
104
|
+
statement: {
|
|
105
|
+
managedRuleGroupStatement: {
|
|
106
|
+
name: ruleName,
|
|
107
|
+
vendorName: 'AWS'
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
overrideAction: { none: {} },
|
|
111
|
+
visibilityConfig: {
|
|
112
|
+
cloudWatchMetricsEnabled: true,
|
|
113
|
+
sampledRequestsEnabled: true,
|
|
114
|
+
metricName: `AWS-${ruleName}`
|
|
115
|
+
}
|
|
116
|
+
} as CfnWebACL.RuleProperty;
|
|
117
|
+
});
|
|
118
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Environment } from "aws-cdk-lib";
|
|
2
|
+
|
|
3
|
+
export * from './lib/tags.ts';
|
|
4
|
+
|
|
5
|
+
interface GetEnvParams {
|
|
6
|
+
account?: number | string;
|
|
7
|
+
region?: string;
|
|
8
|
+
}
|
|
9
|
+
export function getEnv(region?: string): Environment;
|
|
10
|
+
export function getEnv(account?: string | number): Environment;
|
|
11
|
+
export function getEnv({ account, region }: GetEnvParams): Environment;
|
|
12
|
+
export function getEnv(env?: Environment): Environment;
|
|
13
|
+
export function getEnv(env?: string | number | GetEnvParams | Environment) {
|
|
14
|
+
let account: string = process.env.CDK_DEFAULT_ACCOUNT || '';
|
|
15
|
+
let region: string = process.env.CDK_DEFAULT_REGION || 'us-east-1';
|
|
16
|
+
if (env) {
|
|
17
|
+
switch (typeof env) {
|
|
18
|
+
case 'number':
|
|
19
|
+
account = env.toString();
|
|
20
|
+
break;
|
|
21
|
+
case 'string':
|
|
22
|
+
if (/^\d+$/.test(env)) {
|
|
23
|
+
account = env;
|
|
24
|
+
} else {
|
|
25
|
+
region = env;
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
default:
|
|
29
|
+
account = (env as Environment).account ?? '';
|
|
30
|
+
region = (env as Environment).region ?? region;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { account, region };
|
|
35
|
+
}
|
package/src/lib/const.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NPM_LIBRARY_NAME = '@inizioevoke/evosynth';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import type { CloudFrontFunctionRedirectPaths, HttpRedirectCode } from '../constructs/cloudfront.js';
|
|
4
|
+
|
|
5
|
+
const VALID_HTTP_CODES: readonly number[] = [301, 302, 303, 307, 308];
|
|
6
|
+
|
|
7
|
+
function parseCsvLine(line: string): string[] {
|
|
8
|
+
const fields: string[] = [];
|
|
9
|
+
let current = '';
|
|
10
|
+
let inQuotes = false;
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < line.length; i++) {
|
|
13
|
+
const ch = line[i];
|
|
14
|
+
if (inQuotes) {
|
|
15
|
+
if (ch === '"') {
|
|
16
|
+
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
17
|
+
current += '"';
|
|
18
|
+
i++;
|
|
19
|
+
} else {
|
|
20
|
+
inQuotes = false;
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
current += ch;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
if (ch === '"') {
|
|
27
|
+
inQuotes = true;
|
|
28
|
+
} else if (ch === ',') {
|
|
29
|
+
fields.push(current.trim());
|
|
30
|
+
current = '';
|
|
31
|
+
} else {
|
|
32
|
+
current += ch;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
fields.push(current.trim());
|
|
37
|
+
return fields;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseCsvRedirects(filePath: string): CloudFrontFunctionRedirectPaths {
|
|
41
|
+
const resolvedPath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
|
42
|
+
const content = readFileSync(resolvedPath, 'utf8');
|
|
43
|
+
const lines = content.split(/\r?\n/).filter(line => line.trim() !== '');
|
|
44
|
+
|
|
45
|
+
if (lines.length === 0) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const headers = parseCsvLine(lines[0]).map(h => h.toLowerCase());
|
|
50
|
+
const sourceIdx = headers.indexOf('source');
|
|
51
|
+
const destIdx = headers.indexOf('destination');
|
|
52
|
+
const codeIdx = headers.indexOf('http_code');
|
|
53
|
+
|
|
54
|
+
if (sourceIdx === -1) {
|
|
55
|
+
throw new Error("redirects.csv: Required column 'source' is missing.");
|
|
56
|
+
}
|
|
57
|
+
if (destIdx === -1) {
|
|
58
|
+
throw new Error("redirects.csv: Required column 'destination' is missing.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result: CloudFrontFunctionRedirectPaths = {};
|
|
62
|
+
|
|
63
|
+
for (let i = 1; i < lines.length; i++) {
|
|
64
|
+
const rowNum = i + 1;
|
|
65
|
+
const fields = parseCsvLine(lines[i]);
|
|
66
|
+
|
|
67
|
+
const source = fields[sourceIdx] ?? '';
|
|
68
|
+
const destination = fields[destIdx] ?? '';
|
|
69
|
+
|
|
70
|
+
if (!source) {
|
|
71
|
+
throw new Error(`redirects.csv: Row ${rowNum} has an empty 'source' value.`);
|
|
72
|
+
}
|
|
73
|
+
if (!destination) {
|
|
74
|
+
throw new Error(`redirects.csv: Row ${rowNum} has an empty 'destination' value.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (source === destination) {
|
|
78
|
+
throw new Error(`redirects.csv: Row ${rowNum} contains a circular redirect — source and destination are the same: '${source}'.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let httpCode: HttpRedirectCode = 302;
|
|
82
|
+
if (codeIdx !== -1) {
|
|
83
|
+
const rawCode = fields[codeIdx] ?? '';
|
|
84
|
+
if (rawCode !== '') {
|
|
85
|
+
const parsed = parseInt(rawCode, 10);
|
|
86
|
+
if (!VALID_HTTP_CODES.includes(parsed)) {
|
|
87
|
+
throw new Error(`redirects.csv: Row ${rowNum} has an invalid 'http_code' value: '${rawCode}'. Expected a redirect status code (e.g. 301, 302, 307, 308).`);
|
|
88
|
+
}
|
|
89
|
+
httpCode = parsed as HttpRedirectCode;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
result[source] = httpCode === 302 ? destination : [destination, httpCode];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Detect indirect circular chains
|
|
97
|
+
detectCircularChains(result);
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function detectCircularChains(redirects: CloudFrontFunctionRedirectPaths): void {
|
|
103
|
+
const graph = new Map<string, string>();
|
|
104
|
+
for (const [source, value] of Object.entries(redirects)) {
|
|
105
|
+
const dest = Array.isArray(value) ? value[0] : value;
|
|
106
|
+
graph.set(source, dest);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const start of graph.keys()) {
|
|
110
|
+
const visited = new Set<string>();
|
|
111
|
+
let current: string | undefined = start;
|
|
112
|
+
while (current !== undefined && graph.has(current)) {
|
|
113
|
+
if (visited.has(current)) {
|
|
114
|
+
const chain = [...visited, current].join(' → ');
|
|
115
|
+
throw new Error(`redirects.csv: Circular redirect chain detected: ${chain}`);
|
|
116
|
+
}
|
|
117
|
+
visited.add(current);
|
|
118
|
+
current = graph.get(current);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function mergeRedirects(
|
|
124
|
+
base: CloudFrontFunctionRedirectPaths | undefined,
|
|
125
|
+
override: CloudFrontFunctionRedirectPaths | undefined
|
|
126
|
+
): CloudFrontFunctionRedirectPaths {
|
|
127
|
+
if (!base && !override) return {};
|
|
128
|
+
if (!base) return { ...override! };
|
|
129
|
+
if (!override) return { ...base };
|
|
130
|
+
|
|
131
|
+
const merged = { ...base };
|
|
132
|
+
for (const [key, value] of Object.entries(override)) {
|
|
133
|
+
if (key in merged) {
|
|
134
|
+
console.warn(`Redirect conflict for '${key}': override wins.`);
|
|
135
|
+
}
|
|
136
|
+
merged[key] = value;
|
|
137
|
+
}
|
|
138
|
+
return merged;
|
|
139
|
+
}
|
package/src/lib/tags.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { IConstruct } from 'constructs';
|
|
2
|
+
import { Tags } from 'aws-cdk-lib/core';
|
|
3
|
+
import { NPM_LIBRARY_NAME } from './const.ts';
|
|
4
|
+
import { TOOL_VERSION } from './version.ts';
|
|
5
|
+
|
|
6
|
+
export const TAG_EVO_VERSION = 'EvoSynthToolVersion';
|
|
7
|
+
export const TAG_EVO_STAGE = 'EvoSynthStageName';
|
|
8
|
+
export const TAG_EVO_STACK = 'EvoSynthStackName';
|
|
9
|
+
|
|
10
|
+
export function createEvoVersionTag(): { [TAG_EVO_VERSION]: string } {
|
|
11
|
+
return { [TAG_EVO_VERSION]: `${NPM_LIBRARY_NAME}@${TOOL_VERSION}` };
|
|
12
|
+
}
|
|
13
|
+
export function addEvoVersionTag(resource: IConstruct) {
|
|
14
|
+
tagResource(resource, createEvoVersionTag());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createEvoStackTag(value: string): { [TAG_EVO_STACK]: string } {
|
|
18
|
+
return { [TAG_EVO_STACK]: value };
|
|
19
|
+
}
|
|
20
|
+
export function addEvoStackTags(resource: IConstruct, value: string){
|
|
21
|
+
addEvoVersionTag(resource);
|
|
22
|
+
tagResource(resource, createEvoStackTag(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createEvoStageTag(value: string): { [TAG_EVO_STAGE]: string } {
|
|
26
|
+
return { [TAG_EVO_STAGE]: value };
|
|
27
|
+
}
|
|
28
|
+
export function addEvoStageTags(resource: IConstruct, value: string) {
|
|
29
|
+
addEvoVersionTag(resource);
|
|
30
|
+
tagResource(resource, createEvoStageTag(value));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function mergeTags(...tags: (Record<string, string> | null | undefined)[]): Record<string, string> {
|
|
34
|
+
const _tags = tags.filter(t => t !== undefined && t !== null) ?? [];
|
|
35
|
+
return Object.assign({}, ..._tags);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function tagResource(resource: IConstruct, tags?: Record<string, string>): void;
|
|
39
|
+
export function tagResource(resource: IConstruct, tag: string, value: string): void;
|
|
40
|
+
export function tagResource(resource: IConstruct, ...args: any[]): void {
|
|
41
|
+
let tags: Record<string, string> | undefined = undefined;
|
|
42
|
+
switch (args.length) {
|
|
43
|
+
case 1:
|
|
44
|
+
tags = tags || undefined;
|
|
45
|
+
break;
|
|
46
|
+
case 2:
|
|
47
|
+
tags = { [args[0]]: args[1] };
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (tags) {
|
|
51
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
52
|
+
Tags.of(resource).add(key, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type DeepPartial<T> = T extends object ? {
|
|
2
|
+
[P in keyof T]?: DeepPartial<T[P]>;
|
|
3
|
+
} : T;
|
|
4
|
+
|
|
5
|
+
export type WithRequiredProperty<Type, Key extends keyof Type> = Omit<Type, Key> & {
|
|
6
|
+
[Property in Key]-?: Type[Property];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface ResourceProps {
|
|
10
|
+
destroy?: boolean;
|
|
11
|
+
tags?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SsmStringParameter {
|
|
15
|
+
path: string;
|
|
16
|
+
version?: number;
|
|
17
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Stack, StackProps } from 'aws-cdk-lib/core';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { CodeBuildPipelineProject, CodeBuildPipelineProjectProps } from '../../constructs/codebuild.js';
|
|
4
|
+
import { CodePipelineProject, AddSourceStageParams, AddBuildStageParams } from '../../constructs/codepipeline.js';
|
|
5
|
+
import { addEvoStackTags } from '../../lib/tags.ts';
|
|
6
|
+
|
|
7
|
+
export interface CodePipelineStackProps {
|
|
8
|
+
pipelineName?: string;
|
|
9
|
+
sourceStage: AddSourceStageParams;
|
|
10
|
+
buildStage?: Pick<AddBuildStageParams, 'stageName' | 'environmentVariables'>;
|
|
11
|
+
codeBuildProject?: Omit<CodeBuildPipelineProjectProps, 'destroy' | 'tags'>,
|
|
12
|
+
destroy?: boolean;
|
|
13
|
+
tags?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export class CodePipelineStack extends Stack {
|
|
16
|
+
public readonly codeBuildPipelineProject: CodeBuildPipelineProject;
|
|
17
|
+
public readonly codePipelineProject: CodePipelineProject;
|
|
18
|
+
|
|
19
|
+
constructor(scope: Construct, id: string, props: CodePipelineStackProps & StackProps) {
|
|
20
|
+
super(scope, id, props);
|
|
21
|
+
addEvoStackTags(this, 'CodePipelineStack');
|
|
22
|
+
|
|
23
|
+
this.codeBuildPipelineProject = new CodeBuildPipelineProject(this, 'CodeBuildPipelineProject', {
|
|
24
|
+
...(props.codeBuildProject ?? {}),
|
|
25
|
+
destroy: props.destroy
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.codePipelineProject = new CodePipelineProject(this, 'CodePipelineProject', {
|
|
29
|
+
pipelineName: props.pipelineName,
|
|
30
|
+
destroy: props.destroy
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const sourceArtifact = this.codePipelineProject.addSourceStage(props.sourceStage);
|
|
34
|
+
|
|
35
|
+
this.codePipelineProject.addBuildStage({
|
|
36
|
+
stageName: props.buildStage?.stageName,
|
|
37
|
+
codeBuildProject: this.codeBuildPipelineProject.project,
|
|
38
|
+
sourceArtifact: sourceArtifact,
|
|
39
|
+
environmentVariables: props.buildStage?.environmentVariables
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Stack, StackProps, PhysicalName } from 'aws-cdk-lib/core';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
|
|
4
|
+
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
5
|
+
import { Distribution, Function, FunctionEventType, IResponseHeadersPolicy, ResponseHeadersPolicyProps, BehaviorOptions, ResponseHeadersPolicy } from 'aws-cdk-lib/aws-cloudfront';
|
|
6
|
+
import { ARecord, AaaaRecord } from "aws-cdk-lib/aws-route53";
|
|
7
|
+
import { Bucket } from "aws-cdk-lib/aws-s3";
|
|
8
|
+
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
|
|
9
|
+
import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
|
|
10
|
+
|
|
11
|
+
import { SSLCertificate } from '../../constructs/certificatemanager.js';
|
|
12
|
+
import { CloudFrontFunctionRedirects, CloudFrontFunction, CloudFrontDistribution } from '../../constructs/cloudfront.js';
|
|
13
|
+
import { getHostedZone, CloudFrontAlias } from '../../constructs/route53.js';
|
|
14
|
+
import { S3Bucket } from '../../constructs/s3.js';
|
|
15
|
+
import { CloudFrontWebAcl } from '../../constructs/waf.js';
|
|
16
|
+
import { domainAsId } from '../../lib/utils.js';
|
|
17
|
+
import { addEvoStackTags } from '../../lib/tags.ts';
|
|
18
|
+
import { SsmStringParameter } from '../../lib/types.ts';
|
|
19
|
+
import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
20
|
+
|
|
21
|
+
export interface BasicAuthCredentials {
|
|
22
|
+
user: string;
|
|
23
|
+
password: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type AWSManagedResponseHeadersPolicy =
|
|
27
|
+
'CORS_ALLOW_ALL_ORIGINS' |
|
|
28
|
+
'CORS_ALLOW_ALL_ORIGINS_AND_SECURITY_HEADERS' |
|
|
29
|
+
'CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT' |
|
|
30
|
+
'CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS' |
|
|
31
|
+
'SECURITY_HEADERS';
|
|
32
|
+
|
|
33
|
+
type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
|
34
|
+
export interface WebGlobalStackProps {
|
|
35
|
+
envType: 'NOT_PROD' | 'PROD';
|
|
36
|
+
description?: string;
|
|
37
|
+
bucketName?: string;
|
|
38
|
+
hostedZone: string;
|
|
39
|
+
domainName: string;
|
|
40
|
+
basicAuth?: string | BasicAuthCredentials | SsmStringParameter;
|
|
41
|
+
viewerRequestResponse?: string;
|
|
42
|
+
redirects?: CloudFrontFunctionRedirects;
|
|
43
|
+
redirectsCsvPath?: string;
|
|
44
|
+
// attachWebAcl?: boolean | string;
|
|
45
|
+
webAcl?: boolean | string | SsmStringParameter;
|
|
46
|
+
responseHeadersPolicy?: false | AWSManagedResponseHeadersPolicy | UUID | SsmStringParameter;
|
|
47
|
+
responseHeadersPolicyProps?: ResponseHeadersPolicyProps;
|
|
48
|
+
destroy?: boolean;
|
|
49
|
+
tags?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class WebGlobalStack extends Stack {
|
|
53
|
+
public readonly sslCertificate: Certificate;
|
|
54
|
+
public readonly s3Bucket: Bucket;
|
|
55
|
+
public readonly cloudFrontFunction: Function;
|
|
56
|
+
public readonly cloudFrontDistribution: Distribution;
|
|
57
|
+
public readonly webAcl: CfnWebACL | undefined;
|
|
58
|
+
public readonly responseHeadersPolicy: IResponseHeadersPolicy | undefined;
|
|
59
|
+
public readonly aRecord: ARecord;
|
|
60
|
+
public readonly aaaaRecord: AaaaRecord;
|
|
61
|
+
|
|
62
|
+
constructor(scope: Construct, id: string, props: WebGlobalStackProps & StackProps) {
|
|
63
|
+
super(scope, id, props);
|
|
64
|
+
addEvoStackTags(this, 'WebGlobalStack');
|
|
65
|
+
|
|
66
|
+
const hostedZone = getHostedZone(this, 'getHostedZone', props.hostedZone);
|
|
67
|
+
|
|
68
|
+
const _sslCert = new SSLCertificate(this, 'SSLCertificate', {
|
|
69
|
+
hostedZone: hostedZone,
|
|
70
|
+
domainName: props.domainName,
|
|
71
|
+
destroy: props.destroy
|
|
72
|
+
});
|
|
73
|
+
this.sslCertificate = _sslCert.certificate;
|
|
74
|
+
|
|
75
|
+
const _s3Bucket = new S3Bucket(this, 'S3Bucket', {
|
|
76
|
+
bucketName: props.bucketName ?? PhysicalName.GENERATE_IF_NEEDED,
|
|
77
|
+
destroy: props.destroy
|
|
78
|
+
});
|
|
79
|
+
this.s3Bucket = _s3Bucket.bucket;
|
|
80
|
+
|
|
81
|
+
let responseHeadersPolicy: IResponseHeadersPolicy | undefined = undefined;
|
|
82
|
+
if (props.responseHeadersPolicy) {
|
|
83
|
+
if (typeof props.responseHeadersPolicy == 'string') {
|
|
84
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(props.responseHeadersPolicy)) {
|
|
85
|
+
responseHeadersPolicy = ResponseHeadersPolicy.fromResponseHeadersPolicyId(this, 'response-headers-policy-by-id', props.responseHeadersPolicy);
|
|
86
|
+
} else {
|
|
87
|
+
responseHeadersPolicy = ResponseHeadersPolicy[props.responseHeadersPolicy as keyof typeof ResponseHeadersPolicy] as IResponseHeadersPolicy ?? ResponseHeadersPolicy.SECURITY_HEADERS;
|
|
88
|
+
}
|
|
89
|
+
} else if (props.responseHeadersPolicy.path) {
|
|
90
|
+
responseHeadersPolicy = ResponseHeadersPolicy.fromResponseHeadersPolicyId(this, 'response-headers-policy-by-id', StringParameter.valueForStringParameter(this, props.responseHeadersPolicy.path, props.responseHeadersPolicy.version));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!responseHeadersPolicy && props.responseHeadersPolicyProps) {
|
|
94
|
+
responseHeadersPolicy = new ResponseHeadersPolicy(this, 'response-headers-policy', {
|
|
95
|
+
...props.responseHeadersPolicyProps,
|
|
96
|
+
responseHeadersPolicyName: props.responseHeadersPolicyProps.responseHeadersPolicyName || `${domainAsId(props.domainName)}-response-headers-policy`
|
|
97
|
+
});
|
|
98
|
+
this.responseHeadersPolicy = responseHeadersPolicy;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const defaultBehavior: BehaviorOptions = {
|
|
102
|
+
origin: S3BucketOrigin.withOriginAccessControl(this.s3Bucket),
|
|
103
|
+
functionAssociations: [],
|
|
104
|
+
responseHeadersPolicy: responseHeadersPolicy ?? props.responseHeadersPolicy !== false ? ResponseHeadersPolicy.SECURITY_HEADERS : undefined
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let cffn: CloudFrontFunction;
|
|
108
|
+
if (props.basicAuth) {
|
|
109
|
+
if (typeof props.basicAuth != 'string') {
|
|
110
|
+
if ((props.basicAuth as BasicAuthCredentials).user && (props.basicAuth as BasicAuthCredentials).password) {
|
|
111
|
+
props.basicAuth = Buffer.from(`${(props.basicAuth as BasicAuthCredentials).user}:${(props.basicAuth as BasicAuthCredentials).password}`).toString('base64');
|
|
112
|
+
} else if ((props.basicAuth as SsmStringParameter).path) {
|
|
113
|
+
props.basicAuth = StringParameter.valueForStringParameter(this, (props.basicAuth as SsmStringParameter).path, (props.basicAuth as SsmStringParameter).version);
|
|
114
|
+
} else {
|
|
115
|
+
props.basicAuth = undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (props.viewerRequestResponse) {
|
|
120
|
+
cffn = CloudFrontFunction.createViewerRequestResponseFunction(this, null, {
|
|
121
|
+
credentials: props.basicAuth,
|
|
122
|
+
redirects: props.redirects,
|
|
123
|
+
redirectsCsvPath: props.redirectsCsvPath,
|
|
124
|
+
code: props.viewerRequestResponse,
|
|
125
|
+
destroy: props.destroy
|
|
126
|
+
});
|
|
127
|
+
} else if (props.basicAuth) {
|
|
128
|
+
cffn = CloudFrontFunction.createBasicAuthDefaultDoc(this, {
|
|
129
|
+
credentials: props.basicAuth,
|
|
130
|
+
redirects: props.redirects,
|
|
131
|
+
redirectsCsvPath: props.redirectsCsvPath,
|
|
132
|
+
destroy: props.destroy
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
cffn = CloudFrontFunction.createDefaultDoc(this, {
|
|
136
|
+
redirects: props.redirects,
|
|
137
|
+
redirectsCsvPath: props.redirectsCsvPath,
|
|
138
|
+
destroy: props.destroy
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
this.cloudFrontFunction = cffn.fn;
|
|
142
|
+
|
|
143
|
+
defaultBehavior.functionAssociations!.push({
|
|
144
|
+
eventType: FunctionEventType.VIEWER_REQUEST,
|
|
145
|
+
function: this.cloudFrontFunction
|
|
146
|
+
});
|
|
147
|
+
defaultBehavior.functionAssociations!.push({
|
|
148
|
+
eventType: FunctionEventType.VIEWER_RESPONSE,
|
|
149
|
+
function: this.cloudFrontFunction
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let webAclId: string | undefined = undefined;
|
|
153
|
+
if (typeof props.webAcl == 'string' && props.webAcl.startsWith('arn:aws')) {
|
|
154
|
+
webAclId = props.webAcl;
|
|
155
|
+
} else if ((props.webAcl as SsmStringParameter).path) {
|
|
156
|
+
webAclId = StringParameter.valueForStringParameter(this, (props.webAcl as SsmStringParameter).path, (props.webAcl as SsmStringParameter).version);
|
|
157
|
+
} else if (props.webAcl === true) {
|
|
158
|
+
const waf = new CloudFrontWebAcl(this, 'CloudFrontWebAcl', {
|
|
159
|
+
name: `${domainAsId(props.domainName)}`,
|
|
160
|
+
destroy: props.destroy
|
|
161
|
+
});
|
|
162
|
+
this.webAcl = waf.acl;
|
|
163
|
+
webAclId = waf.acl.attrArn;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const cfDistribution = new CloudFrontDistribution(this, 'CloudFrontDistribution', {
|
|
167
|
+
domainName: props.domainName,
|
|
168
|
+
description: props.description ?? props.domainName,
|
|
169
|
+
priceClass: props.envType,
|
|
170
|
+
certificate: this.sslCertificate,
|
|
171
|
+
defaultBehavior,
|
|
172
|
+
webAclId
|
|
173
|
+
});
|
|
174
|
+
this.cloudFrontDistribution = cfDistribution.distribution;
|
|
175
|
+
|
|
176
|
+
const cfAlias = new CloudFrontAlias(this, 'CloudFrontAlias', {
|
|
177
|
+
distribution: cfDistribution.distribution,
|
|
178
|
+
domainName: props.domainName,
|
|
179
|
+
hostedZone: hostedZone
|
|
180
|
+
});
|
|
181
|
+
this.aRecord = cfAlias.aRecord;
|
|
182
|
+
this.aaaaRecord = cfAlias.aaaaRecord;
|
|
183
|
+
}
|
|
184
|
+
}
|