@alchemy.run/archil 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 +57 -4
- package/lib/AWS/S3Disk.d.ts +68 -0
- package/lib/AWS/S3Disk.d.ts.map +1 -0
- package/lib/AWS/S3Disk.js +100 -0
- package/lib/AWS/S3Disk.js.map +1 -0
- package/lib/AWS/index.d.ts +2 -0
- package/lib/AWS/index.d.ts.map +1 -0
- package/lib/AWS/index.js +2 -0
- package/lib/AWS/index.js.map +1 -0
- package/lib/ApiToken.d.ts +63 -0
- package/lib/ApiToken.d.ts.map +1 -0
- package/lib/ApiToken.js +121 -0
- package/lib/ApiToken.js.map +1 -0
- package/lib/ArchilBindings.d.ts +65 -0
- package/lib/ArchilBindings.d.ts.map +1 -0
- package/lib/ArchilBindings.js +128 -0
- package/lib/ArchilBindings.js.map +1 -0
- package/lib/AuthProvider.d.ts +40 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +87 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +27 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/Branch.d.ts +101 -0
- package/lib/Branch.d.ts.map +1 -0
- package/lib/Branch.js +117 -0
- package/lib/Branch.js.map +1 -0
- package/lib/BranchDisk.d.ts +42 -0
- package/lib/BranchDisk.d.ts.map +1 -0
- package/lib/BranchDisk.js +14 -0
- package/lib/BranchDisk.js.map +1 -0
- package/lib/Checkpoint.d.ts +78 -0
- package/lib/Checkpoint.d.ts.map +1 -0
- package/lib/Checkpoint.js +95 -0
- package/lib/Checkpoint.js.map +1 -0
- package/lib/Clients.d.ts +369 -0
- package/lib/Clients.d.ts.map +1 -0
- package/lib/Clients.js +334 -0
- package/lib/Clients.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +69 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +21 -0
- package/lib/ConnectSandbox.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/CreateDisk.d.ts +90 -0
- package/lib/CreateDisk.d.ts.map +1 -0
- package/lib/CreateDisk.js +47 -0
- package/lib/CreateDisk.js.map +1 -0
- package/lib/CreateImage.d.ts +42 -0
- package/lib/CreateImage.d.ts.map +1 -0
- package/lib/CreateImage.js +19 -0
- package/lib/CreateImage.js.map +1 -0
- package/lib/CreateSandbox.d.ts +104 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +98 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/Credentials.d.ts +44 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +57 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Disk.d.ts +283 -0
- package/lib/Disk.d.ts.map +1 -0
- package/lib/Disk.js +294 -0
- package/lib/Disk.js.map +1 -0
- package/lib/DiskObjectsHttp.d.ts +20 -0
- package/lib/DiskObjectsHttp.d.ts.map +1 -0
- package/lib/DiskObjectsHttp.js +51 -0
- package/lib/DiskObjectsHttp.js.map +1 -0
- package/lib/DiskUser.d.ts +189 -0
- package/lib/DiskUser.d.ts.map +1 -0
- package/lib/DiskUser.js +243 -0
- package/lib/DiskUser.js.map +1 -0
- package/lib/ExecDisk.d.ts +57 -0
- package/lib/ExecDisk.d.ts.map +1 -0
- package/lib/ExecDisk.js +3 -0
- package/lib/ExecDisk.js.map +1 -0
- package/lib/ExecDiskHttp.d.ts +5 -0
- package/lib/ExecDiskHttp.d.ts.map +1 -0
- package/lib/ExecDiskHttp.js +18 -0
- package/lib/ExecDiskHttp.js.map +1 -0
- package/lib/ExecDisks.d.ts +54 -0
- package/lib/ExecDisks.d.ts.map +1 -0
- package/lib/ExecDisks.js +37 -0
- package/lib/ExecDisks.js.map +1 -0
- package/lib/Image.d.ts +188 -0
- package/lib/Image.d.ts.map +1 -0
- package/lib/Image.js +191 -0
- package/lib/Image.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/MountDisk.d.ts +190 -0
- package/lib/MountDisk.d.ts.map +1 -0
- package/lib/MountDisk.js +227 -0
- package/lib/MountDisk.js.map +1 -0
- package/lib/Providers.d.ts +25 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +48 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadDisk.d.ts +55 -0
- package/lib/ReadDisk.d.ts.map +1 -0
- package/lib/ReadDisk.js +3 -0
- package/lib/ReadDisk.js.map +1 -0
- package/lib/ReadWriteDisk.d.ts +26 -0
- package/lib/ReadWriteDisk.d.ts.map +1 -0
- package/lib/ReadWriteDisk.js +3 -0
- package/lib/ReadWriteDisk.js.map +1 -0
- package/lib/Sandbox.d.ts +507 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +729 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxOptions.d.ts +185 -0
- package/lib/SandboxOptions.d.ts.map +1 -0
- package/lib/SandboxOptions.js +102 -0
- package/lib/SandboxOptions.js.map +1 -0
- package/lib/SandboxPortToken.d.ts +64 -0
- package/lib/SandboxPortToken.d.ts.map +1 -0
- package/lib/SandboxPortToken.js +77 -0
- package/lib/SandboxPortToken.js.map +1 -0
- package/lib/WriteDisk.d.ts +37 -0
- package/lib/WriteDisk.d.ts.map +1 -0
- package/lib/WriteDisk.js +3 -0
- package/lib/WriteDisk.js.map +1 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/DeployerKey.d.ts +21 -0
- package/lib/internal/DeployerKey.d.ts.map +1 -0
- package/lib/internal/DeployerKey.js +34 -0
- package/lib/internal/DeployerKey.js.map +1 -0
- package/lib/internal/archil.d.ts +87 -0
- package/lib/internal/archil.d.ts.map +1 -0
- package/lib/internal/archil.js +154 -0
- package/lib/internal/archil.js.map +1 -0
- package/lib/internal/hosted.d.ts +90 -0
- package/lib/internal/hosted.d.ts.map +1 -0
- package/lib/internal/hosted.js +291 -0
- package/lib/internal/hosted.js.map +1 -0
- package/package.json +79 -3
- package/src/AWS/S3Disk.ts +125 -0
- package/src/AWS/index.ts +1 -0
- package/src/ApiToken.ts +168 -0
- package/src/ArchilBindings.ts +179 -0
- package/src/AuthProvider.ts +122 -0
- package/src/Bootstrap.ts +54 -0
- package/src/Branch.ts +199 -0
- package/src/BranchDisk.ts +57 -0
- package/src/Checkpoint.ts +145 -0
- package/src/Clients.ts +954 -0
- package/src/ConnectSandbox.ts +91 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateDisk.ts +165 -0
- package/src/CreateImage.ts +73 -0
- package/src/CreateSandbox.ts +258 -0
- package/src/Credentials.ts +101 -0
- package/src/Disk.ts +468 -0
- package/src/DiskObjectsHttp.ts +64 -0
- package/src/DiskUser.ts +369 -0
- package/src/ExecDisk.ts +62 -0
- package/src/ExecDiskHttp.ts +25 -0
- package/src/ExecDisks.ts +100 -0
- package/src/Image.ts +316 -0
- package/src/InferEnv.ts +27 -0
- package/src/MountDisk.ts +466 -0
- package/src/Providers.ts +75 -0
- package/src/ReadDisk.ts +61 -0
- package/src/ReadWriteDisk.ts +31 -0
- package/src/Sandbox.ts +1064 -0
- package/src/SandboxOptions.ts +230 -0
- package/src/SandboxPortToken.ts +125 -0
- package/src/WriteDisk.ts +42 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +59 -0
- package/src/internal/DeployerKey.ts +63 -0
- package/src/internal/archil.ts +273 -0
- package/src/internal/hosted.ts +402 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as Binding from "alchemy/Binding";
|
|
2
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import { ApiAccess } from "./ArchilBindings.ts";
|
|
6
|
+
import { makeSandboxOps, type SandboxOps } from "./Clients.ts";
|
|
7
|
+
import type { Sandbox } from "./Sandbox.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The handle on a deploy-time sandbox: a {@link SandboxHandle} whose id and
|
|
11
|
+
* name resolve at run time.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConnectSandboxClient extends SandboxOps {
|
|
14
|
+
readonly sandboxId: Effect.Effect<string, never, RuntimeContext>;
|
|
15
|
+
readonly name: Effect.Effect<string, never, RuntimeContext>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Act on a sandbox declared at deploy time from a Worker, Lambda, or any
|
|
20
|
+
* other host: run commands, start background processes and terminals,
|
|
21
|
+
* move files, pause and resume, fork it into a new sandbox, open ports,
|
|
22
|
+
* and change its network policy. A program sandbox (`Archil.Sandbox` with
|
|
23
|
+
* `main`) is a sandbox too.
|
|
24
|
+
*
|
|
25
|
+
* ### Driving a Dev Box
|
|
26
|
+
* **Example:** Build on the dev box, fork a preview per pull request
|
|
27
|
+
* ```typescript
|
|
28
|
+
* export default class Api extends Cloudflare.Worker<Api>()(
|
|
29
|
+
* "Api",
|
|
30
|
+
* { main: import.meta.url },
|
|
31
|
+
* Effect.gen(function* () {
|
|
32
|
+
* const devbox = yield* Archil.ConnectSandbox(DevBox);
|
|
33
|
+
* return {
|
|
34
|
+
* fetch: Effect.gen(function* () {
|
|
35
|
+
* yield* devbox.exec("git pull && pnpm build", { cwd: "/workspace" });
|
|
36
|
+
* yield* devbox.files.write("/workspace/.env", "PREVIEW=1\n");
|
|
37
|
+
* const preview = yield* devbox.fork({ name: "pr-42" });
|
|
38
|
+
* const port = yield* preview.exposePort(3000);
|
|
39
|
+
* return HttpServerResponse.text(`https://${port.hostname}`);
|
|
40
|
+
* }),
|
|
41
|
+
* };
|
|
42
|
+
* }).pipe(Effect.provide(Archil.ConnectSandboxHttp)),
|
|
43
|
+
* ) {}
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ### Proxying to a Port
|
|
47
|
+
* **Example:** A Worker in front of the dev box's server
|
|
48
|
+
* ```typescript
|
|
49
|
+
* export default class Api extends Cloudflare.Worker<Api>()(
|
|
50
|
+
* "Api",
|
|
51
|
+
* { main: import.meta.url },
|
|
52
|
+
* Effect.gen(function* () {
|
|
53
|
+
* const devbox = yield* Archil.ConnectSandbox(DevBox);
|
|
54
|
+
* return {
|
|
55
|
+
* fetch: Effect.gen(function* () {
|
|
56
|
+
* const port = yield* devbox.getTcpPort(3000);
|
|
57
|
+
* return yield* port.fetch(yield* HttpServerRequest);
|
|
58
|
+
* }),
|
|
59
|
+
* };
|
|
60
|
+
* }).pipe(Effect.provide(Archil.ConnectSandboxHttp)),
|
|
61
|
+
* ) {}
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @binding
|
|
65
|
+
* @product Sandbox
|
|
66
|
+
*/
|
|
67
|
+
export interface ConnectSandbox extends Binding.Service<
|
|
68
|
+
ConnectSandbox,
|
|
69
|
+
"Archil.ConnectSandbox",
|
|
70
|
+
(sandbox: Sandbox) => Effect.Effect<ConnectSandboxClient>
|
|
71
|
+
> {}
|
|
72
|
+
|
|
73
|
+
export const ConnectSandbox = Binding.Service<ConnectSandbox>("Archil.ConnectSandbox");
|
|
74
|
+
|
|
75
|
+
/** {@link ConnectSandbox} over Archil's HTTP API, usable from any host. */
|
|
76
|
+
export const ConnectSandboxHttp = Layer.effect(
|
|
77
|
+
ConnectSandbox,
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const access = yield* ApiAccess;
|
|
80
|
+
return Effect.fn(function* (sandbox: Sandbox) {
|
|
81
|
+
const authorize = yield* access();
|
|
82
|
+
const sandboxId = yield* sandbox.sandboxId;
|
|
83
|
+
const name = yield* sandbox.name;
|
|
84
|
+
return {
|
|
85
|
+
sandboxId,
|
|
86
|
+
name,
|
|
87
|
+
...makeSandboxOps(sandboxId, authorize),
|
|
88
|
+
} satisfies ConnectSandboxClient;
|
|
89
|
+
});
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as archil from "@distilled.cloud/archil";
|
|
2
|
+
import * as Binding from "alchemy/Binding";
|
|
3
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import { ApiAccess, runtimeName, runtimePrefix } from "./ArchilBindings.ts";
|
|
8
|
+
import { makeDiskHandle, type DiskHandle } from "./Clients.ts";
|
|
9
|
+
import { deleteDisk, ensureDisk, type DiskStorage } from "./Disk.ts";
|
|
10
|
+
import type { ArchilFailed, ArchilNotReady } from "./internal/archil.ts";
|
|
11
|
+
import { findDiskByName, listAllDisks } from "./internal/archil.ts";
|
|
12
|
+
|
|
13
|
+
export interface CreateDiskOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Where disks created by this client keep their files. Omit for
|
|
16
|
+
* Archil-managed storage. A run-time `create` can override it.
|
|
17
|
+
*/
|
|
18
|
+
storage?: DiskStorage;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CreateDiskError =
|
|
22
|
+
| archil.CreateDiskError
|
|
23
|
+
| archil.GetDiskError
|
|
24
|
+
| archil.ListDisksError
|
|
25
|
+
| ArchilFailed
|
|
26
|
+
| ArchilNotReady;
|
|
27
|
+
|
|
28
|
+
export interface CreateDiskClient {
|
|
29
|
+
/**
|
|
30
|
+
* The name prefix this client puts on everything it creates,
|
|
31
|
+
* `alc-<id>-<hash of stack/stage/id>`.
|
|
32
|
+
*/
|
|
33
|
+
readonly prefix: string;
|
|
34
|
+
/**
|
|
35
|
+
* Create a disk (or return the one that already has this name). Names
|
|
36
|
+
* are stamped with this binding's prefix, so `create({ name: "user-1" })`
|
|
37
|
+
* is the same disk on every call.
|
|
38
|
+
*/
|
|
39
|
+
create(
|
|
40
|
+
name: string,
|
|
41
|
+
options?: { readonly storage?: DiskStorage },
|
|
42
|
+
): Effect.Effect<DiskHandle, CreateDiskError, RuntimeContext>;
|
|
43
|
+
/** A disk this client created, or `undefined`. */
|
|
44
|
+
get(name: string): Effect.Effect<DiskHandle | undefined, archil.ListDisksError, RuntimeContext>;
|
|
45
|
+
/** Every disk this client created. */
|
|
46
|
+
list(): Effect.Effect<DiskHandle[], archil.ListDisksError, RuntimeContext>;
|
|
47
|
+
/** Delete a disk this client created. Deleting a missing disk succeeds. */
|
|
48
|
+
delete(
|
|
49
|
+
name: string,
|
|
50
|
+
): Effect.Effect<
|
|
51
|
+
void,
|
|
52
|
+
archil.ListDisksError | Effect.Error<ReturnType<typeof deleteDisk>>,
|
|
53
|
+
RuntimeContext
|
|
54
|
+
>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create disks at run time, one per user, session, or task, by name. This
|
|
59
|
+
* is how most apps use Archil: a disk mounts in about 50 ms, so it can
|
|
60
|
+
* belong to a single request. The client returns full disk handles (exec,
|
|
61
|
+
* grep, objects, branches, disk tokens), the same shape the deploy-time
|
|
62
|
+
* bindings return.
|
|
63
|
+
*
|
|
64
|
+
* Objects created at run time are tagged with the stack, stage and binding
|
|
65
|
+
* (`alchemy::stack`, `alchemy::stage`, `alchemy::id`) and are not deleted
|
|
66
|
+
* when the stack is destroyed. List them by those tags to clean them up.
|
|
67
|
+
* Archil has no tags, labels or descriptions, so the tags are a name
|
|
68
|
+
* prefix, `alc-<id>-<hash of stack/stage/id>`: every name this client
|
|
69
|
+
* creates is `<prefix>-<name>`, and `prefix` on the client is the value.
|
|
70
|
+
*
|
|
71
|
+
* ### Disks per User
|
|
72
|
+
* **Example:** A home disk for each user
|
|
73
|
+
* ```typescript
|
|
74
|
+
* export default class Api extends Cloudflare.Worker<Api>()(
|
|
75
|
+
* "Api",
|
|
76
|
+
* { main: import.meta.url },
|
|
77
|
+
* Effect.gen(function* () {
|
|
78
|
+
* const disks = yield* Archil.CreateDisk();
|
|
79
|
+
* return {
|
|
80
|
+
* fetch: Effect.gen(function* () {
|
|
81
|
+
* const request = yield* HttpServerRequest;
|
|
82
|
+
* const user = request.headers["x-user"] ?? "anonymous";
|
|
83
|
+
* const home = yield* disks.create(`user-${user}`);
|
|
84
|
+
* const result = yield* home.exec("ls -la /mnt/archil");
|
|
85
|
+
* return HttpServerResponse.text(result.stdout);
|
|
86
|
+
* }),
|
|
87
|
+
* };
|
|
88
|
+
* }).pipe(Effect.provide(Archil.CreateDiskHttp)),
|
|
89
|
+
* ) {}
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* ### Cleaning Up
|
|
93
|
+
* **Example:** Delete the disks one binding created
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const disks = yield* Archil.CreateDisk();
|
|
96
|
+
* for (const disk of yield* disks.list()) {
|
|
97
|
+
* yield* disks.delete(disk.name.slice(disks.prefix.length + 1));
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @binding
|
|
102
|
+
* @product Disk
|
|
103
|
+
*/
|
|
104
|
+
export interface CreateDisk extends Binding.Service<
|
|
105
|
+
CreateDisk,
|
|
106
|
+
"Archil.CreateDisk",
|
|
107
|
+
(options?: CreateDiskOptions) => Effect.Effect<CreateDiskClient>
|
|
108
|
+
> {}
|
|
109
|
+
|
|
110
|
+
export const CreateDisk = Binding.Service<CreateDisk>("Archil.CreateDisk");
|
|
111
|
+
|
|
112
|
+
/** {@link CreateDisk} over Archil's HTTP API, usable from any host. */
|
|
113
|
+
export const CreateDiskHttp = Layer.effect(
|
|
114
|
+
CreateDisk,
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
const access = yield* ApiAccess;
|
|
117
|
+
return Effect.fn(function* (options?: CreateDiskOptions) {
|
|
118
|
+
const authorize = yield* access();
|
|
119
|
+
const { prefix: owner } = yield* runtimePrefix("Disks");
|
|
120
|
+
const prefix = Effect.succeed(owner);
|
|
121
|
+
const fullName = (name: string) => Effect.succeed(runtimeName(owner, name));
|
|
122
|
+
const handle = (disk: { id: string; name: string }) =>
|
|
123
|
+
makeDiskHandle({ diskId: disk.id, name: disk.name }, authorize);
|
|
124
|
+
return {
|
|
125
|
+
prefix: owner,
|
|
126
|
+
create: (name, createOptions) =>
|
|
127
|
+
authorize(
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
const disk = yield* ensureDisk(
|
|
130
|
+
yield* fullName(name),
|
|
131
|
+
createOptions?.storage ?? options?.storage,
|
|
132
|
+
);
|
|
133
|
+
return handle(disk);
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
get: (name) =>
|
|
137
|
+
authorize(
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const diskName = yield* fullName(name);
|
|
140
|
+
const disk = yield* findDiskByName(diskName);
|
|
141
|
+
return disk && disk.status !== "deleted" ? handle(disk) : undefined;
|
|
142
|
+
}),
|
|
143
|
+
),
|
|
144
|
+
list: () =>
|
|
145
|
+
authorize(
|
|
146
|
+
Effect.gen(function* () {
|
|
147
|
+
const p = yield* prefix;
|
|
148
|
+
const disks = yield* Stream.runCollect(listAllDisks());
|
|
149
|
+
return disks
|
|
150
|
+
.filter((d) => d.name.startsWith(`${p}-`) && d.status !== "deleted")
|
|
151
|
+
.map(handle);
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
delete: (name) =>
|
|
155
|
+
authorize(
|
|
156
|
+
Effect.gen(function* () {
|
|
157
|
+
const diskName = yield* fullName(name);
|
|
158
|
+
const disk = yield* findDiskByName(diskName);
|
|
159
|
+
if (disk) yield* deleteDisk(disk.id);
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
} satisfies CreateDiskClient;
|
|
163
|
+
});
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ImageRegistry } from "@alchemy.run/docker/Registry";
|
|
2
|
+
import * as archil from "@distilled.cloud/archil";
|
|
3
|
+
import * as Binding from "alchemy/Binding";
|
|
4
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Layer from "effect/Layer";
|
|
7
|
+
import { ApiAccess } from "./ArchilBindings.ts";
|
|
8
|
+
import { pullImage, toImageAttributes, type ImageAttributes } from "./Image.ts";
|
|
9
|
+
import type { ArchilFailed, ArchilNotReady } from "./internal/archil.ts";
|
|
10
|
+
|
|
11
|
+
export interface CreateImageClient {
|
|
12
|
+
/**
|
|
13
|
+
* Pull an OCI reference into an Archil image and wait until it is ready.
|
|
14
|
+
* Takes the same `image` and `registry` props as {@link Image}.
|
|
15
|
+
*/
|
|
16
|
+
build(props: {
|
|
17
|
+
readonly image: string;
|
|
18
|
+
readonly registry?: ImageRegistry;
|
|
19
|
+
}): Effect.Effect<
|
|
20
|
+
ImageAttributes,
|
|
21
|
+
archil.CreateImageError | archil.GetImageError | ArchilFailed | ArchilNotReady,
|
|
22
|
+
RuntimeContext
|
|
23
|
+
>;
|
|
24
|
+
/** An image by id, or `undefined` when it doesn't exist. */
|
|
25
|
+
get(
|
|
26
|
+
imageId: string,
|
|
27
|
+
): Effect.Effect<ImageAttributes | undefined, archil.GetImageError, RuntimeContext>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build sandbox images at run time, e.g. one per customer repository, with
|
|
32
|
+
* the same props as the deploy-time {@link Image}. Pass the result's
|
|
33
|
+
* `imageId` to `CreateSandbox`.
|
|
34
|
+
*
|
|
35
|
+
* ### Building Images
|
|
36
|
+
* **Example:** An image per tenant
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const images = yield* Archil.CreateImage();
|
|
39
|
+
* const image = yield* images.build({ image: `ghcr.io/acme/${tenant}:latest` });
|
|
40
|
+
* const sandbox = yield* sandboxes.create({ name: tenant, image: image.imageId });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @binding
|
|
44
|
+
* @product Image
|
|
45
|
+
*/
|
|
46
|
+
export interface CreateImage extends Binding.Service<
|
|
47
|
+
CreateImage,
|
|
48
|
+
"Archil.CreateImage",
|
|
49
|
+
() => Effect.Effect<CreateImageClient>
|
|
50
|
+
> {}
|
|
51
|
+
|
|
52
|
+
export const CreateImage = Binding.Service<CreateImage>("Archil.CreateImage");
|
|
53
|
+
|
|
54
|
+
/** {@link CreateImage} over Archil's HTTP API, usable from any host. */
|
|
55
|
+
export const CreateImageHttp = Layer.effect(
|
|
56
|
+
CreateImage,
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const access = yield* ApiAccess;
|
|
59
|
+
return Effect.fn(function* () {
|
|
60
|
+
const authorize = yield* access();
|
|
61
|
+
return {
|
|
62
|
+
build: (props) => authorize(pullImage(props.image, props.registry, props.image)),
|
|
63
|
+
get: (imageId) =>
|
|
64
|
+
authorize(
|
|
65
|
+
archil.getImage({ image_id: imageId }).pipe(
|
|
66
|
+
Effect.map(({ data }) => toImageAttributes(data, data.source)),
|
|
67
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
} satisfies CreateImageClient;
|
|
71
|
+
});
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as archil from "@distilled.cloud/archil";
|
|
2
|
+
import * as Binding from "alchemy/Binding";
|
|
3
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Random from "effect/Random";
|
|
7
|
+
import { ApiAccess, runtimeName, runtimePrefix } from "./ArchilBindings.ts";
|
|
8
|
+
import { makeSandboxHandle, type SandboxHandle } from "./Clients.ts";
|
|
9
|
+
import type { Image } from "./Image.ts";
|
|
10
|
+
import type { ArchilFailed, ArchilNotReady } from "./internal/archil.ts";
|
|
11
|
+
import { findSandboxByName, listSandboxes, waitForSandbox } from "./internal/archil.ts";
|
|
12
|
+
import {
|
|
13
|
+
deleteSandbox,
|
|
14
|
+
toCreateSandboxRequest,
|
|
15
|
+
type Sandbox,
|
|
16
|
+
type SandboxProps,
|
|
17
|
+
} from "./Sandbox.ts";
|
|
18
|
+
import {
|
|
19
|
+
MountDiskUnresolved,
|
|
20
|
+
mountDiskId,
|
|
21
|
+
toSandboxMount,
|
|
22
|
+
type DiskRef,
|
|
23
|
+
type MountableDisk,
|
|
24
|
+
type MountOptionUnsupported,
|
|
25
|
+
} from "./SandboxOptions.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A run-time create: a box {@link Sandbox}'s props (no program), with a
|
|
29
|
+
* required name. A mount's disk is a `DiskHandle` (e.g. from
|
|
30
|
+
* `CreateDisk`), or a `MountDisk` reference this host bound at deploy.
|
|
31
|
+
*/
|
|
32
|
+
export interface CreateSandboxRequest extends Omit<
|
|
33
|
+
SandboxProps,
|
|
34
|
+
"name" | "main" | "handler" | "port" | "build" | "extraFiles" | "isExternal"
|
|
35
|
+
> {
|
|
36
|
+
/** Name, stamped with this binding's prefix. Lowercase letters, digits and `-`. */
|
|
37
|
+
readonly name: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CreateSandboxError =
|
|
41
|
+
| archil.CreateSandboxError
|
|
42
|
+
| archil.ForkSandboxError
|
|
43
|
+
| archil.GetSandboxError
|
|
44
|
+
| archil.ListSandboxesError
|
|
45
|
+
| ArchilFailed
|
|
46
|
+
| ArchilNotReady
|
|
47
|
+
| MountOptionUnsupported
|
|
48
|
+
| MountDiskUnresolved;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A mount's disk id at run time: a handle carries it; a declared disk
|
|
52
|
+
* reads the id `MountDisk` bound into this host at deploy.
|
|
53
|
+
*/
|
|
54
|
+
const runtimeDiskId = Effect.fn(function* (disk: MountableDisk, path: string) {
|
|
55
|
+
if (isDiskRef(disk)) return disk.diskId;
|
|
56
|
+
if (Effect.isEffect(disk)) {
|
|
57
|
+
return yield* new MountDiskUnresolved({
|
|
58
|
+
path,
|
|
59
|
+
message: `The disk mounted at ${path} is a declaration: bind it with Archil.MountDisk(disk) and pass that reference, or pass a DiskHandle.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const diskId = yield* disk.diskId;
|
|
63
|
+
return yield* diskId;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const isDiskRef = (disk: MountableDisk): disk is DiskRef => mountDiskId(disk) !== undefined;
|
|
67
|
+
|
|
68
|
+
export interface CreateSandboxClient {
|
|
69
|
+
/**
|
|
70
|
+
* The name prefix this client puts on everything it creates,
|
|
71
|
+
* `alc-<id>-<hash of stack/stage/id>`.
|
|
72
|
+
*/
|
|
73
|
+
readonly prefix: string;
|
|
74
|
+
/**
|
|
75
|
+
* Create a sandbox (or return the one that already has this name) and
|
|
76
|
+
* wait until it is running. Takes a {@link Sandbox}'s props; the bound
|
|
77
|
+
* image is the default.
|
|
78
|
+
*/
|
|
79
|
+
create(
|
|
80
|
+
request: CreateSandboxRequest,
|
|
81
|
+
): Effect.Effect<SandboxHandle, CreateSandboxError, RuntimeContext>;
|
|
82
|
+
/** A sandbox this client created, or `undefined`. */
|
|
83
|
+
get(
|
|
84
|
+
name: string,
|
|
85
|
+
): Effect.Effect<SandboxHandle | undefined, archil.ListSandboxesError, RuntimeContext>;
|
|
86
|
+
/** Every sandbox this client created. */
|
|
87
|
+
list(): Effect.Effect<SandboxHandle[], archil.ListSandboxesError, RuntimeContext>;
|
|
88
|
+
/** Delete a sandbox this client created. Deleting a missing sandbox succeeds. */
|
|
89
|
+
delete(
|
|
90
|
+
name: string,
|
|
91
|
+
): Effect.Effect<
|
|
92
|
+
void,
|
|
93
|
+
archil.ListSandboxesError | Effect.Error<ReturnType<typeof deleteSandbox>>,
|
|
94
|
+
RuntimeContext
|
|
95
|
+
>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create sandboxes at run time, one per user, session, or task: Firecracker
|
|
100
|
+
* microVMs with disks mounted, public ports, and an outbound network policy
|
|
101
|
+
* that can inject credentials at the boundary. Bind an {@link Image} to make
|
|
102
|
+
* it the default image; a run-time `create` takes the same props as a
|
|
103
|
+
* deploy-time {@link Sandbox}. Bind a {@link Sandbox} instead and `create`
|
|
104
|
+
* forks it: each sandbox starts from its memory and disk, with whatever its
|
|
105
|
+
* `run` commands set up, and changes only its own copy.
|
|
106
|
+
*
|
|
107
|
+
* Objects created at run time are tagged with the stack, stage and binding
|
|
108
|
+
* (`alchemy::stack`, `alchemy::stage`, `alchemy::id`) and are not deleted
|
|
109
|
+
* when the stack is destroyed. List them by those tags to clean them up.
|
|
110
|
+
* Archil has no tags, labels or descriptions, so the tags are a name
|
|
111
|
+
* prefix, `alc-<id>-<hash of stack/stage/id>`: every sandbox (and fork)
|
|
112
|
+
* this client creates is `<prefix>-<name>`, and `prefix` on the client is the value.
|
|
113
|
+
*
|
|
114
|
+
* ### Sandboxes per Task
|
|
115
|
+
* **Example:** Run a task in a sandbox with the user's disk mounted
|
|
116
|
+
* ```typescript
|
|
117
|
+
* export default class Api extends Cloudflare.Worker<Api>()(
|
|
118
|
+
* "Api",
|
|
119
|
+
* { main: import.meta.url },
|
|
120
|
+
* Effect.gen(function* () {
|
|
121
|
+
* const disks = yield* Archil.CreateDisk();
|
|
122
|
+
* const sandboxes = yield* Archil.CreateSandbox(Agent);
|
|
123
|
+
* return {
|
|
124
|
+
* fetch: Effect.gen(function* () {
|
|
125
|
+
* const home = yield* disks.create("user-1");
|
|
126
|
+
* const sandbox = yield* sandboxes.create({
|
|
127
|
+
* name: "task-1",
|
|
128
|
+
* mounts: [{ disk: home, path: "/home/user" }],
|
|
129
|
+
* network: { egress: { default: "deny", allow: ["registry.npmjs.org"] } },
|
|
130
|
+
* idleTtlSeconds: 300,
|
|
131
|
+
* });
|
|
132
|
+
* const result = yield* sandbox.exec("npm test", { cwd: "/home/user/app" });
|
|
133
|
+
* yield* sandbox.pause();
|
|
134
|
+
* return HttpServerResponse.text(result.stdout);
|
|
135
|
+
* }),
|
|
136
|
+
* };
|
|
137
|
+
* }).pipe(Effect.provide(Layer.mergeAll(Archil.CreateDiskHttp, Archil.CreateSandboxHttp))),
|
|
138
|
+
* ) {}
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* ### Forking a Sandbox Built at Deploy
|
|
142
|
+
* **Example:** A fork per user from a sandbox set up at deploy
|
|
143
|
+
* ```typescript
|
|
144
|
+
* export const Golden = Archil.Sandbox("Golden", {
|
|
145
|
+
* run: ["git clone --depth 1 https://github.com/sindresorhus/is ~/is"],
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* // in a Worker or program:
|
|
149
|
+
* const sandboxes = yield* Archil.CreateSandbox(Golden);
|
|
150
|
+
* const sandbox = (yield* sandboxes.get(`user-${user}`)) ?? (yield* sandboxes.create({ name: `user-${user}` }));
|
|
151
|
+
* const result = yield* sandbox.exec("git log -1", { cwd: "~/is" });
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @binding
|
|
155
|
+
* @product Sandbox
|
|
156
|
+
*/
|
|
157
|
+
export interface CreateSandbox extends Binding.Service<
|
|
158
|
+
CreateSandbox,
|
|
159
|
+
"Archil.CreateSandbox",
|
|
160
|
+
(base?: Image | Sandbox) => Effect.Effect<CreateSandboxClient>
|
|
161
|
+
> {}
|
|
162
|
+
|
|
163
|
+
export const CreateSandbox = Binding.Service<CreateSandbox>("Archil.CreateSandbox");
|
|
164
|
+
|
|
165
|
+
/** {@link CreateSandbox} over Archil's HTTP API, usable from any host. */
|
|
166
|
+
export const CreateSandboxHttp = Layer.effect(
|
|
167
|
+
CreateSandbox,
|
|
168
|
+
Effect.gen(function* () {
|
|
169
|
+
const access = yield* ApiAccess;
|
|
170
|
+
return Effect.fn(function* (base?: Image | Sandbox) {
|
|
171
|
+
const authorize = yield* access();
|
|
172
|
+
const { prefix: owner } = yield* runtimePrefix("Sandboxes");
|
|
173
|
+
const prefix = Effect.succeed(owner);
|
|
174
|
+
// Resources are proxies (every property reads as an Output), so tell a
|
|
175
|
+
// bound sandbox from a bound image by its type, not by `in`.
|
|
176
|
+
const isSandbox = (b: Image | Sandbox): b is Sandbox => b.Type === "Archil.Sandbox";
|
|
177
|
+
const source = base !== undefined && isSandbox(base) ? yield* base.sandboxId : undefined;
|
|
178
|
+
const imageId = base !== undefined && !isSandbox(base) ? yield* base.imageId : undefined;
|
|
179
|
+
const fullName = (name: string) => Effect.succeed(runtimeName(owner, name));
|
|
180
|
+
// Forks carry the same prefix as the sandboxes they come from; an
|
|
181
|
+
// unnamed fork gets a random name.
|
|
182
|
+
const forkName = Effect.fn(function* (name: string | undefined) {
|
|
183
|
+
const id = (yield* Random.nextIntBetween(0, 2 ** 31)).toString(36);
|
|
184
|
+
return yield* fullName(name ?? `fork-${id}`);
|
|
185
|
+
});
|
|
186
|
+
const handle = (s: { sandbox_id: string; name: string }) =>
|
|
187
|
+
makeSandboxHandle({ sandboxId: s.sandbox_id, name: s.name }, authorize, forkName);
|
|
188
|
+
return {
|
|
189
|
+
prefix: owner,
|
|
190
|
+
create: (request) =>
|
|
191
|
+
authorize(
|
|
192
|
+
Effect.gen(function* () {
|
|
193
|
+
const name = yield* fullName(request.name);
|
|
194
|
+
const existing = yield* findSandboxByName(name);
|
|
195
|
+
if (existing) return handle(existing);
|
|
196
|
+
if (source !== undefined) {
|
|
197
|
+
// A bound sandbox is the base: fork its memory and disk.
|
|
198
|
+
const { data } = yield* archil.forkSandbox({
|
|
199
|
+
sid: yield* source,
|
|
200
|
+
wait: true,
|
|
201
|
+
name,
|
|
202
|
+
});
|
|
203
|
+
return handle(yield* waitForSandbox(data.sandbox_id));
|
|
204
|
+
}
|
|
205
|
+
const defaultImage =
|
|
206
|
+
request.image === undefined && request.baseImage === undefined && imageId
|
|
207
|
+
? yield* imageId
|
|
208
|
+
: request.image;
|
|
209
|
+
const mounts: archil.SandboxMount[] = [];
|
|
210
|
+
for (const mount of request.mounts ?? []) {
|
|
211
|
+
const diskId = yield* runtimeDiskId(mount.disk, mount.path);
|
|
212
|
+
mounts.push(yield* toSandboxMount(diskId, mount));
|
|
213
|
+
}
|
|
214
|
+
const { data } = yield* archil
|
|
215
|
+
.createSandbox(
|
|
216
|
+
toCreateSandboxRequest(name, { ...request, image: defaultImage }, mounts),
|
|
217
|
+
)
|
|
218
|
+
.pipe(
|
|
219
|
+
Effect.catchTag("SandboxNameConflict", (conflict) =>
|
|
220
|
+
findSandboxByName(name).pipe(
|
|
221
|
+
Effect.flatMap((found) =>
|
|
222
|
+
found ? Effect.succeed({ data: found }) : Effect.fail(conflict),
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
return handle(yield* waitForSandbox(data.sandbox_id));
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
get: (name) =>
|
|
231
|
+
authorize(
|
|
232
|
+
Effect.gen(function* () {
|
|
233
|
+
const sandboxName = yield* fullName(name);
|
|
234
|
+
const found = yield* findSandboxByName(sandboxName);
|
|
235
|
+
return found ? handle(found) : undefined;
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
238
|
+
list: () =>
|
|
239
|
+
authorize(
|
|
240
|
+
Effect.gen(function* () {
|
|
241
|
+
const p = yield* prefix;
|
|
242
|
+
return (yield* listSandboxes())
|
|
243
|
+
.filter((s) => s.name.startsWith(`${p}-`) && s.status !== "deleted")
|
|
244
|
+
.map(handle);
|
|
245
|
+
}),
|
|
246
|
+
),
|
|
247
|
+
delete: (name) =>
|
|
248
|
+
authorize(
|
|
249
|
+
Effect.gen(function* () {
|
|
250
|
+
const sandboxName = yield* fullName(name);
|
|
251
|
+
const found = yield* findSandboxByName(sandboxName);
|
|
252
|
+
if (found) yield* deleteSandbox(found.sandbox_id);
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
} satisfies CreateSandboxClient;
|
|
256
|
+
});
|
|
257
|
+
}),
|
|
258
|
+
);
|