@twg-group/container-manager
Version:
Container management for Docker, Swarm, Kubernetes
288 lines • 12 kB
JavaScript
;
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