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,518 @@
{% extends "base.html" %}
{% block title %}Редактировать {{ preset.name }} - DevOpsLab{% endblock %}
{% block page_title %}Редактирование preset'а: {{ preset.name }}{% endblock %}
{% block header_actions %}
<a href="/presets/{{ preset.name }}?category={{ preset.category }}" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад
</a>
{% endblock %}
{% block content %}
<div x-data="presetEditor()">
<form
hx-post="/api/v1/presets/{{ preset.name }}/update?category={{ preset.category }}"
hx-target="#result"
hx-swap="innerHTML"
@submit.prevent="submitForm"
class="card"
>
<!-- Базовая информация -->
<div class="card-header">
<h5 class="mb-0">Базовая информация</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Имя preset'а</label>
<input
type="text"
value="{{ preset.name }}"
class="form-control"
readonly
disabled
>
<div class="form-text">Имя preset'а нельзя изменить</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea
name="description"
x-model="formData.description"
rows="2"
class="form-control"
placeholder="Описание preset'а..."
>{{ preset.data.description if preset.data and preset.data.description else '' }}</textarea>
</div>
<div class="mb-3">
<label class="form-label">Категория</label>
<input
type="text"
value="{{ preset.category }}"
class="form-control"
readonly
disabled
>
<div class="form-text">Категорию нельзя изменить</div>
</div>
<div class="mb-3">
<label class="form-label">Docker Network</label>
<input
type="text"
name="docker_network"
x-model="formData.docker_network"
class="form-control"
placeholder="labnet"
>
</div>
</div>
<!-- Хосты -->
<div class="card-header">
<h5 class="mb-0">Хосты</h5>
</div>
<div class="card-body">
<div class="space-y-2 mb-3" x-ref="hostsContainer">
<template x-for="(host, index) in formData.hosts" :key="index">
<div class="card mb-2">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-md-2">
<label class="form-label small">Имя хоста</label>
<input
type="text"
x-model="host.name"
placeholder="u1"
class="form-control"
required
>
</div>
<div class="col-12 col-md-3">
<label class="form-label small">Семейство образа</label>
<select
x-model="host.family"
class="form-select"
>
<option value="ubuntu20">Ubuntu 20</option>
<option value="ubuntu22">Ubuntu 22</option>
<option value="ubuntu24">Ubuntu 24</option>
<option value="debian9">Debian 9</option>
<option value="debian10">Debian 10</option>
<option value="debian11">Debian 11</option>
<option value="debian12">Debian 12</option>
<option value="centos7">CentOS 7</option>
<option value="centos8">CentOS 8</option>
<option value="centos9">CentOS 9</option>
<option value="alma">AlmaLinux</option>
<option value="rocky">Rocky Linux</option>
<option value="rhel">RHEL</option>
<option value="redos">RedOS</option>
<option value="astra">Astra Linux</option>
<option value="alt9">Alt Linux 9</option>
<option value="alt10">Alt Linux 10</option>
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label small">Группы (через запятую)</label>
<input
type="text"
x-model="host.groups_str"
placeholder="test, web"
class="form-control"
@input="updateHostGroups(index)"
>
</div>
<div class="col-12 col-md-2">
<label class="form-label small">Тип (опционально)</label>
<select
x-model="host.type"
class="form-select"
>
<option value="">Обычный</option>
<option value="dind">DinD</option>
<option value="dood">DOoD</option>
</select>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeHost(index)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</template>
</div>
<button
type="button"
@click="addHost"
class="btn btn-outline-secondary"
>
<i class="fas fa-plus me-2"></i>
Добавить хост
</button>
</div>
<!-- Образы -->
<div class="card-header">
<h5 class="mb-0">Docker образы</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="space-y-2" x-ref="imagesContainer">
<template x-for="(image, key) in formData.images" :key="key">
<div class="row g-2 mb-2 align-items-center">
<div class="col-12 col-md-4">
<input
type="text"
:value="key"
class="form-control"
readonly
disabled
>
</div>
<div class="col-12 col-md-6">
<input
type="text"
x-model="formData.images[key]"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu22-latest"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeImage(key)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</template>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-12 col-md-4">
<input
type="text"
x-model="newImageKey"
class="form-control"
placeholder="Новый ключ (ubuntu25)"
>
</div>
<div class="col-12 col-md-6">
<input
type="text"
x-model="newImageValue"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu25-latest"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="addImage"
class="btn btn-outline-primary w-100"
>
<i class="fas fa-plus"></i>
</button>
</div>
</div>
</div>
<!-- Systemd Defaults -->
<div class="card-header">
<h5 class="mb-0">Systemd Defaults</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
x-model="formData.systemd_defaults.privileged"
id="privileged"
>
<label class="form-check-label" for="privileged">
Privileged
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Command</label>
<input
type="text"
x-model="formData.systemd_defaults.command"
class="form-control"
placeholder="/sbin/init"
>
</div>
<div class="col-12">
<label class="form-label">Volumes (по одному на строку)</label>
<textarea
x-model="formData.systemd_defaults.volumes_str"
@input="updateVolumes"
rows="3"
class="form-control"
placeholder="/sys/fs/cgroup:/sys/fs/cgroup:rw"
></textarea>
</div>
<div class="col-12">
<label class="form-label">Tmpfs (через запятую)</label>
<input
type="text"
x-model="formData.systemd_defaults.tmpfs_str"
@input="updateTmpfs"
class="form-control"
placeholder="/run, /run/lock"
>
</div>
<div class="col-12">
<label class="form-label">Capabilities (через запятую)</label>
<input
type="text"
x-model="formData.systemd_defaults.capabilities_str"
@input="updateCapabilities"
class="form-control"
placeholder="SYS_ADMIN"
>
</div>
</div>
</div>
{% if preset.category == 'k8s' %}
<!-- Kind Clusters (только для k8s) -->
<div class="card-header">
<h5 class="mb-0">Kind Clusters</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="space-y-2" x-ref="clustersContainer">
<template x-for="(cluster, index) in formData.kind_clusters" :key="index">
<div class="row g-2 mb-2 align-items-center">
<div class="col-12 col-md-10">
<input
type="text"
x-model="formData.kind_clusters[index]"
class="form-control"
placeholder="cluster-name"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeCluster(index)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</template>
</div>
</div>
<button
type="button"
@click="addCluster"
class="btn btn-outline-secondary"
>
<i class="fas fa-plus me-2"></i>
Добавить кластер
</button>
</div>
{% endif %}
<!-- Скрытые поля для отправки -->
<input
type="hidden"
name="hosts"
:value="JSON.stringify(formData.hosts.map(h => ({
name: h.name,
family: h.family,
groups: h.groups,
type: h.type || undefined,
supported_platforms: h.supported_platforms || undefined
})))"
>
<input
type="hidden"
name="images"
:value="JSON.stringify(formData.images)"
>
<input
type="hidden"
name="systemd_defaults"
:value="JSON.stringify({
privileged: formData.systemd_defaults.privileged,
command: formData.systemd_defaults.command,
volumes: formData.systemd_defaults.volumes,
tmpfs: formData.systemd_defaults.tmpfs,
capabilities: formData.systemd_defaults.capabilities
})"
>
{% if preset.category == 'k8s' %}
<input
type="hidden"
name="kind_clusters"
:value="JSON.stringify(formData.kind_clusters)"
>
{% endif %}
<!-- Результат -->
<div id="result" class="card-body border-top"></div>
<!-- Кнопки -->
<div class="card-footer">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить изменения
</button>
<a href="/presets/{{ preset.name }}?category={{ preset.category }}" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>
Отмена
</a>
</div>
</div>
</form>
</div>
<script>
function presetEditor() {
// Парсинг данных preset'а из шаблона
const presetData = {% if preset.data %}{{ preset.data | tojson }}{% else %}{}{% endif %};
// Инициализация хостов
const hosts = presetData.hosts || [];
const formattedHosts = hosts.map(host => ({
name: host.name || '',
family: host.family || 'ubuntu22',
groups: host.groups || [],
groups_str: Array.isArray(host.groups) ? host.groups.join(', ') : (host.groups || ''),
type: host.type || '',
supported_platforms: host.supported_platforms || []
}));
// Инициализация образов
const images = presetData.images || {};
// Инициализация systemd defaults
const systemdDefaults = presetData.systemd_defaults || {};
const volumes = systemdDefaults.volumes || [];
const tmpfs = systemdDefaults.tmpfs || [];
const capabilities = systemdDefaults.capabilities || [];
// Извлечение описания из комментария в content
let description = '';
{% if preset.content %}
const contentLines = {{ preset.content | tojson }}.split('\n');
for (const line of contentLines) {
if (line.trim().startsWith('#description:')) {
description = line.split('#description:')[1].trim();
break;
}
}
{% endif %}
return {
formData: {
description: description,
docker_network: presetData.docker_network || 'labnet',
hosts: formattedHosts.length > 0 ? formattedHosts : [{
name: 'u1',
family: 'ubuntu22',
groups: ['test'],
groups_str: 'test',
type: ''
}],
images: images,
systemd_defaults: {
privileged: systemdDefaults.privileged !== undefined ? systemdDefaults.privileged : true,
command: systemdDefaults.command || '/sbin/init',
volumes: volumes,
volumes_str: volumes.join('\n'),
tmpfs: tmpfs,
tmpfs_str: tmpfs.join(', '),
capabilities: capabilities,
capabilities_str: capabilities.join(', ')
},
kind_clusters: presetData.kind_clusters || []
},
newImageKey: '',
newImageValue: '',
addHost() {
this.formData.hosts.push({
name: `u${this.formData.hosts.length + 1}`,
family: 'ubuntu22',
groups: ['test'],
groups_str: 'test',
type: ''
});
},
removeHost(index) {
this.formData.hosts.splice(index, 1);
},
updateHostGroups(index) {
const host = this.formData.hosts[index];
host.groups = host.groups_str.split(',').map(g => g.trim()).filter(g => g);
},
addImage() {
if (this.newImageKey && this.newImageValue) {
this.formData.images[this.newImageKey] = this.newImageValue;
this.newImageKey = '';
this.newImageValue = '';
}
},
removeImage(key) {
delete this.formData.images[key];
},
updateVolumes() {
this.formData.systemd_defaults.volumes =
this.formData.systemd_defaults.volumes_str
.split('\n')
.map(v => v.trim())
.filter(v => v);
},
updateTmpfs() {
this.formData.systemd_defaults.tmpfs =
this.formData.systemd_defaults.tmpfs_str
.split(',')
.map(t => t.trim())
.filter(t => t);
},
updateCapabilities() {
this.formData.systemd_defaults.capabilities =
this.formData.systemd_defaults.capabilities_str
.split(',')
.map(c => c.trim())
.filter(c => c);
},
addCluster() {
this.formData.kind_clusters.push('');
},
removeCluster(index) {
this.formData.kind_clusters.splice(index, 1);
},
submitForm(event) {
// HTMX обработает отправку
}
}
}
</script>
{% endblock %}