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/index.js
CHANGED
|
@@ -13,7 +13,7 @@ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TI
|
|
|
13
13
|
var init_defaults = __esm({
|
|
14
14
|
"src/defaults.ts"() {
|
|
15
15
|
"use strict";
|
|
16
|
-
SDK_VERSION = "0.5.
|
|
16
|
+
SDK_VERSION = "0.5.11";
|
|
17
17
|
API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
|
|
18
18
|
API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
|
|
19
19
|
NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
|
|
@@ -408,11 +408,12 @@ var init_models = __esm({
|
|
|
408
408
|
SandboxStatus3["TERMINATED"] = "terminated";
|
|
409
409
|
return SandboxStatus3;
|
|
410
410
|
})(SandboxStatus || {});
|
|
411
|
-
SnapshotStatus = /* @__PURE__ */ ((
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
411
|
+
SnapshotStatus = /* @__PURE__ */ ((SnapshotStatus2) => {
|
|
412
|
+
SnapshotStatus2["IN_PROGRESS"] = "in_progress";
|
|
413
|
+
SnapshotStatus2["LOCAL_READY"] = "local_ready";
|
|
414
|
+
SnapshotStatus2["COMPLETED"] = "completed";
|
|
415
|
+
SnapshotStatus2["FAILED"] = "failed";
|
|
416
|
+
return SnapshotStatus2;
|
|
416
417
|
})(SnapshotStatus || {});
|
|
417
418
|
ProcessStatus = /* @__PURE__ */ ((ProcessStatus2) => {
|
|
418
419
|
ProcessStatus2["RUNNING"] = "running";
|
|
@@ -3377,11 +3378,10 @@ var init_sandbox = __esm({
|
|
|
3377
3378
|
await client.resume(this.lifecycleIdentifier, options);
|
|
3378
3379
|
}
|
|
3379
3380
|
/**
|
|
3380
|
-
* Create a
|
|
3381
|
-
* be committed.
|
|
3381
|
+
* Create a checkpoint of this sandbox and wait for it to be locally ready.
|
|
3382
3382
|
*
|
|
3383
|
-
* By default blocks until the
|
|
3384
|
-
*
|
|
3383
|
+
* By default blocks until the checkpoint is resumable and returns
|
|
3384
|
+
* `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
|
|
3385
3385
|
* (returns `undefined`).
|
|
3386
3386
|
*/
|
|
3387
3387
|
async checkpoint(options) {
|
|
@@ -3393,7 +3393,8 @@ var init_sandbox = __esm({
|
|
|
3393
3393
|
return client.snapshotAndWait(this.lifecycleIdentifier, {
|
|
3394
3394
|
timeout: options?.timeout,
|
|
3395
3395
|
pollInterval: options?.pollInterval,
|
|
3396
|
-
snapshotType: options?.checkpointType
|
|
3396
|
+
snapshotType: options?.checkpointType,
|
|
3397
|
+
waitUntil: options?.waitUntil
|
|
3397
3398
|
});
|
|
3398
3399
|
}
|
|
3399
3400
|
/**
|
|
@@ -3788,6 +3789,12 @@ __export(client_exports, {
|
|
|
3788
3789
|
function sleep2(ms) {
|
|
3789
3790
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3790
3791
|
}
|
|
3792
|
+
function snapshotStatusSatisfiesWaitCondition(status, waitUntil) {
|
|
3793
|
+
if (waitUntil === "local_ready") {
|
|
3794
|
+
return status === "local_ready" /* LOCAL_READY */ || status === "completed" /* COMPLETED */;
|
|
3795
|
+
}
|
|
3796
|
+
return status === "completed" /* COMPLETED */;
|
|
3797
|
+
}
|
|
3791
3798
|
function formatStartupFailureMessage(sandboxId, status, options) {
|
|
3792
3799
|
const prefix = status === "terminated" /* TERMINATED */ ? `Sandbox ${sandboxId} terminated during startup` : `Sandbox ${sandboxId} became ${status} during startup`;
|
|
3793
3800
|
const detail = formatErrorDetails(options.errorDetails);
|
|
@@ -4088,7 +4095,8 @@ var init_client = __esm({
|
|
|
4088
4095
|
*
|
|
4089
4096
|
* This call **returns immediately** with a `snapshotId` and `in_progress`
|
|
4090
4097
|
* status — the snapshot is created asynchronously. Poll `getSnapshot()` until
|
|
4091
|
-
* `completed
|
|
4098
|
+
* `local_ready`, `completed`, or `failed`, or use `snapshotAndWait()` to
|
|
4099
|
+
* block automatically.
|
|
4092
4100
|
*
|
|
4093
4101
|
* @param options.snapshotType - `"filesystem"` for cold-boot snapshots (e.g. image builds).
|
|
4094
4102
|
* Omit to use the server default (`filesystem`).
|
|
@@ -4129,9 +4137,11 @@ var init_client = __esm({
|
|
|
4129
4137
|
);
|
|
4130
4138
|
}
|
|
4131
4139
|
/**
|
|
4132
|
-
* Create a snapshot and block until it is
|
|
4140
|
+
* Create a snapshot and block until it is locally ready.
|
|
4133
4141
|
*
|
|
4134
|
-
* Combines `snapshot()` with polling `getSnapshot()` until `
|
|
4142
|
+
* Combines `snapshot()` with polling `getSnapshot()` until `local_ready`
|
|
4143
|
+
* or `completed`. Pass `{ waitUntil: "completed" }` when durable
|
|
4144
|
+
* `snapshotUri` metadata is required.
|
|
4135
4145
|
* Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
|
|
4136
4146
|
* without managing the client separately.
|
|
4137
4147
|
*
|
|
@@ -4144,13 +4154,14 @@ var init_client = __esm({
|
|
|
4144
4154
|
async snapshotAndWait(sandboxId, options) {
|
|
4145
4155
|
const timeout = options?.timeout ?? 300;
|
|
4146
4156
|
const pollInterval = options?.pollInterval ?? 1;
|
|
4157
|
+
const waitUntil = options?.waitUntil ?? "local_ready";
|
|
4147
4158
|
const result = await this.snapshot(sandboxId, {
|
|
4148
4159
|
snapshotType: options?.snapshotType
|
|
4149
4160
|
});
|
|
4150
4161
|
const deadline = Date.now() + timeout * 1e3;
|
|
4151
4162
|
while (Date.now() < deadline) {
|
|
4152
4163
|
const info = await this.getSnapshot(result.snapshotId);
|
|
4153
|
-
if (info.status
|
|
4164
|
+
if (snapshotStatusSatisfiesWaitCondition(info.status, waitUntil)) return info;
|
|
4154
4165
|
if (info.status === "failed" /* FAILED */) {
|
|
4155
4166
|
throw new SandboxError(
|
|
4156
4167
|
`Snapshot ${result.snapshotId} failed: ${info.error}`
|
|
@@ -4159,7 +4170,7 @@ var init_client = __esm({
|
|
|
4159
4170
|
await sleep2(pollInterval * 1e3);
|
|
4160
4171
|
}
|
|
4161
4172
|
throw new SandboxError(
|
|
4162
|
-
`Snapshot ${result.snapshotId} did not
|
|
4173
|
+
`Snapshot ${result.snapshotId} did not reach ${waitUntil} within ${timeout}s`
|
|
4163
4174
|
);
|
|
4164
4175
|
}
|
|
4165
4176
|
// --- Pools ---
|
|
@@ -4444,9 +4455,11 @@ __export(sandbox_image_exports, {
|
|
|
4444
4455
|
loadDockerfilePlan: () => loadDockerfilePlan,
|
|
4445
4456
|
loadImagePlan: () => loadImagePlan,
|
|
4446
4457
|
logicalDockerfileLines: () => logicalDockerfileLines,
|
|
4458
|
+
registerImage: () => registerImage,
|
|
4447
4459
|
runCreateSandboxImageCli: () => runCreateSandboxImageCli
|
|
4448
4460
|
});
|
|
4449
4461
|
import { readFile, readdir, stat } from "fs/promises";
|
|
4462
|
+
import { homedir } from "os";
|
|
4450
4463
|
import path from "path";
|
|
4451
4464
|
import { parseArgs } from "util";
|
|
4452
4465
|
function defaultRegisteredName(dockerfilePath) {
|
|
@@ -4577,12 +4590,6 @@ function shellSplit(input) {
|
|
|
4577
4590
|
}
|
|
4578
4591
|
return tokens;
|
|
4579
4592
|
}
|
|
4580
|
-
function shellQuote(value) {
|
|
4581
|
-
if (!value) {
|
|
4582
|
-
return "''";
|
|
4583
|
-
}
|
|
4584
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4585
|
-
}
|
|
4586
4593
|
function stripLeadingFlags(value) {
|
|
4587
4594
|
const flags = {};
|
|
4588
4595
|
let remaining = value.trimStart();
|
|
@@ -4620,75 +4627,6 @@ function parseFromValue(value, lineNumber) {
|
|
|
4620
4627
|
}
|
|
4621
4628
|
return tokens[0];
|
|
4622
4629
|
}
|
|
4623
|
-
function parseCopyLikeValues(value, lineNumber, keyword) {
|
|
4624
|
-
const { flags, remaining } = stripLeadingFlags(value);
|
|
4625
|
-
if ("from" in flags) {
|
|
4626
|
-
throw new Error(
|
|
4627
|
-
`line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
|
|
4628
|
-
);
|
|
4629
|
-
}
|
|
4630
|
-
const payload = remaining.trim();
|
|
4631
|
-
if (!payload) {
|
|
4632
|
-
throw new Error(
|
|
4633
|
-
`line ${lineNumber}: ${keyword} must include source and destination`
|
|
4634
|
-
);
|
|
4635
|
-
}
|
|
4636
|
-
let parts;
|
|
4637
|
-
if (payload.startsWith("[")) {
|
|
4638
|
-
let parsed;
|
|
4639
|
-
try {
|
|
4640
|
-
parsed = JSON.parse(payload);
|
|
4641
|
-
} catch (error) {
|
|
4642
|
-
throw new Error(
|
|
4643
|
-
`line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
|
|
4644
|
-
);
|
|
4645
|
-
}
|
|
4646
|
-
if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
|
|
4647
|
-
throw new Error(
|
|
4648
|
-
`line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
|
|
4649
|
-
);
|
|
4650
|
-
}
|
|
4651
|
-
parts = parsed;
|
|
4652
|
-
} else {
|
|
4653
|
-
parts = shellSplit(payload);
|
|
4654
|
-
if (parts.length < 2) {
|
|
4655
|
-
throw new Error(
|
|
4656
|
-
`line ${lineNumber}: ${keyword} must include at least one source and one destination`
|
|
4657
|
-
);
|
|
4658
|
-
}
|
|
4659
|
-
}
|
|
4660
|
-
return {
|
|
4661
|
-
flags,
|
|
4662
|
-
sources: parts.slice(0, -1),
|
|
4663
|
-
destination: parts[parts.length - 1]
|
|
4664
|
-
};
|
|
4665
|
-
}
|
|
4666
|
-
function parseEnvPairs(value, lineNumber) {
|
|
4667
|
-
const tokens = shellSplit(value);
|
|
4668
|
-
if (tokens.length === 0) {
|
|
4669
|
-
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
4670
|
-
}
|
|
4671
|
-
if (tokens.every((token) => token.includes("="))) {
|
|
4672
|
-
return tokens.map((token) => {
|
|
4673
|
-
const [key, envValue] = token.split(/=(.*)/s, 2);
|
|
4674
|
-
if (!key) {
|
|
4675
|
-
throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
|
|
4676
|
-
}
|
|
4677
|
-
return [key, envValue];
|
|
4678
|
-
});
|
|
4679
|
-
}
|
|
4680
|
-
if (tokens.length < 2) {
|
|
4681
|
-
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
4682
|
-
}
|
|
4683
|
-
return [[tokens[0], tokens.slice(1).join(" ")]];
|
|
4684
|
-
}
|
|
4685
|
-
function resolveContainerPath(containerPath, workingDir) {
|
|
4686
|
-
if (!containerPath) {
|
|
4687
|
-
return workingDir;
|
|
4688
|
-
}
|
|
4689
|
-
const normalized = containerPath.startsWith("/") ? path.posix.normalize(containerPath) : path.posix.normalize(path.posix.join(workingDir, containerPath));
|
|
4690
|
-
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
4691
|
-
}
|
|
4692
4630
|
function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
|
|
4693
4631
|
let baseImage;
|
|
4694
4632
|
const instructions = [];
|
|
@@ -4799,14 +4737,292 @@ function buildContextFromEnv() {
|
|
|
4799
4737
|
};
|
|
4800
4738
|
}
|
|
4801
4739
|
function createDefaultClient(context) {
|
|
4740
|
+
const useScopeHeaders = context.personalAccessToken != null && context.apiKey == null;
|
|
4802
4741
|
return new SandboxClient({
|
|
4803
4742
|
apiUrl: context.apiUrl,
|
|
4804
4743
|
apiKey: context.apiKey ?? context.personalAccessToken,
|
|
4805
|
-
organizationId: context.organizationId,
|
|
4806
|
-
projectId: context.projectId,
|
|
4744
|
+
organizationId: useScopeHeaders ? context.organizationId : void 0,
|
|
4745
|
+
projectId: useScopeHeaders ? context.projectId : void 0,
|
|
4807
4746
|
namespace: context.namespace
|
|
4808
4747
|
});
|
|
4809
4748
|
}
|
|
4749
|
+
function baseApiUrl(context) {
|
|
4750
|
+
return context.apiUrl.replace(/\/+$/, "");
|
|
4751
|
+
}
|
|
4752
|
+
function scopedBuildsPath(context) {
|
|
4753
|
+
return `/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-template-builds`;
|
|
4754
|
+
}
|
|
4755
|
+
function platformHeaders(context) {
|
|
4756
|
+
const headers = {
|
|
4757
|
+
Authorization: `Bearer ${context.bearerToken}`,
|
|
4758
|
+
"Content-Type": "application/json"
|
|
4759
|
+
};
|
|
4760
|
+
if (context.useScopeHeaders) {
|
|
4761
|
+
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
4762
|
+
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
4763
|
+
}
|
|
4764
|
+
return headers;
|
|
4765
|
+
}
|
|
4766
|
+
async function requestJson(url, init, errorPrefix) {
|
|
4767
|
+
const response = await fetch(url, init);
|
|
4768
|
+
if (!response.ok) {
|
|
4769
|
+
throw new Error(
|
|
4770
|
+
`${errorPrefix} (HTTP ${response.status}): ${await response.text()}`
|
|
4771
|
+
);
|
|
4772
|
+
}
|
|
4773
|
+
const text = await response.text();
|
|
4774
|
+
return text ? JSON.parse(text) : {};
|
|
4775
|
+
}
|
|
4776
|
+
async function resolveBuildContext(context) {
|
|
4777
|
+
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
4778
|
+
if (!bearerToken) {
|
|
4779
|
+
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
4780
|
+
}
|
|
4781
|
+
if (context.apiKey) {
|
|
4782
|
+
const scope = await requestJson(
|
|
4783
|
+
`${baseApiUrl(context)}/platform/v1/keys/introspect`,
|
|
4784
|
+
{
|
|
4785
|
+
method: "POST",
|
|
4786
|
+
headers: {
|
|
4787
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
4788
|
+
"Content-Type": "application/json"
|
|
4789
|
+
}
|
|
4790
|
+
},
|
|
4791
|
+
"API key introspection failed"
|
|
4792
|
+
);
|
|
4793
|
+
if (!scope.organizationId || !scope.projectId) {
|
|
4794
|
+
throw new Error("API key introspection response is missing organizationId or projectId");
|
|
4795
|
+
}
|
|
4796
|
+
return {
|
|
4797
|
+
...context,
|
|
4798
|
+
bearerToken,
|
|
4799
|
+
organizationId: scope.organizationId,
|
|
4800
|
+
projectId: scope.projectId,
|
|
4801
|
+
useScopeHeaders: false
|
|
4802
|
+
};
|
|
4803
|
+
}
|
|
4804
|
+
if (!context.organizationId || !context.projectId) {
|
|
4805
|
+
throw new Error(
|
|
4806
|
+
"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."
|
|
4807
|
+
);
|
|
4808
|
+
}
|
|
4809
|
+
return {
|
|
4810
|
+
...context,
|
|
4811
|
+
bearerToken,
|
|
4812
|
+
organizationId: context.organizationId,
|
|
4813
|
+
projectId: context.projectId,
|
|
4814
|
+
useScopeHeaders: true
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4817
|
+
async function prepareRootfsBuild(context, plan, isPublic) {
|
|
4818
|
+
if (!plan.baseImage) {
|
|
4819
|
+
throw new Error("Sandbox image builds require a Dockerfile FROM image or Image baseImage");
|
|
4820
|
+
}
|
|
4821
|
+
const spec = await requestJson(
|
|
4822
|
+
`${baseApiUrl(context)}${scopedBuildsPath(context)}`,
|
|
4823
|
+
{
|
|
4824
|
+
method: "POST",
|
|
4825
|
+
headers: platformHeaders(context),
|
|
4826
|
+
body: JSON.stringify({
|
|
4827
|
+
name: plan.registeredName,
|
|
4828
|
+
dockerfile: plan.dockerfileText,
|
|
4829
|
+
baseImage: plan.baseImage,
|
|
4830
|
+
public: isPublic
|
|
4831
|
+
})
|
|
4832
|
+
},
|
|
4833
|
+
"failed to prepare sandbox image build"
|
|
4834
|
+
);
|
|
4835
|
+
return { prepared: parsePreparedBuild(spec), spec };
|
|
4836
|
+
}
|
|
4837
|
+
function parsePreparedBuild(raw) {
|
|
4838
|
+
const builder = raw.builder;
|
|
4839
|
+
if (!builder) {
|
|
4840
|
+
throw new Error("platform API response is missing rootfs builder configuration");
|
|
4841
|
+
}
|
|
4842
|
+
const prepared = {
|
|
4843
|
+
...raw,
|
|
4844
|
+
buildId: requiredString(raw, "buildId"),
|
|
4845
|
+
snapshotId: requiredString(raw, "snapshotId"),
|
|
4846
|
+
snapshotUri: requiredString(raw, "snapshotUri"),
|
|
4847
|
+
rootfsNodeKind: requiredString(raw, "rootfsNodeKind"),
|
|
4848
|
+
builder: {
|
|
4849
|
+
image: requiredString(builder, "image"),
|
|
4850
|
+
command: requiredString(builder, "command"),
|
|
4851
|
+
cpus: requiredNumber(builder, "cpus"),
|
|
4852
|
+
memoryMb: requiredNumber(builder, "memoryMb"),
|
|
4853
|
+
diskMb: requiredNumber(builder, "diskMb")
|
|
4854
|
+
}
|
|
4855
|
+
};
|
|
4856
|
+
const parent = raw.parent;
|
|
4857
|
+
if (parent != null) {
|
|
4858
|
+
prepared.parent = {
|
|
4859
|
+
parentManifestUri: requiredString(parent, "parentManifestUri"),
|
|
4860
|
+
rootfsDiskBytes: optionalNumber(parent, "rootfsDiskBytes")
|
|
4861
|
+
};
|
|
4862
|
+
}
|
|
4863
|
+
return prepared;
|
|
4864
|
+
}
|
|
4865
|
+
async function completeRootfsBuild(context, buildId, request) {
|
|
4866
|
+
return requestJson(
|
|
4867
|
+
`${baseApiUrl(context)}${scopedBuildsPath(context)}/${encodeURIComponent(buildId)}/complete`,
|
|
4868
|
+
{
|
|
4869
|
+
method: "POST",
|
|
4870
|
+
headers: platformHeaders(context),
|
|
4871
|
+
body: JSON.stringify(request)
|
|
4872
|
+
},
|
|
4873
|
+
"failed to complete sandbox image build"
|
|
4874
|
+
);
|
|
4875
|
+
}
|
|
4876
|
+
async function resolvedDockerConfigJson() {
|
|
4877
|
+
const configDir = process.env.DOCKER_CONFIG ?? path.join(homedir(), ".docker");
|
|
4878
|
+
const configPath = path.join(configDir, "config.json");
|
|
4879
|
+
try {
|
|
4880
|
+
const content = await readFile(configPath, "utf8");
|
|
4881
|
+
const parsed = JSON.parse(content);
|
|
4882
|
+
const auths = parsed.auths;
|
|
4883
|
+
if (auths != null && Object.keys(auths).length > 0) {
|
|
4884
|
+
return JSON.stringify({ auths });
|
|
4885
|
+
}
|
|
4886
|
+
} catch (error) {
|
|
4887
|
+
if (error.code === "ENOENT") {
|
|
4888
|
+
return void 0;
|
|
4889
|
+
}
|
|
4890
|
+
throw error;
|
|
4891
|
+
}
|
|
4892
|
+
return void 0;
|
|
4893
|
+
}
|
|
4894
|
+
function rootfsDiskBytes(diskMb, prepared) {
|
|
4895
|
+
if (diskMb != null) {
|
|
4896
|
+
return diskMb * 1024 * 1024;
|
|
4897
|
+
}
|
|
4898
|
+
if (prepared.parent != null) {
|
|
4899
|
+
if (prepared.parent.rootfsDiskBytes == null) {
|
|
4900
|
+
throw new Error(
|
|
4901
|
+
"platform API did not return parent rootfsDiskBytes for diff build; pass diskMb explicitly or update Platform API"
|
|
4902
|
+
);
|
|
4903
|
+
}
|
|
4904
|
+
return prepared.parent.rootfsDiskBytes;
|
|
4905
|
+
}
|
|
4906
|
+
return DEFAULT_ROOTFS_DISK_MB * 1024 * 1024;
|
|
4907
|
+
}
|
|
4908
|
+
function rootfsDiskBytesToMb(bytes) {
|
|
4909
|
+
return Math.ceil(bytes / (1024 * 1024));
|
|
4910
|
+
}
|
|
4911
|
+
async function buildRootfsSpec(preparedSpec, prepared, plan, diskMb) {
|
|
4912
|
+
const spec = {
|
|
4913
|
+
...preparedSpec,
|
|
4914
|
+
dockerfile: plan.dockerfileText,
|
|
4915
|
+
contextDir: REMOTE_CONTEXT_DIR,
|
|
4916
|
+
baseImage: plan.baseImage,
|
|
4917
|
+
rootfsDiskBytes: rootfsDiskBytes(diskMb, prepared)
|
|
4918
|
+
};
|
|
4919
|
+
const dockerConfigJson = await resolvedDockerConfigJson();
|
|
4920
|
+
if (dockerConfigJson != null) {
|
|
4921
|
+
spec.dockerConfigJson = dockerConfigJson;
|
|
4922
|
+
}
|
|
4923
|
+
return spec;
|
|
4924
|
+
}
|
|
4925
|
+
function rootfsBuilderExecutable(executable) {
|
|
4926
|
+
return executable === ROOTFS_BUILDER_COMMAND ? `${ROOTFS_BUILDER_BIN_DIR}/${ROOTFS_BUILDER_COMMAND}` : executable;
|
|
4927
|
+
}
|
|
4928
|
+
function rootfsBuilderEnv() {
|
|
4929
|
+
return { PATH: ROOTFS_BUILDER_PATH };
|
|
4930
|
+
}
|
|
4931
|
+
async function runRootfsBuilder(sandbox, command, emit, sleep3) {
|
|
4932
|
+
const parts = shellSplit(command);
|
|
4933
|
+
const [executable, ...commandArgs] = parts;
|
|
4934
|
+
if (!executable) {
|
|
4935
|
+
throw new Error("empty rootfs builder command returned by platform API");
|
|
4936
|
+
}
|
|
4937
|
+
await runStreaming(
|
|
4938
|
+
sandbox,
|
|
4939
|
+
emit,
|
|
4940
|
+
sleep3,
|
|
4941
|
+
rootfsBuilderExecutable(executable),
|
|
4942
|
+
[...commandArgs, "--spec", REMOTE_SPEC_PATH, "--metadata-out", REMOTE_METADATA_PATH],
|
|
4943
|
+
rootfsBuilderEnv(),
|
|
4944
|
+
REMOTE_BUILD_DIR
|
|
4945
|
+
);
|
|
4946
|
+
}
|
|
4947
|
+
function metadataString(metadata, snakeKey, camelKey) {
|
|
4948
|
+
const value = metadata[snakeKey] ?? metadata[camelKey];
|
|
4949
|
+
return typeof value === "string" ? value : void 0;
|
|
4950
|
+
}
|
|
4951
|
+
function metadataNumber(metadata, snakeKey, camelKey) {
|
|
4952
|
+
const value = metadata[snakeKey] ?? metadata[camelKey];
|
|
4953
|
+
if (typeof value === "number") {
|
|
4954
|
+
return value;
|
|
4955
|
+
}
|
|
4956
|
+
if (typeof value === "string" && value.trim()) {
|
|
4957
|
+
const parsed = Number(value);
|
|
4958
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
4959
|
+
}
|
|
4960
|
+
return void 0;
|
|
4961
|
+
}
|
|
4962
|
+
function completeRequestFromMetadata(prepared, metadata) {
|
|
4963
|
+
const rootfsNodeKind = metadataString(metadata, "rootfs_node_kind", "rootfsNodeKind") ?? prepared.rootfsNodeKind;
|
|
4964
|
+
const parentManifestUri = metadataString(metadata, "parent_manifest_uri", "parentManifestUri") ?? (rootfsNodeKind === "diff" ? prepared.parent?.parentManifestUri : void 0);
|
|
4965
|
+
if (rootfsNodeKind === "diff" && parentManifestUri == null) {
|
|
4966
|
+
throw new Error("rootfs diff build completed without parent_manifest_uri");
|
|
4967
|
+
}
|
|
4968
|
+
const snapshotFormatVersion = metadataString(
|
|
4969
|
+
metadata,
|
|
4970
|
+
"snapshot_format_version",
|
|
4971
|
+
"snapshotFormatVersion"
|
|
4972
|
+
);
|
|
4973
|
+
const snapshotSizeBytes = metadataNumber(
|
|
4974
|
+
metadata,
|
|
4975
|
+
"snapshot_size_bytes",
|
|
4976
|
+
"snapshotSizeBytes"
|
|
4977
|
+
);
|
|
4978
|
+
const rootfsDiskBytesValue = metadataNumber(
|
|
4979
|
+
metadata,
|
|
4980
|
+
"rootfs_disk_bytes",
|
|
4981
|
+
"rootfsDiskBytes"
|
|
4982
|
+
);
|
|
4983
|
+
if (!snapshotFormatVersion) {
|
|
4984
|
+
throw new Error("rootfs builder metadata is missing snapshot_format_version");
|
|
4985
|
+
}
|
|
4986
|
+
if (snapshotSizeBytes == null) {
|
|
4987
|
+
throw new Error("rootfs builder metadata is missing numeric snapshot_size_bytes");
|
|
4988
|
+
}
|
|
4989
|
+
if (rootfsDiskBytesValue == null) {
|
|
4990
|
+
throw new Error("rootfs builder metadata is missing numeric rootfs_disk_bytes");
|
|
4991
|
+
}
|
|
4992
|
+
return {
|
|
4993
|
+
snapshotId: metadataString(metadata, "snapshot_id", "snapshotId") ?? prepared.snapshotId,
|
|
4994
|
+
snapshotUri: metadataString(metadata, "snapshot_uri", "snapshotUri") ?? prepared.snapshotUri,
|
|
4995
|
+
snapshotFormatVersion,
|
|
4996
|
+
snapshotSizeBytes,
|
|
4997
|
+
rootfsDiskBytes: rootfsDiskBytesValue,
|
|
4998
|
+
rootfsNodeKind,
|
|
4999
|
+
...parentManifestUri ? { parentManifestUri } : {}
|
|
5000
|
+
};
|
|
5001
|
+
}
|
|
5002
|
+
function requiredString(object, key) {
|
|
5003
|
+
const value = object[key];
|
|
5004
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
5005
|
+
throw new Error(`expected '${key}' to be a non-empty string`);
|
|
5006
|
+
}
|
|
5007
|
+
return value;
|
|
5008
|
+
}
|
|
5009
|
+
function requiredNumber(object, key) {
|
|
5010
|
+
const value = object[key];
|
|
5011
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
5012
|
+
throw new Error(`expected '${key}' to be a finite number`);
|
|
5013
|
+
}
|
|
5014
|
+
return value;
|
|
5015
|
+
}
|
|
5016
|
+
function optionalNumber(object, key) {
|
|
5017
|
+
const value = object[key];
|
|
5018
|
+
if (value == null) {
|
|
5019
|
+
return void 0;
|
|
5020
|
+
}
|
|
5021
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
5022
|
+
throw new Error(`expected '${key}' to be a finite number`);
|
|
5023
|
+
}
|
|
5024
|
+
return value;
|
|
5025
|
+
}
|
|
4810
5026
|
async function runChecked(sandbox, command, args, env, workingDir) {
|
|
4811
5027
|
const result = await sandbox.run(command, {
|
|
4812
5028
|
args,
|
|
@@ -4866,18 +5082,6 @@ function emitOutputLines(emit, stream, response, seen) {
|
|
|
4866
5082
|
emit({ type: "build_log", stream, message: line });
|
|
4867
5083
|
}
|
|
4868
5084
|
}
|
|
4869
|
-
function isPathWithinContext(contextDir, localPath) {
|
|
4870
|
-
const relative = path.relative(contextDir, localPath);
|
|
4871
|
-
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
4872
|
-
}
|
|
4873
|
-
function resolveContextSourcePath(contextDir, source) {
|
|
4874
|
-
const resolvedContextDir = path.resolve(contextDir);
|
|
4875
|
-
const resolvedSource = path.resolve(resolvedContextDir, source);
|
|
4876
|
-
if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
|
|
4877
|
-
throw new Error(`Local path escapes the build context: ${source}`);
|
|
4878
|
-
}
|
|
4879
|
-
return resolvedSource;
|
|
4880
|
-
}
|
|
4881
5085
|
async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
4882
5086
|
const fileStats = await stat(localPath).catch(() => null);
|
|
4883
5087
|
if (!fileStats) {
|
|
@@ -4908,184 +5112,26 @@ async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
|
4908
5112
|
}
|
|
4909
5113
|
}
|
|
4910
5114
|
}
|
|
4911
|
-
async function
|
|
4912
|
-
const exportLine = `export ${key}=${shellQuote(value)}`;
|
|
4913
|
-
await runChecked(
|
|
4914
|
-
sandbox,
|
|
4915
|
-
"sh",
|
|
4916
|
-
["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
|
|
4917
|
-
processEnv
|
|
4918
|
-
);
|
|
4919
|
-
}
|
|
4920
|
-
async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
|
|
4921
|
-
const destinationPath = resolveContainerPath(destination, workingDir);
|
|
4922
|
-
if (sources.length > 1 && !destinationPath.endsWith("/")) {
|
|
4923
|
-
throw new Error(
|
|
4924
|
-
`${keyword} with multiple sources requires a directory destination ending in '/'`
|
|
4925
|
-
);
|
|
4926
|
-
}
|
|
4927
|
-
for (const source of sources) {
|
|
4928
|
-
const localSource = resolveContextSourcePath(contextDir, source);
|
|
4929
|
-
const localStats = await stat(localSource).catch(() => null);
|
|
4930
|
-
if (!localStats) {
|
|
4931
|
-
throw new Error(`Local path not found: ${localSource}`);
|
|
4932
|
-
}
|
|
4933
|
-
let remoteDestination = destinationPath;
|
|
4934
|
-
if (sources.length > 1) {
|
|
4935
|
-
remoteDestination = path.posix.join(
|
|
4936
|
-
destinationPath.replace(/\/$/, ""),
|
|
4937
|
-
path.posix.basename(source.replace(/\/$/, ""))
|
|
4938
|
-
);
|
|
4939
|
-
} else if (localStats.isFile() && destinationPath.endsWith("/")) {
|
|
4940
|
-
remoteDestination = path.posix.join(
|
|
4941
|
-
destinationPath.replace(/\/$/, ""),
|
|
4942
|
-
path.basename(source)
|
|
4943
|
-
);
|
|
4944
|
-
}
|
|
4945
|
-
emit({
|
|
4946
|
-
type: "status",
|
|
4947
|
-
message: `${keyword} ${source} -> ${remoteDestination}`
|
|
4948
|
-
});
|
|
4949
|
-
await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
|
|
4950
|
-
}
|
|
4951
|
-
}
|
|
4952
|
-
async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep3) {
|
|
4953
|
-
let destinationPath = resolveContainerPath(destination, workingDir);
|
|
4954
|
-
const parsedUrl = new URL(url);
|
|
4955
|
-
const fileName = path.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
|
|
4956
|
-
if (destinationPath.endsWith("/")) {
|
|
4957
|
-
destinationPath = path.posix.join(destinationPath.replace(/\/$/, ""), fileName);
|
|
4958
|
-
}
|
|
4959
|
-
const parentDir = path.posix.dirname(destinationPath) || "/";
|
|
4960
|
-
emit({
|
|
4961
|
-
type: "status",
|
|
4962
|
-
message: `ADD ${url} -> ${destinationPath}`
|
|
4963
|
-
});
|
|
4964
|
-
await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
|
|
4965
|
-
await runStreaming(
|
|
4966
|
-
sandbox,
|
|
4967
|
-
emit,
|
|
4968
|
-
sleep3,
|
|
4969
|
-
"sh",
|
|
4970
|
-
[
|
|
4971
|
-
"-c",
|
|
4972
|
-
`curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
|
|
4973
|
-
],
|
|
4974
|
-
processEnv,
|
|
4975
|
-
workingDir
|
|
4976
|
-
);
|
|
4977
|
-
}
|
|
4978
|
-
async function executeDockerfilePlan(sandbox, plan, emit, sleep3) {
|
|
4979
|
-
const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
|
|
4980
|
-
let workingDir = "/";
|
|
4981
|
-
for (const instruction of plan.instructions) {
|
|
4982
|
-
const { keyword, value, lineNumber } = instruction;
|
|
4983
|
-
if (keyword === "RUN") {
|
|
4984
|
-
emit({ type: "status", message: `RUN ${value}` });
|
|
4985
|
-
await runStreaming(
|
|
4986
|
-
sandbox,
|
|
4987
|
-
emit,
|
|
4988
|
-
sleep3,
|
|
4989
|
-
"sh",
|
|
4990
|
-
["-c", value],
|
|
4991
|
-
processEnv,
|
|
4992
|
-
workingDir
|
|
4993
|
-
);
|
|
4994
|
-
continue;
|
|
4995
|
-
}
|
|
4996
|
-
if (keyword === "WORKDIR") {
|
|
4997
|
-
const tokens = shellSplit(value);
|
|
4998
|
-
if (tokens.length !== 1) {
|
|
4999
|
-
throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
|
|
5000
|
-
}
|
|
5001
|
-
workingDir = resolveContainerPath(tokens[0], workingDir);
|
|
5002
|
-
emit({ type: "status", message: `WORKDIR ${workingDir}` });
|
|
5003
|
-
await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
|
|
5004
|
-
continue;
|
|
5005
|
-
}
|
|
5006
|
-
if (keyword === "ENV") {
|
|
5007
|
-
for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
|
|
5008
|
-
emit({ type: "status", message: `ENV ${key}=${envValue}` });
|
|
5009
|
-
processEnv[key] = envValue;
|
|
5010
|
-
await persistEnvVar(sandbox, processEnv, key, envValue);
|
|
5011
|
-
}
|
|
5012
|
-
continue;
|
|
5013
|
-
}
|
|
5014
|
-
if (keyword === "COPY") {
|
|
5015
|
-
const { sources, destination } = parseCopyLikeValues(
|
|
5016
|
-
value,
|
|
5017
|
-
lineNumber,
|
|
5018
|
-
keyword
|
|
5019
|
-
);
|
|
5020
|
-
await copyFromContext(
|
|
5021
|
-
sandbox,
|
|
5022
|
-
emit,
|
|
5023
|
-
plan.contextDir,
|
|
5024
|
-
sources,
|
|
5025
|
-
destination,
|
|
5026
|
-
workingDir,
|
|
5027
|
-
keyword
|
|
5028
|
-
);
|
|
5029
|
-
continue;
|
|
5030
|
-
}
|
|
5031
|
-
if (keyword === "ADD") {
|
|
5032
|
-
const { sources, destination } = parseCopyLikeValues(
|
|
5033
|
-
value,
|
|
5034
|
-
lineNumber,
|
|
5035
|
-
keyword
|
|
5036
|
-
);
|
|
5037
|
-
if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
|
|
5038
|
-
await addUrlToSandbox(
|
|
5039
|
-
sandbox,
|
|
5040
|
-
emit,
|
|
5041
|
-
sources[0],
|
|
5042
|
-
destination,
|
|
5043
|
-
workingDir,
|
|
5044
|
-
processEnv,
|
|
5045
|
-
sleep3
|
|
5046
|
-
);
|
|
5047
|
-
} else {
|
|
5048
|
-
await copyFromContext(
|
|
5049
|
-
sandbox,
|
|
5050
|
-
emit,
|
|
5051
|
-
plan.contextDir,
|
|
5052
|
-
sources,
|
|
5053
|
-
destination,
|
|
5054
|
-
workingDir,
|
|
5055
|
-
keyword
|
|
5056
|
-
);
|
|
5057
|
-
}
|
|
5058
|
-
continue;
|
|
5059
|
-
}
|
|
5060
|
-
if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
5061
|
-
emit({
|
|
5062
|
-
type: "warning",
|
|
5063
|
-
message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
|
|
5064
|
-
});
|
|
5065
|
-
continue;
|
|
5066
|
-
}
|
|
5067
|
-
throw new Error(
|
|
5068
|
-
`line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
5069
|
-
);
|
|
5070
|
-
}
|
|
5071
|
-
}
|
|
5072
|
-
async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes, isPublic) {
|
|
5073
|
-
if (!context.organizationId || !context.projectId) {
|
|
5074
|
-
throw new Error(
|
|
5075
|
-
"Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
|
|
5076
|
-
);
|
|
5077
|
-
}
|
|
5115
|
+
async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes2, isPublic, snapshotFormatVersion) {
|
|
5078
5116
|
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
5079
5117
|
if (!bearerToken) {
|
|
5080
5118
|
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
5081
5119
|
}
|
|
5082
5120
|
const baseUrl = context.apiUrl.replace(/\/+$/, "");
|
|
5083
|
-
const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
5084
5121
|
const headers = {
|
|
5085
5122
|
Authorization: `Bearer ${bearerToken}`,
|
|
5086
5123
|
"Content-Type": "application/json"
|
|
5087
5124
|
};
|
|
5088
|
-
|
|
5125
|
+
let url;
|
|
5126
|
+
if (context.apiKey) {
|
|
5127
|
+
url = `${baseUrl}/platform/v1/sandbox-templates`;
|
|
5128
|
+
} else {
|
|
5129
|
+
if (!context.organizationId || !context.projectId) {
|
|
5130
|
+
throw new Error(
|
|
5131
|
+
"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."
|
|
5132
|
+
);
|
|
5133
|
+
}
|
|
5134
|
+
url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
5089
5135
|
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
5090
5136
|
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
5091
5137
|
}
|
|
@@ -5098,8 +5144,9 @@ async function registerImage(context, name, dockerfile, snapshotId, snapshotSand
|
|
|
5098
5144
|
snapshotId,
|
|
5099
5145
|
snapshotSandboxId,
|
|
5100
5146
|
snapshotUri,
|
|
5147
|
+
...snapshotFormatVersion ? { snapshotFormatVersion } : {},
|
|
5101
5148
|
snapshotSizeBytes,
|
|
5102
|
-
rootfsDiskBytes,
|
|
5149
|
+
rootfsDiskBytes: rootfsDiskBytes2,
|
|
5103
5150
|
public: isPublic
|
|
5104
5151
|
})
|
|
5105
5152
|
});
|
|
@@ -5116,65 +5163,71 @@ async function createSandboxImage(source, options = {}, deps = {}) {
|
|
|
5116
5163
|
const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5117
5164
|
const context = buildContextFromEnv();
|
|
5118
5165
|
const clientFactory = deps.createClient ?? createDefaultClient;
|
|
5119
|
-
const register = deps.registerImage ?? ((...args) => registerImage(...args));
|
|
5120
5166
|
const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
|
|
5121
5167
|
emit({ type: "status", message: `Loading ${sourceLabel}...` });
|
|
5122
5168
|
const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
|
|
5123
5169
|
emit({
|
|
5124
5170
|
type: "status",
|
|
5125
|
-
message:
|
|
5171
|
+
message: `Selected image name: ${plan.registeredName}`
|
|
5172
|
+
});
|
|
5173
|
+
emit({ type: "status", message: "Preparing rootfs build..." });
|
|
5174
|
+
const resolvedContext = await resolveBuildContext(context);
|
|
5175
|
+
const { prepared, spec: preparedSpec } = await prepareRootfsBuild(
|
|
5176
|
+
resolvedContext,
|
|
5177
|
+
plan,
|
|
5178
|
+
options.isPublic ?? false
|
|
5179
|
+
);
|
|
5180
|
+
emit({
|
|
5181
|
+
type: "status",
|
|
5182
|
+
message: prepared.rootfsNodeKind === "diff" ? "Build mode: RootfsDiff" : "Build mode: RootfsBase"
|
|
5126
5183
|
});
|
|
5127
5184
|
const client = clientFactory(context);
|
|
5128
5185
|
let sandbox;
|
|
5129
5186
|
try {
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
});
|
|
5187
|
+
const outputRootfsDiskBytes = rootfsDiskBytes(options.diskMb, prepared);
|
|
5188
|
+
const builderDiskMb = Math.max(
|
|
5189
|
+
rootfsDiskBytesToMb(outputRootfsDiskBytes),
|
|
5190
|
+
options.builderDiskMb ?? prepared.builder.diskMb
|
|
5191
|
+
);
|
|
5136
5192
|
emit({
|
|
5137
5193
|
type: "status",
|
|
5138
|
-
message: `
|
|
5194
|
+
message: `Creating rootfs builder sandbox from ${prepared.builder.image}...`
|
|
5139
5195
|
});
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
emit({
|
|
5146
|
-
type: "snapshot_created",
|
|
5147
|
-
snapshot_id: snapshot.snapshotId
|
|
5196
|
+
sandbox = await client.createAndConnect({
|
|
5197
|
+
image: prepared.builder.image,
|
|
5198
|
+
cpus: options.cpus ?? prepared.builder.cpus,
|
|
5199
|
+
memoryMb: options.memoryMb ?? prepared.builder.memoryMb,
|
|
5200
|
+
diskMb: builderDiskMb
|
|
5148
5201
|
});
|
|
5149
|
-
if (!snapshot.snapshotUri) {
|
|
5150
|
-
throw new Error(
|
|
5151
|
-
`Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
|
|
5152
|
-
);
|
|
5153
|
-
}
|
|
5154
|
-
if (snapshot.sizeBytes == null) {
|
|
5155
|
-
throw new Error(
|
|
5156
|
-
`Snapshot ${snapshot.snapshotId} is missing sizeBytes and cannot be registered as a sandbox image.`
|
|
5157
|
-
);
|
|
5158
|
-
}
|
|
5159
|
-
if (snapshot.rootfsDiskBytes == null) {
|
|
5160
|
-
throw new Error(
|
|
5161
|
-
`Snapshot ${snapshot.snapshotId} is missing rootfsDiskBytes and cannot be registered as a sandbox image.`
|
|
5162
|
-
);
|
|
5163
|
-
}
|
|
5164
5202
|
emit({
|
|
5165
5203
|
type: "status",
|
|
5166
|
-
message: `
|
|
5204
|
+
message: `Rootfs builder sandbox ${sandbox.sandboxId} is running`
|
|
5167
5205
|
});
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5206
|
+
emit({ type: "status", message: "Uploading build context..." });
|
|
5207
|
+
await copyLocalPathToSandbox(sandbox, plan.contextDir, REMOTE_CONTEXT_DIR);
|
|
5208
|
+
const spec = await buildRootfsSpec(
|
|
5209
|
+
preparedSpec,
|
|
5210
|
+
prepared,
|
|
5211
|
+
plan,
|
|
5212
|
+
options.diskMb
|
|
5213
|
+
);
|
|
5214
|
+
await runChecked(sandbox, "mkdir", ["-p", path.posix.dirname(REMOTE_SPEC_PATH)]);
|
|
5215
|
+
await sandbox.writeFile(
|
|
5216
|
+
REMOTE_SPEC_PATH,
|
|
5217
|
+
new TextEncoder().encode(JSON.stringify(spec, null, 2))
|
|
5218
|
+
);
|
|
5219
|
+
emit({ type: "status", message: "Running offline rootfs builder..." });
|
|
5220
|
+
await runRootfsBuilder(sandbox, prepared.builder.command, emit, sleep3);
|
|
5221
|
+
const metadataBytes = await sandbox.readFile(REMOTE_METADATA_PATH);
|
|
5222
|
+
const metadata = JSON.parse(
|
|
5223
|
+
new TextDecoder().decode(metadataBytes)
|
|
5224
|
+
);
|
|
5225
|
+
const completeRequest = completeRequestFromMetadata(prepared, metadata);
|
|
5226
|
+
emit({ type: "status", message: "Completing image registration..." });
|
|
5227
|
+
const result = await completeRootfsBuild(
|
|
5228
|
+
resolvedContext,
|
|
5229
|
+
prepared.buildId,
|
|
5230
|
+
completeRequest
|
|
5178
5231
|
);
|
|
5179
5232
|
emit({
|
|
5180
5233
|
type: "image_registered",
|
|
@@ -5202,16 +5255,18 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5202
5255
|
cpus: { type: "string" },
|
|
5203
5256
|
memory: { type: "string" },
|
|
5204
5257
|
disk_mb: { type: "string" },
|
|
5258
|
+
builder_disk_mb: { type: "string" },
|
|
5205
5259
|
public: { type: "boolean", default: false }
|
|
5206
5260
|
}
|
|
5207
5261
|
});
|
|
5208
5262
|
const dockerfilePath = parsed.positionals[0];
|
|
5209
5263
|
if (!dockerfilePath) {
|
|
5210
|
-
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--public]");
|
|
5264
|
+
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--builder_disk_mb MB] [--public]");
|
|
5211
5265
|
}
|
|
5212
5266
|
const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
|
|
5213
5267
|
const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
|
|
5214
5268
|
const diskMb = parsed.values.disk_mb != null ? Number(parsed.values.disk_mb) : void 0;
|
|
5269
|
+
const builderDiskMb = parsed.values.builder_disk_mb != null ? Number(parsed.values.builder_disk_mb) : void 0;
|
|
5215
5270
|
if (cpus != null && !Number.isFinite(cpus)) {
|
|
5216
5271
|
throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
|
|
5217
5272
|
}
|
|
@@ -5221,6 +5276,11 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5221
5276
|
if (diskMb != null && !Number.isInteger(diskMb)) {
|
|
5222
5277
|
throw new Error(`Invalid --disk_mb value: ${parsed.values.disk_mb}`);
|
|
5223
5278
|
}
|
|
5279
|
+
if (builderDiskMb != null && !Number.isInteger(builderDiskMb)) {
|
|
5280
|
+
throw new Error(
|
|
5281
|
+
`Invalid --builder_disk_mb value: ${parsed.values.builder_disk_mb}`
|
|
5282
|
+
);
|
|
5283
|
+
}
|
|
5224
5284
|
await createSandboxImage(
|
|
5225
5285
|
dockerfilePath,
|
|
5226
5286
|
{
|
|
@@ -5228,28 +5288,27 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
5228
5288
|
cpus,
|
|
5229
5289
|
memoryMb,
|
|
5230
5290
|
diskMb,
|
|
5291
|
+
builderDiskMb,
|
|
5231
5292
|
isPublic: parsed.values.public
|
|
5232
5293
|
},
|
|
5233
5294
|
{ emit: ndjsonStdoutEmit }
|
|
5234
5295
|
);
|
|
5235
5296
|
}
|
|
5236
|
-
var
|
|
5297
|
+
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;
|
|
5237
5298
|
var init_sandbox_image = __esm({
|
|
5238
5299
|
"src/sandbox-image.ts"() {
|
|
5239
5300
|
"use strict";
|
|
5240
5301
|
init_models();
|
|
5241
5302
|
init_client();
|
|
5242
5303
|
init_image();
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
"VOLUME"
|
|
5252
|
-
]);
|
|
5304
|
+
DEFAULT_ROOTFS_DISK_MB = 10 * 1024;
|
|
5305
|
+
REMOTE_BUILD_DIR = "/var/lib/tensorlake/rootfs-builder/build";
|
|
5306
|
+
REMOTE_CONTEXT_DIR = "/var/lib/tensorlake/rootfs-builder/build/context";
|
|
5307
|
+
REMOTE_SPEC_PATH = "/var/lib/tensorlake/rootfs-builder/build/spec.json";
|
|
5308
|
+
REMOTE_METADATA_PATH = "/var/lib/tensorlake/rootfs-builder/build/metadata.json";
|
|
5309
|
+
ROOTFS_BUILDER_BIN_DIR = "/usr/local/bin";
|
|
5310
|
+
ROOTFS_BUILDER_PATH = "/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
5311
|
+
ROOTFS_BUILDER_COMMAND = "tl-rootfs-build";
|
|
5253
5312
|
UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
5254
5313
|
"ARG",
|
|
5255
5314
|
"ONBUILD",
|