alasio 7.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 (161) 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 +123 -46
  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 +51 -12
  10. package/dist/cli/src/cluster/k3s.js.map +1 -1
  11. package/dist/cli/src/cluster/node.js +115 -33
  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/init.js +54 -4
  22. package/dist/cli/src/commands/init.js.map +1 -1
  23. package/dist/cli/src/commands/lake.js +40 -6
  24. package/dist/cli/src/commands/lake.js.map +1 -1
  25. package/dist/cli/src/commands/restore-filer-store.js +40 -8
  26. package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
  27. package/dist/cli/src/commands/status.js +216 -20
  28. package/dist/cli/src/commands/status.js.map +1 -1
  29. package/dist/cli/src/commands/uninstall.js +8 -9
  30. package/dist/cli/src/commands/uninstall.js.map +1 -1
  31. package/dist/cli/src/commands/up.js +11 -3
  32. package/dist/cli/src/commands/up.js.map +1 -1
  33. package/dist/cli/src/commands/upgrade.js +1 -1
  34. package/dist/cli/src/commands/upgrade.js.map +1 -1
  35. package/dist/cli/src/commands.js +2 -1
  36. package/dist/cli/src/commands.js.map +1 -1
  37. package/dist/cli/src/config.js +24 -9
  38. package/dist/cli/src/config.js.map +1 -1
  39. package/dist/cli/src/cpu-baseline.js +8 -4
  40. package/dist/cli/src/cpu-baseline.js.map +1 -1
  41. package/dist/cli/src/folders.js +67 -0
  42. package/dist/cli/src/folders.js.map +1 -0
  43. package/dist/cli/src/gateway-calls.js +58 -0
  44. package/dist/cli/src/gateway-calls.js.map +1 -0
  45. package/dist/cli/src/install.js +119 -17
  46. package/dist/cli/src/install.js.map +1 -1
  47. package/dist/cli/src/kept-releases.js +27 -20
  48. package/dist/cli/src/kept-releases.js.map +1 -1
  49. package/dist/cli/src/kube/api.js +11 -5
  50. package/dist/cli/src/kube/api.js.map +1 -1
  51. package/dist/cli/src/kube/apply.js +16 -73
  52. package/dist/cli/src/kube/apply.js.map +1 -1
  53. package/dist/cli/src/kube/rollout.js +84 -26
  54. package/dist/cli/src/kube/rollout.js.map +1 -1
  55. package/dist/cli/src/lake.js +124 -0
  56. package/dist/cli/src/lake.js.map +1 -0
  57. package/dist/cli/src/manifests/admission.js +381 -223
  58. package/dist/cli/src/manifests/admission.js.map +1 -1
  59. package/dist/cli/src/manifests/alasio.js +79 -362
  60. package/dist/cli/src/manifests/alasio.js.map +1 -1
  61. package/dist/cli/src/manifests/branch.js +10 -26
  62. package/dist/cli/src/manifests/branch.js.map +1 -1
  63. package/dist/cli/src/manifests/collector.js +250 -135
  64. package/dist/cli/src/manifests/collector.js.map +1 -1
  65. package/dist/cli/src/manifests/common.js +32 -2
  66. package/dist/cli/src/manifests/common.js.map +1 -1
  67. package/dist/cli/src/manifests/config.js +256 -92
  68. package/dist/cli/src/manifests/config.js.map +1 -1
  69. package/dist/cli/src/manifests/disruption.js +10 -0
  70. package/dist/cli/src/manifests/disruption.js.map +1 -1
  71. package/dist/cli/src/manifests/folders.js +128 -0
  72. package/dist/cli/src/manifests/folders.js.map +1 -0
  73. package/dist/cli/src/manifests/grafana.js +4 -2
  74. package/dist/cli/src/manifests/grafana.js.map +1 -1
  75. package/dist/cli/src/manifests/index.js +15 -8
  76. package/dist/cli/src/manifests/index.js.map +1 -1
  77. package/dist/cli/src/manifests/keda.js +2 -1
  78. package/dist/cli/src/manifests/keda.js.map +1 -1
  79. package/dist/cli/src/manifests/lake.js +350 -73
  80. package/dist/cli/src/manifests/lake.js.map +1 -1
  81. package/dist/cli/src/manifests/log.js +316 -0
  82. package/dist/cli/src/manifests/log.js.map +1 -0
  83. package/dist/cli/src/manifests/neon.js +128 -49
  84. package/dist/cli/src/manifests/neon.js.map +1 -1
  85. package/dist/cli/src/manifests/network-policies.js +263 -100
  86. package/dist/cli/src/manifests/network-policies.js.map +1 -1
  87. package/dist/cli/src/manifests/node-agent.js +24 -8
  88. package/dist/cli/src/manifests/node-agent.js.map +1 -1
  89. package/dist/cli/src/manifests/object-store.js +4 -2
  90. package/dist/cli/src/manifests/object-store.js.map +1 -1
  91. package/dist/cli/src/manifests/sandbox-gateway.js +663 -0
  92. package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
  93. package/dist/cli/src/manifests/scalers.js +16 -5
  94. package/dist/cli/src/manifests/scalers.js.map +1 -1
  95. package/dist/cli/src/manifests/sizing.js +16 -8
  96. package/dist/cli/src/manifests/sizing.js.map +1 -1
  97. package/dist/cli/src/manifests/valkey.js +11 -5
  98. package/dist/cli/src/manifests/valkey.js.map +1 -1
  99. package/dist/cli/src/manifests/vpa.js +3 -1
  100. package/dist/cli/src/manifests/vpa.js.map +1 -1
  101. package/dist/cli/src/manifests/webhook.js +102 -0
  102. package/dist/cli/src/manifests/webhook.js.map +1 -0
  103. package/dist/cli/src/manifests/workspace-storage.js +174 -89
  104. package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
  105. package/dist/cli/src/neon-storage.js +155 -0
  106. package/dist/cli/src/neon-storage.js.map +1 -0
  107. package/dist/cli/src/release.js +9 -6
  108. package/dist/cli/src/release.js.map +1 -1
  109. package/dist/cli/src/secrets.js +45 -4
  110. package/dist/cli/src/secrets.js.map +1 -1
  111. package/dist/cli/src/target.js +1 -1
  112. package/dist/cli/src/target.js.map +1 -1
  113. package/dist/cli/src/telegram.js +56 -10
  114. package/dist/cli/src/telegram.js.map +1 -1
  115. package/dist/cli/src/webhook.js +182 -0
  116. package/dist/cli/src/webhook.js.map +1 -0
  117. package/dist/cluster/node/pins.json +12 -2
  118. package/dist/neon/control/branches.js +126 -21
  119. package/dist/neon/control/branches.js.map +1 -1
  120. package/dist/neon/control/endpoints.js +9 -0
  121. package/dist/neon/control/endpoints.js.map +1 -1
  122. package/dist/neon/control/jwt.js +16 -3
  123. package/dist/neon/control/jwt.js.map +1 -1
  124. package/dist/neon/control/secrets.js +51 -7
  125. package/dist/neon/control/secrets.js.map +1 -1
  126. package/dist/src/binder/names.js +33 -0
  127. package/dist/src/binder/names.js.map +1 -0
  128. package/dist/src/binder/tokens.js +58 -0
  129. package/dist/src/binder/tokens.js.map +1 -0
  130. package/dist/src/branch/names.js +12 -2
  131. package/dist/src/branch/names.js.map +1 -1
  132. package/dist/src/events/names.js +7 -0
  133. package/dist/src/events/names.js.map +1 -0
  134. package/dist/src/gateway/names.js +201 -0
  135. package/dist/src/gateway/names.js.map +1 -0
  136. package/dist/src/harness/names.js +49 -0
  137. package/dist/src/harness/names.js.map +1 -0
  138. package/dist/src/kube/sandbox-shape.js +19 -69
  139. package/dist/src/kube/sandbox-shape.js.map +1 -1
  140. package/dist/src/node-agent/names.js +118 -2
  141. package/dist/src/node-agent/names.js.map +1 -1
  142. package/dist/src/sandbox/names.js +12 -20
  143. package/dist/src/sandbox/names.js.map +1 -1
  144. package/dist/src/sandbox/otlp-resource.js +173 -0
  145. package/dist/src/sandbox/otlp-resource.js.map +1 -0
  146. package/dist/src/sandbox/telemetry.js +43 -0
  147. package/dist/src/sandbox/telemetry.js.map +1 -0
  148. package/dist/src/telegram/deliveries.js +40 -0
  149. package/dist/src/telegram/deliveries.js.map +1 -0
  150. package/dist/src/telemetry/config.js +131 -0
  151. package/dist/src/telemetry/config.js.map +1 -0
  152. package/dist/src/telemetry/forward.js +154 -0
  153. package/dist/src/telemetry/forward.js.map +1 -0
  154. package/package.json +1 -1
  155. package/dist/cli/src/manifests/agent-sandbox.js +0 -92
  156. package/dist/cli/src/manifests/agent-sandbox.js.map +0 -1
  157. package/dist/cli/src/manifests/juicefs-csi.js +0 -319
  158. package/dist/cli/src/manifests/juicefs-csi.js.map +0 -1
  159. package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +0 -6295
  160. package/dist/src/kube/config.js +0 -143
  161. package/dist/src/kube/config.js.map +0 -1
@@ -1,32 +1,32 @@
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
- * pods whole, their images among it.
13
+ * pods whole, their images among it;
14
+ * - a compute's Deployment neon-control patches changes its pod template's restartedAt
15
+ * annotation alone, which restarts it (neon/control/computes.ts): RBAC lets it patch the
16
+ * Deployment whole, its image and command among it.
19
17
  *
20
18
  * Where a policy holds an object to a rendered one, `pinned` makes the CEL that says so:
21
19
  * the object's fields are the rendered ones', key for key and item for item, but for what
22
20
  * a Check allows, and what the API server defaults where the rendered one has nothing.
23
21
  */
24
22
  import { posix } from "node:path";
25
- 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";
26
26
  import { BUSY_ANNOTATIONS, BUSY_LABEL } from "../../../src/kube/busy-label.js";
27
- import { CLONE_VARIABLES } from "../../../src/kube/config.js";
28
- 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";
29
- import { componentName, labels, NAMESPACE, RELEASE } from "./common.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";
29
+ import { componentName, labels, NAMESPACE, neonName, RELEASE } from "./common.js";
30
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. */
31
31
  class Check {
32
32
  cel;
@@ -38,23 +38,12 @@ class Check {
38
38
  const ANY = new Check(() => "true");
39
39
  /** Any string. */
40
40
  const ANY_STRING = new Check((expression) => `type(${expression}) == string`);
41
- /** A map with nothing in it, as the API server defaults one. */
42
- const EMPTY = new Check((expression) => `size(${expression}) == 0`);
43
41
  /** A string all of which `pattern` matches. */
44
42
  const matching = (pattern) => new Check((expression) => `type(${expression}) == string && ${expression}.matches(${JSON.stringify(`^(${pattern})$`)})`);
45
43
  /** What the CEL `value` is, of the object asked about. */
46
44
  const sameAs = (value) => new Check((expression) => `${expression} == ${value}`);
47
45
  /** Any of `alternatives`. */
48
46
  const oneOf = (...alternatives) => new Check((expression, depth) => `(${alternatives.map((each) => pinned(expression, each, depth)).join(" || ")})`);
49
- /** A list of `first`, then any number of items each `rest`. */
50
- const startingWith = (first, rest) => new Check((expression, depth) => {
51
- const item = `item${depth}`;
52
- return [
53
- `size(${expression}) >= ${first.length}`,
54
- ...first.map((each, index) => pinned(`${expression}[${index}]`, each, depth + 1)),
55
- `${expression}.all(${item}, ${[...first, rest].map((each) => pinned(item, each, depth + 1)).join(" || ")})`,
56
- ].join(" && ");
57
- });
58
47
  /** Of a map's key: that it may be absent; where present, `value`. */
59
48
  class Optional extends Check {
60
49
  value;
@@ -124,14 +113,15 @@ function admissionPolicy(name, rules, by, validations, variables) {
124
113
  const binding = { apiVersion: "admissionregistration.k8s.io/v1", kind: "ValidatingAdmissionPolicyBinding", metadata, spec: { policyName: name, validationActions: ["Deny"] } };
125
114
  return [policy, binding];
126
115
  }
116
+ /** CEL that holds where the maps `variables.old<field>` and `variables.new<field>` differ in `keys` alone. */
117
+ const unchanged = (field, keys) => `variables.new${field}.all(key, key in ${JSON.stringify(keys)} || (key in variables.old${field} && variables.old${field}[key] == variables.new${field}[key])) && ` +
118
+ `variables.old${field}.all(key, key in ${JSON.stringify(keys)} || key in variables.new${field})`;
127
119
  /**
128
120
  * What alasio may change of a pod: its busy label and the annotations that go with it
129
121
  * (src/kube/busy.ts), and nothing else; and in its own namespace, of its own pod alone,
130
122
  * not the floor's beside it. Main's alasio's and each branch environment's.
131
123
  */
132
124
  export function busyLabelPolicy() {
133
- const unchanged = (field, keys) => `variables.new${field}.all(key, key in ${JSON.stringify(keys)} || (key in variables.old${field} && variables.old${field}[key] == variables.new${field}[key])) && ` +
134
- `variables.old${field}.all(key, key in ${JSON.stringify(keys)} || key in variables.new${field})`;
135
125
  const component = "app.kubernetes.io/component";
136
126
  return admissionPolicy(componentName("busy-label"), [{ apiGroups: [""], resources: ["pods"], operations: ["UPDATE"] }], BY_ANY_ALASIO, [
137
127
  { expression: "object.spec == oldObject.spec", message: "alasio changes no pod's spec" },
@@ -154,208 +144,384 @@ export function busyLabelPolicy() {
154
144
  { name: "newAnnotations", expression: "object.metadata.?annotations.orValue({})" },
155
145
  ]);
156
146
  }
147
+ /** The annotation of a pod template whose change restarts its pods, as `kubectl rollout restart` sets it. */
148
+ const RESTARTED_AT = "kubectl.kubernetes.io/restartedAt";
149
+ /** The fields of a Deployment's spec but its pod template. */
150
+ const DEPLOYMENT_FIELDS = ["replicas", "selector", "strategy", "minReadySeconds", "revisionHistoryLimit", "paused", "progressDeadlineSeconds"];
157
151
  /**
158
- * What the API server gives a Job, its template, and the template's pod and containers,
159
- * when its maker does not: a pinned Job may have them so. Its selector and its template's
160
- * labels of its own are the Job's; a pod's scheduling and termination are no matter of
161
- * its rights; and a pod's security context, defaulted, is empty.
152
+ * What neon-control may change of a Deployment, which RBAC lets it patch whole, of the
153
+ * computes alone (./neon.ts's computeScaling): its pod template's RESTARTED_AT, which
154
+ * restarts it (neon/control/computes.ts), and nothing else. Its scale it changes through
155
+ * the scale subresource, which this does not match.
162
156
  */
163
- const JOB_DEFAULTS = {
164
- spec: { completions: 1, parallelism: 1, completionMode: "NonIndexed", suspend: false, manualSelector: false, podReplacementPolicy: ANY_STRING, selector: ANY },
165
- metadata: { creationTimestamp: ANY },
166
- labels: {
167
- "batch.kubernetes.io/controller-uid": ANY_STRING,
168
- "controller-uid": ANY_STRING,
169
- "batch.kubernetes.io/job-name": sameAs("object.metadata.name"),
170
- "job-name": sameAs("object.metadata.name"),
171
- },
172
- pod: { dnsPolicy: ANY, schedulerName: ANY, terminationGracePeriodSeconds: ANY, securityContext: EMPTY },
173
- container: { terminationMessagePath: ANY, terminationMessagePolicy: ANY },
174
- };
175
- /**
176
- * What alasio may run as a Job: the clone of a session's volume (src/sandbox/index.ts),
177
- * `job`, its spec and its pod's as rendered, but for the claims each of its containers is
178
- * told, as alasio adds them (cloneJobManifest), each a DNS label (DNS_LABEL). Main's
179
- * alasio's, the one that clones.
180
- */
181
- export function cloneJobPolicy(job) {
182
- const spec = job.spec ?? {};
183
- const pod = spec.template?.spec;
184
- const claims = CLONE_VARIABLES.map((variable) => ({ name: variable, value: matching(DNS_LABEL) }));
185
- const template = spec.template;
186
- const containers = (pod?.containers ?? []).map((container) => defaulted({ ...container, env: [...(container.env ?? []), ...claims] }, JOB_DEFAULTS.container));
187
- const expected = defaulted({
188
- ...spec,
189
- template: {
190
- metadata: defaulted({ ...template?.metadata, labels: defaulted(template?.metadata?.labels ?? {}, JOB_DEFAULTS.labels) }, JOB_DEFAULTS.metadata),
191
- spec: defaulted({ ...pod, containers }, JOB_DEFAULTS.pod),
157
+ export function computeRestartPolicy() {
158
+ const same = (field) => `has(object.spec.${field}) == has(oldObject.spec.${field}) && (!has(object.spec.${field}) || object.spec.${field} == oldObject.spec.${field})`;
159
+ const account = `system:serviceaccount:${NAMESPACE}:${neonName("control")}`;
160
+ return admissionPolicy(componentName("compute-restarts"), [{ apiGroups: ["apps"], resources: ["deployments"], operations: ["UPDATE"] }], `request.userInfo.username == "${account}"`, [
161
+ { expression: "object.spec.template.spec == oldObject.spec.template.spec", message: "neon-control changes no compute's pod spec" },
162
+ { expression: DEPLOYMENT_FIELDS.map(same).join(" && "), message: "neon-control changes nothing of a compute's Deployment's spec but its pod template's annotation" },
163
+ {
164
+ expression: "object.metadata.?labels.orValue({}) == oldObject.metadata.?labels.orValue({}) && " +
165
+ "object.metadata.?annotations.orValue({}) == oldObject.metadata.?annotations.orValue({}) && " +
166
+ "object.metadata.?finalizers.orValue([]) == oldObject.metadata.?finalizers.orValue([]) && " +
167
+ "object.metadata.?ownerReferences.orValue([]) == oldObject.metadata.?ownerReferences.orValue([])",
168
+ message: "neon-control changes no compute Deployment's labels, annotations, finalizers or owners",
192
169
  },
193
- }, JOB_DEFAULTS.spec);
194
- return admissionPolicy(componentName("clone-job"), [{ apiGroups: ["batch"], resources: ["jobs"], operations: ["CREATE", "UPDATE"] }], `request.userInfo.username == "${alasioAccount(NAMESPACE)}"`, [{
195
- expression: pinned("object.spec", expected),
196
- message: "alasio runs no Job but its clone of a session's volume, as alasio up renders it",
197
- }]);
170
+ {
171
+ expression: Object.entries({ labels: "{}", finalizers: "[]", ownerReferences: "[]", name: '""', generateName: '""' })
172
+ .map(([field, none]) => `object.spec.template.metadata.?${field}.orValue(${none}) == oldObject.spec.template.metadata.?${field}.orValue(${none})`)
173
+ .join(" && "),
174
+ message: "neon-control changes no label, finalizer, owner or name of a compute's pod template",
175
+ },
176
+ { expression: unchanged("Annotations", [RESTARTED_AT]), message: `neon-control changes no annotation of a compute's pod template but ${RESTARTED_AT}` },
177
+ ], [
178
+ { name: "oldAnnotations", expression: "oldObject.spec.template.metadata.?annotations.orValue({})" },
179
+ { name: "newAnnotations", expression: "object.spec.template.metadata.?annotations.orValue({})" },
180
+ ]);
198
181
  }
199
- /** A container of a Sandbox's pod template as its CRD stores it: its ports' protocol defaulted. */
200
- const stored = (container) => {
201
- const ports = container["ports"];
202
- return Array.isArray(ports) ? { ...container, ports: ports.map((port) => defaulted(port, { protocol: "TCP" })) } : container;
203
- };
204
- /** The token Secret of the Sandbox asked about, as CEL. */
205
- const OWN_TOKEN = sameAs(`object.metadata.name + ${JSON.stringify(tokenSecretName(""))}`);
206
182
  /** A path segment as sanitizePathToken makes one. */
207
183
  const PATH_TOKEN = "[A-Za-z0-9_][A-Za-z0-9_.-]*";
208
184
  /** `text`, as a pattern that matches it alone. */
209
185
  const literally = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
210
- /** A variable of the environment, set to a string. */
211
- const SET = { name: ANY_STRING, value: ANY_STRING };
212
186
  /** A node's name: a DNS subdomain. */
213
187
  const NODE_NAME = "[a-z0-9]([-a-z0-9.]{0,251}[a-z0-9])?";
214
- /** A pod template's hash, as podTemplateHash makes one (src/kube/sandboxes.ts). */
215
- 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 }));
216
192
  /**
217
- * Where a node agent prepares a snapshot of the Sandbox asked about, of the sessions of
218
- * `namespace`, for its pod to be restored from: under the snapshot cache, in its
219
- * environment's directory, in its workspace's, the Sandbox's own, a snapshot's.
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.
220
198
  */
221
- const restoredFrom = (namespace) => new Check((expression) => {
222
- const own = `${JSON.stringify(`${SNAPSHOT_CACHE_DIR}/${namespace}/`)} + object.metadata.name + "/"`;
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
+ }
243
+ /**
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.
247
+ */
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}) + "/"`;
223
250
  return `type(${expression}) == string && ${expression}.startsWith(${own}) && ${expression}.substring(size(${own})).matches(${JSON.stringify(`^${SNAPSHOT_ID}$`)})`;
224
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)})`);
225
254
  /**
226
- * The Sandbox of a workspace alasio makes from `template`: a session's (`session`, with
227
- * the session's profile, and `current`, the profile its snapshots are restored by) or a
228
- * folder's bayma's (`folder`, with its state root), as sandboxManifest and
229
- * sessionSandboxManifest or hostBaymaManifest make it.
230
- *
231
- * Of a session's, what its snapshots add (src/sandbox/snapshots.ts): the hash of the
232
- * template its pod is made from, which they record; and, restored from one, the image of the Sandbox's own that its
233
- * node prepared, left paused until it is resumed, under a release `current` keeps, on
234
- * 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.
235
259
  */
236
- function sandboxOf(template, workspace) {
237
- const podTemplate = template.podTemplate;
238
- const spec = podTemplate.spec ?? { containers: [] };
239
- const session = "session" in workspace ? workspace.session : null;
240
- const snapshots = "current" in workspace ? (workspace.current.snapshots ?? null) : null;
241
- const bayma = spec.containers.find(({ name }) => name === BAYMA_CONTAINER);
242
- const tokenMount = { name: TOKEN_VOLUME, mountPath: TOKEN_DIR, readOnly: true };
243
- // Under the state root as hostBaymaStateDir joins it: normalized, its one slash after it.
244
- const stateDir = "folder" in workspace ? [STATE_DIR_ARG, matching(`${literally(posix.join(workspace.folder.stateRoot, "/"))}${PATH_TOKEN}/${PATH_TOKEN}`)] : [];
245
- // A session's bayma that exports telemetry reads the token it exports with from its token's file too.
246
- const args = (each) => {
247
- const own = [...(each.args ?? []), ...TOKEN_FILE_ARGS, ...stateDir];
248
- 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)] }] }] } },
249
266
  };
250
- const container = (each) => stored((each.name !== BAYMA_CONTAINER ? each : {
251
- ...each,
252
- args: args(each),
253
- volumeMounts: [...(each.volumeMounts ?? []), tokenMount],
254
- env: optional(startingWith((each.env ?? []), SET)),
255
- }));
256
- const gate = session && session.egressGate !== false
257
- ? [{
258
- name: EGRESS_GATE.name,
259
- ...(bayma?.image === undefined ? {} : { image: bayma.image }),
260
- command: ["node", "-e", EGRESS_GATE_SCRIPT],
261
- ...(bayma?.securityContext === undefined ? {} : { securityContext: bayma.securityContext }),
262
- resources: EGRESS_GATE.resources,
263
- }]
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(" || ")}))`]
264
323
  : [];
265
- const initContainers = [...gate, ...(spec.initContainers ?? [])].map(stored);
266
- const volumes = [...(spec.volumes ?? []), { name: TOKEN_VOLUME, secret: { secretName: OWN_TOKEN, defaultMode: TOKEN_MODE } }];
267
- const own = {
268
- ...MANAGED_BY,
269
- [SANDBOX_LABEL]: sameAs("object.metadata.name"),
270
- ...(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
+ },
271
374
  };
272
- const annotations = (podTemplate.metadata?.annotations ?? {});
273
- const added = session
274
- ? {
275
- [SESSION_TEMPLATE_ANNOTATION]: optional(matching(TEMPLATE_HASH)),
276
- ...(snapshots && session
277
- ? {
278
- [RESTORE_ANNOTATION]: optional(restoredFrom(session.namespace)),
279
- [RESTORE_PAUSED_ANNOTATION]: optional("true"),
280
- }
281
- : {}),
282
- }
283
- : {};
284
- // 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.
285
- const metadataAnnotations = Object.keys(added).length === 0 ? {} : { annotations: Object.keys(annotations).length > 0 ? { ...annotations, ...added } : optional({ ...added }) };
286
- const restoredOn = {
287
- ...(spec.affinity ?? {}),
288
- nodeAffinity: {
289
- 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,
290
410
  },
291
411
  };
292
- const readinessGates = (spec.readinessGates ?? []);
293
- const restorable = snapshots
294
- ? {
295
- runtimeClassName: oneOf(...new Set([spec.runtimeClassName ?? "", ...snapshots.releases.map(({ runtimeClassName }) => runtimeClassName)].filter((each) => each !== ""))),
296
- readinessGates: spec.readinessGates ? oneOf(readinessGates, [...readinessGates, { conditionType: RESUMED_CONDITION }]) : optional([{ conditionType: RESUMED_CONDITION }]),
297
- affinity: spec.affinity ? oneOf(spec.affinity, restoredOn) : optional(restoredOn),
298
- }
299
- : {};
300
- const nameservers = session ? [...(session.fullModeNameservers ?? DEFAULT_FULL_MODE_NAMESERVERS)] : [];
301
- const podSpec = {
302
- ...spec,
303
- containers: spec.containers.map(container),
304
- ...(initContainers.length > 0 ? { initContainers } : {}),
305
- volumes,
306
- // A session's pod speaks to the cluster as no one, and resolves names as its internet says.
307
- ...(session
308
- ? { automountServiceAccountToken: false, enableServiceLinks: false, dnsPolicy: "None", dnsConfig: { nameservers: oneOf(nameservers, ["127.0.0.1"]) } }
309
- : {}),
310
- ...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),
311
454
  };
312
- // A Sandbox restored onto the template before the current one keeps the claims it was made with, of either.
313
- const claims = [...new Set([session, "current" in workspace ? workspace.current : null].map((each) => JSON.stringify(each?.volumeClaimTemplates ?? [])))]
314
- .map((each) => JSON.parse(each))
315
- .filter((each) => each.length > 0);
316
- // Its CRD defaults what is done with it at its shutdown time, which alasio gives none.
317
- return defaulted({
318
- operatingMode: oneOf("Running", "Suspended"),
319
- service: true,
320
- podTemplate: { ...podTemplate, metadata: { ...podTemplate.metadata, labels: { ...podTemplate.metadata?.labels, ...own }, ...metadataAnnotations }, spec: podSpec },
321
- ...(claims.length > 0 ? { volumeClaimTemplates: claims.length === 1 ? claims[0] : oneOf(...claims) } : {}),
322
- }, { 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
+ }]);
323
507
  }
324
- /** The name of the Sandbox policy of the alasio of the namespace `namespace`. */
325
- export const sandboxPolicyName = (namespace) => `${namespace}-sandboxes`;
326
508
  /**
327
- * What alasio may make as a Sandbox, by `account`, the ServiceAccount of the alasio of
328
- * `name`: a session's from `sessions`, or from `previousSessions`, the sessions' template
329
- * before it, which `alasio up` carries forward until the template changes again, so a
330
- * snapshot taken on it restores onto it whole (src/sandbox/snapshots.ts), in any namespace
331
- * but `host`'s; and a folder's bayma's from `host`, in its namespace. A Sandbox moved onto
332
- * no other pod template, as alasio suspends and resumes it, is left as it was made. Each
333
- * 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.
334
512
  */
335
- export function sandboxPolicy(name, account, templates) {
336
- const { sessions, previousSessions, host } = templates;
337
- const ofSessions = sessions
338
- ? [
339
- { name: "sessions", expression: pinned("object.spec", sandboxOf(sessions, { session: sessions, current: sessions })) },
340
- ...(previousSessions ? [{ name: "previousSessions", expression: pinned("object.spec", sandboxOf(previousSessions, { session: previousSessions, current: sessions })) }] : []),
341
- ]
342
- : [];
343
- const ofHost = host ? [{ name: "host", expression: pinned("object.spec", sandboxOf(host, { folder: host })) }] : [];
344
- const session = ofSessions.length > 0 ? `(${ofSessions.map((each) => `variables.${each.name}`).join(" || ")})` : "false";
345
- const made = host ? `(request.namespace == ${JSON.stringify(host.namespace)} ? variables.host : ${session})` : session;
346
- const unmoved = "request.operation == \"UPDATE\" && object.spec.podTemplate == oldObject.spec.podTemplate && " +
347
- "object.spec.?volumeClaimTemplates.orValue([]) == oldObject.spec.?volumeClaimTemplates.orValue([])";
348
- // A restore's pod is left paused, and ready only once resumed; no other pod is paused.
349
- const annotations = "object.spec.podTemplate.?metadata.?annotations.orValue({})";
350
- const restoring = `(${JSON.stringify(RESTORE_ANNOTATION)} in ${annotations})`;
351
- const gated = `object.spec.podTemplate.?spec.?readinessGates.orValue([]).exists(gate, gate.conditionType == ${JSON.stringify(RESUMED_CONDITION)})`;
352
- return admissionPolicy(name, [{ apiGroups: ["agents.x-k8s.io"], resources: ["sandboxes"], operations: ["CREATE", "UPDATE"] }], `request.userInfo.username == "${account}"`, [
353
- { 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)}`, [
354
516
  {
355
- expression: `${restoring} == (${JSON.stringify(RESTORE_PAUSED_ANNOTATION)} in ${annotations}) && (!${restoring} || ${gated})`,
356
- 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",
357
519
  },
358
- ], [...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
+ ]);
359
525
  }
360
526
  /**
361
527
  * The fields of a pod's status but its conditions that every Kubernetes alasio runs on
@@ -378,39 +544,31 @@ const POD_STATUS_FIELDS = [
378
544
  "startTime",
379
545
  ];
380
546
  /**
381
- * What the alasio of `account` may change of a pod's status, as it lets a restored session's
382
- * pod be ready once its node's agent resumed its sandbox (src/sandbox/snapshots.ts): the
383
- * RESUMED_CONDITION of a session's pod in `namespace` that is gated on it, set true, and
384
- * 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.
385
552
  */
386
553
  export function resumedConditionPolicy(name, account, namespace) {
387
554
  const resumed = JSON.stringify(RESUMED_CONDITION);
388
555
  const others = (object) => `${object}.status.?conditions.orValue([]).filter(condition, condition.type != ${resumed})`;
389
556
  return admissionPolicy(name, [{ apiGroups: [""], resources: ["pods/status"], operations: ["UPDATE"] }], `request.userInfo.username == "${account}"`, [
390
557
  {
391
- expression: `request.namespace == ${JSON.stringify(namespace)} && ${JSON.stringify(WORKLOAD_LABEL)} in object.metadata.?labels.orValue({}) && ` +
392
- `object.metadata.labels[${JSON.stringify(WORKLOAD_LABEL)}] == "session" && ` +
558
+ expression: `request.namespace == ${JSON.stringify(namespace)} && ${JSON.stringify(POOL_LABEL)} in object.metadata.?labels.orValue({}) && ` +
393
559
  `object.spec.?readinessGates.orValue([]).exists(gate, gate.conditionType == ${resumed})`,
394
- 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'",
395
561
  },
396
562
  {
397
563
  expression: `${others("object")} == ${others("oldObject")} && ` +
398
564
  `object.status.?conditions.orValue([]).all(condition, condition.type != ${resumed} || condition.status == "True")`,
399
- 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`,
400
566
  },
401
567
  {
402
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(" && ") +
403
569
  " && object.metadata.?labels.orValue({}) == oldObject.metadata.?labels.orValue({}) && object.metadata.?annotations.orValue({}) == oldObject.metadata.?annotations.orValue({})",
404
- 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",
405
571
  },
406
572
  ]);
407
573
  }
408
- /** 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. */
409
- export function secretPolicy() {
410
- return admissionPolicy(componentName("secrets"), [{ apiGroups: [""], resources: ["secrets"], operations: ["CREATE", "UPDATE"] }], BY_ANY_ALASIO, [{
411
- expression: `object.?type.orValue("Opaque") == "Opaque" && object.metadata.name.endsWith(${JSON.stringify(tokenSecretName(""))}) && ` +
412
- `object.?data.orValue({}).all(key, key == ${JSON.stringify(TOKEN_KEY)})`,
413
- message: "alasio makes no Secret but a Sandbox's token",
414
- }]);
415
- }
416
574
  //# sourceMappingURL=admission.js.map