@go-to-k/cdkd 0.1.0 → 0.3.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/README.md +5 -4
- package/dist/cli.js +710 -185
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.3.0.tgz +0 -0
- package/dist/index.js +330 -137
- package/dist/index.js.map +4 -4
- package/package.json +3 -2
- package/dist/go-to-k-cdkd-0.1.0.tgz +0 -0
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -3139,6 +3139,62 @@ var TemplateParser = class {
|
|
|
3139
3139
|
|
|
3140
3140
|
// src/analyzer/dag-builder.ts
|
|
3141
3141
|
import graphlib from "graphlib";
|
|
3142
|
+
|
|
3143
|
+
// src/analyzer/lambda-vpc-deps.ts
|
|
3144
|
+
function extractLambdaVpcDeleteDeps(resources) {
|
|
3145
|
+
const edges = [];
|
|
3146
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3147
|
+
for (const [lambdaId, resource] of Object.entries(resources)) {
|
|
3148
|
+
if (resource.Type !== "AWS::Lambda::Function")
|
|
3149
|
+
continue;
|
|
3150
|
+
const vpcConfig = (resource.Properties ?? {})["VpcConfig"];
|
|
3151
|
+
if (!isObject(vpcConfig))
|
|
3152
|
+
continue;
|
|
3153
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3154
|
+
collectRefIds(vpcConfig["SubnetIds"], targets);
|
|
3155
|
+
collectRefIds(vpcConfig["SecurityGroupIds"], targets);
|
|
3156
|
+
for (const targetId of targets) {
|
|
3157
|
+
if (targetId === lambdaId)
|
|
3158
|
+
continue;
|
|
3159
|
+
if (!(targetId in resources))
|
|
3160
|
+
continue;
|
|
3161
|
+
const key = `${lambdaId}\0${targetId}`;
|
|
3162
|
+
if (seen.has(key))
|
|
3163
|
+
continue;
|
|
3164
|
+
seen.add(key);
|
|
3165
|
+
edges.push({ before: lambdaId, after: targetId });
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
return edges;
|
|
3169
|
+
}
|
|
3170
|
+
function isObject(v) {
|
|
3171
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3172
|
+
}
|
|
3173
|
+
function collectRefIds(value, out) {
|
|
3174
|
+
if (value === null || value === void 0)
|
|
3175
|
+
return;
|
|
3176
|
+
if (Array.isArray(value)) {
|
|
3177
|
+
for (const item of value)
|
|
3178
|
+
collectRefIds(item, out);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
if (!isObject(value))
|
|
3182
|
+
return;
|
|
3183
|
+
if (typeof value["Ref"] === "string") {
|
|
3184
|
+
const ref = value["Ref"];
|
|
3185
|
+
if (!ref.startsWith("AWS::"))
|
|
3186
|
+
out.add(ref);
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
if (Array.isArray(value["Fn::GetAtt"])) {
|
|
3190
|
+
const arr = value["Fn::GetAtt"];
|
|
3191
|
+
if (typeof arr[0] === "string")
|
|
3192
|
+
out.add(arr[0]);
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
// src/analyzer/dag-builder.ts
|
|
3142
3198
|
var { Graph, alg } = graphlib;
|
|
3143
3199
|
var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
|
|
3144
3200
|
"AWS::IAM::Policy",
|
|
@@ -3186,6 +3242,7 @@ var DagBuilder = class {
|
|
|
3186
3242
|
}
|
|
3187
3243
|
this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
|
|
3188
3244
|
edgeCount += this.addCustomResourcePolicyEdges(graph, template);
|
|
3245
|
+
edgeCount += this.addLambdaVpcEdges(graph, template);
|
|
3189
3246
|
if (!alg.isAcyclic(graph)) {
|
|
3190
3247
|
const cycles = this.findCycles(graph);
|
|
3191
3248
|
throw new DependencyError(
|
|
@@ -3378,6 +3435,41 @@ var DagBuilder = class {
|
|
|
3378
3435
|
}
|
|
3379
3436
|
return added;
|
|
3380
3437
|
}
|
|
3438
|
+
/**
|
|
3439
|
+
* Add edges from Subnets / SecurityGroups referenced by an
|
|
3440
|
+
* AWS::Lambda::Function VpcConfig to the Lambda itself.
|
|
3441
|
+
*
|
|
3442
|
+
* Same direction as a normal `Ref`-derived edge (Subnet -> Lambda), so for
|
|
3443
|
+
* deploy this just duplicates what extractDependencies already produced.
|
|
3444
|
+
* The point is robustness: if a future template massages the VpcConfig
|
|
3445
|
+
* shape in a way the recursive extractor doesn't anticipate, this pass
|
|
3446
|
+
* still ties the Lambda to its networking resources so that the
|
|
3447
|
+
* deletion-time reverse traversal continues to delete Lambda before
|
|
3448
|
+
* Subnet / SecurityGroup.
|
|
3449
|
+
*
|
|
3450
|
+
* Returns the number of NEW edges added (existing edges are skipped).
|
|
3451
|
+
*/
|
|
3452
|
+
addLambdaVpcEdges(graph, template) {
|
|
3453
|
+
const edges = extractLambdaVpcDeleteDeps(template.Resources);
|
|
3454
|
+
if (edges.length === 0)
|
|
3455
|
+
return 0;
|
|
3456
|
+
let added = 0;
|
|
3457
|
+
for (const edge of edges) {
|
|
3458
|
+
const depId = edge.after;
|
|
3459
|
+
const dependentId = edge.before;
|
|
3460
|
+
if (!graph.hasNode(depId) || !graph.hasNode(dependentId))
|
|
3461
|
+
continue;
|
|
3462
|
+
if (graph.hasEdge(depId, dependentId))
|
|
3463
|
+
continue;
|
|
3464
|
+
graph.setEdge(depId, dependentId);
|
|
3465
|
+
added++;
|
|
3466
|
+
this.logger.debug(`Added implicit edge (lambda vpc): ${depId} -> ${dependentId}`);
|
|
3467
|
+
}
|
|
3468
|
+
if (added > 0) {
|
|
3469
|
+
this.logger.debug(`Added ${added} implicit edges for Lambda VpcConfig`);
|
|
3470
|
+
}
|
|
3471
|
+
return added;
|
|
3472
|
+
}
|
|
3381
3473
|
isCustomResourceType(type) {
|
|
3382
3474
|
return type === "AWS::CloudFormation::CustomResource" || type.startsWith("Custom::");
|
|
3383
3475
|
}
|
|
@@ -6993,15 +7085,141 @@ var IAMRoleProvider = class {
|
|
|
6993
7085
|
}
|
|
6994
7086
|
};
|
|
6995
7087
|
|
|
7088
|
+
// src/deployment/dag-executor.ts
|
|
7089
|
+
var DagExecutor = class {
|
|
7090
|
+
nodes = /* @__PURE__ */ new Map();
|
|
7091
|
+
logger = getLogger().child("DagExecutor");
|
|
7092
|
+
add(node) {
|
|
7093
|
+
this.nodes.set(node.id, node);
|
|
7094
|
+
}
|
|
7095
|
+
has(id) {
|
|
7096
|
+
return this.nodes.has(id);
|
|
7097
|
+
}
|
|
7098
|
+
size() {
|
|
7099
|
+
return this.nodes.size;
|
|
7100
|
+
}
|
|
7101
|
+
values() {
|
|
7102
|
+
return this.nodes.values();
|
|
7103
|
+
}
|
|
7104
|
+
async execute(concurrency, fn, cancelled = () => false) {
|
|
7105
|
+
let active = 0;
|
|
7106
|
+
const errors = [];
|
|
7107
|
+
return new Promise((resolve4, reject) => {
|
|
7108
|
+
const dispatch = () => {
|
|
7109
|
+
let changed = true;
|
|
7110
|
+
while (changed) {
|
|
7111
|
+
changed = false;
|
|
7112
|
+
for (const node of this.nodes.values()) {
|
|
7113
|
+
if (node.state !== "pending")
|
|
7114
|
+
continue;
|
|
7115
|
+
const hasFailedDep = [...node.dependencies].some((depId) => {
|
|
7116
|
+
const dep = this.nodes.get(depId);
|
|
7117
|
+
return dep && (dep.state === "failed" || dep.state === "skipped");
|
|
7118
|
+
});
|
|
7119
|
+
if (hasFailedDep) {
|
|
7120
|
+
node.state = "skipped";
|
|
7121
|
+
changed = true;
|
|
7122
|
+
this.logger.debug(`Skipped ${node.id}: dependency failed or was skipped`);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
const ready = [];
|
|
7127
|
+
for (const node of this.nodes.values()) {
|
|
7128
|
+
if (node.state !== "pending")
|
|
7129
|
+
continue;
|
|
7130
|
+
const depsReady = [...node.dependencies].every((depId) => {
|
|
7131
|
+
const dep = this.nodes.get(depId);
|
|
7132
|
+
return !dep || dep.state === "completed";
|
|
7133
|
+
});
|
|
7134
|
+
if (depsReady)
|
|
7135
|
+
ready.push(node);
|
|
7136
|
+
}
|
|
7137
|
+
if (!cancelled()) {
|
|
7138
|
+
for (const node of ready) {
|
|
7139
|
+
if (active >= concurrency)
|
|
7140
|
+
break;
|
|
7141
|
+
node.state = "running";
|
|
7142
|
+
active++;
|
|
7143
|
+
fn(node).then(() => {
|
|
7144
|
+
node.state = "completed";
|
|
7145
|
+
}).catch((error) => {
|
|
7146
|
+
node.state = "failed";
|
|
7147
|
+
errors.push({ id: node.id, error });
|
|
7148
|
+
}).finally(() => {
|
|
7149
|
+
active--;
|
|
7150
|
+
dispatch();
|
|
7151
|
+
});
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
if (active === 0) {
|
|
7155
|
+
if (errors.length > 0) {
|
|
7156
|
+
reject(errors[0].error);
|
|
7157
|
+
return;
|
|
7158
|
+
}
|
|
7159
|
+
const stillPending = [...this.nodes.values()].some((n) => n.state === "pending");
|
|
7160
|
+
if (stillPending && !cancelled()) {
|
|
7161
|
+
const pending = [...this.nodes.values()].filter((n) => n.state === "pending").map((n) => n.id);
|
|
7162
|
+
reject(
|
|
7163
|
+
new Error(
|
|
7164
|
+
`Deadlock detected: ${pending.length} node(s) stuck with unresolvable dependencies (${pending.join(", ")})`
|
|
7165
|
+
)
|
|
7166
|
+
);
|
|
7167
|
+
return;
|
|
7168
|
+
}
|
|
7169
|
+
resolve4();
|
|
7170
|
+
}
|
|
7171
|
+
};
|
|
7172
|
+
dispatch();
|
|
7173
|
+
});
|
|
7174
|
+
}
|
|
7175
|
+
};
|
|
7176
|
+
|
|
7177
|
+
// src/analyzer/implicit-delete-deps.ts
|
|
7178
|
+
var IMPLICIT_DELETE_DEPENDENCIES = {
|
|
7179
|
+
// IGW must be deleted AFTER VPCGatewayAttachment
|
|
7180
|
+
"AWS::EC2::InternetGateway": ["AWS::EC2::VPCGatewayAttachment"],
|
|
7181
|
+
// EventBus must be deleted AFTER Rules on that bus
|
|
7182
|
+
"AWS::Events::EventBus": ["AWS::Events::Rule"],
|
|
7183
|
+
// Athena workgroup must be deleted AFTER its named queries
|
|
7184
|
+
"AWS::Athena::WorkGroup": ["AWS::Athena::NamedQuery"],
|
|
7185
|
+
// CloudFront managed-policy-style resources must be deleted AFTER
|
|
7186
|
+
// any Distribution that references them
|
|
7187
|
+
"AWS::CloudFront::ResponseHeadersPolicy": ["AWS::CloudFront::Distribution"],
|
|
7188
|
+
"AWS::CloudFront::CachePolicy": ["AWS::CloudFront::Distribution"],
|
|
7189
|
+
"AWS::CloudFront::OriginAccessControl": ["AWS::CloudFront::Distribution"],
|
|
7190
|
+
// VPC must be deleted AFTER all VPC-dependent resources
|
|
7191
|
+
"AWS::EC2::VPC": [
|
|
7192
|
+
"AWS::EC2::Subnet",
|
|
7193
|
+
"AWS::EC2::SecurityGroup",
|
|
7194
|
+
"AWS::EC2::InternetGateway",
|
|
7195
|
+
"AWS::EC2::EgressOnlyInternetGateway",
|
|
7196
|
+
"AWS::EC2::VPCGatewayAttachment",
|
|
7197
|
+
"AWS::EC2::RouteTable"
|
|
7198
|
+
],
|
|
7199
|
+
// Subnet must be deleted AFTER any Lambda that may still hold an ENI
|
|
7200
|
+
// in it. Lambda DELETE returns immediately but the ENI is detached
|
|
7201
|
+
// asynchronously by AWS, so deleting the Subnet first races the detach
|
|
7202
|
+
// and yields "DependencyViolation".
|
|
7203
|
+
"AWS::EC2::Subnet": ["AWS::EC2::SubnetRouteTableAssociation", "AWS::Lambda::Function"],
|
|
7204
|
+
// RouteTable must be deleted AFTER Route and Association
|
|
7205
|
+
"AWS::EC2::RouteTable": ["AWS::EC2::Route", "AWS::EC2::SubnetRouteTableAssociation"],
|
|
7206
|
+
// SecurityGroup must be deleted AFTER any Lambda whose ENI is bound
|
|
7207
|
+
// to it (same ENI-detach race as Subnet above).
|
|
7208
|
+
"AWS::EC2::SecurityGroup": [
|
|
7209
|
+
"AWS::EC2::SecurityGroupIngress",
|
|
7210
|
+
"AWS::EC2::SecurityGroupEgress",
|
|
7211
|
+
"AWS::Lambda::Function"
|
|
7212
|
+
]
|
|
7213
|
+
};
|
|
7214
|
+
|
|
6996
7215
|
// src/deployment/deploy-engine.ts
|
|
6997
|
-
import pLimit from "p-limit";
|
|
6998
7216
|
var InterruptedError = class extends Error {
|
|
6999
7217
|
constructor() {
|
|
7000
7218
|
super("Deployment interrupted by user (Ctrl+C)");
|
|
7001
7219
|
this.name = "InterruptedError";
|
|
7002
7220
|
}
|
|
7003
7221
|
};
|
|
7004
|
-
var DeployEngine = class
|
|
7222
|
+
var DeployEngine = class {
|
|
7005
7223
|
constructor(stateBackend, lockManager, dagBuilder, diffCalculator, providerRegistry, options = {}, stackRegion) {
|
|
7006
7224
|
this.stateBackend = stateBackend;
|
|
7007
7225
|
this.lockManager = lockManager;
|
|
@@ -7127,6 +7345,7 @@ var DeployEngine = class _DeployEngine {
|
|
|
7127
7345
|
template,
|
|
7128
7346
|
currentState,
|
|
7129
7347
|
changes,
|
|
7348
|
+
dag,
|
|
7130
7349
|
executionLevels,
|
|
7131
7350
|
stackName,
|
|
7132
7351
|
parameterValues,
|
|
@@ -7159,13 +7378,16 @@ var DeployEngine = class _DeployEngine {
|
|
|
7159
7378
|
}
|
|
7160
7379
|
}
|
|
7161
7380
|
/**
|
|
7162
|
-
* Execute deployment by processing resources
|
|
7381
|
+
* Execute deployment by processing resources via event-driven DAG dispatch.
|
|
7163
7382
|
*
|
|
7164
|
-
*
|
|
7165
|
-
*
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7383
|
+
* - CREATE/UPDATE follow forward dependency order (a node starts as soon as
|
|
7384
|
+
* ALL of its dependencies are completed — does not wait for unrelated
|
|
7385
|
+
* siblings in the same "level")
|
|
7386
|
+
* - DELETE follows reverse dependency order (a node starts as soon as all
|
|
7387
|
+
* resources that depend ON it have finished deleting)
|
|
7388
|
+
*/
|
|
7389
|
+
async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
|
|
7390
|
+
const concurrency = this.options.concurrency;
|
|
7169
7391
|
const newResources = { ...currentState.resources };
|
|
7170
7392
|
const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
|
|
7171
7393
|
const completedOperations = [];
|
|
@@ -7196,32 +7418,36 @@ var DeployEngine = class _DeployEngine {
|
|
|
7196
7418
|
Array.from(changes.entries()).filter(([_, change]) => change.changeType === "DELETE").map(([logicalId]) => logicalId)
|
|
7197
7419
|
);
|
|
7198
7420
|
try {
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
}
|
|
7203
|
-
const levelNodes = executionLevels[levelIndex];
|
|
7204
|
-
if (!levelNodes)
|
|
7421
|
+
const createUpdateIds = [];
|
|
7422
|
+
for (const [id, change] of changes.entries()) {
|
|
7423
|
+
if (deleteChanges.has(id))
|
|
7205
7424
|
continue;
|
|
7206
|
-
|
|
7207
|
-
if (deleteChanges.has(id))
|
|
7208
|
-
return false;
|
|
7209
|
-
const change = changes.get(id);
|
|
7210
|
-
return !!change && change.changeType !== "NO_CHANGE";
|
|
7211
|
-
});
|
|
7212
|
-
if (level.length === 0)
|
|
7425
|
+
if (change.changeType === "NO_CHANGE")
|
|
7213
7426
|
continue;
|
|
7427
|
+
createUpdateIds.push(id);
|
|
7428
|
+
}
|
|
7429
|
+
if (createUpdateIds.length > 0) {
|
|
7214
7430
|
this.logger.info(
|
|
7215
|
-
`
|
|
7431
|
+
`Deploying ${createUpdateIds.length} resource(s) (DAG: ${executionLevels.length} levels, max parallel: ${concurrency})`
|
|
7216
7432
|
);
|
|
7217
|
-
const
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7433
|
+
const createUpdateExecutor = new DagExecutor();
|
|
7434
|
+
const provisionable = new Set(createUpdateIds);
|
|
7435
|
+
for (const id of createUpdateIds) {
|
|
7436
|
+
const allDeps = this.dagBuilder.getDirectDependencies(dag, id);
|
|
7437
|
+
const deps = new Set(allDeps.filter((d) => provisionable.has(d)));
|
|
7438
|
+
createUpdateExecutor.add({
|
|
7439
|
+
id,
|
|
7440
|
+
dependencies: deps,
|
|
7441
|
+
state: "pending",
|
|
7442
|
+
data: changes.get(id)
|
|
7443
|
+
});
|
|
7444
|
+
}
|
|
7445
|
+
try {
|
|
7446
|
+
await createUpdateExecutor.execute(
|
|
7447
|
+
concurrency,
|
|
7448
|
+
async (node) => {
|
|
7449
|
+
const logicalId = node.id;
|
|
7450
|
+
const change = node.data;
|
|
7225
7451
|
const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
|
|
7226
7452
|
try {
|
|
7227
7453
|
await this.provisionResource(
|
|
@@ -7248,30 +7474,36 @@ var DeployEngine = class _DeployEngine {
|
|
|
7248
7474
|
properties: newResources[logicalId]?.properties
|
|
7249
7475
|
});
|
|
7250
7476
|
saveStateAfterResource(logicalId);
|
|
7251
|
-
}
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7477
|
+
},
|
|
7478
|
+
() => this.interrupted
|
|
7479
|
+
);
|
|
7480
|
+
} finally {
|
|
7481
|
+
await saveChain;
|
|
7482
|
+
}
|
|
7483
|
+
if (this.interrupted && this.hasPending(createUpdateExecutor)) {
|
|
7484
|
+
throw new InterruptedError();
|
|
7258
7485
|
}
|
|
7259
7486
|
}
|
|
7260
7487
|
if (deleteChanges.size > 0) {
|
|
7261
7488
|
this.logger.info(`Deleting ${deleteChanges.size} resource(s)`);
|
|
7262
|
-
const
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7489
|
+
const deleteDeps = this.buildDeletionDependencies(deleteChanges, currentState);
|
|
7490
|
+
const deleteExecutor = new DagExecutor();
|
|
7491
|
+
for (const id of deleteChanges) {
|
|
7492
|
+
deleteExecutor.add({
|
|
7493
|
+
id,
|
|
7494
|
+
dependencies: deleteDeps.get(id) ?? /* @__PURE__ */ new Set(),
|
|
7495
|
+
state: "pending",
|
|
7496
|
+
data: changes.get(id)
|
|
7497
|
+
});
|
|
7498
|
+
}
|
|
7499
|
+
try {
|
|
7500
|
+
await deleteExecutor.execute(
|
|
7501
|
+
concurrency,
|
|
7502
|
+
async (node) => {
|
|
7503
|
+
const logicalId = node.id;
|
|
7504
|
+
const change = node.data;
|
|
7505
|
+
const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
|
|
7506
|
+
try {
|
|
7275
7507
|
await this.provisionResource(
|
|
7276
7508
|
logicalId,
|
|
7277
7509
|
change,
|
|
@@ -7283,23 +7515,25 @@ var DeployEngine = class _DeployEngine {
|
|
|
7283
7515
|
actualCounts,
|
|
7284
7516
|
progress
|
|
7285
7517
|
);
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7518
|
+
} catch (provisionError) {
|
|
7519
|
+
this.interrupted = true;
|
|
7520
|
+
throw provisionError;
|
|
7521
|
+
}
|
|
7522
|
+
completedOperations.push({
|
|
7523
|
+
logicalId,
|
|
7524
|
+
changeType: "DELETE",
|
|
7525
|
+
resourceType: change.resourceType,
|
|
7526
|
+
previousState
|
|
7527
|
+
});
|
|
7528
|
+
saveStateAfterResource(logicalId);
|
|
7529
|
+
},
|
|
7530
|
+
() => this.interrupted
|
|
7295
7531
|
);
|
|
7532
|
+
} finally {
|
|
7296
7533
|
await saveChain;
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
);
|
|
7300
|
-
if (deleteFailures.length > 0) {
|
|
7301
|
-
throw deleteFailures[0].reason;
|
|
7302
|
-
}
|
|
7534
|
+
}
|
|
7535
|
+
if (this.interrupted && this.hasPending(deleteExecutor)) {
|
|
7536
|
+
throw new InterruptedError();
|
|
7303
7537
|
}
|
|
7304
7538
|
}
|
|
7305
7539
|
} catch (error) {
|
|
@@ -7393,12 +7627,12 @@ var DeployEngine = class _DeployEngine {
|
|
|
7393
7627
|
* - UPDATE → update back to previous properties
|
|
7394
7628
|
* - DELETE → cannot rollback (resource already deleted), log warning
|
|
7395
7629
|
*
|
|
7396
|
-
* Resources
|
|
7397
|
-
* (e.g., IAM Policy depends on IAM Role). When rolling back
|
|
7398
|
-
* dependent resources must be deleted before their
|
|
7399
|
-
* sorts CREATE rollback operations using dependency
|
|
7400
|
-
* then processes UPDATE/DELETE rollbacks, and finally
|
|
7401
|
-
* rollback deletions.
|
|
7630
|
+
* Resources completed concurrently in the dispatcher may have dependencies
|
|
7631
|
+
* between them (e.g., IAM Policy depends on IAM Role). When rolling back
|
|
7632
|
+
* CREATEs (deleting), dependent resources must be deleted before their
|
|
7633
|
+
* dependencies. This method sorts CREATE rollback operations using dependency
|
|
7634
|
+
* information from state, then processes UPDATE/DELETE rollbacks, and finally
|
|
7635
|
+
* processes sorted CREATE rollback deletions.
|
|
7402
7636
|
*/
|
|
7403
7637
|
async performRollback(completedOperations, stateResources, _stackName) {
|
|
7404
7638
|
if (completedOperations.length === 0) {
|
|
@@ -7431,7 +7665,7 @@ var DeployEngine = class _DeployEngine {
|
|
|
7431
7665
|
* Sort CREATE rollback operations so that resources depending on others
|
|
7432
7666
|
* are deleted first (reverse dependency order).
|
|
7433
7667
|
*
|
|
7434
|
-
* Uses state dependencies to
|
|
7668
|
+
* Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
|
|
7435
7669
|
*/
|
|
7436
7670
|
sortRollbackCreates(createOps, stateResources) {
|
|
7437
7671
|
const opMap = /* @__PURE__ */ new Map();
|
|
@@ -7822,48 +8056,34 @@ var DeployEngine = class _DeployEngine {
|
|
|
7822
8056
|
const deps = parser.extractDependencies(resource);
|
|
7823
8057
|
return deps.size > 0 ? [...deps] : void 0;
|
|
7824
8058
|
}
|
|
8059
|
+
// Type-based implicit deletion ordering rules are defined in
|
|
8060
|
+
// src/analyzer/implicit-delete-deps.ts so the deploy DELETE phase and the
|
|
8061
|
+
// standalone destroy command apply the same rules.
|
|
7825
8062
|
/**
|
|
7826
|
-
*
|
|
8063
|
+
* Build a per-resource map of "must be deleted before me" dependencies for
|
|
8064
|
+
* the DELETE phase, derived from state-recorded dependencies plus implicit
|
|
8065
|
+
* type-based ordering rules.
|
|
7827
8066
|
*
|
|
7828
|
-
*
|
|
7829
|
-
*
|
|
7830
|
-
*
|
|
7831
|
-
|
|
7832
|
-
* because AWS won't let you delete an IGW while it's still attached to a VPC.
|
|
7833
|
-
*/
|
|
7834
|
-
static IMPLICIT_DELETE_DEPENDENCIES = {
|
|
7835
|
-
// IGW must be deleted AFTER VPCGatewayAttachment
|
|
7836
|
-
"AWS::EC2::InternetGateway": ["AWS::EC2::VPCGatewayAttachment"],
|
|
7837
|
-
// EventBus must be deleted AFTER Rules on that bus
|
|
7838
|
-
"AWS::Events::EventBus": ["AWS::Events::Rule"],
|
|
7839
|
-
// VPC must be deleted AFTER all VPC-dependent resources
|
|
7840
|
-
"AWS::EC2::VPC": [
|
|
7841
|
-
"AWS::EC2::Subnet",
|
|
7842
|
-
"AWS::EC2::SecurityGroup",
|
|
7843
|
-
"AWS::EC2::InternetGateway",
|
|
7844
|
-
"AWS::EC2::EgressOnlyInternetGateway",
|
|
7845
|
-
"AWS::EC2::VPCGatewayAttachment",
|
|
7846
|
-
"AWS::EC2::RouteTable"
|
|
7847
|
-
],
|
|
7848
|
-
// Subnet must be deleted AFTER RouteTableAssociation
|
|
7849
|
-
"AWS::EC2::Subnet": ["AWS::EC2::SubnetRouteTableAssociation"],
|
|
7850
|
-
// RouteTable must be deleted AFTER Route and Association
|
|
7851
|
-
"AWS::EC2::RouteTable": ["AWS::EC2::Route", "AWS::EC2::SubnetRouteTableAssociation"],
|
|
7852
|
-
// SecurityGroup must be deleted AFTER SecurityGroupIngress/Egress
|
|
7853
|
-
"AWS::EC2::SecurityGroup": ["AWS::EC2::SecurityGroupIngress", "AWS::EC2::SecurityGroupEgress"]
|
|
7854
|
-
};
|
|
8067
|
+
* For a resource X, the returned set contains every resource Y such that Y
|
|
8068
|
+
* must finish deleting before X starts — i.e., Y depends on X (or is otherwise
|
|
8069
|
+
* required to vanish first per implicit type rules).
|
|
8070
|
+
*/
|
|
7855
8071
|
/**
|
|
7856
|
-
*
|
|
7857
|
-
*
|
|
8072
|
+
* Returns true if the executor still has un-started pending nodes —
|
|
8073
|
+
* used to distinguish "SIGINT cancelled real work" from "SIGINT landed
|
|
8074
|
+
* after all nodes already completed" (the latter should not error).
|
|
7858
8075
|
*/
|
|
7859
|
-
|
|
8076
|
+
hasPending(executor) {
|
|
8077
|
+
for (const node of executor.values()) {
|
|
8078
|
+
if (node.state === "pending")
|
|
8079
|
+
return true;
|
|
8080
|
+
}
|
|
8081
|
+
return false;
|
|
8082
|
+
}
|
|
8083
|
+
buildDeletionDependencies(deleteIds, state) {
|
|
7860
8084
|
const dependedBy = /* @__PURE__ */ new Map();
|
|
7861
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
7862
8085
|
for (const id of deleteIds) {
|
|
7863
|
-
|
|
7864
|
-
dependedBy.set(id, /* @__PURE__ */ new Set());
|
|
7865
|
-
if (!inDegree.has(id))
|
|
7866
|
-
inDegree.set(id, 0);
|
|
8086
|
+
dependedBy.set(id, /* @__PURE__ */ new Set());
|
|
7867
8087
|
}
|
|
7868
8088
|
for (const id of deleteIds) {
|
|
7869
8089
|
const resource = state.resources[id];
|
|
@@ -7872,38 +8092,11 @@ var DeployEngine = class _DeployEngine {
|
|
|
7872
8092
|
for (const dep of resource.dependencies) {
|
|
7873
8093
|
if (!deleteIds.has(dep))
|
|
7874
8094
|
continue;
|
|
7875
|
-
if (!dependedBy.has(dep))
|
|
7876
|
-
dependedBy.set(dep, /* @__PURE__ */ new Set());
|
|
7877
8095
|
dependedBy.get(dep).add(id);
|
|
7878
|
-
inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
|
|
7879
8096
|
}
|
|
7880
8097
|
}
|
|
7881
8098
|
this.addImplicitDeleteDependencies(deleteIds, state, dependedBy);
|
|
7882
|
-
|
|
7883
|
-
let remaining = new Set(deleteIds);
|
|
7884
|
-
while (remaining.size > 0) {
|
|
7885
|
-
const level = [];
|
|
7886
|
-
for (const id of remaining) {
|
|
7887
|
-
const dependents = dependedBy.get(id);
|
|
7888
|
-
const hasPendingDependents = dependents ? [...dependents].some((d) => remaining.has(d)) : false;
|
|
7889
|
-
if (!hasPendingDependents) {
|
|
7890
|
-
level.push(id);
|
|
7891
|
-
}
|
|
7892
|
-
}
|
|
7893
|
-
if (level.length === 0) {
|
|
7894
|
-
this.logger.warn(
|
|
7895
|
-
`Circular dependency detected in delete order, deleting remaining ${remaining.size} resources`
|
|
7896
|
-
);
|
|
7897
|
-
levels.push([...remaining]);
|
|
7898
|
-
break;
|
|
7899
|
-
}
|
|
7900
|
-
levels.push(level);
|
|
7901
|
-
remaining = new Set([...remaining].filter((id) => !level.includes(id)));
|
|
7902
|
-
}
|
|
7903
|
-
this.logger.debug(
|
|
7904
|
-
`Delete order: ${levels.length} levels - ${levels.map((l, i) => `L${i + 1}(${l.length})`).join(", ")}`
|
|
7905
|
-
);
|
|
7906
|
-
return levels;
|
|
8099
|
+
return dependedBy;
|
|
7907
8100
|
}
|
|
7908
8101
|
/**
|
|
7909
8102
|
* Add implicit delete dependency edges based on resource type relationships.
|
|
@@ -7931,7 +8124,7 @@ var DeployEngine = class _DeployEngine {
|
|
|
7931
8124
|
const resource = state.resources[id];
|
|
7932
8125
|
if (!resource)
|
|
7933
8126
|
continue;
|
|
7934
|
-
const mustDeleteAfter =
|
|
8127
|
+
const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
|
|
7935
8128
|
if (!mustDeleteAfter)
|
|
7936
8129
|
continue;
|
|
7937
8130
|
for (const depType of mustDeleteAfter) {
|