Files
DevOpsLab/docs/WEB_INTERFACE_QUICKSTART.md
Сергей Антропов 1fbf9185a2 feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
2026-02-15 22:59:02 +03:00

8.2 KiB
Raw Blame History

Быстрый старт веб-интерфейса DevOpsLab

Автор: Сергей Антропов
Сайт: https://devops.org.ru

🚀 Быстрый старт

1. Создание git ветки

git checkout -b feature/web-interface
git push -u origin feature/web-interface

2. Структура проекта

app/
├── main.py              # FastAPI приложение
├── api/v1/endpoints/    # API endpoints
├── templates/           # HTMX шаблоны
├── static/              # CSS, JS
└── core/                # Ядро (MakeExecutor, DockerClient)

3. Основные компоненты

MakeExecutor - выполнение Makefile команд

# app/core/make_executor.py
import subprocess
from typing import Dict, List

class MakeExecutor:
    def execute(self, command: str, args: List[str] = None) -> Dict:
        cmd = ["make"] + command.split() + (args or [])
        result = subprocess.run(cmd, capture_output=True, text=True)
        return {
            "success": result.returncode == 0,
            "stdout": result.stdout,
            "stderr": result.stderr
        }

FastAPI роуты

# app/api/v1/endpoints/roles.py
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from app.core.make_executor import MakeExecutor

router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
executor = MakeExecutor()

@router.get("/roles", response_class=HTMLResponse)
async def list_roles(request: Request):
    # Получаем список ролей через make
    result = executor.execute("role list")
    roles = parse_roles(result["stdout"])
    return templates.TemplateResponse(
        "pages/roles/list.html",
        {"request": request, "roles": roles}
    )

@router.post("/roles/{role_name}/test")
async def test_role(role_name: str, preset: str = "default"):
    # Запуск теста
    result = executor.execute(f"role test {preset}")
    return {"status": "success" if result["success"] else "failed"}

HTMX шаблон

<!-- app/templates/pages/roles/list.html -->
{% extends "base.html" %}

{% block content %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  {% for role in roles %}
  <div class="card">
    <h3>{{ role.name }}</h3>
    <p>{{ role.description }}</p>
    <button 
      hx-post="/api/v1/roles/{{ role.name }}/test"
      hx-target="#test-results"
      class="btn-primary">
      Запустить тест
    </button>
  </div>
  {% endfor %}
</div>
{% endblock %}

4. Примеры страниц

Dashboard

<!-- app/templates/pages/dashboard.html -->
<div class="dashboard">
  <div class="stats">
    <div class="stat-card">
      <h3>Всего ролей</h3>
      <p hx-get="/api/v1/stats/roles" hx-trigger="every 5s">
        {{ total_roles }}
      </p>
    </div>
  </div>
  
  <div class="recent-tests">
    <h2>Последние тесты</h2>
    <div id="test-list" 
         hx-get="/api/v1/tests/recent" 
         hx-trigger="every 10s">
      <!-- Загружается через HTMX -->
    </div>
  </div>
</div>

Создание роли

<!-- app/templates/pages/roles/create.html -->
<form hx-post="/api/v1/roles/create" hx-target="#result">
  <div class="step" id="step-1">
    <h3>Шаг 1: Базовая информация</h3>
    <input name="name" placeholder="Имя роли" required>
    <textarea name="description" placeholder="Описание"></textarea>
    <select name="template">
      <option value="service">Service</option>
      <option value="package">Package</option>
      <option value="config">Config</option>
    </select>
    <button type="button" hx-get="/api/v1/roles/create/step/2">
      Далее
    </button>
  </div>
  
  <div class="step" id="step-2" style="display:none;">
    <h3>Шаг 2: Переменные</h3>
    <div id="variables">
      <div class="variable-row">
        <input name="var_name" placeholder="Имя переменной">
        <input name="var_value" placeholder="Значение">
        <select name="var_type">
          <option value="string">String</option>
          <option value="int">Integer</option>
          <option value="bool">Boolean</option>
        </select>
      </div>
    </div>
    <button type="button" hx-post="/api/v1/roles/create/add-variable">
      Добавить переменную
    </button>
    <button type="submit">Создать роль</button>
  </div>
</form>

5. WebSocket для live логов

# app/api/v1/endpoints/websocket.py
from fastapi import WebSocket
from app.core.make_executor import MakeExecutor

@router.websocket("/ws/test/{test_id}")
async def test_websocket(websocket: WebSocket, test_id: str):
    await websocket.accept()
    executor = MakeExecutor()
    
    # Запуск теста с потоковым выводом
    process = await executor.execute_stream(f"role test {test_id}")
    
    async for line in process.stdout:
        await websocket.send_json({
            "type": "log",
            "data": line.decode()
        })
    
    await websocket.send_json({"type": "complete"})
// app/static/js/test-live.js
const ws = new WebSocket('ws://localhost:8000/ws/test/nginx');
const logContainer = document.getElementById('test-logs');

ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === 'log') {
        logContainer.innerHTML += `<div>${data.data}</div>`;
    }
};

6. Запуск приложения

# app/main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app.api.v1.endpoints import roles, presets, tests

app = FastAPI(title="DevOpsLab Web Interface")

# Статика
app.mount("/static", StaticFiles(directory="app/static"), name="static")

# Шаблоны
templates = Jinja2Templates(directory="app/templates")

# Роуты
app.include_router(roles.router, prefix="/api/v1")
app.include_router(presets.router, prefix="/api/v1")
app.include_router(tests.router, prefix="/api/v1")

@app.get("/")
async def dashboard(request: Request):
    return templates.TemplateResponse("pages/dashboard.html", {"request": request})

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

7. Docker Compose для разработки

# docker-compose.dev.yml
version: '3.8'

services:
  web:
    build: ./app
    ports:
      - "8000:8000"
    volumes:
      - ./app:/app
      - .:/workspace
    environment:
      - PROJECT_ROOT=/workspace
    command: uvicorn main:app --reload --host 0.0.0.0

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  celery:
    build: ./app
    command: celery -A tasks.celery_tasks worker --loglevel=info
    volumes:
      - ./app:/app
      - .:/workspace
    depends_on:
      - redis

8. Команды для разработки

# Установка зависимостей
cd app
pip install -r requirements.txt

# Запуск в режиме разработки
uvicorn main:app --reload

# Запуск через Docker
docker-compose -f docker-compose.dev.yml up

# Запуск Celery worker
celery -A tasks.celery_tasks worker --loglevel=info

📋 Чек-лист реализации

  • Создать git ветку
  • Настроить структуру проекта
  • Реализовать MakeExecutor
  • Создать базовые шаблоны
  • Реализовать Dashboard
  • Реализовать список ролей
  • Реализовать создание роли
  • Реализовать тестирование роли
  • Добавить WebSocket для live логов
  • Настроить Celery для фоновых задач
  • Добавить базу данных для истории
  • Улучшить UI/UX
  • Добавить аутентификацию
  • Написать документацию

Полная версия: WEB_INTERFACE_PROPOSAL.md