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,244 @@
{% extends "base.html" %}
{% block title %}Тестирование {{ role_name }} - DevOpsLab{% endblock %}
{% block page_title %}Тестирование роли: {{ role_name }}{% endblock %}
{% block header_actions %}
<a href="/roles/{{ role_name }}" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад
</a>
{% endblock %}
{% block content %}
<div x-data="testRunner()">
<!-- Настройки теста -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Настройки теста</h5>
</div>
<div class="card-body">
<form @submit.prevent="startTest">
<div class="mb-3">
<label class="form-label">Preset для тестирования</label>
<select
x-model="testConfig.preset"
class="form-select"
>
{% for preset in presets %}
<option value="{{ preset.name }}" data-category="{{ preset.category }}">
{{ preset.name }}
{% if preset.category == 'k8s' %}
<span class="badge bg-info">k8s</span>
{% endif %}
</option>
{% endfor %}
{% if not presets %}
<option value="default">default</option>
{% endif %}
</select>
<div class="form-text">
Выберите preset для тестирования. Preset'ы загружаются из базы данных.
</div>
</div>
<div class="mb-3">
<label class="form-label">Переменные роли (JSON, опционально)</label>
<textarea
x-model="testConfig.variables"
rows="4"
class="form-control font-monospace"
placeholder='{"nginx_version": "1.25.0", "nginx_enabled": true}'
></textarea>
<div class="form-text">
Укажите переменные в формате JSON
</div>
</div>
<div class="mb-3">
<label class="form-label">Опции</label>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
x-model="testConfig.lint"
id="test-lint"
>
<label class="form-check-label" for="test-lint">
Проверка синтаксиса (lint)
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
x-model="testConfig.idempotency"
id="test-idempotency"
>
<label class="form-check-label" for="test-idempotency">
Проверка идемпотентности
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
x-model="testConfig.verbose"
id="test-verbose"
>
<label class="form-check-label" for="test-verbose">
Verbose режим
</label>
</div>
</div>
<button
type="submit"
class="btn btn-success"
:disabled="testRunning"
>
<i class="fas fa-play me-2" x-show="!testRunning"></i>
<i class="fas fa-spinner fa-spin me-2" x-show="testRunning"></i>
<span x-show="!testRunning">Запустить тест</span>
<span x-show="testRunning">Тест выполняется...</span>
</button>
</form>
</div>
</div>
<!-- Live логи -->
<div class="card" x-show="testRunning || logs.length > 0">
<div class="card-header">
<h5 class="mb-0">Логи тестирования</h5>
</div>
<div class="card-body">
<div class="log-container" id="test-logs" x-ref="logContainer" style="max-height: 500px; overflow-y: auto;">
<template x-for="log in logs" :key="log.id">
<div
class="log-line"
:class="{
'log-error': log.type === 'error',
'log-warning': log.type === 'warning',
'log-info': log.type === 'info'
}"
x-text="log.data"
></div>
</template>
</div>
<div class="mt-3 d-flex gap-2">
<button
@click="clearLogs"
class="btn btn-outline-secondary"
>
<i class="fas fa-trash me-2"></i>
Очистить
</button>
<button
@click="downloadLogs"
class="btn btn-outline-secondary"
>
<i class="fas fa-download me-2"></i>
Скачать логи
</button>
</div>
</div>
</div>
</div>
<script>
function testRunner() {
return {
testRunning: false,
logs: [],
testConfig: {
preset: '{{ presets[0].name if presets else "default" }}',
variables: '',
lint: true,
idempotency: false,
verbose: false
},
ws: null,
async startTest() {
this.testRunning = true;
this.logs = [];
// Получаем категорию preset'а из выбранного option
const presetSelect = document.querySelector('select[x-model="testConfig.preset"]');
const selectedOption = presetSelect.options[presetSelect.selectedIndex];
const presetCategory = selectedOption ? (selectedOption.dataset.category || 'main') : 'main';
// Создание WebSocket подключения
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const testId = '{{ role_name }}-' + this.testConfig.preset + '-' + presetCategory;
this.ws = new WebSocket(`${protocol}//${window.location.host}/ws/test/${testId}`);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log' || data.type === 'info') {
this.logs.push({
id: Date.now() + Math.random(),
type: data.level || this.detectLogLevel(data.data),
data: data.data
});
// Автоскролл
this.$nextTick(() => {
const container = this.$refs.logContainer;
if (container) {
container.scrollTop = container.scrollHeight;
}
});
} else if (data.type === 'complete') {
this.testRunning = false;
this.logs.push({
id: Date.now(),
type: 'info',
data: data.data || `✅ Тест завершен: ${data.status}`
});
this.ws.close();
} else if (data.type === 'error') {
this.testRunning = false;
this.logs.push({
id: Date.now(),
type: 'error',
data: data.data || `❌ Ошибка`
});
this.ws.close();
}
};
this.ws.onerror = (error) => {
this.testRunning = false;
this.logs.push({
id: Date.now(),
type: 'error',
data: `❌ Ошибка подключения: ${error}`
});
};
},
detectLogLevel(line) {
const lower = line.toLowerCase();
if (lower.includes('error') || lower.includes('failed')) return 'error';
if (lower.includes('warning') || lower.includes('warn')) return 'warning';
if (lower.includes('changed') || lower.includes('ok')) return 'info';
return 'debug';
},
clearLogs() {
this.logs = [];
},
downloadLogs() {
const content = this.logs.map(l => l.data).join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `test-{{ role_name }}-${Date.now()}.log`;
a.click();
URL.revokeObjectURL(url);
}
}
}
</script>
{% endblock %}