@microagi/alchemy-gcp 0.11.6 → 0.11.8

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.
Files changed (164) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/ArtifactRegistry/Operations.d.ts +1 -1
  3. package/lib/ArtifactRegistry/Repository.d.ts +2 -2
  4. package/lib/ArtifactRegistry/Repository.d.ts.map +1 -1
  5. package/lib/ArtifactRegistry/Repository.js +5 -5
  6. package/lib/ArtifactRegistry/Repository.js.map +1 -1
  7. package/lib/ArtifactRegistry/RepositoryIamMember.d.ts +2 -1
  8. package/lib/ArtifactRegistry/RepositoryIamMember.d.ts.map +1 -1
  9. package/lib/ArtifactRegistry/RepositoryIamMember.js +1 -1
  10. package/lib/CloudResourceManager/Project.d.ts +2 -1
  11. package/lib/CloudResourceManager/Project.d.ts.map +1 -1
  12. package/lib/CloudResourceManager/Project.js +6 -6
  13. package/lib/CloudResourceManager/Project.js.map +1 -1
  14. package/lib/Compute/Address.d.ts +2 -1
  15. package/lib/Compute/Address.d.ts.map +1 -1
  16. package/lib/Compute/Address.js +4 -4
  17. package/lib/Compute/Address.js.map +1 -1
  18. package/lib/Compute/Firewall.d.ts +2 -1
  19. package/lib/Compute/Firewall.d.ts.map +1 -1
  20. package/lib/Compute/Firewall.js +1 -1
  21. package/lib/Compute/ForwardingRule.d.ts +2 -1
  22. package/lib/Compute/ForwardingRule.d.ts.map +1 -1
  23. package/lib/Compute/ForwardingRule.js +4 -4
  24. package/lib/Compute/ForwardingRule.js.map +1 -1
  25. package/lib/Compute/GlobalAddress.d.ts +2 -1
  26. package/lib/Compute/GlobalAddress.d.ts.map +1 -1
  27. package/lib/Compute/GlobalAddress.js +4 -4
  28. package/lib/Compute/GlobalAddress.js.map +1 -1
  29. package/lib/Compute/Network.d.ts +2 -1
  30. package/lib/Compute/Network.d.ts.map +1 -1
  31. package/lib/Compute/Network.js +1 -1
  32. package/lib/Compute/Operations.d.ts +1 -1
  33. package/lib/Compute/ProjectDefaultNetworkTier.d.ts +2 -1
  34. package/lib/Compute/ProjectDefaultNetworkTier.d.ts.map +1 -1
  35. package/lib/Compute/ProjectDefaultNetworkTier.js +1 -1
  36. package/lib/Compute/SharedVpcHost.d.ts +2 -1
  37. package/lib/Compute/SharedVpcHost.d.ts.map +1 -1
  38. package/lib/Compute/SharedVpcHost.js +1 -1
  39. package/lib/Compute/SharedVpcServiceProject.d.ts +2 -1
  40. package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -1
  41. package/lib/Compute/SharedVpcServiceProject.js +1 -1
  42. package/lib/Compute/Subnetwork.d.ts +2 -1
  43. package/lib/Compute/Subnetwork.d.ts.map +1 -1
  44. package/lib/Compute/Subnetwork.js +2 -2
  45. package/lib/Compute/Subnetwork.js.map +1 -1
  46. package/lib/Container/Cluster.d.ts +4 -2
  47. package/lib/Container/Cluster.d.ts.map +1 -1
  48. package/lib/Container/Cluster.js +28 -10
  49. package/lib/Container/Cluster.js.map +1 -1
  50. package/lib/Container/NodePool.d.ts +2 -2
  51. package/lib/Container/NodePool.d.ts.map +1 -1
  52. package/lib/Container/NodePool.js +22 -12
  53. package/lib/Container/NodePool.js.map +1 -1
  54. package/lib/Container/Operations.d.ts +1 -1
  55. package/lib/Iam/Operations.d.ts +42 -0
  56. package/lib/Iam/Operations.d.ts.map +1 -0
  57. package/lib/Iam/Operations.js +48 -0
  58. package/lib/Iam/Operations.js.map +1 -0
  59. package/lib/Iam/ServiceAccount.d.ts +2 -1
  60. package/lib/Iam/ServiceAccount.d.ts.map +1 -1
  61. package/lib/Iam/ServiceAccount.js +1 -1
  62. package/lib/Iam/ServiceAccountKey.d.ts +2 -1
  63. package/lib/Iam/ServiceAccountKey.d.ts.map +1 -1
  64. package/lib/Iam/ServiceAccountKey.js +1 -1
  65. package/lib/Iam/WorkloadIdentityPool.d.ts +101 -0
  66. package/lib/Iam/WorkloadIdentityPool.d.ts.map +1 -0
  67. package/lib/Iam/WorkloadIdentityPool.js +175 -0
  68. package/lib/Iam/WorkloadIdentityPool.js.map +1 -0
  69. package/lib/Iam/WorkloadIdentityPoolProvider.d.ts +125 -0
  70. package/lib/Iam/WorkloadIdentityPoolProvider.d.ts.map +1 -0
  71. package/lib/Iam/WorkloadIdentityPoolProvider.js +222 -0
  72. package/lib/Iam/WorkloadIdentityPoolProvider.js.map +1 -0
  73. package/lib/Iam/index.d.ts +4 -0
  74. package/lib/Iam/index.d.ts.map +1 -1
  75. package/lib/Iam/index.js +2 -0
  76. package/lib/Iam/index.js.map +1 -1
  77. package/lib/Kubernetes/Secret.d.ts.map +1 -1
  78. package/lib/Kubernetes/Secret.js.map +1 -1
  79. package/lib/ManagedLustre/Instance.d.ts +2 -1
  80. package/lib/ManagedLustre/Instance.d.ts.map +1 -1
  81. package/lib/ManagedLustre/Instance.js +6 -6
  82. package/lib/ManagedLustre/Instance.js.map +1 -1
  83. package/lib/Providers.d.ts.map +1 -1
  84. package/lib/Providers.js +5 -1
  85. package/lib/Providers.js.map +1 -1
  86. package/lib/Run/IamSync.d.ts +1 -1
  87. package/lib/Run/Job.d.ts +3 -3
  88. package/lib/Run/Job.d.ts.map +1 -1
  89. package/lib/Run/Job.js +5 -5
  90. package/lib/Run/Job.js.map +1 -1
  91. package/lib/Run/Operations.d.ts +1 -1
  92. package/lib/Run/Service.d.ts +2 -2
  93. package/lib/Run/Service.d.ts.map +1 -1
  94. package/lib/Run/Service.js +9 -7
  95. package/lib/Run/Service.js.map +1 -1
  96. package/lib/Run/index.d.ts +2 -2
  97. package/lib/ServiceNetworking/PsaConnection.d.ts +2 -1
  98. package/lib/ServiceNetworking/PsaConnection.d.ts.map +1 -1
  99. package/lib/ServiceNetworking/PsaConnection.js +1 -1
  100. package/lib/ServiceUsage/ApiEnable.d.ts +2 -1
  101. package/lib/ServiceUsage/ApiEnable.d.ts.map +1 -1
  102. package/lib/ServiceUsage/ApiEnable.js +1 -1
  103. package/lib/Sqladmin/Database.d.ts +2 -1
  104. package/lib/Sqladmin/Database.d.ts.map +1 -1
  105. package/lib/Sqladmin/Database.js +1 -1
  106. package/lib/Sqladmin/Instance.d.ts +2 -1
  107. package/lib/Sqladmin/Instance.d.ts.map +1 -1
  108. package/lib/Sqladmin/Instance.js +7 -7
  109. package/lib/Sqladmin/Instance.js.map +1 -1
  110. package/lib/Sqladmin/Operations.d.ts +1 -1
  111. package/lib/Sqladmin/User.d.ts +2 -1
  112. package/lib/Sqladmin/User.d.ts.map +1 -1
  113. package/lib/Sqladmin/User.js +1 -1
  114. package/lib/Storage/Bucket.d.ts +2 -2
  115. package/lib/Storage/Bucket.d.ts.map +1 -1
  116. package/lib/Storage/Bucket.js +1 -1
  117. package/lib/Storage/StorageBucketIamMember.d.ts +2 -1
  118. package/lib/Storage/StorageBucketIamMember.d.ts.map +1 -1
  119. package/lib/Storage/StorageBucketIamMember.js +1 -1
  120. package/lib/Tags.d.ts +3 -1
  121. package/lib/Tags.d.ts.map +1 -1
  122. package/lib/Tags.js +9 -1
  123. package/lib/Tags.js.map +1 -1
  124. package/package.json +9 -9
  125. package/src/ArtifactRegistry/Operations.ts +1 -1
  126. package/src/ArtifactRegistry/Repository.ts +5 -5
  127. package/src/ArtifactRegistry/RepositoryIamMember.ts +1 -1
  128. package/src/CloudResourceManager/Project.ts +6 -6
  129. package/src/Compute/Address.ts +4 -4
  130. package/src/Compute/Firewall.ts +1 -1
  131. package/src/Compute/ForwardingRule.ts +4 -4
  132. package/src/Compute/GlobalAddress.ts +4 -4
  133. package/src/Compute/Network.ts +1 -1
  134. package/src/Compute/Operations.ts +1 -1
  135. package/src/Compute/ProjectDefaultNetworkTier.ts +1 -1
  136. package/src/Compute/SharedVpcHost.ts +1 -1
  137. package/src/Compute/SharedVpcServiceProject.ts +1 -1
  138. package/src/Compute/Subnetwork.ts +2 -2
  139. package/src/Container/Cluster.ts +36 -11
  140. package/src/Container/NodePool.ts +30 -13
  141. package/src/Container/Operations.ts +1 -1
  142. package/src/Iam/Operations.ts +81 -0
  143. package/src/Iam/ServiceAccount.ts +1 -1
  144. package/src/Iam/ServiceAccountKey.ts +1 -1
  145. package/src/Iam/WorkloadIdentityPool.ts +309 -0
  146. package/src/Iam/WorkloadIdentityPoolProvider.ts +429 -0
  147. package/src/Iam/index.ts +10 -0
  148. package/src/Kubernetes/Secret.ts +4 -1
  149. package/src/ManagedLustre/Instance.ts +6 -6
  150. package/src/Providers.ts +12 -0
  151. package/src/Run/IamSync.ts +1 -1
  152. package/src/Run/Job.ts +6 -6
  153. package/src/Run/Operations.ts +1 -1
  154. package/src/Run/Service.ts +9 -7
  155. package/src/Run/index.ts +2 -2
  156. package/src/ServiceNetworking/PsaConnection.ts +1 -1
  157. package/src/ServiceUsage/ApiEnable.ts +1 -1
  158. package/src/Sqladmin/Database.ts +1 -1
  159. package/src/Sqladmin/Instance.ts +13 -7
  160. package/src/Sqladmin/Operations.ts +1 -1
  161. package/src/Sqladmin/User.ts +1 -1
  162. package/src/Storage/Bucket.ts +1 -1
  163. package/src/Storage/StorageBucketIamMember.ts +1 -1
  164. package/src/Tags.ts +16 -1
@@ -0,0 +1,429 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as iam from "@distilled.cloud/gcp/unstable/iam_v1";
3
+ import { Resource } from "alchemy";
4
+ import { Unowned } from "alchemy/AdoptPolicy";
5
+ import { isResolved } from "alchemy/Diff";
6
+ import * as Provider from "alchemy/Provider";
7
+ import * as Effect from "effect/Effect";
8
+ import {
9
+ descriptionHasAlchemyMarker,
10
+ gcpAlchemyDescription,
11
+ normalizeStringMap,
12
+ stripAlchemyMarker,
13
+ } from "../Tags.ts";
14
+ import type * as GCP from "../Providers.ts";
15
+ import { makeAwaitOperation } from "./Operations.ts";
16
+ import { poolResourceName } from "./WorkloadIdentityPool.ts";
17
+
18
+ /**
19
+ * An OIDC **provider** inside a
20
+ * {@link import("./WorkloadIdentityPool.ts").WorkloadIdentityPool} — the
21
+ * thing that actually declares "I trust tokens issued by this URL".
22
+ *
23
+ * The canonical use is keyless federation from another cloud: point
24
+ * `oidc.issuerUri` at an EKS/AKS/GKE cluster's OIDC issuer, and pods holding
25
+ * a projected ServiceAccount token can impersonate a GSA with no key material
26
+ * anywhere in the system.
27
+ *
28
+ * ### `attributeMapping` is required and `google.subject` is mandatory
29
+ *
30
+ * The mapping turns claims on the incoming token into Google attributes. At
31
+ * minimum `google.subject` must be mapped; it is what `principal://` members
32
+ * match on. For a Kubernetes issuer the subject claim is
33
+ * `system:serviceaccount:{namespace}:{name}`, so:
34
+ *
35
+ * ```typescript
36
+ * attributeMapping: { "google.subject": "assertion.sub" }
37
+ * ```
38
+ *
39
+ * ### Set `allowedAudiences`, or the default is surprising
40
+ *
41
+ * When `oidc.allowedAudiences` is empty, GCP accepts only the *default*
42
+ * audience — the full provider resource name prefixed with
43
+ * `https://iam.googleapis.com/`. A Kubernetes projected token minted for
44
+ * audience `sts.googleapis.com` (or anything else) is then rejected with a
45
+ * generic audience error. Set this explicitly to whatever audience the
46
+ * token is actually minted for.
47
+ *
48
+ * ### Constrain trust with `attributeCondition`
49
+ *
50
+ * An issuer-only trust accepts **every** identity that issuer can mint — on
51
+ * a Kubernetes cluster, that is every ServiceAccount in every namespace. Pair
52
+ * the provider with an `attributeCondition` (and a narrow `principal://`
53
+ * member on the GSA binding) so a token from an unrelated namespace cannot
54
+ * impersonate anything.
55
+ *
56
+ * @section Creating a WorkloadIdentityPoolProvider
57
+ * @example Trust one Kubernetes ServiceAccount on an EKS cluster
58
+ * ```typescript
59
+ * const provider = yield* GCP.WorkloadIdentityPoolProvider("EksOidc", {
60
+ * project: "123456789",
61
+ * poolId: pool.poolId,
62
+ * providerId: "eks-oidc",
63
+ * oidc: {
64
+ * issuerUri: "https://oidc.eks.us-east-1.amazonaws.com/id/ABC123",
65
+ * allowedAudiences: ["sts.googleapis.com"],
66
+ * },
67
+ * attributeMapping: { "google.subject": "assertion.sub" },
68
+ * attributeCondition:
69
+ * "assertion.sub == 'system:serviceaccount:admin:eks-auth-reconciler'",
70
+ * });
71
+ * ```
72
+ */
73
+ export type WorkloadIdentityPoolProviderProps = {
74
+ /** Project that owns the pool. Prefer the project **number**. */
75
+ project: string;
76
+ /** ID of the enclosing pool. */
77
+ poolId: string;
78
+ /**
79
+ * Provider ID, 4–32 characters of `[a-z0-9-]`. Immutable — changing it
80
+ * replaces the provider. The `gcp-` prefix is reserved by Google.
81
+ */
82
+ providerId: string;
83
+ /** The OIDC issuer this provider trusts. */
84
+ oidc: {
85
+ /** Issuer URL. Must be HTTPS and serve an OIDC discovery document. */
86
+ issuerUri: string;
87
+ /**
88
+ * Acceptable `aud` values. Leave unset only if the token is minted for
89
+ * the provider's default audience — see the note above.
90
+ */
91
+ allowedAudiences?: string[];
92
+ /**
93
+ * Inline JWKS, for issuers whose keys are not publicly reachable. When
94
+ * unset, GCP fetches `jwks_uri` from the issuer's discovery document,
95
+ * which requires the issuer to be reachable from Google's network.
96
+ */
97
+ jwksJson?: string;
98
+ };
99
+ /**
100
+ * Claim-to-attribute mapping. Must include `google.subject`.
101
+ */
102
+ attributeMapping: Record<string, string>;
103
+ /** CEL expression further restricting which credentials are accepted. */
104
+ attributeCondition?: string;
105
+ /** Display name, max 32 characters. */
106
+ displayName?: string;
107
+ /** Description, max 256 characters. */
108
+ description?: string;
109
+ /** Disable the provider without deleting it. */
110
+ disabled?: boolean;
111
+ };
112
+
113
+ export type WorkloadIdentityPoolProviderAttributes = {
114
+ /** Full resource name, `…/workloadIdentityPools/{pool}/providers/{id}`. */
115
+ name: string;
116
+ project: string;
117
+ poolId: string;
118
+ providerId: string;
119
+ issuerUri: string | undefined;
120
+ // `readonly` because that is how the SDK models repeated fields; widening it
121
+ // to a mutable array here would only force a copy at every use site.
122
+ allowedAudiences: readonly string[] | undefined;
123
+ attributeMapping: Record<string, string> | undefined;
124
+ attributeCondition: string | undefined;
125
+ displayName: string | undefined;
126
+ description: string | undefined;
127
+ state: string | undefined;
128
+ disabled: boolean | undefined;
129
+ /**
130
+ * The audience string to put in a credential configuration, i.e.
131
+ * `//iam.googleapis.com/{name}`. Provided because assembling it by hand is
132
+ * easy to get subtly wrong (the leading `//`, and no scheme).
133
+ */
134
+ audience: string;
135
+ };
136
+
137
+ export interface WorkloadIdentityPoolProvider
138
+ extends Resource<
139
+ "GCP.WorkloadIdentityPoolProvider",
140
+ WorkloadIdentityPoolProviderProps,
141
+ WorkloadIdentityPoolProviderAttributes,
142
+ never,
143
+ GCP.Providers
144
+ > {}
145
+
146
+ export const WorkloadIdentityPoolProvider =
147
+ Resource<WorkloadIdentityPoolProvider>("GCP.WorkloadIdentityPoolProvider");
148
+
149
+ export const providerResourceName = (
150
+ project: string,
151
+ poolId: string,
152
+ providerId: string,
153
+ ) => `${poolResourceName(project, poolId)}/providers/${providerId}`;
154
+
155
+ /**
156
+ * Compare two claim mappings by CONTENT, not serialisation.
157
+ *
158
+ * `JSON.stringify` preserves key insertion order, and the order GCP returns
159
+ * these keys in need not match the order they were sent in. A raw stringify
160
+ * comparison therefore reports drift for two identical mappings and fires a
161
+ * patch LRO on every single deploy.
162
+ */
163
+ const sameMapping = (
164
+ a: Record<string, string> | undefined,
165
+ b: Record<string, string> | undefined,
166
+ ) => {
167
+ const entries = (r: Record<string, string> | undefined) =>
168
+ Object.entries(r ?? {}).sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0));
169
+ return JSON.stringify(entries(a)) === JSON.stringify(entries(b));
170
+ };
171
+
172
+ /**
173
+ * Compare audience allow-lists as SETS.
174
+ *
175
+ * `allowedAudiences` is an allow-list: membership is what matters, order is
176
+ * not meaningful, and GCP is free to return it in any order. Comparing it
177
+ * positionally would make a reordering look like drift forever.
178
+ */
179
+ const sameAudiences = (
180
+ a: readonly string[] | undefined,
181
+ b: readonly string[] | undefined,
182
+ ) => {
183
+ const sorted = (v: readonly string[] | undefined) => [...(v ?? [])].sort();
184
+ return JSON.stringify(sorted(a)) === JSON.stringify(sorted(b));
185
+ };
186
+
187
+ /**
188
+ * Treat absent and empty-string as the same value.
189
+ *
190
+ * The patch below uses a FIXED `updateMask` while the request body omits
191
+ * unset optional fields — which is deliberate: under a field mask, an omitted
192
+ * field means "clear it", so dropping `displayName` from the props really
193
+ * should clear it on GCP. The hazard is only in the comparison afterwards. If
194
+ * GCP echoes a cleared field back as `""` rather than omitting it, a raw
195
+ * `!==` against `undefined` would report drift on every single deploy and
196
+ * churn a long-running patch operation forever.
197
+ */
198
+ const sameText = (a: string | undefined, b: string | undefined) =>
199
+ (a ?? "") === (b ?? "");
200
+
201
+ export const WorkloadIdentityPoolProviderProvider = () =>
202
+ Provider.effect(
203
+ WorkloadIdentityPoolProvider,
204
+ Effect.gen(function* () {
205
+ const getProvider =
206
+ yield* iam.getProjectsLocationsWorkloadIdentityPoolsProviders;
207
+ const createProvider =
208
+ yield* iam.createProjectsLocationsWorkloadIdentityPoolsProviders;
209
+ const patchProvider =
210
+ yield* iam.patchProjectsLocationsWorkloadIdentityPoolsProviders;
211
+ const deleteProvider =
212
+ yield* iam.deleteProjectsLocationsWorkloadIdentityPoolsProviders;
213
+ const undeleteProvider =
214
+ yield* iam.undeleteProjectsLocationsWorkloadIdentityPoolsProviders;
215
+ const getOperations =
216
+ yield* iam.getProjectsLocationsWorkloadIdentityPoolsProvidersOperations;
217
+ const awaitOperation = makeAwaitOperation(
218
+ getOperations,
219
+ "Workload Identity provider",
220
+ );
221
+
222
+ const observeProvider = (
223
+ project: string,
224
+ poolId: string,
225
+ providerId: string,
226
+ ) =>
227
+ getProvider({
228
+ name: providerResourceName(project, poolId, providerId),
229
+ }).pipe(
230
+ Effect.catchTag("NotFound", () =>
231
+ Effect.succeed(
232
+ undefined as iam.WorkloadIdentityPoolProvider | undefined,
233
+ ),
234
+ ),
235
+ Effect.catchTag("Forbidden", () =>
236
+ Effect.succeed(
237
+ undefined as iam.WorkloadIdentityPoolProvider | undefined,
238
+ ),
239
+ ),
240
+ );
241
+
242
+ const toAttrs = (
243
+ project: string,
244
+ poolId: string,
245
+ providerId: string,
246
+ provider: iam.WorkloadIdentityPoolProvider,
247
+ ): WorkloadIdentityPoolProviderAttributes => {
248
+ const name =
249
+ provider.name ?? providerResourceName(project, poolId, providerId);
250
+ return {
251
+ name,
252
+ project,
253
+ poolId,
254
+ providerId,
255
+ issuerUri: provider.oidc?.issuerUri,
256
+ allowedAudiences: provider.oidc?.allowedAudiences,
257
+ attributeMapping: normalizeStringMap(provider.attributeMapping),
258
+ attributeCondition: provider.attributeCondition,
259
+ displayName: provider.displayName,
260
+ description: stripAlchemyMarker(provider.description),
261
+ state: provider.state,
262
+ disabled: provider.disabled,
263
+ audience: `//iam.googleapis.com/${name}`,
264
+ };
265
+ };
266
+
267
+ return {
268
+ stables: ["name", "project", "poolId", "providerId", "audience"],
269
+ diff: Effect.fn(function* ({ olds = {}, news, output }) {
270
+ if (!isResolved(news)) return undefined;
271
+ const oldProps = olds as Partial<WorkloadIdentityPoolProviderProps>;
272
+ // Prefer live attributes over persisted props, and fall back to the
273
+ // DESIRED value when neither is known — see the equivalent comment
274
+ // in WorkloadIdentityPool.ts. Defaulting to `undefined` would make
275
+ // an adoption replace a live, correct provider.
276
+ //
277
+ // All three fields are part of the resource name; the API cannot
278
+ // move a provider between pools or rename it.
279
+ const currentProject = output?.project || oldProps.project || news.project;
280
+ const currentPoolId = output?.poolId || oldProps.poolId || news.poolId;
281
+ const currentProviderId =
282
+ output?.providerId || oldProps.providerId || news.providerId;
283
+ if (
284
+ currentProject !== news.project ||
285
+ currentPoolId !== news.poolId ||
286
+ currentProviderId !== news.providerId
287
+ ) {
288
+ return { action: "replace" } as const;
289
+ }
290
+ }),
291
+ read: Effect.fn(function* ({ id, olds, output }) {
292
+ const project = output?.project || olds?.project;
293
+ const poolId = output?.poolId || olds?.poolId;
294
+ const providerId = output?.providerId || olds?.providerId;
295
+ if (!project || !poolId || !providerId) return undefined;
296
+ const provider = yield* observeProvider(project, poolId, providerId);
297
+ if (!provider) return undefined;
298
+ const attrs = toAttrs(project, poolId, providerId, provider);
299
+ return (yield* descriptionHasAlchemyMarker(id, provider.description))
300
+ ? attrs
301
+ : Unowned(attrs);
302
+ }),
303
+ reconcile: Effect.fn(function* ({ id, news, session }) {
304
+ const { project, poolId, providerId } = news;
305
+ if (!news.attributeMapping["google.subject"]) {
306
+ // Caught here rather than at the API, which rejects it with a
307
+ // message that does not name the missing key.
308
+ return yield* new ConfigError({
309
+ message: `Workload Identity provider ${providerId} must map 'google.subject' in attributeMapping.`,
310
+ });
311
+ }
312
+ const description = yield* gcpAlchemyDescription(
313
+ id,
314
+ news.description,
315
+ );
316
+ // Scalars are TOTAL over the updateMask below — always present,
317
+ // explicit empty/default when unset — so clearing is stated in code
318
+ // rather than implied by field-mask semantics.
319
+ //
320
+ // `attributeCondition` and `oidc.jwksJson` are the exception and
321
+ // stay conditional: both are validated formats (a CEL expression
322
+ // and a JWKS document), and sending "" risks a parse rejection
323
+ // where omission is unambiguous. They are still cleared correctly
324
+ // when unset — `oidc` is masked as a whole message, so the body's
325
+ // `oidc` replaces it outright.
326
+ const body: iam.WorkloadIdentityPoolProvider = {
327
+ displayName: news.displayName ?? "",
328
+ description,
329
+ disabled: news.disabled ?? false,
330
+ attributeMapping: news.attributeMapping,
331
+ ...(news.attributeCondition
332
+ ? { attributeCondition: news.attributeCondition }
333
+ : {}),
334
+ oidc: {
335
+ issuerUri: news.oidc.issuerUri,
336
+ allowedAudiences: news.oidc.allowedAudiences ?? [],
337
+ ...(news.oidc.jwksJson ? { jwksJson: news.oidc.jwksJson } : {}),
338
+ },
339
+ };
340
+
341
+ let provider = yield* observeProvider(project, poolId, providerId);
342
+
343
+ // Same soft-delete reasoning as the pool: the ID is held until the
344
+ // purge, so undelete rather than strand the deploy.
345
+ if (provider?.state === "DELETED") {
346
+ yield* session.note(
347
+ `Undeleting soft-deleted Workload Identity provider ${providerId}…`,
348
+ );
349
+ const op = yield* undeleteProvider({
350
+ name: providerResourceName(project, poolId, providerId),
351
+ body: {},
352
+ });
353
+ if (op.name) yield* awaitOperation(op.name, session);
354
+ provider = yield* observeProvider(project, poolId, providerId);
355
+ }
356
+
357
+ if (!provider) {
358
+ const op = yield* createProvider({
359
+ parent: poolResourceName(project, poolId),
360
+ workloadIdentityPoolProviderId: providerId,
361
+ body,
362
+ });
363
+ if (op.name) yield* awaitOperation(op.name, session);
364
+ provider = yield* observeProvider(project, poolId, providerId);
365
+ if (!provider) {
366
+ return yield* new ConfigError({
367
+ message: `Workload Identity provider ${providerId} in pool ${poolId} was not readable after create.`,
368
+ });
369
+ }
370
+ } else {
371
+ const needsPatch =
372
+ !sameText(provider.displayName, news.displayName) ||
373
+ !sameText(provider.description, description) ||
374
+ (provider.disabled ?? false) !== (news.disabled ?? false) ||
375
+ !sameText(provider.attributeCondition, news.attributeCondition) ||
376
+ !sameMapping(
377
+ normalizeStringMap(provider.attributeMapping),
378
+ news.attributeMapping,
379
+ ) ||
380
+ !sameText(provider.oidc?.issuerUri, news.oidc.issuerUri) ||
381
+ !sameAudiences(
382
+ provider.oidc?.allowedAudiences,
383
+ news.oidc.allowedAudiences,
384
+ ) ||
385
+ !sameText(provider.oidc?.jwksJson, news.oidc.jwksJson);
386
+ if (needsPatch) {
387
+ const op = yield* patchProvider({
388
+ name: providerResourceName(project, poolId, providerId),
389
+ updateMask:
390
+ "displayName,description,disabled,attributeMapping,attributeCondition,oidc",
391
+ body,
392
+ });
393
+ if (op.name) yield* awaitOperation(op.name, session);
394
+ provider =
395
+ (yield* observeProvider(project, poolId, providerId)) ??
396
+ provider;
397
+ }
398
+ }
399
+
400
+ yield* session.note(
401
+ providerResourceName(project, poolId, providerId),
402
+ );
403
+ return toAttrs(project, poolId, providerId, provider);
404
+ }),
405
+ delete: Effect.fn(function* ({ olds, output, session }) {
406
+ // Props fallback for the same reason as the pool: incomplete
407
+ // attributes must not turn destroy into a silent no-op that leaks a
408
+ // soft-deleted-name-holding resource.
409
+ // `||` not `??`: an empty string is never a valid identity here,
410
+ // and a persisted `""` must fall through to the next source rather
411
+ // than be taken as real. With `??` a corrupt/partial state entry
412
+ // would short-circuit the fallback and turn destroy into a silent
413
+ // no-op (or make diff replace a live resource).
414
+ const project = output?.project || olds?.project;
415
+ const poolId = output?.poolId || olds?.poolId;
416
+ const providerId = output?.providerId || olds?.providerId;
417
+ if (!project || !poolId || !providerId) return;
418
+ const op = yield* deleteProvider({
419
+ name: providerResourceName(project, poolId, providerId),
420
+ }).pipe(
421
+ Effect.catchTag("NotFound", () =>
422
+ Effect.succeed({ name: undefined } as iam.Operation),
423
+ ),
424
+ );
425
+ if (op.name) yield* awaitOperation(op.name, session);
426
+ }),
427
+ };
428
+ }),
429
+ );
package/src/Iam/index.ts CHANGED
@@ -6,3 +6,13 @@ export {
6
6
  ServiceAccountKeyProvider,
7
7
  } from "./ServiceAccountKey.ts";
8
8
  export type { ServiceAccountKeyProps } from "./ServiceAccountKey.ts";
9
+ export {
10
+ WorkloadIdentityPool,
11
+ WorkloadIdentityPoolResourceProvider,
12
+ } from "./WorkloadIdentityPool.ts";
13
+ export type { WorkloadIdentityPoolProps } from "./WorkloadIdentityPool.ts";
14
+ export {
15
+ WorkloadIdentityPoolProvider,
16
+ WorkloadIdentityPoolProviderProvider,
17
+ } from "./WorkloadIdentityPoolProvider.ts";
18
+ export type { WorkloadIdentityPoolProviderProps } from "./WorkloadIdentityPoolProvider.ts";
@@ -123,7 +123,10 @@ type SecretObject = {
123
123
  namespace?: string;
124
124
  uid?: string;
125
125
  resourceVersion?: string;
126
- labels?: Record<string, string>;
126
+ // Distilled's generated `…ObjectMetaLabelsMap` index signature includes
127
+ // `undefined`, so the observed object is only assignable to this local
128
+ // subset if we mirror that.
129
+ labels?: Record<string, string | undefined>;
127
130
  };
128
131
  type?: string;
129
132
  };
@@ -1,5 +1,5 @@
1
1
  import { ConfigError } from "@distilled.cloud/gcp";
2
- import * as lustre from "@distilled.cloud/gcp/lustre-v1";
2
+ import * as lustre from "@distilled.cloud/gcp/lustre_v1";
3
3
  import { Resource } from "alchemy";
4
4
  import { Unowned } from "alchemy/AdoptPolicy";
5
5
  import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
@@ -11,7 +11,7 @@ import * as Duration from "effect/Duration";
11
11
  import * as Effect from "effect/Effect";
12
12
  import * as Schedule from "effect/Schedule";
13
13
  import type { NetworkRefById } from "../Compute/Network.ts";
14
- import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
14
+ import { gcpInternalLabels, hasAlchemyLabels, normalizeStringMap } from "../Tags.ts";
15
15
  import type * as GCP from "../Providers.ts";
16
16
 
17
17
  /**
@@ -221,7 +221,7 @@ const toAttributes = (
221
221
  perUnitStorageThroughput: i.perUnitStorageThroughput,
222
222
  gkeSupportEnabled: i.gkeSupportEnabled,
223
223
  kmsKey: i.kmsKey,
224
- labels: { ...(i.labels ?? {}) },
224
+ labels: normalizeStringMap(i.labels) ?? {},
225
225
  });
226
226
 
227
227
  export const ManagedLustreInstanceProvider = () =>
@@ -295,7 +295,7 @@ export const ManagedLustreInstanceProvider = () =>
295
295
  if ((args.observed.description ?? undefined) !== args.desired.description) {
296
296
  updateMaskFields.push("description");
297
297
  }
298
- const observedLabels = { ...(args.observed.labels ?? {}) };
298
+ const observedLabels = normalizeStringMap(args.observed.labels) ?? {};
299
299
  const labelDiff = diffTags(observedLabels, args.desired.labels);
300
300
  if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
301
301
  updateMaskFields.push("labels");
@@ -434,7 +434,7 @@ export const ManagedLustreInstanceProvider = () =>
434
434
  ),
435
435
  );
436
436
  for (const candidate of page?.instances ?? []) {
437
- if (yield* hasAlchemyLabels(id, candidate.labels)) {
437
+ if (yield* hasAlchemyLabels(id, normalizeStringMap(candidate.labels))) {
438
438
  observed = candidate;
439
439
  break;
440
440
  }
@@ -450,7 +450,7 @@ export const ManagedLustreInstanceProvider = () =>
450
450
  instanceId,
451
451
  network,
452
452
  });
453
- return (yield* hasAlchemyLabels(id, observed.labels))
453
+ return (yield* hasAlchemyLabels(id, normalizeStringMap(observed.labels)))
454
454
  ? attrs
455
455
  : Unowned(attrs);
456
456
  }),
package/src/Providers.ts CHANGED
@@ -39,6 +39,14 @@ import {
39
39
  ServiceAccountKey,
40
40
  ServiceAccountKeyProvider,
41
41
  } from "./Iam/ServiceAccountKey.ts";
42
+ import {
43
+ WorkloadIdentityPool,
44
+ WorkloadIdentityPoolResourceProvider,
45
+ } from "./Iam/WorkloadIdentityPool.ts";
46
+ import {
47
+ WorkloadIdentityPoolProvider,
48
+ WorkloadIdentityPoolProviderProvider,
49
+ } from "./Iam/WorkloadIdentityPoolProvider.ts";
42
50
  import {
43
51
  HelmRelease,
44
52
  HelmReleaseProvider,
@@ -123,6 +131,8 @@ export const providers = () =>
123
131
  ArtifactRegistryRepositoryIamMember,
124
132
  ServiceAccount,
125
133
  ServiceAccountKey,
134
+ WorkloadIdentityPool,
135
+ WorkloadIdentityPoolProvider,
126
136
  KubernetesSecret,
127
137
  KubernetesManifest,
128
138
  HelmRelease,
@@ -156,6 +166,8 @@ export const providers = () =>
156
166
  ArtifactRegistryRepositoryIamMemberProvider(),
157
167
  ServiceAccountProvider(),
158
168
  ServiceAccountKeyProvider(),
169
+ WorkloadIdentityPoolResourceProvider(),
170
+ WorkloadIdentityPoolProviderProvider(),
159
171
  KubernetesSecretProvider(),
160
172
  KubernetesManifestProvider(),
161
173
  HelmReleaseProvider(),
@@ -1,4 +1,4 @@
1
- import type * as run from "@distilled.cloud/gcp/run-v2";
1
+ import type * as run from "@distilled.cloud/gcp/run_v2";
2
2
  import type { ResourceBinding } from "alchemy/Resource";
3
3
  import * as Duration from "effect/Duration";
4
4
  import * as Effect from "effect/Effect";
package/src/Run/Job.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as run from "@distilled.cloud/gcp/run-v2";
1
+ import * as run from "@distilled.cloud/gcp/run_v2";
2
2
  import { Resource } from "alchemy";
3
3
  import { Unowned } from "alchemy/AdoptPolicy";
4
4
  import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
@@ -10,7 +10,7 @@ import * as Duration from "effect/Duration";
10
10
  import * as Effect from "effect/Effect";
11
11
  import * as Schedule from "effect/Schedule";
12
12
  import type * as GCP from "../Providers.ts";
13
- import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
13
+ import { gcpInternalLabels, hasAlchemyLabels, normalizeStringMap } from "../Tags.ts";
14
14
  import { makeSyncIam, type RunIamBindingContract } from "./IamSync.ts";
15
15
  import { makeAwaitOperation } from "./Operations.ts";
16
16
  import {
@@ -34,7 +34,7 @@ import {
34
34
  * converge-to-desired-state model) and is left to user-side code:
35
35
  *
36
36
  * ```typescript
37
- * import * as run from "@distilled.cloud/gcp/run-v2";
37
+ * import * as run from "@distilled.cloud/gcp/run_v2";
38
38
  * const job = yield* GCP.Job("Nightly", { ... });
39
39
  * const runJob = yield* run.runProjectsLocationsJobs;
40
40
  * yield* runJob({
@@ -234,7 +234,7 @@ const toAttributes = (
234
234
  latestCreatedExecution: j.latestCreatedExecution,
235
235
  reconciling: j.reconciling,
236
236
  etag: j.etag,
237
- labels: { ...(j.labels ?? {}) },
237
+ labels: normalizeStringMap(j.labels) ?? {},
238
238
  createTime: j.createTime,
239
239
  updateTime: j.updateTime,
240
240
  });
@@ -260,7 +260,7 @@ const jobMutated = (
260
260
  desiredLabels: Record<string, string>,
261
261
  ): boolean => {
262
262
  const labelDiff = diffTags(
263
- { ...(observed.labels ?? {}) },
263
+ normalizeStringMap(observed.labels) ?? {},
264
264
  desiredLabels,
265
265
  );
266
266
  if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) return true;
@@ -448,7 +448,7 @@ export const JobProvider = () =>
448
448
  const observed = yield* observe(project, location, name);
449
449
  if (!observed) return undefined;
450
450
  const attrs = toAttributes(observed, { project, location, name });
451
- return (yield* hasAlchemyLabels(id, observed.labels))
451
+ return (yield* hasAlchemyLabels(id, normalizeStringMap(observed.labels)))
452
452
  ? attrs
453
453
  : Unowned(attrs);
454
454
  }),
@@ -1,5 +1,5 @@
1
1
  import { ConfigError } from "@distilled.cloud/gcp";
2
- import * as run from "@distilled.cloud/gcp/run-v2";
2
+ import * as run from "@distilled.cloud/gcp/run_v2";
3
3
  import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
4
4
  import * as Duration from "effect/Duration";
5
5
  import * as Effect from "effect/Effect";
@@ -1,4 +1,4 @@
1
- import * as run from "@distilled.cloud/gcp/run-v2";
1
+ import * as run from "@distilled.cloud/gcp/run_v2";
2
2
  import { Resource } from "alchemy";
3
3
  import { Unowned } from "alchemy/AdoptPolicy";
4
4
  import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
@@ -10,7 +10,7 @@ import * as Duration from "effect/Duration";
10
10
  import * as Effect from "effect/Effect";
11
11
  import * as Schedule from "effect/Schedule";
12
12
  import type * as GCP from "../Providers.ts";
13
- import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
13
+ import { gcpInternalLabels, hasAlchemyLabels, normalizeStringMap } from "../Tags.ts";
14
14
  import { makeSyncIam, type RunIamBindingContract } from "./IamSync.ts";
15
15
  import { makeAwaitOperation } from "./Operations.ts";
16
16
  import {
@@ -245,8 +245,10 @@ const toServiceBody = (
245
245
  ? { defaultUriDisabled: news.defaultUriDisabled }
246
246
  : {}),
247
247
  ...(news.scaling ? { scaling: news.scaling } : {}),
248
- ...(news.traffic ? { traffic: news.traffic } : {}),
249
- ...(news.customAudiences ? { customAudiences: news.customAudiences } : {}),
248
+ ...(news.traffic ? { traffic: [...news.traffic] } : {}),
249
+ ...(news.customAudiences
250
+ ? { customAudiences: [...news.customAudiences] }
251
+ : {}),
250
252
  ...(news.binaryAuthorization
251
253
  ? { binaryAuthorization: news.binaryAuthorization }
252
254
  : {}),
@@ -271,7 +273,7 @@ const toAttributes = (
271
273
  terminalCondition: s.terminalCondition,
272
274
  trafficStatuses: s.trafficStatuses ?? [],
273
275
  etag: s.etag,
274
- labels: { ...(s.labels ?? {}) },
276
+ labels: normalizeStringMap(s.labels) ?? {},
275
277
  createTime: s.createTime,
276
278
  updateTime: s.updateTime,
277
279
  });
@@ -314,7 +316,7 @@ export const ServiceProvider = () =>
314
316
  updateMaskFields.push("description");
315
317
  }
316
318
  const labelDiff = diffTags(
317
- { ...(args.observed.labels ?? {}) },
319
+ normalizeStringMap(args.observed.labels) ?? {},
318
320
  args.desiredLabels,
319
321
  );
320
322
  if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
@@ -536,7 +538,7 @@ export const ServiceProvider = () =>
536
538
  const observed = yield* observe(project, location, name);
537
539
  if (!observed) return undefined;
538
540
  const attrs = toAttributes(observed, { project, location, name });
539
- return (yield* hasAlchemyLabels(id, observed.labels))
541
+ return (yield* hasAlchemyLabels(id, normalizeStringMap(observed.labels)))
540
542
  ? attrs
541
543
  : Unowned(attrs);
542
544
  }),
package/src/Run/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type * as run from "@distilled.cloud/gcp/run-v2";
1
+ import type * as run from "@distilled.cloud/gcp/run_v2";
2
2
 
3
3
  export { Job, JobProvider } from "./Job.ts";
4
4
  export type { JobAttributes, JobProps } from "./Job.ts";
@@ -9,7 +9,7 @@ export type { ServiceAttributes, ServiceProps } from "./Service.ts";
9
9
 
10
10
  /**
11
11
  * Clean aliases for the most commonly-used Cloud Run v2 SDK types,
12
- * re-exported from `@distilled.cloud/gcp/run-v2` so consumers don't
12
+ * re-exported from `@distilled.cloud/gcp/run_v2` so consumers don't
13
13
  * need to import from the underlying SDK directly. The aliases are
14
14
  * type-only — they share identity with the distilled types, so values
15
15
  * are interchangeable.