@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.
package/README.md CHANGED
@@ -306,16 +306,16 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
306
306
 
307
307
  ## Installation
308
308
 
309
- ### From npm (experimental)
309
+ ### From npm
310
310
 
311
311
  ```bash
312
- npm i -g @go-to-k/cdkd@experimental # latest experimental
313
- npm i -g @go-to-k/cdkd@0.1.0 # pin to a specific version
312
+ npm i -g @go-to-k/cdkd # latest release
313
+ npm i -g @go-to-k/cdkd@0.0.2 # pin to a specific version
314
314
  ```
315
315
 
316
316
  The installed binary is `cdkd` — run it the same way in either install path.
317
317
 
318
- > Published under the `experimental` dist-tag while the project is in early development. There is no `latest` tag yet always pin to `@experimental` (or a specific version) so `npm i -g @go-to-k/cdkd` does not silently resolve to a future stable release with different behavior.
318
+ > cdkd is an experimental / educational project and is not intended for production usesee the warning at the top of this README. Pin to a specific version if you need reproducible installs.
319
319
 
320
320
  ### From source
321
321
 
package/dist/cli.js CHANGED
@@ -2893,6 +2893,7 @@ import {
2893
2893
  GetObjectCommand,
2894
2894
  PutObjectCommand as PutObjectCommand2,
2895
2895
  DeleteObjectCommand,
2896
+ HeadBucketCommand as HeadBucketCommand2,
2896
2897
  HeadObjectCommand as HeadObjectCommand2,
2897
2898
  ListObjectsV2Command,
2898
2899
  NoSuchKey
@@ -2909,6 +2910,28 @@ var S3StateBackend = class {
2909
2910
  getStateKey(stackName) {
2910
2911
  return `${this.config.prefix}/${stackName}/state.json`;
2911
2912
  }
2913
+ /**
2914
+ * Verify that the configured state bucket exists.
2915
+ *
2916
+ * Called early in deploy/destroy to fail fast before expensive work
2917
+ * (asset publishing, Docker builds) runs against a missing bucket.
2918
+ */
2919
+ async verifyBucketExists() {
2920
+ try {
2921
+ await this.s3Client.send(new HeadBucketCommand2({ Bucket: this.config.bucket }));
2922
+ } catch (error) {
2923
+ const name = error.name;
2924
+ if (name === "NotFound" || name === "NoSuchBucket") {
2925
+ throw new StateError(
2926
+ `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.`
2927
+ );
2928
+ }
2929
+ throw new StateError(
2930
+ `Failed to verify state bucket '${this.config.bucket}': ${error instanceof Error ? error.message : String(error)}`,
2931
+ error instanceof Error ? error : void 0
2932
+ );
2933
+ }
2934
+ }
2912
2935
  /**
2913
2936
  * Check if state exists for a stack
2914
2937
  */
@@ -3496,6 +3519,11 @@ var TemplateParser = class {
3496
3519
 
3497
3520
  // src/analyzer/dag-builder.ts
3498
3521
  var { Graph, alg } = graphlib;
3522
+ var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
3523
+ "AWS::IAM::Policy",
3524
+ "AWS::IAM::RolePolicy",
3525
+ "AWS::IAM::ManagedPolicy"
3526
+ ]);
3499
3527
  var DagBuilder = class {
3500
3528
  logger = getLogger().child("DagBuilder");
3501
3529
  parser = new TemplateParser();
@@ -3536,6 +3564,7 @@ var DagBuilder = class {
3536
3564
  }
3537
3565
  }
3538
3566
  this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
3567
+ edgeCount += this.addCustomResourcePolicyEdges(graph, template);
3539
3568
  if (!alg.isAcyclic(graph)) {
3540
3569
  const cycles = this.findCycles(graph);
3541
3570
  throw new DependencyError(
@@ -3678,6 +3707,123 @@ var DagBuilder = class {
3678
3707
  const deps = this.getAllDependencies(graph, resourceA);
3679
3708
  return deps.has(resourceB);
3680
3709
  }
3710
+ /**
3711
+ * Add implicit edges from IAM::Policy resources to Custom Resources whose
3712
+ * ServiceToken Lambda's execution role those policies attach to.
3713
+ *
3714
+ * Returns the number of edges added.
3715
+ */
3716
+ addCustomResourcePolicyEdges(graph, template) {
3717
+ const rolePolicies = this.buildRolePoliciesMap(template);
3718
+ if (rolePolicies.size === 0) {
3719
+ return 0;
3720
+ }
3721
+ let added = 0;
3722
+ for (const logicalId of this.parser.getResourceIds(template)) {
3723
+ const resource = this.parser.getResource(template, logicalId);
3724
+ if (!resource || !this.isCustomResourceType(resource.Type)) {
3725
+ continue;
3726
+ }
3727
+ const serviceToken = (resource.Properties ?? {})["ServiceToken"];
3728
+ const lambdaId = this.extractLogicalIdFromReference(serviceToken);
3729
+ if (!lambdaId)
3730
+ continue;
3731
+ const lambdaResource = this.parser.getResource(template, lambdaId);
3732
+ if (!lambdaResource || lambdaResource.Type !== "AWS::Lambda::Function") {
3733
+ continue;
3734
+ }
3735
+ const roleId = this.extractLogicalIdFromReference((lambdaResource.Properties ?? {})["Role"]);
3736
+ if (!roleId)
3737
+ continue;
3738
+ const policies = rolePolicies.get(roleId);
3739
+ if (!policies)
3740
+ continue;
3741
+ for (const policyId of policies) {
3742
+ if (policyId === logicalId)
3743
+ continue;
3744
+ if (!graph.hasNode(policyId))
3745
+ continue;
3746
+ if (graph.hasEdge(policyId, logicalId))
3747
+ continue;
3748
+ graph.setEdge(policyId, logicalId);
3749
+ added++;
3750
+ this.logger.debug(
3751
+ `Added implicit edge (custom resource policy): ${policyId} -> ${logicalId}`
3752
+ );
3753
+ }
3754
+ }
3755
+ if (added > 0) {
3756
+ this.logger.debug(`Added ${added} implicit edges for custom resource policies`);
3757
+ }
3758
+ return added;
3759
+ }
3760
+ isCustomResourceType(type) {
3761
+ return type === "AWS::CloudFormation::CustomResource" || type.startsWith("Custom::");
3762
+ }
3763
+ /**
3764
+ * Build a map of roleLogicalId -> Set<policyLogicalId> by scanning the
3765
+ * template for IAM::Policy / IAM::RolePolicy / IAM::ManagedPolicy resources
3766
+ * that attach to a role by Ref/GetAtt.
3767
+ */
3768
+ buildRolePoliciesMap(template) {
3769
+ const map = /* @__PURE__ */ new Map();
3770
+ for (const [policyId, resource] of Object.entries(template.Resources)) {
3771
+ if (!IAM_ROLE_POLICY_TYPES.has(resource.Type))
3772
+ continue;
3773
+ for (const roleId of this.extractAttachedRoleIds(resource)) {
3774
+ let set = map.get(roleId);
3775
+ if (!set) {
3776
+ set = /* @__PURE__ */ new Set();
3777
+ map.set(roleId, set);
3778
+ }
3779
+ set.add(policyId);
3780
+ }
3781
+ }
3782
+ return map;
3783
+ }
3784
+ /**
3785
+ * Extract the logical IDs of IAM::Role resources that a policy resource
3786
+ * attaches to. Supports both `Roles: [Ref]` (IAM::Policy / IAM::ManagedPolicy)
3787
+ * and `RoleName: Ref` (IAM::RolePolicy) shapes.
3788
+ */
3789
+ extractAttachedRoleIds(resource) {
3790
+ const ids = [];
3791
+ const props = resource.Properties ?? {};
3792
+ const roles = props["Roles"];
3793
+ if (Array.isArray(roles)) {
3794
+ for (const entry of roles) {
3795
+ const id = this.extractLogicalIdFromReference(entry);
3796
+ if (id)
3797
+ ids.push(id);
3798
+ }
3799
+ }
3800
+ const roleName = props["RoleName"];
3801
+ const roleNameId = this.extractLogicalIdFromReference(roleName);
3802
+ if (roleNameId)
3803
+ ids.push(roleNameId);
3804
+ return ids;
3805
+ }
3806
+ /**
3807
+ * Extract a resource logical ID from a direct Ref or Fn::GetAtt expression.
3808
+ * Returns undefined for literals or intrinsics we can't statically resolve
3809
+ * (Fn::Join, Fn::ImportValue, etc.) — callers should skip in that case.
3810
+ */
3811
+ extractLogicalIdFromReference(value) {
3812
+ if (typeof value !== "object" || value === null)
3813
+ return void 0;
3814
+ const obj = value;
3815
+ if ("Ref" in obj && typeof obj["Ref"] === "string") {
3816
+ const ref = obj["Ref"];
3817
+ return ref.startsWith("AWS::") ? void 0 : ref;
3818
+ }
3819
+ if ("Fn::GetAtt" in obj) {
3820
+ const getAtt = obj["Fn::GetAtt"];
3821
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string") {
3822
+ return getAtt[0];
3823
+ }
3824
+ }
3825
+ return void 0;
3826
+ }
3681
3827
  };
3682
3828
 
3683
3829
  // src/analyzer/replacement-rules.ts
@@ -26028,6 +26174,11 @@ async function deployCommand(stacks, options) {
26028
26174
  ...options.profile && { profile: options.profile }
26029
26175
  });
26030
26176
  setAwsClients(awsClients);
26177
+ const preflightStateBackend = new S3StateBackend(awsClients.s3, {
26178
+ bucket: stateBucket,
26179
+ prefix: options.statePrefix
26180
+ });
26181
+ await preflightStateBackend.verifyBucketExists();
26031
26182
  let deployInterrupted = false;
26032
26183
  const topLevelSigintHandler = () => {
26033
26184
  if (deployInterrupted) {
@@ -26481,6 +26632,7 @@ async function destroyCommand(stackArgs, options) {
26481
26632
  prefix: options.statePrefix
26482
26633
  };
26483
26634
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig);
26635
+ await stateBackend.verifyBucketExists();
26484
26636
  const lockManager = new LockManager(awsClients.s3, stateConfig);
26485
26637
  const dagBuilder = new DagBuilder();
26486
26638
  const providerRegistry = new ProviderRegistry();
@@ -26861,7 +27013,7 @@ function reorderArgs(argv) {
26861
27013
  }
26862
27014
  async function main() {
26863
27015
  const program = new Command8();
26864
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.0.2");
27016
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.0.4");
26865
27017
  program.addCommand(createBootstrapCommand());
26866
27018
  program.addCommand(createSynthCommand());
26867
27019
  program.addCommand(createDeployCommand());