@go-to-k/cdkd 0.0.4 → 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.
Binary file
package/dist/index.js CHANGED
@@ -649,7 +649,7 @@ Caused by: ${error.cause.message}`;
649
649
  init_aws_clients();
650
650
 
651
651
  // src/synthesis/synthesizer.ts
652
- import { mkdirSync } from "node:fs";
652
+ import { existsSync as existsSync3, mkdirSync, statSync } from "node:fs";
653
653
  import { resolve as resolve3 } from "node:path";
654
654
  import { GetCallerIdentityCommand, STSClient as STSClient2 } from "@aws-sdk/client-sts";
655
655
 
@@ -1846,6 +1846,14 @@ var Synthesizer = class {
1846
1846
  * 5. Return assembly with stacks
1847
1847
  */
1848
1848
  async synthesize(options) {
1849
+ const appPath = resolve3(options.app);
1850
+ if (existsSync3(appPath) && statSync(appPath).isDirectory()) {
1851
+ this.logger.debug(`Using pre-synthesized cloud assembly at ${appPath}`);
1852
+ const manifest = this.assemblyReader.readManifest(appPath);
1853
+ const stacks = this.assemblyReader.getAllStacks(appPath, manifest);
1854
+ this.logger.debug(`Loaded ${stacks.length} stack(s) from pre-synthesized assembly`);
1855
+ return { manifest, assemblyDir: appPath, stacks };
1856
+ }
1849
1857
  const outputDir = resolve3(options.output || "cdk.out");
1850
1858
  mkdirSync(outputDir, { recursive: true });
1851
1859
  const userCdkJson = loadUserCdkJson();
@@ -1933,7 +1941,7 @@ function setsEqual(a, b) {
1933
1941
  import { readFileSync as readFileSync4 } from "node:fs";
1934
1942
 
1935
1943
  // src/assets/file-asset-publisher.ts
1936
- import { createReadStream, statSync } from "node:fs";
1944
+ import { createReadStream, statSync as statSync2 } from "node:fs";
1937
1945
  import { join as join4, basename } from "node:path";
1938
1946
  import { S3Client as S3Client2, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
1939
1947
  var FileAssetPublisher = class {
@@ -2003,7 +2011,7 @@ var FileAssetPublisher = class {
2003
2011
  * Upload a single file to S3
2004
2012
  */
2005
2013
  async uploadFile(client, filePath, bucket, key) {
2006
- const stat = statSync(filePath);
2014
+ const stat = statSync2(filePath);
2007
2015
  const stream = createReadStream(filePath);
2008
2016
  await client.send(
2009
2017
  new PutObjectCommand({
@@ -2025,7 +2033,7 @@ var FileAssetPublisher = class {
2025
2033
  archive.on("data", (chunk) => chunks.push(chunk));
2026
2034
  archive.on("end", () => resolve4(Buffer.concat(chunks)));
2027
2035
  archive.on("error", reject);
2028
- const stat = statSync(dirPath);
2036
+ const stat = statSync2(dirPath);
2029
2037
  if (stat.isDirectory()) {
2030
2038
  archive.directory(dirPath, false);
2031
2039
  } else {
@@ -6985,8 +6993,96 @@ var IAMRoleProvider = class {
6985
6993
  }
6986
6994
  };
6987
6995
 
6996
+ // src/deployment/dag-executor.ts
6997
+ var DagExecutor = class {
6998
+ nodes = /* @__PURE__ */ new Map();
6999
+ logger = getLogger().child("DagExecutor");
7000
+ add(node) {
7001
+ this.nodes.set(node.id, node);
7002
+ }
7003
+ has(id) {
7004
+ return this.nodes.has(id);
7005
+ }
7006
+ size() {
7007
+ return this.nodes.size;
7008
+ }
7009
+ values() {
7010
+ return this.nodes.values();
7011
+ }
7012
+ async execute(concurrency, fn, cancelled = () => false) {
7013
+ let active = 0;
7014
+ const errors = [];
7015
+ return new Promise((resolve4, reject) => {
7016
+ const dispatch = () => {
7017
+ let changed = true;
7018
+ while (changed) {
7019
+ changed = false;
7020
+ for (const node of this.nodes.values()) {
7021
+ if (node.state !== "pending")
7022
+ continue;
7023
+ const hasFailedDep = [...node.dependencies].some((depId) => {
7024
+ const dep = this.nodes.get(depId);
7025
+ return dep && (dep.state === "failed" || dep.state === "skipped");
7026
+ });
7027
+ if (hasFailedDep) {
7028
+ node.state = "skipped";
7029
+ changed = true;
7030
+ this.logger.debug(`Skipped ${node.id}: dependency failed or was skipped`);
7031
+ }
7032
+ }
7033
+ }
7034
+ const ready = [];
7035
+ for (const node of this.nodes.values()) {
7036
+ if (node.state !== "pending")
7037
+ continue;
7038
+ const depsReady = [...node.dependencies].every((depId) => {
7039
+ const dep = this.nodes.get(depId);
7040
+ return !dep || dep.state === "completed";
7041
+ });
7042
+ if (depsReady)
7043
+ ready.push(node);
7044
+ }
7045
+ if (!cancelled()) {
7046
+ for (const node of ready) {
7047
+ if (active >= concurrency)
7048
+ break;
7049
+ node.state = "running";
7050
+ active++;
7051
+ fn(node).then(() => {
7052
+ node.state = "completed";
7053
+ }).catch((error) => {
7054
+ node.state = "failed";
7055
+ errors.push({ id: node.id, error });
7056
+ }).finally(() => {
7057
+ active--;
7058
+ dispatch();
7059
+ });
7060
+ }
7061
+ }
7062
+ if (active === 0) {
7063
+ if (errors.length > 0) {
7064
+ reject(errors[0].error);
7065
+ return;
7066
+ }
7067
+ const stillPending = [...this.nodes.values()].some((n) => n.state === "pending");
7068
+ if (stillPending && !cancelled()) {
7069
+ const pending = [...this.nodes.values()].filter((n) => n.state === "pending").map((n) => n.id);
7070
+ reject(
7071
+ new Error(
7072
+ `Deadlock detected: ${pending.length} node(s) stuck with unresolvable dependencies (${pending.join(", ")})`
7073
+ )
7074
+ );
7075
+ return;
7076
+ }
7077
+ resolve4();
7078
+ }
7079
+ };
7080
+ dispatch();
7081
+ });
7082
+ }
7083
+ };
7084
+
6988
7085
  // src/deployment/deploy-engine.ts
6989
- import pLimit from "p-limit";
6990
7086
  var InterruptedError = class extends Error {
6991
7087
  constructor() {
6992
7088
  super("Deployment interrupted by user (Ctrl+C)");
@@ -7119,6 +7215,7 @@ var DeployEngine = class _DeployEngine {
7119
7215
  template,
7120
7216
  currentState,
7121
7217
  changes,
7218
+ dag,
7122
7219
  executionLevels,
7123
7220
  stackName,
7124
7221
  parameterValues,
@@ -7151,13 +7248,16 @@ var DeployEngine = class _DeployEngine {
7151
7248
  }
7152
7249
  }
7153
7250
  /**
7154
- * Execute deployment by processing resources in DAG order
7251
+ * Execute deployment by processing resources via event-driven DAG dispatch.
7155
7252
  *
7156
- * Important: DELETE operations are executed in reverse dependency order,
7157
- * while CREATE/UPDATE follow normal dependency order.
7158
- */
7159
- async executeDeployment(template, currentState, changes, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
7160
- const limit = pLimit(this.options.concurrency);
7253
+ * - CREATE/UPDATE follow forward dependency order (a node starts as soon as
7254
+ * ALL of its dependencies are completed — does not wait for unrelated
7255
+ * siblings in the same "level")
7256
+ * - DELETE follows reverse dependency order (a node starts as soon as all
7257
+ * resources that depend ON it have finished deleting)
7258
+ */
7259
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
7260
+ const concurrency = this.options.concurrency;
7161
7261
  const newResources = { ...currentState.resources };
7162
7262
  const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
7163
7263
  const completedOperations = [];
@@ -7188,32 +7288,36 @@ var DeployEngine = class _DeployEngine {
7188
7288
  Array.from(changes.entries()).filter(([_, change]) => change.changeType === "DELETE").map(([logicalId]) => logicalId)
7189
7289
  );
7190
7290
  try {
7191
- for (let levelIndex = 0; levelIndex < executionLevels.length; levelIndex++) {
7192
- if (this.interrupted) {
7193
- throw new InterruptedError();
7194
- }
7195
- const levelNodes = executionLevels[levelIndex];
7196
- if (!levelNodes)
7291
+ const createUpdateIds = [];
7292
+ for (const [id, change] of changes.entries()) {
7293
+ if (deleteChanges.has(id))
7197
7294
  continue;
7198
- const level = levelNodes.filter((id) => {
7199
- if (deleteChanges.has(id))
7200
- return false;
7201
- const change = changes.get(id);
7202
- return !!change && change.changeType !== "NO_CHANGE";
7203
- });
7204
- if (level.length === 0)
7295
+ if (change.changeType === "NO_CHANGE")
7205
7296
  continue;
7297
+ createUpdateIds.push(id);
7298
+ }
7299
+ if (createUpdateIds.length > 0) {
7206
7300
  this.logger.info(
7207
- `Level ${levelIndex + 1}/${executionLevels.length} (${level.length} resources)`
7301
+ `Deploying ${createUpdateIds.length} resource(s) (DAG: ${executionLevels.length} levels, max parallel: ${concurrency})`
7208
7302
  );
7209
- const results = await Promise.allSettled(
7210
- level.map(
7211
- (logicalId) => limit(async () => {
7212
- const change = changes.get(logicalId);
7213
- if (!change || change.changeType === "NO_CHANGE") {
7214
- this.logger.debug(`Skipping ${logicalId} (no change)`);
7215
- return;
7216
- }
7303
+ const createUpdateExecutor = new DagExecutor();
7304
+ const provisionable = new Set(createUpdateIds);
7305
+ for (const id of createUpdateIds) {
7306
+ const allDeps = this.dagBuilder.getDirectDependencies(dag, id);
7307
+ const deps = new Set(allDeps.filter((d) => provisionable.has(d)));
7308
+ createUpdateExecutor.add({
7309
+ id,
7310
+ dependencies: deps,
7311
+ state: "pending",
7312
+ data: changes.get(id)
7313
+ });
7314
+ }
7315
+ try {
7316
+ await createUpdateExecutor.execute(
7317
+ concurrency,
7318
+ async (node) => {
7319
+ const logicalId = node.id;
7320
+ const change = node.data;
7217
7321
  const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7218
7322
  try {
7219
7323
  await this.provisionResource(
@@ -7240,30 +7344,36 @@ var DeployEngine = class _DeployEngine {
7240
7344
  properties: newResources[logicalId]?.properties
7241
7345
  });
7242
7346
  saveStateAfterResource(logicalId);
7243
- })
7244
- )
7245
- );
7246
- await saveChain;
7247
- const failures = results.filter((r) => r.status === "rejected");
7248
- if (failures.length > 0) {
7249
- throw failures[0].reason;
7347
+ },
7348
+ () => this.interrupted
7349
+ );
7350
+ } finally {
7351
+ await saveChain;
7352
+ }
7353
+ if (this.interrupted && this.hasPending(createUpdateExecutor)) {
7354
+ throw new InterruptedError();
7250
7355
  }
7251
7356
  }
7252
7357
  if (deleteChanges.size > 0) {
7253
7358
  this.logger.info(`Deleting ${deleteChanges.size} resource(s)`);
7254
- const deletionLevels = this.buildDeletionLevels(deleteChanges, currentState);
7255
- for (let levelIndex = 0; levelIndex < deletionLevels.length; levelIndex++) {
7256
- if (this.interrupted) {
7257
- throw new InterruptedError();
7258
- }
7259
- const level = deletionLevels[levelIndex];
7260
- if (level.length === 0)
7261
- continue;
7262
- const deleteResults = await Promise.allSettled(
7263
- level.map(
7264
- (logicalId) => limit(async () => {
7265
- const change = changes.get(logicalId);
7266
- const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7359
+ const deleteDeps = this.buildDeletionDependencies(deleteChanges, currentState);
7360
+ const deleteExecutor = new DagExecutor();
7361
+ for (const id of deleteChanges) {
7362
+ deleteExecutor.add({
7363
+ id,
7364
+ dependencies: deleteDeps.get(id) ?? /* @__PURE__ */ new Set(),
7365
+ state: "pending",
7366
+ data: changes.get(id)
7367
+ });
7368
+ }
7369
+ try {
7370
+ await deleteExecutor.execute(
7371
+ concurrency,
7372
+ async (node) => {
7373
+ const logicalId = node.id;
7374
+ const change = node.data;
7375
+ const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7376
+ try {
7267
7377
  await this.provisionResource(
7268
7378
  logicalId,
7269
7379
  change,
@@ -7275,23 +7385,25 @@ var DeployEngine = class _DeployEngine {
7275
7385
  actualCounts,
7276
7386
  progress
7277
7387
  );
7278
- completedOperations.push({
7279
- logicalId,
7280
- changeType: "DELETE",
7281
- resourceType: change.resourceType,
7282
- previousState
7283
- });
7284
- saveStateAfterResource(logicalId);
7285
- })
7286
- )
7388
+ } catch (provisionError) {
7389
+ this.interrupted = true;
7390
+ throw provisionError;
7391
+ }
7392
+ completedOperations.push({
7393
+ logicalId,
7394
+ changeType: "DELETE",
7395
+ resourceType: change.resourceType,
7396
+ previousState
7397
+ });
7398
+ saveStateAfterResource(logicalId);
7399
+ },
7400
+ () => this.interrupted
7287
7401
  );
7402
+ } finally {
7288
7403
  await saveChain;
7289
- const deleteFailures = deleteResults.filter(
7290
- (r) => r.status === "rejected"
7291
- );
7292
- if (deleteFailures.length > 0) {
7293
- throw deleteFailures[0].reason;
7294
- }
7404
+ }
7405
+ if (this.interrupted && this.hasPending(deleteExecutor)) {
7406
+ throw new InterruptedError();
7295
7407
  }
7296
7408
  }
7297
7409
  } catch (error) {
@@ -7385,12 +7497,12 @@ var DeployEngine = class _DeployEngine {
7385
7497
  * - UPDATE → update back to previous properties
7386
7498
  * - DELETE → cannot rollback (resource already deleted), log warning
7387
7499
  *
7388
- * Resources created in the same DAG level may have dependencies between them
7389
- * (e.g., IAM Policy depends on IAM Role). When rolling back CREATEs (deleting),
7390
- * dependent resources must be deleted before their dependencies. This method
7391
- * sorts CREATE rollback operations using dependency information from state,
7392
- * then processes UPDATE/DELETE rollbacks, and finally processes sorted CREATE
7393
- * rollback deletions.
7500
+ * Resources completed concurrently in the dispatcher may have dependencies
7501
+ * between them (e.g., IAM Policy depends on IAM Role). When rolling back
7502
+ * CREATEs (deleting), dependent resources must be deleted before their
7503
+ * dependencies. This method sorts CREATE rollback operations using dependency
7504
+ * information from state, then processes UPDATE/DELETE rollbacks, and finally
7505
+ * processes sorted CREATE rollback deletions.
7394
7506
  */
7395
7507
  async performRollback(completedOperations, stateResources, _stackName) {
7396
7508
  if (completedOperations.length === 0) {
@@ -7423,7 +7535,7 @@ var DeployEngine = class _DeployEngine {
7423
7535
  * Sort CREATE rollback operations so that resources depending on others
7424
7536
  * are deleted first (reverse dependency order).
7425
7537
  *
7426
- * Uses state dependencies to build deletion levels, similar to buildDeletionLevels.
7538
+ * Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
7427
7539
  */
7428
7540
  sortRollbackCreates(createOps, stateResources) {
7429
7541
  const opMap = /* @__PURE__ */ new Map();
@@ -7845,17 +7957,30 @@ var DeployEngine = class _DeployEngine {
7845
7957
  "AWS::EC2::SecurityGroup": ["AWS::EC2::SecurityGroupIngress", "AWS::EC2::SecurityGroupEgress"]
7846
7958
  };
7847
7959
  /**
7848
- * Build deletion levels from state dependencies (reverse topological order).
7849
- * Resources that are depended upon by others are deleted LAST.
7960
+ * Build a per-resource map of "must be deleted before me" dependencies for
7961
+ * the DELETE phase, derived from state-recorded dependencies plus implicit
7962
+ * type-based ordering rules.
7963
+ *
7964
+ * For a resource X, the returned set contains every resource Y such that Y
7965
+ * must finish deleting before X starts — i.e., Y depends on X (or is otherwise
7966
+ * required to vanish first per implicit type rules).
7967
+ */
7968
+ /**
7969
+ * Returns true if the executor still has un-started pending nodes —
7970
+ * used to distinguish "SIGINT cancelled real work" from "SIGINT landed
7971
+ * after all nodes already completed" (the latter should not error).
7850
7972
  */
7851
- buildDeletionLevels(deleteIds, state) {
7973
+ hasPending(executor) {
7974
+ for (const node of executor.values()) {
7975
+ if (node.state === "pending")
7976
+ return true;
7977
+ }
7978
+ return false;
7979
+ }
7980
+ buildDeletionDependencies(deleteIds, state) {
7852
7981
  const dependedBy = /* @__PURE__ */ new Map();
7853
- const inDegree = /* @__PURE__ */ new Map();
7854
7982
  for (const id of deleteIds) {
7855
- if (!dependedBy.has(id))
7856
- dependedBy.set(id, /* @__PURE__ */ new Set());
7857
- if (!inDegree.has(id))
7858
- inDegree.set(id, 0);
7983
+ dependedBy.set(id, /* @__PURE__ */ new Set());
7859
7984
  }
7860
7985
  for (const id of deleteIds) {
7861
7986
  const resource = state.resources[id];
@@ -7864,38 +7989,11 @@ var DeployEngine = class _DeployEngine {
7864
7989
  for (const dep of resource.dependencies) {
7865
7990
  if (!deleteIds.has(dep))
7866
7991
  continue;
7867
- if (!dependedBy.has(dep))
7868
- dependedBy.set(dep, /* @__PURE__ */ new Set());
7869
7992
  dependedBy.get(dep).add(id);
7870
- inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
7871
7993
  }
7872
7994
  }
7873
7995
  this.addImplicitDeleteDependencies(deleteIds, state, dependedBy);
7874
- const levels = [];
7875
- let remaining = new Set(deleteIds);
7876
- while (remaining.size > 0) {
7877
- const level = [];
7878
- for (const id of remaining) {
7879
- const dependents = dependedBy.get(id);
7880
- const hasPendingDependents = dependents ? [...dependents].some((d) => remaining.has(d)) : false;
7881
- if (!hasPendingDependents) {
7882
- level.push(id);
7883
- }
7884
- }
7885
- if (level.length === 0) {
7886
- this.logger.warn(
7887
- `Circular dependency detected in delete order, deleting remaining ${remaining.size} resources`
7888
- );
7889
- levels.push([...remaining]);
7890
- break;
7891
- }
7892
- levels.push(level);
7893
- remaining = new Set([...remaining].filter((id) => !level.includes(id)));
7894
- }
7895
- this.logger.debug(
7896
- `Delete order: ${levels.length} levels - ${levels.map((l, i) => `L${i + 1}(${l.length})`).join(", ")}`
7897
- );
7898
- return levels;
7996
+ return dependedBy;
7899
7997
  }
7900
7998
  /**
7901
7999
  * Add implicit delete dependency edges based on resource type relationships.