@fjall/components-infrastructure 7.2.0 → 8.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 +12 -0
- package/dist/lib/app.js +25 -5
- package/dist/lib/config/aws/ipam.js +4 -3
- package/dist/lib/patterns/aws/cdn.d.ts +7 -1
- package/dist/lib/patterns/aws/cdn.js +9 -2
- package/dist/lib/patterns/aws/database.js +4 -3
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +6 -0
- package/dist/lib/patterns/aws/storage.js +3 -0
- package/dist/lib/patterns/aws/targets/fjallTargets.d.ts +9 -4
- package/dist/lib/patterns/aws/targets/fjallTargets.js +11 -5
- package/dist/lib/patterns/aws/targets/targetResolution.d.ts +33 -40
- package/dist/lib/patterns/aws/targets/targetResolution.js +42 -52
- package/dist/lib/resources/aws/backup/backupVault.js +2 -0
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +9 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +2 -1
- package/dist/lib/resources/aws/compute/ecs.js +4 -3
- package/dist/lib/resources/aws/compute/ecsNetworking.js +29 -15
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +2 -1
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +6 -2
- package/dist/lib/resources/aws/database/rdsAurora.js +2 -2
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +3 -2
- package/dist/lib/resources/aws/database/rdsHelpers.js +12 -2
- package/dist/lib/resources/aws/database/rdsInstance.js +3 -2
- package/dist/lib/resources/aws/database/rdsProxyOutput.js +2 -1
- package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
- package/dist/lib/resources/aws/networking/ipamPool.js +2 -1
- package/dist/lib/resources/aws/networking/vpc.js +8 -2
- package/dist/lib/resources/aws/secrets/kms.d.ts +24 -7
- package/dist/lib/resources/aws/secrets/kms.js +29 -6
- package/dist/lib/resources/aws/secrets/parameter.js +3 -1
- package/dist/lib/resources/aws/secrets/secret.js +5 -1
- package/dist/lib/resources/aws/storage/ecr.d.ts +6 -0
- package/dist/lib/resources/aws/storage/ecr.js +8 -4
- package/dist/lib/resources/aws/storage/s3.d.ts +10 -0
- package/dist/lib/resources/aws/storage/s3.js +9 -4
- package/dist/lib/utils/albAliasTargetRegistry.d.ts +44 -0
- package/dist/lib/utils/albAliasTargetRegistry.js +69 -0
- package/dist/lib/utils/env.d.ts +9 -0
- package/dist/lib/utils/env.js +12 -0
- package/dist/lib/utils/exportNaming.d.ts +50 -0
- package/dist/lib/utils/exportNaming.js +52 -0
- package/dist/lib/utils/orgConfigParser.d.ts +3 -8
- package/dist/lib/utils/orgConfigParser.js +4 -14
- package/dist/lib/utils/removalPolicy.d.ts +11 -3
- package/dist/lib/utils/removalPolicy.js +20 -5
- package/dist/lib/utils/resourceNaming.d.ts +50 -0
- package/dist/lib/utils/resourceNaming.js +74 -0
- package/package.json +9 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CfnOutput } from "aws-cdk-lib";
|
|
2
|
+
import { albDnsExportName, albHostedZoneIdExportName } from "@fjall/util";
|
|
3
|
+
import { toPascalCase } from "./capitaliseString.js";
|
|
4
|
+
import { FjallLogger } from "./validationLogger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Keyed on the construct-tree root (the `App`) so two apps synthesised in one
|
|
7
|
+
* process — the platform and organisation stacks do this — never see each
|
|
8
|
+
* other's registrations. Weak so a discarded tree does not leak.
|
|
9
|
+
*/
|
|
10
|
+
const registryByRoot = new WeakMap();
|
|
11
|
+
export function registerAlbAliasTarget(registration) {
|
|
12
|
+
const root = registration.scope.node.root;
|
|
13
|
+
const existing = registryByRoot.get(root);
|
|
14
|
+
if (existing === undefined) {
|
|
15
|
+
registryByRoot.set(root, [registration]);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
existing.push(registration);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Emit the unqualified app-level alias exports for every app in this tree
|
|
22
|
+
* that registered exactly one ALB. Call once, from `App.synth()`, before
|
|
23
|
+
* `super.synth()` — the construct tree is still mutable there, and that is
|
|
24
|
+
* the same window `applyTagsAspect()` uses.
|
|
25
|
+
*/
|
|
26
|
+
export function emitAppLevelAlbAliasExports(root) {
|
|
27
|
+
const registrations = registryByRoot.get(root);
|
|
28
|
+
if (registrations === undefined)
|
|
29
|
+
return;
|
|
30
|
+
// `App.synth()` is re-entrant (CDK caches the assembly and returns it), and
|
|
31
|
+
// a second emission would collide on the output's construct id. Draining
|
|
32
|
+
// makes the call idempotent.
|
|
33
|
+
registryByRoot.delete(root);
|
|
34
|
+
const byApp = new Map();
|
|
35
|
+
for (const registration of registrations) {
|
|
36
|
+
const forApp = byApp.get(registration.appName);
|
|
37
|
+
if (forApp === undefined) {
|
|
38
|
+
byApp.set(registration.appName, [registration]);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
forApp.push(registration);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const [appName, forApp] of byApp) {
|
|
45
|
+
const only = forApp.length === 1 ? forApp[0] : undefined;
|
|
46
|
+
if (only === undefined) {
|
|
47
|
+
const computeNames = forApp.map((r) => r.computeName).join(", ");
|
|
48
|
+
FjallLogger.warn(`App '${appName}' fronts ${String(forApp.length)} load balancers (${computeNames}), ` +
|
|
49
|
+
`so no app-level '${albDnsExportName(appName)}' export was emitted — ` +
|
|
50
|
+
`'the app's ALB' would be ambiguous. Target a specific one with ` +
|
|
51
|
+
`fjallApp('${appName}', '<compute>') in your Domain records.`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Output logical ids are alphanumeric-only; the app name is not.
|
|
55
|
+
const logicalId = `${toPascalCase(appName)}AppAlb`;
|
|
56
|
+
new CfnOutput(only.scope, `${logicalId}DnsName`, {
|
|
57
|
+
key: `${logicalId}DnsName`,
|
|
58
|
+
exportName: albDnsExportName(appName),
|
|
59
|
+
value: only.dnsName,
|
|
60
|
+
description: `ALB DNS name for ${appName} (sole ALB; consumed by fjallApp('${appName}'))`
|
|
61
|
+
});
|
|
62
|
+
new CfnOutput(only.scope, `${logicalId}HostedZoneId`, {
|
|
63
|
+
key: `${logicalId}HostedZoneId`,
|
|
64
|
+
exportName: albHostedZoneIdExportName(appName),
|
|
65
|
+
value: only.hostedZoneId,
|
|
66
|
+
description: `ALB canonical hosted zone ID for ${appName} (sole ALB)`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/lib/utils/env.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ export declare function getEnvironment(): string;
|
|
|
47
47
|
* sentinel meaning "no signal at all".
|
|
48
48
|
*/
|
|
49
49
|
export declare function hasExplicitEnvironmentSignal(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* True when this synth was driven by Fjall rather than run bare. deploy-core
|
|
52
|
+
* supplies `-c orgConfig=` on every synth it drives and nothing else does, so
|
|
53
|
+
* its presence separates "a developer is inspecting a template" from "an
|
|
54
|
+
* account is about to be deployed to" — a distinction consumers that pick a
|
|
55
|
+
* default on missing input need, because the safe default differs between the
|
|
56
|
+
* two. See `envAwareRemovalPolicyDefault`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isFjallDrivenSynth(): boolean;
|
|
50
59
|
/**
|
|
51
60
|
* Resolve an environment-specific value at CDK synth time.
|
|
52
61
|
*
|
package/dist/lib/utils/env.js
CHANGED
|
@@ -77,6 +77,18 @@ export function hasExplicitEnvironmentSignal() {
|
|
|
77
77
|
const ctxValue = parseContextArg("environment");
|
|
78
78
|
return ctxValue !== undefined && ctxValue !== "";
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* True when this synth was driven by Fjall rather than run bare. deploy-core
|
|
82
|
+
* supplies `-c orgConfig=` on every synth it drives and nothing else does, so
|
|
83
|
+
* its presence separates "a developer is inspecting a template" from "an
|
|
84
|
+
* account is about to be deployed to" — a distinction consumers that pick a
|
|
85
|
+
* default on missing input need, because the safe default differs between the
|
|
86
|
+
* two. See `envAwareRemovalPolicyDefault`.
|
|
87
|
+
*/
|
|
88
|
+
export function isFjallDrivenSynth() {
|
|
89
|
+
const raw = parseContextArg(CDK_CONTEXT_KEYS.ORG_CONFIG);
|
|
90
|
+
return raw !== undefined && raw !== "";
|
|
91
|
+
}
|
|
80
92
|
/**
|
|
81
93
|
* Resolve an environment-specific value at CDK synth time.
|
|
82
94
|
*
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Construct } from "constructs";
|
|
2
|
+
/**
|
|
3
|
+
* Qualify a CloudFormation export name with the stack that emits it.
|
|
4
|
+
*
|
|
5
|
+
* Export names are unique per account per region, so a name built only from a
|
|
6
|
+
* construct id or a cluster name is shared by every app that happens to use
|
|
7
|
+
* the same id — and the ids inside a pattern are literals the app author never
|
|
8
|
+
* chooses. Two apps then collide, and the second one fails at CloudFormation
|
|
9
|
+
* execution ("Export with name X is already exported by stack Y") after its
|
|
10
|
+
* first resources exist, so it rolls back rather than refusing up front.
|
|
11
|
+
*
|
|
12
|
+
* Stack names are already unique per account+region (CloudFormation enforces
|
|
13
|
+
* it) and Fjall derives them from the app name, so prefixing with the stack
|
|
14
|
+
* name makes the export globally unique AND puts the app name in it.
|
|
15
|
+
*
|
|
16
|
+
* The suffix must stay last. Consumers match these by suffix and by app-name
|
|
17
|
+
* token, never by whole string — `EcsServiceResolver` filters on
|
|
18
|
+
* `endsWith("deployableservice")` (deploy-core
|
|
19
|
+
* services/infrastructure/EcsServiceResolver.ts) and the webapp finds a linked
|
|
20
|
+
* app's CIDR by `ListExports` + a `-vpc-cidr` suffix filter
|
|
21
|
+
* (webapp app/.server/services/infrastructure/captureVpcCidr.ts). Prefixing is
|
|
22
|
+
* transparent to both; changing the tail is not.
|
|
23
|
+
*
|
|
24
|
+
* NOT for exports a consumer reconstructs by whole name. Those are contracts
|
|
25
|
+
* with their own vocabulary module and must keep their exact spelling:
|
|
26
|
+
* `getDomainExportNames` (@fjall/util), `EXPORT_NAMES`
|
|
27
|
+
* (patterns/aws/targets/targetResolution.ts), `SHARED_ALARM_TOPIC_EXPORT_NAME`,
|
|
28
|
+
* the CloudTrail output keys, and `${toPascalCase(appName)}EcrRepositoryName`
|
|
29
|
+
* (read by `resolveEcrRepositoryName` in the CLI). Account-level singletons
|
|
30
|
+
* (`AccountId`, `Environment`, `OrganisationId`, `FjallAuditRoleArn`, …) also
|
|
31
|
+
* keep their bare names: exactly one is meant to exist, so a collision there
|
|
32
|
+
* correctly reports a duplicate deployment.
|
|
33
|
+
*/
|
|
34
|
+
export declare function stackScopedExportName(scope: Construct, suffix: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Export name for the account's IPAM private default scope.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately unqualified: exactly one IPAM exists per account, so a
|
|
39
|
+
* collision here correctly reports a duplicate deployment rather than an
|
|
40
|
+
* accidental clash between two apps.
|
|
41
|
+
*
|
|
42
|
+
* It lives here rather than beside the construct that emits it because the
|
|
43
|
+
* producer is in `config/` and the consumer (`IpamPool`) is in `resources/`,
|
|
44
|
+
* which may not import from `config/`. Both sides must read the same
|
|
45
|
+
* constant: CloudFormation export names are case-sensitive, and the two sites
|
|
46
|
+
* previously disagreed on the leading capital — so the import could never
|
|
47
|
+
* resolve, and the only reason nothing broke is that every caller passed
|
|
48
|
+
* `ipamScope` explicitly and never reached the fallback.
|
|
49
|
+
*/
|
|
50
|
+
export declare const IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME = "IpamPrivateDefaultScopeId";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
/**
|
|
3
|
+
* Qualify a CloudFormation export name with the stack that emits it.
|
|
4
|
+
*
|
|
5
|
+
* Export names are unique per account per region, so a name built only from a
|
|
6
|
+
* construct id or a cluster name is shared by every app that happens to use
|
|
7
|
+
* the same id — and the ids inside a pattern are literals the app author never
|
|
8
|
+
* chooses. Two apps then collide, and the second one fails at CloudFormation
|
|
9
|
+
* execution ("Export with name X is already exported by stack Y") after its
|
|
10
|
+
* first resources exist, so it rolls back rather than refusing up front.
|
|
11
|
+
*
|
|
12
|
+
* Stack names are already unique per account+region (CloudFormation enforces
|
|
13
|
+
* it) and Fjall derives them from the app name, so prefixing with the stack
|
|
14
|
+
* name makes the export globally unique AND puts the app name in it.
|
|
15
|
+
*
|
|
16
|
+
* The suffix must stay last. Consumers match these by suffix and by app-name
|
|
17
|
+
* token, never by whole string — `EcsServiceResolver` filters on
|
|
18
|
+
* `endsWith("deployableservice")` (deploy-core
|
|
19
|
+
* services/infrastructure/EcsServiceResolver.ts) and the webapp finds a linked
|
|
20
|
+
* app's CIDR by `ListExports` + a `-vpc-cidr` suffix filter
|
|
21
|
+
* (webapp app/.server/services/infrastructure/captureVpcCidr.ts). Prefixing is
|
|
22
|
+
* transparent to both; changing the tail is not.
|
|
23
|
+
*
|
|
24
|
+
* NOT for exports a consumer reconstructs by whole name. Those are contracts
|
|
25
|
+
* with their own vocabulary module and must keep their exact spelling:
|
|
26
|
+
* `getDomainExportNames` (@fjall/util), `EXPORT_NAMES`
|
|
27
|
+
* (patterns/aws/targets/targetResolution.ts), `SHARED_ALARM_TOPIC_EXPORT_NAME`,
|
|
28
|
+
* the CloudTrail output keys, and `${toPascalCase(appName)}EcrRepositoryName`
|
|
29
|
+
* (read by `resolveEcrRepositoryName` in the CLI). Account-level singletons
|
|
30
|
+
* (`AccountId`, `Environment`, `OrganisationId`, `FjallAuditRoleArn`, …) also
|
|
31
|
+
* keep their bare names: exactly one is meant to exist, so a collision there
|
|
32
|
+
* correctly reports a duplicate deployment.
|
|
33
|
+
*/
|
|
34
|
+
export function stackScopedExportName(scope, suffix) {
|
|
35
|
+
return `${Stack.of(scope).stackName}${suffix}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Export name for the account's IPAM private default scope.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately unqualified: exactly one IPAM exists per account, so a
|
|
41
|
+
* collision here correctly reports a duplicate deployment rather than an
|
|
42
|
+
* accidental clash between two apps.
|
|
43
|
+
*
|
|
44
|
+
* It lives here rather than beside the construct that emits it because the
|
|
45
|
+
* producer is in `config/` and the consumer (`IpamPool`) is in `resources/`,
|
|
46
|
+
* which may not import from `config/`. Both sides must read the same
|
|
47
|
+
* constant: CloudFormation export names are case-sensitive, and the two sites
|
|
48
|
+
* previously disagreed on the leading capital — so the import could never
|
|
49
|
+
* resolve, and the only reason nothing broke is that every caller passed
|
|
50
|
+
* `ipamScope` explicitly and never reached the fallback.
|
|
51
|
+
*/
|
|
52
|
+
export const IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME = "IpamPrivateDefaultScopeId";
|
|
@@ -6,15 +6,10 @@ export interface ParsedOrgConfig {
|
|
|
6
6
|
disasterRecoveryRegion?: string;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the wire `environment` for organisation-tier accounts, but scaffolded org
|
|
12
|
-
* entry points gate on `config.environment === "root"` — decode the tier back
|
|
13
|
-
* to the historical "root" marker via the sanctioned `accountTier()` decoder.
|
|
14
|
-
* Workload stages pass through verbatim; a null stage on any other tier stays
|
|
15
|
-
* unresolved (callers fall back to "unknown").
|
|
9
|
+
* Re-exported from `@fjall/util` so the synth-time reader and deploy-core's
|
|
10
|
+
* pre-synth resolver cannot drift. Do not redeclare it here.
|
|
16
11
|
*/
|
|
17
|
-
export
|
|
12
|
+
export { resolveSynthEnvironment } from "@fjall/util";
|
|
18
13
|
/**
|
|
19
14
|
* Parse orgConfig JSON from CDK context into a validated structure.
|
|
20
15
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VAULT_LOCK_MODES, S3_BPA_MODES } from "@fjall/util/config";
|
|
2
|
-
import { ACCOUNT_TIERS,
|
|
2
|
+
import { ACCOUNT_TIERS, maskSensitiveOutput } from "@fjall/util";
|
|
3
3
|
import { FjallLogger } from "./validationLogger.js";
|
|
4
4
|
function isProviderAccount(item) {
|
|
5
5
|
if (typeof item !== "object" || item === null)
|
|
@@ -20,20 +20,10 @@ function isProviderAccount(item) {
|
|
|
20
20
|
typeof rec.acknowledgeImmutableVaultLock === "boolean"));
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the wire `environment` for organisation-tier accounts, but scaffolded org
|
|
26
|
-
* entry points gate on `config.environment === "root"` — decode the tier back
|
|
27
|
-
* to the historical "root" marker via the sanctioned `accountTier()` decoder.
|
|
28
|
-
* Workload stages pass through verbatim; a null stage on any other tier stays
|
|
29
|
-
* unresolved (callers fall back to "unknown").
|
|
23
|
+
* Re-exported from `@fjall/util` so the synth-time reader and deploy-core's
|
|
24
|
+
* pre-synth resolver cannot drift. Do not redeclare it here.
|
|
30
25
|
*/
|
|
31
|
-
export
|
|
32
|
-
if (accountTier(account) === "organisation") {
|
|
33
|
-
return STRUCTURAL_ENVIRONMENTS.ROOT;
|
|
34
|
-
}
|
|
35
|
-
return account.environment ?? undefined;
|
|
36
|
-
}
|
|
26
|
+
export { resolveSynthEnvironment } from "@fjall/util";
|
|
37
27
|
function describeRejectedAccount(item) {
|
|
38
28
|
if (typeof item !== "object" || item === null)
|
|
39
29
|
return "<unidentifiable>";
|
|
@@ -8,10 +8,18 @@ export declare function toRemovalPolicy(value?: "DESTROY" | "RETAIN" | "SNAPSHOT
|
|
|
8
8
|
* benign), an unrecognised value here throws at synth: silently landing a
|
|
9
9
|
* typo like `ENVIRONMENT=prod` on DESTROY deletes data when the stack is
|
|
10
10
|
* deleted. The accept-set derives from `ACCOUNT_STAGES_WITH_ROOT` so a new
|
|
11
|
-
* stage added in `@fjall/util` widens it automatically.
|
|
12
|
-
* sentinel keeps the historical warn-and-DESTROY behaviour — raw `cdk synth`
|
|
13
|
-
* without context and null-stage cascade synths rely on it. An explicitly
|
|
11
|
+
* stage added in `@fjall/util` widens it automatically. An explicitly
|
|
14
12
|
* supplied `ENVIRONMENT=unknown` collides with the sentinel string and would
|
|
15
13
|
* otherwise ride the silent-DESTROY path, so it is treated as unrecognised.
|
|
14
|
+
*
|
|
15
|
+
* The no-signal sentinel keeps warn-and-DESTROY for a BARE `cdk synth`: a
|
|
16
|
+
* developer inspecting a template deploys nothing, so the policy it prints
|
|
17
|
+
* costs nothing. It is not a safe default for a synth that is about to
|
|
18
|
+
* deploy, so a Fjall-driven one (`isFjallDrivenSynth`) throws instead — there,
|
|
19
|
+
* "no signal" means the account resolved to no environment, and reading that
|
|
20
|
+
* as DESTROY is how a production stack ends up with `Delete` on every bucket.
|
|
21
|
+
* deploy-core resolves the environment before synth and passes it explicitly
|
|
22
|
+
* (deploy-core orchestration/deployEnvironment.ts), so a conforming deploy
|
|
23
|
+
* reaches neither branch; this is the backstop for one that does.
|
|
16
24
|
*/
|
|
17
25
|
export declare function envAwareRemovalPolicyDefault(): "DESTROY" | "RETAIN";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ACCOUNT_STAGES_WITH_ROOT } from "@fjall/util";
|
|
2
2
|
import { RemovalPolicy } from "aws-cdk-lib";
|
|
3
|
-
import { getEnvironment, hasExplicitEnvironmentSignal, UNKNOWN_ENVIRONMENT } from "./env.js";
|
|
3
|
+
import { getEnvironment, hasExplicitEnvironmentSignal, isFjallDrivenSynth, UNKNOWN_ENVIRONMENT } from "./env.js";
|
|
4
4
|
export function toRemovalPolicy(value) {
|
|
5
5
|
switch (value) {
|
|
6
6
|
case "DESTROY":
|
|
@@ -21,16 +21,31 @@ const REMOVAL_DEFAULT_ENVIRONMENTS = new Set(ACCOUNT_STAGES_WITH_ROOT);
|
|
|
21
21
|
* benign), an unrecognised value here throws at synth: silently landing a
|
|
22
22
|
* typo like `ENVIRONMENT=prod` on DESTROY deletes data when the stack is
|
|
23
23
|
* deleted. The accept-set derives from `ACCOUNT_STAGES_WITH_ROOT` so a new
|
|
24
|
-
* stage added in `@fjall/util` widens it automatically.
|
|
25
|
-
* sentinel keeps the historical warn-and-DESTROY behaviour — raw `cdk synth`
|
|
26
|
-
* without context and null-stage cascade synths rely on it. An explicitly
|
|
24
|
+
* stage added in `@fjall/util` widens it automatically. An explicitly
|
|
27
25
|
* supplied `ENVIRONMENT=unknown` collides with the sentinel string and would
|
|
28
26
|
* otherwise ride the silent-DESTROY path, so it is treated as unrecognised.
|
|
27
|
+
*
|
|
28
|
+
* The no-signal sentinel keeps warn-and-DESTROY for a BARE `cdk synth`: a
|
|
29
|
+
* developer inspecting a template deploys nothing, so the policy it prints
|
|
30
|
+
* costs nothing. It is not a safe default for a synth that is about to
|
|
31
|
+
* deploy, so a Fjall-driven one (`isFjallDrivenSynth`) throws instead — there,
|
|
32
|
+
* "no signal" means the account resolved to no environment, and reading that
|
|
33
|
+
* as DESTROY is how a production stack ends up with `Delete` on every bucket.
|
|
34
|
+
* deploy-core resolves the environment before synth and passes it explicitly
|
|
35
|
+
* (deploy-core orchestration/deployEnvironment.ts), so a conforming deploy
|
|
36
|
+
* reaches neither branch; this is the backstop for one that does.
|
|
29
37
|
*/
|
|
30
38
|
export function envAwareRemovalPolicyDefault() {
|
|
31
39
|
const environment = getEnvironment();
|
|
32
40
|
if (environment === UNKNOWN_ENVIRONMENT && !hasExplicitEnvironmentSignal()) {
|
|
33
|
-
|
|
41
|
+
if (!isFjallDrivenSynth())
|
|
42
|
+
return "DESTROY";
|
|
43
|
+
throw new Error(`Refusing to resolve the env-aware removal-policy default: this deploy ` +
|
|
44
|
+
`carries an organisation config, but the account it targets resolved ` +
|
|
45
|
+
`to no environment. Defaulting to DESTROY here would delete data when ` +
|
|
46
|
+
`the stack is deleted. Set the target account's environment in the ` +
|
|
47
|
+
`organisation config, or pass -c environment=<value> explicitly. ` +
|
|
48
|
+
`Valid values: ${ACCOUNT_STAGES_WITH_ROOT.join(", ")}.`);
|
|
34
49
|
}
|
|
35
50
|
if (!REMOVAL_DEFAULT_ENVIRONMENTS.has(environment)) {
|
|
36
51
|
throw new Error(`Unrecognised environment "${environment}" — refusing to resolve the ` +
|
|
@@ -41,4 +41,54 @@ export declare class ResourceNaming {
|
|
|
41
41
|
static proxySecurityGroupDescription(name: string): string;
|
|
42
42
|
/** Security group description for read replica. */
|
|
43
43
|
static readReplicaSecurityGroupDescription(name: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* KMS alias for a database's storage encryption key. `name` is the logical
|
|
46
|
+
* database name, and the casing is load-bearing — KMS aliases are
|
|
47
|
+
* case-sensitive and these are live on deployed keys.
|
|
48
|
+
*
|
|
49
|
+
* The three alias helpers return the value handed to `CustomerManagedKey`,
|
|
50
|
+
* which is what CDK prefixes with `alias/`; `kms list-aliases` reports the
|
|
51
|
+
* prefixed form.
|
|
52
|
+
*/
|
|
53
|
+
static storageEncryptionKeyAlias(name: string): string;
|
|
54
|
+
/** KMS alias for a database's Database Insights encryption key. */
|
|
55
|
+
static insightsKeyAlias(name: string): string;
|
|
56
|
+
/** KMS alias for a read replica's Database Insights encryption key. */
|
|
57
|
+
static readReplicaInsightsKeyAlias(name: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Construct id of the RDS child the Database pattern creates for an
|
|
60
|
+
* `Instance` or `Aurora` plan. The instance, cluster, proxy and secret
|
|
61
|
+
* helpers are all fed this id, NOT the Database construct's own id, so
|
|
62
|
+
* anything predicting a deployed name from a resource plan starts here.
|
|
63
|
+
*/
|
|
64
|
+
static rdsChildConstructId(databaseName: string): string;
|
|
65
|
+
/** Construct id of the RdsAuroraGlobal child, which names the cluster. */
|
|
66
|
+
static globalRdsChildConstructId(databaseName: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Construct id of the RdsAurora primary inside RdsAuroraGlobal, which names
|
|
69
|
+
* its instances, proxy and credentials secret.
|
|
70
|
+
*/
|
|
71
|
+
static globalPrimaryConstructId(databaseName: string): string;
|
|
72
|
+
/** Construct id of a database's credentials Secret. */
|
|
73
|
+
static credentialsSecretConstructId(databaseName: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Alias a Secret or CustomerManagedKey takes when the caller supplies none.
|
|
76
|
+
* CDK prefixes it with `alias/`, which is the form `kms list-aliases`
|
|
77
|
+
* reports.
|
|
78
|
+
*/
|
|
79
|
+
static defaultKeyAlias(constructId: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* S3 bucket holding a VPC's flow logs. `id` is the Network construct id —
|
|
82
|
+
* the network name for an additional VPC, `${stackPrefix}Vpc` for the
|
|
83
|
+
* primary. Lowercased rather than kebabed: S3 forbids uppercase and this
|
|
84
|
+
* name is already live on deployed buckets, so the transform cannot change.
|
|
85
|
+
*/
|
|
86
|
+
static vpcFlowLogBucketName(id: string, accountId: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* ALB physical name for an ECS cluster. ELBv2 caps names at 32 characters
|
|
89
|
+
* and rejects a trailing hyphen, so a long cluster name is truncated and
|
|
90
|
+
* then stripped — which means the name is NOT simply
|
|
91
|
+
* `${clusterName}LoadBalancer` and must not be re-derived as if it were.
|
|
92
|
+
*/
|
|
93
|
+
static loadBalancerName(clusterName: string): string;
|
|
44
94
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { toKebab } from "./capitaliseString.js";
|
|
2
|
+
/** ELBv2's hard limit on an Application Load Balancer's name. */
|
|
3
|
+
const LOAD_BALANCER_NAME_MAX_LENGTH = 32;
|
|
2
4
|
/**
|
|
3
5
|
* Centralised resource naming for infrastructure.
|
|
4
6
|
* Ensures consistent, predictable names across constructs and tests.
|
|
@@ -73,4 +75,76 @@ export class ResourceNaming {
|
|
|
73
75
|
static readReplicaSecurityGroupDescription(name) {
|
|
74
76
|
return `Security group for RDS Read Replica ${name}`;
|
|
75
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* KMS alias for a database's storage encryption key. `name` is the logical
|
|
80
|
+
* database name, and the casing is load-bearing — KMS aliases are
|
|
81
|
+
* case-sensitive and these are live on deployed keys.
|
|
82
|
+
*
|
|
83
|
+
* The three alias helpers return the value handed to `CustomerManagedKey`,
|
|
84
|
+
* which is what CDK prefixes with `alias/`; `kms list-aliases` reports the
|
|
85
|
+
* prefixed form.
|
|
86
|
+
*/
|
|
87
|
+
static storageEncryptionKeyAlias(name) {
|
|
88
|
+
return `cmk/rds/${name}/encryptionKey`;
|
|
89
|
+
}
|
|
90
|
+
/** KMS alias for a database's Database Insights encryption key. */
|
|
91
|
+
static insightsKeyAlias(name) {
|
|
92
|
+
return `cmk/rds/${name}/InsightsKey`;
|
|
93
|
+
}
|
|
94
|
+
/** KMS alias for a read replica's Database Insights encryption key. */
|
|
95
|
+
static readReplicaInsightsKeyAlias(name) {
|
|
96
|
+
return `cmk/rds/${name}/ReadReplicaInsightsKey`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Construct id of the RDS child the Database pattern creates for an
|
|
100
|
+
* `Instance` or `Aurora` plan. The instance, cluster, proxy and secret
|
|
101
|
+
* helpers are all fed this id, NOT the Database construct's own id, so
|
|
102
|
+
* anything predicting a deployed name from a resource plan starts here.
|
|
103
|
+
*/
|
|
104
|
+
static rdsChildConstructId(databaseName) {
|
|
105
|
+
return `${databaseName}Rds`;
|
|
106
|
+
}
|
|
107
|
+
/** Construct id of the RdsAuroraGlobal child, which names the cluster. */
|
|
108
|
+
static globalRdsChildConstructId(databaseName) {
|
|
109
|
+
return `${databaseName}RdsGlobal`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Construct id of the RdsAurora primary inside RdsAuroraGlobal, which names
|
|
113
|
+
* its instances, proxy and credentials secret.
|
|
114
|
+
*/
|
|
115
|
+
static globalPrimaryConstructId(databaseName) {
|
|
116
|
+
return `${databaseName}Primary`;
|
|
117
|
+
}
|
|
118
|
+
/** Construct id of a database's credentials Secret. */
|
|
119
|
+
static credentialsSecretConstructId(databaseName) {
|
|
120
|
+
return `${databaseName}Credentials`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Alias a Secret or CustomerManagedKey takes when the caller supplies none.
|
|
124
|
+
* CDK prefixes it with `alias/`, which is the form `kms list-aliases`
|
|
125
|
+
* reports.
|
|
126
|
+
*/
|
|
127
|
+
static defaultKeyAlias(constructId) {
|
|
128
|
+
return `cmk/${constructId}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* S3 bucket holding a VPC's flow logs. `id` is the Network construct id —
|
|
132
|
+
* the network name for an additional VPC, `${stackPrefix}Vpc` for the
|
|
133
|
+
* primary. Lowercased rather than kebabed: S3 forbids uppercase and this
|
|
134
|
+
* name is already live on deployed buckets, so the transform cannot change.
|
|
135
|
+
*/
|
|
136
|
+
static vpcFlowLogBucketName(id, accountId) {
|
|
137
|
+
return `vpc-flowlogs-${id.toLowerCase()}-${accountId}`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* ALB physical name for an ECS cluster. ELBv2 caps names at 32 characters
|
|
141
|
+
* and rejects a trailing hyphen, so a long cluster name is truncated and
|
|
142
|
+
* then stripped — which means the name is NOT simply
|
|
143
|
+
* `${clusterName}LoadBalancer` and must not be re-derived as if it were.
|
|
144
|
+
*/
|
|
145
|
+
static loadBalancerName(clusterName) {
|
|
146
|
+
return `${clusterName}LoadBalancer`
|
|
147
|
+
.slice(0, LOAD_BALANCER_NAME_MAX_LENGTH)
|
|
148
|
+
.replace(/-+$/, "");
|
|
149
|
+
}
|
|
76
150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -47,10 +47,15 @@
|
|
|
47
47
|
"test:watch": "vitest",
|
|
48
48
|
"contract:manifest": "FJALL_WRITE_CONTRACT_MANIFEST=1 vitest run lib/__tests__/_contract/contract.test.ts",
|
|
49
49
|
"cdk": "cdk",
|
|
50
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
50
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.deploy-tests.json",
|
|
51
51
|
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
|
|
52
|
+
"typecheck:deploy-tests": "tsc --noEmit -p tsconfig.deploy-tests.json",
|
|
52
53
|
"check:scripts": "tsc --project tsconfig.scripts.json",
|
|
53
54
|
"deploy-test": "tsx deploy-tests/run.mts",
|
|
55
|
+
"deploy-test:ops": "tsx deploy-tests/ops.mts",
|
|
56
|
+
"deploy-test:select": "tsx deploy-tests/select.mts",
|
|
57
|
+
"deploy-test:pipeline": "tsx deploy-tests/pipeline/renderPipelineSteps.mts",
|
|
58
|
+
"deploy-test:pipeline:check": "tsx deploy-tests/pipeline/renderPipelineSteps.mts --check",
|
|
54
59
|
"lint": "eslint lib --no-warn-ignored",
|
|
55
60
|
"lint:fix": "eslint lib --fix --no-warn-ignored",
|
|
56
61
|
"format": "prettier --write \"lib/**/*.{ts,tsx,js,jsx,json}\" \"scripts/**/*.mts\" \"deploy-tests/**/*.mts\"",
|
|
@@ -74,8 +79,8 @@
|
|
|
74
79
|
},
|
|
75
80
|
"dependencies": {
|
|
76
81
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
77
|
-
"@fjall/generator": "^
|
|
78
|
-
"@fjall/util": "^
|
|
82
|
+
"@fjall/generator": "^8.0.0",
|
|
83
|
+
"@fjall/util": "^8.0.0",
|
|
79
84
|
"constructs": "^10.7.2"
|
|
80
85
|
},
|
|
81
86
|
"overrides": {
|