1import fetch from 'node-fetch' 11type ManifestLayer = { digest: string, size: number } 12type Manifest = { layers?: ManifestLayer[], config?: { digest: string } } 14const getAuthHeaders = () => { 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)}`} 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} 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} 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} 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}`} 60const doesDockImgExist = async ({manifest_url}: {manifest_url: string}) => { 61 const result = await checkDockImgHealth({manifest_url, verifyBlobs: true}) 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}`) 69export const checkBlobHealth = async ({dockreg_host, digest}: {dockreg_host: string, digest: string}) => { 71 const blobResp = await fetch(`https://${dockreg_host}/v2/localrepo/localrepo/blobs/${digest}`, { 72 method: 'HEAD', headers: {Authorization: `Basic ${auth}`} 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, 81 let manifest_url: string, inspectTag: string 83 inspectTag = 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. 89 manifest_url = mkManifestUrl(dockregPoolA?.[0] ?? dockreg_host, repoPath, tag) 93 inspectTag = rez.repo_name_tag 94 manifest_url = rez.manifest_url 98 if (isSuccess) return true 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}`}