@gugananuvem/aws-local-simulator 1.0.20 → 1.0.21
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/package.json +2 -2
- package/src/services/cloudformation/simulador.js +178 -203
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gugananuvem/aws-local-simulator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "Simulador local completo para serviços AWS",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -120,6 +120,6 @@
|
|
|
120
120
|
"optional": true
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
|
-
"buildDate": "2026-04-
|
|
123
|
+
"buildDate": "2026-04-22T17:40:52.588Z",
|
|
124
124
|
"published": true
|
|
125
125
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @fileoverview CloudFormation Simulator
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* - Persistência via LocalStore
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
const { randomUUID } = require(
|
|
18
|
-
const yaml = require(
|
|
19
|
-
const { CloudTrailAudit } = require(
|
|
17
|
+
const { randomUUID } = require("crypto");
|
|
18
|
+
const yaml = require("js-yaml");
|
|
19
|
+
const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
20
20
|
|
|
21
21
|
// ─── Erros tipados ───────────────────────────────────────────────────────────
|
|
22
22
|
|
|
@@ -29,43 +29,38 @@ class CloudFormationError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const Errors = {
|
|
32
|
-
AlreadyExists: (name) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
new CloudFormationError('ChangeSetNotFoundException', `ChangeSet [${name}] does not exist`, 404),
|
|
38
|
-
InvalidTemplate: (msg) =>
|
|
39
|
-
new CloudFormationError('ValidationError', `Template format error: ${msg}`, 400),
|
|
40
|
-
InvalidAction: (msg) =>
|
|
41
|
-
new CloudFormationError('ValidationError', msg, 400),
|
|
32
|
+
AlreadyExists: (name) => new CloudFormationError("AlreadyExistsException", `Stack [${name}] already exists`, 400),
|
|
33
|
+
DoesNotExist: (name) => new CloudFormationError("ValidationError", `Stack with id ${name} does not exist`, 400),
|
|
34
|
+
ChangeSetNotFound: (name) => new CloudFormationError("ChangeSetNotFoundException", `ChangeSet [${name}] does not exist`, 404),
|
|
35
|
+
InvalidTemplate: (msg) => new CloudFormationError("ValidationError", `Template format error: ${msg}`, 400),
|
|
36
|
+
InvalidAction: (msg) => new CloudFormationError("ValidationError", msg, 400),
|
|
42
37
|
};
|
|
43
38
|
|
|
44
39
|
// ─── Constantes ──────────────────────────────────────────────────────────────
|
|
45
40
|
|
|
46
|
-
const REGION =
|
|
47
|
-
const ACCOUNT =
|
|
41
|
+
const REGION = "us-east-1";
|
|
42
|
+
const ACCOUNT = "000000000000";
|
|
48
43
|
|
|
49
44
|
const StackStatus = {
|
|
50
|
-
CREATE_IN_PROGRESS:
|
|
51
|
-
CREATE_COMPLETE:
|
|
52
|
-
CREATE_FAILED:
|
|
53
|
-
UPDATE_IN_PROGRESS:
|
|
54
|
-
UPDATE_COMPLETE:
|
|
55
|
-
UPDATE_FAILED:
|
|
56
|
-
DELETE_IN_PROGRESS:
|
|
57
|
-
DELETE_COMPLETE:
|
|
58
|
-
DELETE_FAILED:
|
|
59
|
-
ROLLBACK_IN_PROGRESS:
|
|
60
|
-
ROLLBACK_COMPLETE:
|
|
45
|
+
CREATE_IN_PROGRESS: "CREATE_IN_PROGRESS",
|
|
46
|
+
CREATE_COMPLETE: "CREATE_COMPLETE",
|
|
47
|
+
CREATE_FAILED: "CREATE_FAILED",
|
|
48
|
+
UPDATE_IN_PROGRESS: "UPDATE_IN_PROGRESS",
|
|
49
|
+
UPDATE_COMPLETE: "UPDATE_COMPLETE",
|
|
50
|
+
UPDATE_FAILED: "UPDATE_FAILED",
|
|
51
|
+
DELETE_IN_PROGRESS: "DELETE_IN_PROGRESS",
|
|
52
|
+
DELETE_COMPLETE: "DELETE_COMPLETE",
|
|
53
|
+
DELETE_FAILED: "DELETE_FAILED",
|
|
54
|
+
ROLLBACK_IN_PROGRESS: "ROLLBACK_IN_PROGRESS",
|
|
55
|
+
ROLLBACK_COMPLETE: "ROLLBACK_COMPLETE",
|
|
61
56
|
};
|
|
62
57
|
|
|
63
58
|
const ChangeSetStatus = {
|
|
64
|
-
CREATE_PENDING:
|
|
65
|
-
CREATE_IN_PROGRESS:
|
|
66
|
-
CREATE_COMPLETE:
|
|
67
|
-
DELETE_COMPLETE:
|
|
68
|
-
FAILED:
|
|
59
|
+
CREATE_PENDING: "CREATE_PENDING",
|
|
60
|
+
CREATE_IN_PROGRESS: "CREATE_IN_PROGRESS",
|
|
61
|
+
CREATE_COMPLETE: "CREATE_COMPLETE",
|
|
62
|
+
DELETE_COMPLETE: "DELETE_COMPLETE",
|
|
63
|
+
FAILED: "FAILED",
|
|
69
64
|
};
|
|
70
65
|
|
|
71
66
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -88,30 +83,30 @@ function changeSetArn(stackName, changeSetName) {
|
|
|
88
83
|
* Faz parse básico do template (JSON ou YAML string → objeto)
|
|
89
84
|
*/
|
|
90
85
|
function parseTemplate(template) {
|
|
91
|
-
if (!template) throw Errors.InvalidTemplate(
|
|
92
|
-
if (typeof template ===
|
|
86
|
+
if (!template) throw Errors.InvalidTemplate("Template body is required");
|
|
87
|
+
if (typeof template === "object") return template;
|
|
93
88
|
try {
|
|
94
89
|
return JSON.parse(template);
|
|
95
90
|
} catch (_) {
|
|
96
91
|
try {
|
|
97
92
|
// Schema customizado que trata tags AWS como !Ref, !Sub, !If, etc.
|
|
98
93
|
const awsSchema = yaml.DEFAULT_SCHEMA.extend([
|
|
99
|
-
new yaml.Type(
|
|
100
|
-
new yaml.Type(
|
|
101
|
-
new yaml.Type(
|
|
102
|
-
new yaml.Type(
|
|
103
|
-
new yaml.Type(
|
|
104
|
-
new yaml.Type(
|
|
105
|
-
new yaml.Type(
|
|
106
|
-
new yaml.Type(
|
|
107
|
-
new yaml.Type(
|
|
108
|
-
new yaml.Type(
|
|
109
|
-
new yaml.Type(
|
|
110
|
-
new yaml.Type(
|
|
111
|
-
new yaml.Type(
|
|
112
|
-
new yaml.Type(
|
|
113
|
-
new yaml.Type(
|
|
114
|
-
new yaml.Type(
|
|
94
|
+
new yaml.Type("!Ref", { kind: "scalar", construct: (d) => ({ Ref: d }) }),
|
|
95
|
+
new yaml.Type("!Sub", { kind: "scalar", construct: (d) => ({ "Fn::Sub": d }) }),
|
|
96
|
+
new yaml.Type("!Sub", { kind: "sequence", construct: (d) => ({ "Fn::Sub": d }) }),
|
|
97
|
+
new yaml.Type("!If", { kind: "sequence", construct: (d) => ({ "Fn::If": d }) }),
|
|
98
|
+
new yaml.Type("!Equals", { kind: "sequence", construct: (d) => ({ "Fn::Equals": d }) }),
|
|
99
|
+
new yaml.Type("!Not", { kind: "sequence", construct: (d) => ({ "Fn::Not": d }) }),
|
|
100
|
+
new yaml.Type("!And", { kind: "sequence", construct: (d) => ({ "Fn::And": d }) }),
|
|
101
|
+
new yaml.Type("!Or", { kind: "sequence", construct: (d) => ({ "Fn::Or": d }) }),
|
|
102
|
+
new yaml.Type("!Select", { kind: "sequence", construct: (d) => ({ "Fn::Select": d }) }),
|
|
103
|
+
new yaml.Type("!Split", { kind: "sequence", construct: (d) => ({ "Fn::Split": d }) }),
|
|
104
|
+
new yaml.Type("!Join", { kind: "sequence", construct: (d) => ({ "Fn::Join": d }) }),
|
|
105
|
+
new yaml.Type("!GetAtt", { kind: "scalar", construct: (d) => ({ "Fn::GetAtt": d.split(".") }) }),
|
|
106
|
+
new yaml.Type("!FindInMap", { kind: "sequence", construct: (d) => ({ "Fn::FindInMap": d }) }),
|
|
107
|
+
new yaml.Type("!Base64", { kind: "scalar", construct: (d) => ({ "Fn::Base64": d }) }),
|
|
108
|
+
new yaml.Type("!Cidr", { kind: "sequence", construct: (d) => ({ "Fn::Cidr": d }) }),
|
|
109
|
+
new yaml.Type("!ImportValue", { kind: "scalar", construct: (d) => ({ "Fn::ImportValue": d }) }),
|
|
115
110
|
]);
|
|
116
111
|
return yaml.load(template, { schema: awsSchema });
|
|
117
112
|
} catch (yamlErr) {
|
|
@@ -130,10 +125,10 @@ function extractResources(parsedTemplate, stackName) {
|
|
|
130
125
|
resources.push({
|
|
131
126
|
LogicalResourceId: logicalId,
|
|
132
127
|
PhysicalResourceId: `${stackName}-${logicalId}-${randomUUID().slice(0, 8)}`,
|
|
133
|
-
ResourceType: resource.Type ||
|
|
134
|
-
ResourceStatus:
|
|
128
|
+
ResourceType: resource.Type || "AWS::CloudFormation::WaitConditionHandle",
|
|
129
|
+
ResourceStatus: "CREATE_COMPLETE",
|
|
135
130
|
Timestamp: now(),
|
|
136
|
-
DriftInformation: { StackResourceDriftStatus:
|
|
131
|
+
DriftInformation: { StackResourceDriftStatus: "NOT_CHECKED" },
|
|
137
132
|
});
|
|
138
133
|
}
|
|
139
134
|
return resources;
|
|
@@ -147,18 +142,16 @@ function resolveParameters(templateParams, inputParams) {
|
|
|
147
142
|
const templateDefs = templateParams || {};
|
|
148
143
|
const inputMap = {};
|
|
149
144
|
|
|
150
|
-
for (const param of
|
|
145
|
+
for (const param of inputParams || []) {
|
|
151
146
|
inputMap[param.ParameterKey] = param;
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
for (const [key, def] of Object.entries(templateDefs)) {
|
|
155
150
|
const input = inputMap[key];
|
|
156
151
|
if (input) {
|
|
157
|
-
resolved[key] = input.UsePreviousValue
|
|
158
|
-
? def.Default || ''
|
|
159
|
-
: (input.ParameterValue || def.Default || '');
|
|
152
|
+
resolved[key] = input.UsePreviousValue ? def.Default || "" : input.ParameterValue || def.Default || "";
|
|
160
153
|
} else {
|
|
161
|
-
resolved[key] = def.Default ||
|
|
154
|
+
resolved[key] = def.Default || "";
|
|
162
155
|
}
|
|
163
156
|
}
|
|
164
157
|
|
|
@@ -179,7 +172,7 @@ function resolveOutputs(parsedTemplate, stackName) {
|
|
|
179
172
|
outputs.push({
|
|
180
173
|
OutputKey: key,
|
|
181
174
|
OutputValue: def.Value || `${stackName}-${key}-output`,
|
|
182
|
-
Description: def.Description ||
|
|
175
|
+
Description: def.Description || "",
|
|
183
176
|
ExportName: def.Export?.Name || undefined,
|
|
184
177
|
});
|
|
185
178
|
}
|
|
@@ -208,7 +201,7 @@ class CloudFormationSimulator {
|
|
|
208
201
|
/** @type {Map<string, Object>} changeSetArn → changeSet */
|
|
209
202
|
this.changeSets = new Map();
|
|
210
203
|
|
|
211
|
-
this.audit = new CloudTrailAudit(
|
|
204
|
+
this.audit = new CloudTrailAudit("cloudformation.amazonaws.com");
|
|
212
205
|
|
|
213
206
|
// Simuladores injetados via injectDependencies
|
|
214
207
|
this.s3Simulator = null;
|
|
@@ -224,21 +217,21 @@ class CloudFormationSimulator {
|
|
|
224
217
|
|
|
225
218
|
async load() {
|
|
226
219
|
try {
|
|
227
|
-
const data = await this.store.read(
|
|
220
|
+
const data = await this.store.read("cloudformation", "data");
|
|
228
221
|
if (data) {
|
|
229
222
|
if (data.stacks) this.stacks = new Map(Object.entries(data.stacks));
|
|
230
223
|
if (data.stackResources) this.stackResources = new Map(Object.entries(data.stackResources));
|
|
231
224
|
if (data.changeSets) this.changeSets = new Map(Object.entries(data.changeSets));
|
|
232
|
-
this.logger.info(
|
|
225
|
+
this.logger.info("[CloudFormation] Loaded persisted data");
|
|
233
226
|
}
|
|
234
227
|
} catch (_) {
|
|
235
|
-
this.logger.debug(
|
|
228
|
+
this.logger.debug("[CloudFormation] No persisted data found, starting fresh");
|
|
236
229
|
}
|
|
237
230
|
}
|
|
238
231
|
|
|
239
232
|
async save() {
|
|
240
233
|
try {
|
|
241
|
-
await this.store.write(
|
|
234
|
+
await this.store.write("cloudformation", "data", {
|
|
242
235
|
stacks: Object.fromEntries(this.stacks),
|
|
243
236
|
stackResources: Object.fromEntries(this.stackResources),
|
|
244
237
|
changeSets: Object.fromEntries(this.changeSets),
|
|
@@ -252,8 +245,10 @@ class CloudFormationSimulator {
|
|
|
252
245
|
this.stacks.clear();
|
|
253
246
|
this.stackResources.clear();
|
|
254
247
|
this.changeSets.clear();
|
|
255
|
-
try {
|
|
256
|
-
|
|
248
|
+
try {
|
|
249
|
+
await this.store.clear("cloudformation");
|
|
250
|
+
} catch (_) {}
|
|
251
|
+
this.logger.info("[CloudFormation] Reset complete");
|
|
257
252
|
}
|
|
258
253
|
|
|
259
254
|
// ─── Stacks ────────────────────────────────────────────────────
|
|
@@ -268,16 +263,16 @@ class CloudFormationSimulator {
|
|
|
268
263
|
Parameters = [],
|
|
269
264
|
Capabilities = [],
|
|
270
265
|
Tags = [],
|
|
271
|
-
OnFailure =
|
|
266
|
+
OnFailure = "ROLLBACK",
|
|
272
267
|
TimeoutInMinutes,
|
|
273
268
|
NotificationARNs = [],
|
|
274
269
|
RoleARN,
|
|
275
270
|
DisableRollback = false,
|
|
276
271
|
}) {
|
|
277
|
-
if (!StackName) throw Errors.InvalidAction(
|
|
272
|
+
if (!StackName) throw Errors.InvalidAction("StackName is required");
|
|
278
273
|
if (this.stacks.has(StackName)) throw Errors.AlreadyExists(StackName);
|
|
279
274
|
|
|
280
|
-
const template = parseTemplate(TemplateBody ||
|
|
275
|
+
const template = parseTemplate(TemplateBody || "{}");
|
|
281
276
|
const stackId = randomUUID();
|
|
282
277
|
const arn = stackArn(StackName, stackId);
|
|
283
278
|
const resolvedParams = resolveParameters(template.Parameters, Parameters);
|
|
@@ -288,7 +283,7 @@ class CloudFormationSimulator {
|
|
|
288
283
|
StackId: arn,
|
|
289
284
|
StackName,
|
|
290
285
|
StackStatus: StackStatus.CREATE_COMPLETE,
|
|
291
|
-
StackStatusReason:
|
|
286
|
+
StackStatusReason: "Stack created successfully",
|
|
292
287
|
CreationTime: now(),
|
|
293
288
|
LastUpdatedTime: now(),
|
|
294
289
|
Parameters: resolvedParams,
|
|
@@ -296,13 +291,13 @@ class CloudFormationSimulator {
|
|
|
296
291
|
Capabilities,
|
|
297
292
|
Tags,
|
|
298
293
|
NotificationARNs,
|
|
299
|
-
RoleARN: RoleARN ||
|
|
294
|
+
RoleARN: RoleARN || "",
|
|
300
295
|
TimeoutInMinutes: TimeoutInMinutes || 0,
|
|
301
296
|
DisableRollback,
|
|
302
297
|
OnFailure,
|
|
303
298
|
TemplateBody: TemplateBody || JSON.stringify(template),
|
|
304
299
|
EnableTerminationProtection: false,
|
|
305
|
-
DriftInformation: { StackDriftStatus:
|
|
300
|
+
DriftInformation: { StackDriftStatus: "NOT_CHECKED" },
|
|
306
301
|
};
|
|
307
302
|
|
|
308
303
|
this.stacks.set(StackName, stack);
|
|
@@ -315,10 +310,10 @@ class CloudFormationSimulator {
|
|
|
315
310
|
await this._provisionResources(StackName, template, resolvedParams);
|
|
316
311
|
|
|
317
312
|
this.audit.record({
|
|
318
|
-
eventName:
|
|
313
|
+
eventName: "CreateStack",
|
|
319
314
|
readOnly: false,
|
|
320
|
-
resources: [{ ARN: arn, type:
|
|
321
|
-
requestParameters: { stackName: StackName }
|
|
315
|
+
resources: [{ ARN: arn, type: "AWS::CloudFormation::Stack" }],
|
|
316
|
+
requestParameters: { stackName: StackName },
|
|
322
317
|
});
|
|
323
318
|
|
|
324
319
|
return { StackId: arn };
|
|
@@ -333,13 +328,13 @@ class CloudFormationSimulator {
|
|
|
333
328
|
// Helper para resolver !Ref de parâmetros
|
|
334
329
|
const resolveRef = (value) => {
|
|
335
330
|
if (!value) return value;
|
|
336
|
-
if (typeof value ===
|
|
337
|
-
const param = resolvedParams.find(p => p.ParameterKey === value.Ref);
|
|
331
|
+
if (typeof value === "object" && value.Ref) {
|
|
332
|
+
const param = resolvedParams.find((p) => p.ParameterKey === value.Ref);
|
|
338
333
|
return param ? param.ParameterValue : value.Ref;
|
|
339
334
|
}
|
|
340
|
-
if (typeof value ===
|
|
341
|
-
return value[
|
|
342
|
-
const param = resolvedParams.find(p => p.ParameterKey === key);
|
|
335
|
+
if (typeof value === "object" && value["Fn::Sub"]) {
|
|
336
|
+
return value["Fn::Sub"].replace(/\$\{([^}]+)\}/g, (_, key) => {
|
|
337
|
+
const param = resolvedParams.find((p) => p.ParameterKey === key);
|
|
343
338
|
return param ? param.ParameterValue : key;
|
|
344
339
|
});
|
|
345
340
|
}
|
|
@@ -354,8 +349,7 @@ class CloudFormationSimulator {
|
|
|
354
349
|
let physicalId = null;
|
|
355
350
|
|
|
356
351
|
switch (resource.Type) {
|
|
357
|
-
|
|
358
|
-
case 'AWS::S3::Bucket': {
|
|
352
|
+
case "AWS::S3::Bucket": {
|
|
359
353
|
if (!this.s3Simulator) break;
|
|
360
354
|
const bucketName = resolveRef(props.BucketName) || `${stackName}-${logicalId}`.toLowerCase();
|
|
361
355
|
this.s3Simulator.createBucket(bucketName);
|
|
@@ -364,7 +358,7 @@ class CloudFormationSimulator {
|
|
|
364
358
|
break;
|
|
365
359
|
}
|
|
366
360
|
|
|
367
|
-
case
|
|
361
|
+
case "AWS::SQS::Queue": {
|
|
368
362
|
if (!this.sqsSimulator) break;
|
|
369
363
|
const queueName = resolveRef(props.QueueName) || `${stackName}-${logicalId}`;
|
|
370
364
|
this.sqsSimulator.createQueue(queueName);
|
|
@@ -373,35 +367,56 @@ class CloudFormationSimulator {
|
|
|
373
367
|
break;
|
|
374
368
|
}
|
|
375
369
|
|
|
376
|
-
case
|
|
370
|
+
case "AWS::DynamoDB::Table": {
|
|
377
371
|
if (!this.dynamoSimulator) break;
|
|
378
372
|
const tableName = resolveRef(props.TableName) || `${stackName}-${logicalId}`;
|
|
379
|
-
const attrDefs = (props.AttributeDefinitions || []).map(a => ({
|
|
373
|
+
const attrDefs = (props.AttributeDefinitions || []).map((a) => ({
|
|
380
374
|
AttributeName: resolveRef(a.AttributeName),
|
|
381
375
|
AttributeType: a.AttributeType,
|
|
382
376
|
}));
|
|
383
|
-
const keySchema = (props.KeySchema || []).map(k => ({
|
|
377
|
+
const keySchema = (props.KeySchema || []).map((k) => ({
|
|
384
378
|
AttributeName: resolveRef(k.AttributeName),
|
|
385
379
|
KeyType: k.KeyType,
|
|
386
380
|
}));
|
|
381
|
+
|
|
382
|
+
// ⭐ NOVO: Processar GlobalSecondaryIndexes
|
|
383
|
+
const gsis = (props.GlobalSecondaryIndexes || []).map((gsi) => ({
|
|
384
|
+
IndexName: gsi.IndexName,
|
|
385
|
+
KeySchema: (gsi.KeySchema || []).map((k) => ({
|
|
386
|
+
AttributeName: resolveRef(k.AttributeName),
|
|
387
|
+
KeyType: k.KeyType,
|
|
388
|
+
})),
|
|
389
|
+
Projection: gsi.Projection || { ProjectionType: "ALL" },
|
|
390
|
+
// Opcional: ProvisionedThroughput (se não for PAY_PER_REQUEST)
|
|
391
|
+
...(props.BillingMode !== "PAY_PER_REQUEST" && gsi.ProvisionedThroughput
|
|
392
|
+
? {
|
|
393
|
+
ProvisionedThroughput: {
|
|
394
|
+
ReadCapacityUnits: gsi.ProvisionedThroughput?.ReadCapacityUnits || 5,
|
|
395
|
+
WriteCapacityUnits: gsi.ProvisionedThroughput?.WriteCapacityUnits || 5,
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
: {}),
|
|
399
|
+
}));
|
|
400
|
+
|
|
387
401
|
await this.dynamoSimulator.createTable({
|
|
388
402
|
TableName: tableName,
|
|
389
403
|
AttributeDefinitions: attrDefs,
|
|
390
404
|
KeySchema: keySchema,
|
|
391
|
-
|
|
405
|
+
GlobalSecondaryIndexes: gsis, // ⭐ NOVO: passa os GSIs
|
|
406
|
+
BillingMode: props.BillingMode || "PAY_PER_REQUEST",
|
|
392
407
|
Tags: props.Tags || [],
|
|
393
408
|
});
|
|
394
409
|
physicalId = tableName;
|
|
395
|
-
this.logger.info(`[CloudFormation] Provisionada DynamoDB table: ${tableName}`);
|
|
410
|
+
this.logger.info(`[CloudFormation] Provisionada DynamoDB table: ${tableName} com ${gsis.length} GSIs`);
|
|
396
411
|
break;
|
|
397
412
|
}
|
|
398
413
|
|
|
399
|
-
case
|
|
414
|
+
case "AWS::Athena::WorkGroup": {
|
|
400
415
|
if (!this.athenaSimulator) break;
|
|
401
416
|
const wgName = resolveRef(props.Name) || `${stackName}-${logicalId}`;
|
|
402
417
|
await this.athenaSimulator.createWorkGroup({
|
|
403
418
|
Name: wgName,
|
|
404
|
-
Description: resolveRef(props.Description) ||
|
|
419
|
+
Description: resolveRef(props.Description) || "",
|
|
405
420
|
Configuration: props.WorkGroupConfiguration || {},
|
|
406
421
|
});
|
|
407
422
|
physicalId = wgName;
|
|
@@ -409,12 +424,12 @@ class CloudFormationSimulator {
|
|
|
409
424
|
break;
|
|
410
425
|
}
|
|
411
426
|
|
|
412
|
-
case
|
|
427
|
+
case "AWS::KMS::Key": {
|
|
413
428
|
if (!this.kmsSimulator) break;
|
|
414
429
|
const result = await this.kmsSimulator.createKey({
|
|
415
430
|
Description: resolveRef(props.Description) || `${stackName}-${logicalId}`,
|
|
416
|
-
KeyUsage: props.KeyUsage ||
|
|
417
|
-
KeySpec: props.KeySpec ||
|
|
431
|
+
KeyUsage: props.KeyUsage || "ENCRYPT_DECRYPT",
|
|
432
|
+
KeySpec: props.KeySpec || "SYMMETRIC_DEFAULT",
|
|
418
433
|
Tags: props.Tags || [],
|
|
419
434
|
});
|
|
420
435
|
physicalId = result.KeyMetadata.KeyId;
|
|
@@ -422,13 +437,13 @@ class CloudFormationSimulator {
|
|
|
422
437
|
break;
|
|
423
438
|
}
|
|
424
439
|
|
|
425
|
-
case
|
|
440
|
+
case "AWS::SecretsManager::Secret": {
|
|
426
441
|
if (!this.secretsSimulator) break;
|
|
427
442
|
const secretName = resolveRef(props.Name) || `${stackName}-${logicalId}`;
|
|
428
443
|
await this.secretsSimulator.createSecret({
|
|
429
444
|
Name: secretName,
|
|
430
|
-
Description: resolveRef(props.Description) ||
|
|
431
|
-
SecretString: resolveRef(props.SecretString) ||
|
|
445
|
+
Description: resolveRef(props.Description) || "",
|
|
446
|
+
SecretString: resolveRef(props.SecretString) || "{}",
|
|
432
447
|
Tags: props.Tags || [],
|
|
433
448
|
});
|
|
434
449
|
physicalId = secretName;
|
|
@@ -436,14 +451,14 @@ class CloudFormationSimulator {
|
|
|
436
451
|
break;
|
|
437
452
|
}
|
|
438
453
|
|
|
439
|
-
case
|
|
454
|
+
case "AWS::SSM::Parameter": {
|
|
440
455
|
if (!this.parameterStoreSimulator) break;
|
|
441
456
|
const paramName = resolveRef(props.Name) || `/${stackName}/${logicalId}`;
|
|
442
457
|
await this.parameterStoreSimulator.putParameter({
|
|
443
458
|
Name: paramName,
|
|
444
|
-
Value: resolveRef(props.Value) ||
|
|
445
|
-
Type: props.Type ||
|
|
446
|
-
Description: resolveRef(props.Description) ||
|
|
459
|
+
Value: resolveRef(props.Value) || "",
|
|
460
|
+
Type: props.Type || "String",
|
|
461
|
+
Description: resolveRef(props.Description) || "",
|
|
447
462
|
Tags: props.Tags || [],
|
|
448
463
|
Overwrite: true,
|
|
449
464
|
});
|
|
@@ -458,10 +473,9 @@ class CloudFormationSimulator {
|
|
|
458
473
|
|
|
459
474
|
// Atualiza o PhysicalResourceId com o nome real do recurso
|
|
460
475
|
if (physicalId) {
|
|
461
|
-
const entry = stackResourceList.find(r => r.LogicalResourceId === logicalId);
|
|
476
|
+
const entry = stackResourceList.find((r) => r.LogicalResourceId === logicalId);
|
|
462
477
|
if (entry) entry.PhysicalResourceId = physicalId;
|
|
463
478
|
}
|
|
464
|
-
|
|
465
479
|
} catch (err) {
|
|
466
480
|
this.logger.warn(`[CloudFormation] Erro ao provisionar ${logicalId} (${resource.Type}): ${err.message}`);
|
|
467
481
|
}
|
|
@@ -471,29 +485,18 @@ class CloudFormationSimulator {
|
|
|
471
485
|
/**
|
|
472
486
|
* UpdateStack
|
|
473
487
|
*/
|
|
474
|
-
async updateStack({
|
|
475
|
-
StackName,
|
|
476
|
-
TemplateBody,
|
|
477
|
-
UsePreviousTemplate = false,
|
|
478
|
-
Parameters = [],
|
|
479
|
-
Capabilities = [],
|
|
480
|
-
Tags,
|
|
481
|
-
RoleARN,
|
|
482
|
-
NotificationARNs,
|
|
483
|
-
}) {
|
|
488
|
+
async updateStack({ StackName, TemplateBody, UsePreviousTemplate = false, Parameters = [], Capabilities = [], Tags, RoleARN, NotificationARNs }) {
|
|
484
489
|
const stack = this._getStack(StackName);
|
|
485
490
|
|
|
486
|
-
const templateBody = UsePreviousTemplate
|
|
487
|
-
? stack.TemplateBody
|
|
488
|
-
: (TemplateBody || stack.TemplateBody);
|
|
491
|
+
const templateBody = UsePreviousTemplate ? stack.TemplateBody : TemplateBody || stack.TemplateBody;
|
|
489
492
|
|
|
490
493
|
const template = parseTemplate(templateBody);
|
|
491
|
-
const resolvedParams = resolveParameters(template.Parameters, Parameters.length ? Parameters : stack.Parameters.map(p => ({ ParameterKey: p.ParameterKey, UsePreviousValue: true })));
|
|
494
|
+
const resolvedParams = resolveParameters(template.Parameters, Parameters.length ? Parameters : stack.Parameters.map((p) => ({ ParameterKey: p.ParameterKey, UsePreviousValue: true })));
|
|
492
495
|
const outputs = resolveOutputs(template, StackName);
|
|
493
496
|
const resources = extractResources(template, StackName);
|
|
494
497
|
|
|
495
498
|
stack.StackStatus = StackStatus.UPDATE_COMPLETE;
|
|
496
|
-
stack.StackStatusReason =
|
|
499
|
+
stack.StackStatusReason = "Stack updated successfully";
|
|
497
500
|
stack.LastUpdatedTime = now();
|
|
498
501
|
stack.TemplateBody = templateBody;
|
|
499
502
|
stack.Parameters = resolvedParams;
|
|
@@ -523,11 +526,7 @@ class CloudFormationSimulator {
|
|
|
523
526
|
const stack = this.stacks.get(StackName);
|
|
524
527
|
|
|
525
528
|
if (stack.EnableTerminationProtection) {
|
|
526
|
-
throw new CloudFormationError(
|
|
527
|
-
'ValidationError',
|
|
528
|
-
`Stack [${StackName}] cannot be deleted while TerminationProtection is enabled`,
|
|
529
|
-
400
|
|
530
|
-
);
|
|
529
|
+
throw new CloudFormationError("ValidationError", `Stack [${StackName}] cannot be deleted while TerminationProtection is enabled`, 400);
|
|
531
530
|
}
|
|
532
531
|
|
|
533
532
|
// Captura recursos antes de remover do map
|
|
@@ -555,49 +554,49 @@ class CloudFormationSimulator {
|
|
|
555
554
|
const { ResourceType, PhysicalResourceId } = resource;
|
|
556
555
|
try {
|
|
557
556
|
switch (ResourceType) {
|
|
558
|
-
case
|
|
557
|
+
case "AWS::S3::Bucket":
|
|
559
558
|
if (this.s3Simulator && PhysicalResourceId) {
|
|
560
559
|
this.s3Simulator.deleteBucket(PhysicalResourceId);
|
|
561
560
|
this.logger.info(`[CloudFormation] Removido S3 bucket: ${PhysicalResourceId}`);
|
|
562
561
|
}
|
|
563
562
|
break;
|
|
564
563
|
|
|
565
|
-
case
|
|
564
|
+
case "AWS::SQS::Queue":
|
|
566
565
|
if (this.sqsSimulator && PhysicalResourceId) {
|
|
567
566
|
this.sqsSimulator.deleteQueue(PhysicalResourceId);
|
|
568
567
|
this.logger.info(`[CloudFormation] Removida SQS queue: ${PhysicalResourceId}`);
|
|
569
568
|
}
|
|
570
569
|
break;
|
|
571
570
|
|
|
572
|
-
case
|
|
571
|
+
case "AWS::DynamoDB::Table":
|
|
573
572
|
if (this.dynamoSimulator && PhysicalResourceId) {
|
|
574
573
|
await this.dynamoSimulator.deleteTable({ TableName: PhysicalResourceId });
|
|
575
574
|
this.logger.info(`[CloudFormation] Removida DynamoDB table: ${PhysicalResourceId}`);
|
|
576
575
|
}
|
|
577
576
|
break;
|
|
578
577
|
|
|
579
|
-
case
|
|
578
|
+
case "AWS::KMS::Key":
|
|
580
579
|
if (this.kmsSimulator && PhysicalResourceId) {
|
|
581
580
|
await this.kmsSimulator.scheduleKeyDeletion({ KeyId: PhysicalResourceId, PendingWindowInDays: 7 });
|
|
582
581
|
this.logger.info(`[CloudFormation] Agendada exclusão KMS key: ${PhysicalResourceId}`);
|
|
583
582
|
}
|
|
584
583
|
break;
|
|
585
584
|
|
|
586
|
-
case
|
|
585
|
+
case "AWS::SecretsManager::Secret":
|
|
587
586
|
if (this.secretsSimulator && PhysicalResourceId) {
|
|
588
587
|
await this.secretsSimulator.deleteSecret({ SecretId: PhysicalResourceId, ForceDeleteWithoutRecovery: true });
|
|
589
588
|
this.logger.info(`[CloudFormation] Removido Secret: ${PhysicalResourceId}`);
|
|
590
589
|
}
|
|
591
590
|
break;
|
|
592
591
|
|
|
593
|
-
case
|
|
592
|
+
case "AWS::SSM::Parameter":
|
|
594
593
|
if (this.parameterStoreSimulator && PhysicalResourceId) {
|
|
595
594
|
await this.parameterStoreSimulator.deleteParameter({ Name: PhysicalResourceId });
|
|
596
595
|
this.logger.info(`[CloudFormation] Removido SSM Parameter: ${PhysicalResourceId}`);
|
|
597
596
|
}
|
|
598
597
|
break;
|
|
599
598
|
|
|
600
|
-
case
|
|
599
|
+
case "AWS::Athena::WorkGroup":
|
|
601
600
|
if (this.athenaSimulator && PhysicalResourceId) {
|
|
602
601
|
await this.athenaSimulator.deleteWorkGroup({ WorkGroup: PhysicalResourceId, RecursiveDeleteOption: true });
|
|
603
602
|
this.logger.info(`[CloudFormation] Removido Athena WorkGroup: ${PhysicalResourceId}`);
|
|
@@ -621,7 +620,7 @@ class CloudFormationSimulator {
|
|
|
621
620
|
const stack = this._getStack(StackName);
|
|
622
621
|
return { Stacks: [this._formatStack(stack)] };
|
|
623
622
|
}
|
|
624
|
-
const stacks = Array.from(this.stacks.values()).map(s => this._formatStack(s));
|
|
623
|
+
const stacks = Array.from(this.stacks.values()).map((s) => this._formatStack(s));
|
|
625
624
|
return { Stacks: stacks };
|
|
626
625
|
}
|
|
627
626
|
|
|
@@ -632,15 +631,15 @@ class CloudFormationSimulator {
|
|
|
632
631
|
let items = Array.from(this.stacks.values());
|
|
633
632
|
|
|
634
633
|
if (StackStatusFilter.length > 0) {
|
|
635
|
-
items = items.filter(s => StackStatusFilter.includes(s.StackStatus));
|
|
634
|
+
items = items.filter((s) => StackStatusFilter.includes(s.StackStatus));
|
|
636
635
|
}
|
|
637
636
|
|
|
638
637
|
let startIdx = 0;
|
|
639
638
|
if (NextToken) {
|
|
640
|
-
startIdx = parseInt(Buffer.from(NextToken,
|
|
639
|
+
startIdx = parseInt(Buffer.from(NextToken, "base64").toString("utf8"), 10) || 0;
|
|
641
640
|
}
|
|
642
641
|
|
|
643
|
-
const page = items.slice(startIdx, startIdx + 100).map(s => ({
|
|
642
|
+
const page = items.slice(startIdx, startIdx + 100).map((s) => ({
|
|
644
643
|
StackId: s.StackId,
|
|
645
644
|
StackName: s.StackName,
|
|
646
645
|
StackStatus: s.StackStatus,
|
|
@@ -652,9 +651,7 @@ class CloudFormationSimulator {
|
|
|
652
651
|
}));
|
|
653
652
|
|
|
654
653
|
const hasMore = startIdx + 100 < items.length;
|
|
655
|
-
const newNextToken = hasMore
|
|
656
|
-
? Buffer.from(String(startIdx + 100)).toString('base64')
|
|
657
|
-
: undefined;
|
|
654
|
+
const newNextToken = hasMore ? Buffer.from(String(startIdx + 100)).toString("base64") : undefined;
|
|
658
655
|
|
|
659
656
|
return { StackSummaries: page, NextToken: newNextToken };
|
|
660
657
|
}
|
|
@@ -665,41 +662,37 @@ class CloudFormationSimulator {
|
|
|
665
662
|
* ValidateTemplate
|
|
666
663
|
*/
|
|
667
664
|
validateTemplate({ TemplateBody, TemplateURL }) {
|
|
668
|
-
const body = TemplateBody ||
|
|
665
|
+
const body = TemplateBody || "{}";
|
|
669
666
|
const template = parseTemplate(body);
|
|
670
667
|
|
|
671
668
|
const parameters = Object.entries(template.Parameters || {}).map(([key, def]) => ({
|
|
672
669
|
ParameterKey: key,
|
|
673
|
-
DefaultValue: def.Default ||
|
|
670
|
+
DefaultValue: def.Default || "",
|
|
674
671
|
NoEcho: def.NoEcho || false,
|
|
675
|
-
Description: def.Description ||
|
|
672
|
+
Description: def.Description || "",
|
|
676
673
|
}));
|
|
677
674
|
|
|
678
675
|
const capabilities = [];
|
|
679
676
|
const resources = Object.values(template.Resources || {});
|
|
680
|
-
const hasIAM = resources.some(r =>
|
|
681
|
-
|
|
682
|
-
);
|
|
683
|
-
if (hasIAM) capabilities.push('CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM');
|
|
677
|
+
const hasIAM = resources.some((r) => r.Type && (r.Type.includes("IAM") || r.Type.includes("Role")));
|
|
678
|
+
if (hasIAM) capabilities.push("CAPABILITY_IAM", "CAPABILITY_NAMED_IAM");
|
|
684
679
|
|
|
685
680
|
return {
|
|
686
681
|
Parameters: parameters,
|
|
687
|
-
Description: template.Description ||
|
|
682
|
+
Description: template.Description || "",
|
|
688
683
|
Capabilities: capabilities,
|
|
689
|
-
CapabilitiesReason: capabilities.length > 0
|
|
690
|
-
? 'The following resource(s) require capabilities: [AWS::IAM::Role]'
|
|
691
|
-
: '',
|
|
684
|
+
CapabilitiesReason: capabilities.length > 0 ? "The following resource(s) require capabilities: [AWS::IAM::Role]" : "",
|
|
692
685
|
};
|
|
693
686
|
}
|
|
694
687
|
|
|
695
688
|
/**
|
|
696
689
|
* GetTemplate
|
|
697
690
|
*/
|
|
698
|
-
getTemplate({ StackName, TemplateStage =
|
|
691
|
+
getTemplate({ StackName, TemplateStage = "Original" }) {
|
|
699
692
|
const stack = this._getStack(StackName);
|
|
700
693
|
return {
|
|
701
|
-
TemplateBody: stack.TemplateBody ||
|
|
702
|
-
StagesAvailable: [
|
|
694
|
+
TemplateBody: stack.TemplateBody || "{}",
|
|
695
|
+
StagesAvailable: ["Original", "Processed"],
|
|
703
696
|
};
|
|
704
697
|
}
|
|
705
698
|
|
|
@@ -712,10 +705,10 @@ class CloudFormationSimulator {
|
|
|
712
705
|
const resources = this.stackResources.get(StackName) || [];
|
|
713
706
|
let filtered = resources;
|
|
714
707
|
if (LogicalResourceId) {
|
|
715
|
-
filtered = resources.filter(r => r.LogicalResourceId === LogicalResourceId);
|
|
708
|
+
filtered = resources.filter((r) => r.LogicalResourceId === LogicalResourceId);
|
|
716
709
|
}
|
|
717
710
|
return {
|
|
718
|
-
StackResources: filtered.map(r => ({ ...r, StackName, StackId: this.stacks.get(StackName)?.StackId })),
|
|
711
|
+
StackResources: filtered.map((r) => ({ ...r, StackName, StackId: this.stacks.get(StackName)?.StackId })),
|
|
719
712
|
};
|
|
720
713
|
}
|
|
721
714
|
|
|
@@ -728,14 +721,12 @@ class CloudFormationSimulator {
|
|
|
728
721
|
|
|
729
722
|
let startIdx = 0;
|
|
730
723
|
if (NextToken) {
|
|
731
|
-
startIdx = parseInt(Buffer.from(NextToken,
|
|
724
|
+
startIdx = parseInt(Buffer.from(NextToken, "base64").toString("utf8"), 10) || 0;
|
|
732
725
|
}
|
|
733
726
|
|
|
734
727
|
const page = resources.slice(startIdx, startIdx + 100);
|
|
735
728
|
const hasMore = startIdx + 100 < resources.length;
|
|
736
|
-
const newNextToken = hasMore
|
|
737
|
-
? Buffer.from(String(startIdx + 100)).toString('base64')
|
|
738
|
-
: undefined;
|
|
729
|
+
const newNextToken = hasMore ? Buffer.from(String(startIdx + 100)).toString("base64") : undefined;
|
|
739
730
|
|
|
740
731
|
return {
|
|
741
732
|
StackResourceSummaries: page,
|
|
@@ -748,31 +739,19 @@ class CloudFormationSimulator {
|
|
|
748
739
|
/**
|
|
749
740
|
* CreateChangeSet
|
|
750
741
|
*/
|
|
751
|
-
async createChangeSet({
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
TemplateBody,
|
|
755
|
-
UsePreviousTemplate = false,
|
|
756
|
-
Parameters = [],
|
|
757
|
-
Capabilities = [],
|
|
758
|
-
Tags = [],
|
|
759
|
-
Description = '',
|
|
760
|
-
ChangeSetType = 'UPDATE',
|
|
761
|
-
}) {
|
|
762
|
-
if (!ChangeSetName) throw Errors.InvalidAction('ChangeSetName is required');
|
|
763
|
-
if (!StackName) throw Errors.InvalidAction('StackName is required');
|
|
742
|
+
async createChangeSet({ StackName, ChangeSetName, TemplateBody, UsePreviousTemplate = false, Parameters = [], Capabilities = [], Tags = [], Description = "", ChangeSetType = "UPDATE" }) {
|
|
743
|
+
if (!ChangeSetName) throw Errors.InvalidAction("ChangeSetName is required");
|
|
744
|
+
if (!StackName) throw Errors.InvalidAction("StackName is required");
|
|
764
745
|
|
|
765
746
|
// Para CREATE, a stack não deve existir; para UPDATE, deve existir
|
|
766
|
-
if (ChangeSetType ===
|
|
747
|
+
if (ChangeSetType === "UPDATE" && !this.stacks.has(StackName)) {
|
|
767
748
|
throw Errors.DoesNotExist(StackName);
|
|
768
749
|
}
|
|
769
750
|
|
|
770
751
|
const csArn = changeSetArn(StackName, ChangeSetName);
|
|
771
752
|
|
|
772
753
|
const existingStack = this.stacks.get(StackName);
|
|
773
|
-
const templateBody = UsePreviousTemplate
|
|
774
|
-
? (existingStack?.TemplateBody || '{}')
|
|
775
|
-
: (TemplateBody || '{}');
|
|
754
|
+
const templateBody = UsePreviousTemplate ? existingStack?.TemplateBody || "{}" : TemplateBody || "{}";
|
|
776
755
|
|
|
777
756
|
const template = parseTemplate(templateBody);
|
|
778
757
|
const newResources = extractResources(template, StackName);
|
|
@@ -787,7 +766,7 @@ class CloudFormationSimulator {
|
|
|
787
766
|
StackName,
|
|
788
767
|
StackId: existingStack?.StackId || stackArn(StackName, randomUUID()),
|
|
789
768
|
Status: ChangeSetStatus.CREATE_COMPLETE,
|
|
790
|
-
StatusReason:
|
|
769
|
+
StatusReason: "Complete",
|
|
791
770
|
Description,
|
|
792
771
|
ChangeSetType,
|
|
793
772
|
CreationTime: now(),
|
|
@@ -796,7 +775,7 @@ class CloudFormationSimulator {
|
|
|
796
775
|
Tags,
|
|
797
776
|
TemplateBody: templateBody,
|
|
798
777
|
Changes: changes,
|
|
799
|
-
ExecutionStatus:
|
|
778
|
+
ExecutionStatus: "AVAILABLE",
|
|
800
779
|
};
|
|
801
780
|
|
|
802
781
|
this.changeSets.set(csArn, changeSet);
|
|
@@ -821,12 +800,8 @@ class CloudFormationSimulator {
|
|
|
821
800
|
async executeChangeSet({ ChangeSetName, StackName, ClientRequestToken }) {
|
|
822
801
|
const changeSet = this._getChangeSet(ChangeSetName, StackName);
|
|
823
802
|
|
|
824
|
-
if (changeSet.ExecutionStatus !==
|
|
825
|
-
throw new CloudFormationError(
|
|
826
|
-
'InvalidChangeSetStatus',
|
|
827
|
-
`ChangeSet [${ChangeSetName}] cannot be executed in its current status [${changeSet.ExecutionStatus}]`,
|
|
828
|
-
400
|
|
829
|
-
);
|
|
803
|
+
if (changeSet.ExecutionStatus !== "AVAILABLE") {
|
|
804
|
+
throw new CloudFormationError("InvalidChangeSetStatus", `ChangeSet [${ChangeSetName}] cannot be executed in its current status [${changeSet.ExecutionStatus}]`, 400);
|
|
830
805
|
}
|
|
831
806
|
|
|
832
807
|
const template = parseTemplate(changeSet.TemplateBody);
|
|
@@ -834,7 +809,7 @@ class CloudFormationSimulator {
|
|
|
834
809
|
const resolvedParams = resolveParameters(template.Parameters, changeSet.Parameters);
|
|
835
810
|
const outputs = resolveOutputs(template, StackName);
|
|
836
811
|
|
|
837
|
-
if (changeSet.ChangeSetType ===
|
|
812
|
+
if (changeSet.ChangeSetType === "CREATE") {
|
|
838
813
|
// Cria a stack
|
|
839
814
|
const stackId = randomUUID();
|
|
840
815
|
const arn = stackArn(StackName, stackId);
|
|
@@ -842,7 +817,7 @@ class CloudFormationSimulator {
|
|
|
842
817
|
StackId: changeSet.StackId || arn,
|
|
843
818
|
StackName,
|
|
844
819
|
StackStatus: StackStatus.CREATE_COMPLETE,
|
|
845
|
-
StackStatusReason:
|
|
820
|
+
StackStatusReason: "Stack created via ChangeSet",
|
|
846
821
|
CreationTime: now(),
|
|
847
822
|
LastUpdatedTime: now(),
|
|
848
823
|
Parameters: resolvedParams,
|
|
@@ -852,7 +827,7 @@ class CloudFormationSimulator {
|
|
|
852
827
|
NotificationARNs: [],
|
|
853
828
|
TemplateBody: changeSet.TemplateBody,
|
|
854
829
|
EnableTerminationProtection: false,
|
|
855
|
-
DriftInformation: { StackDriftStatus:
|
|
830
|
+
DriftInformation: { StackDriftStatus: "NOT_CHECKED" },
|
|
856
831
|
};
|
|
857
832
|
this.stacks.set(StackName, stack);
|
|
858
833
|
} else {
|
|
@@ -860,7 +835,7 @@ class CloudFormationSimulator {
|
|
|
860
835
|
const stack = this.stacks.get(StackName);
|
|
861
836
|
if (stack) {
|
|
862
837
|
stack.StackStatus = StackStatus.UPDATE_COMPLETE;
|
|
863
|
-
stack.StackStatusReason =
|
|
838
|
+
stack.StackStatusReason = "Stack updated via ChangeSet";
|
|
864
839
|
stack.LastUpdatedTime = now();
|
|
865
840
|
stack.TemplateBody = changeSet.TemplateBody;
|
|
866
841
|
stack.Parameters = resolvedParams;
|
|
@@ -869,8 +844,8 @@ class CloudFormationSimulator {
|
|
|
869
844
|
}
|
|
870
845
|
|
|
871
846
|
this.stackResources.set(StackName, resources);
|
|
872
|
-
changeSet.ExecutionStatus =
|
|
873
|
-
changeSet.Status =
|
|
847
|
+
changeSet.ExecutionStatus = "EXECUTE_COMPLETE";
|
|
848
|
+
changeSet.Status = "UPDATE_COMPLETE";
|
|
874
849
|
|
|
875
850
|
this.logger.info(`[CloudFormation] Executed ChangeSet: ${ChangeSetName} on ${StackName}`);
|
|
876
851
|
await this.save();
|
|
@@ -896,8 +871,8 @@ class CloudFormationSimulator {
|
|
|
896
871
|
*/
|
|
897
872
|
listChangeSets({ StackName, NextToken } = {}) {
|
|
898
873
|
const items = Array.from(this.changeSets.values())
|
|
899
|
-
.filter(cs => cs.StackName === StackName)
|
|
900
|
-
.map(cs => ({
|
|
874
|
+
.filter((cs) => cs.StackName === StackName)
|
|
875
|
+
.map((cs) => ({
|
|
901
876
|
ChangeSetId: cs.ChangeSetId,
|
|
902
877
|
ChangeSetName: cs.ChangeSetName,
|
|
903
878
|
StackId: cs.StackId,
|
|
@@ -961,27 +936,27 @@ class CloudFormationSimulator {
|
|
|
961
936
|
Capabilities: stack.Capabilities || [],
|
|
962
937
|
Tags: stack.Tags || [],
|
|
963
938
|
NotificationARNs: stack.NotificationARNs || [],
|
|
964
|
-
RoleARN: stack.RoleARN ||
|
|
939
|
+
RoleARN: stack.RoleARN || "",
|
|
965
940
|
EnableTerminationProtection: stack.EnableTerminationProtection || false,
|
|
966
|
-
DriftInformation: stack.DriftInformation || { StackDriftStatus:
|
|
941
|
+
DriftInformation: stack.DriftInformation || { StackDriftStatus: "NOT_CHECKED" },
|
|
967
942
|
};
|
|
968
943
|
}
|
|
969
944
|
|
|
970
945
|
_computeChanges(currentResources, newResources) {
|
|
971
946
|
const changes = [];
|
|
972
|
-
const currentMap = new Map(currentResources.map(r => [r.LogicalResourceId, r]));
|
|
973
|
-
const newMap = new Map(newResources.map(r => [r.LogicalResourceId, r]));
|
|
947
|
+
const currentMap = new Map(currentResources.map((r) => [r.LogicalResourceId, r]));
|
|
948
|
+
const newMap = new Map(newResources.map((r) => [r.LogicalResourceId, r]));
|
|
974
949
|
|
|
975
950
|
// Adições
|
|
976
951
|
for (const [id, res] of newMap.entries()) {
|
|
977
952
|
if (!currentMap.has(id)) {
|
|
978
953
|
changes.push({
|
|
979
|
-
Type:
|
|
954
|
+
Type: "Resource",
|
|
980
955
|
ResourceChange: {
|
|
981
|
-
Action:
|
|
956
|
+
Action: "Add",
|
|
982
957
|
LogicalResourceId: id,
|
|
983
958
|
ResourceType: res.ResourceType,
|
|
984
|
-
Replacement:
|
|
959
|
+
Replacement: "False",
|
|
985
960
|
Scope: [],
|
|
986
961
|
Details: [],
|
|
987
962
|
},
|
|
@@ -993,13 +968,13 @@ class CloudFormationSimulator {
|
|
|
993
968
|
for (const [id, res] of currentMap.entries()) {
|
|
994
969
|
if (!newMap.has(id)) {
|
|
995
970
|
changes.push({
|
|
996
|
-
Type:
|
|
971
|
+
Type: "Resource",
|
|
997
972
|
ResourceChange: {
|
|
998
|
-
Action:
|
|
973
|
+
Action: "Remove",
|
|
999
974
|
LogicalResourceId: id,
|
|
1000
975
|
PhysicalResourceId: res.PhysicalResourceId,
|
|
1001
976
|
ResourceType: res.ResourceType,
|
|
1002
|
-
Replacement:
|
|
977
|
+
Replacement: "False",
|
|
1003
978
|
Scope: [],
|
|
1004
979
|
Details: [],
|
|
1005
980
|
},
|
|
@@ -1013,14 +988,14 @@ class CloudFormationSimulator {
|
|
|
1013
988
|
const current = currentMap.get(id);
|
|
1014
989
|
if (current.ResourceType !== res.ResourceType) {
|
|
1015
990
|
changes.push({
|
|
1016
|
-
Type:
|
|
991
|
+
Type: "Resource",
|
|
1017
992
|
ResourceChange: {
|
|
1018
|
-
Action:
|
|
993
|
+
Action: "Modify",
|
|
1019
994
|
LogicalResourceId: id,
|
|
1020
995
|
PhysicalResourceId: current.PhysicalResourceId,
|
|
1021
996
|
ResourceType: res.ResourceType,
|
|
1022
|
-
Replacement:
|
|
1023
|
-
Scope: [
|
|
997
|
+
Replacement: "True",
|
|
998
|
+
Scope: ["Properties"],
|
|
1024
999
|
Details: [],
|
|
1025
1000
|
},
|
|
1026
1001
|
});
|