@telorun/k8s-runner 0.6.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/LICENSE +17 -0
- package/README.md +206 -0
- package/dist/bundle-store.d.ts +32 -0
- package/dist/bundle-store.d.ts.map +1 -0
- package/dist/bundle-store.js +86 -0
- package/dist/bundle-store.js.map +1 -0
- package/dist/capabilities.d.ts +15 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +30 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/config.d.ts +95 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +76 -0
- package/dist/config.js.map +1 -0
- package/dist/k8s/backend.d.ts +19 -0
- package/dist/k8s/backend.d.ts.map +1 -0
- package/dist/k8s/backend.js +488 -0
- package/dist/k8s/backend.js.map +1 -0
- package/dist/k8s/client.d.ts +16 -0
- package/dist/k8s/client.d.ts.map +1 -0
- package/dist/k8s/client.js +24 -0
- package/dist/k8s/client.js.map +1 -0
- package/dist/k8s/image-build.d.ts +105 -0
- package/dist/k8s/image-build.d.ts.map +1 -0
- package/dist/k8s/image-build.js +432 -0
- package/dist/k8s/image-build.js.map +1 -0
- package/dist/k8s/ingress.d.ts +15 -0
- package/dist/k8s/ingress.d.ts.map +1 -0
- package/dist/k8s/ingress.js +102 -0
- package/dist/k8s/ingress.js.map +1 -0
- package/dist/k8s/pod-spec.d.ts +41 -0
- package/dist/k8s/pod-spec.d.ts.map +1 -0
- package/dist/k8s/pod-spec.js +146 -0
- package/dist/k8s/pod-spec.js.map +1 -0
- package/dist/limits.d.ts +25 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +58 -0
- package/dist/limits.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/dist/tar.d.ts +10 -0
- package/dist/tar.d.ts.map +1 -0
- package/dist/tar.js +63 -0
- package/dist/tar.js.map +1 -0
- package/package.json +41 -0
- package/src/bundle-store.ts +101 -0
- package/src/capabilities.ts +40 -0
- package/src/config.ts +201 -0
- package/src/k8s/backend-failure.test.ts +148 -0
- package/src/k8s/backend.ts +535 -0
- package/src/k8s/client.ts +39 -0
- package/src/k8s/image-build.test.ts +244 -0
- package/src/k8s/image-build.ts +547 -0
- package/src/k8s/ingress.test.ts +146 -0
- package/src/k8s/ingress.ts +127 -0
- package/src/k8s/pod-spec.ts +180 -0
- package/src/limits.test.ts +59 -0
- package/src/limits.ts +90 -0
- package/src/server.ts +124 -0
- package/src/tar.test.ts +55 -0
- package/src/tar.ts +68 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** Port the workload's `--inspect` server binds inside the session container.
|
|
2
|
+
* Reached by the runner over the cluster pod network (`http://<podIP>:<port>`);
|
|
3
|
+
* never declared as a Service port — only the runner relays the stream out. */
|
|
4
|
+
export const INSPECT_PORT = 9230;
|
|
5
|
+
const APP_DIR = "/app";
|
|
6
|
+
const WORK_DIR = "/work";
|
|
7
|
+
/** Baked, read-only deps (`telo install` output). Set via TELO_CACHE_DIR, NOT
|
|
8
|
+
* mounted — it lives on the image rootfs, so `telo run --no-cache-write` reads
|
|
9
|
+
* it without writing. */
|
|
10
|
+
const DEPS_DIR = "/telo-cache";
|
|
11
|
+
/** Writable HOME / npm scratch under a read-only rootfs. Separate from DEPS_DIR
|
|
12
|
+
* (which is now read-only baked deps, not scratch). */
|
|
13
|
+
const HOME_DIR = "/home/telo";
|
|
14
|
+
const TMP_MOUNT = "/tmp";
|
|
15
|
+
/**
|
|
16
|
+
* Builds the session Pod. Hardening that needs no RuntimeClass is always on
|
|
17
|
+
* (non-root, read-only rootfs, drop-all caps, no service-account token,
|
|
18
|
+
* seccomp RuntimeDefault); a sandbox RuntimeClass is layered on when configured.
|
|
19
|
+
*
|
|
20
|
+
* The body-delivery initContainer fetches the session bundle into the writable
|
|
21
|
+
* `/app` emptyDir; the session container runs `telo run /app/<entry>
|
|
22
|
+
* --no-cache-write` reading its deps from the baked, read-only `/telo-cache`.
|
|
23
|
+
* `readOnlyRootFilesystem` stays on — every write lands on a mounted emptyDir.
|
|
24
|
+
*/
|
|
25
|
+
export function buildSessionPod(args) {
|
|
26
|
+
const { config, limits } = args;
|
|
27
|
+
const resources = {
|
|
28
|
+
limits: {
|
|
29
|
+
cpu: limits.cpu,
|
|
30
|
+
memory: limits.memory,
|
|
31
|
+
"ephemeral-storage": limits.ephemeralStorage,
|
|
32
|
+
},
|
|
33
|
+
requests: {
|
|
34
|
+
cpu: limits.cpu,
|
|
35
|
+
memory: limits.memory,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const envVars = Object.entries(args.env).map(([name, value]) => ({ name, value }));
|
|
39
|
+
// Read deps from the baked, read-only `/telo-cache`; keep HOME/npm scratch on a
|
|
40
|
+
// separate writable emptyDir under the read-only root filesystem.
|
|
41
|
+
envVars.push({ name: "TELO_CACHE_DIR", value: DEPS_DIR });
|
|
42
|
+
envVars.push({ name: "HOME", value: HOME_DIR });
|
|
43
|
+
envVars.push({ name: "npm_config_cache", value: `${HOME_DIR}/.npm` });
|
|
44
|
+
envVars.push({ name: "FORCE_COLOR", value: "1" });
|
|
45
|
+
const pod = {
|
|
46
|
+
apiVersion: "v1",
|
|
47
|
+
kind: "Pod",
|
|
48
|
+
metadata: {
|
|
49
|
+
name: args.podName,
|
|
50
|
+
namespace: config.sessionNamespace,
|
|
51
|
+
labels: {
|
|
52
|
+
"app.kubernetes.io/managed-by": config.managedByLabel,
|
|
53
|
+
"telo.run/session-id": args.sessionId,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
spec: {
|
|
57
|
+
restartPolicy: "Never",
|
|
58
|
+
activeDeadlineSeconds: limits.ttlSeconds,
|
|
59
|
+
automountServiceAccountToken: false,
|
|
60
|
+
// Pull the per-app image from a private registry. The Secret must exist in
|
|
61
|
+
// the session namespace (pull secrets are namespace-scoped).
|
|
62
|
+
...(config.build.imagePullSecret
|
|
63
|
+
? { imagePullSecrets: [{ name: config.build.imagePullSecret }] }
|
|
64
|
+
: {}),
|
|
65
|
+
...(config.runtimeClass ? { runtimeClassName: config.runtimeClass } : {}),
|
|
66
|
+
securityContext: {
|
|
67
|
+
runAsNonRoot: true,
|
|
68
|
+
runAsUser: 1000,
|
|
69
|
+
runAsGroup: 1000,
|
|
70
|
+
fsGroup: 1000,
|
|
71
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
72
|
+
},
|
|
73
|
+
initContainers: [
|
|
74
|
+
{
|
|
75
|
+
// Deliver the per-session body into the writable /app emptyDir. The
|
|
76
|
+
// image bakes only the dependency closure, so the body arrives here.
|
|
77
|
+
name: "body-fetch",
|
|
78
|
+
image: config.initImage,
|
|
79
|
+
command: ["sh", "-c"],
|
|
80
|
+
args: [
|
|
81
|
+
`set -e; wget -qO /tmp/body.tgz "${args.bundleUrl}"; tar xzf /tmp/body.tgz -C ${APP_DIR}`,
|
|
82
|
+
],
|
|
83
|
+
volumeMounts: [
|
|
84
|
+
{ name: "app", mountPath: APP_DIR },
|
|
85
|
+
{ name: "tmp", mountPath: TMP_MOUNT },
|
|
86
|
+
],
|
|
87
|
+
securityContext: hardenedContainerSecurity(),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
containers: [
|
|
91
|
+
{
|
|
92
|
+
name: "session",
|
|
93
|
+
image: args.image,
|
|
94
|
+
// The per-app tag is an immutable content hash, so IfNotPresent lets
|
|
95
|
+
// the kubelet reuse a node-cached layer across runs of the same app.
|
|
96
|
+
imagePullPolicy: "IfNotPresent",
|
|
97
|
+
// Run the delivered body by absolute path; `--no-cache-write` reads
|
|
98
|
+
// the baked deps from TELO_CACHE_DIR and validates in-memory without
|
|
99
|
+
// touching the read-only cache. WORK_DIR is a writable emptyDir cwd so
|
|
100
|
+
// the workload's relative paths resolve under readOnlyRootFilesystem.
|
|
101
|
+
workingDir: WORK_DIR,
|
|
102
|
+
// 0.0.0.0 (not the CLI's loopback default) lets the runner reach the
|
|
103
|
+
// debug server across the pod network; the port is never published.
|
|
104
|
+
command: [
|
|
105
|
+
"telo",
|
|
106
|
+
"run",
|
|
107
|
+
`${APP_DIR}/${args.entryRelativePath}`,
|
|
108
|
+
"--no-cache-write",
|
|
109
|
+
...(args.inspect ? ["--inspect", `0.0.0.0:${INSPECT_PORT}`, "--no-open"] : []),
|
|
110
|
+
],
|
|
111
|
+
env: envVars,
|
|
112
|
+
stdin: true,
|
|
113
|
+
stdinOnce: false,
|
|
114
|
+
tty: true,
|
|
115
|
+
...(args.ports.length > 0
|
|
116
|
+
? { ports: args.ports.map((p) => ({ containerPort: p.port, protocol: p.protocol.toUpperCase() })) }
|
|
117
|
+
: {}),
|
|
118
|
+
resources,
|
|
119
|
+
volumeMounts: [
|
|
120
|
+
{ name: "app", mountPath: APP_DIR },
|
|
121
|
+
{ name: "work", mountPath: WORK_DIR },
|
|
122
|
+
{ name: "home", mountPath: HOME_DIR },
|
|
123
|
+
{ name: "tmp", mountPath: TMP_MOUNT },
|
|
124
|
+
],
|
|
125
|
+
securityContext: hardenedContainerSecurity(),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
volumes: [
|
|
129
|
+
{ name: "app", emptyDir: {} },
|
|
130
|
+
{ name: "work", emptyDir: {} },
|
|
131
|
+
{ name: "home", emptyDir: {} },
|
|
132
|
+
{ name: "tmp", emptyDir: {} },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
return pod;
|
|
137
|
+
}
|
|
138
|
+
function hardenedContainerSecurity() {
|
|
139
|
+
return {
|
|
140
|
+
allowPrivilegeEscalation: false,
|
|
141
|
+
readOnlyRootFilesystem: true,
|
|
142
|
+
runAsNonRoot: true,
|
|
143
|
+
capabilities: { drop: ["ALL"] },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=pod-spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pod-spec.js","sourceRoot":"","sources":["../../src/k8s/pod-spec.ts"],"names":[],"mappings":"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"}
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LimitCeilings } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Effective per-session resource limits. Requests may ask for LESS than the
|
|
4
|
+
* ceiling but never MORE — `min(requested, ceiling)`. Without a control plane
|
|
5
|
+
* the editor talks to the runner directly, so a raisable limit would void the
|
|
6
|
+
* cap; clamp-down-only is the load-bearing invariant.
|
|
7
|
+
*/
|
|
8
|
+
export interface ResolvedLimits {
|
|
9
|
+
cpu: string;
|
|
10
|
+
memory: string;
|
|
11
|
+
ttlSeconds: number;
|
|
12
|
+
ephemeralStorage: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RequestedLimits {
|
|
15
|
+
cpu?: string;
|
|
16
|
+
memory?: string;
|
|
17
|
+
ttlSeconds?: number;
|
|
18
|
+
ephemeralStorage?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function clampLimits(ceilings: LimitCeilings, requested: RequestedLimits | undefined): ResolvedLimits;
|
|
21
|
+
/** CPU → millicores. "500m" → 500; "1" → 1000; "0.5" → 500. */
|
|
22
|
+
export declare function parseCpuMillis(q: string): number | null;
|
|
23
|
+
/** Memory/storage quantity → bytes. Supports binary (Ki/Mi/Gi) and SI (K/M/G). */
|
|
24
|
+
export declare function parseMemoryBytes(q: string): number | null;
|
|
25
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,cAAc,CAchB;AAgBD,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQvD;AAcD,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUzD"}
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function clampLimits(ceilings, requested) {
|
|
2
|
+
return {
|
|
3
|
+
cpu: clampQuantity(ceilings.cpu, requested?.cpu, parseCpuMillis),
|
|
4
|
+
memory: clampQuantity(ceilings.memory, requested?.memory, parseMemoryBytes),
|
|
5
|
+
ephemeralStorage: clampQuantity(ceilings.ephemeralStorage, requested?.ephemeralStorage, parseMemoryBytes),
|
|
6
|
+
ttlSeconds: requested?.ttlSeconds && requested.ttlSeconds > 0
|
|
7
|
+
? Math.min(requested.ttlSeconds, ceilings.ttlSeconds)
|
|
8
|
+
: ceilings.ttlSeconds,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Returns `requested` only when it parses AND is <= ceiling; otherwise the
|
|
12
|
+
* ceiling. Any unparseable/oversized request silently clamps to the cap. */
|
|
13
|
+
function clampQuantity(ceiling, requested, parse) {
|
|
14
|
+
if (!requested)
|
|
15
|
+
return ceiling;
|
|
16
|
+
const r = parse(requested);
|
|
17
|
+
const c = parse(ceiling);
|
|
18
|
+
if (r === null || c === null)
|
|
19
|
+
return ceiling;
|
|
20
|
+
return r <= c ? requested : ceiling;
|
|
21
|
+
}
|
|
22
|
+
/** CPU → millicores. "500m" → 500; "1" → 1000; "0.5" → 500. */
|
|
23
|
+
export function parseCpuMillis(q) {
|
|
24
|
+
const s = q.trim();
|
|
25
|
+
if (s.endsWith("m")) {
|
|
26
|
+
const n = Number(s.slice(0, -1));
|
|
27
|
+
return Number.isFinite(n) ? n : null;
|
|
28
|
+
}
|
|
29
|
+
const n = Number(s);
|
|
30
|
+
return Number.isFinite(n) ? Math.round(n * 1000) : null;
|
|
31
|
+
}
|
|
32
|
+
const MEMORY_SUFFIXES = {
|
|
33
|
+
Ki: 1024,
|
|
34
|
+
Mi: 1024 ** 2,
|
|
35
|
+
Gi: 1024 ** 3,
|
|
36
|
+
Ti: 1024 ** 4,
|
|
37
|
+
K: 1000,
|
|
38
|
+
M: 1000 ** 2,
|
|
39
|
+
G: 1000 ** 3,
|
|
40
|
+
T: 1000 ** 4,
|
|
41
|
+
k: 1000,
|
|
42
|
+
};
|
|
43
|
+
/** Memory/storage quantity → bytes. Supports binary (Ki/Mi/Gi) and SI (K/M/G). */
|
|
44
|
+
export function parseMemoryBytes(q) {
|
|
45
|
+
const s = q.trim();
|
|
46
|
+
const match = /^(\d+(?:\.\d+)?)\s*([A-Za-z]+)?$/.exec(s);
|
|
47
|
+
if (!match)
|
|
48
|
+
return null;
|
|
49
|
+
const value = Number(match[1]);
|
|
50
|
+
if (!Number.isFinite(value))
|
|
51
|
+
return null;
|
|
52
|
+
const suffix = match[2];
|
|
53
|
+
if (!suffix)
|
|
54
|
+
return value;
|
|
55
|
+
const mult = MEMORY_SUFFIXES[suffix];
|
|
56
|
+
return mult ? value * mult : null;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseImageCatalog, type RunnerBackend, type ServerHandle } from "@telorun/runner-core";
|
|
2
|
+
import { BundleStore } from "./bundle-store.js";
|
|
3
|
+
import { type K8sRunnerConfig } from "./config.js";
|
|
4
|
+
export interface ServerDeps {
|
|
5
|
+
backend: RunnerBackend;
|
|
6
|
+
config: K8sRunnerConfig;
|
|
7
|
+
bundleStore: BundleStore;
|
|
8
|
+
/** Base-image catalog whose resolved list becomes the advertised `image`
|
|
9
|
+
* enum + the server-side allowlist. Omitted → `image` locks to defaultImage. */
|
|
10
|
+
catalog?: BaseImageCatalog;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildServer(deps: ServerDeps): Promise<ServerHandle>;
|
|
13
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAIhB,KAAK,aAAa,EAClB,KAAK,YAAY,EAElB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAM3F,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB;qFACiF;IACjF,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAyBzE"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { BaseImageCatalog, buildServer as coreBuildServer, loadTermsFromEnv, stopAllSessions, } from "@telorun/runner-core";
|
|
2
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
3
|
+
import { BundleStore } from "./bundle-store.js";
|
|
4
|
+
import { kubernetesRunnerCapabilities } from "./capabilities.js";
|
|
5
|
+
import { loadK8sRunnerConfig, RunnerConfigError } from "./config.js";
|
|
6
|
+
import { createKubernetesBackend } from "./k8s/backend.js";
|
|
7
|
+
import { createKubeClient } from "./k8s/client.js";
|
|
8
|
+
const VERSION = packageJson.version;
|
|
9
|
+
export async function buildServer(deps) {
|
|
10
|
+
const { catalog } = deps;
|
|
11
|
+
// Load terms once; the capabilities getter is re-resolved per request so a
|
|
12
|
+
// refreshed catalog (new tags) shows up without restarting the runner.
|
|
13
|
+
const terms = loadTermsFromEnv(process.env);
|
|
14
|
+
const handle = await coreBuildServer({
|
|
15
|
+
backend: deps.backend,
|
|
16
|
+
config: deps.config,
|
|
17
|
+
version: VERSION,
|
|
18
|
+
capabilities: () => kubernetesRunnerCapabilities(deps.config.defaultImage, terms, catalog?.current()),
|
|
19
|
+
defaultRegistryUrl: process.env.TELO_REGISTRY_URL,
|
|
20
|
+
validateConfig: catalog
|
|
21
|
+
? (sessionConfig) => catalog.isAllowed(sessionConfig.image)
|
|
22
|
+
? undefined
|
|
23
|
+
: `base image '${sessionConfig.image}' is not offered by this runner. ` +
|
|
24
|
+
`Allowed images: ${catalog.current().join(", ")}`
|
|
25
|
+
: undefined,
|
|
26
|
+
});
|
|
27
|
+
// Mount the internal, tokenized fetch route on the same app so a build Job's
|
|
28
|
+
// initContainer can pull the build-context tarball (bundle + Dockerfile).
|
|
29
|
+
deps.bundleStore.registerRoute(handle.app);
|
|
30
|
+
return handle;
|
|
31
|
+
}
|
|
32
|
+
async function main() {
|
|
33
|
+
let config;
|
|
34
|
+
try {
|
|
35
|
+
config = loadK8sRunnerConfig(process.env);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof RunnerConfigError) {
|
|
39
|
+
process.stderr.write(`${err.message}\n`);
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
const kube = createKubeClient();
|
|
45
|
+
const bundleStore = new BundleStore(config.selfUrl);
|
|
46
|
+
const backend = createKubernetesBackend({ kube, config, bundleStore });
|
|
47
|
+
const catalog = config.baseImageCatalog.enabled
|
|
48
|
+
? new BaseImageCatalog({
|
|
49
|
+
repository: config.baseImageCatalog.repository,
|
|
50
|
+
defaultRef: config.defaultImage,
|
|
51
|
+
filter: config.baseImageCatalog.filter,
|
|
52
|
+
limit: config.baseImageCatalog.limit,
|
|
53
|
+
refreshIntervalMs: config.baseImageCatalog.refreshIntervalMs,
|
|
54
|
+
})
|
|
55
|
+
: undefined;
|
|
56
|
+
const { app, registry } = await buildServer({ backend, config, bundleStore, catalog });
|
|
57
|
+
// Populate the catalog before serving so the first /v1/capabilities carries the
|
|
58
|
+
// full menu; a fetch failure degrades to the default image (surfaced, not
|
|
59
|
+
// swallowed) and the periodic refresh retries.
|
|
60
|
+
if (catalog) {
|
|
61
|
+
await catalog
|
|
62
|
+
.refresh()
|
|
63
|
+
.catch((err) => app.log.warn({ err }, "initial base-image catalog refresh failed; serving default image only"));
|
|
64
|
+
catalog.start((err) => app.log.warn({ err }, "base-image catalog refresh failed"));
|
|
65
|
+
}
|
|
66
|
+
// Reap pods orphaned by a prior runner process (in-memory registry).
|
|
67
|
+
if (backend.reapOrphans) {
|
|
68
|
+
await backend.reapOrphans().catch((err) => app.log.warn({ err }, "orphan reap failed"));
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await app.listen({ port: config.port, host: "0.0.0.0" });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
app.log.error(err);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const shutdown = async (signal) => {
|
|
78
|
+
app.log.info({ signal }, "shutting down");
|
|
79
|
+
catalog?.stop();
|
|
80
|
+
for (const entry of registry.list())
|
|
81
|
+
entry.userStopped = true;
|
|
82
|
+
await app.close();
|
|
83
|
+
await stopAllSessions(registry, app.log);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
};
|
|
86
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
87
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
88
|
+
}
|
|
89
|
+
const isEntrypoint = import.meta.url === `file://${process.argv[1]}`;
|
|
90
|
+
if (isEntrypoint) {
|
|
91
|
+
void main();
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/tar.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RunBundle } from "@telorun/runner-core";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal ustar + gzip writer — enough to ship a Telo bundle to a busybox
|
|
4
|
+
* `tar xzf` initContainer without pulling a tar dependency. Bundle paths are
|
|
5
|
+
* re-normalized (traversal-guarded) before they enter the archive. Gzip runs
|
|
6
|
+
* async so a large, user-controlled bundle doesn't block the event loop (and
|
|
7
|
+
* the runner's HTTP handling) during session creation.
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeBundleTarGz(bundle: RunBundle): Promise<Buffer>;
|
|
10
|
+
//# sourceMappingURL=tar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tar.d.ts","sourceRoot":"","sources":["../src/tar.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAK3E;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBxE"}
|
package/dist/tar.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { gzip } from "node:zlib";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { normalizeBundlePath } from "@telorun/runner-core";
|
|
4
|
+
const BLOCK = 512;
|
|
5
|
+
const gzipAsync = promisify(gzip);
|
|
6
|
+
/**
|
|
7
|
+
* Minimal ustar + gzip writer — enough to ship a Telo bundle to a busybox
|
|
8
|
+
* `tar xzf` initContainer without pulling a tar dependency. Bundle paths are
|
|
9
|
+
* re-normalized (traversal-guarded) before they enter the archive. Gzip runs
|
|
10
|
+
* async so a large, user-controlled bundle doesn't block the event loop (and
|
|
11
|
+
* the runner's HTTP handling) during session creation.
|
|
12
|
+
*/
|
|
13
|
+
export async function makeBundleTarGz(bundle) {
|
|
14
|
+
const blocks = [];
|
|
15
|
+
for (const file of bundle.files) {
|
|
16
|
+
const name = normalizeBundlePath(file.relativePath);
|
|
17
|
+
// ustar names are capped at 100 bytes; truncating would risk path
|
|
18
|
+
// collisions, so reject instead of silently shortening.
|
|
19
|
+
if (Buffer.byteLength(name, "utf8") > 100) {
|
|
20
|
+
throw new Error(`bundle path too long for tar (max 100 bytes): '${name}'`);
|
|
21
|
+
}
|
|
22
|
+
const content = Buffer.from(file.contents, "utf8");
|
|
23
|
+
blocks.push(makeHeader(name, content.byteLength));
|
|
24
|
+
blocks.push(padToBlock(content));
|
|
25
|
+
}
|
|
26
|
+
// Two zero blocks terminate the archive.
|
|
27
|
+
blocks.push(Buffer.alloc(BLOCK * 2));
|
|
28
|
+
return gzipAsync(Buffer.concat(blocks));
|
|
29
|
+
}
|
|
30
|
+
function makeHeader(name, size) {
|
|
31
|
+
const header = Buffer.alloc(BLOCK);
|
|
32
|
+
writeString(header, name, 0, 100);
|
|
33
|
+
writeOctal(header, 0o644, 100, 8); // mode
|
|
34
|
+
writeOctal(header, 0, 108, 8); // uid
|
|
35
|
+
writeOctal(header, 0, 116, 8); // gid
|
|
36
|
+
writeOctal(header, size, 124, 12); // size
|
|
37
|
+
writeOctal(header, 0, 136, 12); // mtime (0 — reproducible)
|
|
38
|
+
header.write(" ", 148, 8, "ascii"); // checksum placeholder (spaces)
|
|
39
|
+
header.write("0", 156, 1, "ascii"); // typeflag: regular file
|
|
40
|
+
header.write("ustar\0", 257, 6, "ascii"); // magic
|
|
41
|
+
header.write("00", 263, 2, "ascii"); // version
|
|
42
|
+
let sum = 0;
|
|
43
|
+
for (let i = 0; i < BLOCK; i++)
|
|
44
|
+
sum += header[i];
|
|
45
|
+
writeOctal(header, sum, 148, 8);
|
|
46
|
+
return header;
|
|
47
|
+
}
|
|
48
|
+
function padToBlock(content) {
|
|
49
|
+
const remainder = content.byteLength % BLOCK;
|
|
50
|
+
if (remainder === 0)
|
|
51
|
+
return content;
|
|
52
|
+
return Buffer.concat([content, Buffer.alloc(BLOCK - remainder)]);
|
|
53
|
+
}
|
|
54
|
+
function writeString(buf, value, offset, length) {
|
|
55
|
+
buf.write(value, offset, length, "utf8");
|
|
56
|
+
}
|
|
57
|
+
/** ustar numeric fields are zero-padded octal, NUL-terminated. */
|
|
58
|
+
function writeOctal(buf, value, offset, length) {
|
|
59
|
+
const octal = value.toString(8);
|
|
60
|
+
const padded = octal.padStart(length - 1, "0").slice(-(length - 1));
|
|
61
|
+
buf.write(padded + "\0", offset, length, "ascii");
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=tar.js.map
|
package/dist/tar.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tar.js","sourceRoot":"","sources":["../src/tar.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telorun/k8s-runner",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Telo Kubernetes Runner - HTTP service that runs Telo Applications as sandboxed Kubernetes Pods. A backend over @telorun/runner-core, sibling to docker-runner.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"telo",
|
|
7
|
+
"kubernetes",
|
|
8
|
+
"runner"
|
|
9
|
+
],
|
|
10
|
+
"author": "Bartosz Pasiński <bartosz.pasinski@codenet.pl>",
|
|
11
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/telorun/telo.git",
|
|
15
|
+
"directory": "apps/k8s-runner"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/server.js",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/**",
|
|
21
|
+
"src/**"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@kubernetes/client-node": "^1.4.0",
|
|
25
|
+
"fastify": "^5.7.2",
|
|
26
|
+
"@telorun/runner-core": "0.5.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"tsx": "^4.19.2",
|
|
31
|
+
"typescript": "^5.0.0",
|
|
32
|
+
"vitest": "^2.1.8"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"dev": "tsx watch src/server.ts",
|
|
37
|
+
"start": "node dist/server.js",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { FastifyInstance } from "fastify";
|
|
4
|
+
import type { RunBundle } from "@telorun/runner-core";
|
|
5
|
+
|
|
6
|
+
import { makeBundleTarGz } from "./tar.js";
|
|
7
|
+
|
|
8
|
+
interface StoredBundle {
|
|
9
|
+
token: string;
|
|
10
|
+
tarGz: Buffer;
|
|
11
|
+
evictTimer: NodeJS.Timeout;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Staged bundles are dropped after this long even if never fetched, so a
|
|
15
|
+
* stuck/never-started Pod can't grow the store unbounded. */
|
|
16
|
+
const STAGING_TTL_MS = 5 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Holds image-build contexts in memory and serves them over a tokenized,
|
|
20
|
+
* cluster-internal URL the build Job's initContainer fetches once. The
|
|
21
|
+
* per-build unguessable token prevents cross-build disclosure; the entry is
|
|
22
|
+
* dropped after first fetch (or explicitly on cleanup).
|
|
23
|
+
*/
|
|
24
|
+
export class BundleStore {
|
|
25
|
+
private readonly bundles = new Map<string, StoredBundle>();
|
|
26
|
+
|
|
27
|
+
constructor(private readonly selfUrl: string) {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stages an image-build context — the bundle plus a generated `Dockerfile` at
|
|
31
|
+
* the context root — and returns the tokenized, single-use URL the build Job's
|
|
32
|
+
* initContainer fetches. Keyed by `id` (a build id).
|
|
33
|
+
*/
|
|
34
|
+
async stageBuildContext(id: string, bundle: RunBundle, dockerfile: string): Promise<string> {
|
|
35
|
+
return this.stage(id, {
|
|
36
|
+
entryRelativePath: bundle.entryRelativePath,
|
|
37
|
+
files: [...bundle.files, { relativePath: "Dockerfile", contents: dockerfile }],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stages the raw session bundle (no Dockerfile) and returns the tokenized,
|
|
43
|
+
* single-use URL the session Pod's body-delivery initContainer fetches into
|
|
44
|
+
* `/app`. Keyed by `sessionId`. The image is keyed only on the dependency
|
|
45
|
+
* closure, so the per-session body is delivered here at boot rather than baked.
|
|
46
|
+
*/
|
|
47
|
+
async stageSessionBundle(sessionId: string, bundle: RunBundle): Promise<string> {
|
|
48
|
+
return this.stage(sessionId, bundle);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async stage(id: string, bundle: RunBundle): Promise<string> {
|
|
52
|
+
this.drop(id);
|
|
53
|
+
const token = randomBytes(24).toString("hex");
|
|
54
|
+
const tarGz = await makeBundleTarGz(bundle);
|
|
55
|
+
const evictTimer = setTimeout(() => this.bundles.delete(id), STAGING_TTL_MS);
|
|
56
|
+
evictTimer.unref?.();
|
|
57
|
+
this.bundles.set(id, { token, tarGz, evictTimer });
|
|
58
|
+
return `${this.selfUrl}/internal/bundles/${id}?token=${token}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
drop(sessionId: string): void {
|
|
62
|
+
const existing = this.bundles.get(sessionId);
|
|
63
|
+
if (existing) clearTimeout(existing.evictTimer);
|
|
64
|
+
this.bundles.delete(sessionId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private take(sessionId: string, token: string): Buffer | null {
|
|
68
|
+
const stored = this.bundles.get(sessionId);
|
|
69
|
+
if (!stored) return null;
|
|
70
|
+
if (!constantTimeEqual(stored.token, token)) return null;
|
|
71
|
+
// Single-use: drop after a successful authenticated fetch.
|
|
72
|
+
this.drop(sessionId);
|
|
73
|
+
return stored.tarGz;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Registers `GET /internal/bundles/:id` on the runner's Fastify app. */
|
|
77
|
+
registerRoute(app: FastifyInstance): void {
|
|
78
|
+
app.get<{ Params: { id: string }; Querystring: { token?: string } }>(
|
|
79
|
+
"/internal/bundles/:id",
|
|
80
|
+
async (req, reply) => {
|
|
81
|
+
const token = req.query.token ?? "";
|
|
82
|
+
const tarGz = this.take(req.params.id, token);
|
|
83
|
+
if (!tarGz) {
|
|
84
|
+
reply.code(404).send({ error: "not_found" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
reply
|
|
88
|
+
.header("content-type", "application/gzip")
|
|
89
|
+
.header("cache-control", "no-store")
|
|
90
|
+
.send(tarGz);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function constantTimeEqual(a: string, b: string): boolean {
|
|
97
|
+
const ab = Buffer.from(a);
|
|
98
|
+
const bb = Buffer.from(b);
|
|
99
|
+
if (ab.byteLength !== bb.byteLength) return false;
|
|
100
|
+
return timingSafeEqual(ab, bb);
|
|
101
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sessionConfigSchema,
|
|
3
|
+
type RunnerCapabilities,
|
|
4
|
+
type RunnerTerms,
|
|
5
|
+
} from "@telorun/runner-core";
|
|
6
|
+
|
|
7
|
+
/** What k8s-runner advertises on `/v1/capabilities`. The runner serves
|
|
8
|
+
* untrusted/anonymous code under a hard-ceiling policy. `image` is a base-image
|
|
9
|
+
* picker: when an `imageEnum` is supplied (the resolved Docker Hub catalog) the
|
|
10
|
+
* editor renders an editable dropdown constrained to that allowlist — which the
|
|
11
|
+
* session route re-validates server-side. Without a catalog (disabled /
|
|
12
|
+
* first-fetch failure) the list collapses to the single `defaultImage` (locked
|
|
13
|
+
* via `enforced`). `pullPolicy` is client-editable: `always` re-pulls the base
|
|
14
|
+
* image when its tag has moved upstream (rebuilding the per-app image), which
|
|
15
|
+
* is how a picked moving tag like `latest-slim` stays current.
|
|
16
|
+
*
|
|
17
|
+
* `terms`, when set (operator-provided via RUNNER_TERMS_*), are enforced: a
|
|
18
|
+
* session won't start until the client acknowledges the current version. */
|
|
19
|
+
export function kubernetesRunnerCapabilities(
|
|
20
|
+
defaultImage: string,
|
|
21
|
+
terms?: RunnerTerms,
|
|
22
|
+
imageEnum?: string[],
|
|
23
|
+
): RunnerCapabilities {
|
|
24
|
+
return {
|
|
25
|
+
displayName: "Telo Public Cloud",
|
|
26
|
+
description:
|
|
27
|
+
"Runs the Telo application in sandboxed cloud environment with capped CPU and memory.",
|
|
28
|
+
config: {
|
|
29
|
+
schema: sessionConfigSchema({
|
|
30
|
+
imageDefault: defaultImage,
|
|
31
|
+
enforced: true,
|
|
32
|
+
imageEnum,
|
|
33
|
+
pullPolicyDescription:
|
|
34
|
+
"Base-image freshness. `always` rebuilds the session image when the base tag has moved upstream (Docker Hub only); `missing` and `never` reuse the cached build.",
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
features: { io: true, ports: true },
|
|
38
|
+
terms,
|
|
39
|
+
};
|
|
40
|
+
}
|