@go-to-k/cdkd 0.147.1 → 0.147.2

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
@@ -36647,9 +36647,14 @@ async function injectRetainPoliciesRecursiveInternal(templateBody, cfnStackName,
36647
36647
  };
36648
36648
  }
36649
36649
  /**
36650
- * Recursive variant of {@link getCloudFormationResourceMapping} that returns
36651
- * the full nested-stack tree rooted at `rootStackName`. For each
36652
- * `AWS::CloudFormation::Stack` row in the root's resources, recursively
36650
+ * Walk a CloudFormation stack and every nested child recursively, returning
36651
+ * the full {@link CfnStackResourceTree} rooted at `rootStackName`. Used by
36652
+ * `cdkd import --migrate-from-cloudformation` to drive BOTH per-child state
36653
+ * writes AND the recursive `injectRetainPoliciesRecursive` walk — both
36654
+ * consumers share this single `DescribeStackResources` tree so an
36655
+ * arbitrarily-deep nesting only costs one round-trip per stack.
36656
+ *
36657
+ * For each `AWS::CloudFormation::Stack` row in the root's resources, recursively
36653
36658
  * calls `DescribeStackResources(<child ARN>)` (AWS accepts the ARN as
36654
36659
  * `StackName`) to populate the child node, and so on to arbitrary depth.
36655
36660
  *
@@ -39498,6 +39503,7 @@ function extractTaskDefinitionProperties(stack, logicalId, resource, context) {
39498
39503
  if (rawNetworkMode === "bridge" || rawNetworkMode === "awsvpc" || rawNetworkMode === "host" || rawNetworkMode === "none") networkMode = rawNetworkMode;
39499
39504
  else throw new EcsTaskResolutionError(`Task definition '${logicalId}' has unsupported NetworkMode '${rawNetworkMode}'. Supported values: bridge / awsvpc / host / none.`);
39500
39505
  if (networkMode === "awsvpc") warnings.push(`NetworkMode 'awsvpc' on '${logicalId}' is mapped to docker bridge locally — docker cannot emulate ENI-per-task. AWS SDK calls still reach public endpoints via the developer network.`);
39506
+ if (props["ProxyConfiguration"]) warnings.push(`Task definition '${logicalId}' declares 'ProxyConfiguration' (custom Envoy / App Mesh bootstrap), which is NOT honored by 'cdkd local start-service' / 'cdkd local run-task' in v1. Local execution will run without the configured proxy. See design doc § 2 for the rationale.`);
39501
39507
  const resources = stack.template.Resources ?? {};
39502
39508
  const subContext = buildSubstitutionContextFromImageContext(context);
39503
39509
  const taskRoleArn = resolveRoleArn(props["TaskRoleArn"], resources, subContext);
@@ -52066,7 +52072,7 @@ function extractServiceProperties(stack, serviceLogicalId, resource, stacks, con
52066
52072
  desiredCount,
52067
52073
  healthCheckGracePeriodSeconds,
52068
52074
  task,
52069
- serviceRegistries: extractServiceRegistries(props["ServiceRegistries"]),
52075
+ serviceRegistries: extractServiceRegistries(props["ServiceRegistries"], serviceLogicalId, warnings),
52070
52076
  warnings
52071
52077
  };
52072
52078
  if (serviceConnect) out.serviceConnect = serviceConnect;
@@ -52139,8 +52145,16 @@ function extractServiceConnect(raw, task) {
52139
52145
  * canonical `Fn::GetAtt: [<CloudMapServiceLogicalId>, 'Arn']` shape;
52140
52146
  * cdkd surfaces the logical id (the AWS-side ARN is irrelevant
52141
52147
  * locally — the registry is in-process).
52148
+ *
52149
+ * Issue #544 — entries with a literal-string `RegistryArn` (rare
52150
+ * locally — would imply the user bound to an existing Cloud Map
52151
+ * service deployed out-of-band) are skipped with a warning, since the
52152
+ * in-process registry cannot resolve an external Cloud Map service
52153
+ * back to its `(namespace, name)` pair. Pre-fix this was a silent
52154
+ * `continue` and the user got no feedback about why the registration
52155
+ * didn't show up.
52142
52156
  */
52143
- function extractServiceRegistries(raw) {
52157
+ function extractServiceRegistries(raw, serviceLogicalId, warnings) {
52144
52158
  if (!Array.isArray(raw)) return [];
52145
52159
  const out = [];
52146
52160
  for (const entry of raw) {
@@ -52148,7 +52162,10 @@ function extractServiceRegistries(raw) {
52148
52162
  const e = entry;
52149
52163
  const registryArn = e["RegistryArn"];
52150
52164
  let cloudMapServiceLogicalId;
52151
- if (typeof registryArn === "string") continue;
52165
+ if (typeof registryArn === "string") {
52166
+ warnings.push(`ECS Service '${serviceLogicalId}' ServiceRegistries[] entry has a literal-string RegistryArn ('${registryArn}'); cdkd cannot resolve external Cloud Map services locally. Skipping this registration; peer services will not discover this endpoint through the in-process registry. Use Fn::GetAtt: [<CloudMapServiceLogicalId>, "Arn"] instead so cdkd can resolve the namespace + service name from the synthesized template.`);
52167
+ continue;
52168
+ }
52152
52169
  if (registryArn && typeof registryArn === "object" && !Array.isArray(registryArn)) {
52153
52170
  const getAtt = registryArn["Fn::GetAtt"];
52154
52171
  if (Array.isArray(getAtt) && typeof getAtt[0] === "string") cloudMapServiceLogicalId = getAtt[0];
@@ -52329,6 +52346,38 @@ function backoffDelayMs(restartCount) {
52329
52346
  return Math.min(1e3 * Math.pow(2, Math.min(restartCount, 10)), 3e4);
52330
52347
  }
52331
52348
  /**
52349
+ * Maximum number of replica indices the per-replica subnet allocator
52350
+ * can serve without modulo-wrap collision. The allocator below walks
52351
+ * the link-local /24 range `169.254.170.0..169.254.253.0` (84 octets)
52352
+ * and **skips 171** because that octet is owned by the shared-service
52353
+ * network in design § 5 Option A (see `SHARED_SVC_SUBNET_OCTET`), so
52354
+ * the usable count is 83. The CLI's `--max-tasks` parser enforces this
52355
+ * cap before any boot work fires.
52356
+ */
52357
+ const SUBNET_ALLOCATOR_RANGE = 83;
52358
+ /**
52359
+ * Defensive per-replica subnet octet allocator (Issue #544). Only used
52360
+ * when callers bypass the CLI's `sharedNetwork` construction — i.e.
52361
+ * test paths that hand-build `ServiceRunnerOptions.discovery` without
52362
+ * `sharedNetwork`, or the bare `cdkd local run-task`-shaped path that
52363
+ * runs one network per task. Production `cdkd local start-service`
52364
+ * runs always go through the shared network (design § 5 Option A) so
52365
+ * this allocator is unreachable in the standard path.
52366
+ *
52367
+ * Returns the second-from-last octet of the per-replica /24 (170 →
52368
+ * `169.254.170.0/24`). Walks the 83-slot output range
52369
+ * `[170, 172, 173, ..., 253]` — 171 is intentionally **skipped**
52370
+ * because it's reserved for the shared-service network sidecar
52371
+ * (`SHARED_SVC_SUBNET_OCTET`), and assigning a per-replica network
52372
+ * the same /24 would have docker reject the duplicate-subnet
52373
+ * `network create` with the cryptic "Pool overlaps with other one on
52374
+ * this address space" error.
52375
+ */
52376
+ function pickSubnetOctet(index) {
52377
+ const candidate = 170 + (index % 83 + 83) % 83;
52378
+ return candidate < 171 ? candidate : candidate + 1;
52379
+ }
52380
+ /**
52332
52381
  * Decide whether a replica that just exited should restart. Pure-
52333
52382
  * functional so the watcher loop's policy is easy to unit-test.
52334
52383
  */
@@ -52507,7 +52556,7 @@ async function bootReplica(service, options, instance) {
52507
52556
  ...options.taskOptions,
52508
52557
  cluster: perReplicaCluster,
52509
52558
  detach: true,
52510
- ...sharedNetwork ? { existingNetwork: sharedNetwork } : { subnetOctet: 170 + instance.index % 84 },
52559
+ ...sharedNetwork ? { existingNetwork: sharedNetwork } : { subnetOctet: pickSubnetOctet(instance.index) },
52511
52560
  ...addHostFlags.length > 0 ? { addHostFlags } : {},
52512
52561
  ...networkAliasesByContainer.size > 0 ? { networkAliasesByContainer } : {}
52513
52562
  };
@@ -52709,8 +52758,13 @@ var CloudMapRegistry = class {
52709
52758
  /**
52710
52759
  * Map<alias, fqdn> — secondary index for ClientAlias short forms.
52711
52760
  * Multiple aliases can point at the same fqdn; one alias only points
52712
- * at one fqdn (last write wins, with a warn surfaced by the resolver
52713
- * when a cross-namespace collision is detected — see design §O6).
52761
+ * at one fqdn. **First-wins on collision** consistent with design
52762
+ * §O6's namespace-level first-wins rule for `PrivateDnsNamespace`. A
52763
+ * collision (two services declaring the same `ClientAlias.DnsName`)
52764
+ * emits a `logger.warn` so users can debug "why does
52765
+ * `wget http://api/` reach service B instead of service A" without
52766
+ * silently shadowing the prior binding. Idempotent re-register of
52767
+ * the same `(alias, targetFqdn)` pair is a no-op and does NOT warn.
52714
52768
  */
52715
52769
  aliasIndex = /* @__PURE__ */ new Map();
52716
52770
  /**
@@ -52746,8 +52800,13 @@ var CloudMapRegistry = class {
52746
52800
  * suffix). Cloud Map / Service Connect does NOT auto-create such
52747
52801
  * aliases — they're populated by `ClientAliases[].DnsName` entries in
52748
52802
  * the consumer service's `ServiceConnectConfiguration`. Aliases are
52749
- * scoped per-CLI-invocation and last-write-wins on collision (the
52750
- * resolver surfaces a `warn` when this happens — see §O6).
52803
+ * scoped per-CLI-invocation and **first-wins on collision**
52804
+ * consistent with design §O6's namespace-level first-wins rule. The
52805
+ * first registration sticks; later attempts to bind the same alias
52806
+ * to a different fqdn are ignored and a `logger.warn` is emitted so
52807
+ * users can debug "why does `wget http://api/` reach service B
52808
+ * instead of service A". Re-registering the same `(alias,
52809
+ * targetFqdn)` pair is idempotent and does NOT warn.
52751
52810
  *
52752
52811
  * @param alias The bare discovery name (e.g. `orders` for an alias to
52753
52812
  * `orders.cdkd-local.local`).
@@ -52757,6 +52816,12 @@ var CloudMapRegistry = class {
52757
52816
  registerAlias(alias, targetFqdn) {
52758
52817
  if (!alias) throw new Error("CloudMapRegistry.registerAlias: alias must be a non-empty string.");
52759
52818
  if (!targetFqdn) throw new Error("CloudMapRegistry.registerAlias: targetFqdn must be a non-empty string.");
52819
+ const existing = this.aliasIndex.get(alias);
52820
+ if (existing !== void 0) {
52821
+ if (existing === targetFqdn) return;
52822
+ getLogger().child("cloud-map-registry").warn(`ClientAlias DnsName collision: '${alias}' was already mapped to '${existing}'; keeping first-wins binding and ignoring new mapping to '${targetFqdn}'. Likely cause: two Service Connect services declared the same ClientAlias.DnsName.`);
52823
+ return;
52824
+ }
52760
52825
  this.aliasIndex.set(alias, targetFqdn);
52761
52826
  }
52762
52827
  /**
@@ -53002,6 +53067,7 @@ async function localStartServiceCommand(targets, options) {
53002
53067
  const logger = getLogger();
53003
53068
  if (options.verbose) logger.setLevel("debug");
53004
53069
  warnIfDeprecatedRegion(options);
53070
+ const skipPull = options.pull === false;
53005
53071
  if (!targets || targets.length === 0) throw new LocalStartServiceError("cdkd local start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend'.");
53006
53072
  const perTarget = targets.map((t) => ({
53007
53073
  target: t,
@@ -53058,7 +53124,7 @@ async function localStartServiceCommand(targets, options) {
53058
53124
  try {
53059
53125
  sharedNetwork = await createSharedSvcNetwork({
53060
53126
  prefix: options.cluster,
53061
- skipPull: options.pull === false,
53127
+ skipPull,
53062
53128
  cluster: options.cluster
53063
53129
  });
53064
53130
  } catch (err) {
@@ -53080,7 +53146,7 @@ async function localStartServiceCommand(targets, options) {
53080
53146
  };
53081
53147
  process.on("SIGINT", sigintHandler);
53082
53148
  process.on("SIGTERM", sigintHandler);
53083
- for (const pt of perTarget) pt.controller = await bootOneTarget(pt.target, pt.runState, stacks, options, discovery);
53149
+ for (const pt of perTarget) pt.controller = await bootOneTarget(pt.target, pt.runState, stacks, options, discovery, skipPull);
53084
53150
  const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
53085
53151
  logger.info(`Service(s) running: ${summary}. Press ^C to shut down.`);
53086
53152
  await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
@@ -53098,7 +53164,7 @@ async function localStartServiceCommand(targets, options) {
53098
53164
  * options) is scoped locally. Returns the started controller for the
53099
53165
  * outer code to wait + tear down.
53100
53166
  */
53101
- async function bootOneTarget(target, runState, stacks, options, discovery) {
53167
+ async function bootOneTarget(target, runState, stacks, options, discovery, skipPull) {
53102
53168
  const logger = getLogger();
53103
53169
  const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
53104
53170
  const service = resolveEcsServiceTarget(target, stacks, imageContext);
@@ -53141,7 +53207,7 @@ async function bootOneTarget(target, runState, stacks, options, discovery) {
53141
53207
  const taskOpts = {
53142
53208
  cluster: options.cluster,
53143
53209
  containerHost: options.containerHost,
53144
- skipPull: options.pull === false,
53210
+ skipPull,
53145
53211
  keepRunning: false,
53146
53212
  detach: true
53147
53213
  };
@@ -53277,22 +53343,26 @@ function parsePositiveInt(raw, flagName) {
53277
53343
  }
53278
53344
  /**
53279
53345
  * Hard cap on `--max-tasks` driven by the per-replica subnet allocator
53280
- * in `ecs-service-runner.ts:bootReplica` (`170 + (index % 84)`). The
53281
- * `% 84` modulo wraps at index 84, collapsing replica 84's `/24` onto
53282
- * replica 0's allocation. Docker rejects the duplicate-subnet network
53283
- * creation with a cryptic "Pool overlaps with other one on this address
53284
- * space" error 30s into the boot by which time some early replicas
53285
- * may have spent docker-run budget. Reject at parse time so the user
53346
+ * in `ecs-service-runner.ts:pickSubnetOctet`. The allocator walks the
53347
+ * link-local /24 range `169.254.170.0..169.254.253.0` and **skips 171**
53348
+ * because that octet is owned by the shared-service network
53349
+ * (`SHARED_SVC_SUBNET_OCTET`) assigning a per-replica network the
53350
+ * same /24 would have docker reject the duplicate-subnet `network
53351
+ * create`. The usable count is therefore 83 (Issue #544); beyond
53352
+ * that, the modulo-wrap collapses replica N's `/24` onto replica 0's
53353
+ * allocation and docker rejects the duplicate-subnet network creation
53354
+ * with a cryptic "Pool overlaps with other one on this address space"
53355
+ * error 30s into the boot — by which time some early replicas may
53356
+ * have spent docker-run budget. Reject at parse time so the user
53286
53357
  * gets an actionable error before any boot work fires.
53287
53358
  *
53288
- * 84 is the count of usable link-local /24 octets in the range
53289
- * `169.254.170.0..169.254.253.0` (255 reserved for broadcast). Raising
53290
- * this requires extending the allocator to walk a different IP range.
53359
+ * Raising this requires extending the allocator to walk a different
53360
+ * IP range.
53291
53361
  */
53292
- const MAX_TASKS_SUBNET_RANGE_CAP = 84;
53362
+ const MAX_TASKS_SUBNET_RANGE_CAP = 83;
53293
53363
  function parseMaxTasks(raw) {
53294
53364
  const parsed = parsePositiveInt(raw, "--max-tasks");
53295
- if (parsed > 84) throw new LocalStartServiceError(`--max-tasks ${parsed} exceeds the per-replica link-local /24 subnet allocator's range (${84}). The allocator in ecs-service-runner.ts assigns each replica its own 169.254.x.0/24 from the range 169.254.170.0..169.254.253.0; replica indices >= ${84} would collide with earlier replicas via modulo wrap. Lower --max-tasks to <= ${84}, or accept reduced local concurrency for high-DesiredCount services.`);
53365
+ if (parsed > 83) throw new LocalStartServiceError(`--max-tasks ${parsed} exceeds the per-replica link-local /24 subnet allocator's range (${83}). The allocator in ecs-service-runner.ts assigns each replica its own 169.254.x.0/24 from the range 169.254.170.0..169.254.253.0; replica indices >= ${83} would collide with earlier replicas via modulo wrap. Lower --max-tasks to <= ${83}, or accept reduced local concurrency for high-DesiredCount services.`);
53296
53366
  return parsed;
53297
53367
  }
53298
53368
  function parseRestartPolicy(raw) {
@@ -53300,7 +53370,7 @@ function parseRestartPolicy(raw) {
53300
53370
  throw new LocalStartServiceError(`--restart-policy must be one of 'on-failure', 'always', or 'none' (got '${raw}').`);
53301
53371
  }
53302
53372
  function createLocalStartServiceCommand() {
53303
- const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${84} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes.").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(localStartServiceCommand));
53373
+ const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes.").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(localStartServiceCommand));
53304
53374
  [
53305
53375
  ...commonOptions,
53306
53376
  ...appOptions,
@@ -56614,7 +56684,7 @@ function reorderArgs(argv) {
56614
56684
  */
56615
56685
  async function main() {
56616
56686
  const program = new Command();
56617
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.147.1");
56687
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.147.2");
56618
56688
  program.addCommand(createBootstrapCommand());
56619
56689
  program.addCommand(createSynthCommand());
56620
56690
  program.addCommand(createListCommand());