Исправление синтаксической ошибки в molecule_executor.py и обновление k8s preset'ов

- Исправлена незакрытая скобка в _build_test_command (строка 745)
- Добавлена поддержка k8s preset'ов: выполнение create_k8s_cluster.py перед create.yml
- Обновлены образы в k8s preset'ах: заменен недоступный ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy на inecs/ansible-lab:ubuntu22-latest
- Обновлены preset'ы в базе данных через SQL
- Обновлены файлы: k8s-single.yml, k8s-multi.yml, k8s-istio-full.yml
This commit is contained in:
Сергей Антропов
2026-02-16 00:31:09 +03:00
parent 1fbf9185a2
commit d4b0d6f848
26 changed files with 1913 additions and 646 deletions

View File

@@ -11,11 +11,10 @@
{% endblock %}
{% block content %}
<div x-data="presetCreator()">
<div x-data="presetCreator()" x-init="init()">
<form
hx-post="/api/v1/presets/create"
hx-target="#result"
hx-swap="innerHTML"
hx-swap="none"
@submit.prevent="submitForm"
class="card"
>
@@ -60,6 +59,89 @@
</div>
</div>
<!-- Docker образы -->
<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 align-items-end">
<div class="col-12 col-md-4">
<label class="form-label small">Dockerfile</label>
<select
x-model="selectedDockerfile"
@change="onDockerfileSelected"
class="form-select"
:disabled="!dockerfilesLoaded"
>
<option value="">Выберите Dockerfile...</option>
<template x-for="dockerfile in dockerfiles" :key="dockerfile.id">
<option :value="dockerfile.name" x-text="dockerfile.name"></option>
</template>
</select>
<div class="form-text" x-show="!dockerfilesLoaded">
<i class="fas fa-spinner fa-spin me-1"></i>Загрузка Dockerfiles...
</div>
<div class="form-text text-danger" x-show="dockerfilesLoaded && dockerfiles.length === 0">
<i class="fas fa-exclamation-triangle me-1"></i>Dockerfiles не найдены
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label small">Значение образа</label>
<input
type="text"
x-model="newImageValue"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu25"
>
</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>
<div class="card-header">
<h5 class="mb-0">Хосты</h5>
</div>
@@ -84,18 +166,10 @@
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="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="astra">Astra Linux</option>
<option value="">Выберите образ...</option>
<template x-for="(image, key) in formData.images" :key="key">
<option :value="key" x-text="key"></option>
</template>
</select>
</div>
<div class="col-12 col-md-4">
@@ -134,15 +208,89 @@
</button>
</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"
>/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>
<!-- Скрытые поля -->
<input
type="hidden"
name="hosts"
:value="JSON.stringify(formData.hosts.map(h => ({name: h.name, family: h.family, groups: h.groups})))"
>
<!-- Результат -->
<div id="result" class="card-body border-top"></div>
<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
})"
>
<!-- Кнопки -->
<div class="card-footer">
@@ -169,15 +317,43 @@ function presetCreator() {
category: 'main',
hosts: [{
name: 'u1',
family: 'ubuntu22',
family: '',
groups_str: 'test, web',
groups: ['test', 'web']
}]
}],
images: {},
systemd_defaults: {
privileged: true,
command: '/sbin/init',
volumes_str: '/sys/fs/cgroup:/sys/fs/cgroup:rw',
volumes: ['/sys/fs/cgroup:/sys/fs/cgroup:rw'],
tmpfs_str: '/run, /run/lock',
tmpfs: ['/run', '/run/lock'],
capabilities_str: 'SYS_ADMIN',
capabilities: ['SYS_ADMIN']
}
},
dockerfiles: {{ dockerfiles | tojson }},
dockerfilesLoaded: true,
selectedDockerfile: '',
newImageKey: '',
newImageValue: '',
init() {
// Dockerfiles уже загружены из шаблона
console.log('Загружено Dockerfiles из шаблона:', this.dockerfiles.length);
},
onDockerfileSelected() {
if (this.selectedDockerfile) {
// Устанавливаем ключ образа из выбранного Dockerfile
this.newImageKey = this.selectedDockerfile;
// Автоматически подставляем значение образа в формате inecs/ansible-lab:{name}
this.newImageValue = `inecs/ansible-lab:${this.selectedDockerfile}`;
}
},
addHost() {
this.formData.hosts.push({
name: `u${this.formData.hosts.length + 1}`,
family: 'ubuntu22',
family: '',
groups_str: 'test',
groups: ['test']
});
@@ -189,10 +365,99 @@ function presetCreator() {
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 = '';
this.selectedDockerfile = '';
}
},
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);
},
submitForm(event) {
// Обновляем массивы перед отправкой
this.updateVolumes();
this.updateTmpfs();
this.updateCapabilities();
// HTMX обработает отправку
}
}
}
// Обработка ответа от HTMX для создания пресета
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('htmx:afterRequest', function(event) {
// Проверяем, что это запрос на создание пресета
if (event.detail.path === '/api/v1/presets/create') {
if (event.detail.xhr.status === 201 || event.detail.xhr.status === 200) {
try {
const response = JSON.parse(event.detail.xhr.responseText);
if (response.success) {
// Показываем модальное окно с успешным сообщением
if (window.showMessageModal) {
window.showMessageModal(
response.message || `Preset '${response.preset_name}' успешно создан`,
'success',
'Успешно',
function() {
// После закрытия модального окна перенаправляем на страницу списка пресетов
window.location.href = '/presets';
}
);
} else {
// Если функция недоступна, просто перенаправляем
window.location.href = '/presets';
}
}
} catch (e) {
console.error('Ошибка парсинга ответа:', e);
if (window.showMessageModal) {
window.showMessageModal('Ошибка при создании preset', 'error');
}
}
} else {
// Ошибка - показываем в модальном окне
try {
const response = JSON.parse(event.detail.xhr.responseText);
const errorMessage = response.detail || response.message || 'Ошибка при создании preset';
if (window.showMessageModal) {
window.showMessageModal(errorMessage, 'error');
} else {
alert(errorMessage);
}
} catch (e) {
if (window.showMessageModal) {
window.showMessageModal('Ошибка при создании preset', 'error');
} else {
alert('Ошибка при создании preset');
}
}
}
}
});
});
</script>
{% endblock %}