UNPKG

@twg-group/container-manager

Version:

Container management for Docker, Swarm, Kubernetes

288 lines 12 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SwarmStrategy = void 0; const dockerode_1 = __importDefault(require("dockerode")); const common_1 = require("@nestjs/common"); const _dto_1 = require("../dto/index"); const base_strategy_1 = require("./base.strategy"); const class_transformer_1 = require("class-transformer"); const nestjs_logger_1 = require("@twg-group/nestjs-logger"); let SwarmStrategy = class SwarmStrategy extends base_strategy_1.BaseStrategy { logger; docker; constructor(logger) { super(logger); this.logger = logger; this.docker = new dockerode_1.default({ socketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock', }); } async start(id) { try { const service = this.docker.getService(id); const serviceInfo = (await service.inspect()); const originalRestartPolicy = serviceInfo.Spec?.TaskTemplate?.RestartPolicy; await service.update({ version: serviceInfo?.Version?.Index, Mode: { Replicated: { Replicas: serviceInfo.Spec?.Mode?.Replicated?.Replicas === 0 ? 1 : (serviceInfo.Spec?.Mode?.Replicated?.Replicas ?? 1), }, }, TaskTemplate: { ForceUpdate: 1, RestartPolicy: originalRestartPolicy ? { ...originalRestartPolicy } : { Condition: 'any' }, }, }); } catch (error) { this.handleError(error, `Failed to start service ${id}`); } } async stop(id, timeout = 10) { try { const service = this.docker.getService(id); const serviceInfo = (await service.inspect()); await service.update({ version: serviceInfo?.Version?.Index, Mode: { Replicated: { Replicas: 0, }, }, TaskTemplate: { ForceUpdate: 1, RestartPolicy: { Condition: serviceInfo.Spec?.TaskTemplate?.RestartPolicy?.Condition ?? 'none', }, }, }); await new Promise((resolve) => setTimeout(resolve, timeout * 1000)); } catch (error) { this.handleError(error, `Failed to stop service ${id}`); } } createPortConfigs(ports) { if (!ports?.length) { return []; } return ports .filter((port) => port.containerPort) .map((port) => ({ Protocol: port.protocol?.toLowerCase() || 'tcp', TargetPort: Number(port.containerPort), PublishedPort: port.hostPort ? Number(port.hostPort) : undefined, PublishMode: 'ingress', })) .filter((config) => !isNaN(config.TargetPort)); } createMounts(volumes) { if (!volumes || volumes.length === 0) return []; return volumes.map((volume) => ({ Type: 'bind', Source: volume.hostPath, Target: volume.containerPath, ReadOnly: volume.mode === 'ro', })); } formatEnvironment(env) { return env ? Object.entries(env).map(([k, v]) => `${k}=${v}`) : []; } async deploy(config) { this.validateConfig(config); const serviceName = config.name || this.generateName('service'); const portConfigs = this.createPortConfigs(config.ports); const mounts = this.createMounts(config.volumes); try { const serviceSpec = { Name: serviceName, TaskTemplate: { ContainerSpec: { Image: config.image, Env: this.formatEnvironment(config.env), Mounts: mounts, }, RestartPolicy: { Condition: config.restartPolicy ? 'any' : 'none', }, Networks: config.network ? [{ Target: config.network }] : [], }, Mode: { Replicated: { Replicas: config.replicas || 1, }, }, EndpointSpec: portConfigs.length > 0 ? { Ports: portConfigs, } : undefined, Labels: config.labels || {}, }; const service = await this.docker.createService(serviceSpec); return service.id; } catch (error) { this.handleError(error, 'Swarm deployment failed'); } } async formatServiceInfo(service) { try { const details = (await this.docker .getService(service.ID) .inspect()); const ports = (details.Endpoint?.Ports || []) .map((p) => { const protocol = p.Protocol || 'tcp'; return p.PublishedPort ? `${p.PublishedPort}:${p.TargetPort}/${protocol}` : `${p.TargetPort}/${protocol}`; }) .filter((p) => !!p) .reduce((acc, p) => (acc.includes(p) ? acc : [...acc, p]), []) .sort((a, b) => a.length - b.length || a.localeCompare(b)); const taskTemplate = details.Spec?.TaskTemplate?.ContainerSpec; const env = (taskTemplate?.Env || []).reduce((acc, line) => { const [key, ...val] = line.split('='); if (key) acc[key] = val.join('='); return acc; }, {}); const desiredReplicas = details.Spec?.Mode?.Replicated?.Replicas ?? 0; const runningTasks = await this.docker .listTasks({ filters: JSON.stringify({ service: [service.ID], 'desired-state': ['running'], }), }) .then((tasks) => tasks.length, () => 0); const status = desiredReplicas === 0 ? 'stopped' : runningTasks === desiredReplicas ? 'running' : runningTasks > 0 ? 'partial' : 'pending'; return (0, class_transformer_1.plainToInstance)(_dto_1.InfoDto, { id: service.ID, name: details.Spec?.Name, image: taskTemplate?.Image, status, ports, createdAt: service.CreatedAt ? new Date(service.CreatedAt).toISOString() : new Date().toISOString(), env, }, { enableImplicitConversion: true }); } catch (error) { return this.handleError(error, `Failed to format service info for ${service.ID}`); } } async list() { try { const services = await this.docker.listServices(); return await Promise.all(services.map((service) => this.formatServiceInfo(service))); } catch (error) { this.handleError(error, 'Failed to list services'); } } async remove(serviceId) { try { const service = this.docker.getService(serviceId); await service.remove(); } catch (error) { this.handleError(error, `Failed to remove service ${serviceId}`); } } async logs(id, since, tail) { try { const tasks = (await this.docker.listTasks({ filters: JSON.stringify({ service: [id] }), })); const allLogs = []; for (const task of tasks) { const containerId = task.Status?.ContainerStatus?.ContainerID; if (containerId) { try { const logBuffer = await this.docker.getContainer(containerId).logs({ since: since, tail: tail, stdout: true, stderr: true, timestamps: true, }); const logsString = logBuffer.toString('utf8'); const parsedLogs = logsString .split('\n') .filter((line) => line.trim().length > 0) .map((line) => { const dockerMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(stdout|stderr)\s+(.+)$/); if (dockerMatch) { return { timestamp: dockerMatch[1] .replace('T', ' ') .replace('Z', ''), stream: dockerMatch[2], message: dockerMatch[3].trim(), }; } return { timestamp: new Date() .toISOString() .replace('T', ' ') .replace('Z', ''), stream: line.toLowerCase().includes('error') ? 'stderr' : 'stdout', message: line .replace(/[\x00-\x1F]/g, ' ') .replace(/\s+/g, ' ') .trim(), }; }) .filter((logDto) => logDto.message && logDto.message.length > 0 && logDto.message !== 'Z'); allLogs.push(...parsedLogs); } catch (logError) { this.logger.warn(`Could not fetch logs for container ${containerId} of task ${task.id}:`, logError); } } } return allLogs.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); } catch (error) { return this.handleError(error, `Failed to get logs for service ${id}`); } } }; exports.SwarmStrategy = SwarmStrategy; exports.SwarmStrategy = SwarmStrategy = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [nestjs_logger_1.Logger]) ], SwarmStrategy); //# sourceMappingURL=swarm.strategy.js.map