EvalueTonSavoir/server/roomsProviders/docker-provider.js
MathieuSevignyLavallee fff5830afd Add README, implementation de l'api Docker sur le quizroom, Fonctionnel
remove docker group

graph generator remake

Docker API implementation
2024-12-07 22:01:55 -05:00

275 lines
9.6 KiB
JavaScript

const Docker = require("dockerode");
const { Room } = require("../models/room.js");
const BaseRoomProvider = require("./base-provider.js");
class DockerRoomProvider extends BaseRoomProvider {
constructor(config, roomRepository) {
super(config, roomRepository);
const dockerSocket = process.env.DOCKER_SOCKET || "/var/run/docker.sock";
this.docker = new Docker({ socketPath: dockerSocket });
this.docker_network = process.env.QUIZ_NETWORK_NAME || 'evaluetonsavoir_quiz_network';
}
async syncInstantiatedRooms() {
let containers = await this.docker.listContainers();
containers = containers.filter(container => container.Image === this.quiz_docker_image);
const containerIds = new Set(containers.map(container => container.Id));
for (let container of containers) {
const container_name = container.Names[0].slice(1);
if (!container_name.startsWith("room_")) {
console.warn(`Le conteneur ${container_name} ne suit pas la convention de nommage, il sera supprimé.`);
const curContainer = this.docker.getContainer(container.Id);
await curContainer.stop();
await curContainer.remove();
containerIds.delete(container.Id);
console.warn(`Le conteneur ${container_name} a été supprimé.`);
}
else {
console.warn(`Conteneur orphelin trouvé : ${container_name}`);
const roomId = container_name.slice(5);
const room = await this.roomRepository.get(roomId);
if (!room) {
console.warn(`Le conteneur n'est pas dans notre base de données.`);
const containerInfo = await this.docker.getContainer(container.Id).inspect();
const containerIP = containerInfo.NetworkSettings.Networks.evaluetonsavoir_quiz_network.IPAddress;
const host = `${containerIP}:4500`;
console.warn(`Création de la salle ${roomId} dans notre base de donnée - hôte : ${host}`);
return await this.roomRepository.create(new Room(roomId, container_name, host));
}
console.warn(`La salle ${roomId} est déjà dans notre base de données.`);
}
}
}
async checkAndPullImage(imageName) {
try {
const images = await this.docker.listImages({ all: true });
//console.log('Images disponibles:', images.map(img => ({
// RepoTags: img.RepoTags || [],
// Id: img.Id
//})));
const imageExists = images.some(img => {
const tags = img.RepoTags || [];
return tags.includes(imageName) ||
tags.includes(`${imageName}:latest`) ||
img.Id.includes(imageName);
});
if (!imageExists) {
console.log(`L'image ${imageName} n'a pas été trouvée localement, tentative de téléchargement...`);
try {
await this.docker.pull(imageName);
console.log(`L'image ${imageName} a été téléchargée avec succès`);
} catch (pullError) {
const localImages = await this.docker.listImages({ all: true });
const foundLocally = localImages.some(img =>
(img.RepoTags || []).includes(imageName) ||
(img.RepoTags || []).includes(`${imageName}:latest`)
);
if (!foundLocally) {
throw new Error(`Impossible de trouver ou de télécharger l'image ${imageName}: ${pullError.message}`);
} else {
console.log(`L'image ${imageName} a été trouvée localement après vérification supplémentaire`);
}
}
} else {
console.log(`L'image ${imageName} a été trouvée localement`);
}
} catch (error) {
throw new Error(`Une erreur est survenue lors de la vérification/téléchargement de l'image ${imageName}: ${error.message}`);
}
}
async createRoom(roomId, options) {
const container_name = `room_${roomId}`;
try {
await this.checkAndPullImage(this.quiz_docker_image);
const containerConfig = {
Image: this.quiz_docker_image,
name: container_name,
HostConfig: {
NetworkMode: this.docker_network,
RestartPolicy: {
Name: 'unless-stopped'
},
Binds: [
'/var/run/docker.sock:/var/run/docker.sock'
]
},
Env: [
`ROOM_ID=${roomId}`,
`PORT=${this.quiz_docker_port}`,
...(options.env || [])
]
};
if (this.quiz_expose_port) {
containerConfig.ExposedPorts = {
[`${this.quiz_docker_port}/tcp`]: {}
};
containerConfig.HostConfig.PortBindings = {
[`${this.quiz_docker_port}/tcp`]: [{ HostPort: '' }] // Empty string for random port
};
}
const container = await this.docker.createContainer(containerConfig);
await container.start();
const containerInfo = await container.inspect();
const networkInfo = containerInfo.NetworkSettings.Networks[this.docker_network];
if (!networkInfo) {
throw new Error(`Le conteneur n'as pu se connecter au réseau: ${this.docker_network}`);
}
const containerIP = networkInfo.IPAddress;
const host = `http://${containerIP}:${this.quiz_docker_port}`;
let health = false;
let attempts = 0;
const maxAttempts = 15;
while (!health && attempts < maxAttempts) {
try {
const response = await fetch(`${host}/health`, {
timeout: 1000
});
if (response.ok) {
health = true;
console.log(`Le conteneur ${container_name} est tombé actif en ${attempts + 1} tentatives`);
} else {
throw new Error(`Health check failed with status ${response.status}`);
}
} catch (error) {
attempts++;
console.log(`Attente du conteneur: ${container_name} (tentative ${attempts}/${maxAttempts})`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (!health) {
console.error(`Container ${container_name} failed health check after ${maxAttempts} attempts`);
await container.stop();
await container.remove();
throw new Error(`Room ${roomId} did not respond within acceptable timeout`);
}
return await this.roomRepository.create(new Room(roomId, container_name, host, 0));
} catch (error) {
console.error(`Échec de la création de la salle ${roomId}:`, error);
throw error;
}
}
async deleteRoom(roomId) {
const container_name = `room_${roomId}`;
await this.roomRepository.delete(roomId);
try {
const container = this.docker.getContainer(container_name);
const containerInfo = await container.inspect();
if (containerInfo) {
await container.stop();
await container.remove();
console.log(`Le conteneur pour la salle ${roomId} a été arrêté et supprimé.`);
}
} catch (error) {
if (error.statusCode === 404) {
console.warn(`Le conteneur pour la salle ${roomId} n'as pas été trouvé, la salle sera supprimée de la base de données.`);
} else {
console.error(`Erreur pour la salle ${roomId}:`, error);
throw new Error("La salle :${roomId} n'as pas pu être supprimée.");
}
}
console.log(`La salle ${roomId} a été supprimée.`);
}
async getRoomStatus(roomId) {
const room = await this.roomRepository.get(roomId);
if (!room) return null;
try {
const container = this.docker.getContainer(room.containerId || `room_${roomId}`);
const info = await container.inspect();
const updatedRoomInfo = {
...room,
status: info.State.Running ? "running" : "terminated",
containerStatus: {
Running: info.State.Running,
StartedAt: info.State.StartedAt,
FinishedAt: info.State.FinishedAt,
},
lastUpdate: Date.now(),
};
await this.roomRepository.update(updatedRoomInfo);
return updatedRoomInfo;
} catch (error) {
if (error.statusCode === 404) {
console.warn(`Le conteneur pour la salle ${roomId} n'as pas été trouvé, il sera mis en état "terminé".`);
const terminatedRoomInfo = {
...room,
status: "terminated",
containerStatus: {
Running: false,
StartedAt: room.containerStatus?.StartedAt || null,
FinishedAt: Date.now(),
},
lastUpdate: Date.now(),
};
await this.roomRepository.update(terminatedRoomInfo);
return terminatedRoomInfo;
} else {
console.error(`Une érreur s'est produite lors de l'obtention de l'état de la salle ${roomId}:`, error);
return null;
}
}
}
async listRooms() {
const rooms = await this.roomRepository.getAll();
return rooms;
}
async cleanup() {
const rooms = await this.roomRepository.getAll();
for (let room of rooms) {
if (room.mustBeCleaned) {
try {
await this.deleteRoom(room.id);
} catch (error) {
console.error(`Érreur lors du néttoyage de la salle ${room.id}:`, error);
}
}
}
let containers = await this.docker.listContainers();
containers = containers.filter(container => container.Image === this.quiz_docker_image);
const roomIds = rooms.map(room => room.id);
for (let container of containers) {
if (!roomIds.includes(container.Names[0].slice(6))) {
const curContainer = this.docker.getContainer(container.Id);
await curContainer.stop();
await curContainer.remove();
console.warn(`Conteneur orphelin ${container.Names[0]} supprimé.`);
}
}
}
}
module.exports = DockerRoomProvider;