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

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 +43 -0
  4. package/lib/BuiltinAdapters.d.ts.map +1 -0
  5. package/lib/BuiltinAdapters.js +129 -0
  6. package/lib/BuiltinAdapters.js.map +1 -0
  7. package/lib/Client.d.ts +97 -0
  8. package/lib/Client.d.ts.map +1 -0
  9. package/lib/Client.js +312 -0
  10. package/lib/Client.js.map +1 -0
  11. package/lib/ClusterAdapter.d.ts +324 -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 +401 -0
  20. package/lib/Deployment.d.ts.map +1 -0
  21. package/lib/Deployment.js +550 -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 +333 -0
  28. package/lib/Job.d.ts.map +1 -0
  29. package/lib/Job.js +443 -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 +157 -0
  36. package/lib/Manifest.d.ts.map +1 -0
  37. package/lib/Manifest.js +205 -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 +202 -0
  77. package/src/Client.ts +496 -0
  78. package/src/ClusterAdapter.ts +373 -0
  79. package/src/Connection.ts +234 -0
  80. package/src/Deployment.ts +872 -0
  81. package/src/HelmChart.ts +388 -0
  82. package/src/Job.ts +709 -0
  83. package/src/LocalCluster.ts +631 -0
  84. package/src/Manifest.ts +301 -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 +504 -0
@@ -0,0 +1,301 @@
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 Data from "effect/Data";
6
+ import * as Effect from "effect/Effect";
7
+ import {
8
+ applyObject,
9
+ connectCluster,
10
+ deleteObject,
11
+ ifClusterExists,
12
+ readObject,
13
+ } from "./Client.ts";
14
+ import { toConnection, type ClusterLike, type Connection } from "./Connection.ts";
15
+ import { driftMask, hashDriftSelection } from "./internal/declared.ts";
16
+ import { connectionIdentity, connectionOfOutput, tryConnectionOf } from "./internal/workload.ts";
17
+ import type { KubernetesObjectDefinition, KubernetesObjectRef } from "./Objects.ts";
18
+ import type { Providers } from "./Providers.ts";
19
+
20
+ export class ManifestInvalidProps extends Data.TaggedError("ManifestInvalidProps")<{
21
+ readonly message: string;
22
+ readonly cause?: unknown;
23
+ }> {}
24
+
25
+ /**
26
+ * A literal Kubernetes object: `apiVersion` + `kind` + `metadata`, with the
27
+ * rest of the object's fields (`spec`, `data`, …) carried as-is. Any kind is
28
+ * accepted — built-in objects and CRDs alike; the API server validates the
29
+ * shape on apply.
30
+ */
31
+ export interface KubernetesManifest {
32
+ apiVersion: string;
33
+ kind: string;
34
+ metadata?: {
35
+ name?: string;
36
+ namespace?: string;
37
+ labels?: Record<string, string>;
38
+ annotations?: Record<string, string>;
39
+ [key: string]: unknown;
40
+ };
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export interface ManifestProps {
45
+ /**
46
+ * Target cluster the manifest is applied onto. Pass a managed cluster
47
+ * resource (e.g. `AWS.EKS.Cluster`), a `Kubernetes.KubeConfig(...)`, or
48
+ * a raw `Kubernetes.Connection`.
49
+ */
50
+ cluster: ClusterLike;
51
+ /**
52
+ * The Kubernetes object to apply (server-side apply, field manager
53
+ * `alchemy`) — a literal object with `apiVersion`, `kind`, `metadata`, and
54
+ * the kind's own fields. Arbitrary CRDs are supported via API discovery.
55
+ */
56
+ manifest: KubernetesManifest;
57
+ }
58
+
59
+ export interface Manifest extends Resource<
60
+ "Kubernetes.Manifest",
61
+ ManifestProps,
62
+ {
63
+ /** The connection of the cluster the object is applied to. */
64
+ connection: Connection;
65
+ /** The Kubernetes API version of the applied object. */
66
+ apiVersion: string;
67
+ /** The Kubernetes kind of the applied object. */
68
+ kind: string;
69
+ /** The name of the applied object. */
70
+ name: string;
71
+ /** The namespace of the applied object (`undefined` for cluster-scoped kinds). */
72
+ namespace: string | undefined;
73
+ /** Reference to the applied Kubernetes object. */
74
+ ref: KubernetesObjectRef;
75
+ /** The server-assigned UID of the applied object, when returned. */
76
+ uid: string | undefined;
77
+ /**
78
+ * Fingerprint of the declared fields last applied. `read` compares a GET
79
+ * projected through {@link driftMask} to this.
80
+ */
81
+ baselineHash?: string;
82
+ /**
83
+ * Declared-field mask from the last apply. Older state omits it and is
84
+ * compared with the same selection against the stored manifest.
85
+ */
86
+ driftMask?: unknown;
87
+ /**
88
+ * Set by `read` when the live object diverges. Reconcile does not persist
89
+ * it; the extra field is what `alchemy drift` compares.
90
+ */
91
+ observedHash?: string;
92
+ },
93
+ {},
94
+ Providers
95
+ > {}
96
+
97
+ /**
98
+ * Applies a raw Kubernetes manifest onto any cluster via server-side
99
+ * apply.
100
+ *
101
+ * Any literal object is accepted — built-in kinds and custom resources
102
+ * alike; unknown kinds are resolved through the Kubernetes API discovery
103
+ * endpoint, so CRDs work without any registration. The target `cluster`
104
+ * can be a managed cluster resource (e.g. `AWS.EKS.Cluster`) or any
105
+ * cluster your kubeconfig can reach (`Kubernetes.KubeConfig(...)`).
106
+ * ### Applying Manifests
107
+ * **Example:** StatefulSet
108
+ * ```typescript
109
+ * const sts = yield* Kubernetes.Manifest("Cache", {
110
+ * cluster,
111
+ * manifest: {
112
+ * apiVersion: "apps/v1",
113
+ * kind: "StatefulSet",
114
+ * metadata: { name: "cache", namespace: "apps" },
115
+ * spec: {
116
+ * serviceName: "cache",
117
+ * replicas: 3,
118
+ * selector: { matchLabels: { app: "cache" } },
119
+ * template: {
120
+ * metadata: { labels: { app: "cache" } },
121
+ * spec: { containers: [{ name: "redis", image: "redis:7" }] },
122
+ * },
123
+ * },
124
+ * },
125
+ * });
126
+ * ```
127
+ *
128
+ * **Example:** Custom resource (CRD)
129
+ * ```typescript
130
+ * const widget = yield* Kubernetes.Manifest("Widget", {
131
+ * cluster,
132
+ * manifest: {
133
+ * apiVersion: "acme.io/v1",
134
+ * kind: "Widget",
135
+ * metadata: { name: "w", namespace: "default" },
136
+ * spec: { size: 3 },
137
+ * },
138
+ * });
139
+ * ```
140
+ *
141
+ * ### Namespaces
142
+ * **Example:** Create a Namespace
143
+ * ```typescript
144
+ * const ns = yield* Kubernetes.Manifest("AppsNamespace", {
145
+ * cluster,
146
+ * manifest: {
147
+ * apiVersion: "v1",
148
+ * kind: "Namespace",
149
+ * metadata: { name: "apps" },
150
+ * },
151
+ * });
152
+ * ```
153
+ *
154
+ * ### Any Cluster
155
+ * **Example:** Apply onto a kubeconfig context
156
+ * ```typescript
157
+ * const local = Kubernetes.KubeConfig({ context: "kind-dev" });
158
+ *
159
+ * const config = yield* Kubernetes.Manifest("AppConfig", {
160
+ * cluster: local,
161
+ * manifest: {
162
+ * apiVersion: "v1",
163
+ * kind: "ConfigMap",
164
+ * metadata: { name: "app-config", namespace: "default" },
165
+ * data: { LOG_LEVEL: "info" },
166
+ * },
167
+ * });
168
+ * ```
169
+ *
170
+ * @resource
171
+ * @product Manifest
172
+ */
173
+ export const Manifest = Resource<Manifest>("Kubernetes.Manifest", {
174
+ aliases: ["AWS.EKS.Manifest"],
175
+ });
176
+
177
+ const toObjectDefinition = (
178
+ manifest: KubernetesManifest,
179
+ ): Effect.Effect<KubernetesObjectDefinition, Error> => {
180
+ const name = manifest.metadata?.name;
181
+ if (!name) {
182
+ return Effect.fail(
183
+ new ManifestInvalidProps({
184
+ message: `Kubernetes.Manifest requires manifest.metadata.name (got ${manifest.apiVersion}/${manifest.kind})`,
185
+ }),
186
+ );
187
+ }
188
+ return Effect.succeed(manifest as KubernetesObjectDefinition);
189
+ };
190
+
191
+ export const ManifestProvider = () =>
192
+ Provider.effect(
193
+ Manifest,
194
+ Effect.gen(function* () {
195
+ return {
196
+ stables: ["connection", "apiVersion", "kind", "name", "namespace"],
197
+ // In-cluster objects have no cloud-side enumeration that attributes
198
+ // them to alchemy; refresh happens per-instance through `read`.
199
+ list: () => Effect.succeed([] as Manifest["Attributes"][]),
200
+ diff: Effect.fn(function* ({ olds = {} as ManifestProps, news }) {
201
+ if (!isResolved(news)) return;
202
+ const oldManifest = olds.manifest as KubernetesManifest | undefined;
203
+ const newManifest = news.manifest as KubernetesManifest;
204
+ const oldCluster = connectionIdentity(tryConnectionOf(olds.cluster));
205
+ const newCluster = connectionIdentity(tryConnectionOf(news.cluster));
206
+ // Object identity (cluster, group/version/kind, name, namespace) is
207
+ // immutable — changing any of it is a replacement.
208
+ if (
209
+ oldManifest &&
210
+ ((oldCluster !== undefined && newCluster !== undefined && oldCluster !== newCluster) ||
211
+ oldManifest.apiVersion !== newManifest.apiVersion ||
212
+ oldManifest.kind !== newManifest.kind ||
213
+ oldManifest.metadata?.name !== newManifest.metadata?.name ||
214
+ oldManifest.metadata?.namespace !== newManifest.metadata?.namespace)
215
+ ) {
216
+ return { action: "replace" } as const;
217
+ }
218
+ }),
219
+ read: Effect.fn(function* ({ output, olds }) {
220
+ if (!output) return undefined;
221
+ const connection = connectionOfOutput(output);
222
+ if (!connection) return undefined;
223
+ // ClusterNotFound fails the drift row. undefined means this object is gone.
224
+ const transport = yield* connectCluster(connection);
225
+ const observed = yield* readObject({
226
+ transport,
227
+ object: output.ref,
228
+ }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.undefined));
229
+ if (!observed) return undefined;
230
+ const uid = (observed as { metadata?: { uid?: string } }).metadata?.uid;
231
+ const attrs = { ...output, uid };
232
+ const manifest = olds?.manifest;
233
+ // A stored mask is the apply-time selection. Older rows have the
234
+ // manifest and no mask; they use that same selection, not the
235
+ // previous whole-object hash.
236
+ const desired =
237
+ output.driftMask === undefined && manifest !== undefined
238
+ ? yield* toObjectDefinition(manifest)
239
+ : undefined;
240
+ const mask = output.driftMask ?? (desired === undefined ? undefined : driftMask(desired));
241
+ if (mask === undefined) return attrs;
242
+ const liveHash = yield* hashDriftSelection(mask, observed);
243
+ const baseline =
244
+ output.driftMask !== undefined && output.baselineHash !== undefined
245
+ ? output.baselineHash
246
+ : desired === undefined
247
+ ? undefined
248
+ : yield* hashDriftSelection(mask, desired);
249
+ if (baseline !== undefined && liveHash !== baseline) {
250
+ return { ...attrs, observedHash: liveHash };
251
+ }
252
+ return attrs;
253
+ }),
254
+ reconcile: Effect.fn(function* ({ news, output, session }) {
255
+ const connection = toConnection(news.cluster);
256
+ const transport = yield* connectCluster(connection);
257
+ const object = yield* toObjectDefinition(news.manifest);
258
+ const ref: KubernetesObjectRef = {
259
+ apiVersion: object.apiVersion,
260
+ kind: object.kind,
261
+ name: object.metadata.name,
262
+ namespace: object.metadata.namespace,
263
+ };
264
+
265
+ // Server-side apply is a true upsert: create-if-missing and
266
+ // converge-if-present in one call, `force: true` so alchemy owns
267
+ // the fields it manages regardless of prior managers.
268
+ const applied = yield* applyObject({ transport, object });
269
+ const witnessed = isPlainObject(applied) ? applied : undefined;
270
+ const baselineHash = yield* hashDriftSelection(object, witnessed ?? object);
271
+
272
+ yield* session.note(
273
+ `Applied ${ref.apiVersion}/${ref.kind} ${ref.namespace ? `${ref.namespace}/` : ""}${ref.name}`,
274
+ );
275
+
276
+ const uid = (applied as { metadata?: { uid?: string } })?.metadata?.uid ?? output?.uid;
277
+
278
+ return {
279
+ connection,
280
+ apiVersion: ref.apiVersion,
281
+ kind: ref.kind,
282
+ name: ref.name,
283
+ namespace: ref.namespace,
284
+ ref,
285
+ uid,
286
+ baselineHash,
287
+ driftMask: driftMask(object, witnessed),
288
+ };
289
+ }),
290
+ delete: Effect.fn(function* ({ output }) {
291
+ const connection = connectionOfOutput(output);
292
+ if (!connection) return;
293
+ const transport = yield* ifClusterExists(connectCluster(connection));
294
+ if (!transport) return;
295
+ // 404 (including a removed CRD's discovery 404) is already success.
296
+ // 403/5xx must surface: the object is still there.
297
+ yield* deleteObject({ transport, object: output.ref });
298
+ }),
299
+ };
300
+ }),
301
+ );
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";