Skip to content

Commit 75c796a

Browse files
committed
ability to set arbitrary env vars on new runners
1 parent 0309078 commit 75c796a

File tree

7 files changed

+137
-8
lines changed

7 files changed

+137
-8
lines changed

apps/supervisor/src/env.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import { randomUUID } from "crypto";
22
import { env as stdEnv } from "std-env";
33
import { z } from "zod";
4-
5-
const BoolEnv = z.preprocess((val) => {
6-
if (typeof val !== "string") {
7-
return val;
8-
}
9-
10-
return ["true", "1"].includes(val.toLowerCase().trim());
11-
}, z.boolean());
4+
import { AdditionalEnvVars, BoolEnv } from "./envUtil.js";
125

136
const Env = z.object({
147
// This will come from `spec.nodeName` in k8s
@@ -75,6 +68,9 @@ const Env = z.object({
7568

7669
// Debug
7770
DEBUG: BoolEnv.default(false),
71+
72+
// Additional environment variables (CSV format)
73+
RUNNER_ADDITIONAL_ENV_VARS: AdditionalEnvVars,
7874
});
7975

8076
export const env = Env.parse(stdEnv);

apps/supervisor/src/envUtil.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, it, expect } from "vitest";
2+
import { BoolEnv, AdditionalEnvVars } from "./envUtil.js";
3+
4+
describe("BoolEnv", () => {
5+
it("should parse string 'true' as true", () => {
6+
expect(BoolEnv.parse("true")).toBe(true);
7+
expect(BoolEnv.parse("TRUE")).toBe(true);
8+
expect(BoolEnv.parse("True")).toBe(true);
9+
});
10+
11+
it("should parse string '1' as true", () => {
12+
expect(BoolEnv.parse("1")).toBe(true);
13+
});
14+
15+
it("should parse string 'false' as false", () => {
16+
expect(BoolEnv.parse("false")).toBe(false);
17+
expect(BoolEnv.parse("FALSE")).toBe(false);
18+
expect(BoolEnv.parse("False")).toBe(false);
19+
});
20+
21+
it("should handle whitespace", () => {
22+
expect(BoolEnv.parse(" true ")).toBe(true);
23+
expect(BoolEnv.parse(" 1 ")).toBe(true);
24+
});
25+
26+
it("should pass through boolean values", () => {
27+
expect(BoolEnv.parse(true)).toBe(true);
28+
expect(BoolEnv.parse(false)).toBe(false);
29+
});
30+
31+
it("should return false for invalid inputs", () => {
32+
expect(BoolEnv.parse("invalid")).toBe(false);
33+
expect(BoolEnv.parse("")).toBe(false);
34+
});
35+
});
36+
37+
describe("AdditionalEnvVars", () => {
38+
it("should parse single key-value pair", () => {
39+
expect(AdditionalEnvVars.parse("FOO=bar")).toEqual({ FOO: "bar" });
40+
});
41+
42+
it("should parse multiple key-value pairs", () => {
43+
expect(AdditionalEnvVars.parse("FOO=bar,BAZ=qux")).toEqual({
44+
FOO: "bar",
45+
BAZ: "qux",
46+
});
47+
});
48+
49+
it("should handle whitespace", () => {
50+
expect(AdditionalEnvVars.parse(" FOO = bar , BAZ = qux ")).toEqual({
51+
FOO: "bar",
52+
BAZ: "qux",
53+
});
54+
});
55+
56+
it("should return undefined for empty string", () => {
57+
expect(AdditionalEnvVars.parse("")).toBeUndefined();
58+
});
59+
60+
it("should return undefined for invalid format", () => {
61+
expect(AdditionalEnvVars.parse("invalid")).toBeUndefined();
62+
});
63+
64+
it("should skip invalid pairs but include valid ones", () => {
65+
expect(AdditionalEnvVars.parse("FOO=bar,INVALID,BAZ=qux")).toEqual({
66+
FOO: "bar",
67+
BAZ: "qux",
68+
});
69+
});
70+
71+
it("should pass through undefined", () => {
72+
expect(AdditionalEnvVars.parse(undefined)).toBeUndefined();
73+
});
74+
75+
it("should handle empty values", () => {
76+
expect(AdditionalEnvVars.parse("FOO=,BAR=value")).toEqual({
77+
BAR: "value",
78+
});
79+
});
80+
});

apps/supervisor/src/envUtil.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z } from "zod";
2+
3+
export const BoolEnv = z.preprocess((val) => {
4+
if (typeof val !== "string") {
5+
return val;
6+
}
7+
8+
return ["true", "1"].includes(val.toLowerCase().trim());
9+
}, z.boolean());
10+
11+
export const AdditionalEnvVars = z.preprocess((val) => {
12+
if (typeof val !== "string") {
13+
return val;
14+
}
15+
16+
if (!val) {
17+
return undefined;
18+
}
19+
20+
try {
21+
const result = val.split(",").reduce(
22+
(acc, pair) => {
23+
const [key, value] = pair.split("=");
24+
if (!key || !value) {
25+
return acc;
26+
}
27+
acc[key.trim()] = value.trim();
28+
return acc;
29+
},
30+
{} as Record<string, string>
31+
);
32+
33+
// Return undefined if no valid key-value pairs were found
34+
return Object.keys(result).length === 0 ? undefined : result;
35+
} catch (error) {
36+
console.warn("Failed to parse additional env vars", { error, val });
37+
return undefined;
38+
}
39+
}, z.record(z.string(), z.string()).optional());

apps/supervisor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class ManagedSupervisor {
6464
imagePullSecrets: env.KUBERNETES_IMAGE_PULL_SECRETS?.split(","),
6565
heartbeatIntervalSeconds: env.RUNNER_HEARTBEAT_INTERVAL_SECONDS,
6666
snapshotPollIntervalSeconds: env.RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS,
67+
additionalEnvVars: env.RUNNER_ADDITIONAL_ENV_VARS,
6768
} satisfies WorkloadManagerOptions;
6869

6970
if (this.isKubernetes) {

apps/supervisor/src/workloadManager/docker.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export class DockerWorkloadManager implements WorkloadManager {
5757
);
5858
}
5959

60+
if (this.opts.additionalEnvVars) {
61+
Object.entries(this.opts.additionalEnvVars).forEach(([key, value]) => {
62+
runArgs.push(`--env=${key}=${value}`);
63+
});
64+
}
65+
6066
if (env.ENFORCE_MACHINE_PRESETS) {
6167
runArgs.push(`--cpus=${opts.machine.cpu}`, `--memory=${opts.machine.memory}G`);
6268
runArgs.push(`--env=TRIGGER_MACHINE_CPU=${opts.machine.cpu}`);

apps/supervisor/src/workloadManager/kubernetes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ export class KubernetesWorkloadManager implements WorkloadManager {
150150
},
151151
]
152152
: []),
153+
...(this.opts.additionalEnvVars
154+
? Object.entries(this.opts.additionalEnvVars).map(([key, value]) => ({
155+
name: key,
156+
value: value,
157+
}))
158+
: []),
153159
],
154160
},
155161
],

apps/supervisor/src/workloadManager/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface WorkloadManagerOptions {
88
imagePullSecrets?: string[];
99
heartbeatIntervalSeconds?: number;
1010
snapshotPollIntervalSeconds?: number;
11+
additionalEnvVars?: Record<string, string>;
1112
}
1213

1314
export interface WorkloadManager {

0 commit comments

Comments
 (0)