@fjall/components-infrastructure 14.1.0 → 14.3.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/patterns/aws/apexDomainPattern.js +1 -1
- package/dist/lib/patterns/aws/cdn.d.ts +83 -4
- package/dist/lib/patterns/aws/cdn.js +279 -69
- package/dist/lib/patterns/aws/cdnAppOrigin.d.ts +83 -0
- package/dist/lib/patterns/aws/cdnAppOrigin.js +252 -0
- package/dist/lib/patterns/aws/computeEcs.d.ts +9 -1
- package/dist/lib/patterns/aws/computeEcs.js +10 -0
- package/dist/lib/patterns/aws/delegatedDomainPattern.js +1 -1
- package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +13 -4
- package/dist/lib/patterns/aws/dnsRecordComposer.js +88 -6
- package/dist/lib/patterns/aws/domainCertificateComposer.js +66 -1
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +7 -0
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +32 -0
- package/dist/lib/patterns/aws/patternDomain.d.ts +9 -0
- package/dist/lib/patterns/aws/patternDomain.js +60 -27
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +37 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +96 -4
- package/dist/lib/resources/aws/compute/ecs.d.ts +11 -0
- package/dist/lib/resources/aws/compute/ecs.js +27 -0
- package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +2 -0
- package/dist/lib/resources/aws/compute/ecsNetworking.js +117 -61
- package/dist/lib/resources/aws/compute/ingressProfile.d.ts +203 -0
- package/dist/lib/resources/aws/compute/ingressProfile.js +215 -0
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +10 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +18 -9
- package/dist/lib/utils/dnsRecordRegistry.d.ts +3 -5
- package/dist/lib/utils/dnsRecordRegistry.js +11 -14
- package/dist/lib/utils/domainTypes.d.ts +11 -0
- package/dist/lib/utils/domainTypes.js +17 -0
- package/dist/lib/utils/managedDomainContext.d.ts +58 -1
- package/dist/lib/utils/managedDomainContext.js +96 -0
- package/package.json +3 -3
|
@@ -8,8 +8,9 @@ import { LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
|
8
8
|
import { DomainCertificate } from "../networking/domainCertificate.js";
|
|
9
9
|
import { AliasRecord } from "../networking/dnsRecord/aliasRecord.js";
|
|
10
10
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { isManagedDomainBinding, isWithinZone, recordLabelWithin } from "../../../utils/domainTypes.js";
|
|
12
|
+
import { readInjectedManagedDomainCoverage, resolveEffectiveManagedDomain } from "../../../utils/managedDomainContext.js";
|
|
13
|
+
import { computeListenerDefault404, planListenerRules } from "./ingressProfile.js";
|
|
13
14
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
14
15
|
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
15
16
|
import { registerAlbAliasTarget } from "../../../utils/albAliasTargetRegistry.js";
|
|
@@ -99,24 +100,15 @@ export function addLoadBalancer(ctx, anyServiceUsesEc2, asgSecurityGroup) {
|
|
|
99
100
|
}
|
|
100
101
|
export function addLoadBalancerListener(ctx, loadBalancer, certificate, additionalCertificates) {
|
|
101
102
|
const port = certificate ? 443 : 80;
|
|
102
|
-
const servicesWithPorts = ctx.props.services.filter((s) => s.containers.some((c) => c.port !== undefined));
|
|
103
|
-
const willHaveMultipleRoutes = servicesWithPorts.length > 1 ||
|
|
104
|
-
servicesWithPorts.some((s) => {
|
|
105
|
-
const rules = Array.isArray(s.routing)
|
|
106
|
-
? s.routing
|
|
107
|
-
: s.routing
|
|
108
|
-
? [s.routing]
|
|
109
|
-
: [];
|
|
110
|
-
return rules.length > 1;
|
|
111
|
-
});
|
|
112
|
-
// CDK rejects listeners with neither a default action nor target groups.
|
|
113
|
-
const noServicePorts = servicesWithPorts.length === 0;
|
|
114
103
|
return addRoutingListener(loadBalancer, `${ctx.props.clusterName}Listener`, {
|
|
115
104
|
port,
|
|
116
105
|
...(certificate && { certificate }),
|
|
117
106
|
...(additionalCertificates !== undefined &&
|
|
118
107
|
additionalCertificates.length > 0 && { additionalCertificates }),
|
|
119
|
-
|
|
108
|
+
// Single home for the default-action predicate (ingressProfile.ts): the
|
|
109
|
+
// ingress profile exports the same value, so downstream verdicts model
|
|
110
|
+
// the listener the emitter actually built (design cdn-app-origin, P5).
|
|
111
|
+
default404: computeListenerDefault404(ctx.props.services)
|
|
120
112
|
});
|
|
121
113
|
}
|
|
122
114
|
/**
|
|
@@ -190,12 +182,6 @@ function preWrapperLogicalId(scope, id) {
|
|
|
190
182
|
.slice(0, 240);
|
|
191
183
|
return human + hash;
|
|
192
184
|
}
|
|
193
|
-
/** Record label for `fqdn` relative to `zoneName` (apex → `@`). */
|
|
194
|
-
function recordLabelWithin(fqdn, zoneName) {
|
|
195
|
-
return fqdn === zoneName
|
|
196
|
-
? DNS_APEX
|
|
197
|
-
: fqdn.slice(0, fqdn.length - zoneName.length - 1);
|
|
198
|
-
}
|
|
199
185
|
export function addHostedZone(ctx, loadBalancer) {
|
|
200
186
|
const props = ctx.props;
|
|
201
187
|
const domainConfig = props.cluster?.domainConfig;
|
|
@@ -206,31 +192,25 @@ export function addHostedZone(ctx, loadBalancer) {
|
|
|
206
192
|
let hostedZone;
|
|
207
193
|
let zoneName;
|
|
208
194
|
let certificate;
|
|
209
|
-
// D2 —
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
if (managed) {
|
|
215
|
-
zoneName =
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// Export-name fallback (bare-CDK synth): same account, same region only.
|
|
229
|
-
hostedZone = AWSHostedZone.fromHostedZoneAttributes(ctx.scope, `${props.clusterName}ManagedHostedZone`, {
|
|
230
|
-
hostedZoneId: Fn.importValue(managed.hostedZoneIdExport),
|
|
231
|
-
zoneName: managed.zoneName
|
|
232
|
-
});
|
|
233
|
-
certificate = Certificate.fromCertificateArn(ctx.scope, `${props.clusterName}ManagedCertificate`, Fn.importValue(managed.certificateArnExport));
|
|
195
|
+
// D2 — the shared precedence and zone-id branch live in
|
|
196
|
+
// resolveEffectiveManagedDomain (explicit beats injected; binding literal
|
|
197
|
+
// beats exports Fn.importValue). Bare-CDK synth has no context entry.
|
|
198
|
+
const effectiveManaged = resolveEffectiveManagedDomain(ctx.scope.node, domainConfig?.managedDomain, domainName, `Cluster '${props.clusterName}'`);
|
|
199
|
+
const managed = effectiveManaged?.managed;
|
|
200
|
+
if (effectiveManaged !== undefined && managed !== undefined) {
|
|
201
|
+
zoneName = effectiveManaged.zoneName;
|
|
202
|
+
hostedZone = AWSHostedZone.fromHostedZoneAttributes(ctx.scope, `${props.clusterName}ManagedHostedZone`, {
|
|
203
|
+
hostedZoneId: effectiveManaged.hostedZoneId,
|
|
204
|
+
zoneName: effectiveManaged.zoneName
|
|
205
|
+
});
|
|
206
|
+
// The regional-certificate lane stays per-form: a binding may omit the
|
|
207
|
+
// ARN (no regional certificate declared), the exports form always
|
|
208
|
+
// imports it.
|
|
209
|
+
const certificateArn = isManagedDomainBinding(managed)
|
|
210
|
+
? managed.certificateArn
|
|
211
|
+
: Fn.importValue(managed.certificateArnExport);
|
|
212
|
+
if (certificateArn !== undefined) {
|
|
213
|
+
certificate = Certificate.fromCertificateArn(ctx.scope, `${props.clusterName}ManagedCertificate`, certificateArn);
|
|
234
214
|
}
|
|
235
215
|
}
|
|
236
216
|
else if (domainConfig?.hostedZone) {
|
|
@@ -302,6 +282,10 @@ export function addHostedZone(ctx, loadBalancer) {
|
|
|
302
282
|
}
|
|
303
283
|
}
|
|
304
284
|
const subjectAlternativeNames = [...routedHosts, ...redirectHosts].filter((h) => h !== domainName);
|
|
285
|
+
// Captured BEFORE the BYO override below: whether the managed lane
|
|
286
|
+
// attached the binding's (or export fallback's) certificate — the D5
|
|
287
|
+
// coverage context describes exactly that certificate, never a BYO one.
|
|
288
|
+
const managedCertificateAttached = certificate !== undefined;
|
|
305
289
|
if (domainConfig?.certificate) {
|
|
306
290
|
certificate = domainConfig.certificate;
|
|
307
291
|
}
|
|
@@ -332,7 +316,55 @@ export function addHostedZone(ctx, loadBalancer) {
|
|
|
332
316
|
exportCertificateArn: false
|
|
333
317
|
}).certificate;
|
|
334
318
|
}
|
|
335
|
-
|
|
319
|
+
// D5 provenance gate (design 2026-08-18 cdn-app-origin): the coverage
|
|
320
|
+
// context describes the zone's CURRENT certificates — the same D2
|
|
321
|
+
// resolution that produces an injected binding. An EXPLICIT pinned BINDING
|
|
322
|
+
// may name OLDER ARNs the current coverage does not describe, so folding
|
|
323
|
+
// it would over-claim and invert P3 (TLS fails at runtime after E6 passes
|
|
324
|
+
// the hostname as covered); it stays opaque, exactly as if no coverage
|
|
325
|
+
// context existed (see EffectiveManagedDomain.explicitPinnedBinding). The
|
|
326
|
+
// explicit EXPORTS form is unaffected: Fn.importValue resolves at deploy
|
|
327
|
+
// to the zone's current certificate — the one the coverage describes.
|
|
328
|
+
const injectedCoverage = effectiveManaged !== undefined && !effectiveManaged.explicitPinnedBinding
|
|
329
|
+
? readInjectedManagedDomainCoverage(ctx.scope.node, effectiveManaged.zoneName, `Cluster '${props.clusterName}'`)
|
|
330
|
+
: undefined;
|
|
331
|
+
// One walk over the additional-certificate sources yields both the
|
|
332
|
+
// listener attachments and their coverage facts — the fold below never
|
|
333
|
+
// re-discriminates the source list.
|
|
334
|
+
const additional = resolveAdditionalListenerCertificates(ctx.scope, props.clusterName, domainConfig?.additionalCertificates, managed, injectedCoverage?.usEast1CertificateHosts);
|
|
335
|
+
const additionalListenerCertificates = additional.certificates;
|
|
336
|
+
// D5 certificate coverage: fold the SAN set of every certificate the
|
|
337
|
+
// listener will carry, where knowable at synth. The cluster-minted
|
|
338
|
+
// certificate's set is the literal minted above; managed certificates are
|
|
339
|
+
// described by the CLI-injected coverage context (absent → opaque, a
|
|
340
|
+
// domain stack predating the hosts outputs); BYO imports are opaque by
|
|
341
|
+
// nature. Fail-open to "partial"/"unknown", never to a false "covered" —
|
|
342
|
+
// consumers (the Cdn origin lane) error only on enumerated non-coverage
|
|
343
|
+
// and warn on opacity.
|
|
344
|
+
const knownCertificateHosts = [];
|
|
345
|
+
let anyOpaqueCertificate = false;
|
|
346
|
+
if (domainConfig?.certificate) {
|
|
347
|
+
anyOpaqueCertificate = true;
|
|
348
|
+
}
|
|
349
|
+
else if (managedCertificateAttached) {
|
|
350
|
+
if (injectedCoverage?.certificateHosts !== undefined) {
|
|
351
|
+
knownCertificateHosts.push(...injectedCoverage.certificateHosts);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
anyOpaqueCertificate = true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
knownCertificateHosts.push(domainName, ...subjectAlternativeNames);
|
|
359
|
+
}
|
|
360
|
+
knownCertificateHosts.push(...additional.knownHosts);
|
|
361
|
+
anyOpaqueCertificate = anyOpaqueCertificate || additional.anyOpaque;
|
|
362
|
+
const dedupedCertificateHosts = [...new Set(knownCertificateHosts)];
|
|
363
|
+
const certificateCoverage = anyOpaqueCertificate
|
|
364
|
+
? dedupedCertificateHosts.length > 0
|
|
365
|
+
? { kind: "partial", hostnames: dedupedCertificateHosts }
|
|
366
|
+
: { kind: "unknown" }
|
|
367
|
+
: { kind: "enumerated", hostnames: dedupedCertificateHosts };
|
|
336
368
|
let aRecord;
|
|
337
369
|
if (loadBalancer) {
|
|
338
370
|
const routingPolicy = domainConfig?.routingPolicy;
|
|
@@ -430,7 +462,16 @@ export function addHostedZone(ctx, loadBalancer) {
|
|
|
430
462
|
}),
|
|
431
463
|
...(redirectHosts.length > 0 && {
|
|
432
464
|
redirectRules: { hostFqdns: redirectHosts, targetHost: domainName }
|
|
433
|
-
})
|
|
465
|
+
}),
|
|
466
|
+
ingress: {
|
|
467
|
+
hostedZone,
|
|
468
|
+
zoneName,
|
|
469
|
+
domainName,
|
|
470
|
+
routedHosts,
|
|
471
|
+
redirectHosts,
|
|
472
|
+
apexRecordMinted: loadBalancer !== undefined && domainConfig?.apexRecord !== "none",
|
|
473
|
+
certificateCoverage
|
|
474
|
+
}
|
|
434
475
|
};
|
|
435
476
|
}
|
|
436
477
|
const US_EAST_1 = "us-east-1";
|
|
@@ -442,13 +483,18 @@ const US_EAST_1 = "us-east-1";
|
|
|
442
483
|
* anything else fails at synth rather than as CloudFormation's opaque
|
|
443
484
|
* deploy-time rejection.
|
|
444
485
|
*/
|
|
445
|
-
function resolveAdditionalListenerCertificates(scope, clusterName, sources, managed) {
|
|
446
|
-
if (sources === undefined || sources.length === 0)
|
|
447
|
-
return [];
|
|
486
|
+
function resolveAdditionalListenerCertificates(scope, clusterName, sources, managed, usEast1CertificateHosts) {
|
|
448
487
|
const certificates = [];
|
|
488
|
+
const knownHosts = [];
|
|
489
|
+
let anyOpaque = false;
|
|
490
|
+
if (sources === undefined || sources.length === 0) {
|
|
491
|
+
return { certificates, knownHosts, anyOpaque };
|
|
492
|
+
}
|
|
449
493
|
for (const source of sources) {
|
|
450
494
|
if ("certificateArn" in source) {
|
|
495
|
+
// BYO import — opaque by nature.
|
|
451
496
|
certificates.push(ListenerCertificate.fromArn(source.certificateArn));
|
|
497
|
+
anyOpaque = true;
|
|
452
498
|
continue;
|
|
453
499
|
}
|
|
454
500
|
if (managed === undefined ||
|
|
@@ -482,8 +528,16 @@ function resolveAdditionalListenerCertificates(scope, clusterName, sources, mana
|
|
|
482
528
|
"stack, or supply certificateArn for a same-region certificate.");
|
|
483
529
|
}
|
|
484
530
|
certificates.push(ListenerCertificate.fromArn(arn));
|
|
531
|
+
// The managed us-east-1 certificate's hosts, when the coverage context
|
|
532
|
+
// describes them (absent → a domain stack predating the hosts outputs).
|
|
533
|
+
if (usEast1CertificateHosts !== undefined) {
|
|
534
|
+
knownHosts.push(...usEast1CertificateHosts);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
anyOpaque = true;
|
|
538
|
+
}
|
|
485
539
|
}
|
|
486
|
-
return certificates;
|
|
540
|
+
return { certificates, knownHosts, anyOpaque };
|
|
487
541
|
}
|
|
488
542
|
export function addDirectAccessOutputs(ctx, autoScalingGroup) {
|
|
489
543
|
if (!ctx.directAccessEnabled || !autoScalingGroup)
|
|
@@ -506,15 +560,17 @@ export function addDirectAccessOutputs(ctx, autoScalingGroup) {
|
|
|
506
560
|
}
|
|
507
561
|
export function registerServiceWithALB(ctx, listener, serviceName, serviceProps, service, primaryContainer, priorityState) {
|
|
508
562
|
const containerPort = primaryContainer.containerPort;
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
563
|
+
// The single home of the listener branching (P5, ingressProfile.ts): the
|
|
564
|
+
// ingress profile's rule model derives from this same plan, so what the
|
|
565
|
+
// profile reports is what this emitter builds.
|
|
566
|
+
const servicePlan = planListenerRules(ctx.props.services, []).services.find((plan) => plan.service === serviceProps);
|
|
567
|
+
if (servicePlan === undefined) {
|
|
568
|
+
throw new Error(`Service '${serviceName}' is not in the listener rule plan — ` +
|
|
569
|
+
"registerServiceWithALB was called for a service with no container " +
|
|
570
|
+
"ports, which can never receive listener traffic.");
|
|
571
|
+
}
|
|
572
|
+
const { routingRules } = servicePlan;
|
|
515
573
|
const healthCheckPath = routingRules.find((r) => r.healthCheckPath)?.healthCheckPath ?? "/";
|
|
516
|
-
const servicesWithPorts = ctx.props.services.filter((s) => s.containers.some((c) => c.port !== undefined));
|
|
517
|
-
const isSingleService = servicesWithPorts.length === 1;
|
|
518
574
|
const healthCheckConfig = isServiceEc2(serviceProps)
|
|
519
575
|
? {
|
|
520
576
|
interval: Duration.seconds(30),
|
|
@@ -530,7 +586,7 @@ export function registerServiceWithALB(ctx, listener, serviceName, serviceProps,
|
|
|
530
586
|
port: `${containerPort}`,
|
|
531
587
|
timeout: Duration.seconds(10)
|
|
532
588
|
};
|
|
533
|
-
if (
|
|
589
|
+
if (servicePlan.mode === "default") {
|
|
534
590
|
return listener.addTargets(`${serviceName}TargetGroup`, {
|
|
535
591
|
targets: [
|
|
536
592
|
service.loadBalancerTarget({
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synth-time ingress profile for an ECS cluster's ALB (design 2026-08-18
|
|
3
|
+
* cdn-app-origin, D2). The profile is the one sanctioned answer to "what
|
|
4
|
+
* does this cluster's listener actually do?" for downstream constructs —
|
|
5
|
+
* today the `Cdn` construct-reference origin lane, which needs a TLS-valid
|
|
6
|
+
* origin hostname, certificate coverage, and a forwarding verdict without
|
|
7
|
+
* re-deriving any of it from props.
|
|
8
|
+
*
|
|
9
|
+
* P5 (model the machinery, don't paraphrase it): every fact here is exported
|
|
10
|
+
* from the SAME computation the emitting code uses — `default404` is the
|
|
11
|
+
* predicate `addLoadBalancerListener` passes to the listener factory, the
|
|
12
|
+
* rule structure derives from the `planListenerRules` plan that
|
|
13
|
+
* `registerServiceWithALB` itself emits from (including the single-service
|
|
14
|
+
* conditions-dropped branch), and the covered SAN set is the one
|
|
15
|
+
* `addHostedZone` mints. Never recompute these facts from cluster props
|
|
16
|
+
* elsewhere; extend the plan or the profile instead.
|
|
17
|
+
*/
|
|
18
|
+
import type { IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
19
|
+
import type { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
20
|
+
import type { EcsRoutingConfig, EcsServiceProps } from "./ecsTypes.js";
|
|
21
|
+
/**
|
|
22
|
+
* What the certificates ATTACHED to the cluster's listener are known to
|
|
23
|
+
* cover at synth:
|
|
24
|
+
*
|
|
25
|
+
* - `enumerated` — every attached certificate's host set is synth-known
|
|
26
|
+
* (cluster-minted certs, or managed certs described by the D5 coverage
|
|
27
|
+
* context). `hostnames` is complete: a non-matching name is provably
|
|
28
|
+
* uncovered.
|
|
29
|
+
* - `partial` — `hostnames` collects the known certs' hosts, but at least
|
|
30
|
+
* one attached certificate is opaque (imported ARN with no coverage
|
|
31
|
+
* context), so a non-matching name may still be covered.
|
|
32
|
+
* - `unknown` — only opaque certificates are attached.
|
|
33
|
+
*/
|
|
34
|
+
export type CertificateCoverage = {
|
|
35
|
+
kind: "enumerated";
|
|
36
|
+
hostnames: string[];
|
|
37
|
+
} | {
|
|
38
|
+
kind: "partial";
|
|
39
|
+
hostnames: string[];
|
|
40
|
+
} | {
|
|
41
|
+
kind: "unknown";
|
|
42
|
+
};
|
|
43
|
+
export type CoverageAnswer = "covered" | "unknown" | "not-covered";
|
|
44
|
+
/**
|
|
45
|
+
* One listener rule as it will actually be emitted. `host` and `path` are
|
|
46
|
+
* ANDed when both present (`buildRoutingConditions`). Rules the emitter
|
|
47
|
+
* DROPS — a single ports-bearing service with ≤1 routing rule loses its
|
|
48
|
+
* conditions and becomes the listener default (`registerServiceWithALB`) —
|
|
49
|
+
* are deliberately absent here, exactly as they are absent from the ALB.
|
|
50
|
+
*/
|
|
51
|
+
export interface EcsIngressRule {
|
|
52
|
+
host?: string;
|
|
53
|
+
path?: string;
|
|
54
|
+
kind: "forward" | "redirect";
|
|
55
|
+
}
|
|
56
|
+
export interface EcsIngressProfile {
|
|
57
|
+
loadBalancer: IApplicationLoadBalancer;
|
|
58
|
+
/** `cluster.loadBalancer === "internal"` — unreachable as a CloudFront origin. */
|
|
59
|
+
internal: boolean;
|
|
60
|
+
/** 443 iff a certificate resolved — the cluster ALB has exactly one listener. */
|
|
61
|
+
listenerPort: 443 | 80;
|
|
62
|
+
/**
|
|
63
|
+
* The cluster's resolved zone — the SAME `IHostedZone` instance the
|
|
64
|
+
* cluster's own records claim with, so a downstream record (the Cdn's
|
|
65
|
+
* origin record) claims under the identical zone identity. The DNS claim
|
|
66
|
+
* registry keys on the literal hosted-zone ID; a name-derived re-import
|
|
67
|
+
* would register under a different key and silently defeat same-name
|
|
68
|
+
* collision detection.
|
|
69
|
+
*/
|
|
70
|
+
hostedZone?: IHostedZone;
|
|
71
|
+
zoneName?: string;
|
|
72
|
+
domainName?: string;
|
|
73
|
+
/** Every `services[].routing[].host`, deduped, in declaration order. */
|
|
74
|
+
routedHosts: string[];
|
|
75
|
+
redirectHosts: string[];
|
|
76
|
+
/**
|
|
77
|
+
* The listener's actual default-action predicate: `true` → fixed-404
|
|
78
|
+
* default; `false` → the sole target group IS the default action and the
|
|
79
|
+
* listener forwards EVERY hostname.
|
|
80
|
+
*/
|
|
81
|
+
default404: boolean;
|
|
82
|
+
rules: EcsIngressRule[];
|
|
83
|
+
certificateCoverage: CertificateCoverage;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Zone-derived profile ingredients assembled by `addHostedZone`, where the
|
|
87
|
+
* zone, hosts, and certificate coverage are already computed for emission.
|
|
88
|
+
*/
|
|
89
|
+
export interface EcsIngressZoneFacts {
|
|
90
|
+
hostedZone: IHostedZone;
|
|
91
|
+
zoneName: string;
|
|
92
|
+
domainName: string;
|
|
93
|
+
routedHosts: string[];
|
|
94
|
+
redirectHosts: string[];
|
|
95
|
+
/** False when `apexRecord: "none"` yielded the domainName record. */
|
|
96
|
+
apexRecordMinted: boolean;
|
|
97
|
+
certificateCoverage: CertificateCoverage;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One ports-bearing service's listener treatment. `mode: "default"` — the
|
|
101
|
+
* sole ports-bearing service with ≤1 routing rule: its target group is
|
|
102
|
+
* added UNCONDITIONED and becomes the listener default action (even a
|
|
103
|
+
* declared `routing.host` does not gate it). `mode: "rules"` — every
|
|
104
|
+
* routing rule becomes a conditioned, prioritised listener rule; the first
|
|
105
|
+
* carries the target group.
|
|
106
|
+
*/
|
|
107
|
+
export interface ListenerServicePlan {
|
|
108
|
+
service: EcsServiceProps;
|
|
109
|
+
/** The service's `routing` config, normalised to an array. */
|
|
110
|
+
routingRules: EcsRoutingConfig[];
|
|
111
|
+
mode: "default" | "rules";
|
|
112
|
+
}
|
|
113
|
+
export interface ListenerRulePlan {
|
|
114
|
+
/** Ports-bearing services in declaration order (portless services never
|
|
115
|
+
* reach the listener). */
|
|
116
|
+
services: ListenerServicePlan[];
|
|
117
|
+
redirectHosts: string[];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The listener's rule structure, planned once (P5). This is the SINGLE home
|
|
121
|
+
* of the branching: `registerServiceWithALB` emits from a service's plan
|
|
122
|
+
* entry, and the ingress-profile facts below (`computeListenerDefault404`,
|
|
123
|
+
* `enumerateListenerRules`) derive from the same plan — the emitter and the
|
|
124
|
+
* model cannot drift because neither re-derives the decisions.
|
|
125
|
+
*/
|
|
126
|
+
export declare function planListenerRules(services: EcsServiceProps[], redirectHosts: string[]): ListenerRulePlan;
|
|
127
|
+
/**
|
|
128
|
+
* The listener's default-action predicate — the value
|
|
129
|
+
* `addLoadBalancerListener` passes to the listener factory as `default404`.
|
|
130
|
+
* `true` exactly when no service plan supplies an unconditioned default
|
|
131
|
+
* target group: ≥2 routes will exist (the emitter then attaches a fixed-404
|
|
132
|
+
* default and conditions every rule), or no service has a port (CDK rejects
|
|
133
|
+
* a listener with neither a default action nor targets).
|
|
134
|
+
*/
|
|
135
|
+
export declare function computeListenerDefault404(services: EcsServiceProps[]): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* The listener's rule structure as `registerServiceWithALB` and
|
|
138
|
+
* `addRedirectHostRules` will emit it — derived from the same
|
|
139
|
+
* `planListenerRules` plan the emitters consume: a `mode: "default"`
|
|
140
|
+
* service contributes NO conditioned rule (its target group is the listener
|
|
141
|
+
* default), and every redirect host contributes a host-matched 301 rule.
|
|
142
|
+
*/
|
|
143
|
+
export declare function enumerateListenerRules(services: EcsServiceProps[], redirectHosts: string[]): EcsIngressRule[];
|
|
144
|
+
/** Lowercase and strip the trailing dot — DNS names are case-insensitive. */
|
|
145
|
+
export declare function normaliseDnsName(name: string): string;
|
|
146
|
+
/**
|
|
147
|
+
* RFC 6125 certificate-name match: exact, or a single-label `*.` wildcard
|
|
148
|
+
* (matches exactly one additional label — `*.example.com` covers
|
|
149
|
+
* `a.example.com`, never `example.com` or `a.b.example.com`). DNS names
|
|
150
|
+
* compare ASCII case-insensitively (RFC 6125 §6.4.1 via RFC 4343).
|
|
151
|
+
*/
|
|
152
|
+
export declare function certNameMatches(pattern: string, hostname: string): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Does the listener's attached-certificate set cover `hostname`?
|
|
155
|
+
* Fail closed where decidable, honest where opaque (P3): only `enumerated`
|
|
156
|
+
* coverage may answer `not-covered`; a miss under `partial`/`unknown` is
|
|
157
|
+
* `unknown` because an opaque certificate may still cover the name.
|
|
158
|
+
*/
|
|
159
|
+
export declare function certificateCovers(coverage: CertificateCoverage, hostname: string): CoverageAnswer;
|
|
160
|
+
export type ForwardingVerdict = {
|
|
161
|
+
verdict: "forwards";
|
|
162
|
+
} | {
|
|
163
|
+
verdict: "partial";
|
|
164
|
+
patterns: string[];
|
|
165
|
+
} | {
|
|
166
|
+
verdict: "none";
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Would the listener forward `Host: <hostname>` requests, for all paths?
|
|
170
|
+
* Computed on the real rule model, never a flattened hostname list:
|
|
171
|
+
*
|
|
172
|
+
* - no fixed-404 default → the default action forwards every hostname;
|
|
173
|
+
* - a host-only rule for the hostname, a host-matched catch-all `/*` path
|
|
174
|
+
* rule, or a host-less catch-all `/*` path rule, forwards all of its
|
|
175
|
+
* paths;
|
|
176
|
+
* - rules that involve the hostname only together with a narrower path
|
|
177
|
+
* condition (ANDed host+path, or host-less non-catch-all paths) forward
|
|
178
|
+
* SOME paths — `partial`, with the patterns named so the caller can
|
|
179
|
+
* surface them;
|
|
180
|
+
* - otherwise every request answers the fixed-404 default — `none`.
|
|
181
|
+
*
|
|
182
|
+
* Hostnames compare case-insensitively (DNS, RFC 4343) — the rule model
|
|
183
|
+
* carries the declared spelling, the caller's hostname may differ in case.
|
|
184
|
+
*/
|
|
185
|
+
export declare function forwardingVerdict(profile: EcsIngressProfile, hostname: string): ForwardingVerdict;
|
|
186
|
+
export declare function buildIngressProfile(options: {
|
|
187
|
+
loadBalancer: IApplicationLoadBalancer;
|
|
188
|
+
internal: boolean;
|
|
189
|
+
services: EcsServiceProps[];
|
|
190
|
+
certificateAttached: boolean;
|
|
191
|
+
zoneFacts?: EcsIngressZoneFacts;
|
|
192
|
+
}): EcsIngressProfile;
|
|
193
|
+
/**
|
|
194
|
+
* W3 (design 2026-08-18 cdn-app-origin): a cluster whose listener default is
|
|
195
|
+
* fixed-404 while its `domainName` has a minted apex record and NO rule
|
|
196
|
+
* forwards it answers 404 on its own primary domain — usually the aftermath
|
|
197
|
+
* of adding a second routing rule (which flips the default action from
|
|
198
|
+
* forward to 404, `computeListenerDefault404`). A warning, not an error:
|
|
199
|
+
* pre-existing clusters can already be in this state, and narrowing them is
|
|
200
|
+
* not this check's mandate. Partial forwarding (path-split services) is the
|
|
201
|
+
* normal multi-route shape and does not warn.
|
|
202
|
+
*/
|
|
203
|
+
export declare function warnWhenRecordedApexUnforwarded(profile: EcsIngressProfile, apexRecordMinted: boolean, clusterName: string): void;
|