GeezerVison is showing github.com — feat(supervisor): reserve configured source ports on run pods · triggerdotdev/trigger.dev@65d123f
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboard +6Lines changed: 6 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +--- | | 2 | +area: supervisor | | 3 | +type: feature | | 4 | +--- | | 5 | + | | 6 | +Set `KUBERNETES_POD_RESERVED_SOURCE_PORTS` on the supervisor (for example `50080,50081`) to stop runs using those ports for outbound connections, which is useful to avoid port clashes. |
Collapse file Copy file name to clipboardExpand all lines: apps/supervisor/src/env.ts +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 7 | 7 | BoolEnv, | 8 | 8 | NodeLabelValue, | 9 | 9 | OrgPlacementOverrides, | | 10 | + ReservedPorts, | 10 | 11 | Tolerations, | 11 | 12 | } from "./envUtil.js"; | 12 | 13 |
| @@ -280,6 +281,7 @@ export const Env = z | 280 | 281 | .default("node-24-plus"), | 281 | 282 | KUBERNETES_RUNNER_SECURITY_CONTEXT: z.enum(["off", "baseline", "restricted"]).default("off"), | 282 | 283 | KUBERNETES_RUNNER_RUN_AS_USER: z.coerce.number().int().min(1).default(1000), | | 284 | + KUBERNETES_POD_RESERVED_SOURCE_PORTS: ReservedPorts, | 283 | 285 |
| 284 | 286 | // Pod DNS config: override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`. | 285 | 287 | // Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked | |
Collapse file Copy file name to clipboardExpand all lines: apps/supervisor/src/envUtil.test.ts +22Lines changed: 22 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 4 | 4 | AdditionalEnvVars, | 5 | 5 | NodeLabelValue, | 6 | 6 | OrgPlacementOverrides, | | 7 | + ReservedPorts, | 7 | 8 | Tolerations, | 8 | 9 | } from "./envUtil.js"; | 9 | 10 |
| @@ -113,6 +114,27 @@ describe("NodeLabelValue", () => { | 113 | 114 | }); | 114 | 115 | }); | 115 | 116 |
| | 117 | +describe("ReservedPorts", () => { | | 118 | + it("is unset when empty", () => { | | 119 | + for (const value of [undefined, "", " "]) { | | 120 | + expect(ReservedPorts.parse(value)).toBeUndefined(); | | 121 | + } | | 122 | + }); | | 123 | + | | 124 | + it("accepts ports and ranges, stripping whitespace the kernel would reject", () => { | | 125 | + expect(ReservedPorts.parse("50080,50081")).toBe("50080,50081"); | | 126 | + expect(ReservedPorts.parse(" 50080 , 50000-50100 ")).toBe("50080,50000-50100"); | | 127 | + expect(ReservedPorts.parse("0,65535,8080-8080")).toBe("0,65535,8080-8080"); | | 128 | + }); | | 129 | + | | 130 | + it.each(["50080,", ",50080", "50080 50081", "65536", "50100-50000", "08080", "0x50", "-1", "a"])( | | 131 | + "refuses %j, so a bad value stops the supervisor from starting", | | 132 | + (value) => { | | 133 | + expect(ReservedPorts.safeParse(value).success).toBe(false); | | 134 | + } | | 135 | + ); | | 136 | +}); | | 137 | + | 116 | 138 | describe("Tolerations", () => { | 117 | 139 | it("should parse key=value entries as Equal", () => { | 118 | 140 | expect(Tolerations.parse("dedicated=runs:NoSchedule")).toEqual([ | |
Collapse file Copy file name to clipboardExpand all lines: apps/supervisor/src/envUtil.ts +30Lines changed: 30 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -146,6 +146,36 @@ export const Tolerations = z.string().transform((val, ctx) => { | 146 | 146 | }); | 147 | 147 | }); | 148 | 148 |
| | 149 | +// The kernel rejects whitespace between entries and reads a leading zero as octal. | | 150 | +const PORT_OR_RANGE = /^(0|[1-9]\d{0,4})(?:-(0|[1-9]\d{0,4}))?$/; | | 151 | + | | 152 | +export const ReservedPorts = z | | 153 | + .string() | | 154 | + .optional() | | 155 | + .transform((val, ctx) => { | | 156 | + if (val === undefined || val.trim() === "") { | | 157 | + return undefined; | | 158 | + } | | 159 | + | | 160 | + const entries = val.split(",").map((entry) => entry.trim()); | | 161 | + | | 162 | + for (const entry of entries) { | | 163 | + const match = PORT_OR_RANGE.exec(entry); | | 164 | + const low = Number(match?.[1]); | | 165 | + const high = Number(match?.[2] ?? match?.[1]); | | 166 | + | | 167 | + if (!match || high > 65535 || low > high) { | | 168 | + ctx.addIssue({ | | 169 | + code: z.ZodIssueCode.custom, | | 170 | + message: `Invalid reserved port "${entry}". Must be a port or an ascending low-high range, within 0-65535.`, | | 171 | + }); | | 172 | + return z.NEVER; | | 173 | + } | | 174 | + } | | 175 | + | | 176 | + return entries.join(","); | | 177 | + }); | | 178 | + | 149 | 179 | /** | 150 | 180 | * Scalar values are coerced: YAML/JSON easily produce `true` or `3` where a label | 151 | 181 | * value is meant, and Kubernetes label values are always strings. An empty value | |
Collapse file Copy file name to clipboardExpand all lines: apps/supervisor/src/workloadManager/kubernetes.test.ts +46Lines changed: 46 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | nodetypeNodeSelector, | 4 | 4 | runPodTolerations, | 5 | 5 | runnerSecurityContext, | | 6 | + withReservedSourcePorts, | 6 | 7 | withRunnerSeccompProfile, | 7 | 8 | withNodeSelector, | 8 | 9 | } from "./kubernetesPodSpec.js"; | @@ -155,6 +156,51 @@ describe("withRunnerSeccompProfile", () => { | 155 | 156 | }); | 156 | 157 | }); | 157 | 158 |
| | 159 | +describe("withReservedSourcePorts", () => { | | 160 | + const reserved = { name: "net.ipv4.ip_local_reserved_ports", value: "50080,50081" }; | | 161 | + | | 162 | + it("leaves the spec untouched when unset", () => { | | 163 | + for (const ports of [undefined, ""]) { | | 164 | + expect(withReservedSourcePorts(basePodSpec, ports)).toBe(basePodSpec); | | 165 | + } | | 166 | + }); | | 167 | + | | 168 | + it("adds the sysctl and keeps the rest of the security context", () => { | | 169 | + expect(withReservedSourcePorts(basePodSpec, "50080,50081")).toEqual({ | | 170 | + ...basePodSpec, | | 171 | + securityContext: { ...basePodSpec.securityContext, sysctls: [reserved] }, | | 172 | + }); | | 173 | + }); | | 174 | + | | 175 | + it("adds a security context when the spec has none", () => { | | 176 | + expect(withReservedSourcePorts({ restartPolicy: "Never" }, "50080,50081")).toEqual({ | | 177 | + restartPolicy: "Never", | | 178 | + securityContext: { sysctls: [reserved] }, | | 179 | + }); | | 180 | + }); | | 181 | + | | 182 | + it("preserves existing sysctls", () => { | | 183 | + const existing = { name: "net.ipv4.ip_local_port_range", value: "32768 60999" }; | | 184 | + const podSpec = { ...basePodSpec, securityContext: { sysctls: [existing] } }; | | 185 | + | | 186 | + expect(withReservedSourcePorts(podSpec, "50080,50081").securityContext?.sysctls).toEqual([ | | 187 | + existing, | | 188 | + reserved, | | 189 | + ]); | | 190 | + }); | | 191 | + | | 192 | + it("replaces an existing value rather than duplicating the name", () => { | | 193 | + const podSpec = { | | 194 | + ...basePodSpec, | | 195 | + securityContext: { sysctls: [{ ...reserved, value: "40000" }] }, | | 196 | + }; | | 197 | + | | 198 | + expect(withReservedSourcePorts(podSpec, "50080,50081").securityContext?.sysctls).toEqual([ | | 199 | + reserved, | | 200 | + ]); | | 201 | + }); | | 202 | +}); | | 203 | + | 158 | 204 | describe("runnerSecurityContext", () => { | 159 | 205 | it("sets nothing when off", () => { | 160 | 206 | expect(runnerSecurityContext("off", 1000, "node-24")).toBeUndefined(); | |
Collapse file Copy file name to clipboardExpand all lines: apps/supervisor/src/workloadManager/kubernetes.ts +10-6Lines changed: 10 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -18,6 +18,7 @@ import { | 18 | 18 | nodetypeNodeSelector, | 19 | 19 | runPodTolerations, | 20 | 20 | runnerSecurityContext, | | 21 | + withReservedSourcePorts, | 21 | 22 | withRunnerSeccompProfile, | 22 | 23 | withNodeSelector, | 23 | 24 | } from "./kubernetesPodSpec.js"; | @@ -137,12 +138,15 @@ export class KubernetesWorkloadManager implements WorkloadManager { | 137 | 138 | ); | 138 | 139 | } | 139 | 140 | } | 140 | | - const podSpec = withRunnerSeccompProfile(basePodSpec, { | 141 | | - profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH, | 142 | | - runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES, | 143 | | - runtime: opts.runtime, | 144 | | - checkpointsEnabled: this.opts.checkpointsEnabled, | 145 | | - }); | | 141 | + const podSpec = withReservedSourcePorts( | | 142 | + withRunnerSeccompProfile(basePodSpec, { | | 143 | + profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH, | | 144 | + runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES, | | 145 | + runtime: opts.runtime, | | 146 | + checkpointsEnabled: this.opts.checkpointsEnabled, | | 147 | + }), | | 148 | + env.KUBERNETES_POD_RESERVED_SOURCE_PORTS | | 149 | + ); | 146 | 150 |
| 147 | 151 | await this.k8s.core.createNamespacedPod({ | 148 | 152 | namespace: this.namespace, | |
Collapse file Copy file name to clipboardExpand all lines: apps/supervisor/src/workloadManager/kubernetesPodSpec.ts +24Lines changed: 24 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -97,6 +97,30 @@ export function withRunnerSeccompProfile( | 97 | 97 | }; | 98 | 98 | } | 99 | 99 |
| | 100 | +const RESERVED_PORTS_SYSCTL = "net.ipv4.ip_local_reserved_ports"; | | 101 | + | | 102 | +export function withReservedSourcePorts( | | 103 | + podSpec: Omit<k8s.V1PodSpec, "containers">, | | 104 | + ports: string | undefined | | 105 | +): Omit<k8s.V1PodSpec, "containers"> { | | 106 | + if (!ports) { | | 107 | + return podSpec; | | 108 | + } | | 109 | + | | 110 | + return { | | 111 | + ...podSpec, | | 112 | + securityContext: { | | 113 | + ...podSpec.securityContext, | | 114 | + sysctls: [ | | 115 | + ...(podSpec.securityContext?.sysctls ?? []).filter( | | 116 | + (sysctl) => sysctl.name !== RESERVED_PORTS_SYSCTL | | 117 | + ), | | 118 | + { name: RESERVED_PORTS_SYSCTL, value: ports }, | | 119 | + ], | | 120 | + }, | | 121 | + }; | | 122 | +} | | 123 | + | 100 | 124 | const BUN_RUN_AS_USER = 1001; | 101 | 125 |
| 102 | 126 | /** | |
|