@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.
Files changed (63) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +206 -0
  3. package/dist/bundle-store.d.ts +32 -0
  4. package/dist/bundle-store.d.ts.map +1 -0
  5. package/dist/bundle-store.js +86 -0
  6. package/dist/bundle-store.js.map +1 -0
  7. package/dist/capabilities.d.ts +15 -0
  8. package/dist/capabilities.d.ts.map +1 -0
  9. package/dist/capabilities.js +30 -0
  10. package/dist/capabilities.js.map +1 -0
  11. package/dist/config.d.ts +95 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +76 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/k8s/backend.d.ts +19 -0
  16. package/dist/k8s/backend.d.ts.map +1 -0
  17. package/dist/k8s/backend.js +488 -0
  18. package/dist/k8s/backend.js.map +1 -0
  19. package/dist/k8s/client.d.ts +16 -0
  20. package/dist/k8s/client.d.ts.map +1 -0
  21. package/dist/k8s/client.js +24 -0
  22. package/dist/k8s/client.js.map +1 -0
  23. package/dist/k8s/image-build.d.ts +105 -0
  24. package/dist/k8s/image-build.d.ts.map +1 -0
  25. package/dist/k8s/image-build.js +432 -0
  26. package/dist/k8s/image-build.js.map +1 -0
  27. package/dist/k8s/ingress.d.ts +15 -0
  28. package/dist/k8s/ingress.d.ts.map +1 -0
  29. package/dist/k8s/ingress.js +102 -0
  30. package/dist/k8s/ingress.js.map +1 -0
  31. package/dist/k8s/pod-spec.d.ts +41 -0
  32. package/dist/k8s/pod-spec.d.ts.map +1 -0
  33. package/dist/k8s/pod-spec.js +146 -0
  34. package/dist/k8s/pod-spec.js.map +1 -0
  35. package/dist/limits.d.ts +25 -0
  36. package/dist/limits.d.ts.map +1 -0
  37. package/dist/limits.js +58 -0
  38. package/dist/limits.js.map +1 -0
  39. package/dist/server.d.ts +13 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +93 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/tar.d.ts +10 -0
  44. package/dist/tar.d.ts.map +1 -0
  45. package/dist/tar.js +63 -0
  46. package/dist/tar.js.map +1 -0
  47. package/package.json +41 -0
  48. package/src/bundle-store.ts +101 -0
  49. package/src/capabilities.ts +40 -0
  50. package/src/config.ts +201 -0
  51. package/src/k8s/backend-failure.test.ts +148 -0
  52. package/src/k8s/backend.ts +535 -0
  53. package/src/k8s/client.ts +39 -0
  54. package/src/k8s/image-build.test.ts +244 -0
  55. package/src/k8s/image-build.ts +547 -0
  56. package/src/k8s/ingress.test.ts +146 -0
  57. package/src/k8s/ingress.ts +127 -0
  58. package/src/k8s/pod-spec.ts +180 -0
  59. package/src/limits.test.ts +59 -0
  60. package/src/limits.ts +90 -0
  61. package/src/server.ts +124 -0
  62. package/src/tar.test.ts +55 -0
  63. package/src/tar.ts +68 -0
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ # SUSTAINABLE USE LICENSE (Fair-code)
2
+
3
+ Copyright (c) 2026 CodeNet Sp. z o.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
6
+
7
+ 1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
8
+
9
+ 2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
10
+
11
+ 3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
12
+
13
+ 4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
14
+
15
+ 5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
16
+
17
+ For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact <contact@codenet.pl>.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # @telorun/k8s-runner
2
+
3
+ HTTP service that runs Telo Applications as sandboxed **Kubernetes Pods**. A
4
+ backend over [`@telorun/runner-core`](../../packages/runner-core), sibling to
5
+ [`docker-runner`](../docker-runner) — it presents the identical `/v1` session
6
+ contract (`/v1/health`, `/v1/capabilities`, `/v1/probe`, `/v1/sessions`,
7
+ `/v1/sessions/:id/events` SSE, `/v1/sessions/:id/io` WS) but spawns a Pod per
8
+ session instead of a container. On `/v1/capabilities` it advertises `image` /
9
+ `pullPolicy` as **`readOnly`** (server-enforced — the runner serves untrusted
10
+ code under a hard-ceiling policy), so the editor lets the user edit only the
11
+ runner URL.
12
+
13
+ ## ⚠️ Security posture
14
+
15
+ The runner is a **dumb executor**: no auth, no per-IP concurrency, no rate
16
+ limiting — only a global `RUNNER_MAX_SESSIONS` backstop. **Do not expose it
17
+ directly to anonymous internet traffic.** Front it with the Telo Cloud control
18
+ plane (or an equivalent abuse-control proxy), which resolves identity → tier →
19
+ limits and forwards to the runner. Standalone, it is safe only on a
20
+ trusted/internal network.
21
+
22
+ Resource limits are **hard ceilings**: a request may ask for *less* than the
23
+ configured cap but never more (`min(requested, ceiling)`). For a bare runner
24
+ serving an anonymous tier, the ceiling *is* the policy.
25
+
26
+ ## How it works
27
+
28
+ Per session the runner resolves the image, creates a Pod (`telo run`), watches it
29
+ for status, attaches a PTY over the Pod `attach` subresource for the interactive
30
+ `/io` channel, and — when an ingress base domain is configured — creates a
31
+ per-session Service + Ingress (`<sessionId>.<base-domain>`) garbage-collected via
32
+ an ownerReference to the Pod.
33
+
34
+ **Every session runs a prebuilt per-app image** — there is no in-pod install
35
+ path. On a session-create the runner stages the bundle plus a generated
36
+ `Dockerfile` (`FROM <base>`, `ENV TELO_CACHE_DIR=/telo-cache`, `RUN telo install`),
37
+ runs a trusted **Kaniko** Job in the `telo-builds` namespace that bakes every
38
+ controller and module manifest into `/telo-cache/{manifests,npm}` and pushes the
39
+ result, then runs the session pod from that image — so the session never installs
40
+ anything on the start path and a slow/unreachable package registry can't stall it.
41
+
42
+ **The image is keyed on the dependency closure, not the whole bundle** — the
43
+ `imports:` set plus any controllers declared by inline `Telo.Definition` docs
44
+ (see `extractDependencyKey`). A body-only edit (resource config, CEL) reuses the
45
+ existing image; only an import or controller change rebuilds. The per-session
46
+ **body is delivered at boot**, not baked: a body-fetch initContainer untars the
47
+ staged bundle into a writable `/app` emptyDir, and the session runs `telo run
48
+ /app/<entry> --no-cache-write` — reading the baked deps from the read-only
49
+ `/telo-cache` and validating in-memory, so `readOnlyRootFilesystem` stays on with
50
+ nothing written to the cache. Builds are existence-checked before building and
51
+ single-flighted; a build failure surfaces as an actionable error carrying the
52
+ build pod's log tail. `RUNNER_IMAGE_REPOSITORY` is therefore **required** — the
53
+ runner refuses to start without a registry to build into.
54
+
55
+ **Coming-up progress** is reported over the `/v1` SSE stream as `progress` events
56
+ (`build` → `provision`) while the session is still `starting`. The session is
57
+ created with a fast `201` carrying the `streamUrl` **before** the build runs, so
58
+ the client connects immediately and sees build + provision progress live; the
59
+ backend then runs in the background and a start failure surfaces as a terminal
60
+ `failed` status on the stream. The session flips to `running` when the Pod reaches
61
+ `Running`.
62
+
63
+ Sandbox hardening is always on (non-root, read-only rootfs, drop-all caps, no
64
+ service-account token, seccomp `RuntimeDefault`); a sandbox RuntimeClass
65
+ (gVisor/Kata) is layered on when configured. In the prebuilt path the per-app
66
+ image is single-tenant, so install scripts run normally inside the trusted build
67
+ (native/postinstall controllers work) with no cross-tenant cache to poison.
68
+
69
+ ## Configuration (env)
70
+
71
+ | Env | Default | Purpose |
72
+ | --- | --- | --- |
73
+ | `RUNNER_SELF_URL` | _(required)_ | Runner's in-cluster base URL (bundle fetch) |
74
+ | `PORT` | `8062` | HTTP listen port |
75
+ | `RUNNER_SESSION_NAMESPACE` | `telo-sessions` | Namespace for session objects |
76
+ | `RUNNER_IMAGE` | `telorun/node:latest-slim` | Default base image; always offered in the picker and the fallback when the catalog is unreachable |
77
+ | `RUNNER_INIT_IMAGE` | `busybox:stable` | Build-context fetch initContainer image (wget + tar) |
78
+ | `RUNNER_RUNTIME_CLASS` | _(unset → runc)_ | Sandbox RuntimeClass (gvisor/kata) |
79
+ | `SESSION_INGRESS_BASE_DOMAIN` | _(unset → logs-only)_ | Wildcard base for per-session ingress |
80
+ | `SESSION_INGRESS_TLS_SECRET` | _(unset → no TLS block)_ | `kubernetes.io/tls` Secret (in `telo-sessions`) the session Ingress presents; must cover `*.<base-domain>`. Set for Cloudflare Full (Strict) / any origin-cert upstream |
81
+ | `RUNNER_MAX_CPU` | `50m` | CPU ceiling |
82
+ | `RUNNER_MAX_MEMORY` | `100Mi` | Memory ceiling |
83
+ | `RUNNER_MAX_TTL_SECONDS` | `3600` | Wall-clock TTL (Pod `activeDeadlineSeconds`) |
84
+ | `RUNNER_MAX_EPHEMERAL_STORAGE` | `512Mi` | Per-Pod ephemeral-storage ceiling |
85
+ | `RUNNER_MAX_SESSIONS` | `32` | Global session backstop; at capacity the oldest exited session is evicted before a new run is rejected |
86
+ | `RUNNER_EXIT_TTL_MS` | `14400000` | How long exited sessions stay in the registry (so the editor can re-attach and replay their history after a reload) before eviction |
87
+ | `RUNNER_TERMS_FILE` | _(unset)_ | Path to the agreement file (plain text / markdown), read at startup — mount it from a `ConfigMap` (e.g. `/etc/telo/terms.md`). Setting this (or `RUNNER_TERMS_BODY`) enables terms: the runner advertises them on `/v1/capabilities` and rejects `POST /v1/sessions` with `428` unless the client sends `x-telo-accepted-terms` matching the version. An unreadable path fails startup. The public cloud should set this |
88
+ | `RUNNER_TERMS_BODY` | _(unset)_ | Inline agreement text, for short notes; ignored when `RUNNER_TERMS_FILE` is set |
89
+ | `RUNNER_TERMS_TITLE` | `Usage agreement` | Heading shown above the agreement |
90
+ | `RUNNER_TERMS_VERSION` | _(hash of body)_ | Acceptance version; defaults to a content hash so any edit to the body automatically re-prompts every client. Set explicitly only to control material-change vs typo |
91
+
92
+ ### Base-image picker
93
+
94
+ The runner advertises a menu of base images on `/v1/capabilities`, resolved from
95
+ a Docker Hub repo's tags (filtered) and cached. The editor renders it as an
96
+ editable `image` dropdown; the chosen image is **re-validated server-side**
97
+ against the same list, so a client that skips the editor can't widen the set.
98
+ `RUNNER_IMAGE` is always offered and is the fallback when Docker Hub is
99
+ unreachable. Disable the catalog to lock `image` to `RUNNER_IMAGE`.
100
+
101
+ Pinned tags (e.g. `0.30.1-slim`) are immutable. A picked **moving** tag like
102
+ `latest-slim` only refreshes when the session's `pullPolicy` is `always`: the
103
+ build then pins the per-app image to the base's current digest, so a moved tag
104
+ yields a new image and rebuilds (otherwise the cached build — keyed on the tag
105
+ string — is reused). Movement detection reads the digest from Docker Hub, so a
106
+ base hosted elsewhere (GHCR, a private registry) can't be tracked — `always`
107
+ degrades to reusing the cached build for it, same as `missing` / `never`.
108
+
109
+ | Env | Default | Purpose |
110
+ | --- | --- | --- |
111
+ | `RUNNER_BASE_IMAGE_CATALOG_ENABLED` | `true` | Resolve + advertise the picker; `false` locks `image` to `RUNNER_IMAGE` |
112
+ | `RUNNER_BASE_IMAGE_REPO` | `telorun/node` | `namespace/repository` queried on Docker Hub |
113
+ | `RUNNER_BASE_IMAGE_PINNED_ONLY` | `true` | Keep only pinned `MAJOR.MINOR.PATCH[-variant]` tags (drops `latest`, `0`, `0.30`) |
114
+ | `RUNNER_BASE_IMAGE_EXCLUDE_SHA` | `true` | Drop commit-hash tags |
115
+ | `RUNNER_BASE_IMAGE_EXCLUDE_PRERELEASE` | `true` | Drop semver prereleases (`-rc.1`, `-alpha`); `-slim` / `-rust-*` variants are kept |
116
+ | `RUNNER_BASE_IMAGE_INCLUDE` | _(unset)_ | Regex a tag must match (escape hatch) |
117
+ | `RUNNER_BASE_IMAGE_EXCLUDE` | _(unset)_ | Regex that drops a matching tag (escape hatch) |
118
+ | `RUNNER_BASE_IMAGE_LIMIT` | `20` | Cap on advertised tags (newest first) |
119
+ | `RUNNER_BASE_IMAGE_REFRESH_SECONDS` | `3600` | Catalog re-fetch cadence |
120
+
121
+ ### Image build (required)
122
+
123
+ | Env | Default | Purpose |
124
+ | --- | --- | --- |
125
+ | `RUNNER_IMAGE_REPOSITORY` | _(required)_ | Registry repo for per-app images; tag = bundle hash |
126
+ | `RUNNER_BUILD_NAMESPACE` | `telo-builds` | Namespace the trusted Kaniko build Jobs run in |
127
+ | `RUNNER_BUILDER_IMAGE` | `gcr.io/kaniko-project/executor:latest` | Image builder |
128
+ | `RUNNER_BUILD_TIMEOUT_SECONDS` | `600` | Build Job deadline / wait budget |
129
+ | `RUNNER_REGISTRY_INSECURE` | `false` | Push/pull over HTTP / self-signed |
130
+ | `RUNNER_REGISTRY_API_URL` | _(unset → always build)_ | HTTP(S) base for the manifest existence check (authenticated via the push Secret) |
131
+ | `RUNNER_REGISTRY_PUSH_SECRET` | _(unset)_ | dockerconfig Secret (in `telo-builds`) Kaniko pushes with; also authenticates the existence check |
132
+ | `RUNNER_IMAGE_PULL_SECRET` | _(unset)_ | dockerconfig Secret (in `telo-sessions`) the kubelet pulls per-app images with |
133
+ | `TELO_REGISTRY_URL` | `https://registry.telo.run` | Telo module registry used by `telo install` |
134
+
135
+ ## Deploy (Helm)
136
+
137
+ The runner **requires a registry to build into** (there is no in-pod install
138
+ fallback), so point it at one your cluster's kubelet can pull from:
139
+
140
+ ```bash
141
+ helm install telo-runner ./chart \
142
+ --set build.repository=registry.example.com/telo-sessions \
143
+ --set-file registry.dockerconfigjson=./dockerconfig.json \ # private-registry auth
144
+ --set session.runtimeClass=gvisor
145
+ ```
146
+
147
+ For a private registry, `registry.dockerconfigjson` creates the dockerconfig
148
+ Secret in **both** namespaces (push in `telo-builds`, pull in `telo-sessions`) and
149
+ wires `RUNNER_REGISTRY_PUSH_SECRET` + `RUNNER_IMAGE_PULL_SECRET` — the kubelet
150
+ needs the pull copy because the per-app image is private. (Or manage the Secrets
151
+ yourself and reference them via `build.pushSecretName` / `build.pullSecretName`.)
152
+ The push Secret doubles as the credential for the manifest existence check, so
153
+ the runner can see an already-built image in a private registry and skip the
154
+ rebuild — without it a private registry answers `401` and every run rebuilds.
155
+ A no-auth registry needs none of this.
156
+
157
+ The chart provisions the static scaffolding: the runner Deployment (single
158
+ replica — the registry is in-memory and the runner reaps orphaned pods on boot),
159
+ Service, scoped RBAC, the `telo-runner` / restricted-PSS `telo-sessions` /
160
+ baseline-PSS `telo-builds` namespaces, a `ResourceQuota`, and NetworkPolicies
161
+ (session pod-to-pod isolation + the build namespace's registry egress). The
162
+ runner creates per-session and per-build objects at runtime.
163
+
164
+ The optional in-cluster registry (`--set registry.enabled=true --set
165
+ build.insecureRegistry=true`) derives `build.repository` for you, but works only
166
+ on clusters whose **nodes are configured to trust it** — otherwise an external/
167
+ cloud registry is simpler. Installing with neither a `build.repository` nor
168
+ `registry.enabled` is rejected at template time.
169
+
170
+ ### Origin TLS (Cloudflare et al.)
171
+
172
+ To have the per-session Ingress present an origin cert (so an upstream like
173
+ Cloudflare in **Full (Strict)** mode validates the origin), give the chart a
174
+ `kubernetes.io/tls` Secret in `telo-sessions`. The cert must cover the wildcard
175
+ `*.<sessionIngress.baseDomain>` — session hosts are a single label
176
+ (`<port>-<sessionId>.<base-domain>`). Two ways:
177
+
178
+ ```bash
179
+ # A — reference a Secret you manage in telo-sessions (cert-manager, your own sync)
180
+ helm install telo-runner ./chart --set sessionIngress.tls.secretName=telo-origin-tls
181
+
182
+ # B — let the chart create the Secret from your cert + key
183
+ helm install telo-runner ./chart \
184
+ --set-file sessionIngress.tls.cert=origin.pem \
185
+ --set-file sessionIngress.tls.key=origin.key
186
+ ```
187
+
188
+ Either wires `SESSION_INGRESS_TLS_SECRET`, and the runner stamps a `spec.tls`
189
+ block on every session Ingress. Leave all three empty to skip TLS at the origin
190
+ (terminated entirely upstream).
191
+
192
+ > **Egress notes.** (1) The kubelet pulls session images directly and does **not**
193
+ > use cluster DNS, so the in-cluster registry only works where nodes trust it
194
+ > (e.g. containerd `registries.conf`). (2) Core NetworkPolicy is CIDR-only and
195
+ > cannot express the package-registry FQDN allowlist the build namespace needs —
196
+ > use a CNI with FQDN policy (Cilium) or an egress proxy to tighten it.
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ pnpm --filter @telorun/k8s-runner build # tsc
202
+ pnpm --filter @telorun/k8s-runner test # vitest (limits clamp, tar, bundle token)
203
+ ```
204
+
205
+ The Kubernetes backend can't be exercised without a cluster; unit tests cover the
206
+ backend-independent logic (limit clamping, the tar writer, bundle tokens).
@@ -0,0 +1,32 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { RunBundle } from "@telorun/runner-core";
3
+ /**
4
+ * Holds image-build contexts in memory and serves them over a tokenized,
5
+ * cluster-internal URL the build Job's initContainer fetches once. The
6
+ * per-build unguessable token prevents cross-build disclosure; the entry is
7
+ * dropped after first fetch (or explicitly on cleanup).
8
+ */
9
+ export declare class BundleStore {
10
+ private readonly selfUrl;
11
+ private readonly bundles;
12
+ constructor(selfUrl: string);
13
+ /**
14
+ * Stages an image-build context — the bundle plus a generated `Dockerfile` at
15
+ * the context root — and returns the tokenized, single-use URL the build Job's
16
+ * initContainer fetches. Keyed by `id` (a build id).
17
+ */
18
+ stageBuildContext(id: string, bundle: RunBundle, dockerfile: string): Promise<string>;
19
+ /**
20
+ * Stages the raw session bundle (no Dockerfile) and returns the tokenized,
21
+ * single-use URL the session Pod's body-delivery initContainer fetches into
22
+ * `/app`. Keyed by `sessionId`. The image is keyed only on the dependency
23
+ * closure, so the per-session body is delivered here at boot rather than baked.
24
+ */
25
+ stageSessionBundle(sessionId: string, bundle: RunBundle): Promise<string>;
26
+ private stage;
27
+ drop(sessionId: string): void;
28
+ private take;
29
+ /** Registers `GET /internal/bundles/:id` on the runner's Fastify app. */
30
+ registerRoute(app: FastifyInstance): void;
31
+ }
32
+ //# sourceMappingURL=bundle-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-store.d.ts","sourceRoot":"","sources":["../src/bundle-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AActD;;;;;GAKG;AACH,qBAAa,WAAW;IAGV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;gBAE9B,OAAO,EAAE,MAAM;IAE5C;;;;OAIG;IACG,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAO3F;;;;;OAKG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;YAIjE,KAAK;IAUnB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAM7B,OAAO,CAAC,IAAI;IASZ,yEAAyE;IACzE,aAAa,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI;CAiB1C"}
@@ -0,0 +1,86 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { makeBundleTarGz } from "./tar.js";
3
+ /** Staged bundles are dropped after this long even if never fetched, so a
4
+ * stuck/never-started Pod can't grow the store unbounded. */
5
+ const STAGING_TTL_MS = 5 * 60 * 1000;
6
+ /**
7
+ * Holds image-build contexts in memory and serves them over a tokenized,
8
+ * cluster-internal URL the build Job's initContainer fetches once. The
9
+ * per-build unguessable token prevents cross-build disclosure; the entry is
10
+ * dropped after first fetch (or explicitly on cleanup).
11
+ */
12
+ export class BundleStore {
13
+ selfUrl;
14
+ bundles = new Map();
15
+ constructor(selfUrl) {
16
+ this.selfUrl = selfUrl;
17
+ }
18
+ /**
19
+ * Stages an image-build context — the bundle plus a generated `Dockerfile` at
20
+ * the context root — and returns the tokenized, single-use URL the build Job's
21
+ * initContainer fetches. Keyed by `id` (a build id).
22
+ */
23
+ async stageBuildContext(id, bundle, dockerfile) {
24
+ return this.stage(id, {
25
+ entryRelativePath: bundle.entryRelativePath,
26
+ files: [...bundle.files, { relativePath: "Dockerfile", contents: dockerfile }],
27
+ });
28
+ }
29
+ /**
30
+ * Stages the raw session bundle (no Dockerfile) and returns the tokenized,
31
+ * single-use URL the session Pod's body-delivery initContainer fetches into
32
+ * `/app`. Keyed by `sessionId`. The image is keyed only on the dependency
33
+ * closure, so the per-session body is delivered here at boot rather than baked.
34
+ */
35
+ async stageSessionBundle(sessionId, bundle) {
36
+ return this.stage(sessionId, bundle);
37
+ }
38
+ async stage(id, bundle) {
39
+ this.drop(id);
40
+ const token = randomBytes(24).toString("hex");
41
+ const tarGz = await makeBundleTarGz(bundle);
42
+ const evictTimer = setTimeout(() => this.bundles.delete(id), STAGING_TTL_MS);
43
+ evictTimer.unref?.();
44
+ this.bundles.set(id, { token, tarGz, evictTimer });
45
+ return `${this.selfUrl}/internal/bundles/${id}?token=${token}`;
46
+ }
47
+ drop(sessionId) {
48
+ const existing = this.bundles.get(sessionId);
49
+ if (existing)
50
+ clearTimeout(existing.evictTimer);
51
+ this.bundles.delete(sessionId);
52
+ }
53
+ take(sessionId, token) {
54
+ const stored = this.bundles.get(sessionId);
55
+ if (!stored)
56
+ return null;
57
+ if (!constantTimeEqual(stored.token, token))
58
+ return null;
59
+ // Single-use: drop after a successful authenticated fetch.
60
+ this.drop(sessionId);
61
+ return stored.tarGz;
62
+ }
63
+ /** Registers `GET /internal/bundles/:id` on the runner's Fastify app. */
64
+ registerRoute(app) {
65
+ app.get("/internal/bundles/:id", async (req, reply) => {
66
+ const token = req.query.token ?? "";
67
+ const tarGz = this.take(req.params.id, token);
68
+ if (!tarGz) {
69
+ reply.code(404).send({ error: "not_found" });
70
+ return;
71
+ }
72
+ reply
73
+ .header("content-type", "application/gzip")
74
+ .header("cache-control", "no-store")
75
+ .send(tarGz);
76
+ });
77
+ }
78
+ }
79
+ function constantTimeEqual(a, b) {
80
+ const ab = Buffer.from(a);
81
+ const bb = Buffer.from(b);
82
+ if (ab.byteLength !== bb.byteLength)
83
+ return false;
84
+ return timingSafeEqual(ab, bb);
85
+ }
86
+ //# sourceMappingURL=bundle-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-store.js","sourceRoot":"","sources":["../src/bundle-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK3D,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAQ3C;8DAC8D;AAC9D,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAErC;;;;;GAKG;AACH,MAAM,OAAO,WAAW;IAGO;IAFZ,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAE3D,YAA6B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;IAEhD;;;;OAIG;IACH,KAAK,CAAC,iBAAiB,CAAC,EAAU,EAAE,MAAiB,EAAE,UAAkB;QACvE,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE;YACpB,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;YAC3C,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;SAC/E,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,kBAAkB,CAAC,SAAiB,EAAE,MAAiB;QAC3D,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,EAAU,EAAE,MAAiB;QAC/C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACd,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC;QAC7E,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC;QACrB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;QACnD,OAAO,GAAG,IAAI,CAAC,OAAO,qBAAqB,EAAE,UAAU,KAAK,EAAE,CAAC;IACjE,CAAC;IAED,IAAI,CAAC,SAAiB;QACpB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,QAAQ;YAAE,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACjC,CAAC;IAEO,IAAI,CAAC,SAAiB,EAAE,KAAa;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACzD,2DAA2D;QAC3D,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACrB,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,yEAAyE;IACzE,aAAa,CAAC,GAAoB;QAChC,GAAG,CAAC,GAAG,CACL,uBAAuB,EACvB,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;YACnB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;YACpC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;gBAC7C,OAAO;YACT,CAAC;YACD,KAAK;iBACF,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAAC;iBAC1C,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC;iBACnC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CACF,CAAC;IACJ,CAAC;CACF;AAED,SAAS,iBAAiB,CAAC,CAAS,EAAE,CAAS;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1B,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,EAAE,CAAC,UAAU,KAAK,EAAE,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAClD,OAAO,eAAe,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { type RunnerCapabilities, type RunnerTerms } from "@telorun/runner-core";
2
+ /** What k8s-runner advertises on `/v1/capabilities`. The runner serves
3
+ * untrusted/anonymous code under a hard-ceiling policy. `image` is a base-image
4
+ * picker: when an `imageEnum` is supplied (the resolved Docker Hub catalog) the
5
+ * editor renders an editable dropdown constrained to that allowlist — which the
6
+ * session route re-validates server-side. Without a catalog (disabled /
7
+ * first-fetch failure) the list collapses to the single `defaultImage` (locked
8
+ * via `enforced`). `pullPolicy` is client-editable: `always` re-pulls the base
9
+ * image when its tag has moved upstream (rebuilding the per-app image), which
10
+ * is how a picked moving tag like `latest-slim` stays current.
11
+ *
12
+ * `terms`, when set (operator-provided via RUNNER_TERMS_*), are enforced: a
13
+ * session won't start until the client acknowledges the current version. */
14
+ export declare function kubernetesRunnerCapabilities(defaultImage: string, terms?: RunnerTerms, imageEnum?: string[]): RunnerCapabilities;
15
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;;6EAW6E;AAC7E,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,WAAW,EACnB,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,kBAAkB,CAiBpB"}
@@ -0,0 +1,30 @@
1
+ import { sessionConfigSchema, } from "@telorun/runner-core";
2
+ /** What k8s-runner advertises on `/v1/capabilities`. The runner serves
3
+ * untrusted/anonymous code under a hard-ceiling policy. `image` is a base-image
4
+ * picker: when an `imageEnum` is supplied (the resolved Docker Hub catalog) the
5
+ * editor renders an editable dropdown constrained to that allowlist — which the
6
+ * session route re-validates server-side. Without a catalog (disabled /
7
+ * first-fetch failure) the list collapses to the single `defaultImage` (locked
8
+ * via `enforced`). `pullPolicy` is client-editable: `always` re-pulls the base
9
+ * image when its tag has moved upstream (rebuilding the per-app image), which
10
+ * is how a picked moving tag like `latest-slim` stays current.
11
+ *
12
+ * `terms`, when set (operator-provided via RUNNER_TERMS_*), are enforced: a
13
+ * session won't start until the client acknowledges the current version. */
14
+ export function kubernetesRunnerCapabilities(defaultImage, terms, imageEnum) {
15
+ return {
16
+ displayName: "Telo Public Cloud",
17
+ description: "Runs the Telo application in sandboxed cloud environment with capped CPU and memory.",
18
+ config: {
19
+ schema: sessionConfigSchema({
20
+ imageDefault: defaultImage,
21
+ enforced: true,
22
+ imageEnum,
23
+ pullPolicyDescription: "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.",
24
+ }),
25
+ },
26
+ features: { io: true, ports: true },
27
+ terms,
28
+ };
29
+ }
30
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;;6EAW6E;AAC7E,MAAM,UAAU,4BAA4B,CAC1C,YAAoB,EACpB,KAAmB,EACnB,SAAoB;IAEpB,OAAO;QACL,WAAW,EAAE,mBAAmB;QAChC,WAAW,EACT,sFAAsF;QACxF,MAAM,EAAE;YACN,MAAM,EAAE,mBAAmB,CAAC;gBAC1B,YAAY,EAAE,YAAY;gBAC1B,QAAQ,EAAE,IAAI;gBACd,SAAS;gBACT,qBAAqB,EACnB,iKAAiK;aACpK,CAAC;SACH;QACD,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;QACnC,KAAK;KACN,CAAC;AACJ,CAAC"}
@@ -0,0 +1,95 @@
1
+ import { RunnerConfigError, type RunnerCoreConfig, type TagFilter } from "@telorun/runner-core";
2
+ export { RunnerConfigError };
3
+ /**
4
+ * Base-image catalog settings. The runner resolves the menu of base images a
5
+ * session may pick from `telorun/node`'s Docker Hub tags (configurable repo),
6
+ * filtered to taste, and advertises it as an editable `image` picker. The
7
+ * configured `defaultImage` is always offered (and is the fallback when Docker
8
+ * Hub is unreachable). Disable to lock `image` to `defaultImage` (the old
9
+ * server-enforced behaviour) — e.g. an air-gapped cluster.
10
+ */
11
+ export interface BaseImageCatalogConfig {
12
+ enabled: boolean;
13
+ /** `namespace/repository` queried on Docker Hub, e.g. `telorun/node`. */
14
+ repository: string;
15
+ filter: TagFilter;
16
+ /** Cap on advertised tags (newest first). */
17
+ limit: number;
18
+ /** Background refresh cadence in ms. */
19
+ refreshIntervalMs: number;
20
+ }
21
+ /**
22
+ * Resource limits the runner will ever grant. These are HARD CEILINGS, not
23
+ * defaults a request can exceed: the effective per-session value is
24
+ * `min(requested, ceiling)` (clamp-down only). For a bare runner serving an
25
+ * anonymous tier directly, the ceiling IS the policy.
26
+ */
27
+ export interface LimitCeilings {
28
+ /** Kubernetes CPU quantity, e.g. "50m". */
29
+ cpu: string;
30
+ /** Kubernetes memory quantity, e.g. "100Mi". */
31
+ memory: string;
32
+ /** Wall-clock TTL in seconds (Pod activeDeadlineSeconds). */
33
+ ttlSeconds: number;
34
+ /** Per-Pod ephemeral-storage limit, e.g. "512Mi". */
35
+ ephemeralStorage: string;
36
+ }
37
+ /**
38
+ * On-cluster image-build settings. The runner ALWAYS prebuilds a self-contained
39
+ * per-app image (controllers + module manifests baked in via `telo install`) and
40
+ * the session pod runs it directly — there is no in-pod install fallback, so a
41
+ * registry repository (`RUNNER_IMAGE_REPOSITORY`) is required.
42
+ */
43
+ export interface ImageBuildConfig {
44
+ /** Registry repository the per-app image is pushed to / pulled from; the tag
45
+ * is the bundle content hash (e.g. `registry.telo-runner.svc:5000/telo-sessions`). */
46
+ repository: string;
47
+ /** Namespace the trusted Kaniko build Jobs run in (registry egress lives here). */
48
+ namespace: string;
49
+ /** Builder image — Kaniko (or a compatible `--context`/`--destination` executor). */
50
+ builderImage: string;
51
+ /** Build Job `activeDeadlineSeconds` and the runner's wait budget. */
52
+ timeoutSeconds: number;
53
+ /** Push/pull to an insecure (HTTP / self-signed) registry — the in-cluster default. */
54
+ insecureRegistry: boolean;
55
+ /** HTTP(S) base for a best-effort manifest existence check (skip build on hit);
56
+ * undefined → always build. */
57
+ registryApiUrl?: string;
58
+ /** Telo module registry `telo install` resolves manifests from during the build. */
59
+ teloRegistryUrl: string;
60
+ /** Optional dockerconfig Secret (in the build namespace) Kaniko pushes with. */
61
+ pushSecretName?: string;
62
+ /** Optional dockerconfig Secret (in the session namespace) the kubelet uses to
63
+ * pull per-app images from a private registry. */
64
+ imagePullSecret?: string;
65
+ }
66
+ export interface K8sRunnerConfig extends RunnerCoreConfig {
67
+ /** Namespace where session Pods/Services/Ingresses are created. */
68
+ sessionNamespace: string;
69
+ /** Default image for spawned session Pods (telorun/node). */
70
+ defaultImage: string;
71
+ /** Small image for the bundle/build-context fetch initContainer (needs wget + tar). */
72
+ initImage: string;
73
+ /** Optional sandbox RuntimeClass (gvisor/kata). Unset → cluster default (runc). */
74
+ runtimeClass?: string;
75
+ /** Wildcard base domain for per-session ingress; unset → logs-only. */
76
+ sessionIngressBaseDomain?: string;
77
+ /** Optional IngressClass name for created session Ingresses. */
78
+ sessionIngressClassName?: string;
79
+ /** Optional `kubernetes.io/tls` Secret (in the session namespace) the per-session
80
+ * Ingress presents so an upstream (e.g. Cloudflare Full (Strict)) can validate
81
+ * the origin. Must cover the wildcard `*.<sessionIngressBaseDomain>`. Unset → no TLS block. */
82
+ sessionIngressTlsSecretName?: string;
83
+ /** On-cluster image build — the only delivery path (always present). */
84
+ build: ImageBuildConfig;
85
+ /** Runner's own in-cluster base URL, used to build the bundle/build-context
86
+ * fetch URL the initContainer curls (e.g. http://k8s-runner.telo-runner:8062). */
87
+ selfUrl: string;
88
+ /** Label applied to every session object, used for orphan reaping. */
89
+ managedByLabel: string;
90
+ limits: LimitCeilings;
91
+ /** Menu of base images a session may pick (advertised as the `image` enum). */
92
+ baseImageCatalog: BaseImageCatalogConfig;
93
+ }
94
+ export declare function loadK8sRunnerConfig(env: NodeJS.ProcessEnv): K8sRunnerConfig;
95
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B;2FACuF;IACvF,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,YAAY,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,gBAAgB,EAAE,OAAO,CAAC;IAC1B;oCACgC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,eAAe,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;uDACmD;IACnD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,gEAAgE;IAChE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;oGAEgG;IAChG,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,wEAAwE;IACxE,KAAK,EAAE,gBAAgB,CAAC;IACxB;uFACmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,+EAA+E;IAC/E,gBAAgB,EAAE,sBAAsB,CAAC;CAC1C;AA0CD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,eAAe,CAsD3E"}
package/dist/config.js ADDED
@@ -0,0 +1,76 @@
1
+ import { loadCoreConfig, parseBool, parsePositiveInt, RunnerConfigError, } from "@telorun/runner-core";
2
+ export { RunnerConfigError };
3
+ const DEFAULT_PORT = 8062;
4
+ /** Compile a single optional regex env var into a one-element `RegExp[]`. */
5
+ function parseTagRegex(raw, field) {
6
+ const v = raw?.trim();
7
+ if (!v)
8
+ return undefined;
9
+ try {
10
+ return [new RegExp(v)];
11
+ }
12
+ catch (err) {
13
+ throw new RunnerConfigError(`${field} is not a valid regular expression: ${err.message}`);
14
+ }
15
+ }
16
+ function loadBaseImageCatalogConfig(env) {
17
+ return {
18
+ enabled: parseBool(env.RUNNER_BASE_IMAGE_CATALOG_ENABLED, true, "RUNNER_BASE_IMAGE_CATALOG_ENABLED"),
19
+ repository: env.RUNNER_BASE_IMAGE_REPO?.trim() || "telorun/node",
20
+ filter: {
21
+ pinnedOnly: parseBool(env.RUNNER_BASE_IMAGE_PINNED_ONLY, true, "RUNNER_BASE_IMAGE_PINNED_ONLY"),
22
+ excludeSha: parseBool(env.RUNNER_BASE_IMAGE_EXCLUDE_SHA, true, "RUNNER_BASE_IMAGE_EXCLUDE_SHA"),
23
+ excludePrerelease: parseBool(env.RUNNER_BASE_IMAGE_EXCLUDE_PRERELEASE, true, "RUNNER_BASE_IMAGE_EXCLUDE_PRERELEASE"),
24
+ include: parseTagRegex(env.RUNNER_BASE_IMAGE_INCLUDE, "RUNNER_BASE_IMAGE_INCLUDE"),
25
+ exclude: parseTagRegex(env.RUNNER_BASE_IMAGE_EXCLUDE, "RUNNER_BASE_IMAGE_EXCLUDE"),
26
+ },
27
+ limit: parsePositiveInt(env.RUNNER_BASE_IMAGE_LIMIT, 20, "RUNNER_BASE_IMAGE_LIMIT"),
28
+ refreshIntervalMs: parsePositiveInt(env.RUNNER_BASE_IMAGE_REFRESH_SECONDS, 3600, "RUNNER_BASE_IMAGE_REFRESH_SECONDS") * 1000,
29
+ };
30
+ }
31
+ export function loadK8sRunnerConfig(env) {
32
+ const selfUrl = env.RUNNER_SELF_URL?.trim();
33
+ if (!selfUrl) {
34
+ throw new RunnerConfigError("RUNNER_SELF_URL env var is required. Set it to the runner's in-cluster base URL " +
35
+ "(e.g. http://k8s-runner.telo-runner.svc:8062) so session initContainers can fetch the bundle.");
36
+ }
37
+ const repository = env.RUNNER_IMAGE_REPOSITORY?.trim();
38
+ if (!repository) {
39
+ throw new RunnerConfigError("RUNNER_IMAGE_REPOSITORY env var is required. The runner prebuilds a per-app session image for " +
40
+ "every run (there is no in-pod install fallback); set it to the registry repository the built " +
41
+ "images are pushed to / pulled from (e.g. registry.example.com/telo-sessions). The tag is the " +
42
+ "bundle content hash.");
43
+ }
44
+ const build = {
45
+ repository: repository.replace(/\/+$/, ""),
46
+ namespace: env.RUNNER_BUILD_NAMESPACE?.trim() || "telo-builds",
47
+ builderImage: env.RUNNER_BUILDER_IMAGE?.trim() || "gcr.io/kaniko-project/executor:latest",
48
+ timeoutSeconds: parsePositiveInt(env.RUNNER_BUILD_TIMEOUT_SECONDS, 600, "RUNNER_BUILD_TIMEOUT_SECONDS"),
49
+ insecureRegistry: env.RUNNER_REGISTRY_INSECURE?.trim() === "true",
50
+ registryApiUrl: env.RUNNER_REGISTRY_API_URL?.trim() || undefined,
51
+ teloRegistryUrl: env.TELO_REGISTRY_URL?.trim() || "https://registry.telo.run",
52
+ pushSecretName: env.RUNNER_REGISTRY_PUSH_SECRET?.trim() || undefined,
53
+ imagePullSecret: env.RUNNER_IMAGE_PULL_SECRET?.trim() || undefined,
54
+ };
55
+ return {
56
+ ...loadCoreConfig(env, { port: DEFAULT_PORT }),
57
+ sessionNamespace: env.RUNNER_SESSION_NAMESPACE?.trim() || "telo-sessions",
58
+ defaultImage: env.RUNNER_IMAGE?.trim() || "telorun/node:latest-slim",
59
+ initImage: env.RUNNER_INIT_IMAGE?.trim() || "busybox:stable",
60
+ runtimeClass: env.RUNNER_RUNTIME_CLASS?.trim() || undefined,
61
+ sessionIngressBaseDomain: env.SESSION_INGRESS_BASE_DOMAIN?.trim() || undefined,
62
+ sessionIngressClassName: env.SESSION_INGRESS_CLASS?.trim() || undefined,
63
+ sessionIngressTlsSecretName: env.SESSION_INGRESS_TLS_SECRET?.trim() || undefined,
64
+ build,
65
+ selfUrl: selfUrl.replace(/\/+$/, ""),
66
+ managedByLabel: env.RUNNER_MANAGED_BY?.trim() || "telo-k8s-runner",
67
+ limits: {
68
+ cpu: env.RUNNER_MAX_CPU?.trim() || "50m",
69
+ memory: env.RUNNER_MAX_MEMORY?.trim() || "100Mi",
70
+ ttlSeconds: parsePositiveInt(env.RUNNER_MAX_TTL_SECONDS, 3600, "RUNNER_MAX_TTL_SECONDS"),
71
+ ephemeralStorage: env.RUNNER_MAX_EPHEMERAL_STORAGE?.trim() || "512Mi",
72
+ },
73
+ baseImageCatalog: loadBaseImageCatalogConfig(env),
74
+ };
75
+ }
76
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { RunnerBackend } from "@telorun/runner-core";
2
+ import type { BundleStore } from "../bundle-store.js";
3
+ import type { K8sRunnerConfig } from "../config.js";
4
+ import type { KubeClient } from "./client.js";
5
+ export interface K8sBackendDeps {
6
+ kube: KubeClient;
7
+ config: K8sRunnerConfig;
8
+ bundleStore: BundleStore;
9
+ }
10
+ export declare function createKubernetesBackend(deps: K8sBackendDeps): RunnerBackend;
11
+ /**
12
+ * Builds an actionable failure message from a terminal Pod status. Init
13
+ * containers are inspected first: a failed init container leaves the main
14
+ * container unstarted, so reading only `containerStatuses` would fall through
15
+ * to the bare "pod failed". For prebuilt session pods the common failure is the
16
+ * main container itself (image pull, OOM, a non-zero exit).
17
+ */
18
+ export declare function podFailureMessage(obj: unknown): string;
19
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/k8s/backend.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAMV,aAAa,EACd,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAkB9C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,cAAc,GAAG,aAAa,CAyU3E;AAiGD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAOtD"}