@go-to-k/cdkd 0.162.3 → 0.163.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1004,9 +1004,12 @@ const MULTI_REGION_RECREATE_BLOCKED_TYPES = new Set(["AWS::DynamoDB::GlobalTable
1004
1004
  * Cheap, synchronous read of the resource's recorded properties only.
1005
1005
  * For `AWS::S3::Bucket` this returns `null` — the live `ListObjectsV2`
1006
1006
  * probe to distinguish empty buckets (safe to recreate) from
1007
- * non-empty (data loss) needs an S3 client + an AWS round-trip and is
1008
- * deferred to a follow-up issue (v1 sync-defers; an `--force-stateful-
1009
- * recreation` is recommended for any potentially-non-empty S3 target).
1007
+ * non-empty (data loss) lives in
1008
+ * `src/deployment/recreate-targets.ts#probeStatefulRecreateTargetsAsync`
1009
+ * (issue [#648]) and runs after this sync first-cut. Sync callers can
1010
+ * still treat `null` as "not stateful" — the deploy command does both
1011
+ * passes back-to-back; only callers that explicitly opt out of the
1012
+ * async probe need to assume conservative "stateful" semantics.
1010
1013
  *
1011
1014
  * Returns the {@link StatefulReason} when the type is stateful (or
1012
1015
  * `null` for non-stateful types).
@@ -1038,11 +1041,47 @@ function renderStatefulReason(reason) {
1038
1041
  //#endregion
1039
1042
  //#region src/deployment/recreate-targets.ts
1040
1043
  /**
1044
+ * Pre-flight validation for `--recreate-via-cc-api <LogicalId>` deploy
1045
+ * flag (issue [#615]).
1046
+ *
1047
+ * Three things to validate before the deploy engine acts on the user's
1048
+ * recreate list:
1049
+ *
1050
+ * 1. Every named logical id MUST exist in the synth template. A typo
1051
+ * should fail fast, not silently skip.
1052
+ * 2. Every named logical id MUST exist in cdkd state (the recreate
1053
+ * operation requires an existing physical resource to destroy +
1054
+ * recreate). A logical id in the template but absent from state
1055
+ * is a CREATE on the next deploy regardless — recreate is a
1056
+ * no-op for fresh deploys and should error out with a clear
1057
+ * message rather than silently apply.
1058
+ * 3. Stateful-resource guard: every named target whose resource type
1059
+ * is in {@link STATEFUL_TYPES} (or conditionally stateful — S3
1060
+ * bucket with objects, LogGroup with retention) MUST be matched
1061
+ * by an explicit `--force-stateful-recreation` flag. The sync
1062
+ * first-cut runs from the recorded properties alone; the live
1063
+ * `s3:ListObjectsV2` probe (issue [#648]) promotes a `null`
1064
+ * reason to `'has-objects'` when a bucket actually contains data.
1065
+ * 4. Multi-region refusal: every named target whose resource type
1066
+ * is in {@link MULTI_REGION_RECREATE_BLOCKED_TYPES} (e.g.
1067
+ * `AWS::DynamoDB::GlobalTable`) is refused outright. Out of
1068
+ * scope for v1; no `--force-stateful-recreation` bypass since
1069
+ * this is a structural limitation, not a data-loss footgun.
1070
+ *
1071
+ * Plus one cross-flag invariant: `--recreate-via-cc-api MyLambda`
1072
+ * combined with `--allow-unsupported-properties AWS::Lambda::Function:LoggingConfig`
1073
+ * on a resource whose template carries `LoggingConfig` is **ambiguous
1074
+ * intent** — does the user want SDK + silent drop, or CC migration?
1075
+ * Fail fast and let the user pick one strategy per resource.
1076
+ */
1077
+ /**
1041
1078
  * Plan-time validation of the user's recreate-via-cc-api list.
1042
1079
  *
1043
- * Pure with respect to AWS — does NOT probe S3 bucket emptiness. The
1044
- * S3 conditional check is deferred to a follow-up issue (the live
1045
- * `s3:ListObjectsV2` probe is out of scope for v1).
1080
+ * Pure with respect to AWS — does NOT probe S3 bucket emptiness. Wrap
1081
+ * the result with {@link probeAndRevalidateStateful} to promote S3
1082
+ * targets' `statefulReason` via a live `s3:ListObjectsV2` round-trip
1083
+ * before rendering errors. The deploy command does this; the validator
1084
+ * itself stays sync so unit tests don't need an S3 mock.
1046
1085
  *
1047
1086
  * Input order is preserved; duplicate logical ids in the user's input
1048
1087
  * are deduplicated.
@@ -1137,6 +1176,84 @@ function renderRecreateTargetsErrors(validation) {
1137
1176
  }
1138
1177
  return lines.length > 0 ? lines.join("\n") : null;
1139
1178
  }
1179
+ /**
1180
+ * Async S3 object probe (issue [#648]).
1181
+ *
1182
+ * For every `AWS::S3::Bucket` target whose sync {@link StatefulReason}
1183
+ * is `null` (the sync map defers — see {@link isStatefulRecreateTargetSync}),
1184
+ * issues a single-page `ListObjectVersions(MaxKeys=1)` against the
1185
+ * bucket's recorded physical id. When the bucket has at least one
1186
+ * current object, prior version, OR delete-marker, promotes the
1187
+ * target's `statefulReason` to `'has-objects'`.
1188
+ *
1189
+ * Uses `ListObjectVersions` rather than `ListObjectsV2` so the probe
1190
+ * mirrors the s3-bucket-provider's `emptyBucket` view: a versioned
1191
+ * bucket whose current keys have all been soft-deleted (so
1192
+ * `ListObjectsV2.KeyCount === 0`) still holds prior versions +
1193
+ * delete-markers that the destroy + recreate cycle would lose. Using
1194
+ * the same listing API as the provider ensures the probe and the
1195
+ * destroy path agree on "empty".
1196
+ *
1197
+ * **Soft-fail on probe errors**: if `ListObjectVersions` throws
1198
+ * (permission denied, bucket-not-found mid-flight, transient network
1199
+ * error), logs a warn and leaves the target's `statefulReason` at the
1200
+ * sync result (`null`). The user can decide to proceed without the
1201
+ * probe by passing `--force-stateful-recreation`.
1202
+ *
1203
+ * Returns a NEW array of targets; the input is not mutated. Non-S3
1204
+ * targets and S3 targets whose sync reason is already non-null are
1205
+ * passed through unchanged.
1206
+ */
1207
+ async function probeStatefulRecreateTargetsAsync(targets, s3Client, logger = getLogger().child("recreate-targets")) {
1208
+ const promoted = [];
1209
+ for (const target of targets) {
1210
+ if (target.resourceType !== "AWS::S3::Bucket" || target.statefulReason !== null) {
1211
+ promoted.push({ ...target });
1212
+ continue;
1213
+ }
1214
+ try {
1215
+ const result = await s3Client.send(new ListObjectVersionsCommand({
1216
+ Bucket: target.physicalId,
1217
+ MaxKeys: 1
1218
+ }));
1219
+ const hasVersions = (result.Versions?.length ?? 0) > 0;
1220
+ const hasDeleteMarkers = (result.DeleteMarkers?.length ?? 0) > 0;
1221
+ if (hasVersions || hasDeleteMarkers) promoted.push({
1222
+ ...target,
1223
+ statefulReason: "has-objects"
1224
+ });
1225
+ else promoted.push({ ...target });
1226
+ } catch (e) {
1227
+ 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)}`);
1228
+ promoted.push({ ...target });
1229
+ }
1230
+ }
1231
+ return promoted;
1232
+ }
1233
+ /**
1234
+ * Async re-validation of the stateful-guard slice of a
1235
+ * {@link RecreateTargetsValidation}, after promoting S3 bucket reasons
1236
+ * via {@link probeStatefulRecreateTargetsAsync}.
1237
+ *
1238
+ * Skips the probe entirely when `forceStatefulRecreation: true` — the
1239
+ * sync validation already omits the blocked list in that case, and
1240
+ * skipping avoids an unnecessary AWS round-trip (plus permission-denied
1241
+ * warn-and-skip cycle on low-privilege CI roles).
1242
+ *
1243
+ * Returns a NEW validation; the input is not mutated. Non-stateful
1244
+ * categories (`unknownLogicalIds` / `missingFromState` /
1245
+ * `ambiguousIntent` / `blockedMultiRegionTargets`) are preserved verbatim.
1246
+ */
1247
+ async function probeAndRevalidateStateful(input) {
1248
+ if (input.forceStatefulRecreation) return input.validation;
1249
+ const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.s3Client);
1250
+ const blockedStatefulTargets = promoted.filter((t) => t.statefulReason !== null);
1251
+ return {
1252
+ ...input.validation,
1253
+ targets: promoted,
1254
+ blockedStatefulTargets
1255
+ };
1256
+ }
1140
1257
 
1141
1258
  //#endregion
1142
1259
  //#region src/state/export-index-store.ts
@@ -33597,18 +33714,22 @@ async function deployCommand(stacks, options) {
33597
33714
  let recreateViaCcApiTargets;
33598
33715
  if (options.recreateViaCcApi?.length) {
33599
33716
  const stateForRecreateCheck = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
33600
- const validation = validateRecreateTargets({
33601
- template: stackInfo.template,
33602
- state: stateForRecreateCheck?.state ?? {
33603
- version: 7,
33604
- stackName: stackInfo.stackName,
33605
- region: stackRegion,
33606
- resources: {},
33607
- outputs: {},
33608
- lastModified: Date.now()
33609
- },
33610
- recreateViaCcApi: options.recreateViaCcApi,
33611
- allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
33717
+ const validation = await probeAndRevalidateStateful({
33718
+ validation: validateRecreateTargets({
33719
+ template: stackInfo.template,
33720
+ state: stateForRecreateCheck?.state ?? {
33721
+ version: 7,
33722
+ stackName: stackInfo.stackName,
33723
+ region: stackRegion,
33724
+ resources: {},
33725
+ outputs: {},
33726
+ lastModified: Date.now()
33727
+ },
33728
+ recreateViaCcApi: options.recreateViaCcApi,
33729
+ allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
33730
+ forceStatefulRecreation: options.forceStatefulRecreation ?? false
33731
+ }),
33732
+ s3Client: stackAwsClients.s3,
33612
33733
  forceStatefulRecreation: options.forceStatefulRecreation ?? false
33613
33734
  });
33614
33735
  const errorBlock = renderRecreateTargetsErrors(validation);
@@ -60328,7 +60449,7 @@ function reorderArgs(argv) {
60328
60449
  */
60329
60450
  async function main() {
60330
60451
  const program = new Command();
60331
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.162.3");
60452
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.163.0");
60332
60453
  program.addCommand(createBootstrapCommand());
60333
60454
  program.addCommand(createSynthCommand());
60334
60455
  program.addCommand(createListCommand());