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,364 @@
"""
Сервис для работы с preset'ами Molecule (работа с БД)
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from pathlib import Path
from typing import Dict, List, Optional
import yaml
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.config import settings
from app.models.database import Preset
class PresetService:
"""Сервис для управления preset'ами из БД"""
@staticmethod
async def get_all_presets(db: AsyncSession, category: Optional[str] = None) -> List[Dict]:
"""Получение списка всех preset'ов из БД"""
query = select(Preset)
if category:
query = query.where(Preset.category == category)
result = await db.execute(query.order_by(Preset.category, Preset.name))
presets = result.scalars().all()
presets_list = []
for preset in presets:
# Парсинг YAML для получения информации
try:
data = yaml.safe_load(preset.content) if preset.content else {}
hosts = data.get("hosts", [])
images = data.get("images", {})
# Получение групп
groups = set()
for host in hosts:
host_groups = host.get("groups", [])
if isinstance(host_groups, list):
groups.update(host_groups)
presets_list.append({
"name": preset.name,
"category": preset.category,
"description": preset.description,
"hosts_count": len(hosts),
"hosts": hosts,
"images": list(images.keys()) if images else [],
"groups": sorted(list(groups)),
"id": preset.id
})
except Exception as e:
presets_list.append({
"name": preset.name,
"category": preset.category,
"description": preset.description,
"error": str(e),
"id": preset.id
})
return presets_list
@staticmethod
async def get_preset(db: AsyncSession, preset_name: str, category: str = "main") -> Optional[Preset]:
"""Получение preset'а по имени из БД"""
result = await db.execute(
select(Preset).where(Preset.name == preset_name, Preset.category == category)
)
return result.scalar_one_or_none()
@staticmethod
async def get_preset_dict(db: AsyncSession, preset_name: str, category: str = "main") -> Dict:
"""Получение preset'а в виде словаря"""
preset = await PresetService.get_preset(db, preset_name, category)
if not preset:
raise ValueError(f"Preset '{preset_name}' не найден")
try:
data = yaml.safe_load(preset.content) if preset.content else {}
except Exception as e:
raise ValueError(f"Ошибка парсинга YAML preset'а: {str(e)}")
return {
"name": preset.name,
"category": preset.category,
"path": f"presets/{preset.category}/{preset.name}.yml", # Виртуальный путь
"content": preset.content,
"data": data,
"description": preset.description,
"id": preset.id
}
@staticmethod
async def create_preset(
db: AsyncSession,
preset_name: str,
description: str = "",
hosts: List[Dict] = None,
category: str = "main",
created_by: Optional[str] = None
) -> Preset:
"""Создание нового preset'а в БД"""
# Проверка существования
existing = await PresetService.get_preset(db, preset_name, category)
if existing:
raise ValueError(f"Preset '{preset_name}' уже существует")
# Генерация содержимого preset'а
content = PresetService._generate_preset_content(description, hosts or [])
# Парсинг для извлечения данных
data = yaml.safe_load(content)
preset = Preset(
name=preset_name,
category=category,
description=description,
content=content,
docker_network=data.get("docker_network", "labnet"),
hosts=data.get("hosts", []),
images=data.get("images", {}),
systemd_defaults=data.get("systemd_defaults", {}),
kind_clusters=data.get("kind_clusters", []),
created_by=created_by
)
db.add(preset)
await db.commit()
await db.refresh(preset)
return preset
@staticmethod
async def update_preset(
db: AsyncSession,
preset_name: str,
content: str,
category: str = "main",
updated_by: Optional[str] = None
) -> Preset:
"""Обновление preset'а (старый метод - через YAML)"""
preset = await PresetService.get_preset(db, preset_name, category)
if not preset:
raise ValueError(f"Preset '{preset_name}' не найден")
# Валидация YAML
try:
data = yaml.safe_load(content)
except yaml.YAMLError as e:
raise ValueError(f"Неверный формат YAML: {str(e)}")
# Извлечение описания из комментария
description = None
for line in content.split('\n'):
if line.strip().startswith('#description:'):
description = line.split('#description:')[1].strip()
break
preset.content = content
preset.description = description or preset.description
preset.docker_network = data.get("docker_network", preset.docker_network)
preset.hosts = data.get("hosts", preset.hosts)
preset.images = data.get("images", preset.images)
preset.systemd_defaults = data.get("systemd_defaults", preset.systemd_defaults)
preset.kind_clusters = data.get("kind_clusters", preset.kind_clusters)
preset.updated_by = updated_by
await db.commit()
await db.refresh(preset)
return preset
@staticmethod
async def update_preset_from_form(
db: AsyncSession,
preset_name: str,
description: str = "",
category: str = "main",
docker_network: str = "labnet",
hosts: List[Dict] = None,
images: Dict = None,
systemd_defaults: Dict = None,
kind_clusters: List = None,
updated_by: Optional[str] = None
) -> Preset:
"""Обновление preset'а из формы"""
preset = await PresetService.get_preset(db, preset_name, category)
if not preset:
raise ValueError(f"Preset '{preset_name}' не найден")
# Генерация содержимого из формы
content = PresetService._generate_preset_content_from_form(
description=description,
docker_network=docker_network,
hosts=hosts or [],
images=images or {},
systemd_defaults=systemd_defaults or {},
kind_clusters=kind_clusters or []
)
# Парсинг для обновления полей
data = yaml.safe_load(content)
preset.content = content
preset.description = description
preset.docker_network = docker_network
preset.hosts = hosts or []
preset.images = images or {}
preset.systemd_defaults = systemd_defaults or {}
preset.kind_clusters = kind_clusters or []
preset.updated_by = updated_by
await db.commit()
await db.refresh(preset)
return preset
@staticmethod
async def delete_preset(db: AsyncSession, preset_name: str, category: str = "main") -> bool:
"""Удаление preset'а из БД"""
preset = await PresetService.get_preset(db, preset_name, category)
if not preset:
raise ValueError(f"Preset '{preset_name}' не найден")
await db.delete(preset)
await db.commit()
return True
@staticmethod
def _generate_preset_content_from_form(
description: str = "",
docker_network: str = "labnet",
hosts: List[Dict] = None,
images: Dict = None,
systemd_defaults: Dict = None,
kind_clusters: List = None
) -> str:
"""Генерация содержимого preset'а из формы"""
# Базовые образы по умолчанию
default_images = {
"alt9": "inecs/ansible-lab:alt9-latest",
"alt10": "inecs/ansible-lab:alt10-latest",
"astra": "inecs/ansible-lab:astra-linux-latest",
"rhel": "inecs/ansible-lab:rhel-latest",
"centos7": "inecs/ansible-lab:centos7-latest",
"centos8": "inecs/ansible-lab:centos8-latest",
"centos9": "inecs/ansible-lab:centos9-latest",
"alma": "inecs/ansible-lab:alma-latest",
"rocky": "inecs/ansible-lab:rocky-latest",
"redos": "inecs/ansible-lab:redos-latest",
"ubuntu20": "inecs/ansible-lab:ubuntu20-latest",
"ubuntu22": "inecs/ansible-lab:ubuntu22-latest",
"ubuntu24": "inecs/ansible-lab:ubuntu24-latest",
"debian9": "inecs/ansible-lab:debian9-latest",
"debian10": "inecs/ansible-lab:debian10-latest",
"debian11": "inecs/ansible-lab:debian11-latest",
"debian12": "inecs/ansible-lab:debian12-latest"
}
# Объединяем с переданными образами
final_images = {**default_images, **(images or {})}
# Systemd defaults по умолчанию
default_systemd = {
"privileged": True,
"command": "/sbin/init",
"volumes": ["/sys/fs/cgroup:/sys/fs/cgroup:rw"],
"tmpfs": ["/run", "/run/lock"],
"capabilities": ["SYS_ADMIN"]
}
# Объединяем с переданными настройками
final_systemd = {**default_systemd, **(systemd_defaults or {})}
# Заголовок
content = f"""---
#description: {description or "Пользовательский preset"}
# Автор: Сергей Антропов
# Сайт: https://devops.org.ru
docker_network: {docker_network}
generated_inventory: "{{{{ molecule_ephemeral_directory }}}}/inventory/hosts.ini"
# systemd-ready образы
images:
"""
# Добавление образов
for key, value in sorted(final_images.items()):
content += f" {key}: \"{value}\"\n"
# Systemd defaults
content += "\nsystemd_defaults:\n"
content += f" privileged: {str(final_systemd.get('privileged', True)).lower()}\n"
content += f" command: \"{final_systemd.get('command', '/sbin/init')}\"\n"
if final_systemd.get('volumes'):
content += " volumes:\n"
for vol in final_systemd['volumes']:
content += f" - \"{vol}\"\n"
if final_systemd.get('tmpfs'):
content += " tmpfs: ["
content += ", ".join([f'"{v}"' for v in final_systemd['tmpfs']])
content += "]\n"
if final_systemd.get('capabilities'):
content += " capabilities: ["
content += ", ".join([f'"{v}"' for v in final_systemd['capabilities']])
content += "]\n"
# Хосты
content += "\nhosts:\n"
if hosts:
for host in hosts:
content += f" - name: {host.get('name', 'host1')}\n"
content += f" family: {host.get('family', 'ubuntu22')}\n"
# Группы
host_groups = host.get('groups', [])
if isinstance(host_groups, str):
host_groups = [g.strip() for g in host_groups.split(',') if g.strip()]
if host_groups:
content += " groups: ["
content += ", ".join([f'"{g}"' for g in host_groups])
content += "]\n"
# Дополнительные поля хоста
if host.get('type'):
content += f" type: {host['type']}\n"
if host.get('supported_platforms'):
platforms = host['supported_platforms']
if isinstance(platforms, str):
platforms = [p.strip() for p in platforms.split(',') if p.strip()]
content += " supported_platforms: ["
content += ", ".join([f'"{p}"' for p in platforms])
content += "]\n"
if host.get('publish'):
content += f" publish: {host['publish']}\n"
else:
# Хост по умолчанию
content += """ - name: u1
family: ubuntu22
groups: [test]
"""
# Kind clusters (для k8s preset'ов)
if kind_clusters:
content += "\nkind_clusters:\n"
for cluster in kind_clusters:
content += f" - {cluster}\n"
return content
@staticmethod
def _generate_preset_content(description: str, hosts: List[Dict]) -> str:
"""Генерация содержимого preset'а"""
return PresetService._generate_preset_content_from_form(
description=description,
docker_network="labnet",
hosts=hosts,
images=None,
systemd_defaults=None,
kind_clusters=None
)