@go-to-k/cdkd 0.23.1 → 0.24.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 CHANGED
@@ -582,15 +582,21 @@ cdkd import MyStack \
582
582
  # CDK CLI compat: read overrides from a JSON file.
583
583
  cdkd import MyStack --resource-mapping mapping.json
584
584
  # mapping.json: { "MyBucket": "my-bucket-name", "MyFn": "my-function-name" }
585
+
586
+ # CDK CLI compat: inline JSON (handy for non-TTY CI scripts).
587
+ cdkd import MyStack --resource-mapping-inline '{"MyBucket":"my-bucket-name"}'
585
588
  ```
586
589
 
587
- When at least one `--resource` flag (or a `--resource-mapping` file) is
588
- supplied, **only the listed resources are imported**. Every other
589
- resource in the template is reported as `out of scope` and left out of
590
- state the next `cdkd deploy` will treat them as new and CREATE them.
591
- This matches the semantics of `cdk import --resource-mapping`. cdkd
592
- validates that every override key is a real logical ID in the
593
- template; a typo aborts the run rather than silently importing nothing.
590
+ When at least one `--resource` flag (or a `--resource-mapping` /
591
+ `--resource-mapping-inline` payload) is supplied, **only the listed
592
+ resources are imported**. Every other resource in the template is
593
+ reported as `out of scope` and left out of state the next `cdkd
594
+ deploy` will treat them as new and CREATE them. This matches the
595
+ semantics of `cdk import --resource-mapping` /
596
+ `--resource-mapping-inline`. cdkd validates that every override key is
597
+ a real logical ID in the template; a typo aborts the run rather than
598
+ silently importing nothing. `--resource-mapping` and
599
+ `--resource-mapping-inline` are mutually exclusive — pick one source.
594
600
 
595
601
  Use selective mode when you want to **adopt a few specific resources**
596
602
  out of a larger stack — for example, you have one S3 bucket that was
@@ -641,6 +647,50 @@ services, anything in Cloud Control API), use the explicit
641
647
  exactly this case. Resource types whose provider does not implement
642
648
  import are reported as `unsupported` and skipped.
643
649
 
650
+ ### `cdkd import` vs upstream `cdk import`
651
+
652
+ cdkd's `import` command mirrors the surface of upstream
653
+ [`cdk import`](https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-import.html)
654
+ where it can, but the underlying mechanism is fundamentally different
655
+ and a handful of upstream-only flags are not implemented. Use this
656
+ table to predict behavior when migrating from `cdk import`.
657
+
658
+ | Topic | `cdk import` (upstream) | `cdkd import` |
659
+ | --- | --- | --- |
660
+ | Mechanism | CloudFormation `CreateChangeSet` with `ResourcesToImport` — atomic, all-or-nothing. | Per-resource SDK calls (e.g. `s3:HeadBucket`, `lambda:GetFunction`, IAM `ListRoleTags`). **Not atomic.** |
661
+ | Failure mode | Failed import rolls the changeset back; the stack is left unchanged. | Per-resource: `imported` / `skipped-not-found` / `skipped-no-impl` / `skipped-out-of-scope` / `failed` rows are summarized. State is written for whatever succeeded — but only after a confirmation prompt (or `--yes`), so a partial run is opt-in. To roll a partial import back, use `cdkd state orphan <stack>` (drops the state record only). |
662
+ | Selective mode (`--resource-mapping <file>`) | Supported. Listed resources are imported; unlisted resources cause the changeset to fail. | Supported. Listed resources are imported; unlisted resources are reported as `out of scope` and left out of state (next `cdkd deploy` will CREATE them). |
663
+ | Selective mode (`--resource <id>=<physical>` repeatable) | Not supported (upstream uses interactive prompts or a mapping file). | Supported as cdkd's CLI-friendly equivalent. |
664
+ | `--resource-mapping-inline '<json>'` | Supported (use in non-TTY environments). | **Not supported.** Use `--resource <id>=<physical>` (repeatable) or `--resource-mapping <file>` instead. |
665
+ | `--record-resource-mapping <file>` | Supported (writes the mapping the user typed at the prompt to a file for re-use). | **Not supported.** cdkd has no interactive prompt to record. |
666
+ | Interactive prompt for missing IDs | Default in TTY — prompts for every resource not covered by a mapping file. | **Not supported.** cdkd is non-interactive: missing logical IDs are looked up by `aws:cdk:path` tag in `auto` / `hybrid` modes, or skipped as `out of scope` in selective mode. The only prompt is the final "write state?" confirmation, which `--yes` skips. |
667
+ | Typo'd logical ID | Aborts with a clear error before any AWS calls. | Aborts with a clear error before any AWS calls — checked against the synthesized template. |
668
+ | Whole-stack tag-based import | **Not supported.** | **cdkd-specific.** With no flags, cdkd looks every resource up by its `aws:cdk:path` tag — the typical case for adopting a stack previously deployed by `cdk deploy`. |
669
+ | Hybrid mode (overrides + tag fallback) | **Not supported.** | **cdkd-specific.** `--auto` together with `--resource` lets listed resources use the explicit physical id while everything else still goes through tag lookup. |
670
+ | Nested stacks (`AWS::CloudFormation::Stack`) | Explicitly unsupported. | Also unsupported in practice — cdkd does not deploy nested CloudFormation stacks at all (no `AWS::CloudFormation::Stack` provider). The `Stack` resource itself would be reported as `unsupported`. CDK Stages (separate top-level stacks) are fine: pass the stack's display path or physical name as the positional argument. |
671
+ | Bootstrap requirement | Bootstrap v12+ (deploy role needs to read the encrypted staging bucket). | cdkd's own state bucket; no CDK bootstrap version requirement. |
672
+ | Resource-type coverage | Whatever [CloudFormation supports for import](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html). | The set of cdkd providers that implement `import()` (see [CLAUDE.md](CLAUDE.md) for the current list). For any other CC-API-supported type, use `--resource <id>=<physical>` to drive the Cloud Control API fallback. The two lists overlap heavily but are not identical. |
673
+ | Confirmation prompt before writing state | n/a (CloudFormation operates atomically). | Yes — cdkd asks before writing the state file. Skip with `--yes`. |
674
+ | `--force` | "Continue even if the diff includes updates or deletions" — about diff strictness. | "Overwrite an existing state record" — about state safety. **Same flag name, different meaning.** |
675
+ | `--dry-run` | Implied by `--no-execute` (creates the changeset without executing). | Native: shows the import plan and exits without writing state. |
676
+
677
+ #### Practical implications when migrating from `cdk import`
678
+
679
+ - If you script around `--resource-mapping <file>`: behavior matches.
680
+ The file format (`{"LogicalId": "physical-id"}`) is the same.
681
+ - If you script around `--resource-mapping-inline`: rewrite as
682
+ repeated `--resource <id>=<physical>` flags, or write a temp file.
683
+ - If your workflow relies on the interactive prompt: rewrite as
684
+ `--resource-mapping <file>`. cdkd will not prompt.
685
+ - If you rely on atomic rollback: cdkd cannot offer that — its
686
+ per-resource model writes state only after the full pass completes
687
+ (and after confirmation), so a partial run is bounded, but if a
688
+ later resource fails after several earlier ones already returned
689
+ successfully and you confirm the write, those earlier ones are
690
+ in cdkd state. Use `cdkd state orphan <stack>` to back out.
691
+ - If you import nested stacks: neither tool supports this. Convert
692
+ to top-level CDK stacks first.
693
+
644
694
  ## State Management
645
695
 
646
696
  State is stored in S3. Keys are scoped by `(stackName, region)` so the same
package/dist/cli.js CHANGED
@@ -7344,13 +7344,11 @@ var CustomResourceProvider = class _CustomResourceProvider {
7344
7344
  );
7345
7345
  }
7346
7346
  try {
7347
- const requestId = `cdkd-${Date.now()}-${Math.random().toString(36).substring(7)}`;
7348
- const responseKey = this.getResponseKey(requestId);
7349
- const responseURL = await this.generateResponseURL(responseKey);
7347
+ const invocation = await this.prepareInvocation();
7350
7348
  const request = {
7351
7349
  RequestType: "Create",
7352
- RequestId: requestId,
7353
- ResponseURL: responseURL,
7350
+ RequestId: invocation.requestId,
7351
+ ResponseURL: invocation.responseURL,
7354
7352
  ResourceType: resourceType,
7355
7353
  LogicalResourceId: logicalId,
7356
7354
  StackId: `arn:aws:cloudformation:us-east-1:000000000000:stack/cdkd-${logicalId}/cdkd`,
@@ -7360,7 +7358,7 @@ var CustomResourceProvider = class _CustomResourceProvider {
7360
7358
  const cfnResponse = await this.sendRequest(
7361
7359
  serviceToken,
7362
7360
  request,
7363
- responseKey,
7361
+ invocation.responseKey,
7364
7362
  logicalId,
7365
7363
  "Create"
7366
7364
  );
@@ -7399,13 +7397,11 @@ var CustomResourceProvider = class _CustomResourceProvider {
7399
7397
  );
7400
7398
  }
7401
7399
  try {
7402
- const requestId = `cdkd-${Date.now()}-${Math.random().toString(36).substring(7)}`;
7403
- const responseKey = this.getResponseKey(requestId);
7404
- const responseURL = await this.generateResponseURL(responseKey);
7400
+ const invocation = await this.prepareInvocation();
7405
7401
  const request = {
7406
7402
  RequestType: "Update",
7407
- RequestId: requestId,
7408
- ResponseURL: responseURL,
7403
+ RequestId: invocation.requestId,
7404
+ ResponseURL: invocation.responseURL,
7409
7405
  ResourceType: resourceType,
7410
7406
  LogicalResourceId: logicalId,
7411
7407
  PhysicalResourceId: physicalId,
@@ -7417,7 +7413,7 @@ var CustomResourceProvider = class _CustomResourceProvider {
7417
7413
  const cfnResponse = await this.sendRequest(
7418
7414
  serviceToken,
7419
7415
  request,
7420
- responseKey,
7416
+ invocation.responseKey,
7421
7417
  logicalId,
7422
7418
  "Update"
7423
7419
  );
@@ -7461,13 +7457,11 @@ var CustomResourceProvider = class _CustomResourceProvider {
7461
7457
  return;
7462
7458
  }
7463
7459
  try {
7464
- const requestId = `cdkd-${Date.now()}-${Math.random().toString(36).substring(7)}`;
7465
- const responseKey = this.getResponseKey(requestId);
7466
- const responseURL = await this.generateResponseURL(responseKey);
7460
+ const invocation = await this.prepareInvocation();
7467
7461
  const request = {
7468
7462
  RequestType: "Delete",
7469
- RequestId: requestId,
7470
- ResponseURL: responseURL,
7463
+ RequestId: invocation.requestId,
7464
+ ResponseURL: invocation.responseURL,
7471
7465
  ResourceType: resourceType,
7472
7466
  LogicalResourceId: logicalId,
7473
7467
  PhysicalResourceId: physicalId,
@@ -7478,7 +7472,7 @@ var CustomResourceProvider = class _CustomResourceProvider {
7478
7472
  const cfnResponse = await this.sendRequest(
7479
7473
  serviceToken,
7480
7474
  request,
7481
- responseKey,
7475
+ invocation.responseKey,
7482
7476
  logicalId,
7483
7477
  "Delete"
7484
7478
  );
@@ -7597,6 +7591,28 @@ var CustomResourceProvider = class _CustomResourceProvider {
7597
7591
  const timeoutMs = isAsyncPattern ? this.asyncResponseTimeoutMs : this.SYNC_RESPONSE_TIMEOUT_MS;
7598
7592
  return await this.pollS3Response(responseKey, logicalId, operation, timeoutMs, isAsyncPattern);
7599
7593
  }
7594
+ /**
7595
+ * Prepare a single Custom Resource invocation: generate the request id,
7596
+ * derive the S3 response key from it, sign the pre-signed PUT URL for that
7597
+ * key, and return all three together.
7598
+ *
7599
+ * **The request id, response key, and response URL must all be derived from
7600
+ * the SAME generation step.** Previously these were generated by separate
7601
+ * calls inside `create` / `update` / `delete`, which made it possible for a
7602
+ * future refactor (e.g. wrapping URL signing in a retry that re-rolls the
7603
+ * id) to silently break the invariant — the Lambda would write to one S3
7604
+ * key while cdkd polled a different one, hanging the deploy until the
7605
+ * polling timeout (up to 1 hour). See issue #90.
7606
+ *
7607
+ * Centralising this in one helper makes that invariant impossible to
7608
+ * violate at the call sites.
7609
+ */
7610
+ async prepareInvocation() {
7611
+ const requestId = `cdkd-${Date.now()}-${Math.random().toString(36).substring(7)}`;
7612
+ const responseKey = this.getResponseKey(requestId);
7613
+ const responseURL = await this.generateResponseURL(responseKey);
7614
+ return { requestId, responseKey, responseURL };
7615
+ }
7600
7616
  /**
7601
7617
  * Generate a pre-signed S3 PUT URL for Lambda to send its response
7602
7618
  */
@@ -34054,7 +34070,11 @@ async function importCommand(stackArg, options) {
34054
34070
  `State already exists for stack '${stackInfo.stackName}' (${targetRegion}). Pass --force to overwrite. (cdkd state import rebuilds the resource map from AWS, so the existing state \u2014 including any drift you've manually edited \u2014 will be lost.)`
34055
34071
  );
34056
34072
  }
34057
- const overrides = parseResourceOverrides(options.resource, options.resourceMapping);
34073
+ const overrides = parseResourceOverrides(
34074
+ options.resource,
34075
+ options.resourceMapping,
34076
+ options.resourceMappingInline
34077
+ );
34058
34078
  if (overrides.size > 0) {
34059
34079
  logger.debug(`User-supplied physical IDs: ${[...overrides.keys()].join(", ")}`);
34060
34080
  }
@@ -34197,28 +34217,30 @@ async function importOne(task) {
34197
34217
  };
34198
34218
  }
34199
34219
  }
34200
- function parseResourceOverrides(flags, mappingFile) {
34220
+ function parseResourceOverrides(flags, mappingFile, mappingInline) {
34201
34221
  const map = /* @__PURE__ */ new Map();
34222
+ if (mappingFile && mappingInline) {
34223
+ throw new Error(
34224
+ "--resource-mapping and --resource-mapping-inline are mutually exclusive; pass only one."
34225
+ );
34226
+ }
34202
34227
  if (mappingFile) {
34203
- let parsed;
34228
+ let raw;
34204
34229
  try {
34205
- parsed = JSON.parse(readFileSync5(mappingFile, "utf-8"));
34230
+ raw = readFileSync5(mappingFile, "utf-8");
34206
34231
  } catch (err) {
34207
34232
  throw new Error(
34208
34233
  `Failed to read --resource-mapping file '${mappingFile}': ` + (err instanceof Error ? err.message : String(err))
34209
34234
  );
34210
34235
  }
34211
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
34212
- throw new Error(
34213
- `--resource-mapping file '${mappingFile}' must be a JSON object {logicalId: physicalId}`
34214
- );
34236
+ const parsed = parseMappingJson(raw, `--resource-mapping file '${mappingFile}'`);
34237
+ for (const [key, value] of Object.entries(parsed)) {
34238
+ map.set(key, value);
34215
34239
  }
34240
+ }
34241
+ if (mappingInline) {
34242
+ const parsed = parseMappingJson(mappingInline, "--resource-mapping-inline");
34216
34243
  for (const [key, value] of Object.entries(parsed)) {
34217
- if (typeof value !== "string") {
34218
- throw new Error(
34219
- `--resource-mapping: value for '${key}' must be a string, got ${typeof value}`
34220
- );
34221
- }
34222
34244
  map.set(key, value);
34223
34245
  }
34224
34246
  }
@@ -34231,6 +34253,27 @@ function parseResourceOverrides(flags, mappingFile) {
34231
34253
  }
34232
34254
  return map;
34233
34255
  }
34256
+ function parseMappingJson(raw, source) {
34257
+ let parsed;
34258
+ try {
34259
+ parsed = JSON.parse(raw);
34260
+ } catch (err) {
34261
+ throw new Error(
34262
+ `Failed to parse ${source} as JSON: ` + (err instanceof Error ? err.message : String(err))
34263
+ );
34264
+ }
34265
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
34266
+ throw new Error(`${source} must be a JSON object {logicalId: physicalId}`);
34267
+ }
34268
+ const out = {};
34269
+ for (const [key, value] of Object.entries(parsed)) {
34270
+ if (typeof value !== "string") {
34271
+ throw new Error(`${source}: value for '${key}' must be a string, got ${typeof value}`);
34272
+ }
34273
+ out[key] = value;
34274
+ }
34275
+ return out;
34276
+ }
34234
34277
  function readCdkPath(resource) {
34235
34278
  const meta = resource.Metadata;
34236
34279
  if (!meta)
@@ -34331,7 +34374,10 @@ function createImportCommand() {
34331
34374
  []
34332
34375
  ).option(
34333
34376
  "--resource-mapping <file>",
34334
- "Path to a JSON file of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping` compatible). Implies selective mode unless --auto is set."
34377
+ "Path to a JSON file of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping` compatible). Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping-inline."
34378
+ ).option(
34379
+ "--resource-mapping-inline <json>",
34380
+ "Inline JSON object of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping-inline` compatible). Same shape as --resource-mapping but supplied as a string \u2014 useful for non-TTY CI scripts that do not want a separate file. Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping."
34335
34381
  ).option(
34336
34382
  "--auto",
34337
34383
  "Hybrid mode: when explicit overrides are supplied, ALSO tag-import every other resource in the template. Without this flag, --resource / --resource-mapping behave as a whitelist (CDK CLI parity).",
@@ -34377,7 +34423,7 @@ function reorderArgs(argv) {
34377
34423
  }
34378
34424
  async function main() {
34379
34425
  const program = new Command13();
34380
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.23.1");
34426
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.24.0");
34381
34427
  program.addCommand(createBootstrapCommand());
34382
34428
  program.addCommand(createSynthCommand());
34383
34429
  program.addCommand(createListCommand());