Files
DLE/webssh-agent/utils/dockerUtils.js

252 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs-extra');
const { execSshCommand, execScpCommand } = require('./sshUtils');
const log = require('./logger');
// Безопасные CLI команды
const execAsync = promisify(exec);
// Разрешенные Docker команды для безопасности
const ALLOWED_DOCKER_COMMANDS = [
'docker save',
'docker load',
'docker images',
'docker ps',
'docker run'
];
// Валидация команд
const validateDockerCommand = (command) => {
return ALLOWED_DOCKER_COMMANDS.some(allowed => command.startsWith(allowed));
};
// Безопасное выполнение Docker команд
const execDockerCommand = async (command) => {
if (!validateDockerCommand(command)) {
throw new Error(`Command not allowed: ${command}`);
}
return execAsync(command);
};
const execLocalCommand = async (command, options = {}) => {
return execAsync(command, { maxBuffer: options.maxBuffer || 1024 * 1024 * 50 });
};
/**
* Экспорт Docker образов и данных с локальной машины
*/
const exportDockerImages = async (sendWebSocketLog) => {
log.info('Экспорт Docker образов и данных с хоста...');
sendWebSocketLog('info', '📦 Начинаем экспорт Docker образов...', 'export_images', 60);
const images = [
{ name: 'postgres:16', file: 'dapp-postgres.tar' },
{ name: 'digital_legal_entitydle-ollama:latest', file: 'dapp-ollama.tar' },
{ name: 'digital_legal_entitydle-vector-search:latest', file: 'dapp-vector-search.tar' },
{ name: 'digital_legal_entitydle-backend:latest', file: 'dapp-backend.tar' },
{ name: 'digital_legal_entitydle-frontend:latest', file: 'dapp-frontend.tar' },
{ name: 'digital_legal_entitydle-frontend-nginx:latest', file: 'dapp-frontend-nginx.tar' },
{ name: 'digital_legal_entitydle-webssh-agent:latest', file: 'dapp-webssh-agent.tar' }
];
// Экспортируем все образы
for (let i = 0; i < images.length; i++) {
const image = images[i];
const progress = 60 + Math.floor((i / images.length) * 10); // 60-70%
sendWebSocketLog('info', `📦 Экспорт образа: ${image.name}`, 'export_images', progress);
try {
const outputPath = `/tmp/${image.file}`;
// Безопасный экспорт через CLI
await execDockerCommand(`docker save ${image.name} > ${outputPath}`);
sendWebSocketLog('success', `✅ Экспорт ${image.name} завершен`, 'export_images', progress);
} catch (error) {
log.error(`Ошибка экспорта ${image.name}: ${error.message}`);
sendWebSocketLog('error', `❌ Ошибка экспорта ${image.name}`, 'export_images', progress);
}
}
// Экспортируем данные из volumes (динамически определяем все volumes приложения)
log.info('Экспорт данных из Docker volumes...');
sendWebSocketLog('info', '📦 Экспорт данных из Docker volumes...', 'export_data', 70);
// Получаем список всех volumes приложения (без node_modules)
const volumesList = await execLocalCommand('docker volume ls -q | grep -E "digital_legal_entitydle_|dapp_" | grep -v node_modules || true');
const volumes = volumesList.stdout.trim().split('\n').filter(v => v && v.endsWith('_data'));
let progress = 72;
const progressStep = Math.floor(8 / Math.max(volumes.length, 1));
for (const volumeName of volumes) {
// Извлекаем имя файла из имени volume (например, digital_legal_entitydle_postgres_data -> postgres_data.tar.gz)
const volumeBaseName = volumeName.replace(/^(digital_legal_entitydle_|dapp_)/, '').replace(/_data$/, '_data');
const outputFile = `${volumeBaseName}.tar.gz`;
sendWebSocketLog('info', `📦 Экспорт данных: ${volumeName}`, 'export_data', progress);
await exportVolumeData(volumeName, outputFile, sendWebSocketLog, progress);
progress += progressStep;
}
// Создаем архив с ВСЕМИ образами и данными приложения
log.info('Создание архива Docker образов и данных на хосте...');
sendWebSocketLog('info', '📦 Создание архива всех данных...', 'export_data', 80);
try {
const tarFiles = images.map(img => img.file).join(' ');
// Динамически собираем список файлов данных из экспортированных volumes
const dataFilesList = await execLocalCommand('ls /tmp/*_data.tar.gz 2>/dev/null | xargs -r basename -a || echo ""');
const dataFiles = dataFilesList.stdout.trim().split('\n').filter(f => f).join(' ');
const archiveCommand = `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles || ''}`.trim();
await execLocalCommand(archiveCommand);
sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80);
} catch (error) {
log.error('Ошибка создания архива: ' + error.message);
sendWebSocketLog('error', '❌ Ошибка создания архива', 'export_data', 80);
}
log.success('Docker образы и данные успешно экспортированы');
sendWebSocketLog('success', '✅ Экспорт данных завершен', 'export_data', 80);
};
/**
* Вспомогательная функция для экспорта данных volume
*/
const exportVolumeData = async (volumeName, outputFile, sendWebSocketLog, progress) => {
try {
// Безопасный экспорт через CLI с временным контейнером
const exportCommand = `docker run --rm -v ${volumeName}:/data:ro -v /tmp:/backup alpine tar czf /backup/${outputFile} -C /data .`;
await execLocalCommand(exportCommand);
sendWebSocketLog('success', `✅ Экспорт ${outputFile} завершен`, 'export_data', progress);
} catch (error) {
log.error(`Ошибка экспорта ${volumeName}: ${error.message}`);
sendWebSocketLog('error', `❌ Ошибка экспорта ${volumeName}`, 'export_data', progress);
}
};
/**
* Передача Docker образов и данных на VDS
*/
const transferDockerImages = async (options, sendWebSocketLog) => {
const { dockerUser } = options;
log.info('Передача Docker образов и данных на VDS...');
sendWebSocketLog('info', '📤 Передача архива на VDS сервер...', 'transfer', 82);
// Передаем архив образов и данных на VDS через SCP
await execScpCommand(
'/tmp/docker-images-and-data.tar.gz',
`/home/${dockerUser}/dapp/docker-images-and-data.tar.gz`,
options
);
sendWebSocketLog('success', '✅ Архив успешно передан на VDS', 'transfer', 85);
log.success('Docker образы и данные успешно переданы на VDS');
};
/**
* Импорт Docker образов и данных на VDS
*/
const importDockerImages = async (options, sendWebSocketLog) => {
const { dockerUser } = options;
// Создаем скрипт импорта на VDS
sendWebSocketLog('info', '📥 Начинаем импорт данных на VDS...', 'import', 85);
const importScript = `#!/bin/bash
set -e
echo "🚀 Импорт Docker образов и данных на VDS..."
# Проверяем наличие архива
if [ ! -f "./docker-images-and-data.tar.gz" ]; then
echo "❌ Файл docker-images-and-data.tar.gz не найден!"
exit 1
fi
# Создаем директорию для распаковки
mkdir -p ./temp-import
# Распаковываем архив
echo "📦 Распаковка архива..."
tar -xzf ./docker-images-and-data.tar.gz -C ./temp-import
# Импортируем ВСЕ образы приложения
echo "📦 Импорт образов..."
for image_file in ./temp-import/dapp-*.tar; do
if [ -f "$image_file" ]; then
echo "📦 Импорт образа: $(basename $image_file)"
docker load -i "$image_file"
fi
done
# 🆕 Динамически определяем volumes для импорта из имен файлов в архиве
echo "📦 Импорт данных в volumes..."
for data_file in ./temp-import/*_data.tar.gz; do
if [ -f "$data_file" ]; then
# Извлекаем имя volume из имени файла (например, postgres_data.tar.gz -> postgres_data)
volume_name=$(basename "$data_file" .tar.gz)
# Используем префикс dapp_ для соответствия docker-compose.prod.yml
full_volume_name="dapp_${volume_name}"
echo "📦 Импорт данных: $full_volume_name"
# Удаляем старый volume если существует
docker volume rm -f "$full_volume_name" 2>/dev/null || true
# Создаем новый volume
docker volume create "$full_volume_name"
# Импортируем данные
docker run --rm -v "$full_volume_name:/data" -v ./temp-import:/backup alpine tar xzf "/backup/$(basename $data_file)" -C /data
fi
done
# Очищаем временные файлы
rm -rf ./temp-import
echo "✅ Образы и данные успешно импортированы!"
echo "📋 Доступные образы:"
docker images | grep -E "digital_legal_entitydle|postgres"
echo "📋 Доступные volumes:"
docker volume ls | grep dapp_`;
await execSshCommand(`echo '${importScript}' | tee /home/${dockerUser}/dapp/import-images-and-data.sh`, options);
await execSshCommand(`chmod +x /home/${dockerUser}/dapp/import-images-and-data.sh`, options);
// Импортируем образы и данные
log.info('Импорт Docker образов и данных...');
sendWebSocketLog('info', '📥 Импорт Docker образов на VDS...', 'import', 87);
await execSshCommand(`cd /home/${dockerUser}/dapp && ./import-images-and-data.sh`, options);
sendWebSocketLog('success', '✅ Импорт Docker образов завершен', 'import', 90);
sendWebSocketLog('info', '📥 Импорт данных в volumes...', 'import', 92);
// Логи от самого скрипта импорта будут видны
sendWebSocketLog('success', '✅ Импорт данных завершен', 'import', 95);
log.success('Docker образы и данные успешно импортированы на VDS');
};
/**
* Очистка временных файлов на локальной машине
*/
const cleanupLocalFiles = async () => {
log.info('Очистка временных файлов на хосте...');
try {
await execLocalCommand("rm -f /tmp/dapp-*.tar /tmp/*_data.tar.gz /tmp/docker-images-and-data.tar.gz");
log.success('Временные файлы очищены');
} catch (error) {
log.error('Ошибка очистки файлов: ' + error.message);
}
};
module.exports = {
exportDockerImages,
transferDockerImages,
importDockerImages,
cleanupLocalFiles
};