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.
- package/dist/cli/src/api-server.js +18 -0
- package/dist/cli/src/api-server.js.map +1 -0
- package/dist/cli/src/branches.js +123 -46
- package/dist/cli/src/branches.js.map +1 -1
- package/dist/cli/src/cluster/docker.js +25 -10
- package/dist/cli/src/cluster/docker.js.map +1 -1
- package/dist/cli/src/cluster/host.js +33 -7
- package/dist/cli/src/cluster/host.js.map +1 -1
- package/dist/cli/src/cluster/k3s.js +51 -12
- package/dist/cli/src/cluster/k3s.js.map +1 -1
- package/dist/cli/src/cluster/node.js +115 -33
- package/dist/cli/src/cluster/node.js.map +1 -1
- package/dist/cli/src/cluster/stargz.js +55 -0
- package/dist/cli/src/cluster/stargz.js.map +1 -0
- package/dist/cli/src/commands/branch.js +5 -4
- package/dist/cli/src/commands/branch.js.map +1 -1
- package/dist/cli/src/commands/common.js +9 -3
- package/dist/cli/src/commands/common.js.map +1 -1
- package/dist/cli/src/commands/folder.js +14 -0
- package/dist/cli/src/commands/folder.js.map +1 -0
- package/dist/cli/src/commands/init.js +54 -4
- package/dist/cli/src/commands/init.js.map +1 -1
- package/dist/cli/src/commands/lake.js +40 -6
- package/dist/cli/src/commands/lake.js.map +1 -1
- package/dist/cli/src/commands/restore-filer-store.js +40 -8
- package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
- package/dist/cli/src/commands/status.js +216 -20
- package/dist/cli/src/commands/status.js.map +1 -1
- package/dist/cli/src/commands/uninstall.js +8 -9
- package/dist/cli/src/commands/uninstall.js.map +1 -1
- package/dist/cli/src/commands/up.js +11 -3
- package/dist/cli/src/commands/up.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +1 -1
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/commands.js +2 -1
- package/dist/cli/src/commands.js.map +1 -1
- package/dist/cli/src/config.js +24 -9
- package/dist/cli/src/config.js.map +1 -1
- package/dist/cli/src/cpu-baseline.js +8 -4
- package/dist/cli/src/cpu-baseline.js.map +1 -1
- package/dist/cli/src/folders.js +67 -0
- package/dist/cli/src/folders.js.map +1 -0
- package/dist/cli/src/gateway-calls.js +58 -0
- package/dist/cli/src/gateway-calls.js.map +1 -0
- package/dist/cli/src/install.js +119 -17
- package/dist/cli/src/install.js.map +1 -1
- package/dist/cli/src/kept-releases.js +27 -20
- package/dist/cli/src/kept-releases.js.map +1 -1
- package/dist/cli/src/kube/api.js +11 -5
- package/dist/cli/src/kube/api.js.map +1 -1
- package/dist/cli/src/kube/apply.js +16 -73
- package/dist/cli/src/kube/apply.js.map +1 -1
- package/dist/cli/src/kube/rollout.js +84 -26
- package/dist/cli/src/kube/rollout.js.map +1 -1
- package/dist/cli/src/lake.js +124 -0
- package/dist/cli/src/lake.js.map +1 -0
- package/dist/cli/src/manifests/admission.js +381 -223
- package/dist/cli/src/manifests/admission.js.map +1 -1
- package/dist/cli/src/manifests/alasio.js +79 -362
- package/dist/cli/src/manifests/alasio.js.map +1 -1
- package/dist/cli/src/manifests/branch.js +10 -26
- package/dist/cli/src/manifests/branch.js.map +1 -1
- package/dist/cli/src/manifests/collector.js +250 -135
- package/dist/cli/src/manifests/collector.js.map +1 -1
- package/dist/cli/src/manifests/common.js +32 -2
- package/dist/cli/src/manifests/common.js.map +1 -1
- package/dist/cli/src/manifests/config.js +256 -92
- package/dist/cli/src/manifests/config.js.map +1 -1
- package/dist/cli/src/manifests/disruption.js +10 -0
- package/dist/cli/src/manifests/disruption.js.map +1 -1
- package/dist/cli/src/manifests/folders.js +128 -0
- package/dist/cli/src/manifests/folders.js.map +1 -0
- package/dist/cli/src/manifests/grafana.js +4 -2
- package/dist/cli/src/manifests/grafana.js.map +1 -1
- package/dist/cli/src/manifests/index.js +15 -8
- package/dist/cli/src/manifests/index.js.map +1 -1
- package/dist/cli/src/manifests/keda.js +2 -1
- package/dist/cli/src/manifests/keda.js.map +1 -1
- package/dist/cli/src/manifests/lake.js +350 -73
- package/dist/cli/src/manifests/lake.js.map +1 -1
- package/dist/cli/src/manifests/log.js +316 -0
- package/dist/cli/src/manifests/log.js.map +1 -0
- package/dist/cli/src/manifests/neon.js +128 -49
- package/dist/cli/src/manifests/neon.js.map +1 -1
- package/dist/cli/src/manifests/network-policies.js +263 -100
- package/dist/cli/src/manifests/network-policies.js.map +1 -1
- package/dist/cli/src/manifests/node-agent.js +24 -8
- package/dist/cli/src/manifests/node-agent.js.map +1 -1
- package/dist/cli/src/manifests/object-store.js +4 -2
- package/dist/cli/src/manifests/object-store.js.map +1 -1
- package/dist/cli/src/manifests/sandbox-gateway.js +663 -0
- package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
- package/dist/cli/src/manifests/scalers.js +16 -5
- package/dist/cli/src/manifests/scalers.js.map +1 -1
- package/dist/cli/src/manifests/sizing.js +16 -8
- package/dist/cli/src/manifests/sizing.js.map +1 -1
- package/dist/cli/src/manifests/valkey.js +11 -5
- package/dist/cli/src/manifests/valkey.js.map +1 -1
- package/dist/cli/src/manifests/vpa.js +3 -1
- package/dist/cli/src/manifests/vpa.js.map +1 -1
- package/dist/cli/src/manifests/webhook.js +102 -0
- package/dist/cli/src/manifests/webhook.js.map +1 -0
- package/dist/cli/src/manifests/workspace-storage.js +174 -89
- package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
- package/dist/cli/src/neon-storage.js +155 -0
- package/dist/cli/src/neon-storage.js.map +1 -0
- package/dist/cli/src/release.js +9 -6
- package/dist/cli/src/release.js.map +1 -1
- package/dist/cli/src/secrets.js +45 -4
- package/dist/cli/src/secrets.js.map +1 -1
- package/dist/cli/src/target.js +1 -1
- package/dist/cli/src/target.js.map +1 -1
- package/dist/cli/src/telegram.js +56 -10
- package/dist/cli/src/telegram.js.map +1 -1
- package/dist/cli/src/webhook.js +182 -0
- package/dist/cli/src/webhook.js.map +1 -0
- package/dist/cluster/node/pins.json +12 -2
- package/dist/neon/control/branches.js +126 -21
- package/dist/neon/control/branches.js.map +1 -1
- package/dist/neon/control/endpoints.js +9 -0
- package/dist/neon/control/endpoints.js.map +1 -1
- package/dist/neon/control/jwt.js +16 -3
- package/dist/neon/control/jwt.js.map +1 -1
- package/dist/neon/control/secrets.js +51 -7
- package/dist/neon/control/secrets.js.map +1 -1
- package/dist/src/binder/names.js +33 -0
- package/dist/src/binder/names.js.map +1 -0
- package/dist/src/binder/tokens.js +58 -0
- package/dist/src/binder/tokens.js.map +1 -0
- package/dist/src/branch/names.js +12 -2
- package/dist/src/branch/names.js.map +1 -1
- package/dist/src/events/names.js +7 -0
- package/dist/src/events/names.js.map +1 -0
- package/dist/src/gateway/names.js +201 -0
- package/dist/src/gateway/names.js.map +1 -0
- package/dist/src/harness/names.js +49 -0
- package/dist/src/harness/names.js.map +1 -0
- package/dist/src/kube/sandbox-shape.js +19 -69
- package/dist/src/kube/sandbox-shape.js.map +1 -1
- package/dist/src/node-agent/names.js +118 -2
- package/dist/src/node-agent/names.js.map +1 -1
- package/dist/src/sandbox/names.js +12 -20
- package/dist/src/sandbox/names.js.map +1 -1
- package/dist/src/sandbox/otlp-resource.js +173 -0
- package/dist/src/sandbox/otlp-resource.js.map +1 -0
- package/dist/src/sandbox/telemetry.js +43 -0
- package/dist/src/sandbox/telemetry.js.map +1 -0
- package/dist/src/telegram/deliveries.js +40 -0
- package/dist/src/telegram/deliveries.js.map +1 -0
- package/dist/src/telemetry/config.js +131 -0
- package/dist/src/telemetry/config.js.map +1 -0
- package/dist/src/telemetry/forward.js +154 -0
- package/dist/src/telemetry/forward.js.map +1 -0
- package/package.json +1 -1
- package/dist/cli/src/manifests/agent-sandbox.js +0 -92
- package/dist/cli/src/manifests/agent-sandbox.js.map +0 -1
- package/dist/cli/src/manifests/juicefs-csi.js +0 -319
- package/dist/cli/src/manifests/juicefs-csi.js.map +0 -1
- package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +0 -6295
- package/dist/src/kube/config.js +0 -143
- 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
|
|
3
|
-
* cluster's, each matching what
|
|
4
|
-
*
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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 {
|
|
28
|
-
import {
|
|
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
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
/**
|
|
215
|
-
const
|
|
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
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
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
|
|
222
|
-
|
|
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
|
-
*
|
|
227
|
-
* the
|
|
228
|
-
*
|
|
229
|
-
*
|
|
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
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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
|
|
336
|
-
const
|
|
337
|
-
|
|
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:
|
|
356
|
-
message: "
|
|
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
|
-
|
|
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
|
|
382
|
-
* pod be ready once its node's agent resumed its sandbox (src/
|
|
383
|
-
* RESUMED_CONDITION of a
|
|
384
|
-
*
|
|
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(
|
|
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: "
|
|
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: `
|
|
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: "
|
|
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
|