| @@ -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" | ||||
| @@ -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/`. | ||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -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" | |||||
| @@ -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)) | |||||
| @@ -0,0 +1 @@ | |||||
| ../../../../../molecule-shared/requirements.yml | |||||
| @@ -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 | |||||