@alchemy.run/docker 2.0.0-beta.85 → 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 +9 -1
- package/lib/BuildHash.d.ts.map +1 -1
- package/lib/BuildHash.js +51 -19
- 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 +24 -1
- package/lib/Container.d.ts.map +1 -1
- package/lib/Container.js +14 -0
- 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 +33 -0
- package/lib/Docker.d.ts.map +1 -1
- package/lib/Docker.js +23 -2
- 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 +40 -12
- package/lib/Image.d.ts.map +1 -1
- package/lib/Image.js +41 -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 +100 -11
- package/lib/ImageSource.d.ts.map +1 -1
- package/lib/ImageSource.js +219 -73
- 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.map +1 -1
- package/lib/Service.d.ts.map +1 -1
- 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 +73 -24
- package/src/BuildPlan.ts +758 -0
- package/src/Container.ts +31 -1
- package/src/Context.ts +27 -1
- package/src/Docker.ts +56 -2
- package/src/Dockerfile.ts +0 -2
- package/src/DockerfileParser.ts +1152 -0
- package/src/Image.ts +45 -14
- package/src/ImageArchive.ts +77 -0
- package/src/ImageSource.ts +322 -103
- package/src/Network.ts +432 -10
- package/src/ShellWord.ts +326 -0
- package/src/Volume.ts +33 -1
package/src/ImageSource.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
6
5
|
import * as Data from "effect/Data";
|
|
7
6
|
import * as Effect from "effect/Effect";
|
|
@@ -10,6 +9,8 @@ import * as Path from "effect/Path";
|
|
|
10
9
|
import type * as Redacted from "effect/Redacted";
|
|
11
10
|
import * as Schedule from "effect/Schedule";
|
|
12
11
|
import type * as rolldown from "rolldown";
|
|
12
|
+
import { type BuildCacheOptions, imageBuildCacheOptions } from "./BuildCache.ts";
|
|
13
|
+
import { hashDockerBuildInputs } from "./BuildHash.ts";
|
|
13
14
|
import { Docker, type RegistryCredentials } from "./Docker.ts";
|
|
14
15
|
import { isInlineDockerfile, type InlineDockerfile } from "./Dockerfile.ts";
|
|
15
16
|
|
|
@@ -30,6 +31,9 @@ import { isInlineDockerfile, type InlineDockerfile } from "./Dockerfile.ts";
|
|
|
30
31
|
* ENTRYPOINT) and pushed as a derived image. `main` absent → the
|
|
31
32
|
* environment runs verbatim: `image` is mirrored (docker pull → tag →
|
|
32
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.
|
|
33
37
|
*
|
|
34
38
|
* Where images land is an {@link ImageRegistryTarget}: ECR supplies one that
|
|
35
39
|
* auto-creates a private repository; a plain registry (a local `registry`
|
|
@@ -48,6 +52,13 @@ export interface BundledImageSource {
|
|
|
48
52
|
* `import.meta.url` from an inline Effect program.
|
|
49
53
|
*/
|
|
50
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>;
|
|
51
62
|
/**
|
|
52
63
|
* Environment image: used as the generated Dockerfile's `FROM`. Any
|
|
53
64
|
* registry ref works (private non-ECR registries require docker
|
|
@@ -79,16 +90,17 @@ export interface BundledImageSource {
|
|
|
79
90
|
* Bundler configuration for the entrypoint. Unused code is tree-shaken.
|
|
80
91
|
* `effect`, alchemy, and `@distilled.cloud` are marked pure so unused
|
|
81
92
|
* parts prune more aggressively. List extra packages with
|
|
82
|
-
* `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.
|
|
83
95
|
*/
|
|
84
|
-
build?: Bundle.BundleConfig;
|
|
96
|
+
build?: Bundle.BundleConfig & BuildCacheOptions;
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
/**
|
|
88
|
-
* Build arguments
|
|
89
|
-
* own Dockerfile (`context` / `dockerfile`).
|
|
100
|
+
* Build arguments, BuildKit secrets and the layer cache for an image built
|
|
101
|
+
* from the user's own Dockerfile (`context` / `dockerfile`).
|
|
90
102
|
*/
|
|
91
|
-
export interface DockerfileBuild {
|
|
103
|
+
export interface DockerfileBuild extends BuildCacheOptions {
|
|
92
104
|
/**
|
|
93
105
|
* Docker build arguments (`--build-arg`), read by `ARG` instructions.
|
|
94
106
|
* They are part of the image's content hash, so changing one rebuilds the
|
|
@@ -110,7 +122,7 @@ export interface DockerfileBuild {
|
|
|
110
122
|
|
|
111
123
|
/** The `build` prop of a source built from the user's own Dockerfile. */
|
|
112
124
|
export interface DockerfileBuildOptions {
|
|
113
|
-
/** Build arguments
|
|
125
|
+
/** Build arguments, BuildKit secrets and the layer cache for the Dockerfile build. */
|
|
114
126
|
build?: DockerfileBuild;
|
|
115
127
|
}
|
|
116
128
|
|
|
@@ -160,6 +172,8 @@ export interface ImageSourceLike {
|
|
|
160
172
|
context?: string;
|
|
161
173
|
dockerfile?: string | InlineDockerfile;
|
|
162
174
|
image?: string;
|
|
175
|
+
/** @internal Mount-binding install lines (see {@link BundledImageSource.install}). */
|
|
176
|
+
install?: ReadonlyArray<string>;
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
export type ImageSourceKind = "main" | "context" | "image";
|
|
@@ -181,20 +195,25 @@ export const imageSourceKind = (source: ImageSourceLike): ImageSourceKind | unde
|
|
|
181
195
|
|
|
182
196
|
/**
|
|
183
197
|
* `build.args` or `build.secrets` set on an image that is not built from the
|
|
184
|
-
* user's own Dockerfile
|
|
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.
|
|
185
200
|
*/
|
|
186
201
|
export class DockerfileBuildConflict extends Data.TaggedError("DockerfileBuildConflict")<{
|
|
187
202
|
readonly id: string;
|
|
188
203
|
readonly source: "main" | "image";
|
|
204
|
+
readonly option?: "cache";
|
|
189
205
|
}> {
|
|
190
206
|
override get message() {
|
|
191
|
-
return
|
|
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}'`;
|
|
192
210
|
}
|
|
193
211
|
}
|
|
194
212
|
|
|
195
213
|
/**
|
|
196
214
|
* Dies with {@link DockerfileBuildConflict} when `build.args` or
|
|
197
|
-
* `build.secrets` are set alongside `main` or `image
|
|
215
|
+
* `build.secrets` are set alongside `main` or `image`, or `build.cache`
|
|
216
|
+
* alongside `image`.
|
|
198
217
|
*/
|
|
199
218
|
export const validateDockerfileBuild = (
|
|
200
219
|
id: string,
|
|
@@ -208,7 +227,9 @@ export const validateDockerfileBuild = (
|
|
|
208
227
|
source: source.main !== undefined ? "main" : "image",
|
|
209
228
|
}),
|
|
210
229
|
)
|
|
211
|
-
:
|
|
230
|
+
: source.main === undefined && source.image !== undefined && source.build?.cache
|
|
231
|
+
? Effect.die(new DockerfileBuildConflict({ id, source: "image", option: "cache" }))
|
|
232
|
+
: Effect.void;
|
|
212
233
|
|
|
213
234
|
/**
|
|
214
235
|
* Validate environment-source exclusivity. Dies (plan-time defect) on
|
|
@@ -292,10 +313,95 @@ export interface ResolveContainerImageOptions {
|
|
|
292
313
|
* generated entry module source.
|
|
293
314
|
*/
|
|
294
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;
|
|
295
322
|
/** Plan-status session used to emit build/push progress notes. */
|
|
296
323
|
session: { note: (message: string) => Effect.Effect<void> };
|
|
297
324
|
}
|
|
298
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
|
+
|
|
299
405
|
/**
|
|
300
406
|
* Resolve the Dockerfile path for a `context` source: always relative to the
|
|
301
407
|
* cwd (absolute paths pass through), defaulting to `${context}/Dockerfile`.
|
|
@@ -356,23 +462,99 @@ export const computeStaticSourceHash = Effect.fn(function* (
|
|
|
356
462
|
buildArgs: source.build?.args,
|
|
357
463
|
})).slice(0, 16);
|
|
358
464
|
}
|
|
359
|
-
const fs = yield* FileSystem.FileSystem;
|
|
360
465
|
const { context, dockerfile } = yield* resolveContextPaths({
|
|
361
466
|
context: source.context!,
|
|
362
467
|
dockerfile: source.dockerfile,
|
|
363
468
|
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return (yield*
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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);
|
|
372
480
|
}
|
|
373
481
|
return undefined;
|
|
374
482
|
});
|
|
375
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
|
+
|
|
376
558
|
/**
|
|
377
559
|
* Init-time constructor for the image-source resolver. Resolves the services
|
|
378
560
|
* that are only available at provider-layer construction (Docker, the
|
|
@@ -385,82 +567,6 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
385
567
|
const { dotAlchemy } = yield* AlchemyContext;
|
|
386
568
|
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
387
569
|
|
|
388
|
-
/**
|
|
389
|
-
* The exact rolldown input/output options `bundleProgram` hands to
|
|
390
|
-
* `Bundle.build` — shared with {@link watchMain} so a dev watcher's
|
|
391
|
-
* `Bundle.watch` observes the identical module graph.
|
|
392
|
-
*/
|
|
393
|
-
const mainBundleOptions = (
|
|
394
|
-
source: BundledImageSource,
|
|
395
|
-
entry: string,
|
|
396
|
-
cwd: string,
|
|
397
|
-
plugins?: rolldown.RolldownPluginOption,
|
|
398
|
-
): {
|
|
399
|
-
inputOptions: rolldown.InputOptions;
|
|
400
|
-
outputOptions: rolldown.OutputOptions;
|
|
401
|
-
} => ({
|
|
402
|
-
inputOptions: {
|
|
403
|
-
...source.build?.input,
|
|
404
|
-
input: entry,
|
|
405
|
-
cwd,
|
|
406
|
-
platform: "node",
|
|
407
|
-
// The container runs on `bun`; keep `bun`/`bun:*` external (the
|
|
408
|
-
// runtime provides them) and resolve the `bun` export condition
|
|
409
|
-
// so `@effect/platform-bun` picks its Bun implementations.
|
|
410
|
-
external: [
|
|
411
|
-
"bun",
|
|
412
|
-
"bun:*",
|
|
413
|
-
...((source.build?.input?.external as string[] | undefined) ?? []),
|
|
414
|
-
],
|
|
415
|
-
resolve: {
|
|
416
|
-
conditionNames: [...Bundle.BUN_CONDITION_NAMES],
|
|
417
|
-
...source.build?.input?.resolve,
|
|
418
|
-
},
|
|
419
|
-
plugins: [source.build?.input?.plugins, plugins],
|
|
420
|
-
},
|
|
421
|
-
outputOptions: {
|
|
422
|
-
...source.build?.output,
|
|
423
|
-
format: "esm",
|
|
424
|
-
sourcemap: source.build?.output?.sourcemap ?? false,
|
|
425
|
-
minify: source.build?.output?.minify ?? false,
|
|
426
|
-
entryFileNames: "index.mjs",
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
/** Bundle the Effect program behind a `main` source. */
|
|
431
|
-
const bundleProgram = Effect.fn(function* (options: {
|
|
432
|
-
source: BundledImageSource;
|
|
433
|
-
isExternal?: boolean;
|
|
434
|
-
bootstrap: (importPath: string) => string;
|
|
435
|
-
}) {
|
|
436
|
-
const { source } = options;
|
|
437
|
-
const realMain = yield* resolveMainPath(source.main);
|
|
438
|
-
const cwd = yield* findCwdForBundle(realMain);
|
|
439
|
-
|
|
440
|
-
const buildBundle = Effect.fn(function* (
|
|
441
|
-
entry: string,
|
|
442
|
-
plugins?: rolldown.RolldownPluginOption,
|
|
443
|
-
) {
|
|
444
|
-
const opts = mainBundleOptions(source, entry, cwd, plugins);
|
|
445
|
-
return yield* Bundle.build(opts.inputOptions, opts.outputOptions, source.build);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
const bundleOutput = options.isExternal
|
|
449
|
-
? yield* buildBundle(realMain)
|
|
450
|
-
: yield* buildBundle(realMain, virtualEntryPlugin(options.bootstrap));
|
|
451
|
-
|
|
452
|
-
// Return every emitted file (entry + shared chunks). Dynamic imports in
|
|
453
|
-
// the Bun HTTP server / AWS SDK split into chunks; dropping any of them
|
|
454
|
-
// crashes the container with `Cannot find module './chunk-XXX.js'`.
|
|
455
|
-
const files = bundleOutput.files.map((file) => ({
|
|
456
|
-
path: file.path,
|
|
457
|
-
content:
|
|
458
|
-
typeof file.content === "string" ? new TextEncoder().encode(file.content) : file.content,
|
|
459
|
-
}));
|
|
460
|
-
|
|
461
|
-
return { files, hash: bundleOutput.hash };
|
|
462
|
-
});
|
|
463
|
-
|
|
464
570
|
/**
|
|
465
571
|
* Bundle a `main` source and compute its content-addressed code hash.
|
|
466
572
|
*
|
|
@@ -477,7 +583,7 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
477
583
|
port?: number;
|
|
478
584
|
platform: string;
|
|
479
585
|
}) {
|
|
480
|
-
const bundled = yield*
|
|
586
|
+
const bundled = yield* bundleMainProgram({
|
|
481
587
|
source: options.source,
|
|
482
588
|
isExternal: options.isExternal,
|
|
483
589
|
bootstrap: options.bootstrap,
|
|
@@ -491,14 +597,15 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
491
597
|
const isPathEnv = df !== undefined && !isInlineDockerfile(df);
|
|
492
598
|
let envIdentity: Record<string, string> = {};
|
|
493
599
|
if (isPathEnv) {
|
|
494
|
-
const fs = yield* FileSystem.FileSystem;
|
|
495
600
|
const { context, dockerfile } = yield* resolveContextPaths({
|
|
496
601
|
context: options.source.context!,
|
|
497
602
|
dockerfile: df,
|
|
498
603
|
});
|
|
499
604
|
envIdentity = {
|
|
500
|
-
envContextHash: yield*
|
|
501
|
-
|
|
605
|
+
envContextHash: yield* hashDockerBuildInputs(
|
|
606
|
+
{ context, dockerfile, platform: options.platform },
|
|
607
|
+
"effective",
|
|
608
|
+
),
|
|
502
609
|
};
|
|
503
610
|
}
|
|
504
611
|
const dockerfile = generateDockerfile(
|
|
@@ -532,6 +639,7 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
532
639
|
: `FROM ${source.image ?? "oven/bun:1"}`;
|
|
533
640
|
const lines = [
|
|
534
641
|
preamble,
|
|
642
|
+
...(source.install ?? []),
|
|
535
643
|
`WORKDIR /app`,
|
|
536
644
|
`COPY index.mjs /app/index.mjs`,
|
|
537
645
|
// Copy any additional rolldown chunks (`chunk-XXX.js`,
|
|
@@ -546,6 +654,30 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
546
654
|
return `${lines.join("\n")}\n`;
|
|
547
655
|
};
|
|
548
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
|
+
|
|
549
681
|
/**
|
|
550
682
|
* Build (or mirror) and push an image to `target`, pushing with the
|
|
551
683
|
* target's credentials or — when it has none — the build machine's own
|
|
@@ -593,17 +725,18 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
593
725
|
platform: options.platform,
|
|
594
726
|
"build-arg": options.build?.args,
|
|
595
727
|
secrets: options.build?.secrets,
|
|
728
|
+
...imageBuildCacheOptions(options.build, options.imageUri, credentials),
|
|
596
729
|
});
|
|
597
730
|
yield* push(options.imageUri, credentials, options.platform);
|
|
598
731
|
return options.imageUri;
|
|
599
732
|
});
|
|
600
733
|
|
|
601
734
|
/**
|
|
602
|
-
* Resolve the image
|
|
735
|
+
* Resolve the image of a props bag's own source: resolve the target repository,
|
|
603
736
|
* compute the content-addressed tag, then build/mirror + push only when
|
|
604
737
|
* that exact tag is not already in the registry (crash-safe convergence).
|
|
605
738
|
*/
|
|
606
|
-
const
|
|
739
|
+
const resolveSource = Effect.fnUntraced(function* <R>(
|
|
607
740
|
options: ResolveContainerImageOptions,
|
|
608
741
|
target: ImageRegistryTarget<R>,
|
|
609
742
|
) {
|
|
@@ -658,11 +791,18 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
658
791
|
});
|
|
659
792
|
envFrom = `alchemy-env-${id.toLowerCase()}:${codeHash}`;
|
|
660
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.
|
|
661
796
|
yield* docker.image.build({
|
|
662
797
|
context: env.context,
|
|
663
798
|
file: env.dockerfile,
|
|
664
799
|
tag: envFrom,
|
|
665
800
|
platform,
|
|
801
|
+
...imageBuildCacheOptions(
|
|
802
|
+
source.build,
|
|
803
|
+
imageUri,
|
|
804
|
+
source.build?.cache ? yield* target.credentials : undefined,
|
|
805
|
+
),
|
|
666
806
|
});
|
|
667
807
|
}
|
|
668
808
|
const finalDockerfile =
|
|
@@ -687,6 +827,7 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
687
827
|
imageUri,
|
|
688
828
|
context: contextDir,
|
|
689
829
|
platform,
|
|
830
|
+
build: envFrom === undefined ? { cache: source.build?.cache } : undefined,
|
|
690
831
|
});
|
|
691
832
|
yield* session.note(`Pushed ${imageUri}`);
|
|
692
833
|
return { imageUri, repositoryUri, codeHash };
|
|
@@ -776,6 +917,74 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
776
917
|
return { imageUri, repositoryUri, codeHash };
|
|
777
918
|
});
|
|
778
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
|
+
|
|
779
988
|
/**
|
|
780
989
|
* Content hash for ANY source kind without building or pushing an image.
|
|
781
990
|
*
|
|
@@ -792,9 +1001,19 @@ export const makeContainerImageSource = Effect.gen(function* () {
|
|
|
792
1001
|
port?: number;
|
|
793
1002
|
isExternal?: boolean;
|
|
794
1003
|
bootstrap: (importPath: string) => string;
|
|
1004
|
+
agent?: ImageAgent;
|
|
795
1005
|
}) {
|
|
796
1006
|
const platform = options.platform ?? "linux/amd64";
|
|
797
|
-
|
|
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") {
|
|
798
1017
|
// Unresolved inline environment content (an Output) can't be hashed
|
|
799
1018
|
// at plan time — return undefined so the diff defers to reconcile.
|
|
800
1019
|
const df = options.source.dockerfile;
|