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.
- package/dist/cli/src/api-server.js +18 -0
- package/dist/cli/src/api-server.js.map +1 -0
- package/dist/cli/src/bound-harnesses.js +98 -0
- package/dist/cli/src/bound-harnesses.js.map +1 -0
- package/dist/cli/src/branches.js +79 -22
- package/dist/cli/src/branches.js.map +1 -1
- package/dist/cli/src/cluster/docker.js +40 -15
- package/dist/cli/src/cluster/docker.js.map +1 -1
- package/dist/cli/src/cluster/host.js +38 -8
- package/dist/cli/src/cluster/host.js.map +1 -1
- package/dist/cli/src/cluster/k3s.js +105 -14
- package/dist/cli/src/cluster/k3s.js.map +1 -1
- package/dist/cli/src/cluster/node.js +142 -33
- package/dist/cli/src/cluster/node.js.map +1 -1
- package/dist/cli/src/cluster/stargz.js +92 -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 +11 -5
- 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/lake.js +1 -1
- package/dist/cli/src/commands/lake.js.map +1 -1
- package/dist/cli/src/commands/login-relay.js +91 -0
- package/dist/cli/src/commands/login-relay.js.map +1 -0
- package/dist/cli/src/commands/login.js +11 -10
- package/dist/cli/src/commands/login.js.map +1 -1
- package/dist/cli/src/commands/logs.js +89 -6
- package/dist/cli/src/commands/logs.js.map +1 -1
- package/dist/cli/src/commands/restart.js +10 -4
- package/dist/cli/src/commands/restart.js.map +1 -1
- package/dist/cli/src/commands/restore-filer-store.js +27 -10
- package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
- package/dist/cli/src/commands/status.js +119 -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 +20 -9
- package/dist/cli/src/commands/up.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +22 -6
- 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 +4 -1
- 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 +63 -0
- package/dist/cli/src/folders.js.map +1 -0
- package/dist/cli/src/gateway-calls.js +64 -0
- package/dist/cli/src/gateway-calls.js.map +1 -0
- package/dist/cli/src/install.js +45 -31
- 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 +61 -15
- package/dist/cli/src/kube/api.js.map +1 -1
- package/dist/cli/src/kube/apply.js +122 -81
- package/dist/cli/src/kube/apply.js.map +1 -1
- package/dist/cli/src/kube/rollout.js +43 -21
- package/dist/cli/src/kube/rollout.js.map +1 -1
- package/dist/cli/src/main.js +2 -1
- package/dist/cli/src/main.js.map +1 -1
- package/dist/cli/src/manifests/admission.js +558 -222
- package/dist/cli/src/manifests/admission.js.map +1 -1
- package/dist/cli/src/manifests/alasio.js +180 -428
- package/dist/cli/src/manifests/alasio.js.map +1 -1
- package/dist/cli/src/manifests/branch.js +16 -27
- package/dist/cli/src/manifests/branch.js.map +1 -1
- package/dist/cli/src/manifests/codex-login.js +101 -0
- package/dist/cli/src/manifests/codex-login.js.map +1 -0
- package/dist/cli/src/manifests/collector.js +83 -62
- package/dist/cli/src/manifests/collector.js.map +1 -1
- package/dist/cli/src/manifests/common.js +82 -25
- package/dist/cli/src/manifests/common.js.map +1 -1
- package/dist/cli/src/manifests/config.js +137 -105
- package/dist/cli/src/manifests/config.js.map +1 -1
- package/dist/cli/src/manifests/disruption.js +18 -3
- package/dist/cli/src/manifests/disruption.js.map +1 -1
- package/dist/cli/src/manifests/folders.js +131 -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/harness-pools.js +236 -0
- package/dist/cli/src/manifests/harness-pools.js.map +1 -0
- package/dist/cli/src/manifests/image-holder.js +61 -0
- package/dist/cli/src/manifests/image-holder.js.map +1 -0
- package/dist/cli/src/manifests/index.js +14 -7
- package/dist/cli/src/manifests/index.js.map +1 -1
- package/dist/cli/src/manifests/keda.js +4 -2
- package/dist/cli/src/manifests/keda.js.map +1 -1
- package/dist/cli/src/manifests/lake.js +18 -20
- package/dist/cli/src/manifests/lake.js.map +1 -1
- package/dist/cli/src/manifests/log.js +84 -19
- package/dist/cli/src/manifests/log.js.map +1 -1
- package/dist/cli/src/manifests/neon.js +60 -80
- package/dist/cli/src/manifests/neon.js.map +1 -1
- package/dist/cli/src/manifests/network-policies.js +232 -94
- package/dist/cli/src/manifests/network-policies.js.map +1 -1
- package/dist/cli/src/manifests/node-agent.js +42 -15
- package/dist/cli/src/manifests/node-agent.js.map +1 -1
- package/dist/cli/src/manifests/object-store.js +99 -25
- package/dist/cli/src/manifests/object-store.js.map +1 -1
- package/dist/cli/src/manifests/sandbox-gateway.js +761 -0
- package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
- package/dist/cli/src/manifests/scalers.js +17 -5
- package/dist/cli/src/manifests/scalers.js.map +1 -1
- package/dist/cli/src/manifests/sizing.js +17 -9
- package/dist/cli/src/manifests/sizing.js.map +1 -1
- package/dist/cli/src/manifests/valkey.js +30 -10
- package/dist/cli/src/manifests/valkey.js.map +1 -1
- package/dist/cli/src/manifests/volume-lock.js +43 -0
- package/dist/cli/src/manifests/volume-lock.js.map +1 -0
- package/dist/cli/src/manifests/vpa.js +15 -2
- package/dist/cli/src/manifests/vpa.js.map +1 -1
- package/dist/cli/src/manifests/workspace-storage.js +182 -94
- package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
- package/dist/cli/src/neon-storage.js +54 -16
- package/dist/cli/src/neon-storage.js.map +1 -1
- package/dist/cli/src/release.js +9 -9
- package/dist/cli/src/release.js.map +1 -1
- package/dist/cli/src/setup-pod.js +44 -0
- package/dist/cli/src/setup-pod.js.map +1 -0
- package/dist/cli/src/target.js +2 -2
- package/dist/cli/src/target.js.map +1 -1
- package/dist/cli/src/telegram.js +13 -4
- package/dist/cli/src/telegram.js.map +1 -1
- package/dist/cli/src/wakes.js +68 -0
- package/dist/cli/src/wakes.js.map +1 -0
- package/dist/cli/src/webhook.js +5 -2
- package/dist/cli/src/webhook.js.map +1 -1
- package/dist/cluster/node/pins.json +11 -5
- package/dist/neon/control/branches.js +13 -6
- package/dist/neon/control/branches.js.map +1 -1
- package/dist/neon/control/endpoints.js +81 -10
- package/dist/neon/control/endpoints.js.map +1 -1
- package/dist/neon/control/secrets.js +24 -8
- package/dist/neon/control/secrets.js.map +1 -1
- package/dist/src/binder/bindings.js +195 -0
- package/dist/src/binder/bindings.js.map +1 -0
- package/dist/src/binder/names.js +63 -0
- package/dist/src/binder/names.js.map +1 -0
- package/dist/src/binder/tokens.js +71 -0
- package/dist/src/binder/tokens.js.map +1 -0
- package/dist/src/branch/names.js +20 -6
- package/dist/src/branch/names.js.map +1 -1
- package/dist/src/events/names.js +28 -0
- package/dist/src/events/names.js.map +1 -0
- package/dist/src/gateway/names.js +240 -0
- package/dist/src/gateway/names.js.map +1 -0
- package/dist/src/harness/names.js +67 -0
- package/dist/src/harness/names.js.map +1 -0
- package/dist/src/kube/client.js +212 -0
- package/dist/src/kube/client.js.map +1 -0
- package/dist/src/kube/sandbox-shape.js +14 -68
- package/dist/src/kube/sandbox-shape.js.map +1 -1
- package/dist/src/kube/started-map.js +12 -0
- package/dist/src/kube/started-map.js.map +1 -0
- package/dist/src/node-agent/names.js +62 -5
- 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/bot-commands.js +17 -0
- package/dist/src/telegram/bot-commands.js.map +1 -0
- package/dist/src/telegram/stop.js +12 -0
- package/dist/src/telegram/stop.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 -320
- 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 -145
- 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
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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 {
|
|
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 {
|
|
31
|
-
import {
|
|
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
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
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
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
/**
|
|
254
|
-
const
|
|
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
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
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
|
|
261
|
-
const own = `${JSON.stringify(`${SNAPSHOT_CACHE_DIR}/${namespace}/`)} +
|
|
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
|
-
*
|
|
266
|
-
* the
|
|
267
|
-
*
|
|
268
|
-
*
|
|
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
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
...(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
364
|
-
export const
|
|
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
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
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
|
-
|
|
375
|
-
const {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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:
|
|
395
|
-
message: "
|
|
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
|
-
]
|
|
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
|
|
421
|
-
* pod be ready once its node's agent resumed its sandbox (src/
|
|
422
|
-
* RESUMED_CONDITION of a
|
|
423
|
-
*
|
|
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(
|
|
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: "
|
|
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: `
|
|
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: "
|
|
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
|