@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,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
controlPlaneUrl,
|
|
3
|
+
Credentials,
|
|
4
|
+
DEFAULT_REGION,
|
|
5
|
+
REGIONS,
|
|
6
|
+
} from "@distilled.cloud/archil/Credentials";
|
|
7
|
+
import { ConfigError } from "@distilled.cloud/core/errors";
|
|
8
|
+
import { lazyCredentials } from "alchemy/Auth/Resolve";
|
|
9
|
+
import * as Context from "effect/Context";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
12
|
+
import * as Redacted from "effect/Redacted";
|
|
13
|
+
import { ARCHIL_AUTH_PROVIDER_NAME, type ArchilResolvedCredentials } from "./AuthProvider.ts";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
Credentials,
|
|
17
|
+
CredentialsFromEnv,
|
|
18
|
+
DEFAULT_REGION,
|
|
19
|
+
REGIONS,
|
|
20
|
+
} from "@distilled.cloud/archil/Credentials";
|
|
21
|
+
|
|
22
|
+
/** Where the current Archil credentials act. */
|
|
23
|
+
export interface ArchilAccount {
|
|
24
|
+
/** Archil region id, e.g. `aws-us-east-1`. */
|
|
25
|
+
readonly region: string;
|
|
26
|
+
/** Archil organization id, when configured. */
|
|
27
|
+
readonly organization: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The region (and organization) the current Archil credentials act in.
|
|
32
|
+
* Holds an effect so it resolves on first use, never at layer build.
|
|
33
|
+
*/
|
|
34
|
+
export class Account extends Context.Service<Account, Effect.Effect<ArchilAccount>>()(
|
|
35
|
+
"Archil.Account",
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
/** The region and organization of the current Archil credentials. */
|
|
39
|
+
export const account = Effect.gen(function* () {
|
|
40
|
+
return yield* yield* Account;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const make = (
|
|
44
|
+
resolve: Effect.Effect<ArchilResolvedCredentials, ConfigError>,
|
|
45
|
+
): Effect.Effect<Context.Context<Credentials | Account>> =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const credentials = Effect.flatMap(resolve, (creds) => {
|
|
48
|
+
const apiBaseUrl = controlPlaneUrl(creds.region);
|
|
49
|
+
return apiBaseUrl
|
|
50
|
+
? Effect.succeed({ apiKey: creds.apiKey, apiBaseUrl, region: creds.region })
|
|
51
|
+
: Effect.fail(
|
|
52
|
+
new ConfigError({
|
|
53
|
+
message: `Unknown Archil region "${creds.region}". Valid regions: ${Object.keys(REGIONS).join(", ")}`,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
// `Account` is typed as never failing, so a credentials failure dies
|
|
58
|
+
// here; the engine still reports it as `CredentialsRequired`.
|
|
59
|
+
const accountInfo = yield* resolve.pipe(
|
|
60
|
+
Effect.map((creds) => ({ region: creds.region, organization: creds.organization })),
|
|
61
|
+
Effect.orDie,
|
|
62
|
+
Effect.cached,
|
|
63
|
+
);
|
|
64
|
+
return Context.make(Credentials, credentials).pipe(Context.add(Account, accountInfo));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build Archil `Credentials` + `Account` from an explicit API key. Useful
|
|
69
|
+
* for tests, or for a runtime that reads its key from a binding.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* Effect.provide(Archil.fromApiKey({ apiKey: "key-...", region: "aws-us-east-1" }))
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export const fromApiKey = (input: {
|
|
77
|
+
apiKey: string | Redacted.Redacted<string>;
|
|
78
|
+
region?: string;
|
|
79
|
+
organization?: string;
|
|
80
|
+
}) =>
|
|
81
|
+
Effect.succeed<ArchilResolvedCredentials>({
|
|
82
|
+
type: "apiKey",
|
|
83
|
+
apiKey: typeof input.apiKey === "string" ? Redacted.make(input.apiKey) : input.apiKey,
|
|
84
|
+
region: input.region ?? DEFAULT_REGION,
|
|
85
|
+
organization: input.organization,
|
|
86
|
+
source: { type: "stored" },
|
|
87
|
+
}).pipe(make, Layer.effectContext);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build Archil `Credentials` + `Account` that resolve through the Alchemy
|
|
91
|
+
* AuthProvider: environment credentials (`ARCHIL_API_KEY`) when present,
|
|
92
|
+
* otherwise the selected profile. Resolution is deferred until the first
|
|
93
|
+
* API call, so building the provider layers never needs a profile.
|
|
94
|
+
*/
|
|
95
|
+
export const fromAuthProvider = () =>
|
|
96
|
+
Layer.effectContext(
|
|
97
|
+
lazyCredentials<ArchilResolvedCredentials, ConfigError>()(
|
|
98
|
+
ARCHIL_AUTH_PROVIDER_NAME,
|
|
99
|
+
(creds) => creds,
|
|
100
|
+
).pipe(Effect.flatMap(make)),
|
|
101
|
+
);
|
package/src/Disk.ts
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import * as archil from "@distilled.cloud/archil";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Redacted from "effect/Redacted";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import {
|
|
10
|
+
diskName,
|
|
11
|
+
findDiskByName,
|
|
12
|
+
getDisk,
|
|
13
|
+
isAlchemyName,
|
|
14
|
+
isRuntimeObjectName,
|
|
15
|
+
waitForDisk,
|
|
16
|
+
waitForDiskGone,
|
|
17
|
+
listAllDisks,
|
|
18
|
+
} from "./internal/archil.ts";
|
|
19
|
+
import type { Providers } from "./Providers.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Amazon S3 bucket behind a disk. Omit the keys when the bucket policy
|
|
23
|
+
* trusts Archil's role for the region (see `ArchilAWS.S3Disk`, which writes
|
|
24
|
+
* that policy for you).
|
|
25
|
+
*/
|
|
26
|
+
export interface S3DiskStorage {
|
|
27
|
+
type: "s3";
|
|
28
|
+
/** Bucket name. */
|
|
29
|
+
bucketName: string;
|
|
30
|
+
/** Prefix within the bucket the disk lives under, e.g. `"data/"`. */
|
|
31
|
+
bucketPrefix?: string;
|
|
32
|
+
/** Access key id. Omit for IAM-role access through the bucket policy. */
|
|
33
|
+
accessKeyId?: Redacted.Redacted<string>;
|
|
34
|
+
/** Secret access key. */
|
|
35
|
+
secretAccessKey?: Redacted.Redacted<string>;
|
|
36
|
+
/** Session token for temporary credentials. */
|
|
37
|
+
sessionToken?: Redacted.Redacted<string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Google Cloud Storage bucket behind a disk, through HMAC keys. */
|
|
41
|
+
export interface GcsDiskStorage {
|
|
42
|
+
type: "gcs";
|
|
43
|
+
bucketName: string;
|
|
44
|
+
bucketPrefix?: string;
|
|
45
|
+
accessKeyId: Redacted.Redacted<string>;
|
|
46
|
+
secretAccessKey: Redacted.Redacted<string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Cloudflare R2 bucket behind a disk. */
|
|
50
|
+
export interface R2DiskStorage {
|
|
51
|
+
type: "r2";
|
|
52
|
+
bucketName: string;
|
|
53
|
+
/** R2 S3 endpoint, `https://<account>.r2.cloudflarestorage.com`. */
|
|
54
|
+
bucketEndpoint: string;
|
|
55
|
+
bucketPrefix?: string;
|
|
56
|
+
accessKeyId: Redacted.Redacted<string>;
|
|
57
|
+
secretAccessKey: Redacted.Redacted<string>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Any S3-compatible object store behind a disk. */
|
|
61
|
+
export interface S3CompatibleDiskStorage {
|
|
62
|
+
type: "s3-compatible";
|
|
63
|
+
bucketName: string;
|
|
64
|
+
bucketEndpoint: string;
|
|
65
|
+
bucketPrefix?: string;
|
|
66
|
+
accessKeyId: Redacted.Redacted<string>;
|
|
67
|
+
secretAccessKey: Redacted.Redacted<string>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Azure Blob Storage container behind a disk, through a service principal. */
|
|
71
|
+
export interface AzureBlobDiskStorage {
|
|
72
|
+
type: "azure-blob";
|
|
73
|
+
storageAccount: string;
|
|
74
|
+
containerName: string;
|
|
75
|
+
bucketPrefix?: string;
|
|
76
|
+
tenantId: string;
|
|
77
|
+
clientId: string;
|
|
78
|
+
clientSecret: Redacted.Redacted<string>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Where a disk's files live. Omit for Archil-managed storage. */
|
|
82
|
+
export type DiskStorage =
|
|
83
|
+
| S3DiskStorage
|
|
84
|
+
| GcsDiskStorage
|
|
85
|
+
| R2DiskStorage
|
|
86
|
+
| S3CompatibleDiskStorage
|
|
87
|
+
| AzureBlobDiskStorage;
|
|
88
|
+
|
|
89
|
+
export interface DiskProps {
|
|
90
|
+
/**
|
|
91
|
+
* Disk name, unique within your Archil account: letters, digits, `-` and
|
|
92
|
+
* `_`, at most 100 characters. If omitted, a unique name is generated
|
|
93
|
+
* from the stack, stage, and logical ID. Changing it replaces the disk.
|
|
94
|
+
*/
|
|
95
|
+
name?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Your own bucket to keep the disk's files in, as ordinary objects any
|
|
98
|
+
* S3 tool can read. Omit for Archil-managed storage. Changing it replaces
|
|
99
|
+
* the disk.
|
|
100
|
+
*/
|
|
101
|
+
storage?: DiskStorage;
|
|
102
|
+
/**
|
|
103
|
+
* IPv4 addresses and CIDR ranges allowed to mount the disk. Empty or
|
|
104
|
+
* omitted allows any address.
|
|
105
|
+
*/
|
|
106
|
+
allowedIps?: string[];
|
|
107
|
+
/**
|
|
108
|
+
* Whether sandboxes and serverless exec may mount the disk.
|
|
109
|
+
* @default true
|
|
110
|
+
*/
|
|
111
|
+
allowSandboxMount?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface DiskAttributes {
|
|
115
|
+
/** Disk id (`dsk-…`). */
|
|
116
|
+
diskId: string;
|
|
117
|
+
/** Disk name. */
|
|
118
|
+
name: string;
|
|
119
|
+
/** Archil organization that owns the disk. */
|
|
120
|
+
organization: string;
|
|
121
|
+
/** Archil region the disk lives in, e.g. `aws-us-east-1`. */
|
|
122
|
+
region: string;
|
|
123
|
+
/** Storage backend: `archil` for Archil-managed storage, else the bucket type. */
|
|
124
|
+
storageType: string;
|
|
125
|
+
/** Bucket name, for a disk on your own bucket. */
|
|
126
|
+
bucketName: string | undefined;
|
|
127
|
+
/** Bucket prefix, for a disk on your own bucket. */
|
|
128
|
+
bucketPrefix: string | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* The session id Archil assumes your bucket's role with. Bucket policies
|
|
131
|
+
* can scope access to exactly this disk with
|
|
132
|
+
* `aws:PrincipalTag/archilSessionId`.
|
|
133
|
+
*/
|
|
134
|
+
sessionId: string | undefined;
|
|
135
|
+
/** IPv4 addresses and CIDR ranges allowed to mount the disk. */
|
|
136
|
+
allowedIps: string[];
|
|
137
|
+
/** Whether sandboxes and serverless exec may mount the disk. */
|
|
138
|
+
allowSandboxMount: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type Disk = Resource<"Archil.Disk", DiskProps, DiskAttributes, never, Providers, DiskLookup>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Props for {@link Disk.lookup}: find a disk this stack
|
|
145
|
+
* doesn't manage by its name.
|
|
146
|
+
*/
|
|
147
|
+
export interface DiskLookup {
|
|
148
|
+
/** Disk name. */
|
|
149
|
+
name: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* An Archil disk: a durable filesystem that mounts in about 50 ms on any
|
|
154
|
+
* Linux machine, sandbox, or serverless exec, with hot data on SSD and the
|
|
155
|
+
* rest in object storage. Keep its files in Archil-managed storage, or in
|
|
156
|
+
* your own bucket as ordinary objects.
|
|
157
|
+
*
|
|
158
|
+
* Declare a disk at deploy time for the few long-lived ones an app shares,
|
|
159
|
+
* like a dataset or a seeded base you branch from. Disks that belong to one
|
|
160
|
+
* user, session, or task are created at run time with
|
|
161
|
+
* {@link CreateDisk}, which returns the same disk shape.
|
|
162
|
+
*
|
|
163
|
+
* Archil has no tags, so Alchemy owns a disk through its generated name.
|
|
164
|
+
* A disk with a fixed `name` that already exists is adopted only with
|
|
165
|
+
* `--adopt`.
|
|
166
|
+
*
|
|
167
|
+
* :::caution
|
|
168
|
+
* Changing `name` or `storage` replaces the disk, and destroying a disk on
|
|
169
|
+
* Archil-managed storage deletes its files. A disk on your own bucket
|
|
170
|
+
* leaves the objects in the bucket.
|
|
171
|
+
* :::
|
|
172
|
+
* @see https://docs.archil.com
|
|
173
|
+
*
|
|
174
|
+
* ### Creating a Disk
|
|
175
|
+
* **Example:** Archil-managed storage
|
|
176
|
+
* ```typescript
|
|
177
|
+
* export const Dataset = Archil.Disk("Dataset", {});
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* **Example:** On your own S3 bucket, with no keys
|
|
181
|
+
* ```typescript
|
|
182
|
+
* export const Store = AWS.S3.Bucket("Store", {});
|
|
183
|
+
* export const Dataset = ArchilAWS.S3Disk("Dataset", { bucket: Store, prefix: "datasets/" });
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* **Example:** On an R2 bucket
|
|
187
|
+
* ```typescript
|
|
188
|
+
* export const Dataset = Archil.Disk("Dataset", {
|
|
189
|
+
* storage: {
|
|
190
|
+
* type: "r2",
|
|
191
|
+
* bucketName: "datasets",
|
|
192
|
+
* bucketEndpoint: "https://<account>.r2.cloudflarestorage.com",
|
|
193
|
+
* accessKeyId: Alchemy.Secret("R2_ACCESS_KEY_ID"),
|
|
194
|
+
* secretAccessKey: Alchemy.Secret("R2_SECRET_ACCESS_KEY"),
|
|
195
|
+
* },
|
|
196
|
+
* });
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* ### Restricting Access
|
|
200
|
+
* **Example:** Only mount from known addresses
|
|
201
|
+
* ```typescript
|
|
202
|
+
* export const Dataset = Archil.Disk("Dataset", {
|
|
203
|
+
* allowedIps: ["203.0.113.0/24"],
|
|
204
|
+
* allowSandboxMount: false,
|
|
205
|
+
* });
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* ### Binding to a Runtime
|
|
209
|
+
* Object bindings give each host its own S3 user on this disk (read-only
|
|
210
|
+
* for `ReadDisk`), so the host never holds an API key.
|
|
211
|
+
*
|
|
212
|
+
* **Example:** Read and write the disk from a program sandbox
|
|
213
|
+
* ```typescript
|
|
214
|
+
* export default class Api extends Archil.Sandbox<Api>()(
|
|
215
|
+
* "Api",
|
|
216
|
+
* { main: import.meta.url },
|
|
217
|
+
* Effect.gen(function* () {
|
|
218
|
+
* const files = yield* Archil.ReadWriteDisk(Dataset);
|
|
219
|
+
* return {
|
|
220
|
+
* fetch: Effect.map(files.getText("README.md"), (text) => HttpServerResponse.text(text)),
|
|
221
|
+
* };
|
|
222
|
+
* }).pipe(Effect.provide(Archil.ReadWriteDiskHttp)),
|
|
223
|
+
* ) {}
|
|
224
|
+
* ```
|
|
225
|
+
*
|
|
226
|
+
* **Example:** Mount it into a sandbox
|
|
227
|
+
* ```typescript
|
|
228
|
+
* export const Box = Archil.Sandbox("Box", {
|
|
229
|
+
* mounts: [{ disk: Dataset, path: "/data", mode: "read-only" }],
|
|
230
|
+
* });
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* **Example:** Mount it in a Cloudflare Container
|
|
234
|
+
* ```typescript
|
|
235
|
+
* const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
|
|
236
|
+
* const path = yield* dataset.mount();
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* ### Runtime Operations
|
|
240
|
+
* **Example:** Run commands and read files
|
|
241
|
+
* ```typescript
|
|
242
|
+
* const dataset = yield* Archil.ExecDisk(Dataset);
|
|
243
|
+
* const files = yield* Archil.ReadDisk(Dataset);
|
|
244
|
+
* const result = yield* dataset.exec("wc -l /mnt/archil/*.csv");
|
|
245
|
+
* const readme = yield* files.getText("README.md");
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* **Example:** Upload a large file and share a link
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const files = yield* Archil.ReadWriteDisk(Dataset);
|
|
251
|
+
* yield* files.upload("exports/2026-11.parquet", body);
|
|
252
|
+
* const url = yield* files.share("exports/2026-11.parquet", 3600);
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* ### Branches and Checkpoints
|
|
256
|
+
* **Example:** Seed a base, then branch per session
|
|
257
|
+
* ```typescript
|
|
258
|
+
* export const Seed = Archil.Disk("Seed", {});
|
|
259
|
+
* export const SeedBase = Effect.gen(function* () {
|
|
260
|
+
* const seed = yield* Seed;
|
|
261
|
+
* return yield* Archil.Checkpoint("SeedBase", {
|
|
262
|
+
* disk: seed.diskId,
|
|
263
|
+
* name: "base",
|
|
264
|
+
* run: ["git clone https://github.com/acme/app ."],
|
|
265
|
+
* });
|
|
266
|
+
* });
|
|
267
|
+
* // runtime
|
|
268
|
+
* const seeds = yield* Archil.BranchDisk(Seed);
|
|
269
|
+
* const branch = yield* seeds.branch({ name: `session-${id}`, fromCheckpoint: "base" });
|
|
270
|
+
* ```
|
|
271
|
+
*
|
|
272
|
+
* ### Looking up an existing disk
|
|
273
|
+
* `Disk.lookup` references a disk Alchemy doesn't manage (created in
|
|
274
|
+
* the Archil console or by another stack). It is read during plan/deploy,
|
|
275
|
+
* never created, updated or deleted.
|
|
276
|
+
*
|
|
277
|
+
* **Example:** Branch an existing disk
|
|
278
|
+
* ```typescript
|
|
279
|
+
* const data = Archil.Disk.lookup({ name: "datasets" });
|
|
280
|
+
* const preview = yield* Archil.Branch("Preview", { disk: data.diskId });
|
|
281
|
+
* ```
|
|
282
|
+
*
|
|
283
|
+
* @resource
|
|
284
|
+
* @product Disk
|
|
285
|
+
*/
|
|
286
|
+
export const Disk = Resource<Disk>("Archil.Disk");
|
|
287
|
+
|
|
288
|
+
const unwrap = (value: Redacted.Redacted<string> | string | undefined) =>
|
|
289
|
+
value === undefined ? undefined : typeof value === "string" ? value : Redacted.value(value);
|
|
290
|
+
|
|
291
|
+
/** The `mounts` entry Archil's create call takes for a storage backend. */
|
|
292
|
+
export const toMountConfig = (storage: DiskStorage): archil.MountConfig => {
|
|
293
|
+
switch (storage.type) {
|
|
294
|
+
case "s3":
|
|
295
|
+
return {
|
|
296
|
+
type: "s3",
|
|
297
|
+
bucketName: storage.bucketName,
|
|
298
|
+
bucketPrefix: storage.bucketPrefix,
|
|
299
|
+
accessKeyId: unwrap(storage.accessKeyId),
|
|
300
|
+
secretAccessKey: unwrap(storage.secretAccessKey),
|
|
301
|
+
sessionToken: unwrap(storage.sessionToken),
|
|
302
|
+
} as archil.MountConfig;
|
|
303
|
+
case "gcs":
|
|
304
|
+
case "r2":
|
|
305
|
+
case "s3-compatible":
|
|
306
|
+
return {
|
|
307
|
+
...storage,
|
|
308
|
+
accessKeyId: unwrap(storage.accessKeyId),
|
|
309
|
+
secretAccessKey: unwrap(storage.secretAccessKey),
|
|
310
|
+
} as archil.MountConfig;
|
|
311
|
+
case "azure-blob":
|
|
312
|
+
return { ...storage, clientSecret: unwrap(storage.clientSecret) } as archil.MountConfig;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/** The comparable identity of a storage backend (no secrets). */
|
|
317
|
+
const storageIdentity = (storage: DiskStorage | undefined) =>
|
|
318
|
+
storage === undefined
|
|
319
|
+
? undefined
|
|
320
|
+
: {
|
|
321
|
+
type: storage.type,
|
|
322
|
+
bucketName: "bucketName" in storage ? storage.bucketName : undefined,
|
|
323
|
+
bucketPrefix: storage.bucketPrefix,
|
|
324
|
+
bucketEndpoint: "bucketEndpoint" in storage ? storage.bucketEndpoint : undefined,
|
|
325
|
+
containerName: "containerName" in storage ? storage.containerName : undefined,
|
|
326
|
+
storageAccount: "storageAccount" in storage ? storage.storageAccount : undefined,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/** Map a disk response (and its synced settings) to attributes. */
|
|
330
|
+
export const toDiskAttributes = (
|
|
331
|
+
disk: archil.DiskResponse,
|
|
332
|
+
settings: { allowedIps: string[]; allowSandboxMount: boolean },
|
|
333
|
+
): DiskAttributes => {
|
|
334
|
+
const first = disk.mounts?.[0];
|
|
335
|
+
// Archil-managed storage is an `s3` mount on Archil's own bucket.
|
|
336
|
+
const mount = first?.config?.bucketName?.startsWith("archil.prod.") ? undefined : first;
|
|
337
|
+
return {
|
|
338
|
+
diskId: disk.id,
|
|
339
|
+
name: disk.name,
|
|
340
|
+
organization: disk.organization,
|
|
341
|
+
region: disk.region,
|
|
342
|
+
storageType: mount?.type ?? "archil",
|
|
343
|
+
bucketName: mount?.config?.bucketName,
|
|
344
|
+
bucketPrefix: mount?.config?.bucketPrefix,
|
|
345
|
+
sessionId: mount?.config?.sessionId,
|
|
346
|
+
allowedIps: settings.allowedIps,
|
|
347
|
+
allowSandboxMount: settings.allowSandboxMount,
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/** Read a disk's mount-access settings. */
|
|
352
|
+
export const readDiskSettings = Effect.fn(function* (id: string) {
|
|
353
|
+
const [ips, mount] = yield* Effect.all(
|
|
354
|
+
[archil.getAllowedIps({ id }), archil.getSandboxMount({ id })],
|
|
355
|
+
{ concurrency: 2 },
|
|
356
|
+
);
|
|
357
|
+
return {
|
|
358
|
+
allowedIps: ips.data.allowedIps ?? [],
|
|
359
|
+
allowSandboxMount: mount.data.allowSandboxMount,
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
/** Converge a disk's mount-access settings to the desired ones. */
|
|
364
|
+
export const syncDiskSettings = Effect.fn(function* (
|
|
365
|
+
id: string,
|
|
366
|
+
desired: { allowedIps?: string[]; allowSandboxMount?: boolean },
|
|
367
|
+
) {
|
|
368
|
+
const observed = yield* readDiskSettings(id);
|
|
369
|
+
const allowedIps = desired.allowedIps ?? [];
|
|
370
|
+
const allowSandboxMount = desired.allowSandboxMount ?? true;
|
|
371
|
+
if (!deepEqual([...observed.allowedIps].sort(), [...allowedIps].sort())) {
|
|
372
|
+
yield* archil.setAllowedIps({ id, allowedIps });
|
|
373
|
+
}
|
|
374
|
+
if (observed.allowSandboxMount !== allowSandboxMount) {
|
|
375
|
+
yield* archil.setSandboxMount({ id, allowSandboxMount });
|
|
376
|
+
}
|
|
377
|
+
return { allowedIps, allowSandboxMount };
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
/** Ensure a disk with this name exists and is available (create-or-observe). */
|
|
381
|
+
export const ensureDisk = Effect.fn(function* (
|
|
382
|
+
name: string,
|
|
383
|
+
storage: DiskStorage | undefined,
|
|
384
|
+
knownId?: string,
|
|
385
|
+
) {
|
|
386
|
+
const found = (knownId ? yield* getDisk(knownId) : undefined) ?? (yield* findDiskByName(name));
|
|
387
|
+
// A disk still being deleted (an earlier destroy) holds the name: let it go.
|
|
388
|
+
if (found?.status === "deleting") yield* waitForDiskGone(found.id);
|
|
389
|
+
const observed = found?.status === "deleting" ? undefined : found;
|
|
390
|
+
if (observed) return yield* waitForDisk(observed.id);
|
|
391
|
+
// Archil answers a create of an existing name with that disk's id, so a
|
|
392
|
+
// concurrent create of the same name needs no special case.
|
|
393
|
+
const { data } = yield* archil.createDisk({
|
|
394
|
+
name,
|
|
395
|
+
mounts: storage ? [toMountConfig(storage)] : undefined,
|
|
396
|
+
});
|
|
397
|
+
return yield* waitForDisk(data.diskId);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
/** Delete a disk and wait until it is gone. Deleting a missing disk succeeds. */
|
|
401
|
+
export const deleteDisk = Effect.fn(function* (id: string) {
|
|
402
|
+
yield* archil.deleteDisk({ id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
403
|
+
yield* waitForDiskGone(id);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
export const DiskProvider = () =>
|
|
407
|
+
Provider.effect(
|
|
408
|
+
Disk,
|
|
409
|
+
Effect.gen(function* () {
|
|
410
|
+
return {
|
|
411
|
+
stables: ["diskId", "name", "organization", "region", "storageType"],
|
|
412
|
+
list: Effect.fn(function* () {
|
|
413
|
+
const disks = yield* Stream.runCollect(listAllDisks());
|
|
414
|
+
return yield* Effect.forEach(
|
|
415
|
+
disks.filter(
|
|
416
|
+
(disk) =>
|
|
417
|
+
(isAlchemyName(disk.name) || isRuntimeObjectName(disk.name)) &&
|
|
418
|
+
disk.status !== "deleted",
|
|
419
|
+
),
|
|
420
|
+
(disk) => readDiskSettings(disk.id).pipe(Effect.map((s) => toDiskAttributes(disk, s))),
|
|
421
|
+
{ concurrency: 4 },
|
|
422
|
+
);
|
|
423
|
+
}),
|
|
424
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
425
|
+
if (!isResolved(news)) return undefined;
|
|
426
|
+
const name = output?.name ?? (yield* diskName(id, olds?.name));
|
|
427
|
+
if (news.name !== undefined && news.name !== name) {
|
|
428
|
+
return { action: "replace" } as const;
|
|
429
|
+
}
|
|
430
|
+
if (!deepEqual(storageIdentity(news.storage), storageIdentity(olds?.storage))) {
|
|
431
|
+
return { action: "replace" } as const;
|
|
432
|
+
}
|
|
433
|
+
return undefined;
|
|
434
|
+
}),
|
|
435
|
+
lookup: Effect.fn(function* ({ name }) {
|
|
436
|
+
const disk = yield* findDiskByName(name);
|
|
437
|
+
return disk ? toDiskAttributes(disk, yield* readDiskSettings(disk.id)) : undefined;
|
|
438
|
+
}),
|
|
439
|
+
|
|
440
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
441
|
+
const name = output?.name ?? (yield* diskName(id, olds?.name));
|
|
442
|
+
const disk =
|
|
443
|
+
(output ? yield* getDisk(output.diskId) : undefined) ?? (yield* findDiskByName(name));
|
|
444
|
+
if (!disk) return undefined;
|
|
445
|
+
const attrs = toDiskAttributes(disk, yield* readDiskSettings(disk.id));
|
|
446
|
+
// A generated name carries this resource's instance suffix, so a
|
|
447
|
+
// disk with that name is ours even when state was lost. A fixed
|
|
448
|
+
// name may belong to anyone.
|
|
449
|
+
if (output === undefined && olds?.name !== undefined && !isAlchemyName(disk.name)) {
|
|
450
|
+
return Unowned(attrs);
|
|
451
|
+
}
|
|
452
|
+
return attrs;
|
|
453
|
+
}),
|
|
454
|
+
reconcile: Effect.fn(function* ({ id, news, output, session }) {
|
|
455
|
+
// Observe + ensure
|
|
456
|
+
const name = output?.name ?? (yield* diskName(id, news.name));
|
|
457
|
+
yield* session.note(`Ensuring disk ${name}...`);
|
|
458
|
+
const disk = yield* ensureDisk(name, news.storage, output?.diskId);
|
|
459
|
+
// Sync
|
|
460
|
+
const settings = yield* syncDiskSettings(disk.id, news);
|
|
461
|
+
return toDiskAttributes(disk, settings);
|
|
462
|
+
}),
|
|
463
|
+
delete: Effect.fn(function* ({ output }) {
|
|
464
|
+
yield* deleteDisk(output.diskId);
|
|
465
|
+
}),
|
|
466
|
+
};
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import { DiskKeyAccess } from "./ArchilBindings.ts";
|
|
4
|
+
import { makeReadDiskClient, makeWriteDiskClient, presignedShare } from "./Clients.ts";
|
|
5
|
+
import type { Disk } from "./Disk.ts";
|
|
6
|
+
import { ReadDisk, type DiskObjectsOptions } from "./ReadDisk.ts";
|
|
7
|
+
import { ReadWriteDisk } from "./ReadWriteDisk.ts";
|
|
8
|
+
import { WriteDisk } from "./WriteDisk.ts";
|
|
9
|
+
|
|
10
|
+
const diskRef = Effect.fn(function* (disk: Disk, options: DiskObjectsOptions | undefined) {
|
|
11
|
+
const diskId = yield* disk.diskId;
|
|
12
|
+
return Effect.map(diskId, (id) => ({ disk: id, branch: options?.branch }));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* {@link ReadDisk} over Archil's S3-compatible API, usable from any host.
|
|
17
|
+
* The host gets a read-only S3 user on the disk.
|
|
18
|
+
*/
|
|
19
|
+
export const ReadDiskHttp = Layer.effect(
|
|
20
|
+
ReadDisk,
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const keys = yield* DiskKeyAccess;
|
|
23
|
+
return Effect.fn(function* (disk: Disk, options?: DiskObjectsOptions) {
|
|
24
|
+
const ref = yield* diskRef(disk, options);
|
|
25
|
+
const run = yield* keys(disk, "read");
|
|
26
|
+
return makeReadDiskClient(ref, run, presignedShare(run));
|
|
27
|
+
});
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* {@link WriteDisk} over Archil's S3-compatible API, usable from any host.
|
|
33
|
+
* The host gets a read-write S3 user on the disk.
|
|
34
|
+
*/
|
|
35
|
+
export const WriteDiskHttp = Layer.effect(
|
|
36
|
+
WriteDisk,
|
|
37
|
+
Effect.gen(function* () {
|
|
38
|
+
const keys = yield* DiskKeyAccess;
|
|
39
|
+
return Effect.fn(function* (disk: Disk, options?: DiskObjectsOptions) {
|
|
40
|
+
const ref = yield* diskRef(disk, options);
|
|
41
|
+
const run = yield* keys(disk, "write");
|
|
42
|
+
return makeWriteDiskClient(ref, run);
|
|
43
|
+
});
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* {@link ReadWriteDisk} over Archil's S3-compatible API, usable from any
|
|
49
|
+
* host. The host gets a read-write S3 user on the disk.
|
|
50
|
+
*/
|
|
51
|
+
export const ReadWriteDiskHttp = Layer.effect(
|
|
52
|
+
ReadWriteDisk,
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
const keys = yield* DiskKeyAccess;
|
|
55
|
+
return Effect.fn(function* (disk: Disk, options?: DiskObjectsOptions) {
|
|
56
|
+
const ref = yield* diskRef(disk, options);
|
|
57
|
+
const run = yield* keys(disk, "write");
|
|
58
|
+
return {
|
|
59
|
+
...makeReadDiskClient(ref, run, presignedShare(run)),
|
|
60
|
+
...makeWriteDiskClient(ref, run),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}),
|
|
64
|
+
);
|