- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
310 lines
12 KiB
HTML
310 lines
12 KiB
HTML
{% 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="deployRunner()">
|
||
<!-- Предупреждение -->
|
||
<div class="alert alert-warning mb-3" role="alert">
|
||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||
<strong>⚠️ ВНИМАНИЕ:</strong> Вы собираетесь изменить реальные серверы! Убедитесь, что вы понимаете последствия.
|
||
</div>
|
||
|
||
<!-- Проверка наличия необходимых файлов -->
|
||
{% if not inventory_exists %}
|
||
<div class="alert alert-danger mb-3" role="alert">
|
||
<i class="fas fa-exclamation-circle me-2"></i>
|
||
<strong>❌ Inventory файл не найден!</strong>
|
||
<p class="mb-0 mt-2">Создайте файл <code>inventory/hosts.ini</code> с вашими серверами или используйте <a href="/deploy/inventory" class="alert-link">редактор inventory</a>.</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if not deploy_playbook_exists %}
|
||
<div class="alert alert-danger mb-3" role="alert">
|
||
<i class="fas fa-exclamation-circle me-2"></i>
|
||
<strong>❌ Playbook deploy.yml не найден!</strong>
|
||
<p class="mb-0 mt-2">Создайте файл <code>roles/deploy.yml</code> для развертывания ролей.</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Настройки деплоя -->
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">Настройки деплоя</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<form @submit.prevent="startDeploy">
|
||
<div class="mb-3">
|
||
<label class="form-label">Inventory файл</label>
|
||
<input
|
||
type="text"
|
||
x-model="deployConfig.inventory"
|
||
value="inventory/hosts.ini"
|
||
class="form-control"
|
||
placeholder="inventory/hosts.ini"
|
||
>
|
||
<div class="form-text">
|
||
Путь к inventory файлу относительно корня проекта
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Limit (опционально)</label>
|
||
<input
|
||
type="text"
|
||
x-model="deployConfig.limit"
|
||
class="form-control"
|
||
placeholder="webservers или host1,host2"
|
||
>
|
||
<div class="form-text">
|
||
Ограничение на хосты для деплоя
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Tags (опционально)</label>
|
||
<input
|
||
type="text"
|
||
x-model="deployConfig.tags"
|
||
class="form-control"
|
||
placeholder="web,database или оставьте пустым для всех тегов"
|
||
>
|
||
<div class="form-text">
|
||
Теги для фильтрации задач (через запятую)
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Дополнительные переменные (JSON, опционально)</label>
|
||
<textarea
|
||
x-model="deployConfig.extra_vars"
|
||
rows="3"
|
||
class="form-control font-monospace"
|
||
placeholder='{"app_version": "1.0.0", "nginx_enabled": true}'
|
||
></textarea>
|
||
<div class="form-text">
|
||
Укажите переменные в формате JSON
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input
|
||
class="form-check-input"
|
||
type="checkbox"
|
||
x-model="deployConfig.check"
|
||
id="deploy-check"
|
||
>
|
||
<label class="form-check-label" for="deploy-check">
|
||
Dry-run режим (--check) - изменения не будут применены
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
class="btn btn-primary"
|
||
:disabled="deployRunning || !inventoryExists || !deployPlaybookExists"
|
||
>
|
||
<i class="fas fa-rocket me-2" x-show="!deployRunning"></i>
|
||
<i class="fas fa-spinner fa-spin me-2" x-show="deployRunning"></i>
|
||
<span x-show="!deployRunning">Запустить деплой</span>
|
||
<span x-show="deployRunning">Деплой выполняется...</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Live логи -->
|
||
<div class="card" x-show="deployRunning || logs.length > 0">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">Логи деплоя</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="log-container" id="deploy-logs" x-ref="logContainer" style="max-height: 500px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 0.25rem; font-family: 'Courier New', monospace; font-size: 0.875rem;">
|
||
<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',
|
||
'log-success': log.type === 'success'
|
||
}"
|
||
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 deployRunner() {
|
||
return {
|
||
deployRunning: false,
|
||
logs: [],
|
||
deployConfig: {
|
||
inventory: 'inventory/hosts.ini',
|
||
limit: '',
|
||
tags: '{{ role_name }}',
|
||
extra_vars: '',
|
||
check: false
|
||
},
|
||
inventoryExists: {{ 'true' if inventory_exists else 'false' }},
|
||
deployPlaybookExists: {{ 'true' if deploy_playbook_exists else 'false' }},
|
||
ws: null,
|
||
async startDeploy() {
|
||
if (!this.inventoryExists || !this.deployPlaybookExists) {
|
||
alert('❌ Необходимые файлы не найдены!');
|
||
return;
|
||
}
|
||
|
||
this.deployRunning = true;
|
||
this.logs = [];
|
||
|
||
// Формируем deploy_id
|
||
const deployId = `deploy-{{ role_name }}-${this.deployConfig.tags || 'none'}`;
|
||
|
||
// Создание WebSocket подключения
|
||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
this.ws = new WebSocket(`${protocol}//${window.location.host}/ws/deploy/${deployId}`);
|
||
|
||
this.ws.onopen = () => {
|
||
// Отправляем параметры деплоя
|
||
this.ws.send(JSON.stringify({
|
||
type: 'start',
|
||
role_name: '{{ role_name }}',
|
||
inventory: this.deployConfig.inventory,
|
||
limit: this.deployConfig.limit || null,
|
||
tags: this.deployConfig.tags ? this.deployConfig.tags.split(',').map(t => t.trim()) : null,
|
||
check: this.deployConfig.check,
|
||
extra_vars: this.deployConfig.extra_vars ? JSON.parse(this.deployConfig.extra_vars) : null
|
||
}));
|
||
};
|
||
|
||
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.deployRunning = false;
|
||
this.logs.push({
|
||
id: Date.now(),
|
||
type: data.status === 'success' ? 'success' : 'error',
|
||
data: data.data || `✅ Деплой завершен: ${data.status}`
|
||
});
|
||
this.ws.close();
|
||
} else if (data.type === 'error') {
|
||
this.deployRunning = false;
|
||
this.logs.push({
|
||
id: Date.now(),
|
||
type: 'error',
|
||
data: data.data || `❌ Ошибка`
|
||
});
|
||
this.ws.close();
|
||
}
|
||
};
|
||
|
||
this.ws.onerror = (error) => {
|
||
this.deployRunning = false;
|
||
this.logs.push({
|
||
id: Date.now(),
|
||
type: 'error',
|
||
data: `❌ Ошибка подключения: ${error}`
|
||
});
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
this.deployRunning = false;
|
||
};
|
||
},
|
||
detectLogLevel(line) {
|
||
const lower = line.toLowerCase();
|
||
if (lower.includes('error') || lower.includes('failed') || lower.includes('fatal')) return 'error';
|
||
if (lower.includes('warning') || lower.includes('warn')) return 'warning';
|
||
if (lower.includes('changed') || lower.includes('ok') || lower.includes('success')) return 'success';
|
||
if (lower.includes('skipping') || lower.includes('ok')) return 'info';
|
||
return 'info';
|
||
},
|
||
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 = `deploy-{{ role_name }}-${Date.now()}.log`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.log-line {
|
||
margin: 0;
|
||
padding: 0.25rem 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.log-error {
|
||
color: #f48771;
|
||
}
|
||
|
||
.log-warning {
|
||
color: #dcdcaa;
|
||
}
|
||
|
||
.log-info {
|
||
color: #569cd6;
|
||
}
|
||
|
||
.log-success {
|
||
color: #4ec9b0;
|
||
}
|
||
</style>
|
||
{% endblock %}
|