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

306 lines
14 KiB
JavaScript
Raw Permalink 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' }
];
// Список реально экспортированных файлов образов
const exportedImageFiles = [];
// Экспортируем все образы
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}`;
// Проверяем, существует ли образ локально
const inspectResult = await execLocalCommand(`docker images -q ${image.name} || true`);
const imageId = inspectResult.stdout.trim();
if (!imageId) {
const msg = `Образ ${image.name} не найден локально, пропускаем экспорт`;
log.warn(msg);
sendWebSocketLog('warning', `⚠️ ${msg}`, 'export_images', progress);
continue;
}
// Безопасный экспорт через CLI
await execDockerCommand(`docker save -o ${outputPath} ${image.name}`);
exportedImageFiles.push(image.file);
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 = exportedImageFiles.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 = tarFiles
? `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles || ''}`.trim()
: `cd /tmp && tar -czf docker-images-and-data.tar.gz ${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
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
# Пропускаем пустые или поврежденные файлы образов
if [ ! -s "$image_file" ]; then
echo "⚠️ Пропуск пустого файла образа: $(basename "$image_file")"
continue
fi
echo "📦 Импорт образа: $(basename "$image_file")"
if ! docker load -i "$image_file"; then
echo "❌ Ошибка импорта образа: $(basename "$image_file"), продолжаем со следующими"
continue
fi
fi
done
# 🆕 Динамически определяем volumes для импорта из имен файлов в архиве
echo "📦 Импорт данных в volumes..."
# Включаем nullglob для безопасной обработки пустых glob-паттернов
shopt -s nullglob
# Инициализируем переменные
volume_name=""
full_volume_name=""
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 2>/dev/null || echo "")
# Проверяем, что volume_name не пустой и не содержит только пробелы
if [ -z "$volume_name" ] || [ -z "$(echo "$volume_name" | tr -d '[:space:]')" ]; then
echo "⚠️ Предупреждение: не удалось извлечь имя volume из файла: $data_file"
volume_name=""
continue
fi
# Используем префикс 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
if ! docker volume create "$full_volume_name"; then
echo "❌ Ошибка создания volume: $full_volume_name"
continue
fi
# Импортируем данные
if ! docker run --rm -v "$full_volume_name:/data" -v "$(pwd)/temp-import:/backup" alpine tar xzf "/backup/$(basename "$data_file")" -C /data; then
echo "❌ Ошибка импорта данных в volume: $full_volume_name"
continue
fi
echo "✅ Данные успешно импортированы в volume: $full_volume_name"
fi
done
shopt -u nullglob
# Очищаем временные файлы
rm -rf ./temp-import
echo "✅ Образы и данные успешно импортированы!"
echo "📋 Доступные образы:"
docker images | grep -E "digital_legal_entitydle|postgres"
echo "📋 Доступные volumes:"
docker volume ls | grep dapp_`;
// Записываем скрипт в файл локально и передаем через SCP для избежания проблем с экранированием
const tempScriptPath = `/tmp/import-images-and-data-${Date.now()}.sh`;
await fs.writeFile(tempScriptPath, importScript, { mode: 0o755 });
try {
await execScpCommand(tempScriptPath, `/home/${dockerUser}/dapp/import-images-and-data.sh`, options);
await execSshCommand(`chmod +x /home/${dockerUser}/dapp/import-images-and-data.sh`, options);
} finally {
// Удаляем временный файл
await fs.remove(tempScriptPath).catch(() => {});
}
// Импортируем образы и данные
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
};