@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,295 @@
1
+ import { isResolved } from "alchemy/Diff";
2
+ import * as Provider from "alchemy/Provider";
3
+ import { Resource } from "alchemy/Resource";
4
+ import { isPlainObject } from "alchemy/Util/data";
5
+ import * as Effect from "effect/Effect";
6
+ import {
7
+ applyObject,
8
+ connectCluster,
9
+ deleteObject,
10
+ ifClusterExists,
11
+ readObject,
12
+ } from "./Client.ts";
13
+ import { toConnection, type ClusterLike, type Connection } from "./Connection.ts";
14
+ import { driftMask, hashDriftSelection } from "./internal/declared.ts";
15
+ import { connectionIdentity, connectionOfOutput, tryConnectionOf } from "./internal/workload.ts";
16
+ import type { KubernetesObjectDefinition, KubernetesObjectRef } from "./Objects.ts";
17
+ import type { Providers } from "./Providers.ts";
18
+
19
+ /**
20
+ * A literal Kubernetes object: `apiVersion` + `kind` + `metadata`, with the
21
+ * rest of the object's fields (`spec`, `data`, …) carried as-is. Any kind is
22
+ * accepted — built-in objects and CRDs alike; the API server validates the
23
+ * shape on apply.
24
+ */
25
+ export interface KubernetesManifest {
26
+ apiVersion: string;
27
+ kind: string;
28
+ metadata?: {
29
+ name?: string;
30
+ namespace?: string;
31
+ labels?: Record<string, string>;
32
+ annotations?: Record<string, string>;
33
+ [key: string]: unknown;
34
+ };
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ export interface ManifestProps {
39
+ /**
40
+ * Target cluster the manifest is applied onto. Pass a managed cluster
41
+ * resource (e.g. `AWS.EKS.Cluster`), a `Kubernetes.KubeConfig(...)`, or
42
+ * a raw `Kubernetes.Connection`.
43
+ */
44
+ cluster: ClusterLike;
45
+ /**
46
+ * The Kubernetes object to apply (server-side apply, field manager
47
+ * `alchemy`) — a literal object with `apiVersion`, `kind`, `metadata`, and
48
+ * the kind's own fields. Arbitrary CRDs are supported via API discovery.
49
+ */
50
+ manifest: KubernetesManifest;
51
+ }
52
+
53
+ export interface Manifest extends Resource<
54
+ "Kubernetes.Manifest",
55
+ ManifestProps,
56
+ {
57
+ /** The connection of the cluster the object is applied to. */
58
+ connection: Connection;
59
+ /** The Kubernetes API version of the applied object. */
60
+ apiVersion: string;
61
+ /** The Kubernetes kind of the applied object. */
62
+ kind: string;
63
+ /** The name of the applied object. */
64
+ name: string;
65
+ /** The namespace of the applied object (`undefined` for cluster-scoped kinds). */
66
+ namespace: string | undefined;
67
+ /** Reference to the applied Kubernetes object. */
68
+ ref: KubernetesObjectRef;
69
+ /** The server-assigned UID of the applied object, when returned. */
70
+ uid: string | undefined;
71
+ /**
72
+ * Fingerprint of the declared fields last applied. `read` compares a GET
73
+ * projected through {@link driftMask} to this.
74
+ */
75
+ baselineHash?: string;
76
+ /**
77
+ * Declared-field mask from the last apply. Older state omits it and is
78
+ * compared with the same selection against the stored manifest.
79
+ */
80
+ driftMask?: unknown;
81
+ /**
82
+ * Set by `read` when the live object diverges. Reconcile does not persist
83
+ * it; the extra field is what `alchemy drift` compares.
84
+ */
85
+ observedHash?: string;
86
+ },
87
+ {},
88
+ Providers
89
+ > {}
90
+
91
+ /**
92
+ * Applies a raw Kubernetes manifest onto any cluster via server-side
93
+ * apply.
94
+ *
95
+ * Any literal object is accepted — built-in kinds and custom resources
96
+ * alike; unknown kinds are resolved through the Kubernetes API discovery
97
+ * endpoint, so CRDs work without any registration. The target `cluster`
98
+ * can be a managed cluster resource (e.g. `AWS.EKS.Cluster`) or any
99
+ * cluster your kubeconfig can reach (`Kubernetes.KubeConfig(...)`).
100
+ * ### Applying Manifests
101
+ * **Example:** StatefulSet
102
+ * ```typescript
103
+ * const sts = yield* Kubernetes.Manifest("Cache", {
104
+ * cluster,
105
+ * manifest: {
106
+ * apiVersion: "apps/v1",
107
+ * kind: "StatefulSet",
108
+ * metadata: { name: "cache", namespace: "apps" },
109
+ * spec: {
110
+ * serviceName: "cache",
111
+ * replicas: 3,
112
+ * selector: { matchLabels: { app: "cache" } },
113
+ * template: {
114
+ * metadata: { labels: { app: "cache" } },
115
+ * spec: { containers: [{ name: "redis", image: "redis:7" }] },
116
+ * },
117
+ * },
118
+ * },
119
+ * });
120
+ * ```
121
+ *
122
+ * **Example:** Custom resource (CRD)
123
+ * ```typescript
124
+ * const widget = yield* Kubernetes.Manifest("Widget", {
125
+ * cluster,
126
+ * manifest: {
127
+ * apiVersion: "acme.io/v1",
128
+ * kind: "Widget",
129
+ * metadata: { name: "w", namespace: "default" },
130
+ * spec: { size: 3 },
131
+ * },
132
+ * });
133
+ * ```
134
+ *
135
+ * ### Namespaces
136
+ * **Example:** Create a Namespace
137
+ * ```typescript
138
+ * const ns = yield* Kubernetes.Manifest("AppsNamespace", {
139
+ * cluster,
140
+ * manifest: {
141
+ * apiVersion: "v1",
142
+ * kind: "Namespace",
143
+ * metadata: { name: "apps" },
144
+ * },
145
+ * });
146
+ * ```
147
+ *
148
+ * ### Any Cluster
149
+ * **Example:** Apply onto a kubeconfig context
150
+ * ```typescript
151
+ * const local = Kubernetes.KubeConfig({ context: "kind-dev" });
152
+ *
153
+ * const config = yield* Kubernetes.Manifest("AppConfig", {
154
+ * cluster: local,
155
+ * manifest: {
156
+ * apiVersion: "v1",
157
+ * kind: "ConfigMap",
158
+ * metadata: { name: "app-config", namespace: "default" },
159
+ * data: { LOG_LEVEL: "info" },
160
+ * },
161
+ * });
162
+ * ```
163
+ *
164
+ * @resource
165
+ * @product Manifest
166
+ */
167
+ export const Manifest = Resource<Manifest>("Kubernetes.Manifest", {
168
+ aliases: ["AWS.EKS.Manifest"],
169
+ });
170
+
171
+ const toObjectDefinition = (
172
+ manifest: KubernetesManifest,
173
+ ): Effect.Effect<KubernetesObjectDefinition, Error> => {
174
+ const name = manifest.metadata?.name;
175
+ if (!name) {
176
+ return Effect.fail(
177
+ new Error(
178
+ `Kubernetes.Manifest requires manifest.metadata.name (got ${manifest.apiVersion}/${manifest.kind})`,
179
+ ),
180
+ );
181
+ }
182
+ return Effect.succeed(manifest as KubernetesObjectDefinition);
183
+ };
184
+
185
+ export const ManifestProvider = () =>
186
+ Provider.effect(
187
+ Manifest,
188
+ Effect.gen(function* () {
189
+ return {
190
+ stables: ["connection", "apiVersion", "kind", "name", "namespace"],
191
+ // In-cluster objects have no cloud-side enumeration that attributes
192
+ // them to alchemy; refresh happens per-instance through `read`.
193
+ list: () => Effect.succeed([] as Manifest["Attributes"][]),
194
+ diff: Effect.fn(function* ({ olds = {} as ManifestProps, news }) {
195
+ if (!isResolved(news)) return;
196
+ const oldManifest = olds.manifest as KubernetesManifest | undefined;
197
+ const newManifest = news.manifest as KubernetesManifest;
198
+ const oldCluster = connectionIdentity(tryConnectionOf(olds.cluster));
199
+ const newCluster = connectionIdentity(tryConnectionOf(news.cluster));
200
+ // Object identity (cluster, group/version/kind, name, namespace) is
201
+ // immutable — changing any of it is a replacement.
202
+ if (
203
+ oldManifest &&
204
+ ((oldCluster !== undefined && newCluster !== undefined && oldCluster !== newCluster) ||
205
+ oldManifest.apiVersion !== newManifest.apiVersion ||
206
+ oldManifest.kind !== newManifest.kind ||
207
+ oldManifest.metadata?.name !== newManifest.metadata?.name ||
208
+ oldManifest.metadata?.namespace !== newManifest.metadata?.namespace)
209
+ ) {
210
+ return { action: "replace" } as const;
211
+ }
212
+ }),
213
+ read: Effect.fn(function* ({ output, olds }) {
214
+ if (!output) return undefined;
215
+ const connection = connectionOfOutput(output);
216
+ if (!connection) return undefined;
217
+ // ClusterNotFound fails the drift row. undefined means this object is gone.
218
+ const transport = yield* connectCluster(connection);
219
+ const observed = yield* readObject({
220
+ transport,
221
+ object: output.ref,
222
+ }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.succeed(undefined)));
223
+ if (!observed) return undefined;
224
+ const uid = (observed as { metadata?: { uid?: string } }).metadata?.uid;
225
+ const attrs = { ...output, uid };
226
+ const manifest = olds?.manifest;
227
+ // A stored mask is the apply-time selection. Older rows have the
228
+ // manifest and no mask; they use that same selection, not the
229
+ // previous whole-object hash.
230
+ const desired =
231
+ output.driftMask === undefined && manifest !== undefined
232
+ ? yield* toObjectDefinition(manifest)
233
+ : undefined;
234
+ const mask = output.driftMask ?? (desired === undefined ? undefined : driftMask(desired));
235
+ if (mask === undefined) return attrs;
236
+ const liveHash = yield* hashDriftSelection(mask, observed);
237
+ const baseline =
238
+ output.driftMask !== undefined && output.baselineHash !== undefined
239
+ ? output.baselineHash
240
+ : desired === undefined
241
+ ? undefined
242
+ : yield* hashDriftSelection(mask, desired);
243
+ if (baseline !== undefined && liveHash !== baseline) {
244
+ return { ...attrs, observedHash: liveHash };
245
+ }
246
+ return attrs;
247
+ }),
248
+ reconcile: Effect.fn(function* ({ news, output, session }) {
249
+ const connection = toConnection(news.cluster);
250
+ const transport = yield* connectCluster(connection);
251
+ const object = yield* toObjectDefinition(news.manifest);
252
+ const ref: KubernetesObjectRef = {
253
+ apiVersion: object.apiVersion,
254
+ kind: object.kind,
255
+ name: object.metadata.name,
256
+ namespace: object.metadata.namespace,
257
+ };
258
+
259
+ // Server-side apply is a true upsert: create-if-missing and
260
+ // converge-if-present in one call, `force: true` so alchemy owns
261
+ // the fields it manages regardless of prior managers.
262
+ const applied = yield* applyObject({ transport, object });
263
+ const witnessed = isPlainObject(applied) ? applied : undefined;
264
+ const baselineHash = yield* hashDriftSelection(object, witnessed ?? object);
265
+
266
+ yield* session.note(
267
+ `Applied ${ref.apiVersion}/${ref.kind} ${ref.namespace ? `${ref.namespace}/` : ""}${ref.name}`,
268
+ );
269
+
270
+ const uid = (applied as { metadata?: { uid?: string } })?.metadata?.uid ?? output?.uid;
271
+
272
+ return {
273
+ connection,
274
+ apiVersion: ref.apiVersion,
275
+ kind: ref.kind,
276
+ name: ref.name,
277
+ namespace: ref.namespace,
278
+ ref,
279
+ uid,
280
+ baselineHash,
281
+ driftMask: driftMask(object, witnessed),
282
+ };
283
+ }),
284
+ delete: Effect.fn(function* ({ output }) {
285
+ const connection = connectionOfOutput(output);
286
+ if (!connection) return;
287
+ const transport = yield* ifClusterExists(connectCluster(connection));
288
+ if (!transport) return;
289
+ // 404 (including a removed CRD's discovery 404) is already success.
290
+ // 403/5xx must surface: the object is still there.
291
+ yield* deleteObject({ transport, object: output.ref });
292
+ }),
293
+ };
294
+ }),
295
+ );
package/src/Objects.ts ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Internal Kubernetes object model + ordering/path helpers powering
3
+ * `Kubernetes.Manifest` and the workload platforms (`Deployment`, `Job`).
4
+ * Not part of the public surface — the public manifest shape is the literal
5
+ * `KubernetesManifest` on `Kubernetes.Manifest`.
6
+ */
7
+
8
+ export interface KubernetesObjectMetadata {
9
+ name: string;
10
+ namespace?: string;
11
+ labels?: Record<string, string>;
12
+ annotations?: Record<string, string>;
13
+ }
14
+
15
+ export type KubernetesObjectDefinition = {
16
+ apiVersion: string;
17
+ kind: string;
18
+ metadata: KubernetesObjectMetadata;
19
+ } & Record<string, unknown>;
20
+
21
+ export interface KubernetesObjectRef {
22
+ apiVersion: string;
23
+ kind: string;
24
+ name: string;
25
+ namespace?: string;
26
+ /**
27
+ * Fingerprint of the declared fields last applied. Reads compare a GET
28
+ * projected through {@link driftMask} to this.
29
+ */
30
+ baselineHash?: string;
31
+ /**
32
+ * Declared-field mask from the last apply. Absent on state written before
33
+ * selections existed; those rows still detect a deleted object.
34
+ */
35
+ driftMask?: unknown;
36
+ }
37
+
38
+ export interface KubernetesObjectBinding {
39
+ type: "kubernetes-object";
40
+ object: KubernetesObjectDefinition;
41
+ }
42
+
43
+ export type KubernetesObjectScope = "Cluster" | "Namespaced";
44
+
45
+ export interface KubernetesObjectKindSpec {
46
+ plural: string;
47
+ scope: KubernetesObjectScope;
48
+ applyRank: number;
49
+ }
50
+
51
+ /** Apply rank for kinds not in the static table (applied last, deleted first). */
52
+ export const DEFAULT_APPLY_RANK = 100;
53
+
54
+ const supportedKinds: Record<string, KubernetesObjectKindSpec> = {
55
+ "v1/Namespace": {
56
+ plural: "namespaces",
57
+ scope: "Cluster",
58
+ applyRank: 10,
59
+ },
60
+ // CRDs apply right after namespaces so a chart's custom resources
61
+ // (default rank 100) always find their definition registered.
62
+ "apiextensions.k8s.io/v1/CustomResourceDefinition": {
63
+ plural: "customresourcedefinitions",
64
+ scope: "Cluster",
65
+ applyRank: 15,
66
+ },
67
+ "v1/ServiceAccount": {
68
+ plural: "serviceaccounts",
69
+ scope: "Namespaced",
70
+ applyRank: 20,
71
+ },
72
+ "v1/ConfigMap": {
73
+ plural: "configmaps",
74
+ scope: "Namespaced",
75
+ applyRank: 30,
76
+ },
77
+ "v1/Secret": {
78
+ plural: "secrets",
79
+ scope: "Namespaced",
80
+ applyRank: 30,
81
+ },
82
+ "v1/Service": {
83
+ plural: "services",
84
+ scope: "Namespaced",
85
+ applyRank: 40,
86
+ },
87
+ "apps/v1/Deployment": {
88
+ plural: "deployments",
89
+ scope: "Namespaced",
90
+ applyRank: 50,
91
+ },
92
+ "apps/v1/StatefulSet": {
93
+ plural: "statefulsets",
94
+ scope: "Namespaced",
95
+ applyRank: 50,
96
+ },
97
+ "apps/v1/DaemonSet": {
98
+ plural: "daemonsets",
99
+ scope: "Namespaced",
100
+ applyRank: 50,
101
+ },
102
+ "batch/v1/Job": {
103
+ plural: "jobs",
104
+ scope: "Namespaced",
105
+ applyRank: 60,
106
+ },
107
+ "batch/v1/CronJob": {
108
+ plural: "cronjobs",
109
+ scope: "Namespaced",
110
+ applyRank: 60,
111
+ },
112
+ "v1/Pod": {
113
+ plural: "pods",
114
+ scope: "Namespaced",
115
+ applyRank: 60,
116
+ },
117
+ };
118
+
119
+ const objectTypeKey = (input: Pick<KubernetesObjectRef, "apiVersion" | "kind">) =>
120
+ `${input.apiVersion}/${input.kind}`;
121
+
122
+ /** Look up the static kind table; `undefined` for kinds needing discovery. */
123
+ export const lookupKubernetesKindSpec = (
124
+ input: Pick<KubernetesObjectRef, "apiVersion" | "kind">,
125
+ ): KubernetesObjectKindSpec | undefined => supportedKinds[objectTypeKey(input)];
126
+
127
+ export const getKubernetesKindSpec = (input: Pick<KubernetesObjectRef, "apiVersion" | "kind">) => {
128
+ const spec = lookupKubernetesKindSpec(input);
129
+ if (!spec) {
130
+ throw new Error(`Unsupported Kubernetes object ${input.apiVersion}/${input.kind}`);
131
+ }
132
+ return spec;
133
+ };
134
+
135
+ const applyRankOf = (input: Pick<KubernetesObjectRef, "apiVersion" | "kind">) =>
136
+ lookupKubernetesKindSpec(input)?.applyRank ?? DEFAULT_APPLY_RANK;
137
+
138
+ export const toKubernetesObjectRef = (object: KubernetesObjectDefinition): KubernetesObjectRef => ({
139
+ apiVersion: object.apiVersion,
140
+ kind: object.kind,
141
+ name: object.metadata.name,
142
+ namespace: object.metadata.namespace,
143
+ });
144
+
145
+ export const kubernetesObjectKey = (
146
+ input: Pick<KubernetesObjectRef, "apiVersion" | "kind" | "name" | "namespace">,
147
+ ) => [input.apiVersion, input.kind, input.namespace ?? "_cluster", input.name].join("/");
148
+
149
+ const compareRefs = (a: KubernetesObjectRef, b: KubernetesObjectRef) =>
150
+ kubernetesObjectKey(a).localeCompare(kubernetesObjectKey(b));
151
+
152
+ export const sortObjectsForApply = (objects: ReadonlyArray<KubernetesObjectDefinition>) =>
153
+ [...objects].sort(
154
+ (a, b) =>
155
+ applyRankOf(a) - applyRankOf(b) ||
156
+ compareRefs(toKubernetesObjectRef(a), toKubernetesObjectRef(b)),
157
+ );
158
+
159
+ export const sortRefsForDelete = (objects: ReadonlyArray<KubernetesObjectRef>) =>
160
+ [...objects].sort((a, b) => applyRankOf(b) - applyRankOf(a) || compareRefs(a, b));
161
+
162
+ export const chunkByApplyRank = (objects: ReadonlyArray<KubernetesObjectDefinition>) => {
163
+ const chunks: KubernetesObjectDefinition[][] = [];
164
+
165
+ for (const object of sortObjectsForApply(objects)) {
166
+ const rank = applyRankOf(object);
167
+ const current = chunks[chunks.length - 1];
168
+ if (!current) {
169
+ chunks.push([object]);
170
+ continue;
171
+ }
172
+
173
+ const currentRank = applyRankOf(current[0]);
174
+ if (currentRank === rank) {
175
+ current.push(object);
176
+ } else {
177
+ chunks.push([object]);
178
+ }
179
+ }
180
+
181
+ return chunks;
182
+ };
183
+
184
+ /**
185
+ * Build the REST path for an object given its (statically-known or
186
+ * discovered) kind spec.
187
+ */
188
+ export const buildKubernetesObjectPathWithSpec = (
189
+ input: Pick<KubernetesObjectRef, "apiVersion" | "kind" | "name" | "namespace">,
190
+ spec: KubernetesObjectKindSpec,
191
+ ) => {
192
+ const [group, version] = input.apiVersion.includes("/")
193
+ ? input.apiVersion.split("/", 2)
194
+ : [undefined, input.apiVersion];
195
+
196
+ const base = group ? `/apis/${group}/${version}` : `/api/${version}`;
197
+
198
+ if (spec.scope === "Namespaced") {
199
+ if (!input.namespace) {
200
+ throw new Error(
201
+ `Kubernetes object ${input.apiVersion}/${input.kind}/${input.name} requires a namespace`,
202
+ );
203
+ }
204
+
205
+ return `${base}/namespaces/${input.namespace}/${spec.plural}/${input.name}`;
206
+ }
207
+
208
+ return `${base}/${spec.plural}/${input.name}`;
209
+ };
210
+
211
+ export const buildKubernetesObjectPath = (
212
+ input: Pick<KubernetesObjectRef, "apiVersion" | "kind" | "name" | "namespace">,
213
+ ) => buildKubernetesObjectPathWithSpec(input, getKubernetesKindSpec(input));
@@ -0,0 +1,52 @@
1
+ import { DockerLive } from "@alchemy.run/docker/Docker";
2
+ import * as Provider from "alchemy/Provider";
3
+ import * as Layer from "effect/Layer";
4
+ import { builtinAdapters } from "./BuiltinAdapters.ts";
5
+ import { Deployment, DeploymentProvider } from "./Deployment.ts";
6
+ import { HelmChart, HelmChartProvider } from "./HelmChart.ts";
7
+ import { Job, JobProvider } from "./Job.ts";
8
+ import { LocalCluster, LocalClusterProvider } from "./LocalCluster.ts";
9
+ import { Manifest, ManifestProvider } from "./Manifest.ts";
10
+
11
+ export class Providers extends Provider.ProviderCollection<Providers>()("Kubernetes") {}
12
+
13
+ /**
14
+ * The Kubernetes provider layer: the cluster-agnostic workload providers
15
+ * (`Deployment`, `Job`, `Manifest`, `HelmChart`), `LocalCluster`, and the
16
+ * built-in cluster adapters (`kubeconfig`, `token`, `client-cert`, `exec`).
17
+ *
18
+ * Managed-cloud clusters need their platform's adapter alongside — e.g.
19
+ * targeting an `AWS.EKS.Cluster` requires `AWS.providers()` in the same
20
+ * stack:
21
+ *
22
+ * ```ts
23
+ * const stack = Alchemy.Stack("app", {
24
+ * providers: Layer.mergeAll(AWS.providers(), Kubernetes.providers()),
25
+ * state: AWS.state(),
26
+ * });
27
+ * ```
28
+ */
29
+ export const providers = () =>
30
+ Layer.effect(
31
+ Providers,
32
+ Provider.collection([Deployment, HelmChart, Job, LocalCluster, Manifest]),
33
+ ).pipe(
34
+ Layer.provide(
35
+ Layer.mergeAll(
36
+ DeploymentProvider(),
37
+ HelmChartProvider(),
38
+ JobProvider(),
39
+ LocalClusterProvider(),
40
+ ManifestProvider(),
41
+ ).pipe(
42
+ // Workloads build `main` / `context` images with the Docker CLI for
43
+ // connections that declare a `registry`.
44
+ Layer.provide(DockerLive),
45
+ ),
46
+ ),
47
+ // The built-in adapters are provideMerged (not just provided) so the
48
+ // workloads' dynamic `findClusterAdapter` lookups see them in the
49
+ // ambient stack context.
50
+ Layer.provideMerge(builtinAdapters()),
51
+ Layer.orDie,
52
+ );
@@ -0,0 +1,2 @@
1
+ // `globalThis.__ALCHEMY_RUNTIME__`, declared by alchemy's Phase module.
2
+ import type {} from "alchemy/Phase";
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./BuiltinAdapters.ts";
2
+ export * from "./ClusterAdapter.ts";
3
+ export * from "./Connection.ts";
4
+ export * from "./Deployment.ts";
5
+ export * from "./HelmChart.ts";
6
+ export * from "./Job.ts";
7
+ export * from "./LocalCluster.ts";
8
+ export * from "./Manifest.ts";
9
+ export * from "./Providers.ts";