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
@@ -1,18 +1,13 @@
1
1
  /**
2
- * What alasio may do beyond what RBAC can say: ValidatingAdmissionPolicies of the
3
- * cluster's, each matching what alasio's ServiceAccount asks (main's, or a branch
4
- * environment's), and holding it to what alasio up renders:
2
+ * What alasio's components may do beyond what RBAC can say: ValidatingAdmissionPolicies of
3
+ * the cluster's, each matching what one of their ServiceAccounts asks, and holding it to
4
+ * what alasio up renders:
5
5
  *
6
- * - a Job alasio makes is the clone of a session's volume, as rendered, but for the
7
- * claims it is told: RBAC lets it make any Job in its namespace, as another
8
- * ServiceAccount there, a controller's whose rights reach the cluster, or of another
9
- * image or command, privileged as the clone is;
10
- * - a Sandbox alasio makes, or moves onto another pod template, is made from the template
11
- * of its namespace's workspaces, but for what alasio adds to each (src/kube/
12
- * sandbox-shape.ts): RBAC lets it make Sandboxes of any pod, as any ServiceAccount, under
13
- * any runtime;
14
- * - a Secret alasio makes is a Sandbox's token: RBAC lets it make Secrets of any type, a
15
- * ServiceAccount's token among them;
6
+ * - a pod made in the folder namespace is a folder instance's, which the sandbox gateway
7
+ * alone makes, of the folder template, but for what the gateway adds to each
8
+ * (src/gateway/names.ts folderPodSpec), and a Secret the gateway makes there is an
9
+ * instance's token: RBAC lets it make pods of any spec, as any ServiceAccount, under any
10
+ * runtime, and Secrets of any type, a ServiceAccount's token among them;
16
11
  * - a pod alasio patches changes its busy label and annotations alone (src/kube/busy.ts),
17
12
  * and in alasio's own namespace is alasio's own: RBAC lets it patch its namespaces'
18
13
  * pods whole, their images among it;
@@ -25,10 +20,12 @@
25
20
  * a Check allows, and what the API server defaults where the rendered one has nothing.
26
21
  */
27
22
  import { posix } from "node:path";
28
- import { branchNamespace } from "../../../src/branch/names.js";
23
+ import { BINDING_LABEL, FREE_LABEL, KEY_ANNOTATION, POOL_LABEL } from "../../../src/binder/names.js";
24
+ import { branchNamespace, branchSessionsNamespace } from "../../../src/branch/names.js";
25
+ import { ATTACH_VOLUME, FOLDER_KEY_PREFIX, FOLDER_POOL, FOLDER_POD, folderPodSpec, folderTokenSecret, ownDirectory, SANDBOX_POD_POLICY } from "../../../src/gateway/names.js";
29
26
  import { BUSY_ANNOTATIONS, BUSY_LABEL } from "../../../src/kube/busy-label.js";
30
- import { CLONE_VARIABLES } from "../../../src/kube/config.js";
31
- import { BAYMA_CONTAINER, DEFAULT_FULL_MODE_NAMESERVERS, EGRESS_GATE, EGRESS_GATE_SCRIPT, HARNESS_LABEL, MANAGED_BY, NET_MODE_LABEL, RESTORE_ANNOTATION, RESTORE_PAUSED_ANNOTATION, RESUMED_CONDITION, SANDBOX_LABEL, SESSION_TEMPLATE_ANNOTATION, SNAPSHOT_CACHE_DIR, SNAPSHOT_ID, STATE_DIR_ARG, TELEMETRY_TOKEN_FILE_ARGS, TOKEN_DIR, TOKEN_FILE_ARGS, TOKEN_KEY, TOKEN_MODE, TOKEN_VOLUME, tokenSecretName, WORKLOAD_LABEL, } from "../../../src/kube/sandbox-shape.js";
27
+ import { RESTORE_ANNOTATION, RESTORE_PAUSED_ANNOTATION, RESUMED_CONDITION, SNAPSHOT_CACHE_DIR, SNAPSHOT_ID } from "../../../src/kube/sandbox-shape.js";
28
+ import { ATTACH_ROOT, RESTORE_POD } from "../../../src/node-agent/names.js";
32
29
  import { componentName, labels, NAMESPACE, neonName, RELEASE } from "./common.js";
33
30
  /** A field's test, beyond its being a rendered value: given the field's CEL expression and how deep it is, the CEL that holds where it passes. */
34
31
  class Check {
@@ -41,23 +38,12 @@ class Check {
41
38
  const ANY = new Check(() => "true");
42
39
  /** Any string. */
43
40
  const ANY_STRING = new Check((expression) => `type(${expression}) == string`);
44
- /** A map with nothing in it, as the API server defaults one. */
45
- const EMPTY = new Check((expression) => `size(${expression}) == 0`);
46
41
  /** A string all of which `pattern` matches. */
47
42
  const matching = (pattern) => new Check((expression) => `type(${expression}) == string && ${expression}.matches(${JSON.stringify(`^(${pattern})$`)})`);
48
43
  /** What the CEL `value` is, of the object asked about. */
49
44
  const sameAs = (value) => new Check((expression) => `${expression} == ${value}`);
50
45
  /** Any of `alternatives`. */
51
46
  const oneOf = (...alternatives) => new Check((expression, depth) => `(${alternatives.map((each) => pinned(expression, each, depth)).join(" || ")})`);
52
- /** A list of `first`, then any number of items each `rest`. */
53
- const startingWith = (first, rest) => new Check((expression, depth) => {
54
- const item = `item${depth}`;
55
- return [
56
- `size(${expression}) >= ${first.length}`,
57
- ...first.map((each, index) => pinned(`${expression}[${index}]`, each, depth + 1)),
58
- `${expression}.all(${item}, ${[...first, rest].map((each) => pinned(item, each, depth + 1)).join(" || ")})`,
59
- ].join(" && ");
60
- });
61
47
  /** Of a map's key: that it may be absent; where present, `value`. */
62
48
  class Optional extends Check {
63
49
  value;
@@ -193,208 +179,349 @@ export function computeRestartPolicy() {
193
179
  { name: "newAnnotations", expression: "object.spec.template.metadata.?annotations.orValue({})" },
194
180
  ]);
195
181
  }
196
- /**
197
- * What the API server gives a Job, its template, and the template's pod and containers,
198
- * when its maker does not: a pinned Job may have them so. Its selector and its template's
199
- * labels of its own are the Job's; a pod's scheduling and termination are no matter of
200
- * its rights; and a pod's security context, defaulted, is empty.
201
- */
202
- const JOB_DEFAULTS = {
203
- spec: { completions: 1, parallelism: 1, completionMode: "NonIndexed", suspend: false, manualSelector: false, podReplacementPolicy: ANY_STRING, selector: ANY },
204
- metadata: { creationTimestamp: ANY },
205
- labels: {
206
- "batch.kubernetes.io/controller-uid": ANY_STRING,
207
- "controller-uid": ANY_STRING,
208
- "batch.kubernetes.io/job-name": sameAs("object.metadata.name"),
209
- "job-name": sameAs("object.metadata.name"),
210
- },
211
- pod: { dnsPolicy: ANY, schedulerName: ANY, terminationGracePeriodSeconds: ANY, securityContext: EMPTY },
212
- container: { terminationMessagePath: ANY, terminationMessagePolicy: ANY },
213
- };
214
- /**
215
- * What alasio may run as a Job: the clone of a session's volume (src/sandbox/index.ts),
216
- * `job`, its spec and its pod's as rendered, but for the claims each of its containers is
217
- * told, as alasio adds them (cloneJobManifest), each a DNS label (DNS_LABEL). Main's
218
- * alasio's, the one that clones.
219
- */
220
- export function cloneJobPolicy(job) {
221
- const spec = job.spec ?? {};
222
- const pod = spec.template?.spec;
223
- const claims = CLONE_VARIABLES.map((variable) => ({ name: variable, value: matching(DNS_LABEL) }));
224
- const template = spec.template;
225
- const containers = (pod?.containers ?? []).map((container) => defaulted({ ...container, env: [...(container.env ?? []), ...claims] }, JOB_DEFAULTS.container));
226
- const expected = defaulted({
227
- ...spec,
228
- template: {
229
- metadata: defaulted({ ...template?.metadata, labels: defaulted(template?.metadata?.labels ?? {}, JOB_DEFAULTS.labels) }, JOB_DEFAULTS.metadata),
230
- spec: defaulted({ ...pod, containers }, JOB_DEFAULTS.pod),
231
- },
232
- }, JOB_DEFAULTS.spec);
233
- return admissionPolicy(componentName("clone-job"), [{ apiGroups: ["batch"], resources: ["jobs"], operations: ["CREATE", "UPDATE"] }], `request.userInfo.username == "${alasioAccount(NAMESPACE)}"`, [{
234
- expression: pinned("object.spec", expected),
235
- message: "alasio runs no Job but its clone of a session's volume, as alasio up renders it",
236
- }]);
237
- }
238
- /** A container of a Sandbox's pod template as its CRD stores it: its ports' protocol defaulted. */
239
- const stored = (container) => {
240
- const ports = container["ports"];
241
- return Array.isArray(ports) ? { ...container, ports: ports.map((port) => defaulted(port, { protocol: "TCP" })) } : container;
242
- };
243
- /** The token Secret of the Sandbox asked about, as CEL. */
244
- const OWN_TOKEN = sameAs(`object.metadata.name + ${JSON.stringify(tokenSecretName(""))}`);
245
182
  /** A path segment as sanitizePathToken makes one. */
246
183
  const PATH_TOKEN = "[A-Za-z0-9_][A-Za-z0-9_.-]*";
247
184
  /** `text`, as a pattern that matches it alone. */
248
185
  const literally = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
249
- /** A variable of the environment, set to a string. */
250
- const SET = { name: ANY_STRING, value: ANY_STRING };
251
186
  /** A node's name: a DNS subdomain. */
252
187
  const NODE_NAME = "[a-z0-9]([-a-z0-9.]{0,251}[a-z0-9])?";
253
- /** A pod template's hash, as podTemplateHash makes one (src/kube/sandboxes.ts). */
254
- const TEMPLATE_HASH = "[0-9a-f]{64}";
188
+ /** Any number. */
189
+ const ANY_NUMBER = new Check((expression) => `type(${expression}) == int || type(${expression}) == double`);
190
+ /** The tolerations the API server gives a pod of a node not ready or unreachable, for five minutes, where it has none of its own of them (DefaultTolerationSeconds). */
191
+ const DEFAULT_TOLERATIONS = ["node.kubernetes.io/not-ready", "node.kubernetes.io/unreachable"].map((key) => ({ key, operator: "Exists", effect: "NoExecute", tolerationSeconds: 300 }));
192
+ /**
193
+ * What the API server gives a sandbox pool's pod, of its template, where the template has
194
+ * not: its own defaults, its priority from its class, its ServiceAccount the namespace's
195
+ * default, which it mounts no token of; of each container, where its termination message
196
+ * goes, and its resources as VPA sizes them; of a probe, its timeout and thresholds; of a
197
+ * port, its protocol; of a field reference, its API version.
198
+ */
199
+ const POOL_POD_DEFAULTS = {
200
+ pod: {
201
+ restartPolicy: "Always",
202
+ terminationGracePeriodSeconds: 30,
203
+ schedulerName: "default-scheduler",
204
+ serviceAccountName: "default",
205
+ serviceAccount: "default",
206
+ priority: ANY_NUMBER,
207
+ preemptionPolicy: oneOf("PreemptLowerPriority", "Never"),
208
+ },
209
+ container: { terminationMessagePath: "/dev/termination-log", terminationMessagePolicy: "File", resources: ANY },
210
+ probe: { timeoutSeconds: 1, successThreshold: 1, failureThreshold: 3 },
211
+ };
212
+ /** `container` of a pool's template, as the API server stores it in a pod (POOL_POD_DEFAULTS). */
213
+ function poolContainer(container) {
214
+ const each = container;
215
+ const probe = each["readinessProbe"];
216
+ return defaulted({
217
+ ...each,
218
+ // Its resources are VPA's, as it sizes the pod as it is made, and resizes it after (pods/resize).
219
+ resources: ANY,
220
+ ...(container.ports ? { ports: container.ports.map((port) => defaulted(port, { protocol: "TCP" })) } : {}),
221
+ ...(container.env
222
+ ? {
223
+ env: container.env.map((variable) => variable.valueFrom?.fieldRef
224
+ ? { ...variable, valueFrom: { fieldRef: defaulted(variable.valueFrom.fieldRef, { apiVersion: "v1" }) } }
225
+ : variable),
226
+ }
227
+ : {}),
228
+ ...(probe ? { readinessProbe: defaulted(probe, POOL_POD_DEFAULTS.probe) } : {}),
229
+ }, POOL_POD_DEFAULTS.container);
230
+ }
231
+ /** The spec of a pod of the pool template `template`, as the API server stores it: what POOL_POD_DEFAULTS adds, and its tolerations, its template's then the API server's own. */
232
+ function poolPodSpec(template) {
233
+ const spec = template.spec ?? { containers: [] };
234
+ const tolerations = (spec.tolerations ?? []);
235
+ return defaulted({
236
+ ...spec,
237
+ containers: spec.containers.map(poolContainer),
238
+ ...(spec.initContainers ? { initContainers: spec.initContainers.map(poolContainer) } : {}),
239
+ // The API server's of a node not ready and unreachable, in either order.
240
+ tolerations: oneOf([...tolerations, ...DEFAULT_TOLERATIONS], [...tolerations, ...[...DEFAULT_TOLERATIONS].reverse()]),
241
+ }, POOL_POD_DEFAULTS.pod);
242
+ }
255
243
  /**
256
- * Where a node agent prepares a snapshot of the Sandbox asked about, of the sessions of
257
- * `namespace`, for its pod to be restored from: under the snapshot cache, in its
258
- * environment's directory, in its workspace's, the Sandbox's own, a snapshot's.
244
+ * The image a restore's pod of main's sessions, `namespace`'s, is restored from, as the
245
+ * gateway's node agent prepared it: of the workspace its binding's key names, in the node's
246
+ * snapshot cache, under a snapshot's id.
259
247
  */
260
- const restoredFrom = (namespace) => new Check((expression) => {
261
- const own = `${JSON.stringify(`${SNAPSHOT_CACHE_DIR}/${namespace}/`)} + object.metadata.name + "/"`;
248
+ const restoredFromKey = (namespace) => new Check((expression) => {
249
+ const own = `${JSON.stringify(`${SNAPSHOT_CACHE_DIR}/${namespace}/`)} + variables.annotations[${JSON.stringify(KEY_ANNOTATION)}].substring(${"sessionfs:".length}) + "/"`;
262
250
  return `type(${expression}) == string && ${expression}.startsWith(${own}) && ${expression}.substring(size(${own})).matches(${JSON.stringify(`^${SNAPSHOT_ID}$`)})`;
263
251
  });
252
+ /** A restore's pod's own directory on its node: its namespace's and its own name's, under every pod's, its name a restore's pod's, as its node's agent makes a directory for no other. */
253
+ const OWN_DIRECTORY = new Check((expression) => `${expression} == ${JSON.stringify(`${ATTACH_ROOT}/`)} + object.metadata.namespace + "/" + object.metadata.name && object.metadata.name.matches(${JSON.stringify(RESTORE_POD.source)})`);
264
254
  /**
265
- * The Sandbox of a workspace alasio makes from `template`: a session's (`session`, with
266
- * the session's profile, and `current`, the profile its snapshots are restored by) or a
267
- * folder's bayma's (`folder`, with its state root), as sandboxManifest and
268
- * sessionSandboxManifest or hostBaymaManifest make it.
269
- *
270
- * Of a session's, what its snapshots add (src/sandbox/snapshots.ts): the hash of the
271
- * template its pod is made from, which they record; and, restored from one, the image of the Sandbox's own that its
272
- * node prepared, left paused until it is resumed, under a release `current` keeps, on
273
- * that node, and ready once resumed.
255
+ * A restore's pod of the pool template `template`, as the gateway makes it (src/gateway/snapshots.ts)
256
+ * and the API server stores it: its template's, bound from the first, never free, its sandbox
257
+ * restored paused from the image its node's agent prepared, under one of `runtimeClasses`, on that
258
+ * node, its own directory there its volume's, ready once resumed, and never started again.
274
259
  */
275
- function sandboxOf(template, workspace) {
276
- const podTemplate = template.podTemplate;
277
- const spec = podTemplate.spec ?? { containers: [] };
278
- const session = "session" in workspace ? workspace.session : null;
279
- const snapshots = "current" in workspace ? (workspace.current.snapshots ?? null) : null;
280
- const bayma = spec.containers.find(({ name }) => name === BAYMA_CONTAINER);
281
- const tokenMount = { name: TOKEN_VOLUME, mountPath: TOKEN_DIR, readOnly: true };
282
- // Under the state root as hostBaymaStateDir joins it: normalized, its one slash after it.
283
- const stateDir = "folder" in workspace ? [STATE_DIR_ARG, matching(`${literally(posix.join(workspace.folder.stateRoot, "/"))}${PATH_TOKEN}/${PATH_TOKEN}`)] : [];
284
- // A session's bayma that exports telemetry reads the token it exports with from its token's file too.
285
- const args = (each) => {
286
- const own = [...(each.args ?? []), ...TOKEN_FILE_ARGS, ...stateDir];
287
- return session ? oneOf(own, [...own, ...TELEMETRY_TOKEN_FILE_ARGS]) : own;
260
+ function restorePod(template, namespace, runtimeClasses) {
261
+ // Its own directory named by its volume's path, its own name's (ownDirectory).
262
+ const owned = ownDirectory(template.spec ?? { containers: [] }, "", "");
263
+ const volumes = (owned.volumes ?? []).map((volume) => (volume.name === ATTACH_VOLUME ? { name: ATTACH_VOLUME, hostPath: { path: OWN_DIRECTORY, type: "Directory" } } : volume));
264
+ const restoredOn = {
265
+ nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchFields: [{ key: "metadata.name", operator: "In", values: [matching(NODE_NAME)] }] }] } },
288
266
  };
289
- const container = (each) => stored((each.name !== BAYMA_CONTAINER ? each : {
290
- ...each,
291
- args: args(each),
292
- volumeMounts: [...(each.volumeMounts ?? []), tokenMount],
293
- env: optional(startingWith((each.env ?? []), SET)),
294
- }));
295
- const gate = session && session.egressGate !== false
296
- ? [{
297
- name: EGRESS_GATE.name,
298
- ...(bayma?.image === undefined ? {} : { image: bayma.image }),
299
- command: ["node", "-e", EGRESS_GATE_SCRIPT],
300
- ...(bayma?.securityContext === undefined ? {} : { securityContext: bayma.securityContext }),
301
- resources: EGRESS_GATE.resources,
302
- }]
267
+ // Its template's spec, as the API server stores a pod of it, but what a restore gives it.
268
+ const restoring = poolPodSpec({
269
+ ...template,
270
+ spec: {
271
+ ...owned,
272
+ volumes,
273
+ restartPolicy: "Never",
274
+ runtimeClassName: oneOf(...runtimeClasses),
275
+ readinessGates: [...(template.spec?.readinessGates ?? []), { conditionType: RESUMED_CONDITION }],
276
+ affinity: restoredOn,
277
+ },
278
+ });
279
+ const { [FREE_LABEL]: _free, ...own } = (template.metadata?.labels ?? {});
280
+ const labels = { ...own, [BINDING_LABEL]: matching("[0-9a-f]{40}") };
281
+ const annotations = {
282
+ ...(template.metadata?.annotations ?? {}),
283
+ [KEY_ANNOTATION]: matching(`sessionfs:${DNS_LABEL}`),
284
+ ...BUSY_ANNOTATIONS,
285
+ [RESTORE_ANNOTATION]: restoredFromKey(namespace),
286
+ [RESTORE_PAUSED_ANNOTATION]: "true",
287
+ vpaObservedContainers: optional(ANY_STRING),
288
+ vpaUpdates: optional(ANY_STRING),
289
+ };
290
+ return `(${pinned("object.spec", restoring)} && ${pinned("variables.labels", labels)} && ${pinned("variables.annotations", annotations)})`;
291
+ }
292
+ /**
293
+ * Every pod of a namespace of sessions, main's or a branch environment's, made or changed by
294
+ * anyone, held to what the pools' templates, `templates`, make, as Pod Security's restricted
295
+ * level, which the namespaces keep warning and auditing of, cannot: a pool's pod mounts its
296
+ * own directory on its node, a hostPath no level but privileged allows, which the namespaces
297
+ * enforce, so this policy is their boundary.
298
+ *
299
+ * - A pod made is a pod of one of `templates`, as the API server stores it (poolPodSpec), its
300
+ * template's labels and annotations, its ReplicaSet's template hash beside them, and what
301
+ * VPA notes; or a restore's of main's sessions, of a template a snapshot restores onto
302
+ * (restorePod), made by the gateway alone; and no other pod is made there.
303
+ * - A pod changed keeps its spec whole (its resources change by pods/resize, which this does
304
+ * not match), and of its labels and annotations changes those a binding has alone (its free
305
+ * label taken off, its binding's put on) and those that keep its node, and of its owners lets
306
+ * go alone.
307
+ * - No pod gets an ephemeral container.
308
+ *
309
+ * It fails closed: where it cannot say, the API server refuses.
310
+ */
311
+ export function sandboxPodPolicy(templates, { sessionsNamespace, restores = null }) {
312
+ const name = SANDBOX_POD_POLICY;
313
+ const metadata = { name, labels: labels("session") };
314
+ // Each template's pin a variable of its own, as CEL holds each expression to 100,000 code points, which all of them together pass.
315
+ const made = templates.map((template, index) => {
316
+ const labels = { ...(template.metadata?.labels ?? {}), "pod-template-hash": ANY_STRING };
317
+ const annotations = { ...(template.metadata?.annotations ?? {}), vpaObservedContainers: optional(ANY_STRING), vpaUpdates: optional(ANY_STRING) };
318
+ return { name: `made${index}`, expression: `${pinned("object.spec", poolPodSpec(template))} && ${pinned("variables.labels", labels)} && ${pinned("variables.annotations", annotations)}` };
319
+ });
320
+ const restoring = (restores?.templates ?? []).map((template, index) => ({ name: `restored${index}`, expression: restorePod(template, sessionsNamespace, restores?.runtimeClasses ?? []) }));
321
+ const restored = restores
322
+ ? [`(request.namespace == ${JSON.stringify(sessionsNamespace)} && request.userInfo.username == ${JSON.stringify(restores.restorer)} && (${restoring.map(({ name }) => `variables.${name}`).join(" || ")}))`]
303
323
  : [];
304
- const initContainers = [...gate, ...(spec.initContainers ?? [])].map(stored);
305
- const volumes = [...(spec.volumes ?? []), { name: TOKEN_VOLUME, secret: { secretName: OWN_TOKEN, defaultMode: TOKEN_MODE } }];
306
- const own = {
307
- ...MANAGED_BY,
308
- [SANDBOX_LABEL]: sameAs("object.metadata.name"),
309
- ...(session ? { [WORKLOAD_LABEL]: "session", [NET_MODE_LABEL]: oneOf("none", "full") } : { [WORKLOAD_LABEL]: "folder", [HARNESS_LABEL]: matching(PATH_TOKEN) }),
324
+ const bound = [BINDING_LABEL, FREE_LABEL];
325
+ const policy = {
326
+ apiVersion: "admissionregistration.k8s.io/v1",
327
+ kind: "ValidatingAdmissionPolicy",
328
+ metadata,
329
+ spec: {
330
+ failurePolicy: "Fail",
331
+ matchConstraints: {
332
+ resourceRules: [
333
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods"], operations: ["CREATE", "UPDATE"] },
334
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods/ephemeralcontainers"], operations: ["CREATE", "UPDATE"] },
335
+ ],
336
+ },
337
+ matchConditions: [{
338
+ name: "of-sessions",
339
+ expression: `request.namespace == ${JSON.stringify(sessionsNamespace)} || request.namespace.matches(${JSON.stringify(`^${literally(branchSessionsNamespace("\u0000")).replace("\u0000", DNS_LABEL)}$`)})`,
340
+ }],
341
+ variables: [
342
+ { name: "labels", expression: "object.metadata.?labels.orValue({})" },
343
+ { name: "annotations", expression: "object.metadata.?annotations.orValue({})" },
344
+ { name: "oldLabels", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?labels.orValue({}) : {}" },
345
+ { name: "newLabels", expression: "object.metadata.?labels.orValue({})" },
346
+ { name: "oldAnnotations", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?annotations.orValue({}) : {}" },
347
+ { name: "newAnnotations", expression: "object.metadata.?annotations.orValue({})" },
348
+ { name: "pooled", expression: `${JSON.stringify(POOL_LABEL)} in variables.labels || ${JSON.stringify(POOL_LABEL)} in variables.oldLabels` },
349
+ ...made,
350
+ ...restoring,
351
+ ],
352
+ validations: [
353
+ { expression: "request.?subResource.orValue('') != 'ephemeralcontainers'", message: "no pod of a namespace of sessions gets an ephemeral container" },
354
+ {
355
+ expression: `request.operation != 'CREATE' || !variables.pooled || ${[...made.map(({ name }) => `variables.${name}`), ...restored].join(" || ")}`,
356
+ message: "a pod of a sandbox pool is its pool's template, as alasio up renders it, or a restore's of one, as the sandbox gateway makes it, and nothing else",
357
+ },
358
+ { expression: "request.operation != 'CREATE' || variables.pooled", message: "no pod is made in a namespace of sessions but a sandbox pool's" },
359
+ { expression: "request.operation != 'UPDATE' || object.spec == oldObject.spec", message: "no pod's spec changes in a namespace of sessions" },
360
+ {
361
+ expression: `request.operation != 'UPDATE' || !variables.pooled || (${unchanged("Labels", bound)})`,
362
+ message: `no label of a sandbox pool's pod changes but ${bound.join(" and ")}, as it is bound`,
363
+ },
364
+ {
365
+ expression: `request.operation != 'UPDATE' || !variables.pooled || (${unchanged("Annotations", [KEY_ANNOTATION, ...Object.keys(BUSY_ANNOTATIONS)])})`,
366
+ message: `no annotation of a sandbox pool's pod changes but ${KEY_ANNOTATION} and what keeps its node, as it is bound`,
367
+ },
368
+ {
369
+ expression: "request.operation != 'UPDATE' || object.metadata.?ownerReferences.orValue([]).all(owner, owner in oldObject.metadata.?ownerReferences.orValue([]))",
370
+ message: "a pod of a namespace of sessions gets no owner it had not",
371
+ },
372
+ ],
373
+ },
310
374
  };
311
- const annotations = (podTemplate.metadata?.annotations ?? {});
312
- const added = session
313
- ? {
314
- [SESSION_TEMPLATE_ANNOTATION]: optional(matching(TEMPLATE_HASH)),
315
- ...(snapshots && session
316
- ? {
317
- [RESTORE_ANNOTATION]: optional(restoredFrom(session.namespace)),
318
- [RESTORE_PAUSED_ANNOTATION]: optional("true"),
319
- }
320
- : {}),
321
- }
322
- : {};
323
- // The template's annotations, and those added where they may be; a template of none has a map of them only where some are added.
324
- const metadataAnnotations = Object.keys(added).length === 0 ? {} : { annotations: Object.keys(annotations).length > 0 ? { ...annotations, ...added } : optional({ ...added }) };
325
- const restoredOn = {
326
- ...(spec.affinity ?? {}),
327
- nodeAffinity: {
328
- requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchFields: [{ key: "metadata.name", operator: "In", values: [matching(NODE_NAME)] }] }] },
375
+ const binding = { apiVersion: "admissionregistration.k8s.io/v1", kind: "ValidatingAdmissionPolicyBinding", metadata, spec: { policyName: name, validationActions: ["Deny"] } };
376
+ return [policy, binding];
377
+ }
378
+ /** The folder pods' policy, which holds every pod of the folder namespace to the folder template (folderPodPolicy). */
379
+ export const FOLDER_POD_POLICY = "alasio-folder-pods";
380
+ /** What stands for a folder instance's pod's own name, and its state directory, in its spec as folderPodSpec makes it, which the policy pins by what they must be. */
381
+ const POD_NAME = "\u0000pod";
382
+ const STATE_DIR = "\u0000state-dir";
383
+ /**
384
+ * The spec `spec` of a folder instance's pod, as the API server stores it: as a pool's pod's
385
+ * (poolPodSpec), and what the API server gives a pod that the folder template, unlike the
386
+ * pools', leaves out: its DNS policy, its Service links, and each of the machine's paths'
387
+ * type where its mount names none.
388
+ */
389
+ function folderPodPinned(spec) {
390
+ const { map, defaults } = folderPodParts(spec);
391
+ return defaulted(map, defaults);
392
+ }
393
+ /** folderPodPinned's map and the defaults it is pinned with. */
394
+ export function folderPodParts(spec) {
395
+ const tolerations = (spec.tolerations ?? []);
396
+ const volumes = (spec.volumes ?? []).map((volume) => volume.hostPath && volume.hostPath.type === undefined ? { ...volume, hostPath: defaulted(volume.hostPath, { type: "" }) } : volume);
397
+ return {
398
+ map: {
399
+ ...spec,
400
+ containers: spec.containers.map(poolContainer),
401
+ ...(spec.volumes ? { volumes } : {}),
402
+ tolerations: oneOf([...tolerations, ...DEFAULT_TOLERATIONS], [...tolerations, ...[...DEFAULT_TOLERATIONS].reverse()]),
403
+ },
404
+ defaults: {
405
+ ...POOL_POD_DEFAULTS.pod,
406
+ // The account the template names, as the API server copies it to the field it deprecated.
407
+ ...(spec.serviceAccountName ? { serviceAccountName: spec.serviceAccountName, serviceAccount: spec.serviceAccountName } : {}),
408
+ dnsPolicy: "ClusterFirst",
409
+ enableServiceLinks: true,
329
410
  },
330
411
  };
331
- const readinessGates = (spec.readinessGates ?? []);
332
- const restorable = snapshots
333
- ? {
334
- runtimeClassName: oneOf(...new Set([spec.runtimeClassName ?? "", ...snapshots.releases.map(({ runtimeClassName }) => runtimeClassName)].filter((each) => each !== ""))),
335
- readinessGates: spec.readinessGates ? oneOf(readinessGates, [...readinessGates, { conditionType: RESUMED_CONDITION }]) : optional([{ conditionType: RESUMED_CONDITION }]),
336
- affinity: spec.affinity ? oneOf(spec.affinity, restoredOn) : optional(restoredOn),
337
- }
338
- : {};
339
- const nameservers = session ? [...(session.fullModeNameservers ?? DEFAULT_FULL_MODE_NAMESERVERS)] : [];
340
- const podSpec = {
341
- ...spec,
342
- containers: spec.containers.map(container),
343
- ...(initContainers.length > 0 ? { initContainers } : {}),
344
- volumes,
345
- // A session's pod speaks to the cluster as no one, and resolves names as its internet says.
346
- ...(session
347
- ? { automountServiceAccountToken: false, enableServiceLinks: false, dnsPolicy: "None", dnsConfig: { nameservers: oneOf(nameservers, ["127.0.0.1"]) } }
348
- : {}),
349
- ...restorable,
412
+ }
413
+ /** `value`, each string of it `stands` names the Check of, as that Check. */
414
+ function standingFor(value, stands) {
415
+ if (typeof value === "string")
416
+ return stands[value] ?? value;
417
+ if (value instanceof Check || typeof value !== "object")
418
+ return value;
419
+ if (Array.isArray(value))
420
+ return value.map((item) => standingFor(item, stands));
421
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, item === undefined ? undefined : standingFor(item, stands)]));
422
+ }
423
+ /**
424
+ * Every pod of the folder namespace, `namespace`, made or changed by anyone, held to what the
425
+ * folder template, `template`, makes (src/gateway/folders.ts): a folder instance's pod is the
426
+ * operator's, of the machine's own mounts and the capabilities bayma's process snapshots need,
427
+ * which no Pod Security level but privileged allows, as the namespace enforces, so this policy
428
+ * is its boundary.
429
+ *
430
+ * - A pod made is made by `maker`, the gateway, alone, and is the template's, as the API server
431
+ * stores it (poolPodSpec), with what folderPodSpec adds, alone: its token's Secret, named for
432
+ * it, and its state directory under `stateRoot`, of a harness's and a conversation's names;
433
+ * bound from the first, to a folder instance's key, its node kept; and named as one.
434
+ * - A pod changed keeps its spec, its labels, and its owners, and of its annotations changes
435
+ * those that keep its node alone.
436
+ * - No pod gets an ephemeral container.
437
+ *
438
+ * It fails closed: where it cannot say, the API server refuses.
439
+ */
440
+ export function folderPodPolicy(template, { namespace, maker, stateRoot }) {
441
+ const metadata = { name: FOLDER_POD_POLICY, labels: labels("folder-bayma") };
442
+ // The spec as folderPodSpec makes it, its own name's and state directory's places standing for what they must be, then pinned.
443
+ const spec = folderPodPinned(standingFor(folderPodSpec(template.spec ?? { containers: [] }, POD_NAME, STATE_DIR), {
444
+ [folderTokenSecret(POD_NAME)]: sameAs(`object.metadata.name + ${JSON.stringify(folderTokenSecret(""))}`),
445
+ [STATE_DIR]: matching(`${literally(posix.join(stateRoot, "/"))}${PATH_TOKEN}/${PATH_TOKEN}`),
446
+ }));
447
+ const podLabels = { ...(template.metadata?.labels ?? {}), [POOL_LABEL]: FOLDER_POOL, [BINDING_LABEL]: matching("[0-9a-f]{40}") };
448
+ const podAnnotations = {
449
+ ...(template.metadata?.annotations ?? {}),
450
+ [KEY_ANNOTATION]: matching(`${literally(FOLDER_KEY_PREFIX)}[a-z][a-z0-9-]{0,30}:.{1,512}`),
451
+ ...BUSY_ANNOTATIONS,
452
+ vpaObservedContainers: optional(ANY_STRING),
453
+ vpaUpdates: optional(ANY_STRING),
350
454
  };
351
- // A Sandbox restored onto the template before the current one keeps the claims it was made with, of either.
352
- const claims = [...new Set([session, "current" in workspace ? workspace.current : null].map((each) => JSON.stringify(each?.volumeClaimTemplates ?? [])))]
353
- .map((each) => JSON.parse(each))
354
- .filter((each) => each.length > 0);
355
- // Its CRD defaults what is done with it at its shutdown time, which alasio gives none.
356
- return defaulted({
357
- operatingMode: oneOf("Running", "Suspended"),
358
- service: true,
359
- podTemplate: { ...podTemplate, metadata: { ...podTemplate.metadata, labels: { ...podTemplate.metadata?.labels, ...own }, ...metadataAnnotations }, spec: podSpec },
360
- ...(claims.length > 0 ? { volumeClaimTemplates: claims.length === 1 ? claims[0] : oneOf(...claims) } : {}),
361
- }, { shutdownPolicy: "Retain" });
455
+ const policy = {
456
+ apiVersion: "admissionregistration.k8s.io/v1",
457
+ kind: "ValidatingAdmissionPolicy",
458
+ metadata,
459
+ spec: {
460
+ failurePolicy: "Fail",
461
+ matchConstraints: {
462
+ resourceRules: [
463
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods"], operations: ["CREATE", "UPDATE"] },
464
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods/ephemeralcontainers"], operations: ["CREATE", "UPDATE"] },
465
+ ],
466
+ },
467
+ matchConditions: [{ name: "of-folders", expression: `request.namespace == ${JSON.stringify(namespace)}` }],
468
+ variables: [
469
+ { name: "oldAnnotations", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?annotations.orValue({}) : {}" },
470
+ { name: "newAnnotations", expression: "object.metadata.?annotations.orValue({})" },
471
+ // Each pin a variable of its own, as CEL holds each expression to 100,000 code points.
472
+ { name: "folderSpec", expression: pinned("object.spec", spec) },
473
+ { name: "folderLabels", expression: pinned("object.metadata.?labels.orValue({})", podLabels) },
474
+ { name: "folderAnnotations", expression: pinned("object.metadata.?annotations.orValue({})", podAnnotations) },
475
+ ],
476
+ validations: [
477
+ { expression: "request.?subResource.orValue('') != 'ephemeralcontainers'", message: "no pod of the folder namespace gets an ephemeral container" },
478
+ { expression: `request.operation != 'CREATE' || request.userInfo.username == ${JSON.stringify(maker)}`, message: "no pod is made in the folder namespace but by the sandbox gateway" },
479
+ { expression: `request.operation != 'CREATE' || object.metadata.name.matches(${JSON.stringify(FOLDER_POD.source)})`, message: "a pod made in the folder namespace is named as a folder instance's" },
480
+ { expression: "request.operation != 'CREATE' || variables.folderSpec", message: "a pod made in the folder namespace is a folder instance's, of the folder template, as alasio up renders it" },
481
+ { expression: "request.operation != 'CREATE' || variables.folderLabels", message: "a pod made in the folder namespace is labelled as a folder instance bound" },
482
+ { expression: "request.operation != 'CREATE' || variables.folderAnnotations", message: "a pod made in the folder namespace is annotated with a folder instance's key, its node kept" },
483
+ { expression: "request.operation != 'UPDATE' || object.spec == oldObject.spec", message: "no pod's spec changes in the folder namespace" },
484
+ { expression: "request.operation != 'UPDATE' || object.metadata.?labels.orValue({}) == oldObject.metadata.?labels.orValue({})", message: "no pod's labels change in the folder namespace" },
485
+ {
486
+ expression: `request.operation != 'UPDATE' || (${unchanged("Annotations", Object.keys(BUSY_ANNOTATIONS))})`,
487
+ message: "no annotation of a folder instance's pod changes but what keeps its node",
488
+ },
489
+ {
490
+ expression: "request.operation != 'UPDATE' || object.metadata.?ownerReferences.orValue([]).all(owner, owner in oldObject.metadata.?ownerReferences.orValue([]))",
491
+ message: "a pod of the folder namespace gets no owner it had not",
492
+ },
493
+ ],
494
+ },
495
+ };
496
+ const binding = { apiVersion: "admissionregistration.k8s.io/v1", kind: "ValidatingAdmissionPolicyBinding", metadata, spec: { policyName: FOLDER_POD_POLICY, validationActions: ["Deny"] } };
497
+ return [policy, binding];
498
+ }
499
+ /** What the sandbox gateway, `maker`, may make as a Secret in the folder namespace, `namespace`: a folder instance's token, of its one key, named for its pod. */
500
+ export function folderSecretPolicy({ namespace, maker }) {
501
+ return admissionPolicy(componentName("folder-secrets"), [{ apiGroups: [""], resources: ["secrets"], operations: ["CREATE", "UPDATE"] }], `request.userInfo.username == ${JSON.stringify(maker)}`, [{
502
+ expression: `request.namespace == ${JSON.stringify(namespace)} && object.?type.orValue("Opaque") == "Opaque" && ` +
503
+ `object.metadata.name.matches(${JSON.stringify(`^${FOLDER_POD.source.slice(1, -1)}${literally(folderTokenSecret(""))}$`)}) && ` +
504
+ `object.?data.orValue({}).all(key, key == "token")`,
505
+ message: "the sandbox gateway makes no Secret but a folder instance's token, in the folder namespace",
506
+ }]);
362
507
  }
363
- /** The name of the Sandbox policy of the alasio of the namespace `namespace`. */
364
- export const sandboxPolicyName = (namespace) => `${namespace}-sandboxes`;
365
508
  /**
366
- * What alasio may make as a Sandbox, by `account`, the ServiceAccount of the alasio of
367
- * `name`: a session's from `sessions`, or from `previousSessions`, the sessions' template
368
- * before it, which `alasio up` carries forward until the template changes again, so a
369
- * snapshot taken on it restores onto it whole (src/sandbox/snapshots.ts), in any namespace
370
- * but `host`'s; and a folder's bayma's from `host`, in its namespace. A Sandbox moved onto
371
- * no other pod template, as alasio suspends and resumes it, is left as it was made. Each
372
- * template is a variable of its own, as each is checked whole.
509
+ * What the sandbox gateway, `maker`, may delete in the folder namespace, `namespace`, as RBAC
510
+ * lets it delete any pod and Secret there: a folder instance's pod, of the folder pool, and a
511
+ * folder instance's token's Secret, alone.
373
512
  */
374
- export function sandboxPolicy(name, account, templates) {
375
- const { sessions, previousSessions, host } = templates;
376
- const ofSessions = sessions
377
- ? [
378
- { name: "sessions", expression: pinned("object.spec", sandboxOf(sessions, { session: sessions, current: sessions })) },
379
- ...(previousSessions ? [{ name: "previousSessions", expression: pinned("object.spec", sandboxOf(previousSessions, { session: previousSessions, current: sessions })) }] : []),
380
- ]
381
- : [];
382
- const ofHost = host ? [{ name: "host", expression: pinned("object.spec", sandboxOf(host, { folder: host })) }] : [];
383
- const session = ofSessions.length > 0 ? `(${ofSessions.map((each) => `variables.${each.name}`).join(" || ")})` : "false";
384
- const made = host ? `(request.namespace == ${JSON.stringify(host.namespace)} ? variables.host : ${session})` : session;
385
- const unmoved = "request.operation == \"UPDATE\" && object.spec.podTemplate == oldObject.spec.podTemplate && " +
386
- "object.spec.?volumeClaimTemplates.orValue([]) == oldObject.spec.?volumeClaimTemplates.orValue([])";
387
- // A restore's pod is left paused, and ready only once resumed; no other pod is paused.
388
- const annotations = "object.spec.podTemplate.?metadata.?annotations.orValue({})";
389
- const restoring = `(${JSON.stringify(RESTORE_ANNOTATION)} in ${annotations})`;
390
- const gated = `object.spec.podTemplate.?spec.?readinessGates.orValue([]).exists(gate, gate.conditionType == ${JSON.stringify(RESUMED_CONDITION)})`;
391
- return admissionPolicy(name, [{ apiGroups: ["agents.x-k8s.io"], resources: ["sandboxes"], operations: ["CREATE", "UPDATE"] }], `request.userInfo.username == "${account}"`, [
392
- { expression: `${unmoved} || ${made}`, message: "alasio makes no Sandbox but from its workspaces' templates, as alasio up renders them" },
513
+ export function folderDeletionPolicy({ namespace, maker }) {
514
+ const pool = `oldObject.metadata.?labels.orValue({})`;
515
+ return admissionPolicy(componentName("folder-deletions"), [{ apiGroups: [""], resources: ["pods", "secrets"], operations: ["DELETE"] }], `request.userInfo.username == ${JSON.stringify(maker)} && request.namespace == ${JSON.stringify(namespace)}`, [
393
516
  {
394
- expression: `${restoring} == (${JSON.stringify(RESTORE_PAUSED_ANNOTATION)} in ${annotations}) && (!${restoring} || ${gated})`,
395
- message: "alasio restores a Sandbox from a snapshot only paused, its pod ready once resumed, and pauses none it does not restore",
517
+ expression: `request.resource.resource != "pods" || (oldObject.metadata.name.matches(${JSON.stringify(FOLDER_POD.source)}) && ${JSON.stringify(POOL_LABEL)} in ${pool} && ${pool}[${JSON.stringify(POOL_LABEL)}] == ${JSON.stringify(FOLDER_POOL)})`,
518
+ message: "the sandbox gateway deletes no pod in the folder namespace but a folder instance's",
396
519
  },
397
- ], [...ofSessions, ...ofHost]);
520
+ {
521
+ expression: `request.resource.resource != "secrets" || oldObject.metadata.name.matches(${JSON.stringify(`^${FOLDER_POD.source.slice(1, -1)}${literally(folderTokenSecret(""))}$`)})`,
522
+ message: "the sandbox gateway deletes no Secret in the folder namespace but a folder instance's token",
523
+ },
524
+ ]);
398
525
  }
399
526
  /**
400
527
  * The fields of a pod's status but its conditions that every Kubernetes alasio runs on
@@ -417,39 +544,31 @@ const POD_STATUS_FIELDS = [
417
544
  "startTime",
418
545
  ];
419
546
  /**
420
- * What the alasio of `account` may change of a pod's status, as it lets a restored session's
421
- * pod be ready once its node's agent resumed its sandbox (src/sandbox/snapshots.ts): the
422
- * RESUMED_CONDITION of a session's pod in `namespace` that is gated on it, set true, and
423
- * nothing else, its addresses above all, which network policies are programmed from.
547
+ * What `account`, the sandbox gateway's, may change of a pod's status, as it lets a restore's
548
+ * pod be ready once its node's agent resumed its sandbox (src/gateway/snapshots.ts): the
549
+ * RESUMED_CONDITION of a pool's pod in `namespace` that is gated on it, set true, and nothing
550
+ * else, its addresses above all, which network policies are programmed from. RBAC lets it
551
+ * patch the status of any pod of the namespace, whole.
424
552
  */
425
553
  export function resumedConditionPolicy(name, account, namespace) {
426
554
  const resumed = JSON.stringify(RESUMED_CONDITION);
427
555
  const others = (object) => `${object}.status.?conditions.orValue([]).filter(condition, condition.type != ${resumed})`;
428
556
  return admissionPolicy(name, [{ apiGroups: [""], resources: ["pods/status"], operations: ["UPDATE"] }], `request.userInfo.username == "${account}"`, [
429
557
  {
430
- expression: `request.namespace == ${JSON.stringify(namespace)} && ${JSON.stringify(WORKLOAD_LABEL)} in object.metadata.?labels.orValue({}) && ` +
431
- `object.metadata.labels[${JSON.stringify(WORKLOAD_LABEL)}] == "session" && ` +
558
+ expression: `request.namespace == ${JSON.stringify(namespace)} && ${JSON.stringify(POOL_LABEL)} in object.metadata.?labels.orValue({}) && ` +
432
559
  `object.spec.?readinessGates.orValue([]).exists(gate, gate.conditionType == ${resumed})`,
433
- message: "alasio sets the status of no pod but a restored session's",
560
+ message: "the sandbox gateway sets the status of no pod but a restore's of its pools'",
434
561
  },
435
562
  {
436
563
  expression: `${others("object")} == ${others("oldObject")} && ` +
437
564
  `object.status.?conditions.orValue([]).all(condition, condition.type != ${resumed} || condition.status == "True")`,
438
- message: `alasio sets no condition of a pod's but ${RESUMED_CONDITION}, and that true`,
565
+ message: `the sandbox gateway sets no condition of a pod's but ${RESUMED_CONDITION}, and that true`,
439
566
  },
440
567
  {
441
568
  expression: POD_STATUS_FIELDS.map((field) => `(has(object.status.${field}) ? has(oldObject.status.${field}) && object.status.${field} == oldObject.status.${field} : !has(oldObject.status.${field}))`).join(" && ") +
442
569
  " && object.metadata.?labels.orValue({}) == oldObject.metadata.?labels.orValue({}) && object.metadata.?annotations.orValue({}) == oldObject.metadata.?annotations.orValue({})",
443
- message: "alasio changes nothing of a pod's status but its conditions, nor its labels or annotations",
570
+ message: "the sandbox gateway changes nothing of a pod's status but its conditions, nor its labels or annotations",
444
571
  },
445
572
  ]);
446
573
  }
447
- /** What alasio may make as a Secret: a Sandbox's token, of its one key, as tokenSecretManifest makes it. Main's alasio's and each branch environment's. */
448
- export function secretPolicy() {
449
- return admissionPolicy(componentName("secrets"), [{ apiGroups: [""], resources: ["secrets"], operations: ["CREATE", "UPDATE"] }], BY_ANY_ALASIO, [{
450
- expression: `object.?type.orValue("Opaque") == "Opaque" && object.metadata.name.endsWith(${JSON.stringify(tokenSecretName(""))}) && ` +
451
- `object.?data.orValue({}).all(key, key == ${JSON.stringify(TOKEN_KEY)})`,
452
- message: "alasio makes no Secret but a Sandbox's token",
453
- }]);
454
- }
455
574
  //# sourceMappingURL=admission.js.map