1/**
2 * The `gcp-gke` {@link ClusterAdapter}: everything platform-specific about
3 * running `Kubernetes.*` workloads on Google Kubernetes Engine.
4 *
5 * - **connect** — Google OAuth bearer tokens against the cluster endpoint,
6 * re-getting the cluster when the connection doesn't carry endpoint/CA.
7 * - **identity** — GKE Workload Identity Federation: every `iam` grant a
8 * GCP binding attaches to the workload is granted directly to the
9 * Kubernetes ServiceAccount's principal
10 * (`principal://iam.googleapis.com/projects/{number}/locations/global/workloadIdentityPools/{project}.svc.id.goog/subject/ns/{namespace}/sa/{ksa}`)
11 * — no Google service account involved. Applied grants are recorded in
12 * the identity state; grants no binding asks for any more are revoked on
13 * the next reconcile, and all of them on delete. With
14 * `identity.gcpServiceAccount`, the KSA is instead annotated with
15 * `iam.gke.io/gcp-service-account`, granted
16 * `roles/iam.workloadIdentityUser` on that GSA, and the binding grants
17 * go to the GSA. The cluster's `workloadIdentityConfig.workloadPool` is
18 * enabled by `GCP.Container.Cluster` (Autopilot enables it by default).
19 * - **registry** — a per-workload Artifact Registry Docker repository;
20 * `main` programs are bundled, `context` Dockerfiles built, and `image`
21 * refs mirrored into it.
22 * - **bootstrap** — generated container entries wiring the GCP credential
23 * chain so Workload Identity's GCE metadata server resolves inside the
24 * pod.
25 * - **loadBalancerDefaults** — GKE backend-service based L4
26 * (`cloud.google.com/l4-rbs`).
27 *
28 * Registered by `GCP.providers()`; resolved dynamically by the
29 * `Kubernetes.*` providers via the connection's `auth.kind`.
30 */
31import * as container from "@distilled.cloud/gcp/container_v1";
32import { Credentials } from "@distilled.cloud/gcp/Credentials";
33import * as Effect from "effect/Effect";
34import type { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";
35import * as FileSystem from "effect/FileSystem";
36import * as Layer from "effect/Layer";
37import * as Path from "effect/Path";
38import * as Redacted from "effect/Redacted";
39import type { HttpClient } from "effect/http/HttpClient";
40import {
41 ClusterAdapter,
42 ClusterNotFoundError,
43 type AdapterLifecycleServices,
44 type ClusterAdapterService,
45 type ClusterTransport,
46 type ImageRegistryDeleteOptions,
47 type ImageRegistryResolveOptions,
48 type ImageRegistryResult,
49 type WorkloadBindingContract,
50 type WorkloadIdentityReconcileOptions,
51} from "../../Kubernetes/ClusterAdapter.ts";
52import type { Connection } from "../../Kubernetes/Connection.ts";
53import { createPhysicalName } from "../../PhysicalName.ts";
54import type { ResourceBinding } from "../../Resource.ts";
55import { Self } from "../../Self.ts";
56import {
57 makeImageSource,
58 type ImageSourceLike,
59} from "../ArtifactRegistry/ImageSource.ts";
60import { GcpEnvironment } from "../Environment.ts";
61import {
62 projectNumber,
63 revokeHostIam,
64 syncHostIam,
65 type AppliedIamGrant,
66 type GcpIamGrant,
67} from "../Host.ts";
68import { revokeIamMembership, updateIamMembership } from "../IamPolicy.ts";
69
70declare module "../../Kubernetes/Connection.ts" {
71 interface AuthRegistry {
72 /**
73 * Authenticate against a GKE cluster with Google OAuth bearer tokens
74 * minted from the ambient GCP credentials. Contributed by
75 * `GCP.providers()` — `GCP.Container.Cluster` attributes expose a
76 * ready-made `connection` carrying this descriptor.
77 */
78 "gcp-gke": {
79 /** GKE cluster id (the `{cluster}` path segment). */
80 clusterId: string;
81 /** Zone or region (`us-central1-a`, `us-central1`, …). */
82 location: string;
83 /**
84 * GCP project id.
85 * @default the ambient GCP project
86 */
87 project?: string;
88 };
89 }
90}
91
92declare module "../../Kubernetes/ClusterAdapter.ts" {
93 interface IdentityStateRegistry {
94 /**
95 * GKE Workload Identity Federation: binding grants are applied to the
96 * Kubernetes ServiceAccount's principal (or to the GSA supplied via
97 * `identity.gcpServiceAccount`).
98 */
99 "gcp-workload-identity": {
100 /** Workload Identity pool (`{project}.svc.id.goog`). */
101 workloadPool: string;
102 /** Project the grants and workload pool belong to. */
103 project?: string;
104 /**
105 * The IAM member binding grants were applied to — the KSA's
106 * `principal://…` identifier, or `serviceAccount:{gsa}`.
107 */
108 member?: string;
109 /** Grants applied to `member`, revoked once no binding asks for them. */
110 iamGrants?: AppliedIamGrant[];
111 /** Existing GSA email stamped on the KSA annotation, if any. */
112 gcpServiceAccount?: string;
113 /**
114 * The KSA principal granted `roles/iam.workloadIdentityUser` on
115 * `gcpServiceAccount`, if any.
116 */
117 impersonator?: string;
118 };
119 }
120 interface RegistryStateRegistry {
121 /** The per-workload Artifact Registry repository holding the image. */
122 "gcp-artifact-registry": {
123 /** Artifact Registry repository id. */
124 repositoryName: string;
125 /** Docker image prefix (`{loc}-docker.pkg.dev/{project}/{repo}/app`). */
126 repositoryUri: string;
127 /** Full resource name `projects/.../repositories/{repository}`. */
128 name: string;
129 /** Artifact Registry location (`us-central1`, …). */
130 location: string;
131 };
132 }
133 interface WorkloadIdentityOptions {
134 /**
135 * Existing Google service-account email the workload runs as (GKE
136 * only). Alchemy annotates the Kubernetes ServiceAccount with
137 * `iam.gke.io/gcp-service-account`, grants the KSA principal
138 * `roles/iam.workloadIdentityUser` on the GSA, and lands binding
139 * grants on the GSA. Omit it (the default) to grant bindings directly
140 * to the KSA's Workload Identity Federation principal.
141 */
142 gcpServiceAccount?: string;
143 }
144 interface WorkloadBindingContract {
145 /**
146 * IAM grants landed on the workload's Workload Identity principal (GKE
147 * only) — the same host-binding channel as `GCP.Run.Service` and
148 * `GCP.Function`.
149 */
150 iam?: GcpIamGrant[];
151 }
152 interface WorkloadServicesRegistry {
153 /** GCP credential-chain services ambient inside GKE workload pods. */
154 gcp: Credentials | GcpEnvironment;
155 }
156}
157
158/** Services the adapter's methods close over at layer build. */
159type GkeAdapterDeps =
160 | Credentials
161 | GcpEnvironment
162 | HttpClient
163 | FileSystem.FileSystem
164 | Path.Path
165 // Hashing an unbundled Node image resolves installed packages.
166 | ChildProcessSpawner;
167
168const GSA_ANNOTATION = "iam.gke.io/gcp-service-account";
169
170/** Zone `us-central1-a` → region `us-central1`; regions pass through. */
171export const regionOfLocation = (location: string) => {
172 const parts = location.split("-");
173 const last = parts[parts.length - 1] ?? "";
174 if (parts.length >= 3 && last.length === 1) {
175 return parts.slice(0, -1).join("-");
176 }
177 return location;
178};
179
180export const apiServerEndpoint = (endpoint: string | undefined) => {
181 if (endpoint === undefined || endpoint.length === 0) return undefined;
182 return endpoint.startsWith("https://") || endpoint.startsWith("http://")
183 ? endpoint
184 : `https://${endpoint}`;
185};
186
187export const workloadPoolOf = (project: string) => `${project}.svc.id.goog`;
188
189/**
190 * The Workload Identity Federation principal of a Kubernetes
191 * ServiceAccount — the IAM member GKE pods running as that KSA
192 * authenticate as. Every cluster in the project shares the pool, so the
193 * same `namespace`/`serviceAccount` pair is one identity project-wide.
194 */
195export const workloadIdentityPrincipal = (options: {
196 projectNumber: string;
197 project: string;
198 namespace: string;
199 serviceAccount: string;
200}) =>
201 `principal://iam.googleapis.com/projects/${options.projectNumber}` +
202 `/locations/global/workloadIdentityPools/${workloadPoolOf(options.project)}` +
203 `/subject/ns/${options.namespace}/sa/${options.serviceAccount}`;
204
205const WORKLOAD_IDENTITY_USER = "roles/iam.workloadIdentityUser";
206
207const gsaResourceName = (email: string) =>
208 `projects/-/serviceAccounts/${email}`;
209
210/** Binding channels a GKE workload can materialize. */
211const GKE_BINDING_KEYS = new Set(["env", "iam"]);
212
213/** Narrow persisted identity state (possibly a legacy shape). */
214const identityStateOf = (state: Record<string, unknown> | undefined) => ({
215 member: typeof state?.member === "string" ? state.member : undefined,
216 iamGrants: Array.isArray(state?.iamGrants)
217 ? (state.iamGrants as AppliedIamGrant[])
218 : [],
219 gcpServiceAccount:
220 typeof state?.gcpServiceAccount === "string"
221 ? state.gcpServiceAccount
222 : undefined,
223 impersonator:
224 typeof state?.impersonator === "string" ? state.impersonator : undefined,
225});
226
227/**
228 * Build a {@link ClusterTransport} for a GKE cluster from known
229 * endpoint/CA. Captures the ambient GCP credentials so the per-request
230 * token mint is self-contained — used by the adapter and by the
231 * `GCP.Container.Cluster` provider's kubernetes-object binding channel.
232 */
233export const makeGkeTransport = Effect.fn(function* (options: {
234 endpoint: string;
235 certificateAuthorityData: string;
236}) {
237 const context = yield* Effect.context<Credentials>();
238 return {
239 endpoint: options.endpoint,
240 certificateAuthorityData: options.certificateAuthorityData,
241 headers: Effect.gen(function* () {
242 const creds = yield* yield* Credentials;
243 return {
244 Authorization: `Bearer ${Redacted.value(creds.accessToken)}`,
245 };
246 }).pipe(Effect.provideContext(context)),
247 } satisfies ClusterTransport;
248});
249
250/**
251 * The `Kubernetes.Connection` of a GKE cluster — stamped on
252 * `GCP.Container.Cluster` attributes so the cluster resource can be passed
253 * directly as any `Kubernetes.*` workload's `cluster`.
254 */
255export const gkeConnectionOf = (options: {
256 clusterId: string;
257 location: string;
258 project?: string | undefined;
259 endpoint?: string | undefined;
260 certificateAuthorityData?: string | undefined;
261}): Connection => ({
262 endpoint: apiServerEndpoint(options.endpoint) ?? options.endpoint,
263 certificateAuthorityData: options.certificateAuthorityData,
264 auth: {
265 kind: "gcp-gke",
266 clusterId: options.clusterId,
267 location: options.location,
268 project: options.project,
269 },
270});
271
272const narrowGkeAuth = (connection: Connection) =>
273 connection.auth.kind === "gcp-gke"
274 ? Effect.succeed(connection.auth)
275 : Effect.die(
276 new Error(
277 `gcp-gke adapter received auth kind '${connection.auth.kind}'`,
278 ),
279 );
280
281const clusterResourceName = (
282 project: string,
283 location: string,
284 clusterId: string,
285) => `projects/${project}/locations/${location}/clusters/${clusterId}`;
286
287const createRepositoryName = (id: string) =>
288 createPhysicalName({
289 id: `${id}-repo`,
290 maxLength: 63,
291 lowercase: true,
292 });
293
294/**
295 * Generated container entry for an Effect-native GKE server: resolves the
296 * program's registered runners and serves the returned `{ fetch }` handler
297 * on `PORT`. Credentials use the GCP chain so Workload Identity's GCE
298 * metadata server resolves inside the pod.
299 */
300export const makeGkeServerBootstrap =
301 (handler: string) =>
302 (importPath: string): string =>
303 `
304import { BunServices } from "@effect/platform-bun";
305import { BunHttpServer } from "alchemy/Http";
306import { Stack } from "alchemy/Stack";
307import { makeEntrypointLayer, reifyBoundConfigProvider } from "alchemy/Runtime";
308import { provideProcessTelemetry } from "alchemy/Telemetry";
309import * as Context from "effect/Context";
310import * as Config from "effect/Config";
311import * as ConfigProvider from "effect/ConfigProvider";
312import * as Effect from "effect/Effect";
313import * as FetchHttpClient from "effect/http/FetchHttpClient";
314import * as Layer from "effect/Layer";
315import * as Logger from "effect/Logger";
316import { fromChain, fromCredentials } from "alchemy/GCP";
317
318import { ${handler} as entrypoint } from ${JSON.stringify(importPath)};
319
320const tag = Context.Service("${Self.key}");
321const layer = makeEntrypointLayer(tag, entrypoint);
322
323const platform = Layer.mergeAll(
324 BunServices.layer,
325 FetchHttpClient.layer,
326 Logger.layer([Logger.consolePretty()]),
327);
328
329const program = tag.pipe(
330 Effect.flatMap((host) =>
331 host.RuntimeContext.exports.pipe(
332 Effect.flatMap((exports) => exports.program),
333 provideProcessTelemetry(host.RuntimeContext),
334 ),
335 ),
336 Effect.provide(
337 layer.pipe(Layer.provideMerge(Layer.effect(
338 Stack,
339 Effect.all([
340 Config.String("ALCHEMY_STACK_NAME"),
341 Config.String("ALCHEMY_STAGE")
342 ]).pipe(
343 Effect.map(([name, stage]) => ({
344 name,
345 stage,
346 bindings: {},
347 resources: {}
348 }))
349 )
350 )),
351 Layer.provideMerge(fromCredentials()),
352 Layer.provideMerge(fromChain()),
353 Layer.provideMerge(BunHttpServer()),
354 Layer.provideMerge(platform),
355 Layer.provideMerge(
356 Layer.succeed(
357 ConfigProvider.ConfigProvider,
358 reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env)
359 )
360 ),
361 )
362 ),
363 Effect.scoped
364);
365
366console.log(\`GKE Deployment bootstrap starting on port \${process.env.PORT ?? 3000}...\`);
367await Effect.runPromise(program).catch((err) => {
368 console.error("GKE Deployment bootstrap failed:", err);
369 process.exit(1);
370});
371`;
372
373/**
374 * Generated container entry for an Effect-native GKE Job: resolves the
375 * program's `run` effect, executes it to completion, and exits. Credentials
376 * use the GCP chain so Workload Identity's GCE metadata server resolves
377 * inside the pod.
378 */
379export const makeGkeJobBootstrap =
380 (handler: string) =>
381 (importPath: string): string =>
382 `
383import { BunServices } from "@effect/platform-bun";
384import { Stack } from "alchemy/Stack";
385import { makeEntrypointLayer, reifyBoundConfigProvider } from "alchemy/Runtime";
386import { provideProcessTelemetry } from "alchemy/Telemetry";
387import * as Context from "effect/Context";
388import * as Config from "effect/Config";
389import * as ConfigProvider from "effect/ConfigProvider";
390import * as Effect from "effect/Effect";
391import * as FetchHttpClient from "effect/http/FetchHttpClient";
392import * as Layer from "effect/Layer";
393import * as Logger from "effect/Logger";
394import { fromChain, fromCredentials } from "alchemy/GCP";
395
396import { ${handler} as entrypoint } from ${JSON.stringify(importPath)};
397
398const tag = Context.Service("${Self.key}");
399const layer = makeEntrypointLayer(tag, entrypoint);
400
401const platform = Layer.mergeAll(
402 BunServices.layer,
403 FetchHttpClient.layer,
404 Logger.layer([Logger.consolePretty()]),
405);
406
407const program = tag.pipe(
408 Effect.flatMap((host) =>
409 host.RuntimeContext.exports.pipe(
410 Effect.flatMap((exports) => exports.program),
411 provideProcessTelemetry(host.RuntimeContext),
412 ),
413 ),
414 Effect.provide(
415 layer.pipe(Layer.provideMerge(Layer.effect(
416 Stack,
417 Effect.all([
418 Config.String("ALCHEMY_STACK_NAME"),
419 Config.String("ALCHEMY_STAGE")
420 ]).pipe(
421 Effect.map(([name, stage]) => ({
422 name,
423 stage,
424 bindings: {},
425 resources: {}
426 }))
427 )
428 )),
429 Layer.provideMerge(fromCredentials()),
430 Layer.provideMerge(fromChain()),
431 Layer.provideMerge(platform),
432 Layer.provideMerge(
433 Layer.succeed(
434 ConfigProvider.ConfigProvider,
435 reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env)
436 )
437 ),
438 )
439 ),
440 Effect.scoped
441);
442
443console.log("GKE Job bootstrap starting...");
444await Effect.runPromise(program).catch((err) => {
445 console.error("GKE Job bootstrap failed:", err);
446 process.exit(1);
447});
448process.exit(0);
449`;
450
451/**
452 * The `gcp-gke` cluster adapter layer. Provided (merged) by
453 * `GCP.providers()` so the `Kubernetes.*` workload providers can resolve
454 * it from the stack context.
455 */
456export const GkeKubernetesAdapter = () =>
457 Layer.effect(
458 ClusterAdapter("gcp-gke"),
459 Effect.gen(function* () {
460 const imageSource = yield* makeImageSource;
461 const context = yield* Effect.context<GkeAdapterDeps>();
462
463 const withGcp = <A, E, R>(self: Effect.Effect<A, E, R>) =>
464 Effect.provideContext(self, context);
465
466 const getLiveCluster = Effect.fn(function* (auth: {
467 clusterId: string;
468 location: string;
469 project?: string | undefined;
470 }) {
471 const cluster = yield* Effect.gen(function* () {
472 const env = yield* GcpEnvironment.current;
473 const project = auth.project ?? env.project;
474 const name = clusterResourceName(
475 project,
476 auth.location,
477 auth.clusterId,
478 );
479 return yield* container
480 .getProjectsLocationsClusters({ name })
481 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
482 }).pipe(withGcp);
483 if (!cluster || cluster.status === "STOPPING") {
484 return yield* Effect.fail(
485 new ClusterNotFoundError({
486 message: `GKE cluster '${auth.clusterId}' no longer exists`,
487 }),
488 );
489 }
490 return cluster;
491 });
492
493 const connect: ClusterAdapterService["connect"] = (connection) =>
494 Effect.gen(function* () {
495 const auth = yield* narrowGkeAuth(connection);
496 let endpoint = apiServerEndpoint(connection.endpoint);
497 let certificateAuthorityData = connection.certificateAuthorityData;
498 if (!endpoint || !certificateAuthorityData) {
499 const cluster = yield* getLiveCluster(auth);
500 endpoint = apiServerEndpoint(cluster.endpoint);
501 certificateAuthorityData = cluster.masterAuth?.clusterCaCertificate;
502 if (!endpoint || !certificateAuthorityData) {
503 return yield* Effect.fail(
504 new Error(
505 `GKE cluster '${auth.clusterId}' has no endpoint or ` +
506 "certificate authority data yet (still creating?)",
507 ),
508 );
509 }
510 }
511 return yield* makeGkeTransport({
512 endpoint,
513 certificateAuthorityData,
514 });
515 }).pipe(
516 withGcp,
517 Effect.mapError((error): ClusterNotFoundError | Error =>
518 error instanceof ClusterNotFoundError || error instanceof Error
519 ? error
520 : new Error(String(error)),
521 ),
522 );
523
524 const identityReconcile = Effect.fn(function* (
525 options: WorkloadIdentityReconcileOptions,
526 ) {
527 const auth = yield* narrowGkeAuth(options.connection);
528 const bindings = options.bindings.filter(
529 (
530 binding: ResourceBinding<WorkloadBindingContract> & {
531 action?: string;
532 },
533 ) => binding.action !== "delete",
534 );
535 const unsupported = [
536 ...new Set(
537 bindings.flatMap((binding) =>
538 Object.entries(binding.data ?? {})
539 .filter(
540 ([key, value]) =>
541 value !== undefined && !GKE_BINDING_KEYS.has(key),
542 )
543 .map(([key]) => key),
544 ),
545 ),
546 ];
547 if (unsupported.length > 0) {
548 return yield* Effect.die(
549 new Error(
550 `'${options.id}': bindings carry ${unsupported.join(", ")}, ` +
551 "which GKE workloads cannot materialize (only env and " +
552 "GCP `iam` grants).",
553 ),
554 );
555 }
556 return yield* Effect.gen(function* () {
557 const env = yield* GcpEnvironment.current;
558 const project = auth.project ?? env.project;
559 const workloadPool = workloadPoolOf(project);
560 const principal = workloadIdentityPrincipal({
561 projectNumber: yield* projectNumber(project),
562 project,
563 namespace: options.namespace,
564 serviceAccount: options.serviceAccount,
565 });
566 const gcpServiceAccount = options.options?.gcpServiceAccount;
567 const member = gcpServiceAccount
568 ? `serviceAccount:${gcpServiceAccount}`
569 : principal;
570 const previous = identityStateOf(options.state);
571
572 // Let the KSA impersonate the user-supplied GSA.
573 if (gcpServiceAccount) {
574 yield* updateIamMembership({
575 kind: "iam.serviceAccount",
576 name: gsaResourceName(gcpServiceAccount),
577 member: principal,
578 add: [WORKLOAD_IDENTITY_USER],
579 });
580 }
581 if (
582 previous.gcpServiceAccount !== undefined &&
583 previous.impersonator !== undefined &&
584 (previous.gcpServiceAccount !== gcpServiceAccount ||
585 previous.impersonator !== principal)
586 ) {
587 yield* revokeIamMembership({
588 kind: "iam.serviceAccount",
589 name: gsaResourceName(previous.gcpServiceAccount),
590 member: previous.impersonator,
591 roles: [WORKLOAD_IDENTITY_USER],
592 });
593 }
594
595 // A renamed KSA / namespace or a GSA switch is a new member: its
596 // predecessor loses everything it was granted.
597 const sameMember = previous.member === member;
598 if (previous.member !== undefined && !sameMember) {
599 yield* revokeHostIam({
600 serviceAccount: previous.member,
601 grants: previous.iamGrants,
602 });
603 }
604 const { grants } = yield* syncHostIam({
605 project,
606 serviceAccount: member,
607 // The principal is shared by every cluster in the project, so
608 // only grants this workload recorded are ever revoked.
609 managed: false,
610 bindings,
611 previous: sameMember ? previous.iamGrants : undefined,
612 });
613
614 return {
615 env: {
616 GOOGLE_PROJECT_ID: project,
617 GOOGLE_CLOUD_PROJECT: project,
618 },
619 serviceAccountAnnotations: gcpServiceAccount
620 ? { [GSA_ANNOTATION]: gcpServiceAccount }
621 : undefined,
622 state: {
623 kind: "gcp-workload-identity" as const,
624 workloadPool,
625 project,
626 member,
627 iamGrants: grants,
628 gcpServiceAccount,
629 impersonator: gcpServiceAccount ? principal : undefined,
630 },
631 };
632 }).pipe(withGcp);
633 });
634
635 const identityDelete = Effect.fn(function* (options: {
636 connection: Connection | undefined;
637 state: Record<string, unknown> | undefined;
638 }) {
639 // Grants live on project / resource IAM policies, not on the
640 // cluster, so they are revoked even when the cluster is gone. The
641 // KSA itself dies with the workload's objects.
642 const state = identityStateOf(options.state);
643 yield* Effect.gen(function* () {
644 if (state.member !== undefined) {
645 yield* revokeHostIam({
646 serviceAccount: state.member,
647 grants: state.iamGrants,
648 });
649 }
650 if (
651 state.gcpServiceAccount !== undefined &&
652 state.impersonator !== undefined
653 ) {
654 yield* revokeIamMembership({
655 kind: "iam.serviceAccount",
656 name: gsaResourceName(state.gcpServiceAccount),
657 member: state.impersonator,
658 roles: [WORKLOAD_IDENTITY_USER],
659 });
660 }
661 }).pipe(withGcp);
662 });
663
664 const registryResolve = (
665 options: ImageRegistryResolveOptions,
666 ): Effect.Effect<ImageRegistryResult, any, AdapterLifecycleServices> =>
667 Effect.gen(function* () {
668 const state = options.state;
669 const env = yield* GcpEnvironment.current;
670 const location =
671 typeof state?.location === "string" ? state.location : env.region;
672 const repositoryName =
673 typeof state?.repositoryName === "string"
674 ? state.repositoryName
675 : yield* createRepositoryName(options.id);
676 const repositoryUri =
677 typeof state?.repositoryUri === "string" &&
678 state.repositoryName === repositoryName
679 ? state.repositoryUri
680 : undefined;
681 const resolved = yield* imageSource.resolve({
682 id: options.id,
683 source: options.source as ImageSourceLike,
684 repositoryName,
685 repositoryUri,
686 location,
687 tags: options.tags,
688 platform: options.platform,
689 port: options.port,
690 isExternal: options.isExternal,
691 bootstrap: options.bootstrap,
692 session: options.session,
693 });
694 return {
695 imageUri: resolved.imageUri,
696 codeHash: resolved.codeHash,
697 state: {
698 kind: "gcp-artifact-registry" as const,
699 repositoryName: resolved.repositoryName,
700 repositoryUri: resolved.repositoryUri,
701 location,
702 name:
703 typeof state?.name === "string" &&
704 state.repositoryName === repositoryName
705 ? state.name
706 : imageSource.resourceName(
707 env.project,
708 location,
709 resolved.repositoryName,
710 ),
711 },
712 } satisfies ImageRegistryResult;
713 }).pipe(withGcp) as Effect.Effect<
714 ImageRegistryResult,
715 any,
716 AdapterLifecycleServices
717 >;
718
719 const registryHash = Effect.fn(function* (options: {
720 source: ImageSourceLike;
721 platform: string;
722 port?: number | undefined;
723 isExternal?: boolean | undefined;
724 bootstrap: (importPath: string) => string;
725 }) {
726 return yield* imageSource
727 .hash({
728 source: options.source,
729 platform: options.platform,
730 port: options.port,
731 isExternal: options.isExternal,
732 bootstrap: options.bootstrap,
733 })
734 .pipe(withGcp);
735 });
736
737 const registryDelete = (
738 options: ImageRegistryDeleteOptions,
739 ): Effect.Effect<void, any, AdapterLifecycleServices> =>
740 Effect.gen(function* () {
741 if (typeof options.state?.name !== "string") return;
742 yield* imageSource.destroyRepository(options.state.name);
743 }).pipe(withGcp) as Effect.Effect<void, any, AdapterLifecycleServices>;
744
745 const loadBalancerDefaults = Effect.fn(function* (_options: {
746 connection: Connection;
747 }) {
748 return {
749 loadBalancerClass: undefined,
750 annotations: {
751 "cloud.google.com/l4-rbs": "enabled",
752 },
753 };
754 });
755
756 return {
757 kind: "Kubernetes.ClusterAdapter" as const,
758 connect,
759 identity: {
760 reconcile: identityReconcile,
761 delete: identityDelete,
762 },
763 registry: {
764 resolve: registryResolve,
765 hash: registryHash,
766 delete: registryDelete,
767 },
768 bootstrap: {
769 server: makeGkeServerBootstrap,
770 job: makeGkeJobBootstrap,
771 },
772 loadBalancerDefaults,
773 } satisfies ClusterAdapterService;
774 }),
775 );