@go-to-k/cdkd 0.50.13 → 0.51.1

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
@@ -7636,6 +7636,7 @@ import {
7636
7636
  UpdateAssumeRolePolicyCommand,
7637
7637
  DeleteRoleCommand,
7638
7638
  GetRoleCommand,
7639
+ GetRolePolicyCommand,
7639
7640
  PutRolePolicyCommand,
7640
7641
  DeleteRolePolicyCommand,
7641
7642
  ListRolePoliciesCommand,
@@ -8248,8 +8249,14 @@ var IAMRoleProvider = class {
8248
8249
  * against state's already-parsed object.
8249
8250
  *
8250
8251
  * Coverage and shape decisions:
8251
- * - `RoleName`, `Description`, `MaxSessionDuration`, `Path`,
8252
- * `PermissionsBoundary` — straight from `Role.*`.
8252
+ * - `RoleName`, `Description`, `MaxSessionDuration`, `Path` — straight
8253
+ * from `Role.*`.
8254
+ * - `PermissionsBoundary` — emitted as `'' ` placeholder when AWS has
8255
+ * none, so a console-side ADD on a role that was deployed without a
8256
+ * boundary surfaces as drift. (The drift comparator's top-level walk
8257
+ * is state-keys-only; without the always-emit placeholder a fresh
8258
+ * `PermissionsBoundary` on the AWS side would never enter
8259
+ * `observedProperties` and the comparator would silently ignore it.)
8253
8260
  * - `AssumeRolePolicyDocument` — `Role.AssumeRolePolicyDocument` is a
8254
8261
  * URL-encoded JSON string; we URL-decode + JSON-parse so cdkd state's
8255
8262
  * object form compares cleanly. (Both shapes — string and object — are
@@ -8257,20 +8264,23 @@ var IAMRoleProvider = class {
8257
8264
  * after intrinsic resolution.)
8258
8265
  * - `ManagedPolicyArns` — array of ARN strings from
8259
8266
  * `ListAttachedRolePolicies`.
8260
- * - `Policies` (inline policies with `PolicyDocument` bodies) is
8261
- * intentionally omitted: surfacing names without bodies guarantees a
8262
- * PolicyDocument-shaped drift on every role, and fetching every body
8263
- * costs one extra `GetRolePolicy` per inline policy. Out of scope for
8264
- * v1 drift detection on inline IAM policy bodies can ship in a
8265
- * follow-up.
8267
+ * - `Policies` inline policies surfaced as `[{PolicyName, PolicyDocument}]`.
8268
+ * `ListRolePolicies` for names + `GetRolePolicy` per name for the
8269
+ * body (URL-decoded + JSON-parsed). Ordering is reconciled against
8270
+ * state's `Policies` array (when supplied via the `properties`
8271
+ * parameter) so a state-vs-AWS positional compare doesn't fire false
8272
+ * drift purely from `ListRolePolicies` returning lexicographic order;
8273
+ * AWS-only policies (added via console) are appended at the end so
8274
+ * they still surface as drift via length / content mismatch.
8266
8275
  * - `Tags` is surfaced via `ListRoleTags` (paginated). CDK's `aws:*`
8267
8276
  * auto-tags are filtered out by `normalizeAwsTagsToCfn` so they don't
8268
- * fire false-positive drift; the result key is omitted entirely when
8269
- * AWS reports no user tags (matches `create()`'s behavior).
8277
+ * fire false-positive drift; always emitted (even when empty) so a
8278
+ * console-side tag ADD on an originally-untagged role surfaces as
8279
+ * drift on the v3 observedProperties baseline.
8270
8280
  *
8271
8281
  * Returns `undefined` when the role is gone (`NoSuchEntityException`).
8272
8282
  */
8273
- async readCurrentState(physicalId, _logicalId, _resourceType) {
8283
+ async readCurrentState(physicalId, _logicalId, _resourceType, properties) {
8274
8284
  let role;
8275
8285
  try {
8276
8286
  const resp = await this.iamClient.send(new GetRoleCommand({ RoleName: physicalId }));
@@ -8291,9 +8301,7 @@ var IAMRoleProvider = class {
8291
8301
  }
8292
8302
  if (role.Path !== void 0)
8293
8303
  result["Path"] = role.Path;
8294
- if (role.PermissionsBoundary?.PermissionsBoundaryArn !== void 0) {
8295
- result["PermissionsBoundary"] = role.PermissionsBoundary.PermissionsBoundaryArn;
8296
- }
8304
+ result["PermissionsBoundary"] = role.PermissionsBoundary?.PermissionsBoundaryArn ?? "";
8297
8305
  if (role.AssumeRolePolicyDocument) {
8298
8306
  try {
8299
8307
  result["AssumeRolePolicyDocument"] = JSON.parse(
@@ -8313,6 +8321,59 @@ var IAMRoleProvider = class {
8313
8321
  if (!(err instanceof NoSuchEntityException))
8314
8322
  throw err;
8315
8323
  }
8324
+ try {
8325
+ const policyNames = [];
8326
+ let policyMarker;
8327
+ while (true) {
8328
+ const listResp = await this.iamClient.send(
8329
+ new ListRolePoliciesCommand({
8330
+ RoleName: physicalId,
8331
+ ...policyMarker ? { Marker: policyMarker } : {}
8332
+ })
8333
+ );
8334
+ for (const name of listResp.PolicyNames ?? [])
8335
+ policyNames.push(name);
8336
+ if (!listResp.IsTruncated)
8337
+ break;
8338
+ policyMarker = listResp.Marker;
8339
+ }
8340
+ const bodies = /* @__PURE__ */ new Map();
8341
+ await Promise.all(
8342
+ policyNames.map(async (name) => {
8343
+ const resp = await this.iamClient.send(
8344
+ new GetRolePolicyCommand({ RoleName: physicalId, PolicyName: name })
8345
+ );
8346
+ if (!resp.PolicyDocument)
8347
+ return;
8348
+ let parsed;
8349
+ try {
8350
+ parsed = JSON.parse(decodeURIComponent(resp.PolicyDocument));
8351
+ } catch {
8352
+ parsed = resp.PolicyDocument;
8353
+ }
8354
+ bodies.set(name, parsed);
8355
+ })
8356
+ );
8357
+ const statePolicies = properties?.["Policies"] ?? [];
8358
+ const remaining = new Set(bodies.keys());
8359
+ const inline = [];
8360
+ for (const sp of statePolicies) {
8361
+ const name = sp?.PolicyName;
8362
+ if (typeof name !== "string")
8363
+ continue;
8364
+ if (bodies.has(name)) {
8365
+ inline.push({ PolicyName: name, PolicyDocument: bodies.get(name) });
8366
+ remaining.delete(name);
8367
+ }
8368
+ }
8369
+ for (const name of [...remaining].sort()) {
8370
+ inline.push({ PolicyName: name, PolicyDocument: bodies.get(name) });
8371
+ }
8372
+ result["Policies"] = inline;
8373
+ } catch (err) {
8374
+ if (!(err instanceof NoSuchEntityException))
8375
+ throw err;
8376
+ }
8316
8377
  try {
8317
8378
  const collected = [];
8318
8379
  let marker;
@@ -8340,18 +8401,6 @@ var IAMRoleProvider = class {
8340
8401
  }
8341
8402
  return result;
8342
8403
  }
8343
- /**
8344
- * `Policies` (inline policy bodies) are intentionally omitted from
8345
- * `readCurrentState`: surfacing the names without bodies would
8346
- * guarantee a `PolicyDocument`-shaped drift on every role, and
8347
- * fetching every body costs one extra `GetRolePolicy` per inline
8348
- * policy. Tell the drift comparator to skip the whole subtree until a
8349
- * dedicated PR adds proper inline-policy drift via per-name
8350
- * `GetRolePolicy`.
8351
- */
8352
- getDriftUnknownPaths() {
8353
- return ["Policies"];
8354
- }
8355
8404
  /**
8356
8405
  * Adopt an existing IAM role into cdkd state.
8357
8406
  *
@@ -8807,6 +8856,65 @@ var DeployEngine = class {
8807
8856
  }
8808
8857
  }
8809
8858
  }
8859
+ /**
8860
+ * Kick off `provider.readCurrentState` for every resource in the
8861
+ * loaded state that lacks `observedProperties` (e.g. state written
8862
+ * by a pre-v3 binary, or a v3 record where a NO_CHANGE-skipped
8863
+ * resource's baseline never landed). Calls go through
8864
+ * `kickOffObservedCapture`, so they share the same fire-and-forget
8865
+ * pipeline, error swallowing, and final-drain wiring that the
8866
+ * post-CREATE / post-UPDATE captures use.
8867
+ *
8868
+ * The deploy critical path does NOT wait on these; the cost is
8869
+ * bounded by `max(per-resource readCurrentState latency)` (typically
8870
+ * ~200-300ms in practice) once at the end-of-deploy drain. Any
8871
+ * resource that subsequently goes through CREATE / UPDATE in the
8872
+ * same deploy will overwrite this entry via the `Map.set` keyed by
8873
+ * `logicalId` (latest-wins) — so there's no double-write to state,
8874
+ * just a wasted SDK call for the (rare) UPDATE / DELETE intersection.
8875
+ *
8876
+ * Resources whose provider lookup throws (e.g. unsupported type) or
8877
+ * lacks `readCurrentState` are silently skipped — same policy as the
8878
+ * manual `cdkd state refresh-observed` command.
8879
+ */
8880
+ kickOffAutoRefreshObservedProperties(stateResources) {
8881
+ if (this.options.captureObservedState !== true)
8882
+ return;
8883
+ if (this.options.dryRun === true)
8884
+ return;
8885
+ let toRefresh = 0;
8886
+ const candidates = [];
8887
+ for (const [logicalId, resource] of Object.entries(stateResources)) {
8888
+ if (resource.observedProperties !== void 0)
8889
+ continue;
8890
+ candidates.push({ logicalId, resource });
8891
+ }
8892
+ if (candidates.length === 0)
8893
+ return;
8894
+ for (const { logicalId, resource } of candidates) {
8895
+ let provider;
8896
+ try {
8897
+ provider = this.providerRegistry.getProvider(resource.resourceType);
8898
+ } catch {
8899
+ continue;
8900
+ }
8901
+ if (!provider.readCurrentState)
8902
+ continue;
8903
+ this.kickOffObservedCapture(
8904
+ provider,
8905
+ logicalId,
8906
+ resource.physicalId,
8907
+ resource.resourceType,
8908
+ resource.properties ?? {}
8909
+ );
8910
+ toRefresh++;
8911
+ }
8912
+ if (toRefresh > 0) {
8913
+ this.logger.warn(
8914
+ `cdkd state schema upgrade detected \u2014 refreshing observed-properties baseline for ${toRefresh} resource(s) (one-time, runs in parallel with deploy)`
8915
+ );
8916
+ }
8917
+ }
8810
8918
  async doDeploy(stackName, template) {
8811
8919
  const startTime = Date.now();
8812
8920
  this.logger.debug(`Starting deployment for stack: ${stackName}`);
@@ -8838,6 +8946,7 @@ var DeployEngine = class {
8838
8946
  this.logger.debug(
8839
8947
  `Loaded current state: ${Object.keys(currentState.resources).length} resources`
8840
8948
  );
8949
+ this.kickOffAutoRefreshObservedProperties(currentState.resources);
8841
8950
  this.logger.debug(`Template has ${Object.keys(template.Resources || {}).length} resources`);
8842
8951
  const parameterValues = await this.resolver.resolveParameters(
8843
8952
  template,
@@ -8882,6 +8991,35 @@ var DeployEngine = class {
8882
8991
  const hasChanges = this.diffCalculator.hasChanges(changes);
8883
8992
  if (!hasChanges) {
8884
8993
  this.logger.info("No changes detected. Stack is up to date.");
8994
+ if (!this.options.dryRun && this.observedCaptureTasks.size > 0) {
8995
+ await this.drainObservedCaptures(currentState.resources);
8996
+ try {
8997
+ const refreshedState = {
8998
+ version: STATE_SCHEMA_VERSION_CURRENT,
8999
+ region: this.stackRegion,
9000
+ stackName: currentState.stackName,
9001
+ resources: currentState.resources,
9002
+ outputs: currentState.outputs,
9003
+ lastModified: Date.now()
9004
+ };
9005
+ const saveOptions = {};
9006
+ if (currentEtag !== void 0)
9007
+ saveOptions.expectedEtag = currentEtag;
9008
+ if (migrationPending)
9009
+ saveOptions.migrateLegacy = true;
9010
+ await this.stateBackend.saveState(
9011
+ stackName,
9012
+ this.stackRegion,
9013
+ refreshedState,
9014
+ saveOptions
9015
+ );
9016
+ this.logger.debug("Persisted refreshed observedProperties (no-change path)");
9017
+ } catch (saveError) {
9018
+ this.logger.warn(
9019
+ `Failed to persist refreshed observedProperties: ${saveError instanceof Error ? saveError.message : String(saveError)} \u2014 drift baseline will be re-fetched on next deploy.`
9020
+ );
9021
+ }
9022
+ }
8885
9023
  return {
8886
9024
  stackName,
8887
9025
  created: 0,