@microagi/alchemy-gcp 0.7.0 → 0.8.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to `@microagi/alchemy-gcp`. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this package
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.8.0 — 2026-06-17
8
+
9
+ ### Added
10
+
11
+ - **`GCP.ServiceAccount`** — Alchemy resource for managing Google
12
+ Cloud service accounts (GSAs). Wraps the iam-v1 API
13
+ (`@distilled.cloud/gcp/unstable/iam-v1` — the `iamcredentials-v1`
14
+ surface was already in the stable index; `iam-v1` lives under
15
+ `unstable/` because the auto-generated codegen hasn't been manually
16
+ reviewed). Adoption gated on an alchemy marker embedded in the
17
+ GSA's `description` field (GSAs have no labels field, so the
18
+ label-based pattern used by `GCP.Project` doesn't apply — see
19
+ `Tags.descriptionHasAlchemyMarker`). Cold recovery (state loss)
20
+ falls back to a project-wide scan-by-marker when persisted
21
+ `output`/`olds` lack the `uniqueId`. Synchronous lifecycle — no LRO
22
+ polling required.
23
+
24
+ - **`GCP.serviceAccountIamMember`** — target-side binding helper, the
25
+ same shape as the existing `projectIamMember` and
26
+ `subnetworkIamMember`. Records a `(role, member)` grant onto the
27
+ GSA's binding bag; the ServiceAccount provider's `reconcile` merges
28
+ all bound entries by role into a single `setIamPolicy` call on the
29
+ GSA. Foreign bindings on the policy (non-alchemy roles, or members
30
+ we don't manage on the same role) are preserved verbatim. Etag
31
+ round-trip handles concurrent edits; `Conflict` triggers a
32
+ re-read + retry.
33
+
34
+ ### Use cases
35
+
36
+ - Workload Identity bindings — `roles/iam.workloadIdentityUser` on a
37
+ GSA from a k8s SA. Replaces the manual `gcloud iam service-accounts
38
+ add-iam-policy-binding` dance that's been applied out-of-band for
39
+ the `research-ui` and `preview-builder` GSAs.
40
+ - Per-namespace `roles/artifactregistry.writer` on a GSA scoped to
41
+ a single research pod (instead of project-wide via
42
+ `projectIamMember`).
43
+ - Impersonation grants — `roles/iam.serviceAccountTokenCreator` /
44
+ `roles/iam.serviceAccountUser`.
45
+
7
46
  ## 0.7.0 — 2026-06-16
8
47
 
9
48
  ### Added
@@ -0,0 +1,104 @@
1
+ import { Resource } from "alchemy";
2
+ import * as Provider from "alchemy/Provider";
3
+ import type * as GCP from "../Providers.ts";
4
+ /**
5
+ * A GCP service account (GSA). Lives under a {@link import("../CloudResourceManager/Project.ts").Project}
6
+ * and serves as the federated identity target for Workload Identity
7
+ * (`iam.gke.io/gcp-service-account: <email>` on a k8s SA) and as the
8
+ * principal in any IAM grant that should out-live a human user.
9
+ *
10
+ * Adoption is gated on an alchemy marker embedded in the GSA's
11
+ * `description` field (see `gcpAlchemyDescription` /
12
+ * `descriptionHasAlchemyMarker`) — GSAs have no labels field, so the
13
+ * label-based adoption gate used by Project can't apply. The marker is
14
+ * only written on insert; `description` is mutable but we never *clear*
15
+ * it because doing so would orphan the adoption signal.
16
+ *
17
+ * All lifecycle operations (`createProjectsServiceAccounts`,
18
+ * `patchProjectsServiceAccounts`, `deleteProjectsServiceAccounts`) are
19
+ * synchronous — no LRO polling required.
20
+ *
21
+ * @section Creating a ServiceAccount
22
+ * @example Minimal GSA under a project
23
+ * ```typescript
24
+ * const gsa = yield* GCP.ServiceAccount("Builder", {
25
+ * project: project.projectId,
26
+ * accountId: "research-builder",
27
+ * displayName: "Research pod build + push (BuildKit client)",
28
+ * });
29
+ * ```
30
+ *
31
+ * @example With explicit description and Workload Identity binding
32
+ * ```typescript
33
+ * const gsa = yield* GCP.ServiceAccount("Builder", {
34
+ * project: project.projectId,
35
+ * accountId: "research-builder",
36
+ * displayName: "Research pod build + push",
37
+ * description: "Used by per-researcher BuildKit clients to push to AR.",
38
+ * });
39
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterA-WorkloadIdentity", {
40
+ * role: "roles/iam.workloadIdentityUser",
41
+ * member: `serviceAccount:${projectA.projectId}.svc.id.goog[researchers/build]`,
42
+ * });
43
+ * ```
44
+ */
45
+ export type ServiceAccountProps = {
46
+ /**
47
+ * GCP project id (e.g. `micro-research-shared`) the GSA lives under.
48
+ * Immutable — changing this orphans the GSA's unique id and any
49
+ * bindings, so `diff` triggers a replacement.
50
+ */
51
+ project: string;
52
+ /**
53
+ * The account id segment of the service account's email. Must match
54
+ * `[a-z]([-a-z0-9]*[a-z0-9])`, 6-30 chars. Immutable — replaces.
55
+ */
56
+ accountId: string;
57
+ /**
58
+ * User-visible display name. Max 100 UTF-8 bytes. Mutable.
59
+ */
60
+ displayName?: string;
61
+ /**
62
+ * User-supplied description (max 256 UTF-8 bytes). The alchemy
63
+ * ownership marker is prepended automatically — see
64
+ * `gcpAlchemyDescription`. The user-supplied portion is preserved
65
+ * verbatim and surfaced back through `Attributes.description`.
66
+ */
67
+ description?: string;
68
+ };
69
+ /**
70
+ * Single (role, members) entry on a GSA's IAM policy. Targets declare
71
+ * this contract so capabilities (e.g. Workload Identity) can `.bind`
72
+ * IAM grants onto the GSA — the provider's `reconcile` merges all
73
+ * bound entries by role into a single `setIamPolicy` call.
74
+ */
75
+ export type ServiceAccountIamBinding = {
76
+ /** IAM role, e.g. `"roles/iam.workloadIdentityUser"`. */
77
+ role: string;
78
+ /** Principals, e.g. `["serviceAccount:foo.svc.id.goog[ns/ksa]"]`. */
79
+ members: ReadonlyArray<string>;
80
+ };
81
+ export type ServiceAccountBindingContract = {
82
+ iamBindings: ReadonlyArray<ServiceAccountIamBinding>;
83
+ };
84
+ export type ServiceAccount = Resource<"GCP.ServiceAccount", ServiceAccountProps, {
85
+ /** Full resource name, e.g. `projects/micro-research-shared/serviceAccounts/research-builder@micro-research-shared.iam.gserviceaccount.com`. */
86
+ name: string;
87
+ /** Email address derived from the account id, e.g. `research-builder@micro-research-shared.iam.gserviceaccount.com`. */
88
+ email: string;
89
+ /** Server-assigned stable numeric id. */
90
+ uniqueId: string;
91
+ /** Project the GSA lives under. */
92
+ projectId: string;
93
+ /** Account id (the part before `@`). */
94
+ accountId: string;
95
+ /** User-visible display name, or `undefined` if never set. */
96
+ displayName: string | undefined;
97
+ /** User-visible description (without the alchemy marker), or `undefined` if never set. */
98
+ description: string | undefined;
99
+ /** Whether the GSA is currently disabled. */
100
+ disabled: boolean;
101
+ }, ServiceAccountBindingContract, GCP.Providers>;
102
+ export declare const ServiceAccount: import("alchemy").ResourceClass<ServiceAccount>;
103
+ export declare const ServiceAccountProvider: () => import("effect/Layer").Layer<Provider.Provider<ServiceAccount>, never, import("alchemy").Stack | import("alchemy").Stage | import("@distilled.cloud/gcp").Credentials | import("effect/unstable/http/HttpClient").HttpClient>;
104
+ //# sourceMappingURL=ServiceAccount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceAccount.d.ts","sourceRoot":"","sources":["../../src/Iam/ServiceAccount.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAU7C,OAAO,KAAK,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,WAAW,EAAE,aAAa,CAAC,wBAAwB,CAAC,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,QAAQ,CACnC,oBAAoB,EACpB,mBAAmB,EACnB;IACE,gJAAgJ;IAChJ,IAAI,EAAE,MAAM,CAAC;IACb,wHAAwH;IACxH,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,0FAA0F;IAC1F,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,6CAA6C;IAC7C,QAAQ,EAAE,OAAO,CAAC;CACnB,EACD,6BAA6B,EAC7B,GAAG,CAAC,SAAS,CACd,CAAC;AAEF,eAAO,MAAM,cAAc,iDAAiD,CAAC;AAgB7E,eAAO,MAAM,sBAAsB,qOAiShC,CAAC"}
@@ -0,0 +1,236 @@
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 { diffTags } from "alchemy/Tags";
8
+ import * as Duration from "effect/Duration";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Schedule from "effect/Schedule";
11
+ import { descriptionHasAlchemyMarker, gcpAlchemyDescription, stripAlchemyMarker, } from "../Tags.js";
12
+ export const ServiceAccount = Resource("GCP.ServiceAccount");
13
+ const toAttributes = (sa) => {
14
+ const accountId = (sa.email ?? "").split("@")[0];
15
+ return {
16
+ name: sa.name ?? "",
17
+ email: sa.email ?? "",
18
+ uniqueId: sa.uniqueId ?? "",
19
+ projectId: sa.projectId ?? "",
20
+ accountId,
21
+ displayName: sa.displayName,
22
+ description: stripAlchemyMarker(sa.description),
23
+ disabled: sa.disabled ?? false,
24
+ };
25
+ };
26
+ export const ServiceAccountProvider = () => Provider.effect(ServiceAccount, Effect.gen(function* () {
27
+ const getProjectsServiceAccounts = yield* iam.getProjectsServiceAccounts;
28
+ const createProjectsServiceAccounts = yield* iam.createProjectsServiceAccounts;
29
+ const patchProjectsServiceAccounts = yield* iam.patchProjectsServiceAccounts;
30
+ const deleteProjectsServiceAccounts = yield* iam.deleteProjectsServiceAccounts;
31
+ const listProjectsServiceAccounts = yield* iam.listProjectsServiceAccounts;
32
+ const getIamPolicyProjectsServiceAccounts = yield* iam.getIamPolicyProjectsServiceAccounts;
33
+ const setIamPolicyProjectsServiceAccounts = yield* iam.setIamPolicyProjectsServiceAccounts;
34
+ const resourceName = (projectId, accountId) => `projects/${projectId}/serviceAccounts/${accountId}`;
35
+ const observeServiceAccount = (projectId, accountId) => getProjectsServiceAccounts({
36
+ name: resourceName(projectId, accountId),
37
+ }).pipe(
38
+ // GSAs that don't exist OR that the caller can't see both
39
+ // collapse to "absent" — same 403→missing pattern as the
40
+ // project observer.
41
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)), Effect.catchTag("Forbidden", () => Effect.succeed(undefined)));
42
+ const requireServiceAccount = (projectId, accountId, context) => observeServiceAccount(projectId, accountId).pipe(Effect.flatMap((sa) => sa
43
+ ? Effect.succeed(sa)
44
+ : Effect.fail(new ConfigError({
45
+ message: `ServiceAccount ${accountId} in project ${projectId} ${context}.`,
46
+ }))));
47
+ // List-page scan for cold recovery. Alchemy marker in the
48
+ // description field identifies our GSAs; the page boundary is
49
+ // arbitrary — we still need to page through to find any marker
50
+ // that happens to fall past the first page.
51
+ const findByAlchemyMarker = Effect.fn(function* (id, projectId) {
52
+ let pageToken;
53
+ // Up to 100 pages × 100 items per page = 10000 GSAs; well past
54
+ // any realistic project size. If we ever exceed that we should
55
+ // cap and warn rather than silently miss matches.
56
+ for (let i = 0; i < 100; i++) {
57
+ const page = yield* listProjectsServiceAccounts({
58
+ name: `projects/${projectId}`,
59
+ ...(pageToken ? { pageToken } : {}),
60
+ pageSize: 100,
61
+ });
62
+ for (const sa of page.accounts ?? []) {
63
+ if (yield* descriptionHasAlchemyMarker(id, sa.description)) {
64
+ return sa;
65
+ }
66
+ }
67
+ pageToken = page.nextPageToken;
68
+ if (!pageToken)
69
+ return undefined;
70
+ }
71
+ return undefined;
72
+ });
73
+ // Sync mutable fields: displayName + description. Both use
74
+ // `updateMask` so we only PATCH what actually changed. Diff
75
+ // against OBSERVED state, not `olds`, so adoption converges
76
+ // correctly when a foreign description is on the GSA.
77
+ const syncMutable = Effect.fn(function* (observed, desired) {
78
+ const updateMaskFields = [];
79
+ if (desired.displayName !== observed.displayName) {
80
+ updateMaskFields.push("display_name");
81
+ }
82
+ const descDiff = diffTags({ description: observed.description ?? "" }, { description: desired.descriptionWithMarker });
83
+ if (descDiff.removed.length > 0 ||
84
+ descDiff.upsert.length > 0) {
85
+ updateMaskFields.push("description");
86
+ }
87
+ if (updateMaskFields.length === 0)
88
+ return observed;
89
+ return yield* patchProjectsServiceAccounts({
90
+ name: observed.name,
91
+ body: {
92
+ updateMask: updateMaskFields.join(","),
93
+ serviceAccount: {
94
+ displayName: desired.displayName,
95
+ description: desired.descriptionWithMarker,
96
+ },
97
+ },
98
+ });
99
+ });
100
+ // Apply merged IAM bindings as a single setIamPolicy. Same
101
+ // foreign-binding preservation + etag-retry pattern as the
102
+ // Project's `syncIam` — see `src/CloudResourceManager/Project.ts:421-474`.
103
+ const syncIam = (args) => Effect.gen(function* () {
104
+ const desiredByRole = new Map();
105
+ for (const b of args.bindings) {
106
+ for (const ib of b.data.iamBindings) {
107
+ const set = desiredByRole.get(ib.role) ?? new Set();
108
+ for (const m of ib.members)
109
+ set.add(m);
110
+ desiredByRole.set(ib.role, set);
111
+ }
112
+ }
113
+ if (desiredByRole.size === 0)
114
+ return;
115
+ const current = yield* getIamPolicyProjectsServiceAccounts({
116
+ resource: args.resourceName,
117
+ "options.requestedPolicyVersion": 3,
118
+ });
119
+ const existingBindings = (current.bindings ?? []).map((b) => ({
120
+ ...b,
121
+ members: [...(b.members ?? [])],
122
+ }));
123
+ let mutated = false;
124
+ for (const [role, members] of desiredByRole) {
125
+ let existing = existingBindings.find((b) => b.role === role && !b.condition);
126
+ if (!existing) {
127
+ existing = { role, members: [] };
128
+ existingBindings.push(existing);
129
+ }
130
+ const merged = new Set([...(existing.members ?? []), ...members]);
131
+ if (merged.size !== (existing.members?.length ?? 0))
132
+ mutated = true;
133
+ existing.members = [...merged];
134
+ }
135
+ if (!mutated)
136
+ return;
137
+ yield* setIamPolicyProjectsServiceAccounts({
138
+ resource: args.resourceName,
139
+ body: {
140
+ policy: { ...current, bindings: existingBindings, version: 3 },
141
+ },
142
+ });
143
+ }).pipe(Effect.retry({
144
+ schedule: Schedule.exponential(Duration.seconds(2)).pipe(Schedule.both(Schedule.recurs(8))),
145
+ }));
146
+ return {
147
+ // Stable identity: accountId + project; both immutable; either
148
+ // change → replace. The server-assigned uniqueId is also stable
149
+ // for the lifetime of the GSA (re-create gets a different one).
150
+ stables: ["accountId", "projectId", "uniqueId", "name", "email"],
151
+ diff: Effect.fn(function* ({ id, news, olds = {}, output }) {
152
+ if (!isResolved(news))
153
+ return undefined;
154
+ if (news.accountId !== (output?.accountId ?? olds.accountId) ||
155
+ news.project !== (output?.projectId ?? olds.project)) {
156
+ return { action: "replace" };
157
+ }
158
+ return undefined;
159
+ }),
160
+ reconcile: Effect.fn(function* ({ id, news, bindings }) {
161
+ const descriptionWithMarker = yield* gcpAlchemyDescription(id, news.description);
162
+ // 1. Observe — cloud state is authoritative. A second
163
+ // scan-by-marker covers the cold-recovery case where
164
+ // persisted `output`/`olds` lost the uniqueId after
165
+ // state loss. We prefer the direct-by-name probe first
166
+ // (cheap) and fall back to the scan only when direct
167
+ // observation returned `undefined`.
168
+ let observed = yield* observeServiceAccount(news.project, news.accountId);
169
+ if (!observed) {
170
+ observed = yield* findByAlchemyMarker(id, news.project);
171
+ }
172
+ // 2. Ensure — create if missing. createProjectsServiceAccounts
173
+ // is synchronous (returns the SA, not an LRO). Conflict
174
+ // covers a peer reconciler race — re-observe.
175
+ if (!observed) {
176
+ observed = yield* createProjectsServiceAccounts({
177
+ name: `projects/${news.project}`,
178
+ body: {
179
+ accountId: news.accountId,
180
+ serviceAccount: {
181
+ displayName: news.displayName,
182
+ description: descriptionWithMarker,
183
+ },
184
+ },
185
+ }).pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
186
+ if (!observed) {
187
+ observed = yield* requireServiceAccount(news.project, news.accountId, "did not appear after create");
188
+ }
189
+ }
190
+ // 3. Sync mutable fields against OBSERVED state.
191
+ const synced = yield* syncMutable(observed, {
192
+ displayName: news.displayName ?? observed.displayName,
193
+ descriptionWithMarker,
194
+ descriptionUserOnly: news.description,
195
+ });
196
+ // 4. Sync IAM bindings (Workload Identity, impersonators, …).
197
+ // Single setIamPolicy on the GSA with merged bindings;
198
+ // foreign roles preserved. Etag-retried internally.
199
+ yield* syncIam({
200
+ resourceName: synced.name,
201
+ bindings,
202
+ });
203
+ return toAttributes(synced);
204
+ }),
205
+ delete: Effect.fn(function* ({ output }) {
206
+ // Synchronous delete — Empty response, no LRO. NotFound
207
+ // means the GSA was already deleted (or never existed in our
208
+ // scope); both are idempotent successes.
209
+ yield* deleteProjectsServiceAccounts({
210
+ name: output.name,
211
+ }).pipe(Effect.catchTag("NotFound", () => Effect.void), Effect.catchTag("Forbidden", () => Effect.void));
212
+ }),
213
+ read: Effect.fn(function* ({ id, output, olds }) {
214
+ // Direct-by-name probe when we have any handle on the
215
+ // accountId. Falls back to a project-wide scan-by-marker
216
+ // for cold recovery after state loss.
217
+ const accountId = output?.accountId ?? olds?.accountId;
218
+ const projectId = output?.projectId ?? olds?.project;
219
+ let observed;
220
+ if (accountId && projectId) {
221
+ observed = yield* observeServiceAccount(projectId, accountId);
222
+ }
223
+ if (!observed && projectId) {
224
+ observed = yield* findByAlchemyMarker(id, projectId);
225
+ }
226
+ if (!observed)
227
+ return undefined;
228
+ const attrs = toAttributes(observed);
229
+ // Adoption gate via description marker (see Tags.ts §description).
230
+ return (yield* descriptionHasAlchemyMarker(id, observed.description))
231
+ ? attrs
232
+ : Unowned(attrs);
233
+ }),
234
+ };
235
+ }));
236
+ //# sourceMappingURL=ServiceAccount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceAccount.js","sourceRoot":"","sources":["../../src/Iam/ServiceAccount.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type { ServiceAccount } from "./ServiceAccount.ts";
3
+ /**
4
+ * Bind a single `(role, member)` IAM grant onto a {@link ServiceAccount}.
5
+ *
6
+ * This is a **target-side binding** — it records the grant onto the
7
+ * GSA's binding bag (`stack.bindings[gsa]`); the ServiceAccount
8
+ * provider's `reconcile` later merges all such bindings (across all
9
+ * callers, including parallel ones from different stack stages) into
10
+ * a single `setIamPolicy` call on the GSA. Same pattern as
11
+ * `projectIamMember` / `subnetworkIamMember`.
12
+ *
13
+ * Identity for dedup: the rendered SID is `IamMember(<gsa>, <key>)`.
14
+ * The engine collapses binds by SID, so callers MUST supply a
15
+ * stack-unique `key` per (gsa, role, member) triple — otherwise a
16
+ * later bind silently overwrites an earlier one with the same key.
17
+ *
18
+ * The reason `key` exists: `member` is usually an `Output<string>`
19
+ * (e.g. `Output.interpolate\`...${cluster.projectId}...\``), and
20
+ * `Output` expressions stringify by their AST shape, not by the
21
+ * eventual value. Two interpolations of the same shape but different
22
+ * literal templates render the same SID. `key` sidesteps that by
23
+ * forcing a static, caller-supplied label into the SID.
24
+ *
25
+ * Use this for the "Pulumi `gcp.serviceAccount.IAMMember`" pattern:
26
+ * authoritative-for-(role, member)-tuple, additive (does not displace
27
+ * other members on the same role).
28
+ *
29
+ * @example Workload Identity binding for one cluster
30
+ * ```typescript
31
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterA-WorkloadIdentity", {
32
+ * role: "roles/iam.workloadIdentityUser",
33
+ * member: `serviceAccount:${clusterA.projectId}.svc.id.goog[researchers/build]`,
34
+ * });
35
+ * ```
36
+ *
37
+ * @example Same GSA, second cluster (separate key → distinct SID)
38
+ * ```typescript
39
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterB-WorkloadIdentity", {
40
+ * role: "roles/iam.workloadIdentityUser",
41
+ * member: `serviceAccount:${clusterB.projectId}.svc.id.goog[researchers/build]`,
42
+ * });
43
+ * ```
44
+ */
45
+ export declare const serviceAccountIamMember: (gsa: ServiceAccount, key: string, args: {
46
+ role: string;
47
+ member: string;
48
+ }) => Effect.Effect<void>;
49
+ //# sourceMappingURL=ServiceAccountIamMember.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceAccountIamMember.d.ts","sourceRoot":"","sources":["../../src/Iam/ServiceAccountIamMember.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,uBAAuB,GAClC,KAAK,cAAc,EACnB,KAAK,MAAM,EACX,MAAM;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,KACrC,MAAM,CAAC,MAAM,CAAC,IAAI,CAMiB,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Bind a single `(role, member)` IAM grant onto a {@link ServiceAccount}.
3
+ *
4
+ * This is a **target-side binding** — it records the grant onto the
5
+ * GSA's binding bag (`stack.bindings[gsa]`); the ServiceAccount
6
+ * provider's `reconcile` later merges all such bindings (across all
7
+ * callers, including parallel ones from different stack stages) into
8
+ * a single `setIamPolicy` call on the GSA. Same pattern as
9
+ * `projectIamMember` / `subnetworkIamMember`.
10
+ *
11
+ * Identity for dedup: the rendered SID is `IamMember(<gsa>, <key>)`.
12
+ * The engine collapses binds by SID, so callers MUST supply a
13
+ * stack-unique `key` per (gsa, role, member) triple — otherwise a
14
+ * later bind silently overwrites an earlier one with the same key.
15
+ *
16
+ * The reason `key` exists: `member` is usually an `Output<string>`
17
+ * (e.g. `Output.interpolate\`...${cluster.projectId}...\``), and
18
+ * `Output` expressions stringify by their AST shape, not by the
19
+ * eventual value. Two interpolations of the same shape but different
20
+ * literal templates render the same SID. `key` sidesteps that by
21
+ * forcing a static, caller-supplied label into the SID.
22
+ *
23
+ * Use this for the "Pulumi `gcp.serviceAccount.IAMMember`" pattern:
24
+ * authoritative-for-(role, member)-tuple, additive (does not displace
25
+ * other members on the same role).
26
+ *
27
+ * @example Workload Identity binding for one cluster
28
+ * ```typescript
29
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterA-WorkloadIdentity", {
30
+ * role: "roles/iam.workloadIdentityUser",
31
+ * member: `serviceAccount:${clusterA.projectId}.svc.id.goog[researchers/build]`,
32
+ * });
33
+ * ```
34
+ *
35
+ * @example Same GSA, second cluster (separate key → distinct SID)
36
+ * ```typescript
37
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterB-WorkloadIdentity", {
38
+ * role: "roles/iam.workloadIdentityUser",
39
+ * member: `serviceAccount:${clusterB.projectId}.svc.id.goog[researchers/build]`,
40
+ * });
41
+ * ```
42
+ */
43
+ export const serviceAccountIamMember = (gsa, key, args) =>
44
+ // Tagged-template `.bind` records into the target's binding bag.
45
+ // ServiceAccount's reconcile sees `bindings: ResourceBinding<…>[]`
46
+ // and merges all entries by role into one setIamPolicy call.
47
+ gsa.bind `IamMember(${gsa}, ${key})`({
48
+ iamBindings: [{ role: args.role, members: [args.member] }],
49
+ });
50
+ //# sourceMappingURL=ServiceAccountIamMember.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceAccountIamMember.js","sourceRoot":"","sources":["../../src/Iam/ServiceAccountIamMember.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,GAAmB,EACnB,GAAW,EACX,IAAsC,EACjB,EAAE;AACvB,iEAAiE;AACjE,mEAAmE;AACnE,6DAA6D;AAC7D,GAAG,CAAC,IAAI,CAAA,aAAa,GAAG,KAAK,GAAG,GAAG,CAAC;IAClC,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;CAC3D,CAAmC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { ServiceAccount, ServiceAccountProvider } from "./ServiceAccount.ts";
2
+ export type { ServiceAccountProps } from "./ServiceAccount.ts";
3
+ export * from "./ServiceAccountIamMember.ts";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Iam/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,cAAc,8BAA8B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { ServiceAccount, ServiceAccountProvider } from "./ServiceAccount.js";
2
+ export * from "./ServiceAccountIamMember.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/Iam/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAE7E,cAAc,8BAA8B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Providers.d.ts","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;;AA+CtC,qBAAa,SAAU,SAAQ,cAA+C;CAAG;AAEjF,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,8QA6DnB,CAAC"}
1
+ {"version":3,"file":"Providers.d.ts","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;;AAmDtC,qBAAa,SAAU,SAAQ,cAA+C;CAAG;AAEjF,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,8QA+DnB,CAAC"}
package/lib/Providers.js CHANGED
@@ -14,6 +14,7 @@ import { SharedVpcServiceProject, SharedVpcServiceProjectProvider, } from "./Com
14
14
  import { Subnetwork, SubnetworkProvider } from "./Compute/Subnetwork.js";
15
15
  import { Cluster, ClusterProvider } from "./Container/Cluster.js";
16
16
  import { NodePool, NodePoolProvider } from "./Container/NodePool.js";
17
+ import { ServiceAccount, ServiceAccountProvider, } from "./Iam/ServiceAccount.js";
17
18
  import { KubernetesManifest, KubernetesManifestProvider, } from "./Kubernetes/KubernetesManifest.js";
18
19
  import { KubernetesSecret, KubernetesSecretProvider, } from "./Kubernetes/Secret.js";
19
20
  import { ManagedLustreInstance, ManagedLustreInstanceProvider, } from "./ManagedLustre/Instance.js";
@@ -62,9 +63,10 @@ export const providers = () => Layer.effect(Providers, Provider.collection([
62
63
  SqlDatabase,
63
64
  SqlUser,
64
65
  ArtifactRegistryRepository,
66
+ ServiceAccount,
65
67
  KubernetesSecret,
66
68
  KubernetesManifest,
67
- ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), ClusterProvider(), NodePoolProvider(), ApiEnableProvider(), NetworkProvider(), SubnetworkProvider(), FirewallProvider(), AddressProvider(), ForwardingRuleProvider(), GlobalAddressProvider(), SharedVpcHostProvider(), SharedVpcServiceProjectProvider(), PsaConnectionProvider(), ManagedLustreInstanceProvider(), ServiceProvider(), JobProvider(), SqlInstanceProvider(), SqlDatabaseProvider(), SqlUserProvider(), ArtifactRegistryRepositoryProvider(), KubernetesSecretProvider(), KubernetesManifestProvider())), Layer.provideMerge(fromAuthProvider()), Layer.provideMerge(GCPAuth),
69
+ ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), ClusterProvider(), NodePoolProvider(), ApiEnableProvider(), NetworkProvider(), SubnetworkProvider(), FirewallProvider(), AddressProvider(), ForwardingRuleProvider(), GlobalAddressProvider(), SharedVpcHostProvider(), SharedVpcServiceProjectProvider(), PsaConnectionProvider(), ManagedLustreInstanceProvider(), ServiceProvider(), JobProvider(), SqlInstanceProvider(), SqlDatabaseProvider(), SqlUserProvider(), ArtifactRegistryRepositoryProvider(), ServiceAccountProvider(), KubernetesSecretProvider(), KubernetesManifestProvider())), Layer.provideMerge(fromAuthProvider()), Layer.provideMerge(GCPAuth),
68
70
  // Auth/config failures (missing ADC, malformed profile) become
69
71
  // unrecoverable defects rather than typed errors — mirrors
70
72
  // `AWS/Providers.ts` and `Cloudflare/Providers.ts`. Resource lifecycle
@@ -1 +1 @@
1
- {"version":3,"file":"Providers.js","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Providers.js","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./ArtifactRegistry/index.ts";
5
5
  export * from "./CloudResourceManager/index.ts";
6
6
  export * from "./Compute/index.ts";
7
7
  export * from "./Container/index.ts";
8
+ export * from "./Iam/index.ts";
8
9
  export * from "./Kubernetes/index.ts";
9
10
  export * from "./ManagedLustre/index.ts";
10
11
  export * from "./Run/index.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EACL,sBAAsB,EACtB,OAAO,GACR,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,aAAa,EACb,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAChC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EACL,sBAAsB,EACtB,OAAO,GACR,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,aAAa,EACb,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAChC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC"}
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ export * from "./ArtifactRegistry/index.js";
4
4
  export * from "./CloudResourceManager/index.js";
5
5
  export * from "./Compute/index.js";
6
6
  export * from "./Container/index.js";
7
+ export * from "./Iam/index.js";
7
8
  export * from "./Kubernetes/index.js";
8
9
  export * from "./ManagedLustre/index.js";
9
10
  export * from "./Run/index.js";
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EACL,sBAAsB,EACtB,OAAO,GACR,MAAM,wBAAwB,CAAC;AAKhC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EACL,sBAAsB,EACtB,OAAO,GACR,MAAM,wBAAwB,CAAC;AAKhC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@microagi/alchemy-gcp",
3
- "version": "0.7.0",
4
- "description": "GCP provider for Alchemy v2 (Effect-based IaC). Resources for Project, Cluster, NodePool, Compute, ManagedLustre, ServiceUsage, ServiceNetworking, Cloud Run (Service + Job + IAM), Cloud SQL (Instance + Database + User with IAM database authn), Artifact Registry (Docker repositories, standard + remote pull-through caches), with ADC-backed credentials and target-side IAM bindings.",
3
+ "version": "0.8.0",
4
+ "description": "GCP provider for Alchemy v2 (Effect-based IaC). Resources for Project, ServiceAccount, Cluster, NodePool, Compute, ManagedLustre, ServiceUsage, ServiceNetworking, Cloud Run (Service + Job + IAM), Cloud SQL (Instance + Database + User with IAM database authn), Artifact Registry (Docker repositories, standard + remote pull-through caches), Kubernetes (typed Secret + generic SSA Manifest), with ADC-backed credentials and target-side IAM bindings.",
5
5
  "keywords": [
6
6
  "alchemy",
7
7
  "alchemy-effect",
@@ -0,0 +1,431 @@
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 { diffTags } from "alchemy/Tags";
8
+ import * as Duration from "effect/Duration";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Schedule from "effect/Schedule";
11
+ import {
12
+ descriptionHasAlchemyMarker,
13
+ gcpAlchemyDescription,
14
+ stripAlchemyMarker,
15
+ } from "../Tags.ts";
16
+ import type * as GCP from "../Providers.ts";
17
+
18
+ /**
19
+ * A GCP service account (GSA). Lives under a {@link import("../CloudResourceManager/Project.ts").Project}
20
+ * and serves as the federated identity target for Workload Identity
21
+ * (`iam.gke.io/gcp-service-account: <email>` on a k8s SA) and as the
22
+ * principal in any IAM grant that should out-live a human user.
23
+ *
24
+ * Adoption is gated on an alchemy marker embedded in the GSA's
25
+ * `description` field (see `gcpAlchemyDescription` /
26
+ * `descriptionHasAlchemyMarker`) — GSAs have no labels field, so the
27
+ * label-based adoption gate used by Project can't apply. The marker is
28
+ * only written on insert; `description` is mutable but we never *clear*
29
+ * it because doing so would orphan the adoption signal.
30
+ *
31
+ * All lifecycle operations (`createProjectsServiceAccounts`,
32
+ * `patchProjectsServiceAccounts`, `deleteProjectsServiceAccounts`) are
33
+ * synchronous — no LRO polling required.
34
+ *
35
+ * @section Creating a ServiceAccount
36
+ * @example Minimal GSA under a project
37
+ * ```typescript
38
+ * const gsa = yield* GCP.ServiceAccount("Builder", {
39
+ * project: project.projectId,
40
+ * accountId: "research-builder",
41
+ * displayName: "Research pod build + push (BuildKit client)",
42
+ * });
43
+ * ```
44
+ *
45
+ * @example With explicit description and Workload Identity binding
46
+ * ```typescript
47
+ * const gsa = yield* GCP.ServiceAccount("Builder", {
48
+ * project: project.projectId,
49
+ * accountId: "research-builder",
50
+ * displayName: "Research pod build + push",
51
+ * description: "Used by per-researcher BuildKit clients to push to AR.",
52
+ * });
53
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterA-WorkloadIdentity", {
54
+ * role: "roles/iam.workloadIdentityUser",
55
+ * member: `serviceAccount:${projectA.projectId}.svc.id.goog[researchers/build]`,
56
+ * });
57
+ * ```
58
+ */
59
+ export type ServiceAccountProps = {
60
+ /**
61
+ * GCP project id (e.g. `micro-research-shared`) the GSA lives under.
62
+ * Immutable — changing this orphans the GSA's unique id and any
63
+ * bindings, so `diff` triggers a replacement.
64
+ */
65
+ project: string;
66
+ /**
67
+ * The account id segment of the service account's email. Must match
68
+ * `[a-z]([-a-z0-9]*[a-z0-9])`, 6-30 chars. Immutable — replaces.
69
+ */
70
+ accountId: string;
71
+ /**
72
+ * User-visible display name. Max 100 UTF-8 bytes. Mutable.
73
+ */
74
+ displayName?: string;
75
+ /**
76
+ * User-supplied description (max 256 UTF-8 bytes). The alchemy
77
+ * ownership marker is prepended automatically — see
78
+ * `gcpAlchemyDescription`. The user-supplied portion is preserved
79
+ * verbatim and surfaced back through `Attributes.description`.
80
+ */
81
+ description?: string;
82
+ };
83
+
84
+ /**
85
+ * Single (role, members) entry on a GSA's IAM policy. Targets declare
86
+ * this contract so capabilities (e.g. Workload Identity) can `.bind`
87
+ * IAM grants onto the GSA — the provider's `reconcile` merges all
88
+ * bound entries by role into a single `setIamPolicy` call.
89
+ */
90
+ export type ServiceAccountIamBinding = {
91
+ /** IAM role, e.g. `"roles/iam.workloadIdentityUser"`. */
92
+ role: string;
93
+ /** Principals, e.g. `["serviceAccount:foo.svc.id.goog[ns/ksa]"]`. */
94
+ members: ReadonlyArray<string>;
95
+ };
96
+
97
+ export type ServiceAccountBindingContract = {
98
+ iamBindings: ReadonlyArray<ServiceAccountIamBinding>;
99
+ };
100
+
101
+ export type ServiceAccount = Resource<
102
+ "GCP.ServiceAccount",
103
+ ServiceAccountProps,
104
+ {
105
+ /** Full resource name, e.g. `projects/micro-research-shared/serviceAccounts/research-builder@micro-research-shared.iam.gserviceaccount.com`. */
106
+ name: string;
107
+ /** Email address derived from the account id, e.g. `research-builder@micro-research-shared.iam.gserviceaccount.com`. */
108
+ email: string;
109
+ /** Server-assigned stable numeric id. */
110
+ uniqueId: string;
111
+ /** Project the GSA lives under. */
112
+ projectId: string;
113
+ /** Account id (the part before `@`). */
114
+ accountId: string;
115
+ /** User-visible display name, or `undefined` if never set. */
116
+ displayName: string | undefined;
117
+ /** User-visible description (without the alchemy marker), or `undefined` if never set. */
118
+ description: string | undefined;
119
+ /** Whether the GSA is currently disabled. */
120
+ disabled: boolean;
121
+ },
122
+ ServiceAccountBindingContract,
123
+ GCP.Providers
124
+ >;
125
+
126
+ export const ServiceAccount = Resource<ServiceAccount>("GCP.ServiceAccount");
127
+
128
+ const toAttributes = (sa: iam.ServiceAccount): ServiceAccount["Attributes"] => {
129
+ const accountId = (sa.email ?? "").split("@")[0];
130
+ return {
131
+ name: sa.name ?? "",
132
+ email: sa.email ?? "",
133
+ uniqueId: sa.uniqueId ?? "",
134
+ projectId: sa.projectId ?? "",
135
+ accountId,
136
+ displayName: sa.displayName,
137
+ description: stripAlchemyMarker(sa.description),
138
+ disabled: sa.disabled ?? false,
139
+ };
140
+ };
141
+
142
+ export const ServiceAccountProvider = () =>
143
+ Provider.effect(
144
+ ServiceAccount,
145
+ Effect.gen(function* () {
146
+ const getProjectsServiceAccounts = yield* iam.getProjectsServiceAccounts;
147
+ const createProjectsServiceAccounts = yield* iam.createProjectsServiceAccounts;
148
+ const patchProjectsServiceAccounts = yield* iam.patchProjectsServiceAccounts;
149
+ const deleteProjectsServiceAccounts = yield* iam.deleteProjectsServiceAccounts;
150
+ const listProjectsServiceAccounts = yield* iam.listProjectsServiceAccounts;
151
+ const getIamPolicyProjectsServiceAccounts = yield* iam.getIamPolicyProjectsServiceAccounts;
152
+ const setIamPolicyProjectsServiceAccounts = yield* iam.setIamPolicyProjectsServiceAccounts;
153
+
154
+ const resourceName = (projectId: string, accountId: string) =>
155
+ `projects/${projectId}/serviceAccounts/${accountId}`;
156
+
157
+ const observeServiceAccount = (projectId: string, accountId: string) =>
158
+ getProjectsServiceAccounts({
159
+ name: resourceName(projectId, accountId),
160
+ }).pipe(
161
+ // GSAs that don't exist OR that the caller can't see both
162
+ // collapse to "absent" — same 403→missing pattern as the
163
+ // project observer.
164
+ Effect.catchTag("NotFound", () =>
165
+ Effect.succeed(undefined as iam.ServiceAccount | undefined),
166
+ ),
167
+ Effect.catchTag("Forbidden", () =>
168
+ Effect.succeed(undefined as iam.ServiceAccount | undefined),
169
+ ),
170
+ );
171
+
172
+ const requireServiceAccount = (
173
+ projectId: string,
174
+ accountId: string,
175
+ context: string,
176
+ ) =>
177
+ observeServiceAccount(projectId, accountId).pipe(
178
+ Effect.flatMap((sa) =>
179
+ sa
180
+ ? Effect.succeed(sa)
181
+ : Effect.fail(
182
+ new ConfigError({
183
+ message: `ServiceAccount ${accountId} in project ${projectId} ${context}.`,
184
+ }),
185
+ ),
186
+ ),
187
+ );
188
+
189
+ // List-page scan for cold recovery. Alchemy marker in the
190
+ // description field identifies our GSAs; the page boundary is
191
+ // arbitrary — we still need to page through to find any marker
192
+ // that happens to fall past the first page.
193
+ const findByAlchemyMarker = Effect.fn(function* (
194
+ id: string,
195
+ projectId: string,
196
+ ) {
197
+ let pageToken: string | undefined;
198
+ // Up to 100 pages × 100 items per page = 10000 GSAs; well past
199
+ // any realistic project size. If we ever exceed that we should
200
+ // cap and warn rather than silently miss matches.
201
+ for (let i = 0; i < 100; i++) {
202
+ const page = yield* listProjectsServiceAccounts({
203
+ name: `projects/${projectId}`,
204
+ ...(pageToken ? { pageToken } : {}),
205
+ pageSize: 100,
206
+ });
207
+ for (const sa of page.accounts ?? []) {
208
+ if (yield* descriptionHasAlchemyMarker(id, sa.description)) {
209
+ return sa;
210
+ }
211
+ }
212
+ pageToken = page.nextPageToken;
213
+ if (!pageToken) return undefined;
214
+ }
215
+ return undefined;
216
+ });
217
+
218
+ // Sync mutable fields: displayName + description. Both use
219
+ // `updateMask` so we only PATCH what actually changed. Diff
220
+ // against OBSERVED state, not `olds`, so adoption converges
221
+ // correctly when a foreign description is on the GSA.
222
+ const syncMutable = Effect.fn(function* (
223
+ observed: iam.ServiceAccount,
224
+ desired: {
225
+ displayName: string | undefined;
226
+ descriptionWithMarker: string;
227
+ descriptionUserOnly: string | undefined;
228
+ },
229
+ ) {
230
+ const updateMaskFields: string[] = [];
231
+ if (desired.displayName !== observed.displayName) {
232
+ updateMaskFields.push("display_name");
233
+ }
234
+ const descDiff = diffTags(
235
+ { description: observed.description ?? "" },
236
+ { description: desired.descriptionWithMarker },
237
+ );
238
+ if (
239
+ descDiff.removed.length > 0 ||
240
+ descDiff.upsert.length > 0
241
+ ) {
242
+ updateMaskFields.push("description");
243
+ }
244
+
245
+ if (updateMaskFields.length === 0) return observed;
246
+
247
+ return yield* patchProjectsServiceAccounts({
248
+ name: observed.name!,
249
+ body: {
250
+ updateMask: updateMaskFields.join(","),
251
+ serviceAccount: {
252
+ displayName: desired.displayName,
253
+ description: desired.descriptionWithMarker,
254
+ },
255
+ },
256
+ });
257
+ });
258
+
259
+ // Apply merged IAM bindings as a single setIamPolicy. Same
260
+ // foreign-binding preservation + etag-retry pattern as the
261
+ // Project's `syncIam` — see `src/CloudResourceManager/Project.ts:421-474`.
262
+ const syncIam = (args: {
263
+ resourceName: string;
264
+ bindings: ReadonlyArray<
265
+ import("alchemy/Resource").ResourceBinding<ServiceAccountBindingContract>
266
+ >;
267
+ }) =>
268
+ Effect.gen(function* () {
269
+ const desiredByRole = new Map<string, Set<string>>();
270
+ for (const b of args.bindings) {
271
+ for (const ib of b.data.iamBindings) {
272
+ const set = desiredByRole.get(ib.role) ?? new Set<string>();
273
+ for (const m of ib.members) set.add(m);
274
+ desiredByRole.set(ib.role, set);
275
+ }
276
+ }
277
+ if (desiredByRole.size === 0) return;
278
+
279
+ const current = yield* getIamPolicyProjectsServiceAccounts({
280
+ resource: args.resourceName,
281
+ "options.requestedPolicyVersion": 3,
282
+ });
283
+
284
+ const existingBindings = (current.bindings ?? []).map((b) => ({
285
+ ...b,
286
+ members: [...(b.members ?? [])],
287
+ }));
288
+ let mutated = false;
289
+ for (const [role, members] of desiredByRole) {
290
+ let existing = existingBindings.find(
291
+ (b) => b.role === role && !b.condition,
292
+ );
293
+ if (!existing) {
294
+ existing = { role, members: [] };
295
+ existingBindings.push(existing);
296
+ }
297
+ const merged = new Set([...(existing.members ?? []), ...members]);
298
+ if (merged.size !== (existing.members?.length ?? 0)) mutated = true;
299
+ existing.members = [...merged];
300
+ }
301
+ if (!mutated) return;
302
+
303
+ yield* setIamPolicyProjectsServiceAccounts({
304
+ resource: args.resourceName,
305
+ body: {
306
+ policy: { ...current, bindings: existingBindings, version: 3 },
307
+ },
308
+ });
309
+ }).pipe(
310
+ Effect.retry({
311
+ schedule: Schedule.exponential(Duration.seconds(2)).pipe(
312
+ Schedule.both(Schedule.recurs(8)),
313
+ ),
314
+ }),
315
+ );
316
+
317
+ return {
318
+ // Stable identity: accountId + project; both immutable; either
319
+ // change → replace. The server-assigned uniqueId is also stable
320
+ // for the lifetime of the GSA (re-create gets a different one).
321
+ stables: ["accountId", "projectId", "uniqueId", "name", "email"],
322
+ diff: Effect.fn(function* ({ id, news, olds = {}, output }) {
323
+ if (!isResolved(news)) return undefined;
324
+ if (
325
+ news.accountId !== (output?.accountId ?? olds.accountId) ||
326
+ news.project !== (output?.projectId ?? olds.project)
327
+ ) {
328
+ return { action: "replace" } as const;
329
+ }
330
+ return undefined;
331
+ }),
332
+ reconcile: Effect.fn(function* ({ id, news, bindings }) {
333
+ const descriptionWithMarker = yield* gcpAlchemyDescription(
334
+ id,
335
+ news.description,
336
+ );
337
+
338
+ // 1. Observe — cloud state is authoritative. A second
339
+ // scan-by-marker covers the cold-recovery case where
340
+ // persisted `output`/`olds` lost the uniqueId after
341
+ // state loss. We prefer the direct-by-name probe first
342
+ // (cheap) and fall back to the scan only when direct
343
+ // observation returned `undefined`.
344
+ let observed = yield* observeServiceAccount(news.project, news.accountId);
345
+ if (!observed) {
346
+ observed = yield* findByAlchemyMarker(id, news.project);
347
+ }
348
+
349
+ // 2. Ensure — create if missing. createProjectsServiceAccounts
350
+ // is synchronous (returns the SA, not an LRO). Conflict
351
+ // covers a peer reconciler race — re-observe.
352
+ if (!observed) {
353
+ observed = yield* createProjectsServiceAccounts({
354
+ name: `projects/${news.project}`,
355
+ body: {
356
+ accountId: news.accountId,
357
+ serviceAccount: {
358
+ displayName: news.displayName,
359
+ description: descriptionWithMarker,
360
+ },
361
+ },
362
+ }).pipe(
363
+ Effect.catchTag("Conflict", () =>
364
+ Effect.succeed(undefined as iam.ServiceAccount | undefined),
365
+ ),
366
+ );
367
+
368
+ if (!observed) {
369
+ observed = yield* requireServiceAccount(
370
+ news.project,
371
+ news.accountId,
372
+ "did not appear after create",
373
+ );
374
+ }
375
+ }
376
+
377
+ // 3. Sync mutable fields against OBSERVED state.
378
+ const synced = yield* syncMutable(observed, {
379
+ displayName: news.displayName ?? observed.displayName,
380
+ descriptionWithMarker,
381
+ descriptionUserOnly: news.description,
382
+ });
383
+
384
+ // 4. Sync IAM bindings (Workload Identity, impersonators, …).
385
+ // Single setIamPolicy on the GSA with merged bindings;
386
+ // foreign roles preserved. Etag-retried internally.
387
+ yield* syncIam({
388
+ resourceName: synced.name!,
389
+ bindings,
390
+ });
391
+
392
+ return toAttributes(synced);
393
+ }),
394
+ delete: Effect.fn(function* ({ output }) {
395
+ // Synchronous delete — Empty response, no LRO. NotFound
396
+ // means the GSA was already deleted (or never existed in our
397
+ // scope); both are idempotent successes.
398
+ yield* deleteProjectsServiceAccounts({
399
+ name: output.name,
400
+ }).pipe(
401
+ Effect.catchTag("NotFound", () => Effect.void),
402
+ Effect.catchTag("Forbidden", () => Effect.void),
403
+ );
404
+ }),
405
+ read: Effect.fn(function* ({ id, output, olds }) {
406
+ // Direct-by-name probe when we have any handle on the
407
+ // accountId. Falls back to a project-wide scan-by-marker
408
+ // for cold recovery after state loss.
409
+ const accountId =
410
+ output?.accountId ?? olds?.accountId;
411
+ const projectId =
412
+ output?.projectId ?? olds?.project;
413
+
414
+ let observed: iam.ServiceAccount | undefined;
415
+ if (accountId && projectId) {
416
+ observed = yield* observeServiceAccount(projectId, accountId);
417
+ }
418
+ if (!observed && projectId) {
419
+ observed = yield* findByAlchemyMarker(id, projectId);
420
+ }
421
+ if (!observed) return undefined;
422
+
423
+ const attrs = toAttributes(observed);
424
+ // Adoption gate via description marker (see Tags.ts §description).
425
+ return (yield* descriptionHasAlchemyMarker(id, observed.description))
426
+ ? attrs
427
+ : Unowned(attrs);
428
+ }),
429
+ };
430
+ }),
431
+ );
@@ -0,0 +1,56 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type { ServiceAccount } from "./ServiceAccount.ts";
3
+
4
+ /**
5
+ * Bind a single `(role, member)` IAM grant onto a {@link ServiceAccount}.
6
+ *
7
+ * This is a **target-side binding** — it records the grant onto the
8
+ * GSA's binding bag (`stack.bindings[gsa]`); the ServiceAccount
9
+ * provider's `reconcile` later merges all such bindings (across all
10
+ * callers, including parallel ones from different stack stages) into
11
+ * a single `setIamPolicy` call on the GSA. Same pattern as
12
+ * `projectIamMember` / `subnetworkIamMember`.
13
+ *
14
+ * Identity for dedup: the rendered SID is `IamMember(<gsa>, <key>)`.
15
+ * The engine collapses binds by SID, so callers MUST supply a
16
+ * stack-unique `key` per (gsa, role, member) triple — otherwise a
17
+ * later bind silently overwrites an earlier one with the same key.
18
+ *
19
+ * The reason `key` exists: `member` is usually an `Output<string>`
20
+ * (e.g. `Output.interpolate\`...${cluster.projectId}...\``), and
21
+ * `Output` expressions stringify by their AST shape, not by the
22
+ * eventual value. Two interpolations of the same shape but different
23
+ * literal templates render the same SID. `key` sidesteps that by
24
+ * forcing a static, caller-supplied label into the SID.
25
+ *
26
+ * Use this for the "Pulumi `gcp.serviceAccount.IAMMember`" pattern:
27
+ * authoritative-for-(role, member)-tuple, additive (does not displace
28
+ * other members on the same role).
29
+ *
30
+ * @example Workload Identity binding for one cluster
31
+ * ```typescript
32
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterA-WorkloadIdentity", {
33
+ * role: "roles/iam.workloadIdentityUser",
34
+ * member: `serviceAccount:${clusterA.projectId}.svc.id.goog[researchers/build]`,
35
+ * });
36
+ * ```
37
+ *
38
+ * @example Same GSA, second cluster (separate key → distinct SID)
39
+ * ```typescript
40
+ * yield* GCP.serviceAccountIamMember(gsa, "ClusterB-WorkloadIdentity", {
41
+ * role: "roles/iam.workloadIdentityUser",
42
+ * member: `serviceAccount:${clusterB.projectId}.svc.id.goog[researchers/build]`,
43
+ * });
44
+ * ```
45
+ */
46
+ export const serviceAccountIamMember = (
47
+ gsa: ServiceAccount,
48
+ key: string,
49
+ args: { role: string; member: string },
50
+ ): Effect.Effect<void> =>
51
+ // Tagged-template `.bind` records into the target's binding bag.
52
+ // ServiceAccount's reconcile sees `bindings: ResourceBinding<…>[]`
53
+ // and merges all entries by role into one setIamPolicy call.
54
+ gsa.bind`IamMember(${gsa}, ${key})`({
55
+ iamBindings: [{ role: args.role, members: [args.member] }],
56
+ }) as unknown as Effect.Effect<void>;
@@ -0,0 +1,3 @@
1
+ export { ServiceAccount, ServiceAccountProvider } from "./ServiceAccount.ts";
2
+ export type { ServiceAccountProps } from "./ServiceAccount.ts";
3
+ export * from "./ServiceAccountIamMember.ts";
package/src/Providers.ts CHANGED
@@ -23,6 +23,10 @@ import {
23
23
  import { Subnetwork, SubnetworkProvider } from "./Compute/Subnetwork.ts";
24
24
  import { Cluster, ClusterProvider } from "./Container/Cluster.ts";
25
25
  import { NodePool, NodePoolProvider } from "./Container/NodePool.ts";
26
+ import {
27
+ ServiceAccount,
28
+ ServiceAccountProvider,
29
+ } from "./Iam/ServiceAccount.ts";
26
30
  import {
27
31
  KubernetesManifest,
28
32
  KubernetesManifestProvider,
@@ -89,6 +93,7 @@ export const providers = () =>
89
93
  SqlDatabase,
90
94
  SqlUser,
91
95
  ArtifactRegistryRepository,
96
+ ServiceAccount,
92
97
  KubernetesSecret,
93
98
  KubernetesManifest,
94
99
  ]),
@@ -115,6 +120,7 @@ export const providers = () =>
115
120
  SqlDatabaseProvider(),
116
121
  SqlUserProvider(),
117
122
  ArtifactRegistryRepositoryProvider(),
123
+ ServiceAccountProvider(),
118
124
  KubernetesSecretProvider(),
119
125
  KubernetesManifestProvider(),
120
126
  ),
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./ArtifactRegistry/index.ts";
11
11
  export * from "./CloudResourceManager/index.ts";
12
12
  export * from "./Compute/index.ts";
13
13
  export * from "./Container/index.ts";
14
+ export * from "./Iam/index.ts";
14
15
  export * from "./Kubernetes/index.ts";
15
16
  export * from "./ManagedLustre/index.ts";
16
17
  export * from "./Run/index.ts";