Files
K3S/docs/molecule-testing.md
Sergey Antropoff 4eaf91e2d2 docs: руководство по своим аддонам и ссылки в README/Molecule/справочнике
- Добавлен docs/custom-addons.md: структура addons/<name>, playbook, group_vars,
  playbooks/addons.yml, Makefile, Molecule, чеклист и см. также.
- docs/addons.md, getting-started.md: отсылки на custom-addons.md.
- README.md: строка в таблице документации.
- docs/molecule-testing.md: уточнены molecule-prometheus/istio (тесты аддонов),
  разделы prometheus-stack/istio, ссылка на руководство в блоке про новые тесты.
- docs/make-reference.md: примечание к make addon-<name>.
2026-04-28 01:43:13 +03:00

554 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Тестирование через Molecule
Molecule — стандартный инструмент для тестирования Ansible ролей и аддонов. Каждый сценарий запускается в Docker-контейнерах, проходит набор автоматических проверок и удаляется. Реальные серверы не нужны.
## Требования
Только **Docker** — Molecule, Python и зависимости уже внутри образа `k3s-ansible`.
```bash
make build # собрать образ (один раз)
make molecule-k3s # запустить тест роли k3s
```
Как это работает: `make molecule-*` запускает контейнер `k3s-ansible` с примонтированным Docker socket. Внутри этого контейнера Molecule создаёт тестовые контейнеры — Docker-in-Docker без демона (только socket от хоста).
---
## Все доступные тесты
```bash
# ─── Роли (и legacy-алиасы) ────────────────────────────────────────────────
make molecule-k3s # роль k3s — 3 ноды (master + worker + Debian), ~8-12 мин
make molecule-prometheus # тест аддона prometheus-stack (molecule-addon prometheus-stack), ~2-3 мин
make molecule-istio # тест аддона istio (molecule-addon istio), ~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 # все аддоны последовательно, ~60 мин
make molecule-all # всё: роли + кластер + аддоны + HTML отчёт
# ─── Отчёт и линтинг ───────────────────────────────────────────────────────
make molecule-report # генерировать HTML из /tmp/molecule-junit/*.xml
make molecule-lint # yamllint + ansible-lint в контейнере
```
---
## Полная проверка проекта (рекомендуемый порядок)
Ниже практический порядок, который помогает поймать максимум ошибок в коде, особенно в `addons/`.
### 1) Базовая проверка плейбуков
```bash
make build
make lint
```
Что это даёт:
- Проверка, что runner-образ собирается на вашей архитектуре (amd64/arm64).
- `ansible-playbook --syntax-check` для `playbooks/site.yml`.
- Быстрый сигнал, что не сломан основной деплой кластера.
### 2) Проверка core-роли и cluster topology
```bash
make molecule-k3s
make molecule-cluster
```
Что это даёт:
- `molecule-k3s`: рендер/идемпотентность роли `k3s`.
- `molecule-cluster`: сценарий 3 master + 2 worker, включая kube-vip и verify-assertions.
### 3) Проверка аддонов
```bash
make molecule-addon-all
```
Важно:
- Команда останавливается на первом упавшем аддоне.
- После фикса обычно запускают сначала точечный тест:
- `make molecule-addon-<name>`
- Затем снова общий прогон:
- `make molecule-addon-all`
### 4) Финальный полный прогон
```bash
make molecule-all
```
Что включает:
- Роль `k3s` и аддонные сценарии `prometheus-stack`, `istio` (через `molecule-addon`, см. [Makefile](../Makefile) `molecule-prometheus` / `molecule-istio`)
- Cluster scenario
- Все addon-сценарии
- Генерацию HTML отчёта
---
## 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 → подготовить контейнер
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/kubernetes/k3s/server/manifests/kube-vip.yaml` |
| VIP `192.168.1.100` | kube-vip manifest | Правильный IP в аннотации |
### Аддон `prometheus-stack` (~2-3 мин)
Сценарий: `addons/prometheus-stack/role/molecule/`. Проверяет рендеринг Jinja2-шаблона Helm values без privileged режима.
| Проверка | Что именно |
| ----------------------------- | ------------------- |
| `grafana.adminUser` | Значение переменной |
| `grafana.persistence.enabled` | `true` |
| `prometheus.retention` | `7d` |
| `alertmanager.enabled` | `true` |
| `nodeExporter.enabled` | `true` |
### Аддон `istio` (~2-3 мин)
Сценарий: `addons/istio/role/molecule/`. Проверяет рендеринг шаблонов (istiod, Kiali, mesh policy и т.д.).
| Файл | Проверка |
| -------------------------- | ---------------------------------------------------------- |
| `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 сек)
```bash
make molecule-lint
```
Запускает `yamllint .` и `ansible-lint` на всём проекте.
### Тест одной роли
```bash
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)
```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
# Войти в 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
```
---
## Частые причины падений addon Molecule
Практика по реальным падениям:
1. **Файл создаётся на тестовой ноде, а читается на localhost**
- Симптом: `open /tmp/<file>.yaml: no such file or directory` при `delegate_to: localhost`.
- Решение: рендерить/копировать файлы тоже на `localhost` (`delegate_to: localhost`, `run_once: true`), либо убрать `delegate_to`.
2. **Неверная проверка булевых значений в `verify.yml`**
- Симптом: assert вида `v.some_flag == true` падает, хотя в сообщении видно `True`.
- Причина: в части сценариев значение приходит строкой.
- Решение: сравнивать нормализованно:
- `(v.some_flag | string | lower) == 'true'`
3. **Multi-document YAML в шаблоне**
- Симптом: `from_yaml` падает на файлах с несколькими документами (`---`).
- Решение: использовать:
- `from_yaml_all | list`
- И проверять нужный документ по индексу/полям.
4. **Недостающие vars в `molecule/default/converge.yml`**
- Симптом: `AnsibleUndefinedVariable: '<var_name>' is undefined`.
- Решение: добавить переменные в `vars:` converge-сценария (даже если в роли есть defaults, тест может их не подхватить в нужном контексте).
5. **Старое имя шаблона в `src:`**
- Симптом: `Could not find or access .../templates/<name>.j2`.
- Решение: сверить актуальные имена файлов в `addons/<name>/role/templates/`.
---
## Написание новых тестов для аддонов
Полный сценарий **добавления нового аддона** (не только Molecule, но и `playbook.yml`, `addons.yml`, `Makefile`): [Свои аддоны для k3s-ansible](custom-addons.md).
Ниже — краткие шаблоны `molecule.yml` / `converge` / `verify`.
### molecule.yml (шаблон)
```yaml
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)
```yaml
- 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 (шаблон)
```yaml
- 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` |
| `'molecule/default/molecule.yml' glob failed` при `molecule-cluster` | Нет default-сценария в корне `molecule/` (служебный shared-state warning) | Если тест завершился `✓ cluster topology: OK`, предупреждение можно игнорировать |
| `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-*` |