alasio 4.1.4 → 4.2.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.
Files changed (49) hide show
  1. package/dist/cli/src/cluster/docker-engine.js +216 -0
  2. package/dist/cli/src/cluster/docker-engine.js.map +1 -0
  3. package/dist/cli/src/cluster/docker.js +294 -199
  4. package/dist/cli/src/cluster/docker.js.map +1 -1
  5. package/dist/cli/src/cluster/host.js +235 -57
  6. package/dist/cli/src/cluster/host.js.map +1 -1
  7. package/dist/cli/src/cluster/k3s.js +108 -0
  8. package/dist/cli/src/cluster/k3s.js.map +1 -0
  9. package/dist/cli/src/cluster/machine.js +173 -0
  10. package/dist/cli/src/cluster/machine.js.map +1 -0
  11. package/dist/cli/src/cluster/node.js +392 -0
  12. package/dist/cli/src/cluster/node.js.map +1 -0
  13. package/dist/cli/src/cluster/root.js +109 -0
  14. package/dist/cli/src/cluster/root.js.map +1 -0
  15. package/dist/cli/src/cluster/tar.js +32 -0
  16. package/dist/cli/src/cluster/tar.js.map +1 -0
  17. package/dist/cli/src/commands/as-root.js +15 -0
  18. package/dist/cli/src/commands/as-root.js.map +1 -0
  19. package/dist/cli/src/commands/common.js +26 -15
  20. package/dist/cli/src/commands/common.js.map +1 -1
  21. package/dist/cli/src/commands/down.js +9 -4
  22. package/dist/cli/src/commands/down.js.map +1 -1
  23. package/dist/cli/src/commands/init.js +35 -28
  24. package/dist/cli/src/commands/init.js.map +1 -1
  25. package/dist/cli/src/commands/status.js +29 -14
  26. package/dist/cli/src/commands/status.js.map +1 -1
  27. package/dist/cli/src/commands/uninstall.js +23 -9
  28. package/dist/cli/src/commands/uninstall.js.map +1 -1
  29. package/dist/cli/src/commands/up.js +26 -9
  30. package/dist/cli/src/commands/up.js.map +1 -1
  31. package/dist/cli/src/commands/upgrade.js +2 -1
  32. package/dist/cli/src/commands/upgrade.js.map +1 -1
  33. package/dist/cli/src/commands.js +2 -1
  34. package/dist/cli/src/commands.js.map +1 -1
  35. package/dist/cli/src/config.js +55 -27
  36. package/dist/cli/src/config.js.map +1 -1
  37. package/dist/cli/src/kube/api.js +1 -0
  38. package/dist/cli/src/kube/api.js.map +1 -1
  39. package/dist/cli/src/main.js +4 -2
  40. package/dist/cli/src/main.js.map +1 -1
  41. package/dist/cli/src/release.js +6 -6
  42. package/dist/cli/src/release.js.map +1 -1
  43. package/dist/cli/src/target.js +43 -27
  44. package/dist/cli/src/target.js.map +1 -1
  45. package/dist/cluster/node/config-v3.toml.tmpl +12 -0
  46. package/dist/cluster/node/pins.json +15 -0
  47. package/package.json +2 -2
  48. package/dist/cli/src/cluster/local.js +0 -392
  49. package/dist/cli/src/cluster/local.js.map +0 -1
@@ -9,7 +9,9 @@
9
9
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
10
10
  import { Cause, Console, Effect, Layer, Logger, Result } from "effect";
11
11
  import { CliError, Command } from "effect/cli";
12
- import { Machine, Sysctl } from "./cluster/host.js";
12
+ import { NodeReleases, Systemd } from "./cluster/host.js";
13
+ import { Machine, RootSystem } from "./cluster/machine.js";
14
+ import { Root } from "./cluster/root.js";
13
15
  import { alasio } from "./commands.js";
14
16
  import { VERSION } from "./release.js";
15
17
  import { TelegramBotApi } from "./telegram.js";
@@ -27,5 +29,5 @@ const report = (cause) => {
27
29
  }
28
30
  return Console.error(Cause.pretty(cause));
29
31
  };
30
- Command.run(alasio, { version: VERSION }).pipe(Effect.tapCause(report), Effect.provide(Layer.mergeAll(TelegramBotApi.layer, Machine.layer, Sysctl.layer, Logger.layer([progress]))), Effect.provide(NodeServices.layer), NodeRuntime.runMain({ disableErrorReporting: true }));
32
+ Command.run(alasio, { version: VERSION }).pipe(Effect.tapCause(report), Effect.provide(Layer.mergeAll(TelegramBotApi.layer, Root.layer, Systemd.layer, NodeReleases.layer, Logger.layer([progress])).pipe(Layer.provideMerge(Layer.mergeAll(Machine.layer, RootSystem.layer)))), Effect.provide(NodeServices.layer), NodeRuntime.runMain({ disableErrorReporting: true }));
31
33
  //# sourceMappingURL=main.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../../../src/main.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,oDAAoD;AACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;IAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACpG,CAAC,CAAC,CAAC;AAEH,mIAAmI;AACnI,MAAM,MAAM,GAAG,CAAC,KAA2B,EAAuB,EAAE;IAClE,IAAI,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACvD,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,KAAK,CAAC,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACvK,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5C,CAAC,CAAC;AAEF,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,CAC5C,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC3G,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,EAClC,WAAW,CAAC,OAAO,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CACrD,CAAC"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../../src/main.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,oDAAoD;AACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;IAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACpG,CAAC,CAAC,CAAC;AAEH,mIAAmI;AACnI,MAAM,MAAM,GAAG,CAAC,KAA2B,EAAuB,EAAE;IAClE,IAAI,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACvD,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,KAAK,CAAC,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACvK,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5C,CAAC,CAAC;AAEF,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,CAC5C,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EACvB,MAAM,CAAC,OAAO,CACZ,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAChH,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CACpE,CACF,EACD,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,EAClC,WAAW,CAAC,OAAO,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CACrD,CAAC"}
@@ -3,13 +3,13 @@
3
3
  * version and the digests of the images it published here, with tooling/pin-release.ts,
4
4
  * before it builds the package; this module holds nothing else.
5
5
  */
6
- export const VERSION = "4.1.4";
6
+ export const VERSION = "4.2.0";
7
7
  /** alasio's own images, tagged with the version and pinned by digest once released. */
8
8
  export const IMAGES = {
9
- alasio: { repository: "ghcr.io/eaucoin/alasio", tag: VERSION, digest: "sha256:ff53068a47a995ae043580913ce2548ac39ebf6ba3e34fa0f3172f59cb2cbc96" },
10
- agent: { repository: "ghcr.io/eaucoin/alasio-agent", tag: VERSION, digest: "sha256:ce88d4a231478f637787113b011d85cbce2dbc3b90988166c319e37aa374d4a2" },
11
- lake: { repository: "ghcr.io/eaucoin/alasio-lake", tag: VERSION, digest: "sha256:d763265d2fc055c9e2a6f146714ef9e0ae5e2f14f66cce1f331f9acdfc2321c4" },
9
+ alasio: { repository: "ghcr.io/eaucoin/alasio", tag: VERSION, digest: "sha256:21d8eb93752e9de02554f056471b32f7898422fcc7d32be5dc5ff34bc3b870c6" },
10
+ agent: { repository: "ghcr.io/eaucoin/alasio-agent", tag: VERSION, digest: "sha256:477587274974df027da1937a2f6b8558527801e0269e77f5207800ab13ce7ee5" },
11
+ lake: { repository: "ghcr.io/eaucoin/alasio-lake", tag: VERSION, digest: "sha256:a9bb1894ea755fff84bacd1d5411d602359001e30e0e2ab2afcd2924d082d4a9" },
12
12
  };
13
- /** The local cluster's node image: k3s with gVisor (cluster/node), pinned like the others. */
14
- export const NODE_IMAGE = { repository: "ghcr.io/eaucoin/alasio-node", tag: VERSION, digest: "sha256:0fd2a244efaf919da93b4ae635999b087a4750f1f6b042a5ce190c3bf3f3b409" };
13
+ /** The node image of the cluster in Docker: k3s with gVisor (cluster/node), pinned like the others. */
14
+ export const NODE_IMAGE = { repository: "ghcr.io/eaucoin/alasio-node", tag: VERSION, digest: "sha256:39089951ebcea855d8c2d54de6978d128d8dbaa57784212e0480747fa9957a94" };
15
15
  //# sourceMappingURL=release.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"release.js","sourceRoot":"","sources":["../../../src/release.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,uFAAuF;AACvF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,MAAM,EAAE,EAAE,UAAU,EAAE,wBAAwB,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE;IACjJ,KAAK,EAAE,EAAE,UAAU,EAAE,8BAA8B,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE;IACtJ,IAAI,EAAE,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE;CACrJ,CAAC;AAEF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE,CAAC"}
1
+ {"version":3,"file":"release.js","sourceRoot":"","sources":["../../../src/release.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,uFAAuF;AACvF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,MAAM,EAAE,EAAE,UAAU,EAAE,wBAAwB,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE;IACjJ,KAAK,EAAE,EAAE,UAAU,EAAE,8BAA8B,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE;IACtJ,IAAI,EAAE,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE;CACrJ,CAAC;AAEF,uGAAuG;AACvG,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,yEAAyE,EAAE,CAAC"}
@@ -1,53 +1,69 @@
1
1
  /**
2
- * The cluster the operator's config targets (./config.ts): the one alasio makes on this
3
- * machine, whose kubeconfig it writes beside the config file, or one a kubeconfig
4
- * reaches; and the services that reach it.
2
+ * The cluster the operator's config targets (./config.ts): one alasio makes on this
3
+ * machine, k3s on the machine itself or in Docker, whose kubeconfig it writes beside the
4
+ * config file, or one a kubeconfig reaches; and the services that reach it.
5
5
  *
6
- * The local cluster's nodes mount the host profile's paths at their own, as folder
7
- * workspaces mount them from the node, but for those the target's own mounts already
8
- * put there (as a node path of /host/tmp mounts the machine's /tmp).
6
+ * The nodes in Docker mount the host profile's paths at their own, as folder workspaces
7
+ * mount them from the node, but for those the target's own mounts already put there (as
8
+ * a node path of /host/tmp mounts the machine's /tmp). k3s on the machine itself has
9
+ * them where they are.
9
10
  */
10
11
  import { Config, Effect, FileSystem, Layer, Option, Result } from "effect";
11
- import { DockerEngine } from "./cluster/docker.js";
12
- import { LocalCluster } from "./cluster/local.js";
13
- import { defaultStoragePath, installConfigOf, localKubeconfigPath, OperatorConfigError } from "./config.js";
12
+ import { DockerEngine } from "./cluster/docker-engine.js";
13
+ import { DockerCluster } from "./cluster/docker.js";
14
+ import { clusterKubeconfigPath, defaultStoragePath, installConfigOf, OperatorConfigError } from "./config.js";
14
15
  import { KubeApi } from "./kube/api.js";
15
16
  /** The cluster `config` targets. */
16
17
  export const resolveTarget = Effect.fnUntraced(function* (config) {
17
18
  const { target } = config;
18
19
  if ("kubeconfig" in target)
19
20
  return { _tag: "Kubeconfig", kubeconfig: target.kubeconfig };
20
- const { local } = target;
21
+ const kubeconfig = { path: clusterKubeconfigPath(config) };
22
+ if ("host" in target) {
23
+ const { host } = target;
24
+ return {
25
+ _tag: "Host",
26
+ cluster: {
27
+ storagePath: host.storagePath ?? (yield* defaultStoragePath),
28
+ hostAliases: host.hostAliases,
29
+ ...(host.registries ? { registries: host.registries } : {}),
30
+ },
31
+ kubeconfig,
32
+ };
33
+ }
34
+ const { docker } = target;
21
35
  const install = installConfigOf(config.install, { claude: false });
22
36
  if (Result.isFailure(install))
23
37
  return yield* new OperatorConfigError({ path: config.path, reason: install.failure });
24
- const { host } = install.success;
25
- const hostPaths = host.enabled ? host.mounts.map(({ hostPath, readOnly }) => ({ source: hostPath, target: hostPath, readOnly })) : [];
38
+ const hostProfile = install.success.host;
39
+ const hostPaths = hostProfile.enabled ? hostProfile.mounts.map(({ hostPath, readOnly }) => ({ source: hostPath, target: hostPath, readOnly })) : [];
26
40
  return {
27
- _tag: "Local",
41
+ _tag: "Docker",
28
42
  cluster: {
29
- name: local.name,
30
- apiPort: local.apiPort,
31
- storagePath: local.storagePath ?? (yield* defaultStoragePath),
32
- ...(local.subnet ? { subnet: local.subnet } : {}),
33
- ...(local.image ? { image: local.image } : {}),
34
- hostAliases: local.hostAliases,
35
- mounts: [...local.mounts, ...hostPaths.filter(({ target }) => !local.mounts.some((mount) => mount.target === target))],
36
- agents: local.agents,
37
- ...(local.registries ? { registries: local.registries } : {}),
43
+ name: docker.name,
44
+ apiPort: docker.apiPort,
45
+ storagePath: docker.storagePath ?? (yield* defaultStoragePath),
46
+ ...(docker.subnet ? { subnet: docker.subnet } : {}),
47
+ ...(docker.image ? { image: docker.image } : {}),
48
+ hostAliases: docker.hostAliases,
49
+ mounts: [...docker.mounts, ...hostPaths.filter(({ target }) => !docker.mounts.some((mount) => mount.target === target))],
50
+ agents: docker.agents,
51
+ ...(docker.registries ? { registries: docker.registries } : {}),
38
52
  },
39
- kubeconfig: { path: localKubeconfigPath(config) },
53
+ kubeconfig,
40
54
  };
41
55
  });
42
56
  /** Docker, as DOCKER_HOST names it. */
43
- const docker = Layer.unwrap(Effect.map(Config.option(Config.String("DOCKER_HOST")), (host) => DockerEngine.layer(Option.match(host, { onNone: () => ({}), onSome: (DOCKER_HOST) => ({ DOCKER_HOST }) }))));
44
- /** The local cluster made with `options`. */
45
- export const localCluster = (options) => LocalCluster.layer(options).pipe(Layer.provide(docker));
57
+ const dockerEngine = Layer.unwrap(Effect.map(Config.option(Config.String("DOCKER_HOST")), (host) => DockerEngine.layer(Option.match(host, { onNone: () => ({}), onSome: (DOCKER_HOST) => ({ DOCKER_HOST }) }))));
58
+ /** The cluster in Docker made with `options`. */
59
+ export const dockerCluster = (options) => DockerCluster.layer(options).pipe(Layer.provide(dockerEngine));
46
60
  /** The API of the cluster `target` is. */
47
61
  export const kubeApi = (target) => KubeApi.layer(target.kubeconfig);
48
62
  /** Where the cluster `target` is, as people read it. */
49
63
  export function describeTarget(target) {
50
- if (target._tag === "Local")
64
+ if (target._tag === "Host")
65
+ return "k3s on this machine";
66
+ if (target._tag === "Docker")
51
67
  return `the cluster ${target.cluster.name} on this machine`;
52
68
  const { path, context } = target.kubeconfig;
53
69
  return `the cluster ${context ? `of context ${context}` : "of the current context"} of ${path ?? "the default kubeconfig"}`;
@@ -1 +1 @@
1
- {"version":3,"file":"target.js","sourceRoot":"","sources":["../../../src/target.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"target.js","sourceRoot":"","sources":["../../../src/target.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ {{ template "base" . }}
2
+
3
+ [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runsc]
4
+ runtime_type = "io.containerd.runsc.v1"
5
+ {{- if .SystemdCgroup }}
6
+
7
+ # Where systemd runs the machine, kubelet's cgroups are systemd's, and runsc places its
8
+ # sandboxes' through systemd too, as runsc.toml beside this file says.
9
+ [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runsc.options]
10
+ TypeUrl = "io.containerd.runsc.v1.options"
11
+ ConfigPath = "/var/lib/rancher/k3s/agent/etc/containerd/runsc.toml"
12
+ {{- end }}
@@ -0,0 +1,15 @@
1
+ {
2
+ "k3s": {
3
+ "version": "v1.37.1+k3s1",
4
+ "image": "rancher/k3s:v1.37.1-k3s1@sha256:ca7f37d993d82ef0dcdcfecb2e0e2618ea541dbaffc620c8cedebe01a82acd0d",
5
+ "sha256": "92b94582eb7b34a8cf532e6006842b9ed215a6783cf56a3dd6271fd35243b2c0",
6
+ "installScript": {
7
+ "commit": "356c025475bbefaa09cf0d7e48e8e2adbbde57db",
8
+ "sha256": "ed01f89fd977bf20ac1516bbebf8370bf3ddbaa55dac8aba610956a4c78cc00b"
9
+ }
10
+ },
11
+ "gvisor": {
12
+ "release": "20260921.0",
13
+ "sha512": "66a5b17354690ec2e4037b54080eeabde55a1503779ceb823d73856283a6836953e105493d7e38fb2dcf85e5c86e0f48c66a9abb0244a78bd612df938702475c"
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alasio",
3
- "version": "4.1.4",
3
+ "version": "4.2.0",
4
4
  "description": "alasio: coding agents you talk to from Telegram, installed and run with npx alasio",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  "node": ">=24"
15
15
  },
16
16
  "scripts": {
17
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
17
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../cluster/node/config-v3.toml.tmpl dist/cluster/node/",
18
18
  "start": "node ./src/main.ts"
19
19
  },
20
20
  "dependencies": {
@@ -1,392 +0,0 @@
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