@go-to-k/cdkd 0.46.0 → 0.47.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/dist/cli.js CHANGED
@@ -629,6 +629,10 @@ var deployOptions = [
629
629
  new Option("--dry-run", "Show changes without applying").default(false),
630
630
  new Option("--skip-assets", "Skip asset publishing").default(false),
631
631
  new Option("--no-rollback", "Skip rollback on deployment failure"),
632
+ new Option(
633
+ "--no-capture-observed-state",
634
+ "Skip capturing AWS-current properties after each create/update (adds a fire-and-forget readCurrentState per resource so cdkd drift can compare against the real deploy-time AWS snapshot instead of the template). On by default. Disable when deploy speed matters more than rich drift detection \u2014 falls back to comparing against template properties (the pre-v3 behavior)."
635
+ ),
632
636
  noWaitOption,
633
637
  aggressiveVpcParallelOption,
634
638
  new Option(
@@ -1351,6 +1355,16 @@ function resolveApp(cliApp) {
1351
1355
  const cdkJson = loadCdkJson();
1352
1356
  return cdkJson?.app ?? void 0;
1353
1357
  }
1358
+ function resolveCaptureObservedState(cliValue) {
1359
+ if (cliValue === false)
1360
+ return false;
1361
+ const cdkJson = loadCdkJson();
1362
+ const cdkdContext = cdkJson?.context?.["cdkd"];
1363
+ const v = cdkdContext?.["captureObservedState"];
1364
+ if (typeof v === "boolean")
1365
+ return v;
1366
+ return true;
1367
+ }
1354
1368
  function resolveStateBucketWithSource(cliBucket) {
1355
1369
  if (cliBucket)
1356
1370
  return { bucket: cliBucket, source: "cli-flag" };
@@ -3727,8 +3741,8 @@ import {
3727
3741
  } from "@aws-sdk/client-s3";
3728
3742
 
3729
3743
  // src/types/state.ts
3730
- var STATE_SCHEMA_VERSION_LEGACY = 1;
3731
- var STATE_SCHEMA_VERSION_CURRENT = 2;
3744
+ var STATE_SCHEMA_VERSION_CURRENT = 3;
3745
+ var STATE_SCHEMA_VERSIONS_READABLE = [1, 2, 3];
3732
3746
 
3733
3747
  // src/utils/aws-region-resolver.ts
3734
3748
  import { GetBucketLocationCommand, S3Client as S3Client3 } from "@aws-sdk/client-s3";
@@ -4237,9 +4251,9 @@ var S3StateBackend = class {
4237
4251
  );
4238
4252
  }
4239
4253
  const v = parsed.version;
4240
- if (v !== STATE_SCHEMA_VERSION_LEGACY && v !== STATE_SCHEMA_VERSION_CURRENT && v !== void 0) {
4254
+ if (v !== void 0 && !STATE_SCHEMA_VERSIONS_READABLE.includes(v)) {
4241
4255
  throw new StateError(
4242
- `Unsupported state schema version ${String(v)} for stack '${stackName}'. This cdkd binary supports versions ${String(STATE_SCHEMA_VERSION_LEGACY)} and ${String(STATE_SCHEMA_VERSION_CURRENT)}. Upgrade cdkd to a version that supports schema ${String(v)}.`
4256
+ `Unsupported state schema version ${String(v)} for stack '${stackName}'. This cdkd binary supports versions ${STATE_SCHEMA_VERSIONS_READABLE.join(", ")}. Upgrade cdkd to a version that supports schema ${String(v)}.`
4243
4257
  );
4244
4258
  }
4245
4259
  return parsed;
@@ -9175,6 +9189,18 @@ var IAMRoleProvider = class {
9175
9189
  }
9176
9190
  return result;
9177
9191
  }
9192
+ /**
9193
+ * `Policies` (inline policy bodies) are intentionally omitted from
9194
+ * `readCurrentState`: surfacing the names without bodies would
9195
+ * guarantee a `PolicyDocument`-shaped drift on every role, and
9196
+ * fetching every body costs one extra `GetRolePolicy` per inline
9197
+ * policy. Tell the drift comparator to skip the whole subtree until a
9198
+ * dedicated PR adds proper inline-policy drift via per-name
9199
+ * `GetRolePolicy`.
9200
+ */
9201
+ getDriftUnknownPaths() {
9202
+ return ["Policies"];
9203
+ }
9178
9204
  /**
9179
9205
  * Adopt an existing IAM role into cdkd state.
9180
9206
  *
@@ -13568,6 +13594,18 @@ var SNSTopicProvider = class {
13568
13594
  }
13569
13595
  return result;
13570
13596
  }
13597
+ /**
13598
+ * `DeliveryStatusLogging` fans out to per-protocol attributes
13599
+ * (`{Protocol}SuccessFeedbackRoleArn` etc.) whose round-trip back to the
13600
+ * CFn array shape is not yet implemented; `Subscription` is managed via
13601
+ * separate `AWS::SNS::Subscription` resources rather than the Topic
13602
+ * itself. Both are absent from `readCurrentState`, so tell the drift
13603
+ * comparator to skip them and avoid the guaranteed false-positive that
13604
+ * would fire on every clean run when the user did template either.
13605
+ */
13606
+ getDriftUnknownPaths() {
13607
+ return ["DeliveryStatusLogging", "Subscription"];
13608
+ }
13571
13609
  /**
13572
13610
  * Adopt an existing SNS topic into cdkd state.
13573
13611
  *
@@ -14843,6 +14881,17 @@ var LambdaFunctionProvider = class {
14843
14881
  throw err;
14844
14882
  }
14845
14883
  }
14884
+ /**
14885
+ * `Code: { S3Bucket, S3Key }` is set on create / update but `GetFunction`
14886
+ * only returns a pre-signed URL for the deployed code, never the original
14887
+ * asset key — so a state-recorded `Code` value can never match an
14888
+ * AWS-current snapshot. Tell the drift comparator to skip the whole
14889
+ * `Code` subtree to avoid the guaranteed false-positive that would fire
14890
+ * on every clean run.
14891
+ */
14892
+ getDriftUnknownPaths() {
14893
+ return ["Code"];
14894
+ }
14846
14895
  /**
14847
14896
  * Adopt an existing Lambda function into cdkd state.
14848
14897
  *
@@ -15986,6 +16035,16 @@ var LambdaLayerVersionProvider = class {
15986
16035
  }
15987
16036
  return result;
15988
16037
  }
16038
+ /**
16039
+ * `Content: { S3Bucket, S3Key }` is set on create but
16040
+ * `GetLayerVersionByArn` only returns a pre-signed URL for the deployed
16041
+ * content — the original asset key is unrecoverable. Tell the drift
16042
+ * comparator to skip the whole `Content` subtree to avoid the guaranteed
16043
+ * false-positive that would fire on every clean run.
16044
+ */
16045
+ getDriftUnknownPaths() {
16046
+ return ["Content"];
16047
+ }
15989
16048
  /**
15990
16049
  * Adopt an existing Lambda layer version into cdkd state.
15991
16050
  *
@@ -17519,6 +17578,16 @@ var SecretsManagerSecretProvider = class {
17519
17578
  throw err;
17520
17579
  }
17521
17580
  }
17581
+ /**
17582
+ * `SecretString` and `GenerateSecretString` are set on create but
17583
+ * `DescribeSecret` does not return the secret value (that lives behind
17584
+ * `GetSecretValue`, which we deliberately never call to avoid surfacing
17585
+ * plaintext through drift). Tell the drift comparator to skip both keys
17586
+ * so they don't fire guaranteed false-positive drift on every clean run.
17587
+ */
17588
+ getDriftUnknownPaths() {
17589
+ return ["SecretString", "GenerateSecretString"];
17590
+ }
17522
17591
  /**
17523
17592
  * Adopt an existing Secrets Manager secret into cdkd state.
17524
17593
  *
@@ -36892,10 +36961,23 @@ var DeployEngine = class {
36892
36961
  this.options.noRollback = options.noRollback ?? false;
36893
36962
  this.options.resourceWarnAfterMs = options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
36894
36963
  this.options.resourceTimeoutMs = options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
36964
+ this.options.captureObservedState = options.captureObservedState ?? true;
36895
36965
  }
36896
36966
  logger = getLogger().child("DeployEngine");
36897
36967
  resolver;
36898
36968
  interrupted = false;
36969
+ /**
36970
+ * In-flight `provider.readCurrentState` promises kicked off after a
36971
+ * successful CREATE / UPDATE. The deploy critical path does NOT
36972
+ * `await` these; instead they're drained at the end of `doDeploy`
36973
+ * (success path only) and the resolved values are merged into
36974
+ * `ResourceState.observedProperties` before the final state save.
36975
+ *
36976
+ * Each Promise resolves to the AWS-current snapshot, or `undefined`
36977
+ * if the provider does not implement `readCurrentState` or the call
36978
+ * threw — never rejects, so an unhandled-rejection cannot escape.
36979
+ */
36980
+ observedCaptureTasks = /* @__PURE__ */ new Map();
36899
36981
  /**
36900
36982
  * Target region for this stack. Required — load-bearing for the
36901
36983
  * region-prefixed S3 state key and recorded in state.json for
@@ -36908,6 +36990,61 @@ var DeployEngine = class {
36908
36990
  async deploy(stackName, template) {
36909
36991
  return withStackName(stackName, () => this.doDeploy(stackName, template));
36910
36992
  }
36993
+ /**
36994
+ * Kick off `provider.readCurrentState` for a freshly-created/updated
36995
+ * resource without blocking the deploy critical path. The promise
36996
+ * lands in `observedCaptureTasks` keyed by `logicalId`; the deploy's
36997
+ * success-path drain (`drainObservedCaptures`) awaits the full set
36998
+ * and merges the resolved values into `ResourceState.observedProperties`
36999
+ * before the final state save.
37000
+ *
37001
+ * Errors are swallowed at the Promise level — readCurrentState
37002
+ * failing must not fail the deploy. The map entry resolves to
37003
+ * `undefined` for failures and for providers without
37004
+ * `readCurrentState`; both translate to "no observedProperties" at
37005
+ * the merge step, which is fine: drift falls back to comparing
37006
+ * against `properties`.
37007
+ */
37008
+ kickOffObservedCapture(provider, logicalId, physicalId, resourceType, resolvedProps) {
37009
+ if (this.options.captureObservedState !== true)
37010
+ return;
37011
+ if (!provider.readCurrentState)
37012
+ return;
37013
+ const promise = provider.readCurrentState(physicalId, logicalId, resourceType, resolvedProps).catch((err) => {
37014
+ this.logger.debug(
37015
+ `observedProperties capture for ${logicalId} (${resourceType}) failed: ${err instanceof Error ? err.message : String(err)} \u2014 drift will fall back to template properties for this resource until the next successful deploy.`
37016
+ );
37017
+ return void 0;
37018
+ });
37019
+ this.observedCaptureTasks.set(logicalId, promise);
37020
+ }
37021
+ /**
37022
+ * Wait for every in-flight `readCurrentState` promise from the
37023
+ * deploy's success path, then merge each resolved snapshot into the
37024
+ * matching `ResourceState.observedProperties`. After this runs the
37025
+ * map is drained so a subsequent deploy starts fresh.
37026
+ *
37027
+ * Called from `doDeploy` immediately before the final `saveState`.
37028
+ * The rollback / failure paths intentionally do NOT call this — a
37029
+ * failed deploy's partial state is already inconsistent, and waiting
37030
+ * on potentially many in-flight reads would slow down the rollback
37031
+ * itself.
37032
+ */
37033
+ async drainObservedCaptures(stateResources) {
37034
+ if (this.observedCaptureTasks.size === 0)
37035
+ return;
37036
+ const entries = Array.from(this.observedCaptureTasks.entries());
37037
+ this.observedCaptureTasks.clear();
37038
+ const resolved = await Promise.all(entries.map(([, p]) => p));
37039
+ for (let i = 0; i < entries.length; i++) {
37040
+ const logicalId = entries[i][0];
37041
+ const observed = resolved[i];
37042
+ const target = stateResources[logicalId];
37043
+ if (target && observed !== void 0) {
37044
+ target.observedProperties = observed;
37045
+ }
37046
+ }
37047
+ }
36911
37048
  async doDeploy(stackName, template) {
36912
37049
  const startTime = Date.now();
36913
37050
  this.logger.debug(`Starting deployment for stack: ${stackName}`);
@@ -37024,6 +37161,7 @@ var DeployEngine = class {
37024
37161
  progress,
37025
37162
  migrationPending
37026
37163
  );
37164
+ await this.drainObservedCaptures(newState.resources);
37027
37165
  const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, newState);
37028
37166
  this.logger.debug(`State saved (ETag: ${newEtag})`);
37029
37167
  const durationMs = Date.now() - startTime;
@@ -37039,6 +37177,7 @@ var DeployEngine = class {
37039
37177
  } finally {
37040
37178
  renderer.stop();
37041
37179
  process.removeListener("SIGINT", sigintHandler);
37180
+ this.observedCaptureTasks.clear();
37042
37181
  try {
37043
37182
  await this.lockManager.releaseLock(stackName, this.stackRegion);
37044
37183
  this.logger.debug("Lock released");
@@ -37592,6 +37731,13 @@ var DeployEngine = class {
37592
37731
  ...result.attributes && { attributes: result.attributes },
37593
37732
  ...dependencies && dependencies.length > 0 && { dependencies }
37594
37733
  };
37734
+ this.kickOffObservedCapture(
37735
+ provider,
37736
+ logicalId,
37737
+ result.physicalId,
37738
+ resourceType,
37739
+ resolvedProps
37740
+ );
37595
37741
  if (counts)
37596
37742
  counts.created++;
37597
37743
  if (progress)
@@ -37670,6 +37816,13 @@ var DeployEngine = class {
37670
37816
  ...createResult.attributes && { attributes: createResult.attributes },
37671
37817
  ...dependencies && dependencies.length > 0 && { dependencies }
37672
37818
  };
37819
+ this.kickOffObservedCapture(
37820
+ provider,
37821
+ logicalId,
37822
+ createResult.physicalId,
37823
+ resourceType,
37824
+ resolvedProps
37825
+ );
37673
37826
  if (counts)
37674
37827
  counts.updated++;
37675
37828
  if (progress)
@@ -37748,6 +37901,13 @@ var DeployEngine = class {
37748
37901
  ...result.attributes && { attributes: result.attributes },
37749
37902
  ...dependencies && dependencies.length > 0 && { dependencies }
37750
37903
  };
37904
+ this.kickOffObservedCapture(
37905
+ provider,
37906
+ logicalId,
37907
+ result.physicalId,
37908
+ resourceType,
37909
+ resolvedProps
37910
+ );
37751
37911
  if (counts)
37752
37912
  counts.updated++;
37753
37913
  if (progress)
@@ -38226,6 +38386,7 @@ Deploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region:
38226
38386
  concurrency: options.concurrency,
38227
38387
  dryRun: options.dryRun,
38228
38388
  noRollback: !options.rollback,
38389
+ captureObservedState: resolveCaptureObservedState(options.captureObservedState),
38229
38390
  ...options.resourceWarnAfter?.globalMs !== void 0 && {
38230
38391
  resourceWarnAfterMs: options.resourceWarnAfter.globalMs
38231
38392
  },
@@ -38531,19 +38692,34 @@ import { Command as Command6, Option as Option3 } from "commander";
38531
38692
  init_aws_clients();
38532
38693
 
38533
38694
  // src/analyzer/drift-calculator.ts
38534
- function calculateResourceDrift(stateProperties, awsProperties) {
38695
+ function calculateResourceDrift(stateProperties, awsProperties, options) {
38535
38696
  const drifts = [];
38697
+ const ignore = options?.ignorePaths ?? [];
38536
38698
  for (const key of Object.keys(stateProperties)) {
38537
- diffAt(key, stateProperties[key], awsProperties[key], drifts);
38699
+ if (isIgnoredPath(key, ignore))
38700
+ continue;
38701
+ diffAt(key, stateProperties[key], awsProperties[key], drifts, ignore);
38538
38702
  }
38539
38703
  return drifts;
38540
38704
  }
38541
- function diffAt(path, stateValue, awsValue, out) {
38705
+ function isIgnoredPath(path, ignorePaths) {
38706
+ for (const entry of ignorePaths) {
38707
+ if (path === entry)
38708
+ return true;
38709
+ if (path.startsWith(`${entry}.`))
38710
+ return true;
38711
+ }
38712
+ return false;
38713
+ }
38714
+ function diffAt(path, stateValue, awsValue, out, ignorePaths) {
38542
38715
  if (deepEqual(stateValue, awsValue))
38543
38716
  return;
38544
38717
  if (isPlainObject(stateValue) && isPlainObject(awsValue) && !Array.isArray(stateValue) && !Array.isArray(awsValue)) {
38545
38718
  for (const key of Object.keys(stateValue)) {
38546
- diffAt(`${path}.${key}`, stateValue[key], awsValue[key], out);
38719
+ const childPath = `${path}.${key}`;
38720
+ if (isIgnoredPath(childPath, ignorePaths))
38721
+ continue;
38722
+ diffAt(childPath, stateValue[key], awsValue[key], out, ignorePaths);
38547
38723
  }
38548
38724
  return;
38549
38725
  }
@@ -38721,9 +38897,6 @@ async function driftCommand(stacks, options) {
38721
38897
  logger.setLevel("debug");
38722
38898
  }
38723
38899
  warnIfDeprecatedRegion(options);
38724
- if (!options.all && stacks.length === 0) {
38725
- throw new Error("Stack name is required. Usage: cdkd drift <stack> [<stack>...] | --all");
38726
- }
38727
38900
  if (options.accept && options.revert) {
38728
38901
  throw new Error(
38729
38902
  "--accept and --revert are mutually exclusive. Use --accept to update cdkd state from AWS, or --revert to push cdkd state values back into AWS."
@@ -38804,6 +38977,21 @@ function resolveTargetRefs(stacks, stateRefs, options) {
38804
38977
  }
38805
38978
  return stateRefs;
38806
38979
  }
38980
+ if (stacks.length === 0) {
38981
+ const candidates = options.stackRegion ? stateRefs.filter((r) => r.region === options.stackRegion) : stateRefs;
38982
+ if (candidates.length === 0) {
38983
+ throw new Error(
38984
+ "No stacks found in state bucket. Run `cdkd deploy` first, or pass --all explicitly."
38985
+ );
38986
+ }
38987
+ if (candidates.length === 1) {
38988
+ return [candidates[0]];
38989
+ }
38990
+ const listing = candidates.map((r) => `${r.stackName}${r.region ? ` (${r.region})` : ""}`).join(", ");
38991
+ throw new Error(
38992
+ `Multiple stacks found in state: ${listing}. Specify stack name(s) or use --all.`
38993
+ );
38994
+ }
38807
38995
  const out = [];
38808
38996
  for (const stackName of stacks) {
38809
38997
  const matches = stateRefs.filter((r) => r.stackName === stackName);
@@ -38901,7 +39089,9 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
38901
39089
  });
38902
39090
  continue;
38903
39091
  }
38904
- const changes = calculateResourceDrift(resource.properties ?? {}, aws);
39092
+ const ignorePaths = provider.getDriftUnknownPaths ? provider.getDriftUnknownPaths(resource.resourceType) : [];
39093
+ const baseline = resource.observedProperties ?? resource.properties ?? {};
39094
+ const changes = calculateResourceDrift(baseline, aws, { ignorePaths });
38905
39095
  if (changes.length === 0) {
38906
39096
  outcomes.push({ kind: "clean", logicalId, resourceType: resource.resourceType });
38907
39097
  } else {
@@ -38973,14 +39163,13 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
38973
39163
  const existing = resources[outcome.logicalId];
38974
39164
  if (!existing)
38975
39165
  continue;
38976
- const newProperties = JSON.parse(JSON.stringify(existing.properties ?? {}));
39166
+ const hasObserved = existing.observedProperties !== void 0;
39167
+ const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
39168
+ const newBaseline = JSON.parse(JSON.stringify(baselineSource));
38977
39169
  for (const change of outcome.changes) {
38978
- setAtPath(newProperties, change.path, change.awsValue);
39170
+ setAtPath(newBaseline, change.path, change.awsValue);
38979
39171
  }
38980
- resources[outcome.logicalId] = {
38981
- ...existing,
38982
- properties: newProperties
38983
- };
39172
+ resources[outcome.logicalId] = hasObserved ? { ...existing, observedProperties: newBaseline } : { ...existing, properties: newBaseline };
38984
39173
  }
38985
39174
  const newState = {
38986
39175
  ...report.state,
@@ -39047,13 +39236,14 @@ async function runRevert(reports, providerRegistry, stateConfig, awsClients, opt
39047
39236
  return;
39048
39237
  }
39049
39238
  const provider = providerRegistry.getProvider(outcome.resourceType);
39239
+ const desiredProperties = stateResource.observedProperties ?? stateResource.properties ?? {};
39050
39240
  try {
39051
39241
  await withRetry(
39052
39242
  () => provider.update(
39053
39243
  outcome.logicalId,
39054
39244
  stateResource.physicalId,
39055
39245
  outcome.resourceType,
39056
- stateResource.properties ?? {},
39246
+ desiredProperties,
39057
39247
  outcome.awsProperties
39058
39248
  ),
39059
39249
  outcome.logicalId,
@@ -42126,6 +42316,7 @@ async function importCommand(stackArg, options) {
42126
42316
  existingState,
42127
42317
  selectiveMode
42128
42318
  );
42319
+ await captureObservedForImportedResources(stackState, providerRegistry, logger);
42129
42320
  const saveOptions = {};
42130
42321
  if (existingEtag) {
42131
42322
  saveOptions.expectedEtag = existingEtag;
@@ -42326,7 +42517,7 @@ function buildStackState(stackName, region, rows, templateParser, template, exis
42326
42517
  };
42327
42518
  }
42328
42519
  return {
42329
- version: 2,
42520
+ version: STATE_SCHEMA_VERSION_CURRENT,
42330
42521
  stackName,
42331
42522
  region,
42332
42523
  resources,
@@ -42419,6 +42610,33 @@ function createImportCommand() {
42419
42610
  function collectMultiple(value, previous) {
42420
42611
  return [...previous ?? [], value];
42421
42612
  }
42613
+ async function captureObservedForImportedResources(stackState, providerRegistry, logger) {
42614
+ const entries = Object.entries(stackState.resources);
42615
+ if (entries.length === 0)
42616
+ return;
42617
+ await Promise.all(
42618
+ entries.map(async ([logicalId, resource]) => {
42619
+ try {
42620
+ const provider = providerRegistry.getProvider(resource.resourceType);
42621
+ if (!provider.readCurrentState)
42622
+ return;
42623
+ const observed = await provider.readCurrentState(
42624
+ resource.physicalId,
42625
+ logicalId,
42626
+ resource.resourceType,
42627
+ resource.properties ?? {}
42628
+ );
42629
+ if (observed !== void 0) {
42630
+ resource.observedProperties = observed;
42631
+ }
42632
+ } catch (err) {
42633
+ logger.debug(
42634
+ `observedProperties capture for imported ${logicalId} (${resource.resourceType}) failed: ${err instanceof Error ? err.message : String(err)} \u2014 drift will fall back to template properties for this resource until the next successful deploy.`
42635
+ );
42636
+ }
42637
+ })
42638
+ );
42639
+ }
42422
42640
 
42423
42641
  // src/cli/index.ts
42424
42642
  var SUBCOMMANDS = /* @__PURE__ */ new Set([
@@ -42448,7 +42666,7 @@ function reorderArgs(argv) {
42448
42666
  }
42449
42667
  async function main() {
42450
42668
  const program = new Command14();
42451
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.46.0");
42669
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.47.0");
42452
42670
  program.addCommand(createBootstrapCommand());
42453
42671
  program.addCommand(createSynthCommand());
42454
42672
  program.addCommand(createListCommand());