alchemy-kubernetes-native 0.1.1

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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1024 @@
1
+ import { RandomProvider, Resource, isResolved } from "alchemy";
2
+ import * as Effect from "effect/Effect";
3
+ import { createHash } from "node:crypto";
4
+ import { Unowned } from "alchemy/AdoptPolicy";
5
+ import * as Provider from "alchemy/Provider";
6
+ import * as Kubernetes from "alchemy/Kubernetes";
7
+ import { findClusterAdapter, toConnection } from "alchemy/Kubernetes";
8
+ import * as Layer from "effect/Layer";
9
+ import { request } from "node:http";
10
+ import { request as request$1 } from "node:https";
11
+ import * as Duration from "effect/Duration";
12
+ import { readFileSync } from "node:fs";
13
+ import { parseAllDocuments, stringify } from "yaml";
14
+ import { execFileSync } from "node:child_process";
15
+ //#region src/resource.ts
16
+ const ObjectResource = Resource("KubernetesApi.Object");
17
+ const Object$1 = (id, props) => {
18
+ const { dependsOn, ...resourceProps } = props;
19
+ return registerObject(id, resourceProps, dependsOn);
20
+ };
21
+ const lifecycleKeys = /* @__PURE__ */ new Set([
22
+ "cluster",
23
+ "fieldManager",
24
+ "forceConflicts",
25
+ "skipAwait",
26
+ "timeoutSeconds",
27
+ "waitFor",
28
+ "deletionPropagation",
29
+ "replaceOnChanges"
30
+ ]);
31
+ const toObjectProps = (apiVersion, kind, props, mode) => {
32
+ const lifecycle = {};
33
+ const manifest = {
34
+ apiVersion,
35
+ kind
36
+ };
37
+ for (const [key, value] of globalThis.Object.entries(props)) {
38
+ if (key === "dependsOn") continue;
39
+ (lifecycleKeys.has(key) ? lifecycle : manifest)[key] = value;
40
+ }
41
+ return {
42
+ ...lifecycle,
43
+ manifest,
44
+ mode
45
+ };
46
+ };
47
+ const registerObject = (id, props, dependency) => {
48
+ const resource = ObjectResource(id, props);
49
+ return dependency === void 0 ? resource : Effect.tap(resource, (registered) => registered.bind("dependsOn", { dependency }));
50
+ };
51
+ const defineBuiltin = (apiVersion, kind) => ((id, props) => registerObject(id, toObjectProps(apiVersion, kind, props, "object"), props.dependsOn));
52
+ const definePatch = (apiVersion, kind) => ((id, props) => registerObject(id, toObjectProps(apiVersion, kind, props, "patch"), props.dependsOn));
53
+ //#endregion
54
+ //#region src/generated.ts
55
+ const api = {
56
+ core: { v1: {
57
+ ConfigMap: defineBuiltin("v1", "ConfigMap"),
58
+ ConfigMapPatch: definePatch("v1", "ConfigMap"),
59
+ Endpoints: defineBuiltin("v1", "Endpoints"),
60
+ EndpointsPatch: definePatch("v1", "Endpoints"),
61
+ Event: defineBuiltin("v1", "Event"),
62
+ EventPatch: definePatch("v1", "Event"),
63
+ LimitRange: defineBuiltin("v1", "LimitRange"),
64
+ LimitRangePatch: definePatch("v1", "LimitRange"),
65
+ Namespace: defineBuiltin("v1", "Namespace"),
66
+ NamespacePatch: definePatch("v1", "Namespace"),
67
+ Node: defineBuiltin("v1", "Node"),
68
+ NodePatch: definePatch("v1", "Node"),
69
+ PersistentVolume: defineBuiltin("v1", "PersistentVolume"),
70
+ PersistentVolumePatch: definePatch("v1", "PersistentVolume"),
71
+ PersistentVolumeClaim: defineBuiltin("v1", "PersistentVolumeClaim"),
72
+ PersistentVolumeClaimPatch: definePatch("v1", "PersistentVolumeClaim"),
73
+ Pod: defineBuiltin("v1", "Pod"),
74
+ PodPatch: definePatch("v1", "Pod"),
75
+ PodTemplate: defineBuiltin("v1", "PodTemplate"),
76
+ PodTemplatePatch: definePatch("v1", "PodTemplate"),
77
+ ReplicationController: defineBuiltin("v1", "ReplicationController"),
78
+ ReplicationControllerPatch: definePatch("v1", "ReplicationController"),
79
+ ResourceQuota: defineBuiltin("v1", "ResourceQuota"),
80
+ ResourceQuotaPatch: definePatch("v1", "ResourceQuota"),
81
+ Service: defineBuiltin("v1", "Service"),
82
+ ServicePatch: definePatch("v1", "Service"),
83
+ ServiceAccount: defineBuiltin("v1", "ServiceAccount"),
84
+ ServiceAccountPatch: definePatch("v1", "ServiceAccount")
85
+ } },
86
+ apps: { v1: {
87
+ ControllerRevision: defineBuiltin("apps/v1", "ControllerRevision"),
88
+ ControllerRevisionPatch: definePatch("apps/v1", "ControllerRevision"),
89
+ DaemonSet: defineBuiltin("apps/v1", "DaemonSet"),
90
+ DaemonSetPatch: definePatch("apps/v1", "DaemonSet"),
91
+ Deployment: defineBuiltin("apps/v1", "Deployment"),
92
+ DeploymentPatch: definePatch("apps/v1", "Deployment"),
93
+ ReplicaSet: defineBuiltin("apps/v1", "ReplicaSet"),
94
+ ReplicaSetPatch: definePatch("apps/v1", "ReplicaSet"),
95
+ StatefulSet: defineBuiltin("apps/v1", "StatefulSet"),
96
+ StatefulSetPatch: definePatch("apps/v1", "StatefulSet")
97
+ } },
98
+ batch: { v1: {
99
+ CronJob: defineBuiltin("batch/v1", "CronJob"),
100
+ CronJobPatch: definePatch("batch/v1", "CronJob"),
101
+ Job: defineBuiltin("batch/v1", "Job"),
102
+ JobPatch: definePatch("batch/v1", "Job")
103
+ } },
104
+ networking: { v1: {
105
+ IPAddress: defineBuiltin("networking.k8s.io/v1", "IPAddress"),
106
+ IPAddressPatch: definePatch("networking.k8s.io/v1", "IPAddress"),
107
+ Ingress: defineBuiltin("networking.k8s.io/v1", "Ingress"),
108
+ IngressPatch: definePatch("networking.k8s.io/v1", "Ingress"),
109
+ IngressClass: defineBuiltin("networking.k8s.io/v1", "IngressClass"),
110
+ IngressClassPatch: definePatch("networking.k8s.io/v1", "IngressClass"),
111
+ NetworkPolicy: defineBuiltin("networking.k8s.io/v1", "NetworkPolicy"),
112
+ NetworkPolicyPatch: definePatch("networking.k8s.io/v1", "NetworkPolicy"),
113
+ ServiceCIDR: defineBuiltin("networking.k8s.io/v1", "ServiceCIDR"),
114
+ ServiceCIDRPatch: definePatch("networking.k8s.io/v1", "ServiceCIDR")
115
+ } },
116
+ rbac: { v1: {
117
+ ClusterRole: defineBuiltin("rbac.authorization.k8s.io/v1", "ClusterRole"),
118
+ ClusterRolePatch: definePatch("rbac.authorization.k8s.io/v1", "ClusterRole"),
119
+ ClusterRoleBinding: defineBuiltin("rbac.authorization.k8s.io/v1", "ClusterRoleBinding"),
120
+ ClusterRoleBindingPatch: definePatch("rbac.authorization.k8s.io/v1", "ClusterRoleBinding"),
121
+ Role: defineBuiltin("rbac.authorization.k8s.io/v1", "Role"),
122
+ RolePatch: definePatch("rbac.authorization.k8s.io/v1", "Role"),
123
+ RoleBinding: defineBuiltin("rbac.authorization.k8s.io/v1", "RoleBinding"),
124
+ RoleBindingPatch: definePatch("rbac.authorization.k8s.io/v1", "RoleBinding")
125
+ } },
126
+ autoscaling: { v2: {
127
+ HorizontalPodAutoscaler: defineBuiltin("autoscaling/v2", "HorizontalPodAutoscaler"),
128
+ HorizontalPodAutoscalerPatch: definePatch("autoscaling/v2", "HorizontalPodAutoscaler")
129
+ } },
130
+ policy: { v1: {
131
+ PodDisruptionBudget: defineBuiltin("policy/v1", "PodDisruptionBudget"),
132
+ PodDisruptionBudgetPatch: definePatch("policy/v1", "PodDisruptionBudget")
133
+ } },
134
+ storage: { v1: {
135
+ CSIDriver: defineBuiltin("storage.k8s.io/v1", "CSIDriver"),
136
+ CSIDriverPatch: definePatch("storage.k8s.io/v1", "CSIDriver"),
137
+ CSINode: defineBuiltin("storage.k8s.io/v1", "CSINode"),
138
+ CSINodePatch: definePatch("storage.k8s.io/v1", "CSINode"),
139
+ CSIStorageCapacity: defineBuiltin("storage.k8s.io/v1", "CSIStorageCapacity"),
140
+ CSIStorageCapacityPatch: definePatch("storage.k8s.io/v1", "CSIStorageCapacity"),
141
+ StorageClass: defineBuiltin("storage.k8s.io/v1", "StorageClass"),
142
+ StorageClassPatch: definePatch("storage.k8s.io/v1", "StorageClass"),
143
+ VolumeAttachment: defineBuiltin("storage.k8s.io/v1", "VolumeAttachment"),
144
+ VolumeAttachmentPatch: definePatch("storage.k8s.io/v1", "VolumeAttachment"),
145
+ VolumeAttributesClass: defineBuiltin("storage.k8s.io/v1", "VolumeAttributesClass"),
146
+ VolumeAttributesClassPatch: definePatch("storage.k8s.io/v1", "VolumeAttributesClass")
147
+ } },
148
+ apiextensions: { v1: {
149
+ CustomResourceDefinition: defineBuiltin("apiextensions.k8s.io/v1", "CustomResourceDefinition"),
150
+ CustomResourceDefinitionPatch: definePatch("apiextensions.k8s.io/v1", "CustomResourceDefinition")
151
+ } }
152
+ };
153
+ const { core, apps, batch, networking, rbac, autoscaling, policy, storage, apiextensions } = api;
154
+ //#endregion
155
+ //#region src/client.ts
156
+ var KubernetesApiError = class extends Error {
157
+ method;
158
+ path;
159
+ statusCode;
160
+ name = "KubernetesApiError";
161
+ immutable;
162
+ constructor(method, path, statusCode, responseBody) {
163
+ super(`Kubernetes API ${method} ${path} returned ${String(statusCode)}`);
164
+ this.method = method;
165
+ this.path = path;
166
+ this.statusCode = statusCode;
167
+ this.immutable = statusCode === 422 && /immutable|may not change|updates? to [\s\S]*spec[\s\S]*forbidden/i.test(responseBody);
168
+ }
169
+ };
170
+ const connectCluster = Effect.fn(function* (cluster) {
171
+ const connection = toConnection(cluster);
172
+ return {
173
+ connection,
174
+ transport: yield* (yield* findClusterAdapter(connection.auth.kind)).connect(connection)
175
+ };
176
+ });
177
+ const requestJson = Effect.fn(function* ({ transport, method, path, body, timeoutMs = 3e4, contentType }) {
178
+ const headers = yield* transport.headers;
179
+ const url = new URL(path, transport.endpoint);
180
+ if (url.protocol !== "http:" && url.protocol !== "https:") return yield* Effect.fail(/* @__PURE__ */ new Error(`Unsupported Kubernetes API protocol: ${url.protocol}`));
181
+ if (url.protocol === "http:" && ![
182
+ "127.0.0.1",
183
+ "::1",
184
+ "localhost"
185
+ ].includes(url.hostname)) return yield* Effect.fail(/* @__PURE__ */ new Error("Refusing to send Kubernetes credentials over remote HTTP"));
186
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
187
+ return yield* Effect.tryPromise({
188
+ try: () => new Promise((resolve, reject) => {
189
+ const request$2 = (url.protocol === "https:" ? request$1 : request)({
190
+ protocol: url.protocol,
191
+ hostname: url.hostname,
192
+ port: url.port || void 0,
193
+ path: `${url.pathname}${url.search}`,
194
+ method,
195
+ headers: {
196
+ ...headers,
197
+ Accept: "application/json",
198
+ ...payload === void 0 ? {} : {
199
+ "Content-Type": contentType ?? "application/json",
200
+ "Content-Length": Buffer.byteLength(payload)
201
+ }
202
+ },
203
+ ...url.protocol === "https:" && transport.certificateAuthorityData !== void 0 ? { ca: Buffer.from(transport.certificateAuthorityData, "base64").toString("utf8") } : {},
204
+ ...url.protocol === "https:" && transport.clientCert !== void 0 ? {
205
+ cert: transport.clientCert.certificate,
206
+ key: transport.clientCert.key
207
+ } : {},
208
+ ...url.protocol === "https:" && transport.insecureSkipTlsVerify ? { rejectUnauthorized: false } : {}
209
+ }, (response) => {
210
+ const chunks = [];
211
+ response.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
212
+ response.on("end", () => {
213
+ const responseBody = Buffer.concat(chunks).toString("utf8");
214
+ const statusCode = response.statusCode ?? 500;
215
+ if (statusCode < 200 || statusCode >= 300) {
216
+ reject(new KubernetesApiError(method, path, statusCode, responseBody));
217
+ return;
218
+ }
219
+ if (responseBody.trim().length === 0) {
220
+ resolve(void 0);
221
+ return;
222
+ }
223
+ try {
224
+ resolve(JSON.parse(responseBody));
225
+ } catch {
226
+ reject(/* @__PURE__ */ new Error("Kubernetes API returned invalid JSON"));
227
+ }
228
+ });
229
+ });
230
+ request$2.setTimeout(timeoutMs, () => request$2.destroy(/* @__PURE__ */ new Error("Kubernetes API request timed out")));
231
+ request$2.on("error", reject);
232
+ if (payload !== void 0) request$2.write(payload);
233
+ request$2.end();
234
+ }),
235
+ catch: (error) => error instanceof KubernetesApiError ? error : /* @__PURE__ */ new Error("Kubernetes API request failed")
236
+ });
237
+ });
238
+ const discoveredKinds = /* @__PURE__ */ new Map();
239
+ const resolveKind = Effect.fn(function* ({ transport, ref }) {
240
+ const cacheKey = `${transport.endpoint}|${ref.apiVersion}|${ref.kind}`;
241
+ const cached = discoveredKinds.get(cacheKey);
242
+ if (cached !== void 0) return cached;
243
+ const path = ref.apiVersion.includes("/") ? `/apis/${ref.apiVersion}` : `/api/${ref.apiVersion}`;
244
+ const resource = (yield* requestJson({
245
+ transport,
246
+ method: "GET",
247
+ path
248
+ })).resources?.find((candidate) => candidate.kind === ref.kind && candidate.name !== void 0 && !candidate.name.includes("/"));
249
+ if (resource?.name === void 0) return yield* Effect.fail(new KubernetesApiError("GET", path, 404, `Kind ${ref.kind} was not found`));
250
+ const result = {
251
+ plural: resource.name,
252
+ namespaced: resource.namespaced === true
253
+ };
254
+ discoveredKinds.set(cacheKey, result);
255
+ return result;
256
+ });
257
+ const objectPath = Effect.fn(function* ({ transport, ref, collection = false }) {
258
+ const kind = yield* resolveKind({
259
+ transport,
260
+ ref
261
+ });
262
+ const root = ref.apiVersion.includes("/") ? `/apis/${ref.apiVersion}` : `/api/${ref.apiVersion}`;
263
+ if (kind.namespaced && ref.namespace === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`${ref.apiVersion}/${ref.kind} ${ref.name} requires metadata.namespace`));
264
+ return `${root}${kind.namespaced ? `/namespaces/${encodeURIComponent(ref.namespace)}` : ""}/${kind.plural}${collection ? "" : `/${encodeURIComponent(ref.name)}`}`;
265
+ });
266
+ const readObject = Effect.fn(function* ({ transport, ref }) {
267
+ return yield* requestJson({
268
+ transport,
269
+ method: "GET",
270
+ path: yield* objectPath({
271
+ transport,
272
+ ref
273
+ })
274
+ });
275
+ });
276
+ const applyObject = Effect.fn(function* ({ transport, object, fieldManager, forceConflicts, dryRun = false }) {
277
+ const ref = {
278
+ apiVersion: object.apiVersion,
279
+ kind: object.kind,
280
+ name: object.metadata.name,
281
+ namespace: object.metadata.namespace
282
+ };
283
+ const query = new URLSearchParams({ fieldManager });
284
+ if (forceConflicts) query.set("force", "true");
285
+ if (dryRun) query.set("dryRun", "All");
286
+ return yield* requestJson({
287
+ transport,
288
+ method: "PATCH",
289
+ path: `${yield* objectPath({
290
+ transport,
291
+ ref
292
+ })}?${query.toString()}`,
293
+ body: object,
294
+ contentType: "application/apply-patch+yaml"
295
+ });
296
+ });
297
+ const deleteObject = Effect.fn(function* ({ transport, ref, uid, propagationPolicy }) {
298
+ return yield* requestJson({
299
+ transport,
300
+ method: "DELETE",
301
+ path: yield* objectPath({
302
+ transport,
303
+ ref
304
+ }),
305
+ body: {
306
+ apiVersion: "v1",
307
+ kind: "DeleteOptions",
308
+ propagationPolicy,
309
+ ...uid === void 0 ? {} : { preconditions: { uid } }
310
+ }
311
+ }).pipe(Effect.catchIf((error) => error instanceof KubernetesApiError && error.statusCode === 404, () => Effect.void));
312
+ });
313
+ const watchObject = Effect.fn(function* ({ transport, ref, resourceVersion, timeoutSeconds, accept }) {
314
+ const headers = yield* transport.headers;
315
+ const basePath = yield* objectPath({
316
+ transport,
317
+ ref,
318
+ collection: true
319
+ });
320
+ const query = new URLSearchParams({
321
+ watch: "true",
322
+ allowWatchBookmarks: "true",
323
+ fieldSelector: `metadata.name=${ref.name}`,
324
+ timeoutSeconds: String(Math.max(1, Math.ceil(timeoutSeconds)))
325
+ });
326
+ if (resourceVersion !== void 0) query.set("resourceVersion", resourceVersion);
327
+ const url = new URL(`${basePath}?${query.toString()}`, transport.endpoint);
328
+ return yield* Effect.tryPromise({
329
+ try: () => new Promise((resolve, reject) => {
330
+ const request$3 = (url.protocol === "https:" ? request$1 : request)({
331
+ protocol: url.protocol,
332
+ hostname: url.hostname,
333
+ port: url.port || void 0,
334
+ path: `${url.pathname}${url.search}`,
335
+ method: "GET",
336
+ headers: {
337
+ ...headers,
338
+ Accept: "application/json"
339
+ },
340
+ ...url.protocol === "https:" && transport.certificateAuthorityData !== void 0 ? { ca: Buffer.from(transport.certificateAuthorityData, "base64").toString("utf8") } : {},
341
+ ...url.protocol === "https:" && transport.clientCert !== void 0 ? {
342
+ cert: transport.clientCert.certificate,
343
+ key: transport.clientCert.key
344
+ } : {},
345
+ ...url.protocol === "https:" && transport.insecureSkipTlsVerify ? { rejectUnauthorized: false } : {}
346
+ }, (response) => {
347
+ const statusCode = response.statusCode ?? 500;
348
+ if (statusCode < 200 || statusCode >= 300) {
349
+ response.resume();
350
+ reject(new KubernetesApiError("GET", basePath, statusCode, ""));
351
+ return;
352
+ }
353
+ let buffer = "";
354
+ let settled = false;
355
+ response.setEncoding("utf8");
356
+ response.on("data", (chunk) => {
357
+ buffer += chunk;
358
+ const lines = buffer.split("\n");
359
+ buffer = lines.pop() ?? "";
360
+ for (const line of lines) {
361
+ if (line.trim().length === 0) continue;
362
+ const event = JSON.parse(line);
363
+ if (event.type === "ERROR" && event.object?.code === 410) {
364
+ settled = true;
365
+ request$3.destroy();
366
+ reject(new KubernetesApiError("GET", basePath, 410, ""));
367
+ return;
368
+ }
369
+ if (event.type === "BOOKMARK") continue;
370
+ if (accept(event.type === "DELETED" ? void 0 : event.object)) {
371
+ settled = true;
372
+ request$3.destroy();
373
+ resolve(true);
374
+ return;
375
+ }
376
+ }
377
+ });
378
+ response.on("end", () => {
379
+ if (!settled) resolve(false);
380
+ });
381
+ });
382
+ request$3.setTimeout((timeoutSeconds + 2) * 1e3, () => {
383
+ request$3.destroy();
384
+ resolve(false);
385
+ });
386
+ request$3.on("error", (error) => {
387
+ if (error.code === "ECONNRESET") resolve(false);
388
+ else reject(error);
389
+ });
390
+ request$3.end();
391
+ }),
392
+ catch: (error) => error instanceof KubernetesApiError ? error : /* @__PURE__ */ new Error("Kubernetes API watch failed")
393
+ });
394
+ });
395
+ //#endregion
396
+ //#region src/providers.ts
397
+ var Providers = class extends Provider.ProviderCollection()("KubernetesApi") {};
398
+ //#endregion
399
+ //#region src/readiness.ts
400
+ const conditionsOf = (object) => {
401
+ const status = object.status;
402
+ return Array.isArray(status?.conditions) ? status.conditions : [];
403
+ };
404
+ const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
405
+ const conditionSummary = (conditions) => conditions.filter(({ type }) => type !== void 0).map(({ type, status, reason }) => `${type}=${status ?? "Unknown"}${reason === void 0 ? "" : `(${reason})`}`).join(", ") || "none";
406
+ const jsonPathValue = (object, expression) => {
407
+ const path = expression.trim().replace(/^\{?\$?\.?/, "").replace(/\}?$/, "");
408
+ if (path.length === 0) return object;
409
+ const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
410
+ let current = object;
411
+ for (const segment of segments) {
412
+ if (typeof current !== "object" || current === null) return void 0;
413
+ current = current[segment];
414
+ }
415
+ return current;
416
+ };
417
+ const kubernetesObjectReadiness = (object, waitFor) => {
418
+ const conditions = conditionsOf(object);
419
+ const summary = conditionSummary(conditions);
420
+ if (typeof object.metadata?.name !== "string" || typeof object.metadata?.uid !== "string") return {
421
+ ready: false,
422
+ terminal: false,
423
+ detail: "waiting for a complete observed object"
424
+ };
425
+ if (waitFor !== void 0) {
426
+ if ("condition" in waitFor) {
427
+ const expected = waitFor.status ?? "True";
428
+ return {
429
+ ready: conditions.some(({ type, status }) => type === waitFor.condition && status === expected),
430
+ terminal: false,
431
+ detail: `waiting for condition ${waitFor.condition}=${expected}; conditions: ${summary}`
432
+ };
433
+ }
434
+ const actual = jsonPathValue(object, waitFor.jsonPath);
435
+ return {
436
+ ready: JSON.stringify(actual) === JSON.stringify(waitFor.equals),
437
+ terminal: false,
438
+ detail: `waiting for ${waitFor.jsonPath}=${JSON.stringify(waitFor.equals)}; current=${JSON.stringify(actual)}`
439
+ };
440
+ }
441
+ const metadata = object.metadata;
442
+ const generation = count(metadata.generation);
443
+ const spec = object.spec;
444
+ const status = object.status;
445
+ const observed = count(status?.observedGeneration);
446
+ switch (object.kind) {
447
+ case "Namespace": return {
448
+ ready: status?.phase === "Active",
449
+ terminal: status?.phase === "Terminating",
450
+ detail: `phase=${String(status?.phase ?? "Pending")}`
451
+ };
452
+ case "CustomResourceDefinition": return {
453
+ ready: conditions.some(({ type, status: conditionStatus }) => type === "Established" && conditionStatus === "True"),
454
+ terminal: conditions.some(({ type, status: conditionStatus }) => type === "NamesAccepted" && conditionStatus === "False"),
455
+ detail: `conditions: ${summary}`
456
+ };
457
+ case "Pod": return {
458
+ ready: conditions.some(({ type, status: conditionStatus }) => type === "Ready" && conditionStatus === "True"),
459
+ terminal: status?.phase === "Failed",
460
+ detail: `phase=${String(status?.phase ?? "Pending")}, conditions: ${summary}`
461
+ };
462
+ case "Deployment": {
463
+ const desired = spec?.replicas === void 0 ? 1 : count(spec.replicas);
464
+ const available = count(status?.availableReplicas);
465
+ return {
466
+ ready: observed >= generation && available >= desired,
467
+ terminal: conditions.some(({ type, status: conditionStatus, reason }) => type === "Progressing" && conditionStatus === "False" && reason === "ProgressDeadlineExceeded"),
468
+ detail: `observedGeneration=${String(observed)}/${String(generation)}, availableReplicas=${String(available)}/${String(desired)}, conditions: ${summary}`
469
+ };
470
+ }
471
+ case "StatefulSet": {
472
+ const desired = spec?.replicas === void 0 ? 1 : count(spec.replicas);
473
+ const ready = count(status?.readyReplicas);
474
+ return {
475
+ ready: observed >= generation && ready >= desired,
476
+ terminal: false,
477
+ detail: `observedGeneration=${String(observed)}/${String(generation)}, readyReplicas=${String(ready)}/${String(desired)}, conditions: ${summary}`
478
+ };
479
+ }
480
+ case "DaemonSet": {
481
+ const desired = count(status?.desiredNumberScheduled);
482
+ const available = count(status?.numberAvailable);
483
+ return {
484
+ ready: observed >= generation && available >= desired,
485
+ terminal: false,
486
+ detail: `observedGeneration=${String(observed)}/${String(generation)}, numberAvailable=${String(available)}/${String(desired)}, conditions: ${summary}`
487
+ };
488
+ }
489
+ case "Job": return {
490
+ ready: conditions.some(({ type, status: conditionStatus }) => (type === "Complete" || type === "SuccessCriteriaMet") && conditionStatus === "True"),
491
+ terminal: conditions.some(({ type, status: conditionStatus }) => (type === "Failed" || type === "FailureTarget") && conditionStatus === "True"),
492
+ detail: `conditions: ${summary}`
493
+ };
494
+ case "PersistentVolumeClaim": return {
495
+ ready: status?.phase === "Bound",
496
+ terminal: status?.phase === "Lost",
497
+ detail: `phase=${String(status?.phase ?? "Pending")}`
498
+ };
499
+ case "Service": {
500
+ if (spec?.type !== "LoadBalancer") return {
501
+ ready: true,
502
+ terminal: false,
503
+ detail: "service accepted"
504
+ };
505
+ const loadBalancer = status?.loadBalancer;
506
+ return {
507
+ ready: (loadBalancer?.ingress?.length ?? 0) > 0,
508
+ terminal: false,
509
+ detail: `loadBalancer.ingress=${String(loadBalancer?.ingress?.length ?? 0)}`
510
+ };
511
+ }
512
+ case "Ingress": {
513
+ const loadBalancer = status?.loadBalancer;
514
+ return {
515
+ ready: (loadBalancer?.ingress?.length ?? 0) > 0,
516
+ terminal: false,
517
+ detail: `loadBalancer.ingress=${String(loadBalancer?.ingress?.length ?? 0)}`
518
+ };
519
+ }
520
+ default: return {
521
+ ready: true,
522
+ terminal: false,
523
+ detail: "object accepted"
524
+ };
525
+ }
526
+ };
527
+ var KubernetesReadinessError = class extends Error {
528
+ ref;
529
+ timedOut;
530
+ name = "KubernetesReadinessError";
531
+ constructor(message, ref, timedOut) {
532
+ super(message);
533
+ this.ref = ref;
534
+ this.timedOut = timedOut;
535
+ }
536
+ };
537
+ const label = (ref) => `${ref.apiVersion}/${ref.kind} ${ref.namespace === void 0 ? "" : `${ref.namespace}/`}${ref.name}`;
538
+ const waitForObjectReady = Effect.fn(function* ({ transport, ref, timeoutSeconds, waitFor }) {
539
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) return yield* Effect.fail(/* @__PURE__ */ new Error("Kubernetes timeoutSeconds must be greater than zero"));
540
+ const deadline = Date.now() + timeoutSeconds * 1e3;
541
+ let lastDetail = "object not found";
542
+ while (Date.now() < deadline) {
543
+ let observed = yield* readObject({
544
+ transport,
545
+ ref
546
+ }).pipe(Effect.catchIf((error) => error instanceof KubernetesApiError && error.statusCode === 404, () => Effect.succeed(void 0)));
547
+ if (observed !== void 0) {
548
+ const readiness = kubernetesObjectReadiness(observed, waitFor);
549
+ lastDetail = readiness.detail;
550
+ if (readiness.ready) return observed;
551
+ if (readiness.terminal) return yield* Effect.fail(new KubernetesReadinessError(`${label(ref)} reached a terminal state: ${readiness.detail}`, ref, false));
552
+ }
553
+ const remainingSeconds = Math.max(1, (deadline - Date.now()) / 1e3);
554
+ if ((yield* watchObject({
555
+ transport,
556
+ ref,
557
+ ...observed?.metadata.resourceVersion === void 0 ? {} : { resourceVersion: observed.metadata.resourceVersion },
558
+ timeoutSeconds: Math.min(15, remainingSeconds),
559
+ accept: (candidate) => {
560
+ observed = candidate;
561
+ if (candidate === void 0) return false;
562
+ const readiness = kubernetesObjectReadiness(candidate, waitFor);
563
+ lastDetail = readiness.detail;
564
+ return readiness.ready || readiness.terminal;
565
+ }
566
+ }).pipe(Effect.catchIf((error) => error instanceof KubernetesApiError && error.statusCode === 410, () => Effect.succeed(false)))) && observed !== void 0) {
567
+ const readiness = kubernetesObjectReadiness(observed, waitFor);
568
+ if (readiness.ready) return observed;
569
+ if (readiness.terminal) return yield* Effect.fail(new KubernetesReadinessError(`${label(ref)} reached a terminal state: ${readiness.detail}`, ref, false));
570
+ }
571
+ if (Date.now() < deadline) yield* Effect.sleep(Duration.millis(250));
572
+ }
573
+ return yield* Effect.fail(new KubernetesReadinessError(`Timed out waiting for ${label(ref)}: ${lastDetail}`, ref, true));
574
+ });
575
+ const waitForObjectDeleted = Effect.fn(function* ({ transport, ref, uid, timeoutSeconds }) {
576
+ const deadline = Date.now() + timeoutSeconds * 1e3;
577
+ let finalizers = [];
578
+ while (Date.now() < deadline) {
579
+ let observed = yield* readObject({
580
+ transport,
581
+ ref
582
+ }).pipe(Effect.catchIf((error) => error instanceof KubernetesApiError && error.statusCode === 404, () => Effect.succeed(void 0)));
583
+ if (observed === void 0) return;
584
+ if (uid !== void 0 && observed.metadata.uid !== uid) return yield* Effect.fail(/* @__PURE__ */ new Error(`Refusing to treat ${label(ref)} as deleted: UID changed from ${uid} to ${String(observed.metadata.uid)}`));
585
+ finalizers = observed.metadata.finalizers ?? [];
586
+ const remainingSeconds = Math.max(1, (deadline - Date.now()) / 1e3);
587
+ if ((yield* watchObject({
588
+ transport,
589
+ ref,
590
+ ...observed.metadata.resourceVersion === void 0 ? {} : { resourceVersion: observed.metadata.resourceVersion },
591
+ timeoutSeconds: Math.min(15, remainingSeconds),
592
+ accept: (candidate) => {
593
+ observed = candidate;
594
+ return candidate === void 0;
595
+ }
596
+ }).pipe(Effect.catchIf((error) => error instanceof KubernetesApiError && error.statusCode === 410, () => Effect.succeed(false)))) || observed === void 0) return;
597
+ }
598
+ return yield* Effect.fail(new KubernetesReadinessError(`Timed out deleting ${label(ref)}${finalizers.length === 0 ? "" : `; blocking finalizers: ${finalizers.join(", ")}`}`, ref, true));
599
+ });
600
+ //#endregion
601
+ //#region src/provider.ts
602
+ const FQN_ANNOTATION = "alchemy.run/fqn";
603
+ const INSTANCE_ANNOTATION = "alchemy.run/instance-id";
604
+ const serverMetadata = /* @__PURE__ */ new Set([
605
+ "uid",
606
+ "resourceVersion",
607
+ "generation",
608
+ "creationTimestamp",
609
+ "deletionTimestamp",
610
+ "deletionGracePeriodSeconds",
611
+ "managedFields",
612
+ "selfLink"
613
+ ]);
614
+ const connectionIdentity = (cluster) => {
615
+ const connection = Kubernetes.toConnection(cluster);
616
+ const auth = connection.auth;
617
+ return JSON.stringify({
618
+ endpoint: connection.endpoint,
619
+ certificateAuthorityData: connection.certificateAuthorityData,
620
+ insecureSkipTlsVerify: connection.insecureSkipTlsVerify,
621
+ auth: Object.fromEntries(Object.entries(auth).map(([key, value]) => [key, [
622
+ "token",
623
+ "key",
624
+ "certificate",
625
+ "env"
626
+ ].includes(key) ? "<credential>" : value]))
627
+ });
628
+ };
629
+ const refOf = (object) => ({
630
+ apiVersion: object.apiVersion,
631
+ kind: object.kind,
632
+ name: object.metadata.name,
633
+ namespace: object.metadata.namespace
634
+ });
635
+ const fieldManagerOf = (fqn, requested) => {
636
+ const manager = requested ?? `alchemy-${createHash("sha256").update(fqn).digest("hex").slice(0, 20)}`;
637
+ if (manager.length === 0 || manager.length > 128) throw new Error("Kubernetes fieldManager must contain 1 to 128 characters");
638
+ return manager;
639
+ };
640
+ const desiredObject = ({ props, fqn, instanceId }) => {
641
+ if (props.manifest.kind === "Secret") throw new Error("Kubernetes Secret is intentionally unavailable through KubernetesApi.Object; use KubernetesAddons.Secret so values never enter plans or live attributes");
642
+ if (typeof props.manifest.apiVersion !== "string" || props.manifest.apiVersion.length === 0 || typeof props.manifest.kind !== "string" || props.manifest.kind.length === 0 || typeof props.manifest.metadata?.name !== "string" || props.manifest.metadata.name.length === 0) throw new Error("Kubernetes objects require apiVersion, kind, and metadata.name");
643
+ const metadata = Object.fromEntries(Object.entries(props.manifest.metadata).filter(([key]) => !serverMetadata.has(key)));
644
+ if ((props.mode ?? "object") === "object") metadata.annotations = {
645
+ ...metadata.annotations ?? {},
646
+ [FQN_ANNOTATION]: fqn,
647
+ [INSTANCE_ANNOTATION]: instanceId
648
+ };
649
+ return {
650
+ ...Object.fromEntries(Object.entries(props.manifest).filter(([key]) => key !== "status" && key !== "metadata")),
651
+ metadata
652
+ };
653
+ };
654
+ const sanitizeObservedObject = (object) => {
655
+ const metadata = Object.fromEntries(Object.entries(object.metadata).filter(([key]) => key !== "managedFields"));
656
+ return {
657
+ ...object,
658
+ metadata
659
+ };
660
+ };
661
+ const attributesOf = ({ connection, object, mode }) => {
662
+ const live = sanitizeObservedObject(object);
663
+ const ref = refOf(live);
664
+ return {
665
+ connection,
666
+ ref,
667
+ apiVersion: ref.apiVersion,
668
+ kind: ref.kind,
669
+ name: ref.name,
670
+ namespace: ref.namespace,
671
+ uid: live.metadata.uid,
672
+ resourceVersion: live.metadata.resourceVersion,
673
+ generation: live.metadata.generation,
674
+ metadata: live.metadata,
675
+ spec: live.spec,
676
+ status: live.status,
677
+ live,
678
+ mode
679
+ };
680
+ };
681
+ const mergeKey = (value) => {
682
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return;
683
+ const item = value;
684
+ for (const key of [
685
+ "name",
686
+ "key",
687
+ "port",
688
+ "containerPort",
689
+ "mountPath",
690
+ "type",
691
+ "topologyKey"
692
+ ]) if ([
693
+ "string",
694
+ "number",
695
+ "boolean"
696
+ ].includes(typeof item[key])) return `${key}:${String(item[key])}`;
697
+ };
698
+ const desiredProjection = (desired, observed) => {
699
+ if (Array.isArray(desired)) {
700
+ if (!Array.isArray(observed)) return observed;
701
+ return desired.map((item, index) => {
702
+ const key = mergeKey(item);
703
+ const candidate = key === void 0 ? observed[index] : observed.find((observedItem) => mergeKey(observedItem) === key);
704
+ return desiredProjection(item, candidate);
705
+ });
706
+ }
707
+ if (typeof desired === "object" && desired !== null) {
708
+ if (typeof observed !== "object" || observed === null) return observed;
709
+ return Object.fromEntries(Object.entries(desired).map(([key, value]) => [key, desiredProjection(value, observed[key])]));
710
+ }
711
+ return observed;
712
+ };
713
+ const equalJson = (left, right) => JSON.stringify(left) === JSON.stringify(right);
714
+ const valueAtPath = (value, path) => {
715
+ const segments = path.replace(/^\$?\.?/, "").replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
716
+ let current = value;
717
+ for (const segment of segments) {
718
+ if (typeof current !== "object" || current === null) return void 0;
719
+ current = current[segment];
720
+ }
721
+ return current;
722
+ };
723
+ const isNotFound = (error) => error instanceof KubernetesApiError && error.statusCode === 404;
724
+ const ObjectProvider = () => Provider.succeed(ObjectResource, {
725
+ stables: [
726
+ "connection",
727
+ "apiVersion",
728
+ "kind",
729
+ "name",
730
+ "namespace",
731
+ "ref"
732
+ ],
733
+ nuke: { skip: true },
734
+ read: ({ fqn, instanceId, olds, output }) => Effect.gen(function* () {
735
+ const cluster = output?.connection ?? olds.cluster;
736
+ const connected = yield* connectCluster(cluster).pipe(Effect.catchIf((error) => error instanceof Kubernetes.ClusterNotFoundError, () => Effect.succeed(void 0)));
737
+ if (connected === void 0) return void 0;
738
+ const desired = desiredObject({
739
+ props: olds,
740
+ fqn,
741
+ instanceId
742
+ });
743
+ const ref = output?.ref ?? refOf(desired);
744
+ const observed = yield* readObject({
745
+ transport: connected.transport,
746
+ ref
747
+ }).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(void 0)));
748
+ if (observed === void 0) return void 0;
749
+ const mode = olds.mode ?? output?.mode ?? "object";
750
+ const attributes = attributesOf({
751
+ connection: connected.connection,
752
+ object: observed,
753
+ mode
754
+ });
755
+ if (mode === "patch") return attributes;
756
+ const annotations = observed.metadata.annotations ?? {};
757
+ return annotations["alchemy.run/fqn"] === fqn && annotations["alchemy.run/instance-id"] === instanceId ? attributes : Unowned(attributes);
758
+ }),
759
+ diff: ({ fqn, instanceId, olds, news, output }) => Effect.gen(function* () {
760
+ if (!isResolved(news)) return void 0;
761
+ const oldDesired = desiredObject({
762
+ props: olds,
763
+ fqn,
764
+ instanceId
765
+ });
766
+ const nextDesired = desiredObject({
767
+ props: news,
768
+ fqn,
769
+ instanceId
770
+ });
771
+ const oldRef = refOf(oldDesired);
772
+ const nextRef = refOf(nextDesired);
773
+ if (connectionIdentity(olds.cluster) !== connectionIdentity(news.cluster) || !equalJson(oldRef, nextRef)) return {
774
+ action: "replace",
775
+ deleteFirst: connectionIdentity(olds.cluster) === connectionIdentity(news.cluster) && oldRef.apiVersion === nextRef.apiVersion && oldRef.kind === nextRef.kind && oldRef.name === nextRef.name && oldRef.namespace === nextRef.namespace
776
+ };
777
+ for (const path of news.replaceOnChanges ?? []) if (!equalJson(valueAtPath(oldDesired, path), valueAtPath(nextDesired, path))) return {
778
+ action: "replace",
779
+ deleteFirst: true
780
+ };
781
+ const connected = output === void 0 ? void 0 : yield* connectCluster(news.cluster);
782
+ const live = connected === void 0 ? void 0 : yield* readObject({
783
+ transport: connected.transport,
784
+ ref: nextRef
785
+ }).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(void 0)));
786
+ const persistedAnnotations = output?.live.metadata.annotations ?? {};
787
+ const takingOwnership = output !== void 0 && (news.mode ?? "object") === "object" && (persistedAnnotations["alchemy.run/fqn"] !== fqn || persistedAnnotations["alchemy.run/instance-id"] !== instanceId);
788
+ if (live !== void 0 && (news.mode ?? "object") === "object" && !takingOwnership) {
789
+ const liveAnnotations = live.metadata.annotations ?? {};
790
+ if (output?.uid !== void 0 && live.metadata.uid !== output.uid || liveAnnotations["alchemy.run/fqn"] !== fqn || liveAnnotations["alchemy.run/instance-id"] !== instanceId) return yield* Effect.fail(/* @__PURE__ */ new Error(`Kubernetes object ${nextRef.apiVersion}/${nextRef.kind} ${nextRef.namespace ?? "_cluster"}/${nextRef.name} no longer has this resource's UID and ownership annotations; refusing to mutate a replacement object`));
791
+ }
792
+ const desiredChanged = !equalJson(oldDesired, nextDesired);
793
+ const objectDrifted = live === void 0 || !equalJson(nextDesired, desiredProjection(nextDesired, live));
794
+ const lifecycleChanged = !equalJson({
795
+ fieldManager: olds.fieldManager,
796
+ forceConflicts: olds.forceConflicts,
797
+ skipAwait: olds.skipAwait,
798
+ timeoutSeconds: olds.timeoutSeconds,
799
+ waitFor: olds.waitFor,
800
+ deletionPropagation: olds.deletionPropagation,
801
+ mode: olds.mode
802
+ }, {
803
+ fieldManager: news.fieldManager,
804
+ forceConflicts: news.forceConflicts,
805
+ skipAwait: news.skipAwait,
806
+ timeoutSeconds: news.timeoutSeconds,
807
+ waitFor: news.waitFor,
808
+ deletionPropagation: news.deletionPropagation,
809
+ mode: news.mode
810
+ });
811
+ if (!desiredChanged && !objectDrifted && !lifecycleChanged) return { action: "noop" };
812
+ if (output !== void 0 && (desiredChanged || objectDrifted)) {
813
+ const dryRun = yield* applyObject({
814
+ transport: connected.transport,
815
+ object: nextDesired,
816
+ fieldManager: fieldManagerOf(fqn, news.fieldManager),
817
+ forceConflicts: news.forceConflicts ?? takingOwnership,
818
+ dryRun: true
819
+ }).pipe(Effect.result);
820
+ if (dryRun._tag === "Failure") {
821
+ const error = dryRun.failure;
822
+ if (error instanceof KubernetesApiError && error.immutable) return {
823
+ action: "replace",
824
+ deleteFirst: true
825
+ };
826
+ if (error instanceof KubernetesApiError && error.statusCode === 409) return yield* Effect.fail(/* @__PURE__ */ new Error(`Kubernetes field ownership conflict for ${nextRef.apiVersion}/${nextRef.kind} ${nextRef.namespace ?? "_cluster"}/${nextRef.name}; set forceConflicts only when this stack should take those fields`));
827
+ return yield* Effect.fail(error);
828
+ }
829
+ }
830
+ return { action: "update" };
831
+ }),
832
+ reconcile: ({ fqn, instanceId, news, olds, output, session }) => Effect.gen(function* () {
833
+ const connected = yield* connectCluster(news.cluster);
834
+ const desired = desiredObject({
835
+ props: news,
836
+ fqn,
837
+ instanceId
838
+ });
839
+ let applied = yield* applyObject({
840
+ transport: connected.transport,
841
+ object: desired,
842
+ fieldManager: fieldManagerOf(fqn, news.fieldManager),
843
+ forceConflicts: news.forceConflicts ?? (olds === void 0 && output !== void 0)
844
+ });
845
+ const ref = refOf(applied);
846
+ if (!(news.skipAwait ?? false)) applied = yield* waitForObjectReady({
847
+ transport: connected.transport,
848
+ ref,
849
+ timeoutSeconds: news.timeoutSeconds ?? 300,
850
+ ...news.waitFor === void 0 ? {} : { waitFor: news.waitFor }
851
+ });
852
+ yield* session.note(`Applied ${ref.apiVersion}/${ref.kind} ${ref.namespace === void 0 ? "" : `${ref.namespace}/`}${ref.name}`);
853
+ return attributesOf({
854
+ connection: connected.connection,
855
+ object: applied,
856
+ mode: news.mode ?? "object"
857
+ });
858
+ }),
859
+ delete: ({ olds, output, session }) => Effect.gen(function* () {
860
+ if (output.mode === "patch" || olds.mode === "patch") return;
861
+ const connected = yield* connectCluster(output.connection).pipe(Effect.catchIf((error) => error instanceof Kubernetes.ClusterNotFoundError, () => Effect.succeed(void 0)));
862
+ if (connected === void 0) return;
863
+ const observed = yield* readObject({
864
+ transport: connected.transport,
865
+ ref: output.ref
866
+ }).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(void 0)));
867
+ if (observed === void 0) return;
868
+ if (output.uid !== void 0 && observed.metadata.uid !== output.uid) return yield* Effect.fail(/* @__PURE__ */ new Error(`Refusing to delete ${output.apiVersion}/${output.kind} ${output.namespace ?? "_cluster"}/${output.name}: expected UID ${output.uid}, found ${String(observed.metadata.uid)}`));
869
+ yield* deleteObject({
870
+ transport: connected.transport,
871
+ ref: output.ref,
872
+ ...output.uid === void 0 ? {} : { uid: output.uid },
873
+ propagationPolicy: olds.deletionPropagation ?? "Foreground"
874
+ });
875
+ yield* waitForObjectDeleted({
876
+ transport: connected.transport,
877
+ ref: output.ref,
878
+ ...output.uid === void 0 ? {} : { uid: output.uid },
879
+ timeoutSeconds: olds.timeoutSeconds ?? 300
880
+ });
881
+ yield* session.note(`Deleted ${output.apiVersion}/${output.kind} ${output.namespace === void 0 ? "" : `${output.namespace}/`}${output.name}`);
882
+ })
883
+ });
884
+ const providers = () => Layer.effect(Providers, Provider.collection([ObjectResource])).pipe(Layer.provide(ObjectProvider()), Layer.provideMerge(RandomProvider()));
885
+ //#endregion
886
+ //#region src/yaml.ts
887
+ const clusterScopedKinds = /* @__PURE__ */ new Set([
888
+ "CustomResourceDefinition",
889
+ "Namespace",
890
+ "Node",
891
+ "PersistentVolume",
892
+ "ClusterRole",
893
+ "ClusterRoleBinding",
894
+ "IngressClass",
895
+ "StorageClass",
896
+ "CSIDriver",
897
+ "CSINode",
898
+ "VolumeAttachment",
899
+ "VolumeAttributesClass"
900
+ ]);
901
+ const applyRank = (object) => {
902
+ if (object.kind === "Namespace") return 10;
903
+ if (object.kind === "CustomResourceDefinition") return 20;
904
+ if ([
905
+ "ServiceAccount",
906
+ "ClusterRole",
907
+ "Role"
908
+ ].includes(object.kind)) return 30;
909
+ if (["ClusterRoleBinding", "RoleBinding"].includes(object.kind)) return 40;
910
+ if ([
911
+ "ConfigMap",
912
+ "Service",
913
+ "PersistentVolumeClaim"
914
+ ].includes(object.kind)) return 50;
915
+ return 100;
916
+ };
917
+ const parseKubernetesYaml = (source) => parseAllDocuments(source).flatMap((document) => {
918
+ if (document.errors.length > 0) throw new Error(`Invalid Kubernetes YAML: ${document.errors[0].message}`);
919
+ const value = document.toJS();
920
+ if (value === null || value === void 0) return [];
921
+ if (typeof value !== "object" || Array.isArray(value)) throw new Error("Every Kubernetes YAML document must be an object");
922
+ const object = value;
923
+ if (object.kind === "List") {
924
+ const items = object.items;
925
+ if (!Array.isArray(items)) throw new Error("Kubernetes List YAML requires an items array");
926
+ return items;
927
+ }
928
+ return [object];
929
+ });
930
+ const normalizeConfigGroupObjects = (props) => {
931
+ const yamlSources = [...typeof props.yaml === "string" ? [props.yaml] : props.yaml ?? [], ...(props.files ?? []).map((file) => readFileSync(file, "utf8"))];
932
+ const objects = [...props.objects ?? [], ...yamlSources.flatMap(parseKubernetesYaml)].map((object) => {
933
+ if (props.defaultNamespace === void 0 || clusterScopedKinds.has(object.kind) || object.metadata?.namespace !== void 0) return object;
934
+ return {
935
+ ...object,
936
+ metadata: {
937
+ ...object.metadata,
938
+ namespace: props.defaultNamespace
939
+ }
940
+ };
941
+ });
942
+ for (const object of objects) {
943
+ if (typeof object.apiVersion !== "string" || typeof object.kind !== "string" || typeof object.metadata?.name !== "string") throw new Error("Every ConfigGroup object requires apiVersion, kind, and metadata.name");
944
+ if (object.kind === "Secret") throw new Error("ConfigGroup refuses Kubernetes Secret documents; use KubernetesAddons.Secret");
945
+ }
946
+ return objects.sort((left, right) => applyRank(left) - applyRank(right) || `${left.apiVersion}/${left.kind}/${left.metadata.namespace ?? ""}/${left.metadata.name}`.localeCompare(`${right.apiVersion}/${right.kind}/${right.metadata.namespace ?? ""}/${right.metadata.name}`));
947
+ };
948
+ const ConfigGroup = (id, props) => Effect.gen(function* () {
949
+ const { cluster, objects, yaml, files, defaultNamespace, dependsOn, ...lifecycle } = props;
950
+ const normalized = normalizeConfigGroupObjects({
951
+ ...objects === void 0 ? {} : { objects },
952
+ ...yaml === void 0 ? {} : { yaml },
953
+ ...files === void 0 ? {} : { files },
954
+ ...defaultNamespace === void 0 ? {} : { defaultNamespace }
955
+ });
956
+ const resources = [];
957
+ let dependency = dependsOn;
958
+ const usedIds = /* @__PURE__ */ new Map();
959
+ for (const object of normalized) {
960
+ const baseId = `${id}${object.kind}${object.metadata.name}`.replace(/[^A-Za-z0-9_-]/g, "_");
961
+ const count = usedIds.get(baseId) ?? 0;
962
+ usedIds.set(baseId, count + 1);
963
+ const logicalId = count === 0 ? baseId : `${baseId}_${String(count + 1)}`;
964
+ const resource = yield* Object$1(logicalId, {
965
+ cluster,
966
+ manifest: object,
967
+ ...lifecycle,
968
+ ...dependency === void 0 ? {} : { dependsOn: dependency }
969
+ });
970
+ resources.push(resource);
971
+ dependency = resource.resourceVersion;
972
+ }
973
+ return {
974
+ resources,
975
+ objects: normalized
976
+ };
977
+ });
978
+ //#endregion
979
+ //#region src/helm.ts
980
+ const helmTemplateArgs = (id, props) => [
981
+ "template",
982
+ props.releaseName ?? id.toLowerCase(),
983
+ props.chart,
984
+ "--skip-tests",
985
+ "--namespace",
986
+ props.namespace ?? "default",
987
+ ...props.includeCrds === false ? [] : ["--include-crds"],
988
+ ...props.repository === void 0 ? [] : ["--repo", props.repository],
989
+ ...props.version === void 0 ? [] : ["--version", props.version],
990
+ ...props.kubeVersion === void 0 ? [] : ["--kube-version", props.kubeVersion],
991
+ ...(props.apiVersions ?? []).flatMap((version) => ["--api-versions", version]),
992
+ "--values",
993
+ "-"
994
+ ];
995
+ const renderHelmChart = (id, props) => {
996
+ try {
997
+ return execFileSync("helm", helmTemplateArgs(id, props), {
998
+ encoding: "utf8",
999
+ input: stringify(props.values ?? {}),
1000
+ maxBuffer: 67108864
1001
+ });
1002
+ } catch {
1003
+ throw new Error(`Failed to render Helm chart ${props.chart}; install helm and verify the chart reference`);
1004
+ }
1005
+ };
1006
+ const HelmChart = (id, props) => {
1007
+ const { cluster, dependsOn, fieldManager, forceConflicts, skipAwait, timeoutSeconds, waitFor, deletionPropagation, replaceOnChanges, ...chart } = props;
1008
+ const rendered = renderHelmChart(id, chart);
1009
+ return ConfigGroup(id, {
1010
+ cluster,
1011
+ yaml: rendered,
1012
+ defaultNamespace: chart.namespace ?? "default",
1013
+ ...dependsOn === void 0 ? {} : { dependsOn },
1014
+ ...fieldManager === void 0 ? {} : { fieldManager },
1015
+ ...forceConflicts === void 0 ? {} : { forceConflicts },
1016
+ ...skipAwait === void 0 ? {} : { skipAwait },
1017
+ ...timeoutSeconds === void 0 ? {} : { timeoutSeconds },
1018
+ ...waitFor === void 0 ? {} : { waitFor },
1019
+ ...deletionPropagation === void 0 ? {} : { deletionPropagation },
1020
+ ...replaceOnChanges === void 0 ? {} : { replaceOnChanges }
1021
+ });
1022
+ };
1023
+ //#endregion
1024
+ export { ConfigGroup, FQN_ANNOTATION, HelmChart, INSTANCE_ANNOTATION, KubernetesApiError, KubernetesReadinessError, Object$1 as Object, ObjectProvider, ObjectResource, Providers, api, apiextensions, applyObject, apps, autoscaling, batch, connectCluster, core, deleteObject, desiredObject, desiredProjection, helmTemplateArgs, jsonPathValue, kubernetesObjectReadiness, networking, normalizeConfigGroupObjects, objectPath, parseKubernetesYaml, policy, providers, rbac, readObject, renderHelmChart, requestJson, resolveKind, sanitizeObservedObject, storage, watchObject };