import { Logger } from "winston";
import { Docker } from "docker-cli-js";
import { z } from "zod";
import { promises as fsPromises } from "fs";
import * as path from "path";
import * as os from "os";
import * as util from "util";
import { exec } from "child_process";
import { generateDockerFile, parseDockerArgs } from "./docker-helpers.js";
const execPromise = util.promisify(exec);
const CA_CERT_FILENAME = "mitm_proxy.crt";
export class DockerService {
private docker: Docker;
private logger: Logger;
private certPath: string;
constructor(certPath: string, logger: Logger) {
const echo = logger.level === "verbose" || logger.level === "silly";
this.docker = new Docker({ echo });
this.certPath = certPath;
this.logger = logger.child({ component: "DockerService" });
}
public async createImageWithCa(args: string[]): Promise<string[]> {
if (!this.certPath) {
return Promise.reject(
`CA certificate path is not set. Please provide a valid path.`,
);
}
await fsPromises.access(this.certPath).catch((accessError) => {
return Promise.reject(
`CA certificate at '${this.certPath}' is not accessible. Error: ${accessError}`,
);
});
const dockerArgs = parseDockerArgs(args);
if (!dockerArgs.image) {
return Promise.reject(`Failed to parse Docker command.`);
}
this.logger.debug(
`Starting to modify lunar.dev Intercepted Docker image from ${dockerArgs.originalImage}`,
);
const detectedOsRaw = await this.detectImageOs(dockerArgs.originalImage);
const detectedOs = z
.enum(["debian", "ubuntu", "alpine", "centos", "rhel", "fedora"])
.safeParse(detectedOsRaw || "");
if (!detectedOs.success) {
return Promise.reject(
`Failed to detect OS for image ${dockerArgs.originalImage}.`,
);
}
const tempDir = await fsPromises
.mkdtemp(path.join(os.tmpdir(), "docker-ca-build-"))
.catch((mkdtempError) => {
return Promise.reject(
`Failed to create temporary directory for Docker build: ${mkdtempError}`,
);
});
const dockerfilePath = path.join(tempDir, "Dockerfile");
const caCertTempPath = path.join(tempDir, CA_CERT_FILENAME);
await fsPromises
.copyFile(this.certPath, caCertTempPath)
.catch((copyError) => {
return Promise.reject(
`Failed to copy CA certificate to ${caCertTempPath}: ${copyError}`,
);
});
let originalUser = "root";
await this.docker
.command(`pull ${dockerArgs.originalImage}`)
.catch((pullError) => {
return Promise.reject(
`Failed to pull original image ${dockerArgs.originalImage}. Error: ${pullError}`,
);
});
const inspectOutput = await this.docker.command(
`inspect ${dockerArgs.originalImage}`,
);
const inspectData = JSON.parse(inspectOutput.raw);
if (
inspectData &&
inspectData.length > 0 &&
inspectData[0].Config &&
inspectData[0].Config.User
) {
if (inspectData[0].Config.User !== "") {
originalUser = inspectData[0].Config.User;
}
}
const dockerFileContent = generateDockerFile(detectedOs.data)({
originalImageName: dockerArgs.originalImage,
originalUser,
caCertFilename: CA_CERT_FILENAME,
});
await fsPromises
.writeFile(dockerfilePath, dockerFileContent, "utf8")
.catch((writeError) => {
return Promise.reject(
`Failed to write Dockerfile at ${dockerfilePath}. Error: ${writeError}`,
);
});
this.logger.debug(
`Building lunar.dev modified Docker image from ${dockerArgs.originalImage}`,
);
const buildCommand = `docker build -q -t ${dockerArgs.image} .`;
const { stderr } = await execPromise(buildCommand, { cwd: tempDir }).catch(
(error) => {
return Promise.reject(
`Failed to build Docker image ${dockerArgs.image}: ${error}`,
);
},
);
if (stderr) {
return Promise.reject(
`Failed to build Docker image ${dockerArgs.image}.`,
);
}
this.logger.debug(`Successfully built Docker image ${dockerArgs.image}.`);
fsPromises
.rm(tempDir, { recursive: true, force: true })
.catch((cleanupError) => {
this.logger.error(
`Error cleaning up temporary directory ${tempDir}:`,
cleanupError,
);
});
return dockerArgs.args;
}
private async detectImageOs(imageName: string): Promise<string> {
await this.docker.command(`pull ${imageName}`).catch((pullError) => {
return Promise.reject(
`Failed to pull image ${imageName}. Error: ${pullError}`,
);
});
const commandToRun = `run --rm --entrypoint "" ${imageName} sh -c 'grep -E "^ID=" /etc/os-release | cut -d= -f2'`;
const { raw } = await this.docker.command(commandToRun).catch((error) => {
return Promise.reject(
`Failed to run command to detect OS for image ${imageName}. Error: ${error}`,
);
});
return raw.trim();
}
}