@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.
Files changed (54) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/accounts.js +1 -99
  3. package/dist/src/aws/organisations/backup.js +1 -30
  4. package/dist/src/aws/organisations/costAllocation.js +1 -28
  5. package/dist/src/aws/organisations/delegatedAdmin.js +3 -43
  6. package/dist/src/aws/organisations/identityCentre.js +1 -23
  7. package/dist/src/aws/organisations/ipam.js +1 -20
  8. package/dist/src/aws/organisations/organisation.js +1 -103
  9. package/dist/src/aws/organisations/organisationalUnits.js +1 -239
  10. package/dist/src/aws/organisations/policies.js +1 -37
  11. package/dist/src/aws/organisations/ram.js +1 -19
  12. package/dist/src/aws/organisations/serviceAccess.js +1 -44
  13. package/dist/src/aws/organisations/trustedAccess.js +1 -19
  14. package/dist/src/aws/utils/regions.js +1 -1
  15. package/dist/src/index.js +1 -65
  16. package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +1 -78
  17. package/dist/src/orchestration/activeDeploymentGuard.js +5 -39
  18. package/dist/src/orchestration/applicationDeploy.js +1 -149
  19. package/dist/src/orchestration/applicationDeployHelpers.js +4 -223
  20. package/dist/src/orchestration/applicationDestroy.js +1 -131
  21. package/dist/src/orchestration/builders/dockerBuilder.js +1 -98
  22. package/dist/src/orchestration/builders/openNextBuilder.js +1 -144
  23. package/dist/src/orchestration/cascadeHelpers.js +1 -160
  24. package/dist/src/orchestration/contextHelpers.js +1 -107
  25. package/dist/src/orchestration/deploy.js +1 -42
  26. package/dist/src/orchestration/destroy.js +1 -67
  27. package/dist/src/orchestration/detectionPipeline.js +1 -84
  28. package/dist/src/orchestration/dockerBuildHelper.js +1 -49
  29. package/dist/src/orchestration/dockerInterface.js +0 -1
  30. package/dist/src/orchestration/domainInterface.js +0 -1
  31. package/dist/src/orchestration/openNextBuild.js +3 -243
  32. package/dist/src/orchestration/organisationDeploy.js +3 -284
  33. package/dist/src/orchestration/organisationDestroy.js +3 -189
  34. package/dist/src/orchestration/organisationSetup.js +1 -247
  35. package/dist/src/orchestration/resolveOperation.js +1 -123
  36. package/dist/src/orchestration/welcomeImageHelper.js +1 -64
  37. package/dist/src/services/application/ApplicationStackService.js +1 -218
  38. package/dist/src/services/application/applicationStackHelpers.js +4 -248
  39. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -244
  40. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -125
  41. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -278
  42. package/dist/src/services/infrastructure/CdkService.js +3 -213
  43. package/dist/src/services/infrastructure/CloudFormationService.js +1 -248
  44. package/dist/src/services/infrastructure/ICdkProcessManager.js +0 -1
  45. package/dist/src/services/supporting/CdkContextBuilder.js +1 -44
  46. package/dist/src/services/supporting/TemplateHashService.js +1 -152
  47. package/dist/src/steps/stepRegistry.js +1 -505
  48. package/dist/src/types/apiClient.js +0 -1
  49. package/dist/src/types/detection.js +0 -1
  50. package/dist/src/types/frameworkBuilder.js +0 -8
  51. package/dist/src/types/params.js +0 -1
  52. package/dist/src/types/patternDetection.js +1 -88
  53. package/dist/src/types/stepDefinitions.js +1 -98
  54. package/package.json +4 -4
@@ -1,189 +1,3 @@
1
- /**
2
- * Organisation destroy orchestration.
3
- *
4
- * Receives pre-authenticated credentials and destroys all organisation
5
- * infrastructure in cascade order:
6
- * 1. Member accounts across ALL regions in parallel
7
- * 2. Platform account in primary region
8
- * 3. Organisation root stack
9
- *
10
- * Auth, verification, and interactive prompts are the caller's job
11
- * (per engine/consumer boundary).
12
- */
13
- import { success, failure } from "@fjall/generator";
14
- import { maskSensitiveOutput, getErrorMessage } from "@fjall/util";
15
- import { stubCallerIdentity } from "../types/deployment/index.js";
16
- import { ORGANISATION_TYPES, getOrganisationStackName } from "../types/operations.js";
17
- import { CdkContextBuilder } from "../services/supporting/CdkContextBuilder.js";
18
- import { buildParamsContext, synthOrFail, forwardOutput, forwardResourceProgress } from "./contextHelpers.js";
19
- import { destroyCascadeAccount } from "./cascadeDestroyHelpers.js";
20
- import { partitionAccounts } from "./cascadeHelpers.js";
21
- import { DEFAULT_REGION } from "../aws/utils/regions.js";
22
- import { STEP_IDS } from "../types/stepDefinitions.js";
23
- const ORG_DESTROY_STEP_ID = STEP_IDS.ORG_DESTROY;
24
- const ORG_DESTROY_STEP_NAME = "Destroying organisation infrastructure";
25
- /**
26
- * Destroy organisation infrastructure with cascade.
27
- *
28
- * The cascade ordering is: members (parallel, multi-region) -> platform -> org stack.
29
- * If any member or platform destruction fails, the org stack is NOT destroyed
30
- * (to avoid orphaning resources).
31
- */
32
- export async function destroyOrganisation(params, services, operation) {
33
- const startTime = Date.now();
34
- const { callbacks } = params;
35
- const providerAccounts = params.orgConfig?.providerAccounts ?? [];
36
- const primaryRegion = params.orgConfig?.primaryRegion ?? services.awsProvider.getRegion();
37
- const allRegions = buildRegionList(params);
38
- const { platformAccount, memberAccounts } = partitionAccounts(providerAccounts);
39
- const cascadeEnabled = params.options?.cascade !== false;
40
- callbacks.onLog?.(`Destroying organisation infrastructure (${providerAccounts.length} accounts, ${allRegions.length} region(s))`, "info");
41
- const cascadeErrors = [];
42
- const stacksDestroyed = [];
43
- if (cascadeEnabled) {
44
- callbacks.onCascadeStart?.();
45
- // Phase 1: Destroy member accounts across all regions in parallel
46
- if (memberAccounts.length > 0) {
47
- callbacks.onCascadePhaseStart?.("accounts");
48
- const pairs = buildAccountRegionPairs(memberAccounts, allRegions);
49
- const memberResults = await Promise.allSettled(pairs.map(({ account, region }) => destroyCascadeAccount(params, services, operation, account, "account", region, callbacks)));
50
- for (let i = 0; i < memberResults.length; i++) {
51
- const result = memberResults[i];
52
- const pair = pairs[i];
53
- if (!result || !pair)
54
- continue;
55
- if (result.status === "fulfilled") {
56
- if (result.value.success) {
57
- stacksDestroyed.push(`Account-${pair.account.name}-${pair.region}`);
58
- }
59
- else {
60
- cascadeErrors.push({
61
- accountId: pair.account.id,
62
- error: result.value.error ?? "Unknown error"
63
- });
64
- }
65
- }
66
- else {
67
- cascadeErrors.push({
68
- accountId: pair.account.id,
69
- error: getErrorMessage(result.reason)
70
- });
71
- }
72
- }
73
- }
74
- // Phase 2: Destroy platform account (primary region only)
75
- if (platformAccount) {
76
- callbacks.onCascadePhaseStart?.("platform");
77
- const platformResult = await destroyCascadeAccount(params, services, operation, platformAccount, "platform", primaryRegion, callbacks);
78
- if (platformResult.success) {
79
- stacksDestroyed.push("Platform");
80
- }
81
- else {
82
- cascadeErrors.push({
83
- accountId: platformAccount.id,
84
- error: platformResult.error ?? "Platform destroy failed"
85
- });
86
- }
87
- }
88
- // Emit cascade completion
89
- callbacks.onCascadeComplete?.({
90
- platformDeployed: false,
91
- domainsDeployed: false,
92
- accountsDeployed: 0,
93
- accountsFailed: cascadeErrors.length,
94
- errors: cascadeErrors
95
- });
96
- if (cascadeErrors.length > 0) {
97
- const errorSummary = cascadeErrors
98
- .map((e) => ` ${e.accountId}: ${e.error}`)
99
- .join("\n");
100
- const cascadeError = new Error(`Cascade destroy completed with ${cascadeErrors.length} failure(s):\n${errorSummary}`);
101
- callbacks.onError?.(cascadeError);
102
- callbacks.onLog?.(maskSensitiveOutput(cascadeError.message), "warn");
103
- }
104
- }
105
- // Phase 3: Destroy the organisation root stack
106
- // Gate: if any cascade failure, skip org stack destroy to avoid orphaning resources
107
- if (cascadeErrors.length > 0) {
108
- const msg = "Skipping organisation root stack destroy due to cascade failures";
109
- callbacks.onLog?.(msg, "warn");
110
- return success({
111
- target: operation.target,
112
- deploymentType: "organisation",
113
- stacksDestroyed,
114
- durationMs: Date.now() - startTime,
115
- warnings: cascadeErrors.map((e) => maskSensitiveOutput(`${e.accountId}: ${e.error}`))
116
- });
117
- }
118
- // Destroy org root stack
119
- callbacks.onStepStart?.(ORG_DESTROY_STEP_ID, ORG_DESTROY_STEP_NAME, 0, 1);
120
- const orgResult = await destroyOrgRootStack(params, services, operation, primaryRegion, callbacks);
121
- if (orgResult.success) {
122
- stacksDestroyed.push("Organisation");
123
- callbacks.onStepComplete?.(ORG_DESTROY_STEP_ID, ORG_DESTROY_STEP_NAME, "completed", 0, 1);
124
- }
125
- else {
126
- callbacks.onStepComplete?.(ORG_DESTROY_STEP_ID, ORG_DESTROY_STEP_NAME, "error", 0, 1);
127
- return failure(orgResult.error);
128
- }
129
- return success({
130
- target: operation.target,
131
- deploymentType: "organisation",
132
- stacksDestroyed,
133
- durationMs: Date.now() - startTime
134
- });
135
- }
136
- /**
137
- * Destroy the organisation root stack via CDK.
138
- */
139
- async function destroyOrgRootStack(params, services, operation, region, callbacks) {
140
- const context = CdkContextBuilder.buildDeploymentContext({
141
- deployType: "organisation",
142
- target: operation.target,
143
- path: operation.path,
144
- region,
145
- callerIdentity: stubCallerIdentity(services.awsProvider.getAccountId()),
146
- ...buildParamsContext({
147
- orgConfig: params.orgConfig,
148
- identity: params.identity
149
- })
150
- }, { verbose: params.options?.verbose }, params.orgConfig);
151
- const stackName = getOrganisationStackName(ORGANISATION_TYPES.ORGANISATION);
152
- callbacks.onLog?.("Synthesising organisation infrastructure…", "info");
153
- const synthResult = await synthOrFail(services, context, callbacks, "Organisation synth failed");
154
- if (!synthResult.success)
155
- return synthResult;
156
- callbacks.onLog?.(`Destroying ${stackName} stack…`, "info");
157
- const destroyResult = await services.cdkService.runCdkDestroy(context, stackName, forwardOutput(callbacks), forwardResourceProgress(callbacks), services.awsProvider, true);
158
- if (!destroyResult.success) {
159
- const error = new Error(maskSensitiveOutput(`Organisation destroy failed: ${destroyResult.error}`));
160
- callbacks.onError?.(error);
161
- return failure(error);
162
- }
163
- return success(undefined);
164
- }
165
- /**
166
- * Build the full list of regions to destroy across (primary + secondary + DR).
167
- */
168
- function buildRegionList(params) {
169
- const primaryRegion = params.orgConfig?.primaryRegion ?? DEFAULT_REGION;
170
- const secondaryRegions = params.orgConfig?.secondaryRegions ?? [];
171
- const allRegions = [primaryRegion, ...secondaryRegions];
172
- const drRegion = params.orgConfig?.disasterRecoveryRegion;
173
- if (drRegion && !allRegions.includes(drRegion)) {
174
- allRegions.push(drRegion);
175
- }
176
- return allRegions;
177
- }
178
- /**
179
- * Build all account x region pairs for parallel destruction.
180
- */
181
- function buildAccountRegionPairs(accounts, regions) {
182
- const pairs = [];
183
- for (const region of regions) {
184
- for (const account of accounts) {
185
- pairs.push({ account, region });
186
- }
187
- }
188
- return pairs;
189
- }
1
+ import{success as R,failure as O}from"@fjall/generator";import{maskSensitiveOutput as h,getErrorMessage as A}from"@fjall/util";import{stubCallerIdentity as I}from"../types/deployment/index.js";import{ORGANISATION_TYPES as P,getOrganisationStackName as T}from"../types/operations.js";import{CdkContextBuilder as $}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as _,synthOrFail as k,forwardOutput as N,forwardResourceProgress as L}from"./contextHelpers.js";import{destroyCascadeAccount as D}from"./cascadeDestroyHelpers.js";import{partitionAccounts as b}from"./cascadeHelpers.js";import{DEFAULT_REGION as v}from"../aws/utils/regions.js";import{STEP_IDS as G}from"../types/stepDefinitions.js";const S=G.ORG_DESTROY,C="Destroying organisation infrastructure";async function V(t,r,n){const e=Date.now(),{callbacks:o}=t,d=t.orgConfig?.providerAccounts??[],f=t.orgConfig?.primaryRegion??r.awsProvider.getRegion(),l=M(t),{platformAccount:g,memberAccounts:m}=b(d),E=t.options?.cascade!==!1;o.onLog?.(`Destroying organisation infrastructure (${d.length} accounts, ${l.length} region(s))`,"info");const s=[],p=[];if(E){if(o.onCascadeStart?.(),m.length>0){o.onCascadePhaseStart?.("accounts");const a=Y(m,l),c=await Promise.allSettled(a.map(({account:i,region:u})=>D(t,r,n,i,"account",u,o)));for(let i=0;i<c.length;i++){const u=c[i],y=a[i];!u||!y||(u.status==="fulfilled"?u.value.success?p.push(`Account-${y.account.name}-${y.region}`):s.push({accountId:y.account.id,error:u.value.error??"Unknown error"}):s.push({accountId:y.account.id,error:A(u.reason)}))}}if(g){o.onCascadePhaseStart?.("platform");const a=await D(t,r,n,g,"platform",f,o);a.success?p.push("Platform"):s.push({accountId:g.id,error:a.error??"Platform destroy failed"})}if(o.onCascadeComplete?.({platformDeployed:!1,domainsDeployed:!1,accountsDeployed:0,accountsFailed:s.length,errors:s}),s.length>0){const a=s.map(i=>` ${i.accountId}: ${i.error}`).join(`
2
+ `),c=new Error(`Cascade destroy completed with ${s.length} failure(s):
3
+ ${a}`);o.onError?.(c),o.onLog?.(h(c.message),"warn")}}if(s.length>0)return o.onLog?.("Skipping organisation root stack destroy due to cascade failures","warn"),R({target:n.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-e,warnings:s.map(c=>h(`${c.accountId}: ${c.error}`))});o.onStepStart?.(S,C,0,1);const w=await x(t,r,n,f,o);if(w.success)p.push("Organisation"),o.onStepComplete?.(S,C,"completed",0,1);else return o.onStepComplete?.(S,C,"error",0,1),O(w.error);return R({target:n.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-e})}async function x(t,r,n,e,o){const d=$.buildDeploymentContext({deployType:"organisation",target:n.target,path:n.path,region:e,callerIdentity:I(r.awsProvider.getAccountId()),..._({orgConfig:t.orgConfig,identity:t.identity})},{verbose:t.options?.verbose},t.orgConfig),f=T(P.ORGANISATION);o.onLog?.("Synthesising organisation infrastructure\u2026","info");const l=await k(r,d,o,"Organisation synth failed");if(!l.success)return l;o.onLog?.(`Destroying ${f} stack\u2026`,"info");const g=await r.cdkService.runCdkDestroy(d,f,N(o),L(o),r.awsProvider,!0);if(!g.success){const m=new Error(h(`Organisation destroy failed: ${g.error}`));return o.onError?.(m),O(m)}return R(void 0)}function M(t){const r=t.orgConfig?.primaryRegion??v,n=t.orgConfig?.secondaryRegions??[],e=[r,...n],o=t.orgConfig?.disasterRecoveryRegion;return o&&!e.includes(o)&&e.push(o),e}function Y(t,r){const n=[];for(const e of r)for(const o of t)n.push({account:o,region:e});return n}export{V as destroyOrganisation};
@@ -1,247 +1 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { OrganizationsClient } from "@aws-sdk/client-organizations";
3
- import { RAMClient } from "@aws-sdk/client-ram";
4
- import { CloudFormationClient } from "@aws-sdk/client-cloudformation";
5
- import { EC2Client } from "@aws-sdk/client-ec2";
6
- import { BackupClient } from "@aws-sdk/client-backup";
7
- import { CostExplorerClient } from "@aws-sdk/client-cost-explorer";
8
- import { SSOAdminClient } from "@aws-sdk/client-sso-admin";
9
- import { ensureOrganisationExists } from "../aws/organisations/organisation.js";
10
- import { enablePolicyTypes } from "../aws/organisations/policies.js";
11
- import { enableServiceAccess } from "../aws/organisations/serviceAccess.js";
12
- import { enableRamSharing } from "../aws/organisations/ram.js";
13
- import { activateTrustedAccess } from "../aws/organisations/trustedAccess.js";
14
- import { enableIpamDelegatedAdmin } from "../aws/organisations/ipam.js";
15
- import { updateBackupGlobalSettings } from "../aws/organisations/backup.js";
16
- import { listAccounts, createAccount } from "../aws/organisations/accounts.js";
17
- import { ensureOrganisationalUnitsExist, placeAccountsInOUs, buildAccountToOUMap } from "../aws/organisations/organisationalUnits.js";
18
- import { activateCostAllocationTags } from "../aws/organisations/costAllocation.js";
19
- import { checkIdentityCentreStatus } from "../aws/organisations/identityCentre.js";
20
- import { registerSecurityDelegates } from "../aws/organisations/delegatedAdmin.js";
21
- /**
22
- * Orchestrate the full AWS Organisation setup sequence.
23
- *
24
- * Runs up to 13 phases sequentially. Non-fatal phase failures are recorded
25
- * and execution continues. The only fatal failure is phase 1
26
- * (create-organisation) since all subsequent phases depend on the org ID.
27
- */
28
- export async function runOrganisationSetup(awsProvider, config, callbacks) {
29
- const phasesCompleted = [];
30
- const phasesSkipped = [];
31
- const errors = [];
32
- const createdAccounts = [];
33
- let identityCentreStatus;
34
- const orgsClient = awsProvider.getClient(OrganizationsClient);
35
- const ramClient = awsProvider.getClient(RAMClient);
36
- const cfnClient = awsProvider.getClient(CloudFormationClient);
37
- const ec2Client = awsProvider.getClient(EC2Client);
38
- const backupClient = awsProvider.getClient(BackupClient);
39
- const ceClient = awsProvider.getClient(CostExplorerClient);
40
- const ssoClient = awsProvider.getClient(SSOAdminClient);
41
- // Phase 1: Ensure organisation exists (fatal if fails)
42
- callbacks?.onPhaseStart?.("create-organisation");
43
- callbacks?.onProgress?.("Ensuring AWS Organisation exists");
44
- const orgResult = await ensureOrganisationExists(orgsClient);
45
- if (!orgResult.success) {
46
- callbacks?.onError?.("create-organisation", orgResult.error);
47
- callbacks?.onPhaseComplete?.("create-organisation", "error");
48
- return failure(orgResult.error);
49
- }
50
- const { orgId, rootId } = orgResult.data;
51
- callbacks?.onPhaseComplete?.("create-organisation", "completed");
52
- phasesCompleted.push("create-organisation");
53
- // Phase 2: Enable policy types
54
- await executePhase("enable-policies", () => {
55
- callbacks?.onProgress?.("Enabling organisation policy types");
56
- return enablePolicyTypes(orgsClient, rootId);
57
- }, phasesCompleted, errors, callbacks);
58
- // Phase 3: Enable service access
59
- await executePhase("enable-service-access", () => {
60
- callbacks?.onProgress?.("Enabling AWS service access");
61
- return enableServiceAccess(orgsClient);
62
- }, phasesCompleted, errors, callbacks);
63
- // Phase 4: Enable RAM sharing
64
- await executePhase("enable-ram-sharing", () => {
65
- callbacks?.onProgress?.("Enabling RAM sharing");
66
- return enableRamSharing(ramClient);
67
- }, phasesCompleted, errors, callbacks);
68
- // Phase 5: Activate trusted access
69
- await executePhase("activate-trusted-access", () => {
70
- callbacks?.onProgress?.("Activating CloudFormation trusted access");
71
- return activateTrustedAccess(cfnClient);
72
- }, phasesCompleted, errors, callbacks);
73
- // Phase 6: Enable IPAM delegated admin (requires a platform account)
74
- if (config.platformAccountId) {
75
- const platformId = config.platformAccountId;
76
- await executePhase("enable-ipam", () => {
77
- callbacks?.onProgress?.("Enabling IPAM delegated administrator");
78
- return enableIpamDelegatedAdmin(ec2Client, platformId);
79
- }, phasesCompleted, errors, callbacks);
80
- }
81
- // Phase 7: Configure backup settings
82
- await executePhase("configure-backup", () => {
83
- callbacks?.onProgress?.("Updating backup global settings");
84
- return updateBackupGlobalSettings(backupClient);
85
- }, phasesCompleted, errors, callbacks);
86
- // Phase 8: Create missing accounts
87
- callbacks?.onPhaseStart?.("create-accounts");
88
- callbacks?.onProgress?.("Checking for missing accounts");
89
- const accountsResult = await createMissingAccounts(orgsClient, config.accounts, createdAccounts);
90
- if (!accountsResult.success) {
91
- errors.push({
92
- phase: "create-accounts",
93
- error: accountsResult.error.message
94
- });
95
- callbacks?.onError?.("create-accounts", accountsResult.error);
96
- callbacks?.onPhaseComplete?.("create-accounts", "error");
97
- }
98
- else {
99
- phasesCompleted.push("create-accounts");
100
- callbacks?.onPhaseComplete?.("create-accounts", "completed");
101
- }
102
- // Phase 9: Ensure organisational units exist
103
- let ouMap = {};
104
- callbacks?.onPhaseStart?.("create-organisational-units");
105
- callbacks?.onProgress?.("Ensuring organisational units exist");
106
- const ouResult = await ensureOrganisationalUnitsExist(orgsClient, rootId, config.organisationalUnits);
107
- if (!ouResult.success) {
108
- errors.push({
109
- phase: "create-organisational-units",
110
- error: ouResult.error.message
111
- });
112
- callbacks?.onError?.("create-organisational-units", ouResult.error);
113
- callbacks?.onPhaseComplete?.("create-organisational-units", "error");
114
- }
115
- else {
116
- ouMap = ouResult.data;
117
- phasesCompleted.push("create-organisational-units");
118
- callbacks?.onPhaseComplete?.("create-organisational-units", "completed");
119
- }
120
- // Phase 10: Place accounts in OUs
121
- if (Object.keys(ouMap).length > 0 && config.accountPlacements) {
122
- const accountInfos = buildAccountInfos(config.accountPlacements);
123
- const accountToOU = !Array.isArray(config.organisationalUnits)
124
- ? buildAccountToOUMap(config.organisationalUnits, ouMap)
125
- : undefined;
126
- await executePhase("place-accounts", () => {
127
- callbacks?.onProgress?.("Placing accounts in organisational units");
128
- return placeAccountsInOUs(orgsClient, ouMap, accountInfos, accountToOU);
129
- }, phasesCompleted, errors, callbacks);
130
- }
131
- else {
132
- phasesSkipped.push("place-accounts");
133
- callbacks?.onPhaseStart?.("place-accounts");
134
- callbacks?.onPhaseComplete?.("place-accounts", "skipped");
135
- }
136
- // Phase 11: Activate cost allocation tags
137
- const tags = config.costAllocationTags ?? [];
138
- if (tags.length > 0) {
139
- await executePhase("activate-cost-tags", () => {
140
- callbacks?.onProgress?.("Activating cost allocation tags");
141
- return activateCostAllocationTags(ceClient, tags.map((t) => ({ TagKey: t })));
142
- }, phasesCompleted, errors, callbacks);
143
- }
144
- else {
145
- phasesSkipped.push("activate-cost-tags");
146
- callbacks?.onPhaseStart?.("activate-cost-tags");
147
- callbacks?.onPhaseComplete?.("activate-cost-tags", "skipped");
148
- }
149
- // Phase 12: Check Identity Centre
150
- if (config.skipIdentityCentre) {
151
- phasesSkipped.push("check-identity-centre");
152
- callbacks?.onPhaseStart?.("check-identity-centre");
153
- callbacks?.onPhaseComplete?.("check-identity-centre", "skipped");
154
- }
155
- else {
156
- callbacks?.onPhaseStart?.("check-identity-centre");
157
- callbacks?.onProgress?.("Checking Identity Centre status");
158
- const icResult = await checkIdentityCentreStatus(ssoClient);
159
- if (!icResult.success) {
160
- errors.push({
161
- phase: "check-identity-centre",
162
- error: icResult.error.message
163
- });
164
- callbacks?.onError?.("check-identity-centre", icResult.error);
165
- callbacks?.onPhaseComplete?.("check-identity-centre", "error");
166
- }
167
- else {
168
- identityCentreStatus = icResult.data.enabled ? "enabled" : "not-enabled";
169
- phasesCompleted.push("check-identity-centre");
170
- callbacks?.onPhaseComplete?.("check-identity-centre", "completed");
171
- }
172
- }
173
- // Phase 13: Register security delegated administrators
174
- const delegateAccountId = config.securityDelegateAccountId;
175
- if (delegateAccountId) {
176
- await executePhase("register-security-delegates", () => {
177
- callbacks?.onProgress?.("Registering security service delegated administrators");
178
- return registerSecurityDelegates(orgsClient, delegateAccountId);
179
- }, phasesCompleted, errors, callbacks);
180
- }
181
- else {
182
- phasesSkipped.push("register-security-delegates");
183
- callbacks?.onPhaseStart?.("register-security-delegates");
184
- callbacks?.onPhaseComplete?.("register-security-delegates", "skipped");
185
- }
186
- return success({
187
- organisationId: orgId,
188
- createdAccounts,
189
- identityCentreStatus,
190
- phasesCompleted,
191
- phasesSkipped,
192
- errors
193
- });
194
- }
195
- /**
196
- * Execute a single phase with standard callback handling.
197
- * Records success or error; never throws.
198
- */
199
- async function executePhase(phase, fn, phasesCompleted, errors, callbacks) {
200
- callbacks?.onPhaseStart?.(phase);
201
- const result = await fn();
202
- if (!result.success) {
203
- errors.push({ phase, error: result.error.message });
204
- callbacks?.onError?.(phase, result.error);
205
- callbacks?.onPhaseComplete?.(phase, "error");
206
- }
207
- else {
208
- phasesCompleted.push(phase);
209
- callbacks?.onPhaseComplete?.(phase, "completed");
210
- }
211
- }
212
- /**
213
- * List existing accounts, then create any that are missing by name.
214
- */
215
- async function createMissingAccounts(client, desiredAccounts, createdAccounts) {
216
- const listResult = await listAccounts(client);
217
- if (!listResult.success) {
218
- return failure(listResult.error);
219
- }
220
- const existingNames = new Set(listResult.data
221
- .map((a) => a.Name?.toLowerCase())
222
- .filter((n) => n !== undefined));
223
- for (const desired of desiredAccounts) {
224
- if (existingNames.has(desired.name.toLowerCase())) {
225
- continue;
226
- }
227
- const createResult = await createAccount(client, desired.name, desired.email);
228
- if (!createResult.success) {
229
- return failure(createResult.error);
230
- }
231
- createdAccounts.push({
232
- name: createResult.data.accountName,
233
- accountId: createResult.data.accountId
234
- });
235
- }
236
- return success(undefined);
237
- }
238
- /**
239
- * Convert account placement config into AccountInfo[] for placeAccountsInOUs.
240
- */
241
- function buildAccountInfos(placements) {
242
- return Object.entries(placements).map(([accountId, environment]) => ({
243
- id: accountId,
244
- name: accountId,
245
- environment
246
- }));
247
- }
1
+ import{success as y,failure as h}from"@fjall/generator";import{OrganizationsClient as U}from"@aws-sdk/client-organizations";import{RAMClient as v}from"@aws-sdk/client-ram";import{CloudFormationClient as T}from"@aws-sdk/client-cloudformation";import{EC2Client as M}from"@aws-sdk/client-ec2";import{BackupClient as D}from"@aws-sdk/client-backup";import{CostExplorerClient as N}from"@aws-sdk/client-cost-explorer";import{SSOAdminClient as j}from"@aws-sdk/client-sso-admin";import{ensureOrganisationExists as B}from"../aws/organisations/organisation.js";import{enablePolicyTypes as F}from"../aws/organisations/policies.js";import{enableServiceAccess as L}from"../aws/organisations/serviceAccess.js";import{enableRamSharing as W}from"../aws/organisations/ram.js";import{activateTrustedAccess as z}from"../aws/organisations/trustedAccess.js";import{enableIpamDelegatedAdmin as G}from"../aws/organisations/ipam.js";import{updateBackupGlobalSettings as K}from"../aws/organisations/backup.js";import{listAccounts as q,createAccount as H}from"../aws/organisations/accounts.js";import{ensureOrganisationalUnitsExist as J,placeAccountsInOUs as Q,buildAccountToOUMap as V}from"../aws/organisations/organisationalUnits.js";import{activateCostAllocationTags as X}from"../aws/organisations/costAllocation.js";import{checkIdentityCentreStatus as Y}from"../aws/organisations/identityCentre.js";import{registerSecurityDelegates as Z}from"../aws/organisations/delegatedAdmin.js";async function le(n,o,e){const r=[],s=[],t=[],c=[];let C;const u=n.getClient(U),A=n.getClient(v),S=n.getClient(T),I=n.getClient(M),E=n.getClient(D),w=n.getClient(N),O=n.getClient(j);e?.onPhaseStart?.("create-organisation"),e?.onProgress?.("Ensuring AWS Organisation exists");const p=await B(u);if(!p.success)return e?.onError?.("create-organisation",p.error),e?.onPhaseComplete?.("create-organisation","error"),h(p.error);const{orgId:R,rootId:f}=p.data;if(e?.onPhaseComplete?.("create-organisation","completed"),r.push("create-organisation"),await a("enable-policies",()=>(e?.onProgress?.("Enabling organisation policy types"),F(u,f)),r,t,e),await a("enable-service-access",()=>(e?.onProgress?.("Enabling AWS service access"),L(u)),r,t,e),await a("enable-ram-sharing",()=>(e?.onProgress?.("Enabling RAM sharing"),W(A)),r,t,e),await a("activate-trusted-access",()=>(e?.onProgress?.("Activating CloudFormation trusted access"),z(S)),r,t,e),o.platformAccountId){const i=o.platformAccountId;await a("enable-ipam",()=>(e?.onProgress?.("Enabling IPAM delegated administrator"),G(I,i)),r,t,e)}await a("configure-backup",()=>(e?.onProgress?.("Updating backup global settings"),K(E)),r,t,e),e?.onPhaseStart?.("create-accounts"),e?.onProgress?.("Checking for missing accounts");const d=await _(u,o.accounts,c);d.success?(r.push("create-accounts"),e?.onPhaseComplete?.("create-accounts","completed")):(t.push({phase:"create-accounts",error:d.error.message}),e?.onError?.("create-accounts",d.error),e?.onPhaseComplete?.("create-accounts","error"));let m={};e?.onPhaseStart?.("create-organisational-units"),e?.onProgress?.("Ensuring organisational units exist");const g=await J(u,f,o.organisationalUnits);if(g.success?(m=g.data,r.push("create-organisational-units"),e?.onPhaseComplete?.("create-organisational-units","completed")):(t.push({phase:"create-organisational-units",error:g.error.message}),e?.onError?.("create-organisational-units",g.error),e?.onPhaseComplete?.("create-organisational-units","error")),Object.keys(m).length>0&&o.accountPlacements){const i=$(o.accountPlacements),x=Array.isArray(o.organisationalUnits)?void 0:V(o.organisationalUnits,m);await a("place-accounts",()=>(e?.onProgress?.("Placing accounts in organisational units"),Q(u,m,i,x)),r,t,e)}else s.push("place-accounts"),e?.onPhaseStart?.("place-accounts"),e?.onPhaseComplete?.("place-accounts","skipped");const P=o.costAllocationTags??[];if(P.length>0?await a("activate-cost-tags",()=>(e?.onProgress?.("Activating cost allocation tags"),X(w,P.map(i=>({TagKey:i})))),r,t,e):(s.push("activate-cost-tags"),e?.onPhaseStart?.("activate-cost-tags"),e?.onPhaseComplete?.("activate-cost-tags","skipped")),o.skipIdentityCentre)s.push("check-identity-centre"),e?.onPhaseStart?.("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","skipped");else{e?.onPhaseStart?.("check-identity-centre"),e?.onProgress?.("Checking Identity Centre status");const i=await Y(O);i.success?(C=i.data.enabled?"enabled":"not-enabled",r.push("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","completed")):(t.push({phase:"check-identity-centre",error:i.error.message}),e?.onError?.("check-identity-centre",i.error),e?.onPhaseComplete?.("check-identity-centre","error"))}const l=o.securityDelegateAccountId;return l?await a("register-security-delegates",()=>(e?.onProgress?.("Registering security service delegated administrators"),Z(u,l)),r,t,e):(s.push("register-security-delegates"),e?.onPhaseStart?.("register-security-delegates"),e?.onPhaseComplete?.("register-security-delegates","skipped")),y({organisationId:R,createdAccounts:c,identityCentreStatus:C,phasesCompleted:r,phasesSkipped:s,errors:t})}async function a(n,o,e,r,s){s?.onPhaseStart?.(n);const t=await o();t.success?(e.push(n),s?.onPhaseComplete?.(n,"completed")):(r.push({phase:n,error:t.error.message}),s?.onError?.(n,t.error),s?.onPhaseComplete?.(n,"error"))}async function _(n,o,e){const r=await q(n);if(!r.success)return h(r.error);const s=new Set(r.data.map(t=>t.Name?.toLowerCase()).filter(t=>t!==void 0));for(const t of o){if(s.has(t.name.toLowerCase()))continue;const c=await H(n,t.name,t.email);if(!c.success)return h(c.error);e.push({name:c.data.accountName,accountId:c.data.accountId})}return y(void 0)}function $(n){return Object.entries(n).map(([o,e])=>({id:o,name:o,environment:e}))}export{le as runOrganisationSetup};
@@ -1,123 +1 @@
1
- import { join, relative } from "path";
2
- import { readdir } from "fs/promises";
3
- import { success, failure } from "@fjall/generator";
4
- import { logger } from "@fjall/util/logger";
5
- import { ORGANISATION_TYPES } from "../types/operations.js";
6
- import { fileExists } from "@fjall/util/fsHelpers";
7
- const ORGANISATION_TYPE_VALUES = new Set(Object.values(ORGANISATION_TYPES));
8
- function isOrganisationType(value) {
9
- return ORGANISATION_TYPE_VALUES.has(value);
10
- }
11
- const TARGET_PATTERN = /^[a-z][a-z0-9-]*$/;
12
- /**
13
- * Determine the deployment operation type from the target string and filesystem.
14
- *
15
- * - If target matches an organisation type → OrganisationOperation
16
- * - If fjall/<target> directory exists → ApplicationOperation
17
- * - Otherwise → failure
18
- */
19
- export async function resolveOperation(target, workingDirectory) {
20
- logger.debug("resolveOperation", "called", { target, workingDirectory });
21
- const workingDirExists = await fileExists(workingDirectory);
22
- logger.debug("resolveOperation", "workingDirectory exists", {
23
- exists: workingDirExists
24
- });
25
- const fjallDir = join(workingDirectory, "fjall");
26
- const fjallDirExists = await fileExists(fjallDir);
27
- logger.debug("resolveOperation", "fjall/ dir check", {
28
- fjallDir,
29
- exists: fjallDirExists
30
- });
31
- if (fjallDirExists && logger.isDebugEnabled()) {
32
- try {
33
- const entries = await readdir(fjallDir);
34
- logger.debug("resolveOperation", "fjall/ contents", { entries });
35
- }
36
- catch (err) {
37
- logger.debug("resolveOperation", "fjall/ readdir failed", {
38
- error: String(err)
39
- });
40
- }
41
- }
42
- // Check for organisation-level deployment
43
- const normalisedTarget = target.toLowerCase();
44
- if (isOrganisationType(normalisedTarget)) {
45
- const orgPath = join(workingDirectory, "fjall", normalisedTarget);
46
- const orgPathExists = await fileExists(orgPath);
47
- logger.debug("resolveOperation", "org type match", {
48
- normalisedTarget,
49
- orgPath,
50
- exists: orgPathExists
51
- });
52
- if (!orgPathExists) {
53
- return failure(new Error(`Organisation target "${target}" resolved to fjall/${normalisedTarget}/ but directory not found`));
54
- }
55
- return success({
56
- kind: "organisation",
57
- type: normalisedTarget,
58
- target,
59
- path: orgPath
60
- });
61
- }
62
- // Check for account-prefixed targets (e.g., "account-prod")
63
- if (normalisedTarget.startsWith("account-")) {
64
- const suffix = normalisedTarget.slice("account-".length);
65
- if (!TARGET_PATTERN.test(suffix)) {
66
- return failure(new Error(`Invalid account target "${target}": suffix must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`));
67
- }
68
- const orgPath = join(workingDirectory, "fjall", ORGANISATION_TYPES.ACCOUNT);
69
- const orgPathExists = await fileExists(orgPath);
70
- logger.debug("resolveOperation", "account-prefixed target", {
71
- suffix,
72
- orgPath,
73
- exists: orgPathExists
74
- });
75
- if (!orgPathExists) {
76
- return failure(new Error(`Organisation target "${target}" resolved to fjall/${ORGANISATION_TYPES.ACCOUNT}/ but directory not found`));
77
- }
78
- return success({
79
- kind: "organisation",
80
- type: ORGANISATION_TYPES.ACCOUNT,
81
- target,
82
- path: orgPath
83
- });
84
- }
85
- // Validate target before joining into filesystem path
86
- if (!TARGET_PATTERN.test(target)) {
87
- logger.debug("resolveOperation", "target failed pattern validation", {
88
- target,
89
- pattern: TARGET_PATTERN.source
90
- });
91
- return failure(new Error(`Invalid target "${target}": must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`));
92
- }
93
- // Check for application deployment
94
- const appPath = join(workingDirectory, "fjall", target);
95
- // Defence-in-depth: ensure resolved path stays under workingDirectory
96
- const rel = relative(workingDirectory, appPath);
97
- if (rel.startsWith("..")) {
98
- return failure(new Error(`Invalid target "${target}": resolved path escapes working directory`));
99
- }
100
- const appPathExists = await fileExists(appPath);
101
- logger.debug("resolveOperation", "app path check", {
102
- appPath,
103
- exists: appPathExists,
104
- relative: rel
105
- });
106
- if (appPathExists) {
107
- logger.debug("resolveOperation", "resolved as application", {
108
- appName: target,
109
- path: appPath
110
- });
111
- return success({
112
- kind: "application",
113
- appName: target,
114
- path: appPath
115
- });
116
- }
117
- logger.debug("resolveOperation", "FAILED — no match", {
118
- target,
119
- workingDirectory,
120
- checkedPath: appPath
121
- });
122
- return failure(new Error(`Target "${target}" is not a recognised organisation type and no directory found at fjall/${target}`));
123
- }
1
+ import{join as c,relative as b}from"path";import{readdir as g}from"fs/promises";import{success as u,failure as i}from"@fjall/generator";import{logger as t}from"@fjall/util/logger";import{ORGANISATION_TYPES as p}from"../types/operations.js";import{fileExists as l}from"@fjall/util/fsHelpers";const w=new Set(Object.values(p));function x(e){return w.has(e)}const f=/^[a-z][a-z0-9-]*$/;async function y(e,a){t.debug("resolveOperation","called",{target:e,workingDirectory:a});const E=await l(a);t.debug("resolveOperation","workingDirectory exists",{exists:E});const d=c(a,"fjall"),h=await l(d);if(t.debug("resolveOperation","fjall/ dir check",{fjallDir:d,exists:h}),h&&t.isDebugEnabled())try{const r=await g(d);t.debug("resolveOperation","fjall/ contents",{entries:r})}catch(r){t.debug("resolveOperation","fjall/ readdir failed",{error:String(r)})}const o=e.toLowerCase();if(x(o)){const r=c(a,"fjall",o),s=await l(r);return t.debug("resolveOperation","org type match",{normalisedTarget:o,orgPath:r,exists:s}),s?u({kind:"organisation",type:o,target:e,path:r}):i(new Error(`Organisation target "${e}" resolved to fjall/${o}/ but directory not found`))}if(o.startsWith("account-")){const r=o.slice(8);if(!f.test(r))return i(new Error(`Invalid account target "${e}": suffix must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`));const s=c(a,"fjall",p.ACCOUNT),m=await l(s);return t.debug("resolveOperation","account-prefixed target",{suffix:r,orgPath:s,exists:m}),m?u({kind:"organisation",type:p.ACCOUNT,target:e,path:s}):i(new Error(`Organisation target "${e}" resolved to fjall/${p.ACCOUNT}/ but directory not found`))}if(!f.test(e))return t.debug("resolveOperation","target failed pattern validation",{target:e,pattern:f.source}),i(new Error(`Invalid target "${e}": must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`));const n=c(a,"fjall",e),v=b(a,n);if(v.startsWith(".."))return i(new Error(`Invalid target "${e}": resolved path escapes working directory`));const O=await l(n);return t.debug("resolveOperation","app path check",{appPath:n,exists:O,relative:v}),O?(t.debug("resolveOperation","resolved as application",{appName:e,path:n}),u({kind:"application",appName:e,path:n})):(t.debug("resolveOperation","FAILED \u2014 no match",{target:e,workingDirectory:a,checkedPath:n}),i(new Error(`Target "${e}" is not a recognised organisation type and no directory found at fjall/${e}`)))}export{y as resolveOperation};
@@ -1,64 +1 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { maskSensitiveOutput } from "@fjall/util";
3
- import { STEP_IDS } from "../types/stepDefinitions.js";
4
- const TAG_IMAGES_STEP_NAME = "Tagging container images";
5
- /**
6
- * Initialise ECR with the welcome image and tag for ECS services.
7
- * Runs before Compute stack for apps without a Dockerfile.
8
- *
9
- * Two phases:
10
- * 1. ECR init — ensures the welcome image exists in the private ECR repo
11
- * 2. Image tagging — copies :latest/:backend to :<serviceName>-latest
12
- * so ECS task definitions can pull the correct tag
13
- */
14
- export async function runWelcomeImageSetup(params, services, operation, callbacks) {
15
- const dockerProvider = params.dockerProvider;
16
- if (!dockerProvider)
17
- return success(undefined);
18
- const accountId = services.awsProvider.getAccountId();
19
- if (!accountId) {
20
- callbacks.onLog?.("Skipping ECR initialisation — account ID not available", "warn");
21
- return success(undefined);
22
- }
23
- const region = services.awsProvider.getRegion();
24
- // Phase 1: Initialise ECR repository with welcome image
25
- callbacks.onStepStart?.(STEP_IDS.DOCKER_OPERATIONS, "Initialising container repository");
26
- callbacks.onLog?.("Initialising ECR repository with welcome image…", "info");
27
- const ecrResult = await dockerProvider.initialiseECR({
28
- appName: operation.appName,
29
- region,
30
- accountId
31
- });
32
- if (!ecrResult.success) {
33
- callbacks.onLog?.(maskSensitiveOutput(`ECR initialisation warning: ${ecrResult.error.message}`), "warn");
34
- }
35
- callbacks.onStepComplete?.(STEP_IDS.DOCKER_OPERATIONS, "Initialising container repository", "completed");
36
- // Phase 2: Tag images for ECS services
37
- if (dockerProvider.tagImages) {
38
- callbacks.onStepStart?.(STEP_IDS.TAG_ECR_IMAGES, TAG_IMAGES_STEP_NAME);
39
- callbacks.onLog?.("Tagging ECR images for ECS services…", "info");
40
- const tagProgress = (message) => {
41
- callbacks.onLog?.(maskSensitiveOutput(message), "info");
42
- };
43
- const tagResult = await dockerProvider.tagImages({
44
- appName: operation.appName,
45
- appPath: operation.path,
46
- region,
47
- accountId
48
- }, tagProgress);
49
- if (!tagResult.success) {
50
- const error = new Error(maskSensitiveOutput(tagResult.error.message));
51
- callbacks.onError?.(error);
52
- callbacks.onStepComplete?.(STEP_IDS.TAG_ECR_IMAGES, TAG_IMAGES_STEP_NAME, "error");
53
- return failure(error);
54
- }
55
- callbacks.onLog?.(`Tagged ${tagResult.data.taggedServices.length} service(s): ${tagResult.data.taggedServices.join(", ")}`, "info");
56
- // Emit sub-step events for each tagged service so the detail panel shows per-service progress
57
- for (const service of tagResult.data.taggedServices) {
58
- callbacks.onStepStart?.(`tag-ecr-images-${service}`, `Tagged ${service}`);
59
- callbacks.onStepComplete?.(`tag-ecr-images-${service}`, `Tagged ${service}`, "completed");
60
- }
61
- callbacks.onStepComplete?.(STEP_IDS.TAG_ECR_IMAGES, TAG_IMAGES_STEP_NAME, "completed");
62
- }
63
- return success(undefined);
64
- }
1
+ import{success as a,failure as f}from"@fjall/generator";import{maskSensitiveOutput as s}from"@fjall/util";import{STEP_IDS as i}from"../types/stepDefinitions.js";const p="Tagging container images";async function R(E,m,r,e){const n=E.dockerProvider;if(!n)return a(void 0);const g=m.awsProvider.getAccountId();if(!g)return e.onLog?.("Skipping ECR initialisation \u2014 account ID not available","warn"),a(void 0);const d=m.awsProvider.getRegion();e.onStepStart?.(i.DOCKER_OPERATIONS,"Initialising container repository"),e.onLog?.("Initialising ECR repository with welcome image\u2026","info");const S=await n.initialiseECR({appName:r.appName,region:d,accountId:g});if(S.success||e.onLog?.(s(`ECR initialisation warning: ${S.error.message}`),"warn"),e.onStepComplete?.(i.DOCKER_OPERATIONS,"Initialising container repository","completed"),n.tagImages){e.onStepStart?.(i.TAG_ECR_IMAGES,p),e.onLog?.("Tagging ECR images for ECS services\u2026","info");const u=o=>{e.onLog?.(s(o),"info")},t=await n.tagImages({appName:r.appName,appPath:r.path,region:d,accountId:g},u);if(!t.success){const o=new Error(s(t.error.message));return e.onError?.(o),e.onStepComplete?.(i.TAG_ECR_IMAGES,p,"error"),f(o)}e.onLog?.(`Tagged ${t.data.taggedServices.length} service(s): ${t.data.taggedServices.join(", ")}`,"info");for(const o of t.data.taggedServices)e.onStepStart?.(`tag-ecr-images-${o}`,`Tagged ${o}`),e.onStepComplete?.(`tag-ecr-images-${o}`,`Tagged ${o}`,"completed");e.onStepComplete?.(i.TAG_ECR_IMAGES,p,"completed")}return a(void 0)}export{R as runWelcomeImageSetup};