@go-to-k/cdkd 0.1.0 → 0.2.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 +198 -108
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.2.0.tgz +0 -0
- package/dist/index.js +197 -107
- package/dist/index.js.map +4 -4
- package/package.json +3 -2
- package/dist/go-to-k-cdkd-0.1.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
|
|
|
78
78
|
│ cdkd Engine │
|
|
79
79
|
│ - DAG Analysis │ Dependency graph construction
|
|
80
80
|
│ - Diff Calc │ Compare with existing resources
|
|
81
|
-
│ - Parallel Exec │
|
|
81
|
+
│ - Parallel Exec │ Event-driven dispatch
|
|
82
82
|
└────────┬────────┘
|
|
83
83
|
│
|
|
84
84
|
┌────┴────┐
|
|
@@ -134,10 +134,11 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
|
|
|
134
134
|
│ ├── Build DAG from template (Ref/Fn::GetAtt/DependsOn)
|
|
135
135
|
│ ├── Calculate diff (CREATE/UPDATE/DELETE)
|
|
136
136
|
│ ├── Resolve intrinsic functions (Ref, Fn::Sub, Fn::Join, etc.)
|
|
137
|
-
│ ├── Execute
|
|
137
|
+
│ ├── Execute via event-driven DAG dispatch (a resource starts as
|
|
138
|
+
│ │ soon as ALL of its own deps complete; no level barrier):
|
|
138
139
|
│ │ ├── SDK Providers (direct API calls, preferred)
|
|
139
140
|
│ │ └── Cloud Control API (fallback, async polling)
|
|
140
|
-
│ ├── Save state after each
|
|
141
|
+
│ ├── Save state after each successful resource (partial state save)
|
|
141
142
|
│ └── Release lock
|
|
142
143
|
└── synth does NOT publish assets or deploy (deploy only)
|
|
143
144
|
```
|
|
@@ -459,7 +460,7 @@ LambdaStack
|
|
|
459
460
|
✓ Deployed LambdaStack (4 resources, 7.2s)
|
|
460
461
|
```
|
|
461
462
|
|
|
462
|
-
Resources
|
|
463
|
+
Resources are dispatched as soon as their own dependencies complete (event-driven DAG). ServiceRole and Table run in parallel; DefaultPolicy starts the moment ServiceRole is done — without waiting for Table — and Handler starts the moment DefaultPolicy is done.
|
|
463
464
|
|
|
464
465
|
## Architecture
|
|
465
466
|
|
package/dist/cli.js
CHANGED
|
@@ -25053,8 +25053,96 @@ function registerAllProviders(registry) {
|
|
|
25053
25053
|
registry.register("AWS::S3Tables::Table", s3TablesProvider);
|
|
25054
25054
|
}
|
|
25055
25055
|
|
|
25056
|
+
// src/deployment/dag-executor.ts
|
|
25057
|
+
var DagExecutor = class {
|
|
25058
|
+
nodes = /* @__PURE__ */ new Map();
|
|
25059
|
+
logger = getLogger().child("DagExecutor");
|
|
25060
|
+
add(node) {
|
|
25061
|
+
this.nodes.set(node.id, node);
|
|
25062
|
+
}
|
|
25063
|
+
has(id) {
|
|
25064
|
+
return this.nodes.has(id);
|
|
25065
|
+
}
|
|
25066
|
+
size() {
|
|
25067
|
+
return this.nodes.size;
|
|
25068
|
+
}
|
|
25069
|
+
values() {
|
|
25070
|
+
return this.nodes.values();
|
|
25071
|
+
}
|
|
25072
|
+
async execute(concurrency, fn, cancelled = () => false) {
|
|
25073
|
+
let active = 0;
|
|
25074
|
+
const errors = [];
|
|
25075
|
+
return new Promise((resolve4, reject) => {
|
|
25076
|
+
const dispatch = () => {
|
|
25077
|
+
let changed = true;
|
|
25078
|
+
while (changed) {
|
|
25079
|
+
changed = false;
|
|
25080
|
+
for (const node of this.nodes.values()) {
|
|
25081
|
+
if (node.state !== "pending")
|
|
25082
|
+
continue;
|
|
25083
|
+
const hasFailedDep = [...node.dependencies].some((depId) => {
|
|
25084
|
+
const dep = this.nodes.get(depId);
|
|
25085
|
+
return dep && (dep.state === "failed" || dep.state === "skipped");
|
|
25086
|
+
});
|
|
25087
|
+
if (hasFailedDep) {
|
|
25088
|
+
node.state = "skipped";
|
|
25089
|
+
changed = true;
|
|
25090
|
+
this.logger.debug(`Skipped ${node.id}: dependency failed or was skipped`);
|
|
25091
|
+
}
|
|
25092
|
+
}
|
|
25093
|
+
}
|
|
25094
|
+
const ready = [];
|
|
25095
|
+
for (const node of this.nodes.values()) {
|
|
25096
|
+
if (node.state !== "pending")
|
|
25097
|
+
continue;
|
|
25098
|
+
const depsReady = [...node.dependencies].every((depId) => {
|
|
25099
|
+
const dep = this.nodes.get(depId);
|
|
25100
|
+
return !dep || dep.state === "completed";
|
|
25101
|
+
});
|
|
25102
|
+
if (depsReady)
|
|
25103
|
+
ready.push(node);
|
|
25104
|
+
}
|
|
25105
|
+
if (!cancelled()) {
|
|
25106
|
+
for (const node of ready) {
|
|
25107
|
+
if (active >= concurrency)
|
|
25108
|
+
break;
|
|
25109
|
+
node.state = "running";
|
|
25110
|
+
active++;
|
|
25111
|
+
fn(node).then(() => {
|
|
25112
|
+
node.state = "completed";
|
|
25113
|
+
}).catch((error) => {
|
|
25114
|
+
node.state = "failed";
|
|
25115
|
+
errors.push({ id: node.id, error });
|
|
25116
|
+
}).finally(() => {
|
|
25117
|
+
active--;
|
|
25118
|
+
dispatch();
|
|
25119
|
+
});
|
|
25120
|
+
}
|
|
25121
|
+
}
|
|
25122
|
+
if (active === 0) {
|
|
25123
|
+
if (errors.length > 0) {
|
|
25124
|
+
reject(errors[0].error);
|
|
25125
|
+
return;
|
|
25126
|
+
}
|
|
25127
|
+
const stillPending = [...this.nodes.values()].some((n) => n.state === "pending");
|
|
25128
|
+
if (stillPending && !cancelled()) {
|
|
25129
|
+
const pending = [...this.nodes.values()].filter((n) => n.state === "pending").map((n) => n.id);
|
|
25130
|
+
reject(
|
|
25131
|
+
new Error(
|
|
25132
|
+
`Deadlock detected: ${pending.length} node(s) stuck with unresolvable dependencies (${pending.join(", ")})`
|
|
25133
|
+
)
|
|
25134
|
+
);
|
|
25135
|
+
return;
|
|
25136
|
+
}
|
|
25137
|
+
resolve4();
|
|
25138
|
+
}
|
|
25139
|
+
};
|
|
25140
|
+
dispatch();
|
|
25141
|
+
});
|
|
25142
|
+
}
|
|
25143
|
+
};
|
|
25144
|
+
|
|
25056
25145
|
// src/deployment/deploy-engine.ts
|
|
25057
|
-
import pLimit from "p-limit";
|
|
25058
25146
|
var InterruptedError = class extends Error {
|
|
25059
25147
|
constructor() {
|
|
25060
25148
|
super("Deployment interrupted by user (Ctrl+C)");
|
|
@@ -25187,6 +25275,7 @@ var DeployEngine = class _DeployEngine {
|
|
|
25187
25275
|
template,
|
|
25188
25276
|
currentState,
|
|
25189
25277
|
changes,
|
|
25278
|
+
dag,
|
|
25190
25279
|
executionLevels,
|
|
25191
25280
|
stackName,
|
|
25192
25281
|
parameterValues,
|
|
@@ -25219,13 +25308,16 @@ var DeployEngine = class _DeployEngine {
|
|
|
25219
25308
|
}
|
|
25220
25309
|
}
|
|
25221
25310
|
/**
|
|
25222
|
-
* Execute deployment by processing resources
|
|
25311
|
+
* Execute deployment by processing resources via event-driven DAG dispatch.
|
|
25223
25312
|
*
|
|
25224
|
-
*
|
|
25225
|
-
*
|
|
25226
|
-
|
|
25227
|
-
|
|
25228
|
-
|
|
25313
|
+
* - CREATE/UPDATE follow forward dependency order (a node starts as soon as
|
|
25314
|
+
* ALL of its dependencies are completed — does not wait for unrelated
|
|
25315
|
+
* siblings in the same "level")
|
|
25316
|
+
* - DELETE follows reverse dependency order (a node starts as soon as all
|
|
25317
|
+
* resources that depend ON it have finished deleting)
|
|
25318
|
+
*/
|
|
25319
|
+
async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
|
|
25320
|
+
const concurrency = this.options.concurrency;
|
|
25229
25321
|
const newResources = { ...currentState.resources };
|
|
25230
25322
|
const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
|
|
25231
25323
|
const completedOperations = [];
|
|
@@ -25256,32 +25348,36 @@ var DeployEngine = class _DeployEngine {
|
|
|
25256
25348
|
Array.from(changes.entries()).filter(([_, change]) => change.changeType === "DELETE").map(([logicalId]) => logicalId)
|
|
25257
25349
|
);
|
|
25258
25350
|
try {
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
}
|
|
25263
|
-
const levelNodes = executionLevels[levelIndex];
|
|
25264
|
-
if (!levelNodes)
|
|
25351
|
+
const createUpdateIds = [];
|
|
25352
|
+
for (const [id, change] of changes.entries()) {
|
|
25353
|
+
if (deleteChanges.has(id))
|
|
25265
25354
|
continue;
|
|
25266
|
-
|
|
25267
|
-
if (deleteChanges.has(id))
|
|
25268
|
-
return false;
|
|
25269
|
-
const change = changes.get(id);
|
|
25270
|
-
return !!change && change.changeType !== "NO_CHANGE";
|
|
25271
|
-
});
|
|
25272
|
-
if (level.length === 0)
|
|
25355
|
+
if (change.changeType === "NO_CHANGE")
|
|
25273
25356
|
continue;
|
|
25357
|
+
createUpdateIds.push(id);
|
|
25358
|
+
}
|
|
25359
|
+
if (createUpdateIds.length > 0) {
|
|
25274
25360
|
this.logger.info(
|
|
25275
|
-
`
|
|
25361
|
+
`Deploying ${createUpdateIds.length} resource(s) (DAG: ${executionLevels.length} levels, max parallel: ${concurrency})`
|
|
25276
25362
|
);
|
|
25277
|
-
const
|
|
25278
|
-
|
|
25279
|
-
|
|
25280
|
-
|
|
25281
|
-
|
|
25282
|
-
|
|
25283
|
-
|
|
25284
|
-
|
|
25363
|
+
const createUpdateExecutor = new DagExecutor();
|
|
25364
|
+
const provisionable = new Set(createUpdateIds);
|
|
25365
|
+
for (const id of createUpdateIds) {
|
|
25366
|
+
const allDeps = this.dagBuilder.getDirectDependencies(dag, id);
|
|
25367
|
+
const deps = new Set(allDeps.filter((d) => provisionable.has(d)));
|
|
25368
|
+
createUpdateExecutor.add({
|
|
25369
|
+
id,
|
|
25370
|
+
dependencies: deps,
|
|
25371
|
+
state: "pending",
|
|
25372
|
+
data: changes.get(id)
|
|
25373
|
+
});
|
|
25374
|
+
}
|
|
25375
|
+
try {
|
|
25376
|
+
await createUpdateExecutor.execute(
|
|
25377
|
+
concurrency,
|
|
25378
|
+
async (node) => {
|
|
25379
|
+
const logicalId = node.id;
|
|
25380
|
+
const change = node.data;
|
|
25285
25381
|
const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
|
|
25286
25382
|
try {
|
|
25287
25383
|
await this.provisionResource(
|
|
@@ -25308,30 +25404,36 @@ var DeployEngine = class _DeployEngine {
|
|
|
25308
25404
|
properties: newResources[logicalId]?.properties
|
|
25309
25405
|
});
|
|
25310
25406
|
saveStateAfterResource(logicalId);
|
|
25311
|
-
}
|
|
25312
|
-
|
|
25313
|
-
|
|
25314
|
-
|
|
25315
|
-
|
|
25316
|
-
|
|
25317
|
-
|
|
25407
|
+
},
|
|
25408
|
+
() => this.interrupted
|
|
25409
|
+
);
|
|
25410
|
+
} finally {
|
|
25411
|
+
await saveChain;
|
|
25412
|
+
}
|
|
25413
|
+
if (this.interrupted && this.hasPending(createUpdateExecutor)) {
|
|
25414
|
+
throw new InterruptedError();
|
|
25318
25415
|
}
|
|
25319
25416
|
}
|
|
25320
25417
|
if (deleteChanges.size > 0) {
|
|
25321
25418
|
this.logger.info(`Deleting ${deleteChanges.size} resource(s)`);
|
|
25322
|
-
const
|
|
25323
|
-
|
|
25324
|
-
|
|
25325
|
-
|
|
25326
|
-
|
|
25327
|
-
|
|
25328
|
-
|
|
25329
|
-
|
|
25330
|
-
|
|
25331
|
-
|
|
25332
|
-
|
|
25333
|
-
|
|
25334
|
-
|
|
25419
|
+
const deleteDeps = this.buildDeletionDependencies(deleteChanges, currentState);
|
|
25420
|
+
const deleteExecutor = new DagExecutor();
|
|
25421
|
+
for (const id of deleteChanges) {
|
|
25422
|
+
deleteExecutor.add({
|
|
25423
|
+
id,
|
|
25424
|
+
dependencies: deleteDeps.get(id) ?? /* @__PURE__ */ new Set(),
|
|
25425
|
+
state: "pending",
|
|
25426
|
+
data: changes.get(id)
|
|
25427
|
+
});
|
|
25428
|
+
}
|
|
25429
|
+
try {
|
|
25430
|
+
await deleteExecutor.execute(
|
|
25431
|
+
concurrency,
|
|
25432
|
+
async (node) => {
|
|
25433
|
+
const logicalId = node.id;
|
|
25434
|
+
const change = node.data;
|
|
25435
|
+
const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
|
|
25436
|
+
try {
|
|
25335
25437
|
await this.provisionResource(
|
|
25336
25438
|
logicalId,
|
|
25337
25439
|
change,
|
|
@@ -25343,23 +25445,25 @@ var DeployEngine = class _DeployEngine {
|
|
|
25343
25445
|
actualCounts,
|
|
25344
25446
|
progress
|
|
25345
25447
|
);
|
|
25346
|
-
|
|
25347
|
-
|
|
25348
|
-
|
|
25349
|
-
|
|
25350
|
-
|
|
25351
|
-
|
|
25352
|
-
|
|
25353
|
-
|
|
25354
|
-
|
|
25448
|
+
} catch (provisionError) {
|
|
25449
|
+
this.interrupted = true;
|
|
25450
|
+
throw provisionError;
|
|
25451
|
+
}
|
|
25452
|
+
completedOperations.push({
|
|
25453
|
+
logicalId,
|
|
25454
|
+
changeType: "DELETE",
|
|
25455
|
+
resourceType: change.resourceType,
|
|
25456
|
+
previousState
|
|
25457
|
+
});
|
|
25458
|
+
saveStateAfterResource(logicalId);
|
|
25459
|
+
},
|
|
25460
|
+
() => this.interrupted
|
|
25355
25461
|
);
|
|
25462
|
+
} finally {
|
|
25356
25463
|
await saveChain;
|
|
25357
|
-
|
|
25358
|
-
|
|
25359
|
-
);
|
|
25360
|
-
if (deleteFailures.length > 0) {
|
|
25361
|
-
throw deleteFailures[0].reason;
|
|
25362
|
-
}
|
|
25464
|
+
}
|
|
25465
|
+
if (this.interrupted && this.hasPending(deleteExecutor)) {
|
|
25466
|
+
throw new InterruptedError();
|
|
25363
25467
|
}
|
|
25364
25468
|
}
|
|
25365
25469
|
} catch (error) {
|
|
@@ -25453,12 +25557,12 @@ var DeployEngine = class _DeployEngine {
|
|
|
25453
25557
|
* - UPDATE → update back to previous properties
|
|
25454
25558
|
* - DELETE → cannot rollback (resource already deleted), log warning
|
|
25455
25559
|
*
|
|
25456
|
-
* Resources
|
|
25457
|
-
* (e.g., IAM Policy depends on IAM Role). When rolling back
|
|
25458
|
-
* dependent resources must be deleted before their
|
|
25459
|
-
* sorts CREATE rollback operations using dependency
|
|
25460
|
-
* then processes UPDATE/DELETE rollbacks, and finally
|
|
25461
|
-
* rollback deletions.
|
|
25560
|
+
* Resources completed concurrently in the dispatcher may have dependencies
|
|
25561
|
+
* between them (e.g., IAM Policy depends on IAM Role). When rolling back
|
|
25562
|
+
* CREATEs (deleting), dependent resources must be deleted before their
|
|
25563
|
+
* dependencies. This method sorts CREATE rollback operations using dependency
|
|
25564
|
+
* information from state, then processes UPDATE/DELETE rollbacks, and finally
|
|
25565
|
+
* processes sorted CREATE rollback deletions.
|
|
25462
25566
|
*/
|
|
25463
25567
|
async performRollback(completedOperations, stateResources, _stackName) {
|
|
25464
25568
|
if (completedOperations.length === 0) {
|
|
@@ -25491,7 +25595,7 @@ var DeployEngine = class _DeployEngine {
|
|
|
25491
25595
|
* Sort CREATE rollback operations so that resources depending on others
|
|
25492
25596
|
* are deleted first (reverse dependency order).
|
|
25493
25597
|
*
|
|
25494
|
-
* Uses state dependencies to
|
|
25598
|
+
* Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
|
|
25495
25599
|
*/
|
|
25496
25600
|
sortRollbackCreates(createOps, stateResources) {
|
|
25497
25601
|
const opMap = /* @__PURE__ */ new Map();
|
|
@@ -25913,17 +26017,30 @@ var DeployEngine = class _DeployEngine {
|
|
|
25913
26017
|
"AWS::EC2::SecurityGroup": ["AWS::EC2::SecurityGroupIngress", "AWS::EC2::SecurityGroupEgress"]
|
|
25914
26018
|
};
|
|
25915
26019
|
/**
|
|
25916
|
-
* Build
|
|
25917
|
-
*
|
|
26020
|
+
* Build a per-resource map of "must be deleted before me" dependencies for
|
|
26021
|
+
* the DELETE phase, derived from state-recorded dependencies plus implicit
|
|
26022
|
+
* type-based ordering rules.
|
|
26023
|
+
*
|
|
26024
|
+
* For a resource X, the returned set contains every resource Y such that Y
|
|
26025
|
+
* must finish deleting before X starts — i.e., Y depends on X (or is otherwise
|
|
26026
|
+
* required to vanish first per implicit type rules).
|
|
26027
|
+
*/
|
|
26028
|
+
/**
|
|
26029
|
+
* Returns true if the executor still has un-started pending nodes —
|
|
26030
|
+
* used to distinguish "SIGINT cancelled real work" from "SIGINT landed
|
|
26031
|
+
* after all nodes already completed" (the latter should not error).
|
|
25918
26032
|
*/
|
|
25919
|
-
|
|
26033
|
+
hasPending(executor) {
|
|
26034
|
+
for (const node of executor.values()) {
|
|
26035
|
+
if (node.state === "pending")
|
|
26036
|
+
return true;
|
|
26037
|
+
}
|
|
26038
|
+
return false;
|
|
26039
|
+
}
|
|
26040
|
+
buildDeletionDependencies(deleteIds, state) {
|
|
25920
26041
|
const dependedBy = /* @__PURE__ */ new Map();
|
|
25921
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
25922
26042
|
for (const id of deleteIds) {
|
|
25923
|
-
|
|
25924
|
-
dependedBy.set(id, /* @__PURE__ */ new Set());
|
|
25925
|
-
if (!inDegree.has(id))
|
|
25926
|
-
inDegree.set(id, 0);
|
|
26043
|
+
dependedBy.set(id, /* @__PURE__ */ new Set());
|
|
25927
26044
|
}
|
|
25928
26045
|
for (const id of deleteIds) {
|
|
25929
26046
|
const resource = state.resources[id];
|
|
@@ -25932,38 +26049,11 @@ var DeployEngine = class _DeployEngine {
|
|
|
25932
26049
|
for (const dep of resource.dependencies) {
|
|
25933
26050
|
if (!deleteIds.has(dep))
|
|
25934
26051
|
continue;
|
|
25935
|
-
if (!dependedBy.has(dep))
|
|
25936
|
-
dependedBy.set(dep, /* @__PURE__ */ new Set());
|
|
25937
26052
|
dependedBy.get(dep).add(id);
|
|
25938
|
-
inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
|
|
25939
26053
|
}
|
|
25940
26054
|
}
|
|
25941
26055
|
this.addImplicitDeleteDependencies(deleteIds, state, dependedBy);
|
|
25942
|
-
|
|
25943
|
-
let remaining = new Set(deleteIds);
|
|
25944
|
-
while (remaining.size > 0) {
|
|
25945
|
-
const level = [];
|
|
25946
|
-
for (const id of remaining) {
|
|
25947
|
-
const dependents = dependedBy.get(id);
|
|
25948
|
-
const hasPendingDependents = dependents ? [...dependents].some((d) => remaining.has(d)) : false;
|
|
25949
|
-
if (!hasPendingDependents) {
|
|
25950
|
-
level.push(id);
|
|
25951
|
-
}
|
|
25952
|
-
}
|
|
25953
|
-
if (level.length === 0) {
|
|
25954
|
-
this.logger.warn(
|
|
25955
|
-
`Circular dependency detected in delete order, deleting remaining ${remaining.size} resources`
|
|
25956
|
-
);
|
|
25957
|
-
levels.push([...remaining]);
|
|
25958
|
-
break;
|
|
25959
|
-
}
|
|
25960
|
-
levels.push(level);
|
|
25961
|
-
remaining = new Set([...remaining].filter((id) => !level.includes(id)));
|
|
25962
|
-
}
|
|
25963
|
-
this.logger.debug(
|
|
25964
|
-
`Delete order: ${levels.length} levels - ${levels.map((l, i) => `L${i + 1}(${l.length})`).join(", ")}`
|
|
25965
|
-
);
|
|
25966
|
-
return levels;
|
|
26056
|
+
return dependedBy;
|
|
25967
26057
|
}
|
|
25968
26058
|
/**
|
|
25969
26059
|
* Add implicit delete dependency edges based on resource type relationships.
|
|
@@ -27029,7 +27119,7 @@ function reorderArgs(argv) {
|
|
|
27029
27119
|
}
|
|
27030
27120
|
async function main() {
|
|
27031
27121
|
const program = new Command8();
|
|
27032
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
27122
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.2.0");
|
|
27033
27123
|
program.addCommand(createBootstrapCommand());
|
|
27034
27124
|
program.addCommand(createSynthCommand());
|
|
27035
27125
|
program.addCommand(createDeployCommand());
|