diff --git a/.github/renovate.json b/.github/renovate.json index 9d1a87caa..04f77e738 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -93,7 +93,8 @@ "roles/custom/matrix-bridge-mautrix-signal/defaults/main.yml", "roles/custom/matrix-bridge-mautrix-telegram/defaults/main.yml", "roles/custom/matrix-bridge-mautrix-whatsapp/defaults/main.yml", - "roles/custom/matrix-bridge-postmoogle/defaults/main.yml" + "roles/custom/matrix-bridge-postmoogle/defaults/main.yml", + "roles/custom/matrix-synapse/defaults/main.yml" ], "matchUpdateTypes": [ "patch" diff --git a/molecule-shared/playbook-context.yml b/molecule-shared/playbook-context.yml index 04ab956a9..9de5262e9 100644 --- a/molecule-shared/playbook-context.yml +++ b/molecule-shared/playbook-context.yml @@ -40,6 +40,9 @@ matrix_admin: '' # They install it themselves by including matrix-base's `ensure_openssl_installed` tasks. matrix_host_command_openssl: "/usr/bin/env openssl" +# Used by homeserver systemd units for startup delays and post-start health checks. +matrix_host_command_sleep: "/usr/bin/env sleep" + # --- Bridge-wide switches (matrix-base) ------------------------------------ # Every bridge role reads these, so they live here rather than in each bridge's scenario. diff --git a/roles/custom/matrix-synapse/molecule/default/converge.yml b/roles/custom/matrix-synapse/molecule/default/converge.yml new file mode 100644 index 000000000..ab59befde --- /dev/null +++ b/roles/custom/matrix-synapse/molecule/default/converge.yml @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Include roles for Synapse 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 Synapse 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 + + # The role's main task file also carries separately invoked maintenance actions + # (media/database imports, user management, and self-checks). Include the same + # initialization, validation, and installation files selected by setup-synapse. + - name: Include Synapse setup task files + ansible.builtin.include_role: + name: "custom/{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + public: true + tasks_from: "{{ role_task_file }}" + loop: + - init.yml + - validate_config.yml + - setup_install.yml + loop_control: + loop_var: role_task_file + +- name: Ensure Synapse 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 Synapse systemd service is started + ansible.builtin.systemd_service: + name: matrix-synapse.service + state: started diff --git a/roles/custom/matrix-synapse/molecule/default/molecule.yml b/roles/custom/matrix-synapse/molecule/default/molecule.yml new file mode 100644 index 000000000..9e76918d8 --- /dev/null +++ b/roles/custom/matrix-synapse/molecule/default/molecule.yml @@ -0,0 +1,114 @@ +# 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: synapse-${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: + # This first scenario deliberately exercises one main Synapse process. Workers and + # everything they bring with them belong in a separate topology-focused scenario. + matrix_synapse_workers_enabled: false + matrix_synapse_redis_enabled: false + matrix_synapse_reverse_proxy_companion_enabled: false + + matrix_synapse_username: matrix + matrix_synapse_uid: 1234 + matrix_synapse_gid: 1234 + matrix_synapse_container_network: synapse-molecule + + # The non-default listener is reached only over the container network. Override the + # upstream image's health command as well, because its default assumes port 8008. + matrix_synapse_container_client_api_port: 18008 + matrix_synapse_container_client_api_host_bind_port: '' + matrix_synapse_container_master_extra_arguments: + - "--health-cmd='curl -fSs http://localhost:18008/health'" + matrix_synapse_container_labels_traefik_enabled: false + matrix_synapse_container_labels_traefik_hostname: matrix.molecule.local + matrix_synapse_systemd_service_post_start_delay_seconds: 0 + + # Postgres is the normal deployment path. Distinct database and user names make the + # resulting Synapse schema evidence that these exact connection settings were used. + matrix_synapse_database_host: matrix-postgres-molecule + matrix_synapse_database_port: 5432 + matrix_synapse_database_user: molecule_synapse + matrix_synapse_database_password: molecule_synapse_pg_password_9b742d + matrix_synapse_database_database: molecule_synapse + matrix_synapse_database_cp_min: 2 + matrix_synapse_database_cp_max: 4 + + # Local scenario secrets only. They never authenticate to an external service. + matrix_synapse_macaroon_secret_key: molecule_synapse_macaroon_4b3f891d + matrix_synapse_registration_shared_secret: molecule_synapse_registration_27ed51a8 + matrix_synapse_form_secret: molecule_synapse_form_96a04c7b + matrix_synapse_password_config_pepper: molecule_synapse_pepper_c0f371e6 + + # These values differ from both role and upstream defaults and are safe to exercise + # without introducing any external dependency. + matrix_synapse_public_baseurl: http://matrix.molecule.local:18008/ + matrix_synapse_presence_enabled: false + matrix_synapse_max_upload_size_mb: 37 + matrix_synapse_url_preview_enabled: false + matrix_synapse_allow_public_rooms_over_federation: false + matrix_synapse_user_directory_search_all_users: true + matrix_synapse_user_directory_prefer_local_users: true + matrix_synapse_trusted_key_servers: [] + matrix_synapse_suppress_key_server_warning: true + + # Keep the scenario fully local and intentionally exclude optional integrations. + matrix_synapse_federation_enabled: false + matrix_synapse_federation_port_openid_resource_required: false + matrix_synapse_tls_federation_listener_enabled: false + matrix_synapse_metrics_enabled: false + matrix_synapse_email_enabled: false + matrix_synapse_matrix_authentication_service_enabled: false + matrix_authentication_service_migration_in_progress: false + matrix_synapse_ext_synapse_s3_storage_provider_enabled: false + matrix_s3_media_store_enabled: false + matrix_playbook_ssl_enabled: false + + # verify.yml is a separate play, so pin paths that otherwise come from role defaults. + # The component version is deliberately loaded from defaults/main.yml instead. + matrix_synapse_base_path: /matrix/synapse + matrix_synapse_config_dir_path: /matrix/synapse/config + matrix_synapse_storage_path: /matrix/synapse/storage + matrix_synapse_media_store_path: /matrix/synapse/storage/media-store + 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 diff --git a/roles/custom/matrix-synapse/molecule/default/prepare.yml b/roles/custom/matrix-synapse/molecule/default/prepare.yml new file mode 100644 index 000000000..498c19002 --- /dev/null +++ b/roles/custom/matrix-synapse/molecule/default/prepare.yml @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Prepare Synapse Molecule tests + 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: 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 + + # The production Synapse unit uses slave bind propagation for its media store. The + # privileged Molecule host must expose a shared parent mount to its nested Docker daemon. + - name: Ensure nested Docker can use Synapse media-store bind propagation + ansible.builtin.command: + argv: + - mount + - --make-rshared + - / + changed_when: false + + - name: Ensure Docker is installed + ansible.builtin.include_role: + name: ansible-role-docker + vars: + docker_daemon_options: + storage-driver: fuse-overlayfs + + - 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' + + # Postgres must attach before the role creates this network during converge. + - name: Ensure the Synapse container network exists + ansible.builtin.command: + argv: + - docker + - network + - create + - "{{ matrix_synapse_container_network }}" + register: matrix_synapse_molecule_network + changed_when: matrix_synapse_molecule_network.rc == 0 + failed_when: + - matrix_synapse_molecule_network.rc != 0 + - "'already exists' not in matrix_synapse_molecule_network.stderr" + + - name: Ensure Postgres is running + ansible.builtin.include_tasks: + file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/postgres.yml" + vars: + molecule_shared_postgres_network: "{{ matrix_synapse_container_network }}" + molecule_shared_postgres_database: "{{ matrix_synapse_database_database }}" + molecule_shared_postgres_username: "{{ matrix_synapse_database_user }}" + molecule_shared_postgres_password: "{{ matrix_synapse_database_password }}" + + # Synapse intentionally refuses databases with locale-dependent collation. Recreate the + # helper's empty scenario database with the production-required C locale before startup. + - name: Remove the locale-dependent empty Synapse database + ansible.builtin.command: + argv: + - docker + - exec + - matrix-postgres-molecule + - psql + - --username={{ matrix_synapse_database_user }} + - --dbname=postgres + - --command=DROP DATABASE IF EXISTS "{{ matrix_synapse_database_database }}" + changed_when: true + + - name: Create the empty Synapse database with C collation + ansible.builtin.command: + argv: + - docker + - exec + - matrix-postgres-molecule + - psql + - --username={{ matrix_synapse_database_user }} + - --dbname=postgres + - >- + --command=CREATE DATABASE "{{ matrix_synapse_database_database }}" + OWNER "{{ matrix_synapse_database_user }}" TEMPLATE template0 + LC_COLLATE 'C' LC_CTYPE 'C' + changed_when: true diff --git a/roles/custom/matrix-synapse/molecule/default/requirements.yml b/roles/custom/matrix-synapse/molecule/default/requirements.yml new file mode 120000 index 000000000..1567a5392 --- /dev/null +++ b/roles/custom/matrix-synapse/molecule/default/requirements.yml @@ -0,0 +1 @@ +../../../../../molecule-shared/requirements.yml \ No newline at end of file diff --git a/roles/custom/matrix-synapse/molecule/default/verify.yml b/roles/custom/matrix-synapse/molecule/default/verify.yml new file mode 100644 index 000000000..450e1345d --- /dev/null +++ b/roles/custom/matrix-synapse/molecule/default/verify.yml @@ -0,0 +1,360 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +# Proves a single main Synapse process starts on the non-default listener the role +# rendered, serves real Matrix APIs, migrates the configured Postgres database, and +# carries no worker, Redis, S3, MAS, email, or other optional-integration topology. +- name: Verify Synapse + 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_synapse_config: "{{ matrix_synapse_config_file.content | b64decode | from_yaml }}" + matrix_synapse_http_listeners: >- + {{ matrix_synapse_config.listeners | selectattr('type', 'equalto', 'http') | list }} + matrix_synapse_http_listener: "{{ matrix_synapse_http_listeners | first | default({}) }}" + + tasks: + # The image version comes from the role, not from the scenario, so an assertion cannot + # remain green merely because both the scenario and runtime are pinned to the same typo. + - 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_synapse_role_defaults + + - name: Wait for the Synapse service to become active + ansible.builtin.systemd_service: + name: matrix-synapse.service + register: matrix_synapse_service + until: matrix_synapse_service.status.ActiveState == 'active' + retries: 30 + delay: 5 + failed_when: false + + - name: Assert the Synapse service is stable + ansible.builtin.assert: + that: + - matrix_synapse_service.status.ActiveState == 'active' + - matrix_synapse_service.status.NRestarts is defined + - matrix_synapse_service.status.NRestarts | int == 0 + fail_msg: >- + matrix-synapse.service is + {{ matrix_synapse_service.status.ActiveState | default('unknown') }} after + {{ matrix_synapse_service.status.NRestarts | default('?') }} restart(s) + success_msg: "matrix-synapse.service is active and has not restarted" + + # Probe over the role's own network. No host port is published, matching deployment. + - name: Request the Synapse health endpoint + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_synapse_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --show-error + - --write-out + - "\nHTTP_STATUS=%{http_code}" + - http://matrix-synapse:{{ matrix_synapse_container_client_api_port }}/health + register: matrix_synapse_health + changed_when: false + until: "'HTTP_STATUS=200' in matrix_synapse_health.stdout" + retries: 24 + delay: 5 + failed_when: false + + - name: Request the Synapse Matrix versions endpoint + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_synapse_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --show-error + - --write-out + - "\nHTTP_STATUS=%{http_code}" + - http://matrix-synapse:{{ matrix_synapse_container_client_api_port }}/_matrix/client/versions + register: matrix_synapse_versions_response + changed_when: false + until: "'HTTP_STATUS=200' in matrix_synapse_versions_response.stdout" + retries: 24 + delay: 5 + failed_when: false + + - name: Assert Synapse serves its health endpoint + ansible.builtin.assert: + that: + - matrix_synapse_health.rc == 0 + - "'HTTP_STATUS=200' in matrix_synapse_health.stdout" + - matrix_synapse_health.stdout.startswith('OK') + fail_msg: >- + Synapse did not serve /health on port + {{ matrix_synapse_container_client_api_port }} + success_msg: "Synapse serves /health" + + - name: Assert Synapse serves its Matrix versions endpoint + ansible.builtin.assert: + that: + - matrix_synapse_versions_response.rc == 0 + - "'HTTP_STATUS=200' in matrix_synapse_versions_response.stdout" + fail_msg: >- + Synapse did not serve /_matrix/client/versions on port + {{ matrix_synapse_container_client_api_port }} + success_msg: "Synapse serves /_matrix/client/versions" + + - name: Parse the Matrix versions response + ansible.builtin.set_fact: + matrix_synapse_versions: >- + {{ matrix_synapse_versions_response.stdout + | regex_replace('\nHTTP_STATUS=200$', '') + | from_json }} + + - name: Assert the versions response is a real Matrix API document + ansible.builtin.assert: + that: + - matrix_synapse_versions.versions is sequence + - matrix_synapse_versions.versions | length > 0 + - "'v1.1' in matrix_synapse_versions.versions" + - matrix_synapse_versions.unstable_features is mapping + fail_msg: "The versions endpoint did not return a Matrix versions document" + success_msg: "The versions endpoint returns a Matrix versions document" + + # The role moved the listener away from 8008. Refusing the upstream default port proves + # the successful requests above did not accidentally hit an unchanged default listener. + - name: Request the upstream-default Synapse port + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_synapse_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --output + - /dev/null + - --connect-timeout + - '2' + - --max-time + - '3' + - http://matrix-synapse:8008/health + register: matrix_synapse_default_listener + changed_when: false + failed_when: false + + - name: Assert Synapse does not listen on the upstream-default port + ansible.builtin.assert: + that: + - matrix_synapse_default_listener.rc != 0 + fail_msg: "Synapse still answers on port 8008, so the non-default listener is not proven" + success_msg: "Only the role-configured client listener answers" + + - name: Read the homeserver configuration the role rendered + ansible.builtin.slurp: + src: "{{ matrix_synapse_config_dir_path }}/homeserver.yaml" + register: matrix_synapse_config_file + + - name: Read the homeserver configuration identity + ansible.builtin.stat: + path: "{{ matrix_synapse_config_dir_path }}/homeserver.yaml" + register: matrix_synapse_config_stat + + - name: Assert the parsed homeserver identity and behavior + ansible.builtin.assert: + that: + - matrix_synapse_config.server_name == matrix_domain + - matrix_synapse_config.public_baseurl == matrix_synapse_public_baseurl + - matrix_synapse_config.max_upload_size == (matrix_synapse_max_upload_size_mb | string) + 'M' + - not matrix_synapse_config.presence.enabled + - not matrix_synapse_config.url_preview_enabled + - not matrix_synapse_config.allow_public_rooms_over_federation + - matrix_synapse_config.user_directory.search_all_users + - matrix_synapse_config.user_directory.prefer_local_users + - matrix_synapse_config.trusted_key_servers == [] + fail_msg: "The parsed homeserver configuration does not carry the scenario's values" + success_msg: "The parsed homeserver configuration carries the scenario's values" + + - name: Assert the parsed homeserver secrets + ansible.builtin.assert: + that: + - matrix_synapse_config.macaroon_secret_key == matrix_synapse_macaroon_secret_key + - matrix_synapse_config.registration_shared_secret == matrix_synapse_registration_shared_secret + - matrix_synapse_config.form_secret == matrix_synapse_form_secret + - matrix_synapse_config.password_config.pepper == matrix_synapse_password_config_pepper + fail_msg: "The parsed homeserver configuration does not carry the scenario's secrets" + success_msg: "The parsed homeserver configuration carries the scenario's secrets" + no_log: true + + - name: Assert the parsed client listener topology + ansible.builtin.assert: + that: + - matrix_synapse_config.listeners | length == 1 + - matrix_synapse_http_listeners | length == 1 + - matrix_synapse_http_listener.port == matrix_synapse_container_client_api_port + - not matrix_synapse_http_listener.tls + - matrix_synapse_http_listener.bind_addresses == ['::'] + - matrix_synapse_http_listener.resources | length == 1 + - matrix_synapse_http_listener.resources[0].names == ['client'] + fail_msg: "The parsed homeserver configuration has an unexpected listener topology" + success_msg: "The parsed homeserver configuration has one non-default client listener" + + - name: Assert the parsed Postgres configuration + ansible.builtin.assert: + that: + - matrix_synapse_config.database.name == 'psycopg2' + - matrix_synapse_config.database.args.host == matrix_synapse_database_host + - matrix_synapse_config.database.args.port == matrix_synapse_database_port + - matrix_synapse_config.database.args.user == matrix_synapse_database_user + - matrix_synapse_config.database.args.password == matrix_synapse_database_password + - matrix_synapse_config.database.args.database == matrix_synapse_database_database + - matrix_synapse_config.database.args.cp_min == matrix_synapse_database_cp_min + - matrix_synapse_config.database.args.cp_max == matrix_synapse_database_cp_max + fail_msg: "The parsed homeserver configuration does not point at scenario Postgres" + success_msg: "The parsed homeserver configuration points at scenario Postgres" + no_log: true + + - name: Assert optional Synapse topology is absent + ansible.builtin.assert: + that: + - not matrix_synapse_config.redis.enabled + - matrix_synapse_config.media_storage_providers == [] + - matrix_synapse_config.modules == [] + - "'matrix_authentication_service' not in matrix_synapse_config" + - "'email' not in matrix_synapse_config" + - "'worker_app' not in matrix_synapse_config" + fail_msg: "An excluded worker, Redis, S3, MAS, email, or module integration is configured" + success_msg: "The configuration contains only the intended main-process topology" + + - name: Assert the homeserver configuration file identity + ansible.builtin.assert: + that: + - matrix_synapse_config_stat.stat.uid | int == matrix_user_uid | int + - matrix_synapse_config_stat.stat.gid | int == matrix_user_gid | int + - matrix_synapse_config_stat.stat.mode == '0644' + fail_msg: "homeserver.yaml has the wrong ownership or mode" + success_msg: "homeserver.yaml has the playbook identity and expected mode" + + # These tables appear only after hostname resolution, authentication, and genuine Synapse + # migrations against the exact non-default database prepared for the scenario. + - name: List the tables Synapse created in Postgres + ansible.builtin.command: + argv: + - docker + - exec + - matrix-postgres-molecule + - psql + - --username={{ matrix_synapse_database_user }} + - --dbname={{ matrix_synapse_database_database }} + - --tuples-only + - --no-align + - --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public' + register: matrix_synapse_tables + changed_when: false + + - name: Assert Synapse migrated its Postgres schema + ansible.builtin.assert: + that: + - matrix_synapse_tables.rc == 0 + - "'schema_version' in matrix_synapse_table_names" + - "'users' in matrix_synapse_table_names" + - "'rooms' in matrix_synapse_table_names" + - "'events' in matrix_synapse_table_names" + - "'state_groups' in matrix_synapse_table_names" + - matrix_synapse_table_names | length > 50 + fail_msg: >- + Synapse did not migrate its schema in {{ matrix_synapse_database_database }} + (found {{ matrix_synapse_table_names | length }} table(s)) + success_msg: "Synapse genuinely migrated the configured Postgres database" + vars: + matrix_synapse_table_names: "{{ matrix_synapse_tables.stdout_lines | select | list }}" + + - name: Inspect the running Synapse container + ansible.builtin.command: + argv: + - docker + - container + - inspect + - matrix-synapse + register: matrix_synapse_container_inspect + changed_when: false + + - name: Parse the running Synapse container inspection + ansible.builtin.set_fact: + matrix_synapse_container: "{{ (matrix_synapse_container_inspect.stdout | from_json) | first }}" + + - name: Assert the running container uses the exact pinned image + ansible.builtin.assert: + that: + - matrix_synapse_container.Config.Image == 'ghcr.io/element-hq/synapse:' + matrix_synapse_role_defaults.matrix_synapse_version + fail_msg: "The running container does not use the exact image defaults/main.yml pins" + success_msg: "The running container uses the exact image defaults/main.yml pins" + + - name: Assert the running container uses the scenario identity + ansible.builtin.assert: + that: + - matrix_synapse_container.Config.User == (matrix_user_uid | string) + ':' + (matrix_user_gid | string) + fail_msg: "The running container does not use the playbook-supplied identity" + success_msg: "The running container uses the playbook-supplied identity" + + - name: Assert the running container security isolation + ansible.builtin.assert: + that: + - matrix_synapse_container.HostConfig.ReadonlyRootfs + - "'ALL' in matrix_synapse_container.HostConfig.CapDrop" + fail_msg: "The running container does not have read-only, capability-dropped isolation" + success_msg: "The running container has read-only, capability-dropped isolation" + + - name: Assert the running container has a read-only config mount + ansible.builtin.assert: + that: + - matrix_synapse_config_mount | length > 0 + - not matrix_synapse_config_mount.RW | default(true) + fail_msg: "The running container does not mount its configuration read-only" + success_msg: "The running container mounts its configuration read-only" + vars: + matrix_synapse_config_mount: >- + {{ matrix_synapse_container.Mounts + | selectattr('Destination', 'equalto', '/data') + | first | default({}) }} + + - name: Assert the running container has a writable storage mount + ansible.builtin.assert: + that: + - matrix_synapse_storage_mount | length > 0 + - matrix_synapse_storage_mount.RW | default(false) + fail_msg: "The running container does not mount its media storage writable" + success_msg: "The running container mounts its media storage writable" + vars: + matrix_synapse_storage_mount: >- + {{ matrix_synapse_container.Mounts + | selectattr('Destination', 'equalto', '/matrix-media-store-parent') + | first | default({}) }} + + - name: Assert the running container is healthy + ansible.builtin.assert: + that: + - matrix_synapse_container.State.Health.Status == 'healthy' + fail_msg: "The running container is not healthy" + success_msg: "The running container is healthy" + + - name: Assert the running container has only its private network + ansible.builtin.assert: + that: + - matrix_synapse_container_network in matrix_synapse_container.NetworkSettings.Networks + - matrix_synapse_container.NetworkSettings.Networks | length == 1 + fail_msg: "The running container has unexpected network attachments" + success_msg: "The running container has only its private network" + + - name: Assert the running container publishes no host ports + ansible.builtin.assert: + that: + - matrix_synapse_container.HostConfig.PortBindings | default({}, true) | length == 0 + fail_msg: "The running container unexpectedly publishes a host port" + success_msg: "The running container publishes no host ports"