@friggframework/devtools 2.0.0--canary.474.884529c.0 → 2.0.0--canary.474.988ec0b.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/frigg-cli/deploy-command/index.js +148 -29
- package/frigg-cli/doctor-command/index.js +249 -0
- package/frigg-cli/index.js +24 -1
- package/frigg-cli/repair-command/index.js +341 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +146 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +229 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +180 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- package/package.json +6 -6
package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ReconcilePropertiesUseCase
|
|
3
|
+
*
|
|
4
|
+
* Use case for reconciling property drift between CloudFormation template
|
|
5
|
+
* and actual cloud resources (frigg repair --reconcile command)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ReconcilePropertiesUseCase = require('./reconcile-properties-use-case');
|
|
9
|
+
const StackIdentifier = require('../../domain/value-objects/stack-identifier');
|
|
10
|
+
const PropertyMismatch = require('../../domain/entities/property-mismatch');
|
|
11
|
+
const PropertyMutability = require('../../domain/value-objects/property-mutability');
|
|
12
|
+
|
|
13
|
+
describe('ReconcilePropertiesUseCase', () => {
|
|
14
|
+
let useCase;
|
|
15
|
+
let mockPropertyReconciler;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Mock property reconciler
|
|
19
|
+
mockPropertyReconciler = {
|
|
20
|
+
canReconcile: jest.fn(),
|
|
21
|
+
reconcileProperty: jest.fn(),
|
|
22
|
+
reconcileMultipleProperties: jest.fn(),
|
|
23
|
+
previewReconciliation: jest.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useCase = new ReconcilePropertiesUseCase({
|
|
27
|
+
propertyReconciler: mockPropertyReconciler,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('reconcileSingleProperty', () => {
|
|
32
|
+
it('should reconcile a single mutable property mismatch', async () => {
|
|
33
|
+
const stackIdentifier = new StackIdentifier({
|
|
34
|
+
stackName: 'my-app-prod',
|
|
35
|
+
region: 'us-east-1',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const mismatch = new PropertyMismatch({
|
|
39
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
40
|
+
expectedValue: true,
|
|
41
|
+
actualValue: false,
|
|
42
|
+
mutability: PropertyMutability.MUTABLE,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Mock reconciler can handle this mismatch
|
|
46
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(true);
|
|
47
|
+
|
|
48
|
+
// Mock reconciliation result
|
|
49
|
+
mockPropertyReconciler.reconcileProperty.mockResolvedValue({
|
|
50
|
+
success: true,
|
|
51
|
+
mode: 'template',
|
|
52
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
53
|
+
oldValue: true,
|
|
54
|
+
newValue: false,
|
|
55
|
+
message: 'Template updated to match actual resource state',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await useCase.reconcileSingleProperty({
|
|
59
|
+
stackIdentifier,
|
|
60
|
+
logicalId: 'MyVPC',
|
|
61
|
+
mismatch,
|
|
62
|
+
mode: 'template',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.success).toBe(true);
|
|
66
|
+
expect(result.mode).toBe('template');
|
|
67
|
+
expect(result.propertyPath).toBe('Properties.EnableDnsSupport');
|
|
68
|
+
expect(mockPropertyReconciler.canReconcile).toHaveBeenCalledWith(mismatch);
|
|
69
|
+
expect(mockPropertyReconciler.reconcileProperty).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should fail if property cannot be reconciled', async () => {
|
|
73
|
+
const stackIdentifier = new StackIdentifier({
|
|
74
|
+
stackName: 'my-app-prod',
|
|
75
|
+
region: 'us-east-1',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const mismatch = new PropertyMismatch({
|
|
79
|
+
propertyPath: 'Properties.CidrBlock',
|
|
80
|
+
expectedValue: '10.0.0.0/16',
|
|
81
|
+
actualValue: '10.1.0.0/16',
|
|
82
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Mock reconciler cannot handle immutable property
|
|
86
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(false);
|
|
87
|
+
|
|
88
|
+
await expect(
|
|
89
|
+
useCase.reconcileSingleProperty({
|
|
90
|
+
stackIdentifier,
|
|
91
|
+
logicalId: 'MyVPC',
|
|
92
|
+
mismatch,
|
|
93
|
+
mode: 'template',
|
|
94
|
+
})
|
|
95
|
+
).rejects.toThrow('Property Properties.CidrBlock cannot be reconciled automatically');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should use resource mode when specified', async () => {
|
|
99
|
+
const stackIdentifier = new StackIdentifier({
|
|
100
|
+
stackName: 'my-app-prod',
|
|
101
|
+
region: 'us-east-1',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const mismatch = new PropertyMismatch({
|
|
105
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
106
|
+
expectedValue: true,
|
|
107
|
+
actualValue: false,
|
|
108
|
+
mutability: PropertyMutability.MUTABLE,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(true);
|
|
112
|
+
|
|
113
|
+
mockPropertyReconciler.reconcileProperty.mockResolvedValue({
|
|
114
|
+
success: true,
|
|
115
|
+
mode: 'resource',
|
|
116
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
117
|
+
oldValue: false,
|
|
118
|
+
newValue: true,
|
|
119
|
+
message: 'Resource updated to match template definition',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await useCase.reconcileSingleProperty({
|
|
123
|
+
stackIdentifier,
|
|
124
|
+
logicalId: 'MyVPC',
|
|
125
|
+
mismatch,
|
|
126
|
+
mode: 'resource',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.success).toBe(true);
|
|
130
|
+
expect(result.mode).toBe('resource');
|
|
131
|
+
expect(mockPropertyReconciler.reconcileProperty).toHaveBeenCalledWith({
|
|
132
|
+
stackIdentifier,
|
|
133
|
+
logicalId: 'MyVPC',
|
|
134
|
+
mismatch,
|
|
135
|
+
mode: 'resource',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('reconcileMultipleProperties', () => {
|
|
141
|
+
it('should reconcile multiple property mismatches for a resource', async () => {
|
|
142
|
+
const stackIdentifier = new StackIdentifier({
|
|
143
|
+
stackName: 'my-app-prod',
|
|
144
|
+
region: 'us-east-1',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const mismatches = [
|
|
148
|
+
new PropertyMismatch({
|
|
149
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
150
|
+
expectedValue: true,
|
|
151
|
+
actualValue: false,
|
|
152
|
+
mutability: PropertyMutability.MUTABLE,
|
|
153
|
+
}),
|
|
154
|
+
new PropertyMismatch({
|
|
155
|
+
propertyPath: 'Properties.EnableDnsHostnames',
|
|
156
|
+
expectedValue: true,
|
|
157
|
+
actualValue: false,
|
|
158
|
+
mutability: PropertyMutability.MUTABLE,
|
|
159
|
+
}),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// Mock all mismatches can be reconciled
|
|
163
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(true);
|
|
164
|
+
|
|
165
|
+
// Mock batch reconciliation
|
|
166
|
+
mockPropertyReconciler.reconcileMultipleProperties.mockResolvedValue({
|
|
167
|
+
reconciledCount: 2,
|
|
168
|
+
failedCount: 0,
|
|
169
|
+
results: [
|
|
170
|
+
{
|
|
171
|
+
success: true,
|
|
172
|
+
mode: 'template',
|
|
173
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
174
|
+
oldValue: true,
|
|
175
|
+
newValue: false,
|
|
176
|
+
message: 'Template updated',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
success: true,
|
|
180
|
+
mode: 'template',
|
|
181
|
+
propertyPath: 'Properties.EnableDnsHostnames',
|
|
182
|
+
oldValue: true,
|
|
183
|
+
newValue: false,
|
|
184
|
+
message: 'Template updated',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
message: 'Reconciled 2 of 2 properties',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = await useCase.reconcileMultipleProperties({
|
|
191
|
+
stackIdentifier,
|
|
192
|
+
logicalId: 'MyVPC',
|
|
193
|
+
mismatches,
|
|
194
|
+
mode: 'template',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.reconciledCount).toBe(2);
|
|
198
|
+
expect(result.failedCount).toBe(0);
|
|
199
|
+
expect(result.results).toHaveLength(2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should skip immutable properties and reconcile mutable ones', async () => {
|
|
203
|
+
const stackIdentifier = new StackIdentifier({
|
|
204
|
+
stackName: 'my-app-prod',
|
|
205
|
+
region: 'us-east-1',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const mismatches = [
|
|
209
|
+
new PropertyMismatch({
|
|
210
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
211
|
+
expectedValue: true,
|
|
212
|
+
actualValue: false,
|
|
213
|
+
mutability: PropertyMutability.MUTABLE,
|
|
214
|
+
}),
|
|
215
|
+
new PropertyMismatch({
|
|
216
|
+
propertyPath: 'Properties.CidrBlock',
|
|
217
|
+
expectedValue: '10.0.0.0/16',
|
|
218
|
+
actualValue: '10.1.0.0/16',
|
|
219
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
220
|
+
}),
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// Mock first can be reconciled, second cannot
|
|
224
|
+
mockPropertyReconciler.canReconcile
|
|
225
|
+
.mockResolvedValueOnce(true)
|
|
226
|
+
.mockResolvedValueOnce(false);
|
|
227
|
+
|
|
228
|
+
// Mock reconciliation of the mutable property
|
|
229
|
+
mockPropertyReconciler.reconcileMultipleProperties.mockResolvedValue({
|
|
230
|
+
reconciledCount: 1,
|
|
231
|
+
failedCount: 0,
|
|
232
|
+
results: [
|
|
233
|
+
{
|
|
234
|
+
success: true,
|
|
235
|
+
mode: 'template',
|
|
236
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
237
|
+
oldValue: true,
|
|
238
|
+
newValue: false,
|
|
239
|
+
message: 'Template updated',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
message: 'Reconciled 1 of 1 properties',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const result = await useCase.reconcileMultipleProperties({
|
|
246
|
+
stackIdentifier,
|
|
247
|
+
logicalId: 'MyVPC',
|
|
248
|
+
mismatches,
|
|
249
|
+
mode: 'template',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result.reconciledCount).toBe(1); // One mutable property reconciled
|
|
253
|
+
expect(result.skippedCount).toBe(1); // One immutable property skipped
|
|
254
|
+
expect(result.reconcilableCount).toBe(1);
|
|
255
|
+
expect(result.results).toHaveLength(1);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('previewReconciliation', () => {
|
|
260
|
+
it('should preview reconciliation changes', async () => {
|
|
261
|
+
const stackIdentifier = new StackIdentifier({
|
|
262
|
+
stackName: 'my-app-prod',
|
|
263
|
+
region: 'us-east-1',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const mismatch = new PropertyMismatch({
|
|
267
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
268
|
+
expectedValue: true,
|
|
269
|
+
actualValue: false,
|
|
270
|
+
mutability: PropertyMutability.MUTABLE,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Mock preview
|
|
274
|
+
mockPropertyReconciler.previewReconciliation.mockResolvedValue({
|
|
275
|
+
canReconcile: true,
|
|
276
|
+
mode: 'template',
|
|
277
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
278
|
+
currentValue: true,
|
|
279
|
+
proposedValue: false,
|
|
280
|
+
impact: 'Will update CloudFormation template to match actual resource state',
|
|
281
|
+
warnings: [],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const preview = await useCase.previewReconciliation({
|
|
285
|
+
stackIdentifier,
|
|
286
|
+
logicalId: 'MyVPC',
|
|
287
|
+
mismatch,
|
|
288
|
+
mode: 'template',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(preview.canReconcile).toBe(true);
|
|
292
|
+
expect(preview.mode).toBe('template');
|
|
293
|
+
expect(preview.impact).toContain('CloudFormation template');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should include warnings for risky reconciliations', async () => {
|
|
297
|
+
const stackIdentifier = new StackIdentifier({
|
|
298
|
+
stackName: 'my-app-prod',
|
|
299
|
+
region: 'us-east-1',
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const mismatch = new PropertyMismatch({
|
|
303
|
+
propertyPath: 'Properties.EngineVersion',
|
|
304
|
+
expectedValue: '13.7',
|
|
305
|
+
actualValue: '13.8',
|
|
306
|
+
mutability: PropertyMutability.CONDITIONAL,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Mock preview with warnings
|
|
310
|
+
mockPropertyReconciler.previewReconciliation.mockResolvedValue({
|
|
311
|
+
canReconcile: true,
|
|
312
|
+
mode: 'resource',
|
|
313
|
+
propertyPath: 'Properties.EngineVersion',
|
|
314
|
+
currentValue: '13.7',
|
|
315
|
+
proposedValue: '13.8',
|
|
316
|
+
impact: 'Will update database engine version - may cause downtime',
|
|
317
|
+
warnings: [
|
|
318
|
+
'Engine version downgrade may not be supported',
|
|
319
|
+
'Downtime may occur during version change',
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const preview = await useCase.previewReconciliation({
|
|
324
|
+
stackIdentifier,
|
|
325
|
+
logicalId: 'MyDBCluster',
|
|
326
|
+
mismatch,
|
|
327
|
+
mode: 'resource',
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(preview.canReconcile).toBe(true);
|
|
331
|
+
expect(preview.warnings).toHaveLength(2);
|
|
332
|
+
expect(preview.warnings[0]).toContain('Engine version');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('constructor', () => {
|
|
337
|
+
it('should require propertyReconciler', () => {
|
|
338
|
+
expect(() => {
|
|
339
|
+
new ReconcilePropertiesUseCase({});
|
|
340
|
+
}).toThrow('propertyReconciler is required');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepairViaImportUseCase - Import Orphaned Resources into CloudFormation Stack
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Use Case
|
|
5
|
+
*
|
|
6
|
+
* Business logic for the "frigg repair --import" command. Orchestrates resource
|
|
7
|
+
* import operations to fix orphaned resources by bringing them under CloudFormation management.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Validate resources can be imported
|
|
11
|
+
* - Retrieve resource details from cloud
|
|
12
|
+
* - Generate CloudFormation template snippets
|
|
13
|
+
* - Execute import operations (single or batch)
|
|
14
|
+
* - Track import operation status
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class RepairViaImportUseCase {
|
|
18
|
+
/**
|
|
19
|
+
* Create use case with required dependencies
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} params
|
|
22
|
+
* @param {IResourceImporter} params.resourceImporter - Resource import operations
|
|
23
|
+
* @param {IResourceDetector} params.resourceDetector - Resource discovery and details
|
|
24
|
+
*/
|
|
25
|
+
constructor({ resourceImporter, resourceDetector }) {
|
|
26
|
+
if (!resourceImporter) {
|
|
27
|
+
throw new Error('resourceImporter is required');
|
|
28
|
+
}
|
|
29
|
+
if (!resourceDetector) {
|
|
30
|
+
throw new Error('resourceDetector is required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.resourceImporter = resourceImporter;
|
|
34
|
+
this.resourceDetector = resourceDetector;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Import a single orphaned resource into a CloudFormation stack
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} params
|
|
41
|
+
* @param {StackIdentifier} params.stackIdentifier - Target stack
|
|
42
|
+
* @param {string} params.logicalId - Desired logical ID for resource in template
|
|
43
|
+
* @param {string} params.physicalId - Physical ID of resource in cloud
|
|
44
|
+
* @param {string} params.resourceType - CloudFormation resource type
|
|
45
|
+
* @returns {Promise<Object>} Import result
|
|
46
|
+
*/
|
|
47
|
+
async importSingleResource({ stackIdentifier, logicalId, physicalId, resourceType }) {
|
|
48
|
+
// 1. Validate resource can be imported
|
|
49
|
+
const validation = await this.resourceImporter.validateImport({
|
|
50
|
+
resourceType,
|
|
51
|
+
physicalId,
|
|
52
|
+
region: stackIdentifier.region,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!validation.canImport) {
|
|
56
|
+
throw new Error(validation.reason);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Get detailed resource properties from cloud
|
|
60
|
+
const resourceDetails = await this.resourceDetector.getResourceDetails({
|
|
61
|
+
resourceType,
|
|
62
|
+
physicalId,
|
|
63
|
+
region: stackIdentifier.region,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 3. Execute import operation
|
|
67
|
+
const importResult = await this.resourceImporter.importResource({
|
|
68
|
+
stackIdentifier,
|
|
69
|
+
logicalId,
|
|
70
|
+
resourceType,
|
|
71
|
+
physicalId,
|
|
72
|
+
properties: resourceDetails.properties,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 4. Return result with warnings if any
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
operationId: importResult.operationId,
|
|
79
|
+
status: importResult.status,
|
|
80
|
+
message: importResult.message,
|
|
81
|
+
warnings: validation.warnings || [],
|
|
82
|
+
resource: {
|
|
83
|
+
logicalId,
|
|
84
|
+
physicalId,
|
|
85
|
+
resourceType,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Import multiple orphaned resources into a stack in batch
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} params
|
|
94
|
+
* @param {StackIdentifier} params.stackIdentifier - Target stack
|
|
95
|
+
* @param {Array<Object>} params.resources - Resources to import
|
|
96
|
+
* @param {string} params.resources[].logicalId - Logical ID
|
|
97
|
+
* @param {string} params.resources[].physicalId - Physical ID
|
|
98
|
+
* @param {string} params.resources[].resourceType - Resource type
|
|
99
|
+
* @returns {Promise<Object>} Batch import result
|
|
100
|
+
*/
|
|
101
|
+
async importMultipleResources({ stackIdentifier, resources }) {
|
|
102
|
+
const validationErrors = [];
|
|
103
|
+
const validResources = [];
|
|
104
|
+
|
|
105
|
+
// 1. Validate all resources first
|
|
106
|
+
for (const resource of resources) {
|
|
107
|
+
try {
|
|
108
|
+
const validation = await this.resourceImporter.validateImport({
|
|
109
|
+
resourceType: resource.resourceType,
|
|
110
|
+
physicalId: resource.physicalId,
|
|
111
|
+
region: stackIdentifier.region,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!validation.canImport) {
|
|
115
|
+
validationErrors.push({
|
|
116
|
+
logicalId: resource.logicalId,
|
|
117
|
+
physicalId: resource.physicalId,
|
|
118
|
+
reason: validation.reason,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get resource details
|
|
124
|
+
const resourceDetails = await this.resourceDetector.getResourceDetails({
|
|
125
|
+
resourceType: resource.resourceType,
|
|
126
|
+
physicalId: resource.physicalId,
|
|
127
|
+
region: stackIdentifier.region,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
validResources.push({
|
|
131
|
+
logicalId: resource.logicalId,
|
|
132
|
+
resourceType: resource.resourceType,
|
|
133
|
+
physicalId: resource.physicalId,
|
|
134
|
+
properties: resourceDetails.properties,
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
validationErrors.push({
|
|
138
|
+
logicalId: resource.logicalId,
|
|
139
|
+
physicalId: resource.physicalId,
|
|
140
|
+
reason: error.message,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. If ANY validation failed, fail the entire batch (all-or-nothing approach)
|
|
146
|
+
if (validationErrors.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
importedCount: 0,
|
|
150
|
+
failedCount: validationErrors.length,
|
|
151
|
+
validationErrors,
|
|
152
|
+
message: `${validationErrors.length} resource(s) failed validation - batch import aborted`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. All validations passed - import resources in batch
|
|
157
|
+
if (validResources.length > 0) {
|
|
158
|
+
const importResult = await this.resourceImporter.importMultipleResources({
|
|
159
|
+
stackIdentifier,
|
|
160
|
+
resources: validResources,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
importedCount: importResult.importedCount,
|
|
166
|
+
failedCount: importResult.failedCount,
|
|
167
|
+
operationId: importResult.operationId,
|
|
168
|
+
status: importResult.status,
|
|
169
|
+
message: importResult.message,
|
|
170
|
+
details: importResult.details,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4. No resources provided
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
importedCount: 0,
|
|
178
|
+
failedCount: 0,
|
|
179
|
+
message: 'No resources provided for import',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get status of an ongoing import operation
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} params
|
|
187
|
+
* @param {string} params.operationId - CloudFormation change set ID
|
|
188
|
+
* @returns {Promise<Object>} Operation status
|
|
189
|
+
*/
|
|
190
|
+
async getImportStatus({ operationId }) {
|
|
191
|
+
return await this.resourceImporter.getImportStatus(operationId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Preview what template changes will be made for an import
|
|
196
|
+
*
|
|
197
|
+
* @param {Object} params
|
|
198
|
+
* @param {StackIdentifier} params.stackIdentifier - Target stack
|
|
199
|
+
* @param {string} params.logicalId - Desired logical ID
|
|
200
|
+
* @param {string} params.physicalId - Physical resource ID
|
|
201
|
+
* @param {string} params.resourceType - Resource type
|
|
202
|
+
* @returns {Promise<Object>} Preview with template snippet
|
|
203
|
+
*/
|
|
204
|
+
async previewImport({ stackIdentifier, logicalId, physicalId, resourceType }) {
|
|
205
|
+
// Get resource details from cloud
|
|
206
|
+
const resourceDetails = await this.resourceDetector.getResourceDetails({
|
|
207
|
+
resourceType,
|
|
208
|
+
physicalId,
|
|
209
|
+
region: stackIdentifier.region,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Generate template snippet
|
|
213
|
+
const templateSnippet = await this.resourceImporter.generateTemplateSnippet({
|
|
214
|
+
logicalId,
|
|
215
|
+
resourceType,
|
|
216
|
+
properties: resourceDetails.properties,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
logicalId,
|
|
221
|
+
physicalId,
|
|
222
|
+
resourceType,
|
|
223
|
+
templateSnippet,
|
|
224
|
+
properties: resourceDetails.properties,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = RepairViaImportUseCase;
|