1import * as _ from 'lodash-es' 32const dockerBuildTimeoutMs = 15 * 60 * 1000 33const dockerPushTimeoutDefaultMs = 45 * 60 * 1000 35const restartDockerDesktop = async () => { 36 if (process.platform !== 'darwin') return false 37 console.log('Restarting Docker Desktop to fix QEMU emulation...') 38 await liveSpawn({cmd: `osascript -e 'tell application "Docker Desktop" to quit'`, isQuiet: true}) 40 await liveSpawn({cmd: `open -a "Docker Desktop"`, isQuiet: true}) 41 for (let i = 0; i < 90; i++) { 42 const {isSuccess} = await liveSpawn({cmd: 'docker info', isQuiet: true}) 44 console.log(`Docker Desktop ready after ${i}s`) 49 console.log('Docker Desktop failed to restart in time') 53export const dockerBuildPush = async ({reqDockPush, ...props}: { 54 reqDockPush?: boolean, cluster_name?: string, dockreg_host?: string, dockLanHost?: string, 55 repo_name_tag: string, dockerfileContent: string, tarSpecsA?: object[], 56 git_sha?: string, git_repo_name?: string, action?: string, name?: string, dfLabel?: string, includeShaInTag?: boolean, 57 gitsshHost?: string, gitsshRepoPath?: string, 59 let {cluster_name, dockreg_host, dockLanHost, repo_name_tag, dockerfileContent, tarSpecsA, git_sha, action, name, dfLabel, includeShaInTag} = props 61 if (action == 'builddf') { 66 const builtDockerImageExists = await doesDockImg2Exist({git_sha, dockerfileContent, reqDockPush, includeShaInTag, repo_name_tag}) 67 if (builtDockerImageExists) return {imageReused: true} 73 const registryPool = dockregPoolA?.length ? dockregPoolA : [await getDockerPushHost({dockreg_host, dockLanHost, builderClusterName: isKanikojob() ? cluster_name : undefined})] 75 const rewriteRepoNameTag = (tag: string, newRegistry: string) => { 76 const parts = tag.split('/') 77 if (parts.length >= 2) { 78 parts[0] = newRegistry 79 return parts.join('/') 84 const tryBuildPush = async (registry: string) => { 85 const tag = rewriteRepoNameTag(repo_name_tag, registry) 87 await doLocalDockerBuild({dockreg_host: registry, dockLanHost, repo_name_tag: tag, dockerfileContent, tarSpecsA, action, name, dfLabel, reqDockPush}) 89 await doKanikoBuild({dockreg_host: registry, dockLanHost, repo_name_tag: tag, dockerfileContent, tarSpecsA, git_sha, action, name, dfLabel, cluster_name: cluster_name!}) 93 let diskFullRecovered = false 94 for (const currentRegistry of registryPool) { 96 await tryBuildPush(currentRegistry) 101 if (!diskFullRecovered) { 103 if (regcacheEnabled && regcacheHost) { 104 console.log(`\n>>> Registry ${currentRegistry} disk full — sweeping stale manifests + restarting regcache to reclaim blobs...`) 105 diskFullRecovered = true 108 try { await tryBuildPush(currentRegistry); return } 118 console.log(`\n>>> Registry ${currentRegistry} not found, creating...`) 120 const created = await createRegistry({dockreg_host: currentRegistry, cluster_name: cluster_name!}) 122 try { await tryBuildPush(currentRegistry); return } 125 console.log(`\n>>> Registry ${currentRegistry} creation failed, trying next...`) 133 throwDebugH({reason: 'all registries exhausted', dockregPoolA: registryPool}) 136type LocalDockerBuildProps = { 139 repo_name_tag: string 140 dockerfileContent: string 145 reqDockPush?: boolean 148const doLocalDockerBuild = async ({dockreg_host, dockLanHost, repo_name_tag, dockerfileContent, tarSpecsA, action, name, dfLabel, reqDockPush}: LocalDockerBuildProps) => { 150 if (action == 'delete') return 157 const dockConfigPath = [envHome, '.docker/config.json'].join('/') as absFileDirPath 158 const dockerConfigJson = JSON.parse(await read1File(dockConfigPath)) 160 _.set(dockerConfigJson, ['auths', dockreg_host], authH) 161 if (dockLanHost) _.set(dockerConfigJson, ['auths', dockLanHost], authH) 162 dockerConfigJson.credsStore = '' 164 await write1File(dockConfigPath, JSON.stringify(dockerConfigJson, null, 2)) 166 // minimal context = Dockerfile + the baked tmpdockertars/*.tar.gz it ADDs (mirrors kaniko's 167 // localctx); fall back to full ptDir only for standalone/raw-COPY Dockerfiles with no tarSpecs. 168 const isStandalone = !tarSpecsA?.length && !/^\s*(COPY|ADD)\b/im.test(dockerfileContent) 169 const ctxTar = (tarSpecsA?.length && !isStandalone) 173 const tryBuild = async () => { 175 console.log(`${buildLabel} build`) 176 const progressFlag = getDockBuildCaps(dockName).supportsProgress ? ' --progress plain' : '' 177 let cmd = `${getDockBuildPrefix(dockName)}${progressFlag} --platform=linux/amd64 -t ${repo_name_tag}` 179 cmd += ` - < ${ctxTar}` 185 const ret = await liveSpawn({cmd, noOutCmd: true, timeoutAfterSec: Math.round(dockerBuildTimeoutMs/1000)}) 186 if (!reqDockPush) return ret 187 const {isSuccess} = ret 189 console.log(`${buildLabel} push`) 190 const pushTimeoutMs = (getAppCfg() as Record<string, unknown>)?.dockerPushTimeoutMs as number || dockerPushTimeoutDefaultMs 191 await liveSpawnThrow({cmd: `${dockName} push ${repo_name_tag}`, noOutCmd: true, timeoutAfterSec: Math.round(pushTimeoutMs/1000)}) 195 let res = await tryBuild() 196 let {stderr, isSuccess, stdout} = res 197 if (isSuccess) return 198 const combinedOutput = stdout + stderr 199 const isExecFormatErr = combinedOutput.includes('exec /bin/sh: exec format error') 200 const isSegfault = res.exitCode === 139 201 const isLocalDiskFull = stderr.includes('no space left on device') || stdout.includes('no space left on device') || stdout.includes(`You don't have enough free space in`) 203 // Check if this is a registry disk full (during push) vs local disk full (during build) 204 // Registry disk full errors typically mention the registry host or have specific patterns 205 const isRegistryFull = isLocalDiskFull && (combinedOutput.includes('Err:28') || combinedOutput.includes('blobs/uploads')) 206 if (isRegistryFull) { 207 throw new Error(`Registry disk full: ${combinedOutput.slice(-500)}`) 211 stderr.includes('At least one invalid signature was encountered.') || 216 if (isSegfault) console.log('Segmentation fault detected, retrying...') 217 else if (isExecFormatErr) console.log('Detected architecture mismatch, pruning build cache and retrying...') 218 await liveSpawnThrow({cmd: `${dockName} system prune --force && ${dockName} builder prune --force && ${dockName} image prune -a -f`}) 219 ;({isSuccess} = await tryBuild()) 220 if (!isSuccess && isExecFormatErr) { 221 const restarted = await restartDockerDesktop() 223 ;({isSuccess} = await tryBuild()) 229 throw 'DockerBuildFailed' 233type KanikoBuildProps = { 236 repo_name_tag: string 237 dockerfileContent: string 246const doKanikoBuild = async ({dockreg_host, dockLanHost, repo_name_tag, dockerfileContent, tarSpecsA, git_sha, action, name, dfLabel, cluster_name}: KanikoBuildProps) => { 247 const kanikoLocalContext = getAppCfg()?.kanikoLocalContext ?? true 248 if (!kanikoLocalContext) { 249 const {gitsshHost: gitsshHostCfg, git_repo_name: gitRepoNameCfg} = getAppCfg() 250 throwIf(() => !(gitsshHostCfg || process.env.GITSSH_HOST || gitRepoNameCfg), {gitsshHostCfg, gitRepoNameCfg}) 257 await kubeJobKaniko({name, dockerfileContent, tarSpecsA: tarSpecsA as any, repo_name_tag, dfLabel: dfLabel || 'dfbuild', cluster_name, dockreg_host, dockLanHost, action, git_sha})