@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.
Files changed (90) hide show
  1. package/README.md +93 -14
  2. package/dist/config.d.ts +44 -9
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +51 -4
  5. package/dist/config.js.map +1 -1
  6. package/dist/k8s/backend.d.ts +4 -0
  7. package/dist/k8s/backend.d.ts.map +1 -1
  8. package/dist/k8s/backend.js +55 -17
  9. package/dist/k8s/backend.js.map +1 -1
  10. package/dist/k8s/client.d.ts +7 -1
  11. package/dist/k8s/client.d.ts.map +1 -1
  12. package/dist/k8s/client.js +3 -1
  13. package/dist/k8s/client.js.map +1 -1
  14. package/dist/k8s/pod-status.d.ts +5 -0
  15. package/dist/k8s/pod-status.d.ts.map +1 -1
  16. package/dist/k8s/pod-status.js +7 -0
  17. package/dist/k8s/pod-status.js.map +1 -1
  18. package/dist/k8s/routing/create-or-replace.d.ts +5 -0
  19. package/dist/k8s/routing/create-or-replace.d.ts.map +1 -0
  20. package/dist/k8s/routing/create-or-replace.js +15 -0
  21. package/dist/k8s/routing/create-or-replace.js.map +1 -0
  22. package/dist/k8s/routing/gateway-router.d.ts +5 -0
  23. package/dist/k8s/routing/gateway-router.d.ts.map +1 -0
  24. package/dist/k8s/routing/gateway-router.js +178 -0
  25. package/dist/k8s/routing/gateway-router.js.map +1 -0
  26. package/dist/k8s/routing/index.d.ts +24 -0
  27. package/dist/k8s/routing/index.d.ts.map +1 -0
  28. package/dist/k8s/routing/index.js +27 -0
  29. package/dist/k8s/routing/index.js.map +1 -0
  30. package/dist/k8s/routing/ingress-router.d.ts +13 -0
  31. package/dist/k8s/routing/ingress-router.d.ts.map +1 -0
  32. package/dist/k8s/routing/ingress-router.js +125 -0
  33. package/dist/k8s/routing/ingress-router.js.map +1 -0
  34. package/dist/k8s/routing/route-health.d.ts +34 -0
  35. package/dist/k8s/routing/route-health.d.ts.map +1 -0
  36. package/dist/k8s/routing/route-health.js +77 -0
  37. package/dist/k8s/routing/route-health.js.map +1 -0
  38. package/dist/k8s/routing/routing-mode.d.ts +45 -0
  39. package/dist/k8s/routing/routing-mode.d.ts.map +1 -0
  40. package/dist/k8s/routing/routing-mode.js +141 -0
  41. package/dist/k8s/routing/routing-mode.js.map +1 -0
  42. package/dist/k8s/routing/session-endpoints.d.ts +25 -0
  43. package/dist/k8s/routing/session-endpoints.d.ts.map +1 -0
  44. package/dist/k8s/routing/session-endpoints.js +36 -0
  45. package/dist/k8s/routing/session-endpoints.js.map +1 -0
  46. package/dist/k8s/routing/session-router.d.ts +57 -0
  47. package/dist/k8s/routing/session-router.d.ts.map +1 -0
  48. package/dist/k8s/routing/session-router.js +2 -0
  49. package/dist/k8s/routing/session-router.js.map +1 -0
  50. package/dist/k8s/routing/session-service.d.ts +14 -0
  51. package/dist/k8s/routing/session-service.d.ts.map +1 -0
  52. package/dist/k8s/routing/session-service.js +42 -0
  53. package/dist/k8s/routing/session-service.js.map +1 -0
  54. package/dist/k8s/watch-session.d.ts +3 -0
  55. package/dist/k8s/watch-session.d.ts.map +1 -1
  56. package/dist/k8s/watch-session.js +82 -75
  57. package/dist/k8s/watch-session.js.map +1 -1
  58. package/dist/k8s/workspace-configmap.d.ts.map +1 -1
  59. package/dist/k8s/workspace-configmap.js +3 -12
  60. package/dist/k8s/workspace-configmap.js.map +1 -1
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +34 -1
  63. package/dist/server.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/config.ts +116 -13
  66. package/src/k8s/backend.ts +73 -23
  67. package/src/k8s/client.ts +17 -1
  68. package/src/k8s/pod-status.ts +8 -0
  69. package/src/k8s/routing/create-or-replace.ts +16 -0
  70. package/src/k8s/routing/gateway-router.test.ts +236 -0
  71. package/src/k8s/routing/gateway-router.ts +219 -0
  72. package/src/k8s/routing/index.ts +44 -0
  73. package/src/k8s/routing/ingress-router.test.ts +236 -0
  74. package/src/k8s/routing/ingress-router.ts +157 -0
  75. package/src/k8s/routing/route-health.test.ts +215 -0
  76. package/src/k8s/routing/route-health.ts +95 -0
  77. package/src/k8s/routing/routing-mode.test.ts +218 -0
  78. package/src/k8s/routing/routing-mode.ts +191 -0
  79. package/src/k8s/routing/session-endpoints.ts +45 -0
  80. package/src/k8s/routing/session-router.ts +59 -0
  81. package/src/k8s/routing/session-service.ts +55 -0
  82. package/src/k8s/watch-session.ts +82 -64
  83. package/src/k8s/workspace-configmap.ts +3 -15
  84. package/src/server.ts +38 -1
  85. package/dist/k8s/ingress.d.ts +0 -15
  86. package/dist/k8s/ingress.d.ts.map +0 -1
  87. package/dist/k8s/ingress.js +0 -102
  88. package/dist/k8s/ingress.js.map +0 -1
  89. package/src/k8s/ingress.test.ts +0 -146
  90. 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
- const backend = createKubernetesBackend({ kube, config, bundleStore });
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.
@@ -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"}
@@ -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
@@ -1 +0,0 @@
1
- {"version":3,"file":"ingress.js","sourceRoot":"","sources":["../../src/k8s/ingress.ts"],"names":[],"mappings":"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"}
@@ -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
- });
@@ -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
- }