test: добавить полное покрытие Molecule + HTML report генератор

Molecule тесты для всех аддонов и кластерный topology тест:

Аддоны (Helm lint + template + assertions):
- addons/technitium-dns/role/molecule/ — Primary/Secondary DNS, CronJob, kube-vip
- addons/authelia/role/molecule/ — OIDC clients, access_control, manifests
- addons/ingress-proxypass/role/molecule/ — proxies, Service/Endpoints/Ingress
- addons/ingress-add-domains/role/molecule/ — entries, Ingress per namespace
- addons/yandex-dns-controller/role/molecule/ — CronJob, ConfigMap, RBAC

Кластер:
- molecule/cluster/ — 3 master (embedded etcd HA) + 2 worker topology тест

Инфраструктура:
- scripts/molecule-report.py — генератор HTML отчётов из JUnit XML
  (читает /tmp/molecule-junit/*.xml → /tmp/molecule-report.html)
- requirements-python.txt — комментарий к отчётному блоку
- docker/entrypoint.sh — добавлены команды molecule-addon, molecule-cluster,
  molecule-report с автоматическим включением junit callback
- Makefile — targets: molecule-cluster, molecule-addon-*, molecule-addon-all,
  molecule-report; molecule-all генерирует HTML отчёт
- docs/molecule-testing.md — полная документация всех сценариев
- docs/addons.md — добавлены technitium-dns и authelia в таблицу аддонов
This commit is contained in:
Sergey Antropoff
2026-04-26 18:57:11 +03:00
parent 225f77598a
commit 91299fcc1b
25 changed files with 2376 additions and 72 deletions

View File

@@ -70,6 +70,9 @@ make addon-netbird
| ingress-proxypass | `addon_ingress_proxypass` | Проксировать внешние сервисы (IP:PORT) через ingress-nginx по домену — Service + Endpoints + Ingress | [](../addons/ingress-proxypass/README.md) |
| ingress-add-domains | `addon_ingress_add_domains` | Добавить домены к существующим K8s сервисам — только Ingress, без Service/Endpoints | [](../addons/ingress-add-domains/README.md) |
| yandex-dns-controller | `addon_yandex_dns_controller` | CronJob-контроллер Yandex 360 DNS — ConfigMap → DNS, safe mode (managed:true only) | [](../addons/yandex-dns-controller/README.md) |
| technitium-dns | `addon_technitium_dns` | HA DNS: Primary + Secondary с kube-vip LB, авто-sync зон, ExternalDNS (RFC 2136) | [](../addons/technitium-dns/README.md) |
| **SSO / Auth** | | | |
| authelia | `addon_authelia` | SSO Forward-auth + OIDC provider: Gitea, Grafana, ArgoCD, MinIO, Vault, Nextcloud | [](../addons/authelia/README.md) |
## Конфигурация addons.yml
@@ -123,6 +126,13 @@ addon_splitgw: false # sing-box + Hysteria2 TPROXY (host или k8
# ── External Services Ingress Proxy ───────────────────────────────────────────
addon_ingress_proxypass: false # проксировать внешние сервисы через ingress-nginx
addon_ingress_add_domains: false # добавить домены к существующим K8s сервисам (только Ingress)
# ── DNS ───────────────────────────────────────────────────────────────────────
addon_yandex_dns_controller: false # Yandex 360 DNS controller (managed records)
addon_technitium_dns: false # HA DNS сервер (Primary+Secondary, kube-vip)
# ── SSO ───────────────────────────────────────────────────────────────────────
addon_authelia: false # SSO Forward-auth + OIDC (Gitea/Grafana/ArgoCD/...)
```
## Зависимости между аддонами
@@ -144,6 +154,9 @@ addon_ingress_add_domains: false # добавить домены к с
| `mediaserver` | `csi-nfs` (рекомендуется) | Shared PVC требует RWX StorageClass |
| `splitgw` | Hysteria2 сервер (vault_hysteria2_url) | URL из Shadowrocket / NekoBox |
| `ingress-proxypass` | `ingress-nginx` | Требует работающий Ingress controller |
| `ingress-add-domains` | `ingress-nginx` | Требует работающий Ingress controller |
| `technitium-dns` | kube-vip | LoadBalancer IP через kube-vip аннотацию |
| `authelia` | `ingress-nginx` | Forward-auth через annotations; PostgreSQL/Redis — опционально |
## MediaServer

View File

@@ -1,6 +1,6 @@
# Тестирование через Molecule
Molecule — стандартный инструмент для тестирования Ansible ролей. Каждая роль запускается в Docker-контейнерах, проходит набор автоматических проверок и удаляется. Реальные серверы не нужны.
Molecule — стандартный инструмент для тестирования Ansible ролей и аддонов. Каждый сценарий запускается в Docker-контейнерах, проходит набор автоматических проверок и удаляется. Реальные серверы не нужны.
## Требования
@@ -8,38 +8,95 @@ Molecule — стандартный инструмент для тестиров
```bash
make build # собрать образ (один раз)
make molecule-k3s # запустить тест
make molecule-k3s # запустить тест роли k3s
```
Как это работает: `make molecule-*` запускает контейнер `k3s-ansible` с примонтированным Docker socket. Внутри этого контейнера Molecule создаёт тестовые контейнеры — Docker внутри Docker без демона (только socket от хоста).
Как это работает: `make molecule-*` запускает контейнер `k3s-ansible` с примонтированным Docker socket. Внутри этого контейнера Molecule создаёт тестовые контейнеры — Docker-in-Docker без демона (только socket от хоста).
## Доступные тесты
---
## Все доступные тесты
```bash
make molecule-k3s # Роль k3s — 3 ноды: master01, worker01, rpi01 (~8-12 мин)
make molecule-prometheus # Роль prometheus-stack (~2-3 мин)
make molecule-istio # Роль istio (~2-3 мин)
make molecule-all # Все тесты последовательно (~15-20 мин)
make molecule-lint # Линтинг YAML + ansible-lint (~30 сек)
# ─── Роли ──────────────────────────────────────────────────────────────────
make molecule-k3s # роль k3s — 3 ноды (master + worker + Debian), ~8-12 мин
make molecule-prometheus # роль prometheus-stack (Helm values шаблоны), ~2-3 мин
make molecule-istio # роль istio + kiali (4 шаблона), ~2-3 мин
# ─── Кластер (3 master + 2 worker, embedded etcd HA) ───────────────────────
make molecule-cluster # topology тест, ~15-20 мин
# ─── Аддоны (Helm lint + template rendering) ───────────────────────────────
make molecule-addon-technitium-dns # ~2-3 мин
make molecule-addon-authelia # ~2-3 мин
make molecule-addon-ingress-proxypass # ~2 мин
make molecule-addon-ingress-add-domains # ~2 мин
make molecule-addon-yandex-dns-controller # ~2 мин
# ─── Группы ────────────────────────────────────────────────────────────────
make molecule-addon-all # все аддоны последовательно, ~15 мин
make molecule-all # всё: роли + аддоны + HTML отчёт, ~40 мин
# ─── Отчёт и линтинг ───────────────────────────────────────────────────────
make molecule-report # генерировать HTML из /tmp/molecule-junit/*.xml
make molecule-lint # yamllint + ansible-lint в контейнере
```
---
## HTML отчёт
После каждого запуска `make molecule-all` автоматически генерируется HTML отчёт. Для ручного запуска:
```bash
make molecule-report
```
Отчёт по умолчанию создаётся в `/tmp/molecule-report.html`. Чтобы открыть:
```bash
open /tmp/molecule-report.html
```
### Как работает
Ansible callback `junit` записывает результаты каждой задачи в XML файлы (`/tmp/molecule-junit/*.xml`). Скрипт `scripts/molecule-report.py` читает эти XML и генерирует один HTML с:
- Общей статистикой: Total / Passed / Failed / Skipped
- Таблицей по каждому сценарию (Expand / Collapse)
- Подробностями ошибок — текст failure прямо в строке задачи
- Авто-раскрытием упавших сценариев
Для включения JUnit callback задай переменные окружения перед запуском:
```bash
export JUNIT_OUTPUT_DIR=/tmp/molecule-junit
export ANSIBLE_CALLBACKS_ENABLED=junit
```
Команды `molecule-addon` и `molecule-cluster` в `docker/entrypoint.sh` задают их автоматически.
---
## Жизненный цикл теста
```
dependency → зависимые роли
lint → yamllint + ansible-lint
syntax → ansible-playbook --syntax-check
create → запустить Docker-контейнер
prepare → подготовить контейнер (Python, collections)
create → запустить Docker-контейнер(ы)
prepare → подготовить контейнер
converge → выполнить тестируемые задачи
idempotency → повторный converge (проверка изменений = 0)
idempotency → повторный converge (проверка: changed=0)
verify → assertions (проверить результаты)
destroy → удалить контейнер
destroy → удалить контейнер(ы)
```
При ошибке на любой фазе тест падает, контейнер удаляется автоматически.
При ошибке на любой фазе тест падает, контейнеры удаляются автоматически.
## Что тестирует каждая роль
---
## Описание тестов
### Роль `k3s` (3 контейнера, ~8-12 мин)
@@ -61,6 +118,30 @@ destroy → удалить контейнер
| `disable: [traefik]` | все | Traefik выключен |
| `net.ipv4.ip_forward = 1` | все | sysctl применён |
### Кластер (`molecule/cluster/`, ~15-20 мин)
Тестирует **topology HA кластера**: 3 master (embedded etcd) + 2 worker.
| Контейнер | Роль | Образ |
|---|---|---|
| `master01` | Primary master (cluster-init) | `geerlingguy/docker-ubuntu2204-ansible` (privileged) |
| `master02` | HA master (join) | `geerlingguy/docker-ubuntu2204-ansible` (privileged) |
| `master03` | HA master (join) | `geerlingguy/docker-ubuntu2204-ansible` (privileged) |
| `worker01` | Worker | `geerlingguy/docker-ubuntu2204-ansible` |
| `worker02` | Worker | `geerlingguy/docker-ubuntu2204-ansible` |
Что проверяет `verify.yml`:
| Проверка | Нода | Что именно |
|---|---|---|
| `cluster-init: true` | master01 | Только первый мастер инициализирует |
| нет ключа `server:` | master01 | Первый мастер не join-ит |
| `server: https://192.168.1.100:6443` | master02, master03 | Join через kube-vip VIP |
| нет `cluster-init` | master02, master03 | Не инициализируют заново |
| `server: https://192.168.1.100:6443` | worker01, worker02 | Подключаются через VIP |
| kube-vip DaemonSet manifest | все master | Файл `/var/lib/rancher/k3s/server/manifests/kube-vip.yaml` |
| VIP `192.168.1.100` | kube-vip manifest | Правильный IP в аннотации |
### Роль `prometheus-stack` (~2-3 мин)
Тестирует рендеринг Jinja2-шаблона без privileged режима.
@@ -84,6 +165,75 @@ destroy → удалить контейнер
| `peer-authentication.yaml` | `kind: PeerAuthentication`, `spec.mtls.mode: STRICT` |
| `kiali-token-secret.yaml` | `type: kubernetes.io/service-account-token` |
### Аддон `technitium-dns` (~2-3 мин)
Helm lint + `helm template` → assertions по Kubernetes манифестам.
| Проверка | Что именно |
|---|---|
| `primary.ip == 192.168.1.53` | values.yaml.j2 рендер |
| `secondary.enabled == true` | secondary DNS включён |
| `dns.forwarders` содержит `1.1.1.1`, `8.8.8.8` | форвардеры |
| `sync.schedule == '*/5 * * * *'` | расписание синхронизации |
| `kind: Deployment` (primary + secondary) | оба Deployment рендерятся |
| `kind: CronJob` | CronJob синхронизации зон |
| `def main` в ConfigMap | sync.py вложен |
| `kube-vip.io/loadbalancerIPs` | аннотация kube-vip |
| `LoadBalancer` | тип Service |
| `port: 53` | DNS порт |
### Аддон `authelia` (~2-3 мин)
| Проверка | Что именно |
|---|---|
| `v.oidc.enabled == true` | OIDC включён |
| Gitea/Grafana clients `enabled: true` | OIDC клиенты |
| `protectedDomains` содержит sonarr, radarr | access control |
| `oidcDomains` содержит gitea, grafana | bypass для OIDC |
| `adminDomains` содержит argocd, vault | admin-only домены |
| `kind: Deployment` | Authelia Deployment |
| `configuration.yml` в Secret | конфиг смонтирован |
| `identity_providers` в манифесте | OIDC секция рендерится |
| `AUTHELIA_JWT_SECRET_FILE` | env var из Secret |
| `authelia.authelia.svc.cluster.local` | URL forward-auth |
### Аддон `ingress-proxypass` (~2 мин)
| Проверка | Что именно |
|---|---|
| `defaults.ingressClass == nginx` | значение по умолчанию |
| 2 proxy записи | plex + router |
| `plex.home.local` в Ingress | хост plex |
| `router` auth `enabled: true` | basic auth включён |
| `kind: Service`, `kind: Endpoints` | созданы для каждого proxy |
| `kind: Secret` | htpasswd Secret для router |
| `proxy-connect-timeout` аннотация | timeout аннотации |
### Аддон `ingress-add-domains` (~2 мин)
| Проверка | Что именно |
|---|---|
| 2 entry записи | gitea + grafana |
| `gitea.home.local` в Ingress | хост gitea |
| `namespace: gitea` | Ingress в namespace сервиса |
| `gitea-http` в backend | правильное имя Service |
| `kind: Secret` | htpasswd Secret для grafana |
| `auth-type` аннотация | basic auth аннотация |
### Аддон `yandex-dns-controller` (~2 мин)
| Проверка | Что именно |
|---|---|
| `controller.schedule == '*/5 * * * *'` | расписание |
| `secret.orgId`, `secret.token` | API credentials |
| `zones.domains[0].name == home.local` | конфигурация зоны |
| `kind: CronJob` | CronJob рендерится |
| `def main` в ConfigMap | controller.py вложен |
| `kind: Secret` | API credentials в Secret |
| `kind: ServiceAccount`, `kind: Role` | RBAC |
---
## Запуск тестов пошагово
### Линтинг (~30 сек)
@@ -92,7 +242,7 @@ destroy → удалить контейнер
make molecule-lint
```
Запускает `yamllint .` и `ansible-lint` на всём проекте. Используй перед каждым коммитом.
Запускает `yamllint .` и `ansible-lint` на всём проекте.
### Тест одной роли
@@ -105,20 +255,7 @@ make molecule-k3s
INFO Running default > create
TASK [Create instance(s)] ****
changed: [localhost] => (item=master01)
changed: [localhost] => (item=worker01)
changed: [localhost] => (item=rpi01)
INFO Running default > converge
TASK [Test server config template rendering] ***
changed: [master01]
INFO Running default > idempotency
PLAY RECAP
master01 : ok=12 changed=0 unreachable=0 failed=0
worker01 : ok=12 changed=0 unreachable=0 failed=0
rpi01 : ok=12 changed=0 unreachable=0 failed=0
INFO Idempotency completed successfully.
...
INFO Running default > verify
TASK [Assert cluster-init is set (только master01)] ***
@@ -128,6 +265,30 @@ INFO Running default > destroy
✓ k3s role: OK
```
### Тест кластера (3 master + 2 worker)
```bash
make molecule-cluster
```
### Тест аддона
```bash
make molecule-addon-authelia
```
Аддон-тесты используют `delegate_to: localhost` для `helm lint` и `helm template` — команды выполняются прямо в runner-контейнере (где есть Helm), результат сохраняется в тестовый контейнер.
### Все тесты + HTML отчёт
```bash
make molecule-all
```
Запускает все сценарии последовательно и генерирует `open /tmp/molecule-report.html`.
---
## Отладка упавших тестов
```bash
@@ -138,84 +299,130 @@ docker run --rm -it \
--entrypoint bash \
k3s-ansible
# Внутри — перейти к нужной роли:
cd /ansible/roles/k3s # или roles/prometheus-stack / addons/jenkins/role
# Роль:
cd /ansible/roles/k3s
molecule converge
molecule login
molecule login --host master01
molecule verify
molecule converge -- -vvv
molecule destroy
# Запускать фазы вручную:
molecule converge # создать контейнер и запустить задачи
molecule login # войти в тестовый контейнер
molecule login --host master01 # конкретная нода
molecule verify # только assertions
molecule converge -- -vvv # подробный вывод
molecule destroy # удалить тестовые контейнеры
# Аддон:
cd /ansible/addons/authelia/role
molecule converge
molecule login
molecule verify
# Отдельные фазы:
molecule lint
molecule syntax
molecule create
molecule prepare
molecule idempotency
# Кластер:
cd /ansible
molecule test -s cluster
molecule converge -s cluster
molecule verify -s cluster
molecule destroy -s cluster
```
## Написание новых тестов
---
### molecule.yml
## Написание новых тестов для аддонов
### molecule.yml (шаблон)
```yaml
driver:
name: docker
platforms:
- name: my-test-instance
- name: master01
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
privileged: true # нужно для sysctl, модулей ядра
groups:
- k3s_master
provisioner:
name: ansible
playbooks:
converge: converge.yml
verify: verify.yml
config_options:
defaults:
interpreter_python: auto_silent
verifier:
name: ansible
```
### converge.yml
### converge.yml (шаблон для аддона с Helm chart)
```yaml
- name: Converge
- name: Converge — my-addon template tests
hosts: all
become: true
become: false
gather_facts: false
vars:
kube_vip_address: "192.168.1.100" # все нужные переменные здесь
k3s_version: "v1.29.3+k3s1"
my_addon_namespace: my-addon
# ... все переменные аддона ...
tasks:
- name: Render template
- name: Render Helm values
ansible.builtin.template:
src: "{{ playbook_dir }}/../../templates/my-template.j2"
dest: /tmp/result.yaml
src: "{{ playbook_dir }}/../../templates/values.yaml.j2"
dest: /tmp/my-addon-values.yaml
mode: "0644"
- name: Run helm lint
ansible.builtin.command: >
helm lint /ansible/addons/my-addon/role/chart
--values /tmp/my-addon-values.yaml --strict
delegate_to: localhost
changed_when: false
- name: Run helm template
ansible.builtin.command: >
helm template my-addon /ansible/addons/my-addon/role/chart
--values /tmp/my-addon-values.yaml --namespace my-addon
delegate_to: localhost
changed_when: false
register: helm_template_result
- name: Save manifests
ansible.builtin.copy:
content: "{{ helm_template_result.stdout }}"
dest: /tmp/my-addon-manifests.yaml
mode: "0644"
```
### verify.yml
### verify.yml (шаблон)
```yaml
- name: Verify
- name: Verify — my-addon templates
hosts: all
tasks:
- name: Read result
- name: Read values
ansible.builtin.slurp:
src: /tmp/result.yaml
register: raw
src: /tmp/my-addon-values.yaml
register: values_raw
- name: Parse YAML
- name: Parse values
ansible.builtin.set_fact:
result: "{{ raw.content | b64decode | from_yaml }}"
v: "{{ values_raw.content | b64decode | from_yaml }}"
- name: Assert key exists
- name: Assert key value
ansible.builtin.assert:
that:
- result.myKey == 'expected-value'
- result.enabled == true
fail_msg: "Ожидалось 'expected-value', получено: {{ result.myKey }}"
that: v.myKey == 'expected'
fail_msg: "myKey неверный: {{ v.myKey }}"
- name: Read manifests
ansible.builtin.slurp:
src: /tmp/my-addon-manifests.yaml
register: manifests_raw
- name: Assert Deployment rendered
ansible.builtin.assert:
that: "'kind: Deployment' in (manifests_raw.content | b64decode)"
fail_msg: "Deployment не найден"
```
---
## Типичные ошибки
| Ошибка | Причина | Решение |
@@ -226,3 +433,6 @@ verifier:
| `yamllint: wrong indentation` | Ошибка отступа | Исправь файл, `make molecule-lint` |
| `ansible-lint: no-changed-when` | shell/command без changed_when | Добавь `changed_when: <условие>` |
| `sysctl: Operation not permitted` | Нет privileged | Добавь `privileged: true` в molecule.yml |
| `helm: command not found` | helm в тестовом контейнере | Используй `delegate_to: localhost` для helm команд |
| JUnit XML не создаётся | Callback не активирован | Задай `ANSIBLE_CALLBACKS_ENABLED=junit` и `JUNIT_OUTPUT_DIR` |
| HTML отчёт пустой | XML файлов нет | Убедись что тесты запускались через `make molecule-*` |