@fjall/deploy-core 0.89.5 → 0.89.6
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 +50 -21
- package/README.md +25 -0
- package/dist/.minified +1 -0
- package/dist/src/__test-utils__/awsMockHelpers.d.ts +20 -0
- package/dist/src/__test-utils__/awsMockHelpers.js +1 -0
- package/dist/src/__test-utils__/index.d.ts +1 -0
- package/dist/src/__test-utils__/index.js +1 -0
- package/dist/src/aws/AwsProvider.js +0 -1
- package/dist/src/aws/SimpleAwsProvider.js +1 -70
- package/dist/src/aws/index.d.ts +4 -2
- package/dist/src/aws/index.js +1 -3
- package/dist/src/aws/organisations/accounts.js +10 -10
- package/dist/src/aws/organisations/backup.js +4 -2
- package/dist/src/aws/organisations/costAllocation.js +4 -2
- package/dist/src/aws/organisations/delegatedAdmin.d.ts +9 -0
- package/dist/src/aws/organisations/delegatedAdmin.js +43 -0
- package/dist/src/aws/organisations/identityCentre.d.ts +1 -1
- package/dist/src/aws/organisations/identityCentre.js +6 -2
- package/dist/src/aws/organisations/index.d.ts +4 -3
- package/dist/src/aws/organisations/index.js +1 -12
- package/dist/src/aws/organisations/ipam.js +4 -2
- package/dist/src/aws/organisations/organisation.js +27 -18
- package/dist/src/aws/organisations/organisationalUnits.d.ts +26 -6
- package/dist/src/aws/organisations/organisationalUnits.js +149 -35
- package/dist/src/aws/organisations/policies.js +4 -3
- package/dist/src/aws/organisations/ram.js +6 -2
- package/dist/src/aws/organisations/serviceAccess.js +12 -6
- package/dist/src/aws/organisations/trustedAccess.js +6 -2
- package/dist/src/aws/organisations/types.d.ts +23 -1
- package/dist/src/aws/organisations/types.js +1 -16
- package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.d.ts +6 -0
- package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.js +1 -0
- package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +48 -0
- package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -0
- package/dist/src/aws/utils/cloudformationEventTypes.d.ts +45 -0
- package/dist/src/aws/utils/cloudformationEventTypes.js +1 -0
- package/dist/src/aws/utils/cloudformationEvents.d.ts +8 -54
- package/dist/src/aws/utils/cloudformationEvents.js +1 -596
- package/dist/src/aws/utils/index.d.ts +5 -0
- package/dist/src/aws/utils/index.js +1 -0
- package/dist/src/aws/utils/stackStatus.js +1 -90
- package/dist/src/events/index.d.ts +13 -0
- package/dist/src/events/index.js +1 -0
- package/dist/src/index.d.ts +34 -17
- package/dist/src/index.js +41 -21
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.d.ts +12 -0
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +78 -0
- package/dist/src/orchestration/activeDeploymentGuard.d.ts +10 -0
- package/dist/src/orchestration/activeDeploymentGuard.js +39 -0
- package/dist/src/orchestration/applicationDeploy.js +46 -229
- package/dist/src/orchestration/applicationDeployHelpers.d.ts +39 -0
- package/dist/src/orchestration/applicationDeployHelpers.js +223 -0
- package/dist/src/orchestration/applicationDestroy.d.ts +14 -0
- package/dist/src/orchestration/applicationDestroy.js +131 -0
- package/dist/src/orchestration/builders/dockerBuilder.d.ts +17 -0
- package/dist/src/orchestration/builders/dockerBuilder.js +98 -0
- package/dist/src/orchestration/builders/frameworkRegistry.d.ts +23 -0
- package/dist/src/orchestration/builders/frameworkRegistry.js +1 -0
- package/dist/src/orchestration/builders/index.d.ts +4 -0
- package/dist/src/orchestration/builders/index.js +1 -0
- package/dist/src/orchestration/builders/openNextBuilder.d.ts +21 -0
- package/dist/src/orchestration/builders/openNextBuilder.js +144 -0
- package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +30 -0
- package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -0
- package/dist/src/orchestration/cascadeHelpers.d.ts +46 -0
- package/dist/src/orchestration/cascadeHelpers.js +160 -0
- package/dist/src/orchestration/contextHelpers.d.ts +46 -2
- package/dist/src/orchestration/contextHelpers.js +93 -1
- package/dist/src/orchestration/destroy.d.ts +13 -0
- package/dist/src/orchestration/destroy.js +67 -0
- package/dist/src/orchestration/detectionPipeline.d.ts +2 -11
- package/dist/src/orchestration/detectionPipeline.js +29 -10
- package/dist/src/orchestration/dockerBuildHelper.d.ts +10 -0
- package/dist/src/orchestration/dockerBuildHelper.js +49 -0
- package/dist/src/orchestration/dockerInterface.d.ts +4 -2
- package/dist/src/orchestration/index.d.ts +8 -1
- package/dist/src/orchestration/index.js +1 -3
- package/dist/src/orchestration/manifestSecretParser.d.ts +11 -0
- package/dist/src/orchestration/manifestSecretParser.js +1 -0
- package/dist/src/orchestration/openNextBuild.d.ts +28 -0
- package/dist/src/orchestration/openNextBuild.js +243 -0
- package/dist/src/orchestration/organisationDeploy.js +110 -233
- package/dist/src/orchestration/organisationDestroy.d.ts +24 -0
- package/dist/src/orchestration/organisationDestroy.js +189 -0
- package/dist/src/orchestration/organisationSetup.d.ts +6 -4
- package/dist/src/orchestration/organisationSetup.js +28 -8
- package/dist/src/orchestration/resolveOperation.js +68 -6
- package/dist/src/orchestration/serviceFactory.d.ts +4 -0
- package/dist/src/orchestration/serviceFactory.js +1 -16
- package/dist/src/orchestration/spawnHelpers.d.ts +47 -0
- package/dist/src/orchestration/spawnHelpers.js +1 -0
- package/dist/src/orchestration/stackCleanup.d.ts +39 -0
- package/dist/src/orchestration/stackCleanup.js +1 -0
- package/dist/src/orchestration/welcomeImageHelper.d.ts +15 -0
- package/dist/src/orchestration/welcomeImageHelper.js +64 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +21 -30
- package/dist/src/services/application/ApplicationStackService.js +16 -234
- package/dist/src/services/application/applicationStackHelpers.d.ts +46 -0
- package/dist/src/services/application/applicationStackHelpers.js +248 -0
- package/dist/src/services/application/index.d.ts +1 -0
- package/dist/src/services/application/index.js +1 -1
- package/dist/src/services/index.d.ts +6 -0
- package/dist/src/services/index.js +1 -0
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -67
- package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +10 -2
- package/dist/src/services/infrastructure/CdkCommandRunner.js +18 -15
- package/dist/src/services/infrastructure/CdkErrorFormatter.js +16 -194
- package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -41
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -1
- package/dist/src/services/infrastructure/CdkOutputParser.js +2 -33
- package/dist/src/services/infrastructure/CdkProcessManager.d.ts +5 -0
- package/dist/src/services/infrastructure/CdkProcessManager.js +81 -47
- package/dist/src/services/infrastructure/CdkService.d.ts +7 -53
- package/dist/src/services/infrastructure/CdkService.js +41 -83
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +50 -0
- package/dist/src/services/infrastructure/CdkServiceTypes.js +0 -0
- package/dist/src/services/infrastructure/CloudFormationService.js +9 -10
- package/dist/src/services/infrastructure/ICdkProcessManager.d.ts +27 -0
- package/dist/src/services/infrastructure/ICdkProcessManager.js +1 -0
- package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.d.ts +9 -0
- package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.js +1 -0
- package/dist/src/services/infrastructure/cdkServiceHelpers.d.ts +9 -0
- package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -0
- package/dist/src/services/infrastructure/constructMapEnrichment.d.ts +7 -0
- package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -0
- package/dist/src/services/infrastructure/index.d.ts +3 -1
- package/dist/src/services/infrastructure/index.js +1 -7
- package/dist/src/services/supporting/TemplateHashService.js +1 -1
- package/dist/src/services/supporting/helpers.js +1 -81
- package/dist/src/services/supporting/index.js +1 -3
- package/dist/src/steps/index.d.ts +1 -0
- package/dist/src/steps/index.js +1 -0
- package/dist/src/steps/stepRegistry.d.ts +71 -0
- package/dist/src/steps/stepRegistry.js +505 -0
- package/dist/src/types/FjallState.js +1 -118
- package/dist/src/types/ProgressEvent.js +1 -48
- package/dist/src/types/application/ApplicationServiceTypes.js +1 -30
- package/dist/src/types/application/index.js +1 -1
- package/dist/src/types/callbacks.d.ts +76 -4
- package/dist/src/types/callbacks.js +0 -1
- package/dist/src/types/constants.d.ts +2 -0
- package/dist/src/types/constants.js +1 -6
- package/dist/src/types/credentials.js +0 -1
- package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +5 -2
- package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -1
- package/dist/src/types/deployment/DeploymentTypes.js +0 -1
- package/dist/src/types/deployment/cloudformation.js +0 -1
- package/dist/src/types/deployment/index.d.ts +3 -1
- package/dist/src/types/deployment/index.js +1 -1
- package/dist/src/types/deployment/parallel.js +1 -10
- package/dist/src/types/deploymentEventSchema.d.ts +158 -0
- package/dist/src/types/deploymentEventSchema.js +1 -0
- package/dist/src/types/detection.d.ts +22 -0
- package/dist/src/types/detection.js +1 -0
- package/dist/src/types/entitlements.d.ts +31 -0
- package/dist/src/types/entitlements.js +0 -0
- package/dist/src/types/errors/CdkError.js +1 -20
- package/dist/src/types/errors/ServiceError.d.ts +2 -1
- package/dist/src/types/errors/ServiceError.js +1 -119
- package/dist/src/types/errors/index.d.ts +2 -0
- package/dist/src/types/errors/index.js +1 -0
- package/dist/src/types/events.d.ts +3 -9
- package/dist/src/types/events.js +0 -5
- package/dist/src/types/frameworkBuilder.d.ts +96 -0
- package/dist/src/types/frameworkBuilder.js +8 -0
- package/dist/src/types/index.d.ts +19 -4
- package/dist/src/types/index.js +1 -9
- package/dist/src/types/operations.d.ts +3 -2
- package/dist/src/types/operations.js +1 -285
- package/dist/src/types/orgConfig.d.ts +2 -10
- package/dist/src/types/orgConfig.js +0 -11
- package/dist/src/types/params.d.ts +60 -1
- package/dist/src/types/patternDetection.d.ts +14 -16
- package/dist/src/types/patternDetection.js +14 -18
- package/dist/src/types/patternTypes.d.ts +19 -0
- package/dist/src/types/patternTypes.js +1 -0
- package/dist/src/types/stepDefinitions.d.ts +163 -0
- package/dist/src/types/stepDefinitions.js +98 -0
- package/dist/src/types/validation.js +0 -1
- package/dist/src/util/dockerfileDetection.d.ts +5 -0
- package/dist/src/util/dockerfileDetection.js +1 -0
- package/dist/src/util/index.d.ts +4 -3
- package/dist/src/util/index.js +1 -3
- package/dist/src/util/sequencedCallbacks.d.ts +44 -0
- package/dist/src/util/sequencedCallbacks.js +1 -0
- package/package.json +49 -8
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +0 -32
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +0 -228
- package/dist/src/aws/utils/errors.d.ts +0 -26
- package/dist/src/aws/utils/errors.js +0 -59
- package/dist/src/util/fsHelpers.d.ts +0 -4
- package/dist/src/util/fsHelpers.js +0 -16
- package/dist/src/util/securityHelpers.d.ts +0 -31
- package/dist/src/util/securityHelpers.js +0 -124
- package/dist/src/util/singleton.d.ts +0 -2
- package/dist/src/util/singleton.js +0 -9
- package/dist/src/util/sleep.d.ts +0 -4
- package/dist/src/util/sleep.js +0 -4
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { CreateOrganizationalUnitCommand, ListOrganizationalUnitsForParentCommand, ListParentsCommand, MoveAccountCommand } from "@aws-sdk/client-organizations";
|
|
2
2
|
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { extractErrorName } from "./types.js";
|
|
3
|
+
import { extractErrorName, isOULeaf, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
|
|
4
|
+
import { getErrorMessage } from "@fjall/util";
|
|
4
5
|
/**
|
|
5
6
|
* List all OUs under a parent, handling pagination.
|
|
6
7
|
*/
|
|
7
8
|
async function listOUsForParent(client, parentId) {
|
|
8
|
-
const response = await client.send(new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }));
|
|
9
|
+
const response = await client.send(new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
9
10
|
let ous = response.OrganizationalUnits ?? [];
|
|
10
11
|
let nextToken = response.NextToken;
|
|
11
12
|
while (nextToken) {
|
|
12
13
|
const nextResponse = await client.send(new ListOrganizationalUnitsForParentCommand({
|
|
13
14
|
ParentId: parentId,
|
|
14
15
|
NextToken: nextToken
|
|
15
|
-
}));
|
|
16
|
+
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
16
17
|
ous = ous.concat(nextResponse.OrganizationalUnits ?? []);
|
|
17
18
|
nextToken = nextResponse.NextToken;
|
|
18
19
|
}
|
|
@@ -23,61 +24,173 @@ async function listOUsForParent(client, parentId) {
|
|
|
23
24
|
*/
|
|
24
25
|
async function getParentId(client, childId) {
|
|
25
26
|
try {
|
|
26
|
-
const response = await client.send(new ListParentsCommand({ ChildId: childId }));
|
|
27
|
+
const response = await client.send(new ListParentsCommand({ ChildId: childId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
27
28
|
return response.Parents?.[0]?.Id;
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
31
|
const errorName = extractErrorName(error);
|
|
31
|
-
if (errorName ===
|
|
32
|
+
if (errorName === AWS_ERROR_NAMES.CHILD_NOT_FOUND) {
|
|
32
33
|
return undefined;
|
|
33
34
|
}
|
|
34
35
|
throw error;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* Find or create an OU by name under a parent. Searches the provided
|
|
40
|
+
* `existingOUs` list first to avoid duplicate creation.
|
|
41
|
+
*/
|
|
42
|
+
async function findOrCreateOU(client, parentId, ouName, existingOUs) {
|
|
43
|
+
const match = existingOUs.find((ou) => ou.Name === ouName);
|
|
44
|
+
if (match?.Id) {
|
|
45
|
+
return success(match.Id);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const response = await client.send(new CreateOrganizationalUnitCommand({
|
|
49
|
+
Name: ouName,
|
|
50
|
+
ParentId: parentId
|
|
51
|
+
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
52
|
+
const ou = response.OrganizationalUnit;
|
|
53
|
+
if (!ou?.Id) {
|
|
54
|
+
return failure(new Error(`OU "${ouName}" was created but has no ID`));
|
|
55
|
+
}
|
|
56
|
+
return success(ou.Id);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return failure(new Error(`Failed to create OU "${ouName}": ${getErrorMessage(error)}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Ensure flat OUs exist directly under root. Original logic, extracted.
|
|
64
|
+
*/
|
|
65
|
+
async function ensureFlatOUs(client, rootId, ouNames) {
|
|
66
|
+
const ouMap = {};
|
|
67
|
+
if (ouNames.length === 0) {
|
|
68
|
+
return success(ouMap);
|
|
69
|
+
}
|
|
70
|
+
// Fetch existing OUs once for the root level
|
|
71
|
+
const existing = await listOUsForParent(client, rootId);
|
|
72
|
+
for (const ouName of ouNames) {
|
|
73
|
+
const capitalised = ouName.charAt(0).toUpperCase() + ouName.slice(1);
|
|
74
|
+
const result = await findOrCreateOU(client, rootId, capitalised, existing);
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
return failure(result.error);
|
|
77
|
+
}
|
|
78
|
+
ouMap[ouName.toLowerCase()] = result.data;
|
|
79
|
+
// Add to existing list so subsequent iterations can see it
|
|
80
|
+
existing.push({ Id: result.data, Name: capitalised });
|
|
81
|
+
}
|
|
82
|
+
return success(ouMap);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Recursively ensure nested OUs exist. Populates `ouMap` with:
|
|
86
|
+
* - Dotted keys for nested OUs (e.g., "workloads.production")
|
|
87
|
+
* - Shorthand leaf-name keys when they don't collide with top-level keys
|
|
88
|
+
*
|
|
89
|
+
* When a shorthand key collides with a top-level key, only the dotted
|
|
90
|
+
* key is stored for the nested OU.
|
|
91
|
+
*/
|
|
92
|
+
async function ensureNestedOUs(client, parentId, tree, ouMap, prefix, topLevelKeys) {
|
|
93
|
+
// Fetch existing OUs once per parent level
|
|
94
|
+
const existing = await listOUsForParent(client, parentId);
|
|
95
|
+
for (const [ouName, children] of Object.entries(tree)) {
|
|
96
|
+
const result = await findOrCreateOU(client, parentId, ouName, existing);
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
return failure(result.error);
|
|
99
|
+
}
|
|
100
|
+
const ouId = result.data;
|
|
101
|
+
const dottedKey = prefix
|
|
102
|
+
? `${prefix}.${ouName.toLowerCase()}`
|
|
103
|
+
: ouName.toLowerCase();
|
|
104
|
+
ouMap[dottedKey] = ouId;
|
|
105
|
+
// Add shorthand key if not colliding with a top-level key
|
|
106
|
+
if (prefix) {
|
|
107
|
+
const shorthand = ouName.toLowerCase();
|
|
108
|
+
if (!topLevelKeys.has(shorthand)) {
|
|
109
|
+
ouMap[shorthand] = ouId;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Add to existing list so subsequent iterations can see it
|
|
113
|
+
existing.push({ Id: ouId, Name: ouName });
|
|
114
|
+
if (!isOULeaf(children)) {
|
|
115
|
+
const recurseResult = await ensureNestedOUs(client, ouId, children, ouMap, dottedKey, topLevelKeys);
|
|
116
|
+
if (!recurseResult.success) {
|
|
117
|
+
return recurseResult;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return success(undefined);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Ensure the specified organisational units exist. Accepts either:
|
|
125
|
+
* - `string[]` (flat): creates OUs directly under root (original behaviour)
|
|
126
|
+
* - `OUTree` (nested): creates a recursive OU hierarchy
|
|
127
|
+
*
|
|
128
|
+
* Returns a map of OU key (lowercase) to OU ID. For nested trees, keys
|
|
129
|
+
* use dot notation (e.g., "workloads.production") with shorthand aliases
|
|
130
|
+
* for leaf names that don't collide with top-level keys.
|
|
40
131
|
*
|
|
41
132
|
* Idempotent — existing OUs are adopted, not duplicated.
|
|
42
133
|
*
|
|
43
|
-
*
|
|
134
|
+
* AWS cannot move OUs between parents. Migrating from flat to nested
|
|
135
|
+
* creates new OUs under new parents; old empty OUs remain at root.
|
|
44
136
|
*/
|
|
45
|
-
export async function ensureOrganisationalUnitsExist(client, rootId,
|
|
137
|
+
export async function ensureOrganisationalUnitsExist(client, rootId, ouConfig) {
|
|
46
138
|
try {
|
|
139
|
+
if (Array.isArray(ouConfig)) {
|
|
140
|
+
return await ensureFlatOUs(client, rootId, ouConfig);
|
|
141
|
+
}
|
|
47
142
|
const ouMap = {};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const match = existing.find((ou) => ou.Name === capitalised);
|
|
53
|
-
if (match?.Id) {
|
|
54
|
-
ouMap[ouName.toLowerCase()] = match.Id;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
// Create the OU
|
|
58
|
-
const response = await client.send(new CreateOrganizationalUnitCommand({
|
|
59
|
-
Name: capitalised,
|
|
60
|
-
ParentId: rootId
|
|
61
|
-
}));
|
|
62
|
-
const ou = response.OrganizationalUnit;
|
|
63
|
-
if (!ou?.Id) {
|
|
64
|
-
return failure(new Error(`OU "${capitalised}" was created but has no ID`));
|
|
65
|
-
}
|
|
66
|
-
ouMap[ouName.toLowerCase()] = ou.Id;
|
|
143
|
+
const topLevelKeys = new Set(Object.keys(ouConfig).map((k) => k.toLowerCase()));
|
|
144
|
+
const result = await ensureNestedOUs(client, rootId, ouConfig, ouMap, "", topLevelKeys);
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
return failure(result.error);
|
|
67
147
|
}
|
|
68
148
|
return success(ouMap);
|
|
69
149
|
}
|
|
70
150
|
catch (error) {
|
|
71
|
-
return failure(new Error(`Failed to ensure OUs exist: ${
|
|
151
|
+
return failure(new Error(`Failed to ensure OUs exist: ${getErrorMessage(error)}`));
|
|
72
152
|
}
|
|
73
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Build a flat map of accountName (lowercase) to OU ID from an OUTree
|
|
156
|
+
* and its resolved OUMap.
|
|
157
|
+
*
|
|
158
|
+
* Walks the tree recursively. For each leaf (string[]), maps each account
|
|
159
|
+
* name to the parent OU's ID using the dotted OUMap key.
|
|
160
|
+
*
|
|
161
|
+
* Callers must pass account names that match the tree leaf values.
|
|
162
|
+
*/
|
|
163
|
+
export function buildAccountToOUMap(tree, ouMap, prefix = "") {
|
|
164
|
+
const result = {};
|
|
165
|
+
for (const [ouName, children] of Object.entries(tree)) {
|
|
166
|
+
const dottedKey = prefix
|
|
167
|
+
? `${prefix}.${ouName.toLowerCase()}`
|
|
168
|
+
: ouName.toLowerCase();
|
|
169
|
+
const ouId = ouMap[dottedKey];
|
|
170
|
+
if (isOULeaf(children)) {
|
|
171
|
+
if (ouId) {
|
|
172
|
+
for (const accountName of children) {
|
|
173
|
+
result[accountName.toLowerCase()] = ouId;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
Object.assign(result, buildAccountToOUMap(children, ouMap, dottedKey));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
74
183
|
/**
|
|
75
184
|
* Place accounts into the correct organisational units.
|
|
76
185
|
* Skips accounts with environment "root" and accounts already in the target OU.
|
|
77
186
|
*
|
|
187
|
+
* When `accountToOU` is provided, looks up the target OU by account name
|
|
188
|
+
* (lowercase) instead of by environment. This supports tree-based placement
|
|
189
|
+
* where the OU is determined by which leaf the account appears in.
|
|
190
|
+
*
|
|
78
191
|
* Idempotent — accounts already in the correct OU are counted but not moved.
|
|
79
192
|
*/
|
|
80
|
-
export async function placeAccountsInOUs(client, ouMap, accounts) {
|
|
193
|
+
export async function placeAccountsInOUs(client, ouMap, accounts, accountToOU) {
|
|
81
194
|
try {
|
|
82
195
|
if (accounts.length === 0) {
|
|
83
196
|
return success({ moved: 0, alreadyPlaced: 0 });
|
|
@@ -88,13 +201,14 @@ export async function placeAccountsInOUs(client, ouMap, accounts) {
|
|
|
88
201
|
if (account.environment === "root") {
|
|
89
202
|
continue;
|
|
90
203
|
}
|
|
91
|
-
const targetOuId =
|
|
204
|
+
const targetOuId = accountToOU
|
|
205
|
+
? accountToOU[account.name.toLowerCase()]
|
|
206
|
+
: ouMap[account.environment.toLowerCase()];
|
|
92
207
|
if (!targetOuId) {
|
|
93
208
|
continue;
|
|
94
209
|
}
|
|
95
210
|
const currentParentId = await getParentId(client, account.id);
|
|
96
211
|
if (!currentParentId) {
|
|
97
|
-
// Account not found in organisation
|
|
98
212
|
continue;
|
|
99
213
|
}
|
|
100
214
|
if (currentParentId === targetOuId) {
|
|
@@ -106,12 +220,12 @@ export async function placeAccountsInOUs(client, ouMap, accounts) {
|
|
|
106
220
|
AccountId: account.id,
|
|
107
221
|
SourceParentId: currentParentId,
|
|
108
222
|
DestinationParentId: targetOuId
|
|
109
|
-
}));
|
|
223
|
+
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
110
224
|
moved++;
|
|
111
225
|
}
|
|
112
226
|
catch (error) {
|
|
113
227
|
const errorName = extractErrorName(error);
|
|
114
|
-
if (errorName ===
|
|
228
|
+
if (errorName === AWS_ERROR_NAMES.ACCOUNT_NOT_FOUND) {
|
|
115
229
|
continue;
|
|
116
230
|
}
|
|
117
231
|
throw error;
|
|
@@ -120,6 +234,6 @@ export async function placeAccountsInOUs(client, ouMap, accounts) {
|
|
|
120
234
|
return success({ moved, alreadyPlaced });
|
|
121
235
|
}
|
|
122
236
|
catch (error) {
|
|
123
|
-
return failure(new Error(`Failed to place accounts in OUs: ${
|
|
237
|
+
return failure(new Error(`Failed to place accounts in OUs: ${getErrorMessage(error)}`));
|
|
124
238
|
}
|
|
125
239
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EnablePolicyTypeCommand, PolicyType } from "@aws-sdk/client-organizations";
|
|
2
2
|
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import {
|
|
3
|
+
import { getErrorMessage } from "@fjall/util";
|
|
4
|
+
import { extractErrorName, SDK_TIMEOUT_MS } from "./types.js";
|
|
4
5
|
const POLICY_TYPES = [
|
|
5
6
|
PolicyType.SERVICE_CONTROL_POLICY,
|
|
6
7
|
PolicyType.TAG_POLICY,
|
|
@@ -18,7 +19,7 @@ export async function enablePolicyTypes(client, rootId) {
|
|
|
18
19
|
await client.send(new EnablePolicyTypeCommand({
|
|
19
20
|
RootId: rootId,
|
|
20
21
|
PolicyType: policyType
|
|
21
|
-
}));
|
|
22
|
+
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
22
23
|
}
|
|
23
24
|
catch (error) {
|
|
24
25
|
const errorName = extractErrorName(error);
|
|
@@ -31,6 +32,6 @@ export async function enablePolicyTypes(client, rootId) {
|
|
|
31
32
|
return success(undefined);
|
|
32
33
|
}
|
|
33
34
|
catch (error) {
|
|
34
|
-
return failure(new Error(`Failed to enable policy types: ${
|
|
35
|
+
return failure(new Error(`Failed to enable policy types: ${getErrorMessage(error)}`));
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { EnableSharingWithAwsOrganizationCommand } from "@aws-sdk/client-ram";
|
|
2
2
|
import { success, failure } from "@fjall/generator";
|
|
3
|
+
import { getErrorMessage } from "@fjall/util";
|
|
4
|
+
import { SDK_TIMEOUT_MS } from "./types.js";
|
|
3
5
|
/**
|
|
4
6
|
* Enable RAM sharing with the AWS Organisation.
|
|
5
7
|
* Idempotent — calling when already enabled is a no-op.
|
|
6
8
|
*/
|
|
7
9
|
export async function enableRamSharing(client) {
|
|
8
10
|
try {
|
|
9
|
-
await client.send(new EnableSharingWithAwsOrganizationCommand({})
|
|
11
|
+
await client.send(new EnableSharingWithAwsOrganizationCommand({}), {
|
|
12
|
+
abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
|
|
13
|
+
});
|
|
10
14
|
return success(undefined);
|
|
11
15
|
}
|
|
12
16
|
catch (error) {
|
|
13
|
-
return failure(new Error(`Failed to enable RAM sharing: ${
|
|
17
|
+
return failure(new Error(`Failed to enable RAM sharing: ${getErrorMessage(error)}`));
|
|
14
18
|
}
|
|
15
19
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { EnableAWSServiceAccessCommand } from "@aws-sdk/client-organizations";
|
|
2
2
|
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { extractErrorName } from "./types.js";
|
|
3
|
+
import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
|
|
4
|
+
import { getErrorMessage } from "@fjall/util";
|
|
4
5
|
const SERVICE_PRINCIPALS = [
|
|
5
6
|
"account.amazonaws.com",
|
|
6
7
|
"sso.amazonaws.com",
|
|
7
8
|
"ipam.amazonaws.com",
|
|
8
9
|
"ram.amazonaws.com",
|
|
9
10
|
"backup.amazonaws.com",
|
|
10
|
-
"member.org.stacksets.cloudformation.amazonaws.com"
|
|
11
|
+
"member.org.stacksets.cloudformation.amazonaws.com",
|
|
12
|
+
"guardduty.amazonaws.com",
|
|
13
|
+
"securityhub.amazonaws.com",
|
|
14
|
+
"config.amazonaws.com",
|
|
15
|
+
"inspector2.amazonaws.com",
|
|
16
|
+
"access-analyzer.amazonaws.com"
|
|
11
17
|
];
|
|
12
18
|
/**
|
|
13
19
|
* Enable AWS service access for all required service principals.
|
|
@@ -19,20 +25,20 @@ export async function enableServiceAccess(client) {
|
|
|
19
25
|
try {
|
|
20
26
|
await client.send(new EnableAWSServiceAccessCommand({
|
|
21
27
|
ServicePrincipal: principal
|
|
22
|
-
}));
|
|
28
|
+
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
23
29
|
}
|
|
24
30
|
catch (error) {
|
|
25
31
|
const errorName = extractErrorName(error);
|
|
26
|
-
if (errorName ===
|
|
32
|
+
if (errorName === AWS_ERROR_NAMES.ACCESS_DENIED) {
|
|
27
33
|
return failure(new Error(`Access denied when enabling service access for ${principal}. ` +
|
|
28
34
|
"Ensure your credentials have organizations:EnableAWSServiceAccess permission."));
|
|
29
35
|
}
|
|
30
|
-
throw error;
|
|
36
|
+
throw new Error(`Service principal ${principal}: ${getErrorMessage(error)}`);
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
return success(undefined);
|
|
34
40
|
}
|
|
35
41
|
catch (error) {
|
|
36
|
-
return failure(new Error(`Failed to enable service access: ${
|
|
42
|
+
return failure(new Error(`Failed to enable service access: ${getErrorMessage(error)}`));
|
|
37
43
|
}
|
|
38
44
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { ActivateOrganizationsAccessCommand } from "@aws-sdk/client-cloudformation";
|
|
2
2
|
import { success, failure } from "@fjall/generator";
|
|
3
|
+
import { getErrorMessage } from "@fjall/util";
|
|
4
|
+
import { SDK_TIMEOUT_MS } from "./types.js";
|
|
3
5
|
/**
|
|
4
6
|
* Activate trusted access for CloudFormation StackSets.
|
|
5
7
|
* Idempotent — calling when already activated is a no-op.
|
|
6
8
|
*/
|
|
7
9
|
export async function activateTrustedAccess(client) {
|
|
8
10
|
try {
|
|
9
|
-
await client.send(new ActivateOrganizationsAccessCommand({})
|
|
11
|
+
await client.send(new ActivateOrganizationsAccessCommand({}), {
|
|
12
|
+
abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
|
|
13
|
+
});
|
|
10
14
|
return success(undefined);
|
|
11
15
|
}
|
|
12
16
|
catch (error) {
|
|
13
|
-
return failure(new Error(`Failed to activate trusted access: ${
|
|
17
|
+
return failure(new Error(`Failed to activate trusted access: ${getErrorMessage(error)}`));
|
|
14
18
|
}
|
|
15
19
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared types for AWS Organisation setup primitives.
|
|
2
|
+
* Shared types and constants for AWS Organisation setup primitives.
|
|
3
3
|
*/
|
|
4
|
+
export declare const SDK_TIMEOUT_MS = 30000;
|
|
5
|
+
export declare const AWS_ERROR_NAMES: {
|
|
6
|
+
readonly ACCESS_DENIED: "AccessDeniedException";
|
|
7
|
+
readonly ACCOUNT_ALREADY_REGISTERED: "AccountAlreadyRegisteredException";
|
|
8
|
+
readonly ACCOUNT_NOT_FOUND: "AccountNotFoundException";
|
|
9
|
+
readonly CHILD_NOT_FOUND: "ChildNotFoundException";
|
|
10
|
+
readonly ORGS_NOT_IN_USE: "AWSOrganizationsNotInUseException";
|
|
11
|
+
};
|
|
4
12
|
export interface OrgDetails {
|
|
5
13
|
orgId: string;
|
|
6
14
|
rootId: string;
|
|
@@ -22,6 +30,20 @@ export interface IdentityCentreStatus {
|
|
|
22
30
|
enabled: boolean;
|
|
23
31
|
instanceCount: number;
|
|
24
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Recursive OU hierarchy. Each key is an OU name.
|
|
35
|
+
* - string[] value = account names to place in that OU (leaf node)
|
|
36
|
+
* - OUTree value = child OUs (branch node, no direct accounts)
|
|
37
|
+
*
|
|
38
|
+
* AWS cannot move OUs between parents. Migrating from a flat string[]
|
|
39
|
+
* to a nested OUTree creates new OUs under new parents; old empty OUs
|
|
40
|
+
* at the root level remain and must be removed manually.
|
|
41
|
+
*/
|
|
42
|
+
export type OUTree = {
|
|
43
|
+
[ouName: string]: string[] | OUTree;
|
|
44
|
+
};
|
|
45
|
+
/** Returns true if the value is a list of account names (leaf node). */
|
|
46
|
+
export declare function isOULeaf(value: string[] | OUTree): value is string[];
|
|
25
47
|
/**
|
|
26
48
|
* Extract the error name from an AWS SDK error.
|
|
27
49
|
* AWS SDK v3 errors have a `name` property that identifies the error type.
|
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared types for AWS Organisation setup primitives.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Extract the error name from an AWS SDK error.
|
|
6
|
-
* AWS SDK v3 errors have a `name` property that identifies the error type.
|
|
7
|
-
*/
|
|
8
|
-
export function extractErrorName(error) {
|
|
9
|
-
if (typeof error === "object" &&
|
|
10
|
-
error !== null &&
|
|
11
|
-
"name" in error &&
|
|
12
|
-
typeof error.name === "string") {
|
|
13
|
-
return error.name;
|
|
14
|
-
}
|
|
15
|
-
return "UnknownError";
|
|
16
|
-
}
|
|
1
|
+
const t=3e4,e={ACCESS_DENIED:"AccessDeniedException",ACCOUNT_ALREADY_REGISTERED:"AccountAlreadyRegisteredException",ACCOUNT_NOT_FOUND:"AccountNotFoundException",CHILD_NOT_FOUND:"ChildNotFoundException",ORGS_NOT_IN_USE:"AWSOrganizationsNotInUseException"};function o(n){return Array.isArray(n)}function E(n){return typeof n=="object"&&n!==null&&"name"in n&&typeof n.name=="string"?n.name:"UnknownError"}export{e as AWS_ERROR_NAMES,t as SDK_TIMEOUT_MS,E as extractErrorName,o as isOULeaf};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import type { EventLogWriter } from "../cloudformationEventTypes.js";
|
|
3
|
+
export declare function createMockEventLogger(): EventLogWriter;
|
|
4
|
+
export declare function createMockAwsClient(sendFn: ReturnType<typeof vi.fn>): {
|
|
5
|
+
getClient: import("vitest").Mock<(...args: any[]) => any>;
|
|
6
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{vi as e}from"vitest";function r(){return{writeEvent:e.fn(),writeDeploymentEnd:e.fn(),writeFailureSummary:e.fn(),writeFailureAnalysis:e.fn(),writeCdkOutput:e.fn(),getLogPath:e.fn(),getLogSummary:e.fn()}}function i(t){return{getClient:e.fn().mockReturnValue({send:t})}}export{i as createMockAwsClient,r as createMockEventLogger};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ResourceEvent } from "@fjall/util/aws";
|
|
2
|
+
import { type EventLogWriter, type EventFailureAnalyser, type AwsClientLike } from "./cloudformationEventTypes.js";
|
|
3
|
+
import type { FailureAnalysis } from "@fjall/util/aws";
|
|
4
|
+
export declare function isTerminalState(status: string): boolean;
|
|
5
|
+
export declare function isSuccessState(status: string): boolean;
|
|
6
|
+
export declare function isIpamResource(resourceType: string): boolean;
|
|
7
|
+
/** Tracks in-progress IPAM resources for concurrency diagnostics */
|
|
8
|
+
export declare class IpamConcurrencyTracker {
|
|
9
|
+
private inProgress;
|
|
10
|
+
get size(): number;
|
|
11
|
+
clear(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Process an IPAM resource event — logs concurrency info and writes
|
|
14
|
+
* diagnostic metadata to the event logger when provided.
|
|
15
|
+
*/
|
|
16
|
+
trackEvent(event: ResourceEvent, eventLogger: EventLogWriter | null): void;
|
|
17
|
+
}
|
|
18
|
+
/** Mutable state passed into pollStackEvents from the monitor class */
|
|
19
|
+
export interface PollContext {
|
|
20
|
+
aws: AwsClientLike;
|
|
21
|
+
seenEventIds: Set<string>;
|
|
22
|
+
activeNestedStacks: Map<string, string>;
|
|
23
|
+
failureReasons: Map<string, string>;
|
|
24
|
+
eventHistory: Map<string, ResourceEvent[]>;
|
|
25
|
+
maxHistorySize: number;
|
|
26
|
+
ipamTracker: IpamConcurrencyTracker;
|
|
27
|
+
eventLogger: EventLogWriter | null;
|
|
28
|
+
}
|
|
29
|
+
/** Poll CloudFormation for new stack events and dispatch them to the callback */
|
|
30
|
+
export declare function pollStackEvents(ctx: PollContext, stackName: string, onResourceUpdate: (event: ResourceEvent) => void): Promise<boolean | "throttled">;
|
|
31
|
+
/** Result of handling stack completion */
|
|
32
|
+
export interface StackCompletionResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
failureMessage?: string;
|
|
35
|
+
analysis: FailureAnalysis | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Handle stack completion — determines success/failure, logs results,
|
|
39
|
+
* and runs failure analysis. Called after pollEvents detects a terminal state.
|
|
40
|
+
*/
|
|
41
|
+
export declare function handleStackCompletion(aws: AwsClientLike, stackName: string, failureReasons: Map<string, string>, eventLogger: EventLogWriter | null, failureAnalyser: EventFailureAnalyser | null, eventHistory: Map<string, ResourceEvent[]>): Promise<StackCompletionResult>;
|
|
42
|
+
/** Fetch the current status of a CloudFormation stack */
|
|
43
|
+
export declare function fetchStackStatus(aws: AwsClientLike, stackName: string): Promise<{
|
|
44
|
+
status: string;
|
|
45
|
+
statusReason?: string;
|
|
46
|
+
} | null>;
|
|
47
|
+
/** Fetch the latest event for each resource in a stack */
|
|
48
|
+
export declare function fetchCurrentResources(aws: AwsClientLike, stackName: string): Promise<ResourceEvent[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CloudFormationClient as g,DescribeStackEventsCommand as y,DescribeStacksCommand as A}from"@aws-sdk/client-cloudformation";import{logger as m}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as C}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as f}from"@fjall/util/aws";import{CF_STACK_RESOURCE_TYPE as I}from"./cloudformationEventTypes.js";import{extractErrorName as P}from"../organisations/types.js";const L=["CREATE_COMPLETE","CREATE_FAILED","DELETE_COMPLETE","DELETE_FAILED","UPDATE_COMPLETE","UPDATE_FAILED","UPDATE_ROLLBACK_COMPLETE","UPDATE_ROLLBACK_FAILED","ROLLBACK_COMPLETE","ROLLBACK_FAILED","IMPORT_COMPLETE","IMPORT_ROLLBACK_COMPLETE","IMPORT_ROLLBACK_FAILED"],h=["CREATE_COMPLETE","UPDATE_COMPLETE","DELETE_COMPLETE","IMPORT_COMPLETE"];function k(s){return L.includes(s)}function K(s){return h.includes(s)}const O=new Set(["AWS::EC2::IPAMPool","AWS::EC2::IPAMPoolCidr","AWS::EC2::IPAM"]);function M(s){return O.has(s)}class b{inProgress=new Map;get size(){return this.inProgress.size}clear(){this.inProgress.clear()}trackEvent(e,i){if(e.status.includes("IN_PROGRESS")){this.inProgress.set(e.logicalId,{resourceType:e.resourceType,startTime:e.timestamp});const o=Array.from(this.inProgress.entries()).map(([a,r])=>`${a} (${r.resourceType.split("::").pop()}, started ${r.startTime.toISOString()})`);m.debug("CloudFormation","IPAM resource started",{logicalId:e.logicalId,resourceType:e.resourceType,concurrentIpamCount:this.inProgress.size,concurrentIpamResources:o}),i&&i.writeEvent(e,{phase:"ipam_concurrency",isMainStack:!0,parentStack:`concurrent=${this.inProgress.size}: ${o.join(", ")}`})}else if(e.status.includes("COMPLETE")||e.status.includes("FAILED")){const o=this.inProgress.has(e.logicalId),a=this.inProgress.get(e.logicalId);this.inProgress.delete(e.logicalId);const r=a?e.timestamp.getTime()-a.startTime.getTime():void 0;if(m.debug("CloudFormation",`IPAM resource ${e.status}`,{logicalId:e.logicalId,resourceType:e.resourceType,status:e.status,statusReason:e.statusReason,durationMs:r,wasTracked:o,remainingIpamCount:this.inProgress.size,remainingIpamResources:Array.from(this.inProgress.keys())}),e.status.includes("FAILED")&&i){const l=Array.from(this.inProgress.entries()).map(([n,c])=>({logicalId:n,resourceType:c.resourceType,startTime:c.startTime.toISOString(),inFlightDurationMs:e.timestamp.getTime()-c.startTime.getTime()}));i.writeEvent(e,{phase:"ipam_failure_snapshot",isMainStack:!0,parentStack:JSON.stringify({failedResource:e.logicalId,reason:e.statusReason,durationMs:r,concurrentIpamAtFailure:l})})}}}}async function v(s,e,i){try{const o=s.aws.getClient(g);let a,r=!1,l=!1;do{const n=new y({StackName:e,...a?{NextToken:a}:{}}),c=await o.send(n);if(!c.StackEvents)break;for(const t of c.StackEvents){const d=`${t.EventId}`;if(s.seenEventIds.has(d)){l=!0;continue}if(s.seenEventIds.add(d),t.ResourceType===I&&t.LogicalResourceId!==e){const E=t.ResourceStatus||"",T=t.LogicalResourceId||"";E.includes("IN_PROGRESS")?s.activeNestedStacks.set(T,E):(E.includes("COMPLETE")||E.includes("FAILED"))&&s.activeNestedStacks.delete(T)}if(t.LogicalResourceId===e&&t.ResourceType===I){const E=t.ResourceStatus||"";k(E)&&(r=!0)}t.ResourceStatus&&t.ResourceStatus.includes("FAILED")&&t.ResourceStatusReason&&(s.failureReasons.set(t.LogicalResourceId||"unknown",t.ResourceStatusReason),m.debug("CloudFormation",`Captured failure: ${t.LogicalResourceId} - ${t.ResourceStatusReason}`));const u={logicalId:t.LogicalResourceId||"",physicalId:t.PhysicalResourceId,resourceType:t.ResourceType||"",status:t.ResourceStatus||"",statusReason:t.ResourceStatusReason,timestamp:t.Timestamp||new Date},p=u.logicalId,R=s.eventHistory.get(p)??[];if(R.push(u),s.eventHistory.set(p,R),s.eventHistory.size>s.maxHistorySize){const E=[...s.eventHistory.keys()].slice(0,s.eventHistory.size-s.maxHistorySize);for(const T of E)s.eventHistory.delete(T)}M(u.resourceType)&&s.ipamTracker.trackEvent(u,s.eventLogger),s.eventLogger&&s.eventLogger.writeEvent(u),m.debug("CloudFormation","pollEvents delivering event",{stackName:e,logicalId:u.logicalId,status:u.status,resourceType:u.resourceType}),i(u)}a=l?void 0:c.NextToken}while(a);return r&&s.activeNestedStacks.size>0?!1:r}catch(o){const a=P(o),r=S(o);return a==="ValidationError"&&r.includes(f)?!1:a==="Throttling"||a==="TooManyRequestsException"||a==="ThrottlingException"?"throttled":(r.includes(f)||m.error("CloudFormation","Unexpected polling error",{error:C(r)}),!1)}}async function $(s,e,i,o,a,r){const l=await _(s,e),n=l?.status||"UNKNOWN",c=n.endsWith("_COMPLETE")&&!n.includes("ROLLBACK")&&!n.includes("FAILED");let t=l?.statusReason;!c&&i.size>0&&(t=(Array.from(i.values())[0]??t)||t);let d=null;return o&&(o.writeDeploymentEnd(c,n,t),!c&&i.size>0&&o.writeFailureSummary(Array.from(i.entries()).map(([u,p])=>({logicalId:u,reason:p}))),!c&&a&&(d=a.analyseFailure(r),d&&o.writeFailureAnalysis({rootCause:d.rootCause.reason,failedResources:d.affectedResources.map(u=>({logicalId:u.logicalId,resourceType:u.resourceType,reason:u.statusReason||"Unknown reason"})),dependencyChain:d.dependencyChain,remediation:d.remediation}))),{success:c,failureMessage:t,analysis:d}}async function _(s,e){try{const i=new A({StackName:e}),r=(await s.getClient(g).send(i)).Stacks?.[0];return r?{status:r.StackStatus||"UNKNOWN",statusReason:r.StackStatusReason}:null}catch(i){return m.debug("CloudFormation","getStackStatus failed",{error:S(i)}),null}}async function H(s,e){const i=[];try{const o=new y({StackName:e}),r=await s.getClient(g).send(o);if(!r.StackEvents)return i;const l=new Map;for(const n of r.StackEvents){const c=n.LogicalResourceId||"";if(c===e&&n.ResourceType===I)continue;const t=l.get(c);(!t||n.Timestamp&&t.timestamp<n.Timestamp)&&l.set(c,{logicalId:c,physicalId:n.PhysicalResourceId,resourceType:n.ResourceType||"",status:n.ResourceStatus||"",statusReason:n.ResourceStatusReason,timestamp:n.Timestamp||new Date})}return Array.from(l.values())}catch(o){const a=P(o),r=S(o);return a==="ValidationError"&&r.includes(f)||r.includes(f)||m.error("CloudFormation","Error getting current resources",{error:C(S(o))}),i}}export{b as IpamConcurrencyTracker,H as fetchCurrentResources,_ as fetchStackStatus,$ as handleStackCompletion,M as isIpamResource,K as isSuccessState,k as isTerminalState,v as pollStackEvents};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AwsSdkClientConstructor } from "../AwsProvider.js";
|
|
2
|
+
import type { ResourceEvent, FailureAnalysis } from "@fjall/util/aws";
|
|
3
|
+
/** Minimal AWS interface — only getClient() is needed for event monitoring */
|
|
4
|
+
export interface AwsClientLike {
|
|
5
|
+
getClient<T>(ClientClass: AwsSdkClientConstructor<T>): T;
|
|
6
|
+
}
|
|
7
|
+
/** Interface for event logging — decouples from concrete CloudFormationEventLogger */
|
|
8
|
+
export interface EventLogWriter {
|
|
9
|
+
writeEvent(event: ResourceEvent, metadata?: {
|
|
10
|
+
phase?: string;
|
|
11
|
+
isMainStack?: boolean;
|
|
12
|
+
parentStack?: string;
|
|
13
|
+
}): void;
|
|
14
|
+
writeDeploymentEnd(success: boolean, finalStatus: string, failureMessage?: string): void;
|
|
15
|
+
writeFailureSummary(failures: Array<{
|
|
16
|
+
logicalId: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
}>): void;
|
|
19
|
+
writeFailureAnalysis(analysis: {
|
|
20
|
+
rootCause: string;
|
|
21
|
+
failedResources: Array<{
|
|
22
|
+
logicalId: string;
|
|
23
|
+
resourceType: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}>;
|
|
26
|
+
dependencyChain?: string[];
|
|
27
|
+
remediation?: string[];
|
|
28
|
+
}): void;
|
|
29
|
+
writeCdkOutput(stream: "stdout" | "stderr", chunk: string): void;
|
|
30
|
+
getLogPath(): string;
|
|
31
|
+
getLogSummary(): string;
|
|
32
|
+
}
|
|
33
|
+
/** Interface for failure analysis — decouples from concrete CloudFormationFailureAnalyser */
|
|
34
|
+
export interface EventFailureAnalyser {
|
|
35
|
+
analyseFailure(eventHistory: Map<string, ResourceEvent[]>): FailureAnalysis | null;
|
|
36
|
+
}
|
|
37
|
+
/** Factory to create an EventLogWriter for a specific deployment */
|
|
38
|
+
export type EventLogWriterFactory = (deploymentId: string, stackName: string, region: string, deploymentName?: string) => EventLogWriter;
|
|
39
|
+
/** Dependencies injected into CloudFormationEventMonitor */
|
|
40
|
+
export interface EventMonitorDeps {
|
|
41
|
+
failureAnalyser?: EventFailureAnalyser;
|
|
42
|
+
eventLogWriterFactory?: EventLogWriterFactory;
|
|
43
|
+
}
|
|
44
|
+
/** CloudFormation resource type for nested stacks */
|
|
45
|
+
export declare const CF_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o="AWS::CloudFormation::Stack";export{o as CF_STACK_RESOURCE_TYPE};
|
|
@@ -1,53 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
export interface ResourceEvent {
|
|
6
|
-
logicalId: string;
|
|
7
|
-
physicalId?: string;
|
|
8
|
-
resourceType: string;
|
|
9
|
-
status: string;
|
|
10
|
-
statusReason?: string;
|
|
11
|
-
timestamp: Date;
|
|
12
|
-
}
|
|
13
|
-
export declare function isResourceEvent(event: unknown): event is ResourceEvent;
|
|
14
|
-
/** Interface for event logging — decouples from concrete CloudFormationEventLogger */
|
|
15
|
-
export interface EventLogWriter {
|
|
16
|
-
writeEvent(event: ResourceEvent, metadata?: {
|
|
17
|
-
phase?: string;
|
|
18
|
-
isMainStack?: boolean;
|
|
19
|
-
parentStack?: string;
|
|
20
|
-
}): void;
|
|
21
|
-
writeDeploymentEnd(success: boolean, finalStatus: string, failureMessage?: string): void;
|
|
22
|
-
writeFailureSummary(failures: Array<{
|
|
23
|
-
logicalId: string;
|
|
24
|
-
reason: string;
|
|
25
|
-
}>): void;
|
|
26
|
-
writeFailureAnalysis(analysis: {
|
|
27
|
-
rootCause: string;
|
|
28
|
-
failedResources: Array<{
|
|
29
|
-
logicalId: string;
|
|
30
|
-
resourceType: string;
|
|
31
|
-
reason: string;
|
|
32
|
-
}>;
|
|
33
|
-
dependencyChain?: string[];
|
|
34
|
-
remediation?: string[];
|
|
35
|
-
}): void;
|
|
36
|
-
writeCdkOutput(stream: "stdout" | "stderr", chunk: string): void;
|
|
37
|
-
getLogPath(): string;
|
|
38
|
-
getLogSummary(): string;
|
|
39
|
-
}
|
|
40
|
-
/** Interface for failure analysis — decouples from concrete CloudFormationFailureAnalyser */
|
|
41
|
-
export interface EventFailureAnalyser {
|
|
42
|
-
analyseFailure(eventHistory: Map<string, ResourceEvent[]>): FailureAnalysis | null;
|
|
43
|
-
}
|
|
44
|
-
/** Factory to create an EventLogWriter for a specific deployment */
|
|
45
|
-
export type EventLogWriterFactory = (deploymentId: string, stackName: string, region: string, deploymentName?: string) => EventLogWriter;
|
|
46
|
-
/** Dependencies injected into CloudFormationEventMonitor */
|
|
47
|
-
export interface EventMonitorDeps {
|
|
48
|
-
failureAnalyser?: EventFailureAnalyser;
|
|
49
|
-
eventLogWriterFactory?: EventLogWriterFactory;
|
|
50
|
-
}
|
|
1
|
+
import { type ResourceEvent, type FailureAnalysis } from "@fjall/util/aws";
|
|
2
|
+
export { type ResourceEvent, isResourceEvent, STACK_NOT_FOUND_PATTERN, CDK_NO_STACKS_MATCH, type FailureAnalysis } from "@fjall/util/aws";
|
|
3
|
+
export type { AwsClientLike, EventLogWriter, EventFailureAnalyser, EventLogWriterFactory, EventMonitorDeps } from "./cloudformationEventTypes.js";
|
|
4
|
+
import { type AwsClientLike, type EventLogWriter, type EventMonitorDeps } from "./cloudformationEventTypes.js";
|
|
51
5
|
export declare class CloudFormationEventMonitor {
|
|
52
6
|
private aws;
|
|
53
7
|
private seenEventIds;
|
|
@@ -63,12 +17,13 @@ export declare class CloudFormationEventMonitor {
|
|
|
63
17
|
private deploymentStartTime;
|
|
64
18
|
private maxHistorySize;
|
|
65
19
|
private maxSeenEventIds;
|
|
66
|
-
private
|
|
67
|
-
constructor(aws:
|
|
20
|
+
private ipamTracker;
|
|
21
|
+
constructor(aws: AwsClientLike, deps?: EventMonitorDeps);
|
|
68
22
|
enableLogging(deploymentId: string, stackName: string, region: string, deploymentName?: string): void;
|
|
69
23
|
startMonitoring(stackName: string, onResourceUpdate: (event: ResourceEvent) => void, onStackComplete?: (success: boolean, message?: string) => void): Promise<void>;
|
|
70
24
|
stopMonitoring(): void;
|
|
71
25
|
private cleanup;
|
|
26
|
+
private handleStackComplete;
|
|
72
27
|
getResourceHistory(logicalId: string): ResourceEvent[];
|
|
73
28
|
getEventHistory(): Map<string, ResourceEvent[]>;
|
|
74
29
|
getFailureAnalysis(): FailureAnalysis | null;
|
|
@@ -87,8 +42,7 @@ export declare class CloudFormationEventMonitor {
|
|
|
87
42
|
failureReason?: string;
|
|
88
43
|
logPath?: string;
|
|
89
44
|
}>;
|
|
90
|
-
private
|
|
91
|
-
private isSuccessState;
|
|
45
|
+
private pollContext;
|
|
92
46
|
private pollEvents;
|
|
93
47
|
getStackStatus(stackName: string): Promise<{
|
|
94
48
|
status: string;
|