tensorlake 0.5.9 → 0.5.11
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/bin/darwin-arm64/tensorlake +0 -0
- package/dist/bin/darwin-arm64/tl +0 -0
- package/dist/bin/linux-x64/tensorlake +0 -0
- package/dist/bin/linux-x64/tl +0 -0
- package/dist/bin/win32-x64/tensorlake.exe +0 -0
- package/dist/bin/win32-x64/tl.exe +0 -0
- package/dist/index.cjs +389 -330
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +389 -330
- package/dist/index.js.map +1 -1
- package/dist/{sandbox-image-BMDaNpZ2.d.cts → sandbox-image-B8kFWVLi.d.cts} +11 -7
- package/dist/{sandbox-image-BMDaNpZ2.d.ts → sandbox-image-B8kFWVLi.d.ts} +11 -7
- package/dist/sandbox-image.cjs +384 -325
- package/dist/sandbox-image.cjs.map +1 -1
- package/dist/sandbox-image.d.cts +1 -1
- package/dist/sandbox-image.d.ts +1 -1
- package/dist/sandbox-image.js +383 -325
- package/dist/sandbox-image.js.map +1 -1
- package/package.json +1 -1
package/dist/sandbox-image.js
CHANGED
|
@@ -61,7 +61,7 @@ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TI
|
|
|
61
61
|
var init_defaults = __esm({
|
|
62
62
|
"src/defaults.ts"() {
|
|
63
63
|
"use strict";
|
|
64
|
-
SDK_VERSION = "0.5.
|
|
64
|
+
SDK_VERSION = "0.5.11";
|
|
65
65
|
API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
|
|
66
66
|
API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
|
|
67
67
|
NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
|
|
@@ -3316,11 +3316,10 @@ var init_sandbox = __esm({
|
|
|
3316
3316
|
await client.resume(this.lifecycleIdentifier, options);
|
|
3317
3317
|
}
|
|
3318
3318
|
/**
|
|
3319
|
-
* Create a
|
|
3320
|
-
* be committed.
|
|
3319
|
+
* Create a checkpoint of this sandbox and wait for it to be locally ready.
|
|
3321
3320
|
*
|
|
3322
|
-
* By default blocks until the
|
|
3323
|
-
*
|
|
3321
|
+
* By default blocks until the checkpoint is resumable and returns
|
|
3322
|
+
* `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
|
|
3324
3323
|
* (returns `undefined`).
|
|
3325
3324
|
*/
|
|
3326
3325
|
async checkpoint(options) {
|
|
@@ -3332,7 +3331,8 @@ var init_sandbox = __esm({
|
|
|
3332
3331
|
return client.snapshotAndWait(this.lifecycleIdentifier, {
|
|
3333
3332
|
timeout: options?.timeout,
|
|
3334
3333
|
pollInterval: options?.pollInterval,
|
|
3335
|
-
snapshotType: options?.checkpointType
|
|
3334
|
+
snapshotType: options?.checkpointType,
|
|
3335
|
+
waitUntil: options?.waitUntil
|
|
3336
3336
|
});
|
|
3337
3337
|
}
|
|
3338
3338
|
/**
|
|
@@ -3727,6 +3727,12 @@ __export(client_exports, {
|
|
|
3727
3727
|
function sleep2(ms) {
|
|
3728
3728
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3729
3729
|
}
|
|
3730
|
+
function snapshotStatusSatisfiesWaitCondition(status, waitUntil) {
|
|
3731
|
+
if (waitUntil === "local_ready") {
|
|
3732
|
+
return status === "local_ready" /* LOCAL_READY */ || status === "completed" /* COMPLETED */;
|
|
3733
|
+
}
|
|
3734
|
+
return status === "completed" /* COMPLETED */;
|
|
3735
|
+
}
|
|
3730
3736
|
function formatStartupFailureMessage(sandboxId, status, options) {
|
|
3731
3737
|
const prefix = status === "terminated" /* TERMINATED */ ? `Sandbox ${sandboxId} terminated during startup` : `Sandbox ${sandboxId} became ${status} during startup`;
|
|
3732
3738
|
const detail = formatErrorDetails(options.errorDetails);
|
|
@@ -4027,7 +4033,8 @@ var init_client = __esm({
|
|
|
4027
4033
|
*
|
|
4028
4034
|
* This call **returns immediately** with a `snapshotId` and `in_progress`
|
|
4029
4035
|
* status — the snapshot is created asynchronously. Poll `getSnapshot()` until
|
|
4030
|
-
* `completed
|
|
4036
|
+
* `local_ready`, `completed`, or `failed`, or use `snapshotAndWait()` to
|
|
4037
|
+
* block automatically.
|
|
4031
4038
|
*
|
|
4032
4039
|
* @param options.snapshotType - `"filesystem"` for cold-boot snapshots (e.g. image builds).
|
|
4033
4040
|
* Omit to use the server default (`filesystem`).
|
|
@@ -4068,9 +4075,11 @@ var init_client = __esm({
|
|
|
4068
4075
|
);
|
|
4069
4076
|
}
|
|
4070
4077
|
/**
|
|
4071
|
-
* Create a snapshot and block until it is
|
|
4078
|
+
* Create a snapshot and block until it is locally ready.
|
|
4072
4079
|
*
|
|
4073
|
-
* Combines `snapshot()` with polling `getSnapshot()` until `
|
|
4080
|
+
* Combines `snapshot()` with polling `getSnapshot()` until `local_ready`
|
|
4081
|
+
* or `completed`. Pass `{ waitUntil: "completed" }` when durable
|
|
4082
|
+
* `snapshotUri` metadata is required.
|
|
4074
4083
|
* Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
|
|
4075
4084
|
* without managing the client separately.
|
|
4076
4085
|
*
|
|
@@ -4083,13 +4092,14 @@ var init_client = __esm({
|
|
|
4083
4092
|
async snapshotAndWait(sandboxId, options) {
|
|
4084
4093
|
const timeout = options?.timeout ?? 300;
|
|
4085
4094
|
const pollInterval = options?.pollInterval ?? 1;
|
|
4095
|
+
const waitUntil = options?.waitUntil ?? "local_ready";
|
|
4086
4096
|
const result = await this.snapshot(sandboxId, {
|
|
4087
4097
|
snapshotType: options?.snapshotType
|
|
4088
4098
|
});
|
|
4089
4099
|
const deadline = Date.now() + timeout * 1e3;
|
|
4090
4100
|
while (Date.now() < deadline) {
|
|
4091
4101
|
const info = await this.getSnapshot(result.snapshotId);
|
|
4092
|
-
if (info.status
|
|
4102
|
+
if (snapshotStatusSatisfiesWaitCondition(info.status, waitUntil)) return info;
|
|
4093
4103
|
if (info.status === "failed" /* FAILED */) {
|
|
4094
4104
|
throw new SandboxError(
|
|
4095
4105
|
`Snapshot ${result.snapshotId} failed: ${info.error}`
|
|
@@ -4098,7 +4108,7 @@ var init_client = __esm({
|
|
|
4098
4108
|
await sleep2(pollInterval * 1e3);
|
|
4099
4109
|
}
|
|
4100
4110
|
throw new SandboxError(
|
|
4101
|
-
`Snapshot ${result.snapshotId} did not
|
|
4111
|
+
`Snapshot ${result.snapshotId} did not reach ${waitUntil} within ${timeout}s`
|
|
4102
4112
|
);
|
|
4103
4113
|
}
|
|
4104
4114
|
// --- Pools ---
|
|
@@ -4283,6 +4293,7 @@ var init_image = __esm({
|
|
|
4283
4293
|
|
|
4284
4294
|
// src/sandbox-image.ts
|
|
4285
4295
|
import { readFile, readdir, stat } from "fs/promises";
|
|
4296
|
+
import { homedir } from "os";
|
|
4286
4297
|
import path from "path";
|
|
4287
4298
|
import { parseArgs } from "util";
|
|
4288
4299
|
function defaultRegisteredName(dockerfilePath) {
|
|
@@ -4413,12 +4424,6 @@ function shellSplit(input) {
|
|
|
4413
4424
|
}
|
|
4414
4425
|
return tokens;
|
|
4415
4426
|
}
|
|
4416
|
-
function shellQuote(value) {
|
|
4417
|
-
if (!value) {
|
|
4418
|
-
return "''";
|
|
4419
|
-
}
|
|
4420
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4421
|
-
}
|
|
4422
4427
|
function stripLeadingFlags(value) {
|
|
4423
4428
|
const flags = {};
|
|
4424
4429
|
let remaining = value.trimStart();
|
|
@@ -4456,75 +4461,6 @@ function parseFromValue(value, lineNumber) {
|
|
|
4456
4461
|
}
|
|
4457
4462
|
return tokens[0];
|
|
4458
4463
|
}
|
|
4459
|
-
function parseCopyLikeValues(value, lineNumber, keyword) {
|
|
4460
|
-
const { flags, remaining } = stripLeadingFlags(value);
|
|
4461
|
-
if ("from" in flags) {
|
|
4462
|
-
throw new Error(
|
|
4463
|
-
`line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
|
|
4464
|
-
);
|
|
4465
|
-
}
|
|
4466
|
-
const payload = remaining.trim();
|
|
4467
|
-
if (!payload) {
|
|
4468
|
-
throw new Error(
|
|
4469
|
-
`line ${lineNumber}: ${keyword} must include source and destination`
|
|
4470
|
-
);
|
|
4471
|
-
}
|
|
4472
|
-
let parts;
|
|
4473
|
-
if (payload.startsWith("[")) {
|
|
4474
|
-
let parsed;
|
|
4475
|
-
try {
|
|
4476
|
-
parsed = JSON.parse(payload);
|
|
4477
|
-
} catch (error) {
|
|
4478
|
-
throw new Error(
|
|
4479
|
-
`line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
|
|
4480
|
-
);
|
|
4481
|
-
}
|
|
4482
|
-
if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
|
|
4483
|
-
throw new Error(
|
|
4484
|
-
`line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
|
|
4485
|
-
);
|
|
4486
|
-
}
|
|
4487
|
-
parts = parsed;
|
|
4488
|
-
} else {
|
|
4489
|
-
parts = shellSplit(payload);
|
|
4490
|
-
if (parts.length < 2) {
|
|
4491
|
-
throw new Error(
|
|
4492
|
-
`line ${lineNumber}: ${keyword} must include at least one source and one destination`
|
|
4493
|
-
);
|
|
4494
|
-
}
|
|
4495
|
-
}
|
|
4496
|
-
return {
|
|
4497
|
-
flags,
|
|
4498
|
-
sources: parts.slice(0, -1),
|
|
4499
|
-
destination: parts[parts.length - 1]
|
|
4500
|
-
};
|
|
4501
|
-
}
|
|
4502
|
-
function parseEnvPairs(value, lineNumber) {
|
|
4503
|
-
const tokens = shellSplit(value);
|
|
4504
|
-
if (tokens.length === 0) {
|
|
4505
|
-
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
4506
|
-
}
|
|
4507
|
-
if (tokens.every((token) => token.includes("="))) {
|
|
4508
|
-
return tokens.map((token) => {
|
|
4509
|
-
const [key, envValue] = token.split(/=(.*)/s, 2);
|
|
4510
|
-
if (!key) {
|
|
4511
|
-
throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
|
|
4512
|
-
}
|
|
4513
|
-
return [key, envValue];
|
|
4514
|
-
});
|
|
4515
|
-
}
|
|
4516
|
-
if (tokens.length < 2) {
|
|
4517
|
-
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
4518
|
-
}
|
|
4519
|
-
return [[tokens[0], tokens.slice(1).join(" ")]];
|
|
4520
|
-
}
|
|
4521
|
-
function resolveContainerPath(containerPath, workingDir) {
|
|
4522
|
-
if (!containerPath) {
|
|
4523
|
-
return workingDir;
|
|
4524
|
-
}
|
|
4525
|
-
const normalized = containerPath.startsWith("/") ? path.posix.normalize(containerPath) : path.posix.normalize(path.posix.join(workingDir, containerPath));
|
|
4526
|
-
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
4527
|
-
}
|
|
4528
4464
|
function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
|
|
4529
4465
|
let baseImage;
|
|
4530
4466
|
const instructions = [];
|
|
@@ -4635,14 +4571,292 @@ function buildContextFromEnv() {
|
|
|
4635
4571
|
};
|
|
4636
4572
|
}
|
|
4637
4573
|
function createDefaultClient(context) {
|
|
4574
|
+
const useScopeHeaders = context.personalAccessToken != null && context.apiKey == null;
|
|
4638
4575
|
return new SandboxClient({
|
|
4639
4576
|
apiUrl: context.apiUrl,
|
|
4640
4577
|
apiKey: context.apiKey ?? context.personalAccessToken,
|
|
4641
|
-
organizationId: context.organizationId,
|
|
4642
|
-
projectId: context.projectId,
|
|
4578
|
+
organizationId: useScopeHeaders ? context.organizationId : void 0,
|
|
4579
|
+
projectId: useScopeHeaders ? context.projectId : void 0,
|
|
4643
4580
|
namespace: context.namespace
|
|
4644
4581
|
});
|
|
4645
4582
|
}
|
|
4583
|
+
function baseApiUrl(context) {
|
|
4584
|
+
return context.apiUrl.replace(/\/+$/, "");
|
|
4585
|
+
}
|
|
4586
|
+
function scopedBuildsPath(context) {
|
|
4587
|
+
return `/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-template-builds`;
|
|
4588
|
+
}
|
|
4589
|
+
function platformHeaders(context) {
|
|
4590
|
+
const headers = {
|
|
4591
|
+
Authorization: `Bearer ${context.bearerToken}`,
|
|
4592
|
+
"Content-Type": "application/json"
|
|
4593
|
+
};
|
|
4594
|
+
if (context.useScopeHeaders) {
|
|
4595
|
+
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
4596
|
+
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
4597
|
+
}
|
|
4598
|
+
return headers;
|
|
4599
|
+
}
|
|
4600
|
+
async function requestJson(url, init, errorPrefix) {
|
|
4601
|
+
const response = await fetch(url, init);
|
|
4602
|
+
if (!response.ok) {
|
|
4603
|
+
throw new Error(
|
|
4604
|
+
`${errorPrefix} (HTTP ${response.status}): ${await response.text()}`
|
|
4605
|
+
);
|
|
4606
|
+
}
|
|
4607
|
+
const text = await response.text();
|
|
4608
|
+
return text ? JSON.parse(text) : {};
|
|
4609
|
+
}
|
|
4610
|
+
async function resolveBuildContext(context) {
|
|
4611
|
+
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
4612
|
+
if (!bearerToken) {
|
|
4613
|
+
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
4614
|
+
}
|
|
4615
|
+
if (context.apiKey) {
|
|
4616
|
+
const scope = await requestJson(
|
|
4617
|
+
`${baseApiUrl(context)}/platform/v1/keys/introspect`,
|
|
4618
|
+
{
|
|
4619
|
+
method: "POST",
|
|
4620
|
+
headers: {
|
|
4621
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
4622
|
+
"Content-Type": "application/json"
|
|
4623
|
+
}
|
|
4624
|
+
},
|
|
4625
|
+
"API key introspection failed"
|
|
4626
|
+
);
|
|
4627
|
+
if (!scope.organizationId || !scope.projectId) {
|
|
4628
|
+
throw new Error("API key introspection response is missing organizationId or projectId");
|
|
4629
|
+
}
|
|
4630
|
+
return {
|
|
4631
|
+
...context,
|
|
4632
|
+
bearerToken,
|
|
4633
|
+
organizationId: scope.organizationId,
|
|
4634
|
+
projectId: scope.projectId,
|
|
4635
|
+
useScopeHeaders: false
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
if (!context.organizationId || !context.projectId) {
|
|
4639
|
+
throw new Error(
|
|
4640
|
+
"Personal Access Token authentication requires TENSORLAKE_ORGANIZATION_ID and TENSORLAKE_PROJECT_ID to be set (e.g. via 'tl login && tl init'). To skip this requirement, authenticate with TENSORLAKE_API_KEY instead \u2014 API keys are bound to a single project at creation."
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4643
|
+
return {
|
|
4644
|
+
...context,
|
|
4645
|
+
bearerToken,
|
|
4646
|
+
organizationId: context.organizationId,
|
|
4647
|
+
projectId: context.projectId,
|
|
4648
|
+
useScopeHeaders: true
|
|
4649
|
+
};
|
|
4650
|
+
}
|
|
4651
|
+
async function prepareRootfsBuild(context, plan, isPublic) {
|
|
4652
|
+
if (!plan.baseImage) {
|
|
4653
|
+
throw new Error("Sandbox image builds require a Dockerfile FROM image or Image baseImage");
|
|
4654
|
+
}
|
|
4655
|
+
const spec = await requestJson(
|
|
4656
|
+
`${baseApiUrl(context)}${scopedBuildsPath(context)}`,
|
|
4657
|
+
{
|
|
4658
|
+
method: "POST",
|
|
4659
|
+
headers: platformHeaders(context),
|
|
4660
|
+
body: JSON.stringify({
|
|
4661
|
+
name: plan.registeredName,
|
|
4662
|
+
dockerfile: plan.dockerfileText,
|
|
4663
|
+
baseImage: plan.baseImage,
|
|
4664
|
+
public: isPublic
|
|
4665
|
+
})
|
|
4666
|
+
},
|
|
4667
|
+
"failed to prepare sandbox image build"
|
|
4668
|
+
);
|
|
4669
|
+
return { prepared: parsePreparedBuild(spec), spec };
|
|
4670
|
+
}
|
|
4671
|
+
function parsePreparedBuild(raw) {
|
|
4672
|
+
const builder = raw.builder;
|
|
4673
|
+
if (!builder) {
|
|
4674
|
+
throw new Error("platform API response is missing rootfs builder configuration");
|
|
4675
|
+
}
|
|
4676
|
+
const prepared = {
|
|
4677
|
+
...raw,
|
|
4678
|
+
buildId: requiredString(raw, "buildId"),
|
|
4679
|
+
snapshotId: requiredString(raw, "snapshotId"),
|
|
4680
|
+
snapshotUri: requiredString(raw, "snapshotUri"),
|
|
4681
|
+
rootfsNodeKind: requiredString(raw, "rootfsNodeKind"),
|
|
4682
|
+
builder: {
|
|
4683
|
+
image: requiredString(builder, "image"),
|
|
4684
|
+
command: requiredString(builder, "command"),
|
|
4685
|
+
cpus: requiredNumber(builder, "cpus"),
|
|
4686
|
+
memoryMb: requiredNumber(builder, "memoryMb"),
|
|
4687
|
+
diskMb: requiredNumber(builder, "diskMb")
|
|
4688
|
+
}
|
|
4689
|
+
};
|
|
4690
|
+
const parent = raw.parent;
|
|
4691
|
+
if (parent != null) {
|
|
4692
|
+
prepared.parent = {
|
|
4693
|
+
parentManifestUri: requiredString(parent, "parentManifestUri"),
|
|
4694
|
+
rootfsDiskBytes: optionalNumber(parent, "rootfsDiskBytes")
|
|
4695
|
+
};
|
|
4696
|
+
}
|
|
4697
|
+
return prepared;
|
|
4698
|
+
}
|
|
4699
|
+
async function completeRootfsBuild(context, buildId, request) {
|
|
4700
|
+
return requestJson(
|
|
4701
|
+
`${baseApiUrl(context)}${scopedBuildsPath(context)}/${encodeURIComponent(buildId)}/complete`,
|
|
4702
|
+
{
|
|
4703
|
+
method: "POST",
|
|
4704
|
+
headers: platformHeaders(context),
|
|
4705
|
+
body: JSON.stringify(request)
|
|
4706
|
+
},
|
|
4707
|
+
"failed to complete sandbox image build"
|
|
4708
|
+
);
|
|
4709
|
+
}
|
|
4710
|
+
async function resolvedDockerConfigJson() {
|
|
4711
|
+
const configDir = process.env.DOCKER_CONFIG ?? path.join(homedir(), ".docker");
|
|
4712
|
+
const configPath = path.join(configDir, "config.json");
|
|
4713
|
+
try {
|
|
4714
|
+
const content = await readFile(configPath, "utf8");
|
|
4715
|
+
const parsed = JSON.parse(content);
|
|
4716
|
+
const auths = parsed.auths;
|
|
4717
|
+
if (auths != null && Object.keys(auths).length > 0) {
|
|
4718
|
+
return JSON.stringify({ auths });
|
|
4719
|
+
}
|
|
4720
|
+
} catch (error) {
|
|
4721
|
+
if (error.code === "ENOENT") {
|
|
4722
|
+
return void 0;
|
|
4723
|
+
}
|
|
4724
|
+
throw error;
|
|
4725
|
+
}
|
|
4726
|
+
return void 0;
|
|
4727
|
+
}
|
|
4728
|
+
function rootfsDiskBytes(diskMb, prepared) {
|
|
4729
|
+
if (diskMb != null) {
|
|
4730
|
+
return diskMb * 1024 * 1024;
|
|
4731
|
+
}
|
|
4732
|
+
if (prepared.parent != null) {
|
|
4733
|
+
if (prepared.parent.rootfsDiskBytes == null) {
|
|
4734
|
+
throw new Error(
|
|
4735
|
+
"platform API did not return parent rootfsDiskBytes for diff build; pass diskMb explicitly or update Platform API"
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4738
|
+
return prepared.parent.rootfsDiskBytes;
|
|
4739
|
+
}
|
|
4740
|
+
return DEFAULT_ROOTFS_DISK_MB * 1024 * 1024;
|
|
4741
|
+
}
|
|
4742
|
+
function rootfsDiskBytesToMb(bytes) {
|
|
4743
|
+
return Math.ceil(bytes / (1024 * 1024));
|
|
4744
|
+
}
|
|
4745
|
+
async function buildRootfsSpec(preparedSpec, prepared, plan, diskMb) {
|
|
4746
|
+
const spec = {
|
|
4747
|
+
...preparedSpec,
|
|
4748
|
+
dockerfile: plan.dockerfileText,
|
|
4749
|
+
contextDir: REMOTE_CONTEXT_DIR,
|
|
4750
|
+
baseImage: plan.baseImage,
|
|
4751
|
+
rootfsDiskBytes: rootfsDiskBytes(diskMb, prepared)
|
|
4752
|
+
};
|
|
4753
|
+
const dockerConfigJson = await resolvedDockerConfigJson();
|
|
4754
|
+
if (dockerConfigJson != null) {
|
|
4755
|
+
spec.dockerConfigJson = dockerConfigJson;
|
|
4756
|
+
}
|
|
4757
|
+
return spec;
|
|
4758
|
+
}
|
|
4759
|
+
function rootfsBuilderExecutable(executable) {
|
|
4760
|
+
return executable === ROOTFS_BUILDER_COMMAND ? `${ROOTFS_BUILDER_BIN_DIR}/${ROOTFS_BUILDER_COMMAND}` : executable;
|
|
4761
|
+
}
|
|
4762
|
+
function rootfsBuilderEnv() {
|
|
4763
|
+
return { PATH: ROOTFS_BUILDER_PATH };
|
|
4764
|
+
}
|
|
4765
|
+
async function runRootfsBuilder(sandbox, command, emit, sleep3) {
|
|
4766
|
+
const parts = shellSplit(command);
|
|
4767
|
+
const [executable, ...commandArgs] = parts;
|
|
4768
|
+
if (!executable) {
|
|
4769
|
+
throw new Error("empty rootfs builder command returned by platform API");
|
|
4770
|
+
}
|
|
4771
|
+
await runStreaming(
|
|
4772
|
+
sandbox,
|
|
4773
|
+
emit,
|
|
4774
|
+
sleep3,
|
|
4775
|
+
rootfsBuilderExecutable(executable),
|
|
4776
|
+
[...commandArgs, "--spec", REMOTE_SPEC_PATH, "--metadata-out", REMOTE_METADATA_PATH],
|
|
4777
|
+
rootfsBuilderEnv(),
|
|
4778
|
+
REMOTE_BUILD_DIR
|
|
4779
|
+
);
|
|
4780
|
+
}
|
|
4781
|
+
function metadataString(metadata, snakeKey, camelKey) {
|
|
4782
|
+
const value = metadata[snakeKey] ?? metadata[camelKey];
|
|
4783
|
+
return typeof value === "string" ? value : void 0;
|
|
4784
|
+
}
|
|
4785
|
+
function metadataNumber(metadata, snakeKey, camelKey) {
|
|
4786
|
+
const value = metadata[snakeKey] ?? metadata[camelKey];
|
|
4787
|
+
if (typeof value === "number") {
|
|
4788
|
+
return value;
|
|
4789
|
+
}
|
|
4790
|
+
if (typeof value === "string" && value.trim()) {
|
|
4791
|
+
const parsed = Number(value);
|
|
4792
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
4793
|
+
}
|
|
4794
|
+
return void 0;
|
|
4795
|
+
}
|
|
4796
|
+
function completeRequestFromMetadata(prepared, metadata) {
|
|
4797
|
+
const rootfsNodeKind = metadataString(metadata, "rootfs_node_kind", "rootfsNodeKind") ?? prepared.rootfsNodeKind;
|
|
4798
|
+
const parentManifestUri = metadataString(metadata, "parent_manifest_uri", "parentManifestUri") ?? (rootfsNodeKind === "diff" ? prepared.parent?.parentManifestUri : void 0);
|
|
4799
|
+
if (rootfsNodeKind === "diff" && parentManifestUri == null) {
|
|
4800
|
+
throw new Error("rootfs diff build completed without parent_manifest_uri");
|
|
4801
|
+
}
|
|
4802
|
+
const snapshotFormatVersion = metadataString(
|
|
4803
|
+
metadata,
|
|
4804
|
+
"snapshot_format_version",
|
|
4805
|
+
"snapshotFormatVersion"
|
|
4806
|
+
);
|
|
4807
|
+
const snapshotSizeBytes = metadataNumber(
|
|
4808
|
+
metadata,
|
|
4809
|
+
"snapshot_size_bytes",
|
|
4810
|
+
"snapshotSizeBytes"
|
|
4811
|
+
);
|
|
4812
|
+
const rootfsDiskBytesValue = metadataNumber(
|
|
4813
|
+
metadata,
|
|
4814
|
+
"rootfs_disk_bytes",
|
|
4815
|
+
"rootfsDiskBytes"
|
|
4816
|
+
);
|
|
4817
|
+
if (!snapshotFormatVersion) {
|
|
4818
|
+
throw new Error("rootfs builder metadata is missing snapshot_format_version");
|
|
4819
|
+
}
|
|
4820
|
+
if (snapshotSizeBytes == null) {
|
|
4821
|
+
throw new Error("rootfs builder metadata is missing numeric snapshot_size_bytes");
|
|
4822
|
+
}
|
|
4823
|
+
if (rootfsDiskBytesValue == null) {
|
|
4824
|
+
throw new Error("rootfs builder metadata is missing numeric rootfs_disk_bytes");
|
|
4825
|
+
}
|
|
4826
|
+
return {
|
|
4827
|
+
snapshotId: metadataString(metadata, "snapshot_id", "snapshotId") ?? prepared.snapshotId,
|
|
4828
|
+
snapshotUri: metadataString(metadata, "snapshot_uri", "snapshotUri") ?? prepared.snapshotUri,
|
|
4829
|
+
snapshotFormatVersion,
|
|
4830
|
+
snapshotSizeBytes,
|
|
4831
|
+
rootfsDiskBytes: rootfsDiskBytesValue,
|
|
4832
|
+
rootfsNodeKind,
|
|
4833
|
+
...parentManifestUri ? { parentManifestUri } : {}
|
|
4834
|
+
};
|
|
4835
|
+
}
|
|
4836
|
+
function requiredString(object, key) {
|
|
4837
|
+
const value = object[key];
|
|
4838
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
4839
|
+
throw new Error(`expected '${key}' to be a non-empty string`);
|
|
4840
|
+
}
|
|
4841
|
+
return value;
|
|
4842
|
+
}
|
|
4843
|
+
function requiredNumber(object, key) {
|
|
4844
|
+
const value = object[key];
|
|
4845
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4846
|
+
throw new Error(`expected '${key}' to be a finite number`);
|
|
4847
|
+
}
|
|
4848
|
+
return value;
|
|
4849
|
+
}
|
|
4850
|
+
function optionalNumber(object, key) {
|
|
4851
|
+
const value = object[key];
|
|
4852
|
+
if (value == null) {
|
|
4853
|
+
return void 0;
|
|
4854
|
+
}
|
|
4855
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4856
|
+
throw new Error(`expected '${key}' to be a finite number`);
|
|
4857
|
+
}
|
|
4858
|
+
return value;
|
|
4859
|
+
}
|
|
4646
4860
|
async function runChecked(sandbox, command, args, env, workingDir) {
|
|
4647
4861
|
const result = await sandbox.run(command, {
|
|
4648
4862
|
args,
|
|
@@ -4702,18 +4916,6 @@ function emitOutputLines(emit, stream, response, seen) {
|
|
|
4702
4916
|
emit({ type: "build_log", stream, message: line });
|
|
4703
4917
|
}
|
|
4704
4918
|
}
|
|
4705
|
-
function isPathWithinContext(contextDir, localPath) {
|
|
4706
|
-
const relative = path.relative(contextDir, localPath);
|
|
4707
|
-
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
4708
|
-
}
|
|
4709
|
-
function resolveContextSourcePath(contextDir, source) {
|
|
4710
|
-
const resolvedContextDir = path.resolve(contextDir);
|
|
4711
|
-
const resolvedSource = path.resolve(resolvedContextDir, source);
|
|
4712
|
-
if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
|
|
4713
|
-
throw new Error(`Local path escapes the build context: ${source}`);
|
|
4714
|
-
}
|
|
4715
|
-
return resolvedSource;
|
|
4716
|
-
}
|
|
4717
4919
|
async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
4718
4920
|
const fileStats = await stat(localPath).catch(() => null);
|
|
4719
4921
|
if (!fileStats) {
|
|
@@ -4744,184 +4946,26 @@ async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
|
4744
4946
|
}
|
|
4745
4947
|
}
|
|
4746
4948
|
}
|
|
4747
|
-
async function
|
|
4748
|
-
const exportLine = `export ${key}=${shellQuote(value)}`;
|
|
4749
|
-
await runChecked(
|
|
4750
|
-
sandbox,
|
|
4751
|
-
"sh",
|
|
4752
|
-
["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
|
|
4753
|
-
processEnv
|
|
4754
|
-
);
|
|
4755
|
-
}
|
|
4756
|
-
async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
|
|
4757
|
-
const destinationPath = resolveContainerPath(destination, workingDir);
|
|
4758
|
-
if (sources.length > 1 && !destinationPath.endsWith("/")) {
|
|
4759
|
-
throw new Error(
|
|
4760
|
-
`${keyword} with multiple sources requires a directory destination ending in '/'`
|
|
4761
|
-
);
|
|
4762
|
-
}
|
|
4763
|
-
for (const source of sources) {
|
|
4764
|
-
const localSource = resolveContextSourcePath(contextDir, source);
|
|
4765
|
-
const localStats = await stat(localSource).catch(() => null);
|
|
4766
|
-
if (!localStats) {
|
|
4767
|
-
throw new Error(`Local path not found: ${localSource}`);
|
|
4768
|
-
}
|
|
4769
|
-
let remoteDestination = destinationPath;
|
|
4770
|
-
if (sources.length > 1) {
|
|
4771
|
-
remoteDestination = path.posix.join(
|
|
4772
|
-
destinationPath.replace(/\/$/, ""),
|
|
4773
|
-
path.posix.basename(source.replace(/\/$/, ""))
|
|
4774
|
-
);
|
|
4775
|
-
} else if (localStats.isFile() && destinationPath.endsWith("/")) {
|
|
4776
|
-
remoteDestination = path.posix.join(
|
|
4777
|
-
destinationPath.replace(/\/$/, ""),
|
|
4778
|
-
path.basename(source)
|
|
4779
|
-
);
|
|
4780
|
-
}
|
|
4781
|
-
emit({
|
|
4782
|
-
type: "status",
|
|
4783
|
-
message: `${keyword} ${source} -> ${remoteDestination}`
|
|
4784
|
-
});
|
|
4785
|
-
await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4788
|
-
async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep3) {
|
|
4789
|
-
let destinationPath = resolveContainerPath(destination, workingDir);
|
|
4790
|
-
const parsedUrl = new URL(url);
|
|
4791
|
-
const fileName = path.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
|
|
4792
|
-
if (destinationPath.endsWith("/")) {
|
|
4793
|
-
destinationPath = path.posix.join(destinationPath.replace(/\/$/, ""), fileName);
|
|
4794
|
-
}
|
|
4795
|
-
const parentDir = path.posix.dirname(destinationPath) || "/";
|
|
4796
|
-
emit({
|
|
4797
|
-
type: "status",
|
|
4798
|
-
message: `ADD ${url} -> ${destinationPath}`
|
|
4799
|
-
});
|
|
4800
|
-
await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
|
|
4801
|
-
await runStreaming(
|
|
4802
|
-
sandbox,
|
|
4803
|
-
emit,
|
|
4804
|
-
sleep3,
|
|
4805
|
-
"sh",
|
|
4806
|
-
[
|
|
4807
|
-
"-c",
|
|
4808
|
-
`curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
|
|
4809
|
-
],
|
|
4810
|
-
processEnv,
|
|
4811
|
-
workingDir
|
|
4812
|
-
);
|
|
4813
|
-
}
|
|
4814
|
-
async function executeDockerfilePlan(sandbox, plan, emit, sleep3) {
|
|
4815
|
-
const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
|
|
4816
|
-
let workingDir = "/";
|
|
4817
|
-
for (const instruction of plan.instructions) {
|
|
4818
|
-
const { keyword, value, lineNumber } = instruction;
|
|
4819
|
-
if (keyword === "RUN") {
|
|
4820
|
-
emit({ type: "status", message: `RUN ${value}` });
|
|
4821
|
-
await runStreaming(
|
|
4822
|
-
sandbox,
|
|
4823
|
-
emit,
|
|
4824
|
-
sleep3,
|
|
4825
|
-
"sh",
|
|
4826
|
-
["-c", value],
|
|
4827
|
-
processEnv,
|
|
4828
|
-
workingDir
|
|
4829
|
-
);
|
|
4830
|
-
continue;
|
|
4831
|
-
}
|
|
4832
|
-
if (keyword === "WORKDIR") {
|
|
4833
|
-
const tokens = shellSplit(value);
|
|
4834
|
-
if (tokens.length !== 1) {
|
|
4835
|
-
throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
|
|
4836
|
-
}
|
|
4837
|
-
workingDir = resolveContainerPath(tokens[0], workingDir);
|
|
4838
|
-
emit({ type: "status", message: `WORKDIR ${workingDir}` });
|
|
4839
|
-
await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
|
|
4840
|
-
continue;
|
|
4841
|
-
}
|
|
4842
|
-
if (keyword === "ENV") {
|
|
4843
|
-
for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
|
|
4844
|
-
emit({ type: "status", message: `ENV ${key}=${envValue}` });
|
|
4845
|
-
processEnv[key] = envValue;
|
|
4846
|
-
await persistEnvVar(sandbox, processEnv, key, envValue);
|
|
4847
|
-
}
|
|
4848
|
-
continue;
|
|
4849
|
-
}
|
|
4850
|
-
if (keyword === "COPY") {
|
|
4851
|
-
const { sources, destination } = parseCopyLikeValues(
|
|
4852
|
-
value,
|
|
4853
|
-
lineNumber,
|
|
4854
|
-
keyword
|
|
4855
|
-
);
|
|
4856
|
-
await copyFromContext(
|
|
4857
|
-
sandbox,
|
|
4858
|
-
emit,
|
|
4859
|
-
plan.contextDir,
|
|
4860
|
-
sources,
|
|
4861
|
-
destination,
|
|
4862
|
-
workingDir,
|
|
4863
|
-
keyword
|
|
4864
|
-
);
|
|
4865
|
-
continue;
|
|
4866
|
-
}
|
|
4867
|
-
if (keyword === "ADD") {
|
|
4868
|
-
const { sources, destination } = parseCopyLikeValues(
|
|
4869
|
-
value,
|
|
4870
|
-
lineNumber,
|
|
4871
|
-
keyword
|
|
4872
|
-
);
|
|
4873
|
-
if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
|
|
4874
|
-
await addUrlToSandbox(
|
|
4875
|
-
sandbox,
|
|
4876
|
-
emit,
|
|
4877
|
-
sources[0],
|
|
4878
|
-
destination,
|
|
4879
|
-
workingDir,
|
|
4880
|
-
processEnv,
|
|
4881
|
-
sleep3
|
|
4882
|
-
);
|
|
4883
|
-
} else {
|
|
4884
|
-
await copyFromContext(
|
|
4885
|
-
sandbox,
|
|
4886
|
-
emit,
|
|
4887
|
-
plan.contextDir,
|
|
4888
|
-
sources,
|
|
4889
|
-
destination,
|
|
4890
|
-
workingDir,
|
|
4891
|
-
keyword
|
|
4892
|
-
);
|
|
4893
|
-
}
|
|
4894
|
-
continue;
|
|
4895
|
-
}
|
|
4896
|
-
if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
4897
|
-
emit({
|
|
4898
|
-
type: "warning",
|
|
4899
|
-
message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
|
|
4900
|
-
});
|
|
4901
|
-
continue;
|
|
4902
|
-
}
|
|
4903
|
-
throw new Error(
|
|
4904
|
-
`line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
4905
|
-
);
|
|
4906
|
-
}
|
|
4907
|
-
}
|
|
4908
|
-
async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes, isPublic) {
|
|
4909
|
-
if (!context.organizationId || !context.projectId) {
|
|
4910
|
-
throw new Error(
|
|
4911
|
-
"Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
|
|
4912
|
-
);
|
|
4913
|
-
}
|
|
4949
|
+
async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes2, isPublic, snapshotFormatVersion) {
|
|
4914
4950
|
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
4915
4951
|
if (!bearerToken) {
|
|
4916
4952
|
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
4917
4953
|
}
|
|
4918
4954
|
const baseUrl = context.apiUrl.replace(/\/+$/, "");
|
|
4919
|
-
const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
4920
4955
|
const headers = {
|
|
4921
4956
|
Authorization: `Bearer ${bearerToken}`,
|
|
4922
4957
|
"Content-Type": "application/json"
|
|
4923
4958
|
};
|
|
4924
|
-
|
|
4959
|
+
let url;
|
|
4960
|
+
if (context.apiKey) {
|
|
4961
|
+
url = `${baseUrl}/platform/v1/sandbox-templates`;
|
|
4962
|
+
} else {
|
|
4963
|
+
if (!context.organizationId || !context.projectId) {
|
|
4964
|
+
throw new Error(
|
|
4965
|
+
"Personal Access Token authentication requires TENSORLAKE_ORGANIZATION_ID and TENSORLAKE_PROJECT_ID to be set (e.g. via 'tl login && tl init'). To skip this requirement, authenticate with TENSORLAKE_API_KEY instead \u2014 API keys are bound to a single project at creation."
|
|
4966
|
+
);
|
|
4967
|
+
}
|
|
4968
|
+
url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
4925
4969
|
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
4926
4970
|
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
4927
4971
|
}
|
|
@@ -4934,8 +4978,9 @@ async function registerImage(context, name, dockerfile, snapshotId, snapshotSand
|
|
|
4934
4978
|
snapshotId,
|
|
4935
4979
|
snapshotSandboxId,
|
|
4936
4980
|
snapshotUri,
|
|
4981
|
+
...snapshotFormatVersion ? { snapshotFormatVersion } : {},
|
|
4937
4982
|
snapshotSizeBytes,
|
|
4938
|
-
rootfsDiskBytes,
|
|
4983
|
+
rootfsDiskBytes: rootfsDiskBytes2,
|
|
4939
4984
|
public: isPublic
|
|
4940
4985
|
})
|
|
4941
4986
|
});
|
|
@@ -4952,65 +4997,71 @@ async function createSandboxImage(source, options = {}, deps = {}) {
|
|
|
4952
4997
|
const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4953
4998
|
const context = buildContextFromEnv();
|
|
4954
4999
|
const clientFactory = deps.createClient ?? createDefaultClient;
|
|
4955
|
-
const register = deps.registerImage ?? ((...args) => registerImage(...args));
|
|
4956
5000
|
const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
|
|
4957
5001
|
emit({ type: "status", message: `Loading ${sourceLabel}...` });
|
|
4958
5002
|
const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
|
|
4959
5003
|
emit({
|
|
4960
5004
|
type: "status",
|
|
4961
|
-
message:
|
|
5005
|
+
message: `Selected image name: ${plan.registeredName}`
|
|
5006
|
+
});
|
|
5007
|
+
emit({ type: "status", message: "Preparing rootfs build..." });
|
|
5008
|
+
const resolvedContext = await resolveBuildContext(context);
|
|
5009
|
+
const { prepared, spec: preparedSpec } = await prepareRootfsBuild(
|
|
5010
|
+
resolvedContext,
|
|
5011
|
+
plan,
|
|
5012
|
+
options.isPublic ?? false
|
|
5013
|
+
);
|
|
5014
|
+
emit({
|
|
5015
|
+
type: "status",
|
|
5016
|
+
message: prepared.rootfsNodeKind === "diff" ? "Build mode: RootfsDiff" : "Build mode: RootfsBase"
|
|
4962
5017
|
});
|
|
4963
5018
|
const client = clientFactory(context);
|
|
4964
5019
|
let sandbox;
|
|
4965
5020
|
try {
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
});
|
|
5021
|
+
const outputRootfsDiskBytes = rootfsDiskBytes(options.diskMb, prepared);
|
|
5022
|
+
const builderDiskMb = Math.max(
|
|
5023
|
+
rootfsDiskBytesToMb(outputRootfsDiskBytes),
|
|
5024
|
+
options.builderDiskMb ?? prepared.builder.diskMb
|
|
5025
|
+
);
|
|
4972
5026
|
emit({
|
|
4973
5027
|
type: "status",
|
|
4974
|
-
message: `
|
|
5028
|
+
message: `Creating rootfs builder sandbox from ${prepared.builder.image}...`
|
|
4975
5029
|
});
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
emit({
|
|
4982
|
-
type: "snapshot_created",
|
|
4983
|
-
snapshot_id: snapshot.snapshotId
|
|
5030
|
+
sandbox = await client.createAndConnect({
|
|
5031
|
+
image: prepared.builder.image,
|
|
5032
|
+
cpus: options.cpus ?? prepared.builder.cpus,
|
|
5033
|
+
memoryMb: options.memoryMb ?? prepared.builder.memoryMb,
|
|
5034
|
+
diskMb: builderDiskMb
|
|
4984
5035
|
});
|
|
4985
|
-
if (!snapshot.snapshotUri) {
|
|
4986
|
-
throw new Error(
|
|
4987
|
-
`Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
|
|
4988
|
-
);
|
|
4989
|
-
}
|
|
4990
|
-
if (snapshot.sizeBytes == null) {
|
|
4991
|
-
throw new Error(
|
|
4992
|
-
`Snapshot ${snapshot.snapshotId} is missing sizeBytes and cannot be registered as a sandbox image.`
|
|
4993
|
-
);
|
|
4994
|
-
}
|
|
4995
|
-
if (snapshot.rootfsDiskBytes == null) {
|
|
4996
|
-
throw new Error(
|
|
4997
|
-
`Snapshot ${snapshot.snapshotId} is missing rootfsDiskBytes and cannot be registered as a sandbox image.`
|
|
4998
|
-
);
|
|
4999
|
-
}
|
|
5000
5036
|
emit({
|
|
5001
5037
|
type: "status",
|
|
5002
|
-
message: `
|
|
5038
|
+
message: `Rootfs builder sandbox ${sandbox.sandboxId} is running`
|
|
5003
5039
|
});
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5040
|
+
emit({ type: "status", message: "Uploading build context..." });
|
|
5041
|
+
await copyLocalPathToSandbox(sandbox, plan.contextDir, REMOTE_CONTEXT_DIR);
|
|
5042
|
+
const spec = await buildRootfsSpec(
|
|
5043
|
+
preparedSpec,
|
|
5044
|
+
prepared,
|
|
5045
|
+
plan,
|
|
5046
|
+
options.diskMb
|
|
5047
|
+
);
|
|
5048
|
+
await runChecked(sandbox, "mkdir", ["-p", path.posix.dirname(REMOTE_SPEC_PATH)]);
|
|
5049
|
+
await sandbox.writeFile(
|
|
5050
|
+
REMOTE_SPEC_PATH,
|
|
5051
|
+
new TextEncoder().encode(JSON.stringify(spec, null, 2))
|
|
5052
|
+
);
|
|
5053
|
+
emit({ type: "status", message: "Running offline rootfs builder..." });
|
|
5054
|
+
await runRootfsBuilder(sandbox, prepared.builder.command, emit, sleep3);
|
|
5055
|
+
const metadataBytes = await sandbox.readFile(REMOTE_METADATA_PATH);
|
|
5056
|
+
const metadata = JSON.parse(
|
|
5057
|
+
new TextDecoder().decode(metadataBytes)
|
|
5058
|
+
);
|
|
5059
|
+
const completeRequest = completeRequestFromMetadata(prepared, metadata);
|
|
5060
|
+
emit({ type: "status", message: "Completing image registration..." });
|
|
5061
|
+
const result = await completeRootfsBuild(
|
|
5062
|
+
resolvedContext,
|
|
5063
|
+
prepared.buildId,
|
|
5064
|
+
completeRequest
|
|
5014
5065
|
);
|
|
5015
5066
|
emit({
|
|
5016
5067
|
type: "image_registered",
|
|
@@ -5038,16 +5089,18 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5038
5089
|
cpus: { type: "string" },
|
|
5039
5090
|
memory: { type: "string" },
|
|
5040
5091
|
disk_mb: { type: "string" },
|
|
5092
|
+
builder_disk_mb: { type: "string" },
|
|
5041
5093
|
public: { type: "boolean", default: false }
|
|
5042
5094
|
}
|
|
5043
5095
|
});
|
|
5044
5096
|
const dockerfilePath = parsed.positionals[0];
|
|
5045
5097
|
if (!dockerfilePath) {
|
|
5046
|
-
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--public]");
|
|
5098
|
+
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--builder_disk_mb MB] [--public]");
|
|
5047
5099
|
}
|
|
5048
5100
|
const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
|
|
5049
5101
|
const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
|
|
5050
5102
|
const diskMb = parsed.values.disk_mb != null ? Number(parsed.values.disk_mb) : void 0;
|
|
5103
|
+
const builderDiskMb = parsed.values.builder_disk_mb != null ? Number(parsed.values.builder_disk_mb) : void 0;
|
|
5051
5104
|
if (cpus != null && !Number.isFinite(cpus)) {
|
|
5052
5105
|
throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
|
|
5053
5106
|
}
|
|
@@ -5057,6 +5110,11 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5057
5110
|
if (diskMb != null && !Number.isInteger(diskMb)) {
|
|
5058
5111
|
throw new Error(`Invalid --disk_mb value: ${parsed.values.disk_mb}`);
|
|
5059
5112
|
}
|
|
5113
|
+
if (builderDiskMb != null && !Number.isInteger(builderDiskMb)) {
|
|
5114
|
+
throw new Error(
|
|
5115
|
+
`Invalid --builder_disk_mb value: ${parsed.values.builder_disk_mb}`
|
|
5116
|
+
);
|
|
5117
|
+
}
|
|
5060
5118
|
await createSandboxImage(
|
|
5061
5119
|
dockerfilePath,
|
|
5062
5120
|
{
|
|
@@ -5064,27 +5122,26 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5064
5122
|
cpus,
|
|
5065
5123
|
memoryMb,
|
|
5066
5124
|
diskMb,
|
|
5125
|
+
builderDiskMb,
|
|
5067
5126
|
isPublic: parsed.values.public
|
|
5068
5127
|
},
|
|
5069
5128
|
{ emit: ndjsonStdoutEmit }
|
|
5070
5129
|
);
|
|
5071
5130
|
}
|
|
5072
|
-
var
|
|
5131
|
+
var DEFAULT_ROOTFS_DISK_MB, REMOTE_BUILD_DIR, REMOTE_CONTEXT_DIR, REMOTE_SPEC_PATH, REMOTE_METADATA_PATH, ROOTFS_BUILDER_BIN_DIR, ROOTFS_BUILDER_PATH, ROOTFS_BUILDER_COMMAND, UNSUPPORTED_DOCKERFILE_INSTRUCTIONS;
|
|
5073
5132
|
var init_sandbox_image = __esm({
|
|
5074
5133
|
"src/sandbox-image.ts"() {
|
|
5075
5134
|
init_models();
|
|
5076
5135
|
init_client();
|
|
5077
5136
|
init_image();
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
"VOLUME"
|
|
5087
|
-
]);
|
|
5137
|
+
DEFAULT_ROOTFS_DISK_MB = 10 * 1024;
|
|
5138
|
+
REMOTE_BUILD_DIR = "/var/lib/tensorlake/rootfs-builder/build";
|
|
5139
|
+
REMOTE_CONTEXT_DIR = "/var/lib/tensorlake/rootfs-builder/build/context";
|
|
5140
|
+
REMOTE_SPEC_PATH = "/var/lib/tensorlake/rootfs-builder/build/spec.json";
|
|
5141
|
+
REMOTE_METADATA_PATH = "/var/lib/tensorlake/rootfs-builder/build/metadata.json";
|
|
5142
|
+
ROOTFS_BUILDER_BIN_DIR = "/usr/local/bin";
|
|
5143
|
+
ROOTFS_BUILDER_PATH = "/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
5144
|
+
ROOTFS_BUILDER_COMMAND = "tl-rootfs-build";
|
|
5088
5145
|
UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
5089
5146
|
"ARG",
|
|
5090
5147
|
"ONBUILD",
|
|
@@ -5100,6 +5157,7 @@ export {
|
|
|
5100
5157
|
loadDockerfilePlan,
|
|
5101
5158
|
loadImagePlan,
|
|
5102
5159
|
logicalDockerfileLines,
|
|
5160
|
+
registerImage,
|
|
5103
5161
|
runCreateSandboxImageCli
|
|
5104
5162
|
};
|
|
5105
5163
|
//# sourceMappingURL=sandbox-image.js.map
|