@layr-labs/ecloud-cli 0.1.2 → 0.2.0-dev.1
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/VERSION +2 -2
- package/dist/commands/auth/generate.js.map +1 -1
- package/dist/commands/auth/login.js.map +1 -1
- package/dist/commands/auth/logout.js.map +1 -1
- package/dist/commands/auth/migrate.js.map +1 -1
- package/dist/commands/auth/whoami.js.map +1 -1
- package/dist/commands/billing/cancel.js +1 -0
- package/dist/commands/billing/cancel.js.map +1 -1
- package/dist/commands/billing/status.js +1 -0
- package/dist/commands/billing/status.js.map +1 -1
- package/dist/commands/billing/subscribe.js +2 -1
- package/dist/commands/billing/subscribe.js.map +1 -1
- package/dist/commands/compute/app/create.js.map +1 -1
- package/dist/commands/compute/app/deploy.js +477 -16
- package/dist/commands/compute/app/deploy.js.map +1 -1
- package/dist/commands/compute/app/info.js +6 -3
- package/dist/commands/compute/app/info.js.map +1 -1
- package/dist/commands/compute/app/list.js +4 -2
- package/dist/commands/compute/app/list.js.map +1 -1
- package/dist/commands/compute/app/logs.js +7 -3
- package/dist/commands/compute/app/logs.js.map +1 -1
- package/dist/commands/compute/app/profile/set.js +7 -3
- package/dist/commands/compute/app/profile/set.js.map +1 -1
- package/dist/commands/compute/app/releases.js +1111 -0
- package/dist/commands/compute/app/releases.js.map +1 -0
- package/dist/commands/compute/app/start.js +7 -3
- package/dist/commands/compute/app/start.js.map +1 -1
- package/dist/commands/compute/app/stop.js +7 -3
- package/dist/commands/compute/app/stop.js.map +1 -1
- package/dist/commands/compute/app/terminate.js +7 -3
- package/dist/commands/compute/app/terminate.js.map +1 -1
- package/dist/commands/compute/app/upgrade.js +449 -9
- package/dist/commands/compute/app/upgrade.js.map +1 -1
- package/dist/commands/compute/build/info.js +500 -0
- package/dist/commands/compute/build/info.js.map +1 -0
- package/dist/commands/compute/build/list.js +494 -0
- package/dist/commands/compute/build/list.js.map +1 -0
- package/dist/commands/compute/build/logs.js +459 -0
- package/dist/commands/compute/build/logs.js.map +1 -0
- package/dist/commands/compute/build/status.js +481 -0
- package/dist/commands/compute/build/status.js.map +1 -0
- package/dist/commands/compute/build/submit.js +618 -0
- package/dist/commands/compute/build/submit.js.map +1 -0
- package/dist/commands/compute/build/verify.js +439 -0
- package/dist/commands/compute/build/verify.js.map +1 -0
- package/dist/commands/compute/environment/list.js.map +1 -1
- package/dist/commands/compute/environment/set.js.map +1 -1
- package/dist/commands/compute/environment/show.js.map +1 -1
- package/dist/commands/compute/undelegate.js +6 -3
- package/dist/commands/compute/undelegate.js.map +1 -1
- package/dist/commands/telemetry/disable.js.map +1 -1
- package/dist/commands/telemetry/enable.js.map +1 -1
- package/dist/commands/telemetry/status.js.map +1 -1
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/version.js.map +1 -1
- package/package.json +6 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/commands/compute/app/deploy.ts
|
|
4
4
|
import { Command, Flags as Flags2 } from "@oclif/core";
|
|
5
|
-
import { getEnvironmentConfig as getEnvironmentConfig3,
|
|
5
|
+
import { getEnvironmentConfig as getEnvironmentConfig3, UserApiClient as UserApiClient3, isMainnet } from "@layr-labs/ecloud-sdk";
|
|
6
6
|
|
|
7
7
|
// src/telemetry.ts
|
|
8
8
|
import {
|
|
@@ -273,7 +273,7 @@ function findAvailableName(environment, baseName) {
|
|
|
273
273
|
|
|
274
274
|
// src/utils/version.ts
|
|
275
275
|
function getCliVersion() {
|
|
276
|
-
return true ? "0.1
|
|
276
|
+
return true ? "0.2.0-dev.1" : "0.0.0";
|
|
277
277
|
}
|
|
278
278
|
function getClientId() {
|
|
279
279
|
return `ecloud-cli/v${getCliVersion()}`;
|
|
@@ -307,6 +307,113 @@ Found Dockerfile in ${cwd}`);
|
|
|
307
307
|
throw new Error(`Unexpected choice: ${choice}`);
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
+
async function promptUseVerifiableBuild() {
|
|
311
|
+
return confirmWithDefault("Build from verifiable source?", false);
|
|
312
|
+
}
|
|
313
|
+
async function promptVerifiableSourceType() {
|
|
314
|
+
return select({
|
|
315
|
+
message: "Choose verifiable source type:",
|
|
316
|
+
choices: [
|
|
317
|
+
{ name: "Build from git source (public repo required)", value: "git" },
|
|
318
|
+
{ name: "Use a prebuilt verifiable image (eigencloud-containers)", value: "prebuilt" }
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function promptVerifiableGitSourceInputs() {
|
|
323
|
+
const repoUrl = (await input({
|
|
324
|
+
message: "Enter public git repository URL:",
|
|
325
|
+
default: "",
|
|
326
|
+
validate: (value) => {
|
|
327
|
+
if (!value.trim()) return "Repository URL is required";
|
|
328
|
+
try {
|
|
329
|
+
const url = new URL(value.trim());
|
|
330
|
+
if (url.protocol !== "https:") return "Repository URL must start with https://";
|
|
331
|
+
if (url.hostname.toLowerCase() !== "github.com")
|
|
332
|
+
return "Repository URL must be a public GitHub HTTPS URL (github.com)";
|
|
333
|
+
const parts = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean);
|
|
334
|
+
if (parts.length < 2) return "Repository URL must be https://github.com/<owner>/<repo>";
|
|
335
|
+
const [owner, repo] = parts;
|
|
336
|
+
if (!owner || !repo) return "Repository URL must be https://github.com/<owner>/<repo>";
|
|
337
|
+
if (repo.toLowerCase() === "settings") return "Repository URL looks invalid";
|
|
338
|
+
if (url.search || url.hash)
|
|
339
|
+
return "Repository URL must not include query params or fragments";
|
|
340
|
+
} catch {
|
|
341
|
+
return "Invalid URL format";
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
})).trim();
|
|
346
|
+
const gitRef = (await input({
|
|
347
|
+
message: "Enter git commit SHA (40 hex chars):",
|
|
348
|
+
default: "",
|
|
349
|
+
validate: (value) => {
|
|
350
|
+
const trimmed = value.trim();
|
|
351
|
+
if (!trimmed) return "Commit SHA is required";
|
|
352
|
+
if (!/^[0-9a-f]{40}$/i.test(trimmed))
|
|
353
|
+
return "Commit must be a 40-character hexadecimal SHA";
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
})).trim();
|
|
357
|
+
const buildContextPath = (await input({
|
|
358
|
+
message: "Enter build context path (relative to repo):",
|
|
359
|
+
default: ".",
|
|
360
|
+
validate: (value) => value.trim() ? true : "Build context path cannot be empty"
|
|
361
|
+
})).trim();
|
|
362
|
+
const dockerfilePath = (await input({
|
|
363
|
+
message: "Enter Dockerfile path (relative to build context):",
|
|
364
|
+
default: "Dockerfile",
|
|
365
|
+
validate: (value) => value.trim() ? true : "Dockerfile path cannot be empty"
|
|
366
|
+
})).trim();
|
|
367
|
+
const caddyfileRaw = (await input({
|
|
368
|
+
message: "Enter Caddyfile path (relative to build context, optional):",
|
|
369
|
+
default: "",
|
|
370
|
+
validate: (value) => {
|
|
371
|
+
const trimmed = value.trim();
|
|
372
|
+
if (!trimmed) return true;
|
|
373
|
+
if (trimmed.includes("..")) return "Caddyfile path must not contain '..'";
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
})).trim();
|
|
377
|
+
const depsRaw = (await input({
|
|
378
|
+
message: "Enter dependency digests (comma-separated sha256:..., optional):",
|
|
379
|
+
default: "",
|
|
380
|
+
validate: (value) => {
|
|
381
|
+
const trimmed = value.trim();
|
|
382
|
+
if (!trimmed) return true;
|
|
383
|
+
const parts = trimmed.split(",").map((p) => p.trim()).filter(Boolean);
|
|
384
|
+
for (const p of parts) {
|
|
385
|
+
if (!/^sha256:[0-9a-f]{64}$/i.test(p)) {
|
|
386
|
+
return `Invalid dependency digest: ${p} (expected sha256:<64 hex>)`;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
})).trim();
|
|
392
|
+
const dependencies = depsRaw === "" ? [] : depsRaw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
393
|
+
return {
|
|
394
|
+
repoUrl,
|
|
395
|
+
gitRef,
|
|
396
|
+
dockerfilePath,
|
|
397
|
+
caddyfilePath: caddyfileRaw === "" ? void 0 : caddyfileRaw,
|
|
398
|
+
buildContextPath,
|
|
399
|
+
dependencies
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function promptVerifiablePrebuiltImageRef() {
|
|
403
|
+
const ref = await input({
|
|
404
|
+
message: "Enter prebuilt verifiable image ref:",
|
|
405
|
+
default: "docker.io/eigenlayer/eigencloud-containers:",
|
|
406
|
+
validate: (value) => {
|
|
407
|
+
const trimmed = value.trim();
|
|
408
|
+
if (!trimmed) return "Image reference is required";
|
|
409
|
+
if (!/^docker\.io\/eigenlayer\/eigencloud-containers:[^@\s]+$/i.test(trimmed)) {
|
|
410
|
+
return "Image ref must match docker.io/eigenlayer/eigencloud-containers:<tag>";
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return ref.trim();
|
|
416
|
+
}
|
|
310
417
|
function extractHostname(registry) {
|
|
311
418
|
let hostname = registry.replace(/^https?:\/\//, "");
|
|
312
419
|
hostname = hostname.split("/")[0];
|
|
@@ -555,9 +662,9 @@ async function getImageReferenceInteractive(imageRef, buildFromDockerfile = fals
|
|
|
555
662
|
});
|
|
556
663
|
return imageRefInput;
|
|
557
664
|
}
|
|
558
|
-
async function getAvailableAppNameInteractive(environment, imageRef) {
|
|
559
|
-
const baseName = extractAppNameFromImage(imageRef);
|
|
560
|
-
const suggestedName = findAvailableName(environment, baseName);
|
|
665
|
+
async function getAvailableAppNameInteractive(environment, imageRef, suggestedBaseName, skipDefaultName) {
|
|
666
|
+
const baseName = skipDefaultName ? void 0 : suggestedBaseName || extractAppNameFromImage(imageRef);
|
|
667
|
+
const suggestedName = baseName ? findAvailableName(environment, baseName) : void 0;
|
|
561
668
|
while (true) {
|
|
562
669
|
console.log("\nApp name selection:");
|
|
563
670
|
const name = await input({
|
|
@@ -580,16 +687,16 @@ async function getAvailableAppNameInteractive(environment, imageRef) {
|
|
|
580
687
|
console.log(`Suggested alternative: ${newSuggested}`);
|
|
581
688
|
}
|
|
582
689
|
}
|
|
583
|
-
async function getOrPromptAppName(appName, environment, imageRef) {
|
|
690
|
+
async function getOrPromptAppName(appName, environment, imageRef, suggestedBaseName, skipDefaultName) {
|
|
584
691
|
if (appName) {
|
|
585
692
|
validateAppName(appName);
|
|
586
693
|
if (isAppNameAvailable(environment, appName)) {
|
|
587
694
|
return appName;
|
|
588
695
|
}
|
|
589
696
|
console.log(`Warning: App name '${appName}' is already taken.`);
|
|
590
|
-
return getAvailableAppNameInteractive(environment, imageRef);
|
|
697
|
+
return getAvailableAppNameInteractive(environment, imageRef, suggestedBaseName, skipDefaultName);
|
|
591
698
|
}
|
|
592
|
-
return getAvailableAppNameInteractive(environment, imageRef);
|
|
699
|
+
return getAvailableAppNameInteractive(environment, imageRef, suggestedBaseName, skipDefaultName);
|
|
593
700
|
}
|
|
594
701
|
async function getEnvFileInteractive(envFilePath) {
|
|
595
702
|
if (envFilePath && fs3.existsSync(envFilePath)) {
|
|
@@ -1037,9 +1144,11 @@ var commonFlags = {
|
|
|
1037
1144
|
default: false
|
|
1038
1145
|
})
|
|
1039
1146
|
};
|
|
1040
|
-
async function validateCommonFlags(flags) {
|
|
1147
|
+
async function validateCommonFlags(flags, options) {
|
|
1041
1148
|
flags["environment"] = await getEnvironmentInteractive(flags["environment"]);
|
|
1042
|
-
|
|
1149
|
+
if (options?.requirePrivateKey !== false) {
|
|
1150
|
+
flags["private-key"] = await getPrivateKeyInteractive(flags["private-key"]);
|
|
1151
|
+
}
|
|
1043
1152
|
return flags;
|
|
1044
1153
|
}
|
|
1045
1154
|
|
|
@@ -1047,6 +1156,7 @@ async function validateCommonFlags(flags) {
|
|
|
1047
1156
|
import {
|
|
1048
1157
|
createComputeModule,
|
|
1049
1158
|
createBillingModule,
|
|
1159
|
+
createBuildModule,
|
|
1050
1160
|
getEnvironmentConfig as getEnvironmentConfig2,
|
|
1051
1161
|
requirePrivateKey,
|
|
1052
1162
|
getPrivateKeyWithSource
|
|
@@ -1072,9 +1182,189 @@ async function createComputeClient(flags) {
|
|
|
1072
1182
|
// CLI already has telemetry, skip SDK telemetry
|
|
1073
1183
|
});
|
|
1074
1184
|
}
|
|
1185
|
+
async function createBuildClient(flags) {
|
|
1186
|
+
flags = await validateCommonFlags(flags, { requirePrivateKey: false });
|
|
1187
|
+
return createBuildModule({
|
|
1188
|
+
verbose: flags.verbose,
|
|
1189
|
+
privateKey: flags["private-key"],
|
|
1190
|
+
environment: flags.environment,
|
|
1191
|
+
clientId: getClientId(),
|
|
1192
|
+
skipTelemetry: true
|
|
1193
|
+
// CLI already has telemetry, skip SDK telemetry
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1075
1196
|
|
|
1076
1197
|
// src/commands/compute/app/deploy.ts
|
|
1077
1198
|
import chalk from "chalk";
|
|
1199
|
+
|
|
1200
|
+
// src/utils/build.ts
|
|
1201
|
+
function formatSourceLink(repoUrl, gitRef) {
|
|
1202
|
+
const normalizedRepo = repoUrl.replace(/\.git$/, "");
|
|
1203
|
+
try {
|
|
1204
|
+
const url = new URL(normalizedRepo);
|
|
1205
|
+
const host = url.host.toLowerCase();
|
|
1206
|
+
if (host === "github.com") {
|
|
1207
|
+
const path4 = url.pathname.replace(/\/+$/, "");
|
|
1208
|
+
if (path4.split("/").filter(Boolean).length >= 2) {
|
|
1209
|
+
return `https://github.com${path4}/tree/${gitRef}`;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
return `${repoUrl}@${gitRef}`;
|
|
1215
|
+
}
|
|
1216
|
+
function extractRepoName(repoUrl) {
|
|
1217
|
+
const normalized = repoUrl.replace(/\.git$/, "");
|
|
1218
|
+
const match = normalized.match(/\/([^/]+?)$/);
|
|
1219
|
+
return match?.[1];
|
|
1220
|
+
}
|
|
1221
|
+
function formatDependencyLines(dependencies) {
|
|
1222
|
+
if (!dependencies || Object.keys(dependencies).length === 0) return [];
|
|
1223
|
+
const lines = [];
|
|
1224
|
+
lines.push("Dependencies (resolved builds):");
|
|
1225
|
+
for (const [digest, dep] of Object.entries(dependencies)) {
|
|
1226
|
+
const name = extractRepoName(dep.repoUrl);
|
|
1227
|
+
const depSource = formatSourceLink(dep.repoUrl, dep.gitRef);
|
|
1228
|
+
lines.push(` - ${digest} \u2713${name ? ` ${name}` : ""}`);
|
|
1229
|
+
lines.push(` ${depSource}`);
|
|
1230
|
+
}
|
|
1231
|
+
return lines;
|
|
1232
|
+
}
|
|
1233
|
+
function formatVerifiableBuildSummary(options) {
|
|
1234
|
+
const lines = [];
|
|
1235
|
+
lines.push("Build completed successfully \u2713");
|
|
1236
|
+
lines.push("");
|
|
1237
|
+
lines.push(`Image: ${options.imageUrl}`);
|
|
1238
|
+
lines.push(`Digest: ${options.imageDigest}`);
|
|
1239
|
+
lines.push(`Source: ${formatSourceLink(options.repoUrl, options.gitRef)}`);
|
|
1240
|
+
const depLines = formatDependencyLines(options.dependencies);
|
|
1241
|
+
if (depLines.length) {
|
|
1242
|
+
lines.push("");
|
|
1243
|
+
lines.push(...depLines);
|
|
1244
|
+
}
|
|
1245
|
+
lines.push("");
|
|
1246
|
+
lines.push("Provenance signature verified \u2713");
|
|
1247
|
+
lines.push(`provenance_signature: ${options.provenanceSignature}`);
|
|
1248
|
+
if (options.buildId) {
|
|
1249
|
+
lines.push("");
|
|
1250
|
+
lines.push(`Build ID: ${options.buildId}`);
|
|
1251
|
+
}
|
|
1252
|
+
return lines;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/utils/verifiableBuild.ts
|
|
1256
|
+
import { BUILD_STATUS } from "@layr-labs/ecloud-sdk";
|
|
1257
|
+
function assertCommitSha40(commit) {
|
|
1258
|
+
if (!/^[0-9a-f]{40}$/i.test(commit)) {
|
|
1259
|
+
throw new Error("Commit must be a 40-character hexadecimal SHA");
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async function runVerifiableBuildAndVerify(client, request, options = {}) {
|
|
1263
|
+
const { buildId } = await client.submit(request);
|
|
1264
|
+
const completed = await client.waitForBuild(buildId, { onLog: options.onLog });
|
|
1265
|
+
if (completed.status !== BUILD_STATUS.SUCCESS) {
|
|
1266
|
+
throw new Error(`Build did not complete successfully (status: ${completed.status})`);
|
|
1267
|
+
}
|
|
1268
|
+
const [build, verify] = await Promise.all([client.get(buildId), client.verify(buildId)]);
|
|
1269
|
+
if (verify.status !== "verified") {
|
|
1270
|
+
throw new Error(`Provenance verification failed: ${verify.error}`);
|
|
1271
|
+
}
|
|
1272
|
+
return { build, verified: verify };
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/utils/dockerhub.ts
|
|
1276
|
+
var DOCKERHUB_OWNER = "eigenlayer";
|
|
1277
|
+
var DOCKERHUB_REPO = "eigencloud-containers";
|
|
1278
|
+
function parseEigencloudContainersImageRef(imageRef) {
|
|
1279
|
+
const trimmed = imageRef.trim();
|
|
1280
|
+
const match = /^docker\.io\/([^/]+)\/([^:@]+):([^@\s]+)$/i.exec(trimmed);
|
|
1281
|
+
if (!match) {
|
|
1282
|
+
throw new Error("Image ref must match docker.io/eigenlayer/eigencloud-containers:<tag>");
|
|
1283
|
+
}
|
|
1284
|
+
const owner = match[1].toLowerCase();
|
|
1285
|
+
const repo = match[2].toLowerCase();
|
|
1286
|
+
const tag = match[3];
|
|
1287
|
+
if (owner !== DOCKERHUB_OWNER || repo !== DOCKERHUB_REPO) {
|
|
1288
|
+
throw new Error(`Image ref must be from docker.io/${DOCKERHUB_OWNER}/${DOCKERHUB_REPO}:<tag>`);
|
|
1289
|
+
}
|
|
1290
|
+
if (!tag.trim()) {
|
|
1291
|
+
throw new Error("Image tag cannot be empty");
|
|
1292
|
+
}
|
|
1293
|
+
return { owner, repo, tag };
|
|
1294
|
+
}
|
|
1295
|
+
function assertEigencloudContainersImageRef(imageRef) {
|
|
1296
|
+
parseEigencloudContainersImageRef(imageRef);
|
|
1297
|
+
}
|
|
1298
|
+
async function getDockerHubToken(owner, repo) {
|
|
1299
|
+
const url = new URL("https://auth.docker.io/token");
|
|
1300
|
+
url.searchParams.set("service", "registry.docker.io");
|
|
1301
|
+
url.searchParams.set("scope", `repository:${owner}/${repo}:pull`);
|
|
1302
|
+
const res = await fetch(url.toString(), { method: "GET" });
|
|
1303
|
+
if (!res.ok) {
|
|
1304
|
+
const body = await safeReadText(res);
|
|
1305
|
+
throw new Error(`Failed to fetch Docker Hub token (${res.status}): ${body || res.statusText}`);
|
|
1306
|
+
}
|
|
1307
|
+
const data = await res.json();
|
|
1308
|
+
if (!data.token) {
|
|
1309
|
+
throw new Error("Docker Hub token response missing 'token'");
|
|
1310
|
+
}
|
|
1311
|
+
return data.token;
|
|
1312
|
+
}
|
|
1313
|
+
async function safeReadText(res) {
|
|
1314
|
+
try {
|
|
1315
|
+
return (await res.text()).trim();
|
|
1316
|
+
} catch {
|
|
1317
|
+
return "";
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async function resolveDockerHubImageDigest(imageRef) {
|
|
1321
|
+
const { owner, repo, tag } = parseEigencloudContainersImageRef(imageRef);
|
|
1322
|
+
const token = await getDockerHubToken(owner, repo);
|
|
1323
|
+
const manifestUrl = `https://registry-1.docker.io/v2/${owner}/${repo}/manifests/${encodeURIComponent(tag)}`;
|
|
1324
|
+
const headers = {
|
|
1325
|
+
Authorization: `Bearer ${token}`,
|
|
1326
|
+
Accept: "application/vnd.docker.distribution.manifest.v2+json"
|
|
1327
|
+
};
|
|
1328
|
+
let res = await fetch(manifestUrl, { method: "HEAD", headers });
|
|
1329
|
+
if (!res.ok) {
|
|
1330
|
+
res = await fetch(manifestUrl, { method: "GET", headers });
|
|
1331
|
+
}
|
|
1332
|
+
if (!res.ok) {
|
|
1333
|
+
const body = await safeReadText(res);
|
|
1334
|
+
throw new Error(
|
|
1335
|
+
`Failed to resolve digest for ${imageRef} (${res.status}) at ${manifestUrl}: ${body || res.statusText}`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
const digest = res.headers.get("docker-content-digest") || res.headers.get("Docker-Content-Digest");
|
|
1339
|
+
if (!digest) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`Docker registry response missing Docker-Content-Digest header for ${imageRef}`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
if (!/^sha256:[0-9a-f]{64}$/i.test(digest)) {
|
|
1345
|
+
throw new Error(`Unexpected digest format from Docker registry: ${digest}`);
|
|
1346
|
+
}
|
|
1347
|
+
return digest;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/utils/tls.ts
|
|
1351
|
+
import fs4 from "fs";
|
|
1352
|
+
function isTlsEnabledFromDomain(domain) {
|
|
1353
|
+
const d = (domain ?? "").trim();
|
|
1354
|
+
if (!d) return false;
|
|
1355
|
+
if (d.toLowerCase() === "localhost") return false;
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
function isTlsEnabledFromEnvFile(envFilePath) {
|
|
1359
|
+
if (!envFilePath) return false;
|
|
1360
|
+
if (!fs4.existsSync(envFilePath)) return false;
|
|
1361
|
+
const envContent = fs4.readFileSync(envFilePath, "utf-8");
|
|
1362
|
+
const match = envContent.match(/^DOMAIN=(.+)$/m);
|
|
1363
|
+
if (!match?.[1]) return false;
|
|
1364
|
+
return isTlsEnabledFromDomain(match[1]);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/commands/compute/app/deploy.ts
|
|
1078
1368
|
var AppDeploy = class _AppDeploy extends Command {
|
|
1079
1369
|
static description = "Deploy new app";
|
|
1080
1370
|
static flags = {
|
|
@@ -1137,6 +1427,33 @@ var AppDeploy = class _AppDeploy extends Command {
|
|
|
1137
1427
|
image: Flags2.string({
|
|
1138
1428
|
required: false,
|
|
1139
1429
|
description: "Path to app icon/logo image - JPG/PNG, max 4MB, square recommended (optional)"
|
|
1430
|
+
}),
|
|
1431
|
+
// Verifiable build flags
|
|
1432
|
+
verifiable: Flags2.boolean({
|
|
1433
|
+
description: "Enable verifiable build mode (either build from git source via --repo/--commit, or deploy a prebuilt verifiable image via --image-ref)",
|
|
1434
|
+
default: false
|
|
1435
|
+
}),
|
|
1436
|
+
repo: Flags2.string({
|
|
1437
|
+
description: "Git repository URL (required with --verifiable git source mode)",
|
|
1438
|
+
env: "ECLOUD_BUILD_REPO"
|
|
1439
|
+
}),
|
|
1440
|
+
commit: Flags2.string({
|
|
1441
|
+
description: "Git commit SHA (required with --verifiable git source mode)",
|
|
1442
|
+
env: "ECLOUD_BUILD_COMMIT"
|
|
1443
|
+
}),
|
|
1444
|
+
"build-dockerfile": Flags2.string({
|
|
1445
|
+
description: "Dockerfile path for verifiable build (git source mode)",
|
|
1446
|
+
default: "Dockerfile",
|
|
1447
|
+
env: "ECLOUD_BUILD_DOCKERFILE"
|
|
1448
|
+
}),
|
|
1449
|
+
"build-context": Flags2.string({
|
|
1450
|
+
description: "Build context path for verifiable build (git source mode)",
|
|
1451
|
+
default: ".",
|
|
1452
|
+
env: "ECLOUD_BUILD_CONTEXT"
|
|
1453
|
+
}),
|
|
1454
|
+
"build-dependencies": Flags2.string({
|
|
1455
|
+
description: "Dependency digests for verifiable build (git source mode) (sha256:...)",
|
|
1456
|
+
multiple: true
|
|
1140
1457
|
})
|
|
1141
1458
|
};
|
|
1142
1459
|
async run() {
|
|
@@ -1147,11 +1464,147 @@ var AppDeploy = class _AppDeploy extends Command {
|
|
|
1147
1464
|
const environmentConfig = getEnvironmentConfig3(environment);
|
|
1148
1465
|
const rpcUrl = flags["rpc-url"] || environmentConfig.defaultRPCURL;
|
|
1149
1466
|
const privateKey = flags["private-key"];
|
|
1150
|
-
|
|
1467
|
+
let buildClient;
|
|
1468
|
+
const getBuildClient = async () => {
|
|
1469
|
+
if (buildClient) return buildClient;
|
|
1470
|
+
buildClient = await createBuildClient({
|
|
1471
|
+
...flags,
|
|
1472
|
+
"private-key": privateKey
|
|
1473
|
+
});
|
|
1474
|
+
return buildClient;
|
|
1475
|
+
};
|
|
1476
|
+
let verifiableImageUrl;
|
|
1477
|
+
let verifiableImageDigest;
|
|
1478
|
+
let suggestedAppBaseName;
|
|
1479
|
+
let skipDefaultAppName = false;
|
|
1480
|
+
let verifiableMode = "none";
|
|
1481
|
+
let envFilePath;
|
|
1482
|
+
const suggestAppBaseNameFromRepoUrl = (repoUrl) => {
|
|
1483
|
+
const normalized = String(repoUrl || "").trim().replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
1484
|
+
if (!normalized) return void 0;
|
|
1485
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
1486
|
+
const lastColon = normalized.lastIndexOf(":");
|
|
1487
|
+
const idx = Math.max(lastSlash, lastColon);
|
|
1488
|
+
const raw = (idx >= 0 ? normalized.slice(idx + 1) : normalized).trim();
|
|
1489
|
+
if (!raw) return void 0;
|
|
1490
|
+
const cleaned = raw.toLowerCase().replace(/_/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
1491
|
+
return cleaned || void 0;
|
|
1492
|
+
};
|
|
1493
|
+
if (flags.verifiable) {
|
|
1494
|
+
if (flags.repo || flags.commit) {
|
|
1495
|
+
verifiableMode = "git";
|
|
1496
|
+
if (!flags.repo)
|
|
1497
|
+
this.error("--repo is required when using --verifiable (git source mode)");
|
|
1498
|
+
if (!flags.commit)
|
|
1499
|
+
this.error("--commit is required when using --verifiable (git source mode)");
|
|
1500
|
+
try {
|
|
1501
|
+
assertCommitSha40(flags.commit);
|
|
1502
|
+
} catch (e) {
|
|
1503
|
+
this.error(e?.message || String(e));
|
|
1504
|
+
}
|
|
1505
|
+
} else if (flags["image-ref"]) {
|
|
1506
|
+
verifiableMode = "prebuilt";
|
|
1507
|
+
try {
|
|
1508
|
+
assertEigencloudContainersImageRef(flags["image-ref"]);
|
|
1509
|
+
} catch (e) {
|
|
1510
|
+
this.error(e?.message || String(e));
|
|
1511
|
+
}
|
|
1512
|
+
} else {
|
|
1513
|
+
this.error(
|
|
1514
|
+
"When using --verifiable, you must provide either --repo/--commit or --image-ref"
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
} else {
|
|
1518
|
+
if (!flags.dockerfile) {
|
|
1519
|
+
const useVerifiable = await promptUseVerifiableBuild();
|
|
1520
|
+
if (useVerifiable) {
|
|
1521
|
+
const sourceType = await promptVerifiableSourceType();
|
|
1522
|
+
verifiableMode = sourceType;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (verifiableMode === "git") {
|
|
1527
|
+
const inputs = flags.verifiable ? {
|
|
1528
|
+
repoUrl: flags.repo,
|
|
1529
|
+
gitRef: flags.commit,
|
|
1530
|
+
dockerfilePath: flags["build-dockerfile"],
|
|
1531
|
+
caddyfilePath: void 0,
|
|
1532
|
+
buildContextPath: flags["build-context"],
|
|
1533
|
+
dependencies: flags["build-dependencies"]
|
|
1534
|
+
} : await promptVerifiableGitSourceInputs();
|
|
1535
|
+
envFilePath = await getEnvFileInteractive(flags["env-file"]);
|
|
1536
|
+
const includeTlsCaddyfile = isTlsEnabledFromEnvFile(envFilePath);
|
|
1537
|
+
if (includeTlsCaddyfile && !inputs.caddyfilePath) {
|
|
1538
|
+
inputs.caddyfilePath = "Caddyfile";
|
|
1539
|
+
}
|
|
1540
|
+
this.log(chalk.blue("Building from source with verifiable build..."));
|
|
1541
|
+
this.log("");
|
|
1542
|
+
const buildClient2 = await getBuildClient();
|
|
1543
|
+
const { build, verified } = await runVerifiableBuildAndVerify(buildClient2, inputs, {
|
|
1544
|
+
onLog: (chunk) => process.stdout.write(chunk)
|
|
1545
|
+
});
|
|
1546
|
+
if (!build.imageUrl || !build.imageDigest) {
|
|
1547
|
+
this.error(
|
|
1548
|
+
"Build completed but did not return imageUrl/imageDigest; cannot deploy verifiable build"
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
verifiableImageUrl = build.imageUrl;
|
|
1552
|
+
verifiableImageDigest = build.imageDigest;
|
|
1553
|
+
suggestedAppBaseName = suggestAppBaseNameFromRepoUrl(build.repoUrl);
|
|
1554
|
+
for (const line of formatVerifiableBuildSummary({
|
|
1555
|
+
buildId: build.buildId,
|
|
1556
|
+
imageUrl: build.imageUrl,
|
|
1557
|
+
imageDigest: build.imageDigest,
|
|
1558
|
+
repoUrl: build.repoUrl,
|
|
1559
|
+
gitRef: build.gitRef,
|
|
1560
|
+
dependencies: build.dependencies,
|
|
1561
|
+
provenanceSignature: verified.provenanceSignature
|
|
1562
|
+
})) {
|
|
1563
|
+
this.log(line);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (verifiableMode === "prebuilt") {
|
|
1567
|
+
const imageRef2 = flags.verifiable ? flags["image-ref"] : await promptVerifiablePrebuiltImageRef();
|
|
1568
|
+
try {
|
|
1569
|
+
assertEigencloudContainersImageRef(imageRef2);
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
this.error(e?.message || String(e));
|
|
1572
|
+
}
|
|
1573
|
+
this.log(chalk.blue("Resolving and verifying prebuilt verifiable image..."));
|
|
1574
|
+
this.log("");
|
|
1575
|
+
const digest = await resolveDockerHubImageDigest(imageRef2);
|
|
1576
|
+
const buildClient2 = await getBuildClient();
|
|
1577
|
+
const verify = await buildClient2.verify(digest);
|
|
1578
|
+
if (verify.status !== "verified") {
|
|
1579
|
+
this.error(`Provenance verification failed: ${verify.error}`);
|
|
1580
|
+
}
|
|
1581
|
+
verifiableImageUrl = imageRef2;
|
|
1582
|
+
verifiableImageDigest = digest;
|
|
1583
|
+
skipDefaultAppName = true;
|
|
1584
|
+
for (const line of formatVerifiableBuildSummary({
|
|
1585
|
+
buildId: verify.buildId,
|
|
1586
|
+
imageUrl: imageRef2,
|
|
1587
|
+
imageDigest: digest,
|
|
1588
|
+
repoUrl: verify.repoUrl,
|
|
1589
|
+
gitRef: verify.gitRef,
|
|
1590
|
+
dependencies: void 0,
|
|
1591
|
+
provenanceSignature: verify.provenanceSignature
|
|
1592
|
+
})) {
|
|
1593
|
+
this.log(line);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const isVerifiable = verifiableMode !== "none";
|
|
1597
|
+
const dockerfilePath = isVerifiable ? "" : await getDockerfileInteractive(flags.dockerfile);
|
|
1151
1598
|
const buildFromDockerfile = dockerfilePath !== "";
|
|
1152
|
-
const imageRef = await getImageReferenceInteractive(flags["image-ref"], buildFromDockerfile);
|
|
1153
|
-
const appName = await getOrPromptAppName(
|
|
1154
|
-
|
|
1599
|
+
const imageRef = verifiableImageUrl ? verifiableImageUrl : await getImageReferenceInteractive(flags["image-ref"], buildFromDockerfile);
|
|
1600
|
+
const appName = await getOrPromptAppName(
|
|
1601
|
+
flags.name,
|
|
1602
|
+
environment,
|
|
1603
|
+
imageRef,
|
|
1604
|
+
suggestedAppBaseName,
|
|
1605
|
+
skipDefaultAppName
|
|
1606
|
+
);
|
|
1607
|
+
envFilePath = envFilePath ?? await getEnvFileInteractive(flags["env-file"]);
|
|
1155
1608
|
const availableTypes = await fetchAvailableInstanceTypes(
|
|
1156
1609
|
environmentConfig,
|
|
1157
1610
|
privateKey,
|
|
@@ -1170,7 +1623,15 @@ var AppDeploy = class _AppDeploy extends Command {
|
|
|
1170
1623
|
flags["resource-usage-monitoring"]
|
|
1171
1624
|
);
|
|
1172
1625
|
const logVisibility = logSettings.publicLogs ? "public" : logSettings.logRedirect ? "private" : "off";
|
|
1173
|
-
const { prepared, gasEstimate } = await compute.app.
|
|
1626
|
+
const { prepared, gasEstimate } = isVerifiable ? await compute.app.prepareDeployFromVerifiableBuild({
|
|
1627
|
+
name: appName,
|
|
1628
|
+
imageRef,
|
|
1629
|
+
imageDigest: verifiableImageDigest,
|
|
1630
|
+
envFile: envFilePath,
|
|
1631
|
+
instanceType,
|
|
1632
|
+
logVisibility,
|
|
1633
|
+
resourceUsageMonitoring
|
|
1634
|
+
}) : await compute.app.prepareDeploy({
|
|
1174
1635
|
name: appName,
|
|
1175
1636
|
dockerfile: dockerfilePath,
|
|
1176
1637
|
imageRef,
|
|
@@ -1238,7 +1699,7 @@ ${chalk.gray(`Deployment cancelled`)}`);
|
|
|
1238
1699
|
const cwd = process.env.INIT_CWD || process.cwd();
|
|
1239
1700
|
setLinkedAppForDirectory(environment, cwd, res.appId);
|
|
1240
1701
|
} catch (err) {
|
|
1241
|
-
|
|
1702
|
+
this.debug(`Failed to link directory to app: ${err.message}`);
|
|
1242
1703
|
}
|
|
1243
1704
|
this.log(
|
|
1244
1705
|
`
|