Skip to content

Commit 7cbd3aa

Browse files
committed
feat(supervisor): add ComputeWorkloadManager for compute gateway
Add a third WorkloadManager implementation that creates sandboxes via the compute gateway HTTP API (POST /api/sandboxes). Uses native fetch with no new dependencies. Enabled by setting COMPUTE_GATEWAY_URL, which takes priority over Kubernetes and Docker providers.
1 parent bc63edd commit 7cbd3aa

3 files changed

Lines changed: 130 additions & 3 deletions

File tree

apps/supervisor/src/env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const Env = z.object({
7777
*/
7878
DOCKER_RUNNER_NETWORKS: z.string().default("host"),
7979

80+
// Compute settings
81+
COMPUTE_GATEWAY_URL: z.string().url().optional(),
82+
COMPUTE_GATEWAY_AUTH_TOKEN: z.string().optional(),
83+
8084
// Kubernetes settings
8185
KUBERNETES_FORCE_ENABLED: BoolEnv.default(false),
8286
KUBERNETES_NAMESPACE: z.string().default("default"),

apps/supervisor/src/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "./resourceMonitor.js";
1515
import { KubernetesWorkloadManager } from "./workloadManager/kubernetes.js";
1616
import { DockerWorkloadManager } from "./workloadManager/docker.js";
17+
import { ComputeWorkloadManager } from "./workloadManager/compute.js";
1718
import {
1819
HttpServer,
1920
CheckpointClient,
@@ -77,9 +78,15 @@ class ManagedSupervisor {
7778
: new DockerResourceMonitor(new Docker())
7879
: new NoopResourceMonitor();
7980

80-
this.workloadManager = this.isKubernetes
81-
? new KubernetesWorkloadManager(workloadManagerOptions)
82-
: new DockerWorkloadManager(workloadManagerOptions);
81+
this.workloadManager = env.COMPUTE_GATEWAY_URL
82+
? new ComputeWorkloadManager({
83+
...workloadManagerOptions,
84+
gatewayUrl: env.COMPUTE_GATEWAY_URL,
85+
gatewayAuthToken: env.COMPUTE_GATEWAY_AUTH_TOKEN,
86+
})
87+
: this.isKubernetes
88+
? new KubernetesWorkloadManager(workloadManagerOptions)
89+
: new DockerWorkloadManager(workloadManagerOptions);
8390

8491
if (this.isKubernetes) {
8592
if (env.POD_CLEANER_ENABLED) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
2+
import {
3+
type WorkloadManager,
4+
type WorkloadManagerCreateOptions,
5+
type WorkloadManagerOptions,
6+
} from "./types.js";
7+
import { env } from "../env.js";
8+
import { getRunnerId } from "../util.js";
9+
import { tryCatch } from "@trigger.dev/core";
10+
11+
type ComputeWorkloadManagerOptions = WorkloadManagerOptions & {
12+
gatewayUrl: string;
13+
gatewayAuthToken?: string;
14+
};
15+
16+
export class ComputeWorkloadManager implements WorkloadManager {
17+
private readonly logger = new SimpleStructuredLogger("compute-workload-manager");
18+
19+
constructor(private opts: ComputeWorkloadManagerOptions) {
20+
if (!opts.workloadApiDomain) {
21+
this.logger.warn(
22+
"⚠️ workloadApiDomain is unset — VMs need an explicit host IP to reach the supervisor"
23+
);
24+
}
25+
}
26+
27+
async create(opts: WorkloadManagerCreateOptions) {
28+
this.logger.log("create()", { opts });
29+
30+
const runnerId = getRunnerId(opts.runFriendlyId, opts.nextAttemptNumber);
31+
32+
const envVars: Record<string, string> = {
33+
OTEL_EXPORTER_OTLP_ENDPOINT: env.OTEL_EXPORTER_OTLP_ENDPOINT,
34+
TRIGGER_DEQUEUED_AT_MS: String(opts.dequeuedAt.getTime()),
35+
TRIGGER_POD_SCHEDULED_AT_MS: String(Date.now()),
36+
TRIGGER_ENV_ID: opts.envId,
37+
TRIGGER_DEPLOYMENT_ID: opts.deploymentFriendlyId,
38+
TRIGGER_DEPLOYMENT_VERSION: opts.deploymentVersion,
39+
TRIGGER_RUN_ID: opts.runFriendlyId,
40+
TRIGGER_SNAPSHOT_ID: opts.snapshotFriendlyId,
41+
TRIGGER_SUPERVISOR_API_PROTOCOL: this.opts.workloadApiProtocol,
42+
TRIGGER_SUPERVISOR_API_PORT: String(this.opts.workloadApiPort),
43+
TRIGGER_SUPERVISOR_API_DOMAIN: this.opts.workloadApiDomain ?? "",
44+
TRIGGER_WORKER_INSTANCE_NAME: env.TRIGGER_WORKER_INSTANCE_NAME,
45+
TRIGGER_RUNNER_ID: runnerId,
46+
TRIGGER_MACHINE_CPU: String(opts.machine.cpu),
47+
TRIGGER_MACHINE_MEMORY: String(opts.machine.memory),
48+
PRETTY_LOGS: String(env.RUNNER_PRETTY_LOGS),
49+
};
50+
51+
if (this.opts.warmStartUrl) {
52+
envVars.TRIGGER_WARM_START_URL = this.opts.warmStartUrl;
53+
}
54+
55+
if (this.opts.metadataUrl) {
56+
envVars.TRIGGER_METADATA_URL = this.opts.metadataUrl;
57+
}
58+
59+
if (this.opts.heartbeatIntervalSeconds) {
60+
envVars.TRIGGER_HEARTBEAT_INTERVAL_SECONDS = String(this.opts.heartbeatIntervalSeconds);
61+
}
62+
63+
if (this.opts.snapshotPollIntervalSeconds) {
64+
envVars.TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS = String(this.opts.snapshotPollIntervalSeconds);
65+
}
66+
67+
if (this.opts.additionalEnvVars) {
68+
Object.assign(envVars, this.opts.additionalEnvVars);
69+
}
70+
71+
const headers: Record<string, string> = {
72+
"Content-Type": "application/json",
73+
};
74+
75+
if (this.opts.gatewayAuthToken) {
76+
headers["Authorization"] = `Bearer ${this.opts.gatewayAuthToken}`;
77+
}
78+
79+
const url = `${this.opts.gatewayUrl}/api/sandboxes`;
80+
81+
const [fetchError, response] = await tryCatch(
82+
fetch(url, {
83+
method: "POST",
84+
headers,
85+
body: JSON.stringify({
86+
image: opts.image,
87+
env: envVars,
88+
}),
89+
})
90+
);
91+
92+
if (fetchError) {
93+
this.logger.error("Failed to create sandbox", { error: fetchError, url });
94+
return;
95+
}
96+
97+
if (!response.ok) {
98+
const [bodyError, body] = await tryCatch(response.text());
99+
this.logger.error("Gateway returned error", {
100+
status: response.status,
101+
body: bodyError ? undefined : body,
102+
url,
103+
});
104+
return;
105+
}
106+
107+
const [parseError, data] = await tryCatch(response.json());
108+
109+
if (parseError) {
110+
this.logger.error("Failed to parse gateway response", { error: parseError });
111+
return;
112+
}
113+
114+
this.logger.debug("create succeeded", { sandboxId: data.id, runnerId });
115+
}
116+
}

0 commit comments

Comments
 (0)