|
- # 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"
|