import Docker, { ContainerInfo } 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: string, containerName: string): Promise<{ id: string, port: number }> { const port = await getPort({ host: INTERNAL_IP, port: portNumbers(52300, 52399)}); if(await isContainerNameInUse(containerName)) { await stopAndRemoveContainerByName(containerName); } const containerOptions: Docker.ContainerCreateOptions = { 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()) .filter((container) => { return container.Names[0].startsWith('/deploy-'); }); } export async function getContainerInfo(containerId: string): Promise { const docker = new Docker(); const container = docker.getContainer(containerId); const containerInfo = await container.inspect(); return containerInfo; } export async function checkImageExists(imageName: string) { 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: string): Promise { 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: Error | null, output: unknown[]) => { 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: string): Promise { const container = docker.getContainer(containerId); const logsStream = await container.logs({ follow: true, stdout: true, stderr: true, }); return logsStream; } export async function isContainerNameInUse(name: string): Promise { const containers = await docker.listContainers({ all: true }); return containers.some((container: ContainerInfo) => 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: string, command: string[]) { 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: string) { 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: string) { const docker = new Docker(); const container = await docker.getContainer(id); await container.stop(); await checkContainerStatus(id); } export async function startContainer(id: string) { const docker = new Docker(); const container = await docker.getContainer(id); await container.start(); await checkContainerStatus(id); } interface BuildDockerImageOptions { cloneUrl: string; user: string; name: string; branch: string; } function createDockerfile(cloneUrl: string, branch: string) { 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: BuildDockerImageOptions) { 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 }