alchemy 0.77.0 → 0.77.1
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/bin/alchemy.js +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/container.d.ts.map +1 -1
- package/lib/cloudflare/container.js +8 -5
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/d1-database.d.ts +13 -3
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +31 -15
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/docker/api.d.ts +4 -0
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +3 -0
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/image.d.ts +50 -12
- package/lib/docker/image.d.ts.map +1 -1
- package/lib/docker/image.js +86 -58
- package/lib/docker/image.js.map +1 -1
- package/lib/docker/remote-image.d.ts +1 -0
- package/lib/docker/remote-image.d.ts.map +1 -1
- package/lib/docker/remote-image.js +2 -0
- package/lib/docker/remote-image.js.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/container.ts +10 -7
- package/src/cloudflare/d1-database.ts +44 -6
- package/src/docker/api.ts +7 -0
- package/src/docker/image.ts +164 -89
- package/src/docker/remote-image.ts +3 -0
- package/workers/cloudflare-state-store.js +57 -57
- package/workers/tunnel-proxy.js +1 -1
package/src/docker/image.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Context } from "../context.ts";
|
|
|
5
5
|
import { Resource } from "../resource.ts";
|
|
6
6
|
import type { Secret } from "../secret.ts";
|
|
7
7
|
import { DockerApi } from "./api.ts";
|
|
8
|
+
import type { RemoteImage } from "./remote-image.ts";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Options for building a Docker image
|
|
@@ -40,9 +41,28 @@ export interface DockerBuildOptions {
|
|
|
40
41
|
target?: string;
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* Use an external cache source for a build
|
|
45
|
+
*
|
|
46
|
+
* @see https://docs.docker.com/reference/cli/docker/buildx/build/#cache-from
|
|
47
|
+
*
|
|
44
48
|
*/
|
|
45
49
|
cacheFrom?: string[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Export build cache to an external cache destination
|
|
53
|
+
*
|
|
54
|
+
* @see https://docs.docker.com/reference/cli/docker/buildx/build/#cache-to
|
|
55
|
+
*
|
|
56
|
+
*/
|
|
57
|
+
cacheTo?: string[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Additional options to pass to the Docker build command. This serves as an escape hatch for any additional options that are not supported by the other properties.
|
|
61
|
+
*
|
|
62
|
+
* @see https://docs.docker.com/reference/cli/docker/buildx/build/#options
|
|
63
|
+
*
|
|
64
|
+
*/
|
|
65
|
+
options?: string[];
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
export interface ImageRegistry {
|
|
@@ -54,22 +74,12 @@ export interface ImageRegistry {
|
|
|
54
74
|
/**
|
|
55
75
|
* Properties for creating a Docker image
|
|
56
76
|
*/
|
|
57
|
-
export
|
|
58
|
-
/**
|
|
59
|
-
* Repository name for the image (e.g., "username/image")
|
|
60
|
-
*/
|
|
61
|
-
name?: string;
|
|
62
|
-
|
|
77
|
+
export type ImageProps = {
|
|
63
78
|
/**
|
|
64
79
|
* Tag for the image (e.g., "latest")
|
|
65
80
|
*/
|
|
66
81
|
tag?: string;
|
|
67
82
|
|
|
68
|
-
/**
|
|
69
|
-
* Build configuration
|
|
70
|
-
*/
|
|
71
|
-
build?: DockerBuildOptions;
|
|
72
|
-
|
|
73
83
|
/**
|
|
74
84
|
* Registry credentials
|
|
75
85
|
*/
|
|
@@ -79,12 +89,36 @@ export interface ImageProps {
|
|
|
79
89
|
* Whether to skip pushing the image to registry
|
|
80
90
|
*/
|
|
81
91
|
skipPush?: boolean;
|
|
82
|
-
}
|
|
92
|
+
} & (
|
|
93
|
+
| {
|
|
94
|
+
/**
|
|
95
|
+
* Image name or reference (e.g., "nginx:alpine")
|
|
96
|
+
*/
|
|
97
|
+
image: string | Image | RemoteImage;
|
|
98
|
+
build?: never;
|
|
99
|
+
name?: never;
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
/**
|
|
103
|
+
* Repository name for the image (e.g., "username/image")
|
|
104
|
+
*
|
|
105
|
+
* @default - the id
|
|
106
|
+
*/
|
|
107
|
+
name?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Build configuration
|
|
110
|
+
*/
|
|
111
|
+
build: DockerBuildOptions;
|
|
112
|
+
|
|
113
|
+
image?: never;
|
|
114
|
+
}
|
|
115
|
+
);
|
|
83
116
|
|
|
84
117
|
/**
|
|
85
118
|
* Docker Image resource
|
|
86
119
|
*/
|
|
87
|
-
export interface Image
|
|
120
|
+
export interface Image {
|
|
121
|
+
kind: "Image";
|
|
88
122
|
/**
|
|
89
123
|
* Image name
|
|
90
124
|
*/
|
|
@@ -109,6 +143,15 @@ export interface Image extends ImageProps {
|
|
|
109
143
|
* Time when the image was built
|
|
110
144
|
*/
|
|
111
145
|
builtAt: number;
|
|
146
|
+
/**
|
|
147
|
+
* Tag for the image
|
|
148
|
+
*/
|
|
149
|
+
tag: string;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build configuration
|
|
153
|
+
*/
|
|
154
|
+
build: DockerBuildOptions | undefined;
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
/**
|
|
@@ -142,12 +185,32 @@ export const Image = Resource(
|
|
|
142
185
|
// No action needed for delete as Docker images aren't automatically removed
|
|
143
186
|
// This is intentional as other resources might depend on the same image
|
|
144
187
|
return this.destroy();
|
|
145
|
-
}
|
|
146
|
-
// Normalize properties
|
|
147
|
-
const tag = props.tag || "latest";
|
|
148
|
-
const name = props.name || id;
|
|
149
|
-
const imageRef = `${name}:${tag}`;
|
|
188
|
+
}
|
|
150
189
|
|
|
190
|
+
const tag = props.tag || "latest";
|
|
191
|
+
const name =
|
|
192
|
+
props.name ||
|
|
193
|
+
(typeof props.image === "string"
|
|
194
|
+
? props.image
|
|
195
|
+
: props.image?.name
|
|
196
|
+
)?.split(":")[0] ||
|
|
197
|
+
id;
|
|
198
|
+
const imageRef = `${name}:${tag}`;
|
|
199
|
+
let imageId: string | undefined;
|
|
200
|
+
if (props.image) {
|
|
201
|
+
const image =
|
|
202
|
+
typeof props.image === "string" ? props.image : props.image.imageRef;
|
|
203
|
+
|
|
204
|
+
const kind =
|
|
205
|
+
typeof props.image === "object" && props.image.kind === "Image"
|
|
206
|
+
? "local"
|
|
207
|
+
: "remote";
|
|
208
|
+
if (kind === "remote") {
|
|
209
|
+
await api.pullImage(image);
|
|
210
|
+
}
|
|
211
|
+
await api.tagImage(image, imageRef);
|
|
212
|
+
// TODO: Extract image ID from pull output if available
|
|
213
|
+
} else {
|
|
151
214
|
let context: string;
|
|
152
215
|
let dockerfile: string;
|
|
153
216
|
if (props.build?.dockerfile && props.build?.context) {
|
|
@@ -170,7 +233,7 @@ export const Image = Resource(
|
|
|
170
233
|
const buildOptions: Record<string, string> = props.build?.args || {};
|
|
171
234
|
|
|
172
235
|
// Add platform if specified
|
|
173
|
-
|
|
236
|
+
const buildArgs = ["build", "-t", imageRef];
|
|
174
237
|
|
|
175
238
|
if (props.build?.platform) {
|
|
176
239
|
buildArgs.push("--platform", props.build.platform);
|
|
@@ -188,11 +251,23 @@ export const Image = Resource(
|
|
|
188
251
|
}
|
|
189
252
|
}
|
|
190
253
|
|
|
254
|
+
// Add cache destinations if specified
|
|
255
|
+
if (props.build?.cacheTo && props.build.cacheTo.length > 0) {
|
|
256
|
+
for (const cacheTarget of props.build.cacheTo) {
|
|
257
|
+
buildArgs.push("--cache-to", cacheTarget);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
191
261
|
// Add build arguments
|
|
192
262
|
for (const [key, value] of Object.entries(buildOptions)) {
|
|
193
263
|
buildArgs.push("--build-arg", `${key}=${value}`);
|
|
194
264
|
}
|
|
195
265
|
|
|
266
|
+
// Add build options if specified
|
|
267
|
+
if (props.build?.options && props.build.options.length > 0) {
|
|
268
|
+
buildArgs.push(...props.build.options);
|
|
269
|
+
}
|
|
270
|
+
|
|
196
271
|
buildArgs.push("-f", dockerfile);
|
|
197
272
|
|
|
198
273
|
// Add context path
|
|
@@ -203,77 +278,77 @@ export const Image = Resource(
|
|
|
203
278
|
|
|
204
279
|
// Extract image ID from build output if available
|
|
205
280
|
const imageIdMatch = /Successfully built ([a-f0-9]+)/.exec(stdout);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Push the image
|
|
245
|
-
const { stdout: pushOut } = await api.exec(["push", targetImage]);
|
|
246
|
-
|
|
247
|
-
// Attempt to extract the repo digest from push output
|
|
248
|
-
const digestMatch = /digest:\s+([a-z0-9]+:[a-f0-9]{64})/.exec(
|
|
249
|
-
pushOut,
|
|
250
|
-
);
|
|
251
|
-
if (digestMatch) {
|
|
252
|
-
const digestHash = digestMatch[1];
|
|
253
|
-
// Strip tag (anything after last :) to build image@digest reference
|
|
254
|
-
const [repoWithoutTag] =
|
|
255
|
-
targetImage.split(":").length > 2
|
|
256
|
-
? [targetImage] // unlikely but safety
|
|
257
|
-
: [targetImage.substring(0, targetImage.lastIndexOf(":"))];
|
|
258
|
-
repoDigest = `${repoWithoutTag}@${digestHash}`;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Update the final image reference to point at the pushed image
|
|
262
|
-
finalImageRef = targetImage;
|
|
263
|
-
} finally {
|
|
264
|
-
// Clean up credentials from the isolated config
|
|
265
|
-
await api.logout(registryHost);
|
|
281
|
+
imageId = imageIdMatch ? imageIdMatch[1] : undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle push if required
|
|
285
|
+
let repoDigest: string | undefined;
|
|
286
|
+
let finalImageRef = imageRef;
|
|
287
|
+
if (props.registry && !props.skipPush) {
|
|
288
|
+
const { server, username, password } = props.registry;
|
|
289
|
+
|
|
290
|
+
// Ensure the registry server does not have trailing slash
|
|
291
|
+
const registryHost = server.replace(/\/$/, "");
|
|
292
|
+
|
|
293
|
+
// Determine if the built image already includes a registry host (e.g. ghcr.io/user/repo)
|
|
294
|
+
const firstSegment = imageRef.split("/")[0];
|
|
295
|
+
const hasRegistryPrefix = firstSegment.includes(".");
|
|
296
|
+
|
|
297
|
+
// Compose the target image reference that will be pushed
|
|
298
|
+
const targetImage = hasRegistryPrefix
|
|
299
|
+
? imageRef // already fully-qualified
|
|
300
|
+
: `${registryHost}/${imageRef}`;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Create a temporary directory that will act as an isolated Docker config
|
|
304
|
+
// (credentials) directory. This prevents race-conditions when multiple
|
|
305
|
+
// concurrent tests perform `docker login` / `logout` by ensuring each
|
|
306
|
+
// Image operation has its own credential store.
|
|
307
|
+
const tempConfigDir = await fs.mkdtemp(
|
|
308
|
+
path.join(os.tmpdir(), "docker-config-"),
|
|
309
|
+
);
|
|
310
|
+
const api = new DockerApi({ configDir: tempConfigDir });
|
|
311
|
+
|
|
312
|
+
// Authenticate to registry using the isolated config directory
|
|
313
|
+
await api.login(registryHost, username, password.unencrypted);
|
|
314
|
+
|
|
315
|
+
// Tag local image with fully qualified name if necessary
|
|
316
|
+
if (targetImage !== imageRef) {
|
|
317
|
+
await api.exec(["tag", imageRef, targetImage]);
|
|
266
318
|
}
|
|
267
|
-
}
|
|
268
319
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
// Push the image
|
|
321
|
+
const { stdout: pushOut } = await api.exec(["push", targetImage]);
|
|
322
|
+
|
|
323
|
+
// Attempt to extract the repo digest from push output
|
|
324
|
+
const digestMatch = /digest:\s+([a-z0-9]+:[a-f0-9]{64})/.exec(pushOut);
|
|
325
|
+
if (digestMatch) {
|
|
326
|
+
const digestHash = digestMatch[1];
|
|
327
|
+
// Strip tag (anything after last :) to build image@digest reference
|
|
328
|
+
const [repoWithoutTag] =
|
|
329
|
+
targetImage.split(":").length > 2
|
|
330
|
+
? [targetImage] // unlikely but safety
|
|
331
|
+
: [targetImage.substring(0, targetImage.lastIndexOf(":"))];
|
|
332
|
+
repoDigest = `${repoWithoutTag}@${digestHash}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Update the final image reference to point at the pushed image
|
|
336
|
+
finalImageRef = targetImage;
|
|
337
|
+
} finally {
|
|
338
|
+
// Clean up credentials from the isolated config
|
|
339
|
+
await api.logout(registryHost);
|
|
340
|
+
}
|
|
277
341
|
}
|
|
342
|
+
return {
|
|
343
|
+
kind: "Image",
|
|
344
|
+
...props,
|
|
345
|
+
tag,
|
|
346
|
+
name,
|
|
347
|
+
imageRef: finalImageRef,
|
|
348
|
+
imageId,
|
|
349
|
+
repoDigest,
|
|
350
|
+
builtAt: Date.now(),
|
|
351
|
+
build: props.build,
|
|
352
|
+
};
|
|
278
353
|
},
|
|
279
354
|
);
|
|
@@ -26,6 +26,7 @@ export interface RemoteImageProps {
|
|
|
26
26
|
* Docker Remote Image resource
|
|
27
27
|
*/
|
|
28
28
|
export interface RemoteImage extends RemoteImageProps {
|
|
29
|
+
kind: "RemoteImage";
|
|
29
30
|
/**
|
|
30
31
|
* Full image reference (name:tag)
|
|
31
32
|
*/
|
|
@@ -71,8 +72,10 @@ export const RemoteImage = Resource(
|
|
|
71
72
|
await api.pullImage(imageRef);
|
|
72
73
|
|
|
73
74
|
return {
|
|
75
|
+
kind: "RemoteImage",
|
|
74
76
|
...props,
|
|
75
77
|
imageRef,
|
|
78
|
+
tag,
|
|
76
79
|
createdAt: Date.now(),
|
|
77
80
|
};
|
|
78
81
|
}
|