@fjall/components-infrastructure 2.34.0 → 3.1.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 +36 -2
- package/dist/lib/app.js +78 -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/buildkite/alarms.d.ts +20 -0
- package/dist/lib/patterns/aws/buildkite/alarms.js +78 -0
- package/dist/lib/patterns/aws/buildkite/buildkite.d.ts +46 -0
- package/dist/lib/patterns/aws/buildkite/buildkite.js +233 -0
- package/dist/lib/patterns/aws/buildkite/iam.d.ts +35 -0
- package/dist/lib/patterns/aws/buildkite/iam.js +176 -0
- package/dist/lib/patterns/aws/buildkite/pins.d.ts +69 -0
- package/dist/lib/patterns/aws/buildkite/pins.js +80 -0
- package/dist/lib/patterns/aws/buildkite/schema.d.ts +80 -0
- package/dist/lib/patterns/aws/buildkite/schema.js +148 -0
- package/dist/lib/patterns/aws/buildkite/userData.d.ts +58 -0
- package/dist/lib/patterns/aws/buildkite/userData.js +226 -0
- package/dist/lib/patterns/aws/buildkite.d.ts +12 -59
- package/dist/lib/patterns/aws/buildkite.js +12 -310
- 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/ec2.d.ts +15 -0
- package/dist/lib/resources/aws/compute/ec2.js +20 -1
- 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
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CnameRecord as CdkCnameRecord } from "aws-cdk-lib/aws-route53";
|
|
3
|
-
import { applyDnsRecordTags, defaultDnsComment,
|
|
3
|
+
import { applyDnsRecordTags, claimDnsRecord, defaultDnsComment, resolveRecordFqdn, resolveTtl } from "./dnsRecordBase.js";
|
|
4
4
|
export class CnameRecord extends Construct {
|
|
5
5
|
record;
|
|
6
6
|
description;
|
|
7
7
|
fqdn;
|
|
8
8
|
constructor(scope, id, props) {
|
|
9
9
|
super(scope, id);
|
|
10
|
-
this.fqdn =
|
|
10
|
+
this.fqdn = resolveRecordFqdn(props.recordName, props.zoneName);
|
|
11
11
|
this.description =
|
|
12
12
|
props.description ?? defaultDnsComment("CNAME", this.fqdn);
|
|
13
|
+
claimDnsRecord(this, props, "CNAME", this.fqdn);
|
|
13
14
|
this.record = new CdkCnameRecord(this, "Record", {
|
|
14
15
|
zone: props.zone,
|
|
15
16
|
recordName: this.fqdn,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
2
|
import { type Construct } from "constructs";
|
|
3
3
|
import type { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
4
|
+
export { resolveRecordFqdn } from "../../../../utils/domainTypes.js";
|
|
4
5
|
export interface DnsRecordCommonProps {
|
|
5
6
|
readonly zone: IHostedZone;
|
|
6
7
|
readonly zoneName: string;
|
|
@@ -11,7 +12,15 @@ export interface DnsRecordCommonProps {
|
|
|
11
12
|
readonly costAllocationDomain?: string;
|
|
12
13
|
}
|
|
13
14
|
export declare const DEFAULT_DNS_TTL_SECONDS = 300;
|
|
14
|
-
export declare function resolveFqdn(zoneName: string, recordName: string): string;
|
|
15
15
|
export declare function resolveTtl(ttlSeconds: number | undefined): Duration;
|
|
16
16
|
export declare function defaultDnsComment(recordType: string, fqdn: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Claim the (zone, name, type) triple in the app-scoped collision registry
|
|
19
|
+
* (design D5, synth-side). Every wrapper in the dnsRecord family calls this
|
|
20
|
+
* from its constructor so a second claimant — same stack or another stack in
|
|
21
|
+
* the app — fails at synth instead of at CloudFormation deploy. Alias records
|
|
22
|
+
* claim their underlying Route53 type ("A"). Observation only: no constructs
|
|
23
|
+
* are created, so construct trees stay byte-identical.
|
|
24
|
+
*/
|
|
25
|
+
export declare function claimDnsRecord(construct: Construct, props: Pick<DnsRecordCommonProps, "zone" | "zoneName">, recordType: string, fqdn: string): void;
|
|
17
26
|
export declare function applyDnsRecordTags(construct: Construct, props: Pick<DnsRecordCommonProps, "zoneName" | "costAllocationEnvironment" | "costAllocationDomain">): void;
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
|
-
import { DNS_APEX } from "@fjall/util";
|
|
3
2
|
import { applyCostAllocationTags } from "../../../../utils/costAllocationTags.js";
|
|
3
|
+
import { registerDnsRecordClaim } from "../../../../utils/dnsRecordRegistry.js";
|
|
4
|
+
// The record wrappers resolve their FQDN via the canonical BIND-semantics
|
|
5
|
+
// resolver — the same function the patterns-layer domain validation calls —
|
|
6
|
+
// re-exported here so the family keeps a single helper surface.
|
|
7
|
+
export { resolveRecordFqdn } from "../../../../utils/domainTypes.js";
|
|
4
8
|
export const DEFAULT_DNS_TTL_SECONDS = 300;
|
|
5
|
-
export function resolveFqdn(zoneName, recordName) {
|
|
6
|
-
return recordName === DNS_APEX ? zoneName : `${recordName}.${zoneName}`;
|
|
7
|
-
}
|
|
8
9
|
export function resolveTtl(ttlSeconds) {
|
|
9
10
|
return Duration.seconds(ttlSeconds ?? DEFAULT_DNS_TTL_SECONDS);
|
|
10
11
|
}
|
|
11
12
|
export function defaultDnsComment(recordType, fqdn) {
|
|
12
13
|
return `Fjall-managed ${recordType} record for ${fqdn}`;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Claim the (zone, name, type) triple in the app-scoped collision registry
|
|
17
|
+
* (design D5, synth-side). Every wrapper in the dnsRecord family calls this
|
|
18
|
+
* from its constructor so a second claimant — same stack or another stack in
|
|
19
|
+
* the app — fails at synth instead of at CloudFormation deploy. Alias records
|
|
20
|
+
* claim their underlying Route53 type ("A"). Observation only: no constructs
|
|
21
|
+
* are created, so construct trees stay byte-identical.
|
|
22
|
+
*/
|
|
23
|
+
export function claimDnsRecord(construct, props, recordType, fqdn) {
|
|
24
|
+
registerDnsRecordClaim(construct, {
|
|
25
|
+
zone: props.zone,
|
|
26
|
+
zoneName: props.zoneName,
|
|
27
|
+
fqdn,
|
|
28
|
+
recordType
|
|
29
|
+
});
|
|
30
|
+
}
|
|
14
31
|
export function applyDnsRecordTags(construct, props) {
|
|
15
32
|
applyCostAllocationTags(construct, {
|
|
16
33
|
service: "dnsRecord",
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { MxRecord as CdkMxRecord } from "aws-cdk-lib/aws-route53";
|
|
3
|
-
import { applyDnsRecordTags, defaultDnsComment,
|
|
3
|
+
import { applyDnsRecordTags, claimDnsRecord, defaultDnsComment, resolveRecordFqdn, resolveTtl } from "./dnsRecordBase.js";
|
|
4
4
|
export class MxRecord extends Construct {
|
|
5
5
|
record;
|
|
6
6
|
description;
|
|
7
7
|
fqdn;
|
|
8
8
|
constructor(scope, id, props) {
|
|
9
9
|
super(scope, id);
|
|
10
|
-
this.fqdn =
|
|
10
|
+
this.fqdn = resolveRecordFqdn(props.recordName, props.zoneName);
|
|
11
11
|
this.description = props.description ?? defaultDnsComment("MX", this.fqdn);
|
|
12
|
+
claimDnsRecord(this, props, "MX", this.fqdn);
|
|
12
13
|
this.record = new CdkMxRecord(this, "Record", {
|
|
13
14
|
zone: props.zone,
|
|
14
15
|
recordName: this.fqdn,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { NsRecord as CdkNsRecord } from "aws-cdk-lib/aws-route53";
|
|
3
|
-
import { applyDnsRecordTags, defaultDnsComment,
|
|
3
|
+
import { applyDnsRecordTags, claimDnsRecord, defaultDnsComment, resolveRecordFqdn, resolveTtl } from "./dnsRecordBase.js";
|
|
4
4
|
export class NsRecord extends Construct {
|
|
5
5
|
record;
|
|
6
6
|
description;
|
|
7
7
|
fqdn;
|
|
8
8
|
constructor(scope, id, props) {
|
|
9
9
|
super(scope, id);
|
|
10
|
-
this.fqdn =
|
|
10
|
+
this.fqdn = resolveRecordFqdn(props.recordName, props.zoneName);
|
|
11
11
|
this.description = props.description ?? defaultDnsComment("NS", this.fqdn);
|
|
12
|
+
claimDnsRecord(this, props, "NS", this.fqdn);
|
|
12
13
|
this.record = new CdkNsRecord(this, "Record", {
|
|
13
14
|
zone: props.zone,
|
|
14
15
|
recordName: this.fqdn,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { SrvRecord as CdkSrvRecord } from "aws-cdk-lib/aws-route53";
|
|
3
|
-
import { applyDnsRecordTags, defaultDnsComment,
|
|
3
|
+
import { applyDnsRecordTags, claimDnsRecord, defaultDnsComment, resolveRecordFqdn, resolveTtl } from "./dnsRecordBase.js";
|
|
4
4
|
export class SrvRecord extends Construct {
|
|
5
5
|
record;
|
|
6
6
|
description;
|
|
7
7
|
fqdn;
|
|
8
8
|
constructor(scope, id, props) {
|
|
9
9
|
super(scope, id);
|
|
10
|
-
this.fqdn =
|
|
10
|
+
this.fqdn = resolveRecordFqdn(props.recordName, props.zoneName);
|
|
11
11
|
this.description = props.description ?? defaultDnsComment("SRV", this.fqdn);
|
|
12
|
+
claimDnsRecord(this, props, "SRV", this.fqdn);
|
|
12
13
|
this.record = new CdkSrvRecord(this, "Record", {
|
|
13
14
|
zone: props.zone,
|
|
14
15
|
recordName: this.fqdn,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { TxtRecord as CdkTxtRecord } from "aws-cdk-lib/aws-route53";
|
|
3
|
-
import { applyDnsRecordTags, defaultDnsComment,
|
|
3
|
+
import { applyDnsRecordTags, claimDnsRecord, defaultDnsComment, resolveRecordFqdn, resolveTtl } from "./dnsRecordBase.js";
|
|
4
4
|
export class TxtRecord extends Construct {
|
|
5
5
|
record;
|
|
6
6
|
description;
|
|
7
7
|
fqdn;
|
|
8
8
|
constructor(scope, id, props) {
|
|
9
9
|
super(scope, id);
|
|
10
|
-
this.fqdn =
|
|
10
|
+
this.fqdn = resolveRecordFqdn(props.recordName, props.zoneName);
|
|
11
11
|
this.description = props.description ?? defaultDnsComment("TXT", this.fqdn);
|
|
12
|
+
claimDnsRecord(this, props, "TXT", this.fqdn);
|
|
12
13
|
this.record = new CdkTxtRecord(this, "Record", {
|
|
13
14
|
zone: props.zone,
|
|
14
15
|
recordName: this.fqdn,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Construct } from "constructs";
|
|
2
|
-
import { type ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
1
|
+
import { type Construct } from "constructs";
|
|
2
|
+
import { Certificate, type ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
3
|
import { type IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
4
4
|
export interface DomainCertificateProps {
|
|
5
5
|
readonly domainName: string;
|
|
@@ -8,6 +8,15 @@ export interface DomainCertificateProps {
|
|
|
8
8
|
readonly description?: string;
|
|
9
9
|
readonly transparencyLogging?: boolean;
|
|
10
10
|
readonly costAllocationEnvironment?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Inherit the environment tag from an outer cascade (the fjall App's
|
|
13
|
+
* global tags) instead of stamping the "management" default when
|
|
14
|
+
* `costAllocationEnvironment` is not given. In-stack certificates minted
|
|
15
|
+
* inside an application (the ECS fallthrough mint) must set this: the
|
|
16
|
+
* cluster's app already tags the resolved environment tree-wide, and the
|
|
17
|
+
* resource-level default would out-tag it (deeper scope wins).
|
|
18
|
+
*/
|
|
19
|
+
readonly inheritCostAllocationEnvironment?: boolean;
|
|
11
20
|
readonly costAllocationDomain?: string;
|
|
12
21
|
/**
|
|
13
22
|
* Publish the `${domain}-certificate-arn` CloudFormation export. Default true.
|
|
@@ -17,9 +26,24 @@ export interface DomainCertificateProps {
|
|
|
17
26
|
*/
|
|
18
27
|
readonly exportCertificateArn?: boolean;
|
|
19
28
|
}
|
|
20
|
-
|
|
29
|
+
/**
|
|
30
|
+
* EXTENDS `Certificate` (subclass, not wrapper) so the underlying
|
|
31
|
+
* `AWS::CertificateManager::Certificate` synthesises at `<id>/Resource` — the
|
|
32
|
+
* byte-identical construct path, and therefore CFN logical ID, of a raw
|
|
33
|
+
* `new Certificate(scope, id)`. Wrapping a `Construct` around an inner
|
|
34
|
+
* `new Certificate(this, "Certificate")` would nest the resource one level
|
|
35
|
+
* deeper (`<id>/Certificate/Resource`), re-hash the logical ID, and make
|
|
36
|
+
* CloudFormation REPLACE a live in-use certificate on adoption (create new +
|
|
37
|
+
* flip listener + delete old) — exactly the churn the adoption design rules
|
|
38
|
+
* out ("same construct ID → zero cert churn"). Pinned by
|
|
39
|
+
* ecsDomainBaseline.test.ts and ecsNetworkingDomain.test.ts.
|
|
40
|
+
*/
|
|
41
|
+
export declare class DomainCertificate extends Certificate {
|
|
42
|
+
/**
|
|
43
|
+
* Self-alias kept for API compatibility with the wrapper era — consumers
|
|
44
|
+
* that read `.certificate` compile and behave unchanged.
|
|
45
|
+
*/
|
|
21
46
|
readonly certificate: ICertificate;
|
|
22
|
-
readonly certificateArn: string;
|
|
23
47
|
readonly description: string;
|
|
24
48
|
constructor(scope: Construct, id: string, props: DomainCertificateProps);
|
|
25
49
|
}
|
|
@@ -1,30 +1,44 @@
|
|
|
1
|
-
import { Construct } from "constructs";
|
|
2
1
|
import { CfnOutput, Tags } from "aws-cdk-lib";
|
|
3
2
|
import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
|
|
4
3
|
import { getDomainExportNames } from "@fjall/util";
|
|
5
4
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
6
5
|
import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* EXTENDS `Certificate` (subclass, not wrapper) so the underlying
|
|
8
|
+
* `AWS::CertificateManager::Certificate` synthesises at `<id>/Resource` — the
|
|
9
|
+
* byte-identical construct path, and therefore CFN logical ID, of a raw
|
|
10
|
+
* `new Certificate(scope, id)`. Wrapping a `Construct` around an inner
|
|
11
|
+
* `new Certificate(this, "Certificate")` would nest the resource one level
|
|
12
|
+
* deeper (`<id>/Certificate/Resource`), re-hash the logical ID, and make
|
|
13
|
+
* CloudFormation REPLACE a live in-use certificate on adoption (create new +
|
|
14
|
+
* flip listener + delete old) — exactly the churn the adoption design rules
|
|
15
|
+
* out ("same construct ID → zero cert churn"). Pinned by
|
|
16
|
+
* ecsDomainBaseline.test.ts and ecsNetworkingDomain.test.ts.
|
|
17
|
+
*/
|
|
18
|
+
export class DomainCertificate extends Certificate {
|
|
19
|
+
/**
|
|
20
|
+
* Self-alias kept for API compatibility with the wrapper era — consumers
|
|
21
|
+
* that read `.certificate` compile and behave unchanged.
|
|
22
|
+
*/
|
|
8
23
|
certificate;
|
|
9
|
-
certificateArn;
|
|
10
24
|
description;
|
|
11
25
|
constructor(scope, id, props) {
|
|
12
|
-
super(scope, id
|
|
13
|
-
this.description =
|
|
14
|
-
props.description ??
|
|
15
|
-
`Fjall-managed ACM certificate for ${props.domainName}`;
|
|
16
|
-
this.certificate = new Certificate(this, "Certificate", {
|
|
26
|
+
super(scope, id, {
|
|
17
27
|
domainName: props.domainName,
|
|
18
28
|
subjectAlternativeNames: props.subjectAlternativeNames,
|
|
19
29
|
validation: CertificateValidation.fromDns(props.hostedZone),
|
|
20
30
|
transparencyLoggingEnabled: props.transparencyLogging ?? true
|
|
21
31
|
});
|
|
22
|
-
this.
|
|
32
|
+
this.certificate = this;
|
|
33
|
+
this.description =
|
|
34
|
+
props.description ??
|
|
35
|
+
`Fjall-managed ACM certificate for ${props.domainName}`;
|
|
23
36
|
Tags.of(this).add("fjall:description", this.description);
|
|
24
37
|
applyCostAllocationTags(this, {
|
|
25
38
|
service: "certificate",
|
|
26
39
|
domain: props.costAllocationDomain ?? props.domainName,
|
|
27
|
-
environment: props.costAllocationEnvironment
|
|
40
|
+
environment: props.costAllocationEnvironment,
|
|
41
|
+
inheritEnvironment: props.inheritCostAllocationEnvironment
|
|
28
42
|
});
|
|
29
43
|
if (props.exportCertificateArn !== false) {
|
|
30
44
|
const safeKey = toPascalCase(props.domainName.split(".").join(""));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { type IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
3
|
+
import { type RemovalPolicyString } from "../messaging/utils.js";
|
|
3
4
|
import { DelegationRole } from "../iam/delegationRole.js";
|
|
4
5
|
import { type AwsStack } from "../base/awsStack.js";
|
|
5
6
|
export interface HostedZoneProps {
|
|
@@ -10,6 +11,16 @@ export interface HostedZoneProps {
|
|
|
10
11
|
readonly organisationIdExportName?: string;
|
|
11
12
|
readonly costAllocationEnvironment?: string;
|
|
12
13
|
readonly costAllocationDomain?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Removal policy for the created zone (D4: zone safety). Default resolves
|
|
16
|
+
* via `envAwareRemovalPolicyDefault()` (production → RETAIN; other
|
|
17
|
+
* recognised stages → DESTROY; unrecognised values fail synth) — deleting
|
|
18
|
+
* a live zone dangles every parent/registrar delegation, so production
|
|
19
|
+
* zones survive stack destruction by default. The flip is metadata-only:
|
|
20
|
+
* the zone's logical ID is unaffected (flipless-retain-flip ADR). Ignored
|
|
21
|
+
* on the import path (`hostedZoneId` set), which manages no zone resource.
|
|
22
|
+
*/
|
|
23
|
+
readonly removalPolicy?: RemovalPolicyString;
|
|
13
24
|
}
|
|
14
25
|
export interface HostedZoneFactoryImportOptions {
|
|
15
26
|
readonly costAllocationEnvironment?: string;
|
|
@@ -3,6 +3,7 @@ import { CfnOutput, Fn, Tags } from "aws-cdk-lib";
|
|
|
3
3
|
import { HostedZone as AWSHostedZone } from "aws-cdk-lib/aws-route53";
|
|
4
4
|
import { getDomainExportNames } from "@fjall/util";
|
|
5
5
|
import { toPascalCase, getSafeZoneName } from "../../../utils/capitaliseString.js";
|
|
6
|
+
import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
6
7
|
import { DelegationRole } from "../iam/delegationRole.js";
|
|
7
8
|
import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
|
|
8
9
|
import { resolveOrgId } from "../../../utils/cdkContext.js";
|
|
@@ -39,6 +40,11 @@ export class HostedZone extends Construct {
|
|
|
39
40
|
zoneName: props.zoneName,
|
|
40
41
|
comment: this.description
|
|
41
42
|
});
|
|
43
|
+
// D4 — resolved inside the create branch only: the env-aware default
|
|
44
|
+
// throws on unrecognised environments, and import-only synths manage
|
|
45
|
+
// no zone resource to police.
|
|
46
|
+
const removalPolicyValue = props.removalPolicy ?? envAwareRemovalPolicyDefault();
|
|
47
|
+
created.applyRemovalPolicy(toRemovalPolicy(removalPolicyValue));
|
|
42
48
|
this.hostedZone = created;
|
|
43
49
|
this.hostedZoneId = created.hostedZoneId;
|
|
44
50
|
this.nameServers = created.hostedZoneNameServers
|
|
@@ -10,6 +10,15 @@ export interface CostAllocationTagsArgs {
|
|
|
10
10
|
readonly service: string;
|
|
11
11
|
readonly domain: string;
|
|
12
12
|
readonly environment?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When `environment` is not given, skip the environment tag at this scope
|
|
15
|
+
* entirely instead of stamping the "management" default, so an outer
|
|
16
|
+
* cascade (the fjall App's global tags carry the account-stage-resolved
|
|
17
|
+
* environment) provides it. Without this, the deeper-scope default wins
|
|
18
|
+
* CDK tag resolution and a production stack's resource is mis-tagged
|
|
19
|
+
* "management". An explicit `environment` always wins over inheritance.
|
|
20
|
+
*/
|
|
21
|
+
readonly inheritEnvironment?: boolean;
|
|
13
22
|
readonly owner?: string;
|
|
14
23
|
}
|
|
15
24
|
export declare function applyCostAllocationTags(scope: IConstruct, args: CostAllocationTagsArgs): void;
|
|
@@ -7,7 +7,12 @@ export const COST_ALLOCATION_TAGS = {
|
|
|
7
7
|
};
|
|
8
8
|
export const DEFAULT_COST_ALLOCATION_ENVIRONMENT = "management";
|
|
9
9
|
export function applyCostAllocationTags(scope, args) {
|
|
10
|
-
|
|
10
|
+
if (args.environment !== undefined) {
|
|
11
|
+
Tags.of(scope).add(COST_ALLOCATION_TAGS.ENVIRONMENT, args.environment);
|
|
12
|
+
}
|
|
13
|
+
else if (args.inheritEnvironment !== true) {
|
|
14
|
+
Tags.of(scope).add(COST_ALLOCATION_TAGS.ENVIRONMENT, DEFAULT_COST_ALLOCATION_ENVIRONMENT);
|
|
15
|
+
}
|
|
11
16
|
Tags.of(scope).add(COST_ALLOCATION_TAGS.SERVICE, args.service);
|
|
12
17
|
Tags.of(scope).add(COST_ALLOCATION_TAGS.DOMAIN, args.domain);
|
|
13
18
|
if (args.owner !== undefined) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
2
|
+
import type { IConstruct } from "constructs";
|
|
3
|
+
/**
|
|
4
|
+
* Synth-time DNS record-collision registry (design D5, synth-side half — the
|
|
5
|
+
* deploy-time preflight against live zones is Phase 2).
|
|
6
|
+
*
|
|
7
|
+
* Route53 allows exactly one record set per (zone, name, type); a second
|
|
8
|
+
* claimant today fails only at CloudFormation deploy with a raw "record set
|
|
9
|
+
* already exists". This registry makes the collision loud at synth: every
|
|
10
|
+
* construct in the dnsRecord wrapper family registers its claim here, and a
|
|
11
|
+
* duplicate claim throws naming both claimant construct paths and the cure.
|
|
12
|
+
*
|
|
13
|
+
* Scoping: claims are held per App (the construct-tree root), so multiple CDK
|
|
14
|
+
* Apps in one process — the vitest convention of a fresh `new CdkApp()` per
|
|
15
|
+
* test — never cross-talk, while stacks within one App share a claim table.
|
|
16
|
+
* Cross-stack duplicates inside one App are exactly the surface nothing
|
|
17
|
+
* catches before the change set executes.
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle follows the manifest-collector precedent (manifestWriter.ts):
|
|
20
|
+
* module-level synth state with a reset hook, wired into
|
|
21
|
+
* `App.resetForTesting()` so the existing test-hygiene convention
|
|
22
|
+
* (resetForTesting + resetManifestCollector in beforeEach/afterEach) keeps
|
|
23
|
+
* working without a third per-test call.
|
|
24
|
+
*/
|
|
25
|
+
export interface DnsRecordClaim {
|
|
26
|
+
/** Zone the record lands in — consulted for identity resolution. */
|
|
27
|
+
readonly zone: IHostedZone;
|
|
28
|
+
/** Zone name as passed to the wrapper (identity fallback and error copy). */
|
|
29
|
+
readonly zoneName: string;
|
|
30
|
+
/** Fully-qualified record name the wrapper resolved. */
|
|
31
|
+
readonly fqdn: string;
|
|
32
|
+
/**
|
|
33
|
+
* Route53 record-set type. Alias records claim their underlying type ("A")
|
|
34
|
+
* — an alias and a plain A record on the same name are a real collision.
|
|
35
|
+
*/
|
|
36
|
+
readonly recordType: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Register a record-set claim for `scope`, throwing when a DIFFERENT
|
|
40
|
+
* construct already claimed the same (zoneIdentity, recordName, recordType)
|
|
41
|
+
* triple within the same App. Re-registration by the same construct path
|
|
42
|
+
* (CDK aspects re-visiting, repeated synth) is idempotent. Pure observation:
|
|
43
|
+
* no constructs are created or mutated, so construct trees — including the
|
|
44
|
+
* eject-contract byte-identical composer IDs — are untouched.
|
|
45
|
+
*/
|
|
46
|
+
export declare function registerDnsRecordClaim(scope: IConstruct, claim: DnsRecordClaim): void;
|
|
47
|
+
/**
|
|
48
|
+
* Reset all claims (for testing). Called by `App.resetForTesting()` alongside
|
|
49
|
+
* the App singleton reset so per-test App recycling starts clean — the
|
|
50
|
+
* manifest-collector lifecycle precedent.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resetDnsRecordRegistry(): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Token } from "aws-cdk-lib";
|
|
2
|
+
/** Claims per App root: claim key → owning construct path. */
|
|
3
|
+
let claimsByRoot = new WeakMap();
|
|
4
|
+
/** Lowercase and strip the trailing dot — DNS names are case-insensitive. */
|
|
5
|
+
function normaliseDnsName(name) {
|
|
6
|
+
return name.toLowerCase().replace(/\.$/, "");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Zone identity tolerates both literal zone IDs and zone names: prefer
|
|
10
|
+
* `hostedZoneId` when it is a known literal (imported or looked-up zones),
|
|
11
|
+
* else fall back to the lowercased zone name — zones created in-app carry an
|
|
12
|
+
* unresolved token ID that is meaningless as an identity.
|
|
13
|
+
*/
|
|
14
|
+
function resolveZoneIdentity(zone, zoneName) {
|
|
15
|
+
const hostedZoneId = zone.hostedZoneId;
|
|
16
|
+
if (hostedZoneId && !Token.isUnresolved(hostedZoneId)) {
|
|
17
|
+
return hostedZoneId;
|
|
18
|
+
}
|
|
19
|
+
return normaliseDnsName(zoneName);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Register a record-set claim for `scope`, throwing when a DIFFERENT
|
|
23
|
+
* construct already claimed the same (zoneIdentity, recordName, recordType)
|
|
24
|
+
* triple within the same App. Re-registration by the same construct path
|
|
25
|
+
* (CDK aspects re-visiting, repeated synth) is idempotent. Pure observation:
|
|
26
|
+
* no constructs are created or mutated, so construct trees — including the
|
|
27
|
+
* eject-contract byte-identical composer IDs — are untouched.
|
|
28
|
+
*/
|
|
29
|
+
export function registerDnsRecordClaim(scope, claim) {
|
|
30
|
+
const root = scope.node.root;
|
|
31
|
+
let claims = claimsByRoot.get(root);
|
|
32
|
+
if (!claims) {
|
|
33
|
+
claims = new Map();
|
|
34
|
+
claimsByRoot.set(root, claims);
|
|
35
|
+
}
|
|
36
|
+
const recordType = claim.recordType.toUpperCase();
|
|
37
|
+
const key = [
|
|
38
|
+
resolveZoneIdentity(claim.zone, claim.zoneName),
|
|
39
|
+
normaliseDnsName(claim.fqdn),
|
|
40
|
+
recordType
|
|
41
|
+
].join("|");
|
|
42
|
+
const claimantPath = scope.node.path;
|
|
43
|
+
const existing = claims.get(key);
|
|
44
|
+
if (existing === undefined) {
|
|
45
|
+
claims.set(key, claimantPath);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (existing === claimantPath) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`DNS record '${claim.fqdn}' (${recordType}) in zone '${claim.zoneName}': ` +
|
|
52
|
+
`already claimed by construct '${existing}' — duplicate claim by '${claimantPath}'. ` +
|
|
53
|
+
`Route53 allows one record set per (zone, name, type); CloudFormation would reject this at deploy. ` +
|
|
54
|
+
`Keep one owner per record — satellites own their app records; the domain stack owns zone-level records. ` +
|
|
55
|
+
`Remove or rename one claimant, or run 'fjall domain records list ${claim.zoneName}' to inspect the zone.`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Reset all claims (for testing). Called by `App.resetForTesting()` alongside
|
|
59
|
+
* the App singleton reset so per-test App recycling starts clean — the
|
|
60
|
+
* manifest-collector lifecycle precedent.
|
|
61
|
+
*/
|
|
62
|
+
export function resetDnsRecordRegistry() {
|
|
63
|
+
claimsByRoot = new WeakMap();
|
|
64
|
+
}
|
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
export { DNS_APEX, getDomainExportNames, type ManagedDomainExports } from "@fjall/util";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
export { DNS_APEX, getDomainExportNames, isManagedDomainBinding, type ManagedDomainBinding, type ManagedDomainExports } from "@fjall/util";
|
|
2
|
+
/**
|
|
3
|
+
* Dot-boundary suffix matching. Rejects pathological cases where a user-supplied
|
|
4
|
+
* zone appears as a suffix of another zone (e.g. 'evilexample.com' against
|
|
5
|
+
* zone 'example.com' — `endsWith('example.com')` returns true without the
|
|
6
|
+
* leading dot). Shared by the patterns-layer domain validation and the
|
|
7
|
+
* resources-layer ECS networking (lowest common layer per generator-standards
|
|
8
|
+
* § Infrastructure Layer Boundaries).
|
|
9
|
+
*/
|
|
10
|
+
export declare function isWithinZone(candidate: string, zoneName: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Canonical BIND-semantics resolver for DNS record names — the single
|
|
13
|
+
* authority shared by the patterns-layer domain validation and the
|
|
14
|
+
* resources-layer dnsRecord family (lowest common layer per
|
|
15
|
+
* generator-standards § Infrastructure Layer Boundaries). Contract:
|
|
16
|
+
*
|
|
17
|
+
* - `'@'` — the zone apex; resolves to the zone name.
|
|
18
|
+
* - Trailing dot — an absolute FQDN; must equal the zone or end with
|
|
19
|
+
* `.<zone>.`, otherwise the name belongs to another zone and is rejected.
|
|
20
|
+
* The conventional trailing dot is shed on return because CloudFormation
|
|
21
|
+
* RecordSet names carry none at this layer (CDK re-appends it).
|
|
22
|
+
* - No trailing dot — relative to the zone regardless of label count
|
|
23
|
+
* (`www` and `s1._domainkey` alike); the zone is appended exactly once.
|
|
24
|
+
* - A dotless name that already ends with the bare zone is ambiguous between
|
|
25
|
+
* the relative and absolute readings and is rejected, naming both honest
|
|
26
|
+
* spellings.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveRecordFqdn(recordName: string, zoneName: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Two-step deploy phase for constructs whose certificates must not be issued
|
|
31
|
+
* before their zone's NS delegation has propagated (design R2 cert-hang
|
|
32
|
+
* guard). `"zone"` synthesises the hosted zone (+ delegation record) only;
|
|
33
|
+
* `"full"` additionally issues certificates. Shared by the delegated `Domain`
|
|
34
|
+
* topology and `DevSubstrate` — lowest common layer per generator-standards
|
|
35
|
+
* § Infrastructure Layer Boundaries.
|
|
36
|
+
*/
|
|
37
|
+
export type DomainDeployPhase = "zone" | "full";
|
|
38
|
+
/**
|
|
39
|
+
* Default deploy phase when `phase` is omitted — the complete build. The
|
|
40
|
+
* two-step guard (R2) sets `"zone"` explicitly for step 1, so the safe
|
|
41
|
+
* default for a single-shot deploy is everything.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DOMAIN_DEPLOY_DEFAULT_PHASE: DomainDeployPhase;
|
|
@@ -1,2 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { DNS_APEX } from "@fjall/util";
|
|
2
|
+
export { DNS_APEX, getDomainExportNames, isManagedDomainBinding } from "@fjall/util";
|
|
3
|
+
/**
|
|
4
|
+
* Dot-boundary suffix matching. Rejects pathological cases where a user-supplied
|
|
5
|
+
* zone appears as a suffix of another zone (e.g. 'evilexample.com' against
|
|
6
|
+
* zone 'example.com' — `endsWith('example.com')` returns true without the
|
|
7
|
+
* leading dot). Shared by the patterns-layer domain validation and the
|
|
8
|
+
* resources-layer ECS networking (lowest common layer per generator-standards
|
|
9
|
+
* § Infrastructure Layer Boundaries).
|
|
10
|
+
*/
|
|
11
|
+
export function isWithinZone(candidate, zoneName) {
|
|
12
|
+
if (candidate === zoneName) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return candidate.endsWith(`.${zoneName}`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Canonical BIND-semantics resolver for DNS record names — the single
|
|
19
|
+
* authority shared by the patterns-layer domain validation and the
|
|
20
|
+
* resources-layer dnsRecord family (lowest common layer per
|
|
21
|
+
* generator-standards § Infrastructure Layer Boundaries). Contract:
|
|
22
|
+
*
|
|
23
|
+
* - `'@'` — the zone apex; resolves to the zone name.
|
|
24
|
+
* - Trailing dot — an absolute FQDN; must equal the zone or end with
|
|
25
|
+
* `.<zone>.`, otherwise the name belongs to another zone and is rejected.
|
|
26
|
+
* The conventional trailing dot is shed on return because CloudFormation
|
|
27
|
+
* RecordSet names carry none at this layer (CDK re-appends it).
|
|
28
|
+
* - No trailing dot — relative to the zone regardless of label count
|
|
29
|
+
* (`www` and `s1._domainkey` alike); the zone is appended exactly once.
|
|
30
|
+
* - A dotless name that already ends with the bare zone is ambiguous between
|
|
31
|
+
* the relative and absolute readings and is rejected, naming both honest
|
|
32
|
+
* spellings.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveRecordFqdn(recordName, zoneName) {
|
|
35
|
+
if (recordName === DNS_APEX) {
|
|
36
|
+
return zoneName;
|
|
37
|
+
}
|
|
38
|
+
if (recordName.endsWith(".")) {
|
|
39
|
+
const absolute = recordName.slice(0, -1);
|
|
40
|
+
if (absolute !== zoneName && !absolute.endsWith(`.${zoneName}`)) {
|
|
41
|
+
throw new Error(`DNS record name '${recordName}' is absolute (trailing dot) but ` +
|
|
42
|
+
`lies outside zone '${zoneName}'. Cure: an absolute name must be ` +
|
|
43
|
+
`'${zoneName}.' or end with '.${zoneName}.'; records for another ` +
|
|
44
|
+
`zone belong on that zone's own Domain construct.`);
|
|
45
|
+
}
|
|
46
|
+
return absolute;
|
|
47
|
+
}
|
|
48
|
+
if (recordName === zoneName || recordName.endsWith(`.${zoneName}`)) {
|
|
49
|
+
const relativeSpelling = recordName === zoneName
|
|
50
|
+
? DNS_APEX
|
|
51
|
+
: recordName.slice(0, -(zoneName.length + 1));
|
|
52
|
+
throw new Error(`DNS record name '${recordName}' ends with zone '${zoneName}' but has ` +
|
|
53
|
+
`no trailing dot, so it is ambiguous between relative ` +
|
|
54
|
+
`('${recordName}.${zoneName}') and absolute ('${recordName}'). ` +
|
|
55
|
+
`Cure: spell it '${relativeSpelling}' (relative to the zone) or ` +
|
|
56
|
+
`'${recordName}.' (absolute).`);
|
|
57
|
+
}
|
|
58
|
+
return `${recordName}.${zoneName}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Default deploy phase when `phase` is omitted — the complete build. The
|
|
62
|
+
* two-step guard (R2) sets `"zone"` explicitly for step 1, so the safe
|
|
63
|
+
* default for a single-shot deploy is everything.
|
|
64
|
+
*/
|
|
65
|
+
export const DOMAIN_DEPLOY_DEFAULT_PHASE = "full";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDK-context channel for CLI-injected managed-domain bindings (design
|
|
3
|
+
* 2026-07-17 domain-management gold-plating, D2 — resolution by injection).
|
|
4
|
+
*
|
|
5
|
+
* At deploy time the Fjall CLI resolves each app-consumed domain against the
|
|
6
|
+
* declared `fjall/domains/` components (DescribeStacks on the domain stack)
|
|
7
|
+
* and injects one context entry per zone:
|
|
8
|
+
*
|
|
9
|
+
* -c fjall:managedDomain:<zoneName>=<JSON ManagedDomainBinding>
|
|
10
|
+
*
|
|
11
|
+
* (emitter: deploy-core `CdkArgumentBuilder.buildContextArgs` — keep the
|
|
12
|
+
* prefix literal in sync). Constructs read the entry ahead of their BYO
|
|
13
|
+
* `zoneName`/`hostedZone` props; an EXPLICIT `managedDomain` prop still wins
|
|
14
|
+
* over the injected value. Bare-CDK synth simply lacks the context and falls
|
|
15
|
+
* back to explicit props.
|
|
16
|
+
*
|
|
17
|
+
* Utils layer on purpose: consumed by both the resources layer
|
|
18
|
+
* (`ecsNetworking.addHostedZone`) and the patterns layer
|
|
19
|
+
* (`patternDomain.resolvePatternZone`), and utils is their lowest common
|
|
20
|
+
* layer (generator-standards § Infrastructure Layer Boundaries).
|
|
21
|
+
*/
|
|
22
|
+
import type { Node } from "constructs";
|
|
23
|
+
import type { ManagedDomainBinding } from "@fjall/util";
|
|
24
|
+
export declare const MANAGED_DOMAIN_CONTEXT_PREFIX: "fjall:managedDomain:";
|
|
25
|
+
export declare function getManagedDomainContextKey(zoneName: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Read the CLI-injected {@link ManagedDomainBinding} for `domainName`,
|
|
28
|
+
* walking exact → parent zones (mirroring the CLI's
|
|
29
|
+
* `DomainService.matchManagedDomain` semantics: `app.example.com` matches a
|
|
30
|
+
* binding injected for `example.com`). Returns undefined when no entry is
|
|
31
|
+
* present (bare-CDK synth); throws a context-prefixed, value-echoing,
|
|
32
|
+
* cure-naming error when an entry exists but is malformed — a corrupt
|
|
33
|
+
* binding must fail the synth, never fall through to a guessed zone.
|
|
34
|
+
*
|
|
35
|
+
* `context` prefixes every error, e.g. `Static site 'marketing'`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readInjectedManagedDomainBinding(node: Node, domainName: string, context: string): ManagedDomainBinding | undefined;
|