alasio 8.0.0 → 10.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/api-server.js +18 -0
- package/dist/cli/src/api-server.js.map +1 -0
- package/dist/cli/src/bound-harnesses.js +98 -0
- package/dist/cli/src/bound-harnesses.js.map +1 -0
- package/dist/cli/src/branches.js +79 -22
- package/dist/cli/src/branches.js.map +1 -1
- package/dist/cli/src/cluster/docker.js +40 -15
- package/dist/cli/src/cluster/docker.js.map +1 -1
- package/dist/cli/src/cluster/host.js +38 -8
- package/dist/cli/src/cluster/host.js.map +1 -1
- package/dist/cli/src/cluster/k3s.js +105 -14
- package/dist/cli/src/cluster/k3s.js.map +1 -1
- package/dist/cli/src/cluster/node.js +142 -33
- package/dist/cli/src/cluster/node.js.map +1 -1
- package/dist/cli/src/cluster/stargz.js +92 -0
- package/dist/cli/src/cluster/stargz.js.map +1 -0
- package/dist/cli/src/commands/branch.js +5 -4
- package/dist/cli/src/commands/branch.js.map +1 -1
- package/dist/cli/src/commands/common.js +11 -5
- package/dist/cli/src/commands/common.js.map +1 -1
- package/dist/cli/src/commands/folder.js +14 -0
- package/dist/cli/src/commands/folder.js.map +1 -0
- package/dist/cli/src/commands/lake.js +1 -1
- package/dist/cli/src/commands/lake.js.map +1 -1
- package/dist/cli/src/commands/login-relay.js +91 -0
- package/dist/cli/src/commands/login-relay.js.map +1 -0
- package/dist/cli/src/commands/login.js +11 -10
- package/dist/cli/src/commands/login.js.map +1 -1
- package/dist/cli/src/commands/logs.js +89 -6
- package/dist/cli/src/commands/logs.js.map +1 -1
- package/dist/cli/src/commands/restart.js +10 -4
- package/dist/cli/src/commands/restart.js.map +1 -1
- package/dist/cli/src/commands/restore-filer-store.js +27 -10
- package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
- package/dist/cli/src/commands/status.js +119 -20
- package/dist/cli/src/commands/status.js.map +1 -1
- package/dist/cli/src/commands/uninstall.js +8 -9
- package/dist/cli/src/commands/uninstall.js.map +1 -1
- package/dist/cli/src/commands/up.js +20 -9
- package/dist/cli/src/commands/up.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +22 -6
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/commands.js +2 -1
- package/dist/cli/src/commands.js.map +1 -1
- package/dist/cli/src/config.js +4 -1
- package/dist/cli/src/config.js.map +1 -1
- package/dist/cli/src/cpu-baseline.js +8 -4
- package/dist/cli/src/cpu-baseline.js.map +1 -1
- package/dist/cli/src/folders.js +63 -0
- package/dist/cli/src/folders.js.map +1 -0
- package/dist/cli/src/gateway-calls.js +64 -0
- package/dist/cli/src/gateway-calls.js.map +1 -0
- package/dist/cli/src/install.js +45 -31
- package/dist/cli/src/install.js.map +1 -1
- package/dist/cli/src/kept-releases.js +27 -20
- package/dist/cli/src/kept-releases.js.map +1 -1
- package/dist/cli/src/kube/api.js +61 -15
- package/dist/cli/src/kube/api.js.map +1 -1
- package/dist/cli/src/kube/apply.js +122 -81
- package/dist/cli/src/kube/apply.js.map +1 -1
- package/dist/cli/src/kube/rollout.js +43 -21
- package/dist/cli/src/kube/rollout.js.map +1 -1
- package/dist/cli/src/main.js +2 -1
- package/dist/cli/src/main.js.map +1 -1
- package/dist/cli/src/manifests/admission.js +558 -222
- package/dist/cli/src/manifests/admission.js.map +1 -1
- package/dist/cli/src/manifests/alasio.js +180 -428
- package/dist/cli/src/manifests/alasio.js.map +1 -1
- package/dist/cli/src/manifests/branch.js +16 -27
- package/dist/cli/src/manifests/branch.js.map +1 -1
- package/dist/cli/src/manifests/codex-login.js +101 -0
- package/dist/cli/src/manifests/codex-login.js.map +1 -0
- package/dist/cli/src/manifests/collector.js +83 -62
- package/dist/cli/src/manifests/collector.js.map +1 -1
- package/dist/cli/src/manifests/common.js +82 -25
- package/dist/cli/src/manifests/common.js.map +1 -1
- package/dist/cli/src/manifests/config.js +137 -105
- package/dist/cli/src/manifests/config.js.map +1 -1
- package/dist/cli/src/manifests/disruption.js +18 -3
- package/dist/cli/src/manifests/disruption.js.map +1 -1
- package/dist/cli/src/manifests/folders.js +131 -0
- package/dist/cli/src/manifests/folders.js.map +1 -0
- package/dist/cli/src/manifests/grafana.js +4 -2
- package/dist/cli/src/manifests/grafana.js.map +1 -1
- package/dist/cli/src/manifests/harness-pools.js +236 -0
- package/dist/cli/src/manifests/harness-pools.js.map +1 -0
- package/dist/cli/src/manifests/image-holder.js +61 -0
- package/dist/cli/src/manifests/image-holder.js.map +1 -0
- package/dist/cli/src/manifests/index.js +14 -7
- package/dist/cli/src/manifests/index.js.map +1 -1
- package/dist/cli/src/manifests/keda.js +4 -2
- package/dist/cli/src/manifests/keda.js.map +1 -1
- package/dist/cli/src/manifests/lake.js +18 -20
- package/dist/cli/src/manifests/lake.js.map +1 -1
- package/dist/cli/src/manifests/log.js +84 -19
- package/dist/cli/src/manifests/log.js.map +1 -1
- package/dist/cli/src/manifests/neon.js +60 -80
- package/dist/cli/src/manifests/neon.js.map +1 -1
- package/dist/cli/src/manifests/network-policies.js +232 -94
- package/dist/cli/src/manifests/network-policies.js.map +1 -1
- package/dist/cli/src/manifests/node-agent.js +42 -15
- package/dist/cli/src/manifests/node-agent.js.map +1 -1
- package/dist/cli/src/manifests/object-store.js +99 -25
- package/dist/cli/src/manifests/object-store.js.map +1 -1
- package/dist/cli/src/manifests/sandbox-gateway.js +761 -0
- package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
- package/dist/cli/src/manifests/scalers.js +17 -5
- package/dist/cli/src/manifests/scalers.js.map +1 -1
- package/dist/cli/src/manifests/sizing.js +17 -9
- package/dist/cli/src/manifests/sizing.js.map +1 -1
- package/dist/cli/src/manifests/valkey.js +30 -10
- package/dist/cli/src/manifests/valkey.js.map +1 -1
- package/dist/cli/src/manifests/volume-lock.js +43 -0
- package/dist/cli/src/manifests/volume-lock.js.map +1 -0
- package/dist/cli/src/manifests/vpa.js +15 -2
- package/dist/cli/src/manifests/vpa.js.map +1 -1
- package/dist/cli/src/manifests/workspace-storage.js +182 -94
- package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
- package/dist/cli/src/neon-storage.js +54 -16
- package/dist/cli/src/neon-storage.js.map +1 -1
- package/dist/cli/src/release.js +9 -9
- package/dist/cli/src/release.js.map +1 -1
- package/dist/cli/src/setup-pod.js +44 -0
- package/dist/cli/src/setup-pod.js.map +1 -0
- package/dist/cli/src/target.js +2 -2
- package/dist/cli/src/target.js.map +1 -1
- package/dist/cli/src/telegram.js +13 -4
- package/dist/cli/src/telegram.js.map +1 -1
- package/dist/cli/src/wakes.js +68 -0
- package/dist/cli/src/wakes.js.map +1 -0
- package/dist/cli/src/webhook.js +5 -2
- package/dist/cli/src/webhook.js.map +1 -1
- package/dist/cluster/node/pins.json +11 -5
- package/dist/neon/control/branches.js +13 -6
- package/dist/neon/control/branches.js.map +1 -1
- package/dist/neon/control/endpoints.js +81 -10
- package/dist/neon/control/endpoints.js.map +1 -1
- package/dist/neon/control/secrets.js +24 -8
- package/dist/neon/control/secrets.js.map +1 -1
- package/dist/src/binder/bindings.js +195 -0
- package/dist/src/binder/bindings.js.map +1 -0
- package/dist/src/binder/names.js +63 -0
- package/dist/src/binder/names.js.map +1 -0
- package/dist/src/binder/tokens.js +71 -0
- package/dist/src/binder/tokens.js.map +1 -0
- package/dist/src/branch/names.js +20 -6
- package/dist/src/branch/names.js.map +1 -1
- package/dist/src/events/names.js +28 -0
- package/dist/src/events/names.js.map +1 -0
- package/dist/src/gateway/names.js +240 -0
- package/dist/src/gateway/names.js.map +1 -0
- package/dist/src/harness/names.js +67 -0
- package/dist/src/harness/names.js.map +1 -0
- package/dist/src/kube/client.js +212 -0
- package/dist/src/kube/client.js.map +1 -0
- package/dist/src/kube/sandbox-shape.js +14 -68
- package/dist/src/kube/sandbox-shape.js.map +1 -1
- package/dist/src/kube/started-map.js +12 -0
- package/dist/src/kube/started-map.js.map +1 -0
- package/dist/src/node-agent/names.js +62 -5
- package/dist/src/node-agent/names.js.map +1 -1
- package/dist/src/sandbox/names.js +12 -20
- package/dist/src/sandbox/names.js.map +1 -1
- package/dist/src/sandbox/otlp-resource.js +173 -0
- package/dist/src/sandbox/otlp-resource.js.map +1 -0
- package/dist/src/sandbox/telemetry.js +43 -0
- package/dist/src/sandbox/telemetry.js.map +1 -0
- package/dist/src/telegram/bot-commands.js +17 -0
- package/dist/src/telegram/bot-commands.js.map +1 -0
- package/dist/src/telegram/stop.js +12 -0
- package/dist/src/telegram/stop.js.map +1 -0
- package/dist/src/telemetry/config.js +131 -0
- package/dist/src/telemetry/config.js.map +1 -0
- package/dist/src/telemetry/forward.js +154 -0
- package/dist/src/telemetry/forward.js.map +1 -0
- package/package.json +1 -1
- package/dist/cli/src/manifests/agent-sandbox.js +0 -92
- package/dist/cli/src/manifests/agent-sandbox.js.map +0 -1
- package/dist/cli/src/manifests/juicefs-csi.js +0 -320
- package/dist/cli/src/manifests/juicefs-csi.js.map +0 -1
- package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +0 -6295
- package/dist/src/kube/config.js +0 -145
- package/dist/src/kube/config.js.map +0 -1
|
@@ -1,311 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* alasio itself: its Deployment, its
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* alasio itself: its Deployment, the namespaces its workspaces run in, its identity in
|
|
3
|
+
* them, and the template it makes its folder workspaces' Sandboxes from (src/kube/
|
|
4
|
+
* config.ts); main's, or a branch environment's in its own namespaces (./branch.ts), and
|
|
5
|
+
* main's Service of the forks it asks the sandbox gateway for them.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { gvisorRuntimeClass, NODE_AGENT_PORT, NODE_AGENT_TOKEN_VARIABLE } from "../../../src/node-agent/names.js";
|
|
7
|
+
import { posix } from "node:path";
|
|
8
|
+
import { BRANCH_PARENT_LABEL, branchSessionsNamespace, forksGroup, forksTopic, MAIN_ENVIRONMENT_NAME } from "../../../src/branch/names.js";
|
|
9
|
+
import { eventsGroup, eventsTopic } from "../../../src/events/names.js";
|
|
10
|
+
import { ALASIO_GATEWAY_CALLER_KEY_VARIABLE, ALASIO_GATEWAY_ENVIRONMENT_VARIABLE, ALASIO_GATEWAY_FOLDERS_VARIABLE, ALASIO_GATEWAY_URL_VARIABLE } from "../../../src/gateway/names.js";
|
|
11
|
+
import { STARTED_CONFIG_MAP } from "../../../src/kube/started-map.js";
|
|
13
12
|
import { gvisorReleases, NODE_PINS } from "../cluster/node.js";
|
|
14
13
|
import { imageReference } from "../images.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
14
|
+
import { busyLabelPolicy } from "./admission.js";
|
|
15
|
+
import { codexLoginObjects, codexLoginUrl } from "./codex-login.js";
|
|
16
|
+
import { foldersRun } from "./folders.js";
|
|
17
|
+
import { carriedPools, GATEWAY, GATEWAY_PORT } from "./sandbox-gateway.js";
|
|
18
|
+
import { alasioGroup, environmentLogSecret, environmentName, lagTrigger, lakeWakeTopic, LOG_BROKERS, logAuthentication, telegramTopic } from "./log.js";
|
|
19
|
+
import { scaledObject } from "./scalers.js";
|
|
20
|
+
import { STOP_GRACE_SECONDS } from "../../../src/telegram/stop.js";
|
|
21
|
+
import { BRANCH_FORK_SECRET, BRANCH_LABEL, componentName, DATABASE_SECRET, envOf, given, imagePullSecrets, labels, lakeRuns, MAIN_ENVIRONMENT, NAMESPACE, otelEnv, RELEASE, replicasOf, restrictedContainer, restrictedPod, roleVerifiers, selectorLabels, SESSIONS_CALLER_SECRET, } from "./common.js";
|
|
18
22
|
import { durationSeconds } from "./config.js";
|
|
19
|
-
import { busyBudget } from "./disruption.js";
|
|
23
|
+
import { busyBudget, replicaBudget, spread } from "./disruption.js";
|
|
20
24
|
import { priority } from "./priorities.js";
|
|
21
25
|
import { verticalPodAutoscaler } from "./sizing.js";
|
|
22
|
-
import { cloneJob } from "./workspace-storage.js";
|
|
23
|
-
/** The port every workspace's bayma serves MCP on. */
|
|
24
|
-
const BAYMA_PORT = 7290;
|
|
25
26
|
/** The namespace of the sessions of the alasio of `environment`: the configured one, or a branch environment's own. */
|
|
26
27
|
export function sessionsNamespace(config, { branch }) {
|
|
27
28
|
return branch ? branchSessionsNamespace(branch) : config.sessions.namespace;
|
|
28
29
|
}
|
|
29
|
-
/** The namespaces whose Sandboxes alasio drives: the sessions', and the host profile's. */
|
|
30
|
-
function workspaceNamespaces(config, environment) {
|
|
31
|
-
return [...(config.sessions.enabled ? [sessionsNamespace(config, environment)] : []), ...(config.host.enabled ? [config.host.namespace] : [])];
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* The template of session filesystems' Sandboxes: an empty, isolated workspace under
|
|
35
|
-
* the sessions' runtime, restricted, on a volume of workspace storage's class when it is
|
|
36
|
-
* on. alasio adds what is per Sandbox: its name and labels, bayma's token, and its
|
|
37
|
-
* network mode. A branch environment's are in its own namespace, and neither clones a
|
|
38
|
-
* session (its parent does, src/branch/fork.ts) nor removes JuiceFS's mount pods, which
|
|
39
|
-
* are main's to drive.
|
|
40
|
-
*/
|
|
41
|
-
function sessionsProfile(config, environment, { cpuBaseline, gvisorReleases }) {
|
|
42
|
-
const { sessions, workspaceStorage } = config;
|
|
43
|
-
const main = environment.branch === null;
|
|
44
|
-
// Snapshotted once every node's CPU features are known, its sandboxes held to those all have.
|
|
45
|
-
const snapshotted = main && nodeAgentRuns(config) && cpuBaseline !== null;
|
|
46
|
-
const agent = imageReference(config.images.agent);
|
|
47
|
-
const storageClassName = sessions.storage.storageClassName || (workspaceStorage.enabled ? workspaceStorage.storageClassName : "");
|
|
48
|
-
// On workspace storage, the volume's mounts follow its mount pod's, as upstream's
|
|
49
|
-
// template has them (gVisor does not, and alasio restarts a session whose mount is
|
|
50
|
-
// gone), and its files are given the pod's group only when its root is not the group's,
|
|
51
|
-
// as walking a JuiceFS volume is slow.
|
|
52
|
-
const propagation = workspaceStorage.enabled ? { mountPropagation: "HostToContainer" } : {};
|
|
53
|
-
const ownership = workspaceStorage.enabled ? { fsGroupChangePolicy: "OnRootMismatch" } : {};
|
|
54
|
-
return {
|
|
55
|
-
namespace: sessionsNamespace(config, environment),
|
|
56
|
-
port: BAYMA_PORT,
|
|
57
|
-
workspaceDir: "/workspace",
|
|
58
|
-
egressGate: sessions.egressGate,
|
|
59
|
-
fullModeNameservers: [...sessions.fullModeNameservers],
|
|
60
|
-
...(main && mountPodsRemoved(config) ? { mountPodNamespace: workspaceStorage.csi.namespace } : {}),
|
|
61
|
-
podTemplate: {
|
|
62
|
-
metadata: {
|
|
63
|
-
labels: { "app.kubernetes.io/part-of": "alasio", "app.kubernetes.io/instance": RELEASE, "app.kubernetes.io/component": "session" },
|
|
64
|
-
// Under gVisor, /tmp is a tmpfs of the sandbox's own, by gVisor's mount hints, rather
|
|
65
|
-
// than the emptyDir's directory on the node: a snapshot of the sandbox holds it, as it
|
|
66
|
-
// holds the files that bayma's runtimes keep open there.
|
|
67
|
-
annotations: {
|
|
68
|
-
"dev.gvisor.spec.mount.tmp.share": "container",
|
|
69
|
-
"dev.gvisor.spec.mount.tmp.type": "tmpfs",
|
|
70
|
-
"dev.gvisor.spec.mount.tmp.options": "rw",
|
|
71
|
-
...(snapshotted ? { [CPU_FEATURES_ANNOTATION]: cpuBaseline.join(",") } : {}),
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
spec: {
|
|
75
|
-
...(sessions.runtimeClassName ? { runtimeClassName: sessions.runtimeClassName } : {}),
|
|
76
|
-
...priority("turn"),
|
|
77
|
-
// A pod that ends is ended: alasio brings the session up in a new one, and nothing
|
|
78
|
-
// starts bayma again in a sandbox that a snapshot has just ended.
|
|
79
|
-
restartPolicy: "Never",
|
|
80
|
-
// Ended by other than alasio, bayma holds its stop for alasio's snapshot, then stops.
|
|
81
|
-
...(snapshotted ? { terminationGracePeriodSeconds: stopWaitSeconds(config) + BAYMA_STOP_SECONDS } : {}),
|
|
82
|
-
securityContext: { ...restrictedPod(1000, 1000), ...ownership },
|
|
83
|
-
...imagePullSecrets(config),
|
|
84
|
-
...given("nodeSelector", { ...sessions.nodeSelector }),
|
|
85
|
-
...given("tolerations", [...sessions.tolerations]),
|
|
86
|
-
initContainers: [
|
|
87
|
-
// The workspace and home on the session's volume, and bayma's toolbelt, which the
|
|
88
|
-
// image installs, linked into the home.
|
|
89
|
-
{
|
|
90
|
-
name: "prepare",
|
|
91
|
-
image: agent,
|
|
92
|
-
imagePullPolicy: config.images.pullPolicy,
|
|
93
|
-
command: [
|
|
94
|
-
"/bin/sh",
|
|
95
|
-
"-c",
|
|
96
|
-
"mkdir -p /data/workspace /data/home/.local/share/bayma && ln -sfn /opt/bayma-data/bayma/toolbelt /data/home/.local/share/bayma/toolbelt",
|
|
97
|
-
],
|
|
98
|
-
securityContext: restrictedContainer(),
|
|
99
|
-
...helperResources(),
|
|
100
|
-
volumeMounts: [{ name: "data", mountPath: "/data" }],
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
containers: [{
|
|
104
|
-
name: "bayma",
|
|
105
|
-
image: agent,
|
|
106
|
-
imagePullPolicy: config.images.pullPolicy,
|
|
107
|
-
command: ["/usr/bin/tini", "-s", "--", "node", "/opt/bayma/bayma.js"],
|
|
108
|
-
args: [
|
|
109
|
-
"mcp-http",
|
|
110
|
-
"--host",
|
|
111
|
-
"0.0.0.0",
|
|
112
|
-
"--port",
|
|
113
|
-
String(BAYMA_PORT),
|
|
114
|
-
"--state-dir",
|
|
115
|
-
"/home/agent/.bayma",
|
|
116
|
-
...(snapshotted ? [STOP_WAIT_ARG, String(stopWaitSeconds(config) * 1000)] : []),
|
|
117
|
-
],
|
|
118
|
-
workingDir: "/workspace",
|
|
119
|
-
env: [
|
|
120
|
-
{ name: "HOME", value: "/home/agent" },
|
|
121
|
-
{ name: "USER", value: "agent" },
|
|
122
|
-
{ name: "BAYMA_PAYLOAD_DIR", value: "/opt/bayma/payload" },
|
|
123
|
-
],
|
|
124
|
-
ports: [{ name: "mcp", containerPort: BAYMA_PORT }],
|
|
125
|
-
securityContext: restrictedContainer(),
|
|
126
|
-
resources: sessions.resources,
|
|
127
|
-
volumeMounts: [
|
|
128
|
-
{ name: "data", mountPath: "/workspace", subPath: "workspace", ...propagation },
|
|
129
|
-
{ name: "data", mountPath: "/home/agent", subPath: "home", ...propagation },
|
|
130
|
-
{ name: "tmp", mountPath: "/tmp" },
|
|
131
|
-
],
|
|
132
|
-
}],
|
|
133
|
-
volumes: [{ name: "tmp", emptyDir: { sizeLimit: "2Gi" } }],
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
volumeClaimTemplates: [{
|
|
137
|
-
metadata: { name: "data" },
|
|
138
|
-
spec: {
|
|
139
|
-
accessModes: ["ReadWriteOnce"],
|
|
140
|
-
...(storageClassName ? { storageClassName } : {}),
|
|
141
|
-
resources: { requests: { storage: sessions.storage.size } },
|
|
142
|
-
},
|
|
143
|
-
}],
|
|
144
|
-
...(main && sessionsForked(config) ? { clone: { claimTemplate: "data", job: cloneJob(config) } } : {}),
|
|
145
|
-
...(snapshotted ? { snapshots: sessionSnapshots(config, gvisorReleases) } : {}),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
30
|
/** How long a session's bayma holds a stop alasio did not make (sessions.snapshots.stopWait). */
|
|
149
|
-
function stopWaitSeconds(config) {
|
|
31
|
+
export function stopWaitSeconds(config) {
|
|
150
32
|
// A duration, as the configuration checks it.
|
|
151
33
|
return durationSeconds(config.sessions.snapshots.stopWait) ?? 0;
|
|
152
34
|
}
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
* where the node agents run: once sessions.snapshots.releaseAfter has passed without an
|
|
156
|
-
* exec or a turn, live every sessions.snapshots.liveEvery while execs run, and as their
|
|
157
|
-
* pods are ended by other than alasio, within sessions.snapshots.stopWait, under the
|
|
158
|
-
* gVisor `releases` alasio keeps (../kept-releases.ts), the
|
|
159
|
-
* current one first, each restored with a RuntimeClass of its own, by the agents in
|
|
160
|
-
* alasio's namespace.
|
|
161
|
-
*/
|
|
162
|
-
function sessionSnapshots(config, releases) {
|
|
163
|
-
return {
|
|
164
|
-
// A duration, as the configuration checks it.
|
|
165
|
-
releaseAfterSeconds: durationSeconds(config.sessions.snapshots.releaseAfter) ?? 0,
|
|
166
|
-
liveEverySeconds: durationSeconds(config.sessions.snapshots.liveEvery) ?? 0,
|
|
167
|
-
releases: releases.map((release) => ({ release, runtimeClassName: gvisorRuntimeClass(release) })),
|
|
168
|
-
agents: {
|
|
169
|
-
namespace: NAMESPACE,
|
|
170
|
-
selector: Object.entries(selectorLabels("node-agent")).map(([label, value]) => `${label}=${value}`).join(","),
|
|
171
|
-
port: NODE_AGENT_PORT,
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
/** Whether alasio forks sessions: where their volumes are workspace storage's, which JuiceFS clones. */
|
|
176
|
-
export function sessionsForked({ sessions, workspaceStorage }) {
|
|
177
|
-
return workspaceStorage.enabled && [workspaceStorage.storageClassName, ""].includes(sessions.storage.storageClassName);
|
|
178
|
-
}
|
|
179
|
-
/** The template of folder workspaces' bayma: bayma itself, as the operator, with the machine's mounts. */
|
|
180
|
-
function hostProfile(config) {
|
|
181
|
-
const { host } = config;
|
|
182
|
-
return {
|
|
183
|
-
namespace: host.namespace,
|
|
184
|
-
port: BAYMA_PORT,
|
|
185
|
-
stateRoot: host.stateRoot,
|
|
186
|
-
podTemplate: {
|
|
187
|
-
metadata: { labels: { "app.kubernetes.io/part-of": "alasio", "app.kubernetes.io/instance": RELEASE, "app.kubernetes.io/component": "folder-bayma" } },
|
|
188
|
-
spec: {
|
|
189
|
-
// May restart alasio, by rolling out its Deployment, as an agent restarts it.
|
|
190
|
-
serviceAccountName: componentName("host-agent"),
|
|
191
|
-
...priority("turn"),
|
|
192
|
-
securityContext: {
|
|
193
|
-
runAsUser: host.uid,
|
|
194
|
-
runAsGroup: host.gid,
|
|
195
|
-
// The group the bayma token's Secret volume is given, which the operator's user
|
|
196
|
-
// reads it as; the node's own paths the mounts give are left as they are.
|
|
197
|
-
fsGroup: host.gid,
|
|
198
|
-
...given("supplementalGroups", [...host.supplementalGroups]),
|
|
199
|
-
},
|
|
200
|
-
...imagePullSecrets(config),
|
|
201
|
-
containers: [{
|
|
202
|
-
name: "bayma",
|
|
203
|
-
image: imageReference(host.baymaImage),
|
|
204
|
-
imagePullPolicy: "IfNotPresent",
|
|
205
|
-
args: ["mcp-http", "--host", "0.0.0.0", "--port", String(BAYMA_PORT), "--default-durability", "checkpointed"],
|
|
206
|
-
workingDir: host.home,
|
|
207
|
-
env: [{ name: "HOME", value: host.home }, ...envOf(host.env)],
|
|
208
|
-
ports: [{ name: "mcp", containerPort: BAYMA_PORT }],
|
|
209
|
-
securityContext: {
|
|
210
|
-
// What bayma needs to snapshot its REPL sessions' processes as it stops and
|
|
211
|
-
// restore them whole as it starts again.
|
|
212
|
-
capabilities: { add: ["CHECKPOINT_RESTORE", "SYS_PTRACE"] },
|
|
213
|
-
seccompProfile: { type: "Unconfined" },
|
|
214
|
-
appArmorProfile: { type: "Unconfined" },
|
|
215
|
-
},
|
|
216
|
-
resources: host.resources,
|
|
217
|
-
...given("volumeMounts", hostVolumeMounts(config)),
|
|
218
|
-
}],
|
|
219
|
-
...given("volumes", hostVolumes(config)),
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/** What a cluster keeps where up has found nothing of it yet: no baseline, the releases pinned, and no bot. */
|
|
225
|
-
export const NOTHING_KEPT = { cpuBaseline: null, gvisorReleases: gvisorReleases(NODE_PINS), botChecksum: null };
|
|
35
|
+
/** What a cluster keeps where up has found nothing of it yet: no baseline, the releases pinned, no bot, no API server known, and none of the mount's Services. */
|
|
36
|
+
export const NOTHING_KEPT = { cpuBaseline: null, gvisorReleases: gvisorReleases(NODE_PINS), botChecksum: null, apiServer: null, mountServices: {} };
|
|
226
37
|
/** Nothing carried forward: a first installation's, and a branch environment's. */
|
|
227
|
-
export const NOTHING_CARRIED = {
|
|
228
|
-
/** The ConfigMap of the templates, by its name. */
|
|
229
|
-
export const SANDBOX_TEMPLATES = componentName("sandbox-templates");
|
|
38
|
+
export const NOTHING_CARRIED = { previousPools: { none: null, full: null }, ...NOTHING_KEPT };
|
|
230
39
|
/**
|
|
231
40
|
* What the installation `config` describes, with what the cluster keeps, `kept`, carries
|
|
232
|
-
* forward from `
|
|
233
|
-
* `
|
|
234
|
-
*
|
|
235
|
-
* template, and no more. Pure, for tests.
|
|
236
|
-
*
|
|
237
|
-
* What it carries is pinned whole into the Sandbox policy (./admission.ts) as a template
|
|
238
|
-
* alasio may make a Sandbox from, read back from the cluster rather than rendered from the
|
|
239
|
-
* config: the ConfigMap is the anchor of that trust. It is one as long as only the
|
|
240
|
-
* operator, by `alasio up`, writes it: alasio's ServiceAccount may write no ConfigMap
|
|
241
|
-
* (./alasio.ts identity). Were alasio given that, it could write any pod template into
|
|
242
|
-
* the ConfigMap, and the next `up` would let it make Sandboxes of it.
|
|
243
|
-
*/
|
|
244
|
-
export function carriedFrom(config, live, kept) {
|
|
245
|
-
const nothing = { ...NOTHING_CARRIED, ...kept };
|
|
246
|
-
if (!config.sessions.enabled || live === undefined)
|
|
247
|
-
return nothing;
|
|
248
|
-
// Carried as the ConfigMap held them, which alasio read as they are: the profile a snapshot recorded the hash of.
|
|
249
|
-
if (Result.isFailure(decodeKubeTemplates(SANDBOX_TEMPLATES, live)))
|
|
250
|
-
return nothing;
|
|
251
|
-
const held = JSON.parse(live);
|
|
252
|
-
if (!held.sessions)
|
|
253
|
-
return nothing;
|
|
254
|
-
// The current profile as the ConfigMap holds it, to compare with what it held.
|
|
255
|
-
const current = JSON.parse(goJson(sessionsProfile(config, MAIN_ENVIRONMENT, kept), ""));
|
|
256
|
-
return { previousSessions: isDeepStrictEqual(held.sessions.podTemplate, current.podTemplate) ? (held.previousSessions ?? null) : held.sessions, ...kept };
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* The CPU baseline the installation whose templates are `live` (as carriedFrom takes
|
|
260
|
-
* them) holds sessions to: what its sessions' template sets, if anything.
|
|
41
|
+
* forward from `liveGateway`, the sandbox gateway's configuration of the installation it
|
|
42
|
+
* replaces (its ConfigMap's `config.json`, if it has one): its pools' templates before the
|
|
43
|
+
* current ones (./sandbox-gateway.ts carriedPools). Pure, for tests.
|
|
261
44
|
*/
|
|
262
|
-
export function
|
|
263
|
-
|
|
264
|
-
return null;
|
|
265
|
-
const held = JSON.parse(live);
|
|
266
|
-
const recorded = held.sessions?.podTemplate.metadata?.annotations?.[CPU_FEATURES_ANNOTATION];
|
|
267
|
-
return recorded ? recorded.split(",") : null;
|
|
268
|
-
}
|
|
269
|
-
/** The ConfigMap of the templates, and their checksum, which alasio's pod carries so it is replaced when they change. */
|
|
270
|
-
function sandboxTemplates(config, environment, carried) {
|
|
271
|
-
const name = SANDBOX_TEMPLATES;
|
|
272
|
-
const templates = goJson({
|
|
273
|
-
...(config.sessions.enabled ? { sessions: sessionsProfile(config, environment, carried) } : {}),
|
|
274
|
-
...(config.sessions.enabled && carried.previousSessions ? { previousSessions: carried.previousSessions } : {}),
|
|
275
|
-
...(config.host.enabled ? { host: hostProfile(config) } : {}),
|
|
276
|
-
}, " ");
|
|
277
|
-
return {
|
|
278
|
-
configMap: { apiVersion: "v1", kind: "ConfigMap", metadata: { name, namespace: environment.namespace, labels: labels("alasio") }, data: { "templates.json": templates } },
|
|
279
|
-
checksum: sha256(templates),
|
|
280
|
-
};
|
|
45
|
+
export function carriedFrom(config, kept, liveGateway) {
|
|
46
|
+
return { ...kept, previousPools: carriedPools(config, liveGateway, kept) };
|
|
281
47
|
}
|
|
282
48
|
/** Where alasio's Secrets of branch environments are mounted: main's key, or a branch's token. */
|
|
283
49
|
const BRANCH_SECRETS = "/run/alasio/branch";
|
|
284
50
|
/**
|
|
285
51
|
* What alasio is told of branch environments: main, where it forks sessions, where the
|
|
286
52
|
* key their tokens are signed with is, from the stack's Secret, which may be made after
|
|
287
|
-
* it starts; a branch, which it is, and
|
|
288
|
-
*
|
|
53
|
+
* it starts; a branch, which it is, and its token to ask its parent to fork the sessions it
|
|
54
|
+
* inherited with, on the log (src/branch/fork.ts).
|
|
289
55
|
*/
|
|
290
|
-
function branchEnv(
|
|
56
|
+
function branchEnv({ branch }) {
|
|
291
57
|
if (branch) {
|
|
292
58
|
return [
|
|
293
59
|
{ name: "ALASIO_BRANCH", value: branch },
|
|
294
|
-
{ name: "ALASIO_PARENT_FORKS_URL", value: `http://${componentName("branches")}.${NAMESPACE}.svc:${BRANCH_FORK_PORT}` },
|
|
295
60
|
{ name: "ALASIO_BRANCH_FORK_TOKEN_FILE", value: `${BRANCH_SECRETS}/token` },
|
|
296
61
|
];
|
|
297
62
|
}
|
|
298
|
-
return
|
|
63
|
+
return [{ name: "ALASIO_BRANCH_FORK_KEY_FILE", value: `${BRANCH_SECRETS}/fork-key` }];
|
|
299
64
|
}
|
|
300
65
|
/** The volume of branch environments' Secret: main's key, kept optional, or a branch's token. */
|
|
301
|
-
function branchSecretVolume(
|
|
66
|
+
function branchSecretVolume({ branch }) {
|
|
302
67
|
if (branch)
|
|
303
68
|
return [{ name: "branch", secret: { secretName: BRANCH_FORK_SECRET, items: [{ key: "token", path: "token" }] } }];
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
/** Whether main forks its sessions for branch environments: where it has Neon to branch, and forks sessions. */
|
|
307
|
-
export function forksForBranches(config) {
|
|
308
|
-
return config.neon.enabled && sessionsForked(config);
|
|
69
|
+
return [{ name: "branch", secret: { secretName: componentName("branches"), items: [{ key: "fork-key", path: "fork-key" }], optional: true } }];
|
|
309
70
|
}
|
|
310
71
|
/**
|
|
311
72
|
* The roles main's alasio makes for what reads its Neon beside it (common.ts
|
|
@@ -319,20 +80,20 @@ function readerRoles(config, { branch }) {
|
|
|
319
80
|
function alasioEnv(config, home, operatorHome, environment) {
|
|
320
81
|
const { alasio, host } = config;
|
|
321
82
|
return [
|
|
322
|
-
{ name: "ALASIO_KUBE_TEMPLATES", value: "/etc/alasio/templates.json" },
|
|
323
83
|
{ name: "ALASIO_STATE_DIR", value: "/var/lib/alasio/state" },
|
|
324
84
|
{ name: "HOME", value: home },
|
|
325
85
|
// A home of alasio's own does not outlast its pod; the operator's keeps their login.
|
|
326
86
|
...(operatorHome ? [] : [{ name: "ALASIO_KEEP_CODEX_LOGIN", value: "1" }]),
|
|
327
|
-
{ name: "ALASIO_DEPLOYMENT", value: RELEASE },
|
|
328
87
|
{ name: "ALASIO_NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
|
|
329
88
|
{ name: "ALASIO_POD", valueFrom: { fieldRef: { fieldPath: "metadata.name" } } },
|
|
330
89
|
{ name: "TELEGRAM_BOT_TOKEN", valueFrom: { secretKeyRef: { name: alasio.telegram.existingSecret, key: "token" } } },
|
|
331
90
|
{ name: "TELEGRAM_ALLOWED_USER_IDS", valueFrom: { secretKeyRef: { name: alasio.telegram.existingSecret, key: "allowedUserIds" } } },
|
|
332
91
|
{ name: "ALASIO_DATABASE_URL_FILE", value: "/run/alasio/database/url" },
|
|
333
92
|
{ name: "ALASIO_LAKE_VERIFIER_FILE", value: "/run/alasio/database/lake-verifier" },
|
|
93
|
+
{ name: "ALASIO_HARNESS_VERIFIER_FILE", value: "/run/alasio/database/harness-verifier" },
|
|
94
|
+
{ name: "ALASIO_CODEX_LOGIN_VERIFIER_FILE", value: "/run/alasio/database/codex-login-verifier" },
|
|
334
95
|
...readerRoles(config, environment).map(({ key, variable }) => ({ name: variable, value: `/run/alasio/database/${key}` })),
|
|
335
|
-
{ name: "ALASIO_LAKE_ENABLED", value: config.lake.enabled
|
|
96
|
+
{ name: "ALASIO_LAKE_ENABLED", value: config.lake.enabled ? "1" : "0" },
|
|
336
97
|
// Its Telegram updates: its environment's topic, read in its group as its principal.
|
|
337
98
|
{ name: "ALASIO_LOG_BROKERS", value: LOG_BROKERS },
|
|
338
99
|
{ name: "ALASIO_LOG_USERNAME", valueFrom: { secretKeyRef: { name: environmentLogSecret(environment), key: "username" } } },
|
|
@@ -341,36 +102,84 @@ function alasioEnv(config, home, operatorHome, environment) {
|
|
|
341
102
|
{ name: "ALASIO_TELEGRAM_GROUP", value: alasioGroup(environmentName(environment)) },
|
|
342
103
|
// Its environment's wake topic, which it tells the lake's loader of each transcript it writes on, as that principal.
|
|
343
104
|
...(lakeRuns(config) ? [{ name: "ALASIO_LAKE_WAKE_TOPIC", value: lakeWakeTopic(environmentName(environment)) }] : []),
|
|
344
|
-
{ name: "ALASIO_TELEMETRY_RECEIVER_SERVICE", value: `${componentName("telemetry")}.${environment.namespace}.svc` },
|
|
345
|
-
{ name: "ALASIO_TELEMETRY_RECEIVER_PORT", value: String(config.telemetry.receiverPort) },
|
|
346
105
|
...(alasio.defaultHarness ? [{ name: "ALASIO_DEFAULT_HARNESS", value: alasio.defaultHarness }] : []),
|
|
347
106
|
...(host.enabled && host.workspaceRoot ? [{ name: "ALASIO_WORKSPACE_ROOT", value: host.workspaceRoot }] : []),
|
|
348
107
|
...(alasio.claude.existingSecret
|
|
349
108
|
? [{ name: "CLAUDE_CODE_OAUTH_TOKEN", valueFrom: { secretKeyRef: { name: alasio.claude.existingSecret, key: alasio.claude.key } } }]
|
|
350
109
|
: []),
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
110
|
+
// Its environment's sessions, through the sandbox gateway, by its environment's own key of the gateway's.
|
|
111
|
+
{ name: ALASIO_GATEWAY_URL_VARIABLE, value: `http://${GATEWAY}.${NAMESPACE}.svc.cluster.local:${GATEWAY_PORT}` },
|
|
112
|
+
{ name: ALASIO_GATEWAY_ENVIRONMENT_VARIABLE, value: environmentName(environment) },
|
|
113
|
+
{ name: ALASIO_GATEWAY_CALLER_KEY_VARIABLE, valueFrom: { secretKeyRef: { name: SESSIONS_CALLER_SECRET, key: "caller-key" } } },
|
|
114
|
+
// Main's folder workspaces, the gateway's folder instances, where the host profile renders them.
|
|
115
|
+
...(environment.branch === null && foldersRun(config) ? [{ name: ALASIO_GATEWAY_FOLDERS_VARIABLE, value: "1" }] : []),
|
|
116
|
+
// Where its conversations' harnesses' pools' pods reach its environment's relay of its Codex login (./codex-login.ts).
|
|
117
|
+
{ name: "ALASIO_CODEX_LOGIN_URL", value: codexLoginUrl(environment) },
|
|
118
|
+
...branchEnv(environment),
|
|
356
119
|
...otelEnv(config, environment),
|
|
357
120
|
...envOf(alasio.env),
|
|
358
121
|
];
|
|
359
122
|
}
|
|
360
123
|
/**
|
|
361
|
-
* alasio's
|
|
362
|
-
*
|
|
363
|
-
*
|
|
124
|
+
* What of the machine alasio's pod mounts under the host profile: what alasio itself reads and
|
|
125
|
+
* writes there, now that no harness runs in its pod, each where the host profile's mounts put it
|
|
126
|
+
* (its folder conversations' harnesses run in pools of their own, with every mount):
|
|
127
|
+
* - the root its folder workspaces are under (ALASIO_WORKSPACE_ROOT, else its home), which it
|
|
128
|
+
* lists and makes folders in, reads a reply's media from, and starts Claude Code in to list
|
|
129
|
+
* the models its login can use (../../../src/harness/claude/models.ts);
|
|
130
|
+
* - where its home is the operator's and outside that root, the operator's Claude and Codex
|
|
131
|
+
* homes in it, `~/.claude` and `~/.codex`: Claude Code's login and settings it lists models
|
|
132
|
+
* with, and the Codex login its app-server keeps fresh and lists models with, and the
|
|
133
|
+
* configuration it gives folder conversations' Codex (../../../src/codex/configuration.ts);
|
|
134
|
+
* the rest of that home its own, an emptyDir.
|
|
135
|
+
* Nothing else: no socket, binary, or other path of the machine any mount gives. A path no mount
|
|
136
|
+
* gives is mounted from nowhere, as it was not given the machine's.
|
|
137
|
+
*/
|
|
138
|
+
export function hostPaths(config, home) {
|
|
139
|
+
const { host } = config;
|
|
140
|
+
if (!host.enabled)
|
|
141
|
+
return { mounts: [], volumes: [], homeIsGiven: false };
|
|
142
|
+
const under = (path, of) => path === of || path.startsWith(`${of.replace(/\/$/u, "")}/`);
|
|
143
|
+
/** Where on the machine the mount that gives `path` has it, and whether it gives it read-only; null where none does. */
|
|
144
|
+
const given = (path) => {
|
|
145
|
+
const [mount] = host.mounts.filter(({ mountPath }) => under(path, mountPath)).sort((a, b) => b.mountPath.length - a.mountPath.length);
|
|
146
|
+
return mount ? { hostPath: posix.join(mount.hostPath, posix.relative(mount.mountPath, path)), readOnly: mount.readOnly } : null;
|
|
147
|
+
};
|
|
148
|
+
const root = host.workspaceRoot || home;
|
|
149
|
+
const operatorHome = host.alasioHome;
|
|
150
|
+
const paths = [
|
|
151
|
+
{ name: "workspace-root", path: root, type: "Directory" },
|
|
152
|
+
...(operatorHome && !under(home, root)
|
|
153
|
+
? [{ name: "claude-home", path: posix.join(home, ".claude"), type: "DirectoryOrCreate" }, { name: "codex-home", path: posix.join(home, ".codex"), type: "DirectoryOrCreate" }]
|
|
154
|
+
: []),
|
|
155
|
+
];
|
|
156
|
+
const mounts = [];
|
|
157
|
+
const volumes = [];
|
|
158
|
+
for (const { name, path, type } of paths) {
|
|
159
|
+
const at = given(path);
|
|
160
|
+
if (!at)
|
|
161
|
+
continue;
|
|
162
|
+
mounts.push({ name, mountPath: path, ...(at.readOnly ? { readOnly: true } : {}) });
|
|
163
|
+
volumes.push({ name, hostPath: { path: at.hostPath, type } });
|
|
164
|
+
}
|
|
165
|
+
return { mounts, volumes, homeIsGiven: operatorHome && under(home, root) && given(root) !== null };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* alasio's Deployment: of alasio.scaling's replicas, which KEDA moves between its min and max
|
|
169
|
+
* where they are apart (alasioScaling). At one at most, an update stops the old pod before the
|
|
170
|
+
* new one starts; beyond, the new one starts beside the old, which drains as it stops, within
|
|
171
|
+
* STOP_GRACE_SECONDS (src/telegram/stop.ts, src/telegram/drain.ts). It keeps no volume: its state is in Neon,
|
|
364
172
|
* and what it writes to disk (received files written for the agent, its home unless it
|
|
365
173
|
* is the operator's) is written again as needed, on emptyDirs. Under the
|
|
366
|
-
* host profile it runs as the operator, in their home, with the machine
|
|
174
|
+
* host profile it runs as the operator, in their home, with what of the machine it reads
|
|
175
|
+
* itself (hostPaths), no more of the machine's mounts. A
|
|
367
176
|
* branch environment's runs in its namespace on the branch's database, told it is the
|
|
368
177
|
* branch, with its token to ask main to fork the sessions it inherited; main's is given
|
|
369
178
|
* the key branch environments' tokens are signed with, where it forks sessions. Its pod
|
|
370
|
-
* carries
|
|
371
|
-
*
|
|
179
|
+
* carries, where up read it, the checksum of its bot's Secret, so it is replaced when it
|
|
180
|
+
* changes.
|
|
372
181
|
*/
|
|
373
|
-
function deployment(config,
|
|
182
|
+
function deployment(config, botChecksum, environment) {
|
|
374
183
|
const { alasio, host } = config;
|
|
375
184
|
const operatorHome = host.enabled && host.alasioHome;
|
|
376
185
|
const home = operatorHome ? host.home : "/var/lib/alasio/home";
|
|
@@ -378,25 +187,28 @@ function deployment(config, templatesChecksum, botChecksum, environment) {
|
|
|
378
187
|
const gid = host.enabled ? host.gid : alasio.runAsGroup;
|
|
379
188
|
const groups = [...new Set([...alasio.supplementalGroups, ...(host.enabled ? host.supplementalGroups : [])])];
|
|
380
189
|
const image = imageReference(config.images.alasio);
|
|
381
|
-
const branchSecret = branchSecretVolume(
|
|
190
|
+
const branchSecret = branchSecretVolume(environment);
|
|
191
|
+
const machine = hostPaths(config, home);
|
|
192
|
+
// Its home its own, an emptyDir, but where the machine's root it is given holds it.
|
|
193
|
+
const ownHome = !machine.homeIsGiven;
|
|
382
194
|
return {
|
|
383
195
|
apiVersion: "apps/v1",
|
|
384
196
|
kind: "Deployment",
|
|
385
197
|
metadata: { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") },
|
|
386
198
|
spec: {
|
|
387
|
-
...replicasOf(
|
|
388
|
-
strategy: { type: "Recreate" },
|
|
199
|
+
...replicasOf(alasio.scaling),
|
|
200
|
+
strategy: alasio.scaling.max > 1 ? { type: "RollingUpdate", rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } } : { type: "Recreate" },
|
|
389
201
|
selector: { matchLabels: selectorLabels("alasio") },
|
|
390
202
|
template: {
|
|
391
203
|
metadata: {
|
|
392
204
|
labels: labels("alasio"),
|
|
393
|
-
|
|
205
|
+
...(botChecksum === null ? {} : { annotations: { "checksum/telegram": botChecksum } }),
|
|
394
206
|
},
|
|
395
207
|
spec: {
|
|
396
208
|
serviceAccountName: RELEASE,
|
|
397
209
|
...priority("turn"),
|
|
398
|
-
// alasio finishes its turns' bookkeeping and records what it interrupts as it stops.
|
|
399
|
-
terminationGracePeriodSeconds:
|
|
210
|
+
// alasio finishes its turns' bookkeeping and records what it interrupts as it stops, and delivers what it owes.
|
|
211
|
+
terminationGracePeriodSeconds: STOP_GRACE_SECONDS,
|
|
400
212
|
securityContext: { ...restrictedPod(uid, gid), ...given("supplementalGroups", groups) },
|
|
401
213
|
...imagePullSecrets(config),
|
|
402
214
|
containers: [{
|
|
@@ -406,68 +218,51 @@ function deployment(config, templatesChecksum, botChecksum, environment) {
|
|
|
406
218
|
workingDir: home,
|
|
407
219
|
env: alasioEnv(config, home, operatorHome, environment),
|
|
408
220
|
...given("envFrom", [...alasio.envFrom]),
|
|
409
|
-
ports: [
|
|
410
|
-
{ name: "telemetry", containerPort: config.telemetry.receiverPort },
|
|
411
|
-
...(environment.branch === null && forksForBranches(config) ? [{ name: "branch-forks", containerPort: BRANCH_FORK_PORT }] : []),
|
|
412
|
-
],
|
|
413
221
|
securityContext: restrictedContainer(),
|
|
414
222
|
resources: alasio.resources,
|
|
415
223
|
volumeMounts: [
|
|
416
224
|
{ name: "state", mountPath: "/var/lib/alasio/state" },
|
|
417
|
-
...(
|
|
418
|
-
{ name: "templates", mountPath: "/etc/alasio", readOnly: true },
|
|
225
|
+
...(ownHome ? [{ name: "alasio-home", mountPath: home }] : []),
|
|
419
226
|
{ name: "database", mountPath: "/run/alasio/database", readOnly: true },
|
|
420
227
|
{ name: "log", mountPath: "/run/alasio/log", readOnly: true },
|
|
421
228
|
...branchSecret.map(({ name }) => ({ name, mountPath: BRANCH_SECRETS, readOnly: true })),
|
|
422
|
-
...
|
|
229
|
+
...machine.mounts,
|
|
423
230
|
],
|
|
424
231
|
}],
|
|
425
232
|
volumes: [
|
|
426
233
|
{ name: "state", emptyDir: {} },
|
|
427
|
-
...(
|
|
428
|
-
{ name: "templates", configMap: { name: componentName("sandbox-templates") } },
|
|
234
|
+
...(ownHome ? [{ name: "alasio-home", emptyDir: {} }] : []),
|
|
429
235
|
{
|
|
430
236
|
name: "database",
|
|
431
237
|
secret: {
|
|
432
|
-
secretName:
|
|
433
|
-
items: ["url", "lake-verifier", ...readerRoles(config, environment).map(({ key }) => key)].map((key) => ({ key, path: key })),
|
|
238
|
+
secretName: DATABASE_SECRET,
|
|
239
|
+
items: ["url", "lake-verifier", "harness-verifier", "codex-login-verifier", ...readerRoles(config, environment).map(({ key }) => key)].map((key) => ({ key, path: key })),
|
|
434
240
|
},
|
|
435
241
|
},
|
|
436
242
|
{ name: "log", secret: { secretName: environmentLogSecret(environment), items: [{ key: "password", path: "password" }] } },
|
|
437
243
|
...branchSecret,
|
|
438
|
-
...
|
|
244
|
+
...machine.volumes,
|
|
439
245
|
],
|
|
440
246
|
...given("nodeSelector", { ...alasio.nodeSelector }),
|
|
441
247
|
...given("tolerations", [...alasio.tolerations]),
|
|
442
248
|
...given("affinity", alasio.affinity),
|
|
249
|
+
// Its replicas, where it may run more than one, on as many nodes as there are.
|
|
250
|
+
...spread("alasio", alasio.scaling),
|
|
443
251
|
},
|
|
444
252
|
},
|
|
445
253
|
},
|
|
446
254
|
};
|
|
447
255
|
}
|
|
448
|
-
/** Where session sandboxes export their telemetry: alasio's receiver. */
|
|
449
|
-
function telemetryService(config, environment) {
|
|
450
|
-
return {
|
|
451
|
-
apiVersion: "v1",
|
|
452
|
-
kind: "Service",
|
|
453
|
-
metadata: { name: componentName("telemetry"), namespace: environment.namespace, labels: labels("alasio") },
|
|
454
|
-
spec: { selector: selectorLabels("alasio"), ports: [{ name: "otlp-http", port: config.telemetry.receiverPort, targetPort: "telemetry" }] },
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
/** Where branch environments ask main to fork the sessions they inherited (src/branch/fork.ts). */
|
|
458
|
-
function branchForksService() {
|
|
459
|
-
return {
|
|
460
|
-
apiVersion: "v1",
|
|
461
|
-
kind: "Service",
|
|
462
|
-
metadata: { name: componentName("branches"), namespace: NAMESPACE, labels: labels("alasio") },
|
|
463
|
-
spec: { selector: selectorLabels("alasio"), ports: [{ name: "branch-forks", port: BRANCH_FORK_PORT, targetPort: "branch-forks" }] },
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
256
|
/**
|
|
467
257
|
* The namespaces workspaces run in, which alasio makes unless told not to. Sessions'
|
|
468
258
|
* enforces Pod Security "restricted": nothing privileged, no host paths, no added
|
|
469
|
-
* capabilities
|
|
470
|
-
*
|
|
259
|
+
* capabilities; but where the sandbox service runs, whose pools' pods mount their own
|
|
260
|
+
* directories on their nodes, a host path no level but "privileged" allows, it enforces that
|
|
261
|
+
* and warns and audits of "restricted", the sandbox pods' policy holding every pod of it to the
|
|
262
|
+
* pools' templates instead (./admission.ts sandboxPodPolicy). The host profile's is
|
|
263
|
+
* "privileged", as its pods mount the machine. A branch environment always has its own,
|
|
264
|
+
* beside its own namespace, which records the environment it was branched from, main's
|
|
265
|
+
* (BRANCH_PARENT_LABEL), as the sandbox gateway forks main's sessions into it for that alone.
|
|
471
266
|
*/
|
|
472
267
|
function namespaces(config, environment) {
|
|
473
268
|
const { sessions, host } = config;
|
|
@@ -475,7 +270,7 @@ function namespaces(config, environment) {
|
|
|
475
270
|
const branched = branch ? { [BRANCH_LABEL]: branch } : {};
|
|
476
271
|
return [
|
|
477
272
|
...(branch ? [{ apiVersion: "v1", kind: "Namespace", metadata: { name: environment.namespace, labels: { ...labels("alasio"), ...branched } } }] : []),
|
|
478
|
-
...(sessions.
|
|
273
|
+
...(sessions.createNamespace || branch
|
|
479
274
|
? [{
|
|
480
275
|
apiVersion: "v1",
|
|
481
276
|
kind: "Namespace",
|
|
@@ -484,9 +279,12 @@ function namespaces(config, environment) {
|
|
|
484
279
|
labels: {
|
|
485
280
|
...labels("session"),
|
|
486
281
|
...branched,
|
|
487
|
-
|
|
488
|
-
"pod-security.kubernetes.io/enforce
|
|
282
|
+
...(branch ? { [BRANCH_PARENT_LABEL]: MAIN_ENVIRONMENT_NAME } : {}),
|
|
283
|
+
"pod-security.kubernetes.io/enforce": "privileged",
|
|
284
|
+
"pod-security.kubernetes.io/audit": "restricted",
|
|
285
|
+
"pod-security.kubernetes.io/audit-version": "latest",
|
|
489
286
|
"pod-security.kubernetes.io/warn": "restricted",
|
|
287
|
+
"pod-security.kubernetes.io/warn-version": "latest",
|
|
490
288
|
},
|
|
491
289
|
},
|
|
492
290
|
}]
|
|
@@ -501,97 +299,34 @@ function namespaces(config, environment) {
|
|
|
501
299
|
];
|
|
502
300
|
}
|
|
503
301
|
/**
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return workspaceStorage.enabled && workspaceStorage.csi.enabled;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* alasio's identity: it drives its workspaces' Sandboxes, their token Secrets, and reads
|
|
513
|
-
* files from sessions through exec, in the namespaces it owns, labelling their pods busy
|
|
514
|
-
* while turns run in them, as it does its own in its namespace (src/kube/busy.ts); where
|
|
515
|
-
* it snapshots sessions, sets a restored session's readiness; where it forks
|
|
516
|
-
* sessions, reads sessions' volume claims and runs the Jobs that clone them in its own;
|
|
517
|
-
* where the node agents run, watches pods in its own, theirs among them, and reads the
|
|
518
|
-
* cluster's nodes, for a restore to go to one that takes its pod; and, where it installs
|
|
519
|
-
* JuiceFS's driver, lists and deletes pods in the driver's namespace, its mount pods; and
|
|
520
|
-
* nothing else.
|
|
302
|
+
* alasio's identity: it labels its own pod busy while turns run in it (src/kube/busy.ts),
|
|
303
|
+
* reads it to stop as it is deleted (src/kube/own-pod.ts), and keeps the time of its last
|
|
304
|
+
* start in a ConfigMap of its own, made here empty, which it may read and patch alone
|
|
305
|
+
* (src/kube/started.ts); nothing else. Its sessions and its folder workspaces are the sandbox
|
|
306
|
+
* gateway's, which it reaches by its key alone.
|
|
521
307
|
*/
|
|
522
308
|
function identity(config, environment) {
|
|
523
|
-
const main = environment.branch === null;
|
|
524
|
-
const binding = (metadata) => ({
|
|
525
|
-
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
526
|
-
kind: "RoleBinding",
|
|
527
|
-
metadata,
|
|
528
|
-
subjects: [{ kind: "ServiceAccount", name: RELEASE, namespace: environment.namespace }],
|
|
529
|
-
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: RELEASE },
|
|
530
|
-
});
|
|
531
|
-
const inDriverNamespace = { name: RELEASE, namespace: config.workspaceStorage.csi.namespace, labels: labels("alasio") };
|
|
532
|
-
const mountPods = {
|
|
533
|
-
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
534
|
-
kind: "Role",
|
|
535
|
-
metadata: inDriverNamespace,
|
|
536
|
-
rules: [{ apiGroups: [""], resources: ["pods"], verbs: ["list", "delete"] }],
|
|
537
|
-
};
|
|
538
309
|
const inOwnNamespace = { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") };
|
|
539
310
|
const own = {
|
|
540
311
|
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
541
312
|
kind: "Role",
|
|
542
313
|
metadata: inOwnNamespace,
|
|
543
314
|
rules: [
|
|
544
|
-
|
|
545
|
-
{ apiGroups: [""], resources: ["
|
|
546
|
-
...(main && sessionsForked(config) ? [{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["create", "get", "delete"] }] : []),
|
|
315
|
+
{ apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "patch"] },
|
|
316
|
+
{ apiGroups: [""], resources: ["configmaps"], resourceNames: [STARTED_CONFIG_MAP], verbs: ["get", "patch"] },
|
|
547
317
|
],
|
|
548
318
|
};
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
const nodesRole = {
|
|
553
|
-
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
554
|
-
kind: "ClusterRole",
|
|
555
|
-
metadata: nodes,
|
|
556
|
-
rules: [{ apiGroups: [""], resources: ["nodes"], verbs: ["get"] }],
|
|
557
|
-
};
|
|
558
|
-
const nodesBinding = {
|
|
319
|
+
// Of no data, which alasio's starts write and `alasio up` leaves to them.
|
|
320
|
+
const started = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: STARTED_CONFIG_MAP, namespace: environment.namespace, labels: labels("alasio") } };
|
|
321
|
+
const binding = {
|
|
559
322
|
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
560
|
-
kind: "
|
|
561
|
-
metadata:
|
|
323
|
+
kind: "RoleBinding",
|
|
324
|
+
metadata: inOwnNamespace,
|
|
562
325
|
subjects: [{ kind: "ServiceAccount", name: RELEASE, namespace: environment.namespace }],
|
|
563
|
-
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "
|
|
326
|
+
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: RELEASE },
|
|
564
327
|
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
...workspaceNamespaces(config, environment).flatMap((namespace) => {
|
|
568
|
-
const metadata = { name: RELEASE, namespace, labels: labels("alasio") };
|
|
569
|
-
const forked = main && sessionsForked(config) && namespace === config.sessions.namespace;
|
|
570
|
-
const snapshotted = main && nodeAgentRuns(config) && namespace === config.sessions.namespace;
|
|
571
|
-
const role = {
|
|
572
|
-
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
573
|
-
kind: "Role",
|
|
574
|
-
metadata,
|
|
575
|
-
rules: [
|
|
576
|
-
{ apiGroups: ["agents.x-k8s.io"], resources: ["sandboxes"], verbs: ["get", "list", "watch", "create", "patch", "delete"] },
|
|
577
|
-
{ apiGroups: [""], resources: ["secrets"], verbs: ["get", "create"] },
|
|
578
|
-
{ apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "patch"] },
|
|
579
|
-
{ apiGroups: [""], resources: ["pods/exec"], verbs: ["create", "get"] },
|
|
580
|
-
...(forked ? [{ apiGroups: [""], resources: ["persistentvolumeclaims"], verbs: ["get"] }] : []),
|
|
581
|
-
// A restored session's readiness, once its sandbox is let run, and the network policies that must hold it first (src/sandbox/snapshots.ts).
|
|
582
|
-
...(snapshotted
|
|
583
|
-
? [{ apiGroups: [""], resources: ["pods/status"], verbs: ["patch"] }, { apiGroups: ["networking.k8s.io"], resources: ["networkpolicies"], verbs: ["list"] }]
|
|
584
|
-
: []),
|
|
585
|
-
],
|
|
586
|
-
};
|
|
587
|
-
return [role, binding(metadata)];
|
|
588
|
-
}),
|
|
589
|
-
own,
|
|
590
|
-
binding(inOwnNamespace),
|
|
591
|
-
...(main && mountPodsRemoved(config) ? [mountPods, binding(inDriverNamespace)] : []),
|
|
592
|
-
...(main && nodeAgentRuns(config) ? [nodesRole, nodesBinding] : []),
|
|
593
|
-
...(config.host.enabled ? hostAgentIdentity(config) : []),
|
|
594
|
-
];
|
|
328
|
+
const serviceAccount = { apiVersion: "v1", kind: "ServiceAccount", metadata: { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") } };
|
|
329
|
+
return [serviceAccount, own, binding, started, ...(environment.branch === null && foldersRun(config) ? hostAgentIdentity(config) : [])];
|
|
595
330
|
}
|
|
596
331
|
/**
|
|
597
332
|
* Folder workspaces' bayma: the operator's agents, which may restart alasio as they
|
|
@@ -624,33 +359,50 @@ function hostAgentIdentity(config) {
|
|
|
624
359
|
};
|
|
625
360
|
return [serviceAccount, role, binding];
|
|
626
361
|
}
|
|
362
|
+
/** How many updates waiting on its environment's Telegram topic ask for one more alasio: a few chats' bursts. */
|
|
363
|
+
const UPDATES_PER_ALASIO = 20;
|
|
364
|
+
/** What a bound harness asks of alasio's scale: so much that any number of them ask for one alasio, to hear and deliver what they do, and no more. */
|
|
365
|
+
const BOUND_TARGET = 1_000_000;
|
|
366
|
+
/**
|
|
367
|
+
* KEDA's scaling of the environment's alasio (deployment), where its scale's min and max are
|
|
368
|
+
* apart: up from none at the first update, event or fork request waiting on its topics, or
|
|
369
|
+
* harness bound to one of its conversations, and up by one for each UPDATES_PER_ALASIO updates
|
|
370
|
+
* waiting; back to its min once none of that has been seen for its cooldown. Its groups' lags
|
|
371
|
+
* read as the log's scaler principal; a group that has read none of a topic counts its records
|
|
372
|
+
* as it reads them, from its first or from the next written (src/events/consume.ts).
|
|
373
|
+
*/
|
|
374
|
+
export function alasioScaling(config, environment, alasio) {
|
|
375
|
+
const name = environmentName(environment);
|
|
376
|
+
const authentication = RELEASE;
|
|
377
|
+
const triggers = [
|
|
378
|
+
lagTrigger(alasioGroup(name), telegramTopic(name), UPDATES_PER_ALASIO, authentication),
|
|
379
|
+
lagTrigger(eventsGroup(name), eventsTopic(name), 1, authentication, environment.branch === null ? "latest" : "earliest"),
|
|
380
|
+
...(environment.branch === null ? [lagTrigger(forksGroup(MAIN_ENVIRONMENT_NAME), forksTopic(MAIN_ENVIRONMENT_NAME), 1, authentication)] : []),
|
|
381
|
+
// Its harnesses bound, of every pool in its namespace, as the gateway counts them.
|
|
382
|
+
{
|
|
383
|
+
type: "metrics-api",
|
|
384
|
+
metadata: { url: `http://${GATEWAY}.${NAMESPACE}.svc.cluster.local:${GATEWAY_PORT}/counts/${environment.namespace}`, valueLocation: "bound", targetValue: String(BOUND_TARGET), activationTargetValue: "0" },
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
const scaledObjects = scaledObject(alasio, config.alasio.scaling, triggers, { pollingSeconds: config.alasio.pollingSeconds, cooldownSeconds: config.alasio.cooldownSeconds });
|
|
388
|
+
return scaledObjects.length > 0 ? [logAuthentication(authentication, environment.namespace, "alasio"), ...scaledObjects] : [];
|
|
389
|
+
}
|
|
627
390
|
/** The objects of the alasio of `environment`, main's unless a branch environment's, with what main's carries forward. */
|
|
628
391
|
export function alasioObjects(config, environment = MAIN_ENVIRONMENT, carried = NOTHING_CARRIED) {
|
|
629
|
-
const
|
|
630
|
-
const alasio = deployment(config, templates.checksum, carried.botChecksum, environment);
|
|
392
|
+
const alasio = deployment(config, carried.botChecksum, environment);
|
|
631
393
|
return [
|
|
632
394
|
...namespaces(config, environment),
|
|
633
395
|
...identity(config, environment),
|
|
634
|
-
templates.configMap,
|
|
635
396
|
alasio,
|
|
397
|
+
...alasioScaling(config, environment, alasio),
|
|
636
398
|
// It runs turns, in its harnesses, which a recreation would end.
|
|
637
|
-
verticalPodAutoscaler(alasio, "
|
|
638
|
-
// A drain waits for the turns running in its pod and its
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
...(
|
|
642
|
-
// What alasio may do beyond what RBAC can say
|
|
643
|
-
...(environment.branch === null ?
|
|
644
|
-
// Where sessions are snapshotted, what alasio may set of a restored session's pod's status.
|
|
645
|
-
...(environment.branch === null && config.sessions.enabled && nodeAgentRuns(config)
|
|
646
|
-
? resumedConditionPolicy(componentName("resumed-condition"), alasioAccount(environment.namespace), config.sessions.namespace)
|
|
647
|
-
: []),
|
|
648
|
-
...(environment.branch === null && sessionsForked(config) ? cloneJobPolicy(cloneJob(config)) : []),
|
|
649
|
-
...sandboxPolicy(sandboxPolicyName(environment.namespace), alasioAccount(environment.namespace), {
|
|
650
|
-
sessions: config.sessions.enabled ? sessionsProfile(config, environment, carried) : null,
|
|
651
|
-
previousSessions: config.sessions.enabled ? carried.previousSessions : null,
|
|
652
|
-
host: config.host.enabled ? hostProfile(config) : null,
|
|
653
|
-
}),
|
|
399
|
+
verticalPodAutoscaler(alasio, "AtStart"),
|
|
400
|
+
// A drain waits for the turns running in its pod to end, and evicts one of its pods not busy at a time.
|
|
401
|
+
busyBudget(environment.namespace),
|
|
402
|
+
...replicaBudget("alasio", environment.namespace, config.alasio.scaling, { unlessBusy: true }),
|
|
403
|
+
...codexLoginObjects(config, environment),
|
|
404
|
+
// What alasio may do beyond what RBAC can say: the cluster's, made with main's.
|
|
405
|
+
...(environment.branch === null ? busyLabelPolicy() : []),
|
|
654
406
|
];
|
|
655
407
|
}
|
|
656
408
|
//# sourceMappingURL=alasio.js.map
|