@alchemy.run/docker 2.0.0-beta.84 → 2.0.0-beta.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/lib/Bootstrap.d.ts.map +1 -1
- package/lib/Bootstrap.js +2 -1
- package/lib/Bootstrap.js.map +1 -1
- package/lib/BuildCache.d.ts +87 -0
- package/lib/BuildCache.d.ts.map +1 -0
- package/lib/BuildCache.js +39 -0
- package/lib/BuildCache.js.map +1 -0
- package/lib/BuildHash.d.ts +20 -3
- package/lib/BuildHash.d.ts.map +1 -1
- package/lib/BuildHash.js +60 -21
- package/lib/BuildHash.js.map +1 -1
- package/lib/BuildPlan.d.ts +202 -0
- package/lib/BuildPlan.d.ts.map +1 -0
- package/lib/BuildPlan.js +514 -0
- package/lib/BuildPlan.js.map +1 -0
- package/lib/Container.d.ts +122 -1
- package/lib/Container.d.ts.map +1 -1
- package/lib/Container.js +232 -4
- package/lib/Container.js.map +1 -1
- package/lib/Context.d.ts +20 -1
- package/lib/Context.d.ts.map +1 -1
- package/lib/Context.js +15 -0
- package/lib/Context.js.map +1 -1
- package/lib/Docker.d.ts +113 -1
- package/lib/Docker.d.ts.map +1 -1
- package/lib/Docker.js +50 -8
- package/lib/Docker.js.map +1 -1
- package/lib/Dockerfile.d.ts +0 -2
- package/lib/Dockerfile.d.ts.map +1 -1
- package/lib/Dockerfile.js.map +1 -1
- package/lib/DockerfileParser.d.ts +125 -0
- package/lib/DockerfileParser.d.ts.map +1 -0
- package/lib/DockerfileParser.js +1040 -0
- package/lib/DockerfileParser.js.map +1 -0
- package/lib/HealthcheckCommand.d.ts.map +1 -1
- package/lib/HealthcheckCommand.js.map +1 -1
- package/lib/Image.d.ts +69 -13
- package/lib/Image.d.ts.map +1 -1
- package/lib/Image.js +59 -9
- package/lib/Image.js.map +1 -1
- package/lib/ImageArchive.d.ts +41 -0
- package/lib/ImageArchive.d.ts.map +1 -0
- package/lib/ImageArchive.js +54 -0
- package/lib/ImageArchive.js.map +1 -0
- package/lib/ImageSource.d.ts +151 -8
- package/lib/ImageSource.d.ts.map +1 -1
- package/lib/ImageSource.js +247 -72
- package/lib/ImageSource.js.map +1 -1
- package/lib/Network.d.ts +175 -3
- package/lib/Network.d.ts.map +1 -1
- package/lib/Network.js +296 -8
- package/lib/Network.js.map +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Registry.d.ts.map +1 -1
- package/lib/Registry.js.map +1 -1
- package/lib/RegistryError.d.ts.map +1 -1
- package/lib/RegistryError.js.map +1 -1
- package/lib/RemoteImage.d.ts.map +1 -1
- package/lib/RemoteImage.js +1 -1
- package/lib/RemoteImage.js.map +1 -1
- package/lib/Service.d.ts +24 -3
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js +154 -12
- package/lib/Service.js.map +1 -1
- package/lib/ServiceImage.d.ts.map +1 -1
- package/lib/ServiceImage.js.map +1 -1
- package/lib/ShellWord.d.ts +28 -0
- package/lib/ShellWord.d.ts.map +1 -0
- package/lib/ShellWord.js +301 -0
- package/lib/ShellWord.js.map +1 -0
- package/lib/Swarm.d.ts.map +1 -1
- package/lib/Swarm.js.map +1 -1
- package/lib/Volume.d.ts +24 -1
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +16 -0
- package/lib/Volume.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +9 -9
- package/src/Bootstrap.ts +2 -0
- package/src/BuildCache.ts +111 -0
- package/src/BuildHash.ts +85 -26
- package/src/BuildPlan.ts +758 -0
- package/src/Container.ts +361 -5
- package/src/Context.ts +27 -1
- package/src/Docker.ts +157 -9
- package/src/Dockerfile.ts +0 -2
- package/src/DockerfileParser.ts +1152 -0
- package/src/Image.ts +75 -15
- package/src/ImageArchive.ts +77 -0
- package/src/ImageSource.ts +391 -99
- package/src/Network.ts +432 -10
- package/src/RemoteImage.ts +1 -1
- package/src/Service.ts +246 -62
- package/src/ShellWord.ts +326 -0
- package/src/Volume.ts +33 -1
package/src/ImageSource.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
2
2
|
import * as Bundle from "alchemy/Bundle/Bundle";
|
|
3
3
|
import { findCwdForBundle, getStableContextDir, resolveMainPath } from "alchemy/Bundle/TempRoot";
|
|
4
|
-
import { hashDirectory } from "alchemy/Command/Memo";
|
|
5
4
|
import { sha256Object } from "alchemy/Util/sha256";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as FileSystem from "effect/FileSystem";
|
|
8
8
|
import * as Path from "effect/Path";
|
|
9
|
+
import type * as Redacted from "effect/Redacted";
|
|
9
10
|
import * as Schedule from "effect/Schedule";
|
|
10
11
|
import type * as rolldown from "rolldown";
|
|
12
|
+
import { type BuildCacheOptions, imageBuildCacheOptions } from "./BuildCache.ts";
|
|
13
|
+
import { hashDockerBuildInputs } from "./BuildHash.ts";
|
|
11
14
|
import { Docker, type RegistryCredentials } from "./Docker.ts";
|
|
12
15
|
import { isInlineDockerfile, type InlineDockerfile } from "./Dockerfile.ts";
|
|
13
16
|
|
|
@@ -28,6 +31,9 @@ import { isInlineDockerfile, type InlineDockerfile } from "./Dockerfile.ts";
|
|
|
28
31
|
* ENTRYPOINT) and pushed as a derived image. `main` absent → the
|
|
29
32
|
* environment runs verbatim: `image` is mirrored (docker pull → tag →
|
|
30
33
|
* push, content-addressed on the ref), a Dockerfile builds as-is.
|
|
34
|
+
* - `main` absent and an {@link ImageAgent} requested → the environment
|
|
35
|
+
* still runs verbatim, and the agent is pushed beside it as an image of
|
|
36
|
+
* its own, for the platform to mount into the environment's container.
|
|
31
37
|
*
|
|
32
38
|
* Where images land is an {@link ImageRegistryTarget}: ECR supplies one that
|
|
33
39
|
* auto-creates a private repository; a plain registry (a local `registry`
|
|
@@ -46,6 +52,13 @@ export interface BundledImageSource {
|
|
|
46
52
|
* `import.meta.url` from an inline Effect program.
|
|
47
53
|
*/
|
|
48
54
|
main: string;
|
|
55
|
+
/**
|
|
56
|
+
* @internal Dockerfile lines contributed by mount bindings (see
|
|
57
|
+
* `alchemy/Mount`), inserted after the environment preamble so a
|
|
58
|
+
* program change doesn't reinstall them. Hosts fold these in from their
|
|
59
|
+
* bindings; users don't set them.
|
|
60
|
+
*/
|
|
61
|
+
install?: ReadonlyArray<string>;
|
|
49
62
|
/**
|
|
50
63
|
* Environment image: used as the generated Dockerfile's `FROM`. Any
|
|
51
64
|
* registry ref works (private non-ECR registries require docker
|
|
@@ -77,16 +90,47 @@ export interface BundledImageSource {
|
|
|
77
90
|
* Bundler configuration for the entrypoint. Unused code is tree-shaken.
|
|
78
91
|
* `effect`, alchemy, and `@distilled.cloud` are marked pure so unused
|
|
79
92
|
* parts prune more aggressively. List extra packages with
|
|
80
|
-
* `pure.packages`, or disable with `pure: false`.
|
|
93
|
+
* `pure.packages`, or disable with `pure: false`. `cache` configures the
|
|
94
|
+
* image build's BuildKit layer cache.
|
|
95
|
+
*/
|
|
96
|
+
build?: Bundle.BundleConfig & BuildCacheOptions;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build arguments, BuildKit secrets and the layer cache for an image built
|
|
101
|
+
* from the user's own Dockerfile (`context` / `dockerfile`).
|
|
102
|
+
*/
|
|
103
|
+
export interface DockerfileBuild extends BuildCacheOptions {
|
|
104
|
+
/**
|
|
105
|
+
* Docker build arguments (`--build-arg`), read by `ARG` instructions.
|
|
106
|
+
* They are part of the image's content hash, so changing one rebuilds the
|
|
107
|
+
* image. Never pass secrets here: build args are visible in the image
|
|
108
|
+
* history. Use {@link secrets} instead.
|
|
81
109
|
*/
|
|
82
|
-
|
|
110
|
+
args?: Record<string, string>;
|
|
111
|
+
/**
|
|
112
|
+
* BuildKit secrets, keyed by id. Mount one in a `RUN` step with
|
|
113
|
+
* `--mount=type=secret,id=<id>` and read it from `/run/secrets/<id>`, or
|
|
114
|
+
* add `env=<NAME>` to expose it as an environment variable for that step.
|
|
115
|
+
*
|
|
116
|
+
* A secret never enters the image, its build history, or `docker`'s argv.
|
|
117
|
+
* Secrets are left out of the image's content hash, so rotating one does
|
|
118
|
+
* not rebuild the image.
|
|
119
|
+
*/
|
|
120
|
+
secrets?: Record<string, Redacted.Redacted<string>>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The `build` prop of a source built from the user's own Dockerfile. */
|
|
124
|
+
export interface DockerfileBuildOptions {
|
|
125
|
+
/** Build arguments, BuildKit secrets and the layer cache for the Dockerfile build. */
|
|
126
|
+
build?: DockerfileBuild;
|
|
83
127
|
}
|
|
84
128
|
|
|
85
129
|
/**
|
|
86
130
|
* Build the image from the user's own Dockerfile and build context — no
|
|
87
131
|
* Effect program is bundled.
|
|
88
132
|
*/
|
|
89
|
-
export interface DockerfileImageSource {
|
|
133
|
+
export interface DockerfileImageSource extends DockerfileBuildOptions {
|
|
90
134
|
/**
|
|
91
135
|
* Docker build context directory. Optional when {@link dockerfile} is
|
|
92
136
|
* inline content (which builds with an empty context).
|
|
@@ -123,10 +167,13 @@ export type ImageSourceProps = BundledImageSource | DockerfileImageSource | Regi
|
|
|
123
167
|
export interface ImageSourceLike {
|
|
124
168
|
main?: string;
|
|
125
169
|
handler?: string;
|
|
126
|
-
build
|
|
170
|
+
/** Bundler config for `main`; Docker build args and secrets for `context`. */
|
|
171
|
+
build?: Bundle.BundleConfig & DockerfileBuild;
|
|
127
172
|
context?: string;
|
|
128
173
|
dockerfile?: string | InlineDockerfile;
|
|
129
174
|
image?: string;
|
|
175
|
+
/** @internal Mount-binding install lines (see {@link BundledImageSource.install}). */
|
|
176
|
+
install?: ReadonlyArray<string>;
|
|
130
177
|
}
|
|
131
178
|
|
|
132
179
|
export type ImageSourceKind = "main" | "context" | "image";
|
|
@@ -146,9 +193,48 @@ export const imageSourceKind = (source: ImageSourceLike): ImageSourceKind | unde
|
|
|
146
193
|
? "context"
|
|
147
194
|
: undefined;
|
|
148
195
|
|
|
196
|
+
/**
|
|
197
|
+
* `build.args` or `build.secrets` set on an image that is not built from the
|
|
198
|
+
* user's own Dockerfile (a `main` program or a pre-built `image`), or
|
|
199
|
+
* `build.cache` set on a pre-built `image`, which is never built.
|
|
200
|
+
*/
|
|
201
|
+
export class DockerfileBuildConflict extends Data.TaggedError("DockerfileBuildConflict")<{
|
|
202
|
+
readonly id: string;
|
|
203
|
+
readonly source: "main" | "image";
|
|
204
|
+
readonly option?: "cache";
|
|
205
|
+
}> {
|
|
206
|
+
override get message() {
|
|
207
|
+
return this.option === "cache"
|
|
208
|
+
? `'${this.id}': 'build.cache' applies to an image Alchemy builds ('main' / 'context' / 'dockerfile'), not to a pre-built 'image'`
|
|
209
|
+
: `'${this.id}': 'build.args' and 'build.secrets' apply to an image built from your own Dockerfile ('context' / 'dockerfile'), not to '${this.source}'`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Dies with {@link DockerfileBuildConflict} when `build.args` or
|
|
215
|
+
* `build.secrets` are set alongside `main` or `image`, or `build.cache`
|
|
216
|
+
* alongside `image`.
|
|
217
|
+
*/
|
|
218
|
+
export const validateDockerfileBuild = (
|
|
219
|
+
id: string,
|
|
220
|
+
source: { main?: string; image?: string; build?: DockerfileBuild },
|
|
221
|
+
): Effect.Effect<void> =>
|
|
222
|
+
(source.main !== undefined || source.image !== undefined) &&
|
|
223
|
+
(source.build?.args !== undefined || source.build?.secrets !== undefined)
|
|
224
|
+
? Effect.die(
|
|
225
|
+
new DockerfileBuildConflict({
|
|
226
|
+
id,
|
|
227
|
+
source: source.main !== undefined ? "main" : "image",
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
: source.main === undefined && source.image !== undefined && source.build?.cache
|
|
231
|
+
? Effect.die(new DockerfileBuildConflict({ id, source: "image", option: "cache" }))
|
|
232
|
+
: Effect.void;
|
|
233
|
+
|
|
149
234
|
/**
|
|
150
235
|
* Validate environment-source exclusivity. Dies (plan-time defect) on
|
|
151
|
-
* `image`+`dockerfile`, `image`+`context`,
|
|
236
|
+
* `image`+`dockerfile`, `image`+`context`, inline-`dockerfile`+`context`, or
|
|
237
|
+
* build args/secrets without a Dockerfile build.
|
|
152
238
|
*/
|
|
153
239
|
export const validateImageSource = (id: string, source: ImageSourceLike): Effect.Effect<void> => {
|
|
154
240
|
if (source.image !== undefined && source.dockerfile !== undefined) {
|
|
@@ -176,7 +262,7 @@ export const validateImageSource = (id: string, source: ImageSourceLike): Effect
|
|
|
176
262
|
),
|
|
177
263
|
);
|
|
178
264
|
}
|
|
179
|
-
return
|
|
265
|
+
return validateDockerfileBuild(id, source);
|
|
180
266
|
};
|
|
181
267
|
|
|
182
268
|
/**
|
|
@@ -227,10 +313,95 @@ export interface ResolveContainerImageOptions {
|
|
|
227
313
|
* generated entry module source.
|
|
228
314
|
*/
|
|
229
315
|
bootstrap: (importPath: string) => string;
|
|
316
|
+
/**
|
|
317
|
+
* Publish an agent beside an image that has no `main`. See
|
|
318
|
+
* {@link ImageAgent}. Ignored for a `main` source, whose program serves the
|
|
319
|
+
* agent itself.
|
|
320
|
+
*/
|
|
321
|
+
agent?: ImageAgent;
|
|
230
322
|
/** Plan-status session used to emit build/push progress notes. */
|
|
231
323
|
session: { note: (message: string) => Effect.Effect<void> };
|
|
232
324
|
}
|
|
233
325
|
|
|
326
|
+
/**
|
|
327
|
+
* A program the platform runs inside a container whose image has none of its
|
|
328
|
+
* own (`image` or `context`), so it can drive the container: a sandbox agent
|
|
329
|
+
* that runs commands and serves files inside it.
|
|
330
|
+
*
|
|
331
|
+
* The image itself is never changed. The agent is bundled and pushed as an
|
|
332
|
+
* image of its own, to the same repository, under an `agent-` tag. Run as a
|
|
333
|
+
* container, that image copies the agent and its Bun runtime into
|
|
334
|
+
* {@link IMAGE_AGENT_DIR} and exits. A platform mounts a volume there, shares
|
|
335
|
+
* it with the image's container, and starts that container with
|
|
336
|
+
* {@link imageAgentEntryPoint}.
|
|
337
|
+
*/
|
|
338
|
+
export interface ImageAgent {
|
|
339
|
+
/**
|
|
340
|
+
* Module that exports `bootstrap(): Promise<void>`, which runs the agent.
|
|
341
|
+
* A path or a `file:` URL.
|
|
342
|
+
*/
|
|
343
|
+
main: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Where an {@link ImageAgent}'s image copies the agent and its runtime, and
|
|
348
|
+
* where the image's container finds them.
|
|
349
|
+
*/
|
|
350
|
+
export const IMAGE_AGENT_DIR = "/opt/alchemy/sandbox";
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The entry point that starts an {@link ImageAgent} from
|
|
354
|
+
* {@link IMAGE_AGENT_DIR}. Bun starts through the loader that came with it,
|
|
355
|
+
* so the image's own libc (glibc, another musl, or none) is never consulted
|
|
356
|
+
* and the image needs nothing installed. The container's command arrives as
|
|
357
|
+
* the agent's arguments.
|
|
358
|
+
*/
|
|
359
|
+
export const imageAgentEntryPoint: ReadonlyArray<string> = [
|
|
360
|
+
`${IMAGE_AGENT_DIR}/ld-musl.so.1`,
|
|
361
|
+
"--library-path",
|
|
362
|
+
`${IMAGE_AGENT_DIR}/lib`,
|
|
363
|
+
`${IMAGE_AGENT_DIR}/bun`,
|
|
364
|
+
`${IMAGE_AGENT_DIR}/index.mjs`,
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The image the agent's runtime comes from. The musl build of Bun runs on any
|
|
369
|
+
* Linux image once its loader and C++ runtime come with it.
|
|
370
|
+
*/
|
|
371
|
+
const AGENT_RUNTIME_IMAGE = "oven/bun:1-alpine";
|
|
372
|
+
|
|
373
|
+
/** Where the agent's bundle sits inside its own image. */
|
|
374
|
+
const AGENT_BUNDLE_DIR = "/agent";
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The Dockerfile of an {@link ImageAgent}'s image: the runtime's image plus
|
|
378
|
+
* the agent's bundle. Its entry point copies the runtime, the loader, the
|
|
379
|
+
* C++ runtime and the bundle into {@link IMAGE_AGENT_DIR}, where the platform
|
|
380
|
+
* mounted a volume, and exits.
|
|
381
|
+
*/
|
|
382
|
+
const AGENT_DOCKERFILE = `${[
|
|
383
|
+
`FROM ${AGENT_RUNTIME_IMAGE}`,
|
|
384
|
+
`COPY index.mjs ${AGENT_BUNDLE_DIR}/index.mjs`,
|
|
385
|
+
`COPY *.js ${AGENT_BUNDLE_DIR}/`,
|
|
386
|
+
`ENTRYPOINT ${JSON.stringify([
|
|
387
|
+
"/bin/sh",
|
|
388
|
+
"-ec",
|
|
389
|
+
[
|
|
390
|
+
`mkdir -p ${IMAGE_AGENT_DIR}/lib`,
|
|
391
|
+
`cp /usr/local/bin/bun ${IMAGE_AGENT_DIR}/bun`,
|
|
392
|
+
`cp /lib/ld-musl-*.so.1 ${IMAGE_AGENT_DIR}/ld-musl.so.1`,
|
|
393
|
+
`cp /usr/lib/libstdc++.so.6 /usr/lib/libgcc_s.so.1 ${IMAGE_AGENT_DIR}/lib/`,
|
|
394
|
+
`cp ${AGENT_BUNDLE_DIR}/* ${IMAGE_AGENT_DIR}/`,
|
|
395
|
+
// The image's container may run as any user.
|
|
396
|
+
`chmod -R a+rX ${IMAGE_AGENT_DIR}`,
|
|
397
|
+
].join(" && "),
|
|
398
|
+
])}`,
|
|
399
|
+
].join("\n")}\n`;
|
|
400
|
+
|
|
401
|
+
/** The generated entry of an {@link ImageAgent}: import the module and run it. */
|
|
402
|
+
const agentEntry = (importPath: string) =>
|
|
403
|
+
`import { bootstrap } from ${JSON.stringify(importPath)};\nawait bootstrap();\n`;
|
|
404
|
+
|
|
234
405
|
/**
|
|
235
406
|
* Resolve the Dockerfile path for a `context` source: always relative to the
|
|
236
407
|
* cwd (absolute paths pass through), defaulting to `${context}/Dockerfile`.
|
|
@@ -287,24 +458,103 @@ export const computeStaticSourceHash = Effect.fn(function* (
|
|
|
287
458
|
return (yield* sha256Object({
|
|
288
459
|
dockerfile: source.dockerfile.content,
|
|
289
460
|
platform: platform ?? "linux/amd64",
|
|
461
|
+
// Absent when unset, so existing hashes hold. Secrets are excluded.
|
|
462
|
+
buildArgs: source.build?.args,
|
|
290
463
|
})).slice(0, 16);
|
|
291
464
|
}
|
|
292
|
-
const fs = yield* FileSystem.FileSystem;
|
|
293
465
|
const { context, dockerfile } = yield* resolveContextPaths({
|
|
294
466
|
context: source.context!,
|
|
295
467
|
dockerfile: source.dockerfile,
|
|
296
468
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return (yield*
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
469
|
+
// Hash what Docker receives: the context `.dockerignore` keeps (never a
|
|
470
|
+
// lockfile above it), the Dockerfile, platform and build args.
|
|
471
|
+
return (yield* hashDockerBuildInputs(
|
|
472
|
+
{
|
|
473
|
+
context,
|
|
474
|
+
dockerfile,
|
|
475
|
+
platform: platform ?? "linux/amd64",
|
|
476
|
+
buildArgs: source.build?.args,
|
|
477
|
+
},
|
|
478
|
+
"effective",
|
|
479
|
+
)).slice(0, 16);
|
|
304
480
|
}
|
|
305
481
|
return undefined;
|
|
306
482
|
});
|
|
307
483
|
|
|
484
|
+
/**
|
|
485
|
+
* The exact rolldown input/output options `bundleMainProgram` hands to
|
|
486
|
+
* `Bundle.build` — shared with {@link watchMain} so a dev watcher's
|
|
487
|
+
* `Bundle.watch` observes the identical module graph.
|
|
488
|
+
*/
|
|
489
|
+
export const mainBundleOptions = (
|
|
490
|
+
source: BundledImageSource,
|
|
491
|
+
entry: string,
|
|
492
|
+
cwd: string,
|
|
493
|
+
plugins?: rolldown.RolldownPluginOption,
|
|
494
|
+
): {
|
|
495
|
+
inputOptions: rolldown.InputOptions;
|
|
496
|
+
outputOptions: rolldown.OutputOptions;
|
|
497
|
+
} => ({
|
|
498
|
+
inputOptions: {
|
|
499
|
+
...source.build?.input,
|
|
500
|
+
input: entry,
|
|
501
|
+
cwd,
|
|
502
|
+
platform: "node",
|
|
503
|
+
// The container runs on `bun`; keep `bun`/`bun:*` external (the
|
|
504
|
+
// runtime provides them) and resolve the `bun` export condition
|
|
505
|
+
// so `@effect/platform-bun` picks its Bun implementations.
|
|
506
|
+
external: ["bun", "bun:*", ...((source.build?.input?.external as string[] | undefined) ?? [])],
|
|
507
|
+
resolve: {
|
|
508
|
+
conditionNames: [...Bundle.BUN_CONDITION_NAMES],
|
|
509
|
+
...source.build?.input?.resolve,
|
|
510
|
+
},
|
|
511
|
+
plugins: [source.build?.input?.plugins, plugins],
|
|
512
|
+
},
|
|
513
|
+
outputOptions: {
|
|
514
|
+
...source.build?.output,
|
|
515
|
+
format: "esm",
|
|
516
|
+
sourcemap: source.build?.output?.sourcemap ?? false,
|
|
517
|
+
minify: source.build?.output?.minify ?? false,
|
|
518
|
+
entryFileNames: "index.mjs",
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Bundle the Effect program behind a `main` source: every emitted file
|
|
524
|
+
* (entry + shared chunks) and the bundle hash. Needs no Docker engine, so
|
|
525
|
+
* builders that are not Docker (`BuildPlan`) share it.
|
|
526
|
+
*/
|
|
527
|
+
export const bundleMainProgram = Effect.fn(function* (options: {
|
|
528
|
+
source: BundledImageSource;
|
|
529
|
+
isExternal?: boolean;
|
|
530
|
+
bootstrap: (importPath: string) => string;
|
|
531
|
+
}) {
|
|
532
|
+
const { source } = options;
|
|
533
|
+
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
534
|
+
const realMain = yield* resolveMainPath(source.main);
|
|
535
|
+
const cwd = yield* findCwdForBundle(realMain);
|
|
536
|
+
|
|
537
|
+
const buildBundle = Effect.fn(function* (entry: string, plugins?: rolldown.RolldownPluginOption) {
|
|
538
|
+
const opts = mainBundleOptions(source, entry, cwd, plugins);
|
|
539
|
+
return yield* Bundle.build(opts.inputOptions, opts.outputOptions, source.build);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const bundleOutput = options.isExternal
|
|
543
|
+
? yield* buildBundle(realMain)
|
|
544
|
+
: yield* buildBundle(realMain, virtualEntryPlugin(options.bootstrap));
|
|
545
|
+
|
|
546
|
+
// Return every emitted file (entry + shared chunks). Dynamic imports in
|
|
547
|
+
// the Bun HTTP server / AWS SDK split into chunks; dropping any of them
|
|
548
|
+
// crashes the container with `Cannot find module './chunk-XXX.js'`.
|
|
549
|
+
const files = bundleOutput.files.map((file) => ({
|
|
550
|
+
path: file.path,
|
|
551
|
+
content:
|
|
552
|
+
typeof file.content === "string" ? new TextEncoder().encode(file.content) : file.content,
|
|
553
|
+
}));
|
|
554
|
+
|
|
555
|
+
return { files, hash: bundleOutput.hash };
|
|
556
|
+
});
|
|
557
|
+
|
|
308
558
|
/**
|
|
309
559
|
* Init-time constructor for the image-source resolver. Resolves the services
|
|
310
560
|
* that are only available at provider-layer construction (Docker, the
|
|
@@ -317,82 +567,6 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
317
567
|
const { dotAlchemy } = yield* AlchemyContext;
|
|
318
568
|
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
319
569
|
|
|
320
|
-
/**
|
|
321
|
-
* The exact rolldown input/output options `bundleProgram` hands to
|
|
322
|
-
* `Bundle.build` — shared with {@link watchMain} so a dev watcher's
|
|
323
|
-
* `Bundle.watch` observes the identical module graph.
|
|
324
|
-
*/
|
|
325
|
-
const mainBundleOptions = (
|
|
326
|
-
source: BundledImageSource,
|
|
327
|
-
entry: string,
|
|
328
|
-
cwd: string,
|
|
329
|
-
plugins?: rolldown.RolldownPluginOption,
|
|
330
|
-
): {
|
|
331
|
-
inputOptions: rolldown.InputOptions;
|
|
332
|
-
outputOptions: rolldown.OutputOptions;
|
|
333
|
-
} => ({
|
|
334
|
-
inputOptions: {
|
|
335
|
-
...source.build?.input,
|
|
336
|
-
input: entry,
|
|
337
|
-
cwd,
|
|
338
|
-
platform: "node",
|
|
339
|
-
// The container runs on `bun`; keep `bun`/`bun:*` external (the
|
|
340
|
-
// runtime provides them) and resolve the `bun` export condition
|
|
341
|
-
// so `@effect/platform-bun` picks its Bun implementations.
|
|
342
|
-
external: [
|
|
343
|
-
"bun",
|
|
344
|
-
"bun:*",
|
|
345
|
-
...((source.build?.input?.external as string[] | undefined) ?? []),
|
|
346
|
-
],
|
|
347
|
-
resolve: {
|
|
348
|
-
conditionNames: [...Bundle.BUN_CONDITION_NAMES],
|
|
349
|
-
...source.build?.input?.resolve,
|
|
350
|
-
},
|
|
351
|
-
plugins: [source.build?.input?.plugins, plugins],
|
|
352
|
-
},
|
|
353
|
-
outputOptions: {
|
|
354
|
-
...source.build?.output,
|
|
355
|
-
format: "esm",
|
|
356
|
-
sourcemap: source.build?.output?.sourcemap ?? false,
|
|
357
|
-
minify: source.build?.output?.minify ?? false,
|
|
358
|
-
entryFileNames: "index.mjs",
|
|
359
|
-
},
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
/** Bundle the Effect program behind a `main` source. */
|
|
363
|
-
const bundleProgram = Effect.fn(function* (options: {
|
|
364
|
-
source: BundledImageSource;
|
|
365
|
-
isExternal?: boolean;
|
|
366
|
-
bootstrap: (importPath: string) => string;
|
|
367
|
-
}) {
|
|
368
|
-
const { source } = options;
|
|
369
|
-
const realMain = yield* resolveMainPath(source.main);
|
|
370
|
-
const cwd = yield* findCwdForBundle(realMain);
|
|
371
|
-
|
|
372
|
-
const buildBundle = Effect.fn(function* (
|
|
373
|
-
entry: string,
|
|
374
|
-
plugins?: rolldown.RolldownPluginOption,
|
|
375
|
-
) {
|
|
376
|
-
const opts = mainBundleOptions(source, entry, cwd, plugins);
|
|
377
|
-
return yield* Bundle.build(opts.inputOptions, opts.outputOptions, source.build);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
const bundleOutput = options.isExternal
|
|
381
|
-
? yield* buildBundle(realMain)
|
|
382
|
-
: yield* buildBundle(realMain, virtualEntryPlugin(options.bootstrap));
|
|
383
|
-
|
|
384
|
-
// Return every emitted file (entry + shared chunks). Dynamic imports in
|
|
385
|
-
// the Bun HTTP server / AWS SDK split into chunks; dropping any of them
|
|
386
|
-
// crashes the container with `Cannot find module './chunk-XXX.js'`.
|
|
387
|
-
const files = bundleOutput.files.map((file) => ({
|
|
388
|
-
path: file.path,
|
|
389
|
-
content:
|
|
390
|
-
typeof file.content === "string" ? new TextEncoder().encode(file.content) : file.content,
|
|
391
|
-
}));
|
|
392
|
-
|
|
393
|
-
return { files, hash: bundleOutput.hash };
|
|
394
|
-
});
|
|
395
|
-
|
|
396
570
|
/**
|
|
397
571
|
* Bundle a `main` source and compute its content-addressed code hash.
|
|
398
572
|
*
|
|
@@ -409,7 +583,7 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
409
583
|
port?: number;
|
|
410
584
|
platform: string;
|
|
411
585
|
}) {
|
|
412
|
-
const bundled = yield*
|
|
586
|
+
const bundled = yield* bundleMainProgram({
|
|
413
587
|
source: options.source,
|
|
414
588
|
isExternal: options.isExternal,
|
|
415
589
|
bootstrap: options.bootstrap,
|
|
@@ -423,14 +597,15 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
423
597
|
const isPathEnv = df !== undefined && !isInlineDockerfile(df);
|
|
424
598
|
let envIdentity: Record<string, string> = {};
|
|
425
599
|
if (isPathEnv) {
|
|
426
|
-
const fs = yield* FileSystem.FileSystem;
|
|
427
600
|
const { context, dockerfile } = yield* resolveContextPaths({
|
|
428
601
|
context: options.source.context!,
|
|
429
602
|
dockerfile: df,
|
|
430
603
|
});
|
|
431
604
|
envIdentity = {
|
|
432
|
-
envContextHash: yield*
|
|
433
|
-
|
|
605
|
+
envContextHash: yield* hashDockerBuildInputs(
|
|
606
|
+
{ context, dockerfile, platform: options.platform },
|
|
607
|
+
"effective",
|
|
608
|
+
),
|
|
434
609
|
};
|
|
435
610
|
}
|
|
436
611
|
const dockerfile = generateDockerfile(
|
|
@@ -460,10 +635,11 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
460
635
|
envFrom !== undefined
|
|
461
636
|
? `FROM ${envFrom}`
|
|
462
637
|
: source.dockerfile !== undefined && isInlineDockerfile(source.dockerfile)
|
|
463
|
-
?
|
|
638
|
+
? (source.dockerfile.content as string).trimEnd()
|
|
464
639
|
: `FROM ${source.image ?? "oven/bun:1"}`;
|
|
465
640
|
const lines = [
|
|
466
641
|
preamble,
|
|
642
|
+
...(source.install ?? []),
|
|
467
643
|
`WORKDIR /app`,
|
|
468
644
|
`COPY index.mjs /app/index.mjs`,
|
|
469
645
|
// Copy any additional rolldown chunks (`chunk-XXX.js`,
|
|
@@ -478,6 +654,30 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
478
654
|
return `${lines.join("\n")}\n`;
|
|
479
655
|
};
|
|
480
656
|
|
|
657
|
+
/**
|
|
658
|
+
* Bundle an {@link ImageAgent} and compute the content-addressed hash of
|
|
659
|
+
* its image: the bundle, the Dockerfile and the platform. `resolve` and
|
|
660
|
+
* `hash` share this, so the plan-time hash and the pushed tag agree.
|
|
661
|
+
*/
|
|
662
|
+
const bundleAgent = Effect.fn(function* (agent: ImageAgent, platform: string) {
|
|
663
|
+
const bundled = yield* bundleMainProgram({
|
|
664
|
+
source: { main: agent.main },
|
|
665
|
+
isExternal: false,
|
|
666
|
+
bootstrap: agentEntry,
|
|
667
|
+
});
|
|
668
|
+
const hash = (yield* sha256Object({
|
|
669
|
+
bundleHash: bundled.hash,
|
|
670
|
+
dockerfile: AGENT_DOCKERFILE,
|
|
671
|
+
platform,
|
|
672
|
+
})).slice(0, 16);
|
|
673
|
+
return { bundled, hash };
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
/** One hash for an image and the agent published beside it. */
|
|
677
|
+
const hashWithAgent = Effect.fn(function* (environment: string, agent: string) {
|
|
678
|
+
return (yield* sha256Object({ environment, agent })).slice(0, 16);
|
|
679
|
+
});
|
|
680
|
+
|
|
481
681
|
/**
|
|
482
682
|
* Build (or mirror) and push an image to `target`, pushing with the
|
|
483
683
|
* target's credentials or — when it has none — the build machine's own
|
|
@@ -514,6 +714,7 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
514
714
|
context: string;
|
|
515
715
|
dockerfile?: string;
|
|
516
716
|
platform?: string;
|
|
717
|
+
build?: DockerfileBuild;
|
|
517
718
|
},
|
|
518
719
|
) {
|
|
519
720
|
const credentials = yield* target.credentials;
|
|
@@ -522,17 +723,20 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
522
723
|
context: options.context,
|
|
523
724
|
file: options.dockerfile,
|
|
524
725
|
platform: options.platform,
|
|
726
|
+
"build-arg": options.build?.args,
|
|
727
|
+
secrets: options.build?.secrets,
|
|
728
|
+
...imageBuildCacheOptions(options.build, options.imageUri, credentials),
|
|
525
729
|
});
|
|
526
730
|
yield* push(options.imageUri, credentials, options.platform);
|
|
527
731
|
return options.imageUri;
|
|
528
732
|
});
|
|
529
733
|
|
|
530
734
|
/**
|
|
531
|
-
* Resolve the image
|
|
735
|
+
* Resolve the image of a props bag's own source: resolve the target repository,
|
|
532
736
|
* compute the content-addressed tag, then build/mirror + push only when
|
|
533
737
|
* that exact tag is not already in the registry (crash-safe convergence).
|
|
534
738
|
*/
|
|
535
|
-
const
|
|
739
|
+
const resolveSource = Effect.fnUntraced(function* <R>(
|
|
536
740
|
options: ResolveContainerImageOptions,
|
|
537
741
|
target: ImageRegistryTarget<R>,
|
|
538
742
|
) {
|
|
@@ -587,11 +791,18 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
587
791
|
});
|
|
588
792
|
envFrom = `alchemy-env-${id.toLowerCase()}:${codeHash}`;
|
|
589
793
|
yield* session.note(`Building environment image for ${id}...`);
|
|
794
|
+
// The user's Dockerfile holds the expensive steps, so the layer
|
|
795
|
+
// cache applies to this stage rather than the bundle layered on top.
|
|
590
796
|
yield* docker.image.build({
|
|
591
797
|
context: env.context,
|
|
592
798
|
file: env.dockerfile,
|
|
593
799
|
tag: envFrom,
|
|
594
800
|
platform,
|
|
801
|
+
...imageBuildCacheOptions(
|
|
802
|
+
source.build,
|
|
803
|
+
imageUri,
|
|
804
|
+
source.build?.cache ? yield* target.credentials : undefined,
|
|
805
|
+
),
|
|
595
806
|
});
|
|
596
807
|
}
|
|
597
808
|
const finalDockerfile =
|
|
@@ -616,6 +827,7 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
616
827
|
imageUri,
|
|
617
828
|
context: contextDir,
|
|
618
829
|
platform,
|
|
830
|
+
build: envFrom === undefined ? { cache: source.build?.cache } : undefined,
|
|
619
831
|
});
|
|
620
832
|
yield* session.note(`Pushed ${imageUri}`);
|
|
621
833
|
return { imageUri, repositoryUri, codeHash };
|
|
@@ -676,6 +888,7 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
676
888
|
imageUri,
|
|
677
889
|
context: contextDir,
|
|
678
890
|
platform,
|
|
891
|
+
build: source.build,
|
|
679
892
|
});
|
|
680
893
|
yield* session.note(`Pushed ${imageUri}`);
|
|
681
894
|
return { imageUri, repositoryUri, codeHash };
|
|
@@ -698,11 +911,80 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
698
911
|
context,
|
|
699
912
|
dockerfile,
|
|
700
913
|
platform,
|
|
914
|
+
build: source.build,
|
|
701
915
|
});
|
|
702
916
|
yield* session.note(`Pushed ${imageUri}`);
|
|
703
917
|
return { imageUri, repositoryUri, codeHash };
|
|
704
918
|
});
|
|
705
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Publish an {@link ImageAgent} as an image of its own, `agent-<hash>` in
|
|
922
|
+
* the target repository, unless that tag is already there.
|
|
923
|
+
*/
|
|
924
|
+
const resolveAgent = Effect.fnUntraced(function* <R>(
|
|
925
|
+
options: {
|
|
926
|
+
id: string;
|
|
927
|
+
agent: ImageAgent;
|
|
928
|
+
platform: string;
|
|
929
|
+
repositoryUri: string;
|
|
930
|
+
session: ResolveContainerImageOptions["session"];
|
|
931
|
+
},
|
|
932
|
+
target: ImageRegistryTarget<R>,
|
|
933
|
+
) {
|
|
934
|
+
const { id, platform, session } = options;
|
|
935
|
+
yield* session.note(`Bundling ${id} sandbox agent...`);
|
|
936
|
+
const { bundled, hash } = yield* bundleAgent(options.agent, platform);
|
|
937
|
+
const tag = `agent-${hash}`;
|
|
938
|
+
const imageUri = `${options.repositoryUri}:${tag}`;
|
|
939
|
+
if (yield* target.hasTag(tag)) {
|
|
940
|
+
return { imageUri, hash };
|
|
941
|
+
}
|
|
942
|
+
const contextDir = yield* getStableContextDir(dotAlchemy, dotAlchemy, `${id}-agent`);
|
|
943
|
+
yield* docker.materialize({
|
|
944
|
+
context: contextDir,
|
|
945
|
+
dockerfile: AGENT_DOCKERFILE,
|
|
946
|
+
files: bundled.files.map((file, index) => ({
|
|
947
|
+
path: index === 0 ? "index.mjs" : file.path,
|
|
948
|
+
content: file.content,
|
|
949
|
+
})),
|
|
950
|
+
});
|
|
951
|
+
yield* session.note(`Building sandbox agent image ${imageUri}...`);
|
|
952
|
+
yield* buildAndPush(target, { imageUri, context: contextDir, platform });
|
|
953
|
+
yield* session.note(`Pushed ${imageUri}`);
|
|
954
|
+
return { imageUri, hash };
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Resolve the image for a props bag, and the {@link ImageAgent} published
|
|
959
|
+
* beside it when one is asked for. The image is the user's own, unchanged;
|
|
960
|
+
* `codeHash` then covers both images.
|
|
961
|
+
*/
|
|
962
|
+
const resolve = Effect.fnUntraced(function* <R>(
|
|
963
|
+
options: ResolveContainerImageOptions,
|
|
964
|
+
target: ImageRegistryTarget<R>,
|
|
965
|
+
) {
|
|
966
|
+
const image = yield* resolveSource(options, target);
|
|
967
|
+
if (options.agent === undefined || imageSourceKind(options.source) === "main") {
|
|
968
|
+
return { ...image, agent: undefined };
|
|
969
|
+
}
|
|
970
|
+
const published = yield* resolveAgent(
|
|
971
|
+
{
|
|
972
|
+
id: options.id,
|
|
973
|
+
agent: options.agent,
|
|
974
|
+
platform: options.platform ?? "linux/amd64",
|
|
975
|
+
repositoryUri: image.repositoryUri,
|
|
976
|
+
session: options.session,
|
|
977
|
+
},
|
|
978
|
+
target,
|
|
979
|
+
);
|
|
980
|
+
return {
|
|
981
|
+
imageUri: image.imageUri,
|
|
982
|
+
repositoryUri: image.repositoryUri,
|
|
983
|
+
codeHash: yield* hashWithAgent(image.codeHash, published.hash),
|
|
984
|
+
agent: { imageUri: published.imageUri },
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
|
|
706
988
|
/**
|
|
707
989
|
* Content hash for ANY source kind without building or pushing an image.
|
|
708
990
|
*
|
|
@@ -719,9 +1001,19 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
719
1001
|
port?: number;
|
|
720
1002
|
isExternal?: boolean;
|
|
721
1003
|
bootstrap: (importPath: string) => string;
|
|
1004
|
+
agent?: ImageAgent;
|
|
722
1005
|
}) {
|
|
723
1006
|
const platform = options.platform ?? "linux/amd64";
|
|
724
|
-
|
|
1007
|
+
const kind = imageSourceKind(options.source);
|
|
1008
|
+
if (options.agent !== undefined && kind !== undefined && kind !== "main") {
|
|
1009
|
+
// The image and its agent are two images. One hash covers both, so a
|
|
1010
|
+
// new agent plans an update just as a new image does.
|
|
1011
|
+
const environment = yield* computeStaticSourceHash(options.source, platform);
|
|
1012
|
+
if (environment === undefined) return undefined;
|
|
1013
|
+
const agent = yield* bundleAgent(options.agent, platform);
|
|
1014
|
+
return yield* hashWithAgent(environment, agent.hash);
|
|
1015
|
+
}
|
|
1016
|
+
if (kind === "main") {
|
|
725
1017
|
// Unresolved inline environment content (an Output) can't be hashed
|
|
726
1018
|
// at plan time — return undefined so the diff defers to reconcile.
|
|
727
1019
|
const df = options.source.dockerfile;
|