docker-pilot
Version:
A powerful, scalable Docker CLI library for managing containerized applications of any size
561 lines • 22.5 kB
JavaScript
"use strict";
/**
* Service Manager for Docker Pilot
* Manages Docker services and their lifecycle
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServiceManager = void 0;
const path = __importStar(require("path"));
const types_1 = require("../types");
const Logger_1 = require("../utils/Logger");
const DockerUtils_1 = require("../utils/DockerUtils");
const i18n_1 = require("../utils/i18n");
class ServiceManager {
constructor(config, options = {}) {
this.config = config;
this.logger = new Logger_1.Logger();
this.dockerUtils = new DockerUtils_1.DockerUtils(this.logger);
// Initialize i18n with config language
this.i18n = new i18n_1.I18n(config.language);
// Initialize options first
const workingDirectory = options.workingDirectory || process.cwd();
this.options = {
projectName: options.projectName || config.projectName,
workingDirectory,
composeFile: options.composeFile || this.findComposeFile(workingDirectory)
};
} /**
* Find Docker Compose file with enhanced search (synchronous for ServiceManager)
*/
findComposeFile(workingDirectory = process.cwd()) {
// First try basic files in working directory
const possibleFiles = [
'docker-compose.yml',
'docker-compose.yaml',
'compose.yml',
'compose.yaml'
];
for (const file of possibleFiles) {
const filePath = path.join(workingDirectory, file);
if (require('fs').existsSync(filePath)) {
this.logger.debug(`Found compose file: ${file}`);
return filePath;
}
}
// If not found in root, try to search recursively in subdirectories (sync version)
const fs = require('fs');
const searchInDir = (dir, maxDepth = 3, currentDepth = 0) => {
if (currentDepth > maxDepth)
return null;
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
// First check for compose files in current directory
for (const file of possibleFiles) {
const filePath = path.join(dir, file);
if (fs.existsSync(filePath)) {
this.logger.debug(`Found compose file in subdirectory: ${path.relative(workingDirectory, filePath)}`);
return filePath;
}
}
// Then search subdirectories
for (const entry of entries) {
if (entry.isDirectory() && !this.shouldSkipDirectory(entry.name)) {
const result = searchInDir(path.join(dir, entry.name), maxDepth, currentDepth + 1);
if (result)
return result;
}
}
}
catch (error) {
this.logger.debug(`Failed to read directory ${dir}:`, error);
}
return null;
};
const foundFile = searchInDir(workingDirectory);
if (foundFile) {
return foundFile;
}
this.logger.debug('No compose file found, using default: docker-compose.yml');
return path.join(workingDirectory, 'docker-compose.yml'); // Default fallback
}
/**
* Check if directory should be skipped during search
*/
shouldSkipDirectory(dirName) {
const skipDirectories = [
'node_modules', '.git', '.vscode', '.idea', 'dist', 'build',
'target', 'out', 'tmp', 'temp', '.next', '.nuxt', 'coverage',
'__pycache__', '.pytest_cache', 'venv', 'env', '.env',
'vendor', 'logs', '.docker'
];
return skipDirectories.includes(dirName) || dirName.startsWith('.');
}
/**
* Create safe options for Docker commands
*/
createDockerOptions(additionalOptions = {}) {
return {
...(this.options.workingDirectory && { cwd: this.options.workingDirectory }),
...(this.options.composeFile && { composeFile: this.options.composeFile }),
...additionalOptions
};
}
/**
* Start all services
*/
async startAll() {
this.logger.loading(this.i18n.t('operation.starting_services'));
try {
const result = await this.dockerUtils.executeComposeCommand('up', ['-d'], this.createDockerOptions());
if (result.success) {
this.logger.success(this.i18n.t('service.all_started'));
await this.displayServiceStatus();
}
else {
this.logger.error(this.i18n.t('service.failed_start', { name: 'services' }), result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(this.i18n.t('error.command_execution'), error);
throw new types_1.ServiceError(`Failed to start services: ${errorMessage}`, { error });
}
} /**
* Stop all services
*/
async stopAll() {
this.logger.loading(this.i18n.t('operation.stopping_services'));
try {
const result = await this.dockerUtils.executeComposeCommand('down', [], this.createDockerOptions());
if (result.success) {
this.logger.success(this.i18n.t('service.all_stopped'));
}
else {
this.logger.error(this.i18n.t('service.failed_stop', { name: 'services' }), result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(this.i18n.t('error.command_execution'), error);
return {
success: false,
output: '',
error: `Failed to stop services: ${errorMessage}`,
executionTime: 0
};
}
} /**
* Restart all services
*/
async restartAll() {
this.logger.loading(this.i18n.t('operation.restarting_services'));
try {
const result = await this.dockerUtils.executeComposeCommand('restart', [], this.createDockerOptions());
if (result.success) {
this.logger.success(this.i18n.t('service.all_restarted'));
await this.displayServiceStatus();
}
else {
this.logger.error(this.i18n.t('service.failed_restart', { name: 'services' }), result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(this.i18n.t('error.command_execution'), error);
return {
success: false,
output: '',
error: `Failed to restart services: ${errorMessage}`,
executionTime: 0
};
}
}
/**
* Start specific service
*/
async startService(serviceName) {
if (!this.isServiceConfigured(serviceName)) {
throw new types_1.ServiceError(this.i18n.t('error.service_not_found', { name: serviceName }));
}
this.logger.loading(this.i18n.t('service.starting', { name: serviceName }));
try {
const result = await this.dockerUtils.executeComposeCommand('up', ['-d', serviceName], this.createDockerOptions());
if (result.success) {
this.logger.success(this.i18n.t('service.started_success', { name: serviceName }));
await this.displayServiceStatus(serviceName);
}
else {
this.logger.error(this.i18n.t('service.failed_start', { name: serviceName }), result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(this.i18n.t('error.command_execution'), error);
throw new types_1.ServiceError(`Failed to start service ${serviceName}: ${errorMessage}`, { serviceName, error });
}
}
/**
* Stop specific service
*/
async stopService(serviceName) {
if (!this.isServiceConfigured(serviceName)) {
throw new types_1.ServiceError(this.i18n.t('error.service_not_found', { name: serviceName }));
}
this.logger.loading(this.i18n.t('service.stopping', { name: serviceName }));
try {
const result = await this.dockerUtils.executeComposeCommand('stop', [serviceName], this.createDockerOptions());
if (result.success) {
this.logger.success(this.i18n.t('service.stopped_success', { name: serviceName }));
}
else {
this.logger.error(this.i18n.t('service.failed_stop', { name: serviceName }), result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(this.i18n.t('error.command_execution'), error);
throw new types_1.ServiceError(`Failed to stop service ${serviceName}: ${errorMessage}`, { serviceName, error });
}
}
/**
* Restart specific service
*/
async restartService(serviceName) {
if (!this.isServiceConfigured(serviceName)) {
throw new types_1.ServiceError(`Service not configured: ${serviceName}`);
}
this.logger.loading(`Restarting service: ${serviceName}`);
try {
const result = await this.dockerUtils.executeComposeCommand('restart', [serviceName], this.createDockerOptions());
if (result.success) {
this.logger.success(`Service ${serviceName} restarted successfully`);
await this.displayServiceStatus(serviceName);
}
else {
this.logger.error(`Failed to restart service ${serviceName}`, result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Error restarting service ${serviceName}`, error);
throw new types_1.ServiceError(`Failed to restart service ${serviceName}: ${errorMessage}`, { serviceName, error });
}
}
/**
* Build services
*/
async buildServices(serviceName, options = {}) {
const target = serviceName || 'all services';
this.logger.loading(`Building ${target}...`);
try {
const args = ['build'];
if (options.noCache)
args.push('--no-cache');
if (options.pull)
args.push('--pull');
if (serviceName)
args.push(serviceName);
const result = await this.dockerUtils.executeComposeCommand('build', args, this.createDockerOptions());
if (result.success) {
this.logger.success(`${target} built successfully`);
}
else {
this.logger.error(`Failed to build ${target}`, result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Error building ${target}`, error);
throw new types_1.ServiceError(`Failed to build ${target}: ${errorMessage}`, { serviceName, error });
}
}
/**
* Rebuild and start services
*/
async rebuildServices(serviceName) {
const target = serviceName || 'all services';
this.logger.loading(`Rebuilding ${target}...`);
try {
// Build first
const buildResult = await this.buildServices(serviceName, { noCache: true });
if (!buildResult.success) {
return [buildResult];
}
// Then start
const startResult = serviceName
? await this.startService(serviceName)
: await this.startAll();
return [buildResult, startResult];
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Error rebuilding ${target}`, error);
throw new types_1.ServiceError(`Failed to rebuild ${target}: ${errorMessage}`, { serviceName, error });
}
} /**
* Get service logs
*/
async getLogs(serviceName, options = {}) {
const target = serviceName || 'all services';
this.logger.info(`Getting logs for ${target}...`);
try {
const logOptions = {
...options,
...(this.options.projectName && { projectName: this.options.projectName }),
...(this.options.composeFile && { composeFile: this.options.composeFile })
};
const result = await this.dockerUtils.getLogs(serviceName || '', logOptions);
// Handle the case where getLogs returns a ChildProcess (when follow is true)
if ('pid' in result && typeof result.pid === 'number') {
// It's a ChildProcess, return success immediately for follow mode
return {
success: true,
output: 'Following logs...',
error: '',
executionTime: 0
};
}
// It's a CommandResult
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Error getting logs for ${target}`, error);
return {
success: false,
output: '',
error: `Failed to get logs for ${target}: ${errorMessage}`,
executionTime: 0
};
}
}
/**
* Execute command in service
*/
async execInService(serviceName, command, options = {}) {
if (!this.isServiceConfigured(serviceName)) {
throw new types_1.ServiceError(`Service not configured: ${serviceName}`);
}
this.logger.info(`Executing command in ${serviceName}: ${command.join(' ')}`);
try {
const execOptions = {
...options,
...(options.interactive && { tty: options.interactive })
};
return await this.dockerUtils.execInService(serviceName, command, execOptions);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Error executing command in ${serviceName}`, error);
throw new types_1.ServiceError(`Failed to execute command in ${serviceName}: ${errorMessage}`, { serviceName, error });
}
}
/**
* Scale service
*/
async scaleService(serviceName, replicas) {
if (!this.isServiceConfigured(serviceName)) {
throw new types_1.ServiceError(`Service not configured: ${serviceName}`);
}
this.logger.loading(`Scaling ${serviceName} to ${replicas} replicas...`);
try {
const result = await this.dockerUtils.scaleService(serviceName, replicas);
if (result.success) {
this.logger.success(`Service ${serviceName} scaled to ${replicas} replicas`);
}
else {
this.logger.error(`Failed to scale service ${serviceName}`, result.error);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Error scaling service ${serviceName}`, error);
throw new types_1.ServiceError(`Failed to scale service ${serviceName}: ${errorMessage}`, { serviceName, error });
}
}
/**
* Get service status
*/ async getServiceStatus(serviceName) {
try {
return await this.dockerUtils.getServiceStatus(this.options.projectName, serviceName, this.options.composeFile ? { composeFile: this.options.composeFile } : undefined);
}
catch (error) {
this.logger.error('Error getting service status', error);
return [];
}
}
/**
* Get project status
*/
async getProjectStatus() {
try {
return await this.dockerUtils.getProjectStatus(this.options.projectName);
}
catch (error) {
this.logger.error('Error getting project status', error);
throw new types_1.ServiceError('Failed to get project status', { error });
}
}
/**
* Display service status in a nice format
*/
async displayServiceStatus(serviceName) {
try {
const services = await this.getServiceStatus(serviceName);
if (services.length === 0) {
this.logger.warn('No services found');
return;
}
this.logger.newLine();
this.logger.info('Service Status:');
this.logger.separator('-', 40);
for (const service of services) {
const config = this.config.services[service.name];
let statusLine = `${service.name}: ${service.state}`;
if (service.health && service.health !== 'none') {
statusLine += ` (${service.health})`;
}
if (config?.port && service.state === 'running') {
const url = service.name === 'redis'
? `redis://localhost:${config.port}`
: `http://localhost:${config.port}`;
statusLine += ` - ${url}`;
}
if (config?.description) {
statusLine += ` - ${config.description}`;
}
this.logger.service(service.name, service.state, {
health: service.health,
uptime: service.uptime,
port: config?.port
});
}
this.logger.newLine();
}
catch (error) {
this.logger.debug('Error displaying service status', error);
}
}
/**
* Check if service is configured
*/
isServiceConfigured(serviceName) {
return serviceName in this.config.services;
}
/**
* Get service configuration
*/
getServiceConfig(serviceName) {
return this.config.services[serviceName] || null;
}
/**
* Get all configured services
*/
getConfiguredServices() {
return Object.keys(this.config.services);
}
/**
* Check if all services are running
*/
async areAllServicesRunning() {
try {
const services = await this.getServiceStatus();
return services.length > 0 && services.every(service => service.state === 'running');
}
catch (error) {
this.logger.error('Error checking service status', error);
return false;
}
}
/**
* Wait for service to be healthy
*/
async waitForServiceHealth(serviceName, timeoutMs = 60000, intervalMs = 2000) {
const startTime = Date.now();
this.logger.loading(`Waiting for ${serviceName} to be healthy...`);
while (Date.now() - startTime < timeoutMs) {
try {
const services = await this.getServiceStatus(serviceName);
const service = services.find(s => s.name === serviceName);
if (service && service.state === 'running' && service.health === 'healthy') {
this.logger.success(`Service ${serviceName} is healthy`);
return true;
}
await this.sleep(intervalMs);
}
catch (error) {
this.logger.debug(`Error checking ${serviceName} health`, error);
await this.sleep(intervalMs);
}
}
this.logger.warn(`Timeout waiting for ${serviceName} to be healthy`);
return false;
}
/**
* Update service manager configuration
*/
updateConfig(config) {
this.config = config;
}
/**
* Update language for ServiceManager
*/
updateLanguage(language) {
this.i18n.setLanguage(language);
}
/**
* Sleep utility
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Get current options
*/
getOptions() {
return { ...this.options };
}
}
exports.ServiceManager = ServiceManager;
//# sourceMappingURL=ServiceManager.js.map