@vercel/container 8.2.1 → 8.2.2
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/index.js +189 -101
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -105,6 +105,9 @@ function elapsed(since) {
|
|
|
105
105
|
function shortDigest(digest) {
|
|
106
106
|
return digest.startsWith("sha256:") ? `${digest.slice(0, 19)}\u2026` : digest;
|
|
107
107
|
}
|
|
108
|
+
function delay(ms) {
|
|
109
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
110
|
+
}
|
|
108
111
|
async function withSpan(parent, name, attrs, fn) {
|
|
109
112
|
if (!parent) {
|
|
110
113
|
return fn(void 0);
|
|
@@ -517,7 +520,7 @@ function formatVcrAuthError(registry, username, detail) {
|
|
|
517
520
|
// src/engines/types.ts
|
|
518
521
|
var VCR_REGISTRY = process.env.VERCEL_VCR_REGISTRY || "vcr.vercel.com";
|
|
519
522
|
var TARGET_PLATFORM = "linux/amd64";
|
|
520
|
-
var DIGEST_RE =
|
|
523
|
+
var DIGEST_RE = /^sha256:[a-f0-9]{64}$/;
|
|
521
524
|
function buildArgFlags(params) {
|
|
522
525
|
const flags = [];
|
|
523
526
|
for (const [key, value] of Object.entries(params.buildArgs ?? {})) {
|
|
@@ -526,6 +529,161 @@ function buildArgFlags(params) {
|
|
|
526
529
|
return flags;
|
|
527
530
|
}
|
|
528
531
|
|
|
532
|
+
// src/registry.ts
|
|
533
|
+
async function resolveRegistryTarget(params) {
|
|
534
|
+
const token = await withSpan(
|
|
535
|
+
params.span,
|
|
536
|
+
"container.mint_oidc",
|
|
537
|
+
{},
|
|
538
|
+
(s) => resolveOidcTokenForBuild(s)
|
|
539
|
+
);
|
|
540
|
+
const claims = decodeOidcClaims(token);
|
|
541
|
+
debug(`registry token: ${tokenFingerprint(token)}`);
|
|
542
|
+
debugTokenClaims("OIDC token claims", token);
|
|
543
|
+
const username = claims.owner_id;
|
|
544
|
+
if (!username) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
"VERCEL_OIDC_TOKEN is missing the `owner_id` (team id) claim required to authenticate to the container registry."
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const fullRepository = [claims.owner, claims.project, params.repository].join(
|
|
550
|
+
"/"
|
|
551
|
+
);
|
|
552
|
+
return {
|
|
553
|
+
token,
|
|
554
|
+
claims,
|
|
555
|
+
username,
|
|
556
|
+
fullRepository,
|
|
557
|
+
imageRef: `${VCR_REGISTRY}/${fullRepository}:${params.tag}`
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
var MANIFEST_MEDIA_TYPES = [
|
|
561
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
562
|
+
"application/vnd.oci.image.index.v1+json",
|
|
563
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
564
|
+
"application/vnd.docker.distribution.manifest.list.v2+json"
|
|
565
|
+
].join(", ");
|
|
566
|
+
var MANIFEST_LOOKUP_ATTEMPTS = 3;
|
|
567
|
+
var MANIFEST_LOOKUP_TIMEOUT_MS = 1e4;
|
|
568
|
+
async function resolvePushedImageDigest(params) {
|
|
569
|
+
const fail = (reason) => new Error(
|
|
570
|
+
`Image ${params.imageRef} was pushed, but its registry digest could not be resolved: ${reason}`
|
|
571
|
+
);
|
|
572
|
+
const ref = parseTaggedImageRef(params.imageRef, params.registry);
|
|
573
|
+
if (!ref) {
|
|
574
|
+
throw fail("Expected a tagged image reference in the push registry.");
|
|
575
|
+
}
|
|
576
|
+
const url = `https://${params.registry}/v2/${ref.repository}/manifests/${encodeURIComponent(ref.tag)}`;
|
|
577
|
+
const authorization = `Basic ${Buffer.from(
|
|
578
|
+
`${params.username}:${params.token}`
|
|
579
|
+
).toString("base64")}`;
|
|
580
|
+
let failure = "Registry request failed.";
|
|
581
|
+
for (let attempt = 1; attempt <= MANIFEST_LOOKUP_ATTEMPTS; attempt++) {
|
|
582
|
+
debug(
|
|
583
|
+
`manifest lookup (${attempt}/${MANIFEST_LOOKUP_ATTEMPTS}): HEAD ${url}`
|
|
584
|
+
);
|
|
585
|
+
let response;
|
|
586
|
+
try {
|
|
587
|
+
response = await fetch(url, {
|
|
588
|
+
method: "HEAD",
|
|
589
|
+
headers: { authorization, accept: MANIFEST_MEDIA_TYPES },
|
|
590
|
+
redirect: "manual",
|
|
591
|
+
signal: AbortSignal.timeout(MANIFEST_LOOKUP_TIMEOUT_MS)
|
|
592
|
+
});
|
|
593
|
+
} catch (err) {
|
|
594
|
+
failure = `Registry request failed: ${describeFetchError(err)}`;
|
|
595
|
+
}
|
|
596
|
+
if (response?.ok) {
|
|
597
|
+
const digest = response.headers.get("docker-content-digest");
|
|
598
|
+
if (!digest || !DIGEST_RE.test(digest)) {
|
|
599
|
+
throw fail("Registry returned a missing or invalid manifest digest.");
|
|
600
|
+
}
|
|
601
|
+
return digest;
|
|
602
|
+
}
|
|
603
|
+
if (response) {
|
|
604
|
+
failure = `Registry returned HTTP ${response.status}.`;
|
|
605
|
+
const retryable = response.status === 404 || response.status === 429 || response.status >= 500;
|
|
606
|
+
if (!retryable) {
|
|
607
|
+
throw fail(failure);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
debug(`manifest lookup failed: ${failure}`);
|
|
611
|
+
if (attempt < MANIFEST_LOOKUP_ATTEMPTS) {
|
|
612
|
+
await delay(500 * 2 ** (attempt - 1));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
throw fail(failure);
|
|
616
|
+
}
|
|
617
|
+
function parseTaggedImageRef(imageRef, registry) {
|
|
618
|
+
const prefix = `${registry}/`;
|
|
619
|
+
if (!imageRef.startsWith(prefix)) {
|
|
620
|
+
return void 0;
|
|
621
|
+
}
|
|
622
|
+
const imagePath = imageRef.slice(prefix.length);
|
|
623
|
+
const tagSeparator = imagePath.lastIndexOf(":");
|
|
624
|
+
if (tagSeparator <= imagePath.lastIndexOf("/") || tagSeparator === imagePath.length - 1) {
|
|
625
|
+
return void 0;
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
repository: imagePath.slice(0, tagSeparator),
|
|
629
|
+
tag: imagePath.slice(tagSeparator + 1)
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function describeFetchError(err) {
|
|
633
|
+
const error = err;
|
|
634
|
+
const cause = error.cause?.message;
|
|
635
|
+
return cause ? `${error.message} (${cause})` : error.message;
|
|
636
|
+
}
|
|
637
|
+
async function ensureRepository(repository, token, claims, span) {
|
|
638
|
+
if (repository.includes("/")) {
|
|
639
|
+
debug(`skipping repository auto-create (fully-qualified "${repository}")`);
|
|
640
|
+
span?.setAttributes({ "repository.create_result": "skipped_qualified" });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const teamId = claims.owner_id;
|
|
644
|
+
const projectId = claims.project_id;
|
|
645
|
+
if (!teamId || !projectId) {
|
|
646
|
+
debug(
|
|
647
|
+
`skipping repository auto-create (missing ${!teamId ? "team id" : "project id"})`
|
|
648
|
+
);
|
|
649
|
+
span?.setAttributes({
|
|
650
|
+
"repository.create_result": "skipped_missing_ids"
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
span?.setAttributes({ "team.id": teamId, "project.id": projectId });
|
|
655
|
+
const apiUrl = (readString(process.env.VERCEL_API_URL) ?? "https://api.vercel.com").replace(/\/+$/, "");
|
|
656
|
+
const url = `${apiUrl}/v1/vcr/repository?teamId=${encodeURIComponent(teamId)}`;
|
|
657
|
+
const body = JSON.stringify({ name: repository, projectId });
|
|
658
|
+
step(`Ensuring registry repository "${repository}"`);
|
|
659
|
+
debug(`repository create: POST ${url}`);
|
|
660
|
+
try {
|
|
661
|
+
const res = await fetch(url, {
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: {
|
|
664
|
+
authorization: `Bearer ${token}`,
|
|
665
|
+
"content-type": "application/json"
|
|
666
|
+
},
|
|
667
|
+
body
|
|
668
|
+
});
|
|
669
|
+
span?.setAttributes({ "repository.create_status": toTag(res.status) });
|
|
670
|
+
if (res.ok) {
|
|
671
|
+
span?.setAttributes({ "repository.create_result": "created" });
|
|
672
|
+
done(`created repository "${repository}"`);
|
|
673
|
+
} else if (res.status === 409) {
|
|
674
|
+
span?.setAttributes({ "repository.create_result": "already_exists" });
|
|
675
|
+
done(`repository "${repository}" already exists`);
|
|
676
|
+
} else {
|
|
677
|
+
span?.setAttributes({ "repository.create_result": "unexpected_status" });
|
|
678
|
+
done("continuing \u2014 push will validate the repository");
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
debug(`repository auto-create failed: ${err.message}`);
|
|
682
|
+
span?.setAttributes({ "repository.create_result": "error" });
|
|
683
|
+
done("continuing \u2014 push will validate the repository");
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
529
687
|
// src/engines/buildah.ts
|
|
530
688
|
async function runBuildah(args, opts = {}) {
|
|
531
689
|
const storageArgs = await buildahStorageArgs();
|
|
@@ -693,6 +851,7 @@ ${stderr}`);
|
|
|
693
851
|
`pushing ${params.imageRef} ` + (zstdEnabled ? "with zstd compression (level=3, force, oci)" : "with default compression (zstd disabled)")
|
|
694
852
|
);
|
|
695
853
|
const pushStart = Date.now();
|
|
854
|
+
let digest;
|
|
696
855
|
try {
|
|
697
856
|
await runBuildah([
|
|
698
857
|
"push",
|
|
@@ -701,12 +860,7 @@ ${stderr}`);
|
|
|
701
860
|
digestFile,
|
|
702
861
|
params.imageRef
|
|
703
862
|
]);
|
|
704
|
-
|
|
705
|
-
const resolved = digest.match(/sha256:[a-f0-9]{64}/)?.[0] ?? (digest || void 0);
|
|
706
|
-
debug(
|
|
707
|
-
`push completed in ${Date.now() - pushStart}ms` + (resolved ? ` (digest ${resolved})` : "")
|
|
708
|
-
);
|
|
709
|
-
return resolved;
|
|
863
|
+
digest = readDigestFile(digestFile);
|
|
710
864
|
} catch (err) {
|
|
711
865
|
const message = err.message;
|
|
712
866
|
if (/denied|forbidden|unauthorized|not found|401|403|404/i.test(message)) {
|
|
@@ -726,8 +880,25 @@ ${stderr}`);
|
|
|
726
880
|
} finally {
|
|
727
881
|
(0, import_node_fs3.rmSync)(digestDir, { recursive: true, force: true });
|
|
728
882
|
}
|
|
883
|
+
debug(`push completed in ${Date.now() - pushStart}ms`);
|
|
884
|
+
if (DIGEST_RE.test(digest)) {
|
|
885
|
+
debug(`pushed digest ${digest}`);
|
|
886
|
+
return digest;
|
|
887
|
+
}
|
|
888
|
+
debug(
|
|
889
|
+
`--digestfile did not contain a digest (${JSON.stringify(digest)}) \u2014 resolving registry manifest`
|
|
890
|
+
);
|
|
891
|
+
return resolvePushedImageDigest(params);
|
|
729
892
|
}
|
|
730
893
|
};
|
|
894
|
+
function readDigestFile(path4) {
|
|
895
|
+
try {
|
|
896
|
+
return (0, import_node_fs3.readFileSync)(path4, "utf8").trim();
|
|
897
|
+
} catch (err) {
|
|
898
|
+
debug(`could not read --digestfile: ${err.message}`);
|
|
899
|
+
return "";
|
|
900
|
+
}
|
|
901
|
+
}
|
|
731
902
|
|
|
732
903
|
// src/engines/docker.ts
|
|
733
904
|
var import_node_child_process2 = require("child_process");
|
|
@@ -989,22 +1160,12 @@ ${version2.trim()}`);
|
|
|
989
1160
|
}
|
|
990
1161
|
},
|
|
991
1162
|
async push(params) {
|
|
1163
|
+
let output;
|
|
992
1164
|
try {
|
|
993
1165
|
info(`pushing ${params.imageRef}`);
|
|
994
1166
|
const pushStart = Date.now();
|
|
995
|
-
|
|
1167
|
+
output = await runDocker(["push", params.imageRef]);
|
|
996
1168
|
debug(`push completed in ${Date.now() - pushStart}ms`);
|
|
997
|
-
let resolvedDigest = stdout.match(/sha256:[a-f0-9]{64}/)?.[0];
|
|
998
|
-
if (!resolvedDigest) {
|
|
999
|
-
debug("digest not found in push output \u2014 inspecting RepoDigests");
|
|
1000
|
-
const inspect = await run(
|
|
1001
|
-
"docker",
|
|
1002
|
-
["inspect", "--format", "{{index .RepoDigests 0}}", params.imageRef],
|
|
1003
|
-
{ quiet: true }
|
|
1004
|
-
);
|
|
1005
|
-
resolvedDigest = inspect.stdout.match(/sha256:[a-f0-9]{64}/)?.[0];
|
|
1006
|
-
}
|
|
1007
|
-
return resolvedDigest;
|
|
1008
1169
|
} catch (err) {
|
|
1009
1170
|
const message = err.message;
|
|
1010
1171
|
if (/denied|forbidden|unauthorized|not found|401|403|404/i.test(message)) {
|
|
@@ -1022,6 +1183,13 @@ ${version2.trim()}`);
|
|
|
1022
1183
|
}
|
|
1023
1184
|
throw err;
|
|
1024
1185
|
}
|
|
1186
|
+
const summary = /^[^\s:]+: digest: (sha256:[a-f0-9]{64}) size: \d+\s*$/m;
|
|
1187
|
+
const digest = output.stdout.match(summary)?.[1] ?? output.stderr.match(summary)?.[1];
|
|
1188
|
+
if (digest) {
|
|
1189
|
+
return digest;
|
|
1190
|
+
}
|
|
1191
|
+
debug("digest not found in push summary \u2014 resolving registry manifest");
|
|
1192
|
+
return resolvePushedImageDigest(params);
|
|
1025
1193
|
}
|
|
1026
1194
|
};
|
|
1027
1195
|
|
|
@@ -1042,84 +1210,6 @@ var import_node_fs10 = require("fs");
|
|
|
1042
1210
|
var import_node_os7 = require("os");
|
|
1043
1211
|
var import_node_path10 = require("path");
|
|
1044
1212
|
|
|
1045
|
-
// src/registry.ts
|
|
1046
|
-
async function resolveRegistryTarget(params) {
|
|
1047
|
-
const token = await withSpan(
|
|
1048
|
-
params.span,
|
|
1049
|
-
"container.mint_oidc",
|
|
1050
|
-
{},
|
|
1051
|
-
(s) => resolveOidcTokenForBuild(s)
|
|
1052
|
-
);
|
|
1053
|
-
const claims = decodeOidcClaims(token);
|
|
1054
|
-
debug(`registry token: ${tokenFingerprint(token)}`);
|
|
1055
|
-
debugTokenClaims("OIDC token claims", token);
|
|
1056
|
-
const username = claims.owner_id;
|
|
1057
|
-
if (!username) {
|
|
1058
|
-
throw new Error(
|
|
1059
|
-
"VERCEL_OIDC_TOKEN is missing the `owner_id` (team id) claim required to authenticate to the container registry."
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1062
|
-
const fullRepository = [claims.owner, claims.project, params.repository].join(
|
|
1063
|
-
"/"
|
|
1064
|
-
);
|
|
1065
|
-
return {
|
|
1066
|
-
token,
|
|
1067
|
-
claims,
|
|
1068
|
-
username,
|
|
1069
|
-
fullRepository,
|
|
1070
|
-
imageRef: `${VCR_REGISTRY}/${fullRepository}:${params.tag}`
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
async function ensureRepository(repository, token, claims, span) {
|
|
1074
|
-
if (repository.includes("/")) {
|
|
1075
|
-
debug(`skipping repository auto-create (fully-qualified "${repository}")`);
|
|
1076
|
-
span?.setAttributes({ "repository.create_result": "skipped_qualified" });
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
const teamId = claims.owner_id;
|
|
1080
|
-
const projectId = claims.project_id;
|
|
1081
|
-
if (!teamId || !projectId) {
|
|
1082
|
-
debug(
|
|
1083
|
-
`skipping repository auto-create (missing ${!teamId ? "team id" : "project id"})`
|
|
1084
|
-
);
|
|
1085
|
-
span?.setAttributes({
|
|
1086
|
-
"repository.create_result": "skipped_missing_ids"
|
|
1087
|
-
});
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
span?.setAttributes({ "team.id": teamId, "project.id": projectId });
|
|
1091
|
-
const apiUrl = (readString(process.env.VERCEL_API_URL) ?? "https://api.vercel.com").replace(/\/+$/, "");
|
|
1092
|
-
const url = `${apiUrl}/v1/vcr/repository?teamId=${encodeURIComponent(teamId)}`;
|
|
1093
|
-
const body = JSON.stringify({ name: repository, projectId });
|
|
1094
|
-
step(`Ensuring registry repository "${repository}"`);
|
|
1095
|
-
debug(`repository create: POST ${url}`);
|
|
1096
|
-
try {
|
|
1097
|
-
const res = await fetch(url, {
|
|
1098
|
-
method: "POST",
|
|
1099
|
-
headers: {
|
|
1100
|
-
authorization: `Bearer ${token}`,
|
|
1101
|
-
"content-type": "application/json"
|
|
1102
|
-
},
|
|
1103
|
-
body
|
|
1104
|
-
});
|
|
1105
|
-
span?.setAttributes({ "repository.create_status": toTag(res.status) });
|
|
1106
|
-
if (res.ok) {
|
|
1107
|
-
span?.setAttributes({ "repository.create_result": "created" });
|
|
1108
|
-
done(`created repository "${repository}"`);
|
|
1109
|
-
} else if (res.status === 409) {
|
|
1110
|
-
span?.setAttributes({ "repository.create_result": "already_exists" });
|
|
1111
|
-
done(`repository "${repository}" already exists`);
|
|
1112
|
-
} else {
|
|
1113
|
-
span?.setAttributes({ "repository.create_result": "unexpected_status" });
|
|
1114
|
-
done("continuing \u2014 push will validate the repository");
|
|
1115
|
-
}
|
|
1116
|
-
} catch (err) {
|
|
1117
|
-
debug(`repository auto-create failed: ${err.message}`);
|
|
1118
|
-
span?.setAttributes({ "repository.create_result": "error" });
|
|
1119
|
-
done("continuing \u2014 push will validate the repository");
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
1213
|
// src/buildpacks/lifecycle/lifecycle.ts
|
|
1124
1214
|
var import_build_utils3 = require("@vercel/build-utils");
|
|
1125
1215
|
var import_node_crypto2 = require("crypto");
|
|
@@ -2767,16 +2857,14 @@ async function buildAndPushImage(params) {
|
|
|
2767
2857
|
{ "image.ref": imageRef },
|
|
2768
2858
|
() => engine.push(buildParams)
|
|
2769
2859
|
);
|
|
2770
|
-
done(
|
|
2771
|
-
digest ? `pushed ${shortDigest(digest)} in ${elapsed(pushStart)}` : `pushed in ${elapsed(pushStart)}`
|
|
2772
|
-
);
|
|
2860
|
+
done(`pushed ${shortDigest(digest)} in ${elapsed(pushStart)}`);
|
|
2773
2861
|
await withSpan(
|
|
2774
2862
|
buildSpan,
|
|
2775
2863
|
"container.report_storage",
|
|
2776
2864
|
{ "container.engine": engine.name },
|
|
2777
2865
|
(s) => engine.reportStorage?.(s) ?? Promise.resolve()
|
|
2778
2866
|
);
|
|
2779
|
-
const resolvedRef =
|
|
2867
|
+
const resolvedRef = `${VCR_REGISTRY}/${fullRepository}@${digest}`;
|
|
2780
2868
|
buildSpan?.setAttributes({
|
|
2781
2869
|
"image.digest": digest,
|
|
2782
2870
|
"image.resolved_ref": resolvedRef
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/container",
|
|
3
|
-
"version": "8.2.
|
|
3
|
+
"version": "8.2.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"homepage": "https://vercel.com/docs",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"build": "node ../../utils/build-builder.mjs",
|
|
32
32
|
"type-check": "tsc --noEmit",
|
|
33
33
|
"test": "vitest run --config ../../vitest.config.mts",
|
|
34
|
-
"test-unit": "vitest run --config ../../vitest.config.mts test/unit.test.ts test/diagnostics.test.ts test/workspace.test.ts test/lifecycle-docker.test.ts test/ruby-version.test.ts",
|
|
34
|
+
"test-unit": "vitest run --config ../../vitest.config.mts test/unit.test.ts test/diagnostics.test.ts test/workspace.test.ts test/lifecycle-docker.test.ts test/ruby-version.test.ts test/docker-push.test.ts",
|
|
35
35
|
"test-e2e": "vitest run --config ../../vitest.config.mts test/e2e.test.ts"
|
|
36
36
|
}
|
|
37
37
|
}
|