@fjall/deploy-core 0.94.0 → 0.95.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
|
@@ -1,189 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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};
|