alasio 8.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) 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/bound-harnesses.js +98 -0
  4. package/dist/cli/src/bound-harnesses.js.map +1 -0
  5. package/dist/cli/src/branches.js +79 -22
  6. package/dist/cli/src/branches.js.map +1 -1
  7. package/dist/cli/src/cluster/docker.js +40 -15
  8. package/dist/cli/src/cluster/docker.js.map +1 -1
  9. package/dist/cli/src/cluster/host.js +38 -8
  10. package/dist/cli/src/cluster/host.js.map +1 -1
  11. package/dist/cli/src/cluster/k3s.js +105 -14
  12. package/dist/cli/src/cluster/k3s.js.map +1 -1
  13. package/dist/cli/src/cluster/node.js +142 -33
  14. package/dist/cli/src/cluster/node.js.map +1 -1
  15. package/dist/cli/src/cluster/stargz.js +92 -0
  16. package/dist/cli/src/cluster/stargz.js.map +1 -0
  17. package/dist/cli/src/commands/branch.js +5 -4
  18. package/dist/cli/src/commands/branch.js.map +1 -1
  19. package/dist/cli/src/commands/common.js +11 -5
  20. package/dist/cli/src/commands/common.js.map +1 -1
  21. package/dist/cli/src/commands/folder.js +14 -0
  22. package/dist/cli/src/commands/folder.js.map +1 -0
  23. package/dist/cli/src/commands/lake.js +1 -1
  24. package/dist/cli/src/commands/lake.js.map +1 -1
  25. package/dist/cli/src/commands/login-relay.js +91 -0
  26. package/dist/cli/src/commands/login-relay.js.map +1 -0
  27. package/dist/cli/src/commands/login.js +11 -10
  28. package/dist/cli/src/commands/login.js.map +1 -1
  29. package/dist/cli/src/commands/logs.js +89 -6
  30. package/dist/cli/src/commands/logs.js.map +1 -1
  31. package/dist/cli/src/commands/restart.js +10 -4
  32. package/dist/cli/src/commands/restart.js.map +1 -1
  33. package/dist/cli/src/commands/restore-filer-store.js +27 -10
  34. package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
  35. package/dist/cli/src/commands/status.js +119 -20
  36. package/dist/cli/src/commands/status.js.map +1 -1
  37. package/dist/cli/src/commands/uninstall.js +8 -9
  38. package/dist/cli/src/commands/uninstall.js.map +1 -1
  39. package/dist/cli/src/commands/up.js +20 -9
  40. package/dist/cli/src/commands/up.js.map +1 -1
  41. package/dist/cli/src/commands/upgrade.js +22 -6
  42. package/dist/cli/src/commands/upgrade.js.map +1 -1
  43. package/dist/cli/src/commands.js +2 -1
  44. package/dist/cli/src/commands.js.map +1 -1
  45. package/dist/cli/src/config.js +4 -1
  46. package/dist/cli/src/config.js.map +1 -1
  47. package/dist/cli/src/cpu-baseline.js +8 -4
  48. package/dist/cli/src/cpu-baseline.js.map +1 -1
  49. package/dist/cli/src/folders.js +63 -0
  50. package/dist/cli/src/folders.js.map +1 -0
  51. package/dist/cli/src/gateway-calls.js +64 -0
  52. package/dist/cli/src/gateway-calls.js.map +1 -0
  53. package/dist/cli/src/install.js +45 -31
  54. package/dist/cli/src/install.js.map +1 -1
  55. package/dist/cli/src/kept-releases.js +27 -20
  56. package/dist/cli/src/kept-releases.js.map +1 -1
  57. package/dist/cli/src/kube/api.js +61 -15
  58. package/dist/cli/src/kube/api.js.map +1 -1
  59. package/dist/cli/src/kube/apply.js +122 -81
  60. package/dist/cli/src/kube/apply.js.map +1 -1
  61. package/dist/cli/src/kube/rollout.js +43 -21
  62. package/dist/cli/src/kube/rollout.js.map +1 -1
  63. package/dist/cli/src/main.js +2 -1
  64. package/dist/cli/src/main.js.map +1 -1
  65. package/dist/cli/src/manifests/admission.js +558 -222
  66. package/dist/cli/src/manifests/admission.js.map +1 -1
  67. package/dist/cli/src/manifests/alasio.js +180 -428
  68. package/dist/cli/src/manifests/alasio.js.map +1 -1
  69. package/dist/cli/src/manifests/branch.js +16 -27
  70. package/dist/cli/src/manifests/branch.js.map +1 -1
  71. package/dist/cli/src/manifests/codex-login.js +101 -0
  72. package/dist/cli/src/manifests/codex-login.js.map +1 -0
  73. package/dist/cli/src/manifests/collector.js +83 -62
  74. package/dist/cli/src/manifests/collector.js.map +1 -1
  75. package/dist/cli/src/manifests/common.js +82 -25
  76. package/dist/cli/src/manifests/common.js.map +1 -1
  77. package/dist/cli/src/manifests/config.js +137 -105
  78. package/dist/cli/src/manifests/config.js.map +1 -1
  79. package/dist/cli/src/manifests/disruption.js +18 -3
  80. package/dist/cli/src/manifests/disruption.js.map +1 -1
  81. package/dist/cli/src/manifests/folders.js +131 -0
  82. package/dist/cli/src/manifests/folders.js.map +1 -0
  83. package/dist/cli/src/manifests/grafana.js +4 -2
  84. package/dist/cli/src/manifests/grafana.js.map +1 -1
  85. package/dist/cli/src/manifests/harness-pools.js +236 -0
  86. package/dist/cli/src/manifests/harness-pools.js.map +1 -0
  87. package/dist/cli/src/manifests/image-holder.js +61 -0
  88. package/dist/cli/src/manifests/image-holder.js.map +1 -0
  89. package/dist/cli/src/manifests/index.js +14 -7
  90. package/dist/cli/src/manifests/index.js.map +1 -1
  91. package/dist/cli/src/manifests/keda.js +4 -2
  92. package/dist/cli/src/manifests/keda.js.map +1 -1
  93. package/dist/cli/src/manifests/lake.js +18 -20
  94. package/dist/cli/src/manifests/lake.js.map +1 -1
  95. package/dist/cli/src/manifests/log.js +84 -19
  96. package/dist/cli/src/manifests/log.js.map +1 -1
  97. package/dist/cli/src/manifests/neon.js +60 -80
  98. package/dist/cli/src/manifests/neon.js.map +1 -1
  99. package/dist/cli/src/manifests/network-policies.js +232 -94
  100. package/dist/cli/src/manifests/network-policies.js.map +1 -1
  101. package/dist/cli/src/manifests/node-agent.js +42 -15
  102. package/dist/cli/src/manifests/node-agent.js.map +1 -1
  103. package/dist/cli/src/manifests/object-store.js +99 -25
  104. package/dist/cli/src/manifests/object-store.js.map +1 -1
  105. package/dist/cli/src/manifests/sandbox-gateway.js +761 -0
  106. package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
  107. package/dist/cli/src/manifests/scalers.js +17 -5
  108. package/dist/cli/src/manifests/scalers.js.map +1 -1
  109. package/dist/cli/src/manifests/sizing.js +17 -9
  110. package/dist/cli/src/manifests/sizing.js.map +1 -1
  111. package/dist/cli/src/manifests/valkey.js +30 -10
  112. package/dist/cli/src/manifests/valkey.js.map +1 -1
  113. package/dist/cli/src/manifests/volume-lock.js +43 -0
  114. package/dist/cli/src/manifests/volume-lock.js.map +1 -0
  115. package/dist/cli/src/manifests/vpa.js +15 -2
  116. package/dist/cli/src/manifests/vpa.js.map +1 -1
  117. package/dist/cli/src/manifests/workspace-storage.js +182 -94
  118. package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
  119. package/dist/cli/src/neon-storage.js +54 -16
  120. package/dist/cli/src/neon-storage.js.map +1 -1
  121. package/dist/cli/src/release.js +9 -9
  122. package/dist/cli/src/release.js.map +1 -1
  123. package/dist/cli/src/setup-pod.js +44 -0
  124. package/dist/cli/src/setup-pod.js.map +1 -0
  125. package/dist/cli/src/target.js +2 -2
  126. package/dist/cli/src/target.js.map +1 -1
  127. package/dist/cli/src/telegram.js +13 -4
  128. package/dist/cli/src/telegram.js.map +1 -1
  129. package/dist/cli/src/wakes.js +68 -0
  130. package/dist/cli/src/wakes.js.map +1 -0
  131. package/dist/cli/src/webhook.js +5 -2
  132. package/dist/cli/src/webhook.js.map +1 -1
  133. package/dist/cluster/node/pins.json +11 -5
  134. package/dist/neon/control/branches.js +13 -6
  135. package/dist/neon/control/branches.js.map +1 -1
  136. package/dist/neon/control/endpoints.js +81 -10
  137. package/dist/neon/control/endpoints.js.map +1 -1
  138. package/dist/neon/control/secrets.js +24 -8
  139. package/dist/neon/control/secrets.js.map +1 -1
  140. package/dist/src/binder/bindings.js +195 -0
  141. package/dist/src/binder/bindings.js.map +1 -0
  142. package/dist/src/binder/names.js +63 -0
  143. package/dist/src/binder/names.js.map +1 -0
  144. package/dist/src/binder/tokens.js +71 -0
  145. package/dist/src/binder/tokens.js.map +1 -0
  146. package/dist/src/branch/names.js +20 -6
  147. package/dist/src/branch/names.js.map +1 -1
  148. package/dist/src/events/names.js +28 -0
  149. package/dist/src/events/names.js.map +1 -0
  150. package/dist/src/gateway/names.js +240 -0
  151. package/dist/src/gateway/names.js.map +1 -0
  152. package/dist/src/harness/names.js +67 -0
  153. package/dist/src/harness/names.js.map +1 -0
  154. package/dist/src/kube/client.js +212 -0
  155. package/dist/src/kube/client.js.map +1 -0
  156. package/dist/src/kube/sandbox-shape.js +14 -68
  157. package/dist/src/kube/sandbox-shape.js.map +1 -1
  158. package/dist/src/kube/started-map.js +12 -0
  159. package/dist/src/kube/started-map.js.map +1 -0
  160. package/dist/src/node-agent/names.js +62 -5
  161. package/dist/src/node-agent/names.js.map +1 -1
  162. package/dist/src/sandbox/names.js +12 -20
  163. package/dist/src/sandbox/names.js.map +1 -1
  164. package/dist/src/sandbox/otlp-resource.js +173 -0
  165. package/dist/src/sandbox/otlp-resource.js.map +1 -0
  166. package/dist/src/sandbox/telemetry.js +43 -0
  167. package/dist/src/sandbox/telemetry.js.map +1 -0
  168. package/dist/src/telegram/bot-commands.js +17 -0
  169. package/dist/src/telegram/bot-commands.js.map +1 -0
  170. package/dist/src/telegram/stop.js +12 -0
  171. package/dist/src/telegram/stop.js.map +1 -0
  172. package/dist/src/telemetry/config.js +131 -0
  173. package/dist/src/telemetry/config.js.map +1 -0
  174. package/dist/src/telemetry/forward.js +154 -0
  175. package/dist/src/telemetry/forward.js.map +1 -0
  176. package/package.json +1 -1
  177. package/dist/cli/src/manifests/agent-sandbox.js +0 -92
  178. package/dist/cli/src/manifests/agent-sandbox.js.map +0 -1
  179. package/dist/cli/src/manifests/juicefs-csi.js +0 -320
  180. package/dist/cli/src/manifests/juicefs-csi.js.map +0 -1
  181. package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +0 -6295
  182. package/dist/src/kube/config.js +0 -145
  183. package/dist/src/kube/config.js.map +0 -1
@@ -1,34 +1,38 @@
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;
11
+ * - a pod the sandbox gateway updates or deletes in an environment's own namespace is one of
12
+ * its harness pools', which it binds and releases, and a Lease it writes there one of their
13
+ * bindings': RBAC lets it update and delete every pod there, and write every Lease;
16
14
  * - a pod alasio patches changes its busy label and annotations alone (src/kube/busy.ts),
17
15
  * and in alasio's own namespace is alasio's own: RBAC lets it patch its namespaces'
18
16
  * pods whole, their images among it;
19
17
  * - a compute's Deployment neon-control patches changes its pod template's restartedAt
20
18
  * annotation alone, which restarts it (neon/control/computes.ts): RBAC lets it patch the
21
- * Deployment whole, its image and command among it.
19
+ * Deployment whole, its image and command among it;
20
+ * - a pod neon-control patches is a compute's, and changes its serving label alone, which
21
+ * takes it off its Service as a sleep stops it: RBAC lets it patch any pod of a
22
+ * compute's namespace whole.
22
23
  *
23
24
  * Where a policy holds an object to a rendered one, `pinned` makes the CEL that says so:
24
25
  * the object's fields are the rendered ones', key for key and item for item, but for what
25
26
  * a Check allows, and what the API server defaults where the rendered one has nothing.
26
27
  */
27
28
  import { posix } from "node:path";
28
- import { branchNamespace } from "../../../src/branch/names.js";
29
+ import { SERVING_LABEL } from "../../../neon/control/endpoints.js";
30
+ import { BINDING_LABEL, FREE_LABEL, KEY_ANNOTATION, POOL_LABEL } from "../../../src/binder/names.js";
31
+ import { branchNamespace, branchSessionsNamespace } from "../../../src/branch/names.js";
32
+ import { ATTACH_VOLUME, FOLDER_KEY_PREFIX, FOLDER_POOL, FOLDER_POD, folderPodSpec, folderTokenSecret, ownDirectory, SANDBOX_POD_POLICY } from "../../../src/gateway/names.js";
29
33
  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";
34
+ import { RESTORE_ANNOTATION, RESTORE_PAUSED_ANNOTATION, RESUMED_CONDITION, SNAPSHOT_CACHE_DIR, SNAPSHOT_ID } from "../../../src/kube/sandbox-shape.js";
35
+ import { ATTACH_ROOT, RESTORE_POD } from "../../../src/node-agent/names.js";
32
36
  import { componentName, labels, NAMESPACE, neonName, RELEASE } from "./common.js";
33
37
  /** 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
38
  class Check {
@@ -41,23 +45,16 @@ class Check {
41
45
  const ANY = new Check(() => "true");
42
46
  /** Any string. */
43
47
  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
48
  /** A string all of which `pattern` matches. */
47
49
  const matching = (pattern) => new Check((expression) => `type(${expression}) == string && ${expression}.matches(${JSON.stringify(`^(${pattern})$`)})`);
48
50
  /** What the CEL `value` is, of the object asked about. */
49
51
  const sameAs = (value) => new Check((expression) => `${expression} == ${value}`);
52
+ /** A quantity, as the API server writes one, of the amount `value` is. */
53
+ const quantityOf = (value) => new Check((expression) => `isQuantity(${expression}) && quantity(${expression}).compareTo(quantity(${JSON.stringify(value)})) == 0`);
54
+ /** A quantity, as the API server writes one, of at least `least`'s amount, and at most `most`'s where given. */
55
+ const quantityWithin = (least, most) => new Check((expression) => [`isQuantity(${expression})`, `quantity(${expression}).compareTo(quantity(${JSON.stringify(least)})) >= 0`, ...(most ? [`quantity(${expression}).compareTo(quantity(${JSON.stringify(most)})) <= 0`] : [])].join(" && "));
50
56
  /** Any of `alternatives`. */
51
57
  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
58
  /** Of a map's key: that it may be absent; where present, `value`. */
62
59
  class Optional extends Check {
63
60
  value;
@@ -105,12 +102,10 @@ function pinnedMap(expression, expected, defaults, depth) {
105
102
  ...extra.map((key) => `(!(${JSON.stringify(key)} in ${expression}) || ${pinned(at(key), defaults[key], depth)})`),
106
103
  ].join(" && ");
107
104
  }
108
- /** The name the ServiceAccount of the alasio of the namespace `namespace` asks as. */
109
- export const alasioAccount = (namespace) => `system:serviceaccount:${namespace}:${RELEASE}`;
110
105
  /** Whether the request is by main's alasio or a branch environment's, as CEL. */
111
106
  const BY_ANY_ALASIO = `request.userInfo.username.matches('^system:serviceaccount:(${NAMESPACE}|${branchNamespace("")}[a-z0-9-]+):${RELEASE}$')`;
112
107
  /** A ValidatingAdmissionPolicy `name` of alasio's, on `rules`, of what `by` asks, refusing what `validations` do not hold for, and its binding. */
113
- function admissionPolicy(name, rules, by, validations, variables) {
108
+ function admissionPolicy(name, rules, by, validations, variables, namespaceSelector) {
114
109
  const metadata = { name, labels: labels("alasio") };
115
110
  const policy = {
116
111
  apiVersion: "admissionregistration.k8s.io/v1",
@@ -118,7 +113,7 @@ function admissionPolicy(name, rules, by, validations, variables) {
118
113
  metadata,
119
114
  spec: {
120
115
  failurePolicy: "Fail",
121
- matchConstraints: { resourceRules: rules.map((rule) => ({ ...rule, apiVersions: ["*"] })) },
116
+ matchConstraints: { resourceRules: rules.map((rule) => ({ ...rule, apiVersions: ["*"] })), ...(namespaceSelector ? { namespaceSelector } : {}) },
122
117
  matchConditions: [{ name: "by-alasio", expression: by }],
123
118
  ...(variables ? { variables } : {}),
124
119
  validations,
@@ -193,208 +188,557 @@ export function computeRestartPolicy() {
193
188
  { name: "newAnnotations", expression: "object.spec.template.metadata.?annotations.orValue({})" },
194
189
  ]);
195
190
  }
191
+ /** The components of the computes' pods: a primary's, and a read replica's. */
192
+ const COMPUTE_COMPONENTS = ["neon-compute", "neon-compute-ro"];
196
193
  /**
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.
194
+ * What neon-control may change of a pod, which RBAC lets it patch whole, of any in a
195
+ * compute's namespace (./neon.ts's computeScaling): of a compute's pod, its serving label
196
+ * (neon/control/endpoints.ts's SERVING_LABEL), which takes it off its Service as a sleep
197
+ * stops it, or gives it back, and nothing else.
219
198
  */
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),
199
+ export function computeServingPolicy() {
200
+ const component = "app.kubernetes.io/component";
201
+ const account = `system:serviceaccount:${NAMESPACE}:${neonName("control")}`;
202
+ return admissionPolicy(componentName("compute-serving"), [{ apiGroups: [""], resources: ["pods"], operations: ["UPDATE"] }], `request.userInfo.username == "${account}"`, [
203
+ { expression: "object.spec == oldObject.spec", message: "neon-control changes no pod's spec" },
204
+ {
205
+ expression: `"${component}" in variables.oldLabels && variables.oldLabels["${component}"] in ${JSON.stringify(COMPUTE_COMPONENTS)}`,
206
+ message: "neon-control labels no pod but a compute's",
231
207
  },
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
- }]);
208
+ {
209
+ expression: "object.metadata.?annotations.orValue({}) == oldObject.metadata.?annotations.orValue({}) && " +
210
+ "object.metadata.?finalizers.orValue([]) == oldObject.metadata.?finalizers.orValue([]) && " +
211
+ "object.metadata.?ownerReferences.orValue([]) == oldObject.metadata.?ownerReferences.orValue([])",
212
+ message: "neon-control changes no pod's annotations, finalizers or owners",
213
+ },
214
+ { expression: unchanged("Labels", [SERVING_LABEL]), message: `neon-control changes no label of a pod's but ${SERVING_LABEL}` },
215
+ {
216
+ expression: `!("${SERVING_LABEL}" in variables.newLabels) || variables.newLabels["${SERVING_LABEL}"] == "true"`,
217
+ message: `neon-control sets ${SERVING_LABEL} to true alone, or takes it off`,
218
+ },
219
+ ], [
220
+ { name: "oldLabels", expression: "oldObject.metadata.?labels.orValue({})" },
221
+ { name: "newLabels", expression: "object.metadata.?labels.orValue({})" },
222
+ ]);
237
223
  }
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
224
  /** A path segment as sanitizePathToken makes one. */
246
225
  const PATH_TOKEN = "[A-Za-z0-9_][A-Za-z0-9_.-]*";
247
226
  /** `text`, as a pattern that matches it alone. */
248
227
  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
228
  /** A node's name: a DNS subdomain. */
252
229
  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}";
230
+ /** Any number. */
231
+ const ANY_NUMBER = new Check((expression) => `type(${expression}) == int || type(${expression}) == double`);
232
+ /** 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). */
233
+ const DEFAULT_TOLERATIONS = ["node.kubernetes.io/not-ready", "node.kubernetes.io/unreachable"].map((key) => ({ key, operator: "Exists", effect: "NoExecute", tolerationSeconds: 300 }));
234
+ /**
235
+ * What the API server gives a sandbox pool's pod, of its template, where the template has
236
+ * not: its own defaults, its priority from its class, its ServiceAccount the namespace's
237
+ * default, which it mounts no token of; of each container, where its termination message
238
+ * goes, and its resources as VPA sizes them; of a probe, its timeout and thresholds; of a
239
+ * port, its protocol; of a field reference, its API version.
240
+ */
241
+ const POOL_POD_DEFAULTS = {
242
+ pod: {
243
+ restartPolicy: "Always",
244
+ terminationGracePeriodSeconds: 30,
245
+ schedulerName: "default-scheduler",
246
+ serviceAccountName: "default",
247
+ serviceAccount: "default",
248
+ priority: ANY_NUMBER,
249
+ preemptionPolicy: oneOf("PreemptLowerPriority", "Never"),
250
+ },
251
+ container: { terminationMessagePath: "/dev/termination-log", terminationMessagePolicy: "File", resources: ANY },
252
+ probe: { timeoutSeconds: 1, successThreshold: 1, failureThreshold: 3 },
253
+ };
254
+ /**
255
+ * `container` of a pool's template, as the API server stores it in a pod (POOL_POD_DEFAULTS):
256
+ * its resources any, as VPA sizes a pool's pod as it is made, and resizes it after
257
+ * (pods/resize).
258
+ */
259
+ const poolContainer = (container) => templateContainer(container, ANY);
260
+ /** `container` of a template, as the API server stores it in a pod (POOL_POD_DEFAULTS), its resources as `resources` says. */
261
+ function templateContainer(container, resources) {
262
+ const each = container;
263
+ const probe = each["readinessProbe"];
264
+ return defaulted({
265
+ ...each,
266
+ resources,
267
+ ...(container.ports ? { ports: container.ports.map((port) => defaulted(port, { protocol: "TCP" })) } : {}),
268
+ ...(container.env
269
+ ? {
270
+ env: container.env.map((variable) => variable.valueFrom?.fieldRef
271
+ ? { ...variable, valueFrom: { fieldRef: defaulted(variable.valueFrom.fieldRef, { apiVersion: "v1" }) } }
272
+ : variable),
273
+ }
274
+ : {}),
275
+ ...(probe ? { readinessProbe: defaulted(probe, POOL_POD_DEFAULTS.probe) } : {}),
276
+ }, POOL_POD_DEFAULTS.container);
277
+ }
278
+ /** 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. */
279
+ function poolPodSpec(template) {
280
+ const spec = template.spec ?? { containers: [] };
281
+ const tolerations = (spec.tolerations ?? []);
282
+ return defaulted({
283
+ ...spec,
284
+ containers: spec.containers.map(poolContainer),
285
+ ...(spec.initContainers ? { initContainers: spec.initContainers.map(poolContainer) } : {}),
286
+ // The API server's of a node not ready and unreachable, in either order.
287
+ tolerations: oneOf([...tolerations, ...DEFAULT_TOLERATIONS], [...tolerations, ...[...DEFAULT_TOLERATIONS].reverse()]),
288
+ }, POOL_POD_DEFAULTS.pod);
289
+ }
255
290
  /**
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.
291
+ * The image a restore's pod of main's sessions, `namespace`'s, is restored from, as the
292
+ * gateway's node agent prepared it: of the workspace its binding's key names, in the node's
293
+ * snapshot cache, under a snapshot's id.
259
294
  */
260
- const restoredFrom = (namespace) => new Check((expression) => {
261
- const own = `${JSON.stringify(`${SNAPSHOT_CACHE_DIR}/${namespace}/`)} + object.metadata.name + "/"`;
295
+ const restoredFromKey = (namespace) => new Check((expression) => {
296
+ const own = `${JSON.stringify(`${SNAPSHOT_CACHE_DIR}/${namespace}/`)} + variables.annotations[${JSON.stringify(KEY_ANNOTATION)}].substring(${"sessionfs:".length}) + "/"`;
262
297
  return `type(${expression}) == string && ${expression}.startsWith(${own}) && ${expression}.substring(size(${own})).matches(${JSON.stringify(`^${SNAPSHOT_ID}$`)})`;
263
298
  });
299
+ /** 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. */
300
+ 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
301
  /**
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.
302
+ * A restore's pod of the pool template `template`, as the gateway makes it (src/gateway/snapshots.ts)
303
+ * and the API server stores it: its template's, bound from the first, never free, its sandbox
304
+ * restored paused from the image its node's agent prepared, under one of `runtimeClasses`, on that
305
+ * node, its own directory there its volume's, ready once resumed, and never started again.
274
306
  */
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;
307
+ function restorePod(template, namespace, runtimeClasses) {
308
+ // Its own directory named by its volume's path, its own name's (ownDirectory).
309
+ const owned = ownDirectory(template.spec ?? { containers: [] }, "", "");
310
+ const volumes = (owned.volumes ?? []).map((volume) => (volume.name === ATTACH_VOLUME ? { name: ATTACH_VOLUME, hostPath: { path: OWN_DIRECTORY, type: "Directory" } } : volume));
311
+ const restoredOn = {
312
+ nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchFields: [{ key: "metadata.name", operator: "In", values: [matching(NODE_NAME)] }] }] } },
288
313
  };
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
- }]
314
+ // Its template's spec, as the API server stores a pod of it, but what a restore gives it.
315
+ const restoring = poolPodSpec({
316
+ ...template,
317
+ spec: {
318
+ ...owned,
319
+ volumes,
320
+ restartPolicy: "Never",
321
+ runtimeClassName: oneOf(...runtimeClasses),
322
+ readinessGates: [...(template.spec?.readinessGates ?? []), { conditionType: RESUMED_CONDITION }],
323
+ affinity: restoredOn,
324
+ },
325
+ });
326
+ const { [FREE_LABEL]: _free, ...own } = (template.metadata?.labels ?? {});
327
+ const labels = { ...own, [BINDING_LABEL]: matching("[0-9a-f]{40}") };
328
+ const annotations = {
329
+ ...(template.metadata?.annotations ?? {}),
330
+ [KEY_ANNOTATION]: matching(`sessionfs:${DNS_LABEL}`),
331
+ ...BUSY_ANNOTATIONS,
332
+ [RESTORE_ANNOTATION]: restoredFromKey(namespace),
333
+ [RESTORE_PAUSED_ANNOTATION]: "true",
334
+ vpaObservedContainers: optional(ANY_STRING),
335
+ vpaUpdates: optional(ANY_STRING),
336
+ };
337
+ return `(${pinned("object.spec", restoring)} && ${pinned("variables.labels", labels)} && ${pinned("variables.annotations", annotations)})`;
338
+ }
339
+ /**
340
+ * Every pod of a namespace of sessions, main's or a branch environment's, made or changed by
341
+ * anyone, held to what the pools' templates, `templates`, make, as Pod Security's restricted
342
+ * level, which the namespaces keep warning and auditing of, cannot: a pool's pod mounts its
343
+ * own directory on its node, a hostPath no level but privileged allows, which the namespaces
344
+ * enforce, so this policy is their boundary.
345
+ *
346
+ * - A pod made is a pod of one of `templates`, as the API server stores it (poolPodSpec), its
347
+ * template's labels and annotations, its ReplicaSet's template hash beside them, and what
348
+ * VPA notes; or a restore's of main's sessions, of a template a snapshot restores onto
349
+ * (restorePod), made by the gateway alone; and no other pod is made there.
350
+ * - A pod changed keeps its spec whole (its resources change by pods/resize, which this does
351
+ * not match), and of its labels and annotations changes those a binding has alone (its free
352
+ * label taken off, its binding's put on) and those that keep its node, and of its owners lets
353
+ * go alone.
354
+ * - No pod gets an ephemeral container.
355
+ *
356
+ * It fails closed: where it cannot say, the API server refuses.
357
+ */
358
+ export function sandboxPodPolicy(templates, { sessionsNamespace, restores = null }) {
359
+ const name = SANDBOX_POD_POLICY;
360
+ const metadata = { name, labels: labels("session") };
361
+ // 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.
362
+ const made = templates.map((template, index) => {
363
+ const labels = { ...(template.metadata?.labels ?? {}), "pod-template-hash": ANY_STRING };
364
+ const annotations = { ...(template.metadata?.annotations ?? {}), vpaObservedContainers: optional(ANY_STRING), vpaUpdates: optional(ANY_STRING) };
365
+ return { name: `made${index}`, expression: `${pinned("object.spec", poolPodSpec(template))} && ${pinned("variables.labels", labels)} && ${pinned("variables.annotations", annotations)}` };
366
+ });
367
+ const restoring = (restores?.templates ?? []).map((template, index) => ({ name: `restored${index}`, expression: restorePod(template, sessionsNamespace, restores?.runtimeClasses ?? []) }));
368
+ const restored = restores
369
+ ? [`(request.namespace == ${JSON.stringify(sessionsNamespace)} && request.userInfo.username == ${JSON.stringify(restores.restorer)} && (${restoring.map(({ name }) => `variables.${name}`).join(" || ")}))`]
303
370
  : [];
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) }),
371
+ const bound = [BINDING_LABEL, FREE_LABEL];
372
+ const policy = {
373
+ apiVersion: "admissionregistration.k8s.io/v1",
374
+ kind: "ValidatingAdmissionPolicy",
375
+ metadata,
376
+ spec: {
377
+ failurePolicy: "Fail",
378
+ matchConstraints: {
379
+ resourceRules: [
380
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods"], operations: ["CREATE", "UPDATE"] },
381
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods/ephemeralcontainers"], operations: ["CREATE", "UPDATE"] },
382
+ ],
383
+ },
384
+ matchConditions: [{
385
+ name: "of-sessions",
386
+ expression: `request.namespace == ${JSON.stringify(sessionsNamespace)} || request.namespace.matches(${JSON.stringify(`^${literally(branchSessionsNamespace("\u0000")).replace("\u0000", DNS_LABEL)}$`)})`,
387
+ }],
388
+ variables: [
389
+ { name: "labels", expression: "object.metadata.?labels.orValue({})" },
390
+ { name: "annotations", expression: "object.metadata.?annotations.orValue({})" },
391
+ { name: "oldLabels", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?labels.orValue({}) : {}" },
392
+ { name: "newLabels", expression: "object.metadata.?labels.orValue({})" },
393
+ { name: "oldAnnotations", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?annotations.orValue({}) : {}" },
394
+ { name: "newAnnotations", expression: "object.metadata.?annotations.orValue({})" },
395
+ { name: "pooled", expression: `${JSON.stringify(POOL_LABEL)} in variables.labels || ${JSON.stringify(POOL_LABEL)} in variables.oldLabels` },
396
+ ...made,
397
+ ...restoring,
398
+ ],
399
+ validations: [
400
+ { expression: "request.?subResource.orValue('') != 'ephemeralcontainers'", message: "no pod of a namespace of sessions gets an ephemeral container" },
401
+ {
402
+ expression: `request.operation != 'CREATE' || !variables.pooled || ${[...made.map(({ name }) => `variables.${name}`), ...restored].join(" || ")}`,
403
+ 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",
404
+ },
405
+ { expression: "request.operation != 'CREATE' || variables.pooled", message: "no pod is made in a namespace of sessions but a sandbox pool's" },
406
+ { expression: "request.operation != 'UPDATE' || object.spec == oldObject.spec", message: "no pod's spec changes in a namespace of sessions" },
407
+ {
408
+ expression: `request.operation != 'UPDATE' || !variables.pooled || (${unchanged("Labels", bound)})`,
409
+ message: `no label of a sandbox pool's pod changes but ${bound.join(" and ")}, as it is bound`,
410
+ },
411
+ {
412
+ expression: `request.operation != 'UPDATE' || !variables.pooled || (${unchanged("Annotations", [KEY_ANNOTATION, ...Object.keys(BUSY_ANNOTATIONS)])})`,
413
+ message: `no annotation of a sandbox pool's pod changes but ${KEY_ANNOTATION} and what keeps its node, as it is bound`,
414
+ },
415
+ {
416
+ expression: "request.operation != 'UPDATE' || object.metadata.?ownerReferences.orValue([]).all(owner, owner in oldObject.metadata.?ownerReferences.orValue([]))",
417
+ message: "a pod of a namespace of sessions gets no owner it had not",
418
+ },
419
+ ],
420
+ },
310
421
  };
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)] }] }] },
422
+ const binding = { apiVersion: "admissionregistration.k8s.io/v1", kind: "ValidatingAdmissionPolicyBinding", metadata, spec: { policyName: name, validationActions: ["Deny"] } };
423
+ return [policy, binding];
424
+ }
425
+ /**
426
+ * What the sandbox gateway, `gateway`, its ServiceAccount, may do to a pod or a Lease of an
427
+ * environment's own namespace, main's or a branch's, where RBAC lets it update and delete pods
428
+ * and write Leases to bind and release its harness pools' (./harness-pools.ts): a pod of one
429
+ * of `pools` alone, which it makes none of, of no other workload beside it; and a Lease of a
430
+ * binding of one of them alone, by its pool's label, as it was and as it is to be, of none of
431
+ * what else keeps Leases there (KEDA's, VPA's, their leaders'). How a pool's pod may change, by
432
+ * anyone, its pool's own policy says (harnessPoolPodPolicy).
433
+ */
434
+ export function harnessPodPolicy(gateway, pools) {
435
+ // An environment's own namespace, main's or a branch's: not a branch's sessions' namespace,
436
+ // whose name may be of the same form, whose sandbox pods the gateway binds, as its labels say.
437
+ const environments = `request.namespace == ${JSON.stringify(NAMESPACE)} || request.namespace.startsWith(${JSON.stringify(branchNamespace(""))})`;
438
+ const ofNoSessions = { matchExpressions: [{ key: "app.kubernetes.io/component", operator: "NotIn", values: ["session"] }] };
439
+ const ofPools = (labels) => `${JSON.stringify(POOL_LABEL)} in ${labels} && ${labels}[${JSON.stringify(POOL_LABEL)}] in ${JSON.stringify(pools)}`;
440
+ return admissionPolicy(componentName("harness-pods"), [
441
+ { apiGroups: [""], resources: ["pods"], operations: ["UPDATE", "DELETE"] },
442
+ { apiGroups: ["coordination.k8s.io"], resources: ["leases"], operations: ["CREATE", "UPDATE", "DELETE"] },
443
+ ], `request.userInfo.username == ${JSON.stringify(gateway)} && (${environments})`, [
444
+ {
445
+ expression: `request.resource.resource != 'pods' || (${ofPools("variables.oldLabels")})`,
446
+ message: "the sandbox gateway changes and deletes no pod of an environment's namespace but its harness pools'",
329
447
  },
448
+ {
449
+ expression: `request.resource.resource != 'leases' || ((request.operation == 'CREATE' || (${ofPools("variables.oldLabels")})) && (request.operation == 'DELETE' || (${ofPools("variables.newLabels")})))`,
450
+ message: "the sandbox gateway writes no Lease of an environment's namespace but its harness pools' bindings'",
451
+ },
452
+ ], [
453
+ { name: "oldLabels", expression: "request.operation == 'CREATE' ? {} : oldObject.metadata.?labels.orValue({})" },
454
+ { name: "newLabels", expression: "request.operation == 'DELETE' ? {} : object.metadata.?labels.orValue({})" },
455
+ ], ofNoSessions);
456
+ }
457
+ /**
458
+ * The spec of a pod of the harness pool template `template`, as the API server stores it: as a
459
+ * sandbox pool's pod's (poolPodSpec), and what the API server gives a pod that a harness
460
+ * pool's template leaves out: its DNS policy, the mode of each Secret's volume, each of the
461
+ * machine's paths' type where its mount names none, and the scheme of its readiness probe's
462
+ * request.
463
+ */
464
+ function harnessPodSpec(template) {
465
+ const spec = template.spec ?? { containers: [] };
466
+ const container = (each) => {
467
+ const probe = each.readinessProbe;
468
+ const httpGet = probe?.httpGet;
469
+ return poolContainer({ ...each, ...(probe && httpGet ? { readinessProbe: { ...probe, httpGet: defaulted(httpGet, { scheme: "HTTP" }) } } : {}) });
330
470
  };
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 = {
471
+ const volumes = (spec.volumes ?? []).map((volume) => volume.secret
472
+ ? { ...volume, secret: defaulted(volume.secret, { defaultMode: 420 }) }
473
+ : volume.hostPath && volume.hostPath.type === undefined
474
+ ? { ...volume, hostPath: defaulted(volume.hostPath, { type: "" }) }
475
+ : volume);
476
+ const tolerations = (spec.tolerations ?? []);
477
+ return defaulted({
341
478
  ...spec,
342
479
  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,
480
+ ...(spec.volumes ? { volumes } : {}),
481
+ tolerations: oneOf([...tolerations, ...DEFAULT_TOLERATIONS], [...tolerations, ...[...DEFAULT_TOLERATIONS].reverse()]),
482
+ }, { ...POOL_POD_DEFAULTS.pod, dnsPolicy: "ClusterFirst" });
483
+ }
484
+ /** `templates` by what each is but for its pool's label, in their order. */
485
+ function kindsOf(templates) {
486
+ const kinds = new Map();
487
+ for (const template of templates) {
488
+ const { [POOL_LABEL]: pool = "", ...others } = template.metadata?.labels ?? {};
489
+ const key = JSON.stringify({ ...template, metadata: { ...template.metadata, labels: others } });
490
+ const kind = kinds.get(key) ?? { template, pools: [] };
491
+ kind.pools.push(pool);
492
+ kinds.set(key, kind);
493
+ }
494
+ return [...kinds.values()];
495
+ }
496
+ /** The name of the policy that holds the harness pools' pods of main's environment (`branch` null) or of the branch environment `branch` to their templates. */
497
+ export const harnessPoolPodPolicyName = (branch) => (branch === null ? componentName("harness-pool-pods") : `${componentName("harness-pool-pods")}-${branch}`);
498
+ /**
499
+ * Every pod of a harness pool (./harness-pools.ts) made in the environment's own namespace
500
+ * `namespace`, held to its pool's template, `templates` as `alasio up` renders them for main
501
+ * (`branch` null) or `alasio branch create` for the branch `branch`, of its own
502
+ * configuration, its overrides among it: main's folder conversations' among them, which run
503
+ * as the operator with the machine's mounts, so none is made of another user, other mounts, or
504
+ * capabilities beyond the template's. Each environment's policy is its own, made and deleted
505
+ * with it, as main's alasio up cannot render a branch's templates, whose configuration is the
506
+ * branch's. A pod is made by its Deployment's ReplicaSet, of its template, as the API server
507
+ * stores it (harnessPodSpec), free, of its pool's labels and the template hash alone; a pod
508
+ * labelled of no harness pool is no harness pool's, and is left to what else holds the
509
+ * namespace. A pool's pod changed by anyone, as it was or as it is to be, keeps its spec whole
510
+ * (its resources change by pods/resize, as VPA resizes it, which this does not match), of its
511
+ * labels and annotations changes those a binding has alone and those that keep its node, and
512
+ * of its owners lets go alone; and none gets an ephemeral container, as a sandbox pool's pod
513
+ * gets none (sandboxPodPolicy).
514
+ *
515
+ * It fails closed: where it cannot say, the API server refuses.
516
+ */
517
+ export function harnessPoolPodPolicy({ namespace, branch, templates }) {
518
+ const name = harnessPoolPodPolicyName(branch);
519
+ const metadata = { name, labels: labels("harness") };
520
+ const pinOf = ({ template, pools }) => {
521
+ const podLabels = { ...(template.metadata?.labels ?? {}), [POOL_LABEL]: oneOf(...pools), "pod-template-hash": ANY_STRING };
522
+ const annotations = { ...(template.metadata?.annotations ?? {}), vpaObservedContainers: optional(ANY_STRING), vpaUpdates: optional(ANY_STRING) };
523
+ return `${pinned("object.spec", harnessPodSpec(template))} && ${pinned("variables.labels", podLabels)} && ${pinned("variables.annotations", annotations)}`;
350
524
  };
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" });
525
+ // Each kind of template's pin a variable of its own, as CEL holds each expression to 100,000
526
+ // code points; the templates of pools that differ in their pool's label alone pinned once, so
527
+ // the policy stays one the API server compiles in time.
528
+ const pins = kindsOf(templates).map((kind, index) => ({ name: `pin${index}`, expression: pinOf(kind) }));
529
+ const pools = [...new Set(templates.map((template) => template.metadata?.labels?.[POOL_LABEL] ?? ""))];
530
+ const ofPools = (pod) => `has(${pod}.metadata.labels) && ${JSON.stringify(POOL_LABEL)} in ${pod}.metadata.labels && ${pod}.metadata.labels[${JSON.stringify(POOL_LABEL)}] in ${JSON.stringify(pools)}`;
531
+ const bound = [BINDING_LABEL, FREE_LABEL];
532
+ const policy = {
533
+ apiVersion: "admissionregistration.k8s.io/v1",
534
+ kind: "ValidatingAdmissionPolicy",
535
+ metadata,
536
+ spec: {
537
+ failurePolicy: "Fail",
538
+ matchConstraints: {
539
+ resourceRules: [
540
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods"], operations: ["CREATE", "UPDATE"] },
541
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods/ephemeralcontainers"], operations: ["CREATE", "UPDATE"] },
542
+ ],
543
+ },
544
+ matchConditions: [
545
+ { name: "of-the-environment", expression: `request.namespace == ${JSON.stringify(namespace)}` },
546
+ { name: "of-harness-pools", expression: `(${ofPools("object")}) || (request.operation == 'UPDATE' && ${ofPools("oldObject")})` },
547
+ ],
548
+ variables: [
549
+ { name: "labels", expression: "object.metadata.?labels.orValue({})" },
550
+ { name: "annotations", expression: "object.metadata.?annotations.orValue({})" },
551
+ { name: "oldLabels", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?labels.orValue({}) : {}" },
552
+ { name: "newLabels", expression: "object.metadata.?labels.orValue({})" },
553
+ { name: "oldAnnotations", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?annotations.orValue({}) : {}" },
554
+ { name: "newAnnotations", expression: "object.metadata.?annotations.orValue({})" },
555
+ ...pins,
556
+ ],
557
+ validations: [
558
+ { expression: "request.?subResource.orValue('') != 'ephemeralcontainers'", message: "no pod of a harness pool gets an ephemeral container" },
559
+ {
560
+ expression: `request.operation != 'CREATE' || ${pins.map(({ name: each }) => `variables.${each}`).join(" || ") || "false"}`,
561
+ message: "a pod of a harness pool is its pool's template, as alasio up renders it, and nothing else",
562
+ },
563
+ { expression: "request.operation != 'UPDATE' || object.spec == oldObject.spec", message: "no harness pool's pod's spec changes" },
564
+ { expression: `request.operation != 'UPDATE' || (${unchanged("Labels", bound)})`, message: `no label of a harness pool's pod changes but ${bound.join(" and ")}, as it is bound` },
565
+ {
566
+ expression: `request.operation != 'UPDATE' || (${unchanged("Annotations", [KEY_ANNOTATION, ...Object.keys(BUSY_ANNOTATIONS)])})`,
567
+ message: `no annotation of a harness pool's pod changes but ${KEY_ANNOTATION} and what keeps its node, as it is bound`,
568
+ },
569
+ {
570
+ expression: "request.operation != 'UPDATE' || object.metadata.?ownerReferences.orValue([]).all(owner, owner in oldObject.metadata.?ownerReferences.orValue([]))",
571
+ message: "a harness pool's pod gets no owner it had not",
572
+ },
573
+ ],
574
+ },
575
+ };
576
+ const binding = { apiVersion: "admissionregistration.k8s.io/v1", kind: "ValidatingAdmissionPolicyBinding", metadata, spec: { policyName: name, validationActions: ["Deny"] } };
577
+ return [policy, binding];
362
578
  }
363
- /** The name of the Sandbox policy of the alasio of the namespace `namespace`. */
364
- export const sandboxPolicyName = (namespace) => `${namespace}-sandboxes`;
579
+ /** The folder pods' policy, which holds every pod of the folder namespace to the folder template (folderPodPolicy). */
580
+ export const FOLDER_POD_POLICY = "alasio-folder-pods";
581
+ /** 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. */
582
+ const POD_NAME = "\u0000pod";
583
+ const STATE_DIR = "\u0000state-dir";
365
584
  /**
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.
585
+ * The spec `spec` of a folder instance's pod, as the API server stores it: as a pool's pod's
586
+ * (poolPodSpec), and what the API server gives a pod that the folder template, unlike the
587
+ * pools', leaves out: its DNS policy, its Service links, and each of the machine's paths'
588
+ * type where its mount names none.
373
589
  */
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" },
590
+ function folderPodPinned(spec) {
591
+ const { map, defaults } = folderPodParts(spec);
592
+ return defaulted(map, defaults);
593
+ }
594
+ /**
595
+ * A folder instance's container's `resources`, its template's: its limits theirs, its CPU
596
+ * request its, and its memory request, where it has one, the gateway's, as it sizes each pod
597
+ * of its folder's instances' use (src/gateway/folders.ts SIZING), between its template's and
598
+ * its limit.
599
+ */
600
+ function folderResources(resources) {
601
+ const quantities = (of) => Object.fromEntries(Object.entries(of ?? {}).map(([name, value]) => [name, quantityOf(value)]));
602
+ const memory = resources?.requests?.["memory"];
603
+ return {
604
+ ...(resources?.limits ? { limits: quantities(resources.limits) } : {}),
605
+ ...(resources?.requests ? { requests: { ...quantities(resources.requests), ...(memory ? { memory: quantityWithin(memory, resources.limits?.["memory"]) } : {}) } } : {}),
606
+ };
607
+ }
608
+ /** folderPodPinned's map and the defaults it is pinned with. */
609
+ export function folderPodParts(spec) {
610
+ const tolerations = (spec.tolerations ?? []);
611
+ const volumes = (spec.volumes ?? []).map((volume) => volume.hostPath && volume.hostPath.type === undefined ? { ...volume, hostPath: defaulted(volume.hostPath, { type: "" }) } : volume);
612
+ return {
613
+ map: {
614
+ ...spec,
615
+ containers: spec.containers.map((container) => templateContainer(container, folderResources(container.resources))),
616
+ ...(spec.volumes ? { volumes } : {}),
617
+ tolerations: oneOf([...tolerations, ...DEFAULT_TOLERATIONS], [...tolerations, ...[...DEFAULT_TOLERATIONS].reverse()]),
618
+ },
619
+ defaults: {
620
+ ...POOL_POD_DEFAULTS.pod,
621
+ // The account the template names, as the API server copies it to the field it deprecated.
622
+ ...(spec.serviceAccountName ? { serviceAccountName: spec.serviceAccountName, serviceAccount: spec.serviceAccountName } : {}),
623
+ dnsPolicy: "ClusterFirst",
624
+ enableServiceLinks: true,
625
+ },
626
+ };
627
+ }
628
+ /** `value`, each string of it `stands` names the Check of, as that Check. */
629
+ function standingFor(value, stands) {
630
+ if (typeof value === "string")
631
+ return stands[value] ?? value;
632
+ if (value instanceof Check || typeof value !== "object")
633
+ return value;
634
+ if (Array.isArray(value))
635
+ return value.map((item) => standingFor(item, stands));
636
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, item === undefined ? undefined : standingFor(item, stands)]));
637
+ }
638
+ /**
639
+ * Every pod of the folder namespace, `namespace`, made or changed by anyone, held to what the
640
+ * folder template, `template`, makes (src/gateway/folders.ts): a folder instance's pod is the
641
+ * operator's, of the machine's own mounts and the capabilities bayma's process snapshots need,
642
+ * which no Pod Security level but privileged allows, as the namespace enforces, so this policy
643
+ * is its boundary.
644
+ *
645
+ * - A pod made is made by `maker`, the gateway, alone, and is the template's, as the API server
646
+ * stores it (poolPodSpec), with what folderPodSpec adds, alone: its token's Secret, named for
647
+ * it, and its state directory under `stateRoot`, of a harness's and a conversation's names;
648
+ * its bayma's memory request the gateway's, between its template's and its limit
649
+ * (folderResources); bound from the first, to a folder instance's key, its node kept; and
650
+ * named as one.
651
+ * - A pod changed keeps its spec, its labels, and its owners, and of its annotations changes
652
+ * those that keep its node alone.
653
+ * - No pod gets an ephemeral container.
654
+ *
655
+ * It fails closed: where it cannot say, the API server refuses.
656
+ */
657
+ export function folderPodPolicy(template, { namespace, maker, stateRoot }) {
658
+ const metadata = { name: FOLDER_POD_POLICY, labels: labels("folder-bayma") };
659
+ // The spec as folderPodSpec makes it, its own name's and state directory's places standing for what they must be, then pinned.
660
+ const spec = folderPodPinned(standingFor(folderPodSpec(template.spec ?? { containers: [] }, POD_NAME, STATE_DIR), {
661
+ [folderTokenSecret(POD_NAME)]: sameAs(`object.metadata.name + ${JSON.stringify(folderTokenSecret(""))}`),
662
+ [STATE_DIR]: matching(`${literally(posix.join(stateRoot, "/"))}${PATH_TOKEN}/${PATH_TOKEN}`),
663
+ }));
664
+ const podLabels = { ...(template.metadata?.labels ?? {}), [POOL_LABEL]: FOLDER_POOL, [BINDING_LABEL]: matching("[0-9a-f]{40}") };
665
+ const podAnnotations = {
666
+ ...(template.metadata?.annotations ?? {}),
667
+ [KEY_ANNOTATION]: matching(`${literally(FOLDER_KEY_PREFIX)}[a-z][a-z0-9-]{0,30}:.{1,512}`),
668
+ ...BUSY_ANNOTATIONS,
669
+ vpaObservedContainers: optional(ANY_STRING),
670
+ vpaUpdates: optional(ANY_STRING),
671
+ };
672
+ const policy = {
673
+ apiVersion: "admissionregistration.k8s.io/v1",
674
+ kind: "ValidatingAdmissionPolicy",
675
+ metadata,
676
+ spec: {
677
+ failurePolicy: "Fail",
678
+ matchConstraints: {
679
+ resourceRules: [
680
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods"], operations: ["CREATE", "UPDATE"] },
681
+ { apiGroups: [""], apiVersions: ["*"], resources: ["pods/ephemeralcontainers"], operations: ["CREATE", "UPDATE"] },
682
+ ],
683
+ },
684
+ matchConditions: [{ name: "of-folders", expression: `request.namespace == ${JSON.stringify(namespace)}` }],
685
+ variables: [
686
+ { name: "oldAnnotations", expression: "request.operation == 'UPDATE' ? oldObject.metadata.?annotations.orValue({}) : {}" },
687
+ { name: "newAnnotations", expression: "object.metadata.?annotations.orValue({})" },
688
+ // Each pin a variable of its own, as CEL holds each expression to 100,000 code points.
689
+ { name: "folderSpec", expression: pinned("object.spec", spec) },
690
+ { name: "folderLabels", expression: pinned("object.metadata.?labels.orValue({})", podLabels) },
691
+ { name: "folderAnnotations", expression: pinned("object.metadata.?annotations.orValue({})", podAnnotations) },
692
+ ],
693
+ validations: [
694
+ { expression: "request.?subResource.orValue('') != 'ephemeralcontainers'", message: "no pod of the folder namespace gets an ephemeral container" },
695
+ { expression: `request.operation != 'CREATE' || request.userInfo.username == ${JSON.stringify(maker)}`, message: "no pod is made in the folder namespace but by the sandbox gateway" },
696
+ { 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" },
697
+ { 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" },
698
+ { expression: "request.operation != 'CREATE' || variables.folderLabels", message: "a pod made in the folder namespace is labelled as a folder instance bound" },
699
+ { 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" },
700
+ { expression: "request.operation != 'UPDATE' || object.spec == oldObject.spec", message: "no pod's spec changes in the folder namespace" },
701
+ { expression: "request.operation != 'UPDATE' || object.metadata.?labels.orValue({}) == oldObject.metadata.?labels.orValue({})", message: "no pod's labels change in the folder namespace" },
702
+ {
703
+ expression: `request.operation != 'UPDATE' || (${unchanged("Annotations", Object.keys(BUSY_ANNOTATIONS))})`,
704
+ message: "no annotation of a folder instance's pod changes but what keeps its node",
705
+ },
706
+ {
707
+ expression: "request.operation != 'UPDATE' || object.metadata.?ownerReferences.orValue([]).all(owner, owner in oldObject.metadata.?ownerReferences.orValue([]))",
708
+ message: "a pod of the folder namespace gets no owner it had not",
709
+ },
710
+ ],
711
+ },
712
+ };
713
+ const binding = { apiVersion: "admissionregistration.k8s.io/v1", kind: "ValidatingAdmissionPolicyBinding", metadata, spec: { policyName: FOLDER_POD_POLICY, validationActions: ["Deny"] } };
714
+ return [policy, binding];
715
+ }
716
+ /** 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. */
717
+ export function folderSecretPolicy({ namespace, maker }) {
718
+ return admissionPolicy(componentName("folder-secrets"), [{ apiGroups: [""], resources: ["secrets"], operations: ["CREATE", "UPDATE"] }], `request.userInfo.username == ${JSON.stringify(maker)}`, [{
719
+ expression: `request.namespace == ${JSON.stringify(namespace)} && object.?type.orValue("Opaque") == "Opaque" && ` +
720
+ `object.metadata.name.matches(${JSON.stringify(`^${FOLDER_POD.source.slice(1, -1)}${literally(folderTokenSecret(""))}$`)}) && ` +
721
+ `object.?data.orValue({}).all(key, key == "token")`,
722
+ message: "the sandbox gateway makes no Secret but a folder instance's token, in the folder namespace",
723
+ }]);
724
+ }
725
+ /**
726
+ * What the sandbox gateway, `maker`, may delete in the folder namespace, `namespace`, as RBAC
727
+ * lets it delete any pod and Secret there: a folder instance's pod, of the folder pool, and a
728
+ * folder instance's token's Secret, alone.
729
+ */
730
+ export function folderDeletionPolicy({ namespace, maker }) {
731
+ const pool = `oldObject.metadata.?labels.orValue({})`;
732
+ return admissionPolicy(componentName("folder-deletions"), [{ apiGroups: [""], resources: ["pods", "secrets"], operations: ["DELETE"] }], `request.userInfo.username == ${JSON.stringify(maker)} && request.namespace == ${JSON.stringify(namespace)}`, [
733
+ {
734
+ 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)})`,
735
+ message: "the sandbox gateway deletes no pod in the folder namespace but a folder instance's",
736
+ },
393
737
  {
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",
738
+ expression: `request.resource.resource != "secrets" || oldObject.metadata.name.matches(${JSON.stringify(`^${FOLDER_POD.source.slice(1, -1)}${literally(folderTokenSecret(""))}$`)})`,
739
+ message: "the sandbox gateway deletes no Secret in the folder namespace but a folder instance's token",
396
740
  },
397
- ], [...ofSessions, ...ofHost]);
741
+ ]);
398
742
  }
399
743
  /**
400
744
  * The fields of a pod's status but its conditions that every Kubernetes alasio runs on
@@ -417,39 +761,31 @@ const POD_STATUS_FIELDS = [
417
761
  "startTime",
418
762
  ];
419
763
  /**
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.
764
+ * What `account`, the sandbox gateway's, may change of a pod's status, as it lets a restore's
765
+ * pod be ready once its node's agent resumed its sandbox (src/gateway/snapshots.ts): the
766
+ * RESUMED_CONDITION of a pool's pod in `namespace` that is gated on it, set true, and nothing
767
+ * else, its addresses above all, which network policies are programmed from. RBAC lets it
768
+ * patch the status of any pod of the namespace, whole.
424
769
  */
425
770
  export function resumedConditionPolicy(name, account, namespace) {
426
771
  const resumed = JSON.stringify(RESUMED_CONDITION);
427
772
  const others = (object) => `${object}.status.?conditions.orValue([]).filter(condition, condition.type != ${resumed})`;
428
773
  return admissionPolicy(name, [{ apiGroups: [""], resources: ["pods/status"], operations: ["UPDATE"] }], `request.userInfo.username == "${account}"`, [
429
774
  {
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" && ` +
775
+ expression: `request.namespace == ${JSON.stringify(namespace)} && ${JSON.stringify(POOL_LABEL)} in object.metadata.?labels.orValue({}) && ` +
432
776
  `object.spec.?readinessGates.orValue([]).exists(gate, gate.conditionType == ${resumed})`,
433
- message: "alasio sets the status of no pod but a restored session's",
777
+ message: "the sandbox gateway sets the status of no pod but a restore's of its pools'",
434
778
  },
435
779
  {
436
780
  expression: `${others("object")} == ${others("oldObject")} && ` +
437
781
  `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`,
782
+ message: `the sandbox gateway sets no condition of a pod's but ${RESUMED_CONDITION}, and that true`,
439
783
  },
440
784
  {
441
785
  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
786
  " && 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",
787
+ message: "the sandbox gateway changes nothing of a pod's status but its conditions, nor its labels or annotations",
444
788
  },
445
789
  ]);
446
790
  }
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
791
  //# sourceMappingURL=admission.js.map