@go-to-k/cdkd 0.115.4 → 0.116.1

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
@@ -4,15 +4,14 @@ Drop-in CDK CLI for existing CDK apps — faster deploys via AWS SDK instead of
4
4
 
5
5
  - **Drop-in CDK compatible** — your existing CDK app code runs as-is.
6
6
  - **Up to 15x faster deploys than the AWS CDK CLI (CloudFormation)**
7
- - **Run AWS resources locally without deploying** — invoke Lambdas, run ECS tasks, and serve API Gateway routes from Docker.
7
+ - **Local dev for CDK apps** — invoke Lambdas, serve API Gateway routes, and run ECS tasks directly from your CDK code, no `cdk synth sam local` round-trip.
8
8
 
9
9
  ![cdkd demo](https://github.com/user-attachments/assets/0128730d-186d-4bd3-abea-aabc80ba4dd5)
10
10
 
11
11
  **cdkd complements the AWS CDK CLI rather than replacing it.** Use cdkd in dev/test for rapid iteration and SAM-style local execution; use the AWS CDK CLI in production for full CloudFormation tooling. Bidirectional migration is supported — [import an existing CloudFormation stack](#importing-existing-resources) into cdkd for iteration, or [export back to CloudFormation](#exporting-a-stack-back-to-cloudformation) when ready for production.
12
12
 
13
- > **⚠️ WARNING: NOT PRODUCTION READY**
14
- >
15
- > An experimental project exploring direct SDK provisioning as an alternative to the AWS CDK CLI — **NOT a replacement** and **NOT suitable for production use**. Features are incomplete, APIs may change without notice, and bugs may affect your AWS infrastructure. Use at your own risk in development / testing environments only.
13
+ > [!IMPORTANT]
14
+ > cdkd is for dev/test workflows only — early in development, not yet production-ready.
16
15
 
17
16
  ## Features
18
17
 
package/dist/cli.js CHANGED
@@ -6434,11 +6434,14 @@ var LambdaFunctionProvider = class {
6434
6434
  * etc.) are filtered at compare time — we still avoid serializing them on
6435
6435
  * the wire.
6436
6436
  *
6437
- * `Code` is intentionally omitted: `GetFunction` returns a pre-signed S3
6438
- * URL for the deployed code, not the asset hash cdkd state holds, so they
6439
- * could never match. Lambda code drift is best detected separately (the
6440
- * function's `CodeSha256` does live in `GetFunction` but is not what
6441
- * cdkd's `Code: { S3Bucket, S3Key }` state property carries).
6437
+ * `Code.S3Bucket` / `Code.S3Key` / `Code.S3ObjectVersion` / `Code.ZipFile`
6438
+ * are not surfaced: `GetFunction` returns a pre-signed S3 URL for the
6439
+ * deployed code, not the asset hash cdkd state holds, so they could
6440
+ * never match. Those keys are declared via `getDriftUnknownPaths` so
6441
+ * the drift comparator skips them. `Code.ImageUri` IS surfaced for
6442
+ * container Lambdas (`PackageType: 'Image'`) — AWS returns it on the
6443
+ * `GetFunction.Code.ImageUri` field, so a console-side image swap is
6444
+ * detectable as drift.
6442
6445
  *
6443
6446
  * `Tags` is surfaced from the `Tags` map on the same `GetFunction`
6444
6447
  * response. CDK's auto-injected `aws:cdk:*` tags (which AWS happily
@@ -6467,6 +6470,7 @@ var LambdaFunctionProvider = class {
6467
6470
  result["Layers"] = (cfg.Layers ?? []).map((l) => l.Arn).filter((arn) => !!arn);
6468
6471
  result["Architectures"] = cfg.Architectures ? [...cfg.Architectures] : [];
6469
6472
  if (cfg.PackageType !== void 0) result["PackageType"] = cfg.PackageType;
6473
+ if (resp.Code?.ImageUri !== void 0) result["Code"] = { ImageUri: resp.Code.ImageUri };
6470
6474
  result["TracingConfig"] = { Mode: cfg.TracingConfig?.Mode ?? "PassThrough" };
6471
6475
  if (cfg.EphemeralStorage?.Size !== void 0) result["EphemeralStorage"] = { Size: cfg.EphemeralStorage.Size };
6472
6476
  result["VpcConfig"] = {
@@ -6482,15 +6486,30 @@ var LambdaFunctionProvider = class {
6482
6486
  }
6483
6487
  }
6484
6488
  /**
6485
- * `Code: { S3Bucket, S3Key }` is set on create / update but `GetFunction`
6486
- * only returns a pre-signed URL for the deployed code, never the original
6487
- * asset key — so a state-recorded `Code` value can never match an
6488
- * AWS-current snapshot. Tell the drift comparator to skip the whole
6489
- * `Code` subtree to avoid the guaranteed false-positive that would fire
6490
- * on every clean run.
6489
+ * Lambda ZIP-package `Code` sub-paths AWS does not return on read.
6490
+ *
6491
+ * `GetFunction` returns a pre-signed S3 URL for ZIP-deployed code
6492
+ * (`Code.Location`), not the original `S3Bucket` / `S3Key` cdkd state
6493
+ * holds. `ZipFile` is inline source that AWS never echoes back. These
6494
+ * three fields are write-only via the GetFunction API (Category 1).
6495
+ *
6496
+ * `Code.ImageUri` IS recoverable — `GetFunction.Code.ImageUri` returns
6497
+ * the templated image URI for container Lambdas — so it is surfaced by
6498
+ * `readCurrentState` and NOT declared drift-unknown. `Code.SourceKMSKeyArn`
6499
+ * is also write-only on the FunctionCodeLocation read shape.
6500
+ *
6501
+ * Pre-PR this method returned the whole `['Code']` subtree as
6502
+ * drift-unknown, which also hid `Code.ImageUri` drift on container
6503
+ * Lambdas. Narrowing the skip-list re-enables that detection.
6491
6504
  */
6492
6505
  getDriftUnknownPaths() {
6493
- return ["Code"];
6506
+ return [
6507
+ "Code.S3Bucket",
6508
+ "Code.S3Key",
6509
+ "Code.S3ObjectVersion",
6510
+ "Code.ZipFile",
6511
+ "Code.SourceKMSKeyArn"
6512
+ ];
6494
6513
  }
6495
6514
  /**
6496
6515
  * Adopt an existing Lambda function into cdkd state.
@@ -43862,7 +43881,17 @@ async function localInvokeCommand(target, options) {
43862
43881
  if (stateAudit && stateAudit.unresolved.some((u) => u.key === key)) continue;
43863
43882
  logger.warn(`Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${lambda.logicalId}":{"${key}":"<literal>"}}) or pass --from-state to recover deployed values.`);
43864
43883
  }
43865
- if (options.fromState && !options.assumeRole && stateForRoleHint) suggestAssumeRoleFromState(stateForRoleHint, lambda.logicalId);
43884
+ let resolvedAssumeRoleArn;
43885
+ if (typeof options.assumeRole === "string") resolvedAssumeRoleArn = options.assumeRole;
43886
+ else if (options.assumeRole === true) if (!stateForRoleHint) logger.warn("--assume-role passed without an ARN, but no cdkd state was loaded. Pair it with --from-state, or pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.");
43887
+ else {
43888
+ const arn = resolveExecutionRoleArnFromState(stateForRoleHint, lambda.logicalId);
43889
+ if (arn) {
43890
+ resolvedAssumeRoleArn = arn;
43891
+ logger.info(`--assume-role: auto-resolved execution role from cdkd state: ${arn}`);
43892
+ } else logger.warn(`--assume-role: could not resolve the execution role ARN from cdkd state for '${lambda.logicalId}'. Pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.`);
43893
+ }
43894
+ else if (options.assumeRole === void 0 && options.fromState && stateForRoleHint) suggestAssumeRoleFromState(stateForRoleHint, lambda.logicalId);
43866
43895
  const event = await readEvent(options);
43867
43896
  const dockerEnv = {
43868
43897
  AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
@@ -43873,14 +43902,22 @@ async function localInvokeCommand(target, options) {
43873
43902
  AWS_LAMBDA_LOG_STREAM_NAME: "local",
43874
43903
  ...envResult.resolved
43875
43904
  };
43876
- if (options.assumeRole) {
43905
+ let assumeSucceeded = false;
43906
+ if (resolvedAssumeRoleArn) {
43877
43907
  const stsRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
43878
- const creds = await assumeLambdaExecutionRole(options.assumeRole, stsRegion);
43879
- dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
43880
- dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
43881
- dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
43882
- if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
43883
- } else forwardAwsEnv(dockerEnv);
43908
+ try {
43909
+ const creds = await assumeLambdaExecutionRole(resolvedAssumeRoleArn, stsRegion);
43910
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
43911
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
43912
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
43913
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
43914
+ assumeSucceeded = true;
43915
+ } catch (err) {
43916
+ const reason = err instanceof Error ? err.message : String(err);
43917
+ logger.warn(`--assume-role: STS AssumeRole(${resolvedAssumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
43918
+ }
43919
+ }
43920
+ if (!assumeSucceeded) forwardAwsEnv(dockerEnv);
43884
43921
  let debugPort;
43885
43922
  if (options.debugPort) {
43886
43923
  debugPort = Number(options.debugPort);
@@ -44171,11 +44208,10 @@ async function readStdin() {
44171
44208
  /**
44172
44209
  * Assume the Lambda execution role and return temporary credentials.
44173
44210
  *
44174
- * Q1 recommendation B (PR 1): closes the "developer has admin creds, the
44175
- * deployed function has narrow ones" skew that SAM users routinely hit.
44176
- * Off by default; opt-in via `--assume-role <arn>`. PR 2's `--from-state`
44177
- * will add auto-resolution from the template's `Role` property; for now
44178
- * the user supplies the ARN explicitly.
44211
+ * Closes the "developer has admin creds, the deployed function has narrow
44212
+ * ones" skew that SAM users routinely hit. Off by default; opt-in via
44213
+ * `--assume-role <arn>` (explicit) or `--assume-role` (bare, auto-resolved
44214
+ * from cdkd state via `resolveExecutionRoleArnFromState`).
44179
44215
  *
44180
44216
  * Mirrors the env-var-write pattern from `applyRoleArnIfSet` in
44181
44217
  * `src/utils/role-arn.ts` but writes the temp creds onto the container's
@@ -44250,26 +44286,51 @@ function materializeInlineCode(handler, source, fileExtension) {
44250
44286
  return dir;
44251
44287
  }
44252
44288
  /**
44253
- * When `--from-state` is set but `--assume-role` is not, log the function's
44254
- * deployed execution role ARN once as a hint. Helps users discover the
44255
- * scoped-credentials path without us silently auto-assuming (auto-assume
44256
- * is a future PR's scope).
44289
+ * When `--from-state` is set but `--assume-role` was NOT passed (not even
44290
+ * bare), log the function's deployed execution role ARN once as a hint.
44291
+ * This is the pre-(#442) behavior kept for backward compatibility with
44292
+ * users who don't want auto-assume. The bare-`--assume-role` path opts
44293
+ * in to actually assuming the resolved ARN; this hint path stays
44294
+ * informational only.
44257
44295
  */
44258
44296
  function suggestAssumeRoleFromState(state, logicalId) {
44259
44297
  const logger = getLogger();
44298
+ const roleArn = resolveExecutionRoleArnFromState(state, logicalId);
44299
+ if (roleArn) logger.info(`Hint: the deployed function uses execution role ${roleArn}. Re-run with --assume-role to invoke under the deployed function's narrow permissions.`);
44300
+ }
44301
+ /**
44302
+ * Resolve the execution-role ARN for a Lambda from cdkd state. Used by
44303
+ * both the `--assume-role` auto-resolve path and the legacy hint path.
44304
+ *
44305
+ * Resolution rules (mirrors the v1 scope spelled out in (#442)):
44306
+ *
44307
+ * - Literal-string `Role` starting with `arn:` → returned verbatim.
44308
+ * - `{ Fn::GetAtt: [<RoleId>, 'Arn'] }` or `{ Ref: <RoleId> }` → looked up
44309
+ * against the sibling IAM Role resource's `attributes.Arn` (recorded
44310
+ * at deploy time by `IAMRoleProvider.create` / drift refresh).
44311
+ * - Any other shape (`Fn::Sub` / `Fn::Join` / cross-stack imports) → not
44312
+ * resolved in v1; the caller surfaces a warn and falls back to dev
44313
+ * creds. Once real CDK apps emit those shapes for `Role` we can
44314
+ * extend the resolver per `feedback_verify_cdk_synth_shape_before_resolver.md`.
44315
+ *
44316
+ * Returns `undefined` when state has no entry for the Lambda, the `Role`
44317
+ * is missing entirely, the referenced sibling has no `Arn` attribute
44318
+ * captured, or the shape is one we don't try to resolve.
44319
+ *
44320
+ * Exported for unit testing.
44321
+ */
44322
+ function resolveExecutionRoleArnFromState(state, logicalId) {
44260
44323
  const lambda = state.resources[logicalId];
44261
- if (!lambda) return;
44324
+ if (!lambda) return void 0;
44262
44325
  const roleRef = lambda.properties?.["Role"] ?? lambda.observedProperties?.["Role"];
44263
- let roleArn;
44264
- if (typeof roleRef === "string" && roleRef.startsWith("arn:")) roleArn = roleRef;
44265
- else if (typeof roleRef === "object" && roleRef !== null) {
44326
+ if (typeof roleRef === "string" && roleRef.startsWith("arn:")) return roleRef;
44327
+ if (typeof roleRef === "object" && roleRef !== null) {
44266
44328
  const refLogicalId = pickReferencedLogicalId(roleRef);
44267
44329
  if (refLogicalId) {
44268
44330
  const cached = state.resources[refLogicalId]?.attributes?.["Arn"];
44269
- if (typeof cached === "string" && cached.startsWith("arn:")) roleArn = cached;
44331
+ if (typeof cached === "string" && cached.startsWith("arn:")) return cached;
44270
44332
  }
44271
44333
  }
44272
- if (roleArn) logger.info(`Hint: the deployed function uses execution role ${roleArn}. Re-run with --assume-role <that-arn> to invoke under the deployed function's narrow permissions.`);
44273
44334
  }
44274
44335
  /**
44275
44336
  * Walk a single-key intrinsic and return the referenced logical ID, or
@@ -44293,7 +44354,7 @@ function pickReferencedLogicalId(intrinsic) {
44293
44354
  */
44294
44355
  function createLocalCommand() {
44295
44356
  const local = new Command("local").description("Local execution of Lambda functions (RIE) and ECS task definitions (Docker required)");
44296
- const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role <arn>", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the \"developer admin / function narrow\" skew). Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default).")).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
44357
+ const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the \"developer admin / function narrow\" skew). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
44297
44358
  [
44298
44359
  ...commonOptions,
44299
44360
  ...appOptions,
@@ -45581,7 +45642,7 @@ function reorderArgs(argv) {
45581
45642
  */
45582
45643
  async function main() {
45583
45644
  const program = new Command();
45584
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.115.4");
45645
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.116.1");
45585
45646
  program.addCommand(createBootstrapCommand());
45586
45647
  program.addCommand(createSynthCommand());
45587
45648
  program.addCommand(createListCommand());