@blaxel/core 0.3.26-preview.313 → 0.3.26-preview.315
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
|
@@ -24,8 +24,8 @@ function missingCredentialsMessage() {
|
|
|
24
24
|
return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
|
|
25
25
|
}
|
|
26
26
|
// Build info - these placeholders are replaced at build time by build:replace-imports
|
|
27
|
-
const BUILD_VERSION = "0.3.26-preview.
|
|
28
|
-
const BUILD_COMMIT = "
|
|
27
|
+
const BUILD_VERSION = "0.3.26-preview.315";
|
|
28
|
+
const BUILD_COMMIT = "18c2c73526c47b9ebc09aa3e16497b19854a02d0";
|
|
29
29
|
const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
|
|
30
30
|
const BLAXEL_API_VERSION = "2026-09-22";
|
|
31
31
|
// Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
|
package/dist/esm/image/image.js
CHANGED
|
@@ -11,6 +11,37 @@ function ensureNodeEnvironment() {
|
|
|
11
11
|
}
|
|
12
12
|
export const SANDBOX_API_IMAGE = "ghcr.io/blaxel-ai/sandbox";
|
|
13
13
|
export const SANDBOX_API_PATH = "/usr/local/bin/sandbox-api";
|
|
14
|
+
// Build settings travel as sandbox labels. The control plane signs the allowed
|
|
15
|
+
// ones into the upload URL, and the upload must send them back as matching
|
|
16
|
+
// x-amz-meta-* headers or storage rejects it.
|
|
17
|
+
const BUILD_UPLOAD_LABELS = [
|
|
18
|
+
"x-blaxel-builder",
|
|
19
|
+
"x-blaxel-experimental",
|
|
20
|
+
"x-blaxel-build-memory",
|
|
21
|
+
"x-blaxel-build-volume",
|
|
22
|
+
"x-blaxel-build-region",
|
|
23
|
+
"x-blaxel-build-cache",
|
|
24
|
+
];
|
|
25
|
+
const MAX_BUILD_UPLOAD_LABEL_VALUE = 64;
|
|
26
|
+
function buildSizeLabel(option, value, min, max) {
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
31
|
+
throw new Error(`${option} must be an integer between ${min} and ${max} MiB`);
|
|
32
|
+
}
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
function buildUploadMetadata(labels) {
|
|
36
|
+
const metadata = {};
|
|
37
|
+
for (const name of BUILD_UPLOAD_LABELS) {
|
|
38
|
+
const value = labels?.[name];
|
|
39
|
+
if (typeof value === "string" && value !== "" && value.length <= MAX_BUILD_UPLOAD_LABEL_VALUE) {
|
|
40
|
+
metadata[name] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return metadata;
|
|
44
|
+
}
|
|
14
45
|
function generateDockerfile(context) {
|
|
15
46
|
// Build the raw Dockerfile content
|
|
16
47
|
const lines = [`FROM ${context.baseImage}`];
|
|
@@ -616,9 +647,10 @@ export class ImageInstance {
|
|
|
616
647
|
void archive.finalize();
|
|
617
648
|
});
|
|
618
649
|
}
|
|
619
|
-
_createSandboxPayload(name, memory = 4096) {
|
|
650
|
+
_createSandboxPayload(name, memory = 4096, buildLabels = {}) {
|
|
620
651
|
const labels = {
|
|
621
652
|
"x-blaxel-auto-generated": "true",
|
|
653
|
+
...buildLabels,
|
|
622
654
|
};
|
|
623
655
|
const metadata = {
|
|
624
656
|
name,
|
|
@@ -660,12 +692,16 @@ export class ImageInstance {
|
|
|
660
692
|
const uploadUrl = response.headers.get("x-blaxel-upload-url");
|
|
661
693
|
return { response, uploadUrl };
|
|
662
694
|
}
|
|
663
|
-
async _uploadZip(uploadUrl, zipContent) {
|
|
695
|
+
async _uploadZip(uploadUrl, zipContent, metadata = {}) {
|
|
696
|
+
const headers = {
|
|
697
|
+
"Content-Type": "application/zip",
|
|
698
|
+
};
|
|
699
|
+
for (const [name, value] of Object.entries(metadata)) {
|
|
700
|
+
headers[`x-amz-meta-${name}`] = value;
|
|
701
|
+
}
|
|
664
702
|
const response = await fetch(uploadUrl, {
|
|
665
703
|
method: "PUT",
|
|
666
|
-
headers
|
|
667
|
-
"Content-Type": "application/zip",
|
|
668
|
-
},
|
|
704
|
+
headers,
|
|
669
705
|
body: zipContent,
|
|
670
706
|
});
|
|
671
707
|
if (response.status >= 400) {
|
|
@@ -736,6 +772,16 @@ export class ImageInstance {
|
|
|
736
772
|
*/
|
|
737
773
|
async build(options) {
|
|
738
774
|
const { name, memory = 4096, timeout = 900000, onStatusChange, sandboxVersion = "latest" } = options;
|
|
775
|
+
const buildLabels = {};
|
|
776
|
+
const buildMemory = buildSizeLabel("buildMemoryMb", options.buildMemoryMb, 1, 32768);
|
|
777
|
+
if (buildMemory !== undefined) {
|
|
778
|
+
buildLabels["x-blaxel-build-memory"] = buildMemory;
|
|
779
|
+
}
|
|
780
|
+
// Keep an explicit 0: it asks for memory-backed scratch instead of the default.
|
|
781
|
+
const buildVolume = buildSizeLabel("buildVolumeMb", options.buildVolumeMb, 0, 131072);
|
|
782
|
+
if (buildVolume !== undefined) {
|
|
783
|
+
buildLabels["x-blaxel-build-volume"] = buildVolume;
|
|
784
|
+
}
|
|
739
785
|
// Prepare image for sandbox deployment (add sandbox-api and entrypoint)
|
|
740
786
|
const preparedImage = this._prepareForSandbox(sandboxVersion);
|
|
741
787
|
// Write the image folder
|
|
@@ -744,7 +790,7 @@ export class ImageInstance {
|
|
|
744
790
|
// Create zip
|
|
745
791
|
const zipContent = await this._createZip(buildDir);
|
|
746
792
|
// Create sandbox payload
|
|
747
|
-
const sandboxPayload = this._createSandboxPayload(name, memory);
|
|
793
|
+
const sandboxPayload = this._createSandboxPayload(name, memory, buildLabels);
|
|
748
794
|
// Create/update sandbox and get upload URL
|
|
749
795
|
const { response, uploadUrl } = await this._createSandboxWithUpload(sandboxPayload);
|
|
750
796
|
if (response.status >= 400) {
|
|
@@ -754,8 +800,18 @@ export class ImageInstance {
|
|
|
754
800
|
if (!uploadUrl) {
|
|
755
801
|
throw new Error("No upload URL returned from API");
|
|
756
802
|
}
|
|
803
|
+
// Sign-matching headers come from the labels the control plane stored,
|
|
804
|
+
// which can include build settings already on an existing sandbox.
|
|
805
|
+
let storedLabels = sandboxPayload.metadata?.labels;
|
|
806
|
+
try {
|
|
807
|
+
const created = (await response.json());
|
|
808
|
+
storedLabels = created.metadata?.labels ?? storedLabels;
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
// Fall back to the labels we sent
|
|
812
|
+
}
|
|
757
813
|
// Upload the zip
|
|
758
|
-
await this._uploadZip(uploadUrl, zipContent);
|
|
814
|
+
await this._uploadZip(uploadUrl, zipContent, buildUploadMetadata(storedLabels));
|
|
759
815
|
// Wait for deployment to complete
|
|
760
816
|
await this._waitForDeployment(name, timeout, 3000, onStatusChange);
|
|
761
817
|
// Get the final sandbox state
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
5
5
|
import { tmpdir } from "os";
|
|
6
6
|
import { join } from "path";
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { settings } from "../common/settings.js";
|
|
8
9
|
import { ImageInstance } from "./image.js";
|
|
9
10
|
// Test fixtures
|
|
10
11
|
let tempDir;
|
|
@@ -541,3 +542,76 @@ describe("Dockerfile instruction order", () => {
|
|
|
541
542
|
expect(runIdx).toBeLessThan(thirdIdx);
|
|
542
543
|
});
|
|
543
544
|
});
|
|
545
|
+
describe("ImageInstance.build worker resources", () => {
|
|
546
|
+
async function buildWith(options, storedLabels) {
|
|
547
|
+
const calls = [];
|
|
548
|
+
vi.spyOn(settings, "authenticate").mockResolvedValue(undefined);
|
|
549
|
+
vi.spyOn(settings, "headers", "get").mockReturnValue({});
|
|
550
|
+
vi.spyOn(settings, "baseUrl", "get").mockReturnValue("https://api.test");
|
|
551
|
+
vi.stubGlobal("fetch", (url, init = {}) => {
|
|
552
|
+
const headers = init.headers;
|
|
553
|
+
const body = typeof init.body === "string" ? JSON.parse(init.body) : undefined;
|
|
554
|
+
calls.push({ url, method: init.method ?? "GET", headers, body });
|
|
555
|
+
if (url.includes("upload=true")) {
|
|
556
|
+
const sent = body.metadata?.labels;
|
|
557
|
+
return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img", labels: storedLabels ?? sent } }), {
|
|
558
|
+
status: 200,
|
|
559
|
+
headers: { "x-blaxel-upload-url": "https://upload.test/object" },
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
if (url.startsWith("https://upload.test")) {
|
|
563
|
+
return Promise.resolve(new Response("", { status: 200 }));
|
|
564
|
+
}
|
|
565
|
+
return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img" }, status: "DEPLOYED" }), { status: 200 }));
|
|
566
|
+
});
|
|
567
|
+
const image = ImageInstance.fromRegistry("alpine:3.20");
|
|
568
|
+
// @ts-expect-error - private method
|
|
569
|
+
vi.spyOn(image, "_prepareForSandbox").mockReturnValue(image);
|
|
570
|
+
// @ts-expect-error - private method
|
|
571
|
+
vi.spyOn(image, "_waitForDeployment").mockResolvedValue("DEPLOYED");
|
|
572
|
+
await image.build({ name: "img", ...options });
|
|
573
|
+
return {
|
|
574
|
+
create: calls.find((call) => call.url.includes("upload=true")),
|
|
575
|
+
upload: calls.find((call) => call.url.startsWith("https://upload.test")),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
afterEach(() => {
|
|
579
|
+
vi.restoreAllMocks();
|
|
580
|
+
vi.unstubAllGlobals();
|
|
581
|
+
});
|
|
582
|
+
it("sends build memory and volume as labels and signed upload headers", async () => {
|
|
583
|
+
const { create, upload } = await buildWith({ buildMemoryMb: 16384, buildVolumeMb: 32768 });
|
|
584
|
+
const labels = create.body.metadata?.labels;
|
|
585
|
+
expect(labels?.["x-blaxel-build-memory"]).toBe("16384");
|
|
586
|
+
expect(labels?.["x-blaxel-build-volume"]).toBe("32768");
|
|
587
|
+
expect(create.body.spec?.runtime?.memory).toBe(4096);
|
|
588
|
+
expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBe("16384");
|
|
589
|
+
expect(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("32768");
|
|
590
|
+
expect(upload.headers["x-amz-meta-x-blaxel-auto-generated"]).toBeUndefined();
|
|
591
|
+
});
|
|
592
|
+
it("keeps an explicit zero volume", async () => {
|
|
593
|
+
const { upload } = await buildWith({ buildVolumeMb: 0 });
|
|
594
|
+
expect(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("0");
|
|
595
|
+
expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBeUndefined();
|
|
596
|
+
});
|
|
597
|
+
it("sends no build headers by default", async () => {
|
|
598
|
+
const { upload } = await buildWith({});
|
|
599
|
+
expect(Object.keys(upload.headers).filter((name) => name.startsWith("x-amz-meta-"))).toEqual([]);
|
|
600
|
+
});
|
|
601
|
+
it("mirrors build labels the control plane already stored", async () => {
|
|
602
|
+
const { upload } = await buildWith({}, { "x-blaxel-auto-generated": "true", "x-blaxel-build-memory": "8192" });
|
|
603
|
+
expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBe("8192");
|
|
604
|
+
});
|
|
605
|
+
it.each([
|
|
606
|
+
[{ buildMemoryMb: 0 }, "buildMemoryMb"],
|
|
607
|
+
[{ buildMemoryMb: 32769 }, "buildMemoryMb"],
|
|
608
|
+
[{ buildMemoryMb: 1.5 }, "buildMemoryMb"],
|
|
609
|
+
[{ buildVolumeMb: -1 }, "buildVolumeMb"],
|
|
610
|
+
[{ buildVolumeMb: 131073 }, "buildVolumeMb"],
|
|
611
|
+
])("rejects out-of-range %o before any request", async (options, option) => {
|
|
612
|
+
const fetchSpy = vi.fn();
|
|
613
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
614
|
+
await expect(ImageInstance.fromRegistry("alpine:3.20").build({ name: "img", ...options })).rejects.toThrow(option);
|
|
615
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
616
|
+
});
|
|
617
|
+
});
|