@go-to-k/cdkd 0.0.2 → 0.0.4

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.
Binary file
package/dist/index.js CHANGED
@@ -2508,6 +2508,7 @@ import {
2508
2508
  GetObjectCommand,
2509
2509
  PutObjectCommand as PutObjectCommand2,
2510
2510
  DeleteObjectCommand,
2511
+ HeadBucketCommand,
2511
2512
  HeadObjectCommand as HeadObjectCommand2,
2512
2513
  ListObjectsV2Command,
2513
2514
  NoSuchKey
@@ -2524,6 +2525,28 @@ var S3StateBackend = class {
2524
2525
  getStateKey(stackName) {
2525
2526
  return `${this.config.prefix}/${stackName}/state.json`;
2526
2527
  }
2528
+ /**
2529
+ * Verify that the configured state bucket exists.
2530
+ *
2531
+ * Called early in deploy/destroy to fail fast before expensive work
2532
+ * (asset publishing, Docker builds) runs against a missing bucket.
2533
+ */
2534
+ async verifyBucketExists() {
2535
+ try {
2536
+ await this.s3Client.send(new HeadBucketCommand({ Bucket: this.config.bucket }));
2537
+ } catch (error) {
2538
+ const name = error.name;
2539
+ if (name === "NotFound" || name === "NoSuchBucket") {
2540
+ throw new StateError(
2541
+ `State bucket '${this.config.bucket}' does not exist. Run 'cdkd bootstrap' to create it, or specify an existing bucket via --state-bucket, CDKD_STATE_BUCKET, or cdk.json context.cdkd.stateBucket.`
2542
+ );
2543
+ }
2544
+ throw new StateError(
2545
+ `Failed to verify state bucket '${this.config.bucket}': ${error instanceof Error ? error.message : String(error)}`,
2546
+ error instanceof Error ? error : void 0
2547
+ );
2548
+ }
2549
+ }
2527
2550
  /**
2528
2551
  * Check if state exists for a stack
2529
2552
  */
@@ -3109,6 +3132,11 @@ var TemplateParser = class {
3109
3132
  // src/analyzer/dag-builder.ts
3110
3133
  import graphlib from "graphlib";
3111
3134
  var { Graph, alg } = graphlib;
3135
+ var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
3136
+ "AWS::IAM::Policy",
3137
+ "AWS::IAM::RolePolicy",
3138
+ "AWS::IAM::ManagedPolicy"
3139
+ ]);
3112
3140
  var DagBuilder = class {
3113
3141
  logger = getLogger().child("DagBuilder");
3114
3142
  parser = new TemplateParser();
@@ -3149,6 +3177,7 @@ var DagBuilder = class {
3149
3177
  }
3150
3178
  }
3151
3179
  this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
3180
+ edgeCount += this.addCustomResourcePolicyEdges(graph, template);
3152
3181
  if (!alg.isAcyclic(graph)) {
3153
3182
  const cycles = this.findCycles(graph);
3154
3183
  throw new DependencyError(
@@ -3291,6 +3320,123 @@ var DagBuilder = class {
3291
3320
  const deps = this.getAllDependencies(graph, resourceA);
3292
3321
  return deps.has(resourceB);
3293
3322
  }
3323
+ /**
3324
+ * Add implicit edges from IAM::Policy resources to Custom Resources whose
3325
+ * ServiceToken Lambda's execution role those policies attach to.
3326
+ *
3327
+ * Returns the number of edges added.
3328
+ */
3329
+ addCustomResourcePolicyEdges(graph, template) {
3330
+ const rolePolicies = this.buildRolePoliciesMap(template);
3331
+ if (rolePolicies.size === 0) {
3332
+ return 0;
3333
+ }
3334
+ let added = 0;
3335
+ for (const logicalId of this.parser.getResourceIds(template)) {
3336
+ const resource = this.parser.getResource(template, logicalId);
3337
+ if (!resource || !this.isCustomResourceType(resource.Type)) {
3338
+ continue;
3339
+ }
3340
+ const serviceToken = (resource.Properties ?? {})["ServiceToken"];
3341
+ const lambdaId = this.extractLogicalIdFromReference(serviceToken);
3342
+ if (!lambdaId)
3343
+ continue;
3344
+ const lambdaResource = this.parser.getResource(template, lambdaId);
3345
+ if (!lambdaResource || lambdaResource.Type !== "AWS::Lambda::Function") {
3346
+ continue;
3347
+ }
3348
+ const roleId = this.extractLogicalIdFromReference((lambdaResource.Properties ?? {})["Role"]);
3349
+ if (!roleId)
3350
+ continue;
3351
+ const policies = rolePolicies.get(roleId);
3352
+ if (!policies)
3353
+ continue;
3354
+ for (const policyId of policies) {
3355
+ if (policyId === logicalId)
3356
+ continue;
3357
+ if (!graph.hasNode(policyId))
3358
+ continue;
3359
+ if (graph.hasEdge(policyId, logicalId))
3360
+ continue;
3361
+ graph.setEdge(policyId, logicalId);
3362
+ added++;
3363
+ this.logger.debug(
3364
+ `Added implicit edge (custom resource policy): ${policyId} -> ${logicalId}`
3365
+ );
3366
+ }
3367
+ }
3368
+ if (added > 0) {
3369
+ this.logger.debug(`Added ${added} implicit edges for custom resource policies`);
3370
+ }
3371
+ return added;
3372
+ }
3373
+ isCustomResourceType(type) {
3374
+ return type === "AWS::CloudFormation::CustomResource" || type.startsWith("Custom::");
3375
+ }
3376
+ /**
3377
+ * Build a map of roleLogicalId -> Set<policyLogicalId> by scanning the
3378
+ * template for IAM::Policy / IAM::RolePolicy / IAM::ManagedPolicy resources
3379
+ * that attach to a role by Ref/GetAtt.
3380
+ */
3381
+ buildRolePoliciesMap(template) {
3382
+ const map = /* @__PURE__ */ new Map();
3383
+ for (const [policyId, resource] of Object.entries(template.Resources)) {
3384
+ if (!IAM_ROLE_POLICY_TYPES.has(resource.Type))
3385
+ continue;
3386
+ for (const roleId of this.extractAttachedRoleIds(resource)) {
3387
+ let set = map.get(roleId);
3388
+ if (!set) {
3389
+ set = /* @__PURE__ */ new Set();
3390
+ map.set(roleId, set);
3391
+ }
3392
+ set.add(policyId);
3393
+ }
3394
+ }
3395
+ return map;
3396
+ }
3397
+ /**
3398
+ * Extract the logical IDs of IAM::Role resources that a policy resource
3399
+ * attaches to. Supports both `Roles: [Ref]` (IAM::Policy / IAM::ManagedPolicy)
3400
+ * and `RoleName: Ref` (IAM::RolePolicy) shapes.
3401
+ */
3402
+ extractAttachedRoleIds(resource) {
3403
+ const ids = [];
3404
+ const props = resource.Properties ?? {};
3405
+ const roles = props["Roles"];
3406
+ if (Array.isArray(roles)) {
3407
+ for (const entry of roles) {
3408
+ const id = this.extractLogicalIdFromReference(entry);
3409
+ if (id)
3410
+ ids.push(id);
3411
+ }
3412
+ }
3413
+ const roleName = props["RoleName"];
3414
+ const roleNameId = this.extractLogicalIdFromReference(roleName);
3415
+ if (roleNameId)
3416
+ ids.push(roleNameId);
3417
+ return ids;
3418
+ }
3419
+ /**
3420
+ * Extract a resource logical ID from a direct Ref or Fn::GetAtt expression.
3421
+ * Returns undefined for literals or intrinsics we can't statically resolve
3422
+ * (Fn::Join, Fn::ImportValue, etc.) — callers should skip in that case.
3423
+ */
3424
+ extractLogicalIdFromReference(value) {
3425
+ if (typeof value !== "object" || value === null)
3426
+ return void 0;
3427
+ const obj = value;
3428
+ if ("Ref" in obj && typeof obj["Ref"] === "string") {
3429
+ const ref = obj["Ref"];
3430
+ return ref.startsWith("AWS::") ? void 0 : ref;
3431
+ }
3432
+ if ("Fn::GetAtt" in obj) {
3433
+ const getAtt = obj["Fn::GetAtt"];
3434
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string") {
3435
+ return getAtt[0];
3436
+ }
3437
+ }
3438
+ return void 0;
3439
+ }
3294
3440
  };
3295
3441
 
3296
3442
  // src/analyzer/replacement-rules.ts