@go-to-k/cdkd 0.165.0 → 0.166.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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-iDMcWcre.js";
3
- import { A as S3StateBackend, B as resolveCaptureObservedState, C as assertRegionMatch, D as DagBuilder, E as DiffCalculator, F as buildDockerImage, G as CFN_TEMPLATE_BODY_LIMIT, H as resolveStateBucketWithDefault, I as Synthesizer, J as findLargeInlineResources, K as CFN_TEMPLATE_URL_LIMIT, L as getDefaultStateBucketName, M as AssetPublisher, N as stringifyValue, O as TemplateParser, P as WorkGraph, Q as resolveBucketRegion, R as getLegacyStateBucketName, S as CloudControlProvider, T as applyRoleArnIfSet, U as resolveStateBucketWithDefaultAndSource, V as resolveSkipPrefix, W as warnDeprecatedNoPrefixCliFlag, X as AssemblyReader, Y as uploadCfnTemplate, _ as matchesCdkPath, a as withRetry, at as LocalStartServiceError, b as ProviderRegistry, bt as normalizeAwsError, c as bold, ct as NestedStackChildDirectDestroyError, d as green, dt as ResourceTimeoutError, et as CdkdError, f as red, ft as ResourceUpdateNotSupportedError, g as CDK_PATH_TAG, h as collectInlinePolicyNamesManagedBySiblings, ht as StackTerminationProtectionError, i as withResourceDeadline, it as LocalMigrateError, j as shouldRetainResource, k as LockManager, l as cyan, lt as PartialFailureError, m as IAMRoleProvider, mt as StackHasActiveImportsError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, p as yellow, pt as RouteDiscoveryError, q as MIGRATE_TMP_PREFIX, r as DeployEngine, rt as LocalInvokeBuildError, s as formatResourceLine, st as MissingCdkCliError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as gray, ut as ProvisioningError, v as normalizeAwsTagsToCfn, w as IntrinsicFunctionResolver, x as findActionableSilentDrops, xt as withErrorHandling, y as resolveExplicitPhysicalId, z as resolveApp } from "./deploy-engine-DEbogepd.js";
3
+ import { A as S3StateBackend, B as resolveCaptureObservedState, C as assertRegionMatch, D as DagBuilder, E as DiffCalculator, F as buildDockerImage, G as CFN_TEMPLATE_BODY_LIMIT, H as resolveStateBucketWithDefault, I as Synthesizer, J as findLargeInlineResources, K as CFN_TEMPLATE_URL_LIMIT, L as getDefaultStateBucketName, M as AssetPublisher, N as stringifyValue, O as TemplateParser, P as WorkGraph, Q as resolveBucketRegion, R as getLegacyStateBucketName, S as CloudControlProvider, T as applyRoleArnIfSet, U as resolveStateBucketWithDefaultAndSource, V as resolveSkipPrefix, W as warnDeprecatedNoPrefixCliFlag, X as AssemblyReader, Y as uploadCfnTemplate, _ as matchesCdkPath, a as withRetry, at as LocalStartServiceError, b as ProviderRegistry, bt as normalizeAwsError, c as bold, ct as NestedStackChildDirectDestroyError, d as green, dt as ResourceTimeoutError, et as CdkdError, f as red, ft as ResourceUpdateNotSupportedError, g as CDK_PATH_TAG, h as collectInlinePolicyNamesManagedBySiblings, ht as StackTerminationProtectionError, i as withResourceDeadline, it as LocalMigrateError, j as shouldRetainResource, k as LockManager, l as cyan, lt as PartialFailureError, m as IAMRoleProvider, mt as StackHasActiveImportsError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, p as yellow, pt as RouteDiscoveryError, q as MIGRATE_TMP_PREFIX, r as DeployEngine, rt as LocalInvokeBuildError, s as formatResourceLine, st as MissingCdkCliError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as gray, ut as ProvisioningError, v as normalizeAwsTagsToCfn, w as IntrinsicFunctionResolver, x as findActionableSilentDrops, xt as withErrorHandling, y as resolveExplicitPhysicalId, z as resolveApp } from "./deploy-engine-BQkk03hJ.js";
4
4
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -449,6 +449,27 @@ function parseRecreateViaCcApiToken(value, previous) {
449
449
  }
450
450
  const recreateViaCcApiOption = new Option("--recreate-via-cc-api <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via Cloud Control API in this deploy, so a top-level CFn property cdkd would otherwise silently drop reaches AWS via CC. Repeatable — pass the flag once per resource. Per-resource opt-in (no bulk / no per-stack shortcut) so the destroy-and-recreate cost is acknowledged for each target. Stateful resource types (RDS, DynamoDB, S3, EFS, ...) refuse unless --force-stateful-recreation is ALSO passed (two-flag protection). Cannot be combined with --allow-unsupported-properties on the same resource type and property.").argParser(parseRecreateViaCcApiToken);
451
451
  /**
452
+ * Issue [#651] — `--recreate-via-sdk-provider <LogicalId>` is the reverse
453
+ * direction of `--recreate-via-cc-api`. Once a resource is sticky on
454
+ * `provisionedBy: 'cc-api'` (e.g. after a #615 SDK→CC migration, or
455
+ * because cdkd auto-routed it via the #614 default-on Cloud Control
456
+ * fallback on a fresh deploy), subsequent SDK Provider backfills
457
+ * (issue #609) do NOT auto-migrate it back — sticky semantics avoid
458
+ * SDK↔CC ping-pong on every backfill release. This flag is the
459
+ * user-initiated CC → SDK migration: destroy + recreate the named
460
+ * resource so the new copy is `provisionedBy: 'sdk'`.
461
+ *
462
+ * Symmetric to `--recreate-via-cc-api`: same per-resource explicit
463
+ * opt-in, same destroy-then-create ordering, same stateful guard,
464
+ * same multi-region refusal, same `Continue? (y/N)` interactive prompt.
465
+ */
466
+ function parseRecreateViaSdkProviderToken(value, previous) {
467
+ const token = value.trim();
468
+ if (!LOGICAL_ID_FORMAT.test(token)) throw new Error(`Invalid --recreate-via-sdk-provider value "${value}": expected a CloudFormation logical id (alphanumeric, starts with a letter, max 255 chars). One --recreate-via-sdk-provider flag per resource — repeat the flag for additional targets.`);
469
+ return [...previous ?? [], token];
470
+ }
471
+ const recreateViaSdkProviderOption = new Option("--recreate-via-sdk-provider <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via cdkd's SDK Provider in this deploy, so a resource currently sticky on provisionedBy: cc-api flips back to provisionedBy: sdk. Used after a #609 backfill release adds SDK coverage for a type the user originally needed CC for (e.g. Lambda LoggingConfig). Repeatable — pass the flag once per resource. Per-resource opt-in. Stateful resource types refuse unless --force-stateful-recreation is ALSO passed.").argParser(parseRecreateViaSdkProviderToken);
472
+ /**
452
473
  * Issue [#615] — `--force-stateful-recreation` (boolean) is the second
453
474
  * flag required to allow `--recreate-via-cc-api` to operate on a
454
475
  * stateful resource (RDS / DynamoDB / EFS / S3 with data / etc.). Two
@@ -480,6 +501,7 @@ const deployOptions = [
480
501
  allowUnsupportedTypesOption,
481
502
  allowUnsupportedPropertiesOption,
482
503
  recreateViaCcApiOption,
504
+ recreateViaSdkProviderOption,
483
505
  forceStatefulRecreationOption,
484
506
  ...resourceTimeoutOptions
485
507
  ];
@@ -1089,16 +1111,31 @@ function renderStatefulReason(reason) {
1089
1111
  */
1090
1112
  const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
1091
1113
  function validateRecreateTargets(input) {
1114
+ const seenCcApi = new Set(input.recreateViaCcApi);
1115
+ const seenSdk = new Set(input.recreateViaSdkProvider ?? []);
1116
+ const conflictingDirections = [...seenCcApi].filter((id) => seenSdk.has(id));
1092
1117
  const seen = /* @__PURE__ */ new Set();
1093
1118
  const targets = [];
1094
1119
  const unknownLogicalIds = [];
1095
1120
  const missingFromState = [];
1096
1121
  const ambiguousIntent = [];
1122
+ const ambiguousIntentSdk = [];
1097
1123
  const blockedStatefulTargets = [];
1098
1124
  const blockedMultiRegionTargets = [];
1099
- for (const logicalId of input.recreateViaCcApi) {
1125
+ const blockedAlreadySdk = [];
1126
+ const blockedNoSdkProvider = [];
1127
+ const conflictSet = new Set(conflictingDirections);
1128
+ const namedTargets = [...input.recreateViaCcApi.map((id) => ({
1129
+ logicalId: id,
1130
+ direction: "to-cc-api"
1131
+ })), ...(input.recreateViaSdkProvider ?? []).map((id) => ({
1132
+ logicalId: id,
1133
+ direction: "to-sdk"
1134
+ }))];
1135
+ for (const { logicalId, direction } of namedTargets) {
1100
1136
  if (seen.has(logicalId)) continue;
1101
1137
  seen.add(logicalId);
1138
+ if (conflictSet.has(logicalId)) continue;
1102
1139
  const templateResource = input.template.Resources?.[logicalId];
1103
1140
  if (!templateResource) {
1104
1141
  unknownLogicalIds.push(logicalId);
@@ -1114,18 +1151,30 @@ function validateRecreateTargets(input) {
1114
1151
  logicalId,
1115
1152
  resourceType,
1116
1153
  physicalId: recordedResource.physicalId,
1117
- statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties)
1154
+ statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties),
1155
+ direction
1118
1156
  };
1119
1157
  targets.push(target);
1120
1158
  if (MULTI_REGION_RECREATE_BLOCKED_TYPES.has(resourceType)) blockedMultiRegionTargets.push(target);
1121
- const actionableDrops = findActionableSilentDrops(resourceType, templateResource.Properties, EMPTY_ALLOW_SET$1);
1122
- for (const { property } of actionableDrops) {
1123
- const allowKey = `${resourceType}:${property}`;
1124
- if (input.allowUnsupportedProperties.has(allowKey)) ambiguousIntent.push({
1159
+ if (direction === "to-cc-api") {
1160
+ const actionableDrops = findActionableSilentDrops(resourceType, templateResource.Properties, EMPTY_ALLOW_SET$1);
1161
+ for (const { property } of actionableDrops) {
1162
+ const allowKey = `${resourceType}:${property}`;
1163
+ if (input.allowUnsupportedProperties.has(allowKey)) ambiguousIntent.push({
1164
+ logicalId,
1165
+ resourceType,
1166
+ property
1167
+ });
1168
+ }
1169
+ } else {
1170
+ const actionableDrops = findActionableSilentDrops(resourceType, templateResource.Properties, input.allowUnsupportedProperties);
1171
+ for (const { property } of actionableDrops) ambiguousIntentSdk.push({
1125
1172
  logicalId,
1126
1173
  resourceType,
1127
1174
  property
1128
1175
  });
1176
+ if (!(recordedResource.provisionedBy === "cc-api")) blockedAlreadySdk.push(target);
1177
+ if (input.hasSdkProvider && !input.hasSdkProvider(resourceType)) blockedNoSdkProvider.push(target);
1129
1178
  }
1130
1179
  if (target.statefulReason !== null && !input.forceStatefulRecreation) blockedStatefulTargets.push(target);
1131
1180
  }
@@ -1134,8 +1183,12 @@ function validateRecreateTargets(input) {
1134
1183
  unknownLogicalIds,
1135
1184
  missingFromState,
1136
1185
  ambiguousIntent,
1186
+ ambiguousIntentSdk,
1137
1187
  blockedStatefulTargets,
1138
- blockedMultiRegionTargets
1188
+ blockedMultiRegionTargets,
1189
+ blockedAlreadySdk,
1190
+ blockedNoSdkProvider,
1191
+ conflictingDirections
1139
1192
  };
1140
1193
  }
1141
1194
  /**
@@ -1147,16 +1200,17 @@ function validateRecreateTargets(input) {
1147
1200
  */
1148
1201
  function renderRecreateTargetsErrors(validation) {
1149
1202
  const lines = [];
1203
+ const FLAG_UMBRELLA = "--recreate-via-cc-api / --recreate-via-sdk-provider";
1150
1204
  if (validation.unknownLogicalIds.length > 0) {
1151
- lines.push(`--recreate-via-cc-api named ${validation.unknownLogicalIds.length} logical id(s) not present in the synth template:`);
1205
+ lines.push(`${FLAG_UMBRELLA} named ${validation.unknownLogicalIds.length} logical id(s) not present in the synth template:`);
1152
1206
  for (const id of validation.unknownLogicalIds) lines.push(` - ${id}`);
1153
1207
  lines.push(" Fix: confirm each id exists in the template (CDK display path is the parent; the logical id is the CFn-emitted name, e.g. cdkd synth | jq '.Resources | keys'). Recreate operates on the synth template's logical ids, not CDK display paths.");
1154
1208
  }
1155
1209
  if (validation.missingFromState.length > 0) {
1156
1210
  if (lines.length > 0) lines.push("");
1157
- lines.push(`--recreate-via-cc-api named ${validation.missingFromState.length} logical id(s) the template declares but cdkd state has no record of:`);
1211
+ lines.push(`${FLAG_UMBRELLA} named ${validation.missingFromState.length} logical id(s) the template declares but cdkd state has no record of:`);
1158
1212
  for (const id of validation.missingFromState) lines.push(` - ${id}`);
1159
- lines.push(" These are fresh CREATEs on the next deploy — recreate has nothing to destroy first. Remove the --recreate-via-cc-api flag for these resources; the new auto-route via Cloud Control (#614) handles fresh deploys.");
1213
+ lines.push(" These are fresh CREATEs on the next deploy — recreate has nothing to destroy first. Remove the flag for these resources; the auto-route via Cloud Control (#614) handles fresh deploys for silent-drop properties, and SDK Provider is the default for everything else.");
1160
1214
  }
1161
1215
  if (validation.ambiguousIntent.length > 0) {
1162
1216
  if (lines.length > 0) lines.push("");
@@ -1166,15 +1220,39 @@ function renderRecreateTargetsErrors(validation) {
1166
1220
  }
1167
1221
  if (validation.blockedStatefulTargets.length > 0) {
1168
1222
  if (lines.length > 0) lines.push("");
1169
- lines.push(`--recreate-via-cc-api would destroy + recreate ${validation.blockedStatefulTargets.length} stateful resource(s). Recreate loses ALL data — no automatic data migration. Re-run with --force-stateful-recreation to acknowledge the data-loss footgun.`);
1223
+ lines.push(`${FLAG_UMBRELLA} would destroy + recreate ${validation.blockedStatefulTargets.length} stateful resource(s). Recreate loses ALL data — no automatic data migration. Re-run with --force-stateful-recreation to acknowledge the data-loss footgun.`);
1170
1224
  for (const blocked of validation.blockedStatefulTargets) lines.push(` - ${blocked.logicalId} (${blocked.resourceType}) — ${renderStatefulReason(blocked.statefulReason)}`);
1171
1225
  }
1172
1226
  if (validation.blockedMultiRegionTargets.length > 0) {
1173
1227
  if (lines.length > 0) lines.push("");
1174
- lines.push(`--recreate-via-cc-api refuses to operate on ${validation.blockedMultiRegionTargets.length} multi-region resource(s) — out of scope for v1 of this flag (the destroy + recreate cycle across replica regions is more involved than the single-region path):`);
1228
+ lines.push(`--recreate-via-cc-api / --recreate-via-sdk-provider refuses to operate on ${validation.blockedMultiRegionTargets.length} multi-region resource(s) — out of scope for v1 of these flags (the destroy + recreate cycle across replica regions is more involved than the single-region path):`);
1175
1229
  for (const blocked of validation.blockedMultiRegionTargets) lines.push(` - ${blocked.logicalId} (${blocked.resourceType})`);
1176
1230
  lines.push(" No --force-stateful-recreation bypass — this category is structurally unsupported in v1. File an issue if you need this path.");
1177
1231
  }
1232
+ if (validation.conflictingDirections.length > 0) {
1233
+ if (lines.length > 0) lines.push("");
1234
+ lines.push(`Conflicting recreate direction — ${validation.conflictingDirections.length} logical id(s) named in BOTH --recreate-via-cc-api AND --recreate-via-sdk-provider:`);
1235
+ for (const id of validation.conflictingDirections) lines.push(` - ${id}`);
1236
+ lines.push(" Fix: pick ONE direction per resource. The two flags drive opposite provisionedBy targets ('cc-api' vs 'sdk').");
1237
+ }
1238
+ if (validation.blockedAlreadySdk.length > 0) {
1239
+ if (lines.length > 0) lines.push("");
1240
+ lines.push(`--recreate-via-sdk-provider named ${validation.blockedAlreadySdk.length} resource(s) that are NOT currently sticky on Cloud Control API (the reverse migration is a no-op):`);
1241
+ for (const blocked of validation.blockedAlreadySdk) lines.push(` - ${blocked.logicalId} (${blocked.resourceType})`);
1242
+ lines.push(" Fix: remove --recreate-via-sdk-provider <id> for these resources. They are already SDK-managed (or pre-v7 legacy state, treated as SDK).");
1243
+ }
1244
+ if (validation.blockedNoSdkProvider.length > 0) {
1245
+ if (lines.length > 0) lines.push("");
1246
+ lines.push(`--recreate-via-sdk-provider named ${validation.blockedNoSdkProvider.length} resource(s) of types cdkd has no SDK provider for (Tier 2 CC-only):`);
1247
+ for (const blocked of validation.blockedNoSdkProvider) lines.push(` - ${blocked.logicalId} (${blocked.resourceType})`);
1248
+ lines.push(" Fix: remove --recreate-via-sdk-provider <id> for these resources. The destroy + recreate would route via Cloud Control anyway — there's no SDK alternative available.");
1249
+ }
1250
+ if (validation.ambiguousIntentSdk.length > 0) {
1251
+ if (lines.length > 0) lines.push("");
1252
+ lines.push(`Inverse ambiguous intent — ${validation.ambiguousIntentSdk.length} --recreate-via-sdk-provider target(s) would IMMEDIATELY be re-routed back to Cloud Control after the recreate because their template uses silent-drop properties NOT in --allow-unsupported-properties:`);
1253
+ for (const overlap of validation.ambiguousIntentSdk) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — template uses ${overlap.property}; the default-on CC auto-route would re-route the recreated resource back to CC immediately`);
1254
+ lines.push(" Fix: pass --allow-unsupported-properties <Type>:<Prop> for each silent-drop property so the recreated resource stays on SDK with the property explicitly dropped. Or drop --recreate-via-sdk-provider — the resource already routes via CC and honors the property.");
1255
+ }
1178
1256
  return lines.length > 0 ? lines.join("\n") : null;
1179
1257
  }
1180
1258
  /**
@@ -1225,7 +1303,7 @@ async function probeStatefulRecreateTargetsAsync(targets, s3Client, logger = get
1225
1303
  });
1226
1304
  else promoted.push({ ...target });
1227
1305
  } catch (e) {
1228
- logger.warn(`--recreate-via-cc-api: live S3 probe failed for ${target.logicalId} (bucket ${target.physicalId}); leaving stateful guard at the sync result. If the bucket might be non-empty, re-run with --force-stateful-recreation. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
1306
+ logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: live S3 probe failed for ${target.logicalId} (bucket ${target.physicalId}); leaving stateful guard at the sync result. If the bucket might be non-empty, re-run with --force-stateful-recreation. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
1229
1307
  promoted.push({ ...target });
1230
1308
  }
1231
1309
  }
@@ -1347,13 +1425,18 @@ function renderDownstreamConsumers(producerStack, consumers) {
1347
1425
  async function promptRecreateConfirm(input) {
1348
1426
  if (input.targets.length === 0) return true;
1349
1427
  const logger = getLogger();
1428
+ const toCcCount = input.targets.filter((t) => t.direction === "to-cc-api").length;
1429
+ const toSdkCount = input.targets.filter((t) => t.direction === "to-sdk").length;
1350
1430
  logger.warn("");
1351
- logger.warn(`--recreate-via-cc-api will destroy + recreate ${input.targets.length} resource(s) via Cloud Control API on stack ${input.stackName}:`);
1431
+ if (toCcCount > 0 && toSdkCount > 0) logger.warn(`recreate-via-cc-api / recreate-via-sdk-provider will destroy + recreate ${input.targets.length} resource(s) on stack ${input.stackName} (${toCcCount} → Cloud Control, ${toSdkCount} → SDK Provider):`);
1432
+ else if (toCcCount > 0) logger.warn(`--recreate-via-cc-api will destroy + recreate ${toCcCount} resource(s) via Cloud Control API on stack ${input.stackName}:`);
1433
+ else logger.warn(`--recreate-via-sdk-provider will destroy + recreate ${toSdkCount} resource(s) via SDK Provider on stack ${input.stackName}:`);
1352
1434
  for (const t of input.targets) {
1353
1435
  const stateful = t.statefulReason !== null;
1354
1436
  const dataLossPrefix = stateful ? "**DATA LOSS** " : "";
1437
+ const directionTag = t.direction === "to-cc-api" ? " [SDK → CC]" : " [CC → SDK]";
1355
1438
  const stateNote = stateful ? ` — stateful (${t.statefulReason}); --force-stateful-recreation acknowledged` : "";
1356
- logger.warn(` - ${dataLossPrefix}${t.logicalId} (${t.resourceType})${stateNote}`);
1439
+ logger.warn(` - ${dataLossPrefix}${t.logicalId} (${t.resourceType})${directionTag}${stateNote}`);
1357
1440
  if (stateful) logger.warn(` DATA: all data in ${t.logicalId} will be lost (no automatic data migration)`);
1358
1441
  }
1359
1442
  if (input.downstreamConsumers && input.downstreamConsumers.length > 0) {
@@ -33834,7 +33917,8 @@ async function deployCommand(stacks, options) {
33834
33917
  }
33835
33918
  }
33836
33919
  let recreateViaCcApiTargets;
33837
- if (options.recreateViaCcApi?.length) {
33920
+ let recreateViaSdkProviderTargets;
33921
+ if (options.recreateViaCcApi?.length || options.recreateViaSdkProvider?.length) {
33838
33922
  const stateForRecreateCheck = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
33839
33923
  const validation = await probeAndRevalidateStateful({
33840
33924
  validation: validateRecreateTargets({
@@ -33847,17 +33931,20 @@ async function deployCommand(stacks, options) {
33847
33931
  outputs: {},
33848
33932
  lastModified: Date.now()
33849
33933
  },
33850
- recreateViaCcApi: options.recreateViaCcApi,
33934
+ recreateViaCcApi: options.recreateViaCcApi ?? [],
33935
+ recreateViaSdkProvider: options.recreateViaSdkProvider ?? [],
33851
33936
  allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
33852
- forceStatefulRecreation: options.forceStatefulRecreation ?? false
33937
+ forceStatefulRecreation: options.forceStatefulRecreation ?? false,
33938
+ hasSdkProvider: (rt) => stackProviderRegistry.getProviderType(rt) === "sdk"
33853
33939
  }),
33854
33940
  s3Client: stackAwsClients.s3,
33855
33941
  forceStatefulRecreation: options.forceStatefulRecreation ?? false
33856
33942
  });
33857
33943
  const errorBlock = renderRecreateTargetsErrors(validation);
33858
- if (errorBlock) throw new CdkdError(errorBlock, "RECREATE_VIA_CC_API_INVALID");
33859
- recreateViaCcApiTargets = new Set(validation.targets.map((t) => t.logicalId));
33860
- if (recreateViaCcApiTargets.size > 0) {
33944
+ if (errorBlock) throw new CdkdError(errorBlock, "RECREATE_TARGETS_INVALID");
33945
+ recreateViaCcApiTargets = new Set(validation.targets.filter((t) => t.direction === "to-cc-api").map((t) => t.logicalId));
33946
+ recreateViaSdkProviderTargets = new Set(validation.targets.filter((t) => t.direction === "to-sdk").map((t) => t.logicalId));
33947
+ if (recreateViaCcApiTargets.size > 0 || recreateViaSdkProviderTargets.size > 0) {
33861
33948
  const downstreamConsumers = await findDownstreamConsumers({
33862
33949
  producerStack: stackInfo.stackName,
33863
33950
  producerRegion: stackRegion,
@@ -33877,6 +33964,7 @@ async function deployCommand(stacks, options) {
33877
33964
  dryRun: options.dryRun,
33878
33965
  noRollback: !options.rollback,
33879
33966
  ...recreateViaCcApiTargets && recreateViaCcApiTargets.size > 0 && { recreateViaCcApiTargets },
33967
+ ...recreateViaSdkProviderTargets && recreateViaSdkProviderTargets.size > 0 && { recreateViaSdkProviderTargets },
33880
33968
  captureObservedState: resolveCaptureObservedState(options.captureObservedState),
33881
33969
  ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
33882
33970
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
@@ -57201,6 +57289,7 @@ async function localRunTaskCommand(target, options) {
57201
57289
  resolvedRoleArn = options.assumeTaskRole;
57202
57290
  assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region);
57203
57291
  }
57292
+ const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
57204
57293
  const envOverrides = readEnvOverridesFile$2(options.envVars);
57205
57294
  const runOpts = {
57206
57295
  cluster: options.cluster,
@@ -57210,7 +57299,7 @@ async function localRunTaskCommand(target, options) {
57210
57299
  detach: options.detach
57211
57300
  };
57212
57301
  if (envOverrides) runOpts.envOverrides = envOverrides;
57213
- if (assumedCredentials) runOpts.taskCredentials = assumedCredentials;
57302
+ if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
57214
57303
  if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
57215
57304
  if (options.platform) runOpts.platformOverride = options.platform;
57216
57305
  if (options.region) runOpts.region = options.region;
@@ -57357,6 +57446,29 @@ function readEnvOverridesFile$2(filePath) {
57357
57446
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
57358
57447
  return parsed;
57359
57448
  }
57449
+ /**
57450
+ * Issue #658: pick the credentials forwarded to the AWS-published
57451
+ * `amazon-ecs-local-container-endpoints` sidecar. Precedence:
57452
+ * 1. `--assume-task-role <arn>` (or bare `--assume-task-role` against
57453
+ * a resolvable `TaskRoleArn`) → STS-assumed temp creds. Highest
57454
+ * priority — when the user opted in to IAM emulation, those creds
57455
+ * drive the sidecar regardless of `--profile`.
57456
+ * 2. `--profile <p>` → resolved via {@link resolveProfileCredentials}
57457
+ * (the SDK's default credential provider chain — SSO / IAM
57458
+ * Identity Center / fromIni / role-assumption). NEW in this PR.
57459
+ * 3. Neither set → `undefined`; the sidecar runs with its own
57460
+ * default credential chain (typically empty inside a fresh
57461
+ * container — user containers will get 4xx from the credentials
57462
+ * endpoint, mimicking IAM-misconfigured prod).
57463
+ *
57464
+ * Extracted as an exported helper so a unit test can exercise every
57465
+ * branch without having to mock the full Synth + Docker + AWS pipeline
57466
+ * (the strategy PR #655 used for the Lambda container path).
57467
+ */
57468
+ async function resolveSidecarCredentials(options, assumedCredentials) {
57469
+ if (assumedCredentials) return assumedCredentials;
57470
+ if (options.profile) return resolveProfileCredentials(options.profile);
57471
+ }
57360
57472
  function createLocalRunTaskCommand() {
57361
57473
  const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition 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 function 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 (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).action(withErrorHandling(localRunTaskCommand));
57362
57474
  [
@@ -58487,11 +58599,13 @@ async function localStartServiceCommand(targets, options) {
58487
58599
  for (const w of index.warnings) logger.warn(w);
58488
58600
  }
58489
58601
  const registry = new CloudMapRegistry();
58602
+ const sidecarCredentials = await resolveSharedSidecarCredentials(options);
58490
58603
  try {
58491
58604
  sharedNetwork = await createSharedSvcNetwork({
58492
58605
  prefix: options.cluster,
58493
58606
  skipPull,
58494
- cluster: options.cluster
58607
+ cluster: options.cluster,
58608
+ ...sidecarCredentials !== void 0 && { credentials: sidecarCredentials }
58495
58609
  });
58496
58610
  } catch (err) {
58497
58611
  throw new LocalStartServiceError(`Failed to create shared service network: ${err instanceof Error ? err.message : String(err)}`);
@@ -58731,6 +58845,34 @@ function parseRestartPolicy(raw) {
58731
58845
  if (raw === "on-failure" || raw === "always" || raw === "none") return raw;
58732
58846
  throw new LocalStartServiceError(`--restart-policy must be one of 'on-failure', 'always', or 'none' (got '${raw}').`);
58733
58847
  }
58848
+ /**
58849
+ * Issue #658: pick the credentials forwarded to the AWS-published
58850
+ * `amazon-ecs-local-container-endpoints` sidecar. `cdkd local
58851
+ * start-service`'s sidecar is SHARED across every replica boot in one
58852
+ * CLI invocation (design § 5 Option A), so this resolves ONCE at
58853
+ * startup. Precedence:
58854
+ * 1. `--profile <p>` → resolved via {@link resolveProfileCredentials}
58855
+ * (the SDK's default credential provider chain — SSO / IAM
58856
+ * Identity Center / fromIni / role-assumption). NEW in this PR.
58857
+ * 2. Not set → `undefined`; the sidecar runs with its own default
58858
+ * credential chain (typically empty inside a fresh container —
58859
+ * user containers will get 4xx from the credentials endpoint).
58860
+ *
58861
+ * Note: per-service `--assume-task-role <Service>=<arn>` overrides are
58862
+ * INTENTIONALLY NOT consulted here. The shared sidecar has no concept
58863
+ * of per-service IAM — per-service `TaskRoleArn` flows into each
58864
+ * container's env via `buildMetadataEnv` at boot time, where the
58865
+ * sidecar's `/role/<role-arn>` path resolves per-request. The shared
58866
+ * sidecar's OWN startup credentials govern only the fallback path
58867
+ * (containers that did not bind a `TaskRoleArn`).
58868
+ *
58869
+ * Extracted as an exported helper so a unit test can exercise both
58870
+ * branches without having to mock the full Synth + Docker + AWS
58871
+ * pipeline (the strategy PR #655 used for the Lambda container path).
58872
+ */
58873
+ async function resolveSharedSidecarCredentials(options) {
58874
+ if (options.profile) return resolveProfileCredentials(options.profile);
58875
+ }
58734
58876
  function createLocalStartServiceCommand() {
58735
58877
  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("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).action(withErrorHandling(localStartServiceCommand));
58736
58878
  [
@@ -60619,7 +60761,7 @@ function reorderArgs(argv) {
60619
60761
  */
60620
60762
  async function main() {
60621
60763
  const program = new Command();
60622
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.165.0");
60764
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.166.1");
60623
60765
  program.addCommand(createBootstrapCommand());
60624
60766
  program.addCommand(createSynthCommand());
60625
60767
  program.addCommand(createListCommand());