@fjall/components-infrastructure 2.34.0 → 3.0.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/dist/lib/app.d.ts +19 -2
- package/dist/lib/app.js +46 -3
- package/dist/lib/patterns/aws/apexDomainPattern.d.ts +11 -12
- package/dist/lib/patterns/aws/apexDomainPattern.js +11 -31
- package/dist/lib/patterns/aws/cdn.js +25 -10
- package/dist/lib/patterns/aws/computeEcs.js +6 -1
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +8 -3
- package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +13 -4
- package/dist/lib/patterns/aws/delegatedDomainPattern.js +57 -8
- package/dist/lib/patterns/aws/devSubstrate.d.ts +2 -1
- package/dist/lib/patterns/aws/devSubstrate.js +18 -8
- package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +2 -9
- package/dist/lib/patterns/aws/dnsRecordComposer.js +5 -97
- package/dist/lib/patterns/aws/domain.d.ts +3 -2
- package/dist/lib/patterns/aws/domain.js +3 -2
- package/dist/lib/patterns/aws/domainValidation.js +71 -46
- package/dist/lib/patterns/aws/externalRecordsPattern.js +2 -11
- package/dist/lib/patterns/aws/index.d.ts +1 -3
- package/dist/lib/patterns/aws/index.js +2 -3
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +22 -8
- package/dist/lib/patterns/aws/interfaces/index.d.ts +0 -1
- package/dist/lib/patterns/aws/interfaces/index.js +3 -0
- package/dist/lib/patterns/aws/interfaces/pattern.d.ts +55 -10
- package/dist/lib/patterns/aws/patternDomain.d.ts +77 -0
- package/dist/lib/patterns/aws/patternDomain.js +198 -0
- package/dist/lib/patterns/aws/payload.js +23 -30
- package/dist/lib/patterns/aws/staticSite.d.ts +1 -0
- package/dist/lib/patterns/aws/staticSite.js +28 -33
- package/dist/lib/resources/aws/base/awsStack.d.ts +8 -0
- package/dist/lib/resources/aws/base/awsStack.js +20 -17
- package/dist/lib/resources/aws/compute/ecs.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecs.js +1 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.js +212 -47
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +60 -9
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +14 -1
- package/dist/lib/resources/aws/compute/ecsValidation.js +51 -0
- package/dist/lib/resources/aws/networking/dnsRecord/aRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aaaaRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +23 -1
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +14 -3
- package/dist/lib/resources/aws/networking/dnsRecord/caaRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/cnameRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.d.ts +10 -1
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +21 -4
- package/dist/lib/resources/aws/networking/dnsRecord/mxRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/nsRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/srvRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/txtRecord.js +3 -2
- package/dist/lib/resources/aws/networking/domainCertificate.d.ts +28 -4
- package/dist/lib/resources/aws/networking/domainCertificate.js +24 -10
- package/dist/lib/resources/aws/networking/hostedZone.d.ts +11 -0
- package/dist/lib/resources/aws/networking/hostedZone.js +6 -0
- package/dist/lib/utils/costAllocationTags.d.ts +9 -0
- package/dist/lib/utils/costAllocationTags.js +6 -1
- package/dist/lib/utils/dnsRecordRegistry.d.ts +52 -0
- package/dist/lib/utils/dnsRecordRegistry.js +64 -0
- package/dist/lib/utils/domainTypes.d.ts +43 -36
- package/dist/lib/utils/domainTypes.js +65 -2
- package/dist/lib/utils/managedDomainContext.d.ts +37 -0
- package/dist/lib/utils/managedDomainContext.js +111 -0
- package/package.json +4 -4
package/dist/lib/app.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type CloudAssembly } from "aws-cdk-lib/cx-api";
|
|
|
3
3
|
import { type Construct } from "constructs";
|
|
4
4
|
import { type IVpc } from "aws-cdk-lib/aws-ec2";
|
|
5
5
|
import { type Role } from "aws-cdk-lib/aws-iam";
|
|
6
|
-
import { AwsStack } from "./resources/index.js";
|
|
6
|
+
import { AwsStack, type AwsStackProps } from "./resources/index.js";
|
|
7
7
|
import { type Ecr } from "./resources/aws/storage/ecr.js";
|
|
8
8
|
import { type AnyDatabase } from "./patterns/aws/database.js";
|
|
9
9
|
import { type INetworkProps, type Network } from "./patterns/aws/network.js";
|
|
@@ -123,8 +123,10 @@ export declare class App extends CdkApp {
|
|
|
123
123
|
*
|
|
124
124
|
* @param key - The key of the stack
|
|
125
125
|
* @param dependencies - The stack(s) that this stack depends on
|
|
126
|
+
* @param props - Stack props applied on first creation only (cached
|
|
127
|
+
* stacks are returned as-is)
|
|
126
128
|
*/
|
|
127
|
-
getStack(key: string, dependencies?: AwsStack | AwsStack[]): AwsStack;
|
|
129
|
+
getStack(key: string, dependencies?: AwsStack | AwsStack[], props?: AwsStackProps): AwsStack;
|
|
128
130
|
/**
|
|
129
131
|
* Retrieve default compute stack - named as `${this.name}Compute`
|
|
130
132
|
*
|
|
@@ -162,6 +164,21 @@ export declare class App extends CdkApp {
|
|
|
162
164
|
* require VPC, but we maintain consistent stack dependency patterns.
|
|
163
165
|
*/
|
|
164
166
|
getDefaultMessagingStack(): AwsStack;
|
|
167
|
+
/**
|
|
168
|
+
* Retrieve the app-owned us-east-1 certificate stack — named
|
|
169
|
+
* `${this.name}UsEast1Certificates` (domain gold-plating D3, BYO path).
|
|
170
|
+
*
|
|
171
|
+
* CloudFront accepts viewer certificates from us-east-1 only, so
|
|
172
|
+
* CloudFront-consuming patterns (staticsite, payload) whose app deploys to
|
|
173
|
+
* another region provision their DomainCertificate here; the distribution
|
|
174
|
+
* in the Cdn stack consumes the ARN through CDK cross-region references.
|
|
175
|
+
*
|
|
176
|
+
* `crossRegionReferences: true` must be set on BOTH the producing and the
|
|
177
|
+
* consuming stack; both get it at construction (the Cdn stack in
|
|
178
|
+
* getDefaultCdnStack, this stack here) and the emitted
|
|
179
|
+
* Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
|
|
180
|
+
*/
|
|
181
|
+
getUsEast1CertificateStack(): AwsStack;
|
|
165
182
|
/**
|
|
166
183
|
* Get a VPC by name. If no name is provided, returns the default VPC.
|
|
167
184
|
*
|
package/dist/lib/app.js
CHANGED
|
@@ -18,6 +18,7 @@ import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
|
|
|
18
18
|
import { FJALL_AUDIT_CONFIG } from "./config/audit.js";
|
|
19
19
|
import { FjallLogger } from "./utils/validationLogger.js";
|
|
20
20
|
import { getManifestCollector, writeManifest } from "./utils/manifestWriter.js";
|
|
21
|
+
import { resetDnsRecordRegistry } from "./utils/dnsRecordRegistry.js";
|
|
21
22
|
import { toPascalCase, toKebab } from "./utils/capitaliseString.js";
|
|
22
23
|
import { COST_ALLOCATION_TAGS } from "./utils/costAllocationTags.js";
|
|
23
24
|
import { DEFAULT_ORG_ID, resolveOrgId } from "./utils/cdkContext.js";
|
|
@@ -168,6 +169,10 @@ export class App extends CdkApp {
|
|
|
168
169
|
}
|
|
169
170
|
static resetForTesting() {
|
|
170
171
|
App.instance = null;
|
|
172
|
+
// App-scoped synth state resets with the App singleton — the DNS
|
|
173
|
+
// record-collision registry (D5) follows the manifest-collector lifecycle
|
|
174
|
+
// without requiring a third per-test reset call.
|
|
175
|
+
resetDnsRecordRegistry();
|
|
171
176
|
}
|
|
172
177
|
/**
|
|
173
178
|
* Retrieve a stack by key. If the stack does not exist, it will be created.
|
|
@@ -175,8 +180,10 @@ export class App extends CdkApp {
|
|
|
175
180
|
*
|
|
176
181
|
* @param key - The key of the stack
|
|
177
182
|
* @param dependencies - The stack(s) that this stack depends on
|
|
183
|
+
* @param props - Stack props applied on first creation only (cached
|
|
184
|
+
* stacks are returned as-is)
|
|
178
185
|
*/
|
|
179
|
-
getStack(key, dependencies) {
|
|
186
|
+
getStack(key, dependencies, props) {
|
|
180
187
|
// Apply the aspect once before creating the first stack
|
|
181
188
|
if (!this.aspectApplied &&
|
|
182
189
|
Object.keys(this.stacks).length === 0 &&
|
|
@@ -184,7 +191,7 @@ export class App extends CdkApp {
|
|
|
184
191
|
this.applyTagsAspect();
|
|
185
192
|
}
|
|
186
193
|
if (!this.stacks[key]) {
|
|
187
|
-
this.stacks[key] = new AwsStack(key, dependencies);
|
|
194
|
+
this.stacks[key] = new AwsStack(key, dependencies, props);
|
|
188
195
|
}
|
|
189
196
|
return this.stacks[key];
|
|
190
197
|
}
|
|
@@ -225,7 +232,10 @@ export class App extends CdkApp {
|
|
|
225
232
|
* stack is synthesised.
|
|
226
233
|
*/
|
|
227
234
|
getDefaultCdnStack() {
|
|
228
|
-
|
|
235
|
+
// crossRegionReferences is enabled at creation so the Cdn stack can
|
|
236
|
+
// consume a us-east-1 certificate (domain gold-plating D3, BYO path).
|
|
237
|
+
// The flag is inert unless a cross-region reference is synthesised.
|
|
238
|
+
return this.getStack(`${this.stackPrefix}Cdn`, this.networkDisabled ? undefined : this.getDefaultNetworkStack(), { crossRegionReferences: true });
|
|
229
239
|
}
|
|
230
240
|
/**
|
|
231
241
|
* Retrieve default messaging stack - named as `${this.name}Messaging`
|
|
@@ -237,6 +247,39 @@ export class App extends CdkApp {
|
|
|
237
247
|
getDefaultMessagingStack() {
|
|
238
248
|
return this.getStack(`${this.stackPrefix}Messaging`, this.getDefaultNetworkStack());
|
|
239
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Retrieve the app-owned us-east-1 certificate stack — named
|
|
252
|
+
* `${this.name}UsEast1Certificates` (domain gold-plating D3, BYO path).
|
|
253
|
+
*
|
|
254
|
+
* CloudFront accepts viewer certificates from us-east-1 only, so
|
|
255
|
+
* CloudFront-consuming patterns (staticsite, payload) whose app deploys to
|
|
256
|
+
* another region provision their DomainCertificate here; the distribution
|
|
257
|
+
* in the Cdn stack consumes the ARN through CDK cross-region references.
|
|
258
|
+
*
|
|
259
|
+
* `crossRegionReferences: true` must be set on BOTH the producing and the
|
|
260
|
+
* consuming stack; both get it at construction (the Cdn stack in
|
|
261
|
+
* getDefaultCdnStack, this stack here) and the emitted
|
|
262
|
+
* Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
|
|
263
|
+
*/
|
|
264
|
+
getUsEast1CertificateStack() {
|
|
265
|
+
const key = `${this.stackPrefix}UsEast1Certificates`;
|
|
266
|
+
if (!this.stacks[key]) {
|
|
267
|
+
const account = process.env.CDK_DEFAULT_ACCOUNT;
|
|
268
|
+
if (!account) {
|
|
269
|
+
throw new Error(`App '${this.name}': the us-east-1 certificate stack needs a ` +
|
|
270
|
+
"concrete account (got CDK_DEFAULT_ACCOUNT undefined). " +
|
|
271
|
+
"Cross-region references resolve only between env-pinned stacks " +
|
|
272
|
+
"— deploy through the Fjall CLI, or export CDK_DEFAULT_ACCOUNT " +
|
|
273
|
+
"and CDK_DEFAULT_REGION before synthesising.");
|
|
274
|
+
}
|
|
275
|
+
this.stacks[key] = new AwsStack(key, undefined, {
|
|
276
|
+
accountId: account,
|
|
277
|
+
region: "us-east-1",
|
|
278
|
+
crossRegionReferences: true
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return this.stacks[key];
|
|
282
|
+
}
|
|
240
283
|
/**
|
|
241
284
|
* Get a VPC by name. If no name is provided, returns the default VPC.
|
|
242
285
|
*
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import type { Construct } from "constructs";
|
|
2
2
|
import type { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
3
3
|
import type { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
4
|
-
import type { ManualRecord, Route53ApexProps
|
|
4
|
+
import type { ManualRecord, Route53ApexProps } from "./interfaces/domain.js";
|
|
5
5
|
export interface ApexDomainPatternResult {
|
|
6
6
|
readonly hostedZone: IHostedZone;
|
|
7
7
|
readonly certificates: Map<string, ICertificate>;
|
|
8
8
|
readonly nameServers: string[] | undefined;
|
|
9
9
|
readonly manualRecords: ManualRecord[];
|
|
10
|
-
readonly delegations: SubdomainDelegation[];
|
|
11
10
|
}
|
|
12
11
|
/**
|
|
13
12
|
* Composition for `registrar: "route53"`. Creates (or imports) the apex
|
|
14
|
-
* `HostedZone
|
|
15
|
-
* each child's nameservers published as exports), and composes all user
|
|
16
|
-
* records + certificates.
|
|
13
|
+
* `HostedZone` and composes all user records + certificates.
|
|
17
14
|
*
|
|
18
|
-
* Delegation
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* parent
|
|
24
|
-
*
|
|
15
|
+
* Delegation direction (D8): child writes NS. The apex zone's contribution
|
|
16
|
+
* to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
|
|
17
|
+
* creates on the create path — the delegated child `Domain` assumes that
|
|
18
|
+
* role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs its own NS
|
|
19
|
+
* records into this zone via `CrossAccountZoneDelegationRecord`. The old
|
|
20
|
+
* parent-writes `delegations[]` path (NS records from `Fn.importValue` of
|
|
21
|
+
* child nameserver exports) was removed with the H13 legacy layer: it was
|
|
22
|
+
* export-locked, child-first-deploy-ordered, and removal required a parent
|
|
23
|
+
* redeploy.
|
|
25
24
|
*/
|
|
26
25
|
export declare function composeApexDomain(scope: Construct, props: Route53ApexProps): ApexDomainPatternResult;
|
|
@@ -1,24 +1,21 @@
|
|
|
1
|
-
import { Fn } from "aws-cdk-lib";
|
|
2
|
-
import { getDomainExportNames } from "@fjall/util";
|
|
3
1
|
import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
4
2
|
import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
|
|
5
|
-
import { NsRecord } from "../../resources/aws/networking/dnsRecord/index.js";
|
|
6
3
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
7
4
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
8
5
|
import { resolveOrgId } from "../../utils/cdkContext.js";
|
|
9
6
|
/**
|
|
10
7
|
* Composition for `registrar: "route53"`. Creates (or imports) the apex
|
|
11
|
-
* `HostedZone
|
|
12
|
-
* each child's nameservers published as exports), and composes all user
|
|
13
|
-
* records + certificates.
|
|
8
|
+
* `HostedZone` and composes all user records + certificates.
|
|
14
9
|
*
|
|
15
|
-
* Delegation
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* parent
|
|
21
|
-
*
|
|
10
|
+
* Delegation direction (D8): child writes NS. The apex zone's contribution
|
|
11
|
+
* to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
|
|
12
|
+
* creates on the create path — the delegated child `Domain` assumes that
|
|
13
|
+
* role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs its own NS
|
|
14
|
+
* records into this zone via `CrossAccountZoneDelegationRecord`. The old
|
|
15
|
+
* parent-writes `delegations[]` path (NS records from `Fn.importValue` of
|
|
16
|
+
* child nameserver exports) was removed with the H13 legacy layer: it was
|
|
17
|
+
* export-locked, child-first-deploy-ordered, and removal required a parent
|
|
18
|
+
* redeploy.
|
|
22
19
|
*/
|
|
23
20
|
export function composeApexDomain(scope, props) {
|
|
24
21
|
const safeZone = toPascalCase(getSafeZoneName(props.zoneName));
|
|
@@ -51,22 +48,6 @@ export function composeApexDomain(scope, props) {
|
|
|
51
48
|
if (props.records && props.records.length > 0) {
|
|
52
49
|
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, props.zoneName, props.records);
|
|
53
50
|
}
|
|
54
|
-
const delegations = props.delegations ?? [];
|
|
55
|
-
delegations.forEach((delegation, index) => {
|
|
56
|
-
const childZoneName = `${delegation.subdomain}.${props.zoneName}`;
|
|
57
|
-
const safeChild = toPascalCase(getSafeZoneName(childZoneName));
|
|
58
|
-
const childExports = getDomainExportNames(childZoneName);
|
|
59
|
-
// Cross-phase dependency: child stack must deploy first; CFN fails here
|
|
60
|
-
// at deploy if the nameservers export does not yet exist.
|
|
61
|
-
const nameserversToken = Fn.importValue(childExports.nameservers);
|
|
62
|
-
new NsRecord(scope, `${safeZone}Delegation${safeChild}${index}`, {
|
|
63
|
-
zone: hostedZoneConstruct.hostedZone,
|
|
64
|
-
zoneName: props.zoneName,
|
|
65
|
-
recordName: delegation.subdomain,
|
|
66
|
-
// Fn.split returns a list token suitable for a multi-value NS record.
|
|
67
|
-
values: Fn.split(",", nameserversToken)
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
51
|
return {
|
|
71
52
|
hostedZone: hostedZoneConstruct.hostedZone,
|
|
72
53
|
certificates,
|
|
@@ -75,8 +56,7 @@ export function composeApexDomain(scope, props) {
|
|
|
75
56
|
nameServers: hostedZoneConstruct.isImported
|
|
76
57
|
? undefined
|
|
77
58
|
: (hostedZoneConstruct.nameServers ?? undefined),
|
|
78
|
-
manualRecords: []
|
|
79
|
-
delegations
|
|
59
|
+
manualRecords: []
|
|
80
60
|
};
|
|
81
61
|
}
|
|
82
62
|
function normaliseCertificate(cert) {
|
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
import { Fn } from "aws-cdk-lib";
|
|
1
|
+
import { Fn, Token } from "aws-cdk-lib";
|
|
2
2
|
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
3
|
import { CloudFrontDistribution } from "../../resources/aws/cdn/index.js";
|
|
4
4
|
import { isStorage } from "./storage.js";
|
|
5
5
|
import { isCompute, isEcsCompute, isLambdaCompute } from "./compute.js";
|
|
6
|
-
import { FjallLogger } from "../../utils/validationLogger.js";
|
|
7
6
|
/**
|
|
8
|
-
* Validates CDN props
|
|
7
|
+
* Validates CDN props — synth-time hard errors for shapes CloudFormation
|
|
8
|
+
* would only reject at deploy.
|
|
9
9
|
*/
|
|
10
10
|
function validateCdnProps(props) {
|
|
11
11
|
const hasCertificate = !!(props.certificate || props.certificateArn);
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// CloudFront accepts viewer certificates from us-east-1 only. A literal
|
|
13
|
+
// ARN is inspectable at synth (design H3/D3 — the old advisory log is
|
|
14
|
+
// replaced by real us-east-1 provisioning in the patterns; this assert
|
|
15
|
+
// covers the BYO-ARN escape hatch), so fail here with the cure rather
|
|
16
|
+
// than at CloudFormation with InvalidViewerCertificate.
|
|
17
|
+
if (props.certificateArn !== undefined &&
|
|
18
|
+
!Token.isUnresolved(props.certificateArn)) {
|
|
19
|
+
const certificateRegion = props.certificateArn.split(":")[3];
|
|
20
|
+
if (certificateRegion !== "us-east-1") {
|
|
21
|
+
throw new Error(`CDN 'certificateArn' is in region '${certificateRegion}' ` +
|
|
22
|
+
`(got '${props.certificateArn}'). CloudFront accepts viewer ` +
|
|
23
|
+
"certificates from us-east-1 only — pass the ARN of a us-east-1 " +
|
|
24
|
+
"certificate, or set the pattern's domain zone identity so it " +
|
|
25
|
+
"provisions one.");
|
|
26
|
+
}
|
|
16
27
|
}
|
|
17
|
-
//
|
|
28
|
+
// Aliases without a viewer certificate fail at CloudFormation deploy; fail
|
|
29
|
+
// at synth with the cure instead.
|
|
18
30
|
if (props.domainNames && props.domainNames.length > 0 && !hasCertificate) {
|
|
19
|
-
|
|
20
|
-
"
|
|
31
|
+
throw new Error(`CDN 'domainNames' [${props.domainNames.join(", ")}] were provided ` +
|
|
32
|
+
"without a certificate. CloudFront serves custom domain names only " +
|
|
33
|
+
"with a us-east-1 ACM certificate — pass 'certificate' or " +
|
|
34
|
+
"'certificateArn', or configure the pattern's 'domain' with a zone " +
|
|
35
|
+
"identity so it provisions one.");
|
|
21
36
|
}
|
|
22
37
|
// Validate ALB origin
|
|
23
38
|
if (props.originType === "alb") {
|
|
@@ -25,7 +25,7 @@ import { FjallLogger } from "../../utils/validationLogger.js";
|
|
|
25
25
|
import { VALIDATION_PATTERNS } from "@fjall/generator";
|
|
26
26
|
import { evaluateBakeGuard } from "@fjall/util/docker";
|
|
27
27
|
import { toKebab, buildParameterPath } from "@fjall/util";
|
|
28
|
-
import { validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
|
|
28
|
+
import { validateEcsDomainConfig, validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
|
|
29
29
|
import { COMPUTE_DEFAULTS, collectImportedSecretNames } from "./compute.js";
|
|
30
30
|
import { isHookMigrations } from "./computeEcsTypes.js";
|
|
31
31
|
export { ScalingType } from "./computeEcsTypes.js";
|
|
@@ -81,6 +81,11 @@ function validateSeparateTaskDef(serviceName, separateTaskDef) {
|
|
|
81
81
|
* Extracted for clarity and detail parity with database/network patterns.
|
|
82
82
|
*/
|
|
83
83
|
export function validateEcsProps(props) {
|
|
84
|
+
// Mirror of the resources-layer domainConfig checks (H8 legacy `region`
|
|
85
|
+
// rejection, routingPolicy shape) — defence-in-depth per generator-standards
|
|
86
|
+
// § "Validate at the Lowest Layer the Field Belongs To". The cluster name is
|
|
87
|
+
// the construct id, unavailable here, so errors prefix with the domain name.
|
|
88
|
+
validateEcsDomainConfig(props.cluster?.domainConfig);
|
|
84
89
|
// Validate services array exists and is not empty
|
|
85
90
|
if (!props.services || props.services.length === 0) {
|
|
86
91
|
throw new Error("At least one service must be specified in 'services' array.");
|
|
@@ -522,6 +522,10 @@ export interface EcsClusterConfig {
|
|
|
522
522
|
* Domain for HTTPS access.
|
|
523
523
|
* - Omit: ALB created with default DNS (*.elb.amazonaws.com)
|
|
524
524
|
* - Specified: Creates ACM certificate + Route53 DNS A record
|
|
525
|
+
*
|
|
526
|
+
* Requires `domainConfig` to name a zone source (`managedDomain` or
|
|
527
|
+
* `hostedZone`) — Fjall never creates a hosted zone inside an app stack
|
|
528
|
+
* (design D1/H1), so a bare `domain` with no zone source throws at synth.
|
|
525
529
|
*/
|
|
526
530
|
domain?: string;
|
|
527
531
|
/**
|
|
@@ -538,9 +542,10 @@ export interface EcsClusterConfig {
|
|
|
538
542
|
*/
|
|
539
543
|
directAccess?: boolean;
|
|
540
544
|
/**
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
545
|
+
* Domain configuration: zone source (managed domain or imported zone),
|
|
546
|
+
* certificate, and routing policy (latency via `routingPolicy`, weighted,
|
|
547
|
+
* geo). Required for any custom domain; enables multi-region deployments
|
|
548
|
+
* with advanced DNS routing.
|
|
544
549
|
*/
|
|
545
550
|
domainConfig?: DomainConfig;
|
|
546
551
|
/**
|
|
@@ -9,9 +9,18 @@ export interface DelegatedDomainPatternResult {
|
|
|
9
9
|
readonly manualRecords: ManualRecord[];
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Composition for `registrar: "external-delegated"
|
|
13
|
-
* for `{delegatedSubdomain}.{zoneName}`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Composition for `registrar: "external-delegated"` — the delegated-child
|
|
13
|
+
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` and,
|
|
14
|
+
* when `parentDelegationRoleArn` is present, UPSERTs the child NS into the
|
|
15
|
+
* parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
|
|
16
|
+
* delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
|
|
17
|
+
* literal — validated in `validateDomainProps` (VD2-a). When absent, the
|
|
18
|
+
* user delegates out of band (external registrar / manual NS) and a synth
|
|
19
|
+
* warning flags that the zone will not resolve until they do.
|
|
20
|
+
*
|
|
21
|
+
* `phase: "zone"` early-returns before certificate issuance (R2 cert-hang
|
|
22
|
+
* guard, shared `DomainDeployPhase` contract with `DevSubstrate`): issuing a
|
|
23
|
+
* DNS-validated certificate before the delegation has propagated hangs ACM
|
|
24
|
+
* until CloudFormation rolls back (~1 h).
|
|
16
25
|
*/
|
|
17
26
|
export declare function composeDelegatedDomain(scope: Construct, props: ExternalDelegatedProps): DelegatedDomainPatternResult;
|
|
@@ -1,22 +1,75 @@
|
|
|
1
|
+
import { Annotations } from "aws-cdk-lib";
|
|
2
|
+
import { Role } from "../../resources/aws/iam/index.js";
|
|
1
3
|
import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
4
|
+
import { CrossAccountDelegationRecord } from "../../resources/aws/networking/crossAccountDelegationRecord.js";
|
|
2
5
|
import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
|
|
3
6
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
4
7
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
8
|
+
import { DOMAIN_DEPLOY_DEFAULT_PHASE } from "../../utils/domainTypes.js";
|
|
5
9
|
/**
|
|
6
|
-
* Composition for `registrar: "external-delegated"
|
|
7
|
-
* for `{delegatedSubdomain}.{zoneName}`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Composition for `registrar: "external-delegated"` — the delegated-child
|
|
11
|
+
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` and,
|
|
12
|
+
* when `parentDelegationRoleArn` is present, UPSERTs the child NS into the
|
|
13
|
+
* parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
|
|
14
|
+
* delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
|
|
15
|
+
* literal — validated in `validateDomainProps` (VD2-a). When absent, the
|
|
16
|
+
* user delegates out of band (external registrar / manual NS) and a synth
|
|
17
|
+
* warning flags that the zone will not resolve until they do.
|
|
18
|
+
*
|
|
19
|
+
* `phase: "zone"` early-returns before certificate issuance (R2 cert-hang
|
|
20
|
+
* guard, shared `DomainDeployPhase` contract with `DevSubstrate`): issuing a
|
|
21
|
+
* DNS-validated certificate before the delegation has propagated hangs ACM
|
|
22
|
+
* until CloudFormation rolls back (~1 h).
|
|
10
23
|
*/
|
|
11
24
|
export function composeDelegatedDomain(scope, props) {
|
|
12
25
|
const effectiveZone = `${props.delegatedSubdomain}.${props.zoneName}`;
|
|
13
26
|
const safeZone = toPascalCase(getSafeZoneName(effectiveZone));
|
|
14
27
|
const hostedZoneConstruct = new HostedZone(scope, `${safeZone}HostedZone`, {
|
|
15
28
|
zoneName: effectiveZone,
|
|
29
|
+
// The PARENT account owns the delegation role — this is the child.
|
|
16
30
|
createDelegationRole: false,
|
|
17
31
|
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
18
32
|
costAllocationDomain: props.zoneName
|
|
19
33
|
});
|
|
34
|
+
// D8 child-writes delegation. The in-stack custom resource assumes the
|
|
35
|
+
// parent account's DelegationRole by LITERAL ARN (Role.fromRoleArn) — NOT
|
|
36
|
+
// Fn.importValue, which resolves same-account only (VD2-a). The parent
|
|
37
|
+
// DelegationRole trusts the whole org (OrganizationPrincipal), so a child
|
|
38
|
+
// account can assume it. The CLI injects the ARN via
|
|
39
|
+
// ManagedDomainBinding.delegationRoleArn at deploy time.
|
|
40
|
+
if (props.parentDelegationRoleArn !== undefined) {
|
|
41
|
+
const parentRole = Role.fromRoleArn(scope, `${safeZone}ParentDelegationRole`, props.parentDelegationRoleArn);
|
|
42
|
+
new CrossAccountDelegationRecord(scope, `${safeZone}DelegationRecord`, {
|
|
43
|
+
delegationRole: parentRole,
|
|
44
|
+
delegatedZone: hostedZoneConstruct.hostedZone,
|
|
45
|
+
delegatedZoneName: effectiveZone,
|
|
46
|
+
parentHostedZoneName: props.zoneName,
|
|
47
|
+
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
48
|
+
costAllocationDomain: props.zoneName
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
Annotations.of(scope).addWarning(`Zone '${effectiveZone}' will not resolve until it is delegated: no ` +
|
|
53
|
+
`parentDelegationRoleArn was provided, so no NS records are written ` +
|
|
54
|
+
`into '${props.zoneName}'. Deploy via the Fjall CLI (which injects ` +
|
|
55
|
+
`the parent DelegationRole ARN from the managed-domain binding) or ` +
|
|
56
|
+
`add the zone's NS records at the parent registrar manually.`);
|
|
57
|
+
}
|
|
58
|
+
const nameServers = hostedZoneConstruct.nameServers ?? [];
|
|
59
|
+
const records = props.records ?? [];
|
|
60
|
+
if (records.length > 0) {
|
|
61
|
+
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, effectiveZone, records);
|
|
62
|
+
}
|
|
63
|
+
// Step 2 of the R2 two-step gate: issuing the cert before the delegation
|
|
64
|
+
// NS has propagated hangs ACM DNS-validation, so "zone" stops here.
|
|
65
|
+
if ((props.phase ?? DOMAIN_DEPLOY_DEFAULT_PHASE) !== "full") {
|
|
66
|
+
return {
|
|
67
|
+
hostedZone: hostedZoneConstruct.hostedZone,
|
|
68
|
+
certificates: new Map(),
|
|
69
|
+
nameServers,
|
|
70
|
+
manualRecords: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
20
73
|
const certificates = new Map();
|
|
21
74
|
(props.certificates ?? []).forEach((cert, index) => {
|
|
22
75
|
const normalised = normaliseCertificate(cert);
|
|
@@ -31,10 +84,6 @@ export function composeDelegatedDomain(scope, props) {
|
|
|
31
84
|
});
|
|
32
85
|
certificates.set(normalised.domainName, dc.certificate);
|
|
33
86
|
});
|
|
34
|
-
if (props.records && props.records.length > 0) {
|
|
35
|
-
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, effectiveZone, props.records);
|
|
36
|
-
}
|
|
37
|
-
const nameServers = hostedZoneConstruct.nameServers ?? [];
|
|
38
87
|
return {
|
|
39
88
|
hostedZone: hostedZoneConstruct.hostedZone,
|
|
40
89
|
certificates,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import type App from "../../app.js";
|
|
3
|
+
import { type DomainDeployPhase } from "../../utils/domainTypes.js";
|
|
3
4
|
/**
|
|
4
5
|
* Props for the internal `DevSubstrate` construct.
|
|
5
6
|
*
|
|
@@ -44,7 +45,7 @@ export interface IDevSubstrateProps {
|
|
|
44
45
|
* only after the delegated NS has propagated, else ACM DNS-validation hangs.
|
|
45
46
|
* Defaults to `"full"`. Inert unless `domain` is set.
|
|
46
47
|
*/
|
|
47
|
-
readonly phase?:
|
|
48
|
+
readonly phase?: DomainDeployPhase;
|
|
48
49
|
/**
|
|
49
50
|
* Public domain the substrate hangs a `dev.<appDomain>` child zone under. When
|
|
50
51
|
* omitted, the substrate stands up no zone/cert (early milestone — G2 reaches
|
|
@@ -24,6 +24,7 @@ import { CrossAccountDelegationRecord } from "../../resources/aws/networking/cro
|
|
|
24
24
|
import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
|
|
25
25
|
import { Role } from "../../resources/aws/iam/role.js";
|
|
26
26
|
import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
27
|
+
import { DOMAIN_DEPLOY_DEFAULT_PHASE } from "../../utils/domainTypes.js";
|
|
27
28
|
import { DEV_AURORA_DEFAULTS } from "../../resources/aws/database/rdsDefaults.js";
|
|
28
29
|
import { DEV_APP_TAG_KEY, DEV_TAG_KEY, DEV_TAG_VALUE } from "../../utils/devSubstrateTags.js";
|
|
29
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -54,12 +55,11 @@ const DEV_SUBSTRATE_PARENT_ROLE_ID = "DevSubstrateParentDelegationRole";
|
|
|
54
55
|
/** Construct ids for the substrate's wildcard cert + :443 HTTPS listener (A6). */
|
|
55
56
|
const DEV_SUBSTRATE_CERT_ID = "DevSubstrateCertificate";
|
|
56
57
|
const DEV_SUBSTRATE_HTTPS_LISTENER_ID = "DevSubstrateHttpsListener";
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const DEV_SUBSTRATE_DEFAULT_PHASE = "full";
|
|
58
|
+
// Default deploy phase when `phase` is omitted — the cert + :443 listener
|
|
59
|
+
// build. Shared with the delegated `Domain` topology via
|
|
60
|
+
// `DOMAIN_DEPLOY_DEFAULT_PHASE` (utils/domainTypes.ts): the R2 two-step guard
|
|
61
|
+
// sets `"zone"` explicitly for step 1, so the safe default for a single-shot
|
|
62
|
+
// deploy is the complete substrate.
|
|
63
63
|
/**
|
|
64
64
|
* The substrate's database mode, surfaced to slot-ops as an export so the
|
|
65
65
|
* hand-created G2 slot knows whether to wire Aurora Data-API creds or a
|
|
@@ -142,6 +142,11 @@ export class DevSubstrate extends Construct {
|
|
|
142
142
|
// exposes for the cluster ARN; it returns databaseCluster.clusterArn.
|
|
143
143
|
dbClusterArn: database.getSnapshotTarget().arn,
|
|
144
144
|
dbSecretArn: database.getCredentials().secret.secretArn,
|
|
145
|
+
// Proxy-less substrate ⇒ getHostEndpoint() is the cluster writer endpoint;
|
|
146
|
+
// it + the port ride the exports because the dev role cannot rds:Describe*
|
|
147
|
+
// at runtime to resolve them.
|
|
148
|
+
dbWriterEndpoint: database.getHostEndpoint(),
|
|
149
|
+
dbPort: database.getHostPort(),
|
|
145
150
|
slotExecutionRoleArn: slotExecutionRole.roleArn,
|
|
146
151
|
slotRepositoryUri: slotRepository.repositoryUri,
|
|
147
152
|
wakerTargetGroupArn: wakerTargetGroup.targetGroupArn,
|
|
@@ -244,7 +249,10 @@ export class DevSubstrate extends Construct {
|
|
|
244
249
|
// fence-pinned name — the reaper finds it by the subtree fence tags. A pinned
|
|
245
250
|
// name would only reintroduce the 32-char ALB-name truncation dance.
|
|
246
251
|
const loadBalancer = createApplicationLoadBalancer(this, DEV_SUBSTRATE_ALB_ID, { vpc, internetFacing: true, securityGroup: albSecurityGroup });
|
|
247
|
-
slotSecurityGroup.connections.allowFrom(albSecurityGroup, Port.tcp(DEV_SUBSTRATE_SLOT_PORT),
|
|
252
|
+
slotSecurityGroup.connections.allowFrom(albSecurityGroup, Port.tcp(DEV_SUBSTRATE_SLOT_PORT),
|
|
253
|
+
// EC2 SG-rule descriptions reject non-ASCII (allowed set is
|
|
254
|
+
// `a-zA-Z0-9. _-:/()#,@[]+=&;{}!$*` — no arrow, em-dash, or even `>`).
|
|
255
|
+
"Dev substrate ALB to slot tasks");
|
|
248
256
|
// Unconditional 404 default — the substrate's only routes are per-slot
|
|
249
257
|
// host-header rules, so an unmatched host must 404, never fall through (the ECS
|
|
250
258
|
// path's 404 is conditional on >=2 routes). The :443 listener + wildcard cert
|
|
@@ -327,7 +335,7 @@ export class DevSubstrate extends Construct {
|
|
|
327
335
|
}
|
|
328
336
|
// Step 2 of the R2 two-step gate: synthesising the cert before the step-1 NS
|
|
329
337
|
// has propagated hangs ACM DNS-validation for hours, so "zone" stops here.
|
|
330
|
-
if ((props.phase ??
|
|
338
|
+
if ((props.phase ?? DOMAIN_DEPLOY_DEFAULT_PHASE) !== "full")
|
|
331
339
|
return { devDomain: childZoneName };
|
|
332
340
|
// exportCertificateArn:false — consumed in-stack by the :443 listener (slots
|
|
333
341
|
// reference the exported LISTENER arn, not the cert), and the wildcard `*.`
|
|
@@ -375,6 +383,8 @@ export class DevSubstrate extends Construct {
|
|
|
375
383
|
emit(this, "AlbCloudWatchName", values.albCloudWatchName);
|
|
376
384
|
emit(databaseScope, "DbClusterArn", values.dbClusterArn);
|
|
377
385
|
emit(databaseScope, "DbSecretArn", values.dbSecretArn);
|
|
386
|
+
emit(databaseScope, "DbWriterEndpoint", values.dbWriterEndpoint);
|
|
387
|
+
emit(databaseScope, "DbPort", values.dbPort);
|
|
378
388
|
emit(this, "SlotExecutionRoleArn", values.slotExecutionRoleArn);
|
|
379
389
|
emit(this, "SlotRepositoryUri", values.slotRepositoryUri);
|
|
380
390
|
emit(this, "WakerTargetGroupArn", values.wakerTargetGroupArn);
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { Construct } from "constructs";
|
|
2
2
|
import type { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
3
|
-
import type { DnsRecord
|
|
3
|
+
import type { DnsRecord } from "./interfaces/domain.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* the `DnsRecordInput` shape from the BIND-primary era where ALIAS sentinels
|
|
7
|
-
* live in `value`. Typed targets are not supported here; use
|
|
8
|
-
* `composeTypedDnsRecords` instead.
|
|
9
|
-
*/
|
|
10
|
-
export declare function composeDnsRecords(scope: Construct, zone: IHostedZone, zoneName: string, records: DnsRecordInput[]): void;
|
|
11
|
-
/**
|
|
12
|
-
* Typed composer consumed by the new `Domain` construct. Accepts the
|
|
5
|
+
* Typed composer consumed by the `Domain` construct. Accepts the
|
|
13
6
|
* discriminated `DnsRecord` union (`StandardRecord | AliasRecord`).
|
|
14
7
|
*
|
|
15
8
|
* `AliasRecord` entries MUST carry a `target` produced by `fjallApp()`,
|