@telorun/k8s-runner 0.13.0 → 0.15.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 (118) hide show
  1. package/README.md +166 -107
  2. package/dist/bundle-store.d.ts +6 -13
  3. package/dist/bundle-store.d.ts.map +1 -1
  4. package/dist/bundle-store.js +6 -18
  5. package/dist/bundle-store.js.map +1 -1
  6. package/dist/capabilities.d.ts +3 -3
  7. package/dist/capabilities.js +4 -4
  8. package/dist/capabilities.js.map +1 -1
  9. package/dist/config.d.ts +47 -37
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +64 -25
  12. package/dist/config.js.map +1 -1
  13. package/dist/k8s/api-error.d.ts +34 -0
  14. package/dist/k8s/api-error.d.ts.map +1 -0
  15. package/dist/k8s/api-error.js +118 -0
  16. package/dist/k8s/api-error.js.map +1 -0
  17. package/dist/k8s/backend.d.ts +4 -0
  18. package/dist/k8s/backend.d.ts.map +1 -1
  19. package/dist/k8s/backend.js +77 -51
  20. package/dist/k8s/backend.js.map +1 -1
  21. package/dist/k8s/client.d.ts +7 -2
  22. package/dist/k8s/client.d.ts.map +1 -1
  23. package/dist/k8s/client.js +3 -2
  24. package/dist/k8s/client.js.map +1 -1
  25. package/dist/k8s/pod-spec.d.ts +8 -9
  26. package/dist/k8s/pod-spec.d.ts.map +1 -1
  27. package/dist/k8s/pod-spec.js +19 -29
  28. package/dist/k8s/pod-spec.js.map +1 -1
  29. package/dist/k8s/pod-status.d.ts +5 -0
  30. package/dist/k8s/pod-status.d.ts.map +1 -1
  31. package/dist/k8s/pod-status.js +9 -2
  32. package/dist/k8s/pod-status.js.map +1 -1
  33. package/dist/k8s/routing/create-or-replace.d.ts +5 -0
  34. package/dist/k8s/routing/create-or-replace.d.ts.map +1 -0
  35. package/dist/k8s/routing/create-or-replace.js +15 -0
  36. package/dist/k8s/routing/create-or-replace.js.map +1 -0
  37. package/dist/k8s/routing/gateway-router.d.ts +5 -0
  38. package/dist/k8s/routing/gateway-router.d.ts.map +1 -0
  39. package/dist/k8s/routing/gateway-router.js +178 -0
  40. package/dist/k8s/routing/gateway-router.js.map +1 -0
  41. package/dist/k8s/routing/index.d.ts +24 -0
  42. package/dist/k8s/routing/index.d.ts.map +1 -0
  43. package/dist/k8s/routing/index.js +27 -0
  44. package/dist/k8s/routing/index.js.map +1 -0
  45. package/dist/k8s/routing/ingress-router.d.ts +13 -0
  46. package/dist/k8s/routing/ingress-router.d.ts.map +1 -0
  47. package/dist/k8s/routing/ingress-router.js +125 -0
  48. package/dist/k8s/routing/ingress-router.js.map +1 -0
  49. package/dist/k8s/routing/route-health.d.ts +34 -0
  50. package/dist/k8s/routing/route-health.d.ts.map +1 -0
  51. package/dist/k8s/routing/route-health.js +77 -0
  52. package/dist/k8s/routing/route-health.js.map +1 -0
  53. package/dist/k8s/routing/routing-mode.d.ts +45 -0
  54. package/dist/k8s/routing/routing-mode.d.ts.map +1 -0
  55. package/dist/k8s/routing/routing-mode.js +141 -0
  56. package/dist/k8s/routing/routing-mode.js.map +1 -0
  57. package/dist/k8s/routing/session-endpoints.d.ts +25 -0
  58. package/dist/k8s/routing/session-endpoints.d.ts.map +1 -0
  59. package/dist/k8s/routing/session-endpoints.js +36 -0
  60. package/dist/k8s/routing/session-endpoints.js.map +1 -0
  61. package/dist/k8s/routing/session-router.d.ts +57 -0
  62. package/dist/k8s/routing/session-router.d.ts.map +1 -0
  63. package/dist/k8s/routing/session-router.js +2 -0
  64. package/dist/k8s/routing/session-router.js.map +1 -0
  65. package/dist/k8s/routing/session-service.d.ts +14 -0
  66. package/dist/k8s/routing/session-service.d.ts.map +1 -0
  67. package/dist/k8s/routing/session-service.js +42 -0
  68. package/dist/k8s/routing/session-service.js.map +1 -0
  69. package/dist/k8s/watch-session.d.ts +7 -5
  70. package/dist/k8s/watch-session.d.ts.map +1 -1
  71. package/dist/k8s/watch-session.js +90 -83
  72. package/dist/k8s/watch-session.js.map +1 -1
  73. package/dist/k8s/workspace-configmap.d.ts.map +1 -1
  74. package/dist/k8s/workspace-configmap.js +3 -12
  75. package/dist/k8s/workspace-configmap.js.map +1 -1
  76. package/dist/server.d.ts.map +1 -1
  77. package/dist/server.js +37 -4
  78. package/dist/server.js.map +1 -1
  79. package/package.json +2 -2
  80. package/src/bundle-store.ts +6 -19
  81. package/src/capabilities.ts +4 -4
  82. package/src/config.ts +130 -68
  83. package/src/k8s/api-error.test.ts +89 -0
  84. package/src/k8s/api-error.ts +128 -0
  85. package/src/k8s/backend.ts +103 -58
  86. package/src/k8s/client.ts +10 -3
  87. package/src/k8s/pod-spec.test.ts +57 -2
  88. package/src/k8s/pod-spec.ts +25 -36
  89. package/src/k8s/pod-status.ts +10 -2
  90. package/src/k8s/routing/create-or-replace.ts +16 -0
  91. package/src/k8s/routing/gateway-router.test.ts +236 -0
  92. package/src/k8s/routing/gateway-router.ts +219 -0
  93. package/src/k8s/routing/index.ts +44 -0
  94. package/src/k8s/routing/ingress-router.test.ts +236 -0
  95. package/src/k8s/routing/ingress-router.ts +157 -0
  96. package/src/k8s/routing/route-health.test.ts +215 -0
  97. package/src/k8s/routing/route-health.ts +95 -0
  98. package/src/k8s/routing/routing-mode.test.ts +218 -0
  99. package/src/k8s/routing/routing-mode.ts +191 -0
  100. package/src/k8s/routing/session-endpoints.ts +45 -0
  101. package/src/k8s/routing/session-router.ts +59 -0
  102. package/src/k8s/routing/session-service.ts +55 -0
  103. package/src/k8s/watch-pod-spec.test.ts +0 -1
  104. package/src/k8s/watch-session.ts +90 -77
  105. package/src/k8s/workspace-configmap.ts +3 -15
  106. package/src/server.ts +41 -4
  107. package/dist/k8s/image-build.d.ts +0 -103
  108. package/dist/k8s/image-build.d.ts.map +0 -1
  109. package/dist/k8s/image-build.js +0 -427
  110. package/dist/k8s/image-build.js.map +0 -1
  111. package/dist/k8s/ingress.d.ts +0 -15
  112. package/dist/k8s/ingress.d.ts.map +0 -1
  113. package/dist/k8s/ingress.js +0 -102
  114. package/dist/k8s/ingress.js.map +0 -1
  115. package/src/k8s/image-build.test.ts +0 -238
  116. package/src/k8s/image-build.ts +0 -540
  117. package/src/k8s/ingress.test.ts +0 -146
  118. package/src/k8s/ingress.ts +0 -127
package/README.md CHANGED
@@ -27,44 +27,45 @@ serving an anonymous tier, the ceiling *is* the policy.
27
27
 
28
28
  Per session the runner resolves the image, creates a Pod (`telo run`), watches it
29
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.
30
+ `/io` channel, and — when a routing base domain is configured — creates a
31
+ per-session Service plus routing objects (an Ingress, or one HTTPRoute per port)
32
+ at `<port>-<sessionId>.<base-domain>`, garbage-collected via an ownerReference to
33
+ the Pod.
34
+
35
+ **Every session runs the plain kernel image.** A body-fetch initContainer untars
36
+ the staged bundle into a writable `/app` emptyDir and the session container runs
37
+ `telo run /app/<entry>`, resolving its own module closure into a `/telo-cache`
38
+ emptyDir on the way up. `readOnlyRootFilesystem` stays on — every write lands on
39
+ a mounted emptyDir. The runner needs **no image registry of its own**: it builds
40
+ nothing, pushes nothing, and creates no Jobs.
41
+
42
+ The runner used to prebuild a self-contained per-app image with an on-cluster
43
+ Kaniko Job, so that a slow package registry could not stall the start path. That
44
+ is gone, and the cost is where it now shows: **a run session downloads its
45
+ closure on every start**, since the cache lives and dies with the pod. A watch
46
+ session pays it once and keeps the cache for the pod's life, which is the shape
47
+ to reach for when start latency matters.
54
48
 
55
49
  **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`.
50
+ (`provision` → `boot`) while the session is still `starting`. The session is
51
+ created with a fast `201` carrying the `streamUrl` **before** the pod is created,
52
+ so the client connects immediately and sees provisioning live; the backend then
53
+ runs in the background and a start failure surfaces as a terminal `failed` status
54
+ on the stream. The session flips to `running` when the Pod reaches `Running`.
62
55
 
63
56
  Sandbox hardening is always on (non-root, read-only rootfs, drop-all caps, no
64
57
  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.
58
+ (gVisor/Kata) is layered on when configured. A controller whose install needs to
59
+ run scripts or build native code runs them **inside the session pod** under that
60
+ hardening there is no trusted build step to run them in.
61
+
62
+ **A Kubernetes API rejection reaches the client as its status and `reason`, never
63
+ as the exception.** The API's error carries the full HTTP dump — the raw `Status`
64
+ body naming the runner's ServiceAccount, the audit id, every response header —
65
+ and a start failure's message travels verbatim to the client as the session's
66
+ terminal status. So the client is told the operation, the HTTP code and the
67
+ one-word reason (`Forbidden`, `NotFound`), plus who can fix a `403`; the raw
68
+ exception rides along as the error's `cause` and lands in the runner's log.
68
69
 
69
70
  ## Watch sessions
70
71
 
@@ -74,7 +75,7 @@ different — a **workspace that runs continuously**. One pod holds a shared
74
75
  `/workspace` volume, a `workspace` container serving the editor's file routes, one
75
76
  `app-<name>` container per application running `telo run --watch`, and optionally
76
77
  a co-resident `agent` drawn from the `RUNNER_APPS` catalog. An edit then costs a
77
- kernel reload instead of a pod: no schedule, no pull, no build.
78
+ kernel reload instead of a pod: no schedule, no pull, no module re-resolve.
78
79
 
79
80
  Off unless `RUNNER_WATCH_SESSIONS` is set. Run sessions are entirely unaffected.
80
81
 
@@ -105,7 +106,7 @@ agent's port the **manifest wins**: the session starts without the agent and say
105
106
  so on its stream, rather than refusing to run the user's app over a container
106
107
  they never asked for and cannot decline. Nothing else is arranged: the pod's containers
107
108
  share one network namespace, so the port simply joins the session's own Service
108
- and Ingress and answers at `<agentPort>-<sessionId>.<base-domain>`. The `running`
109
+ and routing objects and answers at `<agentPort>-<sessionId>.<base-domain>`. The `running`
109
110
  status carries it as an `agent` endpoint beside `endpoints` — separate, because
110
111
  `endpoints` are the ports the user's applications declared, and an
111
112
  operator-launched container is not one of them. It is reachable without auth for
@@ -128,10 +129,10 @@ runner reconciles into a content-addressed ConfigMap — there is no third image
128
129
  build, and a runner upgrade that changes the manifest leaves running sessions
129
130
  mounting the one they booted with.
130
131
 
131
- **Watch sessions never build an image.** The build path exists to put a dependency
132
- closure on disk before boot; a watch session resolves its own into the workspace
133
- volume, which lives as long as the pod, so the download happens once per session
134
- and every later reload resolves from local disk.
132
+ **A watch session resolves its closure once.** It lands in the workspace volume,
133
+ which lives as long as the pod, so the download happens once per session and
134
+ every later reload resolves from local disk where a run session, whose cache
135
+ dies with its pod, pays for it on every start.
135
136
 
136
137
  **One cache for the whole session.** The runner seeds `telo-workspace.yaml` at
137
138
  the workspace root when the session starts, and the kernel anchors its `.telo`
@@ -144,13 +145,11 @@ manifest lives outside the workspace and the walk-up would never reach the
144
145
  marker. A workspace that brings its own marker keeps it — overwriting one with a
145
146
  real `modules:` list would change what `telo release` discovers.
146
147
 
147
- **Egress moves into the session namespace.** Module fetches used to be scoped to
148
- the build namespace; a watch session resolves them itself, so the session
149
- namespace's NetworkPolicy has to reach module registries as well as the model
150
- provider. Core NetworkPolicy is CIDR-only and registries sit behind rotating-IP
151
- CDNs, so a locked-down operator needs a CNI with FQDN policy or an egress proxy
152
- here — the same stated dependency the build namespace already carries, one
153
- namespace over.
148
+ **Egress lives in the session namespace.** Every session resolves its own module
149
+ closure, so the session namespace's NetworkPolicy has to reach the module
150
+ registries as well as the model provider. Core NetworkPolicy is CIDR-only and
151
+ registries sit behind rotating-IP CDNs, so a locked-down operator needs a CNI
152
+ with FQDN policy or an egress proxy here.
154
153
 
155
154
  ### Two nouns on one stream
156
155
 
@@ -214,10 +213,10 @@ transport whose kind does not emit one would silently get no routing.
214
213
  one-shot app completed is not a change. It touches the named app's entry manifest
215
214
  through the same path everything else uses, so it needs no signalling into the
216
215
  container, no shared PID namespace and no `exec` — **RBAC gains only `configmaps:
217
- get, create` and `update` on services/ingresses**, the latter so a reload that
218
- changes an app's declared port set can re-patch its routing. Without that, adding a
219
- `ports:` entry leaves the app bound to a port with no ingress, no error and no
220
- event.
216
+ get, create` and `update` on services and routing objects**, the latter so a reload
217
+ that changes an app's declared port set can re-patch its routing. Without that,
218
+ adding a `ports:` entry leaves the app bound to a port with no route, no error and
219
+ no event.
221
220
 
222
221
  Changing the app set costs a pod recreate because a pod's container list is fixed
223
222
  at creation. That is the only editing action in the design that costs a pod, and
@@ -288,17 +287,24 @@ outcome than a URL that silently reaches the wrong app.
288
287
  | `PORT` | `8062` | HTTP listen port |
289
288
  | `RUNNER_DISPLAY_NAME` | `Telo Runner` | Display name advertised on `/v1/capabilities` (the editor's runner label) |
290
289
  | `RUNNER_DESCRIPTION` | `Runs the Telo application in a cloud environment` | Description advertised on `/v1/capabilities` |
291
- | `RUNNER_APPS` | _(unset → no apps)_ | JSON map of operator-predefined apps launchable by name (chart: inline `apps.catalog`, or `apps.catalogSecret` referencing a Secret holding the JSON — use the Secret whenever entries embed secrets in `env`); see the docker-runner README for the entry shape, including the `port` an entry must declare to be usable as a session's co-resident `agent`. App sessions run the catalog image directly as a pod — no on-cluster build |
290
+ | `RUNNER_APPS` | _(unset → no apps)_ | JSON map of operator-predefined apps launchable by name (chart: inline `apps.catalog`, or `apps.catalogSecret` referencing a Secret holding the JSON — use the Secret whenever entries embed secrets in `env`); see the docker-runner README for the entry shape, including the `port` an entry must declare to be usable as a session's co-resident `agent`. App sessions run the catalog image directly as a pod |
292
291
  | `RUNNER_APP_MAX_CPU` | `500m` | CPU ceiling for predefined-app pods (separate from the anonymous-session ceiling) |
293
292
  | `RUNNER_APP_MAX_MEMORY` | `512Mi` | Memory ceiling for predefined-app pods |
294
293
  | `RUNNER_APP_MAX_TTL_SECONDS` | `21600` | Wall-clock TTL for predefined-app pods (agent sessions are long-lived) |
295
294
  | `RUNNER_APP_MAX_EPHEMERAL_STORAGE` | `1Gi` | Ephemeral-storage ceiling for predefined-app pods |
296
295
  | `RUNNER_SESSION_NAMESPACE` | `telo-sessions` | Namespace for session objects |
297
296
  | `RUNNER_IMAGE` | _(baked at build: the CLI version for a released runner, `telorun/node:latest-slim` for a dev build)_ | Default base image; always offered in the picker and the fallback when the catalog is unreachable. Leave the chart's `session.image` empty to keep the session kernel in lockstep with the runner |
298
- | `RUNNER_INIT_IMAGE` | `busybox:stable` | Build-context fetch initContainer image (wget + tar) |
297
+ | `RUNNER_INIT_IMAGE` | `busybox:stable` | Bundle-fetch initContainer image (wget + tar) |
298
+ | `RUNNER_IMAGE_PULL_SECRET` | _(unset)_ | dockerconfig Secret (in `telo-sessions`) the kubelet pulls session images with — needed only for a kernel or catalog image in a private registry |
299
299
  | `RUNNER_RUNTIME_CLASS` | _(unset → runc)_ | Sandbox RuntimeClass (gvisor/kata) |
300
- | `SESSION_INGRESS_BASE_DOMAIN` | _(unset logs-only)_ | Wildcard base for per-session ingress |
301
- | `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 |
300
+ | `SESSION_ROUTING_MODE` | `auto` | `auto` \| `ingress` \| `gateway` \| `none`. See [Routing layer](#routing-layer) |
301
+ | `SESSION_ROUTING_BASE_DOMAIN` | _(unset → logs-only)_ | Wildcard base for per-session hosts |
302
+ | `SESSION_ROUTE_READY_TIMEOUT_SECONDS` | `60` | How long a published route may go unclaimed before the session reports it `unprogrammed` |
303
+ | `SESSION_INGRESS_CLASS` | _(unset → cluster default)_ | IngressClass for session Ingresses (ingress mode) |
304
+ | `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. **Ingress mode only** — refused at boot in gateway mode, where the cert belongs to the Gateway listener |
305
+ | `SESSION_GATEWAY_NAME` | _(unset)_ | Gateway each session's HTTPRoute attaches to. Required in gateway mode |
306
+ | `SESSION_GATEWAY_NAMESPACE` | _(= session namespace)_ | Namespace of that Gateway |
307
+ | `SESSION_GATEWAY_SECTION_NAME` | _(unset → any listener)_ | Listener name, when only one should carry session traffic |
302
308
  | `RUNNER_MAX_CPU` | `50m` | CPU ceiling |
303
309
  | `RUNNER_MAX_MEMORY` | `100Mi` | Memory ceiling |
304
310
  | `RUNNER_MAX_TTL_SECONDS` | `3600` | Wall-clock TTL (Pod `activeDeadlineSeconds`) |
@@ -327,12 +333,9 @@ against the same list, so a client that skips the editor can't widen the set.
327
333
  unreachable. Disable the catalog to lock `image` to `RUNNER_IMAGE`.
328
334
 
329
335
  Pinned tags (e.g. `0.30.1-slim`) are immutable. A picked **moving** tag like
330
- `latest-slim` only refreshes when the session's `pullPolicy` is `always`: the
331
- build then pins the per-app image to the base's current digest, so a moved tag
332
- yields a new image and rebuilds (otherwise the cached build keyed on the tag
333
- string — is reused). Movement detection reads the digest from Docker Hub, so a
334
- base hosted elsewhere (GHCR, a private registry) can't be tracked — `always`
335
- degrades to reusing the cached build for it, same as `missing` / `never`.
336
+ `latest-slim` only refreshes when the session's `pullPolicy` is `always`, which
337
+ is the Pod's own `imagePullPolicy`: the kubelet re-pulls the tag on every
338
+ session. `missing` and `never` reuse whatever that node already cached.
336
339
 
337
340
  | Env | Default | Purpose |
338
341
  | --- | --- | --- |
@@ -346,88 +349,144 @@ degrades to reusing the cached build for it, same as `missing` / `never`.
346
349
  | `RUNNER_BASE_IMAGE_LIMIT` | `20` | Cap on advertised tags (newest first) |
347
350
  | `RUNNER_BASE_IMAGE_REFRESH_SECONDS` | `3600` | Catalog re-fetch cadence |
348
351
 
349
- ### Image build (required)
350
-
351
- | Env | Default | Purpose |
352
- | --- | --- | --- |
353
- | `RUNNER_IMAGE_REPOSITORY` | _(required)_ | Registry repo for per-app images; tag = bundle hash |
354
- | `RUNNER_BUILD_NAMESPACE` | `telo-builds` | Namespace the trusted Kaniko build Jobs run in |
355
- | `RUNNER_BUILDER_IMAGE` | `gcr.io/kaniko-project/executor:latest` | Image builder |
356
- | `RUNNER_BUILD_TIMEOUT_SECONDS` | `600` | Build Job deadline / wait budget |
357
- | `RUNNER_REGISTRY_INSECURE` | `false` | Push/pull over HTTP / self-signed |
358
- | `RUNNER_REGISTRY_API_URL` | _(unset → always build)_ | HTTP(S) base for the manifest existence check (authenticated via the push Secret) |
359
- | `RUNNER_REGISTRY_PUSH_SECRET` | _(unset)_ | dockerconfig Secret (in `telo-builds`) Kaniko pushes with; also authenticates the existence check |
360
- | `RUNNER_IMAGE_PULL_SECRET` | _(unset)_ | dockerconfig Secret (in `telo-sessions`) the kubelet pulls per-app images with |
361
-
362
352
  ## Deploy (Helm)
363
353
 
364
- The runner **requires a registry to build into** (there is no in-pod install
365
- fallback), so point it at one your cluster's kubelet can pull from:
354
+ The chart is published as an OCI artifact at
355
+ `oci://ghcr.io/telorun/charts/k8s-runner`:
366
356
 
367
357
  ```bash
368
- helm install telo-runner ./chart \
369
- --set build.repository=registry.example.com/telo-sessions \
370
- --set-file registry.dockerconfigjson=./dockerconfig.json \ # private-registry auth
358
+ helm install telo-runner oci://ghcr.io/telorun/charts/k8s-runner \
359
+ --set watch.enabled=true \
371
360
  --set session.runtimeClass=gvisor
372
361
  ```
373
362
 
374
- For a private registry, `registry.dockerconfigjson` creates the dockerconfig
375
- Secret in **both** namespaces (push in `telo-builds`, pull in `telo-sessions`) and
376
- wires `RUNNER_REGISTRY_PUSH_SECRET` + `RUNNER_IMAGE_PULL_SECRET` — the kubelet
377
- needs the pull copy because the per-app image is private. (Or manage the Secrets
378
- yourself and reference them via `build.pushSecretName` / `build.pullSecretName`.)
379
- The push Secret doubles as the credential for the manifest existence check, so
380
- the runner can see an already-built image in a private registry and skip the
381
- rebuild without it a private registry answers `401` and every run rebuilds.
382
- A no-auth registry needs none of this.
363
+ Helm resolves the newest release when no `--version` is given; pass one to pin.
364
+ From a checkout, `./chart` works the same way and is what you want when changing
365
+ the chart itself.
366
+
367
+ **The chart's version IS the runner's.** `version` and `appVersion` both carry
368
+ `@telorun/k8s-runner`'s package version, stamped in the release PR and checked on
369
+ every PR so chart `0.13.0` installs runner image `0.13.0`, with no lookup
370
+ table. That is also why `image.tag` defaults to empty and resolves to the chart's
371
+ own `appVersion` rather than to `latest`: `--version 0.13.0` has to mean one
372
+ thing, where a floating tag meant whatever it had moved to overnight.
383
373
 
384
374
  The chart provisions the static scaffolding: the runner Deployment (single
385
375
  replica — the registry is in-memory and the runner reaps orphaned pods on boot),
386
- Service, scoped RBAC, the `telo-runner` / restricted-PSS `telo-sessions` /
387
- baseline-PSS `telo-builds` namespaces, a `ResourceQuota`, and NetworkPolicies
388
- (session pod-to-pod isolation + the build namespace's registry egress). The
389
- runner creates per-session and per-build objects at runtime.
376
+ Service, scoped RBAC, the `telo-runner` and restricted-PSS `telo-sessions`
377
+ namespaces, a `ResourceQuota`, and the session pod-to-pod NetworkPolicy. The
378
+ runner creates per-session objects at runtime. Nothing else is required — no
379
+ image registry, no build namespace, no push credentials.
380
+
381
+ For a kernel image or an operator catalog image held in a **private registry**,
382
+ create the dockerconfigjson Secret in `telo-sessions` yourself and point
383
+ `session.imagePullSecret` at it; the kubelet pulls with it.
384
+
385
+ ### Routing layer
386
+
387
+ Session hosts are `<port>-<sessionId>.<base-domain>` — a single label, so one
388
+ wildcard DNS record and one wildcard cert cover every session. **Both current
389
+ routing APIs are supported**, because clusters ship Ingress, Gateway API, or
390
+ both, and hardcoding either leaves the runner unroutable on the rest.
391
+
392
+ `sessionRouting.mode` picks the layer:
393
+
394
+ | mode | behaviour |
395
+ | --- | --- |
396
+ | `auto` (default) | Resolve from what is **configured**, then from what the cluster serves |
397
+ | `ingress` | `networking.k8s.io/v1` Ingress |
398
+ | `gateway` | `gateway.networking.k8s.io` HTTPRoute (`v1`, falling back to `v1beta1`) |
399
+ | `none` | Publish nothing — logs-only even with a base domain set |
400
+
401
+ `auto` reads configuration before installation, and that order is the point: a
402
+ named Gateway wins, then a configured IngressClass, then a **default-marked**
403
+ IngressClass (`ingressclass.kubernetes.io/is-default-class` — the cluster has
404
+ nominated where an unqualified Ingress goes, and unqualified is what this runner
405
+ creates), and only then does it look at what merely exists. **Gateway API CRDs are frequently present without being the intended
406
+ path**, so "the API exists" is not evidence of intent. Where two usable layers
407
+ exist and nothing was configured, the runner **refuses to start** rather than
408
+ guessing — a wrong guess publishes routing objects nothing reconciles, which
409
+ produces a healthy pod and a 404 on every URL, with nothing reported anywhere.
410
+
411
+ ```bash
412
+ # Gateway API
413
+ helm install telo-runner ./chart \
414
+ --set sessionRouting.mode=gateway \
415
+ --set sessionRouting.baseDomain=telo.run \
416
+ --set sessionRouting.gateway.name=public \
417
+ --set sessionRouting.gateway.namespace=gateway-system \
418
+ --set sessionRouting.dataPlaneNamespace=gateway-system
419
+ ```
420
+
421
+ The Gateway needs a listener whose hostname covers `*.<base-domain>` and whose
422
+ `allowedRoutes.namespaces` admits `telo-sessions`. A route it does not admit is
423
+ reported per session with the controller's own reason (`NotAllowedByListeners`).
390
424
 
391
- The optional in-cluster registry (`--set registry.enabled=true --set
392
- build.insecureRegistry=true`) derives `build.repository` for you, but works only
393
- on clusters whose **nodes are configured to trust it** otherwise an external/
394
- cloud registry is simpler. Installing with neither a `build.repository` nor
395
- `registry.enabled` is rejected at template time.
425
+ **Upgrading from `sessionIngress.*`** is a breaking values change and the chart
426
+ refuses the old keys rather than ignoring them — Helm drops unknown values
427
+ silently, so an un-migrated file would otherwise boot a runner that logs
428
+ `session routing disabled (logs-only)` and drops every session URL.
429
+
430
+ > **One HTTPRoute per port.** Gateway API scopes `hostnames` to the whole route
431
+ > while a rule matches on path, never host — so a single route carrying every
432
+ > session hostname would send them all to whichever rule matched `/` first. An
433
+ > Ingress rule owns its host, which is why that layer needs only one object.
434
+
435
+ **`sessionRouting.dataPlaneNamespace` is not optional when routing is on.** The
436
+ session NetworkPolicy admits traffic from that namespace only; without it the
437
+ route programs correctly and the workload is still unreachable.
438
+
439
+ ### Route verification
440
+
441
+ The runner already dials each declared port from its own network — that proves
442
+ the app is **listening** and says nothing about whether traffic can **reach** it.
443
+ So it separately verifies that what it published was actually programmed, and
444
+ reports each host as `pending` → `programmed` / `unprogrammed` on the session
445
+ event stream (`type: "route"`). Reported, never fatal: a slow controller and an
446
+ absent one look alike for the first few seconds.
447
+
448
+ Gateway API answers this precisely, from the `Accepted` / `ResolvedRefs`
449
+ conditions a controller writes. Ingress has no rejection signal — a controller
450
+ that refuses a rule simply never writes status — so there an unclaimed route is
451
+ caught by the timeout rather than by a reason the cluster gave.
396
452
 
397
453
  ### Origin TLS (Cloudflare et al.)
398
454
 
455
+ **Ingress mode only.** Under Gateway API the origin certificate belongs to the
456
+ Gateway listener's own `certificateRefs`; setting `SESSION_INGRESS_TLS_SECRET`
457
+ alongside `mode: gateway` is refused at boot rather than silently ignored.
458
+
399
459
  To have the per-session Ingress present an origin cert (so an upstream like
400
460
  Cloudflare in **Full (Strict)** mode validates the origin), give the chart a
401
461
  `kubernetes.io/tls` Secret in `telo-sessions`. The cert must cover the wildcard
402
- `*.<sessionIngress.baseDomain>` — session hosts are a single label
462
+ `*.<sessionRouting.baseDomain>` — session hosts are a single label
403
463
  (`<port>-<sessionId>.<base-domain>`). Two ways:
404
464
 
405
465
  ```bash
406
466
  # A — reference a Secret you manage in telo-sessions (cert-manager, your own sync)
407
- helm install telo-runner ./chart --set sessionIngress.tls.secretName=telo-origin-tls
467
+ helm install telo-runner ./chart --set sessionRouting.ingress.tls.secretName=telo-origin-tls
408
468
 
409
469
  # B — let the chart create the Secret from your cert + key
410
470
  helm install telo-runner ./chart \
411
- --set-file sessionIngress.tls.cert=origin.pem \
412
- --set-file sessionIngress.tls.key=origin.key
471
+ --set-file sessionRouting.ingress.tls.cert=origin.pem \
472
+ --set-file sessionRouting.ingress.tls.key=origin.key
413
473
  ```
414
474
 
415
475
  Either wires `SESSION_INGRESS_TLS_SECRET`, and the runner stamps a `spec.tls`
416
476
  block on every session Ingress. Leave all three empty to skip TLS at the origin
417
477
  (terminated entirely upstream).
418
478
 
419
- > **Egress notes.** (1) The kubelet pulls session images directly and does **not**
420
- > use cluster DNS, so the in-cluster registry only works where nodes trust it
421
- > (e.g. containerd `registries.conf`). (2) Core NetworkPolicy is CIDR-only and
422
- > cannot express the package-registry FQDN allowlist the build namespace needs —
423
- > use a CNI with FQDN policy (Cilium) or an egress proxy to tighten it.
479
+ > **Egress note.** Core NetworkPolicy is CIDR-only and cannot express the
480
+ > package-registry FQDN allowlist the session namespace needs use a CNI with
481
+ > FQDN policy (Cilium) or an egress proxy to tighten it.
424
482
 
425
483
  ## Development
426
484
 
427
485
  ```bash
428
486
  pnpm --filter @telorun/k8s-runner build # tsc
429
- pnpm --filter @telorun/k8s-runner test # vitest (limits clamp, tar, bundle token)
487
+ pnpm --filter @telorun/k8s-runner test # vitest (pod specs, limits clamp, tar, API errors)
430
488
  ```
431
489
 
432
490
  The Kubernetes backend can't be exercised without a cluster; unit tests cover the
433
- backend-independent logic (limit clamping, the tar writer, bundle tokens).
491
+ backend-independent logic (the pod specs, limit clamping, the tar writer, and
492
+ what an API rejection is allowed to tell a client).
@@ -1,9 +1,9 @@
1
1
  import type { FastifyInstance } from "fastify";
2
2
  import type { RunBundle } from "@telorun/runner-core";
3
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
4
+ * Holds session bundles in memory and serves them over a tokenized,
5
+ * cluster-internal URL the session pod's initContainer fetches once. The
6
+ * per-session unguessable token prevents cross-session disclosure; the entry is
7
7
  * dropped after first fetch (or explicitly on cleanup).
8
8
  */
9
9
  export declare class BundleStore {
@@ -11,16 +11,9 @@ export declare class BundleStore {
11
11
  private readonly bundles;
12
12
  constructor(selfUrl: string);
13
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.
14
+ * Stages the session bundle and returns the tokenized, single-use URL the
15
+ * session Pod's body-delivery initContainer fetches into `/app`. Keyed by
16
+ * `sessionId`.
24
17
  */
25
18
  stageSessionBundle(sessionId: string, bundle: RunBundle): Promise<string>;
26
19
  private stage;
@@ -1 +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"}
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,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"}
@@ -4,9 +4,9 @@ import { makeBundleTarGz } from "./tar.js";
4
4
  * stuck/never-started Pod can't grow the store unbounded. */
5
5
  const STAGING_TTL_MS = 5 * 60 * 1000;
6
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
7
+ * Holds session bundles in memory and serves them over a tokenized,
8
+ * cluster-internal URL the session pod's initContainer fetches once. The
9
+ * per-session unguessable token prevents cross-session disclosure; the entry is
10
10
  * dropped after first fetch (or explicitly on cleanup).
11
11
  */
12
12
  export class BundleStore {
@@ -16,21 +16,9 @@ export class BundleStore {
16
16
  this.selfUrl = selfUrl;
17
17
  }
18
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.
19
+ * Stages the session bundle and returns the tokenized, single-use URL the
20
+ * session Pod's body-delivery initContainer fetches into `/app`. Keyed by
21
+ * `sessionId`.
34
22
  */
35
23
  async stageSessionBundle(sessionId, bundle) {
36
24
  return this.stage(sessionId, bundle);
@@ -1 +1 @@
1
- {"version":3,"file":"bundle-store.js","sourceRoot":"","sources":["../src/bundle-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bundle-store.js","sourceRoot":"","sources":["../src/bundle-store.ts"],"names":[],"mappings":"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"}
@@ -19,9 +19,9 @@ export interface KubernetesRunnerCapabilitiesOptions {
19
19
  * studio renders an editable dropdown constrained to that allowlist — which the
20
20
  * session route re-validates server-side. Without a catalog (disabled /
21
21
  * first-fetch failure) the list collapses to the single `defaultImage` (locked
22
- * via `enforced`). `pullPolicy` is client-editable: `always` re-pulls the base
23
- * image when its tag has moved upstream (rebuilding the per-app image), which
24
- * is how a picked moving tag like `latest-slim` stays current.
22
+ * via `enforced`). `pullPolicy` is client-editable and is the pod's own
23
+ * `imagePullPolicy`: `always` re-pulls the kernel image, which is how a picked
24
+ * moving tag like `latest-slim` stays current.
25
25
  *
26
26
  * `terms`, when set (operator-provided via RUNNER_TERMS_*), are enforced: a
27
27
  * session won't start until the client acknowledges the current version. */
@@ -5,9 +5,9 @@ import { sessionConfigSchema, } from "@telorun/runner-core";
5
5
  * studio renders an editable dropdown constrained to that allowlist — which the
6
6
  * session route re-validates server-side. Without a catalog (disabled /
7
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.
8
+ * via `enforced`). `pullPolicy` is client-editable and is the pod's own
9
+ * `imagePullPolicy`: `always` re-pulls the kernel image, which is how a picked
10
+ * moving tag like `latest-slim` stays current.
11
11
  *
12
12
  * `terms`, when set (operator-provided via RUNNER_TERMS_*), are enforced: a
13
13
  * session won't start until the client acknowledges the current version. */
@@ -30,7 +30,7 @@ export function kubernetesRunnerCapabilities(opts) {
30
30
  imageDefault: defaultImage,
31
31
  enforced: true,
32
32
  imageEnum,
33
- 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.",
33
+ pullPolicyDescription: "Kernel-image freshness, applied as the Pod's imagePullPolicy. `always` re-pulls on every session, so a moving tag like `latest-slim` picks up a new release. `missing` pulls only when the node has no copy. `never` never pulls at all — on a node that has not already cached the image the session fails to start (ErrImageNeverPull), so it suits a cluster that pre-loads its images and nothing else.",
34
34
  }),
35
35
  },
36
36
  terms,
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAiB9B;;;;;;;;;;;6EAW6E;AAC7E,MAAM,UAAU,4BAA4B,CAC1C,IAAyC;IAEzC,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC1E,OAAO;QACL,WAAW;QACX,WAAW;QACX,QAAQ,EAAE;YACR,qEAAqE;YACrE,0EAA0E;YAC1E,kCAAkC;YAClC,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC;YACtB,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1E;QACD,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,KAAK;KACN,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAiB9B;;;;;;;;;;;6EAW6E;AAC7E,MAAM,UAAU,4BAA4B,CAC1C,IAAyC;IAEzC,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC1E,OAAO;QACL,WAAW;QACX,WAAW;QACX,QAAQ,EAAE;YACR,qEAAqE;YACrE,0EAA0E;YAC1E,kCAAkC;YAClC,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC;YACtB,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1E;QACD,MAAM,EAAE;YACN,MAAM,EAAE,mBAAmB,CAAC;gBAC1B,YAAY,EAAE,YAAY;gBAC1B,QAAQ,EAAE,IAAI;gBACd,SAAS;gBACT,qBAAqB,EACnB,6YAA6Y;aAChZ,CAAC;SACH;QACD,KAAK;KACN,CAAC;AACJ,CAAC"}