diff --git a/.changeset/smooth-planets-flow.md b/.changeset/smooth-planets-flow.md new file mode 100644 index 0000000000..708932fcca --- /dev/null +++ b/.changeset/smooth-planets-flow.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Update profile switcher diff --git a/.env.example b/.env.example index c89ed5f012..d34cd822fa 100644 --- a/.env.example +++ b/.env.example @@ -82,7 +82,7 @@ COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl # DEPOT_ORG_ID= # DEPOT_TOKEN= -DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry +DEPLOY_REGISTRY_HOST=localhost:5000 # This is the host that the deploy CLI will use to push images to the registry # DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318" # These are needed for the object store (for handling large payloads/outputs) # OBJECT_STORE_BASE_URL="https://{bucket}.{accountId}.r2.cloudflarestorage.com" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5ac964558..709dcca47c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,8 +62,6 @@ branch are tagged into a release periodically. pnpm run docker ``` - This will also start and run a local instance of [pgAdmin](https://www.pgadmin.org/) on [localhost:5480](http://localhost:5480), preconfigured with email `admin@example.com` and pwd `admin`. Then use `postgres` as the password to the Trigger.dev server. - 9. Migrate the database ``` pnpm run db:migrate @@ -94,13 +92,11 @@ We use the `/references/v3-catalog` subdirectory as a staging ground for t First, make sure you are running the webapp according to the instructions above. Then: -1. In Postgres go to the "Organizations" table and on your org set the `v3Enabled` column to `true`. - -2. Visit http://localhost:3030 in your browser and create a new V3 project called "v3-catalog". If you don't see an option for V3, you haven't set the `v3Enabled` flag to true. +1. Visit http://localhost:3030 in your browser and create a new V3 project called "v3-catalog". -3. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`. +2. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`. -4. Build the CLI +3. Build the CLI ```sh # Build the CLI @@ -109,7 +105,7 @@ pnpm run build --filter trigger.dev pnpm i ``` -5. Change into the `/references/v3-catalog` directory and authorize the CLI to the local server: +4. Change into the `/references/v3-catalog` directory and authorize the CLI to the local server: ```sh cd references/v3-catalog diff --git a/apps/supervisor/.env.example b/apps/supervisor/.env.example index 652fc03942..5cb86d5a33 100644 --- a/apps/supervisor/.env.example +++ b/apps/supervisor/.env.example @@ -14,5 +14,4 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost: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/src/env.ts b/apps/supervisor/src/env.ts index 2c60ebdf1f..fd6bd61050 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -10,8 +10,9 @@ const Env = z.object({ // Required settings TRIGGER_API_URL: z.string().url(), - TRIGGER_WORKER_TOKEN: z.string(), + TRIGGER_WORKER_TOKEN: z.string(), // accepts file:// path to read from a file MANAGED_WORKER_SECRET: z.string(), + OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url(), // set on the runners // Workload API settings (coordinator mode) - the workload API is what the run controller connects to TRIGGER_WORKLOAD_API_ENABLED: BoolEnv.default("true"), @@ -29,26 +30,6 @@ const Env = z.object({ RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS: z.coerce.number().optional(), RUNNER_ADDITIONAL_ENV_VARS: AdditionalEnvVars, // optional (csv) RUNNER_PRETTY_LOGS: BoolEnv.default(false), - RUNNER_DOCKER_AUTOREMOVE: BoolEnv.default(true), - /** - * Network mode to use for all runners. Supported standard values are: `bridge`, `host`, `none`, and `container:`. - * Any other value is taken as a custom network's name to which all runners should connect to. - * - * Accepts a list of comma-separated values to attach to multiple networks. Additional networks are interpreted as network names and will be attached after container creation. - * - * **WARNING**: Specifying multiple networks will slightly increase startup times. - * - * @default "host" - */ - RUNNER_DOCKER_NETWORKS: z.string().default("host"), - - // Docker settings - DOCKER_API_VERSION: z.string().default("v1.41"), - DOCKER_PLATFORM: z.string().optional(), // e.g. linux/amd64, linux/arm64 - DOCKER_STRIP_IMAGE_DIGEST: BoolEnv.default(true), - DOCKER_REGISTRY_USERNAME: z.string().optional(), - DOCKER_REGISTRY_PASSWORD: z.string().optional(), - DOCKER_REGISTRY_URL: z.string().optional(), // e.g. https://index.docker.io/v1 // Dequeue settings (provider mode) TRIGGER_DEQUEUE_ENABLED: BoolEnv.default("true"), @@ -62,22 +43,39 @@ const Env = z.object({ TRIGGER_CHECKPOINT_URL: z.string().optional(), TRIGGER_METADATA_URL: z.string().optional(), - // Used by the workload manager, e.g docker/k8s - OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url(), - ENFORCE_MACHINE_PRESETS: z.coerce.boolean().default(false), - KUBERNETES_IMAGE_PULL_SECRETS: z.string().optional(), // csv - // Used by the resource monitor RESOURCE_MONITOR_ENABLED: BoolEnv.default(false), RESOURCE_MONITOR_OVERRIDE_CPU_TOTAL: z.coerce.number().optional(), RESOURCE_MONITOR_OVERRIDE_MEMORY_TOTAL_GB: z.coerce.number().optional(), - // Kubernetes specific settings + // Docker settings + DOCKER_API_VERSION: z.string().default("v1.41"), + DOCKER_PLATFORM: z.string().optional(), // e.g. linux/amd64, linux/arm64 + DOCKER_STRIP_IMAGE_DIGEST: BoolEnv.default(true), + DOCKER_REGISTRY_USERNAME: z.string().optional(), + DOCKER_REGISTRY_PASSWORD: z.string().optional(), + DOCKER_REGISTRY_URL: z.string().optional(), // e.g. https://index.docker.io/v1 + DOCKER_ENFORCE_MACHINE_PRESETS: BoolEnv.default(true), + DOCKER_AUTOREMOVE_EXITED_CONTAINERS: BoolEnv.default(true), + /** + * Network mode to use for all runners. Supported standard values are: `bridge`, `host`, `none`, and `container:`. + * Any other value is taken as a custom network's name to which all runners should connect to. + * + * Accepts a list of comma-separated values to attach to multiple networks. Additional networks are interpreted as network names and will be attached after container creation. + * + * **WARNING**: Specifying multiple networks will slightly increase startup times. + * + * @default "host" + */ + DOCKER_RUNNER_NETWORKS: z.string().default("host"), + + // Kubernetes settings KUBERNETES_FORCE_ENABLED: BoolEnv.default(false), KUBERNETES_NAMESPACE: z.string().default("default"), KUBERNETES_WORKER_NODETYPE_LABEL: z.string().default("v4-worker"), - EPHEMERAL_STORAGE_SIZE_LIMIT: z.string().default("10Gi"), - EPHEMERAL_STORAGE_SIZE_REQUEST: z.string().default("2Gi"), + KUBERNETES_IMAGE_PULL_SECRETS: z.string().optional(), // csv + KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT: z.string().default("10Gi"), + KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST: z.string().default("2Gi"), // Metrics METRICS_ENABLED: BoolEnv.default(true), diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index bfaf6abaed..83fe89c1ed 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -24,6 +24,7 @@ import { collectDefaultMetrics } from "prom-client"; import { register } from "./metrics.js"; import { PodCleaner } from "./services/podCleaner.js"; import { FailedPodHandler } from "./services/failedPodHandler.js"; +import { getWorkerToken } from "./workerToken.js"; if (env.METRICS_COLLECT_DEFAULTS) { collectDefaultMetrics({ register }); @@ -67,7 +68,7 @@ class ManagedSupervisor { heartbeatIntervalSeconds: env.RUNNER_HEARTBEAT_INTERVAL_SECONDS, snapshotPollIntervalSeconds: env.RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS, additionalEnvVars: env.RUNNER_ADDITIONAL_ENV_VARS, - dockerAutoremove: env.RUNNER_DOCKER_AUTOREMOVE, + dockerAutoremove: env.DOCKER_AUTOREMOVE_EXITED_CONTAINERS, } satisfies WorkloadManagerOptions; this.resourceMonitor = env.RESOURCE_MONITOR_ENABLED @@ -119,7 +120,7 @@ class ManagedSupervisor { } this.workerSession = new SupervisorSession({ - workerToken: env.TRIGGER_WORKER_TOKEN, + workerToken: getWorkerToken(), apiUrl: env.TRIGGER_API_URL, instanceName: env.TRIGGER_WORKER_INSTANCE_NAME, managedWorkerSecret: env.MANAGED_WORKER_SECRET, diff --git a/apps/supervisor/src/workerToken.ts b/apps/supervisor/src/workerToken.ts new file mode 100644 index 0000000000..1142796a7a --- /dev/null +++ b/apps/supervisor/src/workerToken.ts @@ -0,0 +1,29 @@ +import { readFileSync } from "fs"; +import { env } from "./env.js"; + +export function getWorkerToken() { + if (!env.TRIGGER_WORKER_TOKEN.startsWith("file://")) { + return env.TRIGGER_WORKER_TOKEN; + } + + const tokenPath = env.TRIGGER_WORKER_TOKEN.replace("file://", ""); + + console.debug( + JSON.stringify({ + message: "🔑 Reading worker token from file", + tokenPath, + }) + ); + + try { + const token = readFileSync(tokenPath, "utf8").trim(); + return token; + } catch (error) { + console.error(`Failed to read worker token from file: ${tokenPath}`, error); + throw new Error( + `Unable to read worker token from file: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } +} diff --git a/apps/supervisor/src/workloadManager/docker.ts b/apps/supervisor/src/workloadManager/docker.ts index d2dc484cc1..e3b39bfed6 100644 --- a/apps/supervisor/src/workloadManager/docker.ts +++ b/apps/supervisor/src/workloadManager/docker.ts @@ -28,7 +28,7 @@ export class DockerWorkloadManager implements WorkloadManager { }); } - this.runnerNetworks = env.RUNNER_DOCKER_NETWORKS.split(","); + this.runnerNetworks = env.DOCKER_RUNNER_NETWORKS.split(","); this.platformOverride = env.DOCKER_PLATFORM; if (this.platformOverride) { @@ -61,6 +61,7 @@ export class DockerWorkloadManager implements WorkloadManager { // Build environment variables const envVars: string[] = [ + `OTEL_EXPORTER_OTLP_ENDPOINT=${env.OTEL_EXPORTER_OTLP_ENDPOINT}`, `TRIGGER_DEQUEUED_AT_MS=${opts.dequeuedAt.getTime()}`, `TRIGGER_POD_SCHEDULED_AT_MS=${Date.now()}`, `TRIGGER_ENV_ID=${opts.envId}`, @@ -70,8 +71,9 @@ export class DockerWorkloadManager implements WorkloadManager { `TRIGGER_SUPERVISOR_API_PORT=${this.opts.workloadApiPort}`, `TRIGGER_SUPERVISOR_API_DOMAIN=${this.opts.workloadApiDomain ?? getDockerHostDomain()}`, `TRIGGER_WORKER_INSTANCE_NAME=${env.TRIGGER_WORKER_INSTANCE_NAME}`, - `OTEL_EXPORTER_OTLP_ENDPOINT=${env.OTEL_EXPORTER_OTLP_ENDPOINT}`, `TRIGGER_RUNNER_ID=${runnerId}`, + `TRIGGER_MACHINE_CPU=${opts.machine.cpu}`, + `TRIGGER_MACHINE_MEMORY=${opts.machine.memory}`, `PRETTY_LOGS=${env.RUNNER_PRETTY_LOGS}`, ]; @@ -110,10 +112,7 @@ export class DockerWorkloadManager implements WorkloadManager { // - If there are multiple networks to attach, this will ensure the runner won't also be connected to the bridge network hostConfig.NetworkMode = firstNetwork; - if (env.ENFORCE_MACHINE_PRESETS) { - envVars.push(`TRIGGER_MACHINE_CPU=${opts.machine.cpu}`); - envVars.push(`TRIGGER_MACHINE_MEMORY=${opts.machine.memory}`); - + if (env.DOCKER_ENFORCE_MACHINE_PRESETS) { hostConfig.NanoCpus = opts.machine.cpu * 1e9; hostConfig.Memory = opts.machine.memory * 1024 * 1024 * 1024; } diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 54dd95a795..d596e18ed6 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -236,13 +236,13 @@ export class KubernetesWorkloadManager implements WorkloadManager { get #defaultResourceRequests(): ResourceQuantities { return { - "ephemeral-storage": env.EPHEMERAL_STORAGE_SIZE_REQUEST, + "ephemeral-storage": env.KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST, }; } get #defaultResourceLimits(): ResourceQuantities { return { - "ephemeral-storage": env.EPHEMERAL_STORAGE_SIZE_LIMIT, + "ephemeral-storage": env.KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT, }; } diff --git a/apps/webapp/app/bootstrap.ts b/apps/webapp/app/bootstrap.ts new file mode 100644 index 0000000000..84c13c061f --- /dev/null +++ b/apps/webapp/app/bootstrap.ts @@ -0,0 +1,75 @@ +import { mkdir, writeFile } from "fs/promises"; +import { prisma } from "./db.server"; +import { env } from "./env.server"; +import { WorkerGroupService } from "./v3/services/worker/workerGroupService.server"; +import { dirname } from "path"; +import { tryCatch } from "@trigger.dev/core"; + +export async function bootstrap() { + if (env.TRIGGER_BOOTSTRAP_ENABLED !== "1") { + return; + } + + if (env.TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME) { + const [error] = await tryCatch(createWorkerGroup()); + if (error) { + console.error("Failed to create worker group", { error }); + } + } +} + +async function createWorkerGroup() { + const workerGroupName = env.TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME; + const tokenPath = env.TRIGGER_BOOTSTRAP_WORKER_TOKEN_PATH; + + const existingWorkerGroup = await prisma.workerInstanceGroup.findFirst({ + where: { + name: workerGroupName, + }, + }); + + if (existingWorkerGroup) { + console.warn(`[bootstrap] Worker group ${workerGroupName} already exists`); + return; + } + + const service = new WorkerGroupService(); + const { token, workerGroup } = await service.createWorkerGroup({ + name: workerGroupName, + }); + + console.log(` +========================== +Trigger.dev Bootstrap - Worker Token + +WARNING: This will only be shown once. Save it now! + +Worker group: +${workerGroup.name} + +Token: +${token.plaintext} + +If using docker compose, set: +TRIGGER_WORKER_TOKEN=${token.plaintext} + +${ + tokenPath + ? `Or, if using a file: +TRIGGER_WORKER_TOKEN=file://${tokenPath}` + : "" +} + +========================== + `); + + if (tokenPath) { + const dir = dirname(tokenPath); + await mkdir(dir, { recursive: true }); + await writeFile(tokenPath, token.plaintext, { + mode: 0o600, + }); + + console.log(`[bootstrap] Worker token saved to ${tokenPath}`); + } +} diff --git a/apps/webapp/app/entry.server.tsx b/apps/webapp/app/entry.server.tsx index 407d09c15c..d05fabd90b 100644 --- a/apps/webapp/app/entry.server.tsx +++ b/apps/webapp/app/entry.server.tsx @@ -15,6 +15,7 @@ import { OperatingSystemPlatform, } from "./components/primitives/OperatingSystemProvider"; import { singleton } from "./utils/singleton"; +import { bootstrap } from "./bootstrap"; const ABORT_DELAY = 30000; @@ -177,6 +178,10 @@ Worker.init().catch((error) => { logError(error); }); +bootstrap().catch((error) => { + logError(error); +}); + function logError(error: unknown, request?: Request) { console.error(error); diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 02377169a3..50e627ebf6 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -214,8 +214,8 @@ const EnvironmentSchema = z.object({ PUBSUB_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(10), - DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(10), + DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(100), + DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(100), DEFAULT_DEV_ENV_EXECUTION_ATTEMPTS: z.coerce.number().int().positive().default(1), TUNNEL_HOST: z.string().optional(), @@ -260,7 +260,6 @@ const EnvironmentSchema = z.object({ INGEST_EVENT_RATE_LIMIT_MAX: z.coerce.number().int().optional(), //v3 - V3_ENABLED: z.string().default("false"), PROVIDER_SECRET: z.string().default("provider-secret"), COORDINATOR_SECRET: z.string().default("coordinator-secret"), DEPOT_TOKEN: z.string().optional(), @@ -278,6 +277,8 @@ const EnvironmentSchema = z.object({ OBJECT_STORE_BASE_URL: z.string().optional(), OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(), OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(), + OBJECT_STORE_REGION: z.string().optional(), + OBJECT_STORE_SERVICE: z.string().default("s3"), EVENTS_BATCH_SIZE: z.coerce.number().int().default(100), EVENTS_BATCH_INTERVAL: z.coerce.number().int().default(1000), EVENTS_DEFAULT_LOG_RETENTION: z.coerce.number().int().default(7), @@ -778,6 +779,14 @@ const EnvironmentSchema = z.object({ RUN_REPLICATION_KEEP_ALIVE_ENABLED: z.string().default("1"), RUN_REPLICATION_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), RUN_REPLICATION_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), + + // Bootstrap + TRIGGER_BOOTSTRAP_ENABLED: z.string().default("0"), + TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME: z.string().optional(), + TRIGGER_BOOTSTRAP_WORKER_TOKEN_PATH: z.string().optional(), + + // Machine presets + MACHINE_PRESETS_OVERRIDE_PATH: z.string().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/features.server.ts b/apps/webapp/app/features.server.ts index 845bcb49f0..e55f91e213 100644 --- a/apps/webapp/app/features.server.ts +++ b/apps/webapp/app/features.server.ts @@ -1,9 +1,7 @@ -import { env } from "./env.server"; import { requestUrl } from "./utils/requestUrl.server"; export type TriggerFeatures = { isManagedCloud: boolean; - v3Enabled: boolean; }; function isManagedCloud(host: string): boolean { @@ -18,7 +16,6 @@ function isManagedCloud(host: string): boolean { function featuresForHost(host: string): TriggerFeatures { return { isManagedCloud: isManagedCloud(host), - v3Enabled: env.V3_ENABLED === "true", }; } diff --git a/apps/webapp/app/hooks/useFeatures.ts b/apps/webapp/app/hooks/useFeatures.ts index e05d9ed7eb..d2e1ac699f 100644 --- a/apps/webapp/app/hooks/useFeatures.ts +++ b/apps/webapp/app/hooks/useFeatures.ts @@ -5,5 +5,5 @@ import type { TriggerFeatures } from "~/features.server"; export function useFeatures(): TriggerFeatures { const routeMatch = useTypedRouteLoaderData("root"); - return routeMatch?.features ?? { isManagedCloud: false, v3Enabled: false }; + return routeMatch?.features ?? { isManagedCloud: false }; } diff --git a/apps/webapp/app/models/admin.server.ts b/apps/webapp/app/models/admin.server.ts index c49c709928..434bcf8ffe 100644 --- a/apps/webapp/app/models/admin.server.ts +++ b/apps/webapp/app/models/admin.server.ts @@ -206,27 +206,6 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se }; } -export async function setV3Enabled(userId: string, id: string, v3Enabled: boolean) { - const user = await prisma.user.findUnique({ - where: { - id: userId, - }, - }); - - if (user?.admin !== true) { - throw new Error("Unauthorized"); - } - - return prisma.organization.update({ - where: { - id, - }, - data: { - v3Enabled, - }, - }); -} - export async function redirectWithImpersonation(request: Request, userId: string, path: string) { const user = await requireUser(request); if (!user.admin) { diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index 61dc99ecd7..9309e66179 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -66,7 +66,7 @@ export async function createOrganization( role: "ADMIN", }, }, - v3Enabled: features.v3Enabled && !features.isManagedCloud, + v3Enabled: !features.isManagedCloud, }, include: { members: true, diff --git a/apps/webapp/app/models/project.server.ts b/apps/webapp/app/models/project.server.ts index 3e4dc2cf8d..10a2f0c02a 100644 --- a/apps/webapp/app/models/project.server.ts +++ b/apps/webapp/app/models/project.server.ts @@ -38,10 +38,6 @@ export async function createProject( if (!organization.v3Enabled) { throw new Error(`Organization can't create v3 projects.`); } - - if (!env.V3_ENABLED) { - throw new Error(`v3 is not available yet.`); - } } //ensure the slug is globally unique diff --git a/apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts index 6dae2995f4..52c93bc6f0 100644 --- a/apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts @@ -117,12 +117,21 @@ export class ApiRetrieveRunPresenter extends BasePresenter { payloadPacket.dataType === "application/store" && typeof payloadPacket.data === "string" ) { - $payloadPresignedUrl = await generatePresignedUrl( + const signed = await generatePresignedUrl( env.project.externalRef, env.slug, payloadPacket.data, "GET" ); + + if (signed.success) { + $payloadPresignedUrl = signed.url; + } else { + logger.error(`Failed to generate presigned URL for payload: ${signed.error}`, { + taskRunId: taskRun.id, + payload: payloadPacket.data, + }); + } } else { $payload = await parsePacket(payloadPacket); } @@ -137,12 +146,21 @@ export class ApiRetrieveRunPresenter extends BasePresenter { outputPacket.dataType === "application/store" && typeof outputPacket.data === "string" ) { - $outputPresignedUrl = await generatePresignedUrl( + const signed = await generatePresignedUrl( env.project.externalRef, env.slug, outputPacket.data, "GET" ); + + if (signed.success) { + $outputPresignedUrl = signed.url; + } else { + logger.error(`Failed to generate presigned URL for output: ${signed.error}`, { + taskRunId: taskRun.id, + output: outputPacket.data, + }); + } } else { $output = await parsePacket(outputPacket); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index 6793f3417c..3cd51e2641 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -17,7 +17,6 @@ import { FormTitle } from "~/components/primitives/FormTitle"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; -import { Select, SelectItem } from "~/components/primitives/Select"; import { ButtonSpinner } from "~/components/primitives/Spinner"; import { prisma } from "~/db.server"; import { featuresForRequest } from "~/features.server"; @@ -61,8 +60,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } //if you don't have v3 access, you must select a plan - const { isManagedCloud, v3Enabled } = featuresForRequest(request); - if (isManagedCloud && v3Enabled && !organization.v3Enabled) { + const { isManagedCloud } = featuresForRequest(request); + if (isManagedCloud && !organization.v3Enabled) { return redirect(selectPlanPath({ slug: organizationSlug })); } @@ -123,10 +122,8 @@ export const action: ActionFunction = async ({ request, params }) => { export default function Page() { const { organization, message } = useTypedLoaderData(); const lastSubmission = useActionData(); - const { v3Enabled, isManagedCloud } = useFeatures(); - const canCreateV3Projects = organization.v3Enabled && v3Enabled; - const canCreateV2Projects = organization.v2Enabled || !isManagedCloud; + const canCreateV3Projects = organization.v3Enabled; const [form, { projectName, projectVersion }] = useForm({ id: "create-project", @@ -165,30 +162,7 @@ export default function Page() { /> {projectName.error} - {canCreateV2Projects && canCreateV3Projects ? ( - - - - {projectVersion.error} - - ) : canCreateV3Projects ? ( + {canCreateV3Projects ? ( ) : ( diff --git a/apps/webapp/app/routes/api.v1.packets.$.ts b/apps/webapp/app/routes/api.v1.packets.$.ts index d6fd54a011..4632df0918 100644 --- a/apps/webapp/app/routes/api.v1.packets.$.ts +++ b/apps/webapp/app/routes/api.v1.packets.$.ts @@ -25,19 +25,19 @@ export async function action({ request, params }: ActionFunctionArgs) { const parsedParams = ParamsSchema.parse(params); const filename = parsedParams["*"]; - const presignedUrl = await generatePresignedUrl( + const signed = await generatePresignedUrl( authenticationResult.environment.project.externalRef, authenticationResult.environment.slug, filename, "PUT" ); - if (!presignedUrl) { - return json({ error: "Failed to generate presigned URL" }, { status: 500 }); + if (!signed.success) { + return json({ error: `Failed to generate presigned URL: ${signed.error}` }, { status: 500 }); } // Caller can now use this URL to upload to that object. - return json({ presignedUrl }); + return json({ presignedUrl: signed.url }); } export const loader = createLoaderApiRoute( @@ -50,18 +50,18 @@ export const loader = createLoaderApiRoute( async ({ params, authentication }) => { const filename = params["*"]; - const presignedUrl = await generatePresignedUrl( + const signed = await generatePresignedUrl( authentication.environment.project.externalRef, authentication.environment.slug, filename, "GET" ); - if (!presignedUrl) { - return json({ error: "Failed to generate presigned URL" }, { status: 500 }); + if (!signed.success) { + return json({ error: `Failed to generate presigned URL: ${signed.error}` }, { status: 500 }); } // Caller can now use this URL to fetch that object. - return json({ presignedUrl }); + return json({ presignedUrl: signed.url }); } ); diff --git a/apps/webapp/app/routes/engine.v1.dev.runs.$runFriendlyId.snapshots.$snapshotFriendlyId.attempts.start.ts b/apps/webapp/app/routes/engine.v1.dev.runs.$runFriendlyId.snapshots.$snapshotFriendlyId.attempts.start.ts index de214064a8..65d729d31d 100644 --- a/apps/webapp/app/routes/engine.v1.dev.runs.$runFriendlyId.snapshots.$snapshotFriendlyId.attempts.start.ts +++ b/apps/webapp/app/routes/engine.v1.dev.runs.$runFriendlyId.snapshots.$snapshotFriendlyId.attempts.start.ts @@ -6,7 +6,7 @@ import { WorkerApiRunAttemptStartResponseBody, } from "@trigger.dev/core/v3/workers"; import { RuntimeEnvironment } from "@trigger.dev/database"; -import { defaultMachine } from "@trigger.dev/platform/v3"; +import { defaultMachine } from "~/services/platform.v3.server"; import { z } from "zod"; import { prisma } from "~/db.server"; import { generateJWTTokenForEnvironment } from "~/services/apiAuth.server"; diff --git a/apps/webapp/app/routes/resources.packets.$environmentId.$.ts b/apps/webapp/app/routes/resources.packets.$environmentId.$.ts index 41f335497d..2c3e280a7e 100644 --- a/apps/webapp/app/routes/resources.packets.$environmentId.$.ts +++ b/apps/webapp/app/routes/resources.packets.$environmentId.$.ts @@ -41,12 +41,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "GET" ); - if (!signed) { - return new Response("Failed to generate presigned URL", { status: 500 }); + if (!signed.success) { + return new Response(`Failed to generate presigned URL: ${signed.error}`, { status: 500 }); } - const response = await fetch(signed.url, { - headers: signed.headers, + const response = await fetch(signed.request.url, { + headers: signed.request.headers, }); return new Response(response.body, { diff --git a/apps/webapp/app/services/platform.v3.server.ts b/apps/webapp/app/services/platform.v3.server.ts index f12270c1e6..f42f719188 100644 --- a/apps/webapp/app/services/platform.v3.server.ts +++ b/apps/webapp/app/services/platform.v3.server.ts @@ -1,10 +1,13 @@ -import { Organization, Project } from "@trigger.dev/database"; +import type { Organization, Project } from "@trigger.dev/database"; import { BillingClient, - Limits, - SetPlanBody, - UsageSeriesParams, - UsageResult, + type Limits, + type SetPlanBody, + type UsageSeriesParams, + type UsageResult, + defaultMachine as defaultMachineFromPlatform, + machines as machinesFromPlatform, + type MachineCode, } from "@trigger.dev/platform/v3"; import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { MemoryStore } from "@unkey/cache/stores"; @@ -17,6 +20,9 @@ import { logger } from "~/services/logger.server"; import { newProjectPath, organizationBillingPath } from "~/utils/pathBuilder"; import { singleton } from "~/utils/singleton"; import { RedisCacheStore } from "./unkey/redisCacheStore.server"; +import { existsSync, readFileSync } from "node:fs"; +import { z } from "zod"; +import { MachinePresetName } from "@trigger.dev/core/v3"; function initializeClient() { if (isCloud() && process.env.BILLING_API_URL && process.env.BILLING_API_KEY) { @@ -67,6 +73,111 @@ function initializePlatformCache() { const platformCache = singleton("platformCache", initializePlatformCache); +type Machines = typeof machinesFromPlatform; + +const MachineOverrideValues = z.object({ + cpu: z.number(), + memory: z.number(), +}); +type MachineOverrideValues = z.infer; + +const MachineOverrides = z.record(MachinePresetName, MachineOverrideValues.partial()); +type MachineOverrides = z.infer; + +const MachinePresetOverrides = z.object({ + defaultMachine: MachinePresetName.optional(), + machines: MachineOverrides.optional(), +}); + +function initializeMachinePresets(): { + defaultMachine: MachineCode; + machines: Machines; +} { + const overrides = getMachinePresetOverrides(); + + if (!overrides) { + return { + defaultMachine: defaultMachineFromPlatform, + machines: machinesFromPlatform, + }; + } + + return { + defaultMachine: overrideDefaultMachine(defaultMachineFromPlatform, overrides.defaultMachine), + machines: overrideMachines(machinesFromPlatform, overrides.machines), + }; +} + +export const { defaultMachine, machines } = singleton("machinePresets", initializeMachinePresets); + +function overrideDefaultMachine(defaultMachine: MachineCode, override?: MachineCode): MachineCode { + if (!override) { + return defaultMachine; + } + + return override; +} + +function overrideMachines(machines: Machines, overrides?: MachineOverrides): Machines { + if (!overrides) { + return machines; + } + + const mergedMachines = { + ...machines, + }; + + for (const machine of Object.keys(overrides) as MachinePresetName[]) { + mergedMachines[machine] = { + ...mergedMachines[machine], + ...overrides[machine], + }; + } + + return mergedMachines; +} + +function getMachinePresetOverrides() { + const path = env.MACHINE_PRESETS_OVERRIDE_PATH; + if (!path) { + return; + } + + const overrides = safeReadMachinePresetOverrides(path); + if (!overrides) { + return; + } + + const parsed = MachinePresetOverrides.safeParse(overrides); + + if (!parsed.success) { + logger.error("Error parsing machine preset overrides", { path, error: parsed.error }); + return; + } + + return parsed.data; +} + +function safeReadMachinePresetOverrides(path: string) { + try { + const fileExists = existsSync(path); + if (!fileExists) { + logger.error("Machine preset overrides file does not exist", { path }); + return; + } + + const fileContents = readFileSync(path, "utf8"); + + return JSON.parse(fileContents); + } catch (error) { + logger.error("Error reading machine preset overrides", { + path, + error: error instanceof Error ? error.message : String(error), + }); + return; + } +} + export async function getCurrentPlan(orgId: string) { if (!client) return undefined; diff --git a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts index 4462a7e15d..66c1ddd9d1 100644 --- a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts +++ b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts @@ -860,7 +860,7 @@ async function resolveBuiltInDevVariables(runtimeEnvironment: RuntimeEnvironment let result: Array = [ { key: "OTEL_EXPORTER_OTLP_ENDPOINT", - value: env.DEV_OTEL_EXPORTER_OTLP_ENDPOINT ?? env.APP_ORIGIN, + value: env.DEV_OTEL_EXPORTER_OTLP_ENDPOINT ?? `${env.APP_ORIGIN.replace(/\/$/, "")}/otel`, }, { key: "TRIGGER_API_URL", diff --git a/apps/webapp/app/v3/machinePresets.server.ts b/apps/webapp/app/v3/machinePresets.server.ts index 84aff460d8..024cb9f114 100644 --- a/apps/webapp/app/v3/machinePresets.server.ts +++ b/apps/webapp/app/v3/machinePresets.server.ts @@ -1,5 +1,5 @@ import { MachineConfig, MachinePreset, MachinePresetName } from "@trigger.dev/core/v3"; -import { defaultMachine, machines } from "@trigger.dev/platform/v3"; +import { defaultMachine, machines } from "~/services/platform.v3.server"; import { logger } from "~/services/logger.server"; export function machinePresetFromConfig(config: unknown): MachinePreset { diff --git a/apps/webapp/app/v3/r2.server.ts b/apps/webapp/app/v3/r2.server.ts index debf18a83e..6717009407 100644 --- a/apps/webapp/app/v3/r2.server.ts +++ b/apps/webapp/app/v3/r2.server.ts @@ -16,6 +16,10 @@ function initializeR2() { return new AwsClient({ accessKeyId: env.OBJECT_STORE_ACCESS_KEY_ID, secretAccessKey: env.OBJECT_STORE_SECRET_ACCESS_KEY, + region: env.OBJECT_STORE_REGION, + // We now set the default value to "s3" in the schema to enhance interoperability with various S3-compatible services. + // Setting this env var to an empty string will restore the previous behavior of not setting a service. + service: env.OBJECT_STORE_SERVICE ? env.OBJECT_STORE_SERVICE : undefined, }); } @@ -152,13 +156,28 @@ export async function generatePresignedRequest( envSlug: string, filename: string, method: "PUT" | "GET" = "PUT" -) { +): Promise< + | { + success: false; + error: string; + } + | { + success: true; + request: Request; + } +> { if (!env.OBJECT_STORE_BASE_URL) { - return; + return { + success: false, + error: "Object store base URL is not set", + }; } if (!r2) { - return; + return { + success: false, + error: "Object store client is not initialized", + }; } const url = new URL(env.OBJECT_STORE_BASE_URL); @@ -182,7 +201,10 @@ export async function generatePresignedRequest( filename, }); - return signed; + return { + success: true, + request: signed, + }; } export async function generatePresignedUrl( @@ -190,8 +212,29 @@ export async function generatePresignedUrl( envSlug: string, filename: string, method: "PUT" | "GET" = "PUT" -) { +): Promise< + | { + success: false; + error: string; + } + | { + success: true; + url: string; + } +> { const signed = await generatePresignedRequest(projectRef, envSlug, filename, method); - return signed?.url; + if (!signed.success) { + return { + success: false, + error: signed.error, + }; + } + + signed; + + return { + success: true, + url: signed.request.url, + }; } diff --git a/apps/webapp/app/v3/runEngine.server.ts b/apps/webapp/app/v3/runEngine.server.ts index bd619391e1..ef2fbc8ae4 100644 --- a/apps/webapp/app/v3/runEngine.server.ts +++ b/apps/webapp/app/v3/runEngine.server.ts @@ -1,5 +1,5 @@ import { RunEngine } from "@internal/run-engine"; -import { defaultMachine } from "@trigger.dev/platform/v3"; +import { defaultMachine } from "~/services/platform.v3.server"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; import { singleton } from "~/utils/singleton"; diff --git a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts index ecdb49724e..e225bfb37a 100644 --- a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts +++ b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts @@ -29,7 +29,7 @@ import { fromFriendlyId, } from "@trigger.dev/core/v3/isomorphic"; import { machinePresetFromName } from "~/v3/machinePresets.server"; -import { defaultMachine } from "@trigger.dev/platform/v3"; +import { defaultMachine } from "~/services/platform.v3.server"; export class WorkerGroupTokenService extends WithRunEngine { private readonly tokenPrefix = "tr_wgt_"; diff --git a/apps/webapp/test/engine/triggerTask.test.ts b/apps/webapp/test/engine/triggerTask.test.ts index 7b1804578d..2210e7992a 100644 --- a/apps/webapp/test/engine/triggerTask.test.ts +++ b/apps/webapp/test/engine/triggerTask.test.ts @@ -5,9 +5,13 @@ vi.mock("~/db.server", () => ({ prisma: {}, })); -vi.mock("~/services/platform.v3.server", () => ({ - getEntitlement: vi.fn(), -})); +vi.mock("~/services/platform.v3.server", async (importOriginal) => { + const actual = (await importOriginal()) as Record; + return { + ...actual, + getEntitlement: vi.fn(), + }; +}); import { RunEngine } from "@internal/run-engine"; import { setupAuthenticatedEnvironment, setupBackgroundWorker } from "@internal/run-engine/tests"; diff --git a/docker/Dockerfile b/docker/Dockerfile index 86a77b5a2f..521062aaf8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,7 +26,7 @@ WORKDIR /triggerdotdev # Corepack is used to install pnpm RUN corepack enable ENV NODE_ENV development -RUN pnpm install --ignore-scripts --no-frozen-lockfile +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --ignore-scripts --no-frozen-lockfile ## Production deps FROM base AS production-deps @@ -34,11 +34,11 @@ WORKDIR /triggerdotdev # Corepack is used to install pnpm RUN corepack enable ENV NODE_ENV production -RUN pnpm install --prod --no-frozen-lockfile +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --prod --no-frozen-lockfile COPY --from=pruner --chown=node:node /triggerdotdev/internal-packages/database/prisma/schema.prisma /triggerdotdev/internal-packages/database/prisma/schema.prisma # RUN pnpm add @prisma/client@5.1.1 -w ENV NPM_CONFIG_IGNORE_WORKSPACE_ROOT_CHECK true -RUN pnpx prisma@5.4.1 generate --schema /triggerdotdev/internal-packages/database/prisma/schema.prisma +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpx prisma@5.4.1 generate --schema /triggerdotdev/internal-packages/database/prisma/schema.prisma ## Builder (builds the webapp) FROM base AS builder diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6fe0fca0bb..02b00de7b4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -3,7 +3,6 @@ version: "3" volumes: database-data: database-data-alt: - pgadmin-data: redis-data: clickhouse: diff --git a/docker/pgadmin/servers.json b/docker/pgadmin/servers.json deleted file mode 100644 index 83f7159bbf..0000000000 --- a/docker/pgadmin/servers.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Servers": { - "1": { - "Name": "Trigger.dev", - "Group": "Trigger.dev", - "Port": 5432, - "Username": "postgres", - "Host": "database", - "SSLMode": "prefer", - "MaintenanceDB": "postgres" - } - } -} diff --git a/packages/cli-v3/src/commands/switch.ts b/packages/cli-v3/src/commands/switch.ts index dc134a3656..5464458b07 100644 --- a/packages/cli-v3/src/commands/switch.ts +++ b/packages/cli-v3/src/commands/switch.ts @@ -84,7 +84,7 @@ export async function switchProfiles(profile: string | undefined, options: Switc } const profileSelection = await select({ - message: "Please select a new profile", + message: "Select a new default profile", initialValue: authConfig.currentProfile, options: profileNames.map((profile) => ({ value: profile, diff --git a/packages/core/src/v3/runEngineWorker/supervisor/session.ts b/packages/core/src/v3/runEngineWorker/supervisor/session.ts index 91c829d437..a458a9cd4c 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/session.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/session.ts @@ -168,7 +168,7 @@ export class SupervisorSession extends EventEmitter { if (!connect.success) { this.logger.error("Failed to connect", { error: connect.error }); - throw new Error("[SupervisorSession]Failed to connect"); + throw new Error("[SupervisorSession] Failed to connect"); } const { workerGroup } = connect.data; diff --git a/references/bun-catalog/README.md b/references/bun-catalog/README.md index b3e0768da1..42401b1f41 100644 --- a/references/bun-catalog/README.md +++ b/references/bun-catalog/README.md @@ -4,11 +4,9 @@ You can test v3 tasks from inside the app in this project. It's designed to be u ## One-time setup -1. In Postgres go to the "Organizations" table and on your org set the `v3Enabled` column to `true`. +1. Create a v3 project in the UI of the webapp, you should now be able to select it from the dropdown. -2. Create a v3 project in the UI of the webapp, you should now be able to select it from the dropdown. - -3. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`. +2. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`. This is so the `trigger.config.ts` file inside the v3-catalog doesn't keep getting changed by people accidentally pushing this. diff --git a/references/v3-catalog/README.md b/references/v3-catalog/README.md index b3e0768da1..42401b1f41 100644 --- a/references/v3-catalog/README.md +++ b/references/v3-catalog/README.md @@ -4,11 +4,9 @@ You can test v3 tasks from inside the app in this project. It's designed to be u ## One-time setup -1. In Postgres go to the "Organizations" table and on your org set the `v3Enabled` column to `true`. +1. Create a v3 project in the UI of the webapp, you should now be able to select it from the dropdown. -2. Create a v3 project in the UI of the webapp, you should now be able to select it from the dropdown. - -3. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`. +2. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`. This is so the `trigger.config.ts` file inside the v3-catalog doesn't keep getting changed by people accidentally pushing this.