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.cjs
CHANGED
|
@@ -83,7 +83,7 @@ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TI
|
|
|
83
83
|
var init_defaults = __esm({
|
|
84
84
|
"src/defaults.ts"() {
|
|
85
85
|
"use strict";
|
|
86
|
-
SDK_VERSION = "0.5.
|
|
86
|
+
SDK_VERSION = "0.5.11";
|
|
87
87
|
API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
|
|
88
88
|
API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
|
|
89
89
|
NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
|
|
@@ -3334,11 +3334,10 @@ var init_sandbox = __esm({
|
|
|
3334
3334
|
await client.resume(this.lifecycleIdentifier, options);
|
|
3335
3335
|
}
|
|
3336
3336
|
/**
|
|
3337
|
-
* Create a
|
|
3338
|
-
* be committed.
|
|
3337
|
+
* Create a checkpoint of this sandbox and wait for it to be locally ready.
|
|
3339
3338
|
*
|
|
3340
|
-
* By default blocks until the
|
|
3341
|
-
*
|
|
3339
|
+
* By default blocks until the checkpoint is resumable and returns
|
|
3340
|
+
* `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
|
|
3342
3341
|
* (returns `undefined`).
|
|
3343
3342
|
*/
|
|
3344
3343
|
async checkpoint(options) {
|
|
@@ -3350,7 +3349,8 @@ var init_sandbox = __esm({
|
|
|
3350
3349
|
return client.snapshotAndWait(this.lifecycleIdentifier, {
|
|
3351
3350
|
timeout: options?.timeout,
|
|
3352
3351
|
pollInterval: options?.pollInterval,
|
|
3353
|
-
snapshotType: options?.checkpointType
|
|
3352
|
+
snapshotType: options?.checkpointType,
|
|
3353
|
+
waitUntil: options?.waitUntil
|
|
3354
3354
|
});
|
|
3355
3355
|
}
|
|
3356
3356
|
/**
|
|
@@ -3745,6 +3745,12 @@ __export(client_exports, {
|
|
|
3745
3745
|
function sleep2(ms) {
|
|
3746
3746
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3747
3747
|
}
|
|
3748
|
+
function snapshotStatusSatisfiesWaitCondition(status, waitUntil) {
|
|
3749
|
+
if (waitUntil === "local_ready") {
|
|
3750
|
+
return status === "local_ready" /* LOCAL_READY */ || status === "completed" /* COMPLETED */;
|
|
3751
|
+
}
|
|
3752
|
+
return status === "completed" /* COMPLETED */;
|
|
3753
|
+
}
|
|
3748
3754
|
function formatStartupFailureMessage(sandboxId, status, options) {
|
|
3749
3755
|
const prefix = status === "terminated" /* TERMINATED */ ? `Sandbox ${sandboxId} terminated during startup` : `Sandbox ${sandboxId} became ${status} during startup`;
|
|
3750
3756
|
const detail = formatErrorDetails(options.errorDetails);
|
|
@@ -4045,7 +4051,8 @@ var init_client = __esm({
|
|
|
4045
4051
|
*
|
|
4046
4052
|
* This call **returns immediately** with a `snapshotId` and `in_progress`
|
|
4047
4053
|
* status — the snapshot is created asynchronously. Poll `getSnapshot()` until
|
|
4048
|
-
* `completed
|
|
4054
|
+
* `local_ready`, `completed`, or `failed`, or use `snapshotAndWait()` to
|
|
4055
|
+
* block automatically.
|
|
4049
4056
|
*
|
|
4050
4057
|
* @param options.snapshotType - `"filesystem"` for cold-boot snapshots (e.g. image builds).
|
|
4051
4058
|
* Omit to use the server default (`filesystem`).
|
|
@@ -4086,9 +4093,11 @@ var init_client = __esm({
|
|
|
4086
4093
|
);
|
|
4087
4094
|
}
|
|
4088
4095
|
/**
|
|
4089
|
-
* Create a snapshot and block until it is
|
|
4096
|
+
* Create a snapshot and block until it is locally ready.
|
|
4090
4097
|
*
|
|
4091
|
-
* Combines `snapshot()` with polling `getSnapshot()` until `
|
|
4098
|
+
* Combines `snapshot()` with polling `getSnapshot()` until `local_ready`
|
|
4099
|
+
* or `completed`. Pass `{ waitUntil: "completed" }` when durable
|
|
4100
|
+
* `snapshotUri` metadata is required.
|
|
4092
4101
|
* Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
|
|
4093
4102
|
* without managing the client separately.
|
|
4094
4103
|
*
|
|
@@ -4101,13 +4110,14 @@ var init_client = __esm({
|
|
|
4101
4110
|
async snapshotAndWait(sandboxId, options) {
|
|
4102
4111
|
const timeout = options?.timeout ?? 300;
|
|
4103
4112
|
const pollInterval = options?.pollInterval ?? 1;
|
|
4113
|
+
const waitUntil = options?.waitUntil ?? "local_ready";
|
|
4104
4114
|
const result = await this.snapshot(sandboxId, {
|
|
4105
4115
|
snapshotType: options?.snapshotType
|
|
4106
4116
|
});
|
|
4107
4117
|
const deadline = Date.now() + timeout * 1e3;
|
|
4108
4118
|
while (Date.now() < deadline) {
|
|
4109
4119
|
const info = await this.getSnapshot(result.snapshotId);
|
|
4110
|
-
if (info.status
|
|
4120
|
+
if (snapshotStatusSatisfiesWaitCondition(info.status, waitUntil)) return info;
|
|
4111
4121
|
if (info.status === "failed" /* FAILED */) {
|
|
4112
4122
|
throw new SandboxError(
|
|
4113
4123
|
`Snapshot ${result.snapshotId} failed: ${info.error}`
|
|
@@ -4116,7 +4126,7 @@ var init_client = __esm({
|
|
|
4116
4126
|
await sleep2(pollInterval * 1e3);
|
|
4117
4127
|
}
|
|
4118
4128
|
throw new SandboxError(
|
|
4119
|
-
`Snapshot ${result.snapshotId} did not
|
|
4129
|
+
`Snapshot ${result.snapshotId} did not reach ${waitUntil} within ${timeout}s`
|
|
4120
4130
|
);
|
|
4121
4131
|
}
|
|
4122
4132
|
// --- Pools ---
|
|
@@ -4307,6 +4317,7 @@ __export(sandbox_image_exports, {
|
|
|
4307
4317
|
loadDockerfilePlan: () => loadDockerfilePlan,
|
|
4308
4318
|
loadImagePlan: () => loadImagePlan,
|
|
4309
4319
|
logicalDockerfileLines: () => logicalDockerfileLines,
|
|
4320
|
+
registerImage: () => registerImage,
|
|
4310
4321
|
runCreateSandboxImageCli: () => runCreateSandboxImageCli
|
|
4311
4322
|
});
|
|
4312
4323
|
module.exports = __toCommonJS(sandbox_image_exports);
|
|
@@ -4438,12 +4449,6 @@ function shellSplit(input) {
|
|
|
4438
4449
|
}
|
|
4439
4450
|
return tokens;
|
|
4440
4451
|
}
|
|
4441
|
-
function shellQuote(value) {
|
|
4442
|
-
if (!value) {
|
|
4443
|
-
return "''";
|
|
4444
|
-
}
|
|
4445
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4446
|
-
}
|
|
4447
4452
|
function stripLeadingFlags(value) {
|
|
4448
4453
|
const flags = {};
|
|
4449
4454
|
let remaining = value.trimStart();
|
|
@@ -4481,75 +4486,6 @@ function parseFromValue(value, lineNumber) {
|
|
|
4481
4486
|
}
|
|
4482
4487
|
return tokens[0];
|
|
4483
4488
|
}
|
|
4484
|
-
function parseCopyLikeValues(value, lineNumber, keyword) {
|
|
4485
|
-
const { flags, remaining } = stripLeadingFlags(value);
|
|
4486
|
-
if ("from" in flags) {
|
|
4487
|
-
throw new Error(
|
|
4488
|
-
`line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
|
|
4489
|
-
);
|
|
4490
|
-
}
|
|
4491
|
-
const payload = remaining.trim();
|
|
4492
|
-
if (!payload) {
|
|
4493
|
-
throw new Error(
|
|
4494
|
-
`line ${lineNumber}: ${keyword} must include source and destination`
|
|
4495
|
-
);
|
|
4496
|
-
}
|
|
4497
|
-
let parts;
|
|
4498
|
-
if (payload.startsWith("[")) {
|
|
4499
|
-
let parsed;
|
|
4500
|
-
try {
|
|
4501
|
-
parsed = JSON.parse(payload);
|
|
4502
|
-
} catch (error) {
|
|
4503
|
-
throw new Error(
|
|
4504
|
-
`line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
|
|
4505
|
-
);
|
|
4506
|
-
}
|
|
4507
|
-
if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
|
|
4508
|
-
throw new Error(
|
|
4509
|
-
`line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
|
|
4510
|
-
);
|
|
4511
|
-
}
|
|
4512
|
-
parts = parsed;
|
|
4513
|
-
} else {
|
|
4514
|
-
parts = shellSplit(payload);
|
|
4515
|
-
if (parts.length < 2) {
|
|
4516
|
-
throw new Error(
|
|
4517
|
-
`line ${lineNumber}: ${keyword} must include at least one source and one destination`
|
|
4518
|
-
);
|
|
4519
|
-
}
|
|
4520
|
-
}
|
|
4521
|
-
return {
|
|
4522
|
-
flags,
|
|
4523
|
-
sources: parts.slice(0, -1),
|
|
4524
|
-
destination: parts[parts.length - 1]
|
|
4525
|
-
};
|
|
4526
|
-
}
|
|
4527
|
-
function parseEnvPairs(value, lineNumber) {
|
|
4528
|
-
const tokens = shellSplit(value);
|
|
4529
|
-
if (tokens.length === 0) {
|
|
4530
|
-
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
4531
|
-
}
|
|
4532
|
-
if (tokens.every((token) => token.includes("="))) {
|
|
4533
|
-
return tokens.map((token) => {
|
|
4534
|
-
const [key, envValue] = token.split(/=(.*)/s, 2);
|
|
4535
|
-
if (!key) {
|
|
4536
|
-
throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
|
|
4537
|
-
}
|
|
4538
|
-
return [key, envValue];
|
|
4539
|
-
});
|
|
4540
|
-
}
|
|
4541
|
-
if (tokens.length < 2) {
|
|
4542
|
-
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
4543
|
-
}
|
|
4544
|
-
return [[tokens[0], tokens.slice(1).join(" ")]];
|
|
4545
|
-
}
|
|
4546
|
-
function resolveContainerPath(containerPath, workingDir) {
|
|
4547
|
-
if (!containerPath) {
|
|
4548
|
-
return workingDir;
|
|
4549
|
-
}
|
|
4550
|
-
const normalized = containerPath.startsWith("/") ? import_node_path.default.posix.normalize(containerPath) : import_node_path.default.posix.normalize(import_node_path.default.posix.join(workingDir, containerPath));
|
|
4551
|
-
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
4552
|
-
}
|
|
4553
4489
|
function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
|
|
4554
4490
|
let baseImage;
|
|
4555
4491
|
const instructions = [];
|
|
@@ -4660,14 +4596,292 @@ function buildContextFromEnv() {
|
|
|
4660
4596
|
};
|
|
4661
4597
|
}
|
|
4662
4598
|
function createDefaultClient(context) {
|
|
4599
|
+
const useScopeHeaders = context.personalAccessToken != null && context.apiKey == null;
|
|
4663
4600
|
return new SandboxClient({
|
|
4664
4601
|
apiUrl: context.apiUrl,
|
|
4665
4602
|
apiKey: context.apiKey ?? context.personalAccessToken,
|
|
4666
|
-
organizationId: context.organizationId,
|
|
4667
|
-
projectId: context.projectId,
|
|
4603
|
+
organizationId: useScopeHeaders ? context.organizationId : void 0,
|
|
4604
|
+
projectId: useScopeHeaders ? context.projectId : void 0,
|
|
4668
4605
|
namespace: context.namespace
|
|
4669
4606
|
});
|
|
4670
4607
|
}
|
|
4608
|
+
function baseApiUrl(context) {
|
|
4609
|
+
return context.apiUrl.replace(/\/+$/, "");
|
|
4610
|
+
}
|
|
4611
|
+
function scopedBuildsPath(context) {
|
|
4612
|
+
return `/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-template-builds`;
|
|
4613
|
+
}
|
|
4614
|
+
function platformHeaders(context) {
|
|
4615
|
+
const headers = {
|
|
4616
|
+
Authorization: `Bearer ${context.bearerToken}`,
|
|
4617
|
+
"Content-Type": "application/json"
|
|
4618
|
+
};
|
|
4619
|
+
if (context.useScopeHeaders) {
|
|
4620
|
+
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
4621
|
+
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
4622
|
+
}
|
|
4623
|
+
return headers;
|
|
4624
|
+
}
|
|
4625
|
+
async function requestJson(url, init, errorPrefix) {
|
|
4626
|
+
const response = await fetch(url, init);
|
|
4627
|
+
if (!response.ok) {
|
|
4628
|
+
throw new Error(
|
|
4629
|
+
`${errorPrefix} (HTTP ${response.status}): ${await response.text()}`
|
|
4630
|
+
);
|
|
4631
|
+
}
|
|
4632
|
+
const text = await response.text();
|
|
4633
|
+
return text ? JSON.parse(text) : {};
|
|
4634
|
+
}
|
|
4635
|
+
async function resolveBuildContext(context) {
|
|
4636
|
+
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
4637
|
+
if (!bearerToken) {
|
|
4638
|
+
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
4639
|
+
}
|
|
4640
|
+
if (context.apiKey) {
|
|
4641
|
+
const scope = await requestJson(
|
|
4642
|
+
`${baseApiUrl(context)}/platform/v1/keys/introspect`,
|
|
4643
|
+
{
|
|
4644
|
+
method: "POST",
|
|
4645
|
+
headers: {
|
|
4646
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
4647
|
+
"Content-Type": "application/json"
|
|
4648
|
+
}
|
|
4649
|
+
},
|
|
4650
|
+
"API key introspection failed"
|
|
4651
|
+
);
|
|
4652
|
+
if (!scope.organizationId || !scope.projectId) {
|
|
4653
|
+
throw new Error("API key introspection response is missing organizationId or projectId");
|
|
4654
|
+
}
|
|
4655
|
+
return {
|
|
4656
|
+
...context,
|
|
4657
|
+
bearerToken,
|
|
4658
|
+
organizationId: scope.organizationId,
|
|
4659
|
+
projectId: scope.projectId,
|
|
4660
|
+
useScopeHeaders: false
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
if (!context.organizationId || !context.projectId) {
|
|
4664
|
+
throw new Error(
|
|
4665
|
+
"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."
|
|
4666
|
+
);
|
|
4667
|
+
}
|
|
4668
|
+
return {
|
|
4669
|
+
...context,
|
|
4670
|
+
bearerToken,
|
|
4671
|
+
organizationId: context.organizationId,
|
|
4672
|
+
projectId: context.projectId,
|
|
4673
|
+
useScopeHeaders: true
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
async function prepareRootfsBuild(context, plan, isPublic) {
|
|
4677
|
+
if (!plan.baseImage) {
|
|
4678
|
+
throw new Error("Sandbox image builds require a Dockerfile FROM image or Image baseImage");
|
|
4679
|
+
}
|
|
4680
|
+
const spec = await requestJson(
|
|
4681
|
+
`${baseApiUrl(context)}${scopedBuildsPath(context)}`,
|
|
4682
|
+
{
|
|
4683
|
+
method: "POST",
|
|
4684
|
+
headers: platformHeaders(context),
|
|
4685
|
+
body: JSON.stringify({
|
|
4686
|
+
name: plan.registeredName,
|
|
4687
|
+
dockerfile: plan.dockerfileText,
|
|
4688
|
+
baseImage: plan.baseImage,
|
|
4689
|
+
public: isPublic
|
|
4690
|
+
})
|
|
4691
|
+
},
|
|
4692
|
+
"failed to prepare sandbox image build"
|
|
4693
|
+
);
|
|
4694
|
+
return { prepared: parsePreparedBuild(spec), spec };
|
|
4695
|
+
}
|
|
4696
|
+
function parsePreparedBuild(raw) {
|
|
4697
|
+
const builder = raw.builder;
|
|
4698
|
+
if (!builder) {
|
|
4699
|
+
throw new Error("platform API response is missing rootfs builder configuration");
|
|
4700
|
+
}
|
|
4701
|
+
const prepared = {
|
|
4702
|
+
...raw,
|
|
4703
|
+
buildId: requiredString(raw, "buildId"),
|
|
4704
|
+
snapshotId: requiredString(raw, "snapshotId"),
|
|
4705
|
+
snapshotUri: requiredString(raw, "snapshotUri"),
|
|
4706
|
+
rootfsNodeKind: requiredString(raw, "rootfsNodeKind"),
|
|
4707
|
+
builder: {
|
|
4708
|
+
image: requiredString(builder, "image"),
|
|
4709
|
+
command: requiredString(builder, "command"),
|
|
4710
|
+
cpus: requiredNumber(builder, "cpus"),
|
|
4711
|
+
memoryMb: requiredNumber(builder, "memoryMb"),
|
|
4712
|
+
diskMb: requiredNumber(builder, "diskMb")
|
|
4713
|
+
}
|
|
4714
|
+
};
|
|
4715
|
+
const parent = raw.parent;
|
|
4716
|
+
if (parent != null) {
|
|
4717
|
+
prepared.parent = {
|
|
4718
|
+
parentManifestUri: requiredString(parent, "parentManifestUri"),
|
|
4719
|
+
rootfsDiskBytes: optionalNumber(parent, "rootfsDiskBytes")
|
|
4720
|
+
};
|
|
4721
|
+
}
|
|
4722
|
+
return prepared;
|
|
4723
|
+
}
|
|
4724
|
+
async function completeRootfsBuild(context, buildId, request) {
|
|
4725
|
+
return requestJson(
|
|
4726
|
+
`${baseApiUrl(context)}${scopedBuildsPath(context)}/${encodeURIComponent(buildId)}/complete`,
|
|
4727
|
+
{
|
|
4728
|
+
method: "POST",
|
|
4729
|
+
headers: platformHeaders(context),
|
|
4730
|
+
body: JSON.stringify(request)
|
|
4731
|
+
},
|
|
4732
|
+
"failed to complete sandbox image build"
|
|
4733
|
+
);
|
|
4734
|
+
}
|
|
4735
|
+
async function resolvedDockerConfigJson() {
|
|
4736
|
+
const configDir = process.env.DOCKER_CONFIG ?? import_node_path.default.join((0, import_node_os.homedir)(), ".docker");
|
|
4737
|
+
const configPath = import_node_path.default.join(configDir, "config.json");
|
|
4738
|
+
try {
|
|
4739
|
+
const content = await (0, import_promises.readFile)(configPath, "utf8");
|
|
4740
|
+
const parsed = JSON.parse(content);
|
|
4741
|
+
const auths = parsed.auths;
|
|
4742
|
+
if (auths != null && Object.keys(auths).length > 0) {
|
|
4743
|
+
return JSON.stringify({ auths });
|
|
4744
|
+
}
|
|
4745
|
+
} catch (error) {
|
|
4746
|
+
if (error.code === "ENOENT") {
|
|
4747
|
+
return void 0;
|
|
4748
|
+
}
|
|
4749
|
+
throw error;
|
|
4750
|
+
}
|
|
4751
|
+
return void 0;
|
|
4752
|
+
}
|
|
4753
|
+
function rootfsDiskBytes(diskMb, prepared) {
|
|
4754
|
+
if (diskMb != null) {
|
|
4755
|
+
return diskMb * 1024 * 1024;
|
|
4756
|
+
}
|
|
4757
|
+
if (prepared.parent != null) {
|
|
4758
|
+
if (prepared.parent.rootfsDiskBytes == null) {
|
|
4759
|
+
throw new Error(
|
|
4760
|
+
"platform API did not return parent rootfsDiskBytes for diff build; pass diskMb explicitly or update Platform API"
|
|
4761
|
+
);
|
|
4762
|
+
}
|
|
4763
|
+
return prepared.parent.rootfsDiskBytes;
|
|
4764
|
+
}
|
|
4765
|
+
return DEFAULT_ROOTFS_DISK_MB * 1024 * 1024;
|
|
4766
|
+
}
|
|
4767
|
+
function rootfsDiskBytesToMb(bytes) {
|
|
4768
|
+
return Math.ceil(bytes / (1024 * 1024));
|
|
4769
|
+
}
|
|
4770
|
+
async function buildRootfsSpec(preparedSpec, prepared, plan, diskMb) {
|
|
4771
|
+
const spec = {
|
|
4772
|
+
...preparedSpec,
|
|
4773
|
+
dockerfile: plan.dockerfileText,
|
|
4774
|
+
contextDir: REMOTE_CONTEXT_DIR,
|
|
4775
|
+
baseImage: plan.baseImage,
|
|
4776
|
+
rootfsDiskBytes: rootfsDiskBytes(diskMb, prepared)
|
|
4777
|
+
};
|
|
4778
|
+
const dockerConfigJson = await resolvedDockerConfigJson();
|
|
4779
|
+
if (dockerConfigJson != null) {
|
|
4780
|
+
spec.dockerConfigJson = dockerConfigJson;
|
|
4781
|
+
}
|
|
4782
|
+
return spec;
|
|
4783
|
+
}
|
|
4784
|
+
function rootfsBuilderExecutable(executable) {
|
|
4785
|
+
return executable === ROOTFS_BUILDER_COMMAND ? `${ROOTFS_BUILDER_BIN_DIR}/${ROOTFS_BUILDER_COMMAND}` : executable;
|
|
4786
|
+
}
|
|
4787
|
+
function rootfsBuilderEnv() {
|
|
4788
|
+
return { PATH: ROOTFS_BUILDER_PATH };
|
|
4789
|
+
}
|
|
4790
|
+
async function runRootfsBuilder(sandbox, command, emit, sleep3) {
|
|
4791
|
+
const parts = shellSplit(command);
|
|
4792
|
+
const [executable, ...commandArgs] = parts;
|
|
4793
|
+
if (!executable) {
|
|
4794
|
+
throw new Error("empty rootfs builder command returned by platform API");
|
|
4795
|
+
}
|
|
4796
|
+
await runStreaming(
|
|
4797
|
+
sandbox,
|
|
4798
|
+
emit,
|
|
4799
|
+
sleep3,
|
|
4800
|
+
rootfsBuilderExecutable(executable),
|
|
4801
|
+
[...commandArgs, "--spec", REMOTE_SPEC_PATH, "--metadata-out", REMOTE_METADATA_PATH],
|
|
4802
|
+
rootfsBuilderEnv(),
|
|
4803
|
+
REMOTE_BUILD_DIR
|
|
4804
|
+
);
|
|
4805
|
+
}
|
|
4806
|
+
function metadataString(metadata, snakeKey, camelKey) {
|
|
4807
|
+
const value = metadata[snakeKey] ?? metadata[camelKey];
|
|
4808
|
+
return typeof value === "string" ? value : void 0;
|
|
4809
|
+
}
|
|
4810
|
+
function metadataNumber(metadata, snakeKey, camelKey) {
|
|
4811
|
+
const value = metadata[snakeKey] ?? metadata[camelKey];
|
|
4812
|
+
if (typeof value === "number") {
|
|
4813
|
+
return value;
|
|
4814
|
+
}
|
|
4815
|
+
if (typeof value === "string" && value.trim()) {
|
|
4816
|
+
const parsed = Number(value);
|
|
4817
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
4818
|
+
}
|
|
4819
|
+
return void 0;
|
|
4820
|
+
}
|
|
4821
|
+
function completeRequestFromMetadata(prepared, metadata) {
|
|
4822
|
+
const rootfsNodeKind = metadataString(metadata, "rootfs_node_kind", "rootfsNodeKind") ?? prepared.rootfsNodeKind;
|
|
4823
|
+
const parentManifestUri = metadataString(metadata, "parent_manifest_uri", "parentManifestUri") ?? (rootfsNodeKind === "diff" ? prepared.parent?.parentManifestUri : void 0);
|
|
4824
|
+
if (rootfsNodeKind === "diff" && parentManifestUri == null) {
|
|
4825
|
+
throw new Error("rootfs diff build completed without parent_manifest_uri");
|
|
4826
|
+
}
|
|
4827
|
+
const snapshotFormatVersion = metadataString(
|
|
4828
|
+
metadata,
|
|
4829
|
+
"snapshot_format_version",
|
|
4830
|
+
"snapshotFormatVersion"
|
|
4831
|
+
);
|
|
4832
|
+
const snapshotSizeBytes = metadataNumber(
|
|
4833
|
+
metadata,
|
|
4834
|
+
"snapshot_size_bytes",
|
|
4835
|
+
"snapshotSizeBytes"
|
|
4836
|
+
);
|
|
4837
|
+
const rootfsDiskBytesValue = metadataNumber(
|
|
4838
|
+
metadata,
|
|
4839
|
+
"rootfs_disk_bytes",
|
|
4840
|
+
"rootfsDiskBytes"
|
|
4841
|
+
);
|
|
4842
|
+
if (!snapshotFormatVersion) {
|
|
4843
|
+
throw new Error("rootfs builder metadata is missing snapshot_format_version");
|
|
4844
|
+
}
|
|
4845
|
+
if (snapshotSizeBytes == null) {
|
|
4846
|
+
throw new Error("rootfs builder metadata is missing numeric snapshot_size_bytes");
|
|
4847
|
+
}
|
|
4848
|
+
if (rootfsDiskBytesValue == null) {
|
|
4849
|
+
throw new Error("rootfs builder metadata is missing numeric rootfs_disk_bytes");
|
|
4850
|
+
}
|
|
4851
|
+
return {
|
|
4852
|
+
snapshotId: metadataString(metadata, "snapshot_id", "snapshotId") ?? prepared.snapshotId,
|
|
4853
|
+
snapshotUri: metadataString(metadata, "snapshot_uri", "snapshotUri") ?? prepared.snapshotUri,
|
|
4854
|
+
snapshotFormatVersion,
|
|
4855
|
+
snapshotSizeBytes,
|
|
4856
|
+
rootfsDiskBytes: rootfsDiskBytesValue,
|
|
4857
|
+
rootfsNodeKind,
|
|
4858
|
+
...parentManifestUri ? { parentManifestUri } : {}
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
function requiredString(object, key) {
|
|
4862
|
+
const value = object[key];
|
|
4863
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
4864
|
+
throw new Error(`expected '${key}' to be a non-empty string`);
|
|
4865
|
+
}
|
|
4866
|
+
return value;
|
|
4867
|
+
}
|
|
4868
|
+
function requiredNumber(object, key) {
|
|
4869
|
+
const value = object[key];
|
|
4870
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4871
|
+
throw new Error(`expected '${key}' to be a finite number`);
|
|
4872
|
+
}
|
|
4873
|
+
return value;
|
|
4874
|
+
}
|
|
4875
|
+
function optionalNumber(object, key) {
|
|
4876
|
+
const value = object[key];
|
|
4877
|
+
if (value == null) {
|
|
4878
|
+
return void 0;
|
|
4879
|
+
}
|
|
4880
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4881
|
+
throw new Error(`expected '${key}' to be a finite number`);
|
|
4882
|
+
}
|
|
4883
|
+
return value;
|
|
4884
|
+
}
|
|
4671
4885
|
async function runChecked(sandbox, command, args, env, workingDir) {
|
|
4672
4886
|
const result = await sandbox.run(command, {
|
|
4673
4887
|
args,
|
|
@@ -4727,18 +4941,6 @@ function emitOutputLines(emit, stream, response, seen) {
|
|
|
4727
4941
|
emit({ type: "build_log", stream, message: line });
|
|
4728
4942
|
}
|
|
4729
4943
|
}
|
|
4730
|
-
function isPathWithinContext(contextDir, localPath) {
|
|
4731
|
-
const relative = import_node_path.default.relative(contextDir, localPath);
|
|
4732
|
-
return relative === "" || !relative.startsWith("..") && !import_node_path.default.isAbsolute(relative);
|
|
4733
|
-
}
|
|
4734
|
-
function resolveContextSourcePath(contextDir, source) {
|
|
4735
|
-
const resolvedContextDir = import_node_path.default.resolve(contextDir);
|
|
4736
|
-
const resolvedSource = import_node_path.default.resolve(resolvedContextDir, source);
|
|
4737
|
-
if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
|
|
4738
|
-
throw new Error(`Local path escapes the build context: ${source}`);
|
|
4739
|
-
}
|
|
4740
|
-
return resolvedSource;
|
|
4741
|
-
}
|
|
4742
4944
|
async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
4743
4945
|
const fileStats = await (0, import_promises.stat)(localPath).catch(() => null);
|
|
4744
4946
|
if (!fileStats) {
|
|
@@ -4769,184 +4971,26 @@ async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
|
4769
4971
|
}
|
|
4770
4972
|
}
|
|
4771
4973
|
}
|
|
4772
|
-
async function
|
|
4773
|
-
const exportLine = `export ${key}=${shellQuote(value)}`;
|
|
4774
|
-
await runChecked(
|
|
4775
|
-
sandbox,
|
|
4776
|
-
"sh",
|
|
4777
|
-
["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
|
|
4778
|
-
processEnv
|
|
4779
|
-
);
|
|
4780
|
-
}
|
|
4781
|
-
async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
|
|
4782
|
-
const destinationPath = resolveContainerPath(destination, workingDir);
|
|
4783
|
-
if (sources.length > 1 && !destinationPath.endsWith("/")) {
|
|
4784
|
-
throw new Error(
|
|
4785
|
-
`${keyword} with multiple sources requires a directory destination ending in '/'`
|
|
4786
|
-
);
|
|
4787
|
-
}
|
|
4788
|
-
for (const source of sources) {
|
|
4789
|
-
const localSource = resolveContextSourcePath(contextDir, source);
|
|
4790
|
-
const localStats = await (0, import_promises.stat)(localSource).catch(() => null);
|
|
4791
|
-
if (!localStats) {
|
|
4792
|
-
throw new Error(`Local path not found: ${localSource}`);
|
|
4793
|
-
}
|
|
4794
|
-
let remoteDestination = destinationPath;
|
|
4795
|
-
if (sources.length > 1) {
|
|
4796
|
-
remoteDestination = import_node_path.default.posix.join(
|
|
4797
|
-
destinationPath.replace(/\/$/, ""),
|
|
4798
|
-
import_node_path.default.posix.basename(source.replace(/\/$/, ""))
|
|
4799
|
-
);
|
|
4800
|
-
} else if (localStats.isFile() && destinationPath.endsWith("/")) {
|
|
4801
|
-
remoteDestination = import_node_path.default.posix.join(
|
|
4802
|
-
destinationPath.replace(/\/$/, ""),
|
|
4803
|
-
import_node_path.default.basename(source)
|
|
4804
|
-
);
|
|
4805
|
-
}
|
|
4806
|
-
emit({
|
|
4807
|
-
type: "status",
|
|
4808
|
-
message: `${keyword} ${source} -> ${remoteDestination}`
|
|
4809
|
-
});
|
|
4810
|
-
await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
|
|
4811
|
-
}
|
|
4812
|
-
}
|
|
4813
|
-
async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep3) {
|
|
4814
|
-
let destinationPath = resolveContainerPath(destination, workingDir);
|
|
4815
|
-
const parsedUrl = new URL(url);
|
|
4816
|
-
const fileName = import_node_path.default.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
|
|
4817
|
-
if (destinationPath.endsWith("/")) {
|
|
4818
|
-
destinationPath = import_node_path.default.posix.join(destinationPath.replace(/\/$/, ""), fileName);
|
|
4819
|
-
}
|
|
4820
|
-
const parentDir = import_node_path.default.posix.dirname(destinationPath) || "/";
|
|
4821
|
-
emit({
|
|
4822
|
-
type: "status",
|
|
4823
|
-
message: `ADD ${url} -> ${destinationPath}`
|
|
4824
|
-
});
|
|
4825
|
-
await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
|
|
4826
|
-
await runStreaming(
|
|
4827
|
-
sandbox,
|
|
4828
|
-
emit,
|
|
4829
|
-
sleep3,
|
|
4830
|
-
"sh",
|
|
4831
|
-
[
|
|
4832
|
-
"-c",
|
|
4833
|
-
`curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
|
|
4834
|
-
],
|
|
4835
|
-
processEnv,
|
|
4836
|
-
workingDir
|
|
4837
|
-
);
|
|
4838
|
-
}
|
|
4839
|
-
async function executeDockerfilePlan(sandbox, plan, emit, sleep3) {
|
|
4840
|
-
const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
|
|
4841
|
-
let workingDir = "/";
|
|
4842
|
-
for (const instruction of plan.instructions) {
|
|
4843
|
-
const { keyword, value, lineNumber } = instruction;
|
|
4844
|
-
if (keyword === "RUN") {
|
|
4845
|
-
emit({ type: "status", message: `RUN ${value}` });
|
|
4846
|
-
await runStreaming(
|
|
4847
|
-
sandbox,
|
|
4848
|
-
emit,
|
|
4849
|
-
sleep3,
|
|
4850
|
-
"sh",
|
|
4851
|
-
["-c", value],
|
|
4852
|
-
processEnv,
|
|
4853
|
-
workingDir
|
|
4854
|
-
);
|
|
4855
|
-
continue;
|
|
4856
|
-
}
|
|
4857
|
-
if (keyword === "WORKDIR") {
|
|
4858
|
-
const tokens = shellSplit(value);
|
|
4859
|
-
if (tokens.length !== 1) {
|
|
4860
|
-
throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
|
|
4861
|
-
}
|
|
4862
|
-
workingDir = resolveContainerPath(tokens[0], workingDir);
|
|
4863
|
-
emit({ type: "status", message: `WORKDIR ${workingDir}` });
|
|
4864
|
-
await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
|
|
4865
|
-
continue;
|
|
4866
|
-
}
|
|
4867
|
-
if (keyword === "ENV") {
|
|
4868
|
-
for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
|
|
4869
|
-
emit({ type: "status", message: `ENV ${key}=${envValue}` });
|
|
4870
|
-
processEnv[key] = envValue;
|
|
4871
|
-
await persistEnvVar(sandbox, processEnv, key, envValue);
|
|
4872
|
-
}
|
|
4873
|
-
continue;
|
|
4874
|
-
}
|
|
4875
|
-
if (keyword === "COPY") {
|
|
4876
|
-
const { sources, destination } = parseCopyLikeValues(
|
|
4877
|
-
value,
|
|
4878
|
-
lineNumber,
|
|
4879
|
-
keyword
|
|
4880
|
-
);
|
|
4881
|
-
await copyFromContext(
|
|
4882
|
-
sandbox,
|
|
4883
|
-
emit,
|
|
4884
|
-
plan.contextDir,
|
|
4885
|
-
sources,
|
|
4886
|
-
destination,
|
|
4887
|
-
workingDir,
|
|
4888
|
-
keyword
|
|
4889
|
-
);
|
|
4890
|
-
continue;
|
|
4891
|
-
}
|
|
4892
|
-
if (keyword === "ADD") {
|
|
4893
|
-
const { sources, destination } = parseCopyLikeValues(
|
|
4894
|
-
value,
|
|
4895
|
-
lineNumber,
|
|
4896
|
-
keyword
|
|
4897
|
-
);
|
|
4898
|
-
if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
|
|
4899
|
-
await addUrlToSandbox(
|
|
4900
|
-
sandbox,
|
|
4901
|
-
emit,
|
|
4902
|
-
sources[0],
|
|
4903
|
-
destination,
|
|
4904
|
-
workingDir,
|
|
4905
|
-
processEnv,
|
|
4906
|
-
sleep3
|
|
4907
|
-
);
|
|
4908
|
-
} else {
|
|
4909
|
-
await copyFromContext(
|
|
4910
|
-
sandbox,
|
|
4911
|
-
emit,
|
|
4912
|
-
plan.contextDir,
|
|
4913
|
-
sources,
|
|
4914
|
-
destination,
|
|
4915
|
-
workingDir,
|
|
4916
|
-
keyword
|
|
4917
|
-
);
|
|
4918
|
-
}
|
|
4919
|
-
continue;
|
|
4920
|
-
}
|
|
4921
|
-
if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
4922
|
-
emit({
|
|
4923
|
-
type: "warning",
|
|
4924
|
-
message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
|
|
4925
|
-
});
|
|
4926
|
-
continue;
|
|
4927
|
-
}
|
|
4928
|
-
throw new Error(
|
|
4929
|
-
`line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
4930
|
-
);
|
|
4931
|
-
}
|
|
4932
|
-
}
|
|
4933
|
-
async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes, isPublic) {
|
|
4934
|
-
if (!context.organizationId || !context.projectId) {
|
|
4935
|
-
throw new Error(
|
|
4936
|
-
"Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
|
|
4937
|
-
);
|
|
4938
|
-
}
|
|
4974
|
+
async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes2, isPublic, snapshotFormatVersion) {
|
|
4939
4975
|
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
4940
4976
|
if (!bearerToken) {
|
|
4941
4977
|
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
4942
4978
|
}
|
|
4943
4979
|
const baseUrl = context.apiUrl.replace(/\/+$/, "");
|
|
4944
|
-
const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
4945
4980
|
const headers = {
|
|
4946
4981
|
Authorization: `Bearer ${bearerToken}`,
|
|
4947
4982
|
"Content-Type": "application/json"
|
|
4948
4983
|
};
|
|
4949
|
-
|
|
4984
|
+
let url;
|
|
4985
|
+
if (context.apiKey) {
|
|
4986
|
+
url = `${baseUrl}/platform/v1/sandbox-templates`;
|
|
4987
|
+
} else {
|
|
4988
|
+
if (!context.organizationId || !context.projectId) {
|
|
4989
|
+
throw new Error(
|
|
4990
|
+
"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."
|
|
4991
|
+
);
|
|
4992
|
+
}
|
|
4993
|
+
url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
4950
4994
|
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
4951
4995
|
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
4952
4996
|
}
|
|
@@ -4959,8 +5003,9 @@ async function registerImage(context, name, dockerfile, snapshotId, snapshotSand
|
|
|
4959
5003
|
snapshotId,
|
|
4960
5004
|
snapshotSandboxId,
|
|
4961
5005
|
snapshotUri,
|
|
5006
|
+
...snapshotFormatVersion ? { snapshotFormatVersion } : {},
|
|
4962
5007
|
snapshotSizeBytes,
|
|
4963
|
-
rootfsDiskBytes,
|
|
5008
|
+
rootfsDiskBytes: rootfsDiskBytes2,
|
|
4964
5009
|
public: isPublic
|
|
4965
5010
|
})
|
|
4966
5011
|
});
|
|
@@ -4977,65 +5022,71 @@ async function createSandboxImage(source, options = {}, deps = {}) {
|
|
|
4977
5022
|
const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4978
5023
|
const context = buildContextFromEnv();
|
|
4979
5024
|
const clientFactory = deps.createClient ?? createDefaultClient;
|
|
4980
|
-
const register = deps.registerImage ?? ((...args) => registerImage(...args));
|
|
4981
5025
|
const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
|
|
4982
5026
|
emit({ type: "status", message: `Loading ${sourceLabel}...` });
|
|
4983
5027
|
const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
|
|
4984
5028
|
emit({
|
|
4985
5029
|
type: "status",
|
|
4986
|
-
message:
|
|
5030
|
+
message: `Selected image name: ${plan.registeredName}`
|
|
5031
|
+
});
|
|
5032
|
+
emit({ type: "status", message: "Preparing rootfs build..." });
|
|
5033
|
+
const resolvedContext = await resolveBuildContext(context);
|
|
5034
|
+
const { prepared, spec: preparedSpec } = await prepareRootfsBuild(
|
|
5035
|
+
resolvedContext,
|
|
5036
|
+
plan,
|
|
5037
|
+
options.isPublic ?? false
|
|
5038
|
+
);
|
|
5039
|
+
emit({
|
|
5040
|
+
type: "status",
|
|
5041
|
+
message: prepared.rootfsNodeKind === "diff" ? "Build mode: RootfsDiff" : "Build mode: RootfsBase"
|
|
4987
5042
|
});
|
|
4988
5043
|
const client = clientFactory(context);
|
|
4989
5044
|
let sandbox;
|
|
4990
5045
|
try {
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
});
|
|
5046
|
+
const outputRootfsDiskBytes = rootfsDiskBytes(options.diskMb, prepared);
|
|
5047
|
+
const builderDiskMb = Math.max(
|
|
5048
|
+
rootfsDiskBytesToMb(outputRootfsDiskBytes),
|
|
5049
|
+
options.builderDiskMb ?? prepared.builder.diskMb
|
|
5050
|
+
);
|
|
4997
5051
|
emit({
|
|
4998
5052
|
type: "status",
|
|
4999
|
-
message: `
|
|
5053
|
+
message: `Creating rootfs builder sandbox from ${prepared.builder.image}...`
|
|
5000
5054
|
});
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
emit({
|
|
5007
|
-
type: "snapshot_created",
|
|
5008
|
-
snapshot_id: snapshot.snapshotId
|
|
5055
|
+
sandbox = await client.createAndConnect({
|
|
5056
|
+
image: prepared.builder.image,
|
|
5057
|
+
cpus: options.cpus ?? prepared.builder.cpus,
|
|
5058
|
+
memoryMb: options.memoryMb ?? prepared.builder.memoryMb,
|
|
5059
|
+
diskMb: builderDiskMb
|
|
5009
5060
|
});
|
|
5010
|
-
if (!snapshot.snapshotUri) {
|
|
5011
|
-
throw new Error(
|
|
5012
|
-
`Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
|
|
5013
|
-
);
|
|
5014
|
-
}
|
|
5015
|
-
if (snapshot.sizeBytes == null) {
|
|
5016
|
-
throw new Error(
|
|
5017
|
-
`Snapshot ${snapshot.snapshotId} is missing sizeBytes and cannot be registered as a sandbox image.`
|
|
5018
|
-
);
|
|
5019
|
-
}
|
|
5020
|
-
if (snapshot.rootfsDiskBytes == null) {
|
|
5021
|
-
throw new Error(
|
|
5022
|
-
`Snapshot ${snapshot.snapshotId} is missing rootfsDiskBytes and cannot be registered as a sandbox image.`
|
|
5023
|
-
);
|
|
5024
|
-
}
|
|
5025
5061
|
emit({
|
|
5026
5062
|
type: "status",
|
|
5027
|
-
message: `
|
|
5063
|
+
message: `Rootfs builder sandbox ${sandbox.sandboxId} is running`
|
|
5028
5064
|
});
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5065
|
+
emit({ type: "status", message: "Uploading build context..." });
|
|
5066
|
+
await copyLocalPathToSandbox(sandbox, plan.contextDir, REMOTE_CONTEXT_DIR);
|
|
5067
|
+
const spec = await buildRootfsSpec(
|
|
5068
|
+
preparedSpec,
|
|
5069
|
+
prepared,
|
|
5070
|
+
plan,
|
|
5071
|
+
options.diskMb
|
|
5072
|
+
);
|
|
5073
|
+
await runChecked(sandbox, "mkdir", ["-p", import_node_path.default.posix.dirname(REMOTE_SPEC_PATH)]);
|
|
5074
|
+
await sandbox.writeFile(
|
|
5075
|
+
REMOTE_SPEC_PATH,
|
|
5076
|
+
new TextEncoder().encode(JSON.stringify(spec, null, 2))
|
|
5077
|
+
);
|
|
5078
|
+
emit({ type: "status", message: "Running offline rootfs builder..." });
|
|
5079
|
+
await runRootfsBuilder(sandbox, prepared.builder.command, emit, sleep3);
|
|
5080
|
+
const metadataBytes = await sandbox.readFile(REMOTE_METADATA_PATH);
|
|
5081
|
+
const metadata = JSON.parse(
|
|
5082
|
+
new TextDecoder().decode(metadataBytes)
|
|
5083
|
+
);
|
|
5084
|
+
const completeRequest = completeRequestFromMetadata(prepared, metadata);
|
|
5085
|
+
emit({ type: "status", message: "Completing image registration..." });
|
|
5086
|
+
const result = await completeRootfsBuild(
|
|
5087
|
+
resolvedContext,
|
|
5088
|
+
prepared.buildId,
|
|
5089
|
+
completeRequest
|
|
5039
5090
|
);
|
|
5040
5091
|
emit({
|
|
5041
5092
|
type: "image_registered",
|
|
@@ -5063,16 +5114,18 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5063
5114
|
cpus: { type: "string" },
|
|
5064
5115
|
memory: { type: "string" },
|
|
5065
5116
|
disk_mb: { type: "string" },
|
|
5117
|
+
builder_disk_mb: { type: "string" },
|
|
5066
5118
|
public: { type: "boolean", default: false }
|
|
5067
5119
|
}
|
|
5068
5120
|
});
|
|
5069
5121
|
const dockerfilePath = parsed.positionals[0];
|
|
5070
5122
|
if (!dockerfilePath) {
|
|
5071
|
-
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--public]");
|
|
5123
|
+
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--builder_disk_mb MB] [--public]");
|
|
5072
5124
|
}
|
|
5073
5125
|
const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
|
|
5074
5126
|
const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
|
|
5075
5127
|
const diskMb = parsed.values.disk_mb != null ? Number(parsed.values.disk_mb) : void 0;
|
|
5128
|
+
const builderDiskMb = parsed.values.builder_disk_mb != null ? Number(parsed.values.builder_disk_mb) : void 0;
|
|
5076
5129
|
if (cpus != null && !Number.isFinite(cpus)) {
|
|
5077
5130
|
throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
|
|
5078
5131
|
}
|
|
@@ -5082,6 +5135,11 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5082
5135
|
if (diskMb != null && !Number.isInteger(diskMb)) {
|
|
5083
5136
|
throw new Error(`Invalid --disk_mb value: ${parsed.values.disk_mb}`);
|
|
5084
5137
|
}
|
|
5138
|
+
if (builderDiskMb != null && !Number.isInteger(builderDiskMb)) {
|
|
5139
|
+
throw new Error(
|
|
5140
|
+
`Invalid --builder_disk_mb value: ${parsed.values.builder_disk_mb}`
|
|
5141
|
+
);
|
|
5142
|
+
}
|
|
5085
5143
|
await createSandboxImage(
|
|
5086
5144
|
dockerfilePath,
|
|
5087
5145
|
{
|
|
@@ -5089,30 +5147,30 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5089
5147
|
cpus,
|
|
5090
5148
|
memoryMb,
|
|
5091
5149
|
diskMb,
|
|
5150
|
+
builderDiskMb,
|
|
5092
5151
|
isPublic: parsed.values.public
|
|
5093
5152
|
},
|
|
5094
5153
|
{ emit: ndjsonStdoutEmit }
|
|
5095
5154
|
);
|
|
5096
5155
|
}
|
|
5097
|
-
var import_promises, import_node_path, import_node_util,
|
|
5156
|
+
var import_promises, import_node_os, import_node_path, import_node_util, 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;
|
|
5098
5157
|
var init_sandbox_image = __esm({
|
|
5099
5158
|
"src/sandbox-image.ts"() {
|
|
5100
5159
|
import_promises = require("fs/promises");
|
|
5160
|
+
import_node_os = require("os");
|
|
5101
5161
|
import_node_path = __toESM(require("path"), 1);
|
|
5102
5162
|
import_node_util = require("util");
|
|
5103
5163
|
init_models();
|
|
5104
5164
|
init_client();
|
|
5105
5165
|
init_image();
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
"VOLUME"
|
|
5115
|
-
]);
|
|
5166
|
+
DEFAULT_ROOTFS_DISK_MB = 10 * 1024;
|
|
5167
|
+
REMOTE_BUILD_DIR = "/var/lib/tensorlake/rootfs-builder/build";
|
|
5168
|
+
REMOTE_CONTEXT_DIR = "/var/lib/tensorlake/rootfs-builder/build/context";
|
|
5169
|
+
REMOTE_SPEC_PATH = "/var/lib/tensorlake/rootfs-builder/build/spec.json";
|
|
5170
|
+
REMOTE_METADATA_PATH = "/var/lib/tensorlake/rootfs-builder/build/metadata.json";
|
|
5171
|
+
ROOTFS_BUILDER_BIN_DIR = "/usr/local/bin";
|
|
5172
|
+
ROOTFS_BUILDER_PATH = "/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
5173
|
+
ROOTFS_BUILDER_COMMAND = "tl-rootfs-build";
|
|
5116
5174
|
UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
5117
5175
|
"ARG",
|
|
5118
5176
|
"ONBUILD",
|
|
@@ -5129,6 +5187,7 @@ init_sandbox_image();
|
|
|
5129
5187
|
loadDockerfilePlan,
|
|
5130
5188
|
loadImagePlan,
|
|
5131
5189
|
logicalDockerfileLines,
|
|
5190
|
+
registerImage,
|
|
5132
5191
|
runCreateSandboxImageCli
|
|
5133
5192
|
});
|
|
5134
5193
|
//# sourceMappingURL=sandbox-image.cjs.map
|