@alchemy.run/e2b 0.0.0 → 2.0.0-beta.87
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/LICENSE +201 -0
- package/README.md +56 -4
- package/lib/ApiKey.d.ts +53 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +107 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +60 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +293 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Bootstrap.d.ts +7 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +25 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +67 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +3 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/ConnectSandboxHttp.d.ts +22 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +30 -0
- package/lib/ConnectSandboxHttp.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateSandbox.d.ts +139 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +3 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/CreateSandboxHttp.d.ts +22 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +99 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CreateSecret.d.ts +57 -0
- package/lib/CreateSecret.d.ts.map +1 -0
- package/lib/CreateSecret.js +3 -0
- package/lib/CreateSecret.js.map +1 -0
- package/lib/CreateSecretHttp.d.ts +20 -0
- package/lib/CreateSecretHttp.d.ts.map +1 -0
- package/lib/CreateSecretHttp.js +64 -0
- package/lib/CreateSecretHttp.js.map +1 -0
- package/lib/CreateSnapshot.d.ts +53 -0
- package/lib/CreateSnapshot.d.ts.map +1 -0
- package/lib/CreateSnapshot.js +3 -0
- package/lib/CreateSnapshot.js.map +1 -0
- package/lib/CreateSnapshotHttp.d.ts +20 -0
- package/lib/CreateSnapshotHttp.d.ts.map +1 -0
- package/lib/CreateSnapshotHttp.js +41 -0
- package/lib/CreateSnapshotHttp.js.map +1 -0
- package/lib/CreateTemplate.d.ts +96 -0
- package/lib/CreateTemplate.d.ts.map +1 -0
- package/lib/CreateTemplate.js +3 -0
- package/lib/CreateTemplate.js.map +1 -0
- package/lib/CreateTemplateHttp.d.ts +20 -0
- package/lib/CreateTemplateHttp.d.ts.map +1 -0
- package/lib/CreateTemplateHttp.js +99 -0
- package/lib/CreateTemplateHttp.js.map +1 -0
- package/lib/CreateVolume.d.ts +58 -0
- package/lib/CreateVolume.d.ts.map +1 -0
- package/lib/CreateVolume.js +3 -0
- package/lib/CreateVolume.js.map +1 -0
- package/lib/CreateVolumeHttp.d.ts +20 -0
- package/lib/CreateVolumeHttp.d.ts.map +1 -0
- package/lib/CreateVolumeHttp.js +51 -0
- package/lib/CreateVolumeHttp.js.map +1 -0
- package/lib/Credentials.d.ts +18 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +55 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/E2BBindings.d.ts +35 -0
- package/lib/E2BBindings.d.ts.map +1 -0
- package/lib/E2BBindings.js +127 -0
- package/lib/E2BBindings.js.map +1 -0
- package/lib/Errors.d.ts +61 -0
- package/lib/Errors.d.ts.map +1 -0
- package/lib/Errors.js +23 -0
- package/lib/Errors.js.map +1 -0
- package/lib/InferEnv.d.ts +26 -0
- package/lib/InferEnv.d.ts.map +1 -0
- package/lib/InferEnv.js +1 -0
- package/lib/InferEnv.js.map +1 -0
- package/lib/MountVolume.d.ts +86 -0
- package/lib/MountVolume.d.ts.map +1 -0
- package/lib/MountVolume.js +3 -0
- package/lib/MountVolume.js.map +1 -0
- package/lib/MountVolumeLive.d.ts +23 -0
- package/lib/MountVolumeLive.d.ts.map +1 -0
- package/lib/MountVolumeLive.js +41 -0
- package/lib/MountVolumeLive.js.map +1 -0
- package/lib/Providers.d.ts +14 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +23 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadTeam.d.ts +41 -0
- package/lib/ReadTeam.d.ts.map +1 -0
- package/lib/ReadTeam.js +3 -0
- package/lib/ReadTeam.js.map +1 -0
- package/lib/ReadTeamHttp.d.ts +20 -0
- package/lib/ReadTeamHttp.d.ts.map +1 -0
- package/lib/ReadTeamHttp.js +40 -0
- package/lib/ReadTeamHttp.js.map +1 -0
- package/lib/ReadVolume.d.ts +25 -0
- package/lib/ReadVolume.d.ts.map +1 -0
- package/lib/ReadVolume.js +3 -0
- package/lib/ReadVolume.js.map +1 -0
- package/lib/ReadVolumeHttp.d.ts +20 -0
- package/lib/ReadVolumeHttp.d.ts.map +1 -0
- package/lib/ReadVolumeHttp.js +27 -0
- package/lib/ReadVolumeHttp.js.map +1 -0
- package/lib/ReadWebhook.d.ts +32 -0
- package/lib/ReadWebhook.d.ts.map +1 -0
- package/lib/ReadWebhook.js +3 -0
- package/lib/ReadWebhook.js.map +1 -0
- package/lib/ReadWebhookHttp.d.ts +20 -0
- package/lib/ReadWebhookHttp.d.ts.map +1 -0
- package/lib/ReadWebhookHttp.js +31 -0
- package/lib/ReadWebhookHttp.js.map +1 -0
- package/lib/ReadWriteVolume.d.ts +26 -0
- package/lib/ReadWriteVolume.d.ts.map +1 -0
- package/lib/ReadWriteVolume.js +3 -0
- package/lib/ReadWriteVolume.js.map +1 -0
- package/lib/ReadWriteVolumeHttp.d.ts +20 -0
- package/lib/ReadWriteVolumeHttp.d.ts.map +1 -0
- package/lib/ReadWriteVolumeHttp.js +27 -0
- package/lib/ReadWriteVolumeHttp.js.map +1 -0
- package/lib/Sandbox.d.ts +295 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +351 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +226 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +1 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/SandboxEvents.d.ts +87 -0
- package/lib/SandboxEvents.d.ts.map +1 -0
- package/lib/SandboxEvents.js +39 -0
- package/lib/SandboxEvents.js.map +1 -0
- package/lib/SandboxEventsLive.d.ts +13 -0
- package/lib/SandboxEventsLive.d.ts.map +1 -0
- package/lib/SandboxEventsLive.js +80 -0
- package/lib/SandboxEventsLive.js.map +1 -0
- package/lib/Secret.d.ts +105 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +161 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Snapshot.d.ts +118 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +158 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Template.d.ts +313 -0
- package/lib/Template.d.ts.map +1 -0
- package/lib/Template.js +421 -0
- package/lib/Template.js.map +1 -0
- package/lib/Volume.d.ts +88 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +136 -0
- package/lib/Volume.js.map +1 -0
- package/lib/VolumeFiles.d.ts +39 -0
- package/lib/VolumeFiles.d.ts.map +1 -0
- package/lib/VolumeFiles.js +1 -0
- package/lib/VolumeFiles.js.map +1 -0
- package/lib/Webhook.d.ts +83 -0
- package/lib/Webhook.d.ts.map +1 -0
- package/lib/Webhook.js +138 -0
- package/lib/Webhook.js.map +1 -0
- package/lib/WriteSecret.d.ts +34 -0
- package/lib/WriteSecret.d.ts.map +1 -0
- package/lib/WriteSecret.js +3 -0
- package/lib/WriteSecret.js.map +1 -0
- package/lib/WriteSecretHttp.d.ts +20 -0
- package/lib/WriteSecretHttp.d.ts.map +1 -0
- package/lib/WriteSecretHttp.js +37 -0
- package/lib/WriteSecretHttp.js.map +1 -0
- package/lib/WriteVolume.d.ts +25 -0
- package/lib/WriteVolume.d.ts.map +1 -0
- package/lib/WriteVolume.js +3 -0
- package/lib/WriteVolume.js.map +1 -0
- package/lib/WriteVolumeHttp.d.ts +20 -0
- package/lib/WriteVolumeHttp.d.ts.map +1 -0
- package/lib/WriteVolumeHttp.js +27 -0
- package/lib/WriteVolumeHttp.js.map +1 -0
- package/lib/index.d.ts +43 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +43 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/Declared.d.ts +29 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +31 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Login.d.ts +45 -0
- package/lib/internal/Login.d.ts.map +1 -0
- package/lib/internal/Login.js +157 -0
- package/lib/internal/Login.js.map +1 -0
- package/lib/internal/Ownership.d.ts +29 -0
- package/lib/internal/Ownership.d.ts.map +1 -0
- package/lib/internal/Ownership.js +49 -0
- package/lib/internal/Ownership.js.map +1 -0
- package/lib/internal/Program.d.ts +31 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +60 -0
- package/lib/internal/Program.js.map +1 -0
- package/lib/internal/SandboxHandle.d.ts +25 -0
- package/lib/internal/SandboxHandle.d.ts.map +1 -0
- package/lib/internal/SandboxHandle.js +316 -0
- package/lib/internal/SandboxHandle.js.map +1 -0
- package/lib/internal/SnapshotBuild.d.ts +19 -0
- package/lib/internal/SnapshotBuild.d.ts.map +1 -0
- package/lib/internal/SnapshotBuild.js +49 -0
- package/lib/internal/SnapshotBuild.js.map +1 -0
- package/lib/internal/Tar.d.ts +10 -0
- package/lib/internal/Tar.d.ts.map +1 -0
- package/lib/internal/Tar.js +61 -0
- package/lib/internal/Tar.js.map +1 -0
- package/lib/internal/TemplateBuild.d.ts +142 -0
- package/lib/internal/TemplateBuild.d.ts.map +1 -0
- package/lib/internal/TemplateBuild.js +220 -0
- package/lib/internal/TemplateBuild.js.map +1 -0
- package/lib/internal/TemplatePlan.d.ts +57 -0
- package/lib/internal/TemplatePlan.d.ts.map +1 -0
- package/lib/internal/TemplatePlan.js +185 -0
- package/lib/internal/TemplatePlan.js.map +1 -0
- package/lib/internal/VolumeHttp.d.ts +5 -0
- package/lib/internal/VolumeHttp.d.ts.map +1 -0
- package/lib/internal/VolumeHttp.js +46 -0
- package/lib/internal/VolumeHttp.js.map +1 -0
- package/package.json +62 -3
- package/src/ApiKey.ts +148 -0
- package/src/AuthProvider.ts +437 -0
- package/src/Bootstrap.ts +51 -0
- package/src/ConnectSandbox.ts +71 -0
- package/src/ConnectSandboxHttp.ts +38 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +157 -0
- package/src/CreateSandboxHttp.ts +131 -0
- package/src/CreateSecret.ts +65 -0
- package/src/CreateSecretHttp.ts +94 -0
- package/src/CreateSnapshot.ts +64 -0
- package/src/CreateSnapshotHttp.ts +55 -0
- package/src/CreateTemplate.ts +135 -0
- package/src/CreateTemplateHttp.ts +152 -0
- package/src/CreateVolume.ts +65 -0
- package/src/CreateVolumeHttp.ts +80 -0
- package/src/Credentials.ts +81 -0
- package/src/E2BBindings.ts +160 -0
- package/src/Errors.ts +47 -0
- package/src/InferEnv.ts +27 -0
- package/src/MountVolume.ts +92 -0
- package/src/MountVolumeLive.ts +49 -0
- package/src/Providers.ts +48 -0
- package/src/ReadTeam.ts +55 -0
- package/src/ReadTeamHttp.ts +44 -0
- package/src/ReadVolume.ts +29 -0
- package/src/ReadVolumeHttp.ts +32 -0
- package/src/ReadWebhook.ts +41 -0
- package/src/ReadWebhookHttp.ts +42 -0
- package/src/ReadWriteVolume.ts +30 -0
- package/src/ReadWriteVolumeHttp.ts +32 -0
- package/src/Sandbox.ts +581 -0
- package/src/SandboxClient.ts +268 -0
- package/src/SandboxEvents.ts +101 -0
- package/src/SandboxEventsLive.ts +118 -0
- package/src/Secret.ts +229 -0
- package/src/Snapshot.ts +234 -0
- package/src/Template.ts +636 -0
- package/src/Volume.ts +173 -0
- package/src/VolumeFiles.ts +53 -0
- package/src/Webhook.ts +215 -0
- package/src/WriteSecret.ts +39 -0
- package/src/WriteSecretHttp.ts +43 -0
- package/src/WriteVolume.ts +29 -0
- package/src/WriteVolumeHttp.ts +32 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +42 -0
- package/src/internal/Declared.ts +80 -0
- package/src/internal/Login.ts +210 -0
- package/src/internal/Ownership.ts +67 -0
- package/src/internal/Program.ts +75 -0
- package/src/internal/SandboxHandle.ts +534 -0
- package/src/internal/SnapshotBuild.ts +74 -0
- package/src/internal/Tar.ts +74 -0
- package/src/internal/TemplateBuild.ts +383 -0
- package/src/internal/TemplatePlan.ts +230 -0
- package/src/internal/VolumeHttp.ts +88 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plans and runs an E2B template build. Shared by the `E2B.Template`
|
|
3
|
+
* resource (which lowers its Dockerfile or `main` through
|
|
4
|
+
* `./TemplatePlan.ts` first) and the runtime `E2B.CreateTemplate` binding.
|
|
5
|
+
*
|
|
6
|
+
* E2B builds remotely only: there is no local Docker step. The flow is
|
|
7
|
+
*
|
|
8
|
+
* 1. `createTemplate` — the template (by name) and a new build id
|
|
9
|
+
* 2. `getTemplateFileUploadLink` per `COPY` hash, uploading what E2B lacks
|
|
10
|
+
* 3. `startTemplateBuild` — base image or template, steps, start/ready command
|
|
11
|
+
* 4. `getTemplateBuildStatus` until the build is `ready` or `error`
|
|
12
|
+
*
|
|
13
|
+
* This module runs where a Worker runs: it imports nothing from
|
|
14
|
+
* `@alchemy.run/docker`.
|
|
15
|
+
*/
|
|
16
|
+
import * as crypto from "node:crypto";
|
|
17
|
+
import * as templates from "@distilled.cloud/e2b/templates";
|
|
18
|
+
import * as Duration from "effect/Duration";
|
|
19
|
+
import * as Effect from "effect/Effect";
|
|
20
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
21
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
22
|
+
import type * as PlatformError from "effect/PlatformError";
|
|
23
|
+
import * as Redacted from "effect/Redacted";
|
|
24
|
+
import * as Schedule from "effect/Schedule";
|
|
25
|
+
import { E2BUnexpectedResponse, TemplateBuildFailed } from "../Errors.ts";
|
|
26
|
+
import { tarGzip } from "./Tar.ts";
|
|
27
|
+
|
|
28
|
+
/** Credentials for a private base image. Passed to E2B's builder for the pull only. */
|
|
29
|
+
export type TemplateRegistry =
|
|
30
|
+
| {
|
|
31
|
+
/** Any registry with username/password auth (Docker Hub, GHCR, …). */
|
|
32
|
+
readonly username: string;
|
|
33
|
+
readonly password: Redacted.Redacted<string>;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
/** Amazon ECR. */
|
|
37
|
+
readonly awsAccessKeyId: string;
|
|
38
|
+
readonly awsSecretAccessKey: Redacted.Redacted<string>;
|
|
39
|
+
readonly awsRegion: string;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
/** Google Artifact Registry / GCR. */
|
|
43
|
+
readonly serviceAccountJson: Redacted.Redacted<string>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** A file written into the template, from memory. */
|
|
47
|
+
export interface TemplateFile {
|
|
48
|
+
/** Absolute path in the template, e.g. `/home/user/app/config.json`. */
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly contents: Uint8Array | string;
|
|
51
|
+
/** @default 0o644 */
|
|
52
|
+
readonly mode?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** E2B's own build inputs: a base plus steps, with no Dockerfile. */
|
|
56
|
+
export interface TemplateSteps {
|
|
57
|
+
/** A registry image to build on, e.g. `"python:3.12-slim"`. */
|
|
58
|
+
readonly image?: string;
|
|
59
|
+
/** Another E2B template (name or id) to build on, reusing its cached layers. */
|
|
60
|
+
readonly fromTemplate?: string;
|
|
61
|
+
/** Environment variables set in the template (`ENV`). */
|
|
62
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
63
|
+
/** Files written into the template. */
|
|
64
|
+
readonly files?: ReadonlyArray<TemplateFile>;
|
|
65
|
+
/** Shell commands run as extra `RUN` steps, after `env` and `files`. */
|
|
66
|
+
readonly run?: ReadonlyArray<string>;
|
|
67
|
+
/** Credentials for a private base image. */
|
|
68
|
+
readonly registry?: TemplateRegistry;
|
|
69
|
+
/**
|
|
70
|
+
* Command started when the template is built and captured running in its
|
|
71
|
+
* snapshot, so every sandbox starts with it already up.
|
|
72
|
+
*/
|
|
73
|
+
readonly startCmd?: string;
|
|
74
|
+
/** Command that exits 0 once `startCmd` is ready; the snapshot is taken then. */
|
|
75
|
+
readonly readyCmd?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Where a build's template lives and how big its sandboxes are. */
|
|
79
|
+
export interface TemplateOptions {
|
|
80
|
+
/** Template name (and alias). */
|
|
81
|
+
readonly name: string;
|
|
82
|
+
/**
|
|
83
|
+
* Tags assigned to this build (e.g. `"v1"`). Sandboxes created from the
|
|
84
|
+
* template id or name boot its `default` tag.
|
|
85
|
+
* @default ["default"]
|
|
86
|
+
*/
|
|
87
|
+
readonly tags?: ReadonlyArray<string>;
|
|
88
|
+
readonly cpuCount?: number;
|
|
89
|
+
readonly memoryMB?: number;
|
|
90
|
+
readonly minFreeDiskMb?: number;
|
|
91
|
+
/** Rebuild every layer, ignoring E2B's cache. */
|
|
92
|
+
readonly force?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TemplateBuildSpec extends TemplateSteps, TemplateOptions {}
|
|
96
|
+
|
|
97
|
+
/** One file a `COPY` step uploads. */
|
|
98
|
+
export interface UploadFile {
|
|
99
|
+
/** Path inside the archive, `/`-separated. */
|
|
100
|
+
readonly path: string;
|
|
101
|
+
readonly mode: number;
|
|
102
|
+
readonly size: number;
|
|
103
|
+
/** Hex SHA-256 of the contents. */
|
|
104
|
+
readonly sha256: string;
|
|
105
|
+
readonly read: Effect.Effect<Uint8Array, PlatformError.PlatformError>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface PlannedStep {
|
|
109
|
+
readonly type: "RUN" | "COPY" | "ENV" | "WORKDIR" | "USER";
|
|
110
|
+
readonly args: ReadonlyArray<string>;
|
|
111
|
+
readonly filesHash?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A base and steps that came before `TemplateSteps`' own: a lowered Dockerfile or program. */
|
|
115
|
+
export interface LoweredSource {
|
|
116
|
+
readonly fromImage: string;
|
|
117
|
+
readonly steps: ReadonlyArray<PlannedStep>;
|
|
118
|
+
readonly uploads: ReadonlyMap<string, ReadonlyArray<UploadFile>>;
|
|
119
|
+
/** The start command the source declares (`ENTRYPOINT` / `CMD`). */
|
|
120
|
+
readonly startCmd: string | undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface TemplateBuildPlan {
|
|
124
|
+
readonly fromImage?: string;
|
|
125
|
+
readonly fromTemplate?: string;
|
|
126
|
+
readonly steps: ReadonlyArray<PlannedStep>;
|
|
127
|
+
readonly startCmd?: string;
|
|
128
|
+
readonly readyCmd?: string;
|
|
129
|
+
/** Files per `COPY` hash, uploaded when E2B does not have them. */
|
|
130
|
+
readonly uploads: ReadonlyMap<string, ReadonlyArray<UploadFile>>;
|
|
131
|
+
/** Content hash of everything that determines the build. A different hash means rebuild. */
|
|
132
|
+
readonly hash: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface TemplateBuildResult {
|
|
136
|
+
readonly templateId: string;
|
|
137
|
+
readonly buildId: string;
|
|
138
|
+
readonly names: ReadonlyArray<string>;
|
|
139
|
+
readonly aliases: ReadonlyArray<string>;
|
|
140
|
+
readonly tags: ReadonlyArray<string>;
|
|
141
|
+
readonly public: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** E2B's default base image, for a template with no `image`, Dockerfile or `fromTemplate`. */
|
|
145
|
+
export const DEFAULT_BASE_IMAGE = "e2bdev/base";
|
|
146
|
+
|
|
147
|
+
const encoder = new TextEncoder();
|
|
148
|
+
|
|
149
|
+
const sha256 = (bytes: Uint8Array | string) =>
|
|
150
|
+
crypto.createHash("sha256").update(bytes).digest("hex");
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The content hash E2B caches a `COPY` step's files under: sha256 over the
|
|
154
|
+
* step, then each file's path, mode, size and content hash. Stable across
|
|
155
|
+
* machines (no owners or timestamps).
|
|
156
|
+
*/
|
|
157
|
+
export const filesHash = (
|
|
158
|
+
src: string,
|
|
159
|
+
dest: string,
|
|
160
|
+
files: ReadonlyArray<Pick<UploadFile, "path" | "mode" | "size" | "sha256">>,
|
|
161
|
+
): string => {
|
|
162
|
+
const hash = crypto.createHash("sha256");
|
|
163
|
+
hash.update(`COPY ${src} ${dest}`);
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
hash.update(`${file.path}\0${file.mode}\0${file.size}\0${file.sha256}\0`);
|
|
166
|
+
}
|
|
167
|
+
return hash.digest("hex");
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** `files` as `COPY` steps: each file is its own upload, named by its content. */
|
|
171
|
+
const fileSteps = (files: ReadonlyArray<TemplateFile>) =>
|
|
172
|
+
files.map((file) => {
|
|
173
|
+
const bytes = typeof file.contents === "string" ? encoder.encode(file.contents) : file.contents;
|
|
174
|
+
const upload: UploadFile = {
|
|
175
|
+
path: `files/${sha256(bytes).slice(0, 16)}`,
|
|
176
|
+
mode: file.mode ?? 0o644,
|
|
177
|
+
size: bytes.byteLength,
|
|
178
|
+
sha256: sha256(bytes),
|
|
179
|
+
read: Effect.succeed(bytes),
|
|
180
|
+
};
|
|
181
|
+
const hash = filesHash(upload.path, file.path, [upload]);
|
|
182
|
+
return {
|
|
183
|
+
step: {
|
|
184
|
+
type: "COPY",
|
|
185
|
+
args: [upload.path, file.path, "", ""],
|
|
186
|
+
filesHash: hash,
|
|
187
|
+
} as PlannedStep,
|
|
188
|
+
hash,
|
|
189
|
+
upload,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Plans a build: the lowered source (if any), then `env`, `files` and `run`
|
|
195
|
+
* as E2B steps, hashed.
|
|
196
|
+
*/
|
|
197
|
+
export const planTemplateBuild = (spec: TemplateBuildSpec, lowered?: LoweredSource) =>
|
|
198
|
+
Effect.gen(function* () {
|
|
199
|
+
if (spec.image !== undefined && spec.fromTemplate !== undefined) {
|
|
200
|
+
return yield* new E2BUnexpectedResponse({
|
|
201
|
+
message: `template ${spec.name}: set one of image or fromTemplate`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const uploads = new Map<string, ReadonlyArray<UploadFile>>(lowered?.uploads ?? []);
|
|
205
|
+
const files = fileSteps(spec.files ?? []);
|
|
206
|
+
for (const file of files) uploads.set(file.hash, [file.upload]);
|
|
207
|
+
const steps: PlannedStep[] = [
|
|
208
|
+
...(lowered?.steps ?? []),
|
|
209
|
+
...(spec.env && Object.keys(spec.env).length > 0
|
|
210
|
+
? [{ type: "ENV" as const, args: Object.entries(spec.env).flat() }]
|
|
211
|
+
: []),
|
|
212
|
+
...files.map((file) => file.step),
|
|
213
|
+
...(spec.run ?? []).map((command) => ({ type: "RUN" as const, args: [command] })),
|
|
214
|
+
];
|
|
215
|
+
const fromTemplate = spec.fromTemplate;
|
|
216
|
+
const fromImage =
|
|
217
|
+
fromTemplate !== undefined
|
|
218
|
+
? undefined
|
|
219
|
+
: (lowered?.fromImage ?? spec.image ?? DEFAULT_BASE_IMAGE);
|
|
220
|
+
const startCmd = spec.startCmd ?? lowered?.startCmd;
|
|
221
|
+
|
|
222
|
+
const hash = sha256(
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
fromImage,
|
|
225
|
+
fromTemplate,
|
|
226
|
+
steps,
|
|
227
|
+
startCmd,
|
|
228
|
+
readyCmd: spec.readyCmd,
|
|
229
|
+
cpuCount: spec.cpuCount,
|
|
230
|
+
memoryMB: spec.memoryMB,
|
|
231
|
+
minFreeDiskMb: spec.minFreeDiskMb,
|
|
232
|
+
registry: spec.registry === undefined ? undefined : Object.keys(spec.registry).sort(),
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
fromImage,
|
|
238
|
+
fromTemplate,
|
|
239
|
+
steps,
|
|
240
|
+
startCmd,
|
|
241
|
+
readyCmd: spec.readyCmd,
|
|
242
|
+
uploads,
|
|
243
|
+
hash,
|
|
244
|
+
} satisfies TemplateBuildPlan;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const registryOf = (registry: TemplateRegistry): templates.FromImageRegistry =>
|
|
248
|
+
// The registry union is opaque on the wire: secrets are unwrapped here, at
|
|
249
|
+
// the point of use, and never logged.
|
|
250
|
+
"awsAccessKeyId" in registry
|
|
251
|
+
? {
|
|
252
|
+
type: "aws",
|
|
253
|
+
awsAccessKeyId: registry.awsAccessKeyId,
|
|
254
|
+
awsSecretAccessKey: Redacted.value(registry.awsSecretAccessKey),
|
|
255
|
+
awsRegion: registry.awsRegion,
|
|
256
|
+
}
|
|
257
|
+
: "serviceAccountJson" in registry
|
|
258
|
+
? { type: "gcp", serviceAccountJson: Redacted.value(registry.serviceAccountJson) }
|
|
259
|
+
: {
|
|
260
|
+
type: "registry",
|
|
261
|
+
username: registry.username,
|
|
262
|
+
password: Redacted.value(registry.password),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** Uploads one `COPY` context unless E2B already has it. */
|
|
266
|
+
const uploadContext = Effect.fn(function* (
|
|
267
|
+
templateId: string,
|
|
268
|
+
hash: string,
|
|
269
|
+
files: ReadonlyArray<UploadFile>,
|
|
270
|
+
) {
|
|
271
|
+
const link = yield* templates.getTemplateFileUploadLink({ templateID: templateId, hash });
|
|
272
|
+
if (link.present) return;
|
|
273
|
+
if (link.url === undefined) {
|
|
274
|
+
return yield* new E2BUnexpectedResponse({
|
|
275
|
+
message: `E2B returned no upload URL for context ${hash} of template ${templateId}`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
const archive = yield* tarGzip(
|
|
279
|
+
yield* Effect.forEach(files, (file) =>
|
|
280
|
+
Effect.map(file.read, (contents) => ({ path: file.path, mode: file.mode, contents })),
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
const client = yield* HttpClient.HttpClient;
|
|
284
|
+
const headers = Object.fromEntries(
|
|
285
|
+
Object.entries(link.headers ?? {}).filter(
|
|
286
|
+
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
// Raw HTTP: a presigned upload URL the API handed back, used as-is. The
|
|
290
|
+
// body is sized (presigned PUTs reject chunked transfer encoding), and
|
|
291
|
+
// carries only the headers E2B returned: the URL is signed without a
|
|
292
|
+
// Content-Type, so sending one fails with SignatureDoesNotMatch.
|
|
293
|
+
const response = yield* client
|
|
294
|
+
.execute(
|
|
295
|
+
HttpClientRequest.put(link.url).pipe(
|
|
296
|
+
HttpClientRequest.bodyUint8Array(archive),
|
|
297
|
+
HttpClientRequest.removeHeader("content-type"),
|
|
298
|
+
HttpClientRequest.setHeaders(headers),
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
.pipe(Effect.retry({ times: 4, schedule: Schedule.exponential("500 millis") }));
|
|
302
|
+
if (response.status < 200 || response.status >= 300) {
|
|
303
|
+
const body = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
304
|
+
return yield* new E2BUnexpectedResponse({
|
|
305
|
+
message: `uploading build context ${hash} for template ${templateId} failed with HTTP ${response.status}: ${body.slice(0, 500)}`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
/** Polls a build until it is ready; fails with its reason and the tail of its logs. */
|
|
311
|
+
export const waitForBuild = Effect.fn(function* (templateId: string, buildId: string) {
|
|
312
|
+
const status = yield* templates
|
|
313
|
+
.getTemplateBuildStatus({ templateID: templateId, buildID: buildId })
|
|
314
|
+
.pipe(
|
|
315
|
+
Effect.repeat({
|
|
316
|
+
// Image pulls and dependency installs take minutes; bounded at ~20 min.
|
|
317
|
+
schedule: Schedule.spaced(Duration.seconds(3)),
|
|
318
|
+
until: (info) => info.status === "ready" || info.status === "error",
|
|
319
|
+
times: 400,
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
if (status.status !== "ready") {
|
|
323
|
+
// `logs` is always empty on the current build path; `logEntries` has them.
|
|
324
|
+
const logs = (status.reason?.logEntries ?? status.logEntries)
|
|
325
|
+
.slice(-40)
|
|
326
|
+
.map((entry) => entry.message);
|
|
327
|
+
return yield* new TemplateBuildFailed({
|
|
328
|
+
message: [
|
|
329
|
+
`E2B template build ${buildId} failed: ${status.reason?.message ?? status.status}`,
|
|
330
|
+
...logs.slice(-15).map((line) => ` ${line}`),
|
|
331
|
+
].join("\n"),
|
|
332
|
+
templateId,
|
|
333
|
+
buildId,
|
|
334
|
+
reason: status.reason?.message,
|
|
335
|
+
logs,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return status;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/** Runs a planned build to completion. */
|
|
342
|
+
export const runTemplateBuild = Effect.fn(function* (
|
|
343
|
+
spec: TemplateBuildSpec,
|
|
344
|
+
plan: TemplateBuildPlan,
|
|
345
|
+
) {
|
|
346
|
+
const created = yield* templates.createTemplate({
|
|
347
|
+
name: spec.name,
|
|
348
|
+
tags: spec.tags === undefined ? undefined : [...spec.tags],
|
|
349
|
+
cpuCount: spec.cpuCount,
|
|
350
|
+
memoryMB: spec.memoryMB,
|
|
351
|
+
minFreeDiskMb: spec.minFreeDiskMb,
|
|
352
|
+
});
|
|
353
|
+
yield* Effect.forEach(
|
|
354
|
+
[...plan.uploads],
|
|
355
|
+
([hash, files]) => uploadContext(created.templateID, hash, files),
|
|
356
|
+
{ concurrency: 4, discard: true },
|
|
357
|
+
);
|
|
358
|
+
yield* templates.startTemplateBuild({
|
|
359
|
+
templateID: created.templateID,
|
|
360
|
+
buildID: created.buildID,
|
|
361
|
+
fromImage: plan.fromImage,
|
|
362
|
+
fromTemplate: plan.fromTemplate,
|
|
363
|
+
fromImageRegistry: spec.registry === undefined ? undefined : registryOf(spec.registry),
|
|
364
|
+
steps: plan.steps.map((step) => ({
|
|
365
|
+
type: step.type,
|
|
366
|
+
args: [...step.args],
|
|
367
|
+
filesHash: step.filesHash,
|
|
368
|
+
force: spec.force,
|
|
369
|
+
})),
|
|
370
|
+
startCmd: plan.startCmd,
|
|
371
|
+
readyCmd: plan.readyCmd,
|
|
372
|
+
force: spec.force,
|
|
373
|
+
});
|
|
374
|
+
yield* waitForBuild(created.templateID, created.buildID);
|
|
375
|
+
return {
|
|
376
|
+
templateId: created.templateID,
|
|
377
|
+
buildId: created.buildID,
|
|
378
|
+
names: created.names,
|
|
379
|
+
aliases: created.aliases,
|
|
380
|
+
tags: created.tags,
|
|
381
|
+
public: created.public,
|
|
382
|
+
} satisfies TemplateBuildResult;
|
|
383
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lowers an image source (`image`, `dockerfile` + `context`, `main`) to E2B
|
|
3
|
+
* build steps through `@alchemy.run/docker/BuildPlan`.
|
|
4
|
+
*
|
|
5
|
+
* E2B does not run `docker build`: its builder takes a base image plus a
|
|
6
|
+
* list of steps (`RUN`, `COPY`, `ENV`, `WORKDIR`, `USER`) and a start
|
|
7
|
+
* command. `BuildPlan.plan` parses the Dockerfile, bundles `main`, collects
|
|
8
|
+
* the context files every `COPY` reads, and rejects what E2B's builder
|
|
9
|
+
* cannot run ({@link E2BCapabilities}). This module maps the plan's final
|
|
10
|
+
* stage onto E2B's steps, one `COPY` step and upload per source.
|
|
11
|
+
*
|
|
12
|
+
* Like E2B's own SDK, the build starts as `root` in `/` and ends as `user`
|
|
13
|
+
* in `/home/user` unless the Dockerfile sets `USER` / `WORKDIR` itself.
|
|
14
|
+
*
|
|
15
|
+
* Deploy-time only: `BuildPlan` bundles and reads the filesystem.
|
|
16
|
+
*/
|
|
17
|
+
import * as BuildPlan from "@alchemy.run/docker/BuildPlan";
|
|
18
|
+
import type { ImageSourceProps } from "@alchemy.run/docker/ImageSource";
|
|
19
|
+
import * as Effect from "effect/Effect";
|
|
20
|
+
import * as FileSystem from "effect/FileSystem";
|
|
21
|
+
import { UnsupportedDockerfileInstruction } from "../Errors.ts";
|
|
22
|
+
import {
|
|
23
|
+
filesHash,
|
|
24
|
+
type LoweredSource,
|
|
25
|
+
type PlannedStep,
|
|
26
|
+
type UploadFile,
|
|
27
|
+
} from "./TemplateBuild.ts";
|
|
28
|
+
|
|
29
|
+
/** What E2B's template builder can build. */
|
|
30
|
+
export const E2BCapabilities: BuildPlan.Capabilities = {
|
|
31
|
+
target: "E2B.Template",
|
|
32
|
+
multiStage: false,
|
|
33
|
+
heredocs: false,
|
|
34
|
+
runMounts: false,
|
|
35
|
+
remoteAdd: false,
|
|
36
|
+
platforms: ["linux/amd64"],
|
|
37
|
+
// E2B's builder takes no build args or build secrets: arg values are
|
|
38
|
+
// written into the steps, and secret mounts fail at plan.
|
|
39
|
+
buildArgs: false,
|
|
40
|
+
secrets: false,
|
|
41
|
+
instructions: {
|
|
42
|
+
SHELL: "unsupported",
|
|
43
|
+
EXPOSE: "ignored",
|
|
44
|
+
LABEL: "ignored",
|
|
45
|
+
VOLUME: "ignored",
|
|
46
|
+
HEALTHCHECK: "ignored",
|
|
47
|
+
STOPSIGNAL: "ignored",
|
|
48
|
+
ONBUILD: "ignored",
|
|
49
|
+
MAINTAINER: "ignored",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** `$VAR`, `${VAR}`, `${VAR:-default}` and `${VAR:+alt}` from ARG/ENV values, as Docker expands them. */
|
|
54
|
+
export const substitute = (text: string, vars: ReadonlyMap<string, string>): string =>
|
|
55
|
+
text.replace(
|
|
56
|
+
/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::([-+])([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
57
|
+
(
|
|
58
|
+
_,
|
|
59
|
+
braced: string | undefined,
|
|
60
|
+
op: string | undefined,
|
|
61
|
+
word: string | undefined,
|
|
62
|
+
bare: string | undefined,
|
|
63
|
+
) => {
|
|
64
|
+
const name = braced ?? bare!;
|
|
65
|
+
const value = vars.get(name);
|
|
66
|
+
if (op === "-") return value !== undefined && value !== "" ? value : (word ?? "");
|
|
67
|
+
if (op === "+") return value !== undefined && value !== "" ? (word ?? "") : "";
|
|
68
|
+
return value ?? "";
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const shellQuote = (word: string) =>
|
|
73
|
+
/^[A-Za-z0-9_\-./=:@%+,]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
74
|
+
|
|
75
|
+
/** An exec-form argv as one shell command, for E2B's string steps. */
|
|
76
|
+
export const shellCommand = (argv: ReadonlyArray<string>) => argv.map(shellQuote).join(" ");
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Lowers a plan's final stage to E2B steps. `args` override `ARG` defaults,
|
|
80
|
+
* like `docker build --build-arg`. An `ARG` becomes an `ENV` step, as in
|
|
81
|
+
* E2B's SDK, so `RUN` steps read it.
|
|
82
|
+
*/
|
|
83
|
+
export const lowerPlan = (plan: BuildPlan.BuildPlan, args: Readonly<Record<string, string>> = {}) =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const fs = yield* FileSystem.FileSystem;
|
|
86
|
+
const fail = (message: string, line: number) =>
|
|
87
|
+
Effect.fail(new UnsupportedDockerfileInstruction({ message, line }));
|
|
88
|
+
/** An ARG's value: the build arg, else its default, else (in a stage) the global value. */
|
|
89
|
+
const argValue = (
|
|
90
|
+
name: string,
|
|
91
|
+
defaultValue: string | undefined,
|
|
92
|
+
vars: ReadonlyMap<string, string>,
|
|
93
|
+
inherited?: string,
|
|
94
|
+
) => args[name] ?? (defaultValue !== undefined ? substitute(defaultValue, vars) : inherited);
|
|
95
|
+
|
|
96
|
+
const global = new Map<string, string>();
|
|
97
|
+
for (const arg of plan.dockerfile.globalArgs) {
|
|
98
|
+
for (const [name, defaultValue] of arg.vars) {
|
|
99
|
+
const value = argValue(name, defaultValue, global);
|
|
100
|
+
if (value !== undefined) global.set(name, value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const fromImage = substitute(plan.base, global);
|
|
104
|
+
// Global ARGs only parameterise FROM; the stage sees the ones it redeclares.
|
|
105
|
+
const vars = new Map<string, string>();
|
|
106
|
+
|
|
107
|
+
const steps: PlannedStep[] = [
|
|
108
|
+
{ type: "USER", args: ["root"] },
|
|
109
|
+
{ type: "WORKDIR", args: ["/"] },
|
|
110
|
+
];
|
|
111
|
+
const uploads = new Map<string, ReadonlyArray<UploadFile>>();
|
|
112
|
+
let userChanged = false;
|
|
113
|
+
let workdirChanged = false;
|
|
114
|
+
|
|
115
|
+
for (const instruction of plan.instructions) {
|
|
116
|
+
switch (instruction._tag) {
|
|
117
|
+
case "ARG": {
|
|
118
|
+
const set: string[] = [];
|
|
119
|
+
for (const [name, defaultValue] of instruction.vars) {
|
|
120
|
+
const value = argValue(name, defaultValue, vars, global.get(name));
|
|
121
|
+
if (value === undefined) continue;
|
|
122
|
+
vars.set(name, value);
|
|
123
|
+
set.push(name, value);
|
|
124
|
+
}
|
|
125
|
+
if (set.length > 0) steps.push({ type: "ENV", args: set });
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "ENV": {
|
|
129
|
+
const pairs = instruction.vars.map(
|
|
130
|
+
([name, value]) => [name, substitute(value, vars)] as const,
|
|
131
|
+
);
|
|
132
|
+
for (const [name, value] of pairs) vars.set(name, value);
|
|
133
|
+
steps.push({ type: "ENV", args: pairs.flat() });
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "RUN":
|
|
137
|
+
steps.push({
|
|
138
|
+
type: "RUN",
|
|
139
|
+
args: [
|
|
140
|
+
instruction.command.form === "exec"
|
|
141
|
+
? shellCommand(instruction.command.argv)
|
|
142
|
+
: instruction.command.script,
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
case "COPY":
|
|
147
|
+
case "ADD": {
|
|
148
|
+
const flag = (name: string) =>
|
|
149
|
+
instruction.flags.find((candidate) => candidate.name === name)?.value ?? "";
|
|
150
|
+
const dest = substitute(instruction.destination, vars);
|
|
151
|
+
for (const raw of instruction.sources) {
|
|
152
|
+
const src = substitute(raw, vars);
|
|
153
|
+
const files = BuildPlan.filesFor(plan, { ...instruction, sources: [src] });
|
|
154
|
+
if (files.length === 0) {
|
|
155
|
+
return yield* fail(
|
|
156
|
+
`${instruction._tag} ${src} matches no files in the build context`,
|
|
157
|
+
instruction.line,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const upload = files.map((file): UploadFile => ({
|
|
161
|
+
path: file.path,
|
|
162
|
+
mode: file.mode,
|
|
163
|
+
size: file.size,
|
|
164
|
+
sha256: file.sha256,
|
|
165
|
+
read: BuildPlan.readContextFile(file).pipe(
|
|
166
|
+
Effect.provideService(FileSystem.FileSystem, fs),
|
|
167
|
+
),
|
|
168
|
+
}));
|
|
169
|
+
const hash = filesHash(src, dest, upload);
|
|
170
|
+
uploads.set(hash, upload);
|
|
171
|
+
steps.push({
|
|
172
|
+
type: "COPY",
|
|
173
|
+
args: [src, dest, substitute(flag("chown"), vars), flag("chmod")],
|
|
174
|
+
filesHash: hash,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case "WORKDIR":
|
|
180
|
+
steps.push({ type: "WORKDIR", args: [substitute(instruction.value, vars)] });
|
|
181
|
+
workdirChanged = true;
|
|
182
|
+
break;
|
|
183
|
+
case "USER":
|
|
184
|
+
steps.push({ type: "USER", args: [substitute(instruction.value, vars)] });
|
|
185
|
+
userChanged = true;
|
|
186
|
+
break;
|
|
187
|
+
// CMD / ENTRYPOINT are the start command; the rest are ignored
|
|
188
|
+
// (BuildPlan rejected what E2B does not support).
|
|
189
|
+
default:
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!userChanged) steps.push({ type: "USER", args: ["user"] });
|
|
195
|
+
if (!workdirChanged) steps.push({ type: "WORKDIR", args: ["/home/user"] });
|
|
196
|
+
|
|
197
|
+
const argv = BuildPlan.startCommand(plan);
|
|
198
|
+
return {
|
|
199
|
+
fromImage,
|
|
200
|
+
steps,
|
|
201
|
+
uploads,
|
|
202
|
+
startCmd: argv === undefined ? undefined : shellCommand(argv),
|
|
203
|
+
} satisfies LoweredSource;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export interface PlanSourceOptions {
|
|
207
|
+
/** Port a `main` program listens on. */
|
|
208
|
+
readonly port?: number;
|
|
209
|
+
/** Bootstrap wrapped around an Effect-native `main`. */
|
|
210
|
+
readonly bootstrap?: (importPath: string) => string;
|
|
211
|
+
/** `ARG` values. */
|
|
212
|
+
readonly args?: Readonly<Record<string, string>>;
|
|
213
|
+
/** Install lines from the program's mount capabilities (`alchemy/Mount`). */
|
|
214
|
+
readonly install?: ReadonlyArray<string>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Plans an image source with E2B's capabilities and lowers it to E2B steps. */
|
|
218
|
+
export const planSource = (source: ImageSourceProps, options: PlanSourceOptions = {}) =>
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
const plan = yield* BuildPlan.plan(source, {
|
|
221
|
+
capabilities: E2BCapabilities,
|
|
222
|
+
...(options.port !== undefined ? { port: options.port } : {}),
|
|
223
|
+
...(options.bootstrap !== undefined ? { bootstrap: options.bootstrap } : {}),
|
|
224
|
+
...(options.install !== undefined && options.install.length > 0
|
|
225
|
+
? { install: options.install }
|
|
226
|
+
: {}),
|
|
227
|
+
});
|
|
228
|
+
for (const warning of plan.warnings) yield* Effect.logDebug(warning);
|
|
229
|
+
return yield* lowerPlan(plan, { ...plan.args, ...options.args });
|
|
230
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The volume-content client over E2B's HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Volume content is authenticated with the volume's own token, which the
|
|
5
|
+
* control plane returns from `getVolume` (team key). The token is fetched
|
|
6
|
+
* once per volume on first use and kept for the life of the runtime; a failed
|
|
7
|
+
* lookup is not cached, so the next call retries it.
|
|
8
|
+
*/
|
|
9
|
+
import { volumeConnectionFor } from "@distilled.cloud/e2b";
|
|
10
|
+
import type { VolumeConnection } from "@distilled.cloud/e2b/Credentials";
|
|
11
|
+
import * as volumeContent from "@distilled.cloud/e2b/volume_content";
|
|
12
|
+
import * as volumes from "@distilled.cloud/e2b/volumes";
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
15
|
+
import type * as HttpClient from "effect/http/HttpClient";
|
|
16
|
+
import * as Layer from "effect/Layer";
|
|
17
|
+
import type { E2BAuth } from "../E2BBindings.ts";
|
|
18
|
+
import type { ReadWriteVolumeClient } from "../VolumeFiles.ts";
|
|
19
|
+
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
|
|
22
|
+
/** The volume-token connection plus an HTTP client: what volume-content calls need. */
|
|
23
|
+
type VolumeLayer = Layer.Layer<VolumeConnection | HttpClient.HttpClient>;
|
|
24
|
+
|
|
25
|
+
export const makeVolumeFiles = (
|
|
26
|
+
auth: E2BAuth,
|
|
27
|
+
volumeIdOf: Effect.Effect<string>,
|
|
28
|
+
): ReadWriteVolumeClient => {
|
|
29
|
+
let cached: { readonly volumeId: string; readonly layer: VolumeLayer } | undefined;
|
|
30
|
+
/** The volume-token connection, looked up once. */
|
|
31
|
+
const connection = Effect.suspend(() =>
|
|
32
|
+
cached !== undefined
|
|
33
|
+
? Effect.succeed(cached)
|
|
34
|
+
: Effect.flatMap(volumeIdOf, (volumeId) =>
|
|
35
|
+
auth.authorize(volumes.getVolume({ volumeID: volumeId })).pipe(
|
|
36
|
+
Effect.map((volume) => {
|
|
37
|
+
cached = {
|
|
38
|
+
volumeId,
|
|
39
|
+
layer: Layer.mergeAll(volumeConnectionFor(volume), FetchHttpClient.layer),
|
|
40
|
+
};
|
|
41
|
+
return cached;
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
/** Runs a volume-content call, given the volume id, with the volume's token. */
|
|
47
|
+
const inVolume = <A, E>(
|
|
48
|
+
call: (volumeID: string) => Effect.Effect<A, E, Layer.Success<VolumeLayer>>,
|
|
49
|
+
) =>
|
|
50
|
+
Effect.flatMap(connection, ({ volumeId, layer }) => call(volumeId).pipe(Effect.provide(layer)));
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
read: (path) => inVolume((volumeID) => volumeContent.readVolumeFile({ volumeID, path })),
|
|
54
|
+
readText: (path) =>
|
|
55
|
+
inVolume((volumeID) => volumeContent.readVolumeFile({ volumeID, path })).pipe(
|
|
56
|
+
Effect.map((bytes) => decoder.decode(bytes)),
|
|
57
|
+
),
|
|
58
|
+
list: (path, options) =>
|
|
59
|
+
inVolume((volumeID) =>
|
|
60
|
+
volumeContent.listVolumeDir({
|
|
61
|
+
volumeID,
|
|
62
|
+
path,
|
|
63
|
+
...(options?.depth === undefined ? {} : { depth: options.depth }),
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
stat: (path) => inVolume((volumeID) => volumeContent.statVolumePath({ volumeID, path })),
|
|
67
|
+
exists: (path) =>
|
|
68
|
+
inVolume((volumeID) => volumeContent.statVolumePath({ volumeID, path })).pipe(
|
|
69
|
+
Effect.as(true),
|
|
70
|
+
Effect.catchTag("NotFound", () => Effect.succeed(false)),
|
|
71
|
+
),
|
|
72
|
+
write: (path, data, options) =>
|
|
73
|
+
inVolume((volumeID) =>
|
|
74
|
+
volumeContent.writeVolumeFile({ volumeID, path, body: data, force: true, ...options }),
|
|
75
|
+
),
|
|
76
|
+
makeDir: (path, options) =>
|
|
77
|
+
inVolume((volumeID) =>
|
|
78
|
+
volumeContent.makeVolumeDir({ volumeID, path, force: true, ...options }),
|
|
79
|
+
),
|
|
80
|
+
chmod: (path, options) =>
|
|
81
|
+
inVolume((volumeID) => volumeContent.updateVolumePath({ volumeID, path, ...options })),
|
|
82
|
+
remove: (path) =>
|
|
83
|
+
inVolume((volumeID) => volumeContent.removeVolumePath({ volumeID, path })).pipe(
|
|
84
|
+
Effect.asVoid,
|
|
85
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
};
|