import Docker from "dockerode"; import { writeFileSync } from "fs"; import * as process from "process"; import rimraf from "rimraf"; import * as tmp from 'tmp'; import getPort, { portNumbers } from 'get-port'; import { INTERNAL_IP } from "../config.js"; const docker = new Docker(); export async function createContainer(imageName, containerName) { const port = await getPort({ host: INTERNAL_IP, port: portNumbers(52300, 52399) }); if (await isContainerNameInUse(containerName)) { await stopAndRemoveContainerByName(containerName); } const containerOptions = { Image: imageName, name: containerName, WorkingDir: '/app', HostConfig: { AutoRemove: false, RestartPolicy: { Name: "always" }, PortBindings: { '3000/tcp': [{ HostPort: '' + port }] } }, }; try { const container = await docker.createContainer(containerOptions); await container.start(); console.log("Started", containerName, "with port", port, '> 3000'); await checkContainerStatus(container.id); return { id: container.id, port }; } catch (err) { console.error("Error creating container:", err); throw err; } } export async function listContainers() { return (await docker.listContainers()) .map(v => v.Names[0]) .filter((full) => { return full.startsWith('deploy-'); }); } export async function getContainerInfo(containerId) { const docker = new Docker(); const container = docker.getContainer(containerId); const containerInfo = await container.inspect(); return containerInfo; } export async function checkImageExists(imageName) { const images = await docker.listImages(); const imageExists = images.some(image => { if (image.RepoTags) { return image.RepoTags.includes(imageName); } else { return false; } }); return imageExists; } export async function pullImage(imageName) { const imageExists = await checkImageExists(imageName); if (!imageExists) { console.log(`Pulling image ${imageName}`); return new Promise((resolve, reject) => { docker.pull(imageName, (err, stream) => { if (err) { reject(err); } docker.modem.followProgress(stream, (err, output) => { if (err) { reject(err); } console.log(`Image ${imageName} has been pulled`); resolve(); }); }); }); } else { console.log(`Image ${imageName} already exists`); return Promise.resolve(); } } export async function attachLogs(containerId) { const container = docker.getContainer(containerId); const logsStream = await container.logs({ follow: true, stdout: true, stderr: true, }); return logsStream; } export async function isContainerNameInUse(name) { const containers = await docker.listContainers({ all: true }); return containers.some((container) => container.Names.includes(`/${name}`)); } async function stopAndRemoveContainerByName(name) { const containers = await docker.listContainers({ all: true }); const matchingContainers = containers.filter(container => container.Names.includes(`/${name}`)); if (matchingContainers.length === 0) { throw new Error(`No container with name '${name}' was found`); } const stoppedContainer = docker.getContainer(matchingContainers[0].Id); try { await stoppedContainer.stop(); } catch (error) { console.error(`Failed to stop container ${name}: ${error.message}`); } try { await stoppedContainer.remove(); console.log(`Container ${name} removed`); } catch (error) { console.error(`Failed to remove container ${name}: ${error.message}`); } } async function checkContainerStatus(containerId) { const container = docker.getContainer(containerId); const containerInfo = await container.inspect(); const status = containerInfo.State.Status; console.log(`Container status: ${status}`); return status; } export async function runCommandInContainer(containerId, command) { console.log('$', ...command); const container = await docker.getContainer(containerId); const exec = await container.exec({ Cmd: command, AttachStdout: true, AttachStderr: true, }); const stream = await exec.start({}); await new Promise((resolve, reject) => { container.modem.demuxStream(stream, process.stdout, process.stderr); stream.on('end', resolve); stream.on('error', reject); }); } export async function restartContainer(containerId) { const docker = new Docker(); const container = await docker.getContainer(containerId); console.log(`Restarting container ${containerId}...`); await container.restart(); await checkContainerStatus(containerId); } export async function stopContainer(id) { const docker = new Docker(); const container = await docker.getContainer(id); await container.stop(); await checkContainerStatus(id); } export async function startContainer(id) { const docker = new Docker(); const container = await docker.getContainer(id); await container.start(); await checkContainerStatus(id); } function createDockerfile(cloneUrl, branch) { return ` FROM node:lts-alpine3.17 RUN apk add git WORKDIR /app RUN git clone "${cloneUrl}" /app --depth 1 -b ${branch} RUN yarn && yarn build EXPOSE 3000 CMD yarn start`.trim(); } export async function buildDockerImage(options) { const { name: context } = tmp.dirSync(); const { user, name, branch, cloneUrl } = options; const imageName = `${user}/${name}:${branch}`; let id = null; try { writeFileSync(context + '/Dockerfile', createDockerfile(cloneUrl, branch)); const stream = await docker.buildImage({ src: ['Dockerfile'], context, }, { //@ts-ignore nocache: true, t: imageName }); //@ts-ignore stream.on('data', d => { const data = JSON.parse(d); const text = data.stream ?? ''; process.stdout.write(text); if (text.startsWith("Successfully built")) { const potentialId = text.split(" ")[2]; id = potentialId; } }); await new Promise((resolve) => { //@ts-ignore docker.modem.followProgress(stream, () => { resolve(void 0); }); }); if (id === null) throw new Error("Unable to generate image " + imageName); console.log("Built Image", imageName); } catch (e) { console.log('do we get here?'); await rimraf(context); throw e; } await rimraf(context); return imageName; }