diff --git a/apps/coordinator/src/checkpointer.ts b/apps/coordinator/src/checkpointer.ts index e7404bb0dd4..b5d4b52a252 100644 --- a/apps/coordinator/src/checkpointer.ts +++ b/apps/coordinator/src/checkpointer.ts @@ -1,5 +1,5 @@ import { ExponentialBackoff } from "@trigger.dev/core/v3/apps"; -import { testDockerCheckpoint } from "@trigger.dev/core/v3/checkpoints"; +import { testDockerCheckpoint } from "@trigger.dev/core/v3/serverOnly"; import { nanoid } from "nanoid"; import fs from "node:fs/promises"; import { ChaosMonkey } from "./chaosMonkey"; diff --git a/apps/supervisor/.env.example b/apps/supervisor/.env.example new file mode 100644 index 00000000000..da91ebb6aa9 --- /dev/null +++ b/apps/supervisor/.env.example @@ -0,0 +1,18 @@ +# This needs to match the token of the worker group you want to connect to +TRIGGER_WORKER_TOKEN= + +# This needs to match the MANAGED_WORKER_SECRET env var on the webapp +MANAGED_WORKER_SECRET=managed-secret + +# Point this at the webapp in prod +TRIGGER_API_URL=http://localhost:3030 + +# Point this at the OTel collector in prod +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:3030/otel +# Use this on macOS +# OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:3030/otel + +# Optional settings +DEBUG=1 +ENFORCE_MACHINE_PRESETS=1 +TRIGGER_DEQUEUE_INTERVAL_MS=1000 \ No newline at end of file diff --git a/apps/supervisor/.nvmrc b/apps/supervisor/.nvmrc new file mode 100644 index 00000000000..dc0bb0f4398 --- /dev/null +++ b/apps/supervisor/.nvmrc @@ -0,0 +1 @@ +v22.12.0 diff --git a/apps/supervisor/README.md b/apps/supervisor/README.md new file mode 100644 index 00000000000..9f2f5b9e234 --- /dev/null +++ b/apps/supervisor/README.md @@ -0,0 +1,49 @@ +# Supervisor + +## Dev setup + +1. Create a worker group + +```sh +api_url=http://localhost:3030 +wg_name=my-worker + +# edit these +admin_pat=tr_pat_... +project_id=clsw6q8wz... + +curl -sS \ + -X POST \ + "$api_url/admin/api/v1/workers" \ + -H "Authorization: Bearer $admin_pat" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$wg_name\", + \"makeDefault\": true, + \"projectId\": \"$project_id\" + }" +``` + +2. Create `.env` and set the worker token + +```sh +cp .env.example .env + +# Then edit your .env and set this to the token.plaintext value +TRIGGER_WORKER_TOKEN=tr_wgt_... +``` + +3. Start the supervisor + +```sh +pnpm dev +``` + +4. Build CLI, then deploy a reference project + +```sh +pnpm exec trigger deploy --self-hosted + +# The additional network flag is required on linux +pnpm exec trigger deploy --self-hosted --network host +``` diff --git a/apps/supervisor/package.json b/apps/supervisor/package.json new file mode 100644 index 00000000000..af256331772 --- /dev/null +++ b/apps/supervisor/package.json @@ -0,0 +1,26 @@ +{ + "name": "supervisor", + "private": true, + "version": "0.0.1", + "main": "dist/index.js", + "type": "module", + "scripts": { + "dev": "tsx --experimental-sqlite --require dotenv/config --watch src/index.ts", + "start": "node --experimental-sqlite dist/index.js", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@kubernetes/client-node": "^1.0.0", + "@trigger.dev/core": "workspace:*", + "dockerode": "^4.0.3", + "nanoid": "^5.0.9", + "socket.io": "4.7.4", + "std-env": "^3.8.0", + "tinyexec": "^0.3.1", + "zod": "3.23.8" + }, + "devDependencies": { + "@types/dockerode": "^3.3.33", + "docker-api-ts": "^0.2.2" + } +} diff --git a/apps/supervisor/src/clients/kubernetes.ts b/apps/supervisor/src/clients/kubernetes.ts new file mode 100644 index 00000000000..ceab5700695 --- /dev/null +++ b/apps/supervisor/src/clients/kubernetes.ts @@ -0,0 +1,39 @@ +import * as k8s from "@kubernetes/client-node"; +import { assertExhaustive } from "@trigger.dev/core/utils"; + +export const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local"; + +export function createK8sApi() { + const kubeConfig = getKubeConfig(); + + const api = { + core: kubeConfig.makeApiClient(k8s.CoreV1Api), + batch: kubeConfig.makeApiClient(k8s.BatchV1Api), + apps: kubeConfig.makeApiClient(k8s.AppsV1Api), + }; + + return api; +} + +export type K8sApi = ReturnType; + +function getKubeConfig() { + console.log("getKubeConfig()", { RUNTIME_ENV }); + + const kubeConfig = new k8s.KubeConfig(); + + switch (RUNTIME_ENV) { + case "local": + kubeConfig.loadFromDefault(); + break; + case "kubernetes": + kubeConfig.loadFromCluster(); + break; + default: + assertExhaustive(RUNTIME_ENV); + } + + return kubeConfig; +} + +export { k8s }; diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts new file mode 100644 index 00000000000..8736ad038b7 --- /dev/null +++ b/apps/supervisor/src/env.ts @@ -0,0 +1,30 @@ +import { randomUUID } from "crypto"; +import { env as stdEnv } from "std-env"; +import { z } from "zod"; +import { getDockerHostDomain } from "./util.js"; + +const Env = z.object({ + // This will come from `status.hostIP` in k8s + WORKER_HOST_IP: z.string().default(getDockerHostDomain()), + TRIGGER_API_URL: z.string().url(), + TRIGGER_WORKER_TOKEN: z.string(), + // This will come from `spec.nodeName` in k8s + TRIGGER_WORKER_INSTANCE_NAME: z.string().default(randomUUID()), + MANAGED_WORKER_SECRET: z.string(), + TRIGGER_WORKLOAD_API_PORT: z.coerce.number().default(8020), + TRIGGER_WORKLOAD_API_PORT_EXTERNAL: z.coerce.number().default(8020), + TRIGGER_WARM_START_URL: z.string().optional(), + TRIGGER_CHECKPOINT_URL: z.string().optional(), + TRIGGER_DEQUEUE_INTERVAL_MS: z.coerce.number().int().default(1000), + + // Used by the workload manager, e.g docker/k8s + DOCKER_NETWORK: z.string().default("host"), + OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url(), + ENFORCE_MACHINE_PRESETS: z.coerce.boolean().default(false), + + // Used by the resource monitor + OVERRIDE_CPU_TOTAL: z.coerce.number().optional(), + OVERRIDE_MEMORY_TOTAL_GB: z.coerce.number().optional(), +}); + +export const env = Env.parse(stdEnv); diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts new file mode 100644 index 00000000000..b7f2400aa67 --- /dev/null +++ b/apps/supervisor/src/index.ts @@ -0,0 +1,254 @@ +import { SupervisorSession } from "@trigger.dev/core/v3/workers"; +import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; +import { env } from "./env.js"; +import { WorkloadServer } from "./workloadServer/index.js"; +import { type WorkloadManager } from "./workloadManager/types.js"; +import Docker from "dockerode"; +import { z } from "zod"; +import { type DequeuedMessage } from "@trigger.dev/core/v3"; +import { + DockerResourceMonitor, + KubernetesResourceMonitor, + type ResourceMonitor, +} from "./resourceMonitor.js"; +import { KubernetesWorkloadManager } from "./workloadManager/kubernetes.js"; +import { DockerWorkloadManager } from "./workloadManager/docker.js"; +import { HttpServer, CheckpointClient } from "@trigger.dev/core/v3/serverOnly"; +import { createK8sApi, RUNTIME_ENV } from "./clients/kubernetes.js"; + +class ManagedSupervisor { + private readonly workerSession: SupervisorSession; + private readonly httpServer: HttpServer; + private readonly workloadServer: WorkloadServer; + private readonly workloadManager: WorkloadManager; + private readonly logger = new SimpleStructuredLogger("managed-worker"); + private readonly resourceMonitor: ResourceMonitor; + private readonly checkpointClient?: CheckpointClient; + + private readonly isKubernetes = RUNTIME_ENV === "kubernetes"; + private readonly warmStartUrl = env.TRIGGER_WARM_START_URL; + + constructor() { + const workerApiUrl = `http://${env.WORKER_HOST_IP}:${env.TRIGGER_WORKLOAD_API_PORT_EXTERNAL}`; + + if (this.warmStartUrl) { + this.logger.log("[ManagedWorker] 🔥 Warm starts enabled", { + warmStartUrl: this.warmStartUrl, + }); + } + + if (this.isKubernetes) { + this.resourceMonitor = new KubernetesResourceMonitor(createK8sApi(), ""); + this.workloadManager = new KubernetesWorkloadManager({ + workerApiUrl, + warmStartUrl: this.warmStartUrl, + }); + } else { + this.resourceMonitor = new DockerResourceMonitor(new Docker()); + this.workloadManager = new DockerWorkloadManager({ + workerApiUrl, + warmStartUrl: this.warmStartUrl, + }); + } + + this.workerSession = new SupervisorSession({ + workerToken: env.TRIGGER_WORKER_TOKEN, + apiUrl: env.TRIGGER_API_URL, + instanceName: env.TRIGGER_WORKER_INSTANCE_NAME, + managedWorkerSecret: env.MANAGED_WORKER_SECRET, + dequeueIntervalMs: env.TRIGGER_DEQUEUE_INTERVAL_MS, + preDequeue: async () => { + if (this.isKubernetes) { + // TODO: Test k8s resource monitor and remove this + return {}; + } + + const resources = await this.resourceMonitor.getNodeResources(); + return { + maxResources: { + cpu: resources.cpuAvailable, + memory: resources.memoryAvailable, + }, + skipDequeue: resources.cpuAvailable < 0.25 || resources.memoryAvailable < 0.25, + }; + }, + preSkip: async () => { + // When the node is full, it should still try to warm start runs + // await this.tryWarmStartAllThisNode(); + }, + }); + + if (env.TRIGGER_CHECKPOINT_URL) { + this.checkpointClient = new CheckpointClient({ + apiUrl: new URL(env.TRIGGER_CHECKPOINT_URL), + workerClient: this.workerSession.httpClient, + }); + } + + // setInterval(async () => { + // const resources = await this.resourceMonitor.getNodeResources(true); + // this.logger.debug("[ManagedWorker] Current resources", { resources }); + // }, 1000); + + this.workerSession.on("runNotification", async ({ time, run }) => { + this.logger.log("[ManagedWorker] runNotification", { time, run }); + + this.workloadServer.notifyRun({ run }); + }); + + this.workerSession.on("runQueueMessage", async ({ time, message }) => { + this.logger.log( + `[ManagedWorker] Received message with timestamp ${time.toLocaleString()}`, + message + ); + + if (message.completedWaitpoints.length > 0) { + this.logger.debug("[ManagedWorker] Run has completed waitpoints", { + runId: message.run.id, + completedWaitpoints: message.completedWaitpoints.length, + }); + // TODO: Do something with them or if we don't need the data here, maybe we shouldn't even send it + } + + if (!message.image) { + this.logger.error("[ManagedWorker] Run has no image", { runId: message.run.id }); + return; + } + + if (message.checkpoint) { + this.logger.log("[ManagedWorker] Restoring run", { runId: message.run.id }); + + try { + const didRestore = await this.checkpointClient?.restoreRun({ + runFriendlyId: message.run.friendlyId, + snapshotFriendlyId: message.snapshot.friendlyId, + checkpoint: message.checkpoint, + }); + + if (didRestore) { + this.logger.log("[ManagedWorker] Restore successful", { runId: message.run.id }); + } else { + this.logger.error("[ManagedWorker] Restore failed", { runId: message.run.id }); + } + } catch (error) { + this.logger.error("[ManagedWorker] Failed to restore run", { error }); + } + + return; + } + + this.logger.log("[ManagedWorker] Scheduling run", { runId: message.run.id }); + + const didWarmStart = await this.tryWarmStart(message); + + if (didWarmStart) { + this.logger.log("[ManagedWorker] Warm start successful", { runId: message.run.id }); + return; + } + + try { + await this.workloadManager.create({ + envId: message.environment.id, + envType: message.environment.type, + image: message.image, + machine: message.run.machine, + orgId: message.organization.id, + projectId: message.project.id, + runId: message.run.id, + runFriendlyId: message.run.friendlyId, + version: message.version, + nextAttemptNumber: message.run.attemptNumber, + snapshotId: message.snapshot.id, + snapshotFriendlyId: message.snapshot.friendlyId, + }); + + this.resourceMonitor.blockResources({ + cpu: message.run.machine.cpu, + memory: message.run.machine.memory, + }); + } catch (error) { + this.logger.error("[ManagedWorker] Failed to create workload", { error }); + } + }); + + // Used for health checks and metrics + this.httpServer = new HttpServer({ port: 8080, host: "0.0.0.0" }).route("/health", "GET", { + handler: async ({ reply }) => { + reply.text("OK"); + }, + }); + + // Responds to workload requests only + this.workloadServer = new WorkloadServer({ + port: env.TRIGGER_WORKLOAD_API_PORT, + workerClient: this.workerSession.httpClient, + checkpointClient: this.checkpointClient, + }); + + this.workloadServer.on("runConnected", this.onRunConnected.bind(this)); + this.workloadServer.on("runDisconnected", this.onRunDisconnected.bind(this)); + } + + async onRunConnected({ run }: { run: { friendlyId: string } }) { + this.logger.debug("[ManagedWorker] Run connected", { run }); + this.workerSession.subscribeToRunNotifications([run.friendlyId]); + } + + async onRunDisconnected({ run }: { run: { friendlyId: string } }) { + this.logger.debug("[ManagedWorker] Run disconnected", { run }); + this.workerSession.unsubscribeFromRunNotifications([run.friendlyId]); + } + + private async tryWarmStart(dequeuedMessage: DequeuedMessage): Promise { + if (!this.warmStartUrl) { + return false; + } + + const warmStartUrlWithPath = new URL("/warm-start", this.warmStartUrl); + + const res = await fetch(warmStartUrlWithPath.href, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ dequeuedMessage }), + }); + + if (!res.ok) { + this.logger.error("[ManagedWorker] Warm start failed", { + runId: dequeuedMessage.run.id, + }); + return false; + } + + const data = await res.json(); + const parsedData = z.object({ didWarmStart: z.boolean() }).safeParse(data); + + if (!parsedData.success) { + this.logger.error("[ManagedWorker] Warm start response invalid", { + runId: dequeuedMessage.run.id, + data, + }); + return false; + } + + return parsedData.data.didWarmStart; + } + + async start() { + this.logger.log("[ManagedWorker] Starting up"); + + await this.workloadServer.start(); + await this.workerSession.start(); + + await this.httpServer.start(); + } + + async stop() { + this.logger.log("[ManagedWorker] Shutting down"); + await this.httpServer.stop(); + } +} + +const worker = new ManagedSupervisor(); +worker.start(); diff --git a/apps/supervisor/src/resourceMonitor.ts b/apps/supervisor/src/resourceMonitor.ts new file mode 100644 index 00000000000..2aa844b9c72 --- /dev/null +++ b/apps/supervisor/src/resourceMonitor.ts @@ -0,0 +1,246 @@ +import type Docker from "dockerode"; +import type * as TDocker from "docker-api-ts"; +import type { MachineResources } from "@trigger.dev/core/v3"; +import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; +import { env } from "./env.js"; +import type { K8sApi } from "./clients/kubernetes.js"; + +const logger = new SimpleStructuredLogger("resource-monitor"); + +interface NodeResources { + cpuTotal: number; // in cores + cpuAvailable: number; + memoryTotal: number; // in bytes + memoryAvailable: number; +} + +interface ResourceRequest { + cpu: number; // in cores + memory: number; // in bytes +} + +export abstract class ResourceMonitor { + protected cacheTimeoutMs = 5_000; + protected lastUpdateMs = 0; + + protected cachedResources: NodeResources = { + cpuTotal: 0, + cpuAvailable: 0, + memoryTotal: 0, + memoryAvailable: 0, + }; + + protected resourceParser: ResourceParser; + + constructor(Parser: new () => ResourceParser) { + this.resourceParser = new Parser(); + } + + abstract getNodeResources(fromCache?: boolean): Promise; + + blockResources(resources: MachineResources): void { + const { cpu, memory } = this.toResourceRequest(resources); + + logger.debug("[ResourceMonitor] Blocking resources", { + raw: resources, + converted: { cpu, memory }, + }); + + this.cachedResources.cpuAvailable -= cpu; + this.cachedResources.memoryAvailable -= memory; + } + + async wouldFit(request: ResourceRequest): Promise { + const resources = await this.getNodeResources(); + return resources.cpuAvailable >= request.cpu && resources.memoryAvailable >= request.memory; + } + + private toResourceRequest(resources: MachineResources): ResourceRequest { + return { + cpu: resources.cpu ?? 0, + memory: this.gbToBytes(resources.memory ?? 0), + }; + } + + private gbToBytes(gb: number): number { + return gb * 1024 * 1024 * 1024; + } + + protected isCacheValid(): boolean { + return this.cachedResources !== null && Date.now() - this.lastUpdateMs < this.cacheTimeoutMs; + } + + protected applyOverrides(resources: NodeResources): NodeResources { + if (!env.OVERRIDE_CPU_TOTAL && !env.OVERRIDE_MEMORY_TOTAL_GB) { + return resources; + } + + logger.debug("[ResourceMonitor] 🛡️ Applying resource overrides", { + cpuTotal: env.OVERRIDE_CPU_TOTAL, + memoryTotalGb: env.OVERRIDE_MEMORY_TOTAL_GB, + }); + + const cpuTotal = env.OVERRIDE_CPU_TOTAL ?? resources.cpuTotal; + const memoryTotal = env.OVERRIDE_MEMORY_TOTAL_GB + ? this.gbToBytes(env.OVERRIDE_MEMORY_TOTAL_GB) + : resources.memoryTotal; + + const cpuDiff = cpuTotal - resources.cpuTotal; + const memoryDiff = memoryTotal - resources.memoryTotal; + + const cpuAvailable = Math.max(0, resources.cpuAvailable + cpuDiff); + const memoryAvailable = Math.max(0, resources.memoryAvailable + memoryDiff); + + return { + cpuTotal, + cpuAvailable, + memoryTotal, + memoryAvailable, + }; + } +} + +export class DockerResourceMonitor extends ResourceMonitor { + private docker: Docker; + + constructor(docker: Docker) { + super(DockerResourceParser); + this.docker = docker; + } + + async getNodeResources(fromCache?: boolean): Promise { + if (this.isCacheValid() || fromCache) { + // logger.debug("[ResourceMonitor] Using cached resources"); + return this.cachedResources; + } + + const info: TDocker.SystemInfo = await this.docker.info(); + const stats = await this.docker.listContainers({ all: true }); + + // Get system-wide resources + const cpuTotal = info.NCPU ?? 0; + const memoryTotal = info.MemTotal ?? 0; + + // Calculate used resources from running containers + let cpuUsed = 0; + let memoryUsed = 0; + + for (const container of stats) { + if (container.State === "running") { + const c = this.docker.getContainer(container.Id); + const { HostConfig } = await c.inspect(); + + const cpu = this.resourceParser.cpu(HostConfig.NanoCpus ?? 0); + const memory = this.resourceParser.memory(HostConfig.Memory ?? 0); + + cpuUsed += cpu; + memoryUsed += memory; + } + } + + this.cachedResources = this.applyOverrides({ + cpuTotal, + cpuAvailable: cpuTotal - cpuUsed, + memoryTotal, + memoryAvailable: memoryTotal - memoryUsed, + }); + + this.lastUpdateMs = Date.now(); + + return this.cachedResources; + } +} + +export class KubernetesResourceMonitor extends ResourceMonitor { + private k8s: K8sApi; + private nodeName: string; + + constructor(k8s: K8sApi, nodeName: string) { + super(KubernetesResourceParser); + this.k8s = k8s; + this.nodeName = nodeName; + } + + async getNodeResources(fromCache?: boolean): Promise { + if (this.isCacheValid() || fromCache) { + logger.debug("[ResourceMonitor] Using cached resources"); + return this.cachedResources; + } + + const node = await this.k8s.core.readNode({ name: this.nodeName }); + const pods = await this.k8s.core.listPodForAllNamespaces({ + // TODO: ensure this includes all pods that consume resources + fieldSelector: `spec.nodeName=${this.nodeName},status.phase=Running`, + }); + + const allocatable = node.status?.allocatable; + const cpuTotal = this.resourceParser.cpu(allocatable?.cpu ?? "0"); + const memoryTotal = this.resourceParser.memory(allocatable?.memory ?? "0"); + + // Sum up resources requested by all pods on this node + let cpuRequested = 0; + let memoryRequested = 0; + + for (const pod of pods.items) { + if (pod.status?.phase === "Running") { + if (!pod.spec) { + continue; + } + + for (const container of pod.spec.containers) { + const resources = container.resources?.requests ?? {}; + cpuRequested += this.resourceParser.cpu(resources.cpu ?? "0"); + memoryRequested += this.resourceParser.memory(resources.memory ?? "0"); + } + } + } + + this.cachedResources = this.applyOverrides({ + cpuTotal, + cpuAvailable: cpuTotal - cpuRequested, + memoryTotal, + memoryAvailable: memoryTotal - memoryRequested, + }); + + this.lastUpdateMs = Date.now(); + + return this.cachedResources; + } +} + +abstract class ResourceParser { + abstract cpu(cpu: number | string): number; + abstract memory(memory: number | string): number; +} + +class DockerResourceParser extends ResourceParser { + cpu(cpu: number): number { + return cpu / 1e9; + } + + memory(memory: number): number { + return memory; + } +} + +class KubernetesResourceParser extends ResourceParser { + cpu(cpu: string): number { + if (cpu.endsWith("m")) { + return parseInt(cpu.slice(0, -1)) / 1000; + } + return parseInt(cpu); + } + + memory(memory: string): number { + if (memory.endsWith("Ki")) { + return parseInt(memory.slice(0, -2)) * 1024; + } + if (memory.endsWith("Mi")) { + return parseInt(memory.slice(0, -2)) * 1024 * 1024; + } + if (memory.endsWith("Gi")) { + return parseInt(memory.slice(0, -2)) * 1024 * 1024 * 1024; + } + return parseInt(memory); + } +} diff --git a/apps/supervisor/src/util.ts b/apps/supervisor/src/util.ts new file mode 100644 index 00000000000..0d465d46998 --- /dev/null +++ b/apps/supervisor/src/util.ts @@ -0,0 +1,30 @@ +import { customAlphabet } from "nanoid"; + +export function getDockerHostDomain() { + const isMacOs = process.platform === "darwin"; + const isWindows = process.platform === "win32"; + + return isMacOs || isWindows ? "host.docker.internal" : "localhost"; +} + +export class IdGenerator { + private alphabet: string; + private length: number; + private prefix: string; + + constructor({ alphabet, length, prefix }: { alphabet: string; length: number; prefix: string }) { + this.alphabet = alphabet; + this.length = length; + this.prefix = prefix; + } + + generate(): string { + return `${this.prefix}${customAlphabet(this.alphabet, this.length)()}`; + } +} + +export const RunnerId = new IdGenerator({ + alphabet: "123456789abcdefghijkmnopqrstuvwxyz", + length: 20, + prefix: "runner_", +}); diff --git a/apps/supervisor/src/workloadManager/docker.ts b/apps/supervisor/src/workloadManager/docker.ts new file mode 100644 index 00000000000..48c0b9b7f3a --- /dev/null +++ b/apps/supervisor/src/workloadManager/docker.ts @@ -0,0 +1,54 @@ +import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; +import { + type WorkloadManager, + type WorkloadManagerCreateOptions, + type WorkloadManagerOptions, +} from "./types.js"; +import { x } from "tinyexec"; +import { env } from "../env.js"; +import { RunnerId } from "../util.js"; + +export class DockerWorkloadManager implements WorkloadManager { + private readonly logger = new SimpleStructuredLogger("docker-workload-provider"); + + constructor(private opts: WorkloadManagerOptions) {} + + async create(opts: WorkloadManagerCreateOptions) { + this.logger.log("[DockerWorkloadProvider] Creating container", { opts }); + + const runnerId = RunnerId.generate(); + const runArgs = [ + "run", + "--detach", + `--network=${env.DOCKER_NETWORK}`, + `--env=TRIGGER_ENV_ID=${opts.envId}`, + `--env=TRIGGER_RUN_ID=${opts.runFriendlyId}`, + `--env=TRIGGER_SNAPSHOT_ID=${opts.snapshotFriendlyId}`, + `--env=TRIGGER_WORKER_API_URL=${this.opts.workerApiUrl}`, + `--env=TRIGGER_WORKER_INSTANCE_NAME=${env.TRIGGER_WORKER_INSTANCE_NAME}`, + `--env=OTEL_EXPORTER_OTLP_ENDPOINT=${env.OTEL_EXPORTER_OTLP_ENDPOINT}`, + `--env=TRIGGER_RUNNER_ID=${runnerId}`, + `--hostname=${runnerId}`, + `--name=${runnerId}`, + ]; + + if (this.opts.warmStartUrl) { + runArgs.push(`--env=TRIGGER_WARM_START_URL=${this.opts.warmStartUrl}`); + } + + if (env.ENFORCE_MACHINE_PRESETS) { + runArgs.push(`--cpus=${opts.machine.cpu}`, `--memory=${opts.machine.memory}G`); + runArgs.push(`--env=TRIGGER_MACHINE_CPU=${opts.machine.cpu}`); + runArgs.push(`--env=TRIGGER_MACHINE_MEMORY=${opts.machine.memory}`); + } + + runArgs.push(`${opts.image}`); + + try { + const { stdout, stderr } = await x("docker", runArgs); + this.logger.debug("[DockerWorkloadProvider] Create succeeded", { stdout, stderr }); + } catch (error) { + this.logger.error("[DockerWorkloadProvider] Create failed:", { opts, error }); + } + } +} diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts new file mode 100644 index 00000000000..ed8dd6ae8c1 --- /dev/null +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -0,0 +1,230 @@ +import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; +import { + type WorkloadManager, + type WorkloadManagerCreateOptions, + type WorkloadManagerOptions, +} from "./types.js"; +import { RunnerId } from "../util.js"; +import type { EnvironmentType, MachinePreset } from "@trigger.dev/core/v3"; +import { env } from "../env.js"; +import { type K8sApi, createK8sApi, type k8s } from "../clients/kubernetes.js"; + +const POD_EPHEMERAL_STORAGE_SIZE_LIMIT = process.env.POD_EPHEMERAL_STORAGE_SIZE_LIMIT || "10Gi"; +const POD_EPHEMERAL_STORAGE_SIZE_REQUEST = process.env.POD_EPHEMERAL_STORAGE_SIZE_REQUEST || "2Gi"; + +type ResourceQuantities = { + [K in "cpu" | "memory" | "ephemeral-storage"]?: string; +}; + +export class KubernetesWorkloadManager implements WorkloadManager { + private readonly logger = new SimpleStructuredLogger("kubernetes-workload-provider"); + private k8s: K8sApi; + private namespace = "default"; + + constructor(private opts: WorkloadManagerOptions) { + this.k8s = createK8sApi(); + } + + async create(opts: WorkloadManagerCreateOptions) { + this.logger.log("[KubernetesWorkloadManager] Creating container", { opts }); + + const runnerId = RunnerId.generate().replace(/_/g, "-"); + + try { + await this.k8s.core.createNamespacedPod({ + namespace: this.namespace, + body: { + metadata: { + name: runnerId, + namespace: this.namespace, + labels: { + ...this.#getSharedLabels(opts), + app: "task-run", + "app.kubernetes.io/part-of": "trigger-worker", + "app.kubernetes.io/component": "create", + run: opts.runId, + }, + }, + spec: { + ...this.#defaultPodSpec, + terminationGracePeriodSeconds: 60 * 60, + containers: [ + { + name: runnerId, + image: opts.image, + ports: [ + { + containerPort: 8000, + }, + ], + resources: this.#getResourcesForMachine(opts.machine), + env: [ + { + name: "TRIGGER_RUN_ID", + value: opts.runFriendlyId, + }, + { + name: "TRIGGER_ENV_ID", + value: opts.envId, + }, + { + name: "TRIGGER_SNAPSHOT_ID", + value: opts.snapshotFriendlyId, + }, + { + name: "TRIGGER_WORKER_API_URL", + value: this.opts.workerApiUrl, + }, + { + name: "TRIGGER_WORKER_INSTANCE_NAME", + value: env.TRIGGER_WORKER_INSTANCE_NAME, + }, + { + name: "OTEL_EXPORTER_OTLP_ENDPOINT", + value: env.OTEL_EXPORTER_OTLP_ENDPOINT, + }, + { + name: "TRIGGER_RUNNER_ID", + value: runnerId, + }, + { + name: "TRIGGER_MACHINE_CPU", + value: `${opts.machine.cpu}`, + }, + { + name: "TRIGGER_MACHINE_MEMORY", + value: `${opts.machine.memory}`, + }, + { + name: "LIMITS_CPU", + valueFrom: { + resourceFieldRef: { + resource: "limits.cpu", + }, + }, + }, + { + name: "LIMITS_MEMORY", + valueFrom: { + resourceFieldRef: { + resource: "limits.memory", + }, + }, + }, + ...(this.opts.warmStartUrl + ? [{ name: "TRIGGER_WARM_START_URL", value: this.opts.warmStartUrl }] + : []), + ], + }, + ], + }, + }, + }); + } catch (err: unknown) { + this.#handleK8sError(err); + } + } + + #throwUnlessRecord(candidate: unknown): asserts candidate is Record { + if (typeof candidate !== "object" || candidate === null) { + throw candidate; + } + } + + #handleK8sError(err: unknown) { + this.#throwUnlessRecord(err); + + if ("body" in err && err.body) { + this.logger.error("[KubernetesWorkloadManager] Create failed", { rawError: err.body }); + this.#throwUnlessRecord(err.body); + + if (typeof err.body.message === "string") { + throw new Error(err.body?.message); + } else { + throw err.body; + } + } else { + this.logger.error("[KubernetesWorkloadManager] Create failed", { rawError: err }); + throw err; + } + } + + #envTypeToLabelValue(type: EnvironmentType) { + switch (type) { + case "PRODUCTION": + return "prod"; + case "STAGING": + return "stg"; + case "DEVELOPMENT": + return "dev"; + case "PREVIEW": + return "preview"; + } + } + + get #defaultPodSpec(): Omit { + return { + restartPolicy: "Never", + automountServiceAccountToken: false, + imagePullSecrets: [ + { + name: "registry-trigger", + }, + { + name: "registry-trigger-failover", + }, + ], + nodeSelector: { + nodetype: "worker-re2", + }, + }; + } + + get #defaultResourceRequests(): ResourceQuantities { + return { + "ephemeral-storage": POD_EPHEMERAL_STORAGE_SIZE_REQUEST, + }; + } + + get #defaultResourceLimits(): ResourceQuantities { + return { + "ephemeral-storage": POD_EPHEMERAL_STORAGE_SIZE_LIMIT, + }; + } + + #getSharedLabels(opts: WorkloadManagerCreateOptions): Record { + return { + env: opts.envId, + envtype: this.#envTypeToLabelValue(opts.envType), + org: opts.orgId, + project: opts.projectId, + }; + } + + #getResourceRequestsForMachine(preset: MachinePreset): ResourceQuantities { + return { + cpu: `${preset.cpu * 0.75}`, + memory: `${preset.memory}G`, + }; + } + + #getResourceLimitsForMachine(preset: MachinePreset): ResourceQuantities { + return { + cpu: `${preset.cpu}`, + memory: `${preset.memory}G`, + }; + } + + #getResourcesForMachine(preset: MachinePreset): k8s.V1ResourceRequirements { + return { + requests: { + ...this.#defaultResourceRequests, + ...this.#getResourceRequestsForMachine(preset), + }, + limits: { + ...this.#defaultResourceLimits, + ...this.#getResourceLimitsForMachine(preset), + }, + }; + } +} diff --git a/apps/supervisor/src/workloadManager/types.ts b/apps/supervisor/src/workloadManager/types.ts new file mode 100644 index 00000000000..57874e53348 --- /dev/null +++ b/apps/supervisor/src/workloadManager/types.ts @@ -0,0 +1,26 @@ +import { type EnvironmentType, type MachinePreset } from "@trigger.dev/core/v3"; + +export interface WorkloadManagerOptions { + workerApiUrl: string; + warmStartUrl?: string; +} + +export interface WorkloadManager { + create: (opts: WorkloadManagerCreateOptions) => Promise; +} + +export interface WorkloadManagerCreateOptions { + image: string; + machine: MachinePreset; + version: string; + nextAttemptNumber?: number; + // identifiers + envId: string; + envType: EnvironmentType; + orgId: string; + projectId: string; + runId: string; + runFriendlyId: string; + snapshotId: string; + snapshotFriendlyId: string; +} diff --git a/apps/supervisor/src/workloadServer/index.ts b/apps/supervisor/src/workloadServer/index.ts new file mode 100644 index 00000000000..2981b073904 --- /dev/null +++ b/apps/supervisor/src/workloadServer/index.ts @@ -0,0 +1,549 @@ +import { type Namespace, Server, type Socket } from "socket.io"; +import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; +import EventEmitter from "node:events"; +import { z } from "zod"; +import { + type SupervisorHttpClient, + WORKLOAD_HEADERS, + type WorkloadClientSocketData, + type WorkloadClientToServerEvents, + type WorkloadContinueRunExecutionResponseBody, + WorkloadDebugLogRequestBody, + type WorkloadDequeueFromVersionResponseBody, + WorkloadHeartbeatRequestBody, + type WorkloadHeartbeatResponseBody, + WorkloadRunAttemptCompleteRequestBody, + type WorkloadRunAttemptCompleteResponseBody, + WorkloadRunAttemptStartRequestBody, + type WorkloadRunAttemptStartResponseBody, + type WorkloadRunLatestSnapshotResponseBody, + type WorkloadServerToClientEvents, + type WorkloadSuspendRunResponseBody, +} from "@trigger.dev/core/v3/workers"; +import { HttpServer, type CheckpointClient } from "@trigger.dev/core/v3/serverOnly"; +import { type IncomingMessage } from "node:http"; + +// Use the official export when upgrading to socket.io@4.8.0 +interface DefaultEventsMap { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [event: string]: (...args: any[]) => void; +} + +const WorkloadActionParams = z.object({ + runFriendlyId: z.string(), + snapshotFriendlyId: z.string(), +}); + +type WorkloadServerEvents = { + runConnected: [ + { + run: { + friendlyId: string; + }; + }, + ]; + runDisconnected: [ + { + run: { + friendlyId: string; + }; + }, + ]; +}; + +type WorkloadServerOptions = { + port: number; + host?: string; + workerClient: SupervisorHttpClient; + checkpointClient?: CheckpointClient; +}; + +export class WorkloadServer extends EventEmitter { + private checkpointClient?: CheckpointClient; + + private readonly httpServer: HttpServer; + private readonly websocketServer: Namespace< + WorkloadClientToServerEvents, + WorkloadServerToClientEvents, + DefaultEventsMap, + WorkloadClientSocketData + >; + + private readonly runSockets = new Map< + string, + Socket< + WorkloadClientToServerEvents, + WorkloadServerToClientEvents, + DefaultEventsMap, + WorkloadClientSocketData + > + >(); + + private readonly workerClient: SupervisorHttpClient; + + constructor(opts: WorkloadServerOptions) { + super(); + + const host = opts.host ?? "0.0.0.0"; + const port = opts.port; + + this.workerClient = opts.workerClient; + this.checkpointClient = opts.checkpointClient; + + this.httpServer = this.createHttpServer({ host, port }); + this.websocketServer = this.createWebsocketServer(); + } + + private runnerIdFromRequest(req: IncomingMessage): string | undefined { + const value = req.headers[WORKLOAD_HEADERS.RUNNER_ID]; + + if (Array.isArray(value)) { + return value[0]; + } + + return value; + } + + private createHttpServer({ host, port }: { host: string; port: number }) { + return new HttpServer({ port, host }) + .route( + "/api/v1/workload-actions/runs/:runFriendlyId/snapshots/:snapshotFriendlyId/attempts/start", + "POST", + { + paramsSchema: WorkloadActionParams, + bodySchema: WorkloadRunAttemptStartRequestBody, + handler: async ({ req, reply, params, body }) => { + const startResponse = await this.workerClient.startRunAttempt( + params.runFriendlyId, + params.snapshotFriendlyId, + body, + this.runnerIdFromRequest(req) + ); + + if (!startResponse.success) { + console.error("Failed to start run", { + params, + error: startResponse.error, + }); + reply.empty(500); + return; + } + + reply.json(startResponse.data satisfies WorkloadRunAttemptStartResponseBody); + return; + }, + } + ) + .route( + "/api/v1/workload-actions/runs/:runFriendlyId/snapshots/:snapshotFriendlyId/attempts/complete", + "POST", + { + paramsSchema: WorkloadActionParams, + bodySchema: WorkloadRunAttemptCompleteRequestBody, + handler: async ({ req, reply, params, body }) => { + console.log("headers", req.headers); + const completeResponse = await this.workerClient.completeRunAttempt( + params.runFriendlyId, + params.snapshotFriendlyId, + body, + this.runnerIdFromRequest(req) + ); + + if (!completeResponse.success) { + console.error("Failed to complete run", { + params, + error: completeResponse.error, + }); + reply.empty(500); + return; + } + + reply.json(completeResponse.data satisfies WorkloadRunAttemptCompleteResponseBody); + return; + }, + } + ) + .route( + "/api/v1/workload-actions/runs/:runFriendlyId/snapshots/:snapshotFriendlyId/heartbeat", + "POST", + { + paramsSchema: WorkloadActionParams, + bodySchema: WorkloadHeartbeatRequestBody, + handler: async ({ req, reply, params, body }) => { + const heartbeatResponse = await this.workerClient.heartbeatRun( + params.runFriendlyId, + params.snapshotFriendlyId, + body, + this.runnerIdFromRequest(req) + ); + + if (!heartbeatResponse.success) { + console.error("Failed to heartbeat run", { + params, + error: heartbeatResponse.error, + }); + reply.empty(500); + return; + } + + reply.json({ + ok: true, + } satisfies WorkloadHeartbeatResponseBody); + }, + } + ) + .route( + "/api/v1/workload-actions/runs/:runFriendlyId/snapshots/:snapshotFriendlyId/suspend", + "GET", + { + paramsSchema: WorkloadActionParams, + handler: async ({ reply, params, req }) => { + console.debug("Suspend request", { params, headers: req.headers }); + + const runnerId = this.runnerIdFromRequest(req); + + if (!runnerId) { + console.error("Invalid headers for suspend request", { + ...params, + headers: req.headers, + }); + reply.json( + { + ok: false, + error: "Invalid headers", + } satisfies WorkloadSuspendRunResponseBody, + false, + 400 + ); + return; + } + + if (!this.checkpointClient) { + console.error("Checkpoint client unavailable - suspending impossible", { params }); + reply.json( + { + ok: false, + error: "Suspends are not enabled", + } satisfies WorkloadSuspendRunResponseBody, + false, + 400 + ); + return; + } + + reply.json( + { + ok: true, + } satisfies WorkloadSuspendRunResponseBody, + false, + 202 + ); + + const suspendResult = await this.checkpointClient.suspendRun({ + runFriendlyId: params.runFriendlyId, + snapshotFriendlyId: params.snapshotFriendlyId, + containerId: runnerId, + runnerId, + }); + + if (!suspendResult) { + console.error("Failed to suspend run", { params }); + return; + } + + console.log("Suspended run", { params }); + }, + } + ) + .route( + "/api/v1/workload-actions/runs/:runFriendlyId/snapshots/:snapshotFriendlyId/continue", + "GET", + { + paramsSchema: WorkloadActionParams, + handler: async ({ req, reply, params }) => { + console.debug("Run continuation request", { params }); + + const continuationResult = await this.workerClient.continueRunExecution( + params.runFriendlyId, + params.snapshotFriendlyId, + this.runnerIdFromRequest(req) + ); + + if (!continuationResult.success) { + console.error("Failed to continue run execution", { params }); + reply.json( + { + ok: false, + error: "Failed to continue run execution", + }, + false, + 400 + ); + return; + } + + reply.json(continuationResult.data as WorkloadContinueRunExecutionResponseBody); + }, + } + ) + .route("/api/v1/workload-actions/runs/:runFriendlyId/snapshots/latest", "GET", { + paramsSchema: WorkloadActionParams.pick({ runFriendlyId: true }), + handler: async ({ req, reply, params }) => { + const latestSnapshotResponse = await this.workerClient.getLatestSnapshot( + params.runFriendlyId, + this.runnerIdFromRequest(req) + ); + + if (!latestSnapshotResponse.success) { + console.error("Failed to get latest snapshot", { + runId: params.runFriendlyId, + error: latestSnapshotResponse.error, + }); + reply.empty(500); + return; + } + + reply.json({ + execution: latestSnapshotResponse.data.execution, + } satisfies WorkloadRunLatestSnapshotResponseBody); + }, + }) + .route("/api/v1/workload-actions/runs/:runFriendlyId/logs/debug", "POST", { + paramsSchema: WorkloadActionParams.pick({ runFriendlyId: true }), + bodySchema: WorkloadDebugLogRequestBody, + handler: async ({ req, reply, params, body }) => { + reply.empty(204); + + await this.workerClient.sendDebugLog( + params.runFriendlyId, + body, + this.runnerIdFromRequest(req) + ); + }, + }) + .route("/api/v1/workload-actions/deployments/:deploymentId/dequeue", "GET", { + paramsSchema: z.object({ + deploymentId: z.string(), + }), + + handler: async ({ req, reply, params }) => { + const dequeueResponse = await this.workerClient.dequeueFromVersion( + params.deploymentId, + 1, + this.runnerIdFromRequest(req) + ); + + if (!dequeueResponse.success) { + console.error("Failed to get latest snapshot", { + deploymentId: params.deploymentId, + error: dequeueResponse.error, + }); + reply.empty(500); + return; + } + + reply.json(dequeueResponse.data satisfies WorkloadDequeueFromVersionResponseBody); + }, + }); + } + + private createWebsocketServer() { + const io = new Server(this.httpServer.server); + + const websocketServer: Namespace< + WorkloadClientToServerEvents, + WorkloadServerToClientEvents, + DefaultEventsMap, + WorkloadClientSocketData + > = io.of("/workload"); + + websocketServer.on("disconnect", (socket) => { + console.log("[WorkloadSocket] disconnect", socket.id); + }); + websocketServer.use(async (socket, next) => { + function setSocketDataFromHeader( + dataKey: keyof typeof socket.data, + headerName: string, + required: boolean = true + ) { + const value = socket.handshake.headers[headerName]; + + if (value) { + if (Array.isArray(value)) { + if (value[0]) { + socket.data[dataKey] = value[0]; + return; + } + } else { + socket.data[dataKey] = value; + return; + } + } + + if (required) { + console.error("[WorkloadSocket] missing required header", { headerName }); + throw new Error("missing header"); + } + } + + try { + setSocketDataFromHeader("deploymentId", WORKLOAD_HEADERS.DEPLOYMENT_ID); + setSocketDataFromHeader("runnerId", WORKLOAD_HEADERS.RUNNER_ID); + } catch (error) { + console.error("[WorkloadSocket] setSocketDataFromHeader error", { error }); + socket.disconnect(true); + return; + } + + console.debug("[WorkloadSocket] auth success", socket.data); + + next(); + }); + websocketServer.on("connection", (socket) => { + const logger = new SimpleStructuredLogger("workload-namespace", undefined, { + namespace: "workload", + socketId: socket.id, + socketData: socket.data, + }); + + const getSocketMetadata = () => { + return { + deploymentId: socket.data.deploymentId, + runId: socket.data.runFriendlyId, + snapshotId: socket.data.snapshotId, + runnerId: socket.data.runnerId, + }; + }; + + const runConnected = (friendlyId: string) => { + logger.debug("runConnected", { ...getSocketMetadata() }); + + // If there's already a run ID set, we should "disconnect" it from this socket + if (socket.data.runFriendlyId) { + logger.debug("runConnected: disconnecting existing run", { + ...getSocketMetadata(), + newRunId: friendlyId, + oldRunId: socket.data.runFriendlyId, + }); + runDisconnected(socket.data.runFriendlyId); + } + + this.runSockets.set(friendlyId, socket); + this.emit("runConnected", { run: { friendlyId } }); + socket.data.runFriendlyId = friendlyId; + }; + + const runDisconnected = (friendlyId: string) => { + logger.debug("runDisconnected", { ...getSocketMetadata() }); + + this.runSockets.delete(friendlyId); + this.emit("runDisconnected", { run: { friendlyId } }); + socket.data.runFriendlyId = undefined; + }; + + logger.log("wsServer socket connected", { ...getSocketMetadata() }); + + // FIXME: where does this get set? + if (socket.data.runFriendlyId) { + runConnected(socket.data.runFriendlyId); + } + + socket.on("disconnecting", (reason, description) => { + logger.log("Socket disconnecting", { ...getSocketMetadata(), reason, description }); + + if (socket.data.runFriendlyId) { + runDisconnected(socket.data.runFriendlyId); + } + }); + + socket.on("disconnect", (reason, description) => { + logger.log("Socket disconnected", { ...getSocketMetadata(), reason, description }); + }); + + socket.on("error", (error) => { + logger.error("Socket error", { + ...getSocketMetadata(), + error: { + name: error.name, + message: error.message, + stack: error.stack, + }, + }); + }); + + socket.on("run:start", async (message) => { + const log = logger.child({ + eventName: "run:start", + ...getSocketMetadata(), + ...message, + }); + + log.log("Handling run:start"); + + try { + runConnected(message.run.friendlyId); + } catch (error) { + log.error("run:start error", { error }); + } + }); + + socket.on("run:stop", async (message) => { + const log = logger.child({ + eventName: "run:stop", + ...getSocketMetadata(), + ...message, + }); + + log.log("Handling run:stop"); + + try { + runDisconnected(message.run.friendlyId); + } catch (error) { + log.error("run:stop error", { error }); + } + }); + }); + + return websocketServer; + } + + notifyRun({ run }: { run: { friendlyId: string } }) { + try { + const runSocket = this.runSockets.get(run.friendlyId); + + if (!runSocket) { + console.debug("[WorkloadServer] notifyRun: Run socket not found", { run }); + + this.workerClient.sendDebugLog(run.friendlyId, { + time: new Date(), + message: "run:notify socket not found on supervisor", + }); + + return; + } + + runSocket.emit("run:notify", { version: "1", run }); + console.debug("[WorkloadServer] run:notify sent", { run }); + + this.workerClient.sendDebugLog(run.friendlyId, { + time: new Date(), + message: "run:notify supervisor -> runner", + }); + } catch (error) { + console.error("[WorkloadServer] Error in notifyRun", { run, error }); + + this.workerClient.sendDebugLog(run.friendlyId, { + time: new Date(), + message: "run:notify error on supervisor", + }); + } + } + + async start() { + await this.httpServer.start(); + } + + async stop() { + await this.httpServer.stop(); + } +} diff --git a/apps/supervisor/tsconfig.json b/apps/supervisor/tsconfig.json new file mode 100644 index 00000000000..176a07ab7ca --- /dev/null +++ b/apps/supervisor/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../.configs/tsconfig.base.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "paths": { + "@trigger.dev/core/v3": ["../../packages/core/src/v3"], + "@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"] + } +} diff --git a/packages/cli-v3/src/entryPoints/managed-run-controller.ts b/packages/cli-v3/src/entryPoints/managed-run-controller.ts index 91c9863dbeb..cf85dce006f 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-controller.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-controller.ts @@ -691,10 +691,12 @@ class ManagedRunController { // Kill the run process await this.taskRunProcess?.kill("SIGKILL"); - const warmStartUrl = new URL( - "/warm-start", - env.TRIGGER_WARM_START_URL ?? env.TRIGGER_WORKER_API_URL - ); + if (!env.TRIGGER_WARM_START_URL) { + console.error("waitForNextRun: warm starts disabled, shutting down"); + process.exit(0); + } + + const warmStartUrl = new URL("/warm-start", env.TRIGGER_WARM_START_URL); const res = await longPoll( warmStartUrl.href, @@ -709,6 +711,7 @@ class ManagedRunController { "x-trigger-worker-instance-name": env.TRIGGER_WORKER_INSTANCE_NAME, }, }, + // TODO: get these from the warm start service instead { timeoutMs: env.TRIGGER_WARM_START_CONNECTION_TIMEOUT_MS, totalDurationMs: env.TRIGGER_WARM_START_TOTAL_DURATION_MS, diff --git a/packages/core/package.json b/packages/core/package.json index 1f961642952..31900084f84 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -59,7 +59,7 @@ "./v3/workers": "./src/v3/workers/index.ts", "./v3/schemas": "./src/v3/schemas/index.ts", "./v3/runEngineWorker": "./src/v3/runEngineWorker/index.ts", - "./v3/checkpoints": "./src/v3/checkpoints/index.ts" + "./v3/serverOnly": "./src/v3/serverOnly/index.ts" }, "sourceDialects": [ "@triggerdotdev/source" @@ -178,8 +178,8 @@ "v3/runEngineWorker": [ "dist/commonjs/v3/runEngineWorker/index.d.ts" ], - "v3/checkpoints": [ - "dist/commonjs/v3/checkpoints/index.d.ts" + "v3/serverOnly": [ + "dist/commonjs/v3/serverOnly/index.d.ts" ] } }, @@ -663,15 +663,15 @@ "default": "./dist/commonjs/v3/runEngineWorker/index.js" } }, - "./v3/checkpoints": { + "./v3/serverOnly": { "import": { - "@triggerdotdev/source": "./src/v3/checkpoints/index.ts", - "types": "./dist/esm/v3/checkpoints/index.d.ts", - "default": "./dist/esm/v3/checkpoints/index.js" + "@triggerdotdev/source": "./src/v3/serverOnly/index.ts", + "types": "./dist/esm/v3/serverOnly/index.d.ts", + "default": "./dist/esm/v3/serverOnly/index.js" }, "require": { - "types": "./dist/commonjs/v3/checkpoints/index.d.ts", - "default": "./dist/commonjs/v3/checkpoints/index.js" + "types": "./dist/commonjs/v3/serverOnly/index.d.ts", + "default": "./dist/commonjs/v3/serverOnly/index.js" } } }, diff --git a/packages/core/src/v3/checkpoints/index.ts b/packages/core/src/v3/checkpoints/index.ts deleted file mode 100644 index c5f3d550a8a..00000000000 --- a/packages/core/src/v3/checkpoints/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./checkpoints.js"; diff --git a/packages/core/src/v3/schemas/checkpoints.ts b/packages/core/src/v3/schemas/checkpoints.ts new file mode 100644 index 00000000000..8cb5ecc32d6 --- /dev/null +++ b/packages/core/src/v3/schemas/checkpoints.ts @@ -0,0 +1,58 @@ +import { CheckpointType } from "./runEngine.js"; +import z from "zod"; + +const CallbackUrl = z + .string() + .url() + .transform((url) => new URL(url)); + +export const CheckpointServiceSuspendRequestBody = z.object({ + type: CheckpointType, + containerId: z.string(), + simulate: z.boolean().optional(), + leaveRunning: z.boolean().optional(), + reason: z.string().optional(), + callbacks: z + .object({ + /** These headers will sent to all callbacks */ + headers: z.record(z.string()).optional(), + /** This will be hit before suspending the container. Suspension will proceed unless we receive an error response. */ + preSuspend: CallbackUrl.optional(), + /** This will be hit after suspending or failure to suspend the container */ + completion: CallbackUrl.optional(), + }) + .optional(), +}); + +export type CheckpointServiceSuspendRequestBody = z.infer< + typeof CheckpointServiceSuspendRequestBody +>; +export type CheckpointServiceSuspendRequestBodyInput = z.input< + typeof CheckpointServiceSuspendRequestBody +>; + +export const CheckpointServiceSuspendResponseBody = z.object({ + ok: z.literal(true), +}); + +export type CheckpointServiceSuspendResponseBody = z.infer< + typeof CheckpointServiceSuspendResponseBody +>; + +export const CheckpointServiceRestoreRequestBody = z.discriminatedUnion("type", [ + z.object({ + type: z.literal(CheckpointType.Enum.DOCKER), + containerId: z.string(), + }), + z.object({ + type: z.literal(CheckpointType.Enum.KUBERNETES), + containerId: z.string(), + }), +]); + +export type CheckpointServiceRestoreRequestBody = z.infer< + typeof CheckpointServiceRestoreRequestBody +>; +export type CheckpointServiceRestoreRequestBodyInput = z.input< + typeof CheckpointServiceRestoreRequestBody +>; diff --git a/packages/core/src/v3/schemas/index.ts b/packages/core/src/v3/schemas/index.ts index f9847e6e210..9e3c468306a 100644 --- a/packages/core/src/v3/schemas/index.ts +++ b/packages/core/src/v3/schemas/index.ts @@ -12,3 +12,4 @@ export * from "./config.js"; export * from "./build.js"; export * from "./runEngine.js"; export * from "./webhooks.js"; +export * from "./checkpoints.js"; diff --git a/packages/core/src/v3/serverOnly/checkpointClient.ts b/packages/core/src/v3/serverOnly/checkpointClient.ts new file mode 100644 index 00000000000..76bb69e2fc1 --- /dev/null +++ b/packages/core/src/v3/serverOnly/checkpointClient.ts @@ -0,0 +1,137 @@ +import { SupervisorHttpClient } from "../runEngineWorker/index.js"; +import { + CheckpointServiceSuspendRequestBodyInput, + CheckpointServiceSuspendResponseBody, + CheckpointServiceRestoreRequestBodyInput, +} from "../schemas/checkpoints.js"; +import { DequeuedMessage } from "../schemas/runEngine.js"; +import { SimpleStructuredLogger } from "../utils/structuredLogger.js"; + +export type CheckpointClientOptions = { + apiUrl: URL; + workerClient: SupervisorHttpClient; +}; + +export class CheckpointClient { + private readonly logger = new SimpleStructuredLogger("checkpoint-client"); + private readonly apiUrl: URL; + private readonly workerClient: SupervisorHttpClient; + + private get restoreUrl() { + return new URL("/api/v1/restore", this.apiUrl); + } + + private get suspendUrl() { + return new URL("/api/v1/suspend", this.apiUrl); + } + + constructor(opts: CheckpointClientOptions) { + this.apiUrl = opts.apiUrl; + this.workerClient = opts.workerClient; + } + + async suspendRun({ + runFriendlyId, + snapshotFriendlyId, + containerId, + runnerId, + }: { + runFriendlyId: string; + snapshotFriendlyId: string; + containerId: string; + runnerId: string; + }): Promise { + const completionUrl = this.workerClient.getSuspendCompletionUrl( + runFriendlyId, + snapshotFriendlyId, + runnerId + ); + + const res = await fetch(this.suspendUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "DOCKER", + containerId, + callbacks: { + completion: completionUrl.url, + headers: completionUrl.headers, + }, + } satisfies CheckpointServiceSuspendRequestBodyInput), + }); + + if (!res.ok) { + this.logger.error("[CheckpointClient] Suspend request failed", { + runFriendlyId, + snapshotFriendlyId, + containerId, + }); + return false; + } + + this.logger.debug("[CheckpointClient] Suspend request success", { + runFriendlyId, + snapshotFriendlyId, + containerId, + status: res.status, + contentType: res.headers.get("content-type"), + }); + + try { + const data = await res.json(); + const parsedData = CheckpointServiceSuspendResponseBody.safeParse(data); + + if (!parsedData.success) { + this.logger.error("[CheckpointClient] Suspend response invalid", { + runFriendlyId, + snapshotFriendlyId, + containerId, + data, + }); + return false; + } + } catch (error) { + this.logger.error("[CheckpointClient] Suspend response error", { + error, + text: await res.text(), + }); + return false; + } + + return true; + } + + async restoreRun({ + runFriendlyId, + snapshotFriendlyId, + checkpoint, + }: { + runFriendlyId: string; + snapshotFriendlyId: string; + checkpoint: NonNullable; + }): Promise { + const res = await fetch(this.restoreUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "DOCKER", + containerId: checkpoint.location, + } satisfies CheckpointServiceRestoreRequestBodyInput), + }); + + if (!res.ok) { + this.logger.error("[CheckpointClient] Restore request failed", { + runFriendlyId, + snapshotFriendlyId, + checkpoint, + }); + return false; + } + + return true; + } +} diff --git a/packages/core/src/v3/checkpoints/checkpoints.ts b/packages/core/src/v3/serverOnly/checkpointTest.ts similarity index 100% rename from packages/core/src/v3/checkpoints/checkpoints.ts rename to packages/core/src/v3/serverOnly/checkpointTest.ts diff --git a/packages/core/src/v3/serverOnly/httpServer.ts b/packages/core/src/v3/serverOnly/httpServer.ts new file mode 100644 index 00000000000..3c38f3968a9 --- /dev/null +++ b/packages/core/src/v3/serverOnly/httpServer.ts @@ -0,0 +1,291 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { z } from "zod"; +import { SimpleStructuredLogger } from "../utils/structuredLogger.js"; +import { HttpReply, getJsonBody } from "../apps/http.js"; + +const logger = new SimpleStructuredLogger("worker-http"); + +type RouteHandler< + TParams extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TQuery extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TBody extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, +> = (ctx: { + params: z.infer; + queryParams: z.infer; + body: z.infer; + req: IncomingMessage; + res: ServerResponse; + reply: HttpReply; +}) => Promise; + +interface RouteDefinition< + TParams extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TQuery extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TBody extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, +> { + paramsSchema?: TParams; + querySchema?: TQuery; + bodySchema?: TBody; + handler: RouteHandler; +} + +const HttpMethod = z.enum([ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "OPTIONS", + "HEAD", + "CONNECT", + "TRACE", +]); +type HttpMethod = z.infer; + +type RouteMap = Partial<{ + [path: string]: Partial<{ + [method in HttpMethod]: RouteDefinition; + }>; +}>; + +type HttpServerOptions = { + port: number; + host: string; +}; + +export class HttpServer { + private readonly port: number; + private readonly host: string; + private routes: RouteMap = {}; + + public readonly server: ReturnType; + + constructor(options: HttpServerOptions) { + this.port = options.port; + this.host = options.host; + + this.server = createServer(async (req, res) => { + const reply = new HttpReply(res); + + try { + const { url, method } = req; + + logger.log(`${method} ${url?.split("?")[0]}`, { url }); + + if (!url) { + logger.error("Request URL is empty", { method }); + return reply.text("Request URL is empty", 400); + } + + if (!method) { + logger.error("Request method is empty", { url }); + return reply.text("Request method is empty", 400); + } + + const httpMethod = HttpMethod.safeParse(method); + + if (!httpMethod.success) { + logger.error("HTTP method not implemented", { url, method }); + return reply.text(`HTTP method ${method} not implemented`, 400); + } + + const route = this.findRoute(url); + + if (!route) { + logger.error("No route match", { url, method }); + return reply.empty(404); + } + + const routeDefinition = this.routes[route]?.[httpMethod.data]; + + // logger.debug("Matched route", { + // url, + // method, + // route, + // routeDefinition, + // }); + + if (!routeDefinition) { + logger.error("Invalid method", { url, method, parsedMethod: httpMethod.data }); + return reply.empty(405); + } + + const { handler, paramsSchema, querySchema, bodySchema } = routeDefinition; + + const params = this.parseRouteParams(route, url); + const parsedParams = this.optionalSchema(paramsSchema, params); + + if (!parsedParams.success) { + logger.error("Failed to parse params schema", { url, method, params }); + return reply.text("Invalid params", 400); + } + + const queryParams = this.parseQueryParams(url); + const parsedQueryParams = this.optionalSchema(querySchema, queryParams); + + if (!parsedQueryParams.success) { + logger.error("Failed to parse query params schema", { url, method, queryParams }); + return reply.text("Invalid query params", 400); + } + + const body = await getJsonBody(req); + const parsedBody = this.optionalSchema(bodySchema, body); + + if (!parsedBody.success) { + logger.error("Failed to parse body schema", { + url, + method, + body, + error: parsedBody.error, + }); + return reply.json({ ok: false, error: "Invalid body" }, false, 400); + } + + try { + await handler({ + reply, + req, + res, + params: parsedParams.data, + queryParams: parsedQueryParams.data, + body: parsedBody.data, + }); + } catch (handlerError) { + logger.error("Route handler error", { error: handlerError }); + return reply.empty(500); + } + } catch (error) { + logger.error("Failed to handle request", { error }); + return reply.empty(500); + } + + return; + }); + + this.server.on("clientError", (_, socket) => { + socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); + }); + } + + route< + TParams extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TQuery extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TBody extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + >(path: `/${string}`, method: HttpMethod, definition: RouteDefinition) { + this.routes[path] = { + ...this.routes[path], + [method]: definition, + }; + return this; + } + + start(): Promise { + return new Promise((resolve) => { + this.server.listen(this.port, this.host, () => { + logger.log("HTTP server listening on port", { port: this.port }); + resolve(); + }); + }); + } + + stop(): Promise { + return new Promise((resolve) => { + this.server.close(() => { + logger.log("HTTP server stopped"); + resolve(); + }); + }); + } + + private optionalSchema< + TSchema extends z.ZodFirstPartySchemaTypes | undefined, + TData extends TSchema extends z.ZodFirstPartySchemaTypes ? z.TypeOf : TData, + >( + schema: TSchema, + data: TData + ): + | { + success: false; + error: string; + } + | { + success: true; + data: TSchema extends z.ZodFirstPartySchemaTypes ? z.infer : TData; + } { + if (!schema) { + return { success: true, data }; + } + + const parsed = schema.safeParse(data); + + if (!parsed.success) { + return { success: false, error: parsed.error.message }; + } + + return { success: true, data: parsed.data }; + } + + private parseQueryParams(url: string): Record { + const { searchParams } = new URL(url, "http://localhost"); + return Object.fromEntries(searchParams.entries()); + } + + private parseRouteParams(route: string, url: string): Record { + const params: Record = {}; + + const routeParts = route.split("/"); + const urlWithoutQueryParams = url.split("?")[0]; + + if (!urlWithoutQueryParams) { + return params; + } + + const urlParts = urlWithoutQueryParams.split("/"); + + routeParts.forEach((part, index) => { + if (part.startsWith(":")) { + const paramName = part.slice(1); + const urlPart = urlParts[index]; + + if (!urlPart) { + return; + } + + params[paramName] = urlPart; + } + }); + + return params; + } + + private findRoute(url: string): string | null { + for (const route in this.routes) { + const routeParts = route.split("/"); + const urlWithoutQueryParams = url.split("?")[0]; + + if (!urlWithoutQueryParams) { + continue; + } + + const urlParts = urlWithoutQueryParams.split("/"); + + if (routeParts.length !== urlParts.length) { + continue; + } + + const matches = routeParts.every((part, i) => { + if (part.startsWith(":")) { + // Always match route params + return true; + } + return part === urlParts[i]; + }); + + if (matches) { + return route; + } + } + + return null; + } +} diff --git a/packages/core/src/v3/serverOnly/index.ts b/packages/core/src/v3/serverOnly/index.ts new file mode 100644 index 00000000000..38941c03f15 --- /dev/null +++ b/packages/core/src/v3/serverOnly/index.ts @@ -0,0 +1,3 @@ +export * from "./checkpointClient.js"; +export * from "./checkpointTest.js"; +export * from "./httpServer.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc1941178d2..7d501c67a4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,6 +169,40 @@ importers: specifier: ^3.57.1 version: 3.57.1(@cloudflare/workers-types@4.20240512.0) + apps/supervisor: + dependencies: + '@kubernetes/client-node': + specifier: ^1.0.0 + version: 1.0.0 + '@trigger.dev/core': + specifier: workspace:* + version: link:../../packages/core + dockerode: + specifier: ^4.0.3 + version: 4.0.4 + nanoid: + specifier: ^5.0.9 + version: 5.1.2 + socket.io: + specifier: 4.7.4 + version: 4.7.4 + std-env: + specifier: ^3.8.0 + version: 3.8.1 + tinyexec: + specifier: ^0.3.1 + version: 0.3.2 + zod: + specifier: 3.23.8 + version: 3.23.8 + devDependencies: + '@types/dockerode': + specifier: ^3.3.33 + version: 3.3.35 + docker-api-ts: + specifier: ^0.2.2 + version: 0.2.2 + apps/webapp: dependencies: '@ariakit/react': @@ -2084,7 +2118,7 @@ packages: dependencies: '@ai-sdk/provider': 1.0.0 eventsource-parser: 3.0.0 - nanoid: 5.0.8 + nanoid: 5.1.2 secure-json-parse: 2.7.0 zod: 3.23.8 dev: false @@ -5215,7 +5249,6 @@ packages: /@balena/dockerignore@1.0.2: resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} - dev: true /@bufbuild/protobuf@1.10.0: resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==} @@ -7533,6 +7566,14 @@ packages: graphql: 16.6.0 dev: false + /@grpc/grpc-js@1.12.6: + resolution: {integrity: sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==} + engines: {node: '>=12.10.0'} + dependencies: + '@grpc/proto-loader': 0.7.13 + '@js-sdsl/ordered-map': 4.4.2 + dev: false + /@grpc/grpc-js@1.8.17: resolution: {integrity: sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==} engines: {node: ^8.13.0 || >=10.10.0} @@ -7540,6 +7581,17 @@ packages: '@grpc/proto-loader': 0.7.7 '@types/node': 18.19.20 + /@grpc/proto-loader@0.7.13: + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.3.2 + yargs: 17.7.2 + dev: false + /@grpc/proto-loader@0.7.7: resolution: {integrity: sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==} engines: {node: '>=6'} @@ -7882,6 +7934,13 @@ packages: wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 + /@isaacs/fs-minipass@4.0.1: + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + dependencies: + minipass: 7.1.2 + dev: false + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7943,6 +8002,28 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.5.0 + /@js-sdsl/ordered-map@4.4.2: + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + dev: false + + /@jsep-plugin/assignment@1.3.0(jsep@1.4.0): + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + dependencies: + jsep: 1.4.0 + dev: false + + /@jsep-plugin/regex@1.0.4(jsep@1.4.0): + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + dependencies: + jsep: 1.4.0 + dev: false + /@jsonhero/path@1.0.21: resolution: {integrity: sha512-gVUDj/92acpVoJwsVJ/RuWOaHyG4oFzn898WNGQItLCTQ+hOaVlEaImhwE1WqOTf+l3dGOUkbSiVKlb3q1hd1Q==} dev: false @@ -7975,6 +8056,33 @@ packages: - utf-8-validate dev: false + /@kubernetes/client-node@1.0.0: + resolution: {integrity: sha512-a8NSvFDSHKFZ0sR1hbPSf8IDFNJwctEU5RodSCNiq/moRXWmrdmqhb1RRQzF+l+TSBaDgHw3YsYNxxE92STBzw==} + dependencies: + '@types/js-yaml': 4.0.9 + '@types/node': 22.13.9 + '@types/node-fetch': 2.6.12 + '@types/stream-buffers': 3.0.7 + '@types/tar': 6.1.4 + '@types/ws': 8.5.12 + form-data: 4.0.0 + isomorphic-ws: 5.0.0(ws@8.18.0) + js-yaml: 4.1.0 + jsonpath-plus: 10.3.0 + node-fetch: 2.6.12 + openid-client: 6.3.3 + rfc4648: 1.5.3 + stream-buffers: 3.0.2 + tar: 7.4.3 + tmp-promise: 3.0.3 + tslib: 2.6.2 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + dev: false + /@lezer/common@1.0.2: resolution: {integrity: sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==} dev: false @@ -16853,15 +16961,15 @@ packages: /@types/docker-modem@3.0.6: resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} dependencies: - '@types/node': 18.19.20 + '@types/node': 20.14.14 '@types/ssh2': 1.15.1 dev: true - /@types/dockerode@3.3.31: - resolution: {integrity: sha512-42R9eoVqJDSvVspV89g7RwRqfNExgievLNWoHkg7NoWIqAmavIbgQBb4oc0qRtHkxE+I3Xxvqv7qVXFABKPBTg==} + /@types/dockerode@3.3.35: + resolution: {integrity: sha512-P+DCMASlsH+QaKkDpekKrP5pLls767PPs+/LrlVbKnEnY5tMpEUa2C6U4gRsdFZengOqxdCIqy16R22Q3pLB6Q==} dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 18.19.20 + '@types/node': 20.14.14 '@types/ssh2': 1.15.1 dev: true @@ -17044,6 +17152,13 @@ packages: '@types/node': 18.19.20 dev: false + /@types/node-fetch@2.6.12: + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + dependencies: + '@types/node': 20.14.14 + form-data: 4.0.0 + dev: false + /@types/node-fetch@2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: @@ -17088,6 +17203,12 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@22.13.9: + resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} + dependencies: + undici-types: 6.20.0 + dev: false + /@types/nodemailer@6.4.17: resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} dependencies: @@ -17271,6 +17392,12 @@ packages: resolution: {integrity: sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==} dev: false + /@types/stream-buffers@3.0.7: + resolution: {integrity: sha512-azOCy05sXVXrO+qklf0c/B07H/oHaIuDDAiHPVwlk3A9Ek+ksHyTeMajLZl3r76FxpPpxem//4Te61G1iW3Giw==} + dependencies: + '@types/node': 20.14.14 + dev: false + /@types/superagent@8.1.9: resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} dependencies: @@ -17292,7 +17419,6 @@ packages: dependencies: '@types/node': 18.19.20 minipass: 4.0.0 - dev: true /@types/tinycolor2@1.4.3: resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==} @@ -17338,8 +17464,7 @@ packages: /@types/ws@8.5.12: resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} dependencies: - '@types/node': 18.19.20 - dev: true + '@types/node': 20.14.14 /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} @@ -17351,7 +17476,7 @@ packages: resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} requiresBuild: true dependencies: - '@types/node': 18.19.20 + '@types/node': 20.14.14 dev: false optional: true @@ -19171,7 +19296,6 @@ packages: resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} engines: {node: '>=10.0.0'} requiresBuild: true - dev: true optional: true /builtins@1.0.3: @@ -19521,12 +19645,16 @@ packages: /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - dev: true /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} + /chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + dev: false + /chrome-trace-event@1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} engines: {node: '>=6.0'} @@ -20013,7 +20141,6 @@ packages: dependencies: buildcheck: 0.0.6 nan: 2.20.0 - dev: true optional: true /cpy-cli@5.0.0: @@ -20661,6 +20788,10 @@ packages: /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + /docker-api-ts@0.2.2: + resolution: {integrity: sha512-ayoc0OuS6lY7b64GeUtKcPzbKMkK70Vh3BYLKKG13cXX+/gGS9LyTNVvvJyvZ19Y6kbE4Kbv+2gwRUD17UVTRA==} + dev: true + /docker-compose@0.24.8: resolution: {integrity: sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==} engines: {node: '>= 6.0.0'} @@ -20680,6 +20811,18 @@ packages: - supports-color dev: true + /docker-modem@5.0.6: + resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} + engines: {node: '>= 8.0'} + dependencies: + debug: 4.3.7 + readable-stream: 3.6.0 + split-ca: 1.0.1 + ssh2: 1.16.0 + transitivePeerDependencies: + - supports-color + dev: false + /dockerode@3.3.5: resolution: {integrity: sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==} engines: {node: '>= 8.0'} @@ -20691,6 +20834,21 @@ packages: - supports-color dev: true + /dockerode@4.0.4: + resolution: {integrity: sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==} + engines: {node: '>= 8.0'} + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.12.6 + '@grpc/proto-loader': 0.7.13 + docker-modem: 5.0.6 + protobufjs: 7.3.2 + tar-fs: 2.0.1 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -22719,7 +22877,6 @@ packages: /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - dev: true /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} @@ -24087,6 +24244,14 @@ packages: ws: 8.16.0 dev: false + /isomorphic-ws@5.0.0(ws@8.18.0): + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + dependencies: + ws: 8.18.0 + dev: false + /isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} dev: false @@ -24149,6 +24314,10 @@ packages: resolution: {integrity: sha512-6rpxTHPAQyWMb9A35BroFl1Sp0ST3DpPcm5EVIxZxdH+e0Hv9fwhyB3XLKFUcHNpdSDnETmBfuPPTTlYz5+USw==} dev: false + /jose@6.0.8: + resolution: {integrity: sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==} + dev: false + /js-beautify@1.15.1: resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} engines: {node: '>=14'} @@ -24208,6 +24377,11 @@ packages: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} dev: false + /jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + dev: false + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -24295,6 +24469,16 @@ packages: graceful-fs: 4.2.11 dev: true + /jsonpath-plus@10.3.0: + resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + dev: false + /jsonpath-plus@7.2.0: resolution: {integrity: sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==} engines: {node: '>=12.0.0'} @@ -25460,7 +25644,6 @@ packages: engines: {node: '>=8'} dependencies: yallist: 4.0.0 - dev: true /minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} @@ -25487,6 +25670,14 @@ packages: minipass: 3.3.6 yallist: 4.0.0 + /minizlib@3.0.1: + resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} + engines: {node: '>= 18'} + dependencies: + minipass: 7.1.2 + rimraf: 5.0.7 + dev: false + /mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} dev: false @@ -25498,7 +25689,6 @@ packages: /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - dev: true /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -25515,7 +25705,6 @@ packages: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true - dev: true /mlly@1.7.1: resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} @@ -25664,8 +25853,8 @@ packages: hasBin: true dev: false - /nanoid@5.0.8: - resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==} + /nanoid@5.1.2: + resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==} engines: {node: ^18 || >=20} hasBin: true dev: false @@ -26097,6 +26286,10 @@ packages: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} dev: false + /oauth4webapi@3.3.0: + resolution: {integrity: sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==} + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -26357,6 +26550,13 @@ packages: dev: false optional: true + /openid-client@6.3.3: + resolution: {integrity: sha512-lTK8AV8SjqCM4qznLX0asVESAwzV39XTVdfMAM185ekuaZCnkWdPzcxMTXNlsm9tsUAMa1Q30MBmKAykdT1LWw==} + dependencies: + jose: 6.0.8 + oauth4webapi: 3.3.0 + dev: false + /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -29020,7 +29220,6 @@ packages: hasBin: true dependencies: glob: 10.3.10 - dev: true /rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} @@ -29556,7 +29755,7 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.4 + debug: 4.3.7 engine.io: 6.5.4 socket.io-adapter: 2.5.4 socket.io-parser: 4.2.4 @@ -29703,7 +29902,6 @@ packages: /split-ca@1.0.1: resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} - dev: true /split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} @@ -29750,7 +29948,6 @@ packages: optionalDependencies: cpu-features: 0.0.10 nan: 2.20.0 - dev: true /sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} @@ -29834,6 +30031,10 @@ packages: /std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: false + + /std-env@3.8.1: + resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} /stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} @@ -30412,7 +30613,6 @@ packages: mkdirp-classic: 0.5.3 pump: 3.0.0 tar-stream: 2.2.0 - dev: true /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} @@ -30441,7 +30641,6 @@ packages: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.0 - dev: true /tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -30473,6 +30672,18 @@ packages: yallist: 4.0.0 dev: false + /tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.1 + mkdirp: 3.0.1 + yallist: 5.0.0 + dev: false + /tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} dependencies: @@ -30582,7 +30793,7 @@ packages: resolution: {integrity: sha512-JBbOhxmygj/ouH/47GnoVNt+c55Telh/45IjVxEbDoswsLchVmJiuKiw/eF6lE5i7LN+/99xsrSCttI3YRtirg==} dependencies: '@balena/dockerignore': 1.0.2 - '@types/dockerode': 3.3.31 + '@types/dockerode': 3.3.35 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 @@ -30739,6 +30950,12 @@ packages: engines: {node: '>=14.0.0'} dev: true + /tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + dependencies: + tmp: 0.2.3 + dev: false + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -30756,7 +30973,6 @@ packages: /tmp@0.2.3: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} - dev: true /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} @@ -31421,6 +31637,10 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + dev: false + /undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} @@ -32252,7 +32472,7 @@ packages: pathe: 1.1.0 picocolors: 1.0.0 source-map: 0.6.1 - std-env: 3.7.0 + std-env: 3.8.1 strip-literal: 1.0.1 tinybench: 2.3.1 tinypool: 0.3.1 @@ -32309,7 +32529,7 @@ packages: magic-string: 0.30.8 pathe: 1.1.1 picocolors: 1.0.0 - std-env: 3.7.0 + std-env: 3.8.1 strip-literal: 2.1.0 tinybench: 2.6.0 tinypool: 0.8.3 @@ -32365,7 +32585,7 @@ packages: magic-string: 0.30.11 pathe: 1.1.2 picocolors: 1.1.1 - std-env: 3.7.0 + std-env: 3.8.1 strip-literal: 2.1.0 tinybench: 2.9.0 tinypool: 0.8.3 @@ -32420,7 +32640,7 @@ packages: execa: 8.0.1 magic-string: 0.30.11 pathe: 1.1.2 - std-env: 3.7.0 + std-env: 3.8.1 tinybench: 2.9.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 @@ -32947,6 +33167,11 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + dev: false + /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'}