@johnhenry/sitefile 0.0.0-stage → 0.0.0
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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +475 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +118 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +109 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +94 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +122 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +451 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +92 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +486 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +34 -0
- package/lib/src/errors.js +20 -0
- package/lib/src/executor.d.ts +386 -0
- package/lib/src/executor.js +2193 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +16 -0
- package/lib/src/export-formats.js +52 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +12 -0
- package/lib/src/index.js +12 -0
- package/lib/src/language/ast.d.ts +353 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +70 -0
- package/lib/src/plugins/cache.js +226 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +109 -0
- package/lib/src/registry.js +505 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +40 -0
- package/lib/src/resources/https.js +256 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/types.d.ts +63 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +122 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +71 -0
- package/lib/src/servers.js +139 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +5 -0
- package/lib/src/wbn.js +147 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +58 -4
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { mkdir, open, rename, rm, stat, utimes } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { CACHE_MAX_ENV, formatLimit } from './cache-limit.js';
|
|
6
|
+
import { sha256 } from './digest.js';
|
|
7
|
+
import { SitefileError, isSitefileError } from './errors.js';
|
|
8
|
+
import { canonicalJson, MEDIA, parseManifest } from './oci.js';
|
|
9
|
+
import { canonicalRef, parseRef } from './ref.js';
|
|
10
|
+
/**
|
|
11
|
+
* TRANSFORM cache shared through an OCI registry (`build --cache-from <ref>` / `--cache-to <ref>`,
|
|
12
|
+
* docs/CLI.md "Registry cache"). The cache is one OCI artifact:
|
|
13
|
+
*
|
|
14
|
+
* manifest application/vnd.oci.image.manifest.v1+json, artifactType CACHE_ARTIFACT_TYPE,
|
|
15
|
+
* config = the empty descriptor ({}), layers = [index, ...result blobs by digest]
|
|
16
|
+
* index CACHE_INDEX_MEDIA: canonical JSON {"entries":{"<key>":{"files":[[path,hex,size]],"kind":k}},"version":1}
|
|
17
|
+
* blobs CACHE_BLOB_MEDIA: result bytes, exactly as in the local cache's blobs/ directory
|
|
18
|
+
*
|
|
19
|
+
* `--cache-from` reads the manifest and the index before the build; a result's blobs are fetched
|
|
20
|
+
* only on a hit (a local miss whose key is in the index), verified, and written to the local cache.
|
|
21
|
+
* `--cache-to` pushes, after a successful build, the entries this build used or wrote; blobs the
|
|
22
|
+
* registry already has are not uploaded again. A registry problem is a warning, never a build failure.
|
|
23
|
+
* Pushed bytes are deterministic and hold no local paths or secrets: keys are SHA-256 hashes, entries
|
|
24
|
+
* name output-relative paths, digests and sizes only.
|
|
25
|
+
*
|
|
26
|
+
* Kept byte-for-byte in step with rust/src/registry_cache.rs.
|
|
27
|
+
*/
|
|
28
|
+
export const CACHE_ARTIFACT_TYPE = 'application/vnd.sitefile.cache.v1+json';
|
|
29
|
+
export const CACHE_INDEX_MEDIA = 'application/vnd.sitefile.cache.index.v1+json';
|
|
30
|
+
export const CACHE_BLOB_MEDIA = 'application/vnd.sitefile.cache.blob.v1';
|
|
31
|
+
export const EMPTY_MEDIA = 'application/vnd.oci.empty.v1+json';
|
|
32
|
+
/** Largest cache index accepted. */
|
|
33
|
+
export const MAX_INDEX_BYTES = 64 * 1024 ** 2;
|
|
34
|
+
const INDEX_VERSION = 1;
|
|
35
|
+
const EMPTY = Buffer.from('{}');
|
|
36
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
37
|
+
/** Validate a `--cache-from` / `--cache-to` reference (E_USAGE when it is not usable). */
|
|
38
|
+
export function parseCacheRef(flag, input) {
|
|
39
|
+
const r = parseRef(input);
|
|
40
|
+
if (r.registry === undefined)
|
|
41
|
+
throw new SitefileError('E_USAGE', `${flag} '${input}' has no registry host (e.g. localhost:5000/team/site-cache:main)`);
|
|
42
|
+
if (flag === '--cache-to' && r.digest !== undefined)
|
|
43
|
+
throw new SitefileError('E_USAGE', `${flag} '${input}' must name a tag, not a digest`);
|
|
44
|
+
return { input, ref: canonicalRef(r) };
|
|
45
|
+
}
|
|
46
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
47
|
+
const messageOf = (e) => (isSitefileError(e) ? e.message : String(e?.message ?? e));
|
|
48
|
+
function invalid() {
|
|
49
|
+
return new SitefileError('E_REGISTRY', 'the cache index is invalid');
|
|
50
|
+
}
|
|
51
|
+
function isRecord(v) {
|
|
52
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
/** Parse and validate an index; every blob it names must be a layer of the manifest with that size. */
|
|
55
|
+
export function parseCacheIndex(bytes, blobs) {
|
|
56
|
+
let j;
|
|
57
|
+
try {
|
|
58
|
+
j = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw invalid();
|
|
62
|
+
}
|
|
63
|
+
if (!isRecord(j) || j.version !== INDEX_VERSION || !isRecord(j.entries))
|
|
64
|
+
throw invalid();
|
|
65
|
+
const out = new Map();
|
|
66
|
+
for (const [key, e] of Object.entries(j.entries)) {
|
|
67
|
+
if (!HEX64.test(key) || !isRecord(e) || (e.kind !== 'file' && e.kind !== 'files') || !Array.isArray(e.files))
|
|
68
|
+
throw invalid();
|
|
69
|
+
if (e.kind === 'file' && e.files.length !== 1)
|
|
70
|
+
throw invalid();
|
|
71
|
+
const files = [];
|
|
72
|
+
const paths = new Set();
|
|
73
|
+
for (const f of e.files) {
|
|
74
|
+
if (!Array.isArray(f) || f.length !== 3)
|
|
75
|
+
throw invalid();
|
|
76
|
+
const [p, hex, size] = f;
|
|
77
|
+
if (typeof p !== 'string' || typeof hex !== 'string' || !HEX64.test(hex) || typeof size !== 'number' || !Number.isSafeInteger(size) || size < 0)
|
|
78
|
+
throw invalid();
|
|
79
|
+
if (e.kind === 'file' ? p !== '' : p === '' || p.startsWith('/') || p.includes('..') || p.includes('\0') || p.includes('\\'))
|
|
80
|
+
throw invalid();
|
|
81
|
+
if (blobs.get(hex) !== size || paths.has(p))
|
|
82
|
+
throw invalid();
|
|
83
|
+
paths.add(p);
|
|
84
|
+
files.push([p, hex, size]);
|
|
85
|
+
}
|
|
86
|
+
out.set(key, { kind: e.kind, files });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/** The index bytes for a set of entries (canonical JSON, keys sorted). */
|
|
91
|
+
export function encodeCacheIndex(entries) {
|
|
92
|
+
const o = {};
|
|
93
|
+
for (const [k, e] of entries)
|
|
94
|
+
o[k] = { kind: e.kind, files: e.files.map((f) => [f[0], f[1], f[2]]) };
|
|
95
|
+
return Buffer.from(canonicalJson({ version: INDEX_VERSION, entries: o }));
|
|
96
|
+
}
|
|
97
|
+
/** The artifact manifest for an index and the result blobs (`[hex, size]`, any order). */
|
|
98
|
+
export function encodeCacheManifest(index, blobs) {
|
|
99
|
+
const sorted = [...blobs].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
100
|
+
return Buffer.from(canonicalJson({
|
|
101
|
+
schemaVersion: 2,
|
|
102
|
+
mediaType: MEDIA.manifest,
|
|
103
|
+
artifactType: CACHE_ARTIFACT_TYPE,
|
|
104
|
+
config: { mediaType: EMPTY_MEDIA, digest: sha256(EMPTY), size: EMPTY.length },
|
|
105
|
+
layers: [{ mediaType: CACHE_INDEX_MEDIA, digest: sha256(index), size: index.byteLength }, ...sorted.map(([hex, size]) => ({ mediaType: CACHE_BLOB_MEDIA, digest: `sha256:${hex}`, size }))],
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
async function collect(stream, max, what) {
|
|
109
|
+
const chunks = [];
|
|
110
|
+
let n = 0;
|
|
111
|
+
for await (const c of stream) {
|
|
112
|
+
n += c.length;
|
|
113
|
+
if (n > max)
|
|
114
|
+
throw new SitefileError('E_TOO_LARGE', `${what} exceeded ${max} bytes`);
|
|
115
|
+
chunks.push(c);
|
|
116
|
+
}
|
|
117
|
+
return Buffer.concat(chunks, n);
|
|
118
|
+
}
|
|
119
|
+
/** sha256 hex and size of a file, or undefined when it cannot be read. */
|
|
120
|
+
async function hashFile(file) {
|
|
121
|
+
try {
|
|
122
|
+
const h = createHash('sha256');
|
|
123
|
+
let size = 0;
|
|
124
|
+
for await (const c of createReadStream(file)) {
|
|
125
|
+
h.update(c);
|
|
126
|
+
size += c.length;
|
|
127
|
+
}
|
|
128
|
+
return { hex: h.digest('hex'), size };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export class RegistryCache {
|
|
135
|
+
o;
|
|
136
|
+
/** Entries this build used or wrote (what `--cache-to` pushes). */
|
|
137
|
+
usedEntries = new Map();
|
|
138
|
+
/** Results fetched from `--cache-from` registries, and their blob bytes downloaded. */
|
|
139
|
+
fetched = 0;
|
|
140
|
+
fetchedBytes = 0;
|
|
141
|
+
sources = [];
|
|
142
|
+
stopped = false;
|
|
143
|
+
constructor(o) {
|
|
144
|
+
this.o = o;
|
|
145
|
+
}
|
|
146
|
+
/** Read every `--cache-from` manifest and index (in order). Problems are warnings. */
|
|
147
|
+
async load() {
|
|
148
|
+
for (const c of this.o.from) {
|
|
149
|
+
try {
|
|
150
|
+
const s = await this.loadOne(c);
|
|
151
|
+
this.sources.push(s);
|
|
152
|
+
this.o.notice(`Cache from ${c.input}: ${plural(s.entries.size, 'entry', 'entries')}`);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
this.o.warn(`--cache-from ${c.input} not used: ${messageOf(e)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async loadOne(c) {
|
|
160
|
+
const client = this.o.client(c.ref.registry);
|
|
161
|
+
const repo = c.ref.name;
|
|
162
|
+
const m = await client.getManifest(repo, c.ref.digest ?? c.ref.tag ?? 'latest');
|
|
163
|
+
let artifactType;
|
|
164
|
+
let layers;
|
|
165
|
+
try {
|
|
166
|
+
artifactType = JSON.parse(Buffer.from(m.bytes).toString('utf8')).artifactType;
|
|
167
|
+
layers = parseManifest(m.bytes).layers;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new SitefileError('E_UNSUPPORTED', `${c.input} is not a sitefile cache (not an OCI manifest)`);
|
|
171
|
+
}
|
|
172
|
+
if (artifactType !== CACHE_ARTIFACT_TYPE) {
|
|
173
|
+
throw new SitefileError('E_UNSUPPORTED', `${c.input} is not a sitefile cache (artifactType ${typeof artifactType === 'string' ? `'${artifactType.slice(0, 100)}'` : 'missing'})`);
|
|
174
|
+
}
|
|
175
|
+
const indexes = layers.filter((l) => l.mediaType === CACHE_INDEX_MEDIA);
|
|
176
|
+
const index = indexes[0];
|
|
177
|
+
if (!index || indexes.length !== 1)
|
|
178
|
+
throw new SitefileError('E_REGISTRY', `${c.input} has no cache index layer`);
|
|
179
|
+
if (index.size > MAX_INDEX_BYTES)
|
|
180
|
+
throw new SitefileError('E_TOO_LARGE', `the cache index is ${index.size} bytes, exceeding the limit of ${MAX_INDEX_BYTES} bytes`);
|
|
181
|
+
const blobs = new Map();
|
|
182
|
+
for (const l of layers)
|
|
183
|
+
if (l.mediaType === CACHE_BLOB_MEDIA)
|
|
184
|
+
blobs.set(l.digest.slice('sha256:'.length), l.size);
|
|
185
|
+
const bytes = await collect(client.getBlob(repo, index), index.size, `cache index ${index.digest}`);
|
|
186
|
+
if (bytes.length !== index.size || sha256(bytes) !== index.digest)
|
|
187
|
+
throw new SitefileError('E_INTEGRITY', `the cache index does not match its digest ${index.digest}`);
|
|
188
|
+
return { name: c.input, client, repo, entries: parseCacheIndex(bytes, blobs), blobs, disabled: false };
|
|
189
|
+
}
|
|
190
|
+
used(key, entry) {
|
|
191
|
+
this.usedEntries.set(key, entry);
|
|
192
|
+
}
|
|
193
|
+
async fetch(key, blobPath) {
|
|
194
|
+
for (const s of this.sources) {
|
|
195
|
+
if (this.stopped)
|
|
196
|
+
return undefined;
|
|
197
|
+
const entry = s.disabled ? undefined : s.entries.get(key);
|
|
198
|
+
if (!entry)
|
|
199
|
+
continue;
|
|
200
|
+
try {
|
|
201
|
+
const missing = [];
|
|
202
|
+
for (const [, hex, size] of entry.files) {
|
|
203
|
+
const st = await stat(blobPath(hex)).catch(() => undefined);
|
|
204
|
+
if (st?.isFile() && st.size === size) {
|
|
205
|
+
const now = new Date();
|
|
206
|
+
await utimes(blobPath(hex), now, now).catch(() => { });
|
|
207
|
+
}
|
|
208
|
+
else if (!missing.some((x) => x[0] === hex))
|
|
209
|
+
missing.push([hex, size]);
|
|
210
|
+
}
|
|
211
|
+
const need = missing.reduce((a, x) => a + x[1], 0);
|
|
212
|
+
if (this.o.limit !== 0 && this.fetchedBytes + need > this.o.limit) {
|
|
213
|
+
this.stopped = true;
|
|
214
|
+
this.o.warn(`--cache-from: not fetching more cached results: they would exceed ${CACHE_MAX_ENV}=${formatLimit(this.o.limit)}`);
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
for (const [hex, size] of missing)
|
|
218
|
+
await this.download(s, hex, size, blobPath(hex));
|
|
219
|
+
this.fetchedBytes += need;
|
|
220
|
+
this.fetched++;
|
|
221
|
+
return entry;
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
s.disabled = true;
|
|
225
|
+
this.o.warn(`--cache-from ${s.name}: ${messageOf(e)}; not using it for the rest of this build`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
/** Stream a result blob into the local cache: verified size and digest, temp file + rename. */
|
|
231
|
+
async download(s, hex, size, file) {
|
|
232
|
+
const digest = `sha256:${hex}`;
|
|
233
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
234
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
235
|
+
const fh = await open(tmp, 'w');
|
|
236
|
+
let ok = false;
|
|
237
|
+
try {
|
|
238
|
+
const h = createHash('sha256');
|
|
239
|
+
let n = 0;
|
|
240
|
+
for await (const c of s.client.getBlob(s.repo, { mediaType: CACHE_BLOB_MEDIA, digest, size })) {
|
|
241
|
+
n += c.length;
|
|
242
|
+
if (n > size)
|
|
243
|
+
break;
|
|
244
|
+
h.update(c);
|
|
245
|
+
await fh.write(c);
|
|
246
|
+
}
|
|
247
|
+
if (n !== size || h.digest('hex') !== hex)
|
|
248
|
+
throw new SitefileError('E_INTEGRITY', `cached blob ${digest} does not match its digest or size`);
|
|
249
|
+
ok = true;
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
await fh.close();
|
|
253
|
+
if (!ok)
|
|
254
|
+
await rm(tmp, { force: true });
|
|
255
|
+
}
|
|
256
|
+
await rename(tmp, file);
|
|
257
|
+
}
|
|
258
|
+
/** Push the used entries to `--cache-to` (after a successful build). Problems are warnings. */
|
|
259
|
+
async push(cacheDir) {
|
|
260
|
+
const to = this.o.to;
|
|
261
|
+
if (!to)
|
|
262
|
+
return;
|
|
263
|
+
try {
|
|
264
|
+
const r = await this.pushTo(to, cacheDir);
|
|
265
|
+
this.o.notice(`Cache to ${to.input}: ${plural(r.entries, 'entry', 'entries')}, ${plural(r.uploaded, 'blob', 'blobs')} uploaded, ${r.mounted} mounted, ${r.existing} already present (${r.digest})`);
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
this.o.warn(`--cache-to ${to.input} not pushed: ${messageOf(e)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async pushTo(to, cacheDir) {
|
|
272
|
+
const blobFile = (hex) => path.join(cacheDir, 'blobs', hex);
|
|
273
|
+
// Only entries whose blobs are all still intact locally.
|
|
274
|
+
const good = new Map();
|
|
275
|
+
const bad = new Set();
|
|
276
|
+
const keys = [...this.usedEntries.keys()].sort();
|
|
277
|
+
const entries = new Map();
|
|
278
|
+
for (const k of keys) {
|
|
279
|
+
const e = this.usedEntries.get(k);
|
|
280
|
+
let keep = true;
|
|
281
|
+
for (const [, hex, size] of e.files) {
|
|
282
|
+
if (!good.has(hex) && !bad.has(hex)) {
|
|
283
|
+
const h = await hashFile(blobFile(hex));
|
|
284
|
+
if (h && h.hex === hex && h.size === size)
|
|
285
|
+
good.set(hex, size);
|
|
286
|
+
else
|
|
287
|
+
bad.add(hex);
|
|
288
|
+
}
|
|
289
|
+
if (bad.has(hex))
|
|
290
|
+
keep = false;
|
|
291
|
+
}
|
|
292
|
+
if (keep)
|
|
293
|
+
entries.set(k, e);
|
|
294
|
+
}
|
|
295
|
+
const blobs = new Map();
|
|
296
|
+
for (const e of entries.values())
|
|
297
|
+
for (const [, hex, size] of e.files)
|
|
298
|
+
blobs.set(hex, size);
|
|
299
|
+
const index = encodeCacheIndex(entries);
|
|
300
|
+
const manifest = encodeCacheManifest(index, [...blobs]);
|
|
301
|
+
const client = this.o.client(to.ref.registry);
|
|
302
|
+
if (manifest.byteLength > client.limits.maxManifestBytes) {
|
|
303
|
+
throw new SitefileError('E_TOO_LARGE', `the cache manifest would be ${manifest.byteLength} bytes, exceeding the limit of ${client.limits.maxManifestBytes} bytes`);
|
|
304
|
+
}
|
|
305
|
+
const repo = to.ref.name;
|
|
306
|
+
await client.ping();
|
|
307
|
+
const counts = { uploaded: 0, mounted: 0, existing: 0 };
|
|
308
|
+
const sorted = [...blobs].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
309
|
+
const uploads = [
|
|
310
|
+
{ digest: sha256(EMPTY), size: EMPTY.length, mediaType: EMPTY_MEDIA, open: () => EMPTY },
|
|
311
|
+
{ digest: sha256(index), size: index.byteLength, mediaType: CACHE_INDEX_MEDIA, open: () => index },
|
|
312
|
+
...sorted.map(([hex, size]) => ({
|
|
313
|
+
digest: `sha256:${hex}`,
|
|
314
|
+
size,
|
|
315
|
+
mediaType: CACHE_BLOB_MEDIA,
|
|
316
|
+
open: () => createReadStream(blobFile(hex)),
|
|
317
|
+
// A blob a --cache-from repository on this registry holds is mounted rather than uploaded.
|
|
318
|
+
mount: this.sources.find((s) => s.client.registry === client.registry && s.repo !== repo && s.blobs.get(hex) === size)?.repo,
|
|
319
|
+
})),
|
|
320
|
+
];
|
|
321
|
+
for (const u of uploads) {
|
|
322
|
+
if (await client.hasBlob(repo, u.digest)) {
|
|
323
|
+
counts.existing++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const r = await client.pushBlob(repo, { mediaType: u.mediaType, digest: u.digest, size: u.size }, { length: u.size, open: u.open }, u.mount);
|
|
327
|
+
counts[r]++;
|
|
328
|
+
}
|
|
329
|
+
const digest = await client.putManifest(repo, to.ref.tag ?? 'latest', manifest, MEDIA.manifest);
|
|
330
|
+
return { entries: entries.size, ...counts, digest };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=registry-cache.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Referrer } from './attest.js';
|
|
2
|
+
import { type Descriptor, type ParsedManifest } from './oci.js';
|
|
3
|
+
import { type ImageRef } from './ref.js';
|
|
4
|
+
import type { RegistryClient } from './registry.js';
|
|
5
|
+
import { Store } from './store.js';
|
|
6
|
+
/** Pick the manifest descriptor for `os/arch` from an image index / manifest list. */
|
|
7
|
+
export declare function selectPlatform(indexBytes: Uint8Array, platform: string): Descriptor;
|
|
8
|
+
export interface PullOptions {
|
|
9
|
+
/** `os/arch` used to pick from an index (default linux/amd64). */
|
|
10
|
+
readonly platform?: string | undefined;
|
|
11
|
+
/** Fail when the image's own platform differs from `platform` (BASE does; `pull` only with --platform). */
|
|
12
|
+
readonly enforcePlatform?: boolean | undefined;
|
|
13
|
+
/** Fetch this digest (an index or a manifest) instead of resolving the reference's tag. */
|
|
14
|
+
readonly rootDigest?: string | undefined;
|
|
15
|
+
/** Tag the image locally as the reference (registry/name:tag). */
|
|
16
|
+
readonly tag?: boolean | undefined;
|
|
17
|
+
readonly onEvent?: ((message: string) => void) | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface PullResult {
|
|
20
|
+
/** The platform manifest digest: the local image ID. */
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** What the reference resolved to (an index digest for multi-platform images, else `id`). */
|
|
23
|
+
readonly rootDigest: string;
|
|
24
|
+
readonly manifest: ParsedManifest;
|
|
25
|
+
readonly config: Readonly<Record<string, unknown>>;
|
|
26
|
+
}
|
|
27
|
+
/** Resolve a tag to the digest it currently points at (one manifest GET), caching the manifest. */
|
|
28
|
+
export declare function resolveDigest(store: Store, client: RegistryClient, ref: ImageRef): Promise<string>;
|
|
29
|
+
/** Pull an image into the store: verifies every blob's sha256 while streaming, then registers the manifest. */
|
|
30
|
+
export declare function pullImage(store: Store, client: RegistryClient, ref: ImageRef, o?: PullOptions): Promise<PullResult>;
|
|
31
|
+
export interface PushOptions {
|
|
32
|
+
readonly onEvent?: ((message: string) => void) | undefined;
|
|
33
|
+
}
|
|
34
|
+
export interface PushResult {
|
|
35
|
+
readonly digest: string;
|
|
36
|
+
readonly size: number;
|
|
37
|
+
readonly uploaded: number;
|
|
38
|
+
readonly mounted: number;
|
|
39
|
+
readonly existing: number;
|
|
40
|
+
/** Attestations pushed (as OCI 1.1 referrers of the image). */
|
|
41
|
+
readonly attestations: number;
|
|
42
|
+
}
|
|
43
|
+
/** Push a stored image to `ref` (a registry reference): blobs that are missing, then the manifest. */
|
|
44
|
+
export declare function pushImage(store: Store, client: RegistryClient, id: string, ref: ImageRef, o?: PushOptions): Promise<PushResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Push attestations as OCI 1.1 referrers of `subject` (distribution spec v1.1): each artifact
|
|
47
|
+
* manifest (with its blobs) by digest. A registry with the referrers API answers with an
|
|
48
|
+
* `OCI-Subject` header; for the others the referrers tag schema is used: the image index at tag
|
|
49
|
+
* `sha256-<hex>` lists them (merged with what it already lists).
|
|
50
|
+
*/
|
|
51
|
+
export declare function pushReferrers(client: RegistryClient, repo: string, subject: string, referrers: readonly Referrer[], say: (m: string) => void): Promise<number>;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { ATTEST, referrersTag } from './attest.js';
|
|
3
|
+
import { isSitefileError, SitefileError } from './errors.js';
|
|
4
|
+
import { canonicalJson, MEDIA, parseConfig, parseManifest, parsePlatform } from './oci.js';
|
|
5
|
+
import { parseRef, withDefaultRegistry } from './ref.js';
|
|
6
|
+
import { shortId, Store } from './store.js';
|
|
7
|
+
const CONFIG_TYPES = new Set([MEDIA.config, MEDIA.dockerConfig]);
|
|
8
|
+
const INDEX_TYPES = new Set([MEDIA.index, MEDIA.dockerList]);
|
|
9
|
+
const MANIFEST_TYPES = new Set([MEDIA.manifest, MEDIA.dockerManifest]);
|
|
10
|
+
/** Pick the manifest descriptor for `os/arch` from an image index / manifest list. */
|
|
11
|
+
export function selectPlatform(indexBytes, platform) {
|
|
12
|
+
const want = parsePlatform(platform);
|
|
13
|
+
let j;
|
|
14
|
+
try {
|
|
15
|
+
j = JSON.parse(Buffer.from(indexBytes).toString('utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
throw new SitefileError('E_REGISTRY', 'the registry returned an unreadable image index', undefined, { cause: e });
|
|
19
|
+
}
|
|
20
|
+
const list = Array.isArray(j.manifests) ? j.manifests : [];
|
|
21
|
+
const have = [];
|
|
22
|
+
const hits = [];
|
|
23
|
+
for (const m of list) {
|
|
24
|
+
const p = m.platform;
|
|
25
|
+
if (!p || p.architecture === 'unknown' || typeof m.digest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(m.digest) || typeof m.size !== 'number')
|
|
26
|
+
continue;
|
|
27
|
+
have.push(`${p.os}/${p.architecture}${p.variant ? `/${p.variant}` : ''}`);
|
|
28
|
+
if (p.os === want.os && p.architecture === want.arch) {
|
|
29
|
+
hits.push({ d: { mediaType: String(m.mediaType ?? MEDIA.manifest), digest: m.digest, size: m.size }, variant: p.variant ?? '' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const best = hits.find((h) => h.variant === '') ?? hits[0];
|
|
33
|
+
if (!best) {
|
|
34
|
+
throw new SitefileError('E_REGISTRY', `the image has no ${platform} variant (available: ${[...new Set(have)].join(', ') || 'none'}); pass --platform`);
|
|
35
|
+
}
|
|
36
|
+
return best.d;
|
|
37
|
+
}
|
|
38
|
+
async function fetchManifestCached(store, client, repo, reference) {
|
|
39
|
+
if (reference.startsWith('sha256:') && (await store.hasBlob(reference))) {
|
|
40
|
+
try {
|
|
41
|
+
const bytes = await store.readBlob(reference);
|
|
42
|
+
return { bytes, digest: reference, mediaType: sniffMediaType(bytes) };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* corrupt cache entry: fetch again */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const m = await client.getManifest(repo, reference);
|
|
49
|
+
await store.putSmallBlob(m.bytes);
|
|
50
|
+
return { ...m, mediaType: m.mediaType || sniffMediaType(m.bytes) };
|
|
51
|
+
}
|
|
52
|
+
function sniffMediaType(bytes) {
|
|
53
|
+
try {
|
|
54
|
+
const j = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
55
|
+
if (typeof j.mediaType === 'string')
|
|
56
|
+
return j.mediaType;
|
|
57
|
+
return Array.isArray(j.manifests) ? MEDIA.index : MEDIA.manifest;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Resolve a tag to the digest it currently points at (one manifest GET), caching the manifest. */
|
|
64
|
+
export async function resolveDigest(store, client, ref) {
|
|
65
|
+
const m = await fetchManifestCached(store, client, ref.name, ref.tag ?? 'latest');
|
|
66
|
+
return m.digest;
|
|
67
|
+
}
|
|
68
|
+
/** Pull an image into the store: verifies every blob's sha256 while streaming, then registers the manifest. */
|
|
69
|
+
export async function pullImage(store, client, ref, o = {}) {
|
|
70
|
+
if (ref.registry === undefined)
|
|
71
|
+
throw new SitefileError('E_USAGE', 'internal: pullImage needs a registry reference');
|
|
72
|
+
const platform = o.platform ?? 'linux/amd64';
|
|
73
|
+
const say = o.onEvent ?? (() => undefined);
|
|
74
|
+
const repo = ref.name;
|
|
75
|
+
const reference = o.rootDigest ?? ref.digest ?? ref.tag ?? 'latest';
|
|
76
|
+
const limits = client.limits;
|
|
77
|
+
const root = await fetchManifestCached(store, client, repo, reference);
|
|
78
|
+
let manifest = root;
|
|
79
|
+
if (INDEX_TYPES.has(root.mediaType)) {
|
|
80
|
+
const d = selectPlatform(root.bytes, platform);
|
|
81
|
+
manifest = await fetchManifestCached(store, client, repo, d.digest);
|
|
82
|
+
}
|
|
83
|
+
else if (!MANIFEST_TYPES.has(root.mediaType)) {
|
|
84
|
+
throw new SitefileError('E_UNSUPPORTED', `${client.registry}/${repo}: unsupported manifest type '${root.mediaType || 'unknown'}' (expected an OCI or Docker v2 image)`);
|
|
85
|
+
}
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = parseManifest(manifest.bytes);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
throw new SitefileError('E_REGISTRY', `${client.registry}/${repo}: ${e.message}`, undefined, { cause: e });
|
|
92
|
+
}
|
|
93
|
+
if (!CONFIG_TYPES.has(parsed.config.mediaType)) {
|
|
94
|
+
throw new SitefileError('E_UNSUPPORTED', `${client.registry}/${repo}: the artifact is not a container image (config type '${parsed.config.mediaType}')`);
|
|
95
|
+
}
|
|
96
|
+
const blobs = [parsed.config, ...parsed.layers];
|
|
97
|
+
let total = 0;
|
|
98
|
+
for (const d of blobs) {
|
|
99
|
+
if (d.size > limits.maxBlobBytes)
|
|
100
|
+
throw new SitefileError('E_TOO_LARGE', `blob ${d.digest} is ${d.size} bytes, exceeding the limit of ${limits.maxBlobBytes} bytes (--max-download-bytes)`);
|
|
101
|
+
total += d.size;
|
|
102
|
+
}
|
|
103
|
+
if (total > limits.maxTotalBytes)
|
|
104
|
+
throw new SitefileError('E_TOO_LARGE', `${client.registry}/${repo} totals ${total} bytes, exceeding the limit of ${limits.maxTotalBytes} bytes`);
|
|
105
|
+
const unique = [...new Map(blobs.map((d) => [d.digest, d])).values()];
|
|
106
|
+
let next = 0;
|
|
107
|
+
const worker = async () => {
|
|
108
|
+
for (;;) {
|
|
109
|
+
const d = unique[next++];
|
|
110
|
+
if (!d)
|
|
111
|
+
return;
|
|
112
|
+
if (await store.hasBlob(d.digest, d.size)) {
|
|
113
|
+
await store.touchBlob(d.digest); // recently used: protected from the cache size limit
|
|
114
|
+
say(`${shortId(d.digest)}: already exists`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
await store.importBlob(d.digest, d.size, client.getBlob(repo, d), limits.maxBlobBytes);
|
|
118
|
+
say(`${shortId(d.digest)}: pulled (${d.size} bytes)`);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
await Promise.all(Array.from({ length: Math.min(3, unique.length) }, worker));
|
|
122
|
+
const configJson = JSON.parse(Buffer.from(await store.readBlob(parsed.config.digest, parsed.config.size)).toString('utf8'));
|
|
123
|
+
if (o.enforcePlatform) {
|
|
124
|
+
const want = parsePlatform(platform);
|
|
125
|
+
const cfg = parseConfig(Buffer.from(JSON.stringify(configJson)));
|
|
126
|
+
if (cfg.os !== want.os || cfg.architecture !== want.arch) {
|
|
127
|
+
throw new SitefileError('E_REGISTRY', `${client.registry}/${repo} is ${cfg.os}/${cfg.architecture}, not ${platform}; pass --platform`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const tagRef = o.tag ? { registry: ref.registry, name: ref.name, tag: ref.tag ?? 'latest', digest: undefined } : undefined;
|
|
131
|
+
const id = await store.addManifest(manifest.bytes, tagRef);
|
|
132
|
+
return { id, rootDigest: root.digest, manifest: parsed, config: configJson };
|
|
133
|
+
}
|
|
134
|
+
/** Push a stored image to `ref` (a registry reference): blobs that are missing, then the manifest. */
|
|
135
|
+
export async function pushImage(store, client, id, ref, o = {}) {
|
|
136
|
+
const say = o.onEvent ?? (() => undefined);
|
|
137
|
+
const repo = ref.name;
|
|
138
|
+
if (ref.digest !== undefined && ref.digest !== id) {
|
|
139
|
+
throw new SitefileError('E_USAGE', `the reference pins ${ref.digest} but the local image is ${id}`);
|
|
140
|
+
}
|
|
141
|
+
const manifestBytes = await store.readBlob(id);
|
|
142
|
+
const parsed = parseManifest(manifestBytes);
|
|
143
|
+
const ann = parsed.annotations['org.opencontainers.image.base.name'];
|
|
144
|
+
// Layers of the BASE image can be mounted from its repository when it lives on this registry.
|
|
145
|
+
let mountFrom;
|
|
146
|
+
let baseLayers = new Set();
|
|
147
|
+
if (ann) {
|
|
148
|
+
try {
|
|
149
|
+
const b = withDefaultRegistry(parseRef(ann));
|
|
150
|
+
if (b.registry === client.registry && b.name !== repo) {
|
|
151
|
+
mountFrom = b.name;
|
|
152
|
+
baseLayers = new Set(parsed.layers.slice(0, -1).map((l) => l.digest));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
/* not a reference we understand */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await client.ping();
|
|
160
|
+
const counts = { uploaded: 0, mounted: 0, existing: 0 };
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (const d of [parsed.config, ...parsed.layers]) {
|
|
163
|
+
if (seen.has(d.digest))
|
|
164
|
+
continue;
|
|
165
|
+
seen.add(d.digest);
|
|
166
|
+
if (await client.hasBlob(repo, d.digest)) {
|
|
167
|
+
counts.existing++;
|
|
168
|
+
say(`${shortId(d.digest)}: layer already exists`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!(await store.hasBlob(d.digest, d.size)))
|
|
172
|
+
throw new SitefileError('E_STORE', `blob ${d.digest} is missing from the local store; cannot push`);
|
|
173
|
+
const r = await client.pushBlob(repo, d, { length: d.size, open: () => store.blobStream(d.digest) }, baseLayers.has(d.digest) ? mountFrom : undefined);
|
|
174
|
+
counts[r]++;
|
|
175
|
+
say(`${shortId(d.digest)}: ${r === 'mounted' ? 'mounted from ' + mountFrom : 'pushed'} (${d.size} bytes)`);
|
|
176
|
+
}
|
|
177
|
+
let mediaType = MEDIA.manifest;
|
|
178
|
+
try {
|
|
179
|
+
const j = JSON.parse(Buffer.from(manifestBytes).toString('utf8'));
|
|
180
|
+
if (typeof j.mediaType === 'string')
|
|
181
|
+
mediaType = j.mediaType;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* parseManifest already succeeded */
|
|
185
|
+
}
|
|
186
|
+
const digest = await client.putManifest(repo, ref.tag ?? ref.digest ?? 'latest', manifestBytes, mediaType);
|
|
187
|
+
if (digest !== id)
|
|
188
|
+
throw new SitefileError('E_INTEGRITY', `the registry reports digest ${digest}, expected ${id}`);
|
|
189
|
+
const attestations = await pushReferrers(client, repo, id, await store.referrers(id), say);
|
|
190
|
+
return { digest, size: manifestBytes.byteLength, ...counts, attestations };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Push attestations as OCI 1.1 referrers of `subject` (distribution spec v1.1): each artifact
|
|
194
|
+
* manifest (with its blobs) by digest. A registry with the referrers API answers with an
|
|
195
|
+
* `OCI-Subject` header; for the others the referrers tag schema is used: the image index at tag
|
|
196
|
+
* `sha256-<hex>` lists them (merged with what it already lists).
|
|
197
|
+
*/
|
|
198
|
+
export async function pushReferrers(client, repo, subject, referrers, say) {
|
|
199
|
+
const fallback = [];
|
|
200
|
+
for (const r of referrers) {
|
|
201
|
+
const blobs = new Map(r.blobs);
|
|
202
|
+
const manifestBytes = blobs.get(r.descriptor.digest);
|
|
203
|
+
if (!manifestBytes)
|
|
204
|
+
throw new SitefileError('E_STORE', `attestation ${r.descriptor.digest} is missing from the local store; cannot push`);
|
|
205
|
+
const m = parseManifest(manifestBytes);
|
|
206
|
+
for (const d of [m.config, ...m.layers]) {
|
|
207
|
+
if (await client.hasBlob(repo, d.digest))
|
|
208
|
+
continue;
|
|
209
|
+
const bytes = blobs.get(d.digest);
|
|
210
|
+
if (!bytes)
|
|
211
|
+
throw new SitefileError('E_STORE', `blob ${d.digest} is missing from the local store; cannot push`);
|
|
212
|
+
await client.pushBlob(repo, d, { length: bytes.byteLength, open: () => bytes });
|
|
213
|
+
}
|
|
214
|
+
const put = await client.putManifestFull(repo, r.descriptor.digest, manifestBytes, MEDIA.manifest);
|
|
215
|
+
if (put.digest !== r.descriptor.digest)
|
|
216
|
+
throw new SitefileError('E_INTEGRITY', `the registry reports digest ${put.digest}, expected ${r.descriptor.digest}`);
|
|
217
|
+
say(`${shortId(r.descriptor.digest)}: attestation pushed (${r.descriptor.annotations?.[ATTEST.predicateAnnotation] ?? r.descriptor.artifactType ?? 'artifact'})`);
|
|
218
|
+
if (put.subject === undefined)
|
|
219
|
+
fallback.push(r.descriptor);
|
|
220
|
+
}
|
|
221
|
+
if (fallback.length) {
|
|
222
|
+
const tag = referrersTag(subject);
|
|
223
|
+
let listed = [];
|
|
224
|
+
try {
|
|
225
|
+
const cur = await client.getManifest(repo, tag);
|
|
226
|
+
const j = JSON.parse(Buffer.from(cur.bytes).toString('utf8'));
|
|
227
|
+
if (Array.isArray(j.manifests))
|
|
228
|
+
listed = j.manifests.filter((x) => typeof x === 'object' && x !== null && !Array.isArray(x));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
// No referrers tag yet (or not an index): start a new one.
|
|
232
|
+
if (!(isSitefileError(e) && e.code === 'E_NOT_FOUND') && !(e instanceof SyntaxError))
|
|
233
|
+
throw e;
|
|
234
|
+
}
|
|
235
|
+
const missing = fallback.filter((f) => !listed.some((l) => l.digest === f.digest));
|
|
236
|
+
if (missing.length === 0) {
|
|
237
|
+
say(`referrers tag ${tag}: up to date`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const manifests = [...listed, ...missing];
|
|
241
|
+
const bytes = new TextEncoder().encode(canonicalJson({ schemaVersion: 2, mediaType: MEDIA.index, manifests }));
|
|
242
|
+
await client.putManifest(repo, tag, bytes, MEDIA.index);
|
|
243
|
+
say(`referrers tag ${tag}: ${manifests.length} attestation${manifests.length === 1 ? '' : 's'}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return referrers.length;
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=registry-ops.js.map
|