@go-to-k/cdkd 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -487,6 +487,33 @@ Two `orphan` variants at different granularities:
487
487
  Both `cdkd destroy` (synth-driven) and `cdkd state destroy`
488
488
  (state-driven, no synth) delete AWS resources + state.
489
489
 
490
+ ## Stack termination protection
491
+
492
+ CDK's `new Stack(app, 'X', { terminationProtection: true })` is
493
+ honored by `cdkd destroy` and `cdkd destroy --all`. A protected
494
+ stack is refused before the lock is acquired and before any
495
+ per-resource delete runs; in `--all` runs sibling unprotected
496
+ stacks still destroy and the protected ones contribute to the
497
+ partial-failure exit code 2.
498
+
499
+ Bypass workflow:
500
+
501
+ 1. Edit the CDK code to set `terminationProtection: false`.
502
+ 2. Redeploy: `cdkd deploy MyStack`.
503
+ 3. Retry: `cdkd destroy MyStack`.
504
+
505
+ `cdkd state destroy` (state-only, no synth) does **not** honor
506
+ `terminationProtection` — the flag is a CDK property surfaced via
507
+ synth and is not stored in cdkd's state.json. Use `cdkd destroy`
508
+ when synth is available, or accept that `state destroy` is the
509
+ explicit "I know what I'm doing, ignore CDK guards" escape hatch.
510
+
511
+ A future `--remove-protection` flag (separate scope) will provide
512
+ an explicit one-shot bypass without editing CDK code.
513
+
514
+ `cdkd diff` (read-only) and `cdkd deploy` (forward-only) are
515
+ unaffected — only destroy is gated.
516
+
490
517
  ## `publish-assets`: synth + build + publish, no deploy
491
518
 
492
519
  `cdkd publish-assets` runs the asset half of the deploy pipeline
package/dist/cli.js CHANGED
@@ -1207,6 +1207,18 @@ var ResourceUpdateNotSupportedError = class _ResourceUpdateNotSupportedError ext
1207
1207
  }
1208
1208
  exitCode = 2;
1209
1209
  };
1210
+ var StackTerminationProtectionError = class _StackTerminationProtectionError extends CdkdError {
1211
+ constructor(stackName, cause) {
1212
+ super(
1213
+ `Stack '${stackName}' has terminationProtection: true and cannot be destroyed. Set terminationProtection: false in the CDK code, redeploy, then retry 'cdkd destroy ${stackName}'.`,
1214
+ "STACK_TERMINATION_PROTECTION",
1215
+ cause
1216
+ );
1217
+ this.stackName = stackName;
1218
+ this.name = "StackTerminationProtectionError";
1219
+ Object.setPrototypeOf(this, _StackTerminationProtectionError.prototype);
1220
+ }
1221
+ };
1210
1222
  function isCdkdError(error) {
1211
1223
  return error instanceof CdkdError;
1212
1224
  }
@@ -1915,7 +1927,10 @@ var AssemblyReader = class {
1915
1927
  assetManifestPath,
1916
1928
  dependencyNames,
1917
1929
  region: env?.region !== "unknown-region" ? env?.region : void 0,
1918
- account: env?.account !== "unknown-account" ? env?.account : void 0
1930
+ account: env?.account !== "unknown-account" ? env?.account : void 0,
1931
+ ...props?.terminationProtection !== void 0 && {
1932
+ terminationProtection: props.terminationProtection
1933
+ }
1919
1934
  };
1920
1935
  }
1921
1936
  /**
@@ -13565,7 +13580,7 @@ var SNSTopicProvider = class {
13565
13580
  if (properties["DeliveryStatusLogging"]) {
13566
13581
  const loggingConfigs = properties["DeliveryStatusLogging"];
13567
13582
  for (const config of loggingConfigs) {
13568
- const protocol = config["Protocol"];
13583
+ const protocol = normalizeDeliveryStatusProtocolOrThrow(config["Protocol"], logicalId);
13569
13584
  if (config["SuccessFeedbackRoleArn"]) {
13570
13585
  await this.snsClient.send(
13571
13586
  new SetTopicAttributesCommand({
@@ -13658,7 +13673,7 @@ var SNSTopicProvider = class {
13658
13673
  if (JSON.stringify(properties["DeliveryStatusLogging"]) !== JSON.stringify(previousProperties["DeliveryStatusLogging"])) {
13659
13674
  const loggingConfigs = properties["DeliveryStatusLogging"] || [];
13660
13675
  for (const config of loggingConfigs) {
13661
- const protocol = config["Protocol"];
13676
+ const protocol = normalizeDeliveryStatusProtocolOrThrow(config["Protocol"], logicalId);
13662
13677
  if (config["SuccessFeedbackRoleArn"]) {
13663
13678
  await this.snsClient.send(
13664
13679
  new SetTopicAttributesCommand({
@@ -13798,16 +13813,22 @@ var SNSTopicProvider = class {
13798
13813
  * FailureFeedbackRoleArn?}]`. Walks the known protocol prefix list
13799
13814
  * (`HTTP` / `HTTPS` / `SQS` / `Lambda` / `Firehose` / `Application`); a
13800
13815
  * protocol is included in the result iff at least one of its three
13801
- * sub-attributes is set on the topic. Entries are sorted by `Protocol`
13802
- * for stable positional compare (AWS does not preserve template order
13803
- * across `GetTopicAttributes` calls).
13816
+ * sub-attributes is set on the topic. Entries are sorted by canonical
13817
+ * PascalCase `Protocol` for stable positional compare (AWS does not
13818
+ * preserve template order across `GetTopicAttributes` calls).
13819
+ *
13820
+ * The emitted `Protocol` value preserves state's case when known
13821
+ * (CDK templates emit lowercase `'lambda'` / `'sqs'` / ...; AWS's
13822
+ * attribute prefix is PascalCase). Without case preservation the
13823
+ * comparator would fire false drift on every clean run for any
13824
+ * lowercase-`Protocol` template.
13804
13825
  *
13805
13826
  * `Subscription` is omitted because CDK manages it via separate
13806
13827
  * `AWS::SNS::Subscription` resources, not as a Topic property.
13807
13828
  *
13808
13829
  * Returns `undefined` when the topic is gone (`NotFoundException`).
13809
13830
  */
13810
- async readCurrentState(physicalId, _logicalId, _resourceType) {
13831
+ async readCurrentState(physicalId, _logicalId, _resourceType, properties) {
13811
13832
  let attrs;
13812
13833
  try {
13813
13834
  const resp = await this.snsClient.send(
@@ -13849,7 +13870,10 @@ var SNSTopicProvider = class {
13849
13870
  }
13850
13871
  }
13851
13872
  }
13852
- result["DeliveryStatusLogging"] = mapDeliveryStatusLogging(attrs);
13873
+ result["DeliveryStatusLogging"] = mapDeliveryStatusLogging(
13874
+ attrs,
13875
+ stateProtocolCaseMap(properties?.["DeliveryStatusLogging"])
13876
+ );
13853
13877
  try {
13854
13878
  const tagsResp = await this.snsClient.send(
13855
13879
  new ListTagsForResourceCommand({ ResourceArn: physicalId })
@@ -13938,7 +13962,55 @@ var SNS_DELIVERY_STATUS_PROTOCOLS = [
13938
13962
  "Lambda",
13939
13963
  "SQS"
13940
13964
  ];
13941
- function mapDeliveryStatusLogging(attrs) {
13965
+ function normalizeDeliveryStatusProtocol(input) {
13966
+ if (typeof input !== "string")
13967
+ return void 0;
13968
+ const lower = input.toLowerCase();
13969
+ switch (lower) {
13970
+ case "application":
13971
+ return "Application";
13972
+ case "firehose":
13973
+ return "Firehose";
13974
+ case "http":
13975
+ return "HTTP";
13976
+ case "https":
13977
+ return "HTTPS";
13978
+ case "lambda":
13979
+ return "Lambda";
13980
+ case "sqs":
13981
+ return "SQS";
13982
+ default:
13983
+ return void 0;
13984
+ }
13985
+ }
13986
+ function normalizeDeliveryStatusProtocolOrThrow(input, logicalId) {
13987
+ const normalized = normalizeDeliveryStatusProtocol(input);
13988
+ if (normalized === void 0) {
13989
+ throw new Error(
13990
+ `SNS topic ${logicalId}: unsupported DeliveryStatusLogging protocol ${JSON.stringify(input)}. Expected one of ${SNS_DELIVERY_STATUS_PROTOCOLS.join(", ")} (case-insensitive).`
13991
+ );
13992
+ }
13993
+ return normalized;
13994
+ }
13995
+ function stateProtocolCaseMap(stateLogging) {
13996
+ const map = /* @__PURE__ */ new Map();
13997
+ if (!Array.isArray(stateLogging))
13998
+ return map;
13999
+ for (const entry of stateLogging) {
14000
+ if (!entry || typeof entry !== "object")
14001
+ continue;
14002
+ const raw = entry["Protocol"];
14003
+ if (typeof raw !== "string")
14004
+ continue;
14005
+ const normalized = normalizeDeliveryStatusProtocol(raw);
14006
+ if (!normalized)
14007
+ continue;
14008
+ if (!map.has(normalized))
14009
+ map.set(normalized, raw);
14010
+ }
14011
+ return map;
14012
+ }
14013
+ function mapDeliveryStatusLogging(attrs, stateCaseMap = /* @__PURE__ */ new Map()) {
13942
14014
  const result = [];
13943
14015
  for (const protocol of SNS_DELIVERY_STATUS_PROTOCOLS) {
13944
14016
  const success = attrs[`${protocol}SuccessFeedbackRoleArn`];
@@ -13946,7 +14018,9 @@ function mapDeliveryStatusLogging(attrs) {
13946
14018
  const failure = attrs[`${protocol}FailureFeedbackRoleArn`];
13947
14019
  if (success === void 0 && sample === void 0 && failure === void 0)
13948
14020
  continue;
13949
- const entry = { Protocol: protocol };
14021
+ const entry = {
14022
+ Protocol: stateCaseMap.get(protocol) ?? protocol
14023
+ };
13950
14024
  if (success !== void 0)
13951
14025
  entry["SuccessFeedbackRoleArn"] = success;
13952
14026
  if (sample !== void 0)
@@ -27595,7 +27669,18 @@ var ECSProvider = class {
27595
27669
  let resp;
27596
27670
  try {
27597
27671
  resp = await this.getClient().send(
27598
- new DescribeClustersCommand({ clusters: [physicalId], include: ["TAGS"] })
27672
+ // AWS DescribeClusters omits `settings` / `configuration` from the
27673
+ // response unless they are explicitly requested via `include`. Without
27674
+ // SETTINGS / CONFIGURATIONS the readCurrentState round-trip silently
27675
+ // surfaces empty `ClusterSettings: []` even when the cluster has
27676
+ // containerInsights enabled — a console-side toggle then can't be
27677
+ // detected as drift because both the deploy-time observedProperties
27678
+ // baseline AND the drift-time AWS read would identically miss the
27679
+ // field. Discovered by the drift-revert integ test (PR #201).
27680
+ new DescribeClustersCommand({
27681
+ clusters: [physicalId],
27682
+ include: ["TAGS", "SETTINGS", "CONFIGURATIONS"]
27683
+ })
27599
27684
  );
27600
27685
  } catch {
27601
27686
  return void 0;
@@ -32342,7 +32427,10 @@ var ServiceDiscoveryProvider = class {
32342
32427
  providerRegion = process.env["AWS_REGION"];
32343
32428
  logger = getLogger().child("ServiceDiscoveryProvider");
32344
32429
  handledProperties = /* @__PURE__ */ new Map([
32345
- ["AWS::ServiceDiscovery::PrivateDnsNamespace", /* @__PURE__ */ new Set(["Name", "Vpc", "Description", "Tags"])],
32430
+ [
32431
+ "AWS::ServiceDiscovery::PrivateDnsNamespace",
32432
+ /* @__PURE__ */ new Set(["Name", "Vpc", "Description", "Tags", "Properties"])
32433
+ ],
32346
32434
  [
32347
32435
  "AWS::ServiceDiscovery::Service",
32348
32436
  /* @__PURE__ */ new Set([
@@ -32444,13 +32532,18 @@ var ServiceDiscoveryProvider = class {
32444
32532
  logicalId
32445
32533
  );
32446
32534
  }
32535
+ const propsBag = properties["Properties"];
32536
+ const dnsProps = propsBag?.["DnsProperties"];
32537
+ const soa = dnsProps?.["SOA"];
32538
+ const inputProperties = soa?.TTL !== void 0 ? { DnsProperties: { SOA: { TTL: Number(soa.TTL) } } } : void 0;
32447
32539
  try {
32448
32540
  const response = await client.send(
32449
32541
  new CreatePrivateDnsNamespaceCommand({
32450
32542
  Name: name,
32451
32543
  Vpc: vpc,
32452
32544
  ...description && { Description: description },
32453
- ...tags && tags.length > 0 && { Tags: tags }
32545
+ ...tags && tags.length > 0 && { Tags: tags },
32546
+ ...inputProperties && { Properties: inputProperties }
32454
32547
  })
32455
32548
  );
32456
32549
  const operationId = response.OperationId;
@@ -32840,6 +32933,12 @@ var ServiceDiscoveryProvider = class {
32840
32933
  if (ns.Name !== void 0)
32841
32934
  result["Name"] = ns.Name;
32842
32935
  result["Description"] = ns.Description ?? "";
32936
+ const soa = ns.Properties?.DnsProperties?.SOA;
32937
+ if (soa?.TTL !== void 0) {
32938
+ result["Properties"] = { DnsProperties: { SOA: { TTL: soa.TTL } } };
32939
+ } else {
32940
+ result["Properties"] = {};
32941
+ }
32843
32942
  if (ns.Arn)
32844
32943
  await this.attachTags(result, ns.Arn);
32845
32944
  return result;
@@ -43153,7 +43252,10 @@ async function destroyCommand(stackArgs, options) {
43153
43252
  appStacks = result.stacks.map((s) => ({
43154
43253
  stackName: s.stackName,
43155
43254
  displayName: s.displayName,
43156
- ...s.region && { region: s.region }
43255
+ ...s.region && { region: s.region },
43256
+ ...s.terminationProtection !== void 0 && {
43257
+ terminationProtection: s.terminationProtection
43258
+ }
43157
43259
  }));
43158
43260
  } catch {
43159
43261
  logger.debug("Could not synthesize app, falling back to state-based stack list");
@@ -43212,6 +43314,12 @@ Preparing to destroy stack: ${stackName}`);
43212
43314
  const refs = stateRefsByName.get(stackName) ?? [];
43213
43315
  const synthStack = appStacks.find((s) => s.stackName === stackName);
43214
43316
  const synthRegion = synthStack?.region;
43317
+ if (synthStack?.terminationProtection === true) {
43318
+ const err = new StackTerminationProtectionError(stackName);
43319
+ logger.error(` \u2717 ${err.message}`);
43320
+ totalErrors++;
43321
+ continue;
43322
+ }
43215
43323
  let stackTargetRegion;
43216
43324
  if (refs.length === 0) {
43217
43325
  logger.warn(`No state found for stack ${stackName}, skipping`);
@@ -46250,7 +46358,7 @@ function reorderArgs(argv) {
46250
46358
  }
46251
46359
  async function main() {
46252
46360
  const program = new Command14();
46253
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.57.0");
46361
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.58.0");
46254
46362
  program.addCommand(createBootstrapCommand());
46255
46363
  program.addCommand(createSynthCommand());
46256
46364
  program.addCommand(createListCommand());