# Тестирование через 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-` - Затем снова общий прогон: - `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/.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: '' is undefined`. - Решение: добавить переменные в `vars:` converge-сценария (даже если в роли есть defaults, тест может их не подхватить в нужном контексте). 5. **Старое имя шаблона в `src:`** - Симптом: `Could not find or access .../templates/.j2`. - Решение: сверить актуальные имена файлов в `addons//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-*` |