Quellcode durchsuchen

Add Molecule coverage for Tuwunel

pull/5584/head
Slavi Pantaleev vor 2 Tagen
Ursprung
Commit
1e289dd88c
8 geänderte Dateien mit 461 neuen und 2 gelöschten Zeilen
  1. +2
    -1
      .github/renovate.json
  2. +3
    -1
      docs/molecule-testing.md
  3. +47
    -0
      roles/custom/matrix-tuwunel/molecule/default/converge.yml
  4. +78
    -0
      roles/custom/matrix-tuwunel/molecule/default/molecule.yml
  5. +56
    -0
      roles/custom/matrix-tuwunel/molecule/default/prepare.yml
  6. +55
    -0
      roles/custom/matrix-tuwunel/molecule/default/probe.py
  7. +1
    -0
      roles/custom/matrix-tuwunel/molecule/default/requirements.yml
  8. +219
    -0
      roles/custom/matrix-tuwunel/molecule/default/verify.yml

+ 2
- 1
.github/renovate.json Datei anzeigen

@@ -156,7 +156,8 @@
"roles/custom/matrix-livekit-jwt-service/defaults/main.yml", "roles/custom/matrix-livekit-jwt-service/defaults/main.yml",
"roles/custom/matrix-static-files/defaults/main.yml", "roles/custom/matrix-static-files/defaults/main.yml",
"roles/custom/matrix-synapse/defaults/main.yml", "roles/custom/matrix-synapse/defaults/main.yml",
"roles/custom/matrix-synapse-auto-compressor/defaults/main.yml"
"roles/custom/matrix-synapse-auto-compressor/defaults/main.yml",
"roles/custom/matrix-tuwunel/defaults/main.yml"
], ],
"matchUpdateTypes": [ "matchUpdateTypes": [
"patch" "patch"


+ 3
- 1
docs/molecule-testing.md Datei anzeigen

@@ -160,7 +160,7 @@ a fresh install.


## Databases ## Databases


Scenarios for roles that have a database run against **Postgres**, not sqlite.
Scenarios for roles that support an external SQL database run against **Postgres**, not sqlite.


That is what `group_vars/matrix_servers` selects whenever postgres is enabled, which is the That is what `group_vars/matrix_servers` selects whenever postgres is enabled, which is the
default, so it is what essentially every deployment runs. sqlite is a path almost nobody is on: default, so it is what essentially every deployment runs. sqlite is a path almost nobody is on:
@@ -180,6 +180,8 @@ cope with it.
Prefer asserting on the schema the component created over a file on disk: tables can only appear Prefer asserting on the schema the component created over a file on disk: tables can only appear
once it has resolved the hostname, authenticated, and run its migrations. once it has resolved the hostname, authenticated, and run its migrations.


Tuwunel uses embedded RocksDB instead. Its scenario exercises token-protected registration and authenticated Matrix APIs against that database, and checks that the configured listener, server name, display-name suffix, upload limit, and client discovery URL reach the running homeserver.

## Reclaiming the disk space ## Reclaiming the disk space


`just molecule-clean` removes what the runs leave under `var/`. `just molecule-clean` removes what the runs leave under `var/`.


+ 47
- 0
roles/custom/matrix-tuwunel/molecule/default/converge.yml Datei anzeigen

@@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
- name: Include roles for Tuwunel Molecule tests
hosts: all
become: true
vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
gather_facts: true
tasks:
- name: Include base roles for Tuwunel Molecule tests
ansible.builtin.include_role:
name: "{{ role_name }}"
public: true
loop:
- com.devture.ansible.role.playbook_help
- com.devture.ansible.role.systemd_docker_base
loop_control:
loop_var: role_name

# Include setup directly: main.yml also carries self-check and migration actions.
- name: Include Tuwunel setup task files
ansible.builtin.include_role:
name: "custom/{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}"
public: true
tasks_from: "{{ role_task_file }}"
loop:
- validate_config.yml
- install.yml
loop_control:
loop_var: role_task_file

- name: Ensure Tuwunel is started
hosts: all
become: true
gather_facts: false
tasks:
- name: Ensure systemd daemon is reloaded
ansible.builtin.systemd_service:
daemon_reload: true

- name: Ensure the Tuwunel systemd service is started
ansible.builtin.systemd_service:
name: matrix-tuwunel.service
state: started

+ 78
- 0
roles/custom/matrix-tuwunel/molecule/default/molecule.yml Datei anzeigen

@@ -0,0 +1,78 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
force: true
driver:
name: docker
platforms:
- name: tuwunel-${MOLECULE_DISTRO:-ubuntu2604}-default
image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2604}-ansible:latest"
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
pre_build_image: true
provisioner:
name: ansible
config_options:
defaults:
callback_result_format: yaml
inventory:
group_vars:
all:
matrix_tuwunel_hostname: matrix.example.com
matrix_tuwunel_container_network: tuwunel-molecule
matrix_tuwunel_config_server_name: example.com
matrix_tuwunel_config_port_number: 16167
matrix_tuwunel_systemd_service_post_start_delay_seconds: 0

# Exercise real local registration against the embedded RocksDB database.
# These credentials never authenticate to an external service.
matrix_tuwunel_config_allow_registration: true
matrix_tuwunel_config_registration_token: molecule-registration-7e290fc5
matrix_tuwunel_config_new_user_displayname_suffix: '[Molecule]'
matrix_tuwunel_config_grant_admin_to_first_user: false
matrix_tuwunel_config_allow_federation: false
matrix_tuwunel_config_trusted_servers: []
matrix_tuwunel_config_max_request_size: 17000000
matrix_tuwunel_config_well_known_client: https://matrix.example.com

# Keep resource use modest on the CI runner. RocksDB is Tuwunel's native
# database; no external database or homeserver fixture is needed.
matrix_tuwunel_config_rocksdb_parallelism_threads: 2
matrix_tuwunel_config_db_cache_capacity_mb: 32
matrix_tuwunel_config_db_write_buffer_capacity_mb: 16

# Labels are inspected on the running container; probes reach it directly
# over its own network without publishing a host port or running Traefik.
matrix_tuwunel_container_labels_traefik_entrypoints: web

# Pin paths needed by verify.yml, but load the image version from the role.
matrix_tuwunel_base_path: /matrix/tuwunel-molecule
matrix_tuwunel_config_path: /matrix/tuwunel-molecule/config
matrix_tuwunel_data_path: /matrix/tuwunel-molecule/data
env:
# Workaround for https://github.com/ansible/molecule/issues/4391
ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/../..:/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:${ANSIBLE_HOME:-~/.ansible}/roles
scenario:
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- verify
- cleanup
- destroy
verifier:
name: ansible

+ 56
- 0
roles/custom/matrix-tuwunel/molecule/default/prepare.yml Datei anzeigen

@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
- name: Prepare Tuwunel Molecule tests
hosts: all
become: true
vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
gather_facts: true
tasks:
- name: Ensure apt cache is updated
ansible.builtin.apt:
update_cache: true
cache_valid_time: 600
when: ansible_facts['os_family'] == 'Debian'

- name: Ensure required packages are installed
ansible.builtin.package:
name:
- python3-requests
- fuse-overlayfs
state: present

- name: Ensure Docker is installed
ansible.builtin.include_role:
name: ansible-role-docker
vars:
docker_daemon_options:
storage-driver: fuse-overlayfs

# matrix-base creates this identity in a full deployment. The role's file tasks
# resolve owner and group by name, so the role-scoped test provides it directly.
- name: Ensure the matrix group exists
ansible.builtin.group:
name: "{{ matrix_group_name }}"
gid: "{{ matrix_user_gid }}"
state: present

- name: Ensure the matrix user exists
ansible.builtin.user:
name: "{{ matrix_user_name }}"
uid: "{{ matrix_user_uid }}"
group: "{{ matrix_group_name }}"
create_home: false
system: true
state: present

- name: Ensure the base data path exists
ansible.builtin.file:
path: "{{ matrix_base_data_path }}"
state: directory
owner: "{{ matrix_user_name }}"
group: "{{ matrix_group_name }}"
mode: "0750"

+ 55
- 0
roles/custom/matrix-tuwunel/molecule/default/probe.py Datei anzeigen

@@ -0,0 +1,55 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

"""Exercise local Matrix registration and report observations to verify.yml."""

import json
import secrets
import sys
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen


base_url, registration_token = sys.argv[1:]


def request(path, body=None, access_token=None):
headers = {"Content-Type": "application/json"}
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
data = json.dumps(body).encode() if body is not None else None
req = Request(base_url + path, data=data, headers=headers)
try:
response = urlopen(req, timeout=15)
except HTTPError as error:
response = error
with response:
return {"status": response.status, "body": json.load(response)}


# A fresh localpart lets `molecule verify` run repeatedly against the same database.
username = "alice_" + secrets.token_hex(6)
credentials = {"username": username, "password": secrets.token_urlsafe(24)}
result = {"username": username}
result["challenge"] = request("/_matrix/client/v3/register", credentials)
session = result["challenge"]["body"].get("session", "")
auth = {"type": "m.login.registration_token", "session": session}
result["bad_token"] = request(
"/_matrix/client/v3/register",
{**credentials, "auth": {**auth, "token": registration_token + "-invalid"}},
)
registration = request(
"/_matrix/client/v3/register",
{**credentials, "auth": {**auth, "token": registration_token}},
)
access_token = registration["body"].pop("access_token", None)
result["registration"] = registration
if access_token:
result["whoami"] = request("/_matrix/client/v3/account/whoami", access_token=access_token)
user_id = quote(registration["body"]["user_id"], safe="")
result["profile"] = request(f"/_matrix/client/v3/profile/{user_id}/displayname")
result["media"] = request("/_matrix/client/v1/media/config", access_token=access_token)
result["well_known"] = request("/.well-known/matrix/client")
print(json.dumps(result))

+ 1
- 0
roles/custom/matrix-tuwunel/molecule/default/requirements.yml Datei anzeigen

@@ -0,0 +1 @@
../../../../../molecule-shared/requirements.yml

+ 219
- 0
roles/custom/matrix-tuwunel/molecule/default/verify.yml Datei anzeigen

@@ -0,0 +1,219 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
- name: Verify Tuwunel
hosts: all
become: true
vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
gather_facts: false
vars:
matrix_tuwunel_molecule_config: "{{ (matrix_tuwunel_molecule_config_result.stdout | from_json).global }}"
matrix_tuwunel_molecule_probe: "{{ matrix_tuwunel_molecule_probe_result.stdout | from_json }}"
matrix_tuwunel_molecule_container: "{{ (matrix_tuwunel_molecule_inspect.stdout | from_json) | first }}"

tasks:
- name: Load the role's defaults under a separate name
ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
name: matrix_tuwunel_role_defaults

# No host ports: a helper reaches the service over its own container network.
- name: Wait for the Matrix versions endpoint on the configured listener
ansible.builtin.command:
argv:
- docker
- run
- --rm
- --network={{ matrix_tuwunel_container_network }}
- "{{ molecule_shared_image_curl }}"
- --fail
- --silent
- --show-error
- --max-time
- '10'
- http://matrix-tuwunel:{{ matrix_tuwunel_config_port_number }}/_matrix/client/versions
register: matrix_tuwunel_molecule_versions
changed_when: false
until: matrix_tuwunel_molecule_versions.rc == 0
retries: 24
delay: 5

- name: Assert the non-default listener serves the Matrix API
ansible.builtin.assert:
that:
- "'v1.1' in (matrix_tuwunel_molecule_versions.stdout | from_json).versions"

- name: Request the default listener which the scenario moved away from
ansible.builtin.command:
argv:
- docker
- run
- --rm
- --network={{ matrix_tuwunel_container_network }}
- "{{ molecule_shared_image_curl }}"
- --silent
- --output
- /dev/null
- --connect-timeout
- '2'
- --max-time
- '3'
- http://matrix-tuwunel:6167/_matrix/client/versions
register: matrix_tuwunel_molecule_default_listener
changed_when: false
failed_when: false

- name: Assert the default listener refuses connections
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_default_listener.rc == 7

# This uses the real homeserver and its embedded RocksDB database. The helper
# only makes requests and records responses; the assertions remain below.
- name: Exercise token-protected registration and authenticated Matrix APIs
ansible.builtin.command:
argv:
- docker
- run
- --rm
- --interactive
- --network={{ matrix_tuwunel_container_network }}
- "{{ molecule_shared_image_python }}"
- python
- '-'
- http://matrix-tuwunel:{{ matrix_tuwunel_config_port_number }}
- "{{ matrix_tuwunel_config_registration_token }}"
stdin: "{{ lookup('file', 'probe.py') }}"
register: matrix_tuwunel_molecule_probe_result
changed_when: false

- name: Assert registration requires the configured token
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_probe.challenge.status == 401
- "matrix_tuwunel_molecule_probe.challenge.body.flows == [{'stages': ['m.login.registration_token']}]"
- matrix_tuwunel_molecule_probe.bad_token.status == 401
- matrix_tuwunel_molecule_probe.bad_token.body.errcode == 'M_FORBIDDEN'

- name: Assert the configured token registers a user on the configured server
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_probe.registration.status == 200
- matrix_tuwunel_molecule_probe.registration.body.user_id == '@' + matrix_tuwunel_molecule_probe.username + ':' + matrix_tuwunel_config_server_name
- matrix_tuwunel_molecule_probe.whoami.status == 200
- matrix_tuwunel_molecule_probe.whoami.body.user_id == matrix_tuwunel_molecule_probe.registration.body.user_id

- name: Assert the configured display-name suffix reaches new users
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_probe.profile.status == 200
- matrix_tuwunel_molecule_probe.profile.body.displayname == matrix_tuwunel_molecule_probe.username + ' ' + matrix_tuwunel_config_new_user_displayname_suffix

- name: Assert the authenticated media API advertises the configured upload limit
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_probe.media.status == 200
- matrix_tuwunel_molecule_probe.media.body['m.upload.size'] == matrix_tuwunel_config_max_request_size

- name: Assert client discovery serves the configured URL
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_probe.well_known.status == 200
# Tuwunel normalizes the configured origin to a URL with a trailing slash.
- matrix_tuwunel_molecule_probe.well_known.body['m.homeserver'].base_url == matrix_tuwunel_config_well_known_client.rstrip('/') + '/'

- name: Parse the rendered TOML configuration
ansible.builtin.command:
argv:
- python3
- '-c'
- "import json, sys, tomllib; print(json.dumps(tomllib.load(open(sys.argv[1], 'rb'))))"
- "{{ matrix_tuwunel_config_path }}/tuwunel.toml"
register: matrix_tuwunel_molecule_config_result
changed_when: false

- name: Assert the parsed TOML carries the scenario's settings
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_config.server_name == matrix_tuwunel_config_server_name
- matrix_tuwunel_molecule_config.port == matrix_tuwunel_config_port_number
- matrix_tuwunel_molecule_config.allow_registration
- matrix_tuwunel_molecule_config.registration_token == matrix_tuwunel_config_registration_token
- matrix_tuwunel_molecule_config.new_user_displayname_suffix == matrix_tuwunel_config_new_user_displayname_suffix
- matrix_tuwunel_molecule_config.max_request_size == matrix_tuwunel_config_max_request_size
- matrix_tuwunel_molecule_config.well_known.client == matrix_tuwunel_config_well_known_client
- not matrix_tuwunel_molecule_config.allow_federation
- matrix_tuwunel_molecule_config.trusted_servers == []
- not matrix_tuwunel_molecule_config.grant_admin_to_first_user

- name: Inspect the running Tuwunel container
ansible.builtin.command:
argv: [docker, container, inspect, matrix-tuwunel]
register: matrix_tuwunel_molecule_inspect
changed_when: false

- name: Assert the running container uses the exact image pinned by the role
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_container.Config.Image == 'ghcr.io/matrix-construct/tuwunel:' + matrix_tuwunel_role_defaults.matrix_tuwunel_version

- name: Assert the running container uses the playbook identity and isolation
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_container.Config.User == (matrix_user_uid | string) + ':' + (matrix_user_gid | string)
- matrix_tuwunel_molecule_container.HostConfig.ReadonlyRootfs
- "'ALL' in matrix_tuwunel_molecule_container.HostConfig.CapDrop"

- name: Assert the role's configuration is mounted read-only and selected at runtime
ansible.builtin.assert:
that:
- "'TUWUNEL_CONFIG=/etc/tuwunel/tuwunel.toml' in matrix_tuwunel_molecule_container.Config.Env"
- matrix_tuwunel_molecule_mount.Source == matrix_tuwunel_config_path
- not matrix_tuwunel_molecule_mount.RW
vars:
matrix_tuwunel_molecule_mount: >-
{{ matrix_tuwunel_molecule_container.Mounts
| selectattr('Destination', 'equalto', '/etc/tuwunel') | first }}

- name: Assert the embedded database has the configured writable data mount
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_mount.Source == matrix_tuwunel_data_path
- matrix_tuwunel_molecule_mount.RW
vars:
matrix_tuwunel_molecule_mount: >-
{{ matrix_tuwunel_molecule_container.Mounts
| selectattr('Destination', 'equalto', '/var/lib/tuwunel') | first }}

- name: Assert the container uses only the scenario network and publishes no ports
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_container.NetworkSettings.Networks.keys() | list == [matrix_tuwunel_container_network]
- matrix_tuwunel_molecule_container.HostConfig.PortBindings | default({}, true) == {}

- name: Assert Traefik targets the configured listener and public hostname
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_labels['traefik.http.services.matrix-tuwunel.loadbalancer.server.port'] == matrix_tuwunel_config_port_number | string
- matrix_tuwunel_molecule_labels['traefik.http.routers.matrix-tuwunel-public-client-api.rule'] == 'Host(`' + matrix_tuwunel_hostname + '`) && PathPrefix(`/_matrix`)'
- matrix_tuwunel_molecule_labels['traefik.http.routers.matrix-tuwunel-public-tuwunel-api.rule'] == 'Host(`' + matrix_tuwunel_hostname + '`) && PathPrefix(`/_tuwunel`)'
- matrix_tuwunel_molecule_labels['traefik.http.routers.matrix-tuwunel-public-client-api.entrypoints'] == matrix_tuwunel_container_labels_traefik_entrypoints
vars:
matrix_tuwunel_molecule_labels: "{{ matrix_tuwunel_molecule_container.Config.Labels }}"

# Check after the API probes: Restart=always alone can hide a crash loop.
- name: Read the Tuwunel service state after the probes
ansible.builtin.systemd_service:
name: matrix-tuwunel.service
register: matrix_tuwunel_molecule_service

- name: Assert the service is active without automatic restarts
ansible.builtin.assert:
that:
- matrix_tuwunel_molecule_service.status.ActiveState == 'active'
- matrix_tuwunel_molecule_service.status.NRestarts is defined
- matrix_tuwunel_molecule_service.status.NRestarts | int == 0

Laden…
Abbrechen
Speichern