alchemy 0.77.0 → 0.77.2
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 +21741 -2128
- package/lib/cloudflare/bucket-custom-domain.d.ts +4 -4
- package/lib/cloudflare/bucket-custom-domain.d.ts.map +1 -1
- package/lib/cloudflare/bucket-custom-domain.js +7 -7
- package/lib/cloudflare/bucket-custom-domain.js.map +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/lib/util/safe-fetch.d.ts +1 -0
- package/lib/util/safe-fetch.d.ts.map +1 -1
- package/lib/util/safe-fetch.js +1 -1
- package/lib/util/safe-fetch.js.map +1 -1
- package/lib/util/telemetry.d.ts.map +1 -1
- package/lib/util/telemetry.js +32 -20
- package/lib/util/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/bucket-custom-domain.ts +9 -9
- 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/src/util/safe-fetch.ts +1 -1
- package/src/util/telemetry.ts +39 -19
- package/workers/cloudflare-state-store.js +57 -57
- package/workers/tunnel-proxy.js +1 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type CloudflareApi,
|
|
9
9
|
type CloudflareApiOptions,
|
|
10
10
|
} from "./api.ts";
|
|
11
|
+
import { withJurisdiction } from "./bucket.ts";
|
|
11
12
|
import { cloneD1Database } from "./d1-clone.ts";
|
|
12
13
|
import { applyLocalD1Migrations } from "./d1-local-migrations.ts";
|
|
13
14
|
import { applyMigrations, listMigrationsFiles } from "./d1-migrations.ts";
|
|
@@ -15,6 +16,8 @@ import { deleteMiniflareBinding } from "./miniflare/delete.ts";
|
|
|
15
16
|
|
|
16
17
|
const DEFAULT_MIGRATIONS_TABLE = "d1_migrations";
|
|
17
18
|
|
|
19
|
+
export type D1DatabaseJurisdiction = "default" | "eu" | "fedramp";
|
|
20
|
+
|
|
18
21
|
type PrimaryLocationHint =
|
|
19
22
|
| "wnam"
|
|
20
23
|
| "enam"
|
|
@@ -115,6 +118,12 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
|
|
|
115
118
|
*/
|
|
116
119
|
force?: boolean;
|
|
117
120
|
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Optional jurisdiction for the bucket
|
|
124
|
+
* Determines the regulatory jurisdiction the bucket data falls under
|
|
125
|
+
*/
|
|
126
|
+
jurisdiction?: D1DatabaseJurisdiction;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
export function isD1Database(resource: any): resource is D1Database {
|
|
@@ -157,6 +166,11 @@ export type D1Database = Pick<
|
|
|
157
166
|
*/
|
|
158
167
|
remote: boolean;
|
|
159
168
|
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The jurisdiction of the database
|
|
172
|
+
*/
|
|
173
|
+
jurisdiction: D1DatabaseJurisdiction;
|
|
160
174
|
};
|
|
161
175
|
|
|
162
176
|
/**
|
|
@@ -260,10 +274,11 @@ const _D1Database = Resource(
|
|
|
260
274
|
async function (
|
|
261
275
|
this: Context<D1Database>,
|
|
262
276
|
id: string,
|
|
263
|
-
props: D1DatabaseProps
|
|
277
|
+
props: D1DatabaseProps,
|
|
264
278
|
): Promise<D1Database> {
|
|
265
279
|
const databaseName =
|
|
266
280
|
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
|
|
281
|
+
const jurisdiction = props.jurisdiction ?? "default";
|
|
267
282
|
|
|
268
283
|
if (this.phase === "update" && this.output?.name !== databaseName) {
|
|
269
284
|
this.replace();
|
|
@@ -294,6 +309,7 @@ const _D1Database = Resource(
|
|
|
294
309
|
migrationsDir: props.migrationsDir,
|
|
295
310
|
migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
|
|
296
311
|
dev,
|
|
312
|
+
jurisdiction,
|
|
297
313
|
};
|
|
298
314
|
}
|
|
299
315
|
|
|
@@ -304,7 +320,7 @@ const _D1Database = Resource(
|
|
|
304
320
|
await deleteMiniflareBinding(this.scope, "d1", this.output.dev.id);
|
|
305
321
|
}
|
|
306
322
|
if (props.delete !== false && this.output?.id) {
|
|
307
|
-
await deleteDatabase(api, this.output.id);
|
|
323
|
+
await deleteDatabase(api, this.output.id, props);
|
|
308
324
|
}
|
|
309
325
|
// Return void (a deleted database has no content)
|
|
310
326
|
return this.destroy();
|
|
@@ -328,7 +344,7 @@ const _D1Database = Resource(
|
|
|
328
344
|
|
|
329
345
|
// If clone property is provided, perform cloning after database creation
|
|
330
346
|
if (props.clone && dbData.result.uuid) {
|
|
331
|
-
await cloneDb(api, props.clone, dbData.result.uuid);
|
|
347
|
+
await cloneDb(api, props.clone, dbData.result.uuid, jurisdiction);
|
|
332
348
|
}
|
|
333
349
|
} catch (error) {
|
|
334
350
|
// Check if this is a "database already exists" error and adopt is enabled
|
|
@@ -339,7 +355,7 @@ const _D1Database = Resource(
|
|
|
339
355
|
) {
|
|
340
356
|
logger.log(`Database ${databaseName} already exists, adopting it`);
|
|
341
357
|
// Find the existing database by name
|
|
342
|
-
const databases = await listDatabases(api, databaseName);
|
|
358
|
+
const databases = await listDatabases(api, databaseName, props);
|
|
343
359
|
const existingDb = databases.find((db) => db.name === databaseName);
|
|
344
360
|
|
|
345
361
|
if (!existingDb) {
|
|
@@ -349,7 +365,7 @@ const _D1Database = Resource(
|
|
|
349
365
|
}
|
|
350
366
|
|
|
351
367
|
// Get the database details using its ID
|
|
352
|
-
dbData = await getDatabase(api, existingDb.id);
|
|
368
|
+
dbData = await getDatabase(api, existingDb.id, props);
|
|
353
369
|
|
|
354
370
|
// Update the database with the provided properties
|
|
355
371
|
if (props.readReplication) {
|
|
@@ -413,6 +429,7 @@ const _D1Database = Resource(
|
|
|
413
429
|
dev,
|
|
414
430
|
migrationsDir: props.migrationsDir,
|
|
415
431
|
migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
|
|
432
|
+
jurisdiction,
|
|
416
433
|
};
|
|
417
434
|
},
|
|
418
435
|
);
|
|
@@ -454,6 +471,9 @@ export async function createDatabase(
|
|
|
454
471
|
const createResponse = await api.post(
|
|
455
472
|
`/accounts/${api.accountId}/d1/database`,
|
|
456
473
|
createPayload,
|
|
474
|
+
{
|
|
475
|
+
headers: withJurisdiction(props),
|
|
476
|
+
},
|
|
457
477
|
);
|
|
458
478
|
|
|
459
479
|
if (!createResponse.ok) {
|
|
@@ -474,6 +494,7 @@ export async function createDatabase(
|
|
|
474
494
|
export async function getDatabase(
|
|
475
495
|
api: CloudflareApi,
|
|
476
496
|
databaseId?: string,
|
|
497
|
+
props: D1DatabaseProps = {},
|
|
477
498
|
): Promise<CloudflareD1Response> {
|
|
478
499
|
if (!databaseId) {
|
|
479
500
|
throw new Error("Database ID is required");
|
|
@@ -481,6 +502,9 @@ export async function getDatabase(
|
|
|
481
502
|
|
|
482
503
|
const response = await api.get(
|
|
483
504
|
`/accounts/${api.accountId}/d1/database/${databaseId}`,
|
|
505
|
+
{
|
|
506
|
+
headers: withJurisdiction(props),
|
|
507
|
+
},
|
|
484
508
|
);
|
|
485
509
|
|
|
486
510
|
if (!response.ok) {
|
|
@@ -496,6 +520,7 @@ export async function getDatabase(
|
|
|
496
520
|
export async function deleteDatabase(
|
|
497
521
|
api: CloudflareApi,
|
|
498
522
|
databaseId?: string,
|
|
523
|
+
props: D1DatabaseProps = {},
|
|
499
524
|
): Promise<void> {
|
|
500
525
|
if (!databaseId) {
|
|
501
526
|
logger.log("No database ID provided, skipping delete");
|
|
@@ -505,6 +530,9 @@ export async function deleteDatabase(
|
|
|
505
530
|
// Delete D1 database
|
|
506
531
|
const deleteResponse = await api.delete(
|
|
507
532
|
`/accounts/${api.accountId}/d1/database/${databaseId}`,
|
|
533
|
+
{
|
|
534
|
+
headers: withJurisdiction(props),
|
|
535
|
+
},
|
|
508
536
|
);
|
|
509
537
|
|
|
510
538
|
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
@@ -524,12 +552,16 @@ export async function deleteDatabase(
|
|
|
524
552
|
export async function listDatabases(
|
|
525
553
|
api: CloudflareApi,
|
|
526
554
|
name?: string,
|
|
555
|
+
props: D1DatabaseProps = {},
|
|
527
556
|
): Promise<{ name: string; id: string }[]> {
|
|
528
557
|
// Construct query string if name is provided
|
|
529
558
|
const queryParams = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
530
559
|
|
|
531
560
|
const response = await api.get(
|
|
532
561
|
`/accounts/${api.accountId}/d1/database${queryParams}`,
|
|
562
|
+
{
|
|
563
|
+
headers: withJurisdiction(props),
|
|
564
|
+
},
|
|
533
565
|
);
|
|
534
566
|
|
|
535
567
|
if (!response.ok) {
|
|
@@ -582,6 +614,9 @@ export async function updateDatabase(
|
|
|
582
614
|
const updateResponse = await api.patch(
|
|
583
615
|
`/accounts/${api.accountId}/d1/database/${databaseId}`,
|
|
584
616
|
updatePayload,
|
|
617
|
+
{
|
|
618
|
+
headers: withJurisdiction(props),
|
|
619
|
+
},
|
|
585
620
|
);
|
|
586
621
|
|
|
587
622
|
if (!updateResponse.ok) {
|
|
@@ -608,6 +643,7 @@ async function cloneDb(
|
|
|
608
643
|
api: CloudflareApi,
|
|
609
644
|
sourceDb: D1Database | { id: string } | { name: string },
|
|
610
645
|
targetDbId: string,
|
|
646
|
+
jurisdiction: D1DatabaseJurisdiction,
|
|
611
647
|
): Promise<void> {
|
|
612
648
|
let sourceId: string;
|
|
613
649
|
|
|
@@ -617,7 +653,9 @@ async function cloneDb(
|
|
|
617
653
|
sourceId = sourceDb.id;
|
|
618
654
|
} else if ("name" in sourceDb && sourceDb.name) {
|
|
619
655
|
// Look up ID by name
|
|
620
|
-
const databases = await listDatabases(api, sourceDb.name
|
|
656
|
+
const databases = await listDatabases(api, sourceDb.name, {
|
|
657
|
+
jurisdiction,
|
|
658
|
+
});
|
|
621
659
|
const foundDb = databases.find((db) => db.name === sourceDb.name);
|
|
622
660
|
|
|
623
661
|
if (!foundDb) {
|
package/src/docker/api.ts
CHANGED
|
@@ -177,6 +177,13 @@ export class DockerApi {
|
|
|
177
177
|
return this.exec(["pull", image]);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
async tagImage(
|
|
181
|
+
source: string,
|
|
182
|
+
target: string,
|
|
183
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
184
|
+
return this.exec(["tag", source, target]);
|
|
185
|
+
}
|
|
186
|
+
|
|
180
187
|
/**
|
|
181
188
|
* Build Docker image
|
|
182
189
|
*
|
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
|
}
|
package/src/util/safe-fetch.ts
CHANGED
package/src/util/telemetry.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { Scope } from "../scope.ts";
|
|
|
12
12
|
import { parseOption } from "./cli-args.ts";
|
|
13
13
|
import { logger } from "./logger.ts";
|
|
14
14
|
import { memoize } from "./memoize.ts";
|
|
15
|
+
import { isTransientNetworkError } from "./safe-fetch.ts";
|
|
15
16
|
|
|
16
17
|
const ALCHEMY_DIR = path.join(os.homedir(), ".alchemy");
|
|
17
18
|
const ID_PATH = path.join(ALCHEMY_DIR, "id");
|
|
@@ -299,25 +300,44 @@ export async function createAndSendEvent(
|
|
|
299
300
|
if (await isTelemetryDisabled()) {
|
|
300
301
|
return;
|
|
301
302
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
303
|
+
|
|
304
|
+
const maxRetries = 3;
|
|
305
|
+
|
|
306
|
+
let telemetryErrors = [];
|
|
307
|
+
|
|
308
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
309
|
+
try {
|
|
310
|
+
const eventData = {
|
|
311
|
+
...data,
|
|
312
|
+
...("duration" in data
|
|
313
|
+
? { duration: Math.round(data.duration * 1000) }
|
|
314
|
+
: {}),
|
|
315
|
+
...(await collectData()),
|
|
316
|
+
...serializeError(error),
|
|
317
|
+
telemetryErrors: JSON.stringify(
|
|
318
|
+
telemetryErrors.map((e) => serializeError(e)),
|
|
319
|
+
),
|
|
320
|
+
};
|
|
321
|
+
await fetchNoResponse(TELEMETRY_API_URL, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: {
|
|
324
|
+
"Content-Type": "application/json",
|
|
325
|
+
},
|
|
326
|
+
body: JSON.stringify(eventData),
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
} catch (error: any) {
|
|
330
|
+
telemetryErrors.push(error);
|
|
331
|
+
|
|
332
|
+
const shouldRetry =
|
|
333
|
+
isTransientNetworkError(error) || isTransientNetworkError(error.cause);
|
|
334
|
+
|
|
335
|
+
if (!shouldRetry || attempt === maxRetries - 1) {
|
|
336
|
+
if (!SUPPRESS_TELEMETRY_ERRORS) {
|
|
337
|
+
logger.warn("Failed to send telemetry event:", error);
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
321
341
|
}
|
|
322
342
|
}
|
|
323
343
|
}
|