1import * as Kubernetes from "@/Kubernetes";
2import { connectCluster } from "@/Kubernetes/internal/client.ts";
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { expect, layer } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Path from "effect/Path";
9
10// The kubeconfig adapter resolved against a REAL kubeconfig file on disk,
11// including a REAL exec credential plugin invocation (`echo` prints the
12// ExecCredential JSON) — the same path `aws eks get-token` / `kubelogin` /
13// `gke-gcloud-auth-plugin` contexts take.
14const describe = layer(
15 Layer.provideMerge(Kubernetes.KubeConfigAdapter, NodeServices.layer),
16);
17
18const CA_PEM = "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n";
19
20const execCredential = JSON.stringify({
21 apiVersion: "client.authentication.k8s.io/v1",
22 kind: "ExecCredential",
23 status: { token: "exec-minted-token" },
24});
25
26const kubeconfig = `
27apiVersion: v1
28kind: Config
29current-context: token-ctx
30clusters:
31 - name: token-cluster
32 cluster:
33 server: https://token.example:6443
34 certificate-authority-data: ${Buffer.from(CA_PEM).toString("base64")}
35 - name: exec-cluster
36 cluster:
37 server: https://exec.example:6443
38 insecure-skip-tls-verify: true
39users:
40 - name: token-user
41 user:
42 token: static-test-token
43 - name: exec-user
44 user:
45 exec:
46 apiVersion: client.authentication.k8s.io/v1
47 command: echo
48 args:
49 - '${execCredential}'
50contexts:
51 - name: token-ctx
52 context:
53 cluster: token-cluster
54 user: token-user
55 - name: exec-ctx
56 context:
57 cluster: exec-cluster
58 user: exec-user
59`;
60
61const writeKubeconfig = Effect.gen(function* () {
62 const fs = yield* FileSystem.FileSystem;
63 const path = yield* Path.Path;
64 const dir = yield* fs.makeTempDirectory({ prefix: "alchemy-kubeconfig-" });
65 const file = path.join(dir, "config");
66 yield* fs.writeFileString(file, kubeconfig);
67 return file;
68});
69
70describe("Kubernetes.KubeConfig", (it) => {
71 it.effect(
72 "resolves the current-context's cluster and static token",
73 () =>
74 Effect.gen(function* () {
75 const file = yield* writeKubeconfig;
76 const transport = yield* connectCluster(
77 Kubernetes.KubeConfig({ path: file }),
78 );
79 expect(transport.endpoint).toBe("https://token.example:6443");
80 expect(
81 Buffer.from(transport.certificateAuthorityData!, "base64").toString(
82 "utf8",
83 ),
84 ).toBe(CA_PEM);
85 const headers = yield* transport.headers;
86 expect(headers.Authorization).toBe("Bearer static-test-token");
87 }),
88 { tags: ["unit", "provider:kubernetes", "local"] },
89 );
90
91 it.effect(
92 "mints credentials through an exec plugin context",
93 () =>
94 Effect.gen(function* () {
95 const file = yield* writeKubeconfig;
96 const transport = yield* connectCluster(
97 Kubernetes.KubeConfig({ path: file, context: "exec-ctx" }),
98 );
99 expect(transport.endpoint).toBe("https://exec.example:6443");
100 expect(transport.insecureSkipTlsVerify).toBe(true);
101 const headers = yield* transport.headers;
102 expect(headers.Authorization).toBe("Bearer exec-minted-token");
103 }),
104 { tags: ["unit", "provider:kubernetes", "local"] },
105 );
106
107 it.effect(
108 "an unknown context fails with a typed KubeConfigError",
109 () =>
110 Effect.gen(function* () {
111 const file = yield* writeKubeconfig;
112 const result = yield* Effect.result(
113 connectCluster(
114 Kubernetes.KubeConfig({ path: file, context: "missing" }),
115 ),
116 );
117 expect(result._tag).toBe("Failure");
118 }),
119 { tags: ["unit", "provider:kubernetes", "local"] },
120 );
121});