🌳
pt0/deployF/dockerF/dockRegApi.mts
1import fetch from 'node-fetch'
2import { getCreds } from './dockRegF.mts'
11type ManifestLayer = { digest: string, size: number }
12type Manifest = { layers?: ManifestLayer[], config?: { digest: string } }
14const getAuthHeaders = () => {
15 const {dockreg_host} = getKlusterCtx()
16 const {auth} = getCreds({dockreg_host})
17 return {Authorization: `Basic ${auth}`, Accept: 'application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json'}
20export const checkDockImgHealth = async ({manifest_url, verifyBlobs = false}: {manifest_url: string, verifyBlobs?: boolean}) => {
21 const headers = getAuthHeaders()
22 const resp = await fetch(manifest_url, {headers})
23 if (resp.status !== 200) {
24 if (resp.status === 404) return {exists: false, reason: 'not found'}
25 const body = await resp.text().catch(() => '')
26 return {exists: false, reason: `status ${resp.status} ${body.slice(0, 200)}`}
27 }
29 try {
30 const manifest = await resp.json() as Manifest
31 const layers = manifest.layers || []
32 const zeroSizeLayers = layers.filter((l) => l.size === 0)
33 if (zeroSizeLayers.length > 0) return {exists: false, reason: `${zeroSizeLayers.length} layers have size=0`, manifest}
35 const repoPath = manifest_url.replace(/\/manifests\/.*$/, '')
36 const checkBlob = async (digest: string) => {
37 const blobResp = await fetch(`${repoPath}/blobs/${digest}`, {method: 'HEAD', headers})
38 if (blobResp.status !== 200) return {ok: false, reason: `blob missing: ${blobResp.status}`, digest}
39 if (blobResp.headers.get('content-length') === '0') return {ok: false, reason: 'blob content-length=0', digest}
40 return {ok: true}
41 }
43 if (manifest.config?.digest) {
44 const configCheck = await checkBlob(manifest.config.digest)
45 if (!configCheck.ok) return {exists: false, reason: `config ${configCheck.reason}`, digest: configCheck.digest}
46 }
48 if (verifyBlobs) {
49 const results = await batchPromCalls(layers, (l) => checkBlob(l.digest), 10, (r) => !r.ok)
50 const failed = results.find((r) => !r.ok)
51 if (failed) return {exists: false, reason: `layer ${failed.reason}`, digest: failed.digest}
52 }
54 return {exists: true, reason: 'ok', layerCount: layers.length}
55 } catch (e: unknown) {
56 return {exists: false, reason: `manifest parse error: ${(e as Error).message}`}
57 }
60const doesDockImgExist = async ({manifest_url}: {manifest_url: string}) => {
61 const result = await checkDockImgHealth({manifest_url, verifyBlobs: true})
62 if (!result.exists) {
63 if (result.reason === 'not found') console.log(`image not found (will build) url=${manifest_url}`)
64 else console.log(`image check failed: ${result.reason}${result.digest ? ` digest=${result.digest}` : ''} url=${manifest_url}`)
65 }
66 return result.exists
69export const checkBlobHealth = async ({dockreg_host, digest}: {dockreg_host: string, digest: string}) => {
70 const {auth} = getCreds({dockreg_host})
71 const blobResp = await fetch(`https://${dockreg_host}/v2/localrepo/localrepo/blobs/${digest}`, {
72 method: 'HEAD', headers: {Authorization: `Basic ${auth}`}
73 })
74 const contentLength = blobResp.headers.get('content-length')
75 return {status: blobResp.status, contentLength, exists: blobResp.status === 200 && contentLength !== '0'}
78export const doesDockImg2Exist = async ({git_sha, dockerfileContent, reqDockPush, includeShaInTag, repo_name_tag}: {
79 git_sha?: string, dockerfileContent?: string, reqDockPush?: boolean, includeShaInTag?: boolean, repo_name_tag?: string,
80}) => {
81 let manifest_url: string, inspectTag: string
82 if (repo_name_tag) {
83 inspectTag = repo_name_tag
84 const {repoPath, tag} = parseRepoNameTag(repo_name_tag)
85 // check the registry the image is pushed to (registryPool[0]), not getDockerPushHost — those
86 // diverge when dockLanHost != dockreg_host (e.g. harv2demo), which made the cache check query the
87 // wrong host and never recognize a just-pushed image.
88 const {dockreg_host, dockregPoolA} = getKlusterCtx()
89 manifest_url = mkManifestUrl(dockregPoolA?.[0] ?? dockreg_host, repoPath, tag)
90 } else {
91 assertDefined(dockerfileContent)
92 const rez = await get2RepoNameTag({git_sha, dockerfileContent, includeShaInTag})
93 inspectTag = rez.repo_name_tag
94 manifest_url = rez.manifest_url
95 }
96 if (!reqDockPush) {
97 const {isSuccess} = await do2ExecFile({cmdA: [getDockName(), 'inspect', inspectTag]})
98 if (isSuccess) return true
99 }
100 return await doesDockImgExist({manifest_url})
103// best-effort delete of an image by tag: HEAD to resolve digest, then DELETE by digest.
104// registries must have REGISTRY_STORAGE_DELETE_ENABLED (this repo's dockregs do).
105export const deleteDockImgByTag = async ({manifest_url}: {manifest_url: string}) => {
106 const headers = getAuthHeaders()
107 const head = await fetch(manifest_url, {method: 'HEAD', headers})
108 if (head.status !== 200) return {deleted: false, reason: `head ${head.status}`}
109 const digest = head.headers.get('Docker-Content-Digest')
110 if (!digest) return {deleted: false, reason: 'no digest'}
111 const delUrl = manifest_url.replace(/\/manifests\/[^/]+$/, `/manifests/${digest}`)
112 const del = await fetch(delUrl, {method: 'DELETE', headers})
113 return {deleted: del.status === 202, reason: `delete ${del.status}`}