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

26 KiB
Raw Permalink Blame History

Тестирование через 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 от хоста).


Все доступные тесты

# ─── Роли (и 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) Базовая проверка плейбуков

make build
make lint

Что это даёт:

  • Проверка, что runner-образ собирается на вашей архитектуре (amd64/arm64).
  • ansible-playbook --syntax-check для playbooks/site.yml.
  • Быстрый сигнал, что не сломан основной деплой кластера.

2) Проверка core-роли и cluster topology

make molecule-k3s
make molecule-cluster

Что это даёт:

  • molecule-k3s: рендер/идемпотентность роли k3s.
  • molecule-cluster: сценарий 3 master + 2 worker, включая kube-vip и verify-assertions.

3) Проверка аддонов

make molecule-addon-all

Важно:

  • Команда останавливается на первом упавшем аддоне.
  • После фикса обычно запускают сначала точечный тест:
    • make molecule-addon-<name>
  • Затем снова общий прогон:
    • make molecule-addon-all

4) Финальный полный прогон

make molecule-all

Что включает:

  • Роль k3s и аддонные сценарии prometheus-stack, istio (через molecule-addon, см. Makefile molecule-prometheus / molecule-istio)
  • Cluster scenario
  • Все addon-сценарии
  • Генерацию HTML отчёта

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/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 сек)

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

Частые причины падений 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.
  1. Неверная проверка булевых значений в verify.yml
  • Симптом: assert вида v.some_flag == true падает, хотя в сообщении видно True.
  • Причина: в части сценариев значение приходит строкой.
  • Решение: сравнивать нормализованно:
    • (v.some_flag | string | lower) == 'true'
  1. Multi-document YAML в шаблоне
  • Симптом: from_yaml падает на файлах с несколькими документами (---).
  • Решение: использовать:
    • from_yaml_all | list
  • И проверять нужный документ по индексу/полям.
  1. Недостающие vars в molecule/default/converge.yml
  • Симптом: AnsibleUndefinedVariable: '<var_name>' is undefined.
  • Решение: добавить переменные в vars: converge-сценария (даже если в роли есть defaults, тест может их не подхватить в нужном контексте).
  1. Старое имя шаблона в src:
  • Симптом: Could not find or access .../templates/<name>.j2.
  • Решение: сверить актуальные имена файлов в addons/<name>/role/templates/.

Написание новых тестов для аддонов

Полный сценарий добавления нового аддона (не только Molecule, но и playbook.yml, addons.yml, Makefile): Свои аддоны для k3s-ansible.

Ниже — краткие шаблоны molecule.yml / converge / verify.

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
'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-*