@beesolve/aws-accounts 1.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/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/accountCreation.js +135 -0
- package/dist/applyLogic.js +1203 -0
- package/dist/awsClientConfig.js +26 -0
- package/dist/awsConfig.js +1365 -0
- package/dist/cli.js +201 -0
- package/dist/commands/graveyard.js +46 -0
- package/dist/commands/regenerate.js +17 -0
- package/dist/commands/remote.js +925 -0
- package/dist/diff.js +1012 -0
- package/dist/error.js +66 -0
- package/dist/helpers.js +21 -0
- package/dist/lambda/handler.js +375 -0
- package/dist/lambdaClient.js +220 -0
- package/dist/logger.js +26 -0
- package/dist/operations.js +218 -0
- package/dist/remoteStateCache.js +38 -0
- package/dist/reservedOuDeletion.js +46 -0
- package/dist/scanLogic.js +456 -0
- package/dist/state.js +618 -0
- package/dist/tags.js +14 -0
- package/dist-lambda/handler.mjs +3558 -0
- package/dist-lambda/lambda.zip +0 -0
- package/package.json +59 -0
|
@@ -0,0 +1,1365 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { build as esbuildBuild } from "esbuild";
|
|
6
|
+
import * as v from "valibot";
|
|
7
|
+
import {
|
|
8
|
+
assertIamPolicyDocument,
|
|
9
|
+
iamActionCatalog,
|
|
10
|
+
iamPolicyDocumentSchema
|
|
11
|
+
} from "@beesolve/iam-policy-ts";
|
|
12
|
+
import {
|
|
13
|
+
createAccessRoleName,
|
|
14
|
+
readStateFile,
|
|
15
|
+
validateState
|
|
16
|
+
} from "./state.js";
|
|
17
|
+
import { assertUnreachable, toRecordByProperty } from "./helpers.js";
|
|
18
|
+
const nonEmptyString = v.pipe(v.string(), v.nonEmpty());
|
|
19
|
+
const pendingCreationId = "__pending_creation__";
|
|
20
|
+
function resolveAccountStateMatchForConfigEntry(props) {
|
|
21
|
+
const matchedByName = props.accountByName[props.account.name];
|
|
22
|
+
if (matchedByName != null) {
|
|
23
|
+
return matchedByName;
|
|
24
|
+
}
|
|
25
|
+
const emailMatches = props.accounts.filter(
|
|
26
|
+
(candidate) => candidate.email === props.account.email
|
|
27
|
+
);
|
|
28
|
+
if (emailMatches.length > 1) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Cannot map config account "${props.account.name}": multiple member accounts use email "${props.account.email}".`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return emailMatches[0];
|
|
34
|
+
}
|
|
35
|
+
const deploymentSchema = v.strictObject({
|
|
36
|
+
profile: v.string(),
|
|
37
|
+
region: v.string(),
|
|
38
|
+
lambdaArn: v.string(),
|
|
39
|
+
stateBucketName: v.string(),
|
|
40
|
+
stateCacheTtlSeconds: v.number()
|
|
41
|
+
});
|
|
42
|
+
const awsContextSchema = v.strictObject({
|
|
43
|
+
version: nonEmptyString,
|
|
44
|
+
generatedAt: nonEmptyString,
|
|
45
|
+
organization: v.strictObject({
|
|
46
|
+
managementAccountId: nonEmptyString,
|
|
47
|
+
rootId: nonEmptyString,
|
|
48
|
+
graveyardOuId: nonEmptyString
|
|
49
|
+
}),
|
|
50
|
+
identityCenter: v.strictObject({
|
|
51
|
+
instanceArn: nonEmptyString,
|
|
52
|
+
identityStoreId: nonEmptyString
|
|
53
|
+
}),
|
|
54
|
+
deployment: v.optional(deploymentSchema)
|
|
55
|
+
});
|
|
56
|
+
const awsConfigModelSchema = v.strictObject({
|
|
57
|
+
organizationalUnits: v.array(
|
|
58
|
+
v.strictObject({
|
|
59
|
+
name: v.string(),
|
|
60
|
+
parentName: v.nullable(v.string()),
|
|
61
|
+
accounts: v.array(
|
|
62
|
+
v.strictObject({
|
|
63
|
+
name: v.string(),
|
|
64
|
+
email: v.string(),
|
|
65
|
+
tags: v.array(
|
|
66
|
+
v.strictObject({
|
|
67
|
+
key: v.string(),
|
|
68
|
+
value: v.string()
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
),
|
|
75
|
+
users: v.array(
|
|
76
|
+
v.strictObject({
|
|
77
|
+
userName: v.string(),
|
|
78
|
+
displayName: v.string(),
|
|
79
|
+
email: v.string()
|
|
80
|
+
})
|
|
81
|
+
),
|
|
82
|
+
groups: v.array(
|
|
83
|
+
v.strictObject({
|
|
84
|
+
displayName: v.string(),
|
|
85
|
+
description: v.optional(v.string()),
|
|
86
|
+
members: v.array(v.string())
|
|
87
|
+
})
|
|
88
|
+
),
|
|
89
|
+
permissionSets: v.array(
|
|
90
|
+
v.strictObject({
|
|
91
|
+
name: v.string(),
|
|
92
|
+
description: v.string(),
|
|
93
|
+
inlinePolicy: v.optional(iamPolicyDocumentSchema),
|
|
94
|
+
awsManagedPolicies: v.array(v.string()),
|
|
95
|
+
customerManagedPolicies: v.array(
|
|
96
|
+
v.strictObject({
|
|
97
|
+
name: v.string(),
|
|
98
|
+
path: v.string()
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
),
|
|
103
|
+
assignments: v.array(
|
|
104
|
+
v.strictObject({
|
|
105
|
+
permissionSet: v.string(),
|
|
106
|
+
group: v.optional(v.string()),
|
|
107
|
+
user: v.optional(v.string()),
|
|
108
|
+
accounts: v.array(v.string())
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
});
|
|
112
|
+
const moduleDirectoryPath = resolve(
|
|
113
|
+
fileURLToPath(new URL(".", import.meta.url))
|
|
114
|
+
);
|
|
115
|
+
const projectRootPath = resolve(moduleDirectoryPath, "..");
|
|
116
|
+
async function writeAwsConfigFromState(props) {
|
|
117
|
+
const state = await readStateFile(props.statePath);
|
|
118
|
+
const context = await readAwsContextFile(props.contextPath);
|
|
119
|
+
assertStateMatchesContext({
|
|
120
|
+
state,
|
|
121
|
+
context
|
|
122
|
+
});
|
|
123
|
+
const mappedConfig = mapStateToAwsConfig({
|
|
124
|
+
state
|
|
125
|
+
});
|
|
126
|
+
const sortedConfig = sortAwsConfigModel({
|
|
127
|
+
config: mappedConfig
|
|
128
|
+
});
|
|
129
|
+
const nextConfigContent = renderAwsConfigTs({
|
|
130
|
+
config: sortedConfig
|
|
131
|
+
});
|
|
132
|
+
const nextTypesContent = renderAwsConfigTypesTs({
|
|
133
|
+
config: sortedConfig
|
|
134
|
+
});
|
|
135
|
+
const [currentConfigContent, currentTypesContent] = await Promise.all([
|
|
136
|
+
readIfExists(props.configPath),
|
|
137
|
+
readIfExists(props.typesPath)
|
|
138
|
+
]);
|
|
139
|
+
const changedFiles = [];
|
|
140
|
+
if (currentConfigContent !== nextConfigContent) {
|
|
141
|
+
changedFiles.push({
|
|
142
|
+
path: props.configPath,
|
|
143
|
+
previousBytes: Buffer.byteLength(currentConfigContent ?? "", "utf8"),
|
|
144
|
+
nextBytes: Buffer.byteLength(nextConfigContent, "utf8"),
|
|
145
|
+
content: nextConfigContent
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (currentTypesContent !== nextTypesContent) {
|
|
149
|
+
changedFiles.push({
|
|
150
|
+
path: props.typesPath,
|
|
151
|
+
previousBytes: Buffer.byteLength(currentTypesContent ?? "", "utf8"),
|
|
152
|
+
nextBytes: Buffer.byteLength(nextTypesContent, "utf8"),
|
|
153
|
+
content: nextTypesContent
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (changedFiles.length === 0) {
|
|
157
|
+
props.logger.log("No changes.");
|
|
158
|
+
return {
|
|
159
|
+
configPath: props.configPath,
|
|
160
|
+
typesPath: props.typesPath,
|
|
161
|
+
files: [
|
|
162
|
+
{
|
|
163
|
+
path: props.configPath,
|
|
164
|
+
status: "unchanged"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
path: props.typesPath,
|
|
168
|
+
status: "unchanged"
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const fileSummaries = changedFiles.map(
|
|
174
|
+
(file) => `${file.path}: ${file.previousBytes} -> ${file.nextBytes} bytes`
|
|
175
|
+
);
|
|
176
|
+
for (const fileSummary of fileSummaries) {
|
|
177
|
+
props.logger.log(fileSummary);
|
|
178
|
+
}
|
|
179
|
+
props.logger.log(
|
|
180
|
+
`Review with: git diff ${props.configPath} ${props.typesPath}`
|
|
181
|
+
);
|
|
182
|
+
const shouldWrite = await props.overwriteConfirmation({
|
|
183
|
+
fileSummaries
|
|
184
|
+
});
|
|
185
|
+
if (!shouldWrite) {
|
|
186
|
+
props.logger.log("Config write cancelled.");
|
|
187
|
+
return {
|
|
188
|
+
configPath: props.configPath,
|
|
189
|
+
typesPath: props.typesPath,
|
|
190
|
+
files: [
|
|
191
|
+
{
|
|
192
|
+
path: props.configPath,
|
|
193
|
+
status: currentConfigContent === nextConfigContent ? "unchanged" : "would-write"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
path: props.typesPath,
|
|
197
|
+
status: currentTypesContent === nextTypesContent ? "unchanged" : "would-write"
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
await Promise.all(
|
|
203
|
+
changedFiles.map((file) => writeFile(file.path, file.content, "utf8"))
|
|
204
|
+
);
|
|
205
|
+
return {
|
|
206
|
+
configPath: props.configPath,
|
|
207
|
+
typesPath: props.typesPath,
|
|
208
|
+
files: [
|
|
209
|
+
{
|
|
210
|
+
path: props.configPath,
|
|
211
|
+
status: currentConfigContent === nextConfigContent ? "unchanged" : "written"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
path: props.typesPath,
|
|
215
|
+
status: currentTypesContent === nextTypesContent ? "unchanged" : "written"
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function regenerateAwsConfigTypes(props) {
|
|
221
|
+
const typesModule = await loadAwsConfigTypesModule({
|
|
222
|
+
typesPath: props.typesPath
|
|
223
|
+
});
|
|
224
|
+
const loadedConfig = await loadAwsConfigFromTsFile({
|
|
225
|
+
configPath: props.configPath,
|
|
226
|
+
schema: typesModule.awsConfigSchema
|
|
227
|
+
});
|
|
228
|
+
const sortedConfig = sortAwsConfigModel({
|
|
229
|
+
config: loadedConfig
|
|
230
|
+
});
|
|
231
|
+
const nextTypesContent = renderAwsConfigTypesTs({
|
|
232
|
+
config: sortedConfig
|
|
233
|
+
});
|
|
234
|
+
const currentTypesContent = await readIfExists(props.typesPath);
|
|
235
|
+
if (currentTypesContent === nextTypesContent) {
|
|
236
|
+
props.logger.log("No changes.");
|
|
237
|
+
return {
|
|
238
|
+
typesPath: props.typesPath,
|
|
239
|
+
changed: false,
|
|
240
|
+
files: [
|
|
241
|
+
{
|
|
242
|
+
path: props.typesPath,
|
|
243
|
+
status: "unchanged"
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const fileSummary = `${props.typesPath}: ${Buffer.byteLength(currentTypesContent ?? "", "utf8")} -> ${Buffer.byteLength(nextTypesContent, "utf8")} bytes`;
|
|
249
|
+
props.logger.log(fileSummary);
|
|
250
|
+
props.logger.log(`Review with: git diff ${props.typesPath}`);
|
|
251
|
+
const shouldWrite = await props.overwriteConfirmation({
|
|
252
|
+
fileSummaries: [fileSummary]
|
|
253
|
+
});
|
|
254
|
+
if (!shouldWrite) {
|
|
255
|
+
props.logger.log("Types write cancelled.");
|
|
256
|
+
return {
|
|
257
|
+
typesPath: props.typesPath,
|
|
258
|
+
changed: false,
|
|
259
|
+
files: [
|
|
260
|
+
{
|
|
261
|
+
path: props.typesPath,
|
|
262
|
+
status: "would-write"
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
await writeFile(props.typesPath, nextTypesContent, "utf8");
|
|
268
|
+
return {
|
|
269
|
+
typesPath: props.typesPath,
|
|
270
|
+
changed: true,
|
|
271
|
+
files: [
|
|
272
|
+
{
|
|
273
|
+
path: props.typesPath,
|
|
274
|
+
status: "written"
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function mapStateToAwsConfig(props) {
|
|
280
|
+
const organizationalUnits = [
|
|
281
|
+
{
|
|
282
|
+
name: "root",
|
|
283
|
+
parentName: null,
|
|
284
|
+
accounts: []
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
const organizationalUnitById = toRecordByProperty(
|
|
288
|
+
props.state.organization.organizationalUnits,
|
|
289
|
+
"id"
|
|
290
|
+
);
|
|
291
|
+
for (const organizationalUnit of props.state.organization.organizationalUnits) {
|
|
292
|
+
if (organizationalUnit.name === "Graveyard") {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const parentName = organizationalUnit.parentId === props.state.organization.rootId ? "root" : organizationalUnitById[organizationalUnit.parentId]?.name;
|
|
296
|
+
if (parentName == null) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`Organizational unit "${organizationalUnit.name}" has unknown parentId "${organizationalUnit.parentId}".`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
organizationalUnits.push({
|
|
302
|
+
name: organizationalUnit.name,
|
|
303
|
+
parentName,
|
|
304
|
+
accounts: []
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const organizationalUnitByName = toRecordByProperty(
|
|
308
|
+
organizationalUnits,
|
|
309
|
+
"name"
|
|
310
|
+
);
|
|
311
|
+
const graveyardOrganizationalUnit = props.state.organization.organizationalUnits.find(
|
|
312
|
+
(organizationalUnit) => organizationalUnit.name === "Graveyard"
|
|
313
|
+
);
|
|
314
|
+
const graveyardOrganizationalUnitId = graveyardOrganizationalUnit?.id;
|
|
315
|
+
for (const account of props.state.organization.accounts) {
|
|
316
|
+
const ownerOuName = account.parentId === props.state.organization.rootId ? "root" : organizationalUnitById[account.parentId]?.name;
|
|
317
|
+
if (ownerOuName == null) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Account "${account.name}" has unknown parentId "${account.parentId}".`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (ownerOuName === "Graveyard") {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const ownerOu = organizationalUnitByName[ownerOuName];
|
|
326
|
+
if (ownerOu == null) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Could not map account "${account.name}" to organizational unit "${ownerOuName}".`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
ownerOu.accounts.push({
|
|
332
|
+
name: account.name,
|
|
333
|
+
email: account.email,
|
|
334
|
+
tags: account.tags ?? []
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const permissionSetByArn = toRecordByProperty(
|
|
338
|
+
props.state.identityCenter.permissionSets,
|
|
339
|
+
"permissionSetArn"
|
|
340
|
+
);
|
|
341
|
+
const groupById = toRecordByProperty(
|
|
342
|
+
props.state.identityCenter.groups,
|
|
343
|
+
"groupId"
|
|
344
|
+
);
|
|
345
|
+
const userById = toRecordByProperty(
|
|
346
|
+
props.state.identityCenter.users,
|
|
347
|
+
"userId"
|
|
348
|
+
);
|
|
349
|
+
const accountById = toRecordByProperty(
|
|
350
|
+
props.state.organization.accounts,
|
|
351
|
+
"id"
|
|
352
|
+
);
|
|
353
|
+
const membersByGroupDisplayName = new Map(
|
|
354
|
+
props.state.identityCenter.groups.map((group) => [
|
|
355
|
+
group.displayName,
|
|
356
|
+
[]
|
|
357
|
+
])
|
|
358
|
+
);
|
|
359
|
+
const assignmentsByKey = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const assignment of props.state.identityCenter.accountAssignments) {
|
|
361
|
+
const permissionSetName = permissionSetByArn[assignment.permissionSetArn]?.name;
|
|
362
|
+
if (permissionSetName == null) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Could not resolve permission set name for assignment permissionSetArn "${assignment.permissionSetArn}".`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const accountName = accountById[assignment.accountId]?.name;
|
|
368
|
+
if (accountName == null) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Could not resolve account name for assignment accountId "${assignment.accountId}".`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const accountParentId = accountById[assignment.accountId]?.parentId;
|
|
374
|
+
if (graveyardOrganizationalUnitId != null && accountParentId === graveyardOrganizationalUnitId) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const principal = mapAssignmentPrincipal({
|
|
378
|
+
assignment,
|
|
379
|
+
groupById,
|
|
380
|
+
userById
|
|
381
|
+
});
|
|
382
|
+
const assignmentKey = `${principal.kind}:${principal.value}|${permissionSetName}`;
|
|
383
|
+
const existingAssignment = assignmentsByKey.get(assignmentKey);
|
|
384
|
+
if (existingAssignment == null) {
|
|
385
|
+
assignmentsByKey.set(assignmentKey, {
|
|
386
|
+
permissionSet: permissionSetName,
|
|
387
|
+
group: principal.kind === "group" ? principal.value : void 0,
|
|
388
|
+
user: principal.kind === "user" ? principal.value : void 0,
|
|
389
|
+
accounts: [accountName]
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (existingAssignment.accounts.includes(accountName) === false) {
|
|
394
|
+
existingAssignment.accounts.push(accountName);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const groupMembership of props.state.identityCenter.groupMemberships) {
|
|
398
|
+
const groupDisplayName = groupById[groupMembership.groupId]?.displayName;
|
|
399
|
+
if (groupDisplayName == null) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`Could not resolve group display name for membership groupId "${groupMembership.groupId}".`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const userName = userById[groupMembership.userId]?.userName;
|
|
405
|
+
if (userName == null) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Could not resolve user name for membership userId "${groupMembership.userId}".`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const members = membersByGroupDisplayName.get(groupDisplayName);
|
|
411
|
+
if (members == null) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`Could not map membership for group "${groupDisplayName}".`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (members.includes(userName) === false) {
|
|
417
|
+
members.push(userName);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const mapped = {
|
|
421
|
+
organizationalUnits,
|
|
422
|
+
users: props.state.identityCenter.users.map((user) => ({
|
|
423
|
+
userName: user.userName,
|
|
424
|
+
displayName: user.displayName,
|
|
425
|
+
email: user.email
|
|
426
|
+
})),
|
|
427
|
+
groups: props.state.identityCenter.groups.map((group) => ({
|
|
428
|
+
displayName: group.displayName,
|
|
429
|
+
description: group.description ?? "",
|
|
430
|
+
members: membersByGroupDisplayName.get(group.displayName) ?? []
|
|
431
|
+
})),
|
|
432
|
+
permissionSets: props.state.identityCenter.permissionSets.map(
|
|
433
|
+
(permissionSet) => ({
|
|
434
|
+
name: permissionSet.name,
|
|
435
|
+
description: permissionSet.description,
|
|
436
|
+
inlinePolicy: permissionSet.inlinePolicy == null ? void 0 : parseInlinePolicyForConfig({
|
|
437
|
+
permissionSetName: permissionSet.name,
|
|
438
|
+
inlinePolicy: permissionSet.inlinePolicy
|
|
439
|
+
}),
|
|
440
|
+
awsManagedPolicies: [...permissionSet.awsManagedPolicies],
|
|
441
|
+
customerManagedPolicies: permissionSet.customerManagedPolicies.map(
|
|
442
|
+
(customerManagedPolicy) => ({
|
|
443
|
+
name: customerManagedPolicy.name,
|
|
444
|
+
path: customerManagedPolicy.path
|
|
445
|
+
})
|
|
446
|
+
)
|
|
447
|
+
})
|
|
448
|
+
),
|
|
449
|
+
assignments: [...assignmentsByKey.values()]
|
|
450
|
+
};
|
|
451
|
+
assertUniqueNames({
|
|
452
|
+
values: mapped.organizationalUnits.map((ou) => ou.name),
|
|
453
|
+
entityName: "organizational unit"
|
|
454
|
+
});
|
|
455
|
+
assertUniqueNames({
|
|
456
|
+
values: mapped.organizationalUnits.flatMap(
|
|
457
|
+
(ou) => ou.accounts.map((account) => account.name)
|
|
458
|
+
),
|
|
459
|
+
entityName: "account"
|
|
460
|
+
});
|
|
461
|
+
assertUniqueNames({
|
|
462
|
+
values: mapped.groups.map((group) => group.displayName),
|
|
463
|
+
entityName: "group"
|
|
464
|
+
});
|
|
465
|
+
assertUniqueNames({
|
|
466
|
+
values: mapped.users.map((user) => user.userName),
|
|
467
|
+
entityName: "user"
|
|
468
|
+
});
|
|
469
|
+
assertUniqueNames({
|
|
470
|
+
values: mapped.permissionSets.map((permissionSet) => permissionSet.name),
|
|
471
|
+
entityName: "permission set"
|
|
472
|
+
});
|
|
473
|
+
return v.parse(awsConfigModelSchema, mapped);
|
|
474
|
+
}
|
|
475
|
+
function mapAwsConfigToState(props) {
|
|
476
|
+
const organizationalUnitByName = toRecordByProperty(
|
|
477
|
+
props.currentState.organization.organizationalUnits,
|
|
478
|
+
"name"
|
|
479
|
+
);
|
|
480
|
+
const accountByName = toRecordByProperty(
|
|
481
|
+
props.currentState.organization.accounts,
|
|
482
|
+
"name"
|
|
483
|
+
);
|
|
484
|
+
const userByUserName = toRecordByProperty(
|
|
485
|
+
props.currentState.identityCenter.users,
|
|
486
|
+
"userName"
|
|
487
|
+
);
|
|
488
|
+
const userById = toRecordByProperty(
|
|
489
|
+
props.currentState.identityCenter.users,
|
|
490
|
+
"userId"
|
|
491
|
+
);
|
|
492
|
+
const groupByDisplayName = toRecordByProperty(
|
|
493
|
+
props.currentState.identityCenter.groups,
|
|
494
|
+
"displayName"
|
|
495
|
+
);
|
|
496
|
+
const groupById = toRecordByProperty(
|
|
497
|
+
props.currentState.identityCenter.groups,
|
|
498
|
+
"groupId"
|
|
499
|
+
);
|
|
500
|
+
const groupMembershipByNameKey = toRecordByProperty(
|
|
501
|
+
props.currentState.identityCenter.groupMemberships,
|
|
502
|
+
(groupMembership) => {
|
|
503
|
+
const currentGroup = groupById[groupMembership.groupId];
|
|
504
|
+
if (currentGroup == null) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Could not resolve current group for membership groupId "${groupMembership.groupId}".`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const currentUser = userById[groupMembership.userId];
|
|
510
|
+
if (currentUser == null) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Could not resolve current user for membership userId "${groupMembership.userId}".`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return createGroupMembershipNameKey({
|
|
516
|
+
groupDisplayName: currentGroup.displayName,
|
|
517
|
+
userName: currentUser.userName
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
const permissionSetByName = toRecordByProperty(
|
|
522
|
+
props.currentState.identityCenter.permissionSets,
|
|
523
|
+
"name"
|
|
524
|
+
);
|
|
525
|
+
const configOrganizationalUnitNameSet = new Set(
|
|
526
|
+
props.config.organizationalUnits.map(
|
|
527
|
+
(organizationalUnit) => organizationalUnit.name
|
|
528
|
+
)
|
|
529
|
+
);
|
|
530
|
+
const mappedOrganizationalUnitIdByName = /* @__PURE__ */ new Map();
|
|
531
|
+
for (const organizationalUnit of props.config.organizationalUnits) {
|
|
532
|
+
if (organizationalUnit.name !== "root" && organizationalUnit.parentName != null && configOrganizationalUnitNameSet.has(organizationalUnit.parentName) === false) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Organizational unit "${organizationalUnit.name}" references unknown parentName "${organizationalUnit.parentName}".`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const mappedId = resolveOrganizationalUnitId({
|
|
538
|
+
organizationalUnitName: organizationalUnit.name,
|
|
539
|
+
matchedOrganizationalUnit: organizationalUnitByName[organizationalUnit.name],
|
|
540
|
+
context: props.context
|
|
541
|
+
});
|
|
542
|
+
mappedOrganizationalUnitIdByName.set(organizationalUnit.name, mappedId);
|
|
543
|
+
}
|
|
544
|
+
const mappedOrganizationalUnits = [];
|
|
545
|
+
for (const organizationalUnit of props.config.organizationalUnits) {
|
|
546
|
+
if (organizationalUnit.name === "root") {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const mappedId = mappedOrganizationalUnitIdByName.get(
|
|
550
|
+
organizationalUnit.name
|
|
551
|
+
);
|
|
552
|
+
if (mappedId == null) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Could not resolve mapped id for organizational unit "${organizationalUnit.name}".`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const parentId = organizationalUnit.parentName == null ? props.context.organization.rootId : mappedOrganizationalUnitIdByName.get(
|
|
558
|
+
organizationalUnit.parentName
|
|
559
|
+
) ?? pendingCreationId;
|
|
560
|
+
const matchedOrganizationalUnit = organizationalUnitByName[organizationalUnit.name];
|
|
561
|
+
mappedOrganizationalUnits.push({
|
|
562
|
+
id: mappedId,
|
|
563
|
+
parentId,
|
|
564
|
+
arn: matchedOrganizationalUnit?.arn ?? pendingCreationId,
|
|
565
|
+
name: organizationalUnit.name
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
for (const managedOrganizationalUnitName of ["Graveyard"]) {
|
|
569
|
+
const managedOuId = resolveOrganizationalUnitId({
|
|
570
|
+
organizationalUnitName: managedOrganizationalUnitName,
|
|
571
|
+
matchedOrganizationalUnit: organizationalUnitByName[managedOrganizationalUnitName],
|
|
572
|
+
context: props.context
|
|
573
|
+
});
|
|
574
|
+
mappedOrganizationalUnitIdByName.set(
|
|
575
|
+
managedOrganizationalUnitName,
|
|
576
|
+
managedOuId
|
|
577
|
+
);
|
|
578
|
+
if (mappedOrganizationalUnits.some(
|
|
579
|
+
(organizationalUnit) => organizationalUnit.id === managedOuId
|
|
580
|
+
)) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const matchedManagedOrganizationalUnit = organizationalUnitByName[managedOrganizationalUnitName];
|
|
584
|
+
mappedOrganizationalUnits.push({
|
|
585
|
+
id: managedOuId,
|
|
586
|
+
parentId: props.context.organization.rootId,
|
|
587
|
+
arn: matchedManagedOrganizationalUnit?.arn ?? pendingCreationId,
|
|
588
|
+
name: managedOrganizationalUnitName
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
const mappedAccountIdByName = /* @__PURE__ */ new Map();
|
|
592
|
+
const mappedAccounts = [];
|
|
593
|
+
for (const organizationalUnit of props.config.organizationalUnits) {
|
|
594
|
+
const ownerParentId = mappedOrganizationalUnitIdByName.get(
|
|
595
|
+
organizationalUnit.name
|
|
596
|
+
);
|
|
597
|
+
if (ownerParentId == null) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`Could not resolve mapped parent id for organizational unit "${organizationalUnit.name}".`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
for (const account of organizationalUnit.accounts) {
|
|
603
|
+
const matchedAccount = resolveAccountStateMatchForConfigEntry({
|
|
604
|
+
account,
|
|
605
|
+
accountByName,
|
|
606
|
+
accounts: props.currentState.organization.accounts
|
|
607
|
+
});
|
|
608
|
+
const mappedId = matchedAccount?.id ?? pendingCreationId;
|
|
609
|
+
mappedAccounts.push({
|
|
610
|
+
id: mappedId,
|
|
611
|
+
arn: matchedAccount?.arn ?? pendingCreationId,
|
|
612
|
+
name: account.name,
|
|
613
|
+
email: account.email,
|
|
614
|
+
status: matchedAccount?.status ?? "ACTIVE",
|
|
615
|
+
parentId: ownerParentId,
|
|
616
|
+
tags: account.tags
|
|
617
|
+
});
|
|
618
|
+
mappedAccountIdByName.set(account.name, mappedId);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const mappedUsers = props.config.users.map((user) => {
|
|
622
|
+
const matchedUser = userByUserName[user.userName];
|
|
623
|
+
return {
|
|
624
|
+
userId: matchedUser?.userId ?? pendingCreationId,
|
|
625
|
+
userName: user.userName,
|
|
626
|
+
displayName: user.displayName,
|
|
627
|
+
email: user.email
|
|
628
|
+
};
|
|
629
|
+
});
|
|
630
|
+
const mappedUserByUserName = toRecordByProperty(
|
|
631
|
+
mappedUsers,
|
|
632
|
+
"userName"
|
|
633
|
+
);
|
|
634
|
+
const mappedGroups = props.config.groups.map((group) => {
|
|
635
|
+
const matchedGroup = groupByDisplayName[group.displayName];
|
|
636
|
+
return {
|
|
637
|
+
groupId: matchedGroup?.groupId ?? pendingCreationId,
|
|
638
|
+
displayName: group.displayName,
|
|
639
|
+
description: group.description ?? ""
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
const mappedGroupByDisplayName = toRecordByProperty(
|
|
643
|
+
mappedGroups,
|
|
644
|
+
"displayName"
|
|
645
|
+
);
|
|
646
|
+
const mappedGroupMemberships = [];
|
|
647
|
+
for (const group of props.config.groups) {
|
|
648
|
+
assertUniqueNames({
|
|
649
|
+
values: group.members,
|
|
650
|
+
entityName: `group member for "${group.displayName}"`
|
|
651
|
+
});
|
|
652
|
+
const groupId = mappedGroupByDisplayName[group.displayName]?.groupId ?? pendingCreationId;
|
|
653
|
+
for (const userName of group.members) {
|
|
654
|
+
const currentMembership = groupMembershipByNameKey[createGroupMembershipNameKey({
|
|
655
|
+
groupDisplayName: group.displayName,
|
|
656
|
+
userName
|
|
657
|
+
})];
|
|
658
|
+
mappedGroupMemberships.push({
|
|
659
|
+
membershipId: currentMembership?.membershipId ?? pendingCreationId,
|
|
660
|
+
groupId,
|
|
661
|
+
userId: mappedUserByUserName[userName]?.userId ?? pendingCreationId
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const mappedPermissionSets = props.config.permissionSets.map((permissionSet) => {
|
|
666
|
+
const matchedPermissionSet = permissionSetByName[permissionSet.name];
|
|
667
|
+
return {
|
|
668
|
+
permissionSetArn: matchedPermissionSet?.permissionSetArn ?? pendingCreationId,
|
|
669
|
+
name: permissionSet.name,
|
|
670
|
+
description: permissionSet.description,
|
|
671
|
+
inlinePolicy: stableStringifyInlinePolicy(permissionSet.inlinePolicy),
|
|
672
|
+
awsManagedPolicies: [...permissionSet.awsManagedPolicies],
|
|
673
|
+
customerManagedPolicies: permissionSet.customerManagedPolicies.map(
|
|
674
|
+
(customerManagedPolicy) => ({
|
|
675
|
+
name: customerManagedPolicy.name,
|
|
676
|
+
path: customerManagedPolicy.path
|
|
677
|
+
})
|
|
678
|
+
)
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
const mappedPermissionSetByName = toRecordByProperty(
|
|
682
|
+
mappedPermissionSets,
|
|
683
|
+
"name"
|
|
684
|
+
);
|
|
685
|
+
const mappedAccountAssignments = [];
|
|
686
|
+
for (const assignment of props.config.assignments) {
|
|
687
|
+
const hasGroupPrincipal = assignment.group != null;
|
|
688
|
+
const hasUserPrincipal = assignment.user != null;
|
|
689
|
+
if (hasGroupPrincipal === hasUserPrincipal) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`Assignment for permission set "${assignment.permissionSet}" must include exactly one principal (group or user).`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const mappedPrincipal = hasGroupPrincipal === true ? {
|
|
695
|
+
principalId: mappedGroupByDisplayName[assignment.group ?? ""]?.groupId ?? pendingCreationId,
|
|
696
|
+
principalType: "GROUP"
|
|
697
|
+
} : {
|
|
698
|
+
principalId: mappedUserByUserName[assignment.user ?? ""]?.userId ?? pendingCreationId,
|
|
699
|
+
principalType: "USER"
|
|
700
|
+
};
|
|
701
|
+
const permissionSetArn = mappedPermissionSetByName[assignment.permissionSet]?.permissionSetArn ?? pendingCreationId;
|
|
702
|
+
for (const accountName of assignment.accounts) {
|
|
703
|
+
mappedAccountAssignments.push({
|
|
704
|
+
accountId: mappedAccountIdByName.get(accountName) ?? pendingCreationId,
|
|
705
|
+
permissionSetArn,
|
|
706
|
+
principalId: mappedPrincipal.principalId,
|
|
707
|
+
principalType: mappedPrincipal.principalType
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const mapped = {
|
|
712
|
+
version: props.currentState.version,
|
|
713
|
+
generatedAt: props.currentState.generatedAt,
|
|
714
|
+
organization: {
|
|
715
|
+
rootId: props.context.organization.rootId,
|
|
716
|
+
organizationalUnits: mappedOrganizationalUnits,
|
|
717
|
+
accounts: mappedAccounts
|
|
718
|
+
},
|
|
719
|
+
identityCenter: {
|
|
720
|
+
instanceArn: props.context.identityCenter.instanceArn,
|
|
721
|
+
identityStoreId: props.context.identityCenter.identityStoreId,
|
|
722
|
+
users: mappedUsers,
|
|
723
|
+
groups: mappedGroups,
|
|
724
|
+
groupMemberships: mappedGroupMemberships,
|
|
725
|
+
permissionSets: mappedPermissionSets,
|
|
726
|
+
accountAssignments: mappedAccountAssignments,
|
|
727
|
+
accessRoles: mappedAccountAssignments.map((assignment) => ({
|
|
728
|
+
accountId: assignment.accountId,
|
|
729
|
+
permissionSetArn: assignment.permissionSetArn,
|
|
730
|
+
principalId: assignment.principalId,
|
|
731
|
+
principalType: assignment.principalType,
|
|
732
|
+
roleName: createAccessRoleName(assignment)
|
|
733
|
+
}))
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
assertUniqueNames({
|
|
737
|
+
values: props.config.organizationalUnits.map(
|
|
738
|
+
(organizationalUnit) => organizationalUnit.name
|
|
739
|
+
),
|
|
740
|
+
entityName: "organizational unit"
|
|
741
|
+
});
|
|
742
|
+
assertUniqueNames({
|
|
743
|
+
values: props.config.organizationalUnits.flatMap(
|
|
744
|
+
(organizationalUnit) => organizationalUnit.accounts.map((account) => account.name)
|
|
745
|
+
),
|
|
746
|
+
entityName: "account"
|
|
747
|
+
});
|
|
748
|
+
assertUniqueNames({
|
|
749
|
+
values: props.config.groups.map((group) => group.displayName),
|
|
750
|
+
entityName: "group"
|
|
751
|
+
});
|
|
752
|
+
assertUniqueNames({
|
|
753
|
+
values: props.config.users.map((user) => user.userName),
|
|
754
|
+
entityName: "user"
|
|
755
|
+
});
|
|
756
|
+
assertUniqueNames({
|
|
757
|
+
values: props.config.permissionSets.map(
|
|
758
|
+
(permissionSet) => permissionSet.name
|
|
759
|
+
),
|
|
760
|
+
entityName: "permission set"
|
|
761
|
+
});
|
|
762
|
+
return validateState(mapped);
|
|
763
|
+
}
|
|
764
|
+
function sortAwsConfigModel(props) {
|
|
765
|
+
const childrenByParentName = /* @__PURE__ */ new Map();
|
|
766
|
+
for (const organizationalUnit of props.config.organizationalUnits) {
|
|
767
|
+
const existingChildren = childrenByParentName.get(organizationalUnit.parentName) ?? [];
|
|
768
|
+
existingChildren.push(organizationalUnit);
|
|
769
|
+
childrenByParentName.set(organizationalUnit.parentName, existingChildren);
|
|
770
|
+
}
|
|
771
|
+
const orderedOrganizationalUnits = [];
|
|
772
|
+
const root = props.config.organizationalUnits.find(
|
|
773
|
+
(ou) => ou.name === "root"
|
|
774
|
+
);
|
|
775
|
+
if (root == null || root.parentName !== null) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
"Config model must include a synthetic root organizational unit with parentName set to null."
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
orderedOrganizationalUnits.push({
|
|
781
|
+
...root,
|
|
782
|
+
accounts: [...root.accounts].sort(
|
|
783
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
784
|
+
)
|
|
785
|
+
});
|
|
786
|
+
const queue = [root.name];
|
|
787
|
+
while (queue.length > 0) {
|
|
788
|
+
const currentParentName = queue.shift();
|
|
789
|
+
if (currentParentName == null) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const children = (childrenByParentName.get(currentParentName) ?? []).filter((ou) => ou.name !== "root").sort((left, right) => left.name.localeCompare(right.name));
|
|
793
|
+
for (const child of children) {
|
|
794
|
+
orderedOrganizationalUnits.push({
|
|
795
|
+
...child,
|
|
796
|
+
accounts: [...child.accounts].sort(
|
|
797
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
798
|
+
)
|
|
799
|
+
});
|
|
800
|
+
queue.push(child.name);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
organizationalUnits: orderedOrganizationalUnits,
|
|
805
|
+
users: [...props.config.users].sort(
|
|
806
|
+
(left, right) => left.userName.localeCompare(right.userName)
|
|
807
|
+
),
|
|
808
|
+
groups: [...props.config.groups].map((group) => ({
|
|
809
|
+
...group,
|
|
810
|
+
members: [...group.members].sort(
|
|
811
|
+
(left, right) => left.localeCompare(right)
|
|
812
|
+
)
|
|
813
|
+
})).sort((left, right) => left.displayName.localeCompare(right.displayName)),
|
|
814
|
+
permissionSets: [...props.config.permissionSets].map((permissionSet) => ({
|
|
815
|
+
...permissionSet,
|
|
816
|
+
inlinePolicy: permissionSet.inlinePolicy == null ? void 0 : sortJsonRecord(permissionSet.inlinePolicy),
|
|
817
|
+
awsManagedPolicies: [...permissionSet.awsManagedPolicies].sort(
|
|
818
|
+
(left, right) => left.localeCompare(right)
|
|
819
|
+
),
|
|
820
|
+
customerManagedPolicies: [...permissionSet.customerManagedPolicies].sort(
|
|
821
|
+
(left, right) => compareStringKeys(left.path, right.path, left.name, right.name)
|
|
822
|
+
)
|
|
823
|
+
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
824
|
+
assignments: [...props.config.assignments].map((assignment) => ({
|
|
825
|
+
...assignment,
|
|
826
|
+
accounts: [...assignment.accounts].sort(
|
|
827
|
+
(left, right) => left.localeCompare(right)
|
|
828
|
+
)
|
|
829
|
+
})).sort((left, right) => {
|
|
830
|
+
const leftPrincipal = left.group ?? left.user ?? "";
|
|
831
|
+
const rightPrincipal = right.group ?? right.user ?? "";
|
|
832
|
+
const principalComparison = leftPrincipal.localeCompare(rightPrincipal);
|
|
833
|
+
if (principalComparison !== 0) {
|
|
834
|
+
return principalComparison;
|
|
835
|
+
}
|
|
836
|
+
return left.permissionSet.localeCompare(right.permissionSet);
|
|
837
|
+
})
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function renderAwsConfigTs(props) {
|
|
841
|
+
const serializedConfig = renderTsValue(props.config, {
|
|
842
|
+
indentLevel: 0,
|
|
843
|
+
withinInlinePolicy: false
|
|
844
|
+
});
|
|
845
|
+
return `import * as v from "valibot";
|
|
846
|
+
import { awsConfigSchema, iam, type AwsConfig } from "./aws.config.types.js";
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Human-editable AWS config.
|
|
850
|
+
* Generated by "init"; refresh picklists after edits with "regenerate".
|
|
851
|
+
* Use helpers like iam.s3("GetObject") for IAM action autocomplete in inline policies.
|
|
852
|
+
* Generated inline policies use those helpers automatically when the action is
|
|
853
|
+
* present in the installed @beesolve/iam-policy-ts catalog.
|
|
854
|
+
* The synthetic { name: "root", parentName: null } entry represents organization root.
|
|
855
|
+
* "Graveyard" is bootstrap-managed and used internally as the account-removal sink;
|
|
856
|
+
* it is intentionally omitted from generated organizationalUnits in this file.
|
|
857
|
+
*/
|
|
858
|
+
const awsConfig: AwsConfig = v.parse(awsConfigSchema, ${serializedConfig} satisfies AwsConfig);
|
|
859
|
+
|
|
860
|
+
export default awsConfig;
|
|
861
|
+
`;
|
|
862
|
+
}
|
|
863
|
+
function renderTsValue(value, props) {
|
|
864
|
+
if (value === null) {
|
|
865
|
+
return "null";
|
|
866
|
+
}
|
|
867
|
+
if (value === void 0) {
|
|
868
|
+
throw new Error("Undefined values must be handled before TypeScript rendering.");
|
|
869
|
+
}
|
|
870
|
+
if (typeof value === "string") {
|
|
871
|
+
return renderTsStringValue(value, props);
|
|
872
|
+
}
|
|
873
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
874
|
+
return JSON.stringify(value);
|
|
875
|
+
}
|
|
876
|
+
if (Array.isArray(value)) {
|
|
877
|
+
return renderTsArray(value, props);
|
|
878
|
+
}
|
|
879
|
+
if (isJsonRecord(value)) {
|
|
880
|
+
return renderTsObject(value, props);
|
|
881
|
+
}
|
|
882
|
+
throw new Error(`Unsupported config value type: ${typeof value}.`);
|
|
883
|
+
}
|
|
884
|
+
function renderTsStringValue(value, props) {
|
|
885
|
+
if (props.withinInlinePolicy && (props.parentPropertyName === "Action" || props.parentPropertyName === "NotAction")) {
|
|
886
|
+
return renderPolicyActionString(value);
|
|
887
|
+
}
|
|
888
|
+
return JSON.stringify(value);
|
|
889
|
+
}
|
|
890
|
+
function renderTsArray(value, props) {
|
|
891
|
+
if (value.length === 0) {
|
|
892
|
+
return "[]";
|
|
893
|
+
}
|
|
894
|
+
const indent = " ".repeat(props.indentLevel);
|
|
895
|
+
const childIndent = " ".repeat(props.indentLevel + 1);
|
|
896
|
+
const renderedItems = value.map(
|
|
897
|
+
(item) => item === void 0 ? "null" : renderTsValue(item, {
|
|
898
|
+
indentLevel: props.indentLevel + 1,
|
|
899
|
+
withinInlinePolicy: props.withinInlinePolicy,
|
|
900
|
+
parentPropertyName: props.parentPropertyName
|
|
901
|
+
})
|
|
902
|
+
);
|
|
903
|
+
return `[
|
|
904
|
+
${renderedItems.map((item) => `${childIndent}${item}`).join(",\n")}
|
|
905
|
+
${indent}]`;
|
|
906
|
+
}
|
|
907
|
+
function renderTsObject(value, props) {
|
|
908
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0);
|
|
909
|
+
if (entries.length === 0) {
|
|
910
|
+
return "{}";
|
|
911
|
+
}
|
|
912
|
+
const indent = " ".repeat(props.indentLevel);
|
|
913
|
+
const childIndent = " ".repeat(props.indentLevel + 1);
|
|
914
|
+
const renderedEntries = entries.map(([key, entryValue]) => {
|
|
915
|
+
const nextWithinInlinePolicy = props.withinInlinePolicy || key === "inlinePolicy";
|
|
916
|
+
const renderedValue = renderTsValue(entryValue, {
|
|
917
|
+
indentLevel: props.indentLevel + 1,
|
|
918
|
+
withinInlinePolicy: nextWithinInlinePolicy,
|
|
919
|
+
parentPropertyName: key
|
|
920
|
+
});
|
|
921
|
+
return `${childIndent}${renderTsObjectKey(key)}: ${renderedValue}`;
|
|
922
|
+
});
|
|
923
|
+
return `{
|
|
924
|
+
${renderedEntries.join(",\n")}
|
|
925
|
+
${indent}}`;
|
|
926
|
+
}
|
|
927
|
+
function renderPolicyActionString(value) {
|
|
928
|
+
const separatorIndex = value.indexOf(":");
|
|
929
|
+
if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
|
|
930
|
+
return JSON.stringify(value);
|
|
931
|
+
}
|
|
932
|
+
const servicePrefix = value.slice(0, separatorIndex);
|
|
933
|
+
const actionName = value.slice(separatorIndex + 1);
|
|
934
|
+
const knownActions = iamActionCatalog[servicePrefix];
|
|
935
|
+
if (knownActions == null || knownActions.includes(actionName) === false) {
|
|
936
|
+
return JSON.stringify(value);
|
|
937
|
+
}
|
|
938
|
+
if (isIdentifierSafeServicePrefix(servicePrefix)) {
|
|
939
|
+
return `iam.${servicePrefix}(${JSON.stringify(actionName)})`;
|
|
940
|
+
}
|
|
941
|
+
return `iam[${JSON.stringify(servicePrefix)}](${JSON.stringify(actionName)})`;
|
|
942
|
+
}
|
|
943
|
+
function isIdentifierSafeServicePrefix(value) {
|
|
944
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
|
|
945
|
+
}
|
|
946
|
+
function renderTsObjectKey(value) {
|
|
947
|
+
return isIdentifierSafeServicePrefix(value) ? value : JSON.stringify(value);
|
|
948
|
+
}
|
|
949
|
+
function renderAwsConfigTypesTs(props) {
|
|
950
|
+
const organizationalUnitNames = props.config.organizationalUnits.map(
|
|
951
|
+
(ou) => ou.name
|
|
952
|
+
);
|
|
953
|
+
const accountNames = props.config.organizationalUnits.flatMap(
|
|
954
|
+
(ou) => ou.accounts.map((account) => account.name)
|
|
955
|
+
);
|
|
956
|
+
const permissionSetNames = props.config.permissionSets.map(
|
|
957
|
+
(permissionSet) => permissionSet.name
|
|
958
|
+
);
|
|
959
|
+
const groupNames = props.config.groups.map((group) => group.displayName);
|
|
960
|
+
const userNames = props.config.users.map((user) => user.userName);
|
|
961
|
+
const organizationalUnitNameSchema = renderPicklistSchema({
|
|
962
|
+
values: organizationalUnitNames
|
|
963
|
+
});
|
|
964
|
+
const accountNameSchema = renderPicklistSchema({
|
|
965
|
+
values: accountNames
|
|
966
|
+
});
|
|
967
|
+
const permissionSetNameSchema = renderPicklistSchema({
|
|
968
|
+
values: permissionSetNames
|
|
969
|
+
});
|
|
970
|
+
const groupNameSchema = renderPicklistSchema({
|
|
971
|
+
values: groupNames
|
|
972
|
+
});
|
|
973
|
+
const userNameSchema = renderPicklistSchema({
|
|
974
|
+
values: userNames
|
|
975
|
+
});
|
|
976
|
+
return `import * as v from "valibot";
|
|
977
|
+
import { iamPolicyDocumentSchema } from "@beesolve/iam-policy-ts";
|
|
978
|
+
export {
|
|
979
|
+
iam,
|
|
980
|
+
iamAction,
|
|
981
|
+
iamActionCatalog,
|
|
982
|
+
iamActionCatalogActionCount,
|
|
983
|
+
iamActionCatalogSourceSha256,
|
|
984
|
+
iamActionCatalogSourceUrl,
|
|
985
|
+
iamPolicyDocumentSchema,
|
|
986
|
+
iamPolicyStatementSchema,
|
|
987
|
+
iamPolicyDocumentStrictSchema,
|
|
988
|
+
iamPolicyStatementStrictSchema,
|
|
989
|
+
isIamPolicyDocument,
|
|
990
|
+
isIamPolicyStatement,
|
|
991
|
+
isIamPolicyDocumentStrict,
|
|
992
|
+
assertIamPolicyDocument,
|
|
993
|
+
assertIamPolicyDocumentStrict,
|
|
994
|
+
} from "@beesolve/iam-policy-ts";
|
|
995
|
+
export type {
|
|
996
|
+
IamActionCatalog,
|
|
997
|
+
IamPolicyServicePrefix,
|
|
998
|
+
IamPolicyActionNameByService,
|
|
999
|
+
IamPolicyActionForService,
|
|
1000
|
+
IamPolicyVersion,
|
|
1001
|
+
IamPolicyScalar,
|
|
1002
|
+
IamPolicyScalarList,
|
|
1003
|
+
IamPolicyStringList,
|
|
1004
|
+
IamPolicyPrincipalMap,
|
|
1005
|
+
IamPolicyPrincipal,
|
|
1006
|
+
IamPolicyConditionBlock,
|
|
1007
|
+
IamPolicyStatement,
|
|
1008
|
+
IamPolicyDocument,
|
|
1009
|
+
IamPolicyStatementStrict,
|
|
1010
|
+
IamPolicyDocumentStrict,
|
|
1011
|
+
} from "@beesolve/iam-policy-ts";
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Generated file. Do not edit by hand.
|
|
1015
|
+
*/
|
|
1016
|
+
const organizationalUnitNameSchema = ${organizationalUnitNameSchema};
|
|
1017
|
+
const accountNameSchema = ${accountNameSchema};
|
|
1018
|
+
const permissionSetNameSchema = ${permissionSetNameSchema};
|
|
1019
|
+
const groupNameSchema = ${groupNameSchema};
|
|
1020
|
+
const userNameSchema = ${userNameSchema};
|
|
1021
|
+
|
|
1022
|
+
export const awsConfigSchema = v.strictObject({
|
|
1023
|
+
organizationalUnits: v.array(
|
|
1024
|
+
v.strictObject({
|
|
1025
|
+
name: v.string(),
|
|
1026
|
+
parentName: v.union([organizationalUnitNameSchema, v.null_()]),
|
|
1027
|
+
accounts: v.array(
|
|
1028
|
+
v.strictObject({
|
|
1029
|
+
name: v.string(),
|
|
1030
|
+
email: v.string(),
|
|
1031
|
+
tags: v.array(
|
|
1032
|
+
v.strictObject({
|
|
1033
|
+
key: v.string(),
|
|
1034
|
+
value: v.string(),
|
|
1035
|
+
}),
|
|
1036
|
+
),
|
|
1037
|
+
}),
|
|
1038
|
+
),
|
|
1039
|
+
}),
|
|
1040
|
+
),
|
|
1041
|
+
users: v.array(
|
|
1042
|
+
v.strictObject({
|
|
1043
|
+
userName: v.string(),
|
|
1044
|
+
displayName: v.string(),
|
|
1045
|
+
email: v.string(),
|
|
1046
|
+
}),
|
|
1047
|
+
),
|
|
1048
|
+
groups: v.array(
|
|
1049
|
+
v.strictObject({
|
|
1050
|
+
displayName: v.string(),
|
|
1051
|
+
description: v.optional(v.string()),
|
|
1052
|
+
members: v.array(userNameSchema),
|
|
1053
|
+
}),
|
|
1054
|
+
),
|
|
1055
|
+
permissionSets: v.array(
|
|
1056
|
+
v.strictObject({
|
|
1057
|
+
name: v.string(),
|
|
1058
|
+
description: v.string(),
|
|
1059
|
+
inlinePolicy: v.optional(iamPolicyDocumentSchema),
|
|
1060
|
+
awsManagedPolicies: v.array(v.string()),
|
|
1061
|
+
customerManagedPolicies: v.array(
|
|
1062
|
+
v.strictObject({
|
|
1063
|
+
name: v.string(),
|
|
1064
|
+
path: v.string(),
|
|
1065
|
+
}),
|
|
1066
|
+
),
|
|
1067
|
+
}),
|
|
1068
|
+
),
|
|
1069
|
+
assignments: v.array(
|
|
1070
|
+
v.strictObject({
|
|
1071
|
+
permissionSet: permissionSetNameSchema,
|
|
1072
|
+
group: v.optional(groupNameSchema),
|
|
1073
|
+
user: v.optional(userNameSchema),
|
|
1074
|
+
accounts: v.array(accountNameSchema),
|
|
1075
|
+
}),
|
|
1076
|
+
),
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
export type AwsConfig = v.InferOutput<typeof awsConfigSchema>;
|
|
1080
|
+
`;
|
|
1081
|
+
}
|
|
1082
|
+
function assertStateMatchesContext(props) {
|
|
1083
|
+
if (props.state.organization.rootId !== props.context.organization.rootId) {
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`state/context mismatch for organization.rootId: state has "${props.state.organization.rootId}" but context has "${props.context.organization.rootId}".`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
const graveyardOrganizationalUnit = props.state.organization.organizationalUnits.find(
|
|
1089
|
+
(ou) => ou.name === "Graveyard"
|
|
1090
|
+
);
|
|
1091
|
+
if (graveyardOrganizationalUnit?.id !== props.context.organization.graveyardOuId) {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
`state/context mismatch for Graveyard OU id: state has "${graveyardOrganizationalUnit?.id ?? "<missing>"}" but context has "${props.context.organization.graveyardOuId}".`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
if (props.state.identityCenter.instanceArn !== props.context.identityCenter.instanceArn || props.state.identityCenter.identityStoreId !== props.context.identityCenter.identityStoreId) {
|
|
1097
|
+
throw new Error(
|
|
1098
|
+
"state/context mismatch for identityCenter.instanceArn or identityCenter.identityStoreId."
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function assertUniqueNames(props) {
|
|
1103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1104
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1105
|
+
for (const value of props.values) {
|
|
1106
|
+
if (seen.has(value)) {
|
|
1107
|
+
duplicates.add(value);
|
|
1108
|
+
}
|
|
1109
|
+
seen.add(value);
|
|
1110
|
+
}
|
|
1111
|
+
if (duplicates.size > 0) {
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
`Duplicate ${props.entityName} names detected: ${[...duplicates.values()].join(", ")}.`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function mapAssignmentPrincipal(props) {
|
|
1118
|
+
const principalType = props.assignment.principalType;
|
|
1119
|
+
if (principalType === "GROUP") {
|
|
1120
|
+
const groupDisplayName = props.groupById[props.assignment.principalId]?.displayName;
|
|
1121
|
+
if (groupDisplayName == null) {
|
|
1122
|
+
throw new Error(
|
|
1123
|
+
`Could not resolve group display name for principalId "${props.assignment.principalId}".`
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
kind: "group",
|
|
1128
|
+
value: groupDisplayName
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
if (principalType === "USER") {
|
|
1132
|
+
const userName = props.userById[props.assignment.principalId]?.userName;
|
|
1133
|
+
if (userName == null) {
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`Could not resolve user name for principalId "${props.assignment.principalId}".`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
kind: "user",
|
|
1140
|
+
value: userName
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
assertUnreachable(
|
|
1144
|
+
principalType,
|
|
1145
|
+
`Unsupported principal type "${principalType}" in account assignment.`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
function createGroupMembershipNameKey(props) {
|
|
1149
|
+
return [props.groupDisplayName, props.userName].join("|");
|
|
1150
|
+
}
|
|
1151
|
+
function parseInlinePolicyForConfig(props) {
|
|
1152
|
+
let parsed;
|
|
1153
|
+
try {
|
|
1154
|
+
parsed = JSON.parse(props.inlinePolicy);
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`Could not parse inline policy for permission set "${props.permissionSetName}": ${message}`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
if (isJsonRecord(parsed) === false) {
|
|
1162
|
+
throw new Error(
|
|
1163
|
+
`Inline policy for permission set "${props.permissionSetName}" must be a JSON object.`
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
return sortJsonRecord(assertIamPolicyDocument(parsed));
|
|
1167
|
+
}
|
|
1168
|
+
function stableStringifyInlinePolicy(inlinePolicy) {
|
|
1169
|
+
if (inlinePolicy == null) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
return JSON.stringify(sortJsonRecord(assertIamPolicyDocument(inlinePolicy)));
|
|
1173
|
+
}
|
|
1174
|
+
function sortJsonRecord(input) {
|
|
1175
|
+
return Object.fromEntries(
|
|
1176
|
+
Object.entries(input).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([key, value]) => [key, sortJsonValue(value)])
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
function sortJsonValue(value) {
|
|
1180
|
+
if (Array.isArray(value)) {
|
|
1181
|
+
return value.map((entry) => sortJsonValue(entry));
|
|
1182
|
+
}
|
|
1183
|
+
if (isJsonRecord(value)) {
|
|
1184
|
+
return sortJsonRecord(value);
|
|
1185
|
+
}
|
|
1186
|
+
return value;
|
|
1187
|
+
}
|
|
1188
|
+
function isJsonRecord(value) {
|
|
1189
|
+
return value != null && typeof value === "object" && Array.isArray(value) === false;
|
|
1190
|
+
}
|
|
1191
|
+
function compareStringKeys(...values) {
|
|
1192
|
+
for (let index = 0; index < values.length; index += 2) {
|
|
1193
|
+
const left = values[index] ?? "";
|
|
1194
|
+
const right = values[index + 1] ?? "";
|
|
1195
|
+
const compared = left.localeCompare(right);
|
|
1196
|
+
if (compared !== 0) {
|
|
1197
|
+
return compared;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return 0;
|
|
1201
|
+
}
|
|
1202
|
+
function resolveOrganizationalUnitId(props) {
|
|
1203
|
+
if (props.organizationalUnitName === "root") {
|
|
1204
|
+
return props.context.organization.rootId;
|
|
1205
|
+
}
|
|
1206
|
+
if (props.organizationalUnitName === "Graveyard") {
|
|
1207
|
+
return props.context.organization.graveyardOuId;
|
|
1208
|
+
}
|
|
1209
|
+
return props.matchedOrganizationalUnit?.id ?? pendingCreationId;
|
|
1210
|
+
}
|
|
1211
|
+
function renderPicklistSchema(props) {
|
|
1212
|
+
if (props.values.length === 0) {
|
|
1213
|
+
return 'v.picklist(["__EMPTY_PICKLIST__"])';
|
|
1214
|
+
}
|
|
1215
|
+
const literals = [...props.values].sort((left, right) => left.localeCompare(right)).map((value) => JSON.stringify(value)).join(", ");
|
|
1216
|
+
return `v.picklist([${literals}])`;
|
|
1217
|
+
}
|
|
1218
|
+
async function readAwsContextFile(path) {
|
|
1219
|
+
const rawContent = await readFile(path, "utf8");
|
|
1220
|
+
const parsed = JSON.parse(rawContent);
|
|
1221
|
+
return v.parse(awsContextSchema, parsed);
|
|
1222
|
+
}
|
|
1223
|
+
async function loadAwsConfigModelFromTsFile(props) {
|
|
1224
|
+
const typesModule = await loadAwsConfigTypesModule({
|
|
1225
|
+
typesPath: props.typesPath
|
|
1226
|
+
});
|
|
1227
|
+
return await loadAwsConfigFromTsFile({
|
|
1228
|
+
configPath: props.configPath,
|
|
1229
|
+
schema: typesModule.awsConfigSchema
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async function readAwsContextFromFile(path) {
|
|
1233
|
+
return readAwsContextFile(path);
|
|
1234
|
+
}
|
|
1235
|
+
async function loadAwsConfigTypesModule(props) {
|
|
1236
|
+
const loadedModule = await loadTsModule({
|
|
1237
|
+
modulePath: props.typesPath
|
|
1238
|
+
});
|
|
1239
|
+
if (loadedModule == null || typeof loadedModule !== "object" || "awsConfigSchema" in loadedModule === false) {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
`Types module "${props.typesPath}" does not export awsConfigSchema.`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
const moduleWithSchema = loadedModule;
|
|
1245
|
+
if (moduleWithSchema.awsConfigSchema == null) {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
`Types module "${props.typesPath}" does not export awsConfigSchema.`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
return {
|
|
1251
|
+
awsConfigSchema: moduleWithSchema.awsConfigSchema
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
async function loadAwsConfigFromTsFile(props) {
|
|
1255
|
+
let loadedModule;
|
|
1256
|
+
try {
|
|
1257
|
+
loadedModule = await loadTsModule({
|
|
1258
|
+
modulePath: props.configPath
|
|
1259
|
+
});
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (isValiErrorLike(error)) {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
`aws.config.ts validation failed: ${error instanceof Error ? error.message : String(error)}. If you recently edited names/references, re-run regenerate after fixing the config.`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
if (loadedModule == null || typeof loadedModule !== "object" || "default" in loadedModule === false) {
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
`Config module "${props.configPath}" must export a default config object.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const moduleWithDefault = loadedModule;
|
|
1274
|
+
if (moduleWithDefault.default == null) {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
`Config module "${props.configPath}" must export a default config object.`
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
const validatedConfig = v.parse(props.schema, moduleWithDefault.default);
|
|
1281
|
+
return v.parse(awsConfigModelSchema, validatedConfig);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
if (isValiErrorLike(error)) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
`aws.config.ts validation failed: ${error instanceof Error ? error.message : String(error)}. If you recently edited names/references, re-run regenerate after fixing the config.`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
throw error;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
async function loadTsModule(props) {
|
|
1292
|
+
const resolvedModulePath = resolve(props.modulePath);
|
|
1293
|
+
const temporaryOutputPath = join(`aws-accounts-${randomUUID()}.mjs`);
|
|
1294
|
+
const temporaryOutputAtProjectRoot = join(
|
|
1295
|
+
projectRootPath,
|
|
1296
|
+
temporaryOutputPath
|
|
1297
|
+
);
|
|
1298
|
+
try {
|
|
1299
|
+
await esbuildBuild({
|
|
1300
|
+
entryPoints: [resolvedModulePath],
|
|
1301
|
+
outfile: temporaryOutputAtProjectRoot,
|
|
1302
|
+
bundle: true,
|
|
1303
|
+
platform: "node",
|
|
1304
|
+
format: "esm",
|
|
1305
|
+
target: "node24",
|
|
1306
|
+
absWorkingDir: projectRootPath,
|
|
1307
|
+
nodePaths: [join(projectRootPath, "node_modules")],
|
|
1308
|
+
write: true
|
|
1309
|
+
});
|
|
1310
|
+
const moduleUrl = pathToFileURL(temporaryOutputAtProjectRoot).href;
|
|
1311
|
+
return await import(moduleUrl);
|
|
1312
|
+
} finally {
|
|
1313
|
+
await safeUnlink(temporaryOutputAtProjectRoot);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async function readIfExists(path) {
|
|
1317
|
+
try {
|
|
1318
|
+
return await readFile(path, "utf8");
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
const code = error.code;
|
|
1321
|
+
if (code === "ENOENT") {
|
|
1322
|
+
return void 0;
|
|
1323
|
+
}
|
|
1324
|
+
throw error;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async function safeUnlink(path) {
|
|
1328
|
+
try {
|
|
1329
|
+
await unlink(path);
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
const code = error.code;
|
|
1332
|
+
if (code === "ENOENT") {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function isValiErrorLike(error) {
|
|
1339
|
+
return error instanceof v.ValiError || error instanceof Error && error.name === "ValiError";
|
|
1340
|
+
}
|
|
1341
|
+
async function regenerateTypesFromState(props) {
|
|
1342
|
+
try {
|
|
1343
|
+
const mappedConfig = mapStateToAwsConfig({ state: props.state });
|
|
1344
|
+
const sortedConfig = sortAwsConfigModel({ config: mappedConfig });
|
|
1345
|
+
const nextTypesContent = renderAwsConfigTypesTs({ config: sortedConfig });
|
|
1346
|
+
const currentTypesContent = await readIfExists(props.typesPath);
|
|
1347
|
+
if (currentTypesContent === nextTypesContent) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
await writeFile(props.typesPath, nextTypesContent, "utf8");
|
|
1351
|
+
props.logger.log("Updated aws.config.types.ts");
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1354
|
+
props.logger.log(`Warning: Failed to regenerate types: ${message}`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
export {
|
|
1358
|
+
awsConfigModelSchema,
|
|
1359
|
+
loadAwsConfigModelFromTsFile,
|
|
1360
|
+
mapAwsConfigToState,
|
|
1361
|
+
readAwsContextFromFile,
|
|
1362
|
+
regenerateAwsConfigTypes,
|
|
1363
|
+
regenerateTypesFromState,
|
|
1364
|
+
writeAwsConfigFromState
|
|
1365
|
+
};
|