1/**
2 * The built-in {@link ClusterAdapter} implementations shipped by
3 * `Kubernetes.providers()`: `kubeconfig`, `token`, `client-cert`, and
4 * `exec`. They cover any cluster `kubectl` can reach; managed-cloud
5 * adapters (EKS's `aws-eks`) are contributed by their provider layers.
6 *
7 * These adapters are auth-only — no workload identity and no managed image
8 * registry. Workloads on such clusters bind through environment variables
9 * and run pre-built `image` references (or images pushed by the user's own
10 * pipeline).
11 */
12import * as Effect from "effect/Effect";
13import * as FileSystem from "effect/FileSystem";
14import * as Layer from "effect/Layer";
15import * as Path from "effect/Path";
16import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
17import {
18 ClusterAdapter,
19 type ClusterAdapterService,
20 type ClusterTransport,
21} from "./ClusterAdapter.ts";
22import type { Connection } from "./Connection.ts";
23import {
24 mintUserCredentials,
25 resolveKubeContext,
26 runExecCredential,
27 type MintedCredentials,
28} from "./internal/kubeconfig.ts";
29
30const toAuthHeaders = (
31 credentials: MintedCredentials,
32): Record<string, string> =>
33 credentials.token ? { Authorization: `Bearer ${credentials.token}` } : {};
34
35const requireEndpoint = (connection: Connection) =>
36 connection.endpoint !== undefined
37 ? Effect.succeed(connection.endpoint)
38 : Effect.fail(
39 new Error(
40 `A '${connection.auth.kind}' Kubernetes connection requires an ` +
41 "explicit `endpoint` (only kubeconfig-backed and managed-cloud " +
42 "connections can discover it)",
43 ),
44 );
45
46/**
47 * `kubeconfig` — resolve the cluster and credentials from a kubeconfig
48 * file, honoring static tokens, client certificates, and `exec` credential
49 * plugins.
50 */
51export const KubeConfigAdapter: Layer.Layer<
52 ClusterAdapterService,
53 never,
54 FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
55> = Layer.effect(
56 ClusterAdapter("kubeconfig"),
57 Effect.gen(function* () {
58 // Capture the platform services at layer build so per-request minting
59 // (exec plugins can re-run on token expiry) needs no ambient context.
60 const context = yield* Effect.context<
61 | FileSystem.FileSystem
62 | Path.Path
63 | ChildProcessSpawner.ChildProcessSpawner
64 >();
65
66 return {
67 kind: "Kubernetes.ClusterAdapter" as const,
68 connect: Effect.fn(function* (connection: Connection) {
69 if (connection.auth.kind !== "kubeconfig") {
70 return yield* Effect.die(
71 new Error(
72 `kubeconfig adapter received auth kind '${connection.auth.kind}'`,
73 ),
74 );
75 }
76 const resolved = yield* resolveKubeContext({
77 path: connection.auth.path,
78 context: connection.auth.context,
79 }).pipe(Effect.provideContext(context));
80
81 // Mint once at connect: static tokens and client certificates are
82 // stable; exec plugins re-mint per request below (their tokens
83 // expire) but any plugin-issued client certificate is captured
84 // here.
85 const initial = yield* mintUserCredentials(resolved.user).pipe(
86 Effect.provideContext(context),
87 );
88
89 const isExec = resolved.user.exec?.command !== undefined;
90 const headers: ClusterTransport["headers"] = isExec
91 ? mintUserCredentials(resolved.user).pipe(
92 Effect.map(toAuthHeaders),
93 Effect.provideContext(context),
94 )
95 : Effect.succeed(toAuthHeaders(initial));
96
97 return {
98 endpoint: connection.endpoint ?? resolved.endpoint,
99 certificateAuthorityData:
100 connection.certificateAuthorityData ??
101 resolved.certificateAuthorityData,
102 insecureSkipTlsVerify:
103 connection.insecureSkipTlsVerify ?? resolved.insecureSkipTlsVerify,
104 headers,
105 clientCert: initial.clientCert,
106 } satisfies ClusterTransport;
107 }),
108 };
109 }),
110);
111
112/** `token` — a static bearer token against an explicit endpoint. */
113export const TokenAdapter: Layer.Layer<ClusterAdapterService> = Layer.succeed(
114 ClusterAdapter("token"),
115 {
116 kind: "Kubernetes.ClusterAdapter" as const,
117 connect: Effect.fn(function* (connection: Connection) {
118 if (connection.auth.kind !== "token") {
119 return yield* Effect.die(
120 new Error(
121 `token adapter received auth kind '${connection.auth.kind}'`,
122 ),
123 );
124 }
125 const token = connection.auth.token;
126 return {
127 endpoint: yield* requireEndpoint(connection),
128 certificateAuthorityData: connection.certificateAuthorityData,
129 insecureSkipTlsVerify: connection.insecureSkipTlsVerify,
130 headers: Effect.succeed({ Authorization: `Bearer ${token}` }),
131 } satisfies ClusterTransport;
132 }),
133 },
134);
135
136/** `client-cert` — mutual TLS against an explicit endpoint. */
137export const ClientCertAdapter: Layer.Layer<ClusterAdapterService> =
138 Layer.succeed(ClusterAdapter("client-cert"), {
139 kind: "Kubernetes.ClusterAdapter" as const,
140 connect: Effect.fn(function* (connection: Connection) {
141 if (connection.auth.kind !== "client-cert") {
142 return yield* Effect.die(
143 new Error(
144 `client-cert adapter received auth kind '${connection.auth.kind}'`,
145 ),
146 );
147 }
148 return {
149 endpoint: yield* requireEndpoint(connection),
150 certificateAuthorityData: connection.certificateAuthorityData,
151 insecureSkipTlsVerify: connection.insecureSkipTlsVerify,
152 headers: Effect.succeed({}),
153 clientCert: {
154 certificate: connection.auth.certificate,
155 key: connection.auth.key,
156 },
157 } satisfies ClusterTransport;
158 }),
159 });
160
161/**
162 * `exec` — mint credentials with a kubeconfig-style exec credential plugin
163 * against an explicit endpoint.
164 */
165export const ExecAdapter: Layer.Layer<
166 ClusterAdapterService,
167 never,
168 ChildProcessSpawner.ChildProcessSpawner
169> = Layer.effect(
170 ClusterAdapter("exec"),
171 Effect.gen(function* () {
172 const context =
173 yield* Effect.context<ChildProcessSpawner.ChildProcessSpawner>();
174
175 return {
176 kind: "Kubernetes.ClusterAdapter" as const,
177 connect: Effect.fn(function* (connection: Connection) {
178 if (connection.auth.kind !== "exec") {
179 return yield* Effect.die(
180 new Error(
181 `exec adapter received auth kind '${connection.auth.kind}'`,
182 ),
183 );
184 }
185 const exec = connection.auth;
186 const initial = yield* runExecCredential(exec).pipe(
187 Effect.provideContext(context),
188 );
189 return {
190 endpoint: yield* requireEndpoint(connection),
191 certificateAuthorityData: connection.certificateAuthorityData,
192 insecureSkipTlsVerify: connection.insecureSkipTlsVerify,
193 headers: runExecCredential(exec).pipe(
194 Effect.map(toAuthHeaders),
195 Effect.provideContext(context),
196 ),
197 clientCert: initial.clientCert,
198 } satisfies ClusterTransport;
199 }),
200 };
201 }),
202);
203
204/** All built-in adapters, merged for `Kubernetes.providers()`. */
205export const builtinAdapters = (): Layer.Layer<
206 ClusterAdapterService,
207 never,
208 FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
209> =>
210 Layer.mergeAll(
211 KubeConfigAdapter,
212 TokenAdapter,
213 ClientCertAdapter,
214 ExecAdapter,
215 );