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