@alchemy.run/kubernetes 0.0.0 → 2.0.0-beta.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +41 -4
  3. package/lib/BuiltinAdapters.d.ts +23 -0
  4. package/lib/BuiltinAdapters.d.ts.map +1 -0
  5. package/lib/BuiltinAdapters.js +124 -0
  6. package/lib/BuiltinAdapters.js.map +1 -0
  7. package/lib/Client.d.ts +80 -0
  8. package/lib/Client.d.ts.map +1 -0
  9. package/lib/Client.js +296 -0
  10. package/lib/Client.js.map +1 -0
  11. package/lib/ClusterAdapter.d.ts +322 -0
  12. package/lib/ClusterAdapter.d.ts.map +1 -0
  13. package/lib/ClusterAdapter.js +65 -0
  14. package/lib/ClusterAdapter.js.map +1 -0
  15. package/lib/Connection.d.ts +210 -0
  16. package/lib/Connection.d.ts.map +1 -0
  17. package/lib/Connection.js +58 -0
  18. package/lib/Connection.js.map +1 -0
  19. package/lib/Deployment.d.ts +382 -0
  20. package/lib/Deployment.d.ts.map +1 -0
  21. package/lib/Deployment.js +533 -0
  22. package/lib/Deployment.js.map +1 -0
  23. package/lib/HelmChart.d.ts +147 -0
  24. package/lib/HelmChart.d.ts.map +1 -0
  25. package/lib/HelmChart.js +248 -0
  26. package/lib/HelmChart.js.map +1 -0
  27. package/lib/Job.d.ts +314 -0
  28. package/lib/Job.d.ts.map +1 -0
  29. package/lib/Job.js +426 -0
  30. package/lib/Job.js.map +1 -0
  31. package/lib/LocalCluster.d.ts +243 -0
  32. package/lib/LocalCluster.d.ts.map +1 -0
  33. package/lib/LocalCluster.js +359 -0
  34. package/lib/LocalCluster.js.map +1 -0
  35. package/lib/Manifest.d.ts +148 -0
  36. package/lib/Manifest.d.ts.map +1 -0
  37. package/lib/Manifest.js +200 -0
  38. package/lib/Manifest.js.map +1 -0
  39. package/lib/Objects.d.ts +60 -0
  40. package/lib/Objects.d.ts.map +1 -0
  41. package/lib/Objects.js +132 -0
  42. package/lib/Objects.js.map +1 -0
  43. package/lib/Providers.d.ts +24 -0
  44. package/lib/Providers.d.ts.map +1 -0
  45. package/lib/Providers.js +36 -0
  46. package/lib/Providers.js.map +1 -0
  47. package/lib/index.d.ts +10 -0
  48. package/lib/index.d.ts.map +1 -0
  49. package/lib/index.js +10 -0
  50. package/lib/index.js.map +1 -0
  51. package/lib/internal/declared.d.ts +19 -0
  52. package/lib/internal/declared.d.ts.map +1 -0
  53. package/lib/internal/declared.js +435 -0
  54. package/lib/internal/declared.js.map +1 -0
  55. package/lib/internal/helm.d.ts +65 -0
  56. package/lib/internal/helm.d.ts.map +1 -0
  57. package/lib/internal/helm.js +173 -0
  58. package/lib/internal/helm.js.map +1 -0
  59. package/lib/internal/kind.d.ts +17 -0
  60. package/lib/internal/kind.d.ts.map +1 -0
  61. package/lib/internal/kind.js +31 -0
  62. package/lib/internal/kind.js.map +1 -0
  63. package/lib/internal/kubeconfig.d.ts +79 -0
  64. package/lib/internal/kubeconfig.d.ts.map +1 -0
  65. package/lib/internal/kubeconfig.js +222 -0
  66. package/lib/internal/kubeconfig.js.map +1 -0
  67. package/lib/internal/registry.d.ts +44 -0
  68. package/lib/internal/registry.d.ts.map +1 -0
  69. package/lib/internal/registry.js +72 -0
  70. package/lib/internal/registry.js.map +1 -0
  71. package/lib/internal/workload.d.ts +127 -0
  72. package/lib/internal/workload.d.ts.map +1 -0
  73. package/lib/internal/workload.js +411 -0
  74. package/lib/internal/workload.js.map +1 -0
  75. package/package.json +55 -3
  76. package/src/BuiltinAdapters.ts +194 -0
  77. package/src/Client.ts +465 -0
  78. package/src/ClusterAdapter.ts +370 -0
  79. package/src/Connection.ts +234 -0
  80. package/src/Deployment.ts +851 -0
  81. package/src/HelmChart.ts +388 -0
  82. package/src/Job.ts +689 -0
  83. package/src/LocalCluster.ts +631 -0
  84. package/src/Manifest.ts +295 -0
  85. package/src/Objects.ts +213 -0
  86. package/src/Providers.ts +52 -0
  87. package/src/globals.d.ts +2 -0
  88. package/src/index.ts +9 -0
  89. package/src/internal/declared.ts +471 -0
  90. package/src/internal/helm.ts +245 -0
  91. package/src/internal/kind.ts +43 -0
  92. package/src/internal/kubeconfig.ts +364 -0
  93. package/src/internal/registry.ts +108 -0
  94. package/src/internal/workload.ts +503 -0
@@ -0,0 +1,388 @@
1
+ import { hashDirectory } from "alchemy/Command/Memo";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import { createPhysicalName } from "alchemy/PhysicalName";
4
+ import * as Provider from "alchemy/Provider";
5
+ import { Resource } from "alchemy/Resource";
6
+ import { collectRedactedSecrets } from "alchemy/Util/Redaction";
7
+ import { sha256Object } from "alchemy/Util/sha256";
8
+ import * as Effect from "effect/Effect";
9
+ import * as FileSystem from "effect/FileSystem";
10
+ import {
11
+ connectCluster,
12
+ deleteObjects,
13
+ ifClusterExists,
14
+ readObject,
15
+ reconcileObjects,
16
+ resolveKindSpec,
17
+ } from "./Client.ts";
18
+ import type { ClusterTransport } from "./ClusterAdapter.ts";
19
+ import { toConnection, type ClusterLike, type Connection } from "./Connection.ts";
20
+ import { hashDriftSelection } from "./internal/declared.ts";
21
+ import { renderHelmChart } from "./internal/helm.ts";
22
+ import { connectionIdentity, connectionOfOutput, tryConnectionOf } from "./internal/workload.ts";
23
+ import {
24
+ kubernetesObjectKey,
25
+ type KubernetesObjectDefinition,
26
+ type KubernetesObjectRef,
27
+ } from "./Objects.ts";
28
+ import type { Providers } from "./Providers.ts";
29
+
30
+ export interface HelmChartProps {
31
+ /**
32
+ * Target cluster the chart's objects are applied onto. Pass a managed
33
+ * cluster resource (e.g. `AWS.EKS.Cluster`), a
34
+ * `Kubernetes.KubeConfig(...)`, or a raw `Kubernetes.Connection`.
35
+ */
36
+ cluster: ClusterLike;
37
+ /**
38
+ * Chart reference: a repository chart name (used with {@link repo}, e.g.
39
+ * `"ingress-nginx"`), an `oci://` registry reference, or a local chart
40
+ * directory path.
41
+ */
42
+ chart: string;
43
+ /**
44
+ * Classic chart repository URL, e.g.
45
+ * `https://kubernetes.github.io/ingress-nginx`. Not needed for `oci://`
46
+ * references or local chart directories.
47
+ */
48
+ repo?: string;
49
+ /**
50
+ * Chart version to render. Pin this for repository/OCI charts — without
51
+ * it, helm resolves the latest version at each deploy and the rendered
52
+ * objects can drift between runs. Ignored for local chart directories.
53
+ */
54
+ version?: string;
55
+ /**
56
+ * Helm release name the chart's templates render with
57
+ * (`.Release.Name`). If omitted, a deterministic name is derived from
58
+ * the stack, stage, and logical ID.
59
+ */
60
+ releaseName?: string;
61
+ /**
62
+ * Kubernetes namespace the objects are rendered into
63
+ * (`.Release.Namespace`; also injected as `metadata.namespace` on
64
+ * namespaced objects the chart leaves namespace-less). The namespace
65
+ * must already exist unless {@link createNamespace} is set.
66
+ * @default "default"
67
+ */
68
+ namespace?: string;
69
+ /**
70
+ * Values passed to the chart — a literal object, the same shape as a
71
+ * `values.yaml` file.
72
+ */
73
+ values?: Record<string, unknown>;
74
+ /**
75
+ * Render objects from the chart's `crds/` directory too.
76
+ * @default true
77
+ */
78
+ includeCrds?: boolean;
79
+ /**
80
+ * Create the target Namespace and track it while this flag stays on.
81
+ * Turning it off leaves the Namespace in the cluster.
82
+ * @default false
83
+ */
84
+ createNamespace?: boolean;
85
+ }
86
+
87
+ export interface HelmChart extends Resource<
88
+ "Kubernetes.HelmChart",
89
+ HelmChartProps,
90
+ {
91
+ /** The connection of the cluster the chart is applied to. */
92
+ connection: Connection;
93
+ /** The Helm release name the chart rendered with. */
94
+ releaseName: string;
95
+ /** The namespace the chart rendered into. */
96
+ namespace: string;
97
+ /** The chart reference that was rendered. */
98
+ chart: string;
99
+ /** The pinned chart version, when one was declared. */
100
+ version: string | undefined;
101
+ /** References to the applied Kubernetes objects. */
102
+ objects: KubernetesObjectRef[];
103
+ /** Content hash of the chart inputs (and local chart files). */
104
+ code: {
105
+ hash: string;
106
+ };
107
+ /**
108
+ * Set by `read` when an applied object is missing or its fingerprint
109
+ * changed. Reconcile does not persist it; the extra field is what
110
+ * `alchemy drift` compares.
111
+ */
112
+ observedHash?: string;
113
+ },
114
+ {},
115
+ Providers
116
+ > {}
117
+
118
+ /**
119
+ * Renders a Helm chart and converges its objects onto any Kubernetes
120
+ * cluster via server-side apply.
121
+ *
122
+ * The chart is rendered locally with the `helm` CLI (`helm template` —
123
+ * install helm on the deploying machine, like Docker for image builds);
124
+ * the rendered objects then flow through the same apply machinery as
125
+ * `Kubernetes.Manifest`: Alchemy owns the object lifecycle, re-applies the
126
+ * full render whenever the chart's inputs change, prunes objects that drop
127
+ * out of the render, and deletes everything on destroy. There is no
128
+ * in-cluster Helm release record; the target `cluster` can be a managed
129
+ * cluster resource (e.g. `AWS.EKS.Cluster`) or any cluster your kubeconfig
130
+ * can reach.
131
+ *
132
+ * Helm lifecycle hooks (`helm.sh/hook`-annotated objects: install/upgrade/
133
+ * delete hooks, tests) are neither executed nor applied — the chart is
134
+ * rendered with `--no-hooks`, so they never enter the managed-object graph.
135
+ * Charts that depend on hooks for correctness should be installed with Helm
136
+ * directly.
137
+ * ### Installing a Chart
138
+ * **Example:** Chart from a repository
139
+ * ```typescript
140
+ * const ingress = yield* Kubernetes.HelmChart("IngressNginx", {
141
+ * cluster,
142
+ * chart: "ingress-nginx",
143
+ * repo: "https://kubernetes.github.io/ingress-nginx",
144
+ * version: "4.11.2",
145
+ * namespace: "ingress-nginx",
146
+ * createNamespace: true,
147
+ * values: {
148
+ * controller: { replicaCount: 2 },
149
+ * },
150
+ * });
151
+ * ```
152
+ *
153
+ * **Example:** OCI chart
154
+ * ```typescript
155
+ * const karpenter = yield* Kubernetes.HelmChart("Karpenter", {
156
+ * cluster,
157
+ * chart: "oci://public.ecr.aws/karpenter/karpenter",
158
+ * version: "1.0.6",
159
+ * namespace: "kube-system",
160
+ * });
161
+ * ```
162
+ *
163
+ * **Example:** Local chart directory
164
+ * ```typescript
165
+ * const app = yield* Kubernetes.HelmChart("App", {
166
+ * cluster,
167
+ * chart: "./charts/app",
168
+ * values: { image: { tag: "v1.2.3" } },
169
+ * });
170
+ * ```
171
+ *
172
+ * @resource
173
+ * @product Helm
174
+ */
175
+ export const HelmChart = Resource<HelmChart>("Kubernetes.HelmChart", {
176
+ aliases: ["AWS.EKS.HelmChart"],
177
+ });
178
+
179
+ /**
180
+ * Hash the chart identity: every render input, plus the chart directory's
181
+ * content hash when `chart` is a local path (so editing a local chart is
182
+ * visible to `diff` even though no prop changed).
183
+ */
184
+ const computeChartHash = Effect.fn(function* (news: HelmChartProps, releaseName: string) {
185
+ const fs = yield* FileSystem.FileSystem;
186
+ const isLocalDir = !news.chart.startsWith("oci://") && (yield* fs.exists(news.chart));
187
+ return yield* sha256Object({
188
+ chart: news.chart,
189
+ repo: news.repo,
190
+ version: news.version,
191
+ releaseName,
192
+ namespace: news.namespace ?? "default",
193
+ values: news.values,
194
+ includeCrds: news.includeCrds ?? true,
195
+ createNamespace: news.createNamespace ?? false,
196
+ localChart: isLocalDir ? yield* hashDirectory({ cwd: news.chart }) : undefined,
197
+ });
198
+ });
199
+
200
+ const resolveReleaseName = (
201
+ id: string,
202
+ news: HelmChartProps,
203
+ output: HelmChart["Attributes"] | undefined,
204
+ ) =>
205
+ Effect.suspend(() => {
206
+ if (news.releaseName) return Effect.succeed(news.releaseName);
207
+ if (output?.releaseName) return Effect.succeed(output.releaseName);
208
+ // Helm rejects release names longer than 53 characters.
209
+ return createPhysicalName({ id, maxLength: 53, lowercase: true });
210
+ });
211
+
212
+ /**
213
+ * Charts commonly omit `metadata.namespace` and rely on the install
214
+ * namespace. Server-side apply addresses objects by explicit path, so
215
+ * inject the target namespace into namespaced objects that omit it
216
+ * (resolving each kind's scope; cluster-scoped objects pass through).
217
+ */
218
+ const injectNamespace = Effect.fn(function* (
219
+ transport: ClusterTransport,
220
+ objects: ReadonlyArray<KubernetesObjectDefinition>,
221
+ namespace: string,
222
+ ) {
223
+ return yield* Effect.forEach(objects, (object) =>
224
+ Effect.gen(function* () {
225
+ if (object.metadata.namespace !== undefined) return object;
226
+ const spec = yield* resolveKindSpec({ transport, input: object });
227
+ if (spec.scope === "Cluster") return object;
228
+ return {
229
+ ...object,
230
+ metadata: { ...object.metadata, namespace },
231
+ } satisfies KubernetesObjectDefinition;
232
+ }),
233
+ );
234
+ });
235
+
236
+ const chartObjects = Effect.fn(function* (
237
+ transport: ClusterTransport,
238
+ props: HelmChartProps,
239
+ releaseName: string,
240
+ ) {
241
+ const namespace = props.namespace ?? "default";
242
+ const redactions = props.values === undefined ? [] : collectRedactedSecrets(props.values);
243
+ const rendered = yield* renderHelmChart({
244
+ chart: props.chart,
245
+ repo: props.repo,
246
+ version: props.version,
247
+ releaseName,
248
+ namespace,
249
+ values: props.values,
250
+ includeCrds: props.includeCrds,
251
+ });
252
+ const placed = yield* injectNamespace(transport, rendered, namespace);
253
+ const objects =
254
+ props.createNamespace && namespace !== "default"
255
+ ? [
256
+ {
257
+ apiVersion: "v1",
258
+ kind: "Namespace",
259
+ metadata: { name: namespace },
260
+ } satisfies KubernetesObjectDefinition,
261
+ ...placed,
262
+ ]
263
+ : placed;
264
+ return { objects, redactions };
265
+ });
266
+
267
+ export const HelmChartProvider = () =>
268
+ Provider.effect(
269
+ HelmChart,
270
+ Effect.gen(function* () {
271
+ return {
272
+ stables: ["connection", "releaseName", "namespace"],
273
+ // In-cluster objects have no cloud-side enumeration that attributes
274
+ // them to alchemy; refresh happens per-instance through `read`.
275
+ list: () => Effect.succeed([] as HelmChart["Attributes"][]),
276
+ diff: Effect.fn(function* ({ id, olds, news, output }) {
277
+ if (!isResolved(news)) return;
278
+ const releaseName = yield* resolveReleaseName(id, news, output);
279
+ const oldCluster = connectionIdentity(tryConnectionOf(olds?.cluster));
280
+ const newCluster = connectionIdentity(tryConnectionOf(news.cluster));
281
+ // Object identity is the (cluster, release, namespace) triple —
282
+ // moving any of it means a different set of objects.
283
+ if (
284
+ output &&
285
+ ((oldCluster !== undefined && newCluster !== undefined && oldCluster !== newCluster) ||
286
+ output.releaseName !== releaseName ||
287
+ output.namespace !== (news.namespace ?? "default"))
288
+ ) {
289
+ return { action: "replace" } as const;
290
+ }
291
+ if (output) {
292
+ const hash = yield* computeChartHash(news, releaseName);
293
+ if (hash !== output.code.hash) {
294
+ return { action: "update" } as const;
295
+ }
296
+ }
297
+ }),
298
+ reconcile: Effect.fn(function* ({ id, news, olds, output, session }) {
299
+ const releaseName = yield* resolveReleaseName(id, news, output);
300
+ const namespace = news.namespace ?? "default";
301
+ const connection = toConnection(news.cluster);
302
+ const transport = yield* connectCluster(connection);
303
+ const hash = yield* computeChartHash(news, releaseName);
304
+
305
+ yield* session.note(
306
+ `Rendering Helm chart ${news.chart}${news.version ? `@${news.version}` : ""}...`,
307
+ );
308
+ const { objects: desiredObjects, redactions } = yield* chartObjects(
309
+ transport,
310
+ news,
311
+ releaseName,
312
+ );
313
+
314
+ yield* session.note(
315
+ `Applying ${String(desiredObjects.length)} objects from ${news.chart}...`,
316
+ );
317
+ // Turning createNamespace off releases the Namespace. It stays in the cluster.
318
+ const previousObjects = (output?.objects ?? []).filter(
319
+ (object) =>
320
+ !(
321
+ olds?.createNamespace === true &&
322
+ news.createNamespace !== true &&
323
+ object.kind === "Namespace" &&
324
+ object.name === namespace
325
+ ),
326
+ );
327
+ const objects = yield* reconcileObjects({
328
+ transport,
329
+ previousObjects,
330
+ desiredObjects,
331
+ redactions,
332
+ });
333
+
334
+ return {
335
+ connection,
336
+ releaseName,
337
+ namespace,
338
+ chart: news.chart,
339
+ version: news.version,
340
+ objects: [...objects],
341
+ code: { hash },
342
+ };
343
+ }),
344
+ read: Effect.fn(function* ({ output }) {
345
+ if (!output) return undefined;
346
+ const connection = connectionOfOutput(output);
347
+ if (!connection) return undefined;
348
+ // ClusterNotFound fails the drift row. undefined means an object is gone.
349
+ // GETs only: a dry-run PATCH needs `patch`, and re-rendering a chart
350
+ // rerolls randAlphaNum into a false drift.
351
+ const transport = yield* connectCluster(connection);
352
+ const notes = yield* Effect.forEach(
353
+ output.objects,
354
+ (object) =>
355
+ Effect.gen(function* () {
356
+ const key = kubernetesObjectKey(object);
357
+ const observed = yield* readObject({ transport, object }).pipe(
358
+ Effect.catchTag("KubernetesNotFound", () => Effect.succeed(undefined)),
359
+ );
360
+ if (!observed) return `missing:${key}`;
361
+ // Older rows stored no selection. Re-rendering the chart is
362
+ // not a read, so only a missing object is drift until apply
363
+ // writes a mask.
364
+ if (object.driftMask === undefined || object.baselineHash === undefined) {
365
+ return undefined;
366
+ }
367
+ const liveHash = yield* hashDriftSelection(object.driftMask, observed);
368
+ return liveHash === object.baselineHash ? undefined : liveHash;
369
+ }),
370
+ { concurrency: 8 },
371
+ );
372
+ const drifted = notes.filter((note): note is string => note !== undefined);
373
+ if (drifted.length === 0) return output;
374
+ return {
375
+ ...output,
376
+ observedHash: yield* sha256Object({ notes: drifted }),
377
+ };
378
+ }),
379
+ delete: Effect.fn(function* ({ output }) {
380
+ const connection = connectionOfOutput(output);
381
+ if (!connection) return;
382
+ const transport = yield* ifClusterExists(connectCluster(connection));
383
+ if (!transport) return;
384
+ yield* deleteObjects({ transport, objects: output.objects });
385
+ }),
386
+ };
387
+ }),
388
+ );