feat: Реорганизация Makefile и добавление Docker образов

- Реорганизован Makefile:
  * Переменные вынесены наверх
  * Справка перенесена в конец
  * Удалены секции molecule и container
  * Объединены presets и preset в одну секцию
  * Переименована секция docker в docker-cmd

- Добавлены Docker образы:
  * ansible-controller - основной контроллер
  * alt-linux, astra-linux, redos - российские дистрибутивы
  * rhel, centos, alma, rocky - RHEL-совместимые образы

- Обновлены preset'ы:
  * Добавлены описания #description: во все preset'ы
  * Переименован docker.yml в docker-test.yml
  * Добавлены новые preset'ы: etcd-patroni, multi-os

- Добавлена документация:
  * docs/examples.md - примеры использования
  * docs/universal-testing.md - универсальное тестирование
  * dockerfiles/README.md - описание Docker образов

- Улучшена функциональность:
  * Единообразный стиль команд make [категория] [действие]
  * Улучшенный вывод информации о preset'ах
  * Добавлены пустые цели для совместимости
This commit is contained in:
Сергей Антропов
2025-10-25 11:29:37 +03:00
parent 60ee5e90a5
commit 696e08aa35
27 changed files with 2101 additions and 250 deletions

View File

@@ -2,51 +2,74 @@
- hosts: localhost
gather_facts: false
vars:
# Получаем preset из переменной окружения или используем default
preset_name: "{{ lookup('env', 'MOLECULE_PRESET') | default('default') }}"
preset_file: "/workspace/molecule/presets/{{ preset_name }}.yml"
# перечисли файлы/глобы, которые нужно временно расшифровать
vault_targets:
- /ansible/vault/secrets.yml
# добавляй сюда свои пути (host_vars/*/vault.yml, group_vars/*/vault.yml, и т.п.)
- /ansible/files/playbooks/group_vars/*/vault.yml
- /ansible/files/playbooks/host_vars/*/vault.yml
- /ansible/roles/**/vars/vault.yml
tasks:
- name: Load preset configuration
include_vars: "{{ preset_file }}"
when: preset_file is file
ignore_errors: true
- name: Install collections
community.docker.docker_container_exec:
container: ansible-controller
command: bash -lc "ansible-galaxy collection install -r /ansible/requirements.yml --force --no-deps --upgrade >/dev/null 2>&1 || true"
- name: Decrypt vault targets (best-effort)
- name: Preflight vault — normalize state (encrypt if plaintext, then decrypt)
community.docker.docker_container_exec:
container: ansible-controller
command: >
bash -lc '
set -euo pipefail;
set -euo pipefail; shopt -s nullglob globstar;
for p in {{ vault_targets | map('quote') | join(' ') }}; do
if [ -e "$p" ]; then
echo "[vault] decrypt $p";
ansible-vault decrypt --vault-password-file /ansible/vault-password.txt "$p" || true;
fi
for f in $p; do
[ -f "$f" ] || continue;
if head -n1 "$f" | grep -q "^\$ANSIBLE_VAULT;"; then
echo "[vault] already encrypted: $f";
else
echo "[vault] plaintext -> encrypt: $f";
ansible-vault encrypt --encrypt-vault-id default --vault-password-file /ansible/vault-password.txt "$f";
fi
echo "[vault] decrypt for run: $f";
ansible-vault decrypt --vault-password-file /ansible/vault-password.txt "$f";
done
done
'
- name: Run external playbook (your lab play)
- name: Run lab playbook
community.docker.docker_container_exec:
container: ansible-controller
command: >
bash -lc "
ANSIBLE_ROLES_PATH=/ansible/roles
ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.ini /ansible/molecule/default/site.yml
ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.ini /ansible/files/playbooks/site.yml
"
- name: Re-encrypt vault targets (always)
- name: Post-run — re-encrypt secrets
community.docker.docker_container_exec:
container: ansible-controller
command: >
bash -lc '
set -euo pipefail;
set -euo pipefail; shopt -s nullglob globstar;
for p in {{ vault_targets | map('quote') | join(' ') }}; do
if [ -e "$p" ]; then
echo "[vault] encrypt $p";
ansible-vault encrypt --encrypt-vault-id default --vault-password-file /ansible/vault-password.txt "$p" || true;
fi
for f in $p; do
[ -f "$f" ] || continue;
if head -n1 "$f" | grep -q "^\$ANSIBLE_VAULT;"; then
echo "[vault] ok (encrypted): $f";
else
echo "[vault] encrypt back: $f";
ansible-vault encrypt --encrypt-vault-id default --vault-password-file /ansible/vault-password.txt "$f" || true;
fi
done
done
'
ignore_errors: true

View File

@@ -11,6 +11,9 @@
generated_inventory: "{{ molecule_ephemeral_directory }}/inventory/hosts.ini"
images:
debian: "ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy"
rhel: "quay.io/centos/centos:stream9-systemd"
ubuntu: "ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy"
centos: "quay.io/centos/centos:stream9-systemd"
systemd_defaults:
privileged: true
command: "/sbin/init"
@@ -24,72 +27,99 @@
groups: [test]
tasks:
- name: Install required collections
command: ansible-galaxy collection install -r /workspace/requirements.yml
delegate_to: localhost
ignore_errors: true
register: collections_install
changed_when: false
run_once: true
become: true
vars:
ansible_python_interpreter: /usr/bin/python3
environment:
ANSIBLE_COLLECTIONS_PATH: /usr/share/ansible/collections
- name: Load preset configuration
include_vars: "{{ preset_file }}"
when: preset_file is file
ignore_errors: true
- name: Ensure network exists
command: docker network create {{ docker_network }}
delegate_to: localhost
ignore_errors: true
community.docker.docker_network:
name: "{{ docker_network }}"
state: present
# SYSTEMD nodes
- name: Pull systemd images
command: docker pull {{ images[item.family] }}
delegate_to: localhost
community.docker.docker_image:
name: "{{ images[item.family] }}"
source: pull
loop: "{{ hosts | selectattr('type','undefined') | list }}"
loop_control: { label: "{{ item.name }}" }
when: item.family is defined and images[item.family] is defined
- name: Start systemd nodes
command: >
docker run -d --name {{ item.name }}
--network {{ docker_network }}
--privileged={{ systemd_defaults.privileged | lower }}
--tmpfs {{ (systemd_defaults.tmpfs | default([])) | join(' --tmpfs ') }}
--cap-add {{ (systemd_defaults.capabilities | default([])) | join(' --cap-add ') }}
{% for port in item.publish | default([]) %}--publish {{ port }} {% endfor %}
{% for key, value in item.env | default({}) | dictsort %}--env {{ key }}={{ value }} {% endfor %}
{% for volume in (systemd_defaults.volumes | default([])) + (item.volumes | default([])) %}--volume {{ volume }} {% endfor %}
{{ images[item.family] }} {{ systemd_defaults.command }}
delegate_to: localhost
community.docker.docker_container:
name: "{{ item.name }}"
image: "{{ images[item.family] }}"
networks:
- name: "{{ docker_network }}"
privileged: "{{ systemd_defaults.privileged }}"
command: "{{ systemd_defaults.command }}"
volumes: "{{ systemd_defaults.volumes | default([]) + (item.volumes | default([])) }}"
tmpfs: "{{ systemd_defaults.tmpfs | default([]) }}"
capabilities: "{{ systemd_defaults.capabilities | default([]) }}"
published_ports: "{{ item.publish | default([]) }}"
environment: "{{ item.env | default({}) }}"
state: started
restart_policy: unless-stopped
loop: "{{ hosts | selectattr('type','undefined') | list }}"
loop_control: { label: "{{ item.name }}" }
when: item.family is defined and images[item.family] is defined
# DinD nodes
- name: Start DinD nodes (docker:27-dind)
command: >
docker run -d --name {{ item.name }}
--network {{ docker_network }}
--privileged=true
--env DOCKER_TLS_CERTDIR=""
{% for port in item.publish | default([]) %}--publish {{ port }} {% endfor %}
--volume {{ item.name }}-docker:/var/lib/docker
docker:27-dind
delegate_to: localhost
community.docker.docker_container:
name: "{{ item.name }}"
image: "docker:27-dind"
networks:
- name: "{{ docker_network }}"
privileged: true
environment:
DOCKER_TLS_CERTDIR: ""
published_ports: "{{ item.publish | default([]) }}"
volumes: "{{ (item.volumes | default([])) + [item.name + '-docker:/var/lib/docker'] }}"
state: started
restart_policy: unless-stopped
loop: "{{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list }}"
loop_control: { label: "{{ item.name }}" }
# DOoD nodes (mount docker.sock)
- name: Start DOoD nodes (systemd + docker.sock mount)
command: >
docker run -d --name {{ item.name }}
--network {{ docker_network }}
--privileged={{ systemd_defaults.privileged | lower }}
--tmpfs {{ (systemd_defaults.tmpfs | default([])) | join(' --tmpfs ') }}
--cap-add {{ (systemd_defaults.capabilities | default([])) | join(' --cap-add ') }}
{% for port in item.publish | default([]) %}--publish {{ port }} {% endfor %}
{% for key, value in item.env | default({}) | dictsort %}--env {{ key }}={{ value }} {% endfor %}
{% for volume in (systemd_defaults.volumes | default([])) + ['/var/run/docker.sock:/var/run/docker.sock'] + (item.volumes | default([])) %}--volume {{ volume }} {% endfor %}
{{ images[item.family] }} {{ systemd_defaults.command }}
delegate_to: localhost
community.docker.docker_container:
name: "{{ item.name }}"
image: "{{ images[item.family] }}"
networks:
- name: "{{ docker_network }}"
privileged: "{{ systemd_defaults.privileged }}"
command: "{{ systemd_defaults.command }}"
volumes: "{{ (systemd_defaults.volumes | default([])) + ['/var/run/docker.sock:/var/run/docker.sock'] + (item.volumes | default([])) }}"
tmpfs: "{{ systemd_defaults.tmpfs | default([]) }}"
capabilities: "{{ systemd_defaults.capabilities | default([]) }}"
published_ports: "{{ item.publish | default([]) }}"
environment: "{{ item.env | default({}) }}"
state: started
restart_policy: unless-stopped
loop: "{{ hosts | selectattr('type','defined') | selectattr('type','equalto','dood') | list }}"
loop_control: { label: "{{ item.name }}" }
when: item.family is defined and images[item.family] is defined
# Build groups map
- name: Build groups map
- name: Initialize groups map
set_fact:
groups_map: "{{ groups_map | default({}) }}"
groups_map: {}
- name: Append hosts to groups
set_fact:
groups_map: "{{ groups_map | combine({ item_group: (groups_map[item_group] | default([])) + [item_name] }) }}"
@@ -122,4 +152,14 @@
copy:
dest: "{{ generated_inventory }}"
content: "{{ inv_content }}"
mode: "0644"
mode: "0644"
- name: Display inventory summary
debug:
msg: |
📋 Inventory Summary:
- Total hosts: {{ hosts | length }}
- Groups: {{ groups_map.keys() | list | join(', ') }}
- Systemd nodes: {{ hosts | selectattr('type','undefined') | list | length }}
- DinD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list | length }}
- DOoD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dood') | list | length }}

View File

@@ -4,7 +4,7 @@
vars:
# Получаем preset из переменной окружения или используем default
preset_name: "{{ lookup('env', 'MOLECULE_PRESET') | default('default') }}"
preset_file: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') | default('/tmp') }}/../presets/{{ preset_name }}.yml"
preset_file: "/workspace/molecule/presets/{{ preset_name }}.yml"
# Fallback значения если preset файл не найден
docker_network: labnet
@@ -19,7 +19,7 @@
when: preset_file is file
ignore_errors: true
- name: Remove containers
- name: Stop and remove containers
community.docker.docker_container:
name: "{{ item.name }}"
state: absent
@@ -36,8 +36,25 @@
loop_control: { label: "{{ item.name }}" }
ignore_errors: true
- name: Remove custom volumes
community.docker.docker_volume:
name: "{{ item.volumes | default([]) | select('match', '^[^:]+$') | list }}"
state: absent
loop: "{{ hosts }}"
loop_control: { label: "{{ item.name }}" }
ignore_errors: true
when: item.volumes is defined
- name: Remove network
community.docker.docker_network:
name: "{{ docker_network }}"
state: absent
ignore_errors: true
ignore_errors: true
- name: Display cleanup summary
debug:
msg: |
🧹 Cleanup Summary:
- Removed containers: {{ hosts | length }}
- Removed DinD volumes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list | length }}
- Network: {{ docker_network }}

122
molecule/default/verify.yml Normal file
View File

@@ -0,0 +1,122 @@
---
- hosts: localhost
gather_facts: false
vars:
# Получаем preset из переменной окружения или используем default
preset_name: "{{ lookup('env', 'MOLECULE_PRESET') | default('default') }}"
preset_file: "/workspace/molecule/presets/{{ preset_name }}.yml"
# Fallback значения если preset файл не найден
docker_network: labnet
hosts:
- name: u1
family: debian
groups: [test]
tasks:
- name: Load preset configuration
include_vars: "{{ preset_file }}"
when: preset_file is file
ignore_errors: true
# Проверка systemd узлов
- name: Check systemd nodes status
community.docker.docker_container_exec:
container: "{{ item.name }}"
command: systemctl is-system-running
loop: "{{ hosts | selectattr('type','undefined') | list }}"
loop_control: { label: "{{ item.name }}" }
register: systemd_status
ignore_errors: true
- name: Display systemd nodes status
debug:
msg: "Systemd node {{ item.0.name }}: {{ item.1.stdout | default('unknown') }}"
loop: "{{ systemd_status.results | default([]) }}"
when: systemd_status is defined
# Проверка DinD узлов
- name: Check DinD nodes docker daemon
community.docker.docker_container_exec:
container: "{{ item.name }}"
command: docker version --format '{{.Server.Version}}'
loop: "{{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list }}"
loop_control: { label: "{{ item.name }}" }
register: dind_status
ignore_errors: true
- name: Display DinD nodes status
debug:
msg: "DinD node {{ item.0.name }}: Docker {{ item.1.stdout | default('not running') }}"
loop: "{{ dind_status.results | default([]) }}"
when: dind_status is defined
# Проверка DOoD узлов
- name: Check DOoD nodes docker access
community.docker.docker_container_exec:
container: "{{ item.name }}"
command: docker ps --format '{{.Names}}'
loop: "{{ hosts | selectattr('type','defined') | selectattr('type','equalto','dood') | list }}"
loop_control: { label: "{{ item.name }}" }
register: dood_status
ignore_errors: true
- name: Display DOoD nodes status
debug:
msg: "DOoD node {{ item.0.name }}: Can access {{ item.1.stdout_lines | length | default(0) }} containers"
loop: "{{ dood_status.results | default([]) }}"
when: dood_status is defined
# Проверка сетевого подключения
- name: Test network connectivity between nodes
community.docker.docker_container_exec:
container: "{{ item.0.name }}"
command: ping -c 1 {{ item.1.name }}
loop: "{{ hosts | subelements(hosts, 'name') }}"
loop_control: { label: "{{ item.0.name }} -> {{ item.1.name }}" }
when: item.0.name != item.1.name
register: ping_results
ignore_errors: true
- name: Display network connectivity results
debug:
msg: "Network test {{ item.0.name }} -> {{ item.1.name }}: {{ 'OK' if item.2.rc == 0 else 'FAILED' }}"
loop: "{{ ping_results.results | default([]) }}"
when: ping_results is defined
# Проверка портов
- name: Check published ports
community.docker.docker_container_exec:
container: "{{ item.name }}"
command: netstat -tlnp
loop: "{{ hosts | selectattr('publish','defined') | list }}"
loop_control: { label: "{{ item.name }}" }
register: port_status
ignore_errors: true
- name: Display port status
debug:
msg: "Node {{ item.0.name }} ports: {{ item.1.stdout_lines | select('match', 'LISTEN') | list | length }} listening"
loop: "{{ port_status.results | default([]) }}"
when: port_status is defined
# Проверка групп
- name: Display inventory groups
debug:
msg: |
📋 Inventory Groups:
{% for group, members in (groups_map | default({}) | dictsort) %}
- {{ group }}: {{ members | join(', ') }}
{% endfor %}
# Финальная сводка
- name: Display verification summary
debug:
msg: |
✅ Verification Summary:
- Total hosts: {{ hosts | length }}
- Systemd nodes: {{ hosts | selectattr('type','undefined') | list | length }}
- DinD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dind') | list | length }}
- DOoD nodes: {{ hosts | selectattr('type','defined') | selectattr('type','equalto','dood') | list | length }}
- Groups: {{ groups_map.keys() | list | join(', ') }}
- Network: {{ docker_network }}