-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(webapp,cli): let self-hosted instances require deploy base images #5005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@trigger.dev/core": patch | ||
| "trigger.dev": patch | ||
| --- | ||
|
|
||
| Self-hosted instances can require custom base images for deploys, such as FIPS-validated or hardened Node images, with the new `DEPLOY_BASE_IMAGES` webapp setting. The CLI builds on the base images the instance specifies. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| type BaseImages = { base?: string; buildBase?: string }; | ||
|
|
||
| /** Base images the operator requires for a runtime, from `runtime=image` csv env vars. */ | ||
| export function resolveDeployBaseImages( | ||
| runtime: string | null | undefined, | ||
| config: { base?: string; buildBase?: string } | ||
| ): BaseImages | undefined { | ||
| if (!runtime) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const base = parseImageMap(config.base)[runtime]; | ||
| const buildBase = parseImageMap(config.buildBase)[runtime]; | ||
|
|
||
| if (!base && !buildBase) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return { | ||
| ...(base ? { base } : {}), | ||
| ...(buildBase ? { buildBase } : {}), | ||
| }; | ||
| } | ||
|
|
||
| function parseImageMap(value: string | undefined): Record<string, string> { | ||
| if (!value) { | ||
| return {}; | ||
| } | ||
|
|
||
| return Object.fromEntries( | ||
| value | ||
| .split(",") | ||
| .map((entry) => entry.trim()) | ||
| .filter(Boolean) | ||
| .flatMap((entry) => { | ||
| const separator = entry.indexOf("="); | ||
| if (separator <= 0) { | ||
| return []; | ||
| } | ||
| const runtime = entry.slice(0, separator).trim(); | ||
| const image = entry.slice(separator + 1).trim(); | ||
| return image ? [[runtime, image] as const] : []; | ||
| }) | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { resolveDeployBaseImages } from "~/v3/deployBaseImages.server"; | ||
|
|
||
| describe("resolveDeployBaseImages", () => { | ||
| it("returns undefined when nothing is configured", () => { | ||
| expect(resolveDeployBaseImages("node-26", {})).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("returns the images configured for the runtime", () => { | ||
| expect( | ||
| resolveDeployBaseImages("node-26", { | ||
| base: "node-24=acme/node-fips:24@sha256:aaa, node-26=acme/node-fips:26@sha256:bbb", | ||
| buildBase: "node-26=acme/node:26-dev@sha256:ccc", | ||
| }) | ||
| ).toEqual({ base: "acme/node-fips:26@sha256:bbb", buildBase: "acme/node:26-dev@sha256:ccc" }); | ||
| }); | ||
|
|
||
| it("returns undefined for runtimes without an entry", () => { | ||
| expect(resolveDeployBaseImages("bun", { base: "node-26=acme/node-fips:26" })).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("returns undefined when the deployment has no runtime", () => { | ||
| expect(resolveDeployBaseImages(null, { base: "node-26=acme/node-fips:26" })).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("skips malformed entries", () => { | ||
| expect( | ||
| resolveDeployBaseImages("node-26", { base: "garbage,=nope,node-26=,node-26=acme/node:26" }) | ||
| ).toEqual({ base: "acme/node:26" }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ import { x } from "tinyexec"; | |
| import { z } from "zod"; | ||
| import chalk from "chalk"; | ||
| import type { CliApiClient } from "../apiClient.js"; | ||
| import { buildWorker } from "../build/buildWorker.js"; | ||
| import { buildWorker, writeContainerfile } from "../build/buildWorker.js"; | ||
| import { resolveAlwaysExternal } from "../build/externals.js"; | ||
| import { createContextArchive, getArchiveSize } from "../deploy/archiveContext.js"; | ||
| import { createBundleArchive } from "../deploy/bundleArchive.js"; | ||
|
|
@@ -638,6 +638,15 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { | |
|
|
||
| warnAboutCanceledDeployments(deployment.canceledDeployments, options.externalId); | ||
|
|
||
| if (deployment.baseImages) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Apply the image override to fresh bundle deploys. If a deploy uses |
||
| logger.debug("Using base images required by the server", deployment.baseImages); | ||
|
|
||
| await writeContainerfile(destination.path, { | ||
| ...buildManifest, | ||
| image: { ...buildManifest.image, ...deployment.baseImages }, | ||
| }); | ||
| } | ||
|
|
||
| // When `externalBuildData` is not present the deployment implicitly goes into the local build path | ||
| // which is used in self-hosted setups. There are a few subtle differences between local builds for the cloud | ||
| // and local builds for self-hosted setups. We need to make the separation of the two paths clearer to avoid confusion. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -808,14 +808,14 @@ RUN apt-get update && \\ | |
| apt-get install -y --no-install-recommends ${TOOLCHAIN_PACKAGES} && \\ | ||
| apt-get clean && \\ | ||
| rm -rf /var/lib/apt/lists/*` | ||
| : `FROM ${BUILD_IMAGE[options.runtime]} AS build | ||
| : `FROM ${options.image?.buildBase ?? BUILD_IMAGE[options.runtime]} AS build | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Honor |
||
|
|
||
| ENV DEBIAN_FRONTEND=noninteractive${ | ||
| userPackages.length > 0 ? `\n\n${aptInstall(userPackages, { repair: false })}` : "" | ||
| }`; | ||
|
|
||
| return { | ||
| baseImage: BASE_IMAGE[options.runtime], | ||
| baseImage: options.image?.base ?? BASE_IMAGE[options.runtime], | ||
| buildStage, | ||
| customization, | ||
| buildArgs, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the requirements for a custom Bun base. A Bun image also needs
nodeonPATH: the generated final stage runsdumb-init node. It needs abunuser: the generated build and final stages specifyUSER bun. An image that provides only the documentedbunexecutable andnodeuser can fail to build or start. State the Node and Bun requirements separately. (docs.docker.com)