/** * TODOLIST * * [ ] 根据操作系统的不同,获取到特定的 Docker 对象 * * [ ] 查询容器状态 * * [ ] 安装镜像容器 * * [ ] 在容器内操作命令, Docker API.exec */ import Dockerode from 'dockerode' import { chmod, createReadStream, createWriteStream, mkdirSync, readFileSync, rmdirSync, rmSync } from 'fs' import { resolve } from 'path' import { removeUnreadCharacter } from '.' import LogUtil from './log' import tar from 'tar-fs' class Docker { docker: Dockerode username: string env: string[] host: string constructor (host = 'host.docker.internal', username: string) { this.host = host this.docker = new Dockerode({ host, port: 2375 }) this.username = username this.env = [] } async checkContainer (name: string) { let targetContainerName = 'logwire_backend_helper.' + name let containers = await this.docker.listContainers({ filters: JSON.stringify({ status: ['created', 'restarting', 'running', 'removing', 'paused', 'exited', 'dead' ], name: [targetContainerName] }) }) if (containers.length) { let container = await this.docker.getContainer(containers[0].Id) return container } else { return } } async checkAndCreateContainer ({ name, img, env, portBindings, cmd, exposedPorts }: { name: string, img: string, env?: string[], portBindings?: Record<string, any>, cmd?: string[], exposedPorts?: Record<string, any> }) { let targetContainerName = 'logwire_backend_helper.' + name let containers = await this.docker.listContainers({ filters: JSON.stringify({ status: ['created', 'restarting', 'running', 'removing', 'paused', 'exited', 'dead' ], name: [targetContainerName] }) }) if (containers.length) { let container = await this.docker.getContainer(containers[0].Id) return container } else { let images = await this.docker.listImages({ filters: `{ "reference": ["${img}"] }` }) if (images.length === 0) { await new Promise<void>((resolve, reject) => { this.docker.pull(img, (err: any, stream: any) => { this.docker.modem.followProgress(stream, onFinished, onProgress); function onFinished(err: any, output: any) { if (!err) { resolve() } else { reject(err) } } function onProgress(event: any) { } }) }) } let opt = { name: targetContainerName, "Tty": true, Image: img, Env: env, Cmd: cmd, ExposedPorts: exposedPorts, HostConfig: { PortBindings: portBindings || {} } } return this.docker.createContainer(opt) } } async startContainer({ container }: { container: Dockerode.Container }) { let info = await container.inspect() if (info.State.Running) { } else { await container.start() } return container } async execContainerCommand ({ container, cmd, dir, quiet }: {container: Dockerode.Container, cmd: string | string[], dir?: string, quiet?: boolean }) { let exec = await container.exec({ Cmd: cmd instanceof Array ? cmd : cmd.split(' '), AttachStdout: true, AttachStderr: true, WorkingDir: dir, User: 'root', Env: this.env }) let result = await exec?.start({}) return new Promise<string>((resolve, reject) => { let logs = '' let data = '' result?.on('data', chunk => { let text = removeUnreadCharacter(chunk.toString()) !quiet && LogUtil.print(this.username, '[info] ' + text ) logs += text data += text logs = logs.substring(logs.length - 1200) }) result?.on('error', error => { logs += error.message + '\n' + error.stack }) result?.on('end', async () => { let info = await exec.inspect() if (info?.ExitCode) { reject({ command: cmd, message: 'ExitCode: ' + info.ExitCode, exitcode: info.ExitCode, logs }) } else { resolve(data) } }) }) } async putArchive ({ container, tarPath, targetPath }: { container: Dockerode.Container, tarPath: string, targetPath: string }) { return container.putArchive(tarPath, { path: targetPath }) } async writeFile ({ container, path, text }: { container: Dockerode.Container, path: string, text: string }) { await this.execContainerCommand({ container, cmd: ['bash', '-c', 'echo ' + text + ' > ' + path] }) } async appendFile({ container, path, text }: { container: Dockerode.Container, path: string, text: string }) { await this.execContainerCommand({ container, cmd: ['bash', '-c', 'echo ' + text + ' >> ' + path] }) } async getFile ({ container, path }: { container: Dockerode.Container, path: string }): Promise<string> { return new Promise(async (resolve, reject) => { try { let readstream = await container.getArchive({ path }) let tempFilePath = './temp_' + Math.random() + '.tar' let tempUnpackFolder = tempFilePath.replace('.tar', '') let tempUnpackFile = tempUnpackFolder + '/' + path.replace(/.*\//, '') mkdirSync(tempUnpackFolder) let writestream = createWriteStream(tempFilePath) readstream.pipe(writestream).on('finish', () => { createReadStream(tempFilePath).pipe(tar.extract(tempUnpackFolder)).on('finish', () => { let content = readFileSync(tempUnpackFile, { encoding: 'utf-8' }) rmSync(tempFilePath) rmSync(tempUnpackFolder, { recursive: true, force: true }) resolve(content) }) }) } catch (err) { reject(err) } }) } async switchJavaVersion ({ container, username, version }: { container: Dockerode.Container, username: string, version: 17 | 8}) { LogUtil.printInfo(username, '更换 Java 环境') await this.execContainerCommand({ container, cmd: 'rm /usr/bin/java'}) if (version === 8) { await this.execContainerCommand({ container, cmd: 'ln -sf /var/java-8-openjdk/bin/java /usr/bin/java' }) } else { await this.execContainerCommand({ container, cmd: 'ln -sf /var/java-17-openjdk/bin/java /usr/bin/java' }) } } async preStartSystem ({ container, username, platform }: { container: Dockerode.Container, username: string, platform: 'v1' | 'v2' }) { await this.execContainerCommand({ container, cmd: 'rm -f /root/.pm2/logs/backend-out.log'}) await this.execContainerCommand({ container, cmd: 'rm -f /root/.pm2/logs/backend-error.log'}) await this.execContainerCommand({ container, cmd: 'rm -f /root/.pm2/logs/gateway-out.log'}) await this.execContainerCommand({ container, cmd: 'rm -f /root/.pm2/logs/gateway-error.log'}) await this.execContainerCommand({ container, cmd: 'rm -f /root/.pm2/logs/platform-out.log'}) await this.execContainerCommand({ container, cmd: 'rm -f /root/.pm2/logs/platform-error.log'}) try { await this.execContainerCommand({ container, cmd: 'pm2 delete backend' }) } catch (err) {} try { await this.execContainerCommand({ container, cmd: 'pm2 delete gateway' }) } catch (err) {} try { await this.execContainerCommand({ container, cmd: 'pm2 delete platform' }) } catch (err) {} if (platform === 'v1') { await this.switchJavaVersion({ container, username, version: 8 }) const statusV2 = (await getPgTableData( 'v2', username))?.status if (statusV2 === 'launched' || statusV2 === 'debug-launched') { await changeProjectStatus( 'v2', username, 'status', 'compiled') // 如果系统在运行,回退到已创建的状态 } } else { await this.switchJavaVersion({ container, username, version: 17 }) const statusV1 = (await getPgTableData( 'v1', username))?.status if (statusV1 === 'launched') { await changeProjectStatus( 'v1', username, 'status', 'compiled') // 如果系统在运行,回退到已创建的状态 } } } } export function createDockerFactory (username: string) { const DOCKERHOST = useRuntimeConfig().public.dockerHost return new Docker(process.env.NODE_ENV === 'development' ? 'localhost': DOCKERHOST, username) }