feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
219
app/services/export_service.py
Normal file
219
app/services/export_service.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Сервис для экспорта ролей в Git репозитории
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
import yaml
|
||||
from git import Repo
|
||||
from git.exc import GitCommandError
|
||||
from app.core.config import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportService:
|
||||
"""Сервис для экспорта ролей в Git репозитории"""
|
||||
|
||||
def __init__(self):
|
||||
self.project_root = settings.PROJECT_ROOT
|
||||
self.roles_dir = self.project_root / "roles"
|
||||
|
||||
async def export_role(
|
||||
self,
|
||||
role_name: str,
|
||||
repo_url: str,
|
||||
branch: str = "main",
|
||||
version: str = None,
|
||||
components: List[str] = None,
|
||||
include_secrets: bool = False,
|
||||
commit_message: str = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Экспорт роли в Git репозиторий
|
||||
|
||||
Args:
|
||||
role_name: Имя роли для экспорта
|
||||
repo_url: URL Git репозитория
|
||||
branch: Ветка для коммита (по умолчанию main)
|
||||
version: Версия роли (для создания тега)
|
||||
components: Список компонентов для экспорта
|
||||
include_secrets: Включать ли секреты из vars/
|
||||
commit_message: Сообщение коммита
|
||||
|
||||
Returns:
|
||||
Информация о результате экспорта
|
||||
"""
|
||||
role_dir = self.roles_dir / role_name
|
||||
|
||||
if not role_dir.exists():
|
||||
raise ValueError(f"Роль '{role_name}' не найдена")
|
||||
|
||||
# Компоненты по умолчанию
|
||||
if components is None:
|
||||
components = ["tasks", "handlers", "defaults", "meta", "templates", "files", "README.md"]
|
||||
|
||||
# Создание временной директории для подготовки файлов
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
export_dir = temp_path / "export"
|
||||
export_dir.mkdir()
|
||||
|
||||
# Копирование выбранных компонентов
|
||||
for component in components:
|
||||
src = role_dir / component
|
||||
if src.exists():
|
||||
dest = export_dir / component
|
||||
if src.is_dir():
|
||||
shutil.copytree(src, dest)
|
||||
else:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dest)
|
||||
|
||||
# Обработка vars (секреты)
|
||||
if "vars" in components:
|
||||
vars_dir = export_dir / "vars"
|
||||
vars_dir.mkdir(exist_ok=True)
|
||||
vars_file = role_dir / "vars" / "main.yml"
|
||||
if vars_file.exists():
|
||||
if include_secrets:
|
||||
shutil.copy2(vars_file, vars_dir / "main.yml")
|
||||
else:
|
||||
# Создать файл без секретов
|
||||
self._create_vars_without_secrets(vars_file, vars_dir / "main.yml")
|
||||
|
||||
# Создание .gitignore
|
||||
self._create_gitignore(export_dir)
|
||||
|
||||
# Создание .ansible-lint если нужно
|
||||
self._create_ansible_lint(export_dir)
|
||||
|
||||
# Клонирование/обновление репозитория
|
||||
repo_dir = temp_path / "repo"
|
||||
try:
|
||||
if repo_dir.exists():
|
||||
repo = Repo(repo_dir)
|
||||
repo.remote().pull()
|
||||
else:
|
||||
repo = Repo.clone_from(repo_url, repo_dir, branch=branch)
|
||||
except GitCommandError as e:
|
||||
raise ValueError(f"Ошибка работы с Git репозиторием: {str(e)}")
|
||||
|
||||
# Копирование файлов в репозиторий
|
||||
for item in export_dir.iterdir():
|
||||
dest = repo_dir / item.name
|
||||
if dest.exists():
|
||||
if dest.is_dir():
|
||||
shutil.rmtree(dest)
|
||||
else:
|
||||
dest.unlink()
|
||||
if item.is_dir():
|
||||
shutil.copytree(item, dest)
|
||||
else:
|
||||
shutil.copy2(item, dest)
|
||||
|
||||
# Коммит и push
|
||||
repo.git.add(A=True)
|
||||
|
||||
if not commit_message:
|
||||
commit_message = f"Export role {role_name}"
|
||||
if version:
|
||||
commit_message += f" v{version}"
|
||||
|
||||
try:
|
||||
repo.index.commit(commit_message)
|
||||
repo.remote().push()
|
||||
|
||||
# Создание тега
|
||||
if version:
|
||||
tag_name = f"v{version}"
|
||||
repo.create_tag(tag_name, message=f"Version {version} of {role_name}")
|
||||
repo.remote().push(tags=True)
|
||||
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"role_name": role_name,
|
||||
"repo_url": repo_url,
|
||||
"branch": branch,
|
||||
"version": version,
|
||||
"commit": commit_hash,
|
||||
"message": f"Роль '{role_name}' успешно экспортирована в {repo_url}"
|
||||
}
|
||||
except GitCommandError as e:
|
||||
raise ValueError(f"Ошибка при коммите/push: {str(e)}")
|
||||
|
||||
def _create_vars_without_secrets(self, src_file: Path, dest_file: Path):
|
||||
"""Создание vars/main.yml без секретов"""
|
||||
try:
|
||||
with open(src_file) as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# Удаление секретных полей (можно настроить)
|
||||
if isinstance(data, dict):
|
||||
# Удаляем поля, содержащие секреты
|
||||
secret_keys = ["password", "secret", "key", "token", "api_key"]
|
||||
for key in list(data.keys()):
|
||||
if any(secret in key.lower() for secret in secret_keys):
|
||||
data[key] = "***REDACTED***"
|
||||
|
||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(dest_file, 'w') as f:
|
||||
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось обработать vars файл: {e}")
|
||||
# Создаем пустой файл
|
||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest_file.write_text("# Секреты не включены в экспорт\n")
|
||||
|
||||
def _create_gitignore(self, export_dir: Path):
|
||||
"""Создание .gitignore для экспортируемой роли"""
|
||||
gitignore_content = """# Ansible role
|
||||
*.retry
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
"""
|
||||
(export_dir / ".gitignore").write_text(gitignore_content)
|
||||
|
||||
def _create_ansible_lint(self, export_dir: Path):
|
||||
"""Создание .ansible-lint если нужно"""
|
||||
ansible_lint_content = """---
|
||||
# Ansible Lint configuration
|
||||
skip_list:
|
||||
- yaml[line-length]
|
||||
- yaml[truthy]
|
||||
"""
|
||||
(export_dir / ".ansible-lint").write_text(ansible_lint_content)
|
||||
|
||||
def get_role_components(self, role_name: str) -> List[str]:
|
||||
"""Получение списка доступных компонентов роли"""
|
||||
role_dir = self.roles_dir / role_name
|
||||
|
||||
if not role_dir.exists():
|
||||
return []
|
||||
|
||||
components = []
|
||||
|
||||
# Стандартные директории
|
||||
for component in ["tasks", "handlers", "defaults", "vars", "meta", "templates", "files"]:
|
||||
if (role_dir / component).exists():
|
||||
components.append(component)
|
||||
|
||||
# Файлы
|
||||
for file in ["README.md", ".gitignore", ".ansible-lint"]:
|
||||
if (role_dir / file).exists():
|
||||
components.append(file)
|
||||
|
||||
return components
|
||||
Reference in New Issue
Block a user