1import * as AWS from "@/AWS";
2import * as Kubernetes from "@/Kubernetes";
3import {
4 parseRenderedManifests,
5 renderHelmChart,
6} from "@/Kubernetes/internal/helm.ts";
7import * as Provider from "@/Provider";
8import * as Test from "@/Test/Alchemy";
9import * as NodeServices from "@effect/platform-node/NodeServices";
10import { expect, layer } from "alchemy-test";
11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
13import * as Result from "effect/Result";
14
15const testOptions = {
16 providers: Layer.mergeAll(AWS.providers(), Kubernetes.providers()),
17};
18const { test } = Test.make(testOptions);
19
20// Rendering shells out to the local helm CLI (like Docker for image
21// builds) — `helm` must be installed on the machine running this suite.
22const chartDir = `${import.meta.dirname}/fixtures/chart`;
23
24const describe = layer(NodeServices.layer);
25
26describe("renderHelmChart (local fixture)", (it) => {
27 it.effect(
28 "renders values, release name, and namespace",
29 () =>
30 Effect.gen(function* () {
31 const objects = yield* renderHelmChart({
32 chart: chartDir,
33 releaseName: "probe",
34 namespace: "demo",
35 values: { message: "hello-from-values" },
36 });
37 expect(objects).toHaveLength(1);
38 const configMap = objects[0]! as unknown as {
39 kind: string;
40 metadata: { name: string };
41 data: Record<string, string>;
42 };
43 expect(configMap.kind).toBe("ConfigMap");
44 expect(configMap.metadata.name).toBe("probe-config");
45 expect(configMap.data.message).toBe("hello-from-values");
46 expect(configMap.data.release).toBe("probe");
47 expect(configMap.data.namespace).toBe("demo");
48 }),
49 { tags: ["provider:kubernetes", "provider:kubernetes:helmchart", "local"] },
50 );
51
52 it.effect(
53 "values toggle conditional templates on and off",
54 () =>
55 Effect.gen(function* () {
56 const withoutSecond = yield* renderHelmChart({
57 chart: chartDir,
58 releaseName: "probe",
59 namespace: "demo",
60 });
61 expect(withoutSecond).toHaveLength(1);
62
63 const withSecond = yield* renderHelmChart({
64 chart: chartDir,
65 releaseName: "probe",
66 namespace: "demo",
67 values: { secondConfigMap: { enabled: true } },
68 });
69 expect(withSecond).toHaveLength(2);
70 expect(withSecond.map((object) => object.metadata.name).sort()).toEqual(
71 ["probe-config", "probe-second"],
72 );
73 }),
74 { tags: ["provider:kubernetes", "provider:kubernetes:helmchart", "local"] },
75 );
76
77 // Regression for #1312: the fixture chart ships a `helm.sh/hook: pre-delete`
78 // Job. HelmChart has no Helm release and no hook lifecycle, so the hook
79 // must never enter the managed-object graph (where it would be created and
80 // reconciled like an ordinary workload on every deploy).
81 it.effect(
82 "excludes Helm lifecycle hooks from the render",
83 () =>
84 Effect.gen(function* () {
85 const objects = yield* renderHelmChart({
86 chart: chartDir,
87 releaseName: "probe",
88 namespace: "demo",
89 });
90 expect(objects.map((object) => object.kind)).not.toContain("Job");
91 expect(objects.map((object) => object.metadata.name)).toEqual([
92 "probe-config",
93 ]);
94 }),
95 { tags: ["provider:kubernetes", "provider:kubernetes:helmchart", "local"] },
96 );
97
98 it.effect(
99 "a bad chart reference fails with a typed HelmError",
100 () =>
101 Effect.gen(function* () {
102 const result = yield* Effect.result(
103 renderHelmChart({
104 chart: `${chartDir}-does-not-exist`,
105 releaseName: "probe",
106 namespace: "demo",
107 }),
108 );
109 expect(Result.isFailure(result)).toBe(true);
110 if (Result.isFailure(result)) {
111 expect(result.failure._tag).toBe("HelmError");
112 }
113 }),
114 { tags: ["provider:kubernetes", "provider:kubernetes:helmchart", "local"] },
115 );
116});
117
118describe("parseRenderedManifests", (it) => {
119 it.effect(
120 "ignores Helm OCI pull metadata",
121 () =>
122 Effect.gen(function* () {
123 const objects = yield* parseRenderedManifests(
124 "oci://registry.example.test/charts/example",
125 `Pulled: registry.example.test/charts/example:1.2.3
126Digest: sha256:0123456789abcdef
127---
128apiVersion: v1
129kind: ConfigMap
130metadata:
131 name: example
132`,
133 );
134
135 expect(objects).toHaveLength(1);
136 expect(objects[0]?.kind).toBe("ConfigMap");
137 expect(objects[0]?.metadata.name).toBe("example");
138 }),
139 {
140 tags: [
141 "unit",
142 "provider:kubernetes",
143 "provider:kubernetes:helmchart",
144 "local",
145 ],
146 },
147 );
148
149 it.effect(
150 "excludes helm.sh/hook-annotated objects (#1312)",
151 () =>
152 Effect.gen(function* () {
153 const objects = yield* parseRenderedManifests(
154 "example",
155 `apiVersion: v1
156kind: ConfigMap
157metadata:
158 name: ordinary
159---
160apiVersion: batch/v1
161kind: Job
162metadata:
163 name: uninstall-hook
164 annotations:
165 helm.sh/hook: pre-delete
166---
167apiVersion: v1
168kind: Pod
169metadata:
170 name: smoke-test
171 annotations:
172 "helm.sh/hook": test
173`,
174 );
175
176 expect(objects.map((object) => object.metadata.name)).toEqual([
177 "ordinary",
178 ]);
179 }),
180 {
181 tags: [
182 "unit",
183 "provider:kubernetes",
184 "provider:kubernetes:helmchart",
185 "local",
186 ],
187 },
188 );
189
190 it.effect(
191 "rejects pull-shaped metadata for non-OCI charts",
192 () =>
193 Effect.gen(function* () {
194 const result = yield* Effect.result(
195 parseRenderedManifests(
196 "example",
197 `Pulled: registry.example.test/charts/example:1.2.3
198Digest: sha256:0123456789abcdef
199`,
200 ),
201 );
202
203 expect(Result.isFailure(result)).toBe(true);
204 if (Result.isFailure(result)) {
205 expect(result.failure._tag).toBe("HelmError");
206 }
207 }),
208 {
209 tags: [
210 "unit",
211 "provider:kubernetes",
212 "provider:kubernetes:helmchart",
213 "local",
214 ],
215 },
216 );
217
218 it.effect(
219 "rejects pull metadata that is not the leading OCI preamble",
220 () =>
221 Effect.gen(function* () {
222 const result = yield* Effect.result(
223 parseRenderedManifests(
224 "oci://registry.example.test/charts/example",
225 `apiVersion: v1
226kind: ConfigMap
227metadata:
228 name: example
229---
230Pulled: registry.example.test/charts/example:1.2.3
231Digest: sha256:0123456789abcdef
232`,
233 ),
234 );
235
236 expect(Result.isFailure(result)).toBe(true);
237 if (Result.isFailure(result)) {
238 expect(result.failure._tag).toBe("HelmError");
239 }
240 }),
241 {
242 tags: [
243 "unit",
244 "provider:kubernetes",
245 "provider:kubernetes:helmchart",
246 "local",
247 ],
248 },
249 );
250});
251
252// Ungated probe: chart objects live in-cluster with no cloud-side
253// enumeration attributing them to alchemy, so `list()` is intentionally
254// empty. Proves the provider is registered and its record type-checks; the
255// live apply path rides the gated Deployment E2E cluster
256// (Deployment.test.ts).
257test.provider(
258 "list returns an empty array (in-cluster objects)",
259 () =>
260 Effect.gen(function* () {
261 const provider = yield* Provider.findProvider(Kubernetes.HelmChart);
262 const all = yield* provider.list();
263 expect(Array.isArray(all)).toBe(true);
264 expect(all).toEqual([]);
265 }),
266 {
267 tags: [
268 "provider:aws",
269 "provider:kubernetes",
270 "provider:kubernetes:helmchart",
271 "local",
272 ],
273 },
274);