import * as step from '@flow-step/step-toolkit' import child_process, {execSync} from "child_process"; import {dirname, isAbsolute, join} from "path"; export function checkDockerInstallation(): boolean { // fix docker: command not found bug on darwin if (step.platform.PLATFORM_DARWIN === step.platform.getPlatform()) { step.addPath("/usr/local/bin") } try { const result = child_process.execSync('docker --version').toString(); step.info(`Docker client is installed: ${result}`) } catch (error) { step.error(`Docker client is not installed: ${error}`); return false; } try { const result = child_process.execSync('docker buildx version').toString(); step.debug(`docker buildx version: ${result}`) // docker-ce // linux github.com/docker/buildx v0.14.0 171fcbe // darwin github.com/docker/buildx v0.12.0-desktop.2 c5a13b51c1ae9358eb691e9a21c955590e26d0a0 if (result.includes('github.com/docker/buildx')) { return true } step.info(`docker buildx version: ${result}`) step.error("please install docker-ce version >= 19.03 to support docker buildx") return false; } catch (error) { step.error(`docker buildx version: ${error}`) return false; } } export async function waitDockerDaemonReady(maxAttempts = 100) { if (step.platform.PLATFORM_WIN === step.platform.getPlatform()) { throw new Error(`Docker build is currently not supported on Windows.`); } let attempt = 0; let ready = false; while (!ready && attempt < maxAttempts) { attempt++; try { let command = 'docker info > /dev/null 2>&1'; if (step.platform.PLATFORM_WIN === step.platform.getPlatform()) { command = 'docker info > $null 2>&1' } execSync(command); ready = true; } catch (error) { if (attempt < maxAttempts) { step.info('Docker daemon not ready, waiting...'); await sleep(200); } else { throw new Error(`Failed to connect to Docker daemon after ${maxAttempts} attempts with err: ${error}.`); } } } step.info('Docker daemon is ready'); } function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } export function createDockerBuildxBuilder() { try { let command = 'docker buildx inspect flow-build-container > /dev/null 2>&1'; if (step.platform.PLATFORM_WIN === step.platform.getPlatform()) { command = 'docker buildx inspect flow-build-container > $null 2>&1' } execSync(command); step.info(`Docker buildx builder is installed`); } catch (error) { step.info('Docker buildx builder not installed, installing...'); let image = 'build-steps-public-registry.cn-beijing.cr.aliyuncs.com/build-steps/docker@sha256:c3cb08891c15763d426ba017fa7ce957537a779837f5bf2f7b50de3796e13918' let arch: string = step.platform.getArch() if (arch === 'arm64') { image = "build-steps-public-registry.cn-beijing.cr.aliyuncs.com/build-steps/docker@sha256:957eb319e91a8f4da122e988fc54632c929c94085ec0590ff47c77ad29a8e33f" } // 先尝试从公网创建 buildx builder try { step.info(`Trying to create Docker buildx builder with public registry image: ${image}`); execSync(`docker buildx create --name flow-build-container --use --bootstrap --driver=docker-container --driver-opt=image=${image},network=host`); step.info('Docker buildx builder installed successfully with public registry'); } catch (publicError) { step.warning(`Failed to create buildx builder with public registry: ${publicError}`); // 公网失败后,尝试从OSS下载镜像 const ossAmd64Url = process.env.DOCKER_BUILDX_OSS_AMD64; const ossArm64Url = process.env.DOCKER_BUILDX_OSS_ARM64; const ossUrl = arch === 'arm64' ? ossArm64Url : ossAmd64Url; step.info(`Trying to download Docker buildx image from OSS: ${ossUrl}`); try { // 下载镜像文件 execSync(`curl -L -o /tmp/docker-buildx-image.tar "${ossUrl}"`); step.info('Docker buildx image downloaded successfully from OSS'); // 导入镜像 execSync('docker load -i /tmp/docker-buildx-image.tar'); step.info('Docker buildx image loaded successfully'); // 清理临时文件 execSync('rm -f /tmp/docker-buildx-image.tar'); // 强制删除已存在的builder(如果存在) try { execSync('docker buildx rm flow-build-container --force', { stdio: 'ignore' }); step.info('Removed existing Docker buildx builder'); } catch (removeError) { // 忽略删除错误,可能builder不存在 step.debug(`Remove builder error (ignored): ${removeError}`); } // 使用从OSS加载的镜像创建builder execSync(`docker buildx create --name flow-build-container --use --bootstrap --driver=docker-container --driver-opt=image=${image},network=host`); step.info('Docker buildx builder installed successfully with OSS image'); } catch (ossError) { step.error(`Failed to download and load image from OSS: ${ossError}`); throw new Error(`Failed to install Docker buildx builder. Public registry error: ${publicError}. OSS error: ${ossError}`); } } step.info('Docker buildx builder installed successfully'); } } export function formatContextPath(contextPath: string | undefined, dockerfilePath: string, projectDir: string): string { // contextPath 不为空,拼接 projectDir 与 contextPath if (!!contextPath) { return join(projectDir, contextPath); } // 以下为 contextPath 为空的情况 // dockerfilePath 是绝对路径 if (isAbsolute(dockerfilePath)) { return dirname(dockerfilePath); } // dockerfilePath 不包含路径 if (!dockerfilePath.includes('/')) { return projectDir; } // dockerfilePath 包含文件路径,且本身非绝对路径 return join(projectDir, dirname(dockerfilePath)); } export function formatDate(date: Date): string { const year = date.getFullYear().toString(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); const seconds = date.getSeconds().toString().padStart(2, '0'); return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; } export function getImageRepositoryFromImageId(imageId: string): string { const parts = imageId.split(':'); if (parts.length === 2) { return parts[0]; } return imageId; } export function getTagFromImageId(imageId: string): string { const parts = imageId.split(':'); switch (parts.length) { case 2: if (parts[1].includes('/')) { return ""; } return parts[1]; case 3: return parts[2]; default: return ""; } } export function parseImage(image: string) { const TIMESTAMP: number = Math.floor(Date.now() / 1000); const DATETIME: string = formatDate(new Date()); image = image.replace(/\$\{TIMESTAMP}/g, TIMESTAMP.toString()); return image.replace(/\$\{DATETIME}/g, DATETIME); } export function convertToVpcImage(image: string): string { const parts = image.split('/'); if (parts.length > 0) { const firstPart = parts[0]; if (firstPart.endsWith('aliyuncs.com')) { const domainParts = firstPart.split('.'); if (domainParts.length > 1 && !domainParts[0].includes('-vpc')) { domainParts[0] += '-vpc'; parts[0] = domainParts.join('.'); return parts.join('/'); } } } return image; } export function getDockerBuildArgs(image: string, targetDockerfilePath: string, contextPath: string, cacheType: string, cacheImageId: string) { let args = ['buildx', 'build', '--progress=plain', '-t', image, '-f', targetDockerfilePath, contextPath, "--push"]; if (cacheType === 'no-cache') { args.push('--no-cache') } else if (cacheType === 'remote') { // 本地缓存不需要传递参数,远程缓存需要追加 --cache-from --cache-to if (cacheImageId !== undefined && cacheImageId !== "") { cacheImageId = cacheImageId as string } else { const imageRepository = getImageRepositoryFromImageId(image) cacheImageId = imageRepository + ":flow-docker-build-cache" } args.push('--cache-from', cacheImageId) args.push('--cache-to', cacheImageId) } return args; }