@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
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":"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"}
|
|
@@ -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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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":"AAAA,OAAO,EACL,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,iBAAiB,GAGlB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAiG7B,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B,6EAA6E;AAC7E,SAAS,aAAa,CAAC,GAAuB,EAAE,KAAa;IAC3D,MAAM,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC;IACtB,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,iBAAiB,CACzB,GAAG,KAAK,uCAAwC,GAAa,CAAC,OAAO,EAAE,CACxE,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,0BAA0B,CAAC,GAAsB;IACxD,OAAO;QACL,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,iCAAiC,EAAE,IAAI,EAAE,mCAAmC,CAAC;QACpG,UAAU,EAAE,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,IAAI,cAAc;QAChE,MAAM,EAAE;YACN,UAAU,EAAE,SAAS,CAAC,GAAG,CAAC,6BAA6B,EAAE,IAAI,EAAE,+BAA+B,CAAC;YAC/F,UAAU,EAAE,SAAS,CAAC,GAAG,CAAC,6BAA6B,EAAE,IAAI,EAAE,+BAA+B,CAAC;YAC/F,iBAAiB,EAAE,SAAS,CAC1B,GAAG,CAAC,oCAAoC,EACxC,IAAI,EACJ,sCAAsC,CACvC;YACD,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,yBAAyB,EAAE,2BAA2B,CAAC;YAClF,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,yBAAyB,EAAE,2BAA2B,CAAC;SACnF;QACD,KAAK,EAAE,gBAAgB,CAAC,GAAG,CAAC,uBAAuB,EAAE,EAAE,EAAE,yBAAyB,CAAC;QACnF,iBAAiB,EACf,gBAAgB,CACd,GAAG,CAAC,iCAAiC,EACrC,IAAI,EACJ,mCAAmC,CACpC,GAAG,IAAI;KACX,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAsB;IACxD,MAAM,OAAO,GAAG,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,iBAAiB,CACzB,kFAAkF;YAChF,+FAA+F,CAClG,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,GAAG,CAAC,uBAAuB,EAAE,IAAI,EAAE,CAAC;IACvD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,iBAAiB,CACzB,gGAAgG;YAC9F,+FAA+F;YAC/F,+FAA+F;YAC/F,sBAAsB,CACzB,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAqB;QAC9B,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAC1C,SAAS,EAAE,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,IAAI,aAAa;QAC9D,YAAY,EAAE,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,IAAI,uCAAuC;QACzF,cAAc,EAAE,gBAAgB,CAC9B,GAAG,CAAC,4BAA4B,EAChC,GAAG,EACH,8BAA8B,CAC/B;QACD,gBAAgB,EAAE,GAAG,CAAC,wBAAwB,EAAE,IAAI,EAAE,KAAK,MAAM;QACjE,cAAc,EAAE,GAAG,CAAC,uBAAuB,EAAE,IAAI,EAAE,IAAI,SAAS;QAChE,eAAe,EAAE,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,2BAA2B;QAC7E,cAAc,EAAE,GAAG,CAAC,2BAA2B,EAAE,IAAI,EAAE,IAAI,SAAS;QACpE,eAAe,EAAE,GAAG,CAAC,wBAAwB,EAAE,IAAI,EAAE,IAAI,SAAS;KACnE,CAAC;IAEF,OAAO;QACL,GAAG,cAAc,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;QAC9C,gBAAgB,EAAE,GAAG,CAAC,wBAAwB,EAAE,IAAI,EAAE,IAAI,eAAe;QACzE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,0BAA0B;QACpE,SAAS,EAAE,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,gBAAgB;QAC5D,YAAY,EAAE,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,IAAI,SAAS;QAC3D,wBAAwB,EAAE,GAAG,CAAC,2BAA2B,EAAE,IAAI,EAAE,IAAI,SAAS;QAC9E,uBAAuB,EAAE,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,IAAI,SAAS;QACvE,2BAA2B,EAAE,GAAG,CAAC,0BAA0B,EAAE,IAAI,EAAE,IAAI,SAAS;QAChF,KAAK;QACL,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACpC,cAAc,EAAE,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,iBAAiB;QAClE,MAAM,EAAE;YACN,GAAG,EAAE,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,KAAK;YACxC,MAAM,EAAE,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,OAAO;YAChD,UAAU,EAAE,gBAAgB,CAAC,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,wBAAwB,CAAC;YACxF,gBAAgB,EAAE,GAAG,CAAC,4BAA4B,EAAE,IAAI,EAAE,IAAI,OAAO;SACtE;QACD,gBAAgB,EAAE,0BAA0B,CAAC,GAAG,CAAC;KAClD,CAAC;AACJ,CAAC"}
|
|
@@ -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"}
|