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/LICENSE +191 -0
- package/README.md +150 -0
- package/dist/index.d.mts +491 -0
- package/dist/index.mjs +1024 -0
- package/package.json +56 -0
- package/schema.lock.json +175 -0
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 };
|