@fjall/components-infrastructure 2.4.8 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/resources/aws/networking/ipamPool.js +21 -4
- package/dist/lib/resources/aws/networking/vpc.js +9 -4
- package/dist/lib/utils/capitaliseString.d.ts +1 -1
- package/dist/lib/utils/capitaliseString.js +1 -1
- package/dist/lib/utils/getAccountId.d.ts +15 -1
- package/dist/lib/utils/getAccountId.js +24 -7
- package/dist/lib/utils/getConfig.js +11 -0
- package/package.json +4 -4
|
@@ -6,6 +6,7 @@ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
|
6
6
|
import { ResourceShare } from "../utilities/resourceShare.js";
|
|
7
7
|
import { CustomResource } from "../utilities/customResource.js";
|
|
8
8
|
import getAccountId from "../../../utils/getAccountId.js";
|
|
9
|
+
import { accountConstructKey, findAccountNameCollision } from "../../../utils/capitaliseString.js";
|
|
9
10
|
import { FjallLogger } from "../../../utils/validationLogger.js";
|
|
10
11
|
const IPAM_TAGS = {
|
|
11
12
|
OPERATIONS_POOL: "fjall:operations:pool",
|
|
@@ -21,6 +22,19 @@ export class IpamPool extends Construct {
|
|
|
21
22
|
if (!regions || regions.length === 0) {
|
|
22
23
|
throw new Error("At least one region must be specified for IPAM pools");
|
|
23
24
|
}
|
|
25
|
+
// Reject name collisions here — the single convergence point all callers
|
|
26
|
+
// reach (Platform's props.accounts, IpamPoolStack, direct instantiation).
|
|
27
|
+
// Two names with the same construct key mint duplicate construct IDs and
|
|
28
|
+
// crash synth with a cryptic CDK message; an empty key has no construct
|
|
29
|
+
// identity. Scan the original (pre-lowercase) names so the message keeps
|
|
30
|
+
// the user's casing — findAccountNameCollision normalises internally.
|
|
31
|
+
const collision = findAccountNameCollision(props.orgAccounts);
|
|
32
|
+
if (collision) {
|
|
33
|
+
if (collision.kind === "empty-construct-key") {
|
|
34
|
+
throw new Error(`IpamPool: org account name "${collision.name}" normalises to an empty construct key — account names must contain at least one alphanumeric character`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`IpamPool: org account names "${collision.existingName}" and "${collision.name}" collide on construct key "${collision.constructKey}" — account names must be unique ignoring case and separators`);
|
|
37
|
+
}
|
|
24
38
|
const organisationAccounts = props.orgAccounts.map((account) => account.toLowerCase());
|
|
25
39
|
// Determine the child-pool CIDR size (defaults to /20)
|
|
26
40
|
const defaultNetmaskLength = props.allocationDefaultNetmaskLength ?? 20;
|
|
@@ -107,7 +121,10 @@ export class IpamPool extends Construct {
|
|
|
107
121
|
let previousAccountCidr;
|
|
108
122
|
for (const account of organisationAccounts) {
|
|
109
123
|
const accountId = getAccountId(account);
|
|
110
|
-
|
|
124
|
+
// Construct IDs only: strips separators so case/format variants can't mint
|
|
125
|
+
// distinct constructs. `account` still feeds tags, descriptions, physical names.
|
|
126
|
+
const constructKey = accountConstructKey(account);
|
|
127
|
+
const ipamPool = new IpamPoolClass(this, `${constructKey}IpamPool${regionSuffix}`, {
|
|
111
128
|
description: `${account} - IPAM pool - ${region}`,
|
|
112
129
|
addressFamily: "ipv4",
|
|
113
130
|
ipamScopeId,
|
|
@@ -138,14 +155,14 @@ export class IpamPool extends Construct {
|
|
|
138
155
|
if (previousAccountCidr) {
|
|
139
156
|
ipamPool.addDependency(previousAccountCidr);
|
|
140
157
|
}
|
|
141
|
-
const cidrAllocation = new CfnIPAMPoolCidr(this, `${
|
|
158
|
+
const cidrAllocation = new CfnIPAMPoolCidr(this, `${constructKey}PoolCidr${provisionedNetmaskLength}${regionSuffix}`, {
|
|
142
159
|
ipamPoolId: ipamPool.attrIpamPoolId,
|
|
143
160
|
netmaskLength: provisionedNetmaskLength
|
|
144
161
|
});
|
|
145
162
|
previousAccountCidr = cidrAllocation;
|
|
146
163
|
// On stack deletion, VPC allocations (not managed by CloudFormation)
|
|
147
164
|
// block CIDR deprovisioning. This custom resource releases them first.
|
|
148
|
-
const allocationCleanup = new CustomResource(this, `${
|
|
165
|
+
const allocationCleanup = new CustomResource(this, `${constructKey}IpamCleanup${regionSuffix}`, {
|
|
149
166
|
runtime: Runtime.NODEJS_22_X,
|
|
150
167
|
timeout: Duration.minutes(5),
|
|
151
168
|
lambdaDescription: `${account} IPAM pool allocation cleanup - ${region}`,
|
|
@@ -204,7 +221,7 @@ exports.handler = async (event) => {
|
|
|
204
221
|
});
|
|
205
222
|
allocationCleanup.node.addDependency(cidrAllocation);
|
|
206
223
|
// Share the pool with the target account so that VPCs can allocate
|
|
207
|
-
const share = new ResourceShare(this, `${
|
|
224
|
+
const share = new ResourceShare(this, `${constructKey}IpamResourceShare${regionSuffix}`, {
|
|
208
225
|
name: `${account}IpamResourceShare.${region}`,
|
|
209
226
|
allowExternalPrincipals: false,
|
|
210
227
|
principals: [accountId],
|
|
@@ -215,13 +215,18 @@ export class Vpc extends ec2.Vpc {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
static ipAddresses(scope, id, props) {
|
|
218
|
-
|
|
218
|
+
// A non-empty pool id is the SOLE signal of IPAM intent: do NOT gate on
|
|
219
|
+
// accountId (awsIpamAllocation never consumes it), or an IPAM-intended VPC
|
|
220
|
+
// whose ambient accountId failed to resolve silently takes CDK's default
|
|
221
|
+
// 10.0.0.0/16 and collides with its org siblings. No pool id = standalone.
|
|
222
|
+
const poolId = props?.ipv4IpamPoolId;
|
|
223
|
+
if (poolId === undefined || poolId === "") {
|
|
219
224
|
return undefined;
|
|
220
225
|
}
|
|
221
|
-
const vpcCidrMask = props
|
|
222
|
-
const subnetCidrMask = props
|
|
226
|
+
const vpcCidrMask = props?.vpcCidrMask ?? 20;
|
|
227
|
+
const subnetCidrMask = props?.subnetCidrMask ?? 23;
|
|
223
228
|
return ec2.IpAddresses.awsIpamAllocation({
|
|
224
|
-
ipv4IpamPoolId:
|
|
229
|
+
ipv4IpamPoolId: poolId,
|
|
225
230
|
ipv4NetmaskLength: vpcCidrMask,
|
|
226
231
|
defaultSubnetIpv4NetmaskLength: subnetCidrMask
|
|
227
232
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, getSafeZoneName } from "@fjall/util";
|
|
1
|
+
export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, getSafeZoneName, accountConstructKey, findAccountNameCollision, type AccountNameCollision } from "@fjall/util";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, getSafeZoneName } from "@fjall/util";
|
|
1
|
+
export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, getSafeZoneName, accountConstructKey, findAccountNameCollision } from "@fjall/util";
|
|
@@ -1 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an account name to its AWS account ID via the org config's
|
|
3
|
+
* name→id map. Matched via `accountConstructKey` (case-insensitive, strips
|
|
4
|
+
* separators too), because provider account names arrive verbatim from AWS
|
|
5
|
+
* Organizations (TitleCase) through reconciliation while callers (e.g. IpamPool)
|
|
6
|
+
* normalise them for construct-ID safety. Matching case-sensitively here would
|
|
7
|
+
* miss every account and silently fall back to the ambient context accountId —
|
|
8
|
+
* producing colliding `IpamPoolId<accountId>` outputs. Collisions across config
|
|
9
|
+
* names are rejected upstream in getConfig, so the normalised key is unambiguous.
|
|
10
|
+
*
|
|
11
|
+
* Throws on no-match rather than returning a sentinel: an unresolvable account
|
|
12
|
+
* named in the org config is a configuration error, and a sentinel id would
|
|
13
|
+
* itself collide across multiple unresolved names.
|
|
14
|
+
*/
|
|
15
|
+
export default function getAccountId(accountName: string): string;
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { getConfig } from "./getConfig.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { accountConstructKey } from "./capitaliseString.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an account name to its AWS account ID via the org config's
|
|
5
|
+
* name→id map. Matched via `accountConstructKey` (case-insensitive, strips
|
|
6
|
+
* separators too), because provider account names arrive verbatim from AWS
|
|
7
|
+
* Organizations (TitleCase) through reconciliation while callers (e.g. IpamPool)
|
|
8
|
+
* normalise them for construct-ID safety. Matching case-sensitively here would
|
|
9
|
+
* miss every account and silently fall back to the ambient context accountId —
|
|
10
|
+
* producing colliding `IpamPoolId<accountId>` outputs. Collisions across config
|
|
11
|
+
* names are rejected upstream in getConfig, so the normalised key is unambiguous.
|
|
12
|
+
*
|
|
13
|
+
* Throws on no-match rather than returning a sentinel: an unresolvable account
|
|
14
|
+
* named in the org config is a configuration error, and a sentinel id would
|
|
15
|
+
* itself collide across multiple unresolved names.
|
|
16
|
+
*/
|
|
17
|
+
export default function getAccountId(accountName) {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
const target = accountConstructKey(accountName);
|
|
20
|
+
for (const [name, id] of Object.entries(config.accountIds ?? {})) {
|
|
21
|
+
if (accountConstructKey(name) === target)
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
const known = Object.keys(config.accountIds ?? {}).join(", ") || "(none)";
|
|
25
|
+
throw new Error(`getAccountId: no provider account named "${accountName}" in org config — known accounts: ${known}`);
|
|
9
26
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import App from "../app.js";
|
|
2
2
|
import { parseOrgConfig } from "./orgConfigParser.js";
|
|
3
|
+
import { findAccountNameCollision } from "./capitaliseString.js";
|
|
3
4
|
import { FjallLogger } from "./validationLogger.js";
|
|
4
5
|
export function getConfig(accountName) {
|
|
5
6
|
const app = App.getInstance();
|
|
@@ -32,6 +33,16 @@ export function getConfig(accountName) {
|
|
|
32
33
|
if (config.disasterRecoveryRegion)
|
|
33
34
|
regions.add(config.disasterRecoveryRegion);
|
|
34
35
|
config.allRegions = [...regions];
|
|
36
|
+
// Reject name collisions here, before synth turns them into a cryptic
|
|
37
|
+
// duplicate-construct-ID crash. Two names with the same construct key are the
|
|
38
|
+
// same account; an empty key has no construct identity.
|
|
39
|
+
const collision = findAccountNameCollision(providerAccounts.map((pa) => pa.name));
|
|
40
|
+
if (collision) {
|
|
41
|
+
if (collision.kind === "empty-construct-key") {
|
|
42
|
+
throw new Error(`getConfig: provider account name "${collision.name}" normalises to an empty construct key — account names must contain at least one alphanumeric character`);
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`getConfig: provider account names "${collision.existingName}" and "${collision.name}" collide on construct key "${collision.constructKey}" — account names must be unique ignoring case and separators`);
|
|
45
|
+
}
|
|
35
46
|
// Build account IDs map from provider accounts (name → id)
|
|
36
47
|
if (providerAccounts.length > 0) {
|
|
37
48
|
config.accountIds = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
66
|
-
"@fjall/generator": "^2.
|
|
67
|
-
"@fjall/util": "^2.
|
|
66
|
+
"@fjall/generator": "^2.6.0",
|
|
67
|
+
"@fjall/util": "^2.6.0",
|
|
68
68
|
"constructs": "^10.0.0",
|
|
69
69
|
"uuid": "^14.0.0"
|
|
70
70
|
},
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=18.0.0"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "93666ff94b8b1d0e360a7710e9266d275d15ee34"
|
|
83
83
|
}
|