@blaxel/core 0.3.26-preview.314 → 0.3.26
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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/common/settings.js +2 -2
- package/dist/cjs/image/image.js +63 -7
- package/dist/cjs/image/image.test.js +74 -0
- package/dist/cjs/types/image/image.browser.d.ts +2 -0
- package/dist/cjs/types/image/image.d.ts +10 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/settings.js +2 -2
- package/dist/cjs-browser/image/image.test.js +74 -0
- package/dist/cjs-browser/types/image/image.browser.d.ts +2 -0
- package/dist/cjs-browser/types/image/image.d.ts +10 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/settings.js +2 -2
- package/dist/esm/image/image.js +63 -7
- package/dist/esm/image/image.test.js +75 -1
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/settings.js +2 -2
- package/dist/esm-browser/image/image.test.js +75 -1
- package/package.json +1 -1
|
@@ -30,8 +30,8 @@ function missingCredentialsMessage() {
|
|
|
30
30
|
return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
|
|
31
31
|
}
|
|
32
32
|
// Build info - these placeholders are replaced at build time by build:replace-imports
|
|
33
|
-
const BUILD_VERSION = "0.3.26
|
|
34
|
-
const BUILD_COMMIT = "
|
|
33
|
+
const BUILD_VERSION = "0.3.26";
|
|
34
|
+
const BUILD_COMMIT = "18c2c73526c47b9ebc09aa3e16497b19854a02d0";
|
|
35
35
|
const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
|
|
36
36
|
const BLAXEL_API_VERSION = "2026-09-22";
|
|
37
37
|
// Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
|
package/dist/cjs/image/image.js
CHANGED
|
@@ -17,6 +17,37 @@ function ensureNodeEnvironment() {
|
|
|
17
17
|
}
|
|
18
18
|
exports.SANDBOX_API_IMAGE = "ghcr.io/blaxel-ai/sandbox";
|
|
19
19
|
exports.SANDBOX_API_PATH = "/usr/local/bin/sandbox-api";
|
|
20
|
+
// Build settings travel as sandbox labels. The control plane signs the allowed
|
|
21
|
+
// ones into the upload URL, and the upload must send them back as matching
|
|
22
|
+
// x-amz-meta-* headers or storage rejects it.
|
|
23
|
+
const BUILD_UPLOAD_LABELS = [
|
|
24
|
+
"x-blaxel-builder",
|
|
25
|
+
"x-blaxel-experimental",
|
|
26
|
+
"x-blaxel-build-memory",
|
|
27
|
+
"x-blaxel-build-volume",
|
|
28
|
+
"x-blaxel-build-region",
|
|
29
|
+
"x-blaxel-build-cache",
|
|
30
|
+
];
|
|
31
|
+
const MAX_BUILD_UPLOAD_LABEL_VALUE = 64;
|
|
32
|
+
function buildSizeLabel(option, value, min, max) {
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
37
|
+
throw new Error(`${option} must be an integer between ${min} and ${max} MiB`);
|
|
38
|
+
}
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
function buildUploadMetadata(labels) {
|
|
42
|
+
const metadata = {};
|
|
43
|
+
for (const name of BUILD_UPLOAD_LABELS) {
|
|
44
|
+
const value = labels?.[name];
|
|
45
|
+
if (typeof value === "string" && value !== "" && value.length <= MAX_BUILD_UPLOAD_LABEL_VALUE) {
|
|
46
|
+
metadata[name] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return metadata;
|
|
50
|
+
}
|
|
20
51
|
function generateDockerfile(context) {
|
|
21
52
|
// Build the raw Dockerfile content
|
|
22
53
|
const lines = [`FROM ${context.baseImage}`];
|
|
@@ -622,9 +653,10 @@ class ImageInstance {
|
|
|
622
653
|
void archive.finalize();
|
|
623
654
|
});
|
|
624
655
|
}
|
|
625
|
-
_createSandboxPayload(name, memory = 4096) {
|
|
656
|
+
_createSandboxPayload(name, memory = 4096, buildLabels = {}) {
|
|
626
657
|
const labels = {
|
|
627
658
|
"x-blaxel-auto-generated": "true",
|
|
659
|
+
...buildLabels,
|
|
628
660
|
};
|
|
629
661
|
const metadata = {
|
|
630
662
|
name,
|
|
@@ -666,12 +698,16 @@ class ImageInstance {
|
|
|
666
698
|
const uploadUrl = response.headers.get("x-blaxel-upload-url");
|
|
667
699
|
return { response, uploadUrl };
|
|
668
700
|
}
|
|
669
|
-
async _uploadZip(uploadUrl, zipContent) {
|
|
701
|
+
async _uploadZip(uploadUrl, zipContent, metadata = {}) {
|
|
702
|
+
const headers = {
|
|
703
|
+
"Content-Type": "application/zip",
|
|
704
|
+
};
|
|
705
|
+
for (const [name, value] of Object.entries(metadata)) {
|
|
706
|
+
headers[`x-amz-meta-${name}`] = value;
|
|
707
|
+
}
|
|
670
708
|
const response = await fetch(uploadUrl, {
|
|
671
709
|
method: "PUT",
|
|
672
|
-
headers
|
|
673
|
-
"Content-Type": "application/zip",
|
|
674
|
-
},
|
|
710
|
+
headers,
|
|
675
711
|
body: zipContent,
|
|
676
712
|
});
|
|
677
713
|
if (response.status >= 400) {
|
|
@@ -742,6 +778,16 @@ class ImageInstance {
|
|
|
742
778
|
*/
|
|
743
779
|
async build(options) {
|
|
744
780
|
const { name, memory = 4096, timeout = 900000, onStatusChange, sandboxVersion = "latest" } = options;
|
|
781
|
+
const buildLabels = {};
|
|
782
|
+
const buildMemory = buildSizeLabel("buildMemoryMb", options.buildMemoryMb, 1, 32768);
|
|
783
|
+
if (buildMemory !== undefined) {
|
|
784
|
+
buildLabels["x-blaxel-build-memory"] = buildMemory;
|
|
785
|
+
}
|
|
786
|
+
// Keep an explicit 0: it asks for memory-backed scratch instead of the default.
|
|
787
|
+
const buildVolume = buildSizeLabel("buildVolumeMb", options.buildVolumeMb, 0, 131072);
|
|
788
|
+
if (buildVolume !== undefined) {
|
|
789
|
+
buildLabels["x-blaxel-build-volume"] = buildVolume;
|
|
790
|
+
}
|
|
745
791
|
// Prepare image for sandbox deployment (add sandbox-api and entrypoint)
|
|
746
792
|
const preparedImage = this._prepareForSandbox(sandboxVersion);
|
|
747
793
|
// Write the image folder
|
|
@@ -750,7 +796,7 @@ class ImageInstance {
|
|
|
750
796
|
// Create zip
|
|
751
797
|
const zipContent = await this._createZip(buildDir);
|
|
752
798
|
// Create sandbox payload
|
|
753
|
-
const sandboxPayload = this._createSandboxPayload(name, memory);
|
|
799
|
+
const sandboxPayload = this._createSandboxPayload(name, memory, buildLabels);
|
|
754
800
|
// Create/update sandbox and get upload URL
|
|
755
801
|
const { response, uploadUrl } = await this._createSandboxWithUpload(sandboxPayload);
|
|
756
802
|
if (response.status >= 400) {
|
|
@@ -760,8 +806,18 @@ class ImageInstance {
|
|
|
760
806
|
if (!uploadUrl) {
|
|
761
807
|
throw new Error("No upload URL returned from API");
|
|
762
808
|
}
|
|
809
|
+
// Sign-matching headers come from the labels the control plane stored,
|
|
810
|
+
// which can include build settings already on an existing sandbox.
|
|
811
|
+
let storedLabels = sandboxPayload.metadata?.labels;
|
|
812
|
+
try {
|
|
813
|
+
const created = (await response.json());
|
|
814
|
+
storedLabels = created.metadata?.labels ?? storedLabels;
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
// Fall back to the labels we sent
|
|
818
|
+
}
|
|
763
819
|
// Upload the zip
|
|
764
|
-
await this._uploadZip(uploadUrl, zipContent);
|
|
820
|
+
await this._uploadZip(uploadUrl, zipContent, buildUploadMetadata(storedLabels));
|
|
765
821
|
// Wait for deployment to complete
|
|
766
822
|
await this._waitForDeployment(name, timeout, 3000, onStatusChange);
|
|
767
823
|
// Get the final sandbox state
|
|
@@ -7,6 +7,7 @@ const fs_1 = require("fs");
|
|
|
7
7
|
const os_1 = require("os");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const vitest_1 = require("vitest");
|
|
10
|
+
const settings_js_1 = require("../common/settings.js");
|
|
10
11
|
const image_js_1 = require("./image.js");
|
|
11
12
|
// Test fixtures
|
|
12
13
|
let tempDir;
|
|
@@ -543,3 +544,76 @@ let tempSourceDir;
|
|
|
543
544
|
(0, vitest_1.expect)(runIdx).toBeLessThan(thirdIdx);
|
|
544
545
|
});
|
|
545
546
|
});
|
|
547
|
+
(0, vitest_1.describe)("ImageInstance.build worker resources", () => {
|
|
548
|
+
async function buildWith(options, storedLabels) {
|
|
549
|
+
const calls = [];
|
|
550
|
+
vitest_1.vi.spyOn(settings_js_1.settings, "authenticate").mockResolvedValue(undefined);
|
|
551
|
+
vitest_1.vi.spyOn(settings_js_1.settings, "headers", "get").mockReturnValue({});
|
|
552
|
+
vitest_1.vi.spyOn(settings_js_1.settings, "baseUrl", "get").mockReturnValue("https://api.test");
|
|
553
|
+
vitest_1.vi.stubGlobal("fetch", (url, init = {}) => {
|
|
554
|
+
const headers = init.headers;
|
|
555
|
+
const body = typeof init.body === "string" ? JSON.parse(init.body) : undefined;
|
|
556
|
+
calls.push({ url, method: init.method ?? "GET", headers, body });
|
|
557
|
+
if (url.includes("upload=true")) {
|
|
558
|
+
const sent = body.metadata?.labels;
|
|
559
|
+
return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img", labels: storedLabels ?? sent } }), {
|
|
560
|
+
status: 200,
|
|
561
|
+
headers: { "x-blaxel-upload-url": "https://upload.test/object" },
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
if (url.startsWith("https://upload.test")) {
|
|
565
|
+
return Promise.resolve(new Response("", { status: 200 }));
|
|
566
|
+
}
|
|
567
|
+
return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img" }, status: "DEPLOYED" }), { status: 200 }));
|
|
568
|
+
});
|
|
569
|
+
const image = image_js_1.ImageInstance.fromRegistry("alpine:3.20");
|
|
570
|
+
// @ts-expect-error - private method
|
|
571
|
+
vitest_1.vi.spyOn(image, "_prepareForSandbox").mockReturnValue(image);
|
|
572
|
+
// @ts-expect-error - private method
|
|
573
|
+
vitest_1.vi.spyOn(image, "_waitForDeployment").mockResolvedValue("DEPLOYED");
|
|
574
|
+
await image.build({ name: "img", ...options });
|
|
575
|
+
return {
|
|
576
|
+
create: calls.find((call) => call.url.includes("upload=true")),
|
|
577
|
+
upload: calls.find((call) => call.url.startsWith("https://upload.test")),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
(0, vitest_1.afterEach)(() => {
|
|
581
|
+
vitest_1.vi.restoreAllMocks();
|
|
582
|
+
vitest_1.vi.unstubAllGlobals();
|
|
583
|
+
});
|
|
584
|
+
(0, vitest_1.it)("sends build memory and volume as labels and signed upload headers", async () => {
|
|
585
|
+
const { create, upload } = await buildWith({ buildMemoryMb: 16384, buildVolumeMb: 32768 });
|
|
586
|
+
const labels = create.body.metadata?.labels;
|
|
587
|
+
(0, vitest_1.expect)(labels?.["x-blaxel-build-memory"]).toBe("16384");
|
|
588
|
+
(0, vitest_1.expect)(labels?.["x-blaxel-build-volume"]).toBe("32768");
|
|
589
|
+
(0, vitest_1.expect)(create.body.spec?.runtime?.memory).toBe(4096);
|
|
590
|
+
(0, vitest_1.expect)(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBe("16384");
|
|
591
|
+
(0, vitest_1.expect)(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("32768");
|
|
592
|
+
(0, vitest_1.expect)(upload.headers["x-amz-meta-x-blaxel-auto-generated"]).toBeUndefined();
|
|
593
|
+
});
|
|
594
|
+
(0, vitest_1.it)("keeps an explicit zero volume", async () => {
|
|
595
|
+
const { upload } = await buildWith({ buildVolumeMb: 0 });
|
|
596
|
+
(0, vitest_1.expect)(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("0");
|
|
597
|
+
(0, vitest_1.expect)(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBeUndefined();
|
|
598
|
+
});
|
|
599
|
+
(0, vitest_1.it)("sends no build headers by default", async () => {
|
|
600
|
+
const { upload } = await buildWith({});
|
|
601
|
+
(0, vitest_1.expect)(Object.keys(upload.headers).filter((name) => name.startsWith("x-amz-meta-"))).toEqual([]);
|
|
602
|
+
});
|
|
603
|
+
(0, vitest_1.it)("mirrors build labels the control plane already stored", async () => {
|
|
604
|
+
const { upload } = await buildWith({}, { "x-blaxel-auto-generated": "true", "x-blaxel-build-memory": "8192" });
|
|
605
|
+
(0, vitest_1.expect)(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBe("8192");
|
|
606
|
+
});
|
|
607
|
+
vitest_1.it.each([
|
|
608
|
+
[{ buildMemoryMb: 0 }, "buildMemoryMb"],
|
|
609
|
+
[{ buildMemoryMb: 32769 }, "buildMemoryMb"],
|
|
610
|
+
[{ buildMemoryMb: 1.5 }, "buildMemoryMb"],
|
|
611
|
+
[{ buildVolumeMb: -1 }, "buildVolumeMb"],
|
|
612
|
+
[{ buildVolumeMb: 131073 }, "buildVolumeMb"],
|
|
613
|
+
])("rejects out-of-range %o before any request", async (options, option) => {
|
|
614
|
+
const fetchSpy = vitest_1.vi.fn();
|
|
615
|
+
vitest_1.vi.stubGlobal("fetch", fetchSpy);
|
|
616
|
+
await (0, vitest_1.expect)(image_js_1.ImageInstance.fromRegistry("alpine:3.20").build({ name: "img", ...options })).rejects.toThrow(option);
|
|
617
|
+
(0, vitest_1.expect)(fetchSpy).not.toHaveBeenCalled();
|
|
618
|
+
});
|
|
619
|
+
});
|
|
@@ -27,6 +27,8 @@ export interface ImageBuildOptions {
|
|
|
27
27
|
timeout?: number;
|
|
28
28
|
onStatusChange?: (status: string) => void;
|
|
29
29
|
sandboxVersion?: string;
|
|
30
|
+
buildMemoryMb?: number;
|
|
31
|
+
buildVolumeMb?: number;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* A fluent builder for creating sandbox images programmatically.
|
|
@@ -28,6 +28,16 @@ export interface ImageBuildOptions {
|
|
|
28
28
|
timeout?: number;
|
|
29
29
|
onStatusChange?: (status: string) => void;
|
|
30
30
|
sandboxVersion?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Memory for the temporary build worker in MiB (1-32768). Same as
|
|
33
|
+
* `bl push --memory`. Does not change the deployed sandbox's memory.
|
|
34
|
+
*/
|
|
35
|
+
buildMemoryMb?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Scratch disk for the temporary build worker in MiB (0-131072). Same as
|
|
38
|
+
* `bl push --volume`; 0 uses memory-backed scratch.
|
|
39
|
+
*/
|
|
40
|
+
buildVolumeMb?: number;
|
|
31
41
|
}
|
|
32
42
|
export interface PipInstallOptions {
|
|
33
43
|
findLinks?: string;
|