diff --git a/README.md b/README.md index 649cdab..be0ddbc 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Sovereign is an Ansible project that deploys a complete self-hosted infrastructu - [New Tenant Setup](#new-tenant-setup) - [Configuration Reference](#configuration-reference) - [Deployment](#deployment) +- [Testing](#testing) - [Maintenance](#maintenance) - [Architecture Notes](#architecture-notes) @@ -41,8 +42,8 @@ Sovereign is an Ansible project that deploys a complete self-hosted infrastructu **Control machine** (where you run Ansible): - Python 3.9+ -- Ansible 8+ (`pip install ansible`) -- Ansible collections (see [Installing Collections](#installing-collections)) +- Ansible 8+ — installed via `pip install -r requirements.txt` (see [Installing Dependencies](#installing-dependencies)) +- Ansible collections (see [Installing Dependencies](#installing-dependencies)) **Target host**: @@ -51,13 +52,18 @@ Sovereign is an Ansible project that deploys a complete self-hosted infrastructu - Ports 80, 443, and 51820/UDP open - DNS A records pointing `` and `*.` to the host IP -### Installing Collections +### Installing Dependencies + +Install Python packages (Ansible, Molecule, and linting tools) and Ansible collections: ```bash +pip install -r requirements.txt ansible-galaxy collection install -r requirements.yml ``` -Required collections: `community.docker >=3.0.0`, `community.general >=8.0.0`, `ansible.posix >=1.5.0`. +Python packages (`requirements.txt`): `ansible`, `molecule`, `ansible-lint`, `yamllint`. + +Ansible collections (`requirements.yml`): `community.docker >=3.0.0`, `community.general >=8.0.0`, `ansible.posix >=1.5.0`. --- @@ -67,7 +73,8 @@ Required collections: `community.docker >=3.0.0`, `community.general >=8.0.0`, ` # 1. Clone the repo git clone sovereign && cd sovereign -# 2. Install Ansible collections +# 2. Install Python packages and Ansible collections +pip install -r requirements.txt ansible-galaxy collection install -r requirements.yml # 3. Configure the target host @@ -207,6 +214,19 @@ All variables live in `inventories/production/group_vars/all.yml`. | `base_domain` | `example.com` | Root domain. All subdomains are derived from this. | | `sovereign_base_dir` | `/opt/sovereign` | Base path on the target host for all service data. | +### Branding + +These variables apply consistent tenant branding across all services that support it. Services apply branding via environment variables, config file templates, or post-deploy API calls (e.g. Nextcloud `occ`, Authentik blueprints). + +| Variable | Default | Description | +|----------|---------|-------------| +| `tenant_name` | `Example Corp` | Display name shown in service UIs, email subjects, and page titles. | +| `tenant_logo_local_path` | `""` | Path to a logo image on the Ansible control machine (PNG recommended). Leave empty to use each service's default logo. Example: `files/logo.png`. | +| `tenant_primary_color` | `#2563eb` | Primary brand colour (hex). Used for backgrounds, buttons, and highlights. | +| `tenant_accent_color` | `#1e40af` | Secondary/accent colour (hex). | + +Services with branding support: Authentik (title, colour, logo via blueprint), Element/Matrix (brand name, theme), Forgejo (app name, logo), Nextcloud (name, colour, logo via `occ`), Jitsi (app name, watermark), Roundcube (product name), Wazuh dashboard (title). + ### Traefik (`common` role) | Variable | Default | Description | @@ -413,6 +433,73 @@ ansible-lint --- +## Testing + +Each role has a [Molecule](https://ansible.readthedocs.io/projects/molecule/) test scenario under `roles//molecule/default/`. Tests run entirely on the local machine — no target host or Docker daemon required. + +### What the tests cover + +- **Directory creation** — all expected data directories are created with correct permissions. +- **Template rendering** — every Jinja2 template renders without errors and with all variables substituted (no unresolved `{{ }}` in output files). +- **Config file content** — role-specific config files (Element `config.json`, Headscale `config.yaml`, Authentik branding blueprint, Roundcube `custom.inc.php`, Jitsi interface config, Wazuh dashboard YAML) contain the expected values. +- **Docker Compose structure** — `docker-compose.yml` references the correct image, Traefik routing labels, GELF logging address, and external network declaration. +- **Idempotency** — Molecule re-runs each role after converge and asserts zero changed tasks. + +Docker/OS tasks (container start, `apt`, `systemd`, `sysctl`, health checks) are skipped during tests via the `molecule_test_mode` variable, which defaults to `false` and has no effect on real deployments. + +### Install test dependencies + +```bash +pip install -r requirements.txt +ansible-galaxy collection install -r requirements.yml +``` + +### Run tests for a single role + +```bash +cd roles/authentik +molecule test +``` + +`molecule test` runs the full lifecycle: dependency → converge → idempotency check → verify → cleanup. + +For a faster iteration loop during development: + +```bash +# Apply the role and run assertions (skip create/destroy lifecycle) +molecule converge && molecule verify + +# Clean up temp files when done +molecule destroy +``` + +### Run tests for all roles + +```bash +for role in roles/*/; do + echo "=== Testing $role ===" + (cd "$role" && molecule test) +done +``` + +### Lint + +```bash +ansible-lint # Ansible best-practice checks across all roles +yamllint . # YAML formatting checks +``` + +Both tools are configured via `.ansible-lint` and `.yamllint` at the repo root. The ansible-lint config mocks Docker and system modules so linting works without a live environment. + +### Adding tests for a new role + +1. Create `roles//molecule/default/` with `molecule.yml`, `converge.yml`, and `verify.yml` following the pattern of an existing simple role (e.g. `roles/website/molecule/default/`). +2. Add the new role's variables to `molecule/shared/vars.yml`. +3. Add `when: not (molecule_test_mode | default(false))` to any tasks that call `community.docker.docker_compose_v2`, `ansible.builtin.uri` (health checks), or `ansible.builtin.command` (docker exec). +4. Add the same guard to the role's restart handler in `handlers/main.yml`. + +--- + ## Maintenance ### Updating a service @@ -488,6 +575,7 @@ Follow the pattern used by existing roles: 3. Add the role to `playbooks/site.yml` with an appropriate tag. 4. Attach the container to the `sovereign` Docker network and add Traefik labels for routing. 5. Add `logging: driver: gelf` with `gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}"` to ship logs. +6. Add a Molecule scenario — see [Adding tests for a new role](#adding-tests-for-a-new-role). ---