273 lines
6.9 KiB
TypeScript
273 lines
6.9 KiB
TypeScript
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<Docker.ContainerInspectInfo> {
|
|
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<void> {
|
|
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<NodeJS.ReadableStream> {
|
|
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<boolean> {
|
|
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
|
|
} |