@telorun/k8s-runner 0.14.0 → 0.15.1
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/README.md +93 -14
- package/dist/config.d.ts +44 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -4
- package/dist/config.js.map +1 -1
- package/dist/k8s/backend.d.ts +4 -0
- package/dist/k8s/backend.d.ts.map +1 -1
- package/dist/k8s/backend.js +55 -17
- package/dist/k8s/backend.js.map +1 -1
- package/dist/k8s/client.d.ts +7 -1
- package/dist/k8s/client.d.ts.map +1 -1
- package/dist/k8s/client.js +3 -1
- package/dist/k8s/client.js.map +1 -1
- package/dist/k8s/pod-status.d.ts +5 -0
- package/dist/k8s/pod-status.d.ts.map +1 -1
- package/dist/k8s/pod-status.js +7 -0
- package/dist/k8s/pod-status.js.map +1 -1
- package/dist/k8s/routing/create-or-replace.d.ts +5 -0
- package/dist/k8s/routing/create-or-replace.d.ts.map +1 -0
- package/dist/k8s/routing/create-or-replace.js +15 -0
- package/dist/k8s/routing/create-or-replace.js.map +1 -0
- package/dist/k8s/routing/gateway-router.d.ts +5 -0
- package/dist/k8s/routing/gateway-router.d.ts.map +1 -0
- package/dist/k8s/routing/gateway-router.js +178 -0
- package/dist/k8s/routing/gateway-router.js.map +1 -0
- package/dist/k8s/routing/index.d.ts +24 -0
- package/dist/k8s/routing/index.d.ts.map +1 -0
- package/dist/k8s/routing/index.js +27 -0
- package/dist/k8s/routing/index.js.map +1 -0
- package/dist/k8s/routing/ingress-router.d.ts +13 -0
- package/dist/k8s/routing/ingress-router.d.ts.map +1 -0
- package/dist/k8s/routing/ingress-router.js +125 -0
- package/dist/k8s/routing/ingress-router.js.map +1 -0
- package/dist/k8s/routing/route-health.d.ts +34 -0
- package/dist/k8s/routing/route-health.d.ts.map +1 -0
- package/dist/k8s/routing/route-health.js +77 -0
- package/dist/k8s/routing/route-health.js.map +1 -0
- package/dist/k8s/routing/routing-mode.d.ts +45 -0
- package/dist/k8s/routing/routing-mode.d.ts.map +1 -0
- package/dist/k8s/routing/routing-mode.js +141 -0
- package/dist/k8s/routing/routing-mode.js.map +1 -0
- package/dist/k8s/routing/session-endpoints.d.ts +25 -0
- package/dist/k8s/routing/session-endpoints.d.ts.map +1 -0
- package/dist/k8s/routing/session-endpoints.js +36 -0
- package/dist/k8s/routing/session-endpoints.js.map +1 -0
- package/dist/k8s/routing/session-router.d.ts +57 -0
- package/dist/k8s/routing/session-router.d.ts.map +1 -0
- package/dist/k8s/routing/session-router.js +2 -0
- package/dist/k8s/routing/session-router.js.map +1 -0
- package/dist/k8s/routing/session-service.d.ts +14 -0
- package/dist/k8s/routing/session-service.d.ts.map +1 -0
- package/dist/k8s/routing/session-service.js +42 -0
- package/dist/k8s/routing/session-service.js.map +1 -0
- package/dist/k8s/watch-session.d.ts +3 -0
- package/dist/k8s/watch-session.d.ts.map +1 -1
- package/dist/k8s/watch-session.js +82 -75
- package/dist/k8s/watch-session.js.map +1 -1
- package/dist/k8s/workspace-configmap.d.ts.map +1 -1
- package/dist/k8s/workspace-configmap.js +3 -12
- package/dist/k8s/workspace-configmap.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +34 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +116 -13
- package/src/k8s/backend.ts +73 -23
- package/src/k8s/client.ts +17 -1
- package/src/k8s/pod-status.ts +8 -0
- package/src/k8s/routing/create-or-replace.ts +16 -0
- package/src/k8s/routing/gateway-router.test.ts +236 -0
- package/src/k8s/routing/gateway-router.ts +219 -0
- package/src/k8s/routing/index.ts +44 -0
- package/src/k8s/routing/ingress-router.test.ts +236 -0
- package/src/k8s/routing/ingress-router.ts +157 -0
- package/src/k8s/routing/route-health.test.ts +215 -0
- package/src/k8s/routing/route-health.ts +95 -0
- package/src/k8s/routing/routing-mode.test.ts +218 -0
- package/src/k8s/routing/routing-mode.ts +191 -0
- package/src/k8s/routing/session-endpoints.ts +45 -0
- package/src/k8s/routing/session-router.ts +59 -0
- package/src/k8s/routing/session-service.ts +55 -0
- package/src/k8s/watch-session.ts +82 -64
- package/src/k8s/workspace-configmap.ts +3 -15
- package/src/server.ts +38 -1
- package/dist/k8s/ingress.d.ts +0 -15
- package/dist/k8s/ingress.d.ts.map +0 -1
- package/dist/k8s/ingress.js +0 -102
- package/dist/k8s/ingress.js.map +0 -1
- package/src/k8s/ingress.test.ts +0 -146
- package/src/k8s/ingress.ts +0 -127
package/src/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { kubernetesRunnerCapabilities } from "./capabilities.js";
|
|
|
16
16
|
import { loadK8sRunnerConfig, RunnerConfigError, type K8sRunnerConfig } from "./config.js";
|
|
17
17
|
import { createKubernetesBackend } from "./k8s/backend.js";
|
|
18
18
|
import { createKubeClient } from "./k8s/client.js";
|
|
19
|
+
import { createSessionRouter } from "./k8s/routing/index.js";
|
|
19
20
|
import { sweepWorkspaceConfigMaps } from "./k8s/workspace-configmap.js";
|
|
20
21
|
|
|
21
22
|
const VERSION: string = packageJson.version;
|
|
@@ -84,7 +85,22 @@ async function main(): Promise<void> {
|
|
|
84
85
|
|
|
85
86
|
const kube = createKubeClient();
|
|
86
87
|
const bundleStore = new BundleStore(config.selfUrl);
|
|
87
|
-
|
|
88
|
+
|
|
89
|
+
// Resolve the routing layer ONCE, here: an unroutable configuration must fail
|
|
90
|
+
// the runner rather than every session it would otherwise accept, publish
|
|
91
|
+
// routes for, and leave 404-ing with nothing reported.
|
|
92
|
+
let routing: Awaited<ReturnType<typeof createSessionRouter>>;
|
|
93
|
+
try {
|
|
94
|
+
routing = await createSessionRouter(kube, config);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err instanceof RunnerConfigError) {
|
|
97
|
+
process.stderr.write(`${err.message}\n`);
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const backend = createKubernetesBackend({ kube, config, bundleStore, router: routing.router });
|
|
88
104
|
|
|
89
105
|
const catalog = config.baseImageCatalog.enabled
|
|
90
106
|
? new BaseImageCatalog({
|
|
@@ -98,6 +114,27 @@ async function main(): Promise<void> {
|
|
|
98
114
|
|
|
99
115
|
const { app, registry } = await buildServer({ backend, config, bundleStore, catalog });
|
|
100
116
|
|
|
117
|
+
// Say which layer carries session traffic. An operator reading "logs-only" here
|
|
118
|
+
// is the difference between a misconfiguration found at boot and one found as a
|
|
119
|
+
// 404 on a URL the editor handed a user.
|
|
120
|
+
if (routing.resolved.layer === "none") {
|
|
121
|
+
app.log.warn({ reason: routing.resolved.reason }, "session routing disabled (logs-only)");
|
|
122
|
+
} else {
|
|
123
|
+
app.log.info(
|
|
124
|
+
{
|
|
125
|
+
layer: routing.resolved.layer,
|
|
126
|
+
...(routing.resolved.layer === "gateway"
|
|
127
|
+
? {
|
|
128
|
+
apiVersion: routing.resolved.apiVersion,
|
|
129
|
+
gateway: `${config.sessionRouting.gateway?.namespace}/${config.sessionRouting.gateway?.name}`,
|
|
130
|
+
}
|
|
131
|
+
: { ingressClass: config.sessionRouting.ingressClassName ?? "(cluster default)" }),
|
|
132
|
+
baseDomain: config.sessionRouting.baseDomain,
|
|
133
|
+
},
|
|
134
|
+
"session routing resolved",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
101
138
|
// Populate the catalog before serving so the first /v1/capabilities carries the
|
|
102
139
|
// full menu; a fetch failure degrades to the default image (surfaced, not
|
|
103
140
|
// swallowed) and the periodic refresh retries.
|
package/dist/k8s/ingress.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { V1Ingress, V1Service } from "@kubernetes/client-node";
|
|
2
|
-
import type { K8sRunnerConfig } from "../config.js";
|
|
3
|
-
import type { PortMapping, RunnerEndpoint } from "@telorun/runner-core";
|
|
4
|
-
export declare function buildSessionService(config: K8sRunnerConfig, sessionId: string, podName: string, podUid: string, ports: PortMapping[]): V1Service;
|
|
5
|
-
export declare function buildSessionIngress(config: K8sRunnerConfig, sessionId: string, serviceName: string, podName: string, podUid: string, ports: PortMapping[]): {
|
|
6
|
-
ingress: V1Ingress;
|
|
7
|
-
hosts: string[];
|
|
8
|
-
};
|
|
9
|
-
/** Endpoints announced on the `running` status. Every tcp port is fronted by its
|
|
10
|
-
* own per-session Ingress host (`<port>-<sessionId>.<domain>`, served on 443) and
|
|
11
|
-
* carries an external `url`. udp ports aren't HTTP-routable, so they keep the
|
|
12
|
-
* host-less form. Without an ingress base domain, host is left blank for the
|
|
13
|
-
* client adapter to fill (parity with docker). */
|
|
14
|
-
export declare function endpointsFor(config: K8sRunnerConfig, sessionId: string, ports: PortMapping[]): RunnerEndpoint[];
|
|
15
|
-
//# sourceMappingURL=ingress.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ingress.d.ts","sourceRoot":"","sources":["../../src/k8s/ingress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAoB,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAgBxE,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,WAAW,EAAE,GACnB,SAAS,CAoBX;AAUD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,WAAW,EAAE,GACnB;IAAE,OAAO,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CA0CzC;AAED;;;;mDAImD;AACnD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,EAAE,GACnB,cAAc,EAAE,CAWlB"}
|
package/dist/k8s/ingress.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/** OwnerReference to the session Pod so the Service + Ingress are garbage
|
|
2
|
-
* collected automatically when the Pod dies — essential for sub-minute
|
|
3
|
-
* sessions that would otherwise leak ingress objects. */
|
|
4
|
-
function podOwnerRef(podName, podUid) {
|
|
5
|
-
return {
|
|
6
|
-
apiVersion: "v1",
|
|
7
|
-
kind: "Pod",
|
|
8
|
-
name: podName,
|
|
9
|
-
uid: podUid,
|
|
10
|
-
controller: true,
|
|
11
|
-
blockOwnerDeletion: true,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
export function buildSessionService(config, sessionId, podName, podUid, ports) {
|
|
15
|
-
return {
|
|
16
|
-
apiVersion: "v1",
|
|
17
|
-
kind: "Service",
|
|
18
|
-
metadata: {
|
|
19
|
-
name: `telo-run-${sessionId}`,
|
|
20
|
-
namespace: config.sessionNamespace,
|
|
21
|
-
labels: { "app.kubernetes.io/managed-by": config.managedByLabel },
|
|
22
|
-
ownerReferences: [podOwnerRef(podName, podUid)],
|
|
23
|
-
},
|
|
24
|
-
spec: {
|
|
25
|
-
selector: { "telo.run/session-id": sessionId },
|
|
26
|
-
ports: ports.map((p) => ({
|
|
27
|
-
name: `p${p.port}`,
|
|
28
|
-
port: p.port,
|
|
29
|
-
targetPort: p.port,
|
|
30
|
-
protocol: p.protocol.toUpperCase(),
|
|
31
|
-
})),
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/** Host fronting a single tcp port: `<port>-<sessionId>.<domain>`. The port
|
|
36
|
-
* rides as a leading label (no dots), so it stays a single label under the base
|
|
37
|
-
* domain — matching the docker runner's proxy scheme and compatible with a
|
|
38
|
-
* single-label wildcard cert (`*.<domain>`). */
|
|
39
|
-
function hostForPort(config, sessionId, port) {
|
|
40
|
-
return `${port}-${sessionId}.${config.sessionIngressBaseDomain}`;
|
|
41
|
-
}
|
|
42
|
-
export function buildSessionIngress(config, sessionId, serviceName, podName, podUid, ports) {
|
|
43
|
-
// Only tcp ports are HTTP-routable; one host rule per port to the matching
|
|
44
|
-
// service port, mirroring the docker runner's per-port URLs.
|
|
45
|
-
const rules = ports
|
|
46
|
-
.filter((p) => p.protocol === "tcp")
|
|
47
|
-
.map((p) => ({
|
|
48
|
-
host: hostForPort(config, sessionId, p.port),
|
|
49
|
-
http: {
|
|
50
|
-
paths: [
|
|
51
|
-
{
|
|
52
|
-
path: "/",
|
|
53
|
-
pathType: "Prefix",
|
|
54
|
-
backend: { service: { name: serviceName, port: { number: p.port } } },
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
}));
|
|
59
|
-
const ingress = {
|
|
60
|
-
apiVersion: "networking.k8s.io/v1",
|
|
61
|
-
kind: "Ingress",
|
|
62
|
-
metadata: {
|
|
63
|
-
name: `telo-run-${sessionId}`,
|
|
64
|
-
namespace: config.sessionNamespace,
|
|
65
|
-
labels: { "app.kubernetes.io/managed-by": config.managedByLabel },
|
|
66
|
-
ownerReferences: [podOwnerRef(podName, podUid)],
|
|
67
|
-
},
|
|
68
|
-
spec: {
|
|
69
|
-
...(config.sessionIngressClassName
|
|
70
|
-
? { ingressClassName: config.sessionIngressClassName }
|
|
71
|
-
: {}),
|
|
72
|
-
// Present the predefined cert (e.g. a Cloudflare Origin cert) so an upstream
|
|
73
|
-
// in Full (Strict) mode can validate the origin. Only meaningful with routable
|
|
74
|
-
// hosts; a single wildcard `*.<domain>` Secret covers every session host.
|
|
75
|
-
...(config.sessionIngressTlsSecretName && rules.length > 0
|
|
76
|
-
? {
|
|
77
|
-
tls: [{ hosts: rules.map((r) => r.host), secretName: config.sessionIngressTlsSecretName }],
|
|
78
|
-
}
|
|
79
|
-
: {}),
|
|
80
|
-
rules,
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
return { ingress, hosts: rules.map((r) => r.host) };
|
|
84
|
-
}
|
|
85
|
-
/** Endpoints announced on the `running` status. Every tcp port is fronted by its
|
|
86
|
-
* own per-session Ingress host (`<port>-<sessionId>.<domain>`, served on 443) and
|
|
87
|
-
* carries an external `url`. udp ports aren't HTTP-routable, so they keep the
|
|
88
|
-
* host-less form. Without an ingress base domain, host is left blank for the
|
|
89
|
-
* client adapter to fill (parity with docker). */
|
|
90
|
-
export function endpointsFor(config, sessionId, ports) {
|
|
91
|
-
if (!config.sessionIngressBaseDomain || ports.length === 0) {
|
|
92
|
-
return ports.map((p) => ({ host: "", port: p.port, protocol: p.protocol }));
|
|
93
|
-
}
|
|
94
|
-
return ports.map((p) => {
|
|
95
|
-
if (p.protocol !== "tcp") {
|
|
96
|
-
return { host: "", port: p.port, protocol: p.protocol };
|
|
97
|
-
}
|
|
98
|
-
const host = hostForPort(config, sessionId, p.port);
|
|
99
|
-
return { host, port: p.port, protocol: p.protocol, url: `https://${host}` };
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
//# sourceMappingURL=ingress.js.map
|
package/dist/k8s/ingress.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ingress.js","sourceRoot":"","sources":["../../src/k8s/ingress.ts"],"names":[],"mappings":"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"}
|
package/src/k8s/ingress.test.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import type { PortMapping } from "@telorun/runner-core";
|
|
4
|
-
|
|
5
|
-
import type { K8sRunnerConfig } from "../config.js";
|
|
6
|
-
import { buildSessionIngress, endpointsFor } from "./ingress.js";
|
|
7
|
-
|
|
8
|
-
const config = {
|
|
9
|
-
sessionNamespace: "telo-sessions",
|
|
10
|
-
sessionIngressBaseDomain: "telo.run",
|
|
11
|
-
sessionIngressClassName: "nginx",
|
|
12
|
-
managedByLabel: "telo-k8s-runner",
|
|
13
|
-
} as K8sRunnerConfig;
|
|
14
|
-
|
|
15
|
-
const sessionId = "hzyayvabgyvz";
|
|
16
|
-
const ports: PortMapping[] = [
|
|
17
|
-
{ port: 8080, protocol: "tcp" },
|
|
18
|
-
{ port: 9090, protocol: "tcp" },
|
|
19
|
-
{ port: 5000, protocol: "udp" },
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
describe("endpointsFor", () => {
|
|
23
|
-
it("gives every tcp port a <port>-<id>.<domain> host + url, udp host-less", () => {
|
|
24
|
-
expect(endpointsFor(config, sessionId, ports)).toEqual([
|
|
25
|
-
{
|
|
26
|
-
host: `8080-${sessionId}.telo.run`,
|
|
27
|
-
port: 8080,
|
|
28
|
-
protocol: "tcp",
|
|
29
|
-
url: `https://8080-${sessionId}.telo.run`,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
host: `9090-${sessionId}.telo.run`,
|
|
33
|
-
port: 9090,
|
|
34
|
-
protocol: "tcp",
|
|
35
|
-
url: `https://9090-${sessionId}.telo.run`,
|
|
36
|
-
},
|
|
37
|
-
{ host: "", port: 5000, protocol: "udp" },
|
|
38
|
-
]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("leaves hosts blank when no ingress base domain is configured", () => {
|
|
42
|
-
const logsOnly = { ...config, sessionIngressBaseDomain: undefined } as K8sRunnerConfig;
|
|
43
|
-
expect(endpointsFor(logsOnly, sessionId, ports)).toEqual([
|
|
44
|
-
{ host: "", port: 8080, protocol: "tcp" },
|
|
45
|
-
{ host: "", port: 9090, protocol: "tcp" },
|
|
46
|
-
{ host: "", port: 5000, protocol: "udp" },
|
|
47
|
-
]);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("buildSessionIngress", () => {
|
|
52
|
-
it("emits one rule per tcp port to the matching service port", () => {
|
|
53
|
-
const { ingress, hosts } = buildSessionIngress(
|
|
54
|
-
config,
|
|
55
|
-
sessionId,
|
|
56
|
-
`telo-run-${sessionId}`,
|
|
57
|
-
"pod",
|
|
58
|
-
"uid",
|
|
59
|
-
ports,
|
|
60
|
-
);
|
|
61
|
-
expect(hosts).toEqual([`8080-${sessionId}.telo.run`, `9090-${sessionId}.telo.run`]);
|
|
62
|
-
expect(ingress.spec?.ingressClassName).toBe("nginx");
|
|
63
|
-
expect(ingress.spec?.rules).toEqual([
|
|
64
|
-
{
|
|
65
|
-
host: `8080-${sessionId}.telo.run`,
|
|
66
|
-
http: {
|
|
67
|
-
paths: [
|
|
68
|
-
{
|
|
69
|
-
path: "/",
|
|
70
|
-
pathType: "Prefix",
|
|
71
|
-
backend: { service: { name: `telo-run-${sessionId}`, port: { number: 8080 } } },
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
host: `9090-${sessionId}.telo.run`,
|
|
78
|
-
http: {
|
|
79
|
-
paths: [
|
|
80
|
-
{
|
|
81
|
-
path: "/",
|
|
82
|
-
pathType: "Prefix",
|
|
83
|
-
backend: { service: { name: `telo-run-${sessionId}`, port: { number: 9090 } } },
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("produces no rules when there are no tcp ports", () => {
|
|
92
|
-
const { ingress, hosts } = buildSessionIngress(
|
|
93
|
-
config,
|
|
94
|
-
sessionId,
|
|
95
|
-
`telo-run-${sessionId}`,
|
|
96
|
-
"pod",
|
|
97
|
-
"uid",
|
|
98
|
-
[{ port: 5000, protocol: "udp" }],
|
|
99
|
-
);
|
|
100
|
-
expect(hosts).toEqual([]);
|
|
101
|
-
expect(ingress.spec?.rules).toEqual([]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("omits the tls block when no tls secret is configured", () => {
|
|
105
|
-
const { ingress } = buildSessionIngress(
|
|
106
|
-
config,
|
|
107
|
-
sessionId,
|
|
108
|
-
`telo-run-${sessionId}`,
|
|
109
|
-
"pod",
|
|
110
|
-
"uid",
|
|
111
|
-
ports,
|
|
112
|
-
);
|
|
113
|
-
expect(ingress.spec?.tls).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("presents the configured tls secret for every tcp host", () => {
|
|
117
|
-
const tlsConfig = { ...config, sessionIngressTlsSecretName: "telo-origin-tls" } as K8sRunnerConfig;
|
|
118
|
-
const { ingress } = buildSessionIngress(
|
|
119
|
-
tlsConfig,
|
|
120
|
-
sessionId,
|
|
121
|
-
`telo-run-${sessionId}`,
|
|
122
|
-
"pod",
|
|
123
|
-
"uid",
|
|
124
|
-
ports,
|
|
125
|
-
);
|
|
126
|
-
expect(ingress.spec?.tls).toEqual([
|
|
127
|
-
{
|
|
128
|
-
hosts: [`8080-${sessionId}.telo.run`, `9090-${sessionId}.telo.run`],
|
|
129
|
-
secretName: "telo-origin-tls",
|
|
130
|
-
},
|
|
131
|
-
]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("omits the tls block when there are no tcp hosts to secure", () => {
|
|
135
|
-
const tlsConfig = { ...config, sessionIngressTlsSecretName: "telo-origin-tls" } as K8sRunnerConfig;
|
|
136
|
-
const { ingress } = buildSessionIngress(
|
|
137
|
-
tlsConfig,
|
|
138
|
-
sessionId,
|
|
139
|
-
`telo-run-${sessionId}`,
|
|
140
|
-
"pod",
|
|
141
|
-
"uid",
|
|
142
|
-
[{ port: 5000, protocol: "udp" }],
|
|
143
|
-
);
|
|
144
|
-
expect(ingress.spec?.tls).toBeUndefined();
|
|
145
|
-
});
|
|
146
|
-
});
|
package/src/k8s/ingress.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import type { V1Ingress, V1OwnerReference, V1Service } from "@kubernetes/client-node";
|
|
2
|
-
|
|
3
|
-
import type { K8sRunnerConfig } from "../config.js";
|
|
4
|
-
import type { PortMapping, RunnerEndpoint } from "@telorun/runner-core";
|
|
5
|
-
|
|
6
|
-
/** OwnerReference to the session Pod so the Service + Ingress are garbage
|
|
7
|
-
* collected automatically when the Pod dies — essential for sub-minute
|
|
8
|
-
* sessions that would otherwise leak ingress objects. */
|
|
9
|
-
function podOwnerRef(podName: string, podUid: string): V1OwnerReference {
|
|
10
|
-
return {
|
|
11
|
-
apiVersion: "v1",
|
|
12
|
-
kind: "Pod",
|
|
13
|
-
name: podName,
|
|
14
|
-
uid: podUid,
|
|
15
|
-
controller: true,
|
|
16
|
-
blockOwnerDeletion: true,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function buildSessionService(
|
|
21
|
-
config: K8sRunnerConfig,
|
|
22
|
-
sessionId: string,
|
|
23
|
-
podName: string,
|
|
24
|
-
podUid: string,
|
|
25
|
-
ports: PortMapping[],
|
|
26
|
-
): V1Service {
|
|
27
|
-
return {
|
|
28
|
-
apiVersion: "v1",
|
|
29
|
-
kind: "Service",
|
|
30
|
-
metadata: {
|
|
31
|
-
name: `telo-run-${sessionId}`,
|
|
32
|
-
namespace: config.sessionNamespace,
|
|
33
|
-
labels: { "app.kubernetes.io/managed-by": config.managedByLabel },
|
|
34
|
-
ownerReferences: [podOwnerRef(podName, podUid)],
|
|
35
|
-
},
|
|
36
|
-
spec: {
|
|
37
|
-
selector: { "telo.run/session-id": sessionId },
|
|
38
|
-
ports: ports.map((p) => ({
|
|
39
|
-
name: `p${p.port}`,
|
|
40
|
-
port: p.port,
|
|
41
|
-
targetPort: p.port,
|
|
42
|
-
protocol: p.protocol.toUpperCase(),
|
|
43
|
-
})),
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Host fronting a single tcp port: `<port>-<sessionId>.<domain>`. The port
|
|
49
|
-
* rides as a leading label (no dots), so it stays a single label under the base
|
|
50
|
-
* domain — matching the docker runner's proxy scheme and compatible with a
|
|
51
|
-
* single-label wildcard cert (`*.<domain>`). */
|
|
52
|
-
function hostForPort(config: K8sRunnerConfig, sessionId: string, port: number): string {
|
|
53
|
-
return `${port}-${sessionId}.${config.sessionIngressBaseDomain}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function buildSessionIngress(
|
|
57
|
-
config: K8sRunnerConfig,
|
|
58
|
-
sessionId: string,
|
|
59
|
-
serviceName: string,
|
|
60
|
-
podName: string,
|
|
61
|
-
podUid: string,
|
|
62
|
-
ports: PortMapping[],
|
|
63
|
-
): { ingress: V1Ingress; hosts: string[] } {
|
|
64
|
-
// Only tcp ports are HTTP-routable; one host rule per port to the matching
|
|
65
|
-
// service port, mirroring the docker runner's per-port URLs.
|
|
66
|
-
const rules = ports
|
|
67
|
-
.filter((p) => p.protocol === "tcp")
|
|
68
|
-
.map((p) => ({
|
|
69
|
-
host: hostForPort(config, sessionId, p.port),
|
|
70
|
-
http: {
|
|
71
|
-
paths: [
|
|
72
|
-
{
|
|
73
|
-
path: "/",
|
|
74
|
-
pathType: "Prefix" as const,
|
|
75
|
-
backend: { service: { name: serviceName, port: { number: p.port } } },
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
}));
|
|
80
|
-
const ingress: V1Ingress = {
|
|
81
|
-
apiVersion: "networking.k8s.io/v1",
|
|
82
|
-
kind: "Ingress",
|
|
83
|
-
metadata: {
|
|
84
|
-
name: `telo-run-${sessionId}`,
|
|
85
|
-
namespace: config.sessionNamespace,
|
|
86
|
-
labels: { "app.kubernetes.io/managed-by": config.managedByLabel },
|
|
87
|
-
ownerReferences: [podOwnerRef(podName, podUid)],
|
|
88
|
-
},
|
|
89
|
-
spec: {
|
|
90
|
-
...(config.sessionIngressClassName
|
|
91
|
-
? { ingressClassName: config.sessionIngressClassName }
|
|
92
|
-
: {}),
|
|
93
|
-
// Present the predefined cert (e.g. a Cloudflare Origin cert) so an upstream
|
|
94
|
-
// in Full (Strict) mode can validate the origin. Only meaningful with routable
|
|
95
|
-
// hosts; a single wildcard `*.<domain>` Secret covers every session host.
|
|
96
|
-
...(config.sessionIngressTlsSecretName && rules.length > 0
|
|
97
|
-
? {
|
|
98
|
-
tls: [{ hosts: rules.map((r) => r.host), secretName: config.sessionIngressTlsSecretName }],
|
|
99
|
-
}
|
|
100
|
-
: {}),
|
|
101
|
-
rules,
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
return { ingress, hosts: rules.map((r) => r.host) };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Endpoints announced on the `running` status. Every tcp port is fronted by its
|
|
108
|
-
* own per-session Ingress host (`<port>-<sessionId>.<domain>`, served on 443) and
|
|
109
|
-
* carries an external `url`. udp ports aren't HTTP-routable, so they keep the
|
|
110
|
-
* host-less form. Without an ingress base domain, host is left blank for the
|
|
111
|
-
* client adapter to fill (parity with docker). */
|
|
112
|
-
export function endpointsFor(
|
|
113
|
-
config: K8sRunnerConfig,
|
|
114
|
-
sessionId: string,
|
|
115
|
-
ports: PortMapping[],
|
|
116
|
-
): RunnerEndpoint[] {
|
|
117
|
-
if (!config.sessionIngressBaseDomain || ports.length === 0) {
|
|
118
|
-
return ports.map((p) => ({ host: "", port: p.port, protocol: p.protocol }));
|
|
119
|
-
}
|
|
120
|
-
return ports.map((p) => {
|
|
121
|
-
if (p.protocol !== "tcp") {
|
|
122
|
-
return { host: "", port: p.port, protocol: p.protocol };
|
|
123
|
-
}
|
|
124
|
-
const host = hostForPort(config, sessionId, p.port);
|
|
125
|
-
return { host, port: p.port, protocol: p.protocol, url: `https://${host}` };
|
|
126
|
-
});
|
|
127
|
-
}
|