1import type * as Redacted from "effect/Redacted";
2
3/**
4 * The cluster-agnostic Kubernetes connection model.
5 *
6 * Every `Kubernetes.*` workload (`Deployment`, `Job`, `Manifest`,
7 * `HelmChart`) targets a cluster through a serializable {@link Connection}:
8 * the API server endpoint (or enough information to discover it) plus an
9 * {@link ConnectionAuth} descriptor whose `kind` selects the
10 * {@link ClusterAdapter} that knows how to authenticate requests — and,
11 * for managed clouds, how to provision workload identity and container
12 * images.
13 *
14 * A `Connection` is plain data on purpose: it is resolved from resource
15 * attributes at reconcile time and persisted on workload attributes so
16 * `delete` can reconnect without the original resource graph.
17 */
18
19/**
20 * The auth descriptor registry, keyed by adapter kind. Cloud providers
21 * extend it via module augmentation — e.g. AWS registers:
22 *
23 * ```ts
24 * declare module "../../Kubernetes/Connection.ts" {
25 * interface AuthRegistry {
26 * "aws-eks": { clusterName: string; region?: string };
27 * }
28 * }
29 * ```
30 *
31 * The built-in kinds cover anything `kubectl` can reach: a kubeconfig
32 * context (including `exec` credential plugins — `aws eks get-token`,
33 * `kubelogin`, `gke-gcloud-auth-plugin`), a static bearer token, a client
34 * certificate, or a raw exec credential plugin invocation.
35 */
36export interface AuthRegistry {
37 /**
38 * Resolve the cluster, user, and credentials from a kubeconfig file —
39 * the "anything kubectl can reach" adapter. Honors client certificates,
40 * static tokens, and `exec` credential plugins declared in the file.
41 */
42 kubeconfig: {
43 /**
44 * Path to the kubeconfig file.
45 * @default `$KUBECONFIG` or `~/.kube/config`
46 */
47 path?: string;
48 /**
49 * The context to use.
50 * @default the file's `current-context`
51 */
52 context?: string;
53 };
54 /** Authenticate with a static bearer token. */
55 token: {
56 /** The bearer token sent as `Authorization: Bearer <token>`. */
57 token: string;
58 };
59 /** Authenticate with a client certificate (mutual TLS). */
60 "client-cert": {
61 /** PEM-encoded client certificate. */
62 certificate: string;
63 /** PEM-encoded client private key. */
64 key: string;
65 };
66 /**
67 * Mint credentials with a kubeconfig-style exec credential plugin
68 * (`client.authentication.k8s.io` ExecCredential protocol).
69 */
70 exec: {
71 /** The command to run (e.g. `aws`). */
72 command: string;
73 /** Arguments (e.g. `["eks", "get-token", "--cluster-name", "x"]`). */
74 args?: string[];
75 /** Extra environment variables for the plugin process. */
76 env?: Record<string, string>;
77 };
78}
79
80/**
81 * The discriminated auth union — one member per {@link AuthRegistry} entry,
82 * each tagged with its registry key as `kind`.
83 */
84export type ConnectionAuth = {
85 [K in keyof AuthRegistry]: { readonly kind: K } & AuthRegistry[K];
86}[keyof AuthRegistry];
87
88/**
89 * A container registry the cluster's nodes can pull from. `Deployment` and
90 * `Job` workloads with a `main` program or a `context` Dockerfile are built
91 * on the deploying machine and pushed here as `<server>/<name>:<hash>`.
92 *
93 * Clusters whose platform adapter provides its own registry (EKS pushes to
94 * ECR) ignore this setting.
95 */
96export interface ContainerRegistry {
97 /**
98 * Registry host, optionally followed by the namespace images are pushed
99 * under — e.g. `localhost:5001`, `ghcr.io/acme`, or
100 * `us-docker.pkg.dev/my-project/apps`.
101 */
102 server: string;
103 /**
104 * Username for pushing. Omit `username` and `password` to push with the
105 * deploying machine's own Docker login (`docker login`, credential
106 * helpers).
107 */
108 username?: string;
109 /** Password or access token for pushing. */
110 password?: string | Redacted.Redacted<string>;
111}
112
113/**
114 * A serializable description of how to reach and authenticate against a
115 * Kubernetes API server.
116 */
117export interface Connection {
118 /**
119 * The API server endpoint URL. Optional when the auth adapter can
120 * discover it (a kubeconfig file carries the server; the EKS adapter
121 * re-describes the cluster).
122 */
123 endpoint?: string;
124 /** Base64-encoded PEM certificate authority bundle for the endpoint. */
125 certificateAuthorityData?: string;
126 /** Skip TLS verification (self-signed local clusters). */
127 insecureSkipTlsVerify?: boolean;
128 /** How to authenticate — selects the {@link ClusterAdapter} by `kind`. */
129 auth: ConnectionAuth;
130 /**
131 * Where `main` and `context` workloads push their images. Required for
132 * those sources unless the cluster's platform adapter provides a
133 * registry (EKS → ECR).
134 */
135 registry?: ContainerRegistry;
136 /**
137 * CPU architecture of the cluster's nodes — the default target of
138 * workload image builds when a workload doesn't set `architecture`.
139 * @default "amd64"
140 */
141 architecture?: "amd64" | "arm64";
142}
143
144/**
145 * Anything a `Kubernetes.*` workload accepts as its `cluster` prop: a raw
146 * {@link Connection}, or any value exposing one as a `connection` field —
147 * which is how managed cluster resources (`AWS.EKS.Cluster`) participate:
148 * their attributes carry a `connection`, so the whole resource can be
149 * passed directly.
150 */
151export type ClusterLike = Connection | { connection: Connection };
152
153const isConnection = (value: ClusterLike): value is Connection =>
154 "auth" in value && value.auth !== undefined;
155
156/**
157 * Normalize a {@link ClusterLike} to its {@link Connection}. Throws when
158 * the value carries neither shape — e.g. a managed cluster resource whose
159 * attributes predate the `connection` field.
160 */
161export const toConnection = (cluster: ClusterLike): Connection => {
162 if (isConnection(cluster)) return cluster;
163 if (cluster.connection !== undefined) return cluster.connection;
164 throw new Error(
165 "The `cluster` value carries no Kubernetes connection. Pass a cluster " +
166 "resource whose attributes expose `connection` (e.g. `AWS.EKS.Cluster`" +
167 " — redeploy the cluster if it was created before the `connection` " +
168 "attribute existed), a `Kubernetes.KubeConfig(...)`, or a raw " +
169 "`Kubernetes.Connection`.",
170 );
171};
172
173/**
174 * Connect `Kubernetes.*` workloads to any cluster your local kubeconfig can
175 * reach — k3s, kind, on-prem, AKS, GKE, anything `kubectl` works against.
176 * Exec credential plugins declared in the file (`aws eks get-token`,
177 * `kubelogin`, `gke-gcloud-auth-plugin`) are honored, so cloud-CLI-managed
178 * contexts work as-is.
179 *
180 * This is a plain helper, not a resource: nothing is provisioned and the
181 * cluster appears in no plan — it only describes how to reach the API
182 * server.
183 *
184 * ```ts
185 * const cluster = Kubernetes.KubeConfig({ context: "prod-east" });
186 *
187 * const api = yield* Kubernetes.Deployment("Api", {
188 * cluster,
189 * image: "ghcr.io/acme/api:v3",
190 * port: 8080,
191 * });
192 * ```
193 *
194 * Add a `registry` to build `main` programs and `context` Dockerfiles for
195 * the cluster:
196 *
197 * ```ts
198 * const cluster = Kubernetes.KubeConfig({
199 * context: "prod-east",
200 * registry: { server: "ghcr.io/acme" },
201 * });
202 * ```
203 */
204export const KubeConfig = (options?: {
205 /**
206 * Path to the kubeconfig file.
207 * @default `$KUBECONFIG` or `~/.kube/config`
208 */
209 path?: string;
210 /**
211 * The context to use.
212 * @default the file's `current-context`
213 */
214 context?: string;
215 /**
216 * Where `main` and `context` workloads push their images. The cluster's
217 * nodes must be able to pull from it.
218 */
219 registry?: ContainerRegistry;
220 /**
221 * CPU architecture of the cluster's nodes, used as the default image
222 * build target.
223 * @default "amd64"
224 */
225 architecture?: "amd64" | "arm64";
226}): Connection => ({
227 auth: {
228 kind: "kubeconfig",
229 path: options?.path,
230 context: options?.context,
231 },
232 ...(options?.registry !== undefined ? { registry: options.registry } : {}),
233 ...(options?.architecture !== undefined
234 ? { architecture: options.architecture }
235 : {}),
236});