alasio 8.0.0 → 9.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.
Files changed (134) hide show
  1. package/dist/cli/src/api-server.js +18 -0
  2. package/dist/cli/src/api-server.js.map +1 -0
  3. package/dist/cli/src/branches.js +61 -20
  4. package/dist/cli/src/branches.js.map +1 -1
  5. package/dist/cli/src/cluster/docker.js +25 -10
  6. package/dist/cli/src/cluster/docker.js.map +1 -1
  7. package/dist/cli/src/cluster/host.js +33 -7
  8. package/dist/cli/src/cluster/host.js.map +1 -1
  9. package/dist/cli/src/cluster/k3s.js +43 -11
  10. package/dist/cli/src/cluster/k3s.js.map +1 -1
  11. package/dist/cli/src/cluster/node.js +107 -32
  12. package/dist/cli/src/cluster/node.js.map +1 -1
  13. package/dist/cli/src/cluster/stargz.js +55 -0
  14. package/dist/cli/src/cluster/stargz.js.map +1 -0
  15. package/dist/cli/src/commands/branch.js +5 -4
  16. package/dist/cli/src/commands/branch.js.map +1 -1
  17. package/dist/cli/src/commands/common.js +9 -3
  18. package/dist/cli/src/commands/common.js.map +1 -1
  19. package/dist/cli/src/commands/folder.js +14 -0
  20. package/dist/cli/src/commands/folder.js.map +1 -0
  21. package/dist/cli/src/commands/status.js +64 -12
  22. package/dist/cli/src/commands/status.js.map +1 -1
  23. package/dist/cli/src/commands/uninstall.js +8 -9
  24. package/dist/cli/src/commands/uninstall.js.map +1 -1
  25. package/dist/cli/src/commands/up.js +11 -3
  26. package/dist/cli/src/commands/up.js.map +1 -1
  27. package/dist/cli/src/commands/upgrade.js +1 -1
  28. package/dist/cli/src/commands/upgrade.js.map +1 -1
  29. package/dist/cli/src/commands.js +2 -1
  30. package/dist/cli/src/commands.js.map +1 -1
  31. package/dist/cli/src/config.js +4 -1
  32. package/dist/cli/src/config.js.map +1 -1
  33. package/dist/cli/src/cpu-baseline.js +8 -4
  34. package/dist/cli/src/cpu-baseline.js.map +1 -1
  35. package/dist/cli/src/folders.js +67 -0
  36. package/dist/cli/src/folders.js.map +1 -0
  37. package/dist/cli/src/gateway-calls.js +58 -0
  38. package/dist/cli/src/gateway-calls.js.map +1 -0
  39. package/dist/cli/src/install.js +77 -29
  40. package/dist/cli/src/install.js.map +1 -1
  41. package/dist/cli/src/kept-releases.js +27 -20
  42. package/dist/cli/src/kept-releases.js.map +1 -1
  43. package/dist/cli/src/kube/api.js +2 -3
  44. package/dist/cli/src/kube/api.js.map +1 -1
  45. package/dist/cli/src/kube/apply.js +15 -72
  46. package/dist/cli/src/kube/apply.js.map +1 -1
  47. package/dist/cli/src/kube/rollout.js +28 -16
  48. package/dist/cli/src/kube/rollout.js.map +1 -1
  49. package/dist/cli/src/manifests/admission.js +342 -223
  50. package/dist/cli/src/manifests/admission.js.map +1 -1
  51. package/dist/cli/src/manifests/alasio.js +58 -394
  52. package/dist/cli/src/manifests/alasio.js.map +1 -1
  53. package/dist/cli/src/manifests/branch.js +10 -26
  54. package/dist/cli/src/manifests/branch.js.map +1 -1
  55. package/dist/cli/src/manifests/collector.js +60 -44
  56. package/dist/cli/src/manifests/collector.js.map +1 -1
  57. package/dist/cli/src/manifests/common.js +32 -2
  58. package/dist/cli/src/manifests/common.js.map +1 -1
  59. package/dist/cli/src/manifests/config.js +64 -81
  60. package/dist/cli/src/manifests/config.js.map +1 -1
  61. package/dist/cli/src/manifests/disruption.js +10 -0
  62. package/dist/cli/src/manifests/disruption.js.map +1 -1
  63. package/dist/cli/src/manifests/folders.js +128 -0
  64. package/dist/cli/src/manifests/folders.js.map +1 -0
  65. package/dist/cli/src/manifests/grafana.js +4 -2
  66. package/dist/cli/src/manifests/grafana.js.map +1 -1
  67. package/dist/cli/src/manifests/index.js +10 -7
  68. package/dist/cli/src/manifests/index.js.map +1 -1
  69. package/dist/cli/src/manifests/lake.js +18 -4
  70. package/dist/cli/src/manifests/lake.js.map +1 -1
  71. package/dist/cli/src/manifests/log.js +32 -14
  72. package/dist/cli/src/manifests/log.js.map +1 -1
  73. package/dist/cli/src/manifests/neon.js +3 -3
  74. package/dist/cli/src/manifests/neon.js.map +1 -1
  75. package/dist/cli/src/manifests/network-policies.js +140 -74
  76. package/dist/cli/src/manifests/network-policies.js.map +1 -1
  77. package/dist/cli/src/manifests/node-agent.js +11 -4
  78. package/dist/cli/src/manifests/node-agent.js.map +1 -1
  79. package/dist/cli/src/manifests/object-store.js +4 -2
  80. package/dist/cli/src/manifests/object-store.js.map +1 -1
  81. package/dist/cli/src/manifests/sandbox-gateway.js +663 -0
  82. package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
  83. package/dist/cli/src/manifests/scalers.js +16 -5
  84. package/dist/cli/src/manifests/scalers.js.map +1 -1
  85. package/dist/cli/src/manifests/sizing.js +16 -8
  86. package/dist/cli/src/manifests/sizing.js.map +1 -1
  87. package/dist/cli/src/manifests/valkey.js +11 -5
  88. package/dist/cli/src/manifests/valkey.js.map +1 -1
  89. package/dist/cli/src/manifests/vpa.js +3 -1
  90. package/dist/cli/src/manifests/vpa.js.map +1 -1
  91. package/dist/cli/src/manifests/workspace-storage.js +174 -89
  92. package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
  93. package/dist/cli/src/neon-storage.js +54 -14
  94. package/dist/cli/src/neon-storage.js.map +1 -1
  95. package/dist/cli/src/release.js +9 -9
  96. package/dist/cluster/node/pins.json +9 -4
  97. package/dist/neon/control/branches.js +6 -6
  98. package/dist/neon/control/branches.js.map +1 -1
  99. package/dist/neon/control/secrets.js +14 -3
  100. package/dist/neon/control/secrets.js.map +1 -1
  101. package/dist/src/binder/names.js +33 -0
  102. package/dist/src/binder/names.js.map +1 -0
  103. package/dist/src/binder/tokens.js +58 -0
  104. package/dist/src/binder/tokens.js.map +1 -0
  105. package/dist/src/branch/names.js +12 -2
  106. package/dist/src/branch/names.js.map +1 -1
  107. package/dist/src/events/names.js +7 -0
  108. package/dist/src/events/names.js.map +1 -0
  109. package/dist/src/gateway/names.js +201 -0
  110. package/dist/src/gateway/names.js.map +1 -0
  111. package/dist/src/harness/names.js +49 -0
  112. package/dist/src/harness/names.js.map +1 -0
  113. package/dist/src/kube/sandbox-shape.js +8 -68
  114. package/dist/src/kube/sandbox-shape.js.map +1 -1
  115. package/dist/src/node-agent/names.js +52 -2
  116. package/dist/src/node-agent/names.js.map +1 -1
  117. package/dist/src/sandbox/names.js +12 -20
  118. package/dist/src/sandbox/names.js.map +1 -1
  119. package/dist/src/sandbox/otlp-resource.js +173 -0
  120. package/dist/src/sandbox/otlp-resource.js.map +1 -0
  121. package/dist/src/sandbox/telemetry.js +43 -0
  122. package/dist/src/sandbox/telemetry.js.map +1 -0
  123. package/dist/src/telemetry/config.js +131 -0
  124. package/dist/src/telemetry/config.js.map +1 -0
  125. package/dist/src/telemetry/forward.js +154 -0
  126. package/dist/src/telemetry/forward.js.map +1 -0
  127. package/package.json +1 -1
  128. package/dist/cli/src/manifests/agent-sandbox.js +0 -92
  129. package/dist/cli/src/manifests/agent-sandbox.js.map +0 -1
  130. package/dist/cli/src/manifests/juicefs-csi.js +0 -320
  131. package/dist/cli/src/manifests/juicefs-csi.js.map +0 -1
  132. package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +0 -6295
  133. package/dist/src/kube/config.js +0 -145
  134. package/dist/src/kube/config.js.map +0 -1
@@ -0,0 +1,663 @@
1
+ /**
2
+ * The sandbox service (src/gateway/, src/binder/): session workspaces' sandboxes as pools
3
+ * of pods already running, each bound to one workspace while it has work, through the
4
+ * gateway.
5
+ *
6
+ * - The gateway: alasio's image, as its own program, the one door to a pool's pod, of main's
7
+ * sessions and of every branch environment's, which it finds by their namespaces; its
8
+ * configuration, rendered here; its keys, made by the stack's setup
9
+ * (neon/control/kube-setup.ts); a Service of it, which alasio binds through and KEDA asks
10
+ * each pool's counts of, and a headless one its replicas find each other by. In each
11
+ * environment's sessions' namespace (sandboxPoolObjects) it may bind, claim and delete the
12
+ * pods of its pools, hold their Leases, and read their Deployments, whose templates a
13
+ * release's snapshot records; and it finds the node agents.
14
+ * - A pool of each of a session's internets, `none` and `full`, in each environment: a
15
+ * Deployment in its sessions' namespace of pods of the agent image under the sessions' runtime, each its own directory
16
+ * on its node (src/node-agent/names.ts ATTACH_ROOT, by `subPathExpr` of its namespace and its name) at /attach,
17
+ * where its workspace is attached once it is bound, its bayma started once its binding's
18
+ * token is beside it (sandbox/agent/start.sh). KEDA keeps `min` free, and makes more, to
19
+ * `max`, for the binds that wait; a bound pod is out of its Deployment, which replaces it.
20
+ * A pod that ends is ended: it serves one binding. Its template changes as a whole: the old
21
+ * free pods go as the new come, as none is ready before it is bound. A bound pod is evicted
22
+ * by no drain (./disruption.ts boundBudget).
23
+ * - The session telemetry receiver (src/receiver/): alasio's image, as its own program,
24
+ * watching the pools' Leases of every environment, which bound pods' bayma exports to.
25
+ * - The node's mount of workspace storage, a pod of each node's own, held through upgrades of
26
+ * the node agent, which attaches workspaces from it: a new one takes its mount over before
27
+ * the old goes.
28
+ */
29
+ import { createHash } from "node:crypto";
30
+ import { isDeepStrictEqual } from "node:util";
31
+ import { Schema } from "effect";
32
+ import { freeLabels } from "../../../src/binder/names.js";
33
+ import { MAIN_ENVIRONMENT_NAME } from "../../../src/branch/names.js";
34
+ import { BINDING_KEY_VARIABLE, CALLER_KEY_VARIABLE, EVENTS_BROKERS_VARIABLE, EVENTS_PASSWORD_FILE_VARIABLE, EVENTS_USERNAME_VARIABLE, GATEWAY_CONFIG_VARIABLE, GatewayConfig as GatewayConfigSchema, FOLDERS_NAMESPACE_VARIABLE, RECEIVER_PORT_VARIABLE, SESSIONS_NAMESPACE_VARIABLE, templateHash, } from "../../../src/gateway/names.js";
35
+ import { BAYMA_CONTAINER, BAYMA_STOP_SECONDS, CPU_FEATURES_ANNOTATION, NET_MODE_LABEL, SESSION_TEMPLATE_ANNOTATION, STOP_WAIT_ARG } from "../../../src/kube/sandbox-shape.js";
36
+ import { ATTACH_ROOT, goferOf, gvisorRuntimeClass, NODE_AGENT_PORT, NODE_AGENT_TOKEN_VARIABLE, WORKSPACES_ROOT } from "../../../src/node-agent/names.js";
37
+ import { memoryBytes } from "../capacity.js";
38
+ import { NODE_PINS } from "../cluster/node.js";
39
+ import { imageReference } from "../images.js";
40
+ import { READY_ONCE_BOUND_ANNOTATION } from "../kube/rollout.js";
41
+ import { sessionsNamespace, stopWaitSeconds } from "./alasio.js";
42
+ import { folderTemplate, foldersRun } from "./folders.js";
43
+ import { accountRole, componentName, given, imagePullSecrets, labels, MAIN_ENVIRONMENT, NAMESPACE, nodeAgentRuns, otelEnv, RELEASE, replicasOf, restrictedContainer, restrictedPod, selectorLabels, sha256, } from "./common.js";
44
+ import { resumedConditionPolicy, sandboxPodPolicy } from "./admission.js";
45
+ import { durationSeconds } from "./config.js";
46
+ import { LOG_BROKERS, logPrincipalSecret } from "./log.js";
47
+ import { boundBudget } from "./disruption.js";
48
+ import { priority } from "./priorities.js";
49
+ import { scaledObject } from "./scalers.js";
50
+ import { verticalPodAutoscaler } from "./sizing.js";
51
+ import { JUICEFS_ADMIN, VALKEY, VALKEY_ADDRESS, WORKSPACES_CREDENTIALS, workspacesBucketUrl } from "./workspace-storage.js";
52
+ /** The gateway's name, its pods' component, and its Service's; the headless Service its replicas find each other by. */
53
+ export const GATEWAY = componentName("sandbox-gateway");
54
+ export const GATEWAY_COMPONENT = "sandbox-gateway";
55
+ const GATEWAY_PEERS = componentName("sandbox-gateway-peers");
56
+ /** The port the gateway serves on. */
57
+ export const GATEWAY_PORT = 8080;
58
+ /** The receiver's name and its pods' component. */
59
+ export const RECEIVER = componentName("receiver");
60
+ export const RECEIVER_COMPONENT = "receiver";
61
+ /** The node's mount of workspace storage, its DaemonSet's name and its pods' component. */
62
+ export const WORKSPACE_MOUNT = componentName("workspace-mount");
63
+ export const WORKSPACE_MOUNT_COMPONENT = "workspace-mount";
64
+ /** The port every pool pod's bayma serves MCP on. */
65
+ export const POOL_POD_PORT = 7290;
66
+ /** Each pool's name, by its sessions' internet. */
67
+ export const POOLS = { none: "sandboxes-none", full: "sandboxes-full" };
68
+ /** The component of a pool's pods: a session's. */
69
+ export const POOL_COMPONENT = "session";
70
+ /** Whether the sandbox service runs: for session workspaces on workspace storage, where the node agents run, which attach them, and Neon's setup makes its keys. */
71
+ export function sandboxServiceRuns(config) {
72
+ return config.workspaceStorage.enabled && nodeAgentRuns(config);
73
+ }
74
+ /** The pools of `environment`, by its sessions' internet. */
75
+ export const poolsOf = (config, environment) => {
76
+ const namespace = sessionsNamespace(config, environment);
77
+ return { none: { name: POOLS.none, namespace }, full: { name: POOLS.full, namespace } };
78
+ };
79
+ /**
80
+ * Each sandbox pool's template before the current one, as the gateway's configuration it
81
+ * replaces, `liveGateway` (its ConfigMap's `config.json`), carried it: the one it rendered, where
82
+ * the template changes, or else the one it carried; none where it rendered none, as where its
83
+ * sessions were not snapshotted. A template is carried for one change of it, and no more
84
+ * (./alasio.ts carriedFrom); and pinned into the sandbox pods'
85
+ * policy for the restores of the snapshots taken on it, read back from the cluster: the
86
+ * ConfigMap is that trust's anchor, which only the operator, by `alasio up`, writes. Pure, for tests.
87
+ */
88
+ export function carriedPools(config, liveGateway, kept) {
89
+ const none = { none: null, full: null };
90
+ if (!sandboxServiceRuns(config) || !poolsSnapshotted(config, kept.cpuBaseline) || liveGateway === undefined)
91
+ return none;
92
+ const held = Schema.decodeUnknownOption(Schema.fromJsonString(GatewayConfigSchema))(liveGateway);
93
+ if (held._tag === "None" || held.value.snapshots === null)
94
+ return none;
95
+ const pools = poolsOf(config, MAIN_ENVIRONMENT);
96
+ const carry = (mode) => {
97
+ const { current, previous } = held.value.snapshots?.templates[mode] ?? { current: {}, previous: null };
98
+ const now = JSON.parse(JSON.stringify(poolTemplate(config, pools[mode], mode, kept.cpuBaseline)));
99
+ return (isDeepStrictEqual(current, now) ? previous : current);
100
+ };
101
+ return { none: carry("none"), full: carry("full") };
102
+ }
103
+ /**
104
+ * The CPU baseline the installation whose gateway's configuration is `liveGateway` (as
105
+ * carriedPools takes it) holds sessions to: what its pools' current template sets, if
106
+ * anything, as only a pool of snapshotted sessions is held to one. Pure, for tests.
107
+ */
108
+ export function recordedCpuBaseline(liveGateway) {
109
+ if (liveGateway === undefined)
110
+ return null;
111
+ const held = Schema.decodeUnknownOption(Schema.fromJsonString(GatewayConfigSchema))(liveGateway);
112
+ if (held._tag === "None")
113
+ return null;
114
+ const recorded = held.value.snapshots?.templates.none.current?.metadata?.annotations?.[CPU_FEATURES_ANNOTATION];
115
+ return recorded ? recorded.split(",") : null;
116
+ }
117
+ /** Whether the sandbox pools' sessions are snapshotted: where the node agents run, which save them, and the nodes' CPU baseline is recorded, which they are held to. */
118
+ export const poolsSnapshotted = (config, cpuBaseline) => nodeAgentRuns(config) && cpuBaseline !== null;
119
+ /**
120
+ * The template of a pool's pods, as the module says: a session's, under its internet `mode`.
121
+ * Where its sessions are snapshotted (poolsSnapshotted), as main's are by the gateway, it holds
122
+ * them to the nodes' CPU baseline `cpuBaseline`, its bayma holds a stop it is not given for the
123
+ * gateway's save (sessions.snapshots.stopWait), and it records its own hash
124
+ * (SESSION_TEMPLATE_ANNOTATION), which a snapshot taken on it names it by. Every environment's
125
+ * pools have the same.
126
+ */
127
+ export function poolTemplate(config, pool, mode, cpuBaseline) {
128
+ const template = unhashedPoolTemplate(config, pool, mode, cpuBaseline);
129
+ if (!poolsSnapshotted(config, cpuBaseline))
130
+ return template;
131
+ return { ...template, metadata: { ...template.metadata, annotations: { ...template.metadata?.annotations, [SESSION_TEMPLATE_ANNOTATION]: templateHash(template) } } };
132
+ }
133
+ /** A pool's template, as poolTemplate says, but its hash. */
134
+ function unhashedPoolTemplate(config, pool, mode, cpuBaseline) {
135
+ const { sessions } = config;
136
+ const agent = imageReference(config.images.agent);
137
+ const snapshotted = poolsSnapshotted(config, cpuBaseline);
138
+ return {
139
+ metadata: {
140
+ labels: { "app.kubernetes.io/part-of": "alasio", "app.kubernetes.io/instance": RELEASE, "app.kubernetes.io/component": POOL_COMPONENT, [NET_MODE_LABEL]: mode, ...freeLabels(pool) },
141
+ // Under gVisor, /tmp is a tmpfs of the sandbox's own, by gVisor's mount hints, rather
142
+ // than the emptyDir's directory on the node: a snapshot of the sandbox holds it.
143
+ annotations: {
144
+ "dev.gvisor.spec.mount.tmp.share": "container",
145
+ "dev.gvisor.spec.mount.tmp.type": "tmpfs",
146
+ "dev.gvisor.spec.mount.tmp.options": "rw",
147
+ ...(snapshotted ? { [CPU_FEATURES_ANNOTATION]: cpuBaseline.join(",") } : {}),
148
+ },
149
+ },
150
+ spec: {
151
+ ...(sessions.runtimeClassName ? { runtimeClassName: sessions.runtimeClassName } : {}),
152
+ ...priority("turn"),
153
+ // Ended by other than the gateway, bayma holds its stop for the gateway's save, then stops.
154
+ ...(snapshotted ? { terminationGracePeriodSeconds: stopWaitSeconds(config) + BAYMA_STOP_SECONDS } : {}),
155
+ securityContext: restrictedPod(1000, 1000),
156
+ automountServiceAccountToken: false,
157
+ enableServiceLinks: false,
158
+ // Names resolved as its internet says, as a session's are: by none but itself without one.
159
+ dnsPolicy: "None",
160
+ dnsConfig: { nameservers: mode === "full" ? [...sessions.fullModeNameservers] : ["127.0.0.1"] },
161
+ ...imagePullSecrets(config),
162
+ ...given("nodeSelector", { ...sessions.nodeSelector }),
163
+ ...given("tolerations", [...sessions.tolerations]),
164
+ containers: [{
165
+ name: BAYMA_CONTAINER,
166
+ image: agent,
167
+ imagePullPolicy: config.images.pullPolicy,
168
+ // bayma, and its hook as a release's snapshot runs it, once its workspace is attached.
169
+ command: ["/usr/bin/tini", "-s", "--", "/usr/local/bin/alasio-start", "node", "/opt/bayma/bayma.js"],
170
+ args: [
171
+ "mcp-http",
172
+ "--host",
173
+ "0.0.0.0",
174
+ "--port",
175
+ String(POOL_POD_PORT),
176
+ "--state-dir",
177
+ "/home/agent/.bayma",
178
+ "--token-file",
179
+ "/attach/token",
180
+ "--telemetry-token-file",
181
+ "/attach/token",
182
+ ...(snapshotted ? [STOP_WAIT_ARG, String(stopWaitSeconds(config) * 1000)] : []),
183
+ ],
184
+ workingDir: "/attach",
185
+ env: [
186
+ { name: "POD_NAME", valueFrom: { fieldRef: { fieldPath: "metadata.name" } } },
187
+ { name: "POD_NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
188
+ { name: "HOME", value: "/home/agent" },
189
+ { name: "USER", value: "agent" },
190
+ { name: "BAYMA_PAYLOAD_DIR", value: "/opt/bayma/payload" },
191
+ ],
192
+ ports: [{ name: "mcp", containerPort: POOL_POD_PORT }],
193
+ readinessProbe: { tcpSocket: { port: "mcp" }, periodSeconds: 2 },
194
+ securityContext: restrictedContainer(),
195
+ resources: sessions.resources,
196
+ volumeMounts: [
197
+ // Its own directory on its node, by its namespace and its name, as pods of two namespaces
198
+ // may share a name, which its workspace is attached under once it is bound.
199
+ { name: "attach", mountPath: "/attach", subPathExpr: "$(POD_NAMESPACE)/$(POD_NAME)", mountPropagation: "HostToContainer" },
200
+ { name: "tmp", mountPath: "/tmp" },
201
+ ],
202
+ }],
203
+ volumes: [{ name: "attach", hostPath: { path: ATTACH_ROOT, type: "DirectoryOrCreate" } }, { name: "tmp", emptyDir: { sizeLimit: "2Gi" } }],
204
+ },
205
+ };
206
+ }
207
+ /** A pool's Deployment, and the ScaledObject that keeps its free pods. */
208
+ function poolObjects(config, pool, mode, cpuBaseline) {
209
+ const template = poolTemplate(config, pool, mode, cpuBaseline);
210
+ const deployment = {
211
+ apiVersion: "apps/v1",
212
+ kind: "Deployment",
213
+ // Ready, as `alasio up` and `alasio status` say it, once its pods run: each is ready only once bound.
214
+ metadata: { name: pool.name, namespace: pool.namespace, labels: labels(POOL_COMPONENT), annotations: { [READY_ONCE_BOUND_ANNOTATION]: "true" } },
215
+ spec: {
216
+ ...replicasOf(config.sessions.pools),
217
+ selector: { matchLabels: { ...freeLabels(pool) } },
218
+ // None is ready before it is bound: the old free pods go as the new come.
219
+ strategy: { type: "Recreate" },
220
+ template,
221
+ },
222
+ };
223
+ // As many free as binds wait for, past `min`: each is claimed by one.
224
+ const waiting = {
225
+ type: "metrics-api",
226
+ metadata: { url: `http://${GATEWAY}.${NAMESPACE}.svc.cluster.local:${GATEWAY_PORT}/counts/${pool.namespace}/${pool.name}`, valueLocation: "waiting", targetValue: "1" },
227
+ };
228
+ // Sized as recommended alone: a sandbox's resources change only as a new pod is made.
229
+ return [deployment, ...scaledObject(deployment, config.sessions.pools, [waiting]), verticalPodAutoscaler(deployment, "Recommended")];
230
+ }
231
+ /** The gateway's configuration, as src/gateway/gateway.ts reads it. */
232
+ function gatewayConfig(config, carried) {
233
+ const pools = poolsOf(config, MAIN_ENVIRONMENT);
234
+ const templates = (mode) => ({ current: { ...poolTemplate(config, pools[mode], mode, carried.cpuBaseline) }, previous: carried.previousPools[mode] ? { ...carried.previousPools[mode] } : null });
235
+ return {
236
+ port: GATEWAY_PORT,
237
+ peers: `${GATEWAY_PEERS}.${NAMESPACE}.svc.cluster.local`,
238
+ podPort: POOL_POD_PORT,
239
+ waitSeconds: 120,
240
+ // A duration, as the configuration checks it.
241
+ releaseAfterSeconds: durationSeconds(config.sessions.snapshots.releaseAfter) ?? 0,
242
+ environment: { name: MAIN_ENVIRONMENT_NAME, namespace: sessionsNamespace(config, MAIN_ENVIRONMENT) },
243
+ // A new session's volume's size, as the configuration checks it.
244
+ quotaBytes: memoryBytes(config.sessions.storage.size),
245
+ // Under the releases the cluster keeps, each restored with a RuntimeClass of its own.
246
+ snapshots: poolsSnapshotted(config, carried.cpuBaseline)
247
+ ? {
248
+ liveEverySeconds: durationSeconds(config.sessions.snapshots.liveEvery) ?? 0,
249
+ releases: carried.gvisorReleases.map((release) => ({ release, runtimeClassName: gvisorRuntimeClass(release), gofer: goferOf(release, NODE_PINS.gvisor.restoresThroughSocketSince) })),
250
+ templates: { none: templates("none"), full: templates("full") },
251
+ }
252
+ : null,
253
+ pools: { none: POOLS.none, full: POOLS.full },
254
+ nodeAgents: {
255
+ namespace: NAMESPACE,
256
+ selector: Object.entries(selectorLabels("node-agent")).map(([label, value]) => `${label}=${value}`).join(","),
257
+ port: NODE_AGENT_PORT,
258
+ },
259
+ receiver: { host: `${RECEIVER}.${NAMESPACE}.svc.cluster.local`, port: config.telemetry.receiverPort },
260
+ // Folder conversations' instances, of the host profile, where the installation has folder workspaces.
261
+ folders: foldersRun(config)
262
+ ? { namespace: config.host.namespace, template: { ...folderTemplate(config) }, stateRoot: config.host.stateRoot, releaseAfterSeconds: durationSeconds(config.host.releaseAfter) ?? 0 }
263
+ : null,
264
+ };
265
+ }
266
+ /**
267
+ * What lets the ServiceAccount `account` find the branch environments, by their namespaces
268
+ * (src/gateway/environments.ts): a ClusterRole that reads namespaces, and nothing else of the
269
+ * cluster, and its binding.
270
+ */
271
+ function environmentsFinder(account, component) {
272
+ const metadata = { name: account, labels: labels(component) };
273
+ return [
274
+ { apiVersion: "rbac.authorization.k8s.io/v1", kind: "ClusterRole", metadata, rules: [{ apiGroups: [""], resources: ["namespaces"], verbs: ["get", "list", "watch"] }] },
275
+ {
276
+ apiVersion: "rbac.authorization.k8s.io/v1",
277
+ kind: "ClusterRoleBinding",
278
+ metadata,
279
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "ClusterRole", name: account },
280
+ subjects: [{ kind: "ServiceAccount", name: account, namespace: NAMESPACE }],
281
+ },
282
+ ];
283
+ }
284
+ /** The ServiceAccount `account` of alasio's namespace. */
285
+ const serviceAccount = (account, component) => ({ apiVersion: "v1", kind: "ServiceAccount", metadata: { name: account, namespace: NAMESPACE, labels: labels(component) } });
286
+ /** The user the gateway's ServiceAccount is to the API server, which the sandbox pods' policy names as the one maker of restores' pods. */
287
+ export const GATEWAY_USER = `system:serviceaccount:${NAMESPACE}:${GATEWAY}`;
288
+ /**
289
+ * The gateway's identity: its ServiceAccount, the node agents it finds, the branch
290
+ * environments it finds, and the nodes it reads whether a restore's pod may be scheduled onto;
291
+ * what it may do in each environment's pools is sandboxPoolObjects'.
292
+ */
293
+ function gatewayIdentity() {
294
+ const nodes = { name: `${GATEWAY}-nodes`, labels: labels(GATEWAY_COMPONENT) };
295
+ const nodesRole = { apiVersion: "rbac.authorization.k8s.io/v1", kind: "ClusterRole", metadata: nodes, rules: [{ apiGroups: [""], resources: ["nodes"], verbs: ["get"] }] };
296
+ const nodesBinding = {
297
+ apiVersion: "rbac.authorization.k8s.io/v1",
298
+ kind: "ClusterRoleBinding",
299
+ metadata: nodes,
300
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "ClusterRole", name: nodes.name },
301
+ subjects: [{ kind: "ServiceAccount", name: GATEWAY, namespace: NAMESPACE }],
302
+ };
303
+ return [
304
+ serviceAccount(GATEWAY, GATEWAY_COMPONENT),
305
+ nodesRole,
306
+ nodesBinding,
307
+ // The node agents, by their pods, which it attaches workspaces through.
308
+ ...accountRole(GATEWAY, GATEWAY_COMPONENT, NAMESPACE, [{ apiGroups: [""], resources: ["pods"], verbs: ["list"] }]),
309
+ ...environmentsFinder(GATEWAY, GATEWAY_COMPONENT),
310
+ ];
311
+ }
312
+ /** The gateway's Deployment, its configuration's ConfigMap, and its Services. */
313
+ function gatewayObjects(config, carried) {
314
+ const rendered = JSON.stringify(gatewayConfig(config, carried), null, 2);
315
+ const configMap = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: GATEWAY, namespace: NAMESPACE, labels: labels(GATEWAY_COMPONENT) }, data: { "config.json": rendered } };
316
+ const key = (variable, field) => ({ name: variable, valueFrom: { secretKeyRef: { name: GATEWAY, key: field } } });
317
+ const deployment = {
318
+ apiVersion: "apps/v1",
319
+ kind: "Deployment",
320
+ metadata: { name: GATEWAY, namespace: NAMESPACE, labels: labels(GATEWAY_COMPONENT) },
321
+ spec: {
322
+ replicas: 1,
323
+ selector: { matchLabels: selectorLabels(GATEWAY_COMPONENT) },
324
+ template: {
325
+ metadata: { labels: labels(GATEWAY_COMPONENT), annotations: { "checksum/config": sha256(rendered) } },
326
+ spec: {
327
+ serviceAccountName: GATEWAY,
328
+ ...priority("floor"),
329
+ securityContext: restrictedPod(1000, 1000),
330
+ ...imagePullSecrets(config),
331
+ containers: [{
332
+ name: GATEWAY_COMPONENT,
333
+ image: imageReference(config.images.alasio),
334
+ imagePullPolicy: config.images.pullPolicy,
335
+ command: ["node", "/opt/alasio/src/gateway/main.ts"],
336
+ env: [
337
+ { name: GATEWAY_CONFIG_VARIABLE, value: "/etc/gateway/config.json" },
338
+ key(CALLER_KEY_VARIABLE, "caller-key"),
339
+ key(BINDING_KEY_VARIABLE, "binding-key"),
340
+ { name: NODE_AGENT_TOKEN_VARIABLE, valueFrom: { secretKeyRef: { name: componentName("node-agent"), key: "token" } } },
341
+ { name: "POD_IP", valueFrom: { fieldRef: { fieldPath: "status.podIP" } } },
342
+ // Every environment's events topic, which it writes as its principal on the log (src/events/).
343
+ { name: EVENTS_BROKERS_VARIABLE, value: LOG_BROKERS },
344
+ { name: EVENTS_USERNAME_VARIABLE, valueFrom: { secretKeyRef: { name: logPrincipalSecret("sandbox-gateway"), key: "username" } } },
345
+ { name: EVENTS_PASSWORD_FILE_VARIABLE, value: "/run/alasio/log/password" },
346
+ ...(otelEnv(config).length > 0 ? [{ name: "OTEL_SERVICE_NAME", value: GATEWAY }, ...otelEnv(config)] : []),
347
+ ],
348
+ ports: [{ name: "gateway", containerPort: GATEWAY_PORT }],
349
+ readinessProbe: { httpGet: { path: "/healthz", port: "gateway" }, periodSeconds: 5 },
350
+ securityContext: restrictedContainer(),
351
+ resources: config.sessions.gateway.resources,
352
+ volumeMounts: [{ name: "config", mountPath: "/etc/gateway", readOnly: true }, { name: "log", mountPath: "/run/alasio/log", readOnly: true }],
353
+ }],
354
+ volumes: [{ name: "config", configMap: { name: GATEWAY } }, { name: "log", secret: { secretName: logPrincipalSecret("sandbox-gateway"), items: [{ key: "password", path: "password" }] } }],
355
+ },
356
+ },
357
+ },
358
+ };
359
+ const service = (name, headless) => ({
360
+ apiVersion: "v1",
361
+ kind: "Service",
362
+ metadata: { name, namespace: NAMESPACE, labels: labels(GATEWAY_COMPONENT) },
363
+ spec: { ...(headless ? { clusterIP: "None" } : {}), selector: selectorLabels(GATEWAY_COMPONENT), ports: [{ name: "gateway", port: GATEWAY_PORT, targetPort: "gateway" }] },
364
+ });
365
+ return [configMap, deployment, verticalPodAutoscaler(deployment, "InPlace"), service(GATEWAY, false), service(GATEWAY_PEERS, true)];
366
+ }
367
+ /** The receiver: its identity, which finds the branch environments, whose pools' Leases sandboxPoolObjects lets it watch, its Deployment and its Service. */
368
+ function receiverObjects(config) {
369
+ const port = config.telemetry.receiverPort;
370
+ const deployment = {
371
+ apiVersion: "apps/v1",
372
+ kind: "Deployment",
373
+ metadata: { name: RECEIVER, namespace: NAMESPACE, labels: labels(RECEIVER_COMPONENT) },
374
+ spec: {
375
+ replicas: 1,
376
+ selector: { matchLabels: selectorLabels(RECEIVER_COMPONENT) },
377
+ template: {
378
+ metadata: { labels: labels(RECEIVER_COMPONENT) },
379
+ spec: {
380
+ serviceAccountName: RECEIVER,
381
+ ...priority("floor"),
382
+ securityContext: restrictedPod(1000, 1000),
383
+ ...imagePullSecrets(config),
384
+ containers: [{
385
+ name: RECEIVER_COMPONENT,
386
+ image: imageReference(config.images.alasio),
387
+ imagePullPolicy: config.images.pullPolicy,
388
+ command: ["node", "/opt/alasio/src/receiver/main.ts"],
389
+ env: [
390
+ { name: RECEIVER_PORT_VARIABLE, value: String(port) },
391
+ { name: BINDING_KEY_VARIABLE, valueFrom: { secretKeyRef: { name: GATEWAY, key: "binding-key" } } },
392
+ { name: SESSIONS_NAMESPACE_VARIABLE, value: sessionsNamespace(config, MAIN_ENVIRONMENT) },
393
+ ...(foldersRun(config) ? [{ name: FOLDERS_NAMESPACE_VARIABLE, value: config.host.namespace }] : []),
394
+ ...(otelEnv(config).length > 0 ? [{ name: "OTEL_SERVICE_NAME", value: RECEIVER }, ...otelEnv(config)] : []),
395
+ ],
396
+ ports: [{ name: "otlp-http", containerPort: port }],
397
+ securityContext: restrictedContainer(),
398
+ resources: config.sessions.receiver.resources,
399
+ }],
400
+ },
401
+ },
402
+ },
403
+ };
404
+ const service = {
405
+ apiVersion: "v1",
406
+ kind: "Service",
407
+ metadata: { name: RECEIVER, namespace: NAMESPACE, labels: labels(RECEIVER_COMPONENT) },
408
+ spec: { selector: selectorLabels(RECEIVER_COMPONENT), ports: [{ name: "otlp-http", port, targetPort: "otlp-http" }] },
409
+ };
410
+ return [serviceAccount(RECEIVER, RECEIVER_COMPONENT), ...environmentsFinder(RECEIVER, RECEIVER_COMPONENT), deployment, verticalPodAutoscaler(deployment, "InPlace"), service];
411
+ }
412
+ /** Where a workspace mount's pod keeps what is its own: its juicefs's log and pid, and whether it has handed its mount over. */
413
+ export const MOUNT_RUN = "/run/alasio";
414
+ /** Where JuiceFS keeps a mount's open handles on the node, which the mount that takes it over reads. */
415
+ export const JUICEFS_STATE = "/var/lib/alasio/juicefs-state";
416
+ /** The port each node's mount serves JuiceFS's client's metrics on, in its pod's own network, which the collector's scraper reads. */
417
+ export const JUICEFS_METRICS_PORT = 9567;
418
+ /** Where the node runs JuiceFS's command from: its static release, copied from alasio's image, under its digest. */
419
+ export const JUICEFS_BIN = "/var/lib/alasio/bin";
420
+ /** Where the node's mount of workspace storage caches its blocks. */
421
+ export const JUICEFS_CACHE = "/var/lib/alasio/juicefs-cache";
422
+ /** What JuiceFS's mount says once it has handed its FUSE session over to another mount of the same mount point, and ends. */
423
+ export const HANDED_OVER = "transfer FUSE session to others";
424
+ /**
425
+ * What JuiceFS's mount says once its own process serves the mount point: mounted afresh, or
426
+ * having taken another's FUSE session over, as it waits until the mount point names its process
427
+ * and no longer the one before.
428
+ */
429
+ export const SERVING = "is ready at";
430
+ /**
431
+ * The Services the nodes' mount of workspace storage reaches by the addresses it resolves them to
432
+ * as it starts (workspaceMountScript): the metadata engine's, Valkey's, and the bundled object
433
+ * store's, where the bucket is its.
434
+ */
435
+ export const mountServices = (config) => [VALKEY, ...(config.objectStore.bundled.enabled ? [componentName("seaweedfs")] : [])];
436
+ /** The annotation of the mount's template that changes, rolling it, where what it reaches by address does. */
437
+ export const MOUNT_SERVICES_ANNOTATION = "checksum/storage-services";
438
+ /**
439
+ * What the mount reaches by address, as a checksum: the UIDs of its Services, `uids`, and the
440
+ * URLs it resolves, so a Service made anew, whose address is another, or another bucket, rolls it.
441
+ */
442
+ function mountServicesChecksum(config, uids) {
443
+ const reached = { metadata: VALKEY_ADDRESS, bucket: workspacesBucketUrl(config), services: Object.fromEntries(mountServices(config).map((name) => [name, uids[name] ?? null])) };
444
+ return createHash("sha256").update(JSON.stringify(reached)).digest("hex").slice(0, 16);
445
+ }
446
+ export const MOUNT_PATHS = {
447
+ run: MOUNT_RUN,
448
+ bin: JUICEFS_BIN,
449
+ state: JUICEFS_STATE,
450
+ cache: JUICEFS_CACHE,
451
+ juicefs: "/usr/local/bin/juicefs",
452
+ mountinfo: "/proc/1/mountinfo",
453
+ inNode: "nsenter -t 1 -m --",
454
+ resolve: "getent hosts",
455
+ };
456
+ /**
457
+ * The script of a workspace mount's pod: JuiceFS's mount of workspace storage at `root`, made
458
+ * in the node's own mount namespace, so it is the node's alone, and no container's mounts, torn
459
+ * down as their container ends, take it with them; its log copied out; and, once it ends, idle
460
+ * where it ended by handing its mount over, as it said, to the mount of the pod that replaces it,
461
+ * until its own pod is deleted, or, where it ended any other way, a failure, so kubelet starts it
462
+ * again and it mounts anew. Asked to end while it mounts, it has its mount end too.
463
+ *
464
+ * Its juicefs is JuiceFS's static release, of alasio's image, which runs as it is in the node's
465
+ * namespace: copied to the node, under its digest, never over the one a mount runs. A mount left
466
+ * at `root` whose juicefs is gone, as a pod killed with no successor leaves one, is unmounted
467
+ * first, lazily, so the next mounts afresh.
468
+ *
469
+ * In the node's mount namespace its juicefs reads the node's resolv.conf, which resolves none of
470
+ * the cluster's names, its network the pod's still: so the metadata engine's and the bucket's,
471
+ * where they are a Service's, are resolved here, by the pod's DNS, and it is given their
472
+ * addresses. A Service's address lasts as long as it does: `up` rolls the mount where one is made
473
+ * anew, or the bucket is another (MOUNT_SERVICES_ANNOTATION), and the pod that replaces it reaches
474
+ * the new address.
475
+ *
476
+ * Its juicefs, run in the node's mount namespace, is still this pod's process, of its cgroup: the
477
+ * pod's teardown kills it. So a pod hands its mount over before it is torn down, never after: the
478
+ * pod that replaces it starts beside it (maxSurge), takes its FUSE session over, and only then is
479
+ * it deleted, idle, its juicefs ended already. `paths` are MOUNT_PATHS but for tests.
480
+ */
481
+ export function workspaceMountScript(root, cacheSizeMiB, paths = MOUNT_PATHS) {
482
+ const { run, bin, state, cache, juicefs, mountinfo, inNode, resolve } = paths;
483
+ return [
484
+ // `url`, its host a Service's name given as its address, by the pod's DNS.
485
+ "resolved() {",
486
+ ' scheme=${1%%://*}; rest=${1#*://}; auth=""',
487
+ ' case $rest in *@*) auth="${rest%@*}@"; rest=${rest##*@};; esac',
488
+ ' host=${rest%%[:/]*}; tail=${rest#"$host"}; address=$host',
489
+ " case $host in *.svc|*.svc.cluster.local)",
490
+ ` address=$(${resolve} "$host" | awk '{ print $1; exit }')`,
491
+ ' [ -n "$address" ] || { echo "$host resolves to no address" >&2; return 1; };;',
492
+ " esac",
493
+ ` printf '%s://%s%s%s\n' "$scheme" "$auth" "$address" "$tail"`,
494
+ "}",
495
+ // The metadata engine's password given in META_PASSWORD, which JuiceFS reads, not in its
496
+ // address on the command line, which every process of the node's PIDs could read.
497
+ `META_PASSWORD=$(node -e 'process.stdout.write(decodeURIComponent(new URL(process.env.META_URL).password))') || exit 1`,
498
+ "export META_PASSWORD",
499
+ `meta=$(resolved "$(node -e 'const url = new URL(process.env.META_URL); url.password = ""; process.stdout.write(url.href)')") || exit 1`,
500
+ 'bucket=$(resolved "$BUCKET") || exit 1',
501
+ `run=${run}`,
502
+ `mkdir -p "$run" ${bin} ${state} ${cache}`,
503
+ `rm -f "$run/handed-over"`,
504
+ // Made, empty, before the mount starts or the log is followed, which would not find it yet.
505
+ `: > "$run/juicefs.log"`,
506
+ `node_juicefs="${bin}/juicefs-$(sha256sum ${juicefs} | cut -c1-16)"`,
507
+ `if [ ! -x "$node_juicefs" ]; then cp ${juicefs} "$node_juicefs.$$" && chmod 755 "$node_juicefs.$$" && mv "$node_juicefs.$$" "$node_juicefs"; fi`,
508
+ // A mount of a juicefs gone, which reads as nothing: unmounted, so this one mounts afresh.
509
+ `if awk -v root=${root} '$5 == root { found = 1 } END { exit !found }' ${mountinfo} && ! ${inNode} ls ${root} >/dev/null 2>&1; then`,
510
+ ` ${inNode} umount -l ${root} && echo "unmounted the node's mount its juicefs left, which served nothing"`,
511
+ "fi",
512
+ `${inNode} mkdir -p ${root}`,
513
+ `TMPDIR=${state} ${inNode} "$node_juicefs" mount --no-bgjob --metrics 0.0.0.0:${JUICEFS_METRICS_PORT} --bucket "$bucket" --cache-dir ${cache} --cache-size ${cacheSizeMiB} "$meta" ${root} >> "$run/juicefs.log" 2>&1 &`,
514
+ "pid=$!",
515
+ `echo "$pid" > "$run/juicefs.pid"`,
516
+ `tail -n +1 -f "$run/juicefs.log" &`,
517
+ "tailer=$!",
518
+ `trap 'kill -TERM "$pid"' TERM INT`,
519
+ `wait "$pid"; code=$?`,
520
+ // A TERM's wait ends as the trap runs; the mount's own end is waited for.
521
+ `while kill -0 "$pid" 2>/dev/null; do wait "$pid"; code=$?; done`,
522
+ "sleep 1",
523
+ `kill "$tailer" 2>/dev/null`,
524
+ `if grep -qF '${HANDED_OVER}' "$run/juicefs.log"; then`,
525
+ ` touch "$run/handed-over"`,
526
+ " echo 'the mount is handed over to the pod that replaces this one, which is idle until it is deleted'",
527
+ " trap 'exit 0' TERM INT",
528
+ " while :; do sleep 3600 & wait $!; done",
529
+ "fi",
530
+ `echo "juicefs mount ended ($code) without handing its mount over" >&2`,
531
+ `[ "$code" -ne 0 ] && exit "$code"`,
532
+ "exit 1",
533
+ ].join("\n");
534
+ }
535
+ /**
536
+ * The readiness of a workspace mount's pod: its own juicefs runs, has said it serves the mount
537
+ * point (SERVING), and has not handed its mount over, and the node's mount table holds JuiceFS's
538
+ * mount at `root`: this pod's juicefs serves the node's mount point. The table alone holds the
539
+ * mount of the pod it replaces as its juicefs starts, before it takes that mount over, which the
540
+ * pod it replaces would be deleted for, its mount with it.
541
+ */
542
+ export function workspaceMountReady(root, paths = MOUNT_PATHS) {
543
+ const { run, mountinfo } = paths;
544
+ return [
545
+ `[ ! -e ${run}/handed-over ]`,
546
+ `kill -0 "$(cat ${run}/juicefs.pid)" 2>/dev/null`,
547
+ `grep -qF '${SERVING}' ${run}/juicefs.log`,
548
+ `awk -v root=${root} '$5 == root { for (i = 6; i < NF; i += 1) if ($i == "-") { if ($(i + 1) == "fuse.juicefs") found = 1; break } } END { exit !found }' ${mountinfo}`,
549
+ ].join(" && ");
550
+ }
551
+ /**
552
+ * The node's mount of workspace storage, at WORKSPACES_ROOT, the node's own (workspaceMountScript),
553
+ * which the node agent attaches workspaces from. A mount held by a pod that goes takes every
554
+ * workspace attached from it with it, so a new pod of it starts beside the old (maxSurge), and
555
+ * its mount of the same mount point takes the old's FUSE session over, which needs them to share
556
+ * the node's PIDs and mount namespace; the old pod idles once it has handed its mount over, not
557
+ * ready, and is deleted once the new is ready. It runs alasio's image, for JuiceFS's static
558
+ * release, of the version the mount image is (../../../Dockerfile), privileged, as it enters
559
+ * the node's mount namespace.
560
+ */
561
+ function workspaceMount(config, mountServices) {
562
+ const credential = (name, key) => ({ name, valueFrom: { secretKeyRef: { name: WORKSPACES_CREDENTIALS, key } } });
563
+ const nodePaths = [["bin", JUICEFS_BIN], ["state", JUICEFS_STATE], ["cache", JUICEFS_CACHE]];
564
+ return {
565
+ apiVersion: "apps/v1",
566
+ kind: "DaemonSet",
567
+ metadata: { name: WORKSPACE_MOUNT, namespace: NAMESPACE, labels: labels(WORKSPACE_MOUNT_COMPONENT) },
568
+ spec: {
569
+ selector: { matchLabels: selectorLabels(WORKSPACE_MOUNT_COMPONENT) },
570
+ updateStrategy: { type: "RollingUpdate", rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } },
571
+ template: {
572
+ metadata: {
573
+ labels: { ...labels(WORKSPACE_MOUNT_COMPONENT), ...JUICEFS_ADMIN },
574
+ annotations: { [MOUNT_SERVICES_ANNOTATION]: mountServicesChecksum(config, mountServices) },
575
+ },
576
+ spec: {
577
+ ...priority("floor"),
578
+ ...imagePullSecrets(config),
579
+ ...given("nodeSelector", { ...config.sessions.nodeSelector }),
580
+ ...given("tolerations", [...config.sessions.tolerations]),
581
+ automountServiceAccountToken: false,
582
+ // The mount that takes another's over signals it, by its PID on the node; and it enters the node's mount namespace by PID 1's.
583
+ hostPID: true,
584
+ containers: [{
585
+ name: WORKSPACE_MOUNT_COMPONENT,
586
+ image: imageReference(config.images.alasio),
587
+ imagePullPolicy: config.images.pullPolicy,
588
+ command: ["sh", "-c", workspaceMountScript(WORKSPACES_ROOT, config.workspaceStorage.cacheSizeMiB)],
589
+ env: [credential("META_URL", "metaurl"), credential("BUCKET", "bucket"), credential("ACCESS_KEY", "access-key"), credential("SECRET_KEY", "secret-key")],
590
+ ports: [{ name: "metrics", containerPort: JUICEFS_METRICS_PORT }],
591
+ readinessProbe: { exec: { command: ["sh", "-c", workspaceMountReady(WORKSPACES_ROOT)] }, periodSeconds: 2 },
592
+ securityContext: { privileged: true },
593
+ resources: config.workspaceStorage.mount.resources,
594
+ // The node's directories, where the pod reaches them as the node does.
595
+ volumeMounts: [...nodePaths.map(([name, path]) => ({ name, mountPath: path })), { name: "run", mountPath: MOUNT_RUN }],
596
+ }],
597
+ volumes: [...nodePaths.map(([name, path]) => ({ name, hostPath: { path, type: "DirectoryOrCreate" } })), { name: "run", emptyDir: {} }],
598
+ },
599
+ },
600
+ },
601
+ };
602
+ }
603
+ /**
604
+ * What the sandbox service has in `environment`'s sessions' namespace, main's or a branch
605
+ * environment's (./branch.ts): its pools, the budget of their bound pods, and what the gateway
606
+ * may do there, binding in its pools, and the receiver, watching their Leases.
607
+ */
608
+ export function sandboxPoolObjects(config, environment, cpuBaseline) {
609
+ if (!sandboxServiceRuns(config))
610
+ return [];
611
+ const pools = poolsOf(config, environment);
612
+ const namespace = pools.none.namespace;
613
+ return [
614
+ ...accountRole(GATEWAY, GATEWAY_COMPONENT, namespace, [
615
+ // Its pools' pods, claimed by an update of the version read, and deleted as they are released; and a restore's, made, which the sandbox pods' policy holds to its pool's template.
616
+ { apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "watch", "create", "update", "delete"] },
617
+ // A restore's pod made ready once its sandbox is resumed, by its readiness gate's condition.
618
+ { apiGroups: [""], resources: ["pods/status"], verbs: ["patch"] },
619
+ // The network policies that select a restore's pod, which its node's agent waits for in force before it resumes it.
620
+ { apiGroups: ["networking.k8s.io"], resources: ["networkpolicies"], verbs: ["list"] },
621
+ { apiGroups: ["coordination.k8s.io"], resources: ["leases"], verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] },
622
+ // Its pools' templates, which a release's snapshot records.
623
+ { apiGroups: ["apps"], resources: ["deployments"], verbs: ["get"], resourceNames: [POOLS.none, POOLS.full] },
624
+ // What the API server tells of the pods it refused to make, as the sandbox pods' policy refused them (src/gateway/refusals.ts).
625
+ { apiGroups: [""], resources: ["events"], verbs: ["list"] },
626
+ ]),
627
+ ...accountRole(RECEIVER, RECEIVER_COMPONENT, namespace, [{ apiGroups: ["coordination.k8s.io"], resources: ["leases"], verbs: ["list", "watch"] }]),
628
+ ...poolObjects(config, pools.none, "none", cpuBaseline),
629
+ ...poolObjects(config, pools.full, "full", cpuBaseline),
630
+ boundBudget(namespace),
631
+ ];
632
+ }
633
+ /** The sandbox service's objects: its gateway, its receiver, and the nodes' mount, main's alone, which serve every environment, and main's pools. */
634
+ export function sandboxServiceObjects(config, carried) {
635
+ if (!sandboxServiceRuns(config))
636
+ return [];
637
+ const mount = workspaceMount(config, carried.mountServices);
638
+ const pools = poolsOf(config, MAIN_ENVIRONMENT);
639
+ return [
640
+ ...gatewayIdentity(),
641
+ ...gatewayObjects(config, carried),
642
+ // Every pod of every environment's sessions held to the pools' templates, as Pod Security cannot.
643
+ ...sandboxPodPolicy([poolTemplate(config, pools.none, "none", carried.cpuBaseline), poolTemplate(config, pools.full, "full", carried.cpuBaseline)], {
644
+ sessionsNamespace: sessionsNamespace(config, MAIN_ENVIRONMENT),
645
+ // Main's restores, by the gateway alone, onto each pool's template and the one before it, under the releases kept.
646
+ restores: poolsSnapshotted(config, carried.cpuBaseline)
647
+ ? {
648
+ restorer: GATEWAY_USER,
649
+ templates: ["none", "full"].flatMap((mode) => [poolTemplate(config, pools[mode], mode, carried.cpuBaseline), ...(carried.previousPools[mode] ? [carried.previousPools[mode]] : [])]),
650
+ runtimeClasses: carried.gvisorReleases.map(gvisorRuntimeClass),
651
+ }
652
+ : null,
653
+ }),
654
+ // Where main's sessions are snapshotted, what the gateway may set of a restore's pod's status.
655
+ ...(poolsSnapshotted(config, carried.cpuBaseline) ? resumedConditionPolicy(componentName("resumed-condition"), GATEWAY_USER, pools.none.namespace) : []),
656
+ ...receiverObjects(config),
657
+ ...sandboxPoolObjects(config, MAIN_ENVIRONMENT, carried.cpuBaseline),
658
+ mount,
659
+ // Sized as recommended alone, as a pod of it is only replaced, by one that takes its mount over.
660
+ verticalPodAutoscaler(mount, "Recommended"),
661
+ ];
662
+ }
663
+ //# sourceMappingURL=sandbox-gateway.js.map