@vercel/container 8.2.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +205 -103
  2. 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 = /sha256:[a-f0-9]{64}/;
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
- const digest = (0, import_node_fs3.readFileSync)(digestFile, "utf8").trim();
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
- const { stdout } = await runDocker(["push", params.imageRef]);
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");
@@ -2470,12 +2560,26 @@ async function startContainer(options, reuseKey) {
2470
2560
  mergedEnv[key] = value;
2471
2561
  }
2472
2562
  }
2563
+ const devResourceEndpoints = meta?.devResourceEndpoints;
2473
2564
  const runtimeCacheEndpoint = mergedEnv.RUNTIME_CACHE_ENDPOINT;
2474
- const containerRuntimeCacheEndpoint = runtimeCacheEndpoint ? useContainerHost(runtimeCacheEndpoint) : void 0;
2475
- const needsHostGateway = containerRuntimeCacheEndpoint !== void 0 && containerRuntimeCacheEndpoint !== runtimeCacheEndpoint;
2565
+ const runtimeCacheProxy = devResourceEndpoints?.runtimeCache;
2566
+ const usesRuntimeCacheProxy = runtimeCacheEndpoint === runtimeCacheProxy?.direct;
2567
+ const containerRuntimeCacheEndpoint = runtimeCacheEndpoint ? useContainerHost(
2568
+ usesRuntimeCacheProxy && runtimeCacheProxy ? runtimeCacheProxy.proxy : runtimeCacheEndpoint
2569
+ ) : void 0;
2570
+ const queueBaseUrl = mergedEnv.VERCEL_QUEUE_BASE_URL;
2571
+ const queueProxy = devResourceEndpoints?.queues;
2572
+ const usesQueueProxy = queueBaseUrl === queueProxy?.direct;
2573
+ const containerQueueBaseUrl = queueBaseUrl ? useContainerHost(
2574
+ usesQueueProxy && queueProxy ? queueProxy.proxy : queueBaseUrl
2575
+ ) : void 0;
2576
+ const needsHostGateway = containerRuntimeCacheEndpoint !== void 0 && containerRuntimeCacheEndpoint !== runtimeCacheEndpoint || containerQueueBaseUrl !== void 0 && containerQueueBaseUrl !== queueBaseUrl;
2476
2577
  if (containerRuntimeCacheEndpoint) {
2477
2578
  mergedEnv.RUNTIME_CACHE_ENDPOINT = containerRuntimeCacheEndpoint;
2478
2579
  }
2580
+ if (containerQueueBaseUrl) {
2581
+ mergedEnv.VERCEL_QUEUE_BASE_URL = containerQueueBaseUrl;
2582
+ }
2479
2583
  mergedEnv.PORT = String(containerPort);
2480
2584
  const envFilePath = writeEnvFile(mergedEnv);
2481
2585
  const rawCommand = config.command;
@@ -2753,16 +2857,14 @@ async function buildAndPushImage(params) {
2753
2857
  { "image.ref": imageRef },
2754
2858
  () => engine.push(buildParams)
2755
2859
  );
2756
- done(
2757
- digest ? `pushed ${shortDigest(digest)} in ${elapsed(pushStart)}` : `pushed in ${elapsed(pushStart)}`
2758
- );
2860
+ done(`pushed ${shortDigest(digest)} in ${elapsed(pushStart)}`);
2759
2861
  await withSpan(
2760
2862
  buildSpan,
2761
2863
  "container.report_storage",
2762
2864
  { "container.engine": engine.name },
2763
2865
  (s) => engine.reportStorage?.(s) ?? Promise.resolve()
2764
2866
  );
2765
- const resolvedRef = digest ? `${VCR_REGISTRY}/${fullRepository}@${digest}` : imageRef;
2867
+ const resolvedRef = `${VCR_REGISTRY}/${fullRepository}@${digest}`;
2766
2868
  buildSpan?.setAttributes({
2767
2869
  "image.digest": digest,
2768
2870
  "image.resolved_ref": resolvedRef
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/container",
3
- "version": "8.2.0",
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
  }