@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,213 +1,3 @@
1
- import { existsSync } from "fs";
2
- import { join } from "path";
3
- import { logger } from "@fjall/util/logger";
4
- import { success, failure } from "@fjall/generator";
5
- import { DEFAULT_REGION } from "../../aws/utils/regions.js";
6
- import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
7
- import { CdkEventMonitor, startStackMonitoring } from "./CdkEventMonitoring.js";
8
- import { analyseDeployOutput, analyseDestroyResult, createEnhancedOutputCallback } from "./CdkOutputAnalyser.js";
9
- import { CdkCommandRunner } from "./CdkCommandRunner.js";
10
- import { CdkArgumentBuilder } from "./CdkArgumentBuilder.js";
11
- import { CdkProcessManager } from "./CdkProcessManager.js";
12
- import { wrapWithConstructMapEnrichment } from "./constructMapEnrichment.js";
13
- import { STACK_DETECTION_FALLBACK_MS, resolveStackName, getFallbackStackName, buildDeploymentCdkContext } from "./cdkServiceHelpers.js";
14
- export class CdkService {
15
- commandRunner;
16
- eventMonitor;
17
- constructor(options) {
18
- const processManager = options?.processManager ??
19
- new CdkProcessManager(new CdkArgumentBuilder());
20
- this.commandRunner = new CdkCommandRunner(processManager);
21
- this.eventMonitor = new CdkEventMonitor({
22
- eventLogWriterFactory: options?.eventLogWriterFactory
23
- });
24
- }
25
- dispose() {
26
- this.commandRunner.dispose();
27
- }
28
- // Delegate low-level commands to CdkCommandRunner
29
- async checkDifferences(path, stackName, options) {
30
- return this.commandRunner.checkDifferences(path, stackName, options);
31
- }
32
- async deploy(path, stackName, options) {
33
- return this.commandRunner.deploy(path, stackName, options);
34
- }
35
- async destroy(path, stackName, options) {
36
- return this.commandRunner.destroy(path, stackName, options);
37
- }
38
- async runImport(path, resourceMappingFile, options) {
39
- return this.commandRunner.runImport(path, resourceMappingFile, options);
40
- }
41
- async synth(path, options) {
42
- return this.commandRunner.synth(path, options);
43
- }
44
- async bootstrap(accountId, region, options) {
45
- return this.commandRunner.bootstrap(accountId, region, options);
46
- }
47
- async runCdkSynth(context, onOutput) {
48
- const accountId = context.callerIdentity?.Account;
49
- try {
50
- const result = await this.synth(context.path, {
51
- outputCallback: onOutput,
52
- context: buildDeploymentCdkContext(context, accountId, context.region || DEFAULT_REGION)
53
- });
54
- if (!result.success) {
55
- return failure(result.error || "Failed to synthesise CloudFormation template");
56
- }
57
- return success({
58
- message: "CloudFormation template synthesised",
59
- details: result.data.output
60
- ? { synthesisTime: result.data.output }
61
- : undefined
62
- });
63
- }
64
- catch (error) {
65
- return failure(`CDK synth failed: ${getErrorMessage(error)}`);
66
- }
67
- }
68
- async runCdkBootstrap(context, onOutput, credentials) {
69
- const accountId = context.callerIdentity?.Account;
70
- const region = context.region || DEFAULT_REGION;
71
- try {
72
- if (!accountId) {
73
- return failure("No AWS account ID available");
74
- }
75
- const nodeModulesPath = join(context.path, "node_modules");
76
- if (!existsSync(nodeModulesPath)) {
77
- return failure(`Dependencies not installed. Please run 'npm install' in ${context.path} before deploying.`);
78
- }
79
- const result = await this.bootstrap(accountId, region, {
80
- outputCallback: onOutput,
81
- credentials
82
- });
83
- if (!result.success) {
84
- return failure(result.error || "Failed to bootstrap AWS environment");
85
- }
86
- return success({ message: "AWS environment bootstrapped" });
87
- }
88
- catch (error) {
89
- return failure(`CDK bootstrap failed: ${getErrorMessage(error)}`);
90
- }
91
- }
92
- async runCdkDiff(context, onOutput) {
93
- const accountId = context.callerIdentity?.Account;
94
- try {
95
- const result = await this.checkDifferences(context.path, undefined, {
96
- verbose: context.options?.verbose,
97
- outputCallback: onOutput,
98
- context: buildDeploymentCdkContext(context, accountId, context.region || DEFAULT_REGION)
99
- });
100
- if (!result.success) {
101
- return failure(`CDK diff failed: ${result.error.message}`);
102
- }
103
- return success({
104
- message: "Diff check complete",
105
- details: {
106
- hasDifferences: result.data.hasDifferences,
107
- details: result.data.details
108
- }
109
- });
110
- }
111
- catch (error) {
112
- return failure(`CDK diff failed: ${getErrorMessage(error)}`);
113
- }
114
- }
115
- async runCdkDeploy(context, stackPattern, onOutput, onResourceProgress, aws, credentials) {
116
- const accountId = context.callerIdentity?.Account;
117
- const region = context.region || DEFAULT_REGION;
118
- if (!accountId) {
119
- return failure("AWS account ID not available. Please ensure AWS credentials are properly configured.");
120
- }
121
- if (!aws) {
122
- return failure("AwsProvider is required for deployment monitoring.");
123
- }
124
- // Read construct map from fjall-manifest.json (written during synth)
125
- const enrichedOnResourceProgress = wrapWithConstructMapEnrichment(context.path, onResourceProgress);
126
- let cfnMonitor = null;
127
- let fallbackMonitoringTimeout;
128
- try {
129
- const targetStackName = resolveStackName(stackPattern, context) ??
130
- getFallbackStackName(context);
131
- cfnMonitor = await this.eventMonitor.createEventMonitor("deploy", targetStackName, region, context, aws);
132
- if (onOutput) {
133
- onOutput(maskSensitiveOutput(`Starting CloudFormation deployment of ${targetStackName}...\n`));
134
- onOutput(`Monitoring CloudFormation events (CDK process running in background)...\n`);
135
- }
136
- const state = {
137
- cdkOutput: "",
138
- actualStackName: targetStackName,
139
- stackDetected: false,
140
- monitoringPromise: null
141
- };
142
- const enhancedOutputCallback = createEnhancedOutputCallback(state, onOutput, cfnMonitor, enrichedOnResourceProgress);
143
- // Fall back to predicted stack name if actual name not detected in time
144
- fallbackMonitoringTimeout = setTimeout(() => {
145
- if (!state.stackDetected && cfnMonitor && !state.monitoringPromise) {
146
- logger.debug("CdkService", "Fallback monitoring STARTING", {
147
- targetStackName,
148
- stackDetected: state.stackDetected,
149
- hasOnResourceProgress: !!onResourceProgress
150
- });
151
- state.monitoringPromise = startStackMonitoring(cfnMonitor, targetStackName, enrichedOnResourceProgress);
152
- }
153
- }, STACK_DETECTION_FALLBACK_MS);
154
- const result = await this.deploy(context.path, targetStackName, {
155
- verbose: context.options?.verbose,
156
- outputCallback: enhancedOutputCallback,
157
- useCdkOut: true,
158
- cdkOutputLogger: cfnMonitor?.getEventLogger() ?? undefined,
159
- context: buildDeploymentCdkContext(context, accountId, region),
160
- credentials
161
- });
162
- return analyseDeployOutput(state.cdkOutput, result, state.actualStackName);
163
- }
164
- catch (error) {
165
- const errorMsg = `CDK deploy failed: ${getErrorMessage(error)}`;
166
- logger.error("CdkService", "CDK deployment exception", {
167
- error: errorMsg
168
- });
169
- return failure(errorMsg);
170
- }
171
- finally {
172
- clearTimeout(fallbackMonitoringTimeout);
173
- if (cfnMonitor) {
174
- cfnMonitor.stopMonitoring();
175
- }
176
- }
177
- }
178
- async runCdkDestroy(context, stackPattern, onOutput, onResourceProgress, aws, useCdkOut, credentials) {
179
- const accountId = context.callerIdentity?.Account;
180
- const region = context.region || DEFAULT_REGION;
181
- let cfnMonitor = null;
182
- try {
183
- const targetStackName = resolveStackName(stackPattern, context);
184
- if (accountId && targetStackName && aws) {
185
- cfnMonitor = await this.eventMonitor.createEventMonitor("destroy", targetStackName, region, context, aws);
186
- cfnMonitor.startMonitoring(targetStackName, (event) => {
187
- onResourceProgress?.(event);
188
- }, (_success, _message) => {
189
- // Stack destroy completed — no console output per architecture
190
- });
191
- }
192
- const result = await this.destroy(context.path, stackPattern, {
193
- verbose: context.options?.verbose,
194
- outputCallback: onOutput,
195
- useCdkOut,
196
- cdkOutputLogger: cfnMonitor?.getEventLogger() ?? undefined,
197
- context: buildDeploymentCdkContext(context, accountId, region, {
198
- includeImageVersion: false
199
- }),
200
- credentials
201
- });
202
- return analyseDestroyResult(result);
203
- }
204
- catch (error) {
205
- return failure(`CDK destroy failed: ${getErrorMessage(error)}`);
206
- }
207
- finally {
208
- if (cfnMonitor) {
209
- cfnMonitor.stopMonitoring();
210
- }
211
- }
212
- }
213
- }
1
+ import{existsSync as v}from"fs";import{join as M}from"path";import{logger as C}from"@fjall/util/logger";import{success as h,failure as s}from"@fjall/generator";import{DEFAULT_REGION as f}from"../../aws/utils/regions.js";import{getErrorMessage as g,maskSensitiveOutput as S}from"@fjall/util";import{CdkEventMonitor as I,startStackMonitoring as A}from"./CdkEventMonitoring.js";import{analyseDeployOutput as R,analyseDestroyResult as E,createEnhancedOutputCallback as F}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as w}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as K}from"./CdkArgumentBuilder.js";import{CdkProcessManager as N}from"./CdkProcessManager.js";import{wrapWithConstructMapEnrichment as O}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as T,resolveStackName as D,getFallbackStackName as L,buildDeploymentCdkContext as y}from"./cdkServiceHelpers.js";class X{commandRunner;eventMonitor;constructor(e){const t=e?.processManager??new N(new K);this.commandRunner=new w(t),this.eventMonitor=new I({eventLogWriterFactory:e?.eventLogWriterFactory})}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t){const r=e.callerIdentity?.Account;try{const n=await this.synth(e.path,{outputCallback:t,context:y(e,r,e.region||f)});return n.success?h({message:"CloudFormation template synthesised",details:n.data.output?{synthesisTime:n.data.output}:void 0}):s(n.error||"Failed to synthesise CloudFormation template")}catch(n){return s(`CDK synth failed: ${g(n)}`)}}async runCdkBootstrap(e,t,r){const n=e.callerIdentity?.Account,u=e.region||f;try{if(!n)return s("No AWS account ID available");const d=M(e.path,"node_modules");if(!v(d))return s(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const l=await this.bootstrap(n,u,{outputCallback:t,credentials:r});return l.success?h({message:"AWS environment bootstrapped"}):s(l.error||"Failed to bootstrap AWS environment")}catch(d){return s(`CDK bootstrap failed: ${g(d)}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:y(e,r,e.region||f)});return n.success?h({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):s(`CDK diff failed: ${n.error.message}`)}catch(n){return s(`CDK diff failed: ${g(n)}`)}}async runCdkDeploy(e,t,r,n,u,d){const l=e.callerIdentity?.Account,m=e.region||f;if(!l)return s("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!u)return s("AwsProvider is required for deployment monitoring.");const p=O(e.path,n);let o=null,c;try{const i=D(t,e)??L(e);o=await this.eventMonitor.createEventMonitor("deploy",i,m,e,u),r&&(r(S(`Starting CloudFormation deployment of ${i}...
2
+ `)),r(`Monitoring CloudFormation events (CDK process running in background)...
3
+ `));const a={cdkOutput:"",actualStackName:i,stackDetected:!1,monitoringPromise:null},k=F(a,r,o,p);c=setTimeout(()=>{!a.stackDetected&&o&&!a.monitoringPromise&&(C.debug("CdkService","Fallback monitoring STARTING",{targetStackName:i,stackDetected:a.stackDetected,hasOnResourceProgress:!!n}),a.monitoringPromise=A(o,i,p))},T);const b=await this.deploy(e.path,i,{verbose:e.options?.verbose,outputCallback:k,useCdkOut:!0,cdkOutputLogger:o?.getEventLogger()??void 0,context:y(e,l,m),credentials:d});return R(a.cdkOutput,b,a.actualStackName)}catch(i){const a=`CDK deploy failed: ${g(i)}`;return C.error("CdkService","CDK deployment exception",{error:a}),s(a)}finally{clearTimeout(c),o&&o.stopMonitoring()}}async runCdkDestroy(e,t,r,n,u,d,l){const m=e.callerIdentity?.Account,p=e.region||f;let o=null;try{const c=D(t,e);m&&c&&u&&(o=await this.eventMonitor.createEventMonitor("destroy",c,p,e,u),o.startMonitoring(c,a=>{n?.(a)},(a,k)=>{}));const i=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:d,cdkOutputLogger:o?.getEventLogger()??void 0,context:y(e,m,p,{includeImageVersion:!1}),credentials:l});return E(i)}catch(c){return s(`CDK destroy failed: ${g(c)}`)}finally{o&&o.stopMonitoring()}}}export{X as CdkService};
@@ -1,248 +1 @@
1
- import { CloudFormationClient, DeleteStackCommand, DescribeStacksCommand, ListExportsCommand } from "@aws-sdk/client-cloudformation";
2
- import { stackStatusMap } from "../../aws/utils/stackStatus.js";
3
- import { success, failure } from "@fjall/generator";
4
- import { BaseServiceError } from "../../types/errors/ServiceError.js";
5
- import { logger } from "@fjall/util/logger";
6
- import { getErrorMessage, sleep } from "@fjall/util";
7
- import { STACK_NOT_FOUND_PATTERN } from "@fjall/util/aws";
8
- import { extractErrorName } from "../../aws/organisations/types.js";
9
- /**
10
- * CloudFormation-specific error
11
- */
12
- export class CloudFormationError extends BaseServiceError {
13
- errorType;
14
- stackName;
15
- stackStatus;
16
- constructor(message, errorType, stackName, stackStatus, details, recoverable = false) {
17
- super(`CFN_${errorType.toUpperCase()}`, message, details, recoverable);
18
- this.errorType = errorType;
19
- this.stackName = stackName;
20
- this.stackStatus = stackStatus;
21
- }
22
- }
23
- /**
24
- * CloudFormation service using the AwsProvider interface.
25
- *
26
- * Unlike the CLI version which lazily initialises AwsContext,
27
- * deploy-core receives a pre-configured AwsProvider at construction.
28
- */
29
- export class CloudFormationService {
30
- aws;
31
- constructor(aws) {
32
- this.aws = aws;
33
- }
34
- /**
35
- * Classify an AWS SDK error into a typed CloudFormationError.
36
- */
37
- classifyAwsError(error, fallbackMessage, stackName) {
38
- const errorName = extractErrorName(error);
39
- const errorMessage = getErrorMessage(error);
40
- if (errorName === "CredentialsError" || errorName === "UnauthorizedError") {
41
- return new CloudFormationError(`AWS credentials error: ${errorMessage}`, "auth_error", stackName, undefined, error, false);
42
- }
43
- if (errorName === "Throttling" ||
44
- errorName === "TooManyRequestsException") {
45
- return new CloudFormationError(`AWS rate limit exceeded: ${errorMessage}`, "throttled", stackName, undefined, error, true);
46
- }
47
- if (errorName === "NetworkingError" || errorName === "ENOTFOUND") {
48
- return new CloudFormationError(`Network error: ${errorMessage}`, "network_error", stackName, undefined, error, true);
49
- }
50
- return new CloudFormationError(`${fallbackMessage}: ${errorMessage}`, "unknown", stackName, undefined, error);
51
- }
52
- /**
53
- * Get stack outputs for services that need them
54
- */
55
- async getStackOutputs(stackName, callbacks) {
56
- callbacks?.onStackCheck?.(stackName);
57
- const client = this.aws.getClient(CloudFormationClient);
58
- const command = new DescribeStacksCommand({ StackName: stackName });
59
- try {
60
- const response = await client.send(command);
61
- const stack = response.Stacks?.[0];
62
- if (!stack?.Outputs) {
63
- return success([]);
64
- }
65
- const outputs = stack.Outputs.map((output) => ({
66
- OutputKey: output.OutputKey,
67
- OutputValue: output.OutputValue,
68
- ExportName: output.ExportName
69
- }));
70
- callbacks?.onOutputsRetrieved?.(stackName, outputs.length);
71
- return success(outputs);
72
- }
73
- catch (error) {
74
- if (error instanceof Error &&
75
- error.name === "ValidationError" &&
76
- error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
77
- callbacks?.onStackNotFound?.(stackName);
78
- return success([]);
79
- }
80
- return failure(new CloudFormationError(`Failed to get outputs for stack ${stackName}: ${getErrorMessage(error)}`, "unknown", stackName, undefined, error));
81
- }
82
- }
83
- /**
84
- * Get stack status directly
85
- */
86
- async getStackStatus(stackName, callbacks) {
87
- callbacks?.onStackCheck?.(stackName);
88
- const client = this.aws.getClient(CloudFormationClient);
89
- const command = new DescribeStacksCommand({ StackName: stackName });
90
- try {
91
- const response = await client.send(command);
92
- const stack = response.Stacks?.[0];
93
- if (!stack) {
94
- return success({
95
- status: "DOES_NOT_EXIST",
96
- safeToRedeploy: "Yes",
97
- description: "Stack does not exist yet"
98
- });
99
- }
100
- const status = stack.StackStatus || "UNKNOWN";
101
- const statusInfo = stackStatusMap[status] || stackStatusMap["UNKNOWN"];
102
- callbacks?.onStackFound?.(stackName, status);
103
- return success({
104
- status,
105
- safeToRedeploy: statusInfo.safeToRedeploy,
106
- description: statusInfo.description,
107
- statusReason: stack.StackStatusReason
108
- });
109
- }
110
- catch (error) {
111
- if (error instanceof Error &&
112
- error.name === "ValidationError" &&
113
- error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
114
- return success({
115
- status: "DOES_NOT_EXIST",
116
- safeToRedeploy: "Yes",
117
- description: "Stack does not exist yet"
118
- });
119
- }
120
- return failure(this.classifyAwsError(error, `Failed to get stack status for ${stackName}`, stackName));
121
- }
122
- }
123
- /**
124
- * Internal helper to list all CloudFormation exports with pagination.
125
- */
126
- async listAllExports(earlyExit) {
127
- const client = this.aws.getClient(CloudFormationClient);
128
- const allExports = [];
129
- try {
130
- let nextToken;
131
- do {
132
- const command = new ListExportsCommand({ NextToken: nextToken });
133
- const response = await client.send(command);
134
- const pageExports = response.Exports || [];
135
- for (const exp of pageExports) {
136
- if (exp.Name && exp.Value) {
137
- allExports.push({ Name: exp.Name, Value: exp.Value });
138
- }
139
- }
140
- if (earlyExit?.(pageExports)) {
141
- break;
142
- }
143
- nextToken = response.NextToken;
144
- } while (nextToken);
145
- return success(allExports);
146
- }
147
- catch (error) {
148
- return failure(new CloudFormationError(`Failed to list exports: ${getErrorMessage(error)}`, "unknown", undefined, undefined, error, false));
149
- }
150
- }
151
- /**
152
- * Look up specific CloudFormation exports by exact name.
153
- */
154
- async getExportsByNames(names) {
155
- if (names.length === 0) {
156
- return success(new Map());
157
- }
158
- const nameSet = new Set(names);
159
- const found = new Map();
160
- const result = await this.listAllExports((pageExports) => {
161
- for (const exp of pageExports) {
162
- if (exp.Name && nameSet.has(exp.Name) && exp.Value) {
163
- found.set(exp.Name, exp.Value);
164
- }
165
- }
166
- return found.size >= nameSet.size;
167
- });
168
- if (!result.success) {
169
- return failure(result.error);
170
- }
171
- return success(found);
172
- }
173
- /**
174
- * List all CloudFormation exports
175
- */
176
- async listExports(callbacks) {
177
- const result = await this.listAllExports();
178
- if (result.success) {
179
- callbacks?.onExportsRetrieved?.(result.data.length);
180
- }
181
- return result;
182
- }
183
- /**
184
- * Delete a CloudFormation stack directly via the API.
185
- * Unlike CDK destroy, this works on stacks in DELETE_FAILED state.
186
- */
187
- async deleteStack(stackName) {
188
- const client = this.aws.getClient(CloudFormationClient);
189
- try {
190
- await client.send(new DeleteStackCommand({ StackName: stackName }));
191
- return success(undefined);
192
- }
193
- catch (error) {
194
- return failure(this.classifyAwsError(error, `Failed to delete stack ${stackName}`, stackName));
195
- }
196
- }
197
- /**
198
- * Check whether a CloudFormation stack exists and is in a deployable state.
199
- */
200
- async stackExists(stackName, client) {
201
- const cfnClient = client ?? this.aws.getClient(CloudFormationClient);
202
- try {
203
- const response = await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }));
204
- const status = response.Stacks?.[0]?.StackStatus;
205
- return !!status && status !== "REVIEW_IN_PROGRESS";
206
- }
207
- catch (error) {
208
- if (error instanceof Error &&
209
- error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
210
- return false;
211
- }
212
- logger.debug("CloudFormationService", "Error checking stack existence, assuming exists", {
213
- stackName,
214
- error: getErrorMessage(error)
215
- });
216
- return true;
217
- }
218
- }
219
- /**
220
- * Poll stack status until deletion completes, fails, or times out.
221
- */
222
- async waitForDeleteComplete(stackName, options) {
223
- const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000;
224
- const pollIntervalMs = options?.pollIntervalMs ?? 5000;
225
- const start = Date.now();
226
- while (Date.now() - start < timeoutMs) {
227
- const statusResult = await this.getStackStatus(stackName);
228
- if (!statusResult.success) {
229
- if (statusResult.error.recoverable) {
230
- options?.onProgress?.(`Transient error polling stack ${stackName}, retrying: ${statusResult.error.message}`);
231
- await sleep(pollIntervalMs);
232
- continue;
233
- }
234
- return failure(statusResult.error);
235
- }
236
- const status = statusResult.data?.status;
237
- if (status === "DELETE_COMPLETE" || status === "DOES_NOT_EXIST") {
238
- return success(undefined);
239
- }
240
- if (status === "DELETE_FAILED") {
241
- return failure(new CloudFormationError(`Stack ${stackName} deletion failed: ${statusResult.data?.statusReason || "unknown reason"}`, "stack_failed", stackName, status, undefined, false));
242
- }
243
- options?.onProgress?.(`Stack ${stackName} status: ${status ?? "unknown"}`);
244
- await sleep(pollIntervalMs);
245
- }
246
- return failure(new CloudFormationError(`Timed out waiting for stack ${stackName} deletion after ${Math.round(timeoutMs / 1000)}s`, "timeout", stackName, undefined, undefined, true));
247
- }
248
- }
1
+ import{CloudFormationClient as d,DeleteStackCommand as g,DescribeStacksCommand as p,ListExportsCommand as h}from"@aws-sdk/client-cloudformation";import{stackStatusMap as E}from"../../aws/utils/stackStatus.js";import{success as u,failure as c}from"@fjall/generator";import{BaseServiceError as k}from"../../types/errors/ServiceError.js";import{logger as x}from"@fjall/util/logger";import{getErrorMessage as f,sleep as S}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as w}from"@fjall/util/aws";import{extractErrorName as y}from"../../aws/organisations/types.js";class l extends k{errorType;stackName;stackStatus;constructor(t,r,n,e,s,o=!1){super(`CFN_${r.toUpperCase()}`,t,s,o),this.errorType=r,this.stackName=n,this.stackStatus=e}}class D{aws;constructor(t){this.aws=t}classifyAwsError(t,r,n){const e=y(t),s=f(t);return e==="CredentialsError"||e==="UnauthorizedError"?new l(`AWS credentials error: ${s}`,"auth_error",n,void 0,t,!1):e==="Throttling"||e==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${s}`,"throttled",n,void 0,t,!0):e==="NetworkingError"||e==="ENOTFOUND"?new l(`Network error: ${s}`,"network_error",n,void 0,t,!0):new l(`${r}: ${s}`,"unknown",n,void 0,t)}async getStackOutputs(t,r){r?.onStackCheck?.(t);const n=this.aws.getClient(d),e=new p({StackName:t});try{const o=(await n.send(e)).Stacks?.[0];if(!o?.Outputs)return u([]);const a=o.Outputs.map(i=>({OutputKey:i.OutputKey,OutputValue:i.OutputValue,ExportName:i.ExportName}));return r?.onOutputsRetrieved?.(t,a.length),u(a)}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(w)?(r?.onStackNotFound?.(t),u([])):c(new l(`Failed to get outputs for stack ${t}: ${f(s)}`,"unknown",t,void 0,s))}}async getStackStatus(t,r){r?.onStackCheck?.(t);const n=this.aws.getClient(d),e=new p({StackName:t});try{const o=(await n.send(e)).Stacks?.[0];if(!o)return u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=o.StackStatus||"UNKNOWN",i=E[a]||E.UNKNOWN;return r?.onStackFound?.(t,a),u({status:a,safeToRedeploy:i.safeToRedeploy,description:i.description,statusReason:o.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(w)?u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const r=this.aws.getClient(d),n=[];try{let e;do{const s=new h({NextToken:e}),o=await r.send(s),a=o.Exports||[];for(const i of a)i.Name&&i.Value&&n.push({Name:i.Name,Value:i.Value});if(t?.(a))break;e=o.NextToken}while(e);return u(n)}catch(e){return c(new l(`Failed to list exports: ${f(e)}`,"unknown",void 0,void 0,e,!1))}}async getExportsByNames(t){if(t.length===0)return u(new Map);const r=new Set(t),n=new Map,e=await this.listAllExports(s=>{for(const o of s)o.Name&&r.has(o.Name)&&o.Value&&n.set(o.Name,o.Value);return n.size>=r.size});return e.success?u(n):c(e.error)}async listExports(t){const r=await this.listAllExports();return r.success&&t?.onExportsRetrieved?.(r.data.length),r}async deleteStack(t){const r=this.aws.getClient(d);try{return await r.send(new g({StackName:t})),u(void 0)}catch(n){return c(this.classifyAwsError(n,`Failed to delete stack ${t}`,t))}}async stackExists(t,r){const n=r??this.aws.getClient(d);try{const s=(await n.send(new p({StackName:t}))).Stacks?.[0]?.StackStatus;return!!s&&s!=="REVIEW_IN_PROGRESS"}catch(e){return e instanceof Error&&e.message?.includes(w)?!1:(x.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:f(e)}),!0)}}async waitForDeleteComplete(t,r){const n=r?.timeoutMs??6e5,e=r?.pollIntervalMs??5e3,s=Date.now();for(;Date.now()-s<n;){const o=await this.getStackStatus(t);if(!o.success){if(o.error.recoverable){r?.onProgress?.(`Transient error polling stack ${t}, retrying: ${o.error.message}`),await S(e);continue}return c(o.error)}const a=o.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return u(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));r?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await S(e)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(n/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,D as CloudFormationService};
@@ -1,44 +1 @@
1
- import { DEFAULT_REGION } from "@fjall/generator";
2
- /**
3
- * Builder for creating standardised CDK deployment contexts.
4
- * Ensures all required fields are present with proper defaults.
5
- *
6
- * Unlike the CLI version, this uses OrgConfig (injected) rather than
7
- * Config.loadConfig() (file-based) for region resolution.
8
- */
9
- export class CdkContextBuilder {
10
- /**
11
- * Build a standardised deployment context for CDK operations.
12
- * Ensures all required fields are present with proper defaults.
13
- */
14
- static buildDeploymentContext(input, options, orgConfig) {
15
- return {
16
- deployType: input.deployType,
17
- target: input.target,
18
- path: input.path,
19
- options: options,
20
- stackOutputs: input.stackOutputs || {},
21
- callerIdentity: input.callerIdentity,
22
- region: input.region || orgConfig?.primaryRegion || DEFAULT_REGION,
23
- isManagedAccount: input.isManagedAccount,
24
- accountName: input.accountName,
25
- logPath: input.logPath,
26
- imageVersion: input.imageVersion,
27
- orgId: input.orgId,
28
- rootId: input.rootId,
29
- managementAccountId: input.managementAccountId,
30
- ipamPoolId: input.ipamPoolId,
31
- fjallOrgId: input.fjallOrgId,
32
- orgConfig: input.orgConfig
33
- };
34
- }
35
- /**
36
- * Build context from an existing context with updated options
37
- */
38
- static updateContext(existingContext, updates) {
39
- return {
40
- ...existingContext,
41
- ...updates
42
- };
43
- }
44
- }
1
+ import{DEFAULT_REGION as t}from"@fjall/generator";class d{static buildDeploymentContext(o,e,a){return{deployType:o.deployType,target:o.target,path:o.path,options:e,stackOutputs:o.stackOutputs||{},callerIdentity:o.callerIdentity,region:o.region||a?.primaryRegion||t,isManagedAccount:o.isManagedAccount,accountName:o.accountName,logPath:o.logPath,imageVersion:o.imageVersion,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,orgConfig:o.orgConfig}}static updateContext(o,e){return{...o,...e}}}export{d as CdkContextBuilder};