@fjall/deploy-core 0.94.1 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/aws/organisations/accounts.js +1 -99
- package/dist/src/aws/organisations/backup.js +1 -30
- package/dist/src/aws/organisations/costAllocation.js +1 -28
- package/dist/src/aws/organisations/delegatedAdmin.js +3 -43
- package/dist/src/aws/organisations/identityCentre.js +1 -23
- package/dist/src/aws/organisations/ipam.js +1 -20
- package/dist/src/aws/organisations/organisation.js +1 -103
- package/dist/src/aws/organisations/organisationalUnits.js +1 -239
- package/dist/src/aws/organisations/policies.js +1 -37
- package/dist/src/aws/organisations/ram.js +1 -19
- package/dist/src/aws/organisations/serviceAccess.js +1 -44
- package/dist/src/aws/organisations/trustedAccess.js +1 -19
- package/dist/src/aws/utils/regions.js +1 -1
- package/dist/src/index.js +1 -65
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +1 -78
- package/dist/src/orchestration/activeDeploymentGuard.js +5 -39
- package/dist/src/orchestration/applicationDeploy.js +1 -149
- package/dist/src/orchestration/applicationDeployHelpers.js +4 -223
- package/dist/src/orchestration/applicationDestroy.js +1 -131
- package/dist/src/orchestration/builders/dockerBuilder.js +1 -98
- package/dist/src/orchestration/builders/openNextBuilder.js +1 -144
- package/dist/src/orchestration/cascadeHelpers.js +1 -160
- package/dist/src/orchestration/contextHelpers.js +1 -107
- package/dist/src/orchestration/deploy.js +1 -42
- package/dist/src/orchestration/destroy.js +1 -67
- package/dist/src/orchestration/detectionPipeline.js +1 -84
- package/dist/src/orchestration/dockerBuildHelper.js +1 -49
- package/dist/src/orchestration/dockerInterface.js +0 -1
- package/dist/src/orchestration/domainInterface.js +0 -1
- package/dist/src/orchestration/openNextBuild.js +3 -243
- package/dist/src/orchestration/organisationDeploy.js +3 -284
- package/dist/src/orchestration/organisationDestroy.js +3 -189
- package/dist/src/orchestration/organisationSetup.js +1 -247
- package/dist/src/orchestration/resolveOperation.js +1 -123
- package/dist/src/orchestration/welcomeImageHelper.js +1 -64
- package/dist/src/services/application/ApplicationStackService.js +1 -218
- package/dist/src/services/application/applicationStackHelpers.js +4 -248
- package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -244
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -125
- package/dist/src/services/infrastructure/CdkProcessManager.js +3 -278
- package/dist/src/services/infrastructure/CdkService.js +3 -213
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -248
- package/dist/src/services/infrastructure/ICdkProcessManager.js +0 -1
- package/dist/src/services/supporting/CdkContextBuilder.js +1 -44
- package/dist/src/services/supporting/TemplateHashService.js +1 -152
- package/dist/src/steps/stepRegistry.js +1 -505
- package/dist/src/types/apiClient.js +0 -1
- package/dist/src/types/detection.js +0 -1
- package/dist/src/types/frameworkBuilder.js +0 -8
- package/dist/src/types/params.js +0 -1
- package/dist/src/types/patternDetection.js +1 -88
- package/dist/src/types/stepDefinitions.js +1 -98
- package/package.json +4 -4
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
114 files minified at 2026-04-
|
|
1
|
+
114 files minified at 2026-04-27T00:08:20.549Z
|
|
@@ -1,99 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { sleep, getErrorMessage } from "@fjall/util";
|
|
4
|
-
import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
|
|
5
|
-
/**
|
|
6
|
-
* List all accounts in the organisation, handling pagination.
|
|
7
|
-
*/
|
|
8
|
-
export async function listAccounts(client) {
|
|
9
|
-
try {
|
|
10
|
-
const response = await client.send(new ListAccountsCommand({ MaxResults: 20 }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
11
|
-
let accounts = response.Accounts ?? [];
|
|
12
|
-
let nextToken = response.NextToken;
|
|
13
|
-
while (nextToken) {
|
|
14
|
-
const nextResponse = await client.send(new ListAccountsCommand({
|
|
15
|
-
MaxResults: 20,
|
|
16
|
-
NextToken: nextToken
|
|
17
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
18
|
-
accounts = accounts.concat(nextResponse.Accounts ?? []);
|
|
19
|
-
nextToken = nextResponse.NextToken;
|
|
20
|
-
}
|
|
21
|
-
return success(accounts);
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
const errorName = extractErrorName(error);
|
|
25
|
-
if (errorName === AWS_ERROR_NAMES.ORGS_NOT_IN_USE) {
|
|
26
|
-
return failure(new Error("AWS Organisations is not enabled for this account"));
|
|
27
|
-
}
|
|
28
|
-
return failure(new Error(`Failed to list accounts: ${getErrorMessage(error)}`));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Find an account by ID in the organisation.
|
|
33
|
-
*/
|
|
34
|
-
export async function findAccount(client, accountId) {
|
|
35
|
-
const listResult = await listAccounts(client);
|
|
36
|
-
if (!listResult.success) {
|
|
37
|
-
return listResult;
|
|
38
|
-
}
|
|
39
|
-
return success(listResult.data.find((acc) => acc.Id === accountId));
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Create an AWS account and poll until creation completes.
|
|
43
|
-
*
|
|
44
|
-
* @param maxAttempts Maximum polling attempts (default: 180 = ~15 minutes at 5s intervals)
|
|
45
|
-
* @param delayMs Delay between polling attempts in milliseconds (default: 5000)
|
|
46
|
-
*/
|
|
47
|
-
export async function createAccount(client, accountName, email, maxAttempts = 180, delayMs = 5000) {
|
|
48
|
-
try {
|
|
49
|
-
let response;
|
|
50
|
-
try {
|
|
51
|
-
response = await client.send(new CreateAccountCommand({
|
|
52
|
-
AccountName: accountName,
|
|
53
|
-
Email: email
|
|
54
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
const errorName = extractErrorName(error);
|
|
58
|
-
if (errorName === AWS_ERROR_NAMES.ACCESS_DENIED) {
|
|
59
|
-
return failure(new Error(`Access denied when creating account "${accountName}". ` +
|
|
60
|
-
"Ensure your credentials have organizations:CreateAccount permission."));
|
|
61
|
-
}
|
|
62
|
-
if (errorName === "DuplicateAccountException") {
|
|
63
|
-
return failure(new Error(`An account with the email "${email}" already exists in the organisation.`));
|
|
64
|
-
}
|
|
65
|
-
if (errorName === "FinalizingOrganizationException") {
|
|
66
|
-
return failure(new Error("The organisation is still being initialised. Please wait and try again."));
|
|
67
|
-
}
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
const requestId = response.CreateAccountStatus?.Id;
|
|
71
|
-
if (!requestId) {
|
|
72
|
-
return failure(new Error(`CreateAccount request for "${accountName}" did not return a status ID`));
|
|
73
|
-
}
|
|
74
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
75
|
-
const statusResponse = await client.send(new DescribeCreateAccountStatusCommand({
|
|
76
|
-
CreateAccountRequestId: requestId
|
|
77
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
78
|
-
const status = statusResponse.CreateAccountStatus;
|
|
79
|
-
if (!status) {
|
|
80
|
-
return failure(new Error(`No status returned for CreateAccount request ${requestId}`));
|
|
81
|
-
}
|
|
82
|
-
if (status.State === CreateAccountState.SUCCEEDED) {
|
|
83
|
-
if (!status.AccountId) {
|
|
84
|
-
return failure(new Error(`Account creation succeeded but no account ID returned for "${accountName}"`));
|
|
85
|
-
}
|
|
86
|
-
return success({ accountId: status.AccountId, accountName });
|
|
87
|
-
}
|
|
88
|
-
if (status.State === CreateAccountState.FAILED) {
|
|
89
|
-
return failure(new Error(`Account creation failed for "${accountName}": ${status.FailureReason}`));
|
|
90
|
-
}
|
|
91
|
-
// Still IN_PROGRESS
|
|
92
|
-
await sleep(delayMs);
|
|
93
|
-
}
|
|
94
|
-
return failure(new Error(`Account creation for "${accountName}" timed out after ${(maxAttempts * delayMs) / 1000} seconds`));
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
return failure(new Error(`Failed to create account "${accountName}": ${getErrorMessage(error)}`));
|
|
98
|
-
}
|
|
99
|
-
}
|
|
1
|
+
import{ListAccountsCommand as f,CreateAccountCommand as m,CreateAccountState as E,DescribeCreateAccountStatusCommand as p}from"@aws-sdk/client-organizations";import{success as A,failure as e}from"@fjall/generator";import{sleep as C,getErrorMessage as w}from"@fjall/util";import{extractErrorName as S,SDK_TIMEOUT_MS as d,AWS_ERROR_NAMES as g}from"./types.js";async function x(o){try{const t=await o.send(new f({MaxResults:20}),{abortSignal:AbortSignal.timeout(d)});let r=t.Accounts??[],n=t.NextToken;for(;n;){const s=await o.send(new f({MaxResults:20,NextToken:n}),{abortSignal:AbortSignal.timeout(d)});r=r.concat(s.Accounts??[]),n=s.NextToken}return A(r)}catch(t){return S(t)===g.ORGS_NOT_IN_USE?e(new Error("AWS Organisations is not enabled for this account")):e(new Error(`Failed to list accounts: ${w(t)}`))}}async function $(o,t){const r=await x(o);return r.success?A(r.data.find(n=>n.Id===t)):r}async function D(o,t,r,n=180,s=5e3){try{let i;try{i=await o.send(new m({AccountName:t,Email:r}),{abortSignal:AbortSignal.timeout(d)})}catch(c){const u=S(c);if(u===g.ACCESS_DENIED)return e(new Error(`Access denied when creating account "${t}". Ensure your credentials have organizations:CreateAccount permission.`));if(u==="DuplicateAccountException")return e(new Error(`An account with the email "${r}" already exists in the organisation.`));if(u==="FinalizingOrganizationException")return e(new Error("The organisation is still being initialised. Please wait and try again."));throw c}const l=i.CreateAccountStatus?.Id;if(!l)return e(new Error(`CreateAccount request for "${t}" did not return a status ID`));for(let c=0;c<n;c++){const a=(await o.send(new p({CreateAccountRequestId:l}),{abortSignal:AbortSignal.timeout(d)})).CreateAccountStatus;if(!a)return e(new Error(`No status returned for CreateAccount request ${l}`));if(a.State===E.SUCCEEDED)return a.AccountId?A({accountId:a.AccountId,accountName:t}):e(new Error(`Account creation succeeded but no account ID returned for "${t}"`));if(a.State===E.FAILED)return e(new Error(`Account creation failed for "${t}": ${a.FailureReason}`));await C(s)}return e(new Error(`Account creation for "${t}" timed out after ${n*s/1e3} seconds`))}catch(i){return e(new Error(`Failed to create account "${t}": ${w(i)}`))}}export{D as createAccount,$ as findAccount,x as listAccounts};
|
|
@@ -1,30 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { getErrorMessage } from "@fjall/util";
|
|
4
|
-
import { SDK_TIMEOUT_MS } from "./types.js";
|
|
5
|
-
const DEFAULT_BACKUP_SETTINGS = {
|
|
6
|
-
enableCrossAccountBackup: true,
|
|
7
|
-
enableDelegatedAdministrator: true,
|
|
8
|
-
enableMpa: false
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Update AWS Backup global settings for the organisation.
|
|
12
|
-
* Idempotent — safe to call repeatedly with the same settings.
|
|
13
|
-
*/
|
|
14
|
-
export async function updateBackupGlobalSettings(client, settings) {
|
|
15
|
-
try {
|
|
16
|
-
const finalSettings = { ...DEFAULT_BACKUP_SETTINGS, ...settings };
|
|
17
|
-
const globalSettings = {
|
|
18
|
-
isCrossAccountBackupEnabled: finalSettings.enableCrossAccountBackup.toString(),
|
|
19
|
-
isDelegatedAdministratorEnabled: finalSettings.enableDelegatedAdministrator.toString(),
|
|
20
|
-
isMpaEnabled: finalSettings.enableMpa.toString()
|
|
21
|
-
};
|
|
22
|
-
await client.send(new UpdateGlobalSettingsCommand({
|
|
23
|
-
GlobalSettings: globalSettings
|
|
24
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
25
|
-
return success(undefined);
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
return failure(new Error(`Failed to update backup global settings: ${getErrorMessage(error)}`));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
import{UpdateGlobalSettingsCommand as r}from"@aws-sdk/client-backup";import{success as o,failure as i}from"@fjall/generator";import{getErrorMessage as s}from"@fjall/util";import{SDK_TIMEOUT_MS as l}from"./types.js";const c={enableCrossAccountBackup:!0,enableDelegatedAdministrator:!0,enableMpa:!1};async function p(e,a){try{const t={...c,...a},n={isCrossAccountBackupEnabled:t.enableCrossAccountBackup.toString(),isDelegatedAdministratorEnabled:t.enableDelegatedAdministrator.toString(),isMpaEnabled:t.enableMpa.toString()};return await e.send(new r({GlobalSettings:n}),{abortSignal:AbortSignal.timeout(l)}),o(void 0)}catch(t){return i(new Error(`Failed to update backup global settings: ${s(t)}`))}}export{p as updateBackupGlobalSettings};
|
|
@@ -1,28 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { getErrorMessage } from "@fjall/util";
|
|
4
|
-
import { SDK_TIMEOUT_MS } from "./types.js";
|
|
5
|
-
/**
|
|
6
|
-
* Activate cost allocation tags for billing tracking.
|
|
7
|
-
* Idempotent — activating already-active tags is a no-op.
|
|
8
|
-
*
|
|
9
|
-
* Returns success with no action if the tags array is empty.
|
|
10
|
-
*/
|
|
11
|
-
export async function activateCostAllocationTags(client, tags) {
|
|
12
|
-
if (tags.length === 0) {
|
|
13
|
-
return success(undefined);
|
|
14
|
-
}
|
|
15
|
-
try {
|
|
16
|
-
const costAllocationTagsStatus = tags.map((tag) => ({
|
|
17
|
-
TagKey: tag.TagKey,
|
|
18
|
-
Status: "Active"
|
|
19
|
-
}));
|
|
20
|
-
await client.send(new UpdateCostAllocationTagsStatusCommand({
|
|
21
|
-
CostAllocationTagsStatus: costAllocationTagsStatus
|
|
22
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
23
|
-
return success(undefined);
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
return failure(new Error(`Failed to activate cost allocation tags: ${getErrorMessage(error)}`));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
import{UpdateCostAllocationTagsStatusCommand as n}from"@aws-sdk/client-cost-explorer";import{success as a,failure as i}from"@fjall/generator";import{getErrorMessage as s}from"@fjall/util";import{SDK_TIMEOUT_MS as c}from"./types.js";async function f(r,o){if(o.length===0)return a(void 0);try{const t=o.map(e=>({TagKey:e.TagKey,Status:"Active"}));return await r.send(new n({CostAllocationTagsStatus:t}),{abortSignal:AbortSignal.timeout(c)}),a(void 0)}catch(t){return i(new Error(`Failed to activate cost allocation tags: ${s(t)}`))}}export{f as activateCostAllocationTags};
|
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { getErrorMessage } from "@fjall/util";
|
|
5
|
-
const SECURITY_SERVICE_PRINCIPALS = [
|
|
6
|
-
"guardduty.amazonaws.com",
|
|
7
|
-
"securityhub.amazonaws.com",
|
|
8
|
-
"config.amazonaws.com",
|
|
9
|
-
"inspector2.amazonaws.com",
|
|
10
|
-
"access-analyzer.amazonaws.com"
|
|
11
|
-
];
|
|
12
|
-
/**
|
|
13
|
-
* Register delegated administrators for security services.
|
|
14
|
-
* Idempotent — already-registered delegates are silently skipped.
|
|
15
|
-
*/
|
|
16
|
-
export async function registerSecurityDelegates(client, delegateAccountId, services = SECURITY_SERVICE_PRINCIPALS) {
|
|
17
|
-
const errors = [];
|
|
18
|
-
for (const service of services) {
|
|
19
|
-
try {
|
|
20
|
-
await client.send(new RegisterDelegatedAdministratorCommand({
|
|
21
|
-
AccountId: delegateAccountId,
|
|
22
|
-
ServicePrincipal: service
|
|
23
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
const errorName = extractErrorName(error);
|
|
27
|
-
if (errorName === AWS_ERROR_NAMES.ACCOUNT_ALREADY_REGISTERED) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (errorName === AWS_ERROR_NAMES.ACCESS_DENIED) {
|
|
31
|
-
const prefix = errors.length > 0 ? `${errors.join("; ")}; ` : "";
|
|
32
|
-
return failure(new Error(`${prefix}Access denied when registering delegated admin for ${service}. ` +
|
|
33
|
-
"Ensure your credentials have organizations:RegisterDelegatedAdministrator permission."));
|
|
34
|
-
}
|
|
35
|
-
errors.push(`${service}: ${getErrorMessage(error)}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (errors.length > 0) {
|
|
39
|
-
return failure(new Error(`Failed to register security delegates for account ${delegateAccountId}:\n ${errors.join("\n ")}`));
|
|
40
|
-
}
|
|
41
|
-
return success(undefined);
|
|
42
|
-
}
|
|
43
|
-
export { SECURITY_SERVICE_PRINCIPALS };
|
|
1
|
+
import{RegisterDelegatedAdministratorCommand as u}from"@aws-sdk/client-organizations";import{success as E,failure as a}from"@fjall/generator";import{extractErrorName as d,SDK_TIMEOUT_MS as f,AWS_ERROR_NAMES as i}from"./types.js";import{getErrorMessage as S}from"@fjall/util";const s=["guardduty.amazonaws.com","securityhub.amazonaws.com","config.amazonaws.com","inspector2.amazonaws.com","access-analyzer.amazonaws.com"];async function w(c,o,m=s){const r=[];for(const e of m)try{await c.send(new u({AccountId:o,ServicePrincipal:e}),{abortSignal:AbortSignal.timeout(f)})}catch(t){const n=d(t);if(n===i.ACCOUNT_ALREADY_REGISTERED)continue;if(n===i.ACCESS_DENIED){const g=r.length>0?`${r.join("; ")}; `:"";return a(new Error(`${g}Access denied when registering delegated admin for ${e}. Ensure your credentials have organizations:RegisterDelegatedAdministrator permission.`))}r.push(`${e}: ${S(t)}`)}return r.length>0?a(new Error(`Failed to register security delegates for account ${o}:
|
|
2
|
+
${r.join(`
|
|
3
|
+
`)}`)):E(void 0)}export{s as SECURITY_SERVICE_PRINCIPALS,w as registerSecurityDelegates};
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { getErrorMessage } from "@fjall/util";
|
|
4
|
-
import { SDK_TIMEOUT_MS } from "./types.js";
|
|
5
|
-
/**
|
|
6
|
-
* Check if AWS Identity Centre (SSO) is enabled.
|
|
7
|
-
* Returns the number of SSO instances found.
|
|
8
|
-
*/
|
|
9
|
-
export async function checkIdentityCentreStatus(client) {
|
|
10
|
-
try {
|
|
11
|
-
const response = await client.send(new ListInstancesCommand({}), {
|
|
12
|
-
abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
|
|
13
|
-
});
|
|
14
|
-
const instances = response.Instances ?? [];
|
|
15
|
-
return success({
|
|
16
|
-
enabled: instances.length > 0,
|
|
17
|
-
instanceCount: instances.length
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
return failure(new Error(`Failed to check Identity Centre status: ${getErrorMessage(error)}`));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
import{ListInstancesCommand as r}from"@aws-sdk/client-sso-admin";import{success as s,failure as o}from"@fjall/generator";import{getErrorMessage as a}from"@fjall/util";import{SDK_TIMEOUT_MS as c}from"./types.js";async function p(n){try{const e=(await n.send(new r({}),{abortSignal:AbortSignal.timeout(c)})).Instances??[];return s({enabled:e.length>0,instanceCount:e.length})}catch(t){return o(new Error(`Failed to check Identity Centre status: ${a(t)}`))}}export{p as checkIdentityCentreStatus};
|
|
@@ -1,20 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { getErrorMessage } from "@fjall/util";
|
|
4
|
-
import { SDK_TIMEOUT_MS } from "./types.js";
|
|
5
|
-
/**
|
|
6
|
-
* Enable IPAM delegated administrator for the given account.
|
|
7
|
-
* Idempotent — calling when already delegated is a no-op.
|
|
8
|
-
*/
|
|
9
|
-
export async function enableIpamDelegatedAdmin(client, delegatedAdminAccountId) {
|
|
10
|
-
try {
|
|
11
|
-
await client.send(new EnableIpamOrganizationAdminAccountCommand({
|
|
12
|
-
DryRun: false,
|
|
13
|
-
DelegatedAdminAccountId: delegatedAdminAccountId
|
|
14
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
15
|
-
return success(undefined);
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
return failure(new Error(`Failed to enable IPAM delegation for account ${delegatedAdminAccountId}: ${getErrorMessage(error)}`));
|
|
19
|
-
}
|
|
20
|
-
}
|
|
1
|
+
import{EnableIpamOrganizationAdminAccountCommand as o}from"@aws-sdk/client-ec2";import{success as a,failure as t}from"@fjall/generator";import{getErrorMessage as i}from"@fjall/util";import{SDK_TIMEOUT_MS as m}from"./types.js";async function d(e,r){try{return await e.send(new o({DryRun:!1,DelegatedAdminAccountId:r}),{abortSignal:AbortSignal.timeout(m)}),a(void 0)}catch(n){return t(new Error(`Failed to enable IPAM delegation for account ${r}: ${i(n)}`))}}export{d as enableIpamDelegatedAdmin};
|
|
@@ -1,103 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { getErrorMessage } from "@fjall/util";
|
|
4
|
-
import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
|
|
5
|
-
/**
|
|
6
|
-
* Ensure an AWS Organisation exists, creating one if necessary.
|
|
7
|
-
* Idempotent — safe to call when the organisation already exists.
|
|
8
|
-
*/
|
|
9
|
-
export async function ensureOrganisationExists(client) {
|
|
10
|
-
try {
|
|
11
|
-
let created = false;
|
|
12
|
-
// Try to describe existing organisation
|
|
13
|
-
let org = await describeOrganisationRaw(client);
|
|
14
|
-
if (!org) {
|
|
15
|
-
// Create new organisation with all features
|
|
16
|
-
try {
|
|
17
|
-
const createResponse = await client.send(new CreateOrganizationCommand({ FeatureSet: "ALL" }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
18
|
-
org = createResponse.Organization ?? null;
|
|
19
|
-
created = true;
|
|
20
|
-
}
|
|
21
|
-
catch (createError) {
|
|
22
|
-
const errorName = extractErrorName(createError);
|
|
23
|
-
if (errorName === "AlreadyInOrganizationException") {
|
|
24
|
-
// Race condition — another process created it
|
|
25
|
-
org = await describeOrganisationRaw(client);
|
|
26
|
-
if (!org) {
|
|
27
|
-
return failure(new Error("Account is already in an organisation but DescribeOrganization returned nothing"));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
throw createError;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (!org?.Id || !org.MasterAccountId) {
|
|
36
|
-
return failure(new Error("Organisation is missing required fields (Id or MasterAccountId)"));
|
|
37
|
-
}
|
|
38
|
-
// Get root ID
|
|
39
|
-
const rootResult = await getOrganisationRootId(client);
|
|
40
|
-
if (!rootResult.success)
|
|
41
|
-
return rootResult;
|
|
42
|
-
return success({
|
|
43
|
-
orgId: org.Id,
|
|
44
|
-
rootId: rootResult.data,
|
|
45
|
-
managementAccountId: org.MasterAccountId,
|
|
46
|
-
created
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
return failure(new Error(`Failed to ensure organisation exists: ${getErrorMessage(error)}`));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Describe the current AWS Organisation, returning null if none exists.
|
|
55
|
-
*/
|
|
56
|
-
export async function describeOrganisation(client) {
|
|
57
|
-
try {
|
|
58
|
-
const org = await describeOrganisationRaw(client);
|
|
59
|
-
if (!org) {
|
|
60
|
-
return success(null);
|
|
61
|
-
}
|
|
62
|
-
if (!org.Id || !org.MasterAccountId) {
|
|
63
|
-
return failure(new Error("Organisation is missing required fields (Id or MasterAccountId)"));
|
|
64
|
-
}
|
|
65
|
-
const rootResult = await getOrganisationRootId(client);
|
|
66
|
-
if (!rootResult.success)
|
|
67
|
-
return rootResult;
|
|
68
|
-
return success({
|
|
69
|
-
orgId: org.Id,
|
|
70
|
-
rootId: rootResult.data,
|
|
71
|
-
managementAccountId: org.MasterAccountId,
|
|
72
|
-
created: false
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
return failure(new Error(`Failed to describe organisation: ${getErrorMessage(error)}`));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async function getOrganisationRootId(client) {
|
|
80
|
-
const rootsResponse = await client.send(new ListRootsCommand({}), {
|
|
81
|
-
abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
|
|
82
|
-
});
|
|
83
|
-
const roots = rootsResponse.Roots ?? [];
|
|
84
|
-
if (roots.length === 0 || !roots[0]?.Id) {
|
|
85
|
-
return failure(new Error("No organisation root found"));
|
|
86
|
-
}
|
|
87
|
-
return success(roots[0].Id);
|
|
88
|
-
}
|
|
89
|
-
async function describeOrganisationRaw(client) {
|
|
90
|
-
try {
|
|
91
|
-
const response = await client.send(new DescribeOrganizationCommand({}), {
|
|
92
|
-
abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
|
|
93
|
-
});
|
|
94
|
-
return response.Organization ?? null;
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
const errorName = extractErrorName(error);
|
|
98
|
-
if (errorName === AWS_ERROR_NAMES.ORGS_NOT_IN_USE) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
1
|
+
import{DescribeOrganizationCommand as m,CreateOrganizationCommand as l,ListRootsCommand as f}from"@aws-sdk/client-organizations";import{success as n,failure as o}from"@fjall/generator";import{getErrorMessage as u}from"@fjall/util";import{extractErrorName as d,SDK_TIMEOUT_MS as s,AWS_ERROR_NAMES as I}from"./types.js";async function p(e){try{let r=!1,t=await c(e);if(!t)try{t=(await e.send(new l({FeatureSet:"ALL"}),{abortSignal:AbortSignal.timeout(s)})).Organization??null,r=!0}catch(i){if(d(i)==="AlreadyInOrganizationException"){if(t=await c(e),!t)return o(new Error("Account is already in an organisation but DescribeOrganization returned nothing"))}else throw i}if(!t?.Id||!t.MasterAccountId)return o(new Error("Organisation is missing required fields (Id or MasterAccountId)"));const a=await g(e);return a.success?n({orgId:t.Id,rootId:a.data,managementAccountId:t.MasterAccountId,created:r}):a}catch(r){return o(new Error(`Failed to ensure organisation exists: ${u(r)}`))}}async function S(e){try{const r=await c(e);if(!r)return n(null);if(!r.Id||!r.MasterAccountId)return o(new Error("Organisation is missing required fields (Id or MasterAccountId)"));const t=await g(e);return t.success?n({orgId:r.Id,rootId:t.data,managementAccountId:r.MasterAccountId,created:!1}):t}catch(r){return o(new Error(`Failed to describe organisation: ${u(r)}`))}}async function g(e){const t=(await e.send(new f({}),{abortSignal:AbortSignal.timeout(s)})).Roots??[];return t.length===0||!t[0]?.Id?o(new Error("No organisation root found")):n(t[0].Id)}async function c(e){try{return(await e.send(new m({}),{abortSignal:AbortSignal.timeout(s)})).Organization??null}catch(r){if(d(r)===I.ORGS_NOT_IN_USE)return null;throw r}}export{S as describeOrganisation,p as ensureOrganisationExists};
|
|
@@ -1,239 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { extractErrorName, isOULeaf, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
|
|
4
|
-
import { getErrorMessage } from "@fjall/util";
|
|
5
|
-
/**
|
|
6
|
-
* List all OUs under a parent, handling pagination.
|
|
7
|
-
*/
|
|
8
|
-
async function listOUsForParent(client, parentId) {
|
|
9
|
-
const response = await client.send(new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
10
|
-
let ous = response.OrganizationalUnits ?? [];
|
|
11
|
-
let nextToken = response.NextToken;
|
|
12
|
-
while (nextToken) {
|
|
13
|
-
const nextResponse = await client.send(new ListOrganizationalUnitsForParentCommand({
|
|
14
|
-
ParentId: parentId,
|
|
15
|
-
NextToken: nextToken
|
|
16
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
17
|
-
ous = ous.concat(nextResponse.OrganizationalUnits ?? []);
|
|
18
|
-
nextToken = nextResponse.NextToken;
|
|
19
|
-
}
|
|
20
|
-
return ous;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Get the parent ID for a child (account or OU).
|
|
24
|
-
*/
|
|
25
|
-
async function getParentId(client, childId) {
|
|
26
|
-
try {
|
|
27
|
-
const response = await client.send(new ListParentsCommand({ ChildId: childId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
28
|
-
return response.Parents?.[0]?.Id;
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
const errorName = extractErrorName(error);
|
|
32
|
-
if (errorName === AWS_ERROR_NAMES.CHILD_NOT_FOUND) {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
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.
|
|
131
|
-
*
|
|
132
|
-
* Idempotent — existing OUs are adopted, not duplicated.
|
|
133
|
-
*
|
|
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.
|
|
136
|
-
*/
|
|
137
|
-
export async function ensureOrganisationalUnitsExist(client, rootId, ouConfig) {
|
|
138
|
-
try {
|
|
139
|
-
if (Array.isArray(ouConfig)) {
|
|
140
|
-
return await ensureFlatOUs(client, rootId, ouConfig);
|
|
141
|
-
}
|
|
142
|
-
const ouMap = {};
|
|
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);
|
|
147
|
-
}
|
|
148
|
-
return success(ouMap);
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
return failure(new Error(`Failed to ensure OUs exist: ${getErrorMessage(error)}`));
|
|
152
|
-
}
|
|
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
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Place accounts into the correct organisational units.
|
|
185
|
-
* Skips accounts with environment "root" and accounts already in the target OU.
|
|
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
|
-
*
|
|
191
|
-
* Idempotent — accounts already in the correct OU are counted but not moved.
|
|
192
|
-
*/
|
|
193
|
-
export async function placeAccountsInOUs(client, ouMap, accounts, accountToOU) {
|
|
194
|
-
try {
|
|
195
|
-
if (accounts.length === 0) {
|
|
196
|
-
return success({ moved: 0, alreadyPlaced: 0 });
|
|
197
|
-
}
|
|
198
|
-
let moved = 0;
|
|
199
|
-
let alreadyPlaced = 0;
|
|
200
|
-
for (const account of accounts) {
|
|
201
|
-
if (account.environment === "root") {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
const targetOuId = accountToOU
|
|
205
|
-
? accountToOU[account.name.toLowerCase()]
|
|
206
|
-
: ouMap[account.environment.toLowerCase()];
|
|
207
|
-
if (!targetOuId) {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
const currentParentId = await getParentId(client, account.id);
|
|
211
|
-
if (!currentParentId) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (currentParentId === targetOuId) {
|
|
215
|
-
alreadyPlaced++;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
try {
|
|
219
|
-
await client.send(new MoveAccountCommand({
|
|
220
|
-
AccountId: account.id,
|
|
221
|
-
SourceParentId: currentParentId,
|
|
222
|
-
DestinationParentId: targetOuId
|
|
223
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
224
|
-
moved++;
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
const errorName = extractErrorName(error);
|
|
228
|
-
if (errorName === AWS_ERROR_NAMES.ACCOUNT_NOT_FOUND) {
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
throw error;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return success({ moved, alreadyPlaced });
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
return failure(new Error(`Failed to place accounts in OUs: ${getErrorMessage(error)}`));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
1
|
+
import{CreateOrganizationalUnitCommand as p,ListOrganizationalUnitsForParentCommand as h,ListParentsCommand as S,MoveAccountCommand as A}from"@aws-sdk/client-organizations";import{success as u,failure as d}from"@fjall/generator";import{extractErrorName as U,isOULeaf as y,SDK_TIMEOUT_MS as w,AWS_ERROR_NAMES as I}from"./types.js";import{getErrorMessage as O}from"@fjall/util";async function N(o,a){const t=await o.send(new h({ParentId:a}),{abortSignal:AbortSignal.timeout(w)});let e=t.OrganizationalUnits??[],n=t.NextToken;for(;n;){const r=await o.send(new h({ParentId:a,NextToken:n}),{abortSignal:AbortSignal.timeout(w)});e=e.concat(r.OrganizationalUnits??[]),n=r.NextToken}return e}async function L(o,a){try{return(await o.send(new S({ChildId:a}),{abortSignal:AbortSignal.timeout(w)})).Parents?.[0]?.Id}catch(t){if(U(t)===I.CHILD_NOT_FOUND)return;throw t}}async function C(o,a,t,e){const n=e.find(r=>r.Name===t);if(n?.Id)return u(n.Id);try{const s=(await o.send(new p({Name:t,ParentId:a}),{abortSignal:AbortSignal.timeout(w)})).OrganizationalUnit;return s?.Id?u(s.Id):d(new Error(`OU "${t}" was created but has no ID`))}catch(r){return d(new Error(`Failed to create OU "${t}": ${O(r)}`))}}async function P(o,a,t){const e={};if(t.length===0)return u(e);const n=await N(o,a);for(const r of t){const s=r.charAt(0).toUpperCase()+r.slice(1),i=await C(o,a,s,n);if(!i.success)return d(i.error);e[r.toLowerCase()]=i.data,n.push({Id:i.data,Name:s})}return u(e)}async function b(o,a,t,e,n,r){const s=await N(o,a);for(const[i,c]of Object.entries(t)){const f=await C(o,a,i,s);if(!f.success)return d(f.error);const l=f.data,g=n?`${n}.${i.toLowerCase()}`:i.toLowerCase();if(e[g]=l,n){const m=i.toLowerCase();r.has(m)||(e[m]=l)}if(s.push({Id:l,Name:i}),!y(c)){const m=await b(o,l,c,e,g,r);if(!m.success)return m}}return u(void 0)}async function _(o,a,t){try{if(Array.isArray(t))return await P(o,a,t);const e={},n=new Set(Object.keys(t).map(s=>s.toLowerCase())),r=await b(o,a,t,e,"",n);return r.success?u(e):d(r.error)}catch(e){return d(new Error(`Failed to ensure OUs exist: ${O(e)}`))}}function x(o,a,t=""){const e={};for(const[n,r]of Object.entries(o)){const s=t?`${t}.${n.toLowerCase()}`:n.toLowerCase(),i=a[s];if(y(r)){if(i)for(const c of r)e[c.toLowerCase()]=i}else Object.assign(e,x(r,a,s))}return e}async function D(o,a,t,e){try{if(t.length===0)return u({moved:0,alreadyPlaced:0});let n=0,r=0;for(const s of t){if(s.environment==="root")continue;const i=e?e[s.name.toLowerCase()]:a[s.environment.toLowerCase()];if(!i)continue;const c=await L(o,s.id);if(c){if(c===i){r++;continue}try{await o.send(new A({AccountId:s.id,SourceParentId:c,DestinationParentId:i}),{abortSignal:AbortSignal.timeout(w)}),n++}catch(f){if(U(f)===I.ACCOUNT_NOT_FOUND)continue;throw f}}}return u({moved:n,alreadyPlaced:r})}catch(n){return d(new Error(`Failed to place accounts in OUs: ${O(n)}`))}}export{x as buildAccountToOUMap,_ as ensureOrganisationalUnitsExist,D as placeAccountsInOUs};
|
|
@@ -1,37 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { success, failure } from "@fjall/generator";
|
|
3
|
-
import { getErrorMessage } from "@fjall/util";
|
|
4
|
-
import { extractErrorName, SDK_TIMEOUT_MS } from "./types.js";
|
|
5
|
-
const POLICY_TYPES = [
|
|
6
|
-
PolicyType.SERVICE_CONTROL_POLICY,
|
|
7
|
-
PolicyType.TAG_POLICY,
|
|
8
|
-
PolicyType.BACKUP_POLICY,
|
|
9
|
-
PolicyType.AISERVICES_OPT_OUT_POLICY
|
|
10
|
-
];
|
|
11
|
-
/**
|
|
12
|
-
* Enable all required policy types on the organisation root.
|
|
13
|
-
* Idempotent — skips policy types that are already enabled.
|
|
14
|
-
*/
|
|
15
|
-
export async function enablePolicyTypes(client, rootId) {
|
|
16
|
-
try {
|
|
17
|
-
for (const policyType of POLICY_TYPES) {
|
|
18
|
-
try {
|
|
19
|
-
await client.send(new EnablePolicyTypeCommand({
|
|
20
|
-
RootId: rootId,
|
|
21
|
-
PolicyType: policyType
|
|
22
|
-
}), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
const errorName = extractErrorName(error);
|
|
26
|
-
if (errorName === "PolicyTypeAlreadyEnabledException") {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return success(undefined);
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
return failure(new Error(`Failed to enable policy types: ${getErrorMessage(error)}`));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
import{EnablePolicyTypeCommand as i,PolicyType as r}from"@aws-sdk/client-organizations";import{success as a,failure as c}from"@fjall/generator";import{getErrorMessage as y}from"@fjall/util";import{extractErrorName as l,SDK_TIMEOUT_MS as p}from"./types.js";const m=[r.SERVICE_CONTROL_POLICY,r.TAG_POLICY,r.BACKUP_POLICY,r.AISERVICES_OPT_OUT_POLICY];async function C(t,n){try{for(const e of m)try{await t.send(new i({RootId:n,PolicyType:e}),{abortSignal:AbortSignal.timeout(p)})}catch(o){if(l(o)==="PolicyTypeAlreadyEnabledException")continue;throw o}return a(void 0)}catch(e){return c(new Error(`Failed to enable policy types: ${y(e)}`))}}export{C as enablePolicyTypes};
|