phantom-fleet
Version:
Phantom Fleet: Optimize Docker images with dynamic proxy management.
231 lines (214 loc) • 9.18 kB
text/typescript
import {createProxyMiddleware} from 'http-proxy-middleware';
import cors from "cors"
import express, {Express, NextFunction, Request, Response} from "express";
import Docker from "dockerode";
import Dockerode from "dockerode";
import {IPhantomFleet} from "./interface";
import portscanner from "portscanner"
class PhantomFleet implements IPhantomFleet {
docker: Dockerode
container: Docker.Container | null = null;
timer: NodeJS.Timeout | null = null;
app: Express
CONTAINER_NAME: string = ""
LOG_START_TEXT: string | number = ""
TIMEOUT_INACTIVE: number
IMAGE: string = ""
PORT: string | number = ""
HOST_PORT: string | number = ""
TARGET: string
APP_PORT: number
constructor(CONTAINER_NAME: string, LOG_START_TEXT: string, TIMEOUT_INACTIVE: number, IMAGE: string, PORT: string | number, HOST_PORT: string | number, TARGET: string, APP_PORT: number, ENV: string[]) {
this.CONTAINER_NAME = CONTAINER_NAME
this.LOG_START_TEXT = LOG_START_TEXT
this.TIMEOUT_INACTIVE = TIMEOUT_INACTIVE
this.IMAGE = IMAGE
this.PORT = PORT
this.TARGET = TARGET
this.APP_PORT = APP_PORT
this.HOST_PORT = HOST_PORT
this.docker = new Docker();
this.app = express();
this.app.use(cors())
}
public start = () => {
try {
this.app.use(async (req: Request, res: Response, next: NextFunction) => {
if (this.container === null) {
this.container = await this.docker.getContainer(this.CONTAINER_NAME);
await this.startContainer();
this.restartTimer()
} else {
this.container = await this.docker.getContainer(this.CONTAINER_NAME);
const containerInfo = await this.container.inspect();
console.log("++containerInfo.State.Paused", containerInfo.State.Paused)
}
next();
});
this.app.use(
'/',
createProxyMiddleware({
target: this.TARGET,
changeOrigin: true,
onError: async (err: any, req, res: any) => {
this.container = null
if (err.code === 'ECONNREFUSED') {
console.error('Connection refused, restarting container...');
await this.startContainer();
// @ts-ignore
createProxyMiddleware({
target: this.TARGET,
changeOrigin: true,
})(req, res)
this.restartTimer()
} else {
res.status(500).send('Proxy Error');
}
}
})
)
portscanner.checkPortStatus(this.APP_PORT, '127.0.0.1', (error, status) => {
if (status === "open") {
console.log(`The port ${this.APP_PORT} is busy!`)
} else {
this.app.listen(this.APP_PORT, () => {
console.log(`Proxy server listening on port ${this.APP_PORT}...`);
});
}
})
} catch (e) {
console.log(e)
}
}
public remove = () => {
this.container && this.container.stop((err, data) => {
if (err) {
console.error('Error stopping the container:', err);
} else {
this.container && this.container.remove({force: true}, function (err, data) {
if (err) {
if (err.statusCode === 409) {
console.error('Container is already in progress of removal');
} else {
console.error('Error removing the container:', err);
}
} else {
console.log('Container removed successfully');
}
});
}
})
}
public stop = () => {
this.stopContainer()
}
private stopContainer = async () => {
if (this.container) {
await this.container.pause();
this.container = null;
console.log(`Container ${this.CONTAINER_NAME} stopped.`);
}
}
private pullImage = async (imageName: string) => {
return new Promise((resolve, reject) => {
this.docker.pull(imageName, (err: any, stream: any) => {
if (err) reject(err);
this.docker.modem.followProgress(stream, (err: any, output: unknown) => {
if (err) reject(err);
else resolve(output);
});
});
});
};
private waitForLogMessage = async (container: any, message: any) => {
const logsStream = await container.attach({
stream: true,
stdout: true,
stderr: true,
timestamps: true,
});
return new Promise<void>((resolve, reject) => {
logsStream.on('data', (data: any) => {
const logMessage = data.toString();
if (logMessage.includes(message)) {
logsStream.destroy(); // Destroy the stream when the message is found
resolve();
}
});
logsStream.on('end', () => {
reject(new Error(`Message "${message}" not found in the logs.`));
});
logsStream.on('error', (err: any) => {
reject(err);
});
});
};
private createNewContainer = async () => {
let exposedPortsObj: Record<string, any> = {};
exposedPortsObj["PORT"] = {};
let hostConfigObj: Record<string, any> = {};
hostConfigObj[this.PORT] = [{HostPort: this.HOST_PORT}];
const newContainer = await this.docker.createContainer({
Image: this.IMAGE,
name: this.CONTAINER_NAME,
ExposedPorts: exposedPortsObj,
HostConfig: {
PortBindings: hostConfigObj
},
});
this.container = this.docker.getContainer(newContainer.id);
await this.container.start();
console.log(`Waiting for "${this.LOG_START_TEXT}" message in logs...`);
try {
if (this.LOG_START_TEXT) await this.waitForLogMessage(this.container, this.LOG_START_TEXT);
console.log(`"${this.LOG_START_TEXT}" message found in logs.`);
} catch (error: any) {
console.error(`Error waiting for "${this.LOG_START_TEXT}" message:`, error.message);
}
console.log(`Container ${this.CONTAINER_NAME} started.`);
}
private restartTimer = () => {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.stopContainer, this.TIMEOUT_INACTIVE);
};
private startContainer = async () => {
try {
const containers = await this.docker.listContainers({all: true});
const existingContainer = containers.find(
(containerInfo) => containerInfo.Names.includes(`/${this.CONTAINER_NAME}`)
)
if (existingContainer) {
this.container = await this.docker.getContainer(this.CONTAINER_NAME);
const containerInfo = await this.container.inspect();
console.log("++containerInfo.State.Paused", containerInfo.State.Paused)
if (!containerInfo.State.Running) {
await this.container.start();
await this.container.logs({
stdout: true,
stderr: true,
timestamps: true,
});
if (this.LOG_START_TEXT) await this.waitForLogMessage(this.container, this.LOG_START_TEXT);
console.log(`Container ${this.CONTAINER_NAME} run.`);
} else {
await this.container.unpause();
console.log(`Container ${this.CONTAINER_NAME} started.`);
}
} else {
const images = await this.docker.listImages();
const existingImage = images.find(
(imageInfo) => imageInfo.RepoTags && imageInfo.RepoTags.includes(this.IMAGE)
);
if (!existingImage) {
// Если образа нет локально, скачиваем его с Docker Hub
console.log(`Image ${this.IMAGE} not found locally. Pulling from Docker Hub...`);
await this.pullImage(this.IMAGE);
}
await this.createNewContainer()
}
} catch (err: any) {
console.error(`Error starting container ${this.CONTAINER_NAME}:`, err.message);
}
};
}
export default PhantomFleet