alasio 4.0.0
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/cli/src/cluster/docker.js +219 -0
- package/dist/cli/src/cluster/docker.js.map +1 -0
- package/dist/cli/src/cluster/local.js +392 -0
- package/dist/cli/src/cluster/local.js.map +1 -0
- package/dist/cli/src/commands/common.js +108 -0
- package/dist/cli/src/commands/common.js.map +1 -0
- package/dist/cli/src/commands/down.js +26 -0
- package/dist/cli/src/commands/down.js.map +1 -0
- package/dist/cli/src/commands/init.js +314 -0
- package/dist/cli/src/commands/init.js.map +1 -0
- package/dist/cli/src/commands/lake.js +33 -0
- package/dist/cli/src/commands/lake.js.map +1 -0
- package/dist/cli/src/commands/login.js +38 -0
- package/dist/cli/src/commands/login.js.map +1 -0
- package/dist/cli/src/commands/logs.js +27 -0
- package/dist/cli/src/commands/logs.js.map +1 -0
- package/dist/cli/src/commands/restart.js +23 -0
- package/dist/cli/src/commands/restart.js.map +1 -0
- package/dist/cli/src/commands/status.js +58 -0
- package/dist/cli/src/commands/status.js.map +1 -0
- package/dist/cli/src/commands/uninstall.js +47 -0
- package/dist/cli/src/commands/uninstall.js.map +1 -0
- package/dist/cli/src/commands/up.js +33 -0
- package/dist/cli/src/commands/up.js.map +1 -0
- package/dist/cli/src/commands/upgrade.js +50 -0
- package/dist/cli/src/commands/upgrade.js.map +1 -0
- package/dist/cli/src/commands.js +16 -0
- package/dist/cli/src/commands.js.map +1 -0
- package/dist/cli/src/config.js +169 -0
- package/dist/cli/src/config.js.map +1 -0
- package/dist/cli/src/images.js +7 -0
- package/dist/cli/src/images.js.map +1 -0
- package/dist/cli/src/install.js +39 -0
- package/dist/cli/src/install.js.map +1 -0
- package/dist/cli/src/kube/api.js +231 -0
- package/dist/cli/src/kube/api.js.map +1 -0
- package/dist/cli/src/kube/apply.js +98 -0
- package/dist/cli/src/kube/apply.js.map +1 -0
- package/dist/cli/src/kube/rollout.js +166 -0
- package/dist/cli/src/kube/rollout.js.map +1 -0
- package/dist/cli/src/main.js +30 -0
- package/dist/cli/src/main.js.map +1 -0
- package/dist/cli/src/manifests/agent-sandbox.js +89 -0
- package/dist/cli/src/manifests/agent-sandbox.js.map +1 -0
- package/dist/cli/src/manifests/alasio.js +367 -0
- package/dist/cli/src/manifests/alasio.js.map +1 -0
- package/dist/cli/src/manifests/common.js +157 -0
- package/dist/cli/src/manifests/common.js.map +1 -0
- package/dist/cli/src/manifests/config.js +283 -0
- package/dist/cli/src/manifests/config.js.map +1 -0
- package/dist/cli/src/manifests/index.js +20 -0
- package/dist/cli/src/manifests/index.js.map +1 -0
- package/dist/cli/src/manifests/lake.js +56 -0
- package/dist/cli/src/manifests/lake.js.map +1 -0
- package/dist/cli/src/manifests/neon.js +656 -0
- package/dist/cli/src/manifests/neon.js.map +1 -0
- package/dist/cli/src/manifests/network-policies.js +86 -0
- package/dist/cli/src/manifests/network-policies.js.map +1 -0
- package/dist/cli/src/manifests/object-store.js +204 -0
- package/dist/cli/src/manifests/object-store.js.map +1 -0
- package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +6295 -0
- package/dist/cli/src/release.js +15 -0
- package/dist/cli/src/release.js.map +1 -0
- package/dist/cli/src/secrets.js +49 -0
- package/dist/cli/src/secrets.js.map +1 -0
- package/dist/cli/src/target.js +55 -0
- package/dist/cli/src/target.js.map +1 -0
- package/dist/cli/src/telegram.js +27 -0
- package/dist/cli/src/telegram.js.map +1 -0
- package/dist/src/kube/config.js +99 -0
- package/dist/src/kube/config.js.map +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker's Engine API, over the Unix socket Docker listens on: as much of it as the local
|
|
3
|
+
* cluster (./local.ts) needs, its images, network, volumes and containers and commands
|
|
4
|
+
* run in them, called with node:http, so neither the Docker CLI nor a client library is
|
|
5
|
+
* needed. Paths are those of API version 1.44 (Docker 25), the oldest it is written for.
|
|
6
|
+
*
|
|
7
|
+
* The socket is the one `DOCKER_HOST` names (`unix://PATH`), /var/run/docker.sock when
|
|
8
|
+
* it names none.
|
|
9
|
+
*/
|
|
10
|
+
import { request as httpRequest, STATUS_CODES } from "node:http";
|
|
11
|
+
import { buffer } from "node:stream/consumers";
|
|
12
|
+
import { Context, Effect, Layer, Result, Schema } from "effect";
|
|
13
|
+
const API_VERSION = "v1.44";
|
|
14
|
+
const DEFAULT_SOCKET = "/var/run/docker.sock";
|
|
15
|
+
/** Docker refused a call, or could not be reached: the status it answered with, when it answered. */
|
|
16
|
+
export class DockerError extends Schema.TaggedError()("DockerError", {
|
|
17
|
+
/** The call, as its method and path. */
|
|
18
|
+
call: Schema.String,
|
|
19
|
+
/** Docker's HTTP status (404 for absent, 409 for a conflict), when it answered. */
|
|
20
|
+
status: Schema.optional(Schema.Number),
|
|
21
|
+
/** What Docker answered, or why it could not be reached. */
|
|
22
|
+
reason: Schema.String,
|
|
23
|
+
}) {
|
|
24
|
+
get message() {
|
|
25
|
+
return `Docker ${this.status === undefined ? "failed" : `answered ${this.status} to`} ${this.call}: ${this.reason}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** `DOCKER_HOST` names a Docker that is not reached over a Unix socket. */
|
|
29
|
+
export class DockerHostUnsupported extends Schema.TaggedError()("DockerHostUnsupported", {
|
|
30
|
+
host: Schema.String,
|
|
31
|
+
}) {
|
|
32
|
+
get message() {
|
|
33
|
+
return `DOCKER_HOST is ${this.host}, but alasio reaches Docker only over a Unix socket (unix://PATH)`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Whether `error` is Docker's answer `status`. */
|
|
37
|
+
export const hasStatus = (status) => (error) => error.status === status;
|
|
38
|
+
/** The socket of the Docker `env` names, by `DOCKER_HOST`. */
|
|
39
|
+
export function dockerSocket(env) {
|
|
40
|
+
const host = env["DOCKER_HOST"];
|
|
41
|
+
if (!host)
|
|
42
|
+
return Result.succeed(DEFAULT_SOCKET);
|
|
43
|
+
if (host.startsWith("unix://"))
|
|
44
|
+
return Result.succeed(host.slice("unix://".length));
|
|
45
|
+
return Result.fail(new DockerHostUnsupported({ host }));
|
|
46
|
+
}
|
|
47
|
+
/** Docker's Engine API, as the local cluster calls it. Objects are addressed by name. */
|
|
48
|
+
export class DockerEngine extends Context.Service()("alasio/cluster/DockerEngine") {
|
|
49
|
+
/** The Docker `env` names (dockerSocket). */
|
|
50
|
+
static layer = (env = process.env) => Layer.effect(DockerEngine, Effect.map(Effect.fromResult(dockerSocket(env)), makeDockerEngine));
|
|
51
|
+
}
|
|
52
|
+
const describe = (call) => `${call.method} ${call.path}`;
|
|
53
|
+
const unreachable = (call, cause) => new DockerError({ call: describe(call), reason: cause instanceof Error ? cause.message : String(cause) });
|
|
54
|
+
/** A label filter of a list call, in the JSON Docker's `filters` parameter takes. */
|
|
55
|
+
const labelFilter = (labels) => JSON.stringify({ label: Object.entries(labels).map(([key, value]) => `${key}=${value}`) });
|
|
56
|
+
/** The content of the first regular file in `tar`, a tar archive as Docker's archive endpoint sends one; null when it holds none. */
|
|
57
|
+
export function firstFile(tar) {
|
|
58
|
+
const BLOCK = 512;
|
|
59
|
+
for (let offset = 0; offset + BLOCK <= tar.length;) {
|
|
60
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
61
|
+
// Two zero blocks end an archive.
|
|
62
|
+
if (header.every((byte) => byte === 0))
|
|
63
|
+
return null;
|
|
64
|
+
const size = Number.parseInt(header.toString("latin1", 124, 136).replace(/\0.*$/su, "").trim() || "0", 8);
|
|
65
|
+
const type = header[156];
|
|
66
|
+
const start = offset + BLOCK;
|
|
67
|
+
// "0", or NUL in archives older than POSIX's.
|
|
68
|
+
if (type === 0x30 || type === 0)
|
|
69
|
+
return tar.subarray(start, start + size);
|
|
70
|
+
offset = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The output of a command whose exec ran without a terminal: Docker sends it in frames of
|
|
76
|
+
* an 8-byte header (the stream, 1 for stdout or 2 for stderr, and the size, big-endian, at
|
|
77
|
+
* byte 4) and what was written.
|
|
78
|
+
*/
|
|
79
|
+
export function demultiplex(raw) {
|
|
80
|
+
const stdout = [];
|
|
81
|
+
const stderr = [];
|
|
82
|
+
for (let offset = 0; offset + 8 <= raw.length;) {
|
|
83
|
+
const size = raw.readUInt32BE(offset + 4);
|
|
84
|
+
const payload = raw.subarray(offset + 8, offset + 8 + size);
|
|
85
|
+
(raw[offset] === 2 ? stderr : stdout).push(payload);
|
|
86
|
+
offset += 8 + size;
|
|
87
|
+
}
|
|
88
|
+
return { stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) };
|
|
89
|
+
}
|
|
90
|
+
/** The Engine API on the Unix socket `socketPath`. */
|
|
91
|
+
function makeDockerEngine(socketPath) {
|
|
92
|
+
const send = (call, headers = {}) => {
|
|
93
|
+
const query = new URLSearchParams();
|
|
94
|
+
for (const [key, value] of Object.entries(call.query ?? {})) {
|
|
95
|
+
for (const each of typeof value === "string" ? [value] : value)
|
|
96
|
+
query.append(key, each);
|
|
97
|
+
}
|
|
98
|
+
const body = call.body === undefined ? undefined : JSON.stringify(call.body);
|
|
99
|
+
const request = httpRequest({
|
|
100
|
+
socketPath,
|
|
101
|
+
method: call.method,
|
|
102
|
+
path: `/${API_VERSION}${call.path}${query.size ? `?${query}` : ""}`,
|
|
103
|
+
headers: body === undefined ? headers : { ...headers, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
104
|
+
});
|
|
105
|
+
request.end(body);
|
|
106
|
+
return request;
|
|
107
|
+
};
|
|
108
|
+
const readAll = (call, stream) => Effect.tryPromise({ try: () => buffer(stream), catch: (cause) => unreachable(call, cause) });
|
|
109
|
+
/** Docker's refusal of `call`: the status of `response`, and the message its body holds. */
|
|
110
|
+
const refusal = (call, response) => readAll(call, response).pipe(Effect.flatMap((body) => {
|
|
111
|
+
const status = response.statusCode ?? 0;
|
|
112
|
+
const text = body.toString("utf8").trim();
|
|
113
|
+
let reason = text || STATUS_CODES[status] || "no message";
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(text);
|
|
116
|
+
if (typeof parsed === "object" && parsed !== null && "message" in parsed && typeof parsed.message === "string")
|
|
117
|
+
reason = parsed.message;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Not JSON: the text is the message.
|
|
121
|
+
}
|
|
122
|
+
return Effect.fail(new DockerError({ call: describe(call), status, reason }));
|
|
123
|
+
}));
|
|
124
|
+
/** The response to `call`, open while the scope is; a status of 400 or over is its refusal (304, already so, is not). */
|
|
125
|
+
const open = (call) => Effect.acquireRelease(Effect.callback((resume) => {
|
|
126
|
+
const request = send(call);
|
|
127
|
+
request.on("response", (response) => resume(Effect.succeed(response)));
|
|
128
|
+
request.on("error", (cause) => resume(Effect.fail(unreachable(call, cause))));
|
|
129
|
+
return Effect.sync(() => request.destroy());
|
|
130
|
+
}), (response) => Effect.sync(() => response.destroy())).pipe(Effect.tap((response) => ((response.statusCode ?? 0) >= 400 ? refusal(call, response) : Effect.void)));
|
|
131
|
+
const body = (call) => Effect.scoped(Effect.flatMap(open(call), (response) => readAll(call, response)));
|
|
132
|
+
/** The JSON Docker answers `call` with, as the Engine API documents it. */
|
|
133
|
+
const json = (call) => Effect.map(body(call), (content) => JSON.parse(content.toString("utf8")));
|
|
134
|
+
const orNull = (effect) => effect.pipe(Effect.catchIf(hasStatus(404), () => Effect.succeed(null)));
|
|
135
|
+
const orGone = (effect) => effect.pipe(Effect.asVoid, Effect.catchIf(hasStatus(404), () => Effect.void));
|
|
136
|
+
const name = (value) => encodeURIComponent(value);
|
|
137
|
+
/**
|
|
138
|
+
* Starts exec `id` attached, `stdin` written to it and then closed, and collects what it
|
|
139
|
+
* writes until it ends. Docker upgrades the connection to a raw stream for this.
|
|
140
|
+
*/
|
|
141
|
+
const attach = (id, stdin) => {
|
|
142
|
+
const call = { method: "POST", path: `/exec/${id}/start`, body: { Detach: false, Tty: false } };
|
|
143
|
+
return Effect.callback((resume) => {
|
|
144
|
+
const request = send(call, { Connection: "Upgrade", Upgrade: "tcp" });
|
|
145
|
+
let stream = null;
|
|
146
|
+
request.on("upgrade", (_response, socket, head) => {
|
|
147
|
+
stream = socket;
|
|
148
|
+
const chunks = [head];
|
|
149
|
+
socket.on("data", (chunk) => chunks.push(chunk));
|
|
150
|
+
socket.on("error", (cause) => resume(Effect.fail(unreachable(call, cause))));
|
|
151
|
+
socket.on("close", () => resume(Effect.succeed(Buffer.concat(chunks))));
|
|
152
|
+
if (stdin !== undefined)
|
|
153
|
+
socket.end(stdin);
|
|
154
|
+
});
|
|
155
|
+
// A daemon that answers without upgrading refused.
|
|
156
|
+
request.on("response", (response) => resume(refusal(call, response)));
|
|
157
|
+
request.on("error", (cause) => resume(Effect.fail(unreachable(call, cause))));
|
|
158
|
+
return Effect.sync(() => {
|
|
159
|
+
request.destroy();
|
|
160
|
+
stream?.destroy();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
const exec = Effect.fnUntraced(function* (container, command, { stdin } = {}) {
|
|
165
|
+
const { Id: id } = yield* json({
|
|
166
|
+
method: "POST",
|
|
167
|
+
path: `/containers/${name(container)}/exec`,
|
|
168
|
+
body: { AttachStdin: stdin !== undefined, AttachStdout: true, AttachStderr: true, Tty: false, Cmd: command },
|
|
169
|
+
});
|
|
170
|
+
const { stdout, stderr } = demultiplex(yield* attach(id, stdin));
|
|
171
|
+
// Docker records the exit code before it closes the exec's streams.
|
|
172
|
+
const inspect = { method: "GET", path: `/exec/${id}/json` };
|
|
173
|
+
const { ExitCode: exitCode } = yield* json(inspect);
|
|
174
|
+
if (exitCode === null)
|
|
175
|
+
return yield* new DockerError({ call: describe(inspect), reason: `${JSON.stringify(command[0])} has no exit code` });
|
|
176
|
+
return { exitCode, stdout, stderr: stderr.toString("utf8") };
|
|
177
|
+
});
|
|
178
|
+
const pullImage = Effect.fnUntraced(function* (reference) {
|
|
179
|
+
const call = { method: "POST", path: "/images/create", query: { fromImage: reference } };
|
|
180
|
+
// A pull that fails once it has begun still answers 200: its progress, one JSON object
|
|
181
|
+
// a line, ends with the error.
|
|
182
|
+
const progress = (yield* body(call)).toString("utf8").split("\n").filter((line) => line.trim());
|
|
183
|
+
for (const line of progress) {
|
|
184
|
+
const { error } = JSON.parse(line);
|
|
185
|
+
if (error)
|
|
186
|
+
return yield* new DockerError({ call: describe(call), reason: error });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const readFile = Effect.fnUntraced(function* (container, path) {
|
|
190
|
+
const call = { method: "GET", path: `/containers/${name(container)}/archive`, query: { path } };
|
|
191
|
+
const file = firstFile(yield* body(call));
|
|
192
|
+
if (!file)
|
|
193
|
+
return yield* new DockerError({ call: describe(call), reason: `${path} in ${container} is not a file` });
|
|
194
|
+
return file;
|
|
195
|
+
});
|
|
196
|
+
return DockerEngine.of({
|
|
197
|
+
version: json({ method: "GET", path: "/version" }),
|
|
198
|
+
// A reference as it is: the route takes its slashes and colons.
|
|
199
|
+
inspectImage: (reference) => orNull(json({ method: "GET", path: `/images/${reference}/json` })),
|
|
200
|
+
pullImage,
|
|
201
|
+
inspectNetwork: (network) => orNull(json({ method: "GET", path: `/networks/${name(network)}` })),
|
|
202
|
+
listNetworks: json({ method: "GET", path: "/networks" }),
|
|
203
|
+
createNetwork: (network) => Effect.asVoid(body({ method: "POST", path: "/networks/create", body: network })),
|
|
204
|
+
removeNetwork: (network) => orGone(body({ method: "DELETE", path: `/networks/${name(network)}` })),
|
|
205
|
+
disconnectNetwork: (network, container) => Effect.asVoid(body({ method: "POST", path: `/networks/${name(network)}/disconnect`, body: { Container: container, Force: true } })),
|
|
206
|
+
createVolume: (volume) => Effect.asVoid(body({ method: "POST", path: "/volumes/create", body: volume })),
|
|
207
|
+
listVolumes: (labels) => json({ method: "GET", path: "/volumes", query: { filters: labelFilter(labels) } }).pipe(Effect.map(({ Volumes }) => Volumes ?? [])),
|
|
208
|
+
removeVolume: (volume) => orGone(body({ method: "DELETE", path: `/volumes/${name(volume)}` })),
|
|
209
|
+
listContainers: (labels) => json({ method: "GET", path: "/containers/json", query: { all: "true", filters: labelFilter(labels) } }),
|
|
210
|
+
inspectContainer: (container) => orNull(json({ method: "GET", path: `/containers/${name(container)}/json` })),
|
|
211
|
+
createContainer: (container, spec) => Effect.asVoid(body({ method: "POST", path: "/containers/create", query: { name: container }, body: spec })),
|
|
212
|
+
startContainer: (container) => Effect.asVoid(body({ method: "POST", path: `/containers/${name(container)}/start` })),
|
|
213
|
+
stopContainer: (container) => Effect.asVoid(body({ method: "POST", path: `/containers/${name(container)}/stop` })),
|
|
214
|
+
removeContainer: (container) => orGone(body({ method: "DELETE", path: `/containers/${name(container)}`, query: { force: "true" } })),
|
|
215
|
+
exec,
|
|
216
|
+
readFile,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=docker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker.js","sourceRoot":"","sources":["../../../../src/cluster/docker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAA4C,OAAO,IAAI,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE3G,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAE5E,MAAM,WAAW,GAAG,OAAO,CAAC;AAC5B,MAAM,cAAc,GAAG,sBAAsB,CAAC;AAE9C,qGAAqG;AACrG,MAAM,OAAO,WAAY,SAAQ,MAAM,CAAC,WAAW,EAAe,CAAC,aAAa,EAAE;IAChF,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;IACtC,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC,MAAM;CACtB,CAAC;IACA,IAAa,OAAO;QAClB,OAAO,UAAU,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;IACtH,CAAC;CACF;AAED,2EAA2E;AAC3E,MAAM,OAAO,qBAAsB,SAAQ,MAAM,CAAC,WAAW,EAAyB,CAAC,uBAAuB,EAAE;IAC9G,IAAI,EAAE,MAAM,CAAC,MAAM;CACpB,CAAC;IACA,IAAa,OAAO;QAClB,OAAO,kBAAkB,IAAI,CAAC,IAAI,mEAAmE,CAAC;IACxG,CAAC;CACF;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,CAAC,KAAkB,EAAW,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC;AAEtG,8DAA8D;AAC9D,MAAM,UAAU,YAAY,CAAC,GAAgC;IAC3D,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IACjD,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IACpF,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,qBAAqB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAC1D,CAAC;AAgGD,yFAAyF;AACzF,MAAM,OAAO,YAAa,SAAQ,OAAO,CAAC,OAAO,EAyC7C,CAAC,6BAA6B,CAAC;IACjC,6CAA6C;IAC7C,MAAM,CAAU,KAAK,GAAG,CAAC,GAAG,GAAgC,OAAO,CAAC,GAAG,EAAoD,EAAE,CAC3H,KAAK,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC;CAClG;AAUD,MAAM,QAAQ,GAAG,CAAC,IAAgB,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;AAE7E,MAAM,WAAW,GAAG,CAAC,IAAgB,EAAE,KAAc,EAAe,EAAE,CACpE,IAAI,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAE5G,qFAAqF;AACrF,MAAM,WAAW,GAAG,CAAC,MAAwC,EAAU,EAAE,CACvE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAE7F,qIAAqI;AACrI,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,KAAK,GAAG,GAAG,CAAC;IAClB,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QACnD,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC;QACpD,kCAAkC;QAClC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;QAC1G,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;QAC7B,8CAA8C;QAC9C,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC;QAC1E,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;IACnD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAC/C,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;QAC5D,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpD,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;AAC1E,CAAC;AAED,sDAAsD;AACtD,SAAS,gBAAgB,CAAC,UAAkB;IAC1C,MAAM,IAAI,GAAG,CAAC,IAAgB,EAAE,OAAO,GAAqC,EAAE,EAAiB,EAAE;QAC/F,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;QACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;YAC5D,KAAK,MAAM,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1F,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7E,MAAM,OAAO,GAAG,WAAW,CAAC;YAC1B,UAAU;YACV,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,IAAI,EAAE,IAAI,WAAW,GAAG,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;YACnE,OAAO,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE;SACtI,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,IAAgB,EAAE,MAAgB,EAAsC,EAAE,CACzF,MAAM,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;IAE/F,4FAA4F;IAC5F,MAAM,OAAO,GAAG,CAAC,IAAgB,EAAE,QAAyB,EAAqC,EAAE,CACjG,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,IAAI,CAC1B,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACtB,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,IAAI,MAAM,GAAG,IAAI,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC;QAC1D,IAAI,CAAC;YACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,SAAS,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;gBAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;QAC1I,CAAC;QAAC,MAAM,CAAC;YACP,qCAAqC;QACvC,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,CAAC,CACH,CAAC;IAEJ,yHAAyH;IACzH,MAAM,IAAI,GAAG,CAAC,IAAgB,EAA4D,EAAE,CAC1F,MAAM,CAAC,cAAc,CACnB,MAAM,CAAC,QAAQ,CAA+B,CAAC,MAAM,EAAE,EAAE;QACvD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACvE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9E,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,EACF,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CACpD,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAEhH,MAAM,IAAI,GAAG,CAAC,IAAgB,EAAsC,EAAE,CACpE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEnF,2EAA2E;IAC3E,MAAM,IAAI,GAAG,CAAI,IAAgB,EAAiC,EAAE,CAClE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAM,CAAC,CAAC;IAEjF,MAAM,MAAM,GAAG,CAAI,MAAqC,EAAwC,EAAE,CAChG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAE1E,MAAM,MAAM,GAAG,CAAC,MAA2C,EAAoC,EAAE,CAC/F,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAEhF,MAAM,IAAI,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAElE;;;OAGG;IACH,MAAM,MAAM,GAAG,CAAC,EAAU,EAAE,KAA2B,EAAsC,EAAE;QAC7F,MAAM,IAAI,GAAe,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC;QAC5G,OAAO,MAAM,CAAC,QAAQ,CAAsB,CAAC,MAAM,EAAE,EAAE;YACrD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,IAAI,MAAM,GAAkB,IAAI,CAAC;YACjC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;gBAChD,MAAM,GAAG,MAAM,CAAC;gBAChB,MAAM,MAAM,GAAa,CAAC,IAAI,CAAC,CAAC;gBAChC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBACzD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC7E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;gBACxE,IAAI,KAAK,KAAK,SAAS;oBAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC7C,CAAC,CAAC,CAAC;YACH,mDAAmD;YACnD,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;YACtE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9E,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACtB,OAAO,CAAC,OAAO,EAAE,CAAC;gBAClB,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACtC,SAAiB,EACjB,OAA0B,EAC1B,EAAE,KAAK,EAAE,GAAgB,EAAE;QAE3B,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAA0B;YACtD,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,OAAO;YAC3C,IAAI,EAAE,EAAE,WAAW,EAAE,KAAK,KAAK,SAAS,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE;SAC7G,CAAC,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;QACjE,oEAAoE;QACpE,MAAM,OAAO,GAAe,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;QACxE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAuC,OAAO,CAAC,CAAC;QAC1F,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC,CAAC,IAAI,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,mBAAmB,EAAE,CAAC,CAAC;QAC5I,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAC,SAAiB;QAC7D,MAAM,IAAI,GAAe,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,CAAC;QACrG,uFAAuF;QACvF,+BAA+B;QAC/B,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAChG,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgC,CAAC;YAClE,IAAI,KAAK;gBAAE,OAAO,KAAK,CAAC,CAAC,IAAI,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAC,SAAiB,EAAE,IAAY;QAC1E,MAAM,IAAI,GAAe,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QAC5G,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC,CAAC,IAAI,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,OAAO,SAAS,gBAAgB,EAAE,CAAC,CAAC;QACpH,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,OAAO,YAAY,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QAClD,gEAAgE;QAChE,YAAY,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,SAAS,OAAO,EAAE,CAAC,CAAC;QAC/F,SAAS;QACT,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QAChG,YAAY,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QACxD,aAAa,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5G,aAAa,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QAClG,iBAAiB,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,CACxC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACrI,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACxG,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CACtB,IAAI,CAAiD,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CACrI,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,CAC3C;QACH,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9F,cAAc,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACnI,gBAAgB,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7G,eAAe,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACjJ,cAAc,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;QACpH,aAAa,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;QAClH,eAAe,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACpI,IAAI;QACJ,QAAQ;KACT,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local cluster: k3s in Docker, made through Docker's Engine API (./docker.ts) from
|
|
3
|
+
* alasio's node image (cluster/node), which is k3s with gVisor registered as containerd's
|
|
4
|
+
* `runsc` runtime and an entrypoint that readies its container for k3s.
|
|
5
|
+
*
|
|
6
|
+
* A cluster NAME is a bridge network NAME, a server container NAME-server-0 and agent
|
|
7
|
+
* containers NAME-agent-I, each with named volumes for what k3s, kubelet and the CNI
|
|
8
|
+
* keep (NAME-server-0-k3s and so on), so a node made anew, as when its image or settings
|
|
9
|
+
* change, keeps its state. All of it is labelled as the cluster's, and nothing that is
|
|
10
|
+
* not is changed. The nodes have fixed addresses in the network's subnet: the server the
|
|
11
|
+
* second, after the gateway, and the agents those after it. Docker gives fixed addresses
|
|
12
|
+
* only in a subnet chosen for the network, so one is chosen when none is given: the first
|
|
13
|
+
* 172.30.N.0/24 no other network overlaps.
|
|
14
|
+
*
|
|
15
|
+
* The registries the nodes pull from are k3s's registries.yaml, which the node image's
|
|
16
|
+
* entrypoint writes from the variable each node is made with, so a node is made anew when
|
|
17
|
+
* they change, as when its other settings do.
|
|
18
|
+
*
|
|
19
|
+
* The API server is published on the loopback only. What is done in the cluster itself,
|
|
20
|
+
* waiting for it and configuring it, is done with the kubectl k3s carries in the server
|
|
21
|
+
* node, so this machine needs none of Kubernetes' tools.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
24
|
+
import { dirname } from "node:path";
|
|
25
|
+
import { KubeConfig } from "@kubernetes/client-node";
|
|
26
|
+
import { Context, Duration, Effect, FileSystem, Layer, Schedule, Schema } from "effect";
|
|
27
|
+
import { imageReference } from "../images.js";
|
|
28
|
+
import { NODE_IMAGE } from "../release.js";
|
|
29
|
+
import { DockerEngine } from "./docker.js";
|
|
30
|
+
/** The cluster cannot be made as asked: something of its name is not its own, or its settings cannot hold. */
|
|
31
|
+
export class ClusterUnusable extends Schema.TaggedError()("ClusterUnusable", {
|
|
32
|
+
cluster: Schema.String,
|
|
33
|
+
reason: Schema.String,
|
|
34
|
+
}) {
|
|
35
|
+
get message() {
|
|
36
|
+
return `cluster ${this.cluster} cannot be made: ${this.reason}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The cluster did not come up in time: what it was waited for, and why it last was not. */
|
|
40
|
+
export class ClusterNotReady extends Schema.TaggedError()("ClusterNotReady", {
|
|
41
|
+
cluster: Schema.String,
|
|
42
|
+
waitingFor: Schema.String,
|
|
43
|
+
/** How long it was given, in seconds. */
|
|
44
|
+
within: Schema.Number,
|
|
45
|
+
reason: Schema.String,
|
|
46
|
+
}) {
|
|
47
|
+
get message() {
|
|
48
|
+
return `cluster ${this.cluster} was not ready within ${this.within}s, waiting for ${this.waitingFor}: ${this.reason}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** What a wait for the cluster has not seen yet; the wait's ClusterNotReady says it when it gives up. */
|
|
52
|
+
class NotYet extends Schema.TaggedError()("NotYet", {
|
|
53
|
+
message: Schema.String,
|
|
54
|
+
}) {
|
|
55
|
+
}
|
|
56
|
+
/** A command run in a node exited with another code than 0. */
|
|
57
|
+
export class NodeCommandFailed extends Schema.TaggedError()("NodeCommandFailed", {
|
|
58
|
+
node: Schema.String,
|
|
59
|
+
command: Schema.String,
|
|
60
|
+
exitCode: Schema.Number,
|
|
61
|
+
stderr: Schema.String,
|
|
62
|
+
}) {
|
|
63
|
+
get message() {
|
|
64
|
+
return `${this.command} in ${this.node} exited with ${this.exitCode}${this.stderr.trim() ? `: ${this.stderr.trim()}` : ""}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** The local cluster of the options its layer was given. */
|
|
68
|
+
export class LocalCluster extends Context.Service()("alasio/cluster/LocalCluster") {
|
|
69
|
+
static layer = (options) => Layer.effect(LocalCluster, makeLocalCluster(options));
|
|
70
|
+
}
|
|
71
|
+
const CLUSTER_LABEL = "alasio.cluster";
|
|
72
|
+
const ROLE_LABEL = "alasio.role";
|
|
73
|
+
/** The label of a node's container that says what it was made from: specHash's. */
|
|
74
|
+
const SPEC_LABEL = "alasio.spec";
|
|
75
|
+
/** What a node keeps, each in a volume of its own: the volume's suffix, and where it is mounted. */
|
|
76
|
+
const NODE_VOLUMES = [
|
|
77
|
+
["k3s", "/var/lib/rancher/k3s"],
|
|
78
|
+
["kubelet", "/var/lib/kubelet"],
|
|
79
|
+
["cni", "/var/lib/cni"],
|
|
80
|
+
["log", "/var/log"],
|
|
81
|
+
];
|
|
82
|
+
/** The API server's port in the server node. */
|
|
83
|
+
const K3S_PORT = 6443;
|
|
84
|
+
const API_PORT = `${K3S_PORT}/tcp`;
|
|
85
|
+
const K3S_KUBECONFIG = "/etc/rancher/k3s/k3s.yaml";
|
|
86
|
+
/** The token the server made on its first start, which agents join with. */
|
|
87
|
+
const K3S_TOKEN = "/var/lib/rancher/k3s/server/token";
|
|
88
|
+
/** The variable the node image's entrypoint writes k3s's registries.yaml from, before k3s starts. */
|
|
89
|
+
const REGISTRIES_ENV = "ALASIO_REGISTRIES";
|
|
90
|
+
/**
|
|
91
|
+
* kubelet's thresholds for a machine whose disk the cluster shares with everything else
|
|
92
|
+
* on it: it evicts only when the disk is nearly full, and collects unused images only then,
|
|
93
|
+
* so images in use are not evicted.
|
|
94
|
+
*/
|
|
95
|
+
const KUBELET_ARGS = [
|
|
96
|
+
"--kubelet-arg=eviction-hard=imagefs.available<5%,nodefs.available<5%",
|
|
97
|
+
"--kubelet-arg=eviction-minimum-reclaim=imagefs.available=1%,nodefs.available=1%",
|
|
98
|
+
"--kubelet-arg=image-gc-high-threshold=98",
|
|
99
|
+
"--kubelet-arg=image-gc-low-threshold=95",
|
|
100
|
+
];
|
|
101
|
+
/** gVisor, as the node image registers it with containerd. */
|
|
102
|
+
const GVISOR_RUNTIME_CLASS = { apiVersion: "node.k8s.io/v1", kind: "RuntimeClass", metadata: { name: "gvisor" }, handler: "runsc" };
|
|
103
|
+
// A first start pulls k3s's own images, so it is given minutes.
|
|
104
|
+
const READY_TIMEOUT = "5 minutes";
|
|
105
|
+
const POLL = "1 second";
|
|
106
|
+
/** The `index`th address of the IPv4 subnet `cidr` (a.b.c.d/n), its network address the 0th; null when it has no such host address. */
|
|
107
|
+
export function subnetAddress(cidr, index) {
|
|
108
|
+
const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/u.exec(cidr);
|
|
109
|
+
if (!match)
|
|
110
|
+
return null;
|
|
111
|
+
const octets = match.slice(1, 5).map(Number);
|
|
112
|
+
const prefix = Number(match[5]);
|
|
113
|
+
if (octets.some((octet) => octet > 255) || prefix > 30)
|
|
114
|
+
return null;
|
|
115
|
+
// The last address is the subnet's broadcast address.
|
|
116
|
+
if (index < 1 || index >= 2 ** (32 - prefix) - 1)
|
|
117
|
+
return null;
|
|
118
|
+
const network = octets.reduce((value, octet) => value * 256 + octet, 0);
|
|
119
|
+
const address = network - (network % 2 ** (32 - prefix)) + index;
|
|
120
|
+
return [24, 16, 8, 0].map((shift) => Math.floor(address / 2 ** shift) % 256).join(".");
|
|
121
|
+
}
|
|
122
|
+
/** The first and last addresses of the IPv4 subnet `cidr`, as numbers; null when it is none. */
|
|
123
|
+
function subnetRange(cidr) {
|
|
124
|
+
const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/u.exec(cidr);
|
|
125
|
+
if (!match)
|
|
126
|
+
return null;
|
|
127
|
+
const octets = match.slice(1, 5).map(Number);
|
|
128
|
+
const prefix = Number(match[5]);
|
|
129
|
+
if (octets.some((octet) => octet > 255) || prefix > 32)
|
|
130
|
+
return null;
|
|
131
|
+
const size = 2 ** (32 - prefix);
|
|
132
|
+
const first = octets.reduce((value, octet) => value * 256 + octet, 0);
|
|
133
|
+
return [first - (first % size), first - (first % size) + size - 1];
|
|
134
|
+
}
|
|
135
|
+
/** The first 172.30.N.0/24 that overlaps none of the `taken` subnets (others than IPv4 are not in the way), or null when all do. Pure, for tests. */
|
|
136
|
+
export function freeSubnet(taken) {
|
|
137
|
+
const ranges = taken.flatMap((cidr) => {
|
|
138
|
+
const range = subnetRange(cidr);
|
|
139
|
+
return range ? [range] : [];
|
|
140
|
+
});
|
|
141
|
+
for (let third = 0; third < 256; third++) {
|
|
142
|
+
const candidate = `172.30.${third}.0/24`;
|
|
143
|
+
// A /24 written as such always has a range.
|
|
144
|
+
const [first, last] = subnetRange(candidate);
|
|
145
|
+
if (!ranges.some(([start, end]) => start <= last && first <= end))
|
|
146
|
+
return candidate;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* CoreDNS's NodeHosts, a hosts file, with `aliases`: k3s keeps a line for each node in
|
|
152
|
+
* it, which stays, and every other line is the cluster's aliases, which are replaced.
|
|
153
|
+
* Pure, for tests.
|
|
154
|
+
*/
|
|
155
|
+
export function nodeHostsWith(nodeHosts, nodes, aliases) {
|
|
156
|
+
const kept = nodeHosts.split("\n").filter((line) => line.trim().split(/\s+/u).slice(1).some((host) => nodes.includes(host)));
|
|
157
|
+
return [...kept, ...aliases.map(({ ip, hostnames }) => `${ip} ${hostnames.join(" ")}`)].join("\n");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The kubeconfig k3s wrote in the server node (`k3sYaml`), named after the cluster and
|
|
161
|
+
* pointed at the API server's port on the loopback; null when it is not a kubeconfig with
|
|
162
|
+
* a cluster and a user, as while k3s writes it.
|
|
163
|
+
*/
|
|
164
|
+
export function localKubeconfig(k3sYaml, cluster, apiPort) {
|
|
165
|
+
const written = new KubeConfig();
|
|
166
|
+
try {
|
|
167
|
+
written.loadFromString(k3sYaml);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const server = written.getCurrentCluster();
|
|
173
|
+
const user = written.getCurrentUser();
|
|
174
|
+
if (!server || !user)
|
|
175
|
+
return null;
|
|
176
|
+
const local = new KubeConfig();
|
|
177
|
+
local.loadFromOptions({
|
|
178
|
+
clusters: [{ ...server, name: cluster, server: `https://127.0.0.1:${apiPort}` }],
|
|
179
|
+
users: [{ ...user, name: cluster }],
|
|
180
|
+
contexts: [{ name: cluster, cluster, user: cluster }],
|
|
181
|
+
currentContext: cluster,
|
|
182
|
+
});
|
|
183
|
+
return local.exportConfig();
|
|
184
|
+
}
|
|
185
|
+
/** k3s's registries.yaml of `registries`, written as JSON, which YAML reads as it is. Pure, for tests. */
|
|
186
|
+
export function registriesYaml({ mirrors = {}, configs = {} }) {
|
|
187
|
+
return JSON.stringify({
|
|
188
|
+
mirrors,
|
|
189
|
+
configs: Object.fromEntries(Object.entries(configs).map(([host, { tls }]) => [
|
|
190
|
+
host,
|
|
191
|
+
{ tls: { ca_file: tls.caFile, cert_file: tls.certFile, key_file: tls.keyFile, insecure_skip_verify: tls.insecureSkipVerify } },
|
|
192
|
+
])),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/** A digest of `spec` that two specs share only when they are the same. */
|
|
196
|
+
const specHash = (spec) => createHash("sha256").update(JSON.stringify(spec)).digest("hex");
|
|
197
|
+
const makeLocalCluster = Effect.fnUntraced(function* ({ name: cluster, apiPort, storagePath, image = imageReference(NODE_IMAGE), subnet, hostAliases = [], mounts = [], agents = 0, registries, readyTimeout = READY_TIMEOUT, poll = POLL, }) {
|
|
198
|
+
const docker = yield* DockerEngine;
|
|
199
|
+
const fs = yield* FileSystem.FileSystem;
|
|
200
|
+
const labels = { [CLUSTER_LABEL]: cluster };
|
|
201
|
+
const server = `${cluster}-server-0`;
|
|
202
|
+
const nodes = [server, ...Array.from({ length: agents }, (_, index) => `${cluster}-agent-${index}`)];
|
|
203
|
+
const unusable = (reason) => new ClusterUnusable({ cluster, reason });
|
|
204
|
+
/**
|
|
205
|
+
* The container of `node`, with what every node has: privileged, as k3s in Docker needs,
|
|
206
|
+
* its volumes, the storage directory and the host mounts, and the registries.
|
|
207
|
+
*/
|
|
208
|
+
const nodeContainer = (node, role, address, command, env) => ({
|
|
209
|
+
Image: image,
|
|
210
|
+
Hostname: node,
|
|
211
|
+
Cmd: command,
|
|
212
|
+
Env: [...env, ...(registries ? [`${REGISTRIES_ENV}=${registriesYaml(registries)}`] : [])],
|
|
213
|
+
Labels: { ...labels, [ROLE_LABEL]: role },
|
|
214
|
+
...(role === "server" ? { ExposedPorts: { [API_PORT]: {} } } : {}),
|
|
215
|
+
HostConfig: {
|
|
216
|
+
Privileged: true,
|
|
217
|
+
Init: true,
|
|
218
|
+
CgroupnsMode: "private",
|
|
219
|
+
RestartPolicy: { Name: "unless-stopped" },
|
|
220
|
+
SecurityOpt: ["label=disable"],
|
|
221
|
+
Tmpfs: { "/run": "", "/var/run": "" },
|
|
222
|
+
Mounts: [
|
|
223
|
+
...NODE_VOLUMES.map(([suffix, target]) => ({ Type: "volume", Source: `${node}-${suffix}`, Target: target })),
|
|
224
|
+
{ Type: "bind", Source: storagePath, Target: storagePath },
|
|
225
|
+
...mounts.map(({ source, target, readOnly = false }) => ({ Type: "bind", Source: source, Target: target, ReadOnly: readOnly })),
|
|
226
|
+
],
|
|
227
|
+
ExtraHosts: hostAliases.flatMap(({ ip, hostnames }) => hostnames.map((hostname) => `${hostname}:${ip}`)),
|
|
228
|
+
...(role === "server" ? { PortBindings: { [API_PORT]: [{ HostIp: "127.0.0.1", HostPort: String(apiPort) }] } } : {}),
|
|
229
|
+
},
|
|
230
|
+
NetworkingConfig: { EndpointsConfig: { [cluster]: { IPAMConfig: { IPv4Address: address } } } },
|
|
231
|
+
});
|
|
232
|
+
/** The cluster's network, made unless it exists; its subnet, which must be `subnet` when that is given. */
|
|
233
|
+
const ensureNetwork = Effect.fnUntraced(function* () {
|
|
234
|
+
const existing = yield* docker.inspectNetwork(cluster);
|
|
235
|
+
if (existing && existing.Labels?.[CLUSTER_LABEL] !== cluster)
|
|
236
|
+
return yield* unusable(`a network named ${cluster} is not its own`);
|
|
237
|
+
let network = existing;
|
|
238
|
+
if (!network) {
|
|
239
|
+
const chosen = subnet ?? freeSubnet((yield* docker.listNetworks).flatMap(({ IPAM }) => (IPAM.Config ?? []).flatMap(({ Subnet }) => Subnet ?? [])));
|
|
240
|
+
if (!chosen)
|
|
241
|
+
return yield* unusable("every 172.30.N.0/24 is another network's: give the cluster a subnet");
|
|
242
|
+
yield* Effect.logInfo(`making network ${cluster} (${chosen})`);
|
|
243
|
+
yield* docker.createNetwork({ Name: cluster, Driver: "bridge", IPAM: { Config: [{ Subnet: chosen }] }, Labels: labels });
|
|
244
|
+
network = yield* docker.inspectNetwork(cluster);
|
|
245
|
+
}
|
|
246
|
+
const actual = network?.IPAM.Config?.[0]?.Subnet;
|
|
247
|
+
if (!actual)
|
|
248
|
+
return yield* unusable(`network ${cluster} has no subnet`);
|
|
249
|
+
if (subnet && actual !== subnet)
|
|
250
|
+
return yield* unusable(`network ${cluster} has the subnet ${actual}, not ${subnet}`);
|
|
251
|
+
return actual;
|
|
252
|
+
});
|
|
253
|
+
/** The `index`th node's address in `networkSubnet`. */
|
|
254
|
+
const nodeAddress = (networkSubnet, index) => {
|
|
255
|
+
const address = subnetAddress(networkSubnet, 2 + index);
|
|
256
|
+
return address ? Effect.succeed(address) : Effect.fail(unusable(`its subnet ${networkSubnet} has no room for ${nodes.length} nodes`));
|
|
257
|
+
};
|
|
258
|
+
/** `node`'s container made as `spec` says, anew when it was made otherwise, and running. */
|
|
259
|
+
const ensureNode = Effect.fnUntraced(function* (node, spec) {
|
|
260
|
+
const hash = specHash(spec);
|
|
261
|
+
const existing = yield* docker.inspectContainer(node);
|
|
262
|
+
if (existing && existing.Config.Labels?.[CLUSTER_LABEL] !== cluster)
|
|
263
|
+
return yield* unusable(`a container named ${node} is not one of its nodes`);
|
|
264
|
+
const current = existing !== null && existing.Config.Labels?.[SPEC_LABEL] === hash;
|
|
265
|
+
if (existing && !current) {
|
|
266
|
+
yield* Effect.logInfo(`making node ${node} anew, from its current image and settings`);
|
|
267
|
+
yield* docker.stopContainer(node);
|
|
268
|
+
yield* docker.removeContainer(node);
|
|
269
|
+
}
|
|
270
|
+
if (!current) {
|
|
271
|
+
yield* Effect.logInfo(`making node ${node}`);
|
|
272
|
+
yield* Effect.forEach(NODE_VOLUMES, ([suffix]) => docker.createVolume({ Name: `${node}-${suffix}`, Labels: labels }), { discard: true });
|
|
273
|
+
yield* docker.createContainer(node, { ...spec, Labels: { ...spec.Labels, [SPEC_LABEL]: hash } });
|
|
274
|
+
}
|
|
275
|
+
if (!current || !existing.State.Running) {
|
|
276
|
+
yield* Effect.logInfo(`starting node ${node}`);
|
|
277
|
+
yield* docker.startContainer(node);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
/** The output of `command` in `node`, which fails unless it exits with 0. */
|
|
281
|
+
const inNode = (node, command, options) => docker.exec(node, command, options).pipe(Effect.flatMap(({ exitCode, stdout, stderr }) => exitCode === 0 ? Effect.succeed(stdout) : Effect.fail(new NodeCommandFailed({ node, command: command.join(" "), exitCode, stderr }))));
|
|
282
|
+
const kubectl = (args, options) => inNode(server, ["kubectl", ...args], options);
|
|
283
|
+
/** `check` once it succeeds, tried every `poll` for `readyTimeout`; why it last failed, when it never does. */
|
|
284
|
+
const awaitReady = (waitingFor, check) => Effect.logInfo(`waiting for ${waitingFor}`).pipe(Effect.andThen(check.pipe(Effect.mapError((error) => new ClusterNotReady({ cluster, waitingFor, within: Duration.toSeconds(readyTimeout), reason: error.message })), Effect.retry(Schedule.max([Schedule.spaced(poll), Schedule.during(readyTimeout)])))));
|
|
285
|
+
/** When each node's container last started, in milliseconds since the epoch. */
|
|
286
|
+
const startTimes = Effect.forEach(nodes, (node) => Effect.map(docker.inspectContainer(node), (container) => [node, Date.parse(container?.State.StartedAt ?? "")])).pipe(Effect.map((entries) => new Map(entries)));
|
|
287
|
+
/**
|
|
288
|
+
* Every node ready, by a Ready condition its kubelet reported since its container
|
|
289
|
+
* started: one from before, which a node stopped while ready keeps until its kubelet
|
|
290
|
+
* reports again, is not yet the node's. The condition's heartbeat is to the second.
|
|
291
|
+
*/
|
|
292
|
+
const nodesReady = (started) => kubectl(["get", "nodes", "--output=json"]).pipe(Effect.flatMap((stdout) => {
|
|
293
|
+
const { items } = JSON.parse(stdout.toString("utf8"));
|
|
294
|
+
const ready = new Set(items
|
|
295
|
+
.filter((node) => node.status?.conditions?.some(({ type, status, lastHeartbeatTime }) => type === "Ready" && status === "True" && lastHeartbeatTime !== undefined
|
|
296
|
+
&& new Date(lastHeartbeatTime).getTime() >= Math.floor((started.get(node.metadata?.name ?? "") ?? Infinity) / 1000) * 1000))
|
|
297
|
+
.map((node) => node.metadata?.name));
|
|
298
|
+
const waiting = nodes.filter((node) => !ready.has(node));
|
|
299
|
+
return waiting.length === 0 ? Effect.void : Effect.fail(new NotYet({ message: `not reported ready since started: ${waiting.join(", ")}` }));
|
|
300
|
+
}));
|
|
301
|
+
/** CoreDNS's NodeHosts with the host aliases, patched only when they differ, and only over the version read. */
|
|
302
|
+
const applyHostAliases = Effect.gen(function* () {
|
|
303
|
+
const coredns = JSON.parse((yield* kubectl(["--namespace=kube-system", "get", "configmap", "coredns", "--output=json"])).toString("utf8"));
|
|
304
|
+
const nodeHosts = coredns.data?.["NodeHosts"] ?? "";
|
|
305
|
+
const desired = nodeHostsWith(nodeHosts, nodes, hostAliases);
|
|
306
|
+
if (desired === nodeHosts)
|
|
307
|
+
return;
|
|
308
|
+
const patch = { metadata: { resourceVersion: coredns.metadata?.resourceVersion }, data: { NodeHosts: desired } };
|
|
309
|
+
yield* kubectl(["--namespace=kube-system", "patch", "configmap", "coredns", "--type=merge", `--patch=${JSON.stringify(patch)}`]);
|
|
310
|
+
});
|
|
311
|
+
const writeKubeconfig = Effect.fnUntraced(function* (path) {
|
|
312
|
+
const content = yield* awaitReady("its kubeconfig", docker.readFile(server, K3S_KUBECONFIG).pipe(Effect.flatMap((written) => {
|
|
313
|
+
const local = localKubeconfig(written.toString("utf8"), cluster, apiPort);
|
|
314
|
+
return local ? Effect.succeed(local) : Effect.fail(new NotYet({ message: `${K3S_KUBECONFIG} is not yet a kubeconfig with a cluster and a user` }));
|
|
315
|
+
})));
|
|
316
|
+
// Written whole beside it, readable only by its owner, and moved over it.
|
|
317
|
+
yield* fs.makeDirectory(dirname(path), { recursive: true, mode: 0o700 });
|
|
318
|
+
const written = `${path}.${randomUUID()}`;
|
|
319
|
+
yield* fs.writeFileString(written, content, { flag: "wx", mode: 0o600 });
|
|
320
|
+
yield* fs.rename(written, path);
|
|
321
|
+
yield* Effect.logInfo(`wrote the kubeconfig of cluster ${cluster} to ${path}`);
|
|
322
|
+
});
|
|
323
|
+
const up = Effect.fnUntraced(function* (kubeconfig) {
|
|
324
|
+
if (!(yield* docker.inspectImage(image))) {
|
|
325
|
+
yield* Effect.logInfo(`pulling ${image}`);
|
|
326
|
+
yield* docker.pullImage(image);
|
|
327
|
+
}
|
|
328
|
+
const networkSubnet = yield* ensureNetwork();
|
|
329
|
+
yield* fs.makeDirectory(storagePath, { recursive: true });
|
|
330
|
+
yield* ensureNode(server, nodeContainer(server, "server", yield* nodeAddress(networkSubnet, 0), [
|
|
331
|
+
"server",
|
|
332
|
+
"--disable=traefik",
|
|
333
|
+
// Where the kubeconfig reaches it.
|
|
334
|
+
"--tls-san=127.0.0.1",
|
|
335
|
+
`--default-local-storage-path=${storagePath}`,
|
|
336
|
+
...KUBELET_ARGS,
|
|
337
|
+
], []));
|
|
338
|
+
yield* awaitReady("its API server", kubectl(["get", "--raw=/readyz"]));
|
|
339
|
+
if (agents > 0) {
|
|
340
|
+
const token = (yield* awaitReady("its token", docker.readFile(server, K3S_TOKEN))).toString("utf8").trim();
|
|
341
|
+
for (const [index, agent] of nodes.slice(1).entries()) {
|
|
342
|
+
yield* ensureNode(agent, nodeContainer(agent, "agent", yield* nodeAddress(networkSubnet, 1 + index), ["agent", ...KUBELET_ARGS], [
|
|
343
|
+
`K3S_URL=https://${server}:${K3S_PORT}`,
|
|
344
|
+
`K3S_TOKEN=${token}`,
|
|
345
|
+
]));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
yield* awaitReady("its nodes", Effect.flatMap(startTimes, nodesReady));
|
|
349
|
+
yield* awaitReady("CoreDNS's NodeHosts", applyHostAliases);
|
|
350
|
+
yield* awaitReady("the gvisor RuntimeClass", kubectl(["apply", "--filename=-"], { stdin: JSON.stringify(GVISOR_RUNTIME_CLASS) }));
|
|
351
|
+
yield* writeKubeconfig(kubeconfig);
|
|
352
|
+
yield* Effect.logInfo(`cluster ${cluster} is up`);
|
|
353
|
+
});
|
|
354
|
+
/** The cluster's nodes, the server first. */
|
|
355
|
+
const existingNodes = docker.listContainers(labels).pipe(Effect.map((found) => found
|
|
356
|
+
.map((container) => ({
|
|
357
|
+
name: container.Names[0]?.replace(/^\//u, "") ?? "",
|
|
358
|
+
role: container.Labels[ROLE_LABEL] === "server" ? "server" : "agent",
|
|
359
|
+
state: container.State,
|
|
360
|
+
}))
|
|
361
|
+
.sort((a, b) => (a.role === b.role ? a.name.localeCompare(b.name) : a.role === "server" ? -1 : 1))));
|
|
362
|
+
return LocalCluster.of({
|
|
363
|
+
up,
|
|
364
|
+
// The agents first, so none is left without its server.
|
|
365
|
+
down: existingNodes.pipe(Effect.flatMap((found) => Effect.forEach(found.toReversed(), ({ name }) => docker.stopContainer(name), { discard: true })), Effect.andThen(Effect.logInfo(`stopped cluster ${cluster}`))),
|
|
366
|
+
remove: ({ volumes = false, storage = false } = {}) => Effect.gen(function* () {
|
|
367
|
+
const found = yield* existingNodes;
|
|
368
|
+
if (storage && found.some(({ name }) => name === server)) {
|
|
369
|
+
yield* docker.startContainer(server);
|
|
370
|
+
yield* inNode(server, ["find", storagePath, "-mindepth", "1", "-delete"]);
|
|
371
|
+
}
|
|
372
|
+
yield* Effect.forEach(found, ({ name }) => docker.removeContainer(name), { discard: true });
|
|
373
|
+
const network = yield* docker.inspectNetwork(cluster);
|
|
374
|
+
if (network?.Labels?.[CLUSTER_LABEL] === cluster) {
|
|
375
|
+
// Docker removes no network a container is on: others' (a collector given an
|
|
376
|
+
// address on it, say) are taken off it, and keep running.
|
|
377
|
+
yield* Effect.forEach(Object.values(network.Containers ?? {}), ({ Name }) => docker.disconnectNetwork(cluster, Name).pipe(Effect.andThen(Effect.logInfo(`took ${Name} off network ${cluster}`))), { discard: true });
|
|
378
|
+
yield* docker.removeNetwork(cluster);
|
|
379
|
+
}
|
|
380
|
+
if (volumes)
|
|
381
|
+
yield* Effect.forEach(yield* docker.listVolumes(labels), ({ Name }) => docker.removeVolume(Name), { discard: true });
|
|
382
|
+
if (storage)
|
|
383
|
+
yield* fs.remove(storagePath, { recursive: true, force: true });
|
|
384
|
+
yield* Effect.logInfo(`removed cluster ${cluster}${volumes ? " and its volumes" : ""}${storage ? `, and ${storagePath}` : ""}`);
|
|
385
|
+
}),
|
|
386
|
+
status: Effect.all({
|
|
387
|
+
docker: Effect.map(docker.version, ({ Version }) => Version),
|
|
388
|
+
nodes: existingNodes,
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
//# sourceMappingURL=local.js.map
|