1/**
2 * The extension seam between the cluster-agnostic `Kubernetes.*` workloads
3 * and the platform a cluster runs on.
4 *
5 * A {@link ClusterAdapterService} is registered under a keyed Context tag —
6 * `Kubernetes.ClusterAdapter/<auth kind>` — and resolved dynamically from
7 * the ambient provider context by {@link findClusterAdapter}, mirroring how
8 * resource providers are resolved by type. `Kubernetes.providers()` ships
9 * the built-in adapters (`kubeconfig`, `token`, `client-cert`, `exec`);
10 * cloud provider layers contribute theirs (`AWS.providers()` registers
11 * `aws-eks`).
12 *
13 * An adapter owns everything platform-specific:
14 *
15 * - **connect** (required) — resolve the API server endpoint/CA and mint
16 * per-request auth headers.
17 * - **identity** (optional) — provision workload identity for a namespace +
18 * service account and translate host bindings into cloud credentials
19 * (EKS Pod Identity, GKE Workload Identity Federation).
20 * - **registry** (optional) — build/mirror container images into a managed
21 * registry the cluster can pull from (ECR on EKS, Artifact Registry on GKE).
22 * - **bootstrap** (optional) — platform-specific generated container
23 * entries for Effect-native workloads (e.g. wiring the AWS credential
24 * chain for Pod Identity).
25 * - **loadBalancerDefaults** (optional) — platform defaults for
26 * `LoadBalancer` Services (EKS Auto Mode's `loadBalancerClass` +
27 * internet-facing scheme).
28 */
29import * as Context from "effect/Context";
30import * as Data from "effect/Data";
31import * as Effect from "effect/Effect";
32import * as Option from "effect/Option";
33import type * as Bundle from "../Bundle/Bundle.ts";
34import type { InstanceId } from "../InstanceId.ts";
35import type { ResourceBinding } from "../Resource.ts";
36import type { InlineDockerfile } from "../Docker/Dockerfile.ts";
37import type { Stack } from "../Stack.ts";
38import type { Stage } from "../Stage.ts";
39import type { Connection } from "./Connection.ts";
40
41/**
42 * Per-resource engine services ambient inside every provider lifecycle
43 * operation. Adapter methods run inside the workload providers' lifecycle
44 * ops, so they may require these (physical-name generation needs the
45 * resource's InstanceId + Stack + Stage) — everything else an adapter
46 * needs is captured at its layer build.
47 */
48export type AdapterLifecycleServices = InstanceId | Stack | Stage;
49
50/**
51 * The target cluster no longer exists (definitively — e.g. the managed
52 * control plane is deleted or deleting). Distinct from transient
53 * unreachability: `read`/`delete` treat this as "everything in-cluster is
54 * already gone".
55 */
56export class ClusterNotFoundError extends Data.TaggedError(
57 "Kubernetes.ClusterNotFoundError",
58)<{
59 message: string;
60}> {}
61
62/**
63 * A resolved transport to a cluster's API server. `headers` is an Effect
64 * so short-lived tokens (EKS SigV4 presigns, exec plugin credentials) are
65 * minted per request.
66 */
67export interface ClusterTransport {
68 /** The API server endpoint URL. */
69 endpoint: string;
70 /** Base64-encoded PEM certificate authority bundle. */
71 certificateAuthorityData?: string;
72 /** Skip TLS verification (self-signed local clusters). */
73 insecureSkipTlsVerify?: boolean;
74 /** Mint the auth headers for one request. */
75 headers: Effect.Effect<Record<string, string>, Error>;
76 /** PEM client certificate credentials for mutual TLS. */
77 clientCert?: { certificate: string; key: string };
78}
79
80/**
81 * Workload identity state persisted on a workload's attributes, keyed by
82 * adapter kind. Cloud providers extend it via module augmentation — AWS
83 * registers:
84 *
85 * ```ts
86 * declare module "../../Kubernetes/ClusterAdapter.ts" {
87 * interface IdentityStateRegistry {
88 * "aws-pod-identity": {
89 * roleArn: string;
90 * roleName: string;
91 * associationArn: string;
92 * associationId: string;
93 * };
94 * }
95 * }
96 * ```
97 */
98export interface IdentityStateRegistry {}
99
100/** The discriminated identity-state union across all registered adapters. */
101export type IdentityState = {
102 [K in keyof IdentityStateRegistry]: {
103 readonly kind: K;
104 } & IdentityStateRegistry[K];
105}[keyof IdentityStateRegistry];
106
107/**
108 * Image-registry state persisted on a workload's attributes, keyed by
109 * adapter kind (AWS registers `"aws-ecr"` with the repository name/URI).
110 * The built-in `"registry"` entry records images pushed to a connection's
111 * {@link Connection.registry}.
112 */
113export interface RegistryStateRegistry {
114 registry: {
115 /** The registry `server` the image was pushed to. */
116 server: string;
117 /** The repository the image was pushed to (`<server>/<name>`). */
118 repository: string;
119 };
120}
121
122/** The discriminated registry-state union across all registered adapters. */
123export type RegistryState = {
124 [K in keyof RegistryStateRegistry]: {
125 readonly kind: K;
126 } & RegistryStateRegistry[K];
127}[keyof RegistryStateRegistry];
128
129/**
130 * Cloud-specific workload identity options, extended via module
131 * augmentation (AWS adds `managedPolicyArns`, GCP `gcpServiceAccount`).
132 */
133export interface WorkloadIdentityOptions {}
134
135/**
136 * The binding contract of `Kubernetes.Deployment` / `Kubernetes.Job`
137 * hosts. The core contract is environment variables; cloud providers
138 * augment it with their credential-grant channels (AWS adds
139 * `policyStatements`, GCP adds `iam`), which the matching {@link ClusterAdapterService}'s
140 * identity adapter materializes at deploy time.
141 */
142export interface WorkloadBindingContract {
143 env?: Record<string, any>;
144}
145
146/**
147 * Ambient runtime services the workload platforms assume inside the
148 * deployed container, keyed by contributor and extended via module
149 * augmentation — AWS registers its credential-chain services so inline
150 * impls can use AWS bindings without providing them:
151 *
152 * ```ts
153 * declare module "../../Kubernetes/ClusterAdapter.ts" {
154 * interface WorkloadServicesRegistry {
155 * aws: Credentials | Region | AWSEnvironment;
156 * }
157 * }
158 * ```
159 */
160export interface WorkloadServicesRegistry {}
161
162/** The union of all registered ambient workload services. */
163export type WorkloadServices =
164 WorkloadServicesRegistry[keyof WorkloadServicesRegistry];
165
166/**
167 * The image-source shape shared by every workload: exactly one of `main`
168 * (bundle an inline Effect program), `context`/`dockerfile` (build the
169 * user's Dockerfile), or `image` (a pre-built registry reference).
170 * Structurally mirrors the AWS container platforms' source props.
171 */
172export interface WorkloadImageSource {
173 main?: string;
174 handler?: string;
175 /**
176 * Bundler configuration for `main`. Unused code is tree-shaken.
177 * `effect`, alchemy, and `@distilled.cloud` are marked pure so unused
178 * parts prune more aggressively. List extra packages with
179 * `pure.packages`, or disable with `pure: false`.
180 */
181 build?: Bundle.BundleConfig;
182 context?: string;
183 dockerfile?: string | InlineDockerfile;
184 image?: string;
185}
186
187export interface WorkloadIdentityReconcileOptions {
188 /** Logical resource id. */
189 id: string;
190 /** The resolved cluster connection. */
191 connection: Connection;
192 /** The namespace the workload runs in. */
193 namespace: string;
194 /** The service account name the workload's pods run as. */
195 serviceAccount: string;
196 /** Host bindings attached to the workload. */
197 bindings: ResourceBinding<WorkloadBindingContract>[];
198 /** Cloud-specific identity options from the workload's `identity` prop. */
199 options: WorkloadIdentityOptions | undefined;
200 /**
201 * Previously persisted identity state, if any — untyped hints because
202 * legacy pre-rename rows carry the old flat attribute shape; adapters
203 * narrow structurally and re-observe cloud state as the authority.
204 */
205 state: Record<string, unknown> | undefined;
206 /** Internal + user tags for identity-owned cloud resources. */
207 tags: Record<string, string>;
208}
209
210export interface WorkloadIdentityResult {
211 /** Extra container environment collected from bindings. */
212 env: Record<string, any>;
213 /** Annotations to stamp on the workload's ServiceAccount object. */
214 serviceAccountAnnotations?: Record<string, string>;
215 /** Serializable state persisted on the workload's attributes. */
216 state: IdentityState | undefined;
217}
218
219export interface WorkloadIdentityDeleteOptions {
220 /** The persisted cluster connection (may reference a gone cluster). */
221 connection: Connection | undefined;
222 /**
223 * The persisted identity state. Untyped because legacy pre-rename rows
224 * carry the old flat attribute shape — adapters narrow structurally.
225 */
226 state: Record<string, unknown> | undefined;
227}
228
229export interface ImageRegistryResolveOptions {
230 /** Logical resource id — keys generated repository names. */
231 id: string;
232 /** The image-source props bag. */
233 source: WorkloadImageSource;
234 /** Target image platform (`linux/amd64` / `linux/arm64`). */
235 platform: string;
236 /** Port the generated Dockerfile exposes (`main` sources only). */
237 port?: number;
238 /** True when `main` bundles as-is (no Effect bootstrap entry). */
239 isExternal?: boolean;
240 /** The generated-entry bootstrap wrapped around `main`. */
241 bootstrap: (importPath: string) => string;
242 /** Tags for registry-owned cloud resources. */
243 tags: Record<string, string>;
244 /**
245 * Previously persisted registry state, if any — untyped hints because
246 * legacy pre-rename rows carry the old flat attribute shape; adapters
247 * narrow structurally.
248 */
249 state: Record<string, unknown> | undefined;
250 /** Plan-status session for build/push progress notes. */
251 session: { note: (message: string) => Effect.Effect<void> };
252}
253
254export interface ImageRegistryResult {
255 /** Full image reference the pod spec should run. */
256 imageUri: string;
257 /** Content hash identifying the image. */
258 codeHash: string;
259 /** Serializable state persisted on the workload's attributes. */
260 state: RegistryState | undefined;
261}
262
263export interface ImageRegistryHashOptions {
264 source: WorkloadImageSource;
265 platform: string;
266 port?: number;
267 isExternal?: boolean;
268 bootstrap: (importPath: string) => string;
269}
270
271export interface ImageRegistryDeleteOptions {
272 /**
273 * The persisted registry state. Untyped because legacy pre-rename rows
274 * carry the old flat attribute shape — adapters narrow structurally.
275 */
276 state: Record<string, unknown> | undefined;
277}
278
279export interface ClusterAdapterService {
280 readonly kind: "Kubernetes.ClusterAdapter";
281 /**
282 * Resolve the transport for a connection: discover the endpoint/CA when
283 * the connection doesn't carry them, and return a per-request auth
284 * header mint. Fails with {@link ClusterNotFoundError} when the cluster
285 * definitively no longer exists.
286 */
287 readonly connect: (
288 connection: Connection,
289 ) => Effect.Effect<ClusterTransport, ClusterNotFoundError | Error>;
290 /** Workload identity provisioning (Pod Identity on EKS, Workload Identity Federation on GKE). */
291 readonly identity?: {
292 readonly reconcile: (
293 options: WorkloadIdentityReconcileOptions,
294 ) => Effect.Effect<WorkloadIdentityResult, any, AdapterLifecycleServices>;
295 readonly delete: (
296 options: WorkloadIdentityDeleteOptions,
297 ) => Effect.Effect<void, any, AdapterLifecycleServices>;
298 };
299 /** Managed container-image registry (ECR on EKS, Artifact Registry on GKE). */
300 readonly registry?: {
301 readonly resolve: (
302 options: ImageRegistryResolveOptions,
303 ) => Effect.Effect<ImageRegistryResult, any, AdapterLifecycleServices>;
304 /** Plan-time content hash used by `diff` to surface source drift. */
305 readonly hash: (
306 options: ImageRegistryHashOptions,
307 ) => Effect.Effect<string | undefined, any, AdapterLifecycleServices>;
308 readonly delete: (
309 options: ImageRegistryDeleteOptions,
310 ) => Effect.Effect<void, any, AdapterLifecycleServices>;
311 };
312 /**
313 * Platform-specific generated container entries for Effect-native
314 * workloads. Defaults to the platform-neutral bootstraps when omitted.
315 */
316 readonly bootstrap?: {
317 readonly server?: (handler: string) => (importPath: string) => string;
318 readonly job?: (handler: string) => (importPath: string) => string;
319 };
320 /**
321 * Platform defaults applied to a `LoadBalancer` Service (EKS Auto Mode
322 * sets `loadBalancerClass: eks.amazonaws.com/nlb` and defaults the
323 * scheme to internet-facing). User `serviceAnnotations` always win.
324 */
325 readonly loadBalancerDefaults?: (options: {
326 connection: Connection;
327 }) => Effect.Effect<
328 {
329 loadBalancerClass?: string | undefined;
330 annotations?: Record<string, string>;
331 },
332 any,
333 AdapterLifecycleServices
334 >;
335}
336
337const adapterKey = (authKind: string) =>
338 `Kubernetes.ClusterAdapter/${authKind}`;
339
340/**
341 * The keyed Context tag for an adapter. Same auth kind → same tag, so a
342 * layer built with `ClusterAdapter("aws-eks")` is found by any dynamic
343 * lookup for that kind.
344 */
345export const ClusterAdapter = (
346 authKind: string,
347): Context.Service<ClusterAdapterService, ClusterAdapterService> =>
348 Context.Service<ClusterAdapterService, ClusterAdapterService>()(
349 adapterKey(authKind),
350 ) as any;
351
352/**
353 * Resolve the {@link ClusterAdapterService} for a connection's auth kind
354 * from the ambient context (the stack's composed provider layers). Dies
355 * with setup guidance when no adapter is registered — that means the
356 * provider layer contributing it (e.g. `AWS.providers()` for `aws-eks`)
357 * is missing from the stack.
358 */
359export const findClusterAdapter = (
360 authKind: string,
361): Effect.Effect<ClusterAdapterService> =>
362 Effect.serviceOption(ClusterAdapter(authKind)).pipe(
363 Effect.flatMap(
364 Option.match({
365 onSome: (adapter) => Effect.succeed(adapter),
366 onNone: () =>
367 Effect.die(
368 new Error(
369 `No Kubernetes cluster adapter is registered for auth kind ` +
370 `'${authKind}'. Add the provider layer that contributes it ` +
371 `to the stack's providers — e.g. 'aws-eks' ships with ` +
372 "`AWS.providers()`; the built-in kinds (kubeconfig, token, " +
373 "client-cert, exec) ship with `Kubernetes.providers()`. " +
374 "Compose multiple provider layers with " +
375 "`Layer.mergeAll(AWS.providers(), Kubernetes.providers())`.",
376 ),
377 ),
378 }),
379 ),
380 );