feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile

- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
Сергей Антропов
2026-02-15 22:59:02 +03:00
parent 23e1a6037b
commit 1fbf9185a2
232 changed files with 38075 additions and 5 deletions

View 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