@go-to-k/cdkd 0.99.0 → 0.99.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
@@ -4217,23 +4217,83 @@ var S3BucketPolicyProvider = class {
4217
4217
  /**
4218
4218
  * Adopt an existing S3 bucket policy into cdkd state.
4219
4219
  *
4220
- * **Explicit override only.** An `S3::BucketPolicy` is a policy document
4221
- * attached to a bucket via `PutBucketPolicy` it has no standalone
4222
- * identity and is not independently taggable. There is no `aws:cdk:path`
4223
- * tag to look up by; only the bucket itself is taggable.
4220
+ * The operational identifier for an `S3::BucketPolicy` is the **bucket
4221
+ * name** every AWS SDK call (`PutBucketPolicy` / `GetBucketPolicy` /
4222
+ * `DeleteBucketPolicy`) takes the bucket name via the `Bucket`
4223
+ * parameter, and cdkd's `create()` records `properties.Bucket` as the
4224
+ * resource's `physicalId` so subsequent `update()` / `delete()` /
4225
+ * `readCurrentState()` calls hit the right bucket. A `BucketPolicy`
4226
+ * has no standalone identity, no taggable ARN, and no `aws:cdk:path`
4227
+ * lookup — only the bucket itself is taggable.
4228
+ *
4229
+ * Resolution order (closes [#356](https://github.com/go-to-k/cdkd/issues/356)):
4230
+ *
4231
+ * 1. **`knownPhysicalId` if it matches an S3 bucket name shape.**
4232
+ * Preserves the `cdkd import --resource <logicalId>=<bucketName>`
4233
+ * path that has always worked.
4234
+ * 2. **`properties.Bucket` if it is a literal bucket name.** Closes
4235
+ * the `--migrate-from-cloudformation` case: AWS CloudFormation's
4236
+ * `DescribeStackResources` returns the CFn-generated policy NAME
4237
+ * for `AWS::S3::BucketPolicy` (e.g.
4238
+ * `MyStack-MyBucketPolicy-XXXXXXXXXX`), which is NOT a valid S3
4239
+ * bucket name. The first time cdkd touches the imported state with
4240
+ * that name, `readCurrentState` → `GetBucketPolicy` rejects it.
4241
+ * 3. **Hard error** when neither path resolves a bucket name. This
4242
+ * covers (a) `--migrate-from-cloudformation` against a CFn stack
4243
+ * whose template carries `Bucket: {Ref: <MyBucket>}` (the typical
4244
+ * CDK shape) when the referenced bucket is NOT in the importable
4245
+ * set (or hasn't been imported yet in the current run), and (b)
4246
+ * explicit `--resource <logicalId>=<non-bucket-name>` typos.
4247
+ * Pointing the user at `--resource <logicalId>=<bucketName>` is
4248
+ * the recovery path that always works.
4224
4249
  *
4225
- * Users adopting an existing bucket policy should pass
4226
- * `--resource <logicalId>=<bucketName>` (matching the physical id
4227
- * format returned by `create()`).
4250
+ * Intrinsic-valued `Bucket` (e.g. `{Ref: <MyBucket>}`) falls into
4251
+ * branch 3 here even when the referenced sibling has been imported in
4252
+ * the same run `import()` is called BEFORE
4253
+ * `resolveImportedProperties` runs the synth template's Properties
4254
+ * through the intrinsic resolver, so the raw intrinsic object is what
4255
+ * we see. The recovery message names `--resource` as the explicit
4256
+ * escape hatch.
4228
4257
  */
4229
4258
  async import(input) {
4230
- if (input.knownPhysicalId) return {
4259
+ if (input.knownPhysicalId && isS3BucketName(input.knownPhysicalId)) return {
4231
4260
  physicalId: input.knownPhysicalId,
4232
4261
  attributes: {}
4233
4262
  };
4234
- return null;
4263
+ const bucket = input.properties["Bucket"];
4264
+ if (typeof bucket === "string" && isS3BucketName(bucket)) return {
4265
+ physicalId: bucket,
4266
+ attributes: {}
4267
+ };
4268
+ const knownNote = input.knownPhysicalId ? ` Got knownPhysicalId='${input.knownPhysicalId}' (not a valid S3 bucket name; CloudFormation returns the policy resource NAME for AWS::S3::BucketPolicy, which is not the operational identifier).` : "";
4269
+ const bucketNote = bucket !== void 0 ? ` Properties.Bucket=${JSON.stringify(bucket)} did not resolve to a literal bucket name (intrinsic-valued entries like {Ref: <Bucket>} are not resolved at import time).` : " Properties.Bucket is missing.";
4270
+ throw new Error(`Cannot determine bucket name for ${input.resourceType} '${input.logicalId}'.${knownNote}${bucketNote} Re-run with --resource ${input.logicalId}=<bucketName> (e.g. my-bucket-12345) to point cdkd at the bucket this policy is attached to.`);
4235
4271
  }
4236
4272
  };
4273
+ /**
4274
+ * Recognize an S3 bucket name. AWS rules
4275
+ * (https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html):
4276
+ * - 3-63 characters
4277
+ * - lowercase letters, digits, hyphens, and dots
4278
+ * - must start and end with a letter or digit
4279
+ * - no consecutive dots
4280
+ * - no `xn--` prefix (reserved for IDN bucket names)
4281
+ * - no `-s3alias` suffix (reserved for S3 Access Point aliases)
4282
+ * - no `--ol-s3` suffix (reserved for S3 on Outposts)
4283
+ *
4284
+ * A practical pattern that excludes the obvious CFn-generated names like
4285
+ * `MyStack-MyBucketPolicy-XXXXXXXXXX` (which contain uppercase letters
4286
+ * and exceed 63 chars in common cases) while accepting every normal CDK
4287
+ * auto-generated and user-declared bucket name.
4288
+ */
4289
+ function isS3BucketName(value) {
4290
+ if (value.length < 3 || value.length > 63) return false;
4291
+ if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(value)) return false;
4292
+ if (value.includes("..")) return false;
4293
+ if (value.startsWith("xn--")) return false;
4294
+ if (value.endsWith("-s3alias") || value.endsWith("--ol-s3")) return false;
4295
+ return true;
4296
+ }
4237
4297
 
4238
4298
  //#endregion
4239
4299
  //#region src/provisioning/providers/sqs-queue-provider.ts
@@ -5593,22 +5653,60 @@ var SNSTopicPolicyProvider = class {
5593
5653
  /**
5594
5654
  * Adopt an existing SNS topic policy into cdkd state.
5595
5655
  *
5596
- * **Explicit override only.** A `TopicPolicy` is an attachment to one or
5597
- * more SNS topics applied via `SetTopicAttributes(AttributeName=Policy)`
5598
- * it has no standalone identity and is not independently taggable. There
5599
- * is no `aws:cdk:path` tag to look up by, and the policy has no name/ARN
5600
- * of its own.
5656
+ * The operational identifier for a `TopicPolicy` is the **comma-joined
5657
+ * list of SNS topic ARNs** the policy is attached to every AWS SDK
5658
+ * call (`SetTopicAttributes` / `GetTopicAttributes`) takes a topic ARN
5659
+ * via the `TopicArn` parameter, and cdkd's `create()` records
5660
+ * `topics.join(',')` as the resource's `physicalId` so subsequent
5661
+ * `update()` / `delete()` / `readCurrentState()` calls hit the right
5662
+ * topic(s). A `TopicPolicy` has no standalone identity, no taggable
5663
+ * ARN, and no `aws:cdk:path` lookup — only the parent topics are
5664
+ * taggable.
5665
+ *
5666
+ * Resolution order (closes [#356](https://github.com/go-to-k/cdkd/issues/356)):
5667
+ *
5668
+ * 1. **`knownPhysicalId` if it is a comma-joined list of SNS topic ARNs.**
5669
+ * Preserves the `cdkd import --resource <logicalId>=<topic-arns>`
5670
+ * path that has always worked.
5671
+ * 2. **`properties.Topics.join(',')` if every entry is a literal topic
5672
+ * ARN.** Closes the `--migrate-from-cloudformation` case: AWS
5673
+ * CloudFormation's `DescribeStackResources` returns the CFn-generated
5674
+ * policy NAME for `AWS::SNS::TopicPolicy` (e.g.
5675
+ * `MyStack-MyTopicPolicy-XXXXXXXXXX`), which is NOT a valid topic
5676
+ * ARN. The first time cdkd touches the imported state with that
5677
+ * name, `readCurrentState` → `GetTopicAttributes` rejects it.
5678
+ * 3. **Hard error** when neither path resolves a topic-ARN list. This
5679
+ * covers (a) `--migrate-from-cloudformation` against a CFn stack
5680
+ * whose template carries `Topics: [{Ref: <MyTopic>}]` (the typical
5681
+ * CDK shape) when the referenced topic is NOT in the importable
5682
+ * set (or hasn't been imported yet in the current run), and (b)
5683
+ * explicit `--resource <logicalId>=<non-arn>` typos. Pointing the
5684
+ * user at `--resource <logicalId>=<topic-arns>` is the recovery
5685
+ * path that always works.
5601
5686
  *
5602
- * Users adopting an existing topic policy should pass
5603
- * `--resource <logicalId>=<comma-joined-topic-ARNs>` (matching the
5604
- * physical id format returned by `create()`).
5687
+ * Intrinsic-valued `Topics` entries (e.g. `{Ref: <MyTopic>}`) fall into
5688
+ * branch 3 here even when the referenced sibling has been imported in
5689
+ * the same run `import()` is called BEFORE
5690
+ * `resolveImportedProperties` runs the synth template's Properties
5691
+ * through the intrinsic resolver, so the raw intrinsic object is what
5692
+ * we see. The recovery message names `--resource` as the explicit
5693
+ * escape hatch.
5605
5694
  */
5606
5695
  async import(input) {
5607
- if (input.knownPhysicalId) return {
5696
+ if (input.knownPhysicalId && isSnsTopicArnList(input.knownPhysicalId)) return {
5608
5697
  physicalId: input.knownPhysicalId,
5609
5698
  attributes: {}
5610
5699
  };
5611
- return null;
5700
+ const topics = input.properties["Topics"];
5701
+ if (Array.isArray(topics) && topics.length > 0) {
5702
+ if (topics.every((t) => typeof t === "string" && isSnsTopicArn(t))) return {
5703
+ physicalId: topics.join(","),
5704
+ attributes: {}
5705
+ };
5706
+ }
5707
+ const knownNote = input.knownPhysicalId ? ` Got knownPhysicalId='${input.knownPhysicalId}' (not a comma-joined list of SNS topic ARNs; CloudFormation returns the policy resource NAME for AWS::SNS::TopicPolicy, which is not the operational identifier).` : "";
5708
+ const topicsNote = Array.isArray(topics) && topics.length > 0 ? ` Properties.Topics=${JSON.stringify(topics)} did not resolve to a list of literal topic ARNs (intrinsic-valued entries like {Ref: <Topic>} are not resolved at import time).` : " Properties.Topics is missing or empty.";
5709
+ throw new Error(`Cannot determine topic ARNs for ${input.resourceType} '${input.logicalId}'.${knownNote}${topicsNote} Re-run with --resource ${input.logicalId}=<comma-joined-topic-ARNs> (e.g. arn:aws:sns:${input.region}:<account>:<topic-name>) to point cdkd at the topic(s) this policy is attached to.`);
5612
5710
  }
5613
5711
  /**
5614
5712
  * Set the policy on a single SNS topic
@@ -5621,6 +5719,31 @@ var SNSTopicPolicyProvider = class {
5621
5719
  }));
5622
5720
  }
5623
5721
  };
5722
+ /**
5723
+ * Recognize a single SNS topic ARN. AWS standard form is
5724
+ * `arn:<partition>:sns:<region>:<account>:<name>`; FIFO topics end in
5725
+ * `.fifo`. Accepts every partition (`aws` / `aws-cn` / `aws-us-gov` /
5726
+ * `aws-iso` / etc.) via the broader `arn:<partition>:sns:` prefix shape.
5727
+ */
5728
+ function isSnsTopicArn(value) {
5729
+ return /^arn:[a-z0-9-]+:sns:[a-z0-9-]+:\d{12}:[\w.-]+$/.test(value);
5730
+ }
5731
+ /**
5732
+ * Recognize a comma-joined list of SNS topic ARNs. cdkd's `create()`
5733
+ * records `topics.join(',')` as the `physicalId`, so a single ARN
5734
+ * (`arn:aws:sns:us-east-1:123456789012:my-topic`) is also accepted.
5735
+ * Every comma-separated segment must be a valid SNS topic ARN — a CFn
5736
+ * generated name like `MyStack-MyTopicPolicy-XXX` is correctly rejected
5737
+ * because it does not match the ARN prefix, and a partially-valid
5738
+ * mixture (one literal ARN + one CFn name) is also rejected so we fall
5739
+ * back to the properties-based resolution rather than baking a half-bad
5740
+ * list into state.
5741
+ */
5742
+ function isSnsTopicArnList(value) {
5743
+ const segments = value.split(",");
5744
+ if (segments.length === 0) return false;
5745
+ return segments.every((s) => isSnsTopicArn(s));
5746
+ }
5624
5747
 
5625
5748
  //#endregion
5626
5749
  //#region src/provisioning/providers/lambda-function-provider.ts
@@ -31999,7 +32122,8 @@ async function importCommand(stackArg, options) {
31999
32122
  stackName: stackInfo.stackName,
32000
32123
  region: targetRegion,
32001
32124
  providerRegistry,
32002
- override: overrides.get(logicalId)
32125
+ override: overrides.get(logicalId),
32126
+ overrides
32003
32127
  });
32004
32128
  rows.push(outcome);
32005
32129
  }
@@ -32053,9 +32177,54 @@ async function importCommand(stackArg, options) {
32053
32177
  awsClients.destroy();
32054
32178
  }
32055
32179
  }
32180
+ /**
32181
+ * Recursively substitute `{Ref: <LogicalId>}` shapes in an arbitrary value
32182
+ * tree with the matching entry from `overrides`. Used to bridge the gap
32183
+ * between CDK synth's template (which carries raw intrinsics) and what a
32184
+ * provider's `import()` needs to see at the time it's called — specifically
32185
+ * for sub-resource providers like `SQSQueuePolicyProvider` whose fallback
32186
+ * path reads `properties.<ParentKey>` as a literal operational identifier
32187
+ * (queue URL / topic ARN / bucket name) rather than the unresolved intrinsic.
32188
+ *
32189
+ * Scope is intentionally narrow:
32190
+ * - Only `{Ref: <X>}` shapes are substituted. `Fn::GetAtt` is NOT handled
32191
+ * here — the overrides map carries physical IDs only, not the
32192
+ * per-resource attributes a GetAtt resolution needs. Full GetAtt /
32193
+ * Fn::Sub / Fn::Join handling happens later in
32194
+ * `resolveImportedProperties` against the populated `stackState.resources`.
32195
+ * - Pseudo-parameter refs (`AWS::Region` / `AWS::AccountId` / etc.) are
32196
+ * left untouched — those are handled by the full resolver post-import.
32197
+ * - When the `Ref` target is NOT in the overrides map, the intrinsic is
32198
+ * left in place (the post-import resolver may resolve it from the
32199
+ * `stackState.resources` built by other imports).
32200
+ *
32201
+ * Closes issue #361 — `AWS::SQS::QueuePolicy` under
32202
+ * `--migrate-from-cloudformation` previously hard-errored because
32203
+ * `properties.Queues[0]` arrived at `provider.import()` as
32204
+ * `{Ref: <Queue>}` and the queue URL needed for the fallback identification
32205
+ * branch was never substituted in.
32206
+ *
32207
+ * Pure-functional — does not mutate `value`.
32208
+ */
32209
+ function substituteOverrideRefs(value, overrides) {
32210
+ if (value === null || value === void 0) return value;
32211
+ if (typeof value !== "object") return value;
32212
+ if (Array.isArray(value)) return value.map((v) => substituteOverrideRefs(v, overrides));
32213
+ const obj = value;
32214
+ const keys = Object.keys(obj);
32215
+ if (keys.length === 1 && keys[0] === "Ref" && typeof obj["Ref"] === "string") {
32216
+ const refTarget = obj["Ref"];
32217
+ const resolved = overrides.get(refTarget);
32218
+ if (resolved !== void 0) return resolved;
32219
+ return value;
32220
+ }
32221
+ const result = {};
32222
+ for (const [k, v] of Object.entries(obj)) result[k] = substituteOverrideRefs(v, overrides);
32223
+ return result;
32224
+ }
32056
32225
  async function importOne(task) {
32057
32226
  const logger = getLogger();
32058
- const { logicalId, resource, stackName, region, providerRegistry, override } = task;
32227
+ const { logicalId, resource, stackName, region, providerRegistry, override, overrides } = task;
32059
32228
  if (!providerRegistry.hasProvider(resource.Type)) return {
32060
32229
  logicalId,
32061
32230
  resourceType: resource.Type,
@@ -32070,13 +32239,14 @@ async function importOne(task) {
32070
32239
  reason: `provider does not implement import (yet)`
32071
32240
  };
32072
32241
  const cdkPath = readCdkPath(resource);
32242
+ const properties = substituteOverrideRefs(resource.Properties ?? {}, overrides);
32073
32243
  const input = {
32074
32244
  logicalId,
32075
32245
  resourceType: resource.Type,
32076
32246
  cdkPath,
32077
32247
  stackName,
32078
32248
  region,
32079
- properties: resource.Properties ?? {},
32249
+ properties,
32080
32250
  ...override !== void 0 && { knownPhysicalId: override }
32081
32251
  };
32082
32252
  try {
@@ -42424,7 +42594,7 @@ function reorderArgs(argv) {
42424
42594
  */
42425
42595
  async function main() {
42426
42596
  const program = new Command();
42427
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.98.2");
42597
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.99.1");
42428
42598
  program.addCommand(createBootstrapCommand());
42429
42599
  program.addCommand(createSynthCommand());
42430
42600
  program.addCommand(createListCommand());