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 в таблицу аддонов
17 KiB
Тестирование через Molecule
Molecule — стандартный инструмент для тестирования Ansible ролей и аддонов. Каждый сценарий запускается в Docker-контейнерах, проходит набор автоматических проверок и удаляется. Реальные серверы не нужны.
Требования
Только Docker — Molecule, Python и зависимости уже внутри образа k3s-ansible.
make build # собрать образ (один раз)
make molecule-k3s # запустить тест роли k3s
Как это работает: make molecule-* запускает контейнер k3s-ansible с примонтированным Docker socket. Внутри этого контейнера Molecule создаёт тестовые контейнеры — Docker-in-Docker без демона (только socket от хоста).
Все доступные тесты
# ─── Роли ──────────────────────────────────────────────────────────────────
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 отчёт. Для ручного запуска:
make molecule-report
Отчёт по умолчанию создаётся в /tmp/molecule-report.html. Чтобы открыть:
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 задай переменные окружения перед запуском:
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 → подготовить контейнер
converge → выполнить тестируемые задачи
idempotency → повторный converge (проверка: changed=0)
verify → assertions (проверить результаты)
destroy → удалить контейнер(ы)
При ошибке на любой фазе тест падает, контейнеры удаляются автоматически.
Описание тестов
Роль k3s (3 контейнера, ~8-12 мин)
| Контейнер | Образ | Соответствует |
|---|---|---|
master01 |
geerlingguy/docker-ubuntu2204-ansible |
Ubuntu 22.04 x86_64 |
worker01 |
geerlingguy/docker-ubuntu2204-ansible |
Ubuntu 22.04 x86_64 |
rpi01 |
geerlingguy/docker-debian12-ansible |
Debian 12 (Raspberry Pi OS) |
Что проверяет verify.yml:
| Проверка | Нода | Что именно |
|---|---|---|
/etc/kubernetes/k3s существует |
все | Директория создана |
config.yaml существует, права 0600 |
все | Файл конфига |
cluster-init: true |
master01 | Первый мастер инициализирует |
server: https://...:6443 |
worker01, rpi01 | Присоединяются к master01 |
cluster-cidr: 10.42.0.0/16 |
все | Правильная подсеть |
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 режима.
| Проверка | Что именно |
|---|---|
grafana.adminUser |
Значение переменной |
grafana.persistence.enabled |
true |
prometheus.retention |
7d |
alertmanager.enabled |
true |
nodeExporter.enabled |
true |
Роль istio (~2-3 мин)
Тестирует рендеринг всех четырёх шаблонов роли.
| Файл | Проверка |
|---|---|
istiod-values.yaml |
Ресурсы pilot, meshConfig, enablePrometheusMerge: true |
kiali-values.yaml |
auth.strategy: token, Prometheus URL |
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 сек)
make molecule-lint
Запускает yamllint . и ansible-lint на всём проекте.
Тест одной роли
make molecule-k3s
Ожидаемый вывод (успешный):
INFO Running default > create
TASK [Create instance(s)] ****
changed: [localhost] => (item=master01)
...
INFO Running default > verify
TASK [Assert cluster-init is set (только master01)] ***
ok: [master01] => {"msg": "All assertions passed"}
INFO Running default > destroy
✓ k3s role: OK
Тест кластера (3 master + 2 worker)
make molecule-cluster
Тест аддона
make molecule-addon-authelia
Аддон-тесты используют delegate_to: localhost для helm lint и helm template — команды выполняются прямо в runner-контейнере (где есть Helm), результат сохраняется в тестовый контейнер.
Все тесты + HTML отчёт
make molecule-all
Запускает все сценарии последовательно и генерирует open /tmp/molecule-report.html.
Отладка упавших тестов
# Войти в runner-контейнер с Docker socket:
docker run --rm -it \
-v $(pwd):/ansible \
-v /var/run/docker.sock:/var/run/docker.sock \
--entrypoint bash \
k3s-ansible
# Роль:
cd /ansible/roles/k3s
molecule converge
molecule login
molecule login --host master01
molecule verify
molecule converge -- -vvv
molecule destroy
# Аддон:
cd /ansible/addons/authelia/role
molecule converge
molecule login
molecule verify
# Кластер:
cd /ansible
molecule test -s cluster
molecule converge -s cluster
molecule verify -s cluster
molecule destroy -s cluster
Написание новых тестов для аддонов
molecule.yml (шаблон)
driver:
name: docker
platforms:
- name: master01
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
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 (шаблон для аддона с Helm chart)
- name: Converge — my-addon template tests
hosts: all
become: false
gather_facts: false
vars:
my_addon_namespace: my-addon
# ... все переменные аддона ...
tasks:
- name: Render Helm values
ansible.builtin.template:
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 (шаблон)
- name: Verify — my-addon templates
hosts: all
tasks:
- name: Read values
ansible.builtin.slurp:
src: /tmp/my-addon-values.yaml
register: values_raw
- name: Parse values
ansible.builtin.set_fact:
v: "{{ values_raw.content | b64decode | from_yaml }}"
- name: Assert key value
ansible.builtin.assert:
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 не найден"
Типичные ошибки
| Ошибка | Причина | Решение |
|---|---|---|
Unable to pull image |
Нет интернета | docker pull geerlingguy/docker-ubuntu2204-ansible:latest |
FAILED: assert ... is defined |
Переменная не задана | Добавь в секцию vars: converge.yml |
Idempotency: CHANGED |
Таск не идемпотентен | Добавь changed_when: false или исправь задачу |
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-* |