- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
220 lines
8.4 KiB
Python
220 lines
8.4 KiB
Python
"""
|
||
Сервис для экспорта ролей в 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
|