@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
@@ -0,0 +1,45 @@
1
+ import type { K8sRunnerConfig } from "../../config.js";
2
+ import type { KubeClient } from "../client.js";
3
+ export type ResolvedRouting = {
4
+ layer: "none";
5
+ reason: string;
6
+ } | {
7
+ layer: "ingress";
8
+ } | {
9
+ layer: "gateway";
10
+ apiVersion: string;
11
+ };
12
+ export interface ClusterRoutingSupport {
13
+ /** Served Gateway API version, preferred first; undefined when the group is
14
+ * not served at all. */
15
+ gatewayApiVersion?: string;
16
+ /** Number of IngressClasses, or **undefined when the list could not be read**.
17
+ * `networking.k8s.io` is always served, so the API being present is no
18
+ * evidence anyone can route with it — an installed controller registers a
19
+ * class, and that is the usable signal. Zero and unknown are kept apart
20
+ * because a refusal phrased from them is a different claim: one is about the
21
+ * cluster, the other about this runner's permissions. */
22
+ ingressClassCount?: number;
23
+ /** Set when at least one IngressClass carries the default-class annotation. */
24
+ hasDefaultIngressClass: boolean;
25
+ /** Why the IngressClass list could not be read, when it could not. */
26
+ ingressClassError?: unknown;
27
+ }
28
+ /** What the cluster can actually route with. Both probes fail SOFT — discovery
29
+ * and IngressClass listing are the runner's least important permissions — but a
30
+ * failure is REPORTED rather than folded into a zero, so nothing downstream can
31
+ * turn a permission error into a claim about the cluster. */
32
+ export declare function detectRoutingSupport(kube: KubeClient): Promise<ClusterRoutingSupport>;
33
+ /**
34
+ * Decide the routing layer ONCE, at boot, so a misconfiguration fails the runner
35
+ * instead of 404-ing every session it later accepts.
36
+ *
37
+ * `auto` resolves on what is CONFIGURED before what is INSTALLED, and that order
38
+ * is the whole point: Gateway API CRDs are frequently present without being the
39
+ * intended path, so "both APIs available" is not evidence of intent and must not
40
+ * decide anything. Only a cluster that offers two usable layers and was told
41
+ * nothing is genuinely ambiguous — and there the answer is to refuse, because
42
+ * guessing wrong publishes routes nothing reconciles, which is silent.
43
+ */
44
+ export declare function resolveRouting(kube: KubeClient, config: K8sRunnerConfig): Promise<ResolvedRouting>;
45
+ //# sourceMappingURL=routing-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-mode.d.ts","sourceRoot":"","sources":["../../../src/k8s/routing/routing-mode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAQ/C,MAAM,MAAM,eAAe,GACvB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GACpB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAM7C,MAAM,WAAW,qBAAqB;IACpC;6BACyB;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;8DAK0D;IAC1D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,sBAAsB,EAAE,OAAO,CAAC;IAChC,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;8DAG8D;AAC9D,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyB3F;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,eAAe,CAAC,CAmB1B"}
@@ -0,0 +1,141 @@
1
+ import { RunnerConfigError } from "../../config.js";
2
+ import { apiReason } from "../api-error.js";
3
+ const GATEWAY_GROUP = "gateway.networking.k8s.io";
4
+ /** Preference order among served Gateway API versions. `v1` is GA; plenty of
5
+ * clusters still serve only `v1beta1`, and an HTTPRoute posted at a version the
6
+ * apiserver does not serve is a 404 at publish time rather than a bad route. */
7
+ const GATEWAY_VERSIONS = ["v1", "v1beta1"];
8
+ /** The annotation by which a cluster nominates the class unqualified Ingresses
9
+ * go to — which is exactly what this runner creates when no class is set. */
10
+ const DEFAULT_CLASS_ANNOTATION = "ingressclass.kubernetes.io/is-default-class";
11
+ /** What the cluster can actually route with. Both probes fail SOFT — discovery
12
+ * and IngressClass listing are the runner's least important permissions — but a
13
+ * failure is REPORTED rather than folded into a zero, so nothing downstream can
14
+ * turn a permission error into a claim about the cluster. */
15
+ export async function detectRoutingSupport(kube) {
16
+ let gatewayApiVersion;
17
+ try {
18
+ const groups = await kube.apis.getAPIVersions();
19
+ const group = groups.groups?.find((g) => g.name === GATEWAY_GROUP);
20
+ const served = new Set((group?.versions ?? []).map((v) => v.version));
21
+ gatewayApiVersion =
22
+ GATEWAY_VERSIONS.find((v) => served.has(v)) ?? group?.preferredVersion?.version;
23
+ }
24
+ catch {
25
+ gatewayApiVersion = undefined;
26
+ }
27
+ try {
28
+ const classes = await kube.networking.listIngressClass();
29
+ const items = classes.items ?? [];
30
+ return {
31
+ gatewayApiVersion,
32
+ ingressClassCount: items.length,
33
+ hasDefaultIngressClass: items.some((c) => c.metadata?.annotations?.[DEFAULT_CLASS_ANNOTATION] === "true"),
34
+ };
35
+ }
36
+ catch (err) {
37
+ return { gatewayApiVersion, hasDefaultIngressClass: false, ingressClassError: err };
38
+ }
39
+ }
40
+ /**
41
+ * Decide the routing layer ONCE, at boot, so a misconfiguration fails the runner
42
+ * instead of 404-ing every session it later accepts.
43
+ *
44
+ * `auto` resolves on what is CONFIGURED before what is INSTALLED, and that order
45
+ * is the whole point: Gateway API CRDs are frequently present without being the
46
+ * intended path, so "both APIs available" is not evidence of intent and must not
47
+ * decide anything. Only a cluster that offers two usable layers and was told
48
+ * nothing is genuinely ambiguous — and there the answer is to refuse, because
49
+ * guessing wrong publishes routes nothing reconciles, which is silent.
50
+ */
51
+ export async function resolveRouting(kube, config) {
52
+ const routing = config.sessionRouting;
53
+ if (routing.mode === "none") {
54
+ return { layer: "none", reason: "SESSION_ROUTING_MODE=none" };
55
+ }
56
+ // The base domain is what makes a host constructible at all, so it stays the
57
+ // master switch it has always been — no domain, no routing, whatever the mode.
58
+ if (!routing.baseDomain) {
59
+ return {
60
+ layer: "none",
61
+ reason: "SESSION_ROUTING_BASE_DOMAIN is unset, so no session host can be constructed",
62
+ };
63
+ }
64
+ const support = await detectRoutingSupport(kube);
65
+ const resolved = decideLayer(config, support);
66
+ assertTlsMatchesLayer(config, resolved);
67
+ return resolved;
68
+ }
69
+ /** The origin certificate is refused HERE as well as at config load, because
70
+ * under `auto` the effective layer is not known until now: a named Gateway wins,
71
+ * so `mode: auto` + a Gateway + `SESSION_INGRESS_TLS_SECRET` passed the
72
+ * config-time check and then routed on Gateway API with the certificate
73
+ * ignored — plaintext origin traffic under a configuration that reads as
74
+ * TLS-configured, which is the silent downgrade the check exists to prevent. */
75
+ function assertTlsMatchesLayer(config, resolved) {
76
+ if (resolved.layer !== "gateway" || !config.sessionRouting.tlsSecretName)
77
+ return;
78
+ throw new RunnerConfigError("SESSION_INGRESS_TLS_SECRET is set but session routing resolved to Gateway API. Under Gateway " +
79
+ "API the origin certificate belongs to the Gateway listener's own `tls.certificateRefs`, not " +
80
+ "to the per-session route, so this Secret would be silently ignored. Move the reference to " +
81
+ "the Gateway and unset this, or set SESSION_ROUTING_MODE=ingress.");
82
+ }
83
+ function decideLayer(config, support) {
84
+ const routing = config.sessionRouting;
85
+ if (routing.mode === "ingress")
86
+ return { layer: "ingress" };
87
+ if (routing.mode === "gateway") {
88
+ if (!support.gatewayApiVersion) {
89
+ throw new RunnerConfigError(`SESSION_ROUTING_MODE=gateway but the cluster does not serve '${GATEWAY_GROUP}'. ` +
90
+ "Install the Gateway API CRDs, or set SESSION_ROUTING_MODE=ingress.");
91
+ }
92
+ return { layer: "gateway", apiVersion: support.gatewayApiVersion };
93
+ }
94
+ // auto — configured target first.
95
+ if (routing.gateway) {
96
+ if (!support.gatewayApiVersion) {
97
+ throw new RunnerConfigError(`SESSION_GATEWAY_NAME is set but the cluster does not serve '${GATEWAY_GROUP}'. ` +
98
+ "Install the Gateway API CRDs, or unset SESSION_GATEWAY_NAME to route via Ingress.");
99
+ }
100
+ return { layer: "gateway", apiVersion: support.gatewayApiVersion };
101
+ }
102
+ if (routing.ingressClassName)
103
+ return { layer: "ingress" };
104
+ // A DEFAULT-marked IngressClass is configuration, not mere availability: the
105
+ // cluster has nominated where an unqualified Ingress goes, and unqualified is
106
+ // exactly what this runner creates. It therefore ranks with a named Gateway
107
+ // and a configured class rather than with "an IngressClass exists" — without
108
+ // which every cluster that ships the Gateway CRDs beside a default ingress
109
+ // controller would refuse to boot after this upgrade, having worked before.
110
+ if (support.hasDefaultIngressClass)
111
+ return { layer: "ingress" };
112
+ const gatewayUsable = Boolean(support.gatewayApiVersion);
113
+ const ingressUsable = (support.ingressClassCount ?? 0) > 0;
114
+ if (gatewayUsable && ingressUsable) {
115
+ throw new RunnerConfigError(`SESSION_ROUTING_MODE=auto cannot choose: this cluster serves '${GATEWAY_GROUP}' AND has ` +
116
+ `${support.ingressClassCount} IngressClass(es) with none marked default, and nothing says ` +
117
+ "which should carry session traffic. Set SESSION_ROUTING_MODE=ingress (with " +
118
+ "SESSION_INGRESS_CLASS) or SESSION_ROUTING_MODE=gateway (with SESSION_GATEWAY_NAME).");
119
+ }
120
+ if (gatewayUsable) {
121
+ throw new RunnerConfigError(`SESSION_ROUTING_MODE=auto resolved to Gateway API, but SESSION_GATEWAY_NAME is unset — a ` +
122
+ "route has to name the Gateway it attaches to, and picking one of several would fail as " +
123
+ "silently as publishing none. Set SESSION_GATEWAY_NAME (and SESSION_GATEWAY_NAMESPACE " +
124
+ `when the Gateway is not in '${config.sessionNamespace}').`);
125
+ }
126
+ if (ingressUsable)
127
+ return { layer: "ingress" };
128
+ // Say which of the two it is. Reporting a permission failure as "no
129
+ // IngressClass exists" is a claim about the cluster drawn from a fact about
130
+ // this runner's access, and it sends an operator to install a controller they
131
+ // already have.
132
+ if (support.ingressClassError !== undefined) {
133
+ throw new RunnerConfigError("SESSION_ROUTING_BASE_DOMAIN is set but the routing layer could not be resolved: listing " +
134
+ `IngressClasses failed (${apiReason(support.ingressClassError)}) and '${GATEWAY_GROUP}' is ` +
135
+ "not served. Grant `list` on `ingressclasses`, or set SESSION_ROUTING_MODE explicitly.");
136
+ }
137
+ throw new RunnerConfigError("SESSION_ROUTING_BASE_DOMAIN is set but this cluster offers no usable routing layer: no " +
138
+ `IngressClass exists and '${GATEWAY_GROUP}' is not served. Install an Ingress controller or ` +
139
+ "the Gateway API, or set SESSION_ROUTING_MODE=none to run logs-only.");
140
+ }
141
+ //# sourceMappingURL=routing-mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-mode.js","sourceRoot":"","sources":["../../../src/k8s/routing/routing-mode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { PortMapping, RunnerEndpoint } from "@telorun/runner-core";
2
+ import type { K8sRunnerConfig } from "../../config.js";
3
+ /** Host fronting a single tcp port: `<port>-<sessionId>.<domain>`. The port rides
4
+ * as a leading label (no dots), so it stays a single label under the base domain
5
+ * — matching the docker runner's proxy scheme and compatible with a single-label
6
+ * wildcard cert (`*.<domain>`).
7
+ *
8
+ * Shared by both routing layers on purpose: the host scheme is what a client is
9
+ * told and what DNS is configured for, so it must not vary with the cluster's
10
+ * choice of Ingress or Gateway API. */
11
+ export declare function hostForPort(config: K8sRunnerConfig, sessionId: string, port: number): string;
12
+ /**
13
+ * Endpoints announced on the `running` status. Every tcp port is fronted by its
14
+ * own per-session host (served on 443) and carries an external `url`. udp ports
15
+ * aren't HTTP-routable, so they keep the host-less form. Without a base domain,
16
+ * host is left blank for the client adapter to fill (parity with docker).
17
+ *
18
+ * `routed` is what says a URL will be served, and it is separate from the base
19
+ * domain because the two can disagree: `SESSION_ROUTING_MODE=none` with a domain
20
+ * configured is a valid, documented setup that publishes nothing. Keying on the
21
+ * domain alone handed the editor a URL nothing serves — and the route watch
22
+ * never runs in that mode, so no `route` event would have said so either.
23
+ */
24
+ export declare function endpointsFor(config: K8sRunnerConfig, sessionId: string, ports: PortMapping[], routed: boolean): RunnerEndpoint[];
25
+ //# sourceMappingURL=session-endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-endpoints.d.ts","sourceRoot":"","sources":["../../../src/k8s/routing/session-endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;;;;;wCAOwC;AACxC,wBAAgB,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,EAAE,EACpB,MAAM,EAAE,OAAO,GACd,cAAc,EAAE,CAWlB"}
@@ -0,0 +1,36 @@
1
+ /** Host fronting a single tcp port: `<port>-<sessionId>.<domain>`. The port rides
2
+ * as a leading label (no dots), so it stays a single label under the base domain
3
+ * — matching the docker runner's proxy scheme and compatible with a single-label
4
+ * wildcard cert (`*.<domain>`).
5
+ *
6
+ * Shared by both routing layers on purpose: the host scheme is what a client is
7
+ * told and what DNS is configured for, so it must not vary with the cluster's
8
+ * choice of Ingress or Gateway API. */
9
+ export function hostForPort(config, sessionId, port) {
10
+ return `${port}-${sessionId}.${config.sessionRouting.baseDomain}`;
11
+ }
12
+ /**
13
+ * Endpoints announced on the `running` status. Every tcp port is fronted by its
14
+ * own per-session host (served on 443) and carries an external `url`. udp ports
15
+ * aren't HTTP-routable, so they keep the host-less form. Without a base domain,
16
+ * host is left blank for the client adapter to fill (parity with docker).
17
+ *
18
+ * `routed` is what says a URL will be served, and it is separate from the base
19
+ * domain because the two can disagree: `SESSION_ROUTING_MODE=none` with a domain
20
+ * configured is a valid, documented setup that publishes nothing. Keying on the
21
+ * domain alone handed the editor a URL nothing serves — and the route watch
22
+ * never runs in that mode, so no `route` event would have said so either.
23
+ */
24
+ export function endpointsFor(config, sessionId, ports, routed) {
25
+ if (!routed || !config.sessionRouting.baseDomain || ports.length === 0) {
26
+ return ports.map((p) => ({ host: "", port: p.port, protocol: p.protocol }));
27
+ }
28
+ return ports.map((p) => {
29
+ if (p.protocol !== "tcp") {
30
+ return { host: "", port: p.port, protocol: p.protocol };
31
+ }
32
+ const host = hostForPort(config, sessionId, p.port);
33
+ return { host, port: p.port, protocol: p.protocol, url: `https://${host}` };
34
+ });
35
+ }
36
+ //# sourceMappingURL=session-endpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-endpoints.js","sourceRoot":"","sources":["../../../src/k8s/routing/session-endpoints.ts"],"names":[],"mappings":"AAIA;;;;;;;wCAOwC;AACxC,MAAM,UAAU,WAAW,CAAC,MAAuB,EAAE,SAAiB,EAAE,IAAY;IAClF,OAAO,GAAG,IAAI,IAAI,SAAS,IAAI,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,CAAC;AACpE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAuB,EACvB,SAAiB,EACjB,KAAoB,EACpB,MAAe;IAEf,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACrB,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YACzB,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC1D,CAAC;QACD,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QACpD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,GAAG,EAAE,WAAW,IAAI,EAAE,EAAE,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { PortMapping } from "@telorun/runner-core";
2
+ /** One published host and the session port behind it. */
3
+ export interface PublishedRoute {
4
+ host: string;
5
+ port: number;
6
+ }
7
+ export interface PublishRouteArgs {
8
+ sessionId: string;
9
+ /** The Service the routing object points at. */
10
+ serviceName: string;
11
+ /** Owner of every object created — see `podOwnerRef`. */
12
+ podName: string;
13
+ podUid: string;
14
+ /** Every port the session must carry. Non-tcp ports are ignored: they are not
15
+ * HTTP-routable, and the Service still exposes them. */
16
+ ports: PortMapping[];
17
+ }
18
+ /**
19
+ * What a routing layer has to say about one published host.
20
+ *
21
+ * `pending` is a real answer, not an absence: a controller that has not yet
22
+ * written status is indistinguishable from one that will never claim the route,
23
+ * and only time separates them.
24
+ */
25
+ export type RouteVerdict = {
26
+ kind: "programmed";
27
+ } | {
28
+ kind: "pending";
29
+ } | {
30
+ kind: "rejected";
31
+ reason: string;
32
+ };
33
+ /**
34
+ * How this cluster publishes a session's public routes.
35
+ *
36
+ * The seam exists because Ingress and Gateway API are both current: neither is a
37
+ * migration target for the other, clusters ship one, the other, or both, and a
38
+ * runner that hardcodes either is unroutable on half of them. It deliberately
39
+ * stays inside the k8s backend — docker publishes host ports through its own
40
+ * proxy and has no use for it, and `runner-core` owns the backend-neutral `/v1`
41
+ * contract, where "Ingress or HTTPRoute" is not a distinction that exists.
42
+ */
43
+ export interface SessionRouter {
44
+ /** Names the layer for diagnostics. */
45
+ readonly layer: "ingress" | "gateway";
46
+ /** Create-or-replace this session's routing objects. Returns the hosts it
47
+ * published, which is what the route watch then asks about. */
48
+ publish(args: PublishRouteArgs): Promise<PublishedRoute[]>;
49
+ /** This layer's current verdict on one published host. Asked repeatedly by the
50
+ * route watch until it settles or the deadline passes. */
51
+ verdictFor(sessionId: string, route: PublishedRoute): Promise<RouteVerdict>;
52
+ /** What to tell an operator when a route is still `pending` at the deadline.
53
+ * Layer-specific because the causes are: no controller for the IngressClass,
54
+ * versus a Gateway that never claimed the route. */
55
+ unclaimedReason(): string;
56
+ }
57
+ //# sourceMappingURL=session-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-router.d.ts","sourceRoot":"","sources":["../../../src/k8s/routing/session-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf;6DACyD;IACzD,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IAEtC;oEACgE;IAChE,OAAO,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAE3D;+DAC2D;IAC3D,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAE5E;;yDAEqD;IACrD,eAAe,IAAI,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-router.js","sourceRoot":"","sources":["../../../src/k8s/routing/session-router.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ import type { V1OwnerReference, V1Service } from "@kubernetes/client-node";
2
+ import type { PortMapping } from "@telorun/runner-core";
3
+ import type { K8sRunnerConfig } from "../../config.js";
4
+ /** OwnerReference to the session Pod so every routing object is garbage collected
5
+ * when the Pod dies — essential for sub-minute sessions that would otherwise leak
6
+ * routing objects. Both routing layers use it: an HTTPRoute is namespaced like an
7
+ * Ingress, so the same ownership works unchanged. */
8
+ export declare function podOwnerRef(podName: string, podUid: string): V1OwnerReference;
9
+ /** The name every routing object for one session shares. */
10
+ export declare function sessionObjectName(sessionId: string): string;
11
+ /** The Service both routing layers point at — the routing layer decides how
12
+ * traffic ARRIVES, never what it arrives at, so this is layer-neutral. */
13
+ export declare function buildSessionService(config: K8sRunnerConfig, sessionId: string, podName: string, podUid: string, ports: PortMapping[]): V1Service;
14
+ //# sourceMappingURL=session-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-service.d.ts","sourceRoot":"","sources":["../../../src/k8s/routing/session-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAE3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;sDAGsD;AACtD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAS7E;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;2EAC2E;AAC3E,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"}
@@ -0,0 +1,42 @@
1
+ /** OwnerReference to the session Pod so every routing object is garbage collected
2
+ * when the Pod dies — essential for sub-minute sessions that would otherwise leak
3
+ * routing objects. Both routing layers use it: an HTTPRoute is namespaced like an
4
+ * Ingress, so the same ownership works unchanged. */
5
+ export function podOwnerRef(podName, podUid) {
6
+ return {
7
+ apiVersion: "v1",
8
+ kind: "Pod",
9
+ name: podName,
10
+ uid: podUid,
11
+ controller: true,
12
+ blockOwnerDeletion: true,
13
+ };
14
+ }
15
+ /** The name every routing object for one session shares. */
16
+ export function sessionObjectName(sessionId) {
17
+ return `telo-run-${sessionId}`;
18
+ }
19
+ /** The Service both routing layers point at — the routing layer decides how
20
+ * traffic ARRIVES, never what it arrives at, so this is layer-neutral. */
21
+ export function buildSessionService(config, sessionId, podName, podUid, ports) {
22
+ return {
23
+ apiVersion: "v1",
24
+ kind: "Service",
25
+ metadata: {
26
+ name: sessionObjectName(sessionId),
27
+ namespace: config.sessionNamespace,
28
+ labels: { "app.kubernetes.io/managed-by": config.managedByLabel },
29
+ ownerReferences: [podOwnerRef(podName, podUid)],
30
+ },
31
+ spec: {
32
+ selector: { "telo.run/session-id": sessionId },
33
+ ports: ports.map((p) => ({
34
+ name: `p${p.port}`,
35
+ port: p.port,
36
+ targetPort: p.port,
37
+ protocol: p.protocol.toUpperCase(),
38
+ })),
39
+ },
40
+ };
41
+ }
42
+ //# sourceMappingURL=session-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-service.js","sourceRoot":"","sources":["../../../src/k8s/routing/session-service.ts"],"names":[],"mappings":"AAMA;;;sDAGsD;AACtD,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,MAAc;IACzD,OAAO;QACL,UAAU,EAAE,IAAI;QAChB,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,OAAO;QACb,GAAG,EAAE,MAAM;QACX,UAAU,EAAE,IAAI;QAChB,kBAAkB,EAAE,IAAI;KACzB,CAAC;AACJ,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,OAAO,YAAY,SAAS,EAAE,CAAC;AACjC,CAAC;AAED;2EAC2E;AAC3E,MAAM,UAAU,mBAAmB,CACjC,MAAuB,EACvB,SAAiB,EACjB,OAAe,EACf,MAAc,EACd,KAAoB;IAEpB,OAAO;QACL,UAAU,EAAE,IAAI;QAChB,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,IAAI,EAAE,iBAAiB,CAAC,SAAS,CAAC;YAClC,SAAS,EAAE,MAAM,CAAC,gBAAgB;YAClC,MAAM,EAAE,EAAE,8BAA8B,EAAE,MAAM,CAAC,cAAc,EAAE;YACjE,eAAe,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;SAChD;QACD,IAAI,EAAE;YACJ,QAAQ,EAAE,EAAE,qBAAqB,EAAE,SAAS,EAAE;YAC9C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvB,IAAI,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE;gBAClB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,UAAU,EAAE,CAAC,CAAC,IAAI;gBAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE;aACnC,CAAC,CAAC;SACJ;KACF,CAAC;AACJ,CAAC"}
@@ -1,9 +1,12 @@
1
1
  import type { BackendSession, BackendStartSpec } from "@telorun/runner-core";
2
2
  import type { K8sRunnerConfig } from "../config.js";
3
3
  import type { KubeClient } from "./client.js";
4
+ import { type SessionRouter } from "./routing/index.js";
4
5
  export interface WatchSessionDeps {
5
6
  kube: KubeClient;
6
7
  config: K8sRunnerConfig;
8
+ /** Resolved at boot; absent → logs-only. */
9
+ router?: SessionRouter;
7
10
  }
8
11
  /**
9
12
  * A watch session: one pod, one workspace volume, one container per running
@@ -1 +1 @@
1
- {"version":3,"file":"watch-session.d.ts","sourceRoot":"","sources":["../../src/k8s/watch-session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,cAAc,EACd,gBAAgB,EAMjB,MAAM,sBAAsB,CAAC;AAW9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAsB9C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,eAAe,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CAwkBzB"}
1
+ {"version":3,"file":"watch-session.d.ts","sourceRoot":"","sources":["../../src/k8s/watch-session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,cAAc,EACd,gBAAgB,EAMjB,MAAM,sBAAsB,CAAC;AAW9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAqB5B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,eAAe,CAAC;IACxB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CAqlBzB"}