@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.
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,160 +1 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { logger } from "@fjall/util/logger";
3
- import { maskSensitiveOutput } from "@fjall/util";
4
- import { ORGANISATION_TYPES, getOrganisationStackName } from "../types/operations.js";
5
- import { CdkContextBuilder } from "../services/supporting/CdkContextBuilder.js";
6
- import { stubCallerIdentity } from "../types/deployment/index.js";
7
- import { CloudFormationService } from "../services/infrastructure/CloudFormationService.js";
8
- import { buildParamsContext, collectStackOutputs, assumeCascadeRole, forwardOutput } from "./contextHelpers.js";
9
- import { STRUCTURAL_ENVIRONMENTS } from "@fjall/util";
10
- /**
11
- * Partition provider accounts into platform and member accounts.
12
- * Used by both deploy and destroy orchestration.
13
- */
14
- export function partitionAccounts(providerAccounts) {
15
- const platformAccount = providerAccounts.find((acc) => acc.environment === STRUCTURAL_ENVIRONMENTS.PLATFORM);
16
- const memberAccounts = providerAccounts.filter((acc) => acc.environment !== STRUCTURAL_ENVIRONMENTS.ROOT &&
17
- acc.environment !== STRUCTURAL_ENVIRONMENTS.PLATFORM);
18
- return { platformAccount, memberAccounts };
19
- }
20
- // Re-export for backwards compatibility (canonical definition in contextHelpers)
21
- export { buildCascadeRoleArn } from "./contextHelpers.js";
22
- export async function deployCascadeAccount(params, services, operation, account, deployType, callbacks, ipamPoolId) {
23
- const operationKey = `${account.name}-${account.id}`;
24
- const region = services.awsProvider.getRegion();
25
- callbacks.onCascadeAccountStart?.(operationKey, account.id, region, deployType);
26
- // Assume role in target account
27
- const roleResult = await assumeCascadeRole(services.awsProvider, account.id, region, `fjall-cascade-${account.name}`);
28
- if (!roleResult.success) {
29
- callbacks.onCascadeAccountComplete?.(operationKey, false, maskSensitiveOutput(roleResult.error.message), region);
30
- return failure(new Error(`Failed to assume role for ${account.name}: ${maskSensitiveOutput(roleResult.error.message)}`));
31
- }
32
- const { provider: accountProvider, credentials: cascadeCredentials } = roleResult.data;
33
- // Build context for this account (includes IPAM pool ID if available)
34
- const accountContext = CdkContextBuilder.buildDeploymentContext({
35
- deployType,
36
- target: operation.target,
37
- path: operation.path,
38
- region,
39
- accountName: account.name,
40
- callerIdentity: stubCallerIdentity(account.id),
41
- ipamPoolId,
42
- ...buildParamsContext({
43
- orgConfig: params.orgConfig,
44
- identity: params.identity,
45
- skipOidc: params.options?.skipOidc
46
- })
47
- }, { verbose: params.options?.verbose }, params.orgConfig);
48
- // Bootstrap the target account
49
- callbacks.onCascadeAccountPhaseChange?.(operationKey, "bootstrap", region);
50
- const bootstrapResult = await services.cdkService.runCdkBootstrap(accountContext, forwardOutput(callbacks), cascadeCredentials);
51
- if (!bootstrapResult.success) {
52
- callbacks.onCascadeAccountComplete?.(operationKey, false, maskSensitiveOutput(`Bootstrap failed: ${bootstrapResult.error}`), region);
53
- return failure(new Error(`Bootstrap failed for ${account.name}: ${maskSensitiveOutput(bootstrapResult.error)}`));
54
- }
55
- // Deploy the account stack
56
- callbacks.onCascadeAccountPhaseChange?.(operationKey, "deploy", region);
57
- const stackName = getOrganisationStackName(deployType === "platform"
58
- ? ORGANISATION_TYPES.PLATFORM
59
- : ORGANISATION_TYPES.ACCOUNT);
60
- const deployResult = await services.cdkService.runCdkDeploy(accountContext, stackName, forwardOutput(callbacks), (event) => callbacks.onCascadeAccountResourceProgress?.(operationKey, event, region), accountProvider, cascadeCredentials);
61
- if (!deployResult.success) {
62
- callbacks.onCascadeAccountComplete?.(operationKey, false, maskSensitiveOutput(deployResult.error), region);
63
- return failure(new Error(maskSensitiveOutput(deployResult.error)));
64
- }
65
- // Capture stack outputs (OIDC role ARN, etc.) from the target account
66
- const accountCfn = new CloudFormationService(accountProvider);
67
- const outputsResult = await accountCfn.getStackOutputs(stackName);
68
- if (!outputsResult.success) {
69
- logger.debug("cascadeHelpers", "Failed to read cascade account stack outputs (non-critical)", { stackName, account: account.name });
70
- }
71
- const outputs = collectStackOutputs(outputsResult);
72
- callbacks.onCascadeAccountComplete?.(operationKey, true, undefined, region, outputs);
73
- return success({ outputs });
74
- }
75
- /**
76
- * Read Platform stack outputs to extract IPAM pool IDs for member accounts.
77
- * Output keys follow the pattern `IpamPoolId{12-digit-accountId}{regionSuffix}`.
78
- * Returns a map keyed by `{accountId}-{regionSuffix}` → pool ID.
79
- * Non-fatal: returns empty map on any error.
80
- */
81
- export async function readPlatformIpamPoolIds(services, platformAccount, callbacks) {
82
- const poolIds = new Map();
83
- // Assume role in platform account to read its stack outputs
84
- const region = services.awsProvider.getRegion();
85
- const roleResult = await assumeCascadeRole(services.awsProvider, platformAccount.id, region, `fjall-ipam-read-${platformAccount.name}`);
86
- if (!roleResult.success) {
87
- logger.debug("organisationDeploy", `Cannot read Platform outputs: ${roleResult.error.message}`);
88
- return poolIds;
89
- }
90
- const platformCfn = new CloudFormationService(roleResult.data.provider);
91
- const platformStackName = getOrganisationStackName(ORGANISATION_TYPES.PLATFORM);
92
- const outputsResult = await platformCfn.getStackOutputs(platformStackName);
93
- if (!outputsResult.success) {
94
- logger.debug("organisationDeploy", `Failed to read Platform stack outputs: ${outputsResult.error.message}`);
95
- return poolIds;
96
- }
97
- const ipamPattern = /^IpamPoolId(\d{12})(\w+)$/;
98
- for (const output of outputsResult.data) {
99
- const match = output.OutputKey?.match(ipamPattern);
100
- if (match && output.OutputValue) {
101
- const key = `${match[1]}-${match[2]}`;
102
- poolIds.set(key, output.OutputValue);
103
- }
104
- }
105
- if (poolIds.size > 0) {
106
- callbacks.onLog?.(`Read ${poolIds.size} IPAM pool ID(s) from Platform stack`, "info");
107
- }
108
- return poolIds;
109
- }
110
- /**
111
- * Deploy configured domains: apex domains sequentially, then delegated
112
- * domains in parallel. Delegates to the caller-provided DomainDeployProvider.
113
- */
114
- export async function deployDomains(provider, callbacks) {
115
- const domains = provider.getDomains();
116
- if (domains.length === 0) {
117
- return { domainsDeployed: 0, errors: [] };
118
- }
119
- callbacks.onCascadePhaseStart?.("domains");
120
- const apexDomains = domains.filter((d) => d.type === "apex");
121
- const delegatedDomains = domains.filter((d) => d.type === "delegated");
122
- let domainsDeployed = 0;
123
- const errors = [];
124
- // Phase A: Apex domains (sequential — delegation roles must exist first)
125
- for (const domain of apexDomains) {
126
- const result = await provider.deployDomain(domain.name, callbacks);
127
- if (result.success) {
128
- domainsDeployed++;
129
- }
130
- else {
131
- errors.push(`${domain.name}: ${result.error.message}`);
132
- }
133
- }
134
- // Phase B: Delegated subdomains (parallel — independent of each other)
135
- if (delegatedDomains.length > 0) {
136
- const subdomainResults = await Promise.allSettled(delegatedDomains.map((domain) => provider.deployDomain(domain.name, callbacks)));
137
- for (let i = 0; i < subdomainResults.length; i++) {
138
- const settled = subdomainResults[i];
139
- const domain = delegatedDomains[i];
140
- if (!settled || !domain)
141
- continue;
142
- if (settled.status === "fulfilled") {
143
- if (settled.value.success) {
144
- domainsDeployed++;
145
- }
146
- else {
147
- errors.push(`${domain.name}: ${settled.value.error.message}`);
148
- }
149
- }
150
- else {
151
- const message = settled.reason instanceof Error
152
- ? settled.reason.message
153
- : String(settled.reason);
154
- errors.push(`${domain.name}: ${message}`);
155
- }
156
- }
157
- }
158
- callbacks.onCascadePhaseComplete?.("domains");
159
- return { domainsDeployed, errors };
160
- }
1
+ import{success as v,failure as P}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as l}from"@fjall/util";import{ORGANISATION_TYPES as O,getOrganisationStackName as A}from"../types/operations.js";import{CdkContextBuilder as x}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as N}from"../types/deployment/index.js";import{CloudFormationService as S}from"../services/infrastructure/CloudFormationService.js";import{buildParamsContext as T,collectStackOutputs as F,assumeCascadeRole as h,forwardOutput as D}from"./contextHelpers.js";import{STRUCTURAL_ENVIRONMENTS as $}from"@fjall/util";function _(r){const s=r.find(e=>e.environment===$.PLATFORM),i=r.filter(e=>e.environment!==$.ROOT&&e.environment!==$.PLATFORM);return{platformAccount:s,memberAccounts:i}}import{buildCascadeRoleArn as J}from"./contextHelpers.js";async function b(r,s,i,e,c,n,d){const o=`${e.name}-${e.id}`,t=s.awsProvider.getRegion();n.onCascadeAccountStart?.(o,e.id,t,c);const a=await h(s.awsProvider,e.id,t,`fjall-cascade-${e.name}`);if(!a.success)return n.onCascadeAccountComplete?.(o,!1,l(a.error.message),t),P(new Error(`Failed to assume role for ${e.name}: ${l(a.error.message)}`));const{provider:u,credentials:m}=a.data,p=x.buildDeploymentContext({deployType:c,target:i.target,path:i.path,region:t,accountName:e.name,callerIdentity:N(e.id),ipamPoolId:d,...T({orgConfig:r.orgConfig,identity:r.identity,skipOidc:r.options?.skipOidc})},{verbose:r.options?.verbose},r.orgConfig);n.onCascadeAccountPhaseChange?.(o,"bootstrap",t);const f=await s.cdkService.runCdkBootstrap(p,D(n),m);if(!f.success)return n.onCascadeAccountComplete?.(o,!1,l(`Bootstrap failed: ${f.error}`),t),P(new Error(`Bootstrap failed for ${e.name}: ${l(f.error)}`));n.onCascadeAccountPhaseChange?.(o,"deploy",t);const g=A(c==="platform"?O.PLATFORM:O.ACCOUNT),C=await s.cdkService.runCdkDeploy(p,g,D(n),I=>n.onCascadeAccountResourceProgress?.(o,I,t),u,m);if(!C.success)return n.onCascadeAccountComplete?.(o,!1,l(C.error),t),P(new Error(l(C.error)));const w=await new S(u).getStackOutputs(g);w.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:g,account:e.name});const y=F(w);return n.onCascadeAccountComplete?.(o,!0,void 0,t,y),v({outputs:y})}async function G(r,s,i){const e=new Map,c=r.awsProvider.getRegion(),n=await h(r.awsProvider,s.id,c,`fjall-ipam-read-${s.name}`);if(!n.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${n.error.message}`),e;const d=new S(n.data.provider),o=A(O.PLATFORM),t=await d.getStackOutputs(o);if(!t.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${t.error.message}`),e;const a=/^IpamPoolId(\d{12})(\w+)$/;for(const u of t.data){const m=u.OutputKey?.match(a);if(m&&u.OutputValue){const p=`${m[1]}-${m[2]}`;e.set(p,u.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function H(r,s){const i=r.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};s.onCascadePhaseStart?.("domains");const e=i.filter(o=>o.type==="apex"),c=i.filter(o=>o.type==="delegated");let n=0;const d=[];for(const o of e){const t=await r.deployDomain(o.name,s);t.success?n++:d.push(`${o.name}: ${t.error.message}`)}if(c.length>0){const o=await Promise.allSettled(c.map(t=>r.deployDomain(t.name,s)));for(let t=0;t<o.length;t++){const a=o[t],u=c[t];if(!(!a||!u))if(a.status==="fulfilled")a.value.success?n++:d.push(`${u.name}: ${a.value.error.message}`);else{const m=a.reason instanceof Error?a.reason.message:String(a.reason);d.push(`${u.name}: ${m}`)}}}return s.onCascadePhaseComplete?.("domains"),{domainsDeployed:n,errors:d}}export{J as buildCascadeRoleArn,b as deployCascadeAccount,H as deployDomains,_ as partitionAccounts,G as readPlatformIpamPoolIds};
@@ -1,107 +1 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
3
- import { SimpleAwsProvider } from "../aws/SimpleAwsProvider.js";
4
- /**
5
- * Build the orgConfig/identity context fields shared by all deployment paths.
6
- * CdkContextBuilder expects orgConfig as a JSON string and fjallOrgId as a string.
7
- */
8
- export function buildParamsContext(params) {
9
- return {
10
- ...(params.orgConfig !== undefined
11
- ? { orgConfig: JSON.stringify(params.orgConfig) }
12
- : {}),
13
- ...(params.identity !== undefined
14
- ? { fjallOrgId: params.identity.fjallOrgId }
15
- : {}),
16
- ...(params.skipOidc ? { fjallOidcConfigured: true } : {})
17
- };
18
- }
19
- /** Forward onOutput callback — reduces lambda repetition across orchestration files. */
20
- export function forwardOutput(callbacks) {
21
- return (chunk) => callbacks.onOutput?.(chunk);
22
- }
23
- /** Forward onResourceProgress callback — reduces lambda repetition across orchestration files. */
24
- export function forwardResourceProgress(callbacks) {
25
- return (event) => callbacks.onResourceProgress?.(event);
26
- }
27
- /** Convert CloudFormation getStackOutputs result to Record<string, string>. */
28
- export function collectStackOutputs(result) {
29
- if (!result.success || result.data.length === 0)
30
- return undefined;
31
- const record = {};
32
- for (const output of result.data) {
33
- if (output.OutputKey && output.OutputValue !== undefined) {
34
- record[output.OutputKey] = output.OutputValue;
35
- }
36
- }
37
- return Object.keys(record).length > 0 ? record : undefined;
38
- }
39
- /** Role name created by the Account CDK stack for cross-account cascade operations. */
40
- const CASCADE_DEPLOYMENT_ROLE = "fjall-deployment-role";
41
- /** Build the IAM role ARN used for cross-account cascade operations. */
42
- export function buildCascadeRoleArn(accountId) {
43
- return `arn:aws:iam::${accountId}:role/${CASCADE_DEPLOYMENT_ROLE}`;
44
- }
45
- /**
46
- * Assume a cross-account cascade role and create a scoped provider.
47
- * Callers handle failure (callbacks, logging, return type) — this helper
48
- * only owns the assume + provider construction.
49
- */
50
- export async function assumeCascadeRole(awsProvider, accountId, region, sessionName) {
51
- if (!awsProvider.assumeRole) {
52
- return failure(new Error("AwsProvider does not support assumeRole"));
53
- }
54
- const roleArn = buildCascadeRoleArn(accountId);
55
- let assumed;
56
- try {
57
- assumed = await awsProvider.assumeRole(roleArn, sessionName);
58
- }
59
- catch (err) {
60
- return failure(new Error(getErrorMessage(err)));
61
- }
62
- const provider = new SimpleAwsProvider({
63
- accessKeyId: assumed.accessKeyId,
64
- secretAccessKey: assumed.secretAccessKey,
65
- sessionToken: assumed.sessionToken,
66
- region,
67
- accountId
68
- });
69
- return success({
70
- provider,
71
- credentials: {
72
- accessKeyId: assumed.accessKeyId,
73
- secretAccessKey: assumed.secretAccessKey,
74
- sessionToken: assumed.sessionToken
75
- }
76
- });
77
- }
78
- /**
79
- * Run CDK synth and return failure with masked error if it fails.
80
- * Calls `onError` on the callbacks so callers only need to handle
81
- * step-specific reporting (e.g. onStepComplete) before returning.
82
- */
83
- export async function synthOrFail(services, context, callbacks, errorPrefix) {
84
- const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
85
- if (!synthResult.success) {
86
- const error = new Error(maskSensitiveOutput(`${errorPrefix}: ${synthResult.error}`));
87
- callbacks.onError?.(error);
88
- return failure(error);
89
- }
90
- return success(undefined);
91
- }
92
- /**
93
- * Run CDK bootstrap and return failure with masked error if it fails.
94
- * Emits onCDKBootstrap status callbacks and calls onError on failure.
95
- */
96
- export async function bootstrapOrFail(services, context, callbacks) {
97
- callbacks.onCDKBootstrap?.("bootstrapping");
98
- const bootstrapResult = await services.cdkService.runCdkBootstrap(context, forwardOutput(callbacks));
99
- if (!bootstrapResult.success) {
100
- callbacks.onCDKBootstrap?.("failed");
101
- const error = new Error(maskSensitiveOutput(`Bootstrap failed: ${bootstrapResult.error}`));
102
- callbacks.onError?.(error);
103
- return failure(error);
104
- }
105
- callbacks.onCDKBootstrap?.("complete");
106
- return success(undefined);
107
- }
1
+ import{success as i,failure as u}from"@fjall/generator";import{getErrorMessage as a,maskSensitiveOutput as c}from"@fjall/util";import{SimpleAwsProvider as p}from"../aws/SimpleAwsProvider.js";function K(e){return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{}}}function l(e){return t=>e.onOutput?.(t)}function w(e){return t=>e.onResourceProgress?.(t)}function E(e){if(!e.success||e.data.length===0)return;const t={};for(const r of e.data)r.OutputKey&&r.OutputValue!==void 0&&(t[r.OutputKey]=r.OutputValue);return Object.keys(t).length>0?t:void 0}const y="fjall-deployment-role";function O(e){return`arn:aws:iam::${e}:role/${y}`}async function A(e,t,r,s){if(!e.assumeRole)return u(new Error("AwsProvider does not support assumeRole"));const n=O(t);let o;try{o=await e.assumeRole(n,s)}catch(f){return u(new Error(a(f)))}const d=new p({accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken,region:r,accountId:t});return i({provider:d,credentials:{accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken}})}async function R(e,t,r,s){const n=await e.cdkService.runCdkSynth(t,o=>r.onCdkOutput?.(o,"synth"));if(!n.success){const o=new Error(c(`${s}: ${n.error}`));return r.onError?.(o),u(o)}return i(void 0)}async function x(e,t,r){r.onCDKBootstrap?.("bootstrapping");const s=await e.cdkService.runCdkBootstrap(t,l(r));if(!s.success){r.onCDKBootstrap?.("failed");const n=new Error(c(`Bootstrap failed: ${s.error}`));return r.onError?.(n),u(n)}return r.onCDKBootstrap?.("complete"),i(void 0)}export{A as assumeCascadeRole,x as bootstrapOrFail,O as buildCascadeRoleArn,K as buildParamsContext,E as collectStackOutputs,l as forwardOutput,w as forwardResourceProgress,R as synthOrFail};
@@ -1,42 +1 @@
1
- import { success } from "@fjall/generator";
2
- import { isApplicationOperation } from "../types/operations.js";
3
- import { createDeployServices } from "./serviceFactory.js";
4
- import { resolveOperation } from "./resolveOperation.js";
5
- import { deployApplication } from "./applicationDeploy.js";
6
- import { deployOrganisation } from "./organisationDeploy.js";
7
- /**
8
- * Deploy infrastructure for a target.
9
- *
10
- * Primary entry point for both CLI and webapp worker. Determines whether
11
- * the target is an application or organisation deployment and routes
12
- * to the appropriate orchestrator.
13
- */
14
- export async function deploy(params) {
15
- const startTime = Date.now();
16
- const services = createDeployServices(params);
17
- const opResult = await resolveOperation(params.target, params.workingDirectory);
18
- if (!opResult.success) {
19
- params.callbacks.onError?.(opResult.error);
20
- return opResult;
21
- }
22
- const operation = opResult.data;
23
- if (isApplicationOperation(operation)) {
24
- const result = await deployApplication(params, services, operation);
25
- // Attach duration if not already set
26
- if (result.success && result.data.durationMs === undefined) {
27
- return success({
28
- ...result.data,
29
- durationMs: Date.now() - startTime
30
- });
31
- }
32
- return result;
33
- }
34
- const result = await deployOrganisation(params, services, operation);
35
- if (result.success && result.data.durationMs === undefined) {
36
- return success({
37
- ...result.data,
38
- durationMs: Date.now() - startTime
39
- });
40
- }
41
- return result;
42
- }
1
+ import{success as a}from"@fjall/generator";import{isApplicationOperation as c}from"../types/operations.js";import{createDeployServices as u}from"./serviceFactory.js";import{resolveOperation as d}from"./resolveOperation.js";import{deployApplication as p}from"./applicationDeploy.js";import{deployOrganisation as f}from"./organisationDeploy.js";async function g(t){const n=Date.now(),s=u(t),o=await d(t.target,t.workingDirectory);if(!o.success)return t.callbacks.onError?.(o.error),o;const i=o.data;if(c(i)){const e=await p(t,s,i);return e.success&&e.data.durationMs===void 0?a({...e.data,durationMs:Date.now()-n}):e}const r=await f(t,s,i);return r.success&&r.data.durationMs===void 0?a({...r.data,durationMs:Date.now()-n}):r}export{g as deploy};
@@ -1,67 +1 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { isApplicationOperation, isOrganisationOperation, getApplicationStackName, getOrganisationStackName, APPLICATION_STACKS } from "../types/operations.js";
3
- import { createDeployServices } from "./serviceFactory.js";
4
- import { resolveOperation } from "./resolveOperation.js";
5
- import { destroyApplication } from "./applicationDestroy.js";
6
- import { destroyOrganisation } from "./organisationDestroy.js";
7
- import { checkActiveDeployments } from "./activeDeploymentGuard.js";
8
- /**
9
- * Destroy infrastructure for a target.
10
- *
11
- * Primary entry point for both CLI and webapp worker. Determines whether
12
- * the target is an application or organisation and routes to the
13
- * appropriate orchestrator.
14
- *
15
- * Mirrors the deploy() entry point -- same resolution, same service
16
- * factory, same callback contract.
17
- */
18
- export async function destroy(params) {
19
- const startTime = Date.now();
20
- const services = createDeployServices({
21
- target: params.target,
22
- workingDirectory: params.workingDirectory,
23
- awsCredentials: params.awsCredentials,
24
- callbacks: params.callbacks,
25
- options: {
26
- verbose: params.options?.verbose,
27
- force: params.options?.force,
28
- cascade: params.options?.cascade,
29
- skipConfirmation: params.options?.skipConfirmation
30
- },
31
- orgConfig: params.orgConfig,
32
- identity: params.identity
33
- });
34
- const opResult = await resolveOperation(params.target, params.workingDirectory);
35
- if (!opResult.success) {
36
- params.callbacks.onError?.(opResult.error);
37
- return opResult;
38
- }
39
- const operation = opResult.data;
40
- // Pre-flight: reject if any target stacks are mid-operation (unless --force)
41
- if (!params.options?.force) {
42
- const stackNames = isApplicationOperation(operation)
43
- ? Object.values(APPLICATION_STACKS).map((stack) => getApplicationStackName(operation.appName, stack))
44
- : [getOrganisationStackName(operation.type)];
45
- const guardResult = await checkActiveDeployments(stackNames, services.cfnService);
46
- if (!guardResult.success) {
47
- params.callbacks.onError?.(guardResult.error);
48
- return failure(guardResult.error);
49
- }
50
- }
51
- if (isApplicationOperation(operation)) {
52
- const result = await destroyApplication(params, services, operation);
53
- if (result.success && result.data.durationMs === undefined) {
54
- return success({
55
- ...result.data,
56
- durationMs: Date.now() - startTime
57
- });
58
- }
59
- return result;
60
- }
61
- if (isOrganisationOperation(operation)) {
62
- return destroyOrganisation(params, services, operation);
63
- }
64
- const error = new Error(`Unsupported destroy target: ${params.target}`);
65
- params.callbacks.onError?.(error);
66
- return failure(error);
67
- }
1
+ import{success as u,failure as s}from"@fjall/generator";import{isApplicationOperation as a,isOrganisationOperation as d,getApplicationStackName as g,getOrganisationStackName as p,APPLICATION_STACKS as k}from"../types/operations.js";import{createDeployServices as y}from"./serviceFactory.js";import{resolveOperation as w}from"./resolveOperation.js";import{destroyApplication as b}from"./applicationDestroy.js";import{destroyOrganisation as v}from"./organisationDestroy.js";import{checkActiveDeployments as C}from"./activeDeploymentGuard.js";async function T(o){const f=Date.now(),i=y({target:o.target,workingDirectory:o.workingDirectory,awsCredentials:o.awsCredentials,callbacks:o.callbacks,options:{verbose:o.options?.verbose,force:o.options?.force,cascade:o.options?.cascade,skipConfirmation:o.options?.skipConfirmation},orgConfig:o.orgConfig,identity:o.identity}),r=await w(o.target,o.workingDirectory);if(!r.success)return o.callbacks.onError?.(r.error),r;const t=r.data;if(!o.options?.force){const e=a(t)?Object.values(k).map(l=>g(t.appName,l)):[p(t.type)],n=await C(e,i.cfnService);if(!n.success)return o.callbacks.onError?.(n.error),s(n.error)}if(a(t)){const e=await b(o,i,t);return e.success&&e.data.durationMs===void 0?u({...e.data,durationMs:Date.now()-f}):e}if(d(t))return v(o,i,t);const c=new Error(`Unsupported destroy target: ${o.target}`);return o.callbacks.onError?.(c),s(c)}export{T as destroy};
@@ -1,84 +1 @@
1
- import { join } from "path";
2
- import { success, failure } from "@fjall/generator";
3
- import { logger } from "@fjall/util/logger";
4
- import { maskSensitiveOutput } from "@fjall/util";
5
- import { hasDockerfile } from "../util/dockerfileDetection.js";
6
- import { deriveResourcesFromManifestStacks } from "../types/patternDetection.js";
7
- import { emitProgress, PROGRESS_MESSAGES } from "../services/supporting/helpers.js";
8
- import { parseRequiredSecretsFromManifest } from "./manifestSecretParser.js";
9
- /**
10
- * Pre-deployment analysis pipeline.
11
- *
12
- * Detects the application pattern, synthesises infrastructure, computes
13
- * template hashes, and determines which stacks have changed.
14
- */
15
- export async function runDetectionPipeline(operation, services, context, callbacks) {
16
- // 1. Detect application pattern
17
- callbacks.onDetectionPhaseChange?.("detect", "started");
18
- const resolved = services.frameworkRegistry.resolve({
19
- appPath: operation.path
20
- });
21
- const pattern = resolved?.detection.pattern ?? null;
22
- const hasDatabase = resolved?.detection.hasDatabase ?? false;
23
- callbacks.onLog?.(`Pattern detected: ${pattern ?? "standard"}`, "info");
24
- callbacks.onDetectionPhaseChange?.("detect", "completed");
25
- const cdkOutPath = join(operation.path, "cdk.out");
26
- // 2. Synthesise infrastructure
27
- callbacks.onDetectionPhaseChange?.("synth", "started");
28
- emitProgress(callbacks, PROGRESS_MESSAGES.SYNTH);
29
- const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
30
- if (!synthResult.success) {
31
- return failure(new Error(maskSensitiveOutput(`CDK synthesis failed: ${synthResult.error}`)));
32
- }
33
- callbacks.onDetectionPhaseChange?.("synth", "completed");
34
- // 2b. Parse required secrets from manifest (pure data, no AWS calls)
35
- const requiredSecrets = parseRequiredSecretsFromManifest(cdkOutPath);
36
- if (requiredSecrets.length > 0) {
37
- callbacks.onLog?.(`Found ${requiredSecrets.length} required secret path(s) in manifest`, "info");
38
- }
39
- // 3. Compute template hashes
40
- callbacks.onDetectionPhaseChange?.("hash", "started");
41
- emitProgress(callbacks, PROGRESS_MESSAGES.HASH);
42
- const hashResult = await services.hashService.getTemplateHashes(cdkOutPath);
43
- if (!hashResult.success) {
44
- return failure(new Error(maskSensitiveOutput(`Template hash computation failed: ${hashResult.error.message}`)));
45
- }
46
- const currentHashes = hashResult.data;
47
- // 4. Compare with stored state
48
- const comparisonResult = await services.hashService.compareWithState(currentHashes, operation.path);
49
- if (!comparisonResult.success) {
50
- return failure(new Error(maskSensitiveOutput(`Hash comparison failed: ${comparisonResult.error.message}`)));
51
- }
52
- const comparison = comparisonResult.data;
53
- callbacks.onDetectionPhaseChange?.("hash", "completed");
54
- // 5. Stale hash detection: unchanged templates whose stacks don't exist in CFN
55
- const stackChanges = new Map(comparison.stackChanges);
56
- for (const [stackName, hasChanged] of comparison.stackChanges) {
57
- if (!hasChanged) {
58
- const exists = await services.cfnService.stackExists(stackName);
59
- if (!exists) {
60
- logger.debug("detectionPipeline", "Stale hash detected — stack missing in CFN", {
61
- stackName
62
- });
63
- stackChanges.set(stackName, true);
64
- }
65
- }
66
- }
67
- // 6. Derive resource flags from manifest stacks
68
- const stackNames = Array.from(currentHashes.keys());
69
- const resources = deriveResourcesFromManifestStacks(stackNames);
70
- // 7. Detect Dockerfile on disk (Compute stack alone is not sufficient)
71
- const dockerfilePresent = resources.hasCompute && hasDockerfile(operation.path);
72
- const hasDifferences = Array.from(stackChanges.values()).some(Boolean);
73
- callbacks.onLog?.(`Detection complete: ${comparison.changedCount} changed, ${comparison.unchangedCount} unchanged`, "info");
74
- return success({
75
- pattern,
76
- hasDatabase,
77
- hasDifferences,
78
- stackChanges,
79
- currentHashes,
80
- resources,
81
- hasDockerfile: dockerfilePresent,
82
- requiredSecrets
83
- });
84
- }
1
+ import{join as E}from"path";import{success as $,failure as m}from"@fjall/generator";import{logger as H}from"@fjall/util/logger";import{maskSensitiveOutput as d}from"@fjall/util";import{hasDockerfile as v}from"../util/dockerfileDetection.js";import{deriveResourcesFromManifestStacks as x}from"../types/patternDetection.js";import{emitProgress as C,PROGRESS_MESSAGES as D}from"../services/supporting/helpers.js";import{parseRequiredSecretsFromManifest as A}from"./manifestSecretParser.js";async function B(n,t,P,e){e.onDetectionPhaseChange?.("detect","started");const u=t.frameworkRegistry.resolve({appPath:n.path}),f=u?.detection.pattern??null,y=u?.detection.hasDatabase??!1;e.onLog?.(`Pattern detected: ${f??"standard"}`,"info"),e.onDetectionPhaseChange?.("detect","completed");const p=E(n.path,"cdk.out");e.onDetectionPhaseChange?.("synth","started"),C(e,D.SYNTH);const g=await t.cdkService.runCdkSynth(P,s=>e.onCdkOutput?.(s,"synth"));if(!g.success)return m(new Error(d(`CDK synthesis failed: ${g.error}`)));e.onDetectionPhaseChange?.("synth","completed");const r=A(p);r.length>0&&e.onLog?.(`Found ${r.length} required secret path(s) in manifest`,"info"),e.onDetectionPhaseChange?.("hash","started"),C(e,D.HASH);const a=await t.hashService.getTemplateHashes(p);if(!a.success)return m(new Error(d(`Template hash computation failed: ${a.error.message}`)));const i=a.data,h=await t.hashService.compareWithState(i,n.path);if(!h.success)return m(new Error(d(`Hash comparison failed: ${h.error.message}`)));const o=h.data;e.onDetectionPhaseChange?.("hash","completed");const c=new Map(o.stackChanges);for(const[s,l]of o.stackChanges)l||await t.cfnService.stackExists(s)||(H.debug("detectionPipeline","Stale hash detected \u2014 stack missing in CFN",{stackName:s}),c.set(s,!0));const w=Array.from(i.keys()),S=x(w),k=S.hasCompute&&v(n.path),R=Array.from(c.values()).some(Boolean);return e.onLog?.(`Detection complete: ${o.changedCount} changed, ${o.unchangedCount} unchanged`,"info"),$({pattern:f,hasDatabase:y,hasDifferences:R,stackChanges:c,currentHashes:i,resources:S,hasDockerfile:k,requiredSecrets:r})}export{B as runDetectionPipeline};
@@ -1,49 +1 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { maskSensitiveOutput } from "@fjall/util";
3
- import { STEP_IDS } from "../types/stepDefinitions.js";
4
- const DOCKER_BUILD_STEP_NAME = "Building and pushing Docker image";
5
- /**
6
- * Run Docker build and push before deploying the Compute stack.
7
- * Initialises ECR if needed, then builds and pushes the image.
8
- */
9
- export async function runDockerBuild(params, services, operation, callbacks) {
10
- const dockerProvider = params.dockerProvider;
11
- if (!dockerProvider)
12
- return success(undefined);
13
- const accountId = services.awsProvider.getAccountId();
14
- if (!accountId) {
15
- callbacks.onLog?.("Skipping Docker build — account ID not available", "warn");
16
- return success(undefined);
17
- }
18
- const region = services.awsProvider.getRegion();
19
- callbacks.onStepStart?.(STEP_IDS.DOCKER_OPERATIONS, DOCKER_BUILD_STEP_NAME);
20
- // Initialise ECR repository (non-fatal — CDK creates it if needed)
21
- callbacks.onLog?.("Initialising ECR repository…", "info");
22
- const ecrResult = await dockerProvider.initialiseECR({
23
- appName: operation.appName,
24
- region,
25
- accountId
26
- });
27
- if (!ecrResult.success) {
28
- callbacks.onLog?.(`ECR initialisation failed: ${ecrResult.error.message}`, "warn");
29
- }
30
- // Build and push Docker image
31
- callbacks.onLog?.("Building and pushing Docker image…", "info");
32
- const buildResult = await dockerProvider.buildAndPush({
33
- appName: operation.appName,
34
- appPath: operation.path,
35
- region,
36
- accountId
37
- }, (message, percentage, layerId, status) => {
38
- callbacks.onDockerProgress?.(maskSensitiveOutput(message), percentage, layerId, status);
39
- });
40
- if (!buildResult.success) {
41
- callbacks.onStepComplete?.(STEP_IDS.DOCKER_OPERATIONS, DOCKER_BUILD_STEP_NAME, "error");
42
- const error = new Error(maskSensitiveOutput(buildResult.error.message));
43
- callbacks.onError?.(error);
44
- return failure(error);
45
- }
46
- callbacks.onStepComplete?.(STEP_IDS.DOCKER_OPERATIONS, DOCKER_BUILD_STEP_NAME, "completed");
47
- callbacks.onLog?.(`Docker image pushed: ${buildResult.data.imageUri}`, "info");
48
- return success(undefined);
49
- }
1
+ import{success as u,failure as c}from"@fjall/generator";import{maskSensitiveOutput as m}from"@fjall/util";import{STEP_IDS as d}from"../types/stepDefinitions.js";const p="Building and pushing Docker image";async function C(f,s,r,e){const i=f.dockerProvider;if(!i)return u(void 0);const n=s.awsProvider.getAccountId();if(!n)return e.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),u(void 0);const a=s.awsProvider.getRegion();e.onStepStart?.(d.DOCKER_OPERATIONS,p),e.onLog?.("Initialising ECR repository\u2026","info");const g=await i.initialiseECR({appName:r.appName,region:a,accountId:n});g.success||e.onLog?.(`ECR initialisation failed: ${g.error.message}`,"warn"),e.onLog?.("Building and pushing Docker image\u2026","info");const t=await i.buildAndPush({appName:r.appName,appPath:r.path,region:a,accountId:n},(o,E,D,R)=>{e.onDockerProgress?.(m(o),E,D,R)});if(!t.success){e.onStepComplete?.(d.DOCKER_OPERATIONS,p,"error");const o=new Error(m(t.error.message));return e.onError?.(o),c(o)}return e.onStepComplete?.(d.DOCKER_OPERATIONS,p,"completed"),e.onLog?.(`Docker image pushed: ${t.data.imageUri}`,"info"),u(void 0)}export{C as runDockerBuild};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};