""" Сервис для экспорта ролей в 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