@go-to-k/cdkd 0.51.0 → 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.
package/dist/cli.js CHANGED
@@ -8481,6 +8481,7 @@ import {
8481
8481
  UpdateAssumeRolePolicyCommand,
8482
8482
  DeleteRoleCommand,
8483
8483
  GetRoleCommand,
8484
+ GetRolePolicyCommand,
8484
8485
  PutRolePolicyCommand,
8485
8486
  DeleteRolePolicyCommand,
8486
8487
  ListRolePoliciesCommand,
@@ -9093,8 +9094,14 @@ var IAMRoleProvider = class {
9093
9094
  * against state's already-parsed object.
9094
9095
  *
9095
9096
  * Coverage and shape decisions:
9096
- * - `RoleName`, `Description`, `MaxSessionDuration`, `Path`,
9097
- * `PermissionsBoundary` — straight from `Role.*`.
9097
+ * - `RoleName`, `Description`, `MaxSessionDuration`, `Path` — straight
9098
+ * from `Role.*`.
9099
+ * - `PermissionsBoundary` — emitted as `'' ` placeholder when AWS has
9100
+ * none, so a console-side ADD on a role that was deployed without a
9101
+ * boundary surfaces as drift. (The drift comparator's top-level walk
9102
+ * is state-keys-only; without the always-emit placeholder a fresh
9103
+ * `PermissionsBoundary` on the AWS side would never enter
9104
+ * `observedProperties` and the comparator would silently ignore it.)
9098
9105
  * - `AssumeRolePolicyDocument` — `Role.AssumeRolePolicyDocument` is a
9099
9106
  * URL-encoded JSON string; we URL-decode + JSON-parse so cdkd state's
9100
9107
  * object form compares cleanly. (Both shapes — string and object — are
@@ -9102,20 +9109,23 @@ var IAMRoleProvider = class {
9102
9109
  * after intrinsic resolution.)
9103
9110
  * - `ManagedPolicyArns` — array of ARN strings from
9104
9111
  * `ListAttachedRolePolicies`.
9105
- * - `Policies` (inline policies with `PolicyDocument` bodies) is
9106
- * intentionally omitted: surfacing names without bodies guarantees a
9107
- * PolicyDocument-shaped drift on every role, and fetching every body
9108
- * costs one extra `GetRolePolicy` per inline policy. Out of scope for
9109
- * v1 drift detection on inline IAM policy bodies can ship in a
9110
- * follow-up.
9112
+ * - `Policies` inline policies surfaced as `[{PolicyName, PolicyDocument}]`.
9113
+ * `ListRolePolicies` for names + `GetRolePolicy` per name for the
9114
+ * body (URL-decoded + JSON-parsed). Ordering is reconciled against
9115
+ * state's `Policies` array (when supplied via the `properties`
9116
+ * parameter) so a state-vs-AWS positional compare doesn't fire false
9117
+ * drift purely from `ListRolePolicies` returning lexicographic order;
9118
+ * AWS-only policies (added via console) are appended at the end so
9119
+ * they still surface as drift via length / content mismatch.
9111
9120
  * - `Tags` is surfaced via `ListRoleTags` (paginated). CDK's `aws:*`
9112
9121
  * auto-tags are filtered out by `normalizeAwsTagsToCfn` so they don't
9113
- * fire false-positive drift; the result key is omitted entirely when
9114
- * AWS reports no user tags (matches `create()`'s behavior).
9122
+ * fire false-positive drift; always emitted (even when empty) so a
9123
+ * console-side tag ADD on an originally-untagged role surfaces as
9124
+ * drift on the v3 observedProperties baseline.
9115
9125
  *
9116
9126
  * Returns `undefined` when the role is gone (`NoSuchEntityException`).
9117
9127
  */
9118
- async readCurrentState(physicalId, _logicalId, _resourceType) {
9128
+ async readCurrentState(physicalId, _logicalId, _resourceType, properties) {
9119
9129
  let role;
9120
9130
  try {
9121
9131
  const resp = await this.iamClient.send(new GetRoleCommand({ RoleName: physicalId }));
@@ -9136,9 +9146,7 @@ var IAMRoleProvider = class {
9136
9146
  }
9137
9147
  if (role.Path !== void 0)
9138
9148
  result["Path"] = role.Path;
9139
- if (role.PermissionsBoundary?.PermissionsBoundaryArn !== void 0) {
9140
- result["PermissionsBoundary"] = role.PermissionsBoundary.PermissionsBoundaryArn;
9141
- }
9149
+ result["PermissionsBoundary"] = role.PermissionsBoundary?.PermissionsBoundaryArn ?? "";
9142
9150
  if (role.AssumeRolePolicyDocument) {
9143
9151
  try {
9144
9152
  result["AssumeRolePolicyDocument"] = JSON.parse(
@@ -9158,6 +9166,59 @@ var IAMRoleProvider = class {
9158
9166
  if (!(err instanceof NoSuchEntityException))
9159
9167
  throw err;
9160
9168
  }
9169
+ try {
9170
+ const policyNames = [];
9171
+ let policyMarker;
9172
+ while (true) {
9173
+ const listResp = await this.iamClient.send(
9174
+ new ListRolePoliciesCommand({
9175
+ RoleName: physicalId,
9176
+ ...policyMarker ? { Marker: policyMarker } : {}
9177
+ })
9178
+ );
9179
+ for (const name of listResp.PolicyNames ?? [])
9180
+ policyNames.push(name);
9181
+ if (!listResp.IsTruncated)
9182
+ break;
9183
+ policyMarker = listResp.Marker;
9184
+ }
9185
+ const bodies = /* @__PURE__ */ new Map();
9186
+ await Promise.all(
9187
+ policyNames.map(async (name) => {
9188
+ const resp = await this.iamClient.send(
9189
+ new GetRolePolicyCommand({ RoleName: physicalId, PolicyName: name })
9190
+ );
9191
+ if (!resp.PolicyDocument)
9192
+ return;
9193
+ let parsed;
9194
+ try {
9195
+ parsed = JSON.parse(decodeURIComponent(resp.PolicyDocument));
9196
+ } catch {
9197
+ parsed = resp.PolicyDocument;
9198
+ }
9199
+ bodies.set(name, parsed);
9200
+ })
9201
+ );
9202
+ const statePolicies = properties?.["Policies"] ?? [];
9203
+ const remaining = new Set(bodies.keys());
9204
+ const inline = [];
9205
+ for (const sp of statePolicies) {
9206
+ const name = sp?.PolicyName;
9207
+ if (typeof name !== "string")
9208
+ continue;
9209
+ if (bodies.has(name)) {
9210
+ inline.push({ PolicyName: name, PolicyDocument: bodies.get(name) });
9211
+ remaining.delete(name);
9212
+ }
9213
+ }
9214
+ for (const name of [...remaining].sort()) {
9215
+ inline.push({ PolicyName: name, PolicyDocument: bodies.get(name) });
9216
+ }
9217
+ result["Policies"] = inline;
9218
+ } catch (err) {
9219
+ if (!(err instanceof NoSuchEntityException))
9220
+ throw err;
9221
+ }
9161
9222
  try {
9162
9223
  const collected = [];
9163
9224
  let marker;
@@ -9185,18 +9246,6 @@ var IAMRoleProvider = class {
9185
9246
  }
9186
9247
  return result;
9187
9248
  }
9188
- /**
9189
- * `Policies` (inline policy bodies) are intentionally omitted from
9190
- * `readCurrentState`: surfacing the names without bodies would
9191
- * guarantee a `PolicyDocument`-shaped drift on every role, and
9192
- * fetching every body costs one extra `GetRolePolicy` per inline
9193
- * policy. Tell the drift comparator to skip the whole subtree until a
9194
- * dedicated PR adds proper inline-policy drift via per-name
9195
- * `GetRolePolicy`.
9196
- */
9197
- getDriftUnknownPaths() {
9198
- return ["Policies"];
9199
- }
9200
9249
  /**
9201
9250
  * Adopt an existing IAM role into cdkd state.
9202
9251
  *
@@ -9259,7 +9308,7 @@ import {
9259
9308
  DeleteGroupPolicyCommand,
9260
9309
  PutUserPolicyCommand,
9261
9310
  DeleteUserPolicyCommand,
9262
- GetRolePolicyCommand,
9311
+ GetRolePolicyCommand as GetRolePolicyCommand2,
9263
9312
  GetGroupPolicyCommand,
9264
9313
  GetUserPolicyCommand,
9265
9314
  NoSuchEntityException as NoSuchEntityException2
@@ -9648,7 +9697,7 @@ var IAMPolicyProvider = class {
9648
9697
  try {
9649
9698
  if (roles && roles.length > 0) {
9650
9699
  const resp = await this.iamClient.send(
9651
- new GetRolePolicyCommand({ RoleName: roles[0], PolicyName: policyName })
9700
+ new GetRolePolicyCommand2({ RoleName: roles[0], PolicyName: policyName })
9652
9701
  );
9653
9702
  liveDocument = this.decodePolicyDocument(resp.PolicyDocument);
9654
9703
  } else if (groups && groups.length > 0) {
@@ -10023,9 +10072,11 @@ import {
10023
10072
  PutUserPolicyCommand as PutUserPolicyCommand2,
10024
10073
  DeleteUserPolicyCommand as DeleteUserPolicyCommand2,
10025
10074
  ListUserPoliciesCommand,
10075
+ GetUserPolicyCommand as GetUserPolicyCommand2,
10026
10076
  PutGroupPolicyCommand as PutGroupPolicyCommand2,
10027
10077
  DeleteGroupPolicyCommand as DeleteGroupPolicyCommand2,
10028
10078
  ListGroupPoliciesCommand,
10079
+ GetGroupPolicyCommand as GetGroupPolicyCommand2,
10029
10080
  CreateLoginProfileCommand,
10030
10081
  UpdateLoginProfileCommand,
10031
10082
  AddUserToGroupCommand,
@@ -11052,17 +11103,21 @@ var IAMUserGroupProvider = class {
11052
11103
  * UserToGroupAddition in CFn-property shape.
11053
11104
  *
11054
11105
  * - **AWS::IAM::User**: `GetUser` for `UserName`, `Path`,
11055
- * `PermissionsBoundary` (re-shaped from `PermissionsBoundary.Arn`);
11106
+ * `PermissionsBoundary` (re-shaped from `PermissionsBoundary.Arn`,
11107
+ * always-emit `''` placeholder so console-side ADD on a user
11108
+ * deployed without a boundary surfaces as drift);
11056
11109
  * `ListAttachedUserPolicies` for `ManagedPolicyArns`;
11057
- * `ListGroupsForUser` for `Groups`. `Tags`, `LoginProfile`, and
11058
- * `Policies` (inline policy bodies) are intentionally omitted
11059
- * same shape decisions as `iam-role-provider` (LoginProfile contains a
11060
- * one-time password and inline policy bodies cost N extra round-trips
11061
- * that are out of scope for v1).
11110
+ * `ListGroupsForUser` for `Groups`; `ListUserPolicies` +
11111
+ * `GetUserPolicy` per name for inline `Policies` (URL-decoded +
11112
+ * JSON-parsed, capped at IAM's documented 10-per-user limit, order
11113
+ * reconciled against state's `Policies` array). `Tags` and
11114
+ * `LoginProfile` remain omitted Tags will land in a follow-up,
11115
+ * LoginProfile contains a one-time password we never want to surface
11116
+ * through drift.
11062
11117
  * - **AWS::IAM::Group**: `GetGroup` for `GroupName`, `Path`;
11063
- * `ListAttachedGroupPolicies` for `ManagedPolicyArns`. `Policies`
11064
- * (inline policy bodies) is omitted for the same reason as User /
11065
- * Role.
11118
+ * `ListAttachedGroupPolicies` for `ManagedPolicyArns`;
11119
+ * `ListGroupPolicies` + `GetGroupPolicy` per name for inline
11120
+ * `Policies`.
11066
11121
  * - **AWS::IAM::UserToGroupAddition**: SKIPPED — returns `undefined`
11067
11122
  * because the resource is metadata-only (group-membership attachments
11068
11123
  * written via `AddUserToGroup`). A meaningful drift check would
@@ -11074,12 +11129,12 @@ var IAMUserGroupProvider = class {
11074
11129
  * Returns `undefined` when the user / group is gone
11075
11130
  * (`NoSuchEntityException`).
11076
11131
  */
11077
- async readCurrentState(physicalId, logicalId, resourceType) {
11132
+ async readCurrentState(physicalId, logicalId, resourceType, properties) {
11078
11133
  switch (resourceType) {
11079
11134
  case "AWS::IAM::User":
11080
- return this.readUserCurrentState(physicalId);
11135
+ return this.readUserCurrentState(physicalId, properties);
11081
11136
  case "AWS::IAM::Group":
11082
- return this.readGroupCurrentState(physicalId);
11137
+ return this.readGroupCurrentState(physicalId, properties);
11083
11138
  case "AWS::IAM::UserToGroupAddition":
11084
11139
  return void 0;
11085
11140
  default:
@@ -11089,7 +11144,7 @@ var IAMUserGroupProvider = class {
11089
11144
  return void 0;
11090
11145
  }
11091
11146
  }
11092
- async readUserCurrentState(physicalId) {
11147
+ async readUserCurrentState(physicalId, properties) {
11093
11148
  let user;
11094
11149
  try {
11095
11150
  const resp = await this.iamClient.send(new GetUserCommand({ UserName: physicalId }));
@@ -11106,9 +11161,7 @@ var IAMUserGroupProvider = class {
11106
11161
  result["UserName"] = user.UserName;
11107
11162
  if (user.Path !== void 0)
11108
11163
  result["Path"] = user.Path;
11109
- if (user.PermissionsBoundary?.PermissionsBoundaryArn !== void 0) {
11110
- result["PermissionsBoundary"] = user.PermissionsBoundary.PermissionsBoundaryArn;
11111
- }
11164
+ result["PermissionsBoundary"] = user.PermissionsBoundary?.PermissionsBoundaryArn ?? "";
11112
11165
  try {
11113
11166
  const attached = await this.iamClient.send(
11114
11167
  new ListAttachedUserPoliciesCommand({ UserName: physicalId })
@@ -11129,9 +11182,20 @@ var IAMUserGroupProvider = class {
11129
11182
  if (!(err instanceof NoSuchEntityException4))
11130
11183
  throw err;
11131
11184
  }
11185
+ try {
11186
+ const inline = await this.collectInlinePolicies(
11187
+ "user",
11188
+ physicalId,
11189
+ properties?.["Policies"] ?? []
11190
+ );
11191
+ result["Policies"] = inline;
11192
+ } catch (err) {
11193
+ if (!(err instanceof NoSuchEntityException4))
11194
+ throw err;
11195
+ }
11132
11196
  return result;
11133
11197
  }
11134
- async readGroupCurrentState(physicalId) {
11198
+ async readGroupCurrentState(physicalId, properties) {
11135
11199
  let group;
11136
11200
  try {
11137
11201
  const resp = await this.iamClient.send(new GetGroupCommand({ GroupName: physicalId }));
@@ -11158,8 +11222,83 @@ var IAMUserGroupProvider = class {
11158
11222
  if (!(err instanceof NoSuchEntityException4))
11159
11223
  throw err;
11160
11224
  }
11225
+ try {
11226
+ const inline = await this.collectInlinePolicies(
11227
+ "group",
11228
+ physicalId,
11229
+ properties?.["Policies"] ?? []
11230
+ );
11231
+ result["Policies"] = inline;
11232
+ } catch (err) {
11233
+ if (!(err instanceof NoSuchEntityException4))
11234
+ throw err;
11235
+ }
11161
11236
  return result;
11162
11237
  }
11238
+ /**
11239
+ * Shared inline-policy fetcher for User / Group readCurrentState.
11240
+ * Mirrors `IAMRoleProvider.readCurrentState`'s inline-policy handling:
11241
+ * paginated `List*Policies` for names → parallel `Get*Policy` per name
11242
+ * for bodies (URL-decoded + JSON-parsed) → reconcile order against
11243
+ * `statePolicies` so a positional compare doesn't fire false drift on
11244
+ * the lexicographic order returned by AWS.
11245
+ */
11246
+ async collectInlinePolicies(kind, physicalId, statePolicies) {
11247
+ const policyNames = [];
11248
+ let marker;
11249
+ while (true) {
11250
+ const listResp = kind === "user" ? await this.iamClient.send(
11251
+ new ListUserPoliciesCommand({
11252
+ UserName: physicalId,
11253
+ ...marker ? { Marker: marker } : {}
11254
+ })
11255
+ ) : await this.iamClient.send(
11256
+ new ListGroupPoliciesCommand({
11257
+ GroupName: physicalId,
11258
+ ...marker ? { Marker: marker } : {}
11259
+ })
11260
+ );
11261
+ for (const name of listResp.PolicyNames ?? [])
11262
+ policyNames.push(name);
11263
+ if (!listResp.IsTruncated)
11264
+ break;
11265
+ marker = listResp.Marker;
11266
+ }
11267
+ const bodies = /* @__PURE__ */ new Map();
11268
+ await Promise.all(
11269
+ policyNames.map(async (name) => {
11270
+ const resp = kind === "user" ? await this.iamClient.send(
11271
+ new GetUserPolicyCommand2({ UserName: physicalId, PolicyName: name })
11272
+ ) : await this.iamClient.send(
11273
+ new GetGroupPolicyCommand2({ GroupName: physicalId, PolicyName: name })
11274
+ );
11275
+ if (!resp.PolicyDocument)
11276
+ return;
11277
+ let parsed;
11278
+ try {
11279
+ parsed = JSON.parse(decodeURIComponent(resp.PolicyDocument));
11280
+ } catch {
11281
+ parsed = resp.PolicyDocument;
11282
+ }
11283
+ bodies.set(name, parsed);
11284
+ })
11285
+ );
11286
+ const remaining = new Set(bodies.keys());
11287
+ const inline = [];
11288
+ for (const sp of statePolicies) {
11289
+ const name = sp?.PolicyName;
11290
+ if (typeof name !== "string")
11291
+ continue;
11292
+ if (bodies.has(name)) {
11293
+ inline.push({ PolicyName: name, PolicyDocument: bodies.get(name) });
11294
+ remaining.delete(name);
11295
+ }
11296
+ }
11297
+ for (const name of [...remaining].sort()) {
11298
+ inline.push({ PolicyName: name, PolicyDocument: bodies.get(name) });
11299
+ }
11300
+ return inline;
11301
+ }
11163
11302
  // ─── Import dispatch ──────────────────────────────────────────────
11164
11303
  /**
11165
11304
  * Adopt an existing IAM user / group / user-to-group addition into cdkd state.
@@ -16867,6 +17006,7 @@ import {
16867
17006
  CreateLogGroupCommand,
16868
17007
  DeleteLogGroupCommand,
16869
17008
  DescribeLogGroupsCommand,
17009
+ GetDataProtectionPolicyCommand,
16870
17010
  ListTagsForResourceCommand as ListTagsForResourceCommand2,
16871
17011
  PutRetentionPolicyCommand,
16872
17012
  DeleteRetentionPolicyCommand,
@@ -17108,19 +17248,6 @@ var LogsLogGroupProvider = class {
17108
17248
  }
17109
17249
  return this.buildArn(physicalId);
17110
17250
  }
17111
- /**
17112
- * Drift comparator skip-list: properties readCurrentState deliberately
17113
- * cannot round-trip from AWS yet. `DataProtectionPolicy` lives behind
17114
- * its own `GetDataProtectionPolicy` API call (not in
17115
- * `DescribeLogGroups` output) — declaring it here prevents
17116
- * guaranteed false-positive drift on every clean run for log groups
17117
- * deployed with a data-protection policy. Lifting this guard requires
17118
- * a per-group `GetDataProtectionPolicy` round-trip in
17119
- * `readCurrentState`.
17120
- */
17121
- getDriftUnknownPaths() {
17122
- return ["DataProtectionPolicy"];
17123
- }
17124
17251
  /**
17125
17252
  * Read the AWS-current log group configuration in CFn-property shape.
17126
17253
  *
@@ -17131,10 +17258,16 @@ var LogsLogGroupProvider = class {
17131
17258
  * `RetentionInDays`).
17132
17259
  *
17133
17260
  * Coverage: `LogGroupName`, `KmsKeyId`, `RetentionInDays`,
17134
- * `LogGroupClass`, `Tags`. Other handledProperties (`DataProtectionPolicy`,
17135
- * `FieldIndexPolicies`, `ResourcePolicyDocument`,
17136
- * `DeletionProtectionEnabled`, `BearerTokenAuthenticationEnabled`) need
17137
- * their own per-property API call and are out of scope for v1.
17261
+ * `LogGroupClass`, `Tags`, plus `DataProtectionPolicy` (via
17262
+ * `GetDataProtectionPolicy`, JSON-parsed back to the object form
17263
+ * cdkd state holds). Still out of scope: `FieldIndexPolicies`
17264
+ * (separate `DescribeFieldIndexPolicies` call, follow-up),
17265
+ * `ResourcePolicyDocument` (managed by the separate
17266
+ * `AWS::Logs::ResourcePolicy` resource type — account-wide, not
17267
+ * per-log-group), `DeletionProtectionEnabled` (not surfaced by
17268
+ * `DescribeLogGroups`; would need a yet-undocumented separate API),
17269
+ * `BearerTokenAuthenticationEnabled` (specialized X-Ray / service-log
17270
+ * endpoint feature, also not in `DescribeLogGroups`).
17138
17271
  *
17139
17272
  * Tags are read via `ListTagsForResource` (using the log-group ARN from
17140
17273
  * the same `DescribeLogGroups` response). CDK's `aws:*` auto-tags are
@@ -17173,6 +17306,21 @@ var LogsLogGroupProvider = class {
17173
17306
  }
17174
17307
  }
17175
17308
  result["Tags"] = tags;
17309
+ let dpp = "";
17310
+ try {
17311
+ const dppResp = await this.logsClient.send(
17312
+ new GetDataProtectionPolicyCommand({ logGroupIdentifier: physicalId })
17313
+ );
17314
+ if (dppResp.policyDocument) {
17315
+ try {
17316
+ dpp = JSON.parse(dppResp.policyDocument);
17317
+ } catch {
17318
+ dpp = dppResp.policyDocument;
17319
+ }
17320
+ }
17321
+ } catch {
17322
+ }
17323
+ result["DataProtectionPolicy"] = dpp;
17176
17324
  return result;
17177
17325
  } catch (err) {
17178
17326
  if (err instanceof ResourceNotFoundException7)
@@ -26406,6 +26554,7 @@ import {
26406
26554
  CreateLoadBalancerCommand,
26407
26555
  DeleteLoadBalancerCommand,
26408
26556
  DescribeLoadBalancersCommand as DescribeLoadBalancersCommand2,
26557
+ DescribeLoadBalancerAttributesCommand,
26409
26558
  CreateTargetGroupCommand,
26410
26559
  DeleteTargetGroupCommand,
26411
26560
  ModifyTargetGroupCommand,
@@ -26498,7 +26647,13 @@ var ELBv2Provider = class {
26498
26647
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
26499
26648
  switch (resourceType) {
26500
26649
  case "AWS::ElasticLoadBalancingV2::LoadBalancer":
26501
- return this.updateLoadBalancer(logicalId, physicalId, resourceType, properties);
26650
+ return this.updateLoadBalancer(
26651
+ logicalId,
26652
+ physicalId,
26653
+ resourceType,
26654
+ properties,
26655
+ previousProperties
26656
+ );
26502
26657
  case "AWS::ElasticLoadBalancingV2::TargetGroup":
26503
26658
  return this.updateTargetGroup(
26504
26659
  logicalId,
@@ -26603,14 +26758,44 @@ var ELBv2Provider = class {
26603
26758
  );
26604
26759
  }
26605
26760
  }
26606
- updateLoadBalancer(logicalId, _physicalId, _resourceType, _properties) {
26607
- return Promise.reject(
26608
- new ResourceUpdateNotSupportedError(
26761
+ async updateLoadBalancer(logicalId, physicalId, _resourceType, properties, previousProperties) {
26762
+ const newAttrs = properties["LoadBalancerAttributes"] ?? [];
26763
+ const oldAttrs = previousProperties["LoadBalancerAttributes"] ?? [];
26764
+ const stripAttrs = (p) => {
26765
+ const { LoadBalancerAttributes: _, ...rest } = p;
26766
+ return rest;
26767
+ };
26768
+ if (JSON.stringify(stripAttrs(properties)) !== JSON.stringify(stripAttrs(previousProperties))) {
26769
+ throw new ResourceUpdateNotSupportedError(
26609
26770
  "AWS::ElasticLoadBalancingV2::LoadBalancer",
26610
26771
  logicalId,
26611
- "ELBv2 LoadBalancer in-place updates are not yet implemented in cdkd; re-deploy with cdkd deploy --replace, or destroy + redeploy the stack"
26612
- )
26613
- );
26772
+ "ELBv2 LoadBalancer in-place updates are only supported for LoadBalancerAttributes; for Name / Type / Scheme / Subnets / SecurityGroups / IpAddressType / Tags, re-deploy with cdkd deploy --replace, or destroy + redeploy the stack"
26773
+ );
26774
+ }
26775
+ const newMap = new Map(newAttrs.map((a) => [a.Key, a.Value]));
26776
+ const oldMap = new Map(oldAttrs.map((a) => [a.Key, a.Value]));
26777
+ const submitted = [];
26778
+ for (const [k, v] of newMap) {
26779
+ if (oldMap.get(k) !== v)
26780
+ submitted.push({ Key: k, Value: v });
26781
+ }
26782
+ for (const [k] of oldMap) {
26783
+ if (!newMap.has(k))
26784
+ submitted.push({ Key: k, Value: "" });
26785
+ }
26786
+ if (submitted.length > 0) {
26787
+ const { ModifyLoadBalancerAttributesCommand } = await import("@aws-sdk/client-elastic-load-balancing-v2");
26788
+ await this.getClient().send(
26789
+ new ModifyLoadBalancerAttributesCommand({
26790
+ LoadBalancerArn: physicalId,
26791
+ Attributes: submitted
26792
+ })
26793
+ );
26794
+ this.logger.debug(
26795
+ `Applied ${submitted.length} LoadBalancerAttributes change(s) for ${logicalId}`
26796
+ );
26797
+ }
26798
+ return { physicalId, wasReplaced: false };
26614
26799
  }
26615
26800
  async deleteLoadBalancer(logicalId, physicalId, resourceType, context) {
26616
26801
  this.logger.debug(`Deleting LoadBalancer ${logicalId}: ${physicalId}`);
@@ -26956,10 +27141,12 @@ var ELBv2Provider = class {
26956
27141
  * Dispatch per resource type:
26957
27142
  * - `LoadBalancer` → `DescribeLoadBalancers` (Name, Subnets via
26958
27143
  * `AvailabilityZones[].SubnetId`, SecurityGroups, Scheme, Type,
26959
- * IpAddressType). LoadBalancerAttributes is omitted for v1 — it
26960
- * requires a separate `DescribeLoadBalancerAttributes` call and the
26961
- * drift comparator only descends into keys present in state, so an
26962
- * absent key cannot fire false drift.
27144
+ * IpAddressType) plus `DescribeLoadBalancerAttributes` for the full
27145
+ * `LoadBalancerAttributes` `[{Key, Value}]` array (sorted by Key for
27146
+ * stable positional compare). AWS returns every attribute valid for
27147
+ * this LB type including defaults the user did not template; on the
27148
+ * v3 observedProperties baseline that's load-bearing — a console-side
27149
+ * change to ANY attribute (templated or not) surfaces as drift.
26963
27150
  * - `TargetGroup` → `DescribeTargetGroups` (Protocol, Port, VpcId,
26964
27151
  * TargetType, ProtocolVersion, HealthCheck*, Matcher, Name).
26965
27152
  * - `Listener` → `DescribeListeners` (LoadBalancerArn, Certificates,
@@ -27009,6 +27196,18 @@ var ELBv2Provider = class {
27009
27196
  result["Type"] = lb.Type;
27010
27197
  if (lb.IpAddressType !== void 0)
27011
27198
  result["IpAddressType"] = lb.IpAddressType;
27199
+ try {
27200
+ const attrsResp = await this.getClient().send(
27201
+ new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: physicalId })
27202
+ );
27203
+ const attrs = (attrsResp.Attributes ?? []).filter(
27204
+ (a) => typeof a.Key === "string" && typeof a.Value === "string"
27205
+ ).map((a) => ({ Key: a.Key, Value: a.Value })).sort((a, b) => a.Key.localeCompare(b.Key));
27206
+ result["LoadBalancerAttributes"] = attrs;
27207
+ } catch (err) {
27208
+ if (this.isNotFoundError(err))
27209
+ return void 0;
27210
+ }
27012
27211
  await this.attachTags(result, physicalId);
27013
27212
  return result;
27014
27213
  }
@@ -28889,8 +29088,10 @@ var Route53Provider = class {
28889
29088
  * PrivateZone}, VPCs from `VPCs[]`, HostedZoneTags via
28890
29089
  * `ListTagsForResource(ResourceType=hostedzone, ResourceId=<idTail>)`
28891
29090
  * with `aws:*` filtered out and the key omitted when empty).
28892
- * QueryLoggingConfig is skipped because it's a separate
28893
- * `ListQueryLoggingConfigs` call and the v1 surface does not surface it.
29091
+ * QueryLoggingConfig is surfaced via a follow-up
29092
+ * `ListQueryLoggingConfigs(HostedZoneId)` (filter to the one
29093
+ * config per zone — CFn enforces 0 or 1 — and reshape
29094
+ * `CloudWatchLogsLogGroupArn` to match cdkd state).
28894
29095
  * - `RecordSet` → `ListResourceRecordSets` filtered to the exact
28895
29096
  * `(name, type)` pair from the composite physicalId
28896
29097
  * (`{zoneId}|{name}|{type}`). Surfaces TTL, ResourceRecords (with
@@ -28955,6 +29156,24 @@ var Route53Provider = class {
28955
29156
  `Route53 ListTagsForResource(${idTail}) failed: ${err instanceof Error ? err.message : String(err)}`
28956
29157
  );
28957
29158
  }
29159
+ try {
29160
+ const qlcResp = await this.getClient().send(
29161
+ new ListQueryLoggingConfigsCommand({ HostedZoneId: idTail })
29162
+ );
29163
+ const qlc = qlcResp.QueryLoggingConfigs?.[0];
29164
+ if (qlc?.CloudWatchLogsLogGroupArn) {
29165
+ result["QueryLoggingConfig"] = {
29166
+ CloudWatchLogsLogGroupArn: qlc.CloudWatchLogsLogGroupArn
29167
+ };
29168
+ } else {
29169
+ result["QueryLoggingConfig"] = {};
29170
+ }
29171
+ } catch (err) {
29172
+ this.logger.debug(
29173
+ `Route53 ListQueryLoggingConfigs(${idTail}) failed: ${err instanceof Error ? err.message : String(err)}`
29174
+ );
29175
+ result["QueryLoggingConfig"] = {};
29176
+ }
28958
29177
  return result;
28959
29178
  }
28960
29179
  async readRecordSet(physicalId) {
@@ -32726,6 +32945,8 @@ import {
32726
32945
  KMSClient as KMSClient2,
32727
32946
  CreateKeyCommand,
32728
32947
  DescribeKeyCommand,
32948
+ GetKeyPolicyCommand,
32949
+ GetKeyRotationStatusCommand,
32729
32950
  ListAliasesCommand as ListAliasesCommand2,
32730
32951
  ListKeysCommand,
32731
32952
  ListResourceTagsCommand,
@@ -33193,6 +33414,36 @@ var KMSProvider = class {
33193
33414
  if (md.Origin !== void 0)
33194
33415
  result["Origin"] = md.Origin;
33195
33416
  if (md.KeyId) {
33417
+ try {
33418
+ const policyResp = await this.getClient().send(
33419
+ new GetKeyPolicyCommand({ KeyId: md.KeyId, PolicyName: "default" })
33420
+ );
33421
+ if (policyResp.Policy) {
33422
+ try {
33423
+ result["KeyPolicy"] = JSON.parse(policyResp.Policy);
33424
+ } catch {
33425
+ result["KeyPolicy"] = policyResp.Policy;
33426
+ }
33427
+ }
33428
+ } catch (err) {
33429
+ if (err instanceof NotFoundException5)
33430
+ return void 0;
33431
+ }
33432
+ const isSymmetric = md.KeySpec === void 0 || md.KeySpec === "SYMMETRIC_DEFAULT";
33433
+ if (isSymmetric) {
33434
+ try {
33435
+ const rotationResp = await this.getClient().send(
33436
+ new GetKeyRotationStatusCommand({ KeyId: md.KeyId })
33437
+ );
33438
+ result["EnableKeyRotation"] = rotationResp.KeyRotationEnabled ?? false;
33439
+ if (rotationResp.RotationPeriodInDays !== void 0) {
33440
+ result["RotationPeriodInDays"] = rotationResp.RotationPeriodInDays;
33441
+ }
33442
+ } catch (err) {
33443
+ if (err instanceof NotFoundException5)
33444
+ return void 0;
33445
+ }
33446
+ }
33196
33447
  try {
33197
33448
  const tagsResp = await this.getClient().send(
33198
33449
  new ListResourceTagsCommand({ KeyId: md.KeyId })
@@ -33211,28 +33462,17 @@ var KMSProvider = class {
33211
33462
  * drift comparator skips them instead of firing guaranteed false-
33212
33463
  * positive drift on every clean run.
33213
33464
  *
33214
- * - `KeyPolicy`: cdkd does NOT call `GetKeyPolicy` in `readCurrentState`.
33215
- * The policy body needs JSON parsing for comparison and a separate
33216
- * SDK call; deferred to a follow-up. Until then, any user who
33217
- * templates `KeyPolicy` would see guaranteed drift.
33218
- * - `EnableKeyRotation` / `RotationPeriodInDays`: cdkd does NOT call
33219
- * `GetKeyRotationStatus`. Same reason — deferred to a follow-up.
33220
- * `EnableKeyRotation` is also a Class 1 candidate (only valid for
33221
- * `KeySpec=SYMMETRIC_DEFAULT`); when we lift this gap the read side
33222
- * must gate the emit on the discriminator.
33223
33465
  * - `BypassPolicyLockoutSafetyCheck` / `PendingWindowInDays`: not part
33224
33466
  * of the persisted AWS state visible via `DescribeKey` — both are
33225
33467
  * create / delete-time-only inputs.
33468
+ *
33469
+ * `KeyPolicy`, `EnableKeyRotation`, and `RotationPeriodInDays` are now
33470
+ * read by `readCurrentState` (`GetKeyPolicy` and `GetKeyRotationStatus`
33471
+ * respectively), so they no longer need to be declared here.
33226
33472
  */
33227
33473
  getDriftUnknownPaths(resourceType) {
33228
33474
  if (resourceType === "AWS::KMS::Key") {
33229
- return [
33230
- "KeyPolicy",
33231
- "EnableKeyRotation",
33232
- "RotationPeriodInDays",
33233
- "BypassPolicyLockoutSafetyCheck",
33234
- "PendingWindowInDays"
33235
- ];
33475
+ return ["BypassPolicyLockoutSafetyCheck", "PendingWindowInDays"];
33236
33476
  }
33237
33477
  return [];
33238
33478
  }
@@ -33821,6 +34061,7 @@ import {
33821
34061
  DescribeAccessPointsCommand,
33822
34062
  DescribeLifecycleConfigurationCommand,
33823
34063
  DescribeBackupPolicyCommand,
34064
+ DescribeMountTargetSecurityGroupsCommand,
33824
34065
  FileSystemNotFound,
33825
34066
  MountTargetNotFound,
33826
34067
  AccessPointNotFound
@@ -34247,8 +34488,10 @@ var EFSProvider = class {
34247
34488
  * the corresponding key without failing the whole snapshot.
34248
34489
  * - `AccessPoint` → `DescribeAccessPoints` filtered by id (PosixUser,
34249
34490
  * RootDirectory).
34250
- * - `MountTarget` → `DescribeMountTargets` (FileSystemId, SubnetId).
34251
- * SecurityGroups requires a separate call and is omitted for v1.
34491
+ * - `MountTarget` → `DescribeMountTargets` (FileSystemId, SubnetId)
34492
+ * plus `DescribeMountTargetSecurityGroups` for the SG list (always-
34493
+ * emit `[]` when AWS reports none so a console-side ADD on a
34494
+ * previously-unconfigured mount target is detectable).
34252
34495
  *
34253
34496
  * `FileSystemTags` (the CFn property name on `AWS::EFS::FileSystem`) is
34254
34497
  * surfaced from the same `DescribeFileSystems` response — `aws:*`
@@ -34405,6 +34648,17 @@ var EFSProvider = class {
34405
34648
  result["FileSystemId"] = mt.FileSystemId;
34406
34649
  if (mt.SubnetId !== void 0)
34407
34650
  result["SubnetId"] = mt.SubnetId;
34651
+ let securityGroups = [];
34652
+ try {
34653
+ const sgResp = await this.getClient().send(
34654
+ new DescribeMountTargetSecurityGroupsCommand({ MountTargetId: physicalId })
34655
+ );
34656
+ securityGroups = (sgResp.SecurityGroups ?? []).filter(
34657
+ (s) => typeof s === "string"
34658
+ );
34659
+ } catch {
34660
+ }
34661
+ result["SecurityGroups"] = securityGroups;
34408
34662
  return result;
34409
34663
  }
34410
34664
  /**
@@ -35014,6 +35268,7 @@ import {
35014
35268
  GetTrailCommand,
35015
35269
  GetTrailStatusCommand,
35016
35270
  GetEventSelectorsCommand,
35271
+ GetInsightSelectorsCommand,
35017
35272
  ListTrailsCommand,
35018
35273
  ListTagsCommand as ListTagsCommand3,
35019
35274
  AddTagsCommand as AddTagsCommand2,
@@ -35186,15 +35441,13 @@ var CloudTrailProvider = class {
35186
35441
  const newInsightSelectors = properties["InsightSelectors"];
35187
35442
  const oldInsightSelectors = previousProperties["InsightSelectors"];
35188
35443
  if (JSON.stringify(newInsightSelectors) !== JSON.stringify(oldInsightSelectors)) {
35189
- if (newInsightSelectors && newInsightSelectors.length > 0) {
35190
- this.logger.debug(`Updating insight selectors for CloudTrail Trail ${logicalId}`);
35191
- await this.getClient().send(
35192
- new PutInsightSelectorsCommand({
35193
- TrailName: physicalId,
35194
- InsightSelectors: newInsightSelectors
35195
- })
35196
- );
35197
- }
35444
+ this.logger.debug(`Updating insight selectors for CloudTrail Trail ${logicalId}`);
35445
+ await this.getClient().send(
35446
+ new PutInsightSelectorsCommand({
35447
+ TrailName: physicalId,
35448
+ InsightSelectors: newInsightSelectors ?? []
35449
+ })
35450
+ );
35198
35451
  }
35199
35452
  const oldIsLogging = previousProperties["IsLogging"];
35200
35453
  if (isLogging !== oldIsLogging) {
@@ -35327,8 +35580,10 @@ var CloudTrailProvider = class {
35327
35580
  * auto-tags are filtered out and the result key is omitted when AWS
35328
35581
  * reports no user tags.
35329
35582
  *
35330
- * `InsightSelectors` is skipped for v1 (separate call + shape mapping
35331
- * still TBD).
35583
+ * `InsightSelectors` is surfaced via a follow-up `GetInsightSelectors`
35584
+ * call — same shape on both sides (`[{InsightType}]`). The key is
35585
+ * always emitted (`[]` when AWS reports none) so a console-side ADD
35586
+ * is detectable on the v3 observedProperties baseline.
35332
35587
  *
35333
35588
  * Returns `undefined` when the trail is gone (`TrailNotFoundException`).
35334
35589
  */
@@ -35377,6 +35632,17 @@ var CloudTrailProvider = class {
35377
35632
  }
35378
35633
  } catch {
35379
35634
  }
35635
+ let insightSelectors = [];
35636
+ try {
35637
+ const insight = await this.getClient().send(
35638
+ new GetInsightSelectorsCommand({ TrailName: physicalId })
35639
+ );
35640
+ insightSelectors = (insight.InsightSelectors ?? []).map((s) => ({
35641
+ ...s.InsightType !== void 0 && { InsightType: s.InsightType }
35642
+ }));
35643
+ } catch {
35644
+ }
35645
+ result["InsightSelectors"] = insightSelectors;
35380
35646
  let tags = [];
35381
35647
  if (trail.TrailARN) {
35382
35648
  try {
@@ -35720,12 +35986,21 @@ var CodeBuildProvider = class {
35720
35986
  *
35721
35987
  * Issues `BatchGetProjects` and re-shapes the SDK's camelCase response back
35722
35988
  * to CFn's PascalCase shape (the `mapProperties` helper above goes the
35723
- * other way at create time). The drift comparator only descends into
35724
- * keys present in cdkd state, so we focus on the high-value top-level
35725
- * fields and the most commonly-set `Source` / `Artifacts` /
35726
- * `Environment` sub-fields. Less common nested config (full
35727
- * `LogsConfig`, `VpcConfig` rebuild, secondary sources/artifacts, etc.)
35728
- * is left to a follow-up surfacing them with a partial shape would
35989
+ * other way at create time). Coverage targets every user-controllable
35990
+ * top-level field via the always-emit pattern (PR #145):
35991
+ * - `Source` / `Artifacts` / `Environment` (with `EnvironmentVariables`
35992
+ * sub-array): full reshape to CFn shape.
35993
+ * - `LogsConfig` (`CloudWatchLogs` + `S3Logs` sub-shapes): always-emit
35994
+ * placeholder so a console-side enable on a previously-default
35995
+ * project surfaces as drift.
35996
+ * - `VpcConfig` (VpcId / Subnets / SecurityGroupIds): always-emit
35997
+ * placeholder.
35998
+ * - `Cache` (Type / Location / Modes): always-emit placeholder.
35999
+ *
36000
+ * Still v1-omitted (known gaps, follow-up): `SecondarySources` /
36001
+ * `SecondaryArtifacts` / `SecondarySourceVersions` / `FileSystemLocations` /
36002
+ * `Triggers` / `BuildBatchConfig` / `ResourceAccessRole`. These are
36003
+ * rarely set in practice and surfacing them with partial shape would
35729
36004
  * fire false drift on every project that uses them.
35730
36005
  *
35731
36006
  * Tags are surfaced from the same `BatchGetProjects` response (CodeBuild
@@ -35840,6 +36115,51 @@ var CodeBuildProvider = class {
35840
36115
  });
35841
36116
  result["Environment"] = env;
35842
36117
  }
36118
+ {
36119
+ const logs = {};
36120
+ const cw = {
36121
+ Status: project.logsConfig?.cloudWatchLogs?.status ?? "ENABLED"
36122
+ };
36123
+ if (project.logsConfig?.cloudWatchLogs?.groupName !== void 0) {
36124
+ cw["GroupName"] = project.logsConfig.cloudWatchLogs.groupName;
36125
+ }
36126
+ if (project.logsConfig?.cloudWatchLogs?.streamName !== void 0) {
36127
+ cw["StreamName"] = project.logsConfig.cloudWatchLogs.streamName;
36128
+ }
36129
+ logs["CloudWatchLogs"] = cw;
36130
+ const s3 = {
36131
+ Status: project.logsConfig?.s3Logs?.status ?? "DISABLED"
36132
+ };
36133
+ if (project.logsConfig?.s3Logs?.location !== void 0) {
36134
+ s3["Location"] = project.logsConfig.s3Logs.location;
36135
+ }
36136
+ if (project.logsConfig?.s3Logs?.encryptionDisabled !== void 0) {
36137
+ s3["EncryptionDisabled"] = project.logsConfig.s3Logs.encryptionDisabled;
36138
+ }
36139
+ if (project.logsConfig?.s3Logs?.bucketOwnerAccess !== void 0) {
36140
+ s3["BucketOwnerAccess"] = project.logsConfig.s3Logs.bucketOwnerAccess;
36141
+ }
36142
+ logs["S3Logs"] = s3;
36143
+ result["LogsConfig"] = logs;
36144
+ }
36145
+ if (project.vpcConfig?.vpcId !== void 0) {
36146
+ const vpc = { VpcId: project.vpcConfig.vpcId };
36147
+ vpc["Subnets"] = project.vpcConfig.subnets ?? [];
36148
+ vpc["SecurityGroupIds"] = project.vpcConfig.securityGroupIds ?? [];
36149
+ result["VpcConfig"] = vpc;
36150
+ } else {
36151
+ result["VpcConfig"] = {};
36152
+ }
36153
+ {
36154
+ const cache2 = {
36155
+ Type: project.cache?.type ?? "NO_CACHE"
36156
+ };
36157
+ if (project.cache?.location !== void 0)
36158
+ cache2["Location"] = project.cache.location;
36159
+ if (project.cache?.modes !== void 0)
36160
+ cache2["Modes"] = project.cache.modes;
36161
+ result["Cache"] = cache2;
36162
+ }
35843
36163
  const tags = normalizeAwsTagsToCfn(project.tags);
35844
36164
  result["Tags"] = tags;
35845
36165
  return result;
@@ -43979,7 +44299,7 @@ function reorderArgs(argv) {
43979
44299
  }
43980
44300
  async function main() {
43981
44301
  const program = new Command14();
43982
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.51.0");
44302
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.51.1");
43983
44303
  program.addCommand(createBootstrapCommand());
43984
44304
  program.addCommand(createSynthCommand());
43985
44305
  program.addCommand(createListCommand());