From 8cf346b9ecaa72e617b7a61f256fcb85ad780c4a Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Sun, 30 Aug 2026 18:07:46 +0300 Subject: [PATCH] Test matrix-synapse-auto-compressor with Molecule --- .github/renovate.json | 3 +- .../molecule/default/converge.yml | 63 ++ .../molecule/default/molecule.yml | 119 ++++ .../molecule/default/prepare.yml | 128 ++++ .../molecule/default/requirements.yml | 1 + .../molecule/default/verify.yml | 591 ++++++++++++++++++ 6 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 roles/custom/matrix-synapse-auto-compressor/molecule/default/converge.yml create mode 100644 roles/custom/matrix-synapse-auto-compressor/molecule/default/molecule.yml create mode 100644 roles/custom/matrix-synapse-auto-compressor/molecule/default/prepare.yml create mode 120000 roles/custom/matrix-synapse-auto-compressor/molecule/default/requirements.yml create mode 100644 roles/custom/matrix-synapse-auto-compressor/molecule/default/verify.yml diff --git a/.github/renovate.json b/.github/renovate.json index c666eb5eb..df8633b4e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -139,7 +139,8 @@ "roles/custom/matrix-client-element/defaults/main.yml", "roles/custom/matrix-ketesa/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" ], "matchUpdateTypes": [ "patch" diff --git a/roles/custom/matrix-synapse-auto-compressor/molecule/default/converge.yml b/roles/custom/matrix-synapse-auto-compressor/molecule/default/converge.yml new file mode 100644 index 000000000..862fd3812 --- /dev/null +++ b/roles/custom/matrix-synapse-auto-compressor/molecule/default/converge.yml @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Include roles for matrix-synapse-auto-compressor 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: Include base roles for matrix-synapse-auto-compressor 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 + + # A genuine Synapse installation and migration is part of the fixture; hand-made subsets + # of the schema cannot establish compatibility with what Synapse actually deploys. + - name: Include Synapse setup task files + ansible.builtin.include_role: + name: custom/matrix-synapse + public: true + tasks_from: "{{ role_task_file }}" + loop: + - init.yml + - validate_config.yml + - setup_install.yml + loop_control: + loop_var: role_task_file + + - name: Include matrix-synapse-auto-compressor + ansible.builtin.include_role: + name: "custom/{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + public: true + vars: + matrix_synapse_auto_compressor_postgres_image: "{{ molecule_shared_image_postgres }}" + +- name: Start the Synapse fixture and compressor timer + 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 + + - name: Ensure the compressor timer is enabled and started + ansible.builtin.systemd_service: + name: matrix-synapse-auto-compressor.timer + enabled: true + state: started diff --git a/roles/custom/matrix-synapse-auto-compressor/molecule/default/molecule.yml b/roles/custom/matrix-synapse-auto-compressor/molecule/default/molecule.yml new file mode 100644 index 000000000..91c9ea3fa --- /dev/null +++ b/roles/custom/matrix-synapse-auto-compressor/molecule/default/molecule.yml @@ -0,0 +1,119 @@ +# 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: matrix-synapse-auto-compressor-${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: + # A real single-process Synapse supplies its migrated schema and the state groups + # exercised by the compressor. Optional Synapse topology stays out of this 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-auto-compressor-molecule + 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 + + matrix_synapse_database_host: matrix-postgres-molecule + matrix_synapse_database_port: 5432 + matrix_synapse_database_user: molecule_compressor + matrix_synapse_database_password: molecule_compressor_pg_password_9b742d + matrix_synapse_database_database: molecule_compressor + + matrix_synapse_macaroon_secret_key: molecule_compressor_macaroon_4b3f891d + matrix_synapse_registration_shared_secret: molecule_compressor_registration_27ed51a8 + matrix_synapse_form_secret: molecule_compressor_form_96a04c7b + matrix_synapse_password_config_pepper: molecule_compressor_pepper_c0f371e6 + matrix_synapse_public_baseurl: http://matrix.molecule.local:18008/ + matrix_synapse_rc_message: + per_second: 1000 + burst_count: 1000 + matrix_synapse_trusted_key_servers: [] + matrix_synapse_suppress_key_server_warning: true + 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 + + # These non-defaults exercise every role-supplied runtime and scheduling input. + matrix_synapse_auto_compressor_uid: 1234 + matrix_synapse_auto_compressor_gid: 1234 + matrix_synapse_auto_compressor_container_network: synapse-auto-compressor-molecule + matrix_synapse_auto_compressor_container_additional_networks: + - synapse-auto-compressor-extra-molecule + matrix_synapse_auto_compressor_container_extra_arguments: + - "--label=molecule.contract=synapse-auto-compressor" + matrix_synapse_auto_compressor_database_hostname: matrix-postgres-molecule + matrix_synapse_auto_compressor_database_port: 5432 + matrix_synapse_auto_compressor_database_username: molecule_compressor + matrix_synapse_auto_compressor_database_password: molecule_compressor_pg_password_9b742d + matrix_synapse_auto_compressor_database_name: molecule_compressor + matrix_synapse_auto_compressor_schedule: "Mon..Fri *-*-* 03:17:00" + matrix_synapse_auto_compressor_schedule_randomized_delay_sec: 47min + matrix_synapse_auto_compressor_chunk_size: 128 + matrix_synapse_auto_compressor_chunks_to_compress: 2 + # Keep the transient container alive briefly after genuine compression so verify.yml + # can inspect the runtime contract before Type=oneshot removes it. + matrix_synapse_auto_compressor_command: >- + synapse_auto_compressor -p $$POSTGRES_LOCATION -c 128 -n 2 && sleep 45 + + # verify.yml is a separate play, so paths derived from role defaults are pinned here. + 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 + matrix_synapse_auto_compressor_base_path: /matrix/synapse-auto-compressor + 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-auto-compressor/molecule/default/prepare.yml b/roles/custom/matrix-synapse-auto-compressor/molecule/default/prepare.yml new file mode 100644 index 000000000..76c4674ce --- /dev/null +++ b/roles/custom/matrix-synapse-auto-compressor/molecule/default/prepare.yml @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Prepare matrix-synapse-auto-compressor 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 + + # Synapse's production unit uses slave bind propagation for the media store. + - 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 shared Synapse and compressor network exists + ansible.builtin.command: + argv: + - docker + - network + - create + - "{{ matrix_synapse_auto_compressor_container_network }}" + register: matrix_synapse_auto_compressor_molecule_network + changed_when: matrix_synapse_auto_compressor_molecule_network.rc == 0 + failed_when: + - matrix_synapse_auto_compressor_molecule_network.rc != 0 + - "'already exists' not in matrix_synapse_auto_compressor_molecule_network.stderr" + + - name: Ensure the compressor additional network exists + ansible.builtin.command: + argv: + - docker + - network + - create + - "{{ matrix_synapse_auto_compressor_container_additional_networks | first }}" + register: matrix_synapse_auto_compressor_molecule_additional_network + changed_when: matrix_synapse_auto_compressor_molecule_additional_network.rc == 0 + failed_when: + - matrix_synapse_auto_compressor_molecule_additional_network.rc != 0 + - "'already exists' not in matrix_synapse_auto_compressor_molecule_additional_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_auto_compressor_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 refuses locale-dependent database collation. + - 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-auto-compressor/molecule/default/requirements.yml b/roles/custom/matrix-synapse-auto-compressor/molecule/default/requirements.yml new file mode 120000 index 000000000..1567a5392 --- /dev/null +++ b/roles/custom/matrix-synapse-auto-compressor/molecule/default/requirements.yml @@ -0,0 +1 @@ +../../../../../molecule-shared/requirements.yml \ No newline at end of file diff --git a/roles/custom/matrix-synapse-auto-compressor/molecule/default/verify.yml b/roles/custom/matrix-synapse-auto-compressor/molecule/default/verify.yml new file mode 100644 index 000000000..7df084117 --- /dev/null +++ b/roles/custom/matrix-synapse-auto-compressor/molecule/default/verify.yml @@ -0,0 +1,591 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +# Proves the role drives the real compressor against a genuinely migrated Synapse +# Postgres database, and observes the otherwise-transient container while it runs. +- name: Verify matrix-synapse-auto-compressor + 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_auto_compressor_expected_postgres_location: >- + postgres://molecule_compressor:molecule_compressor_pg_password_9b742d@matrix-postgres-molecule:5432/molecule_compressor + matrix_synapse_auto_compressor_expected_command: >- + synapse_auto_compressor -p $POSTGRES_LOCATION -c 128 -n 2 && sleep 45 + matrix_synapse_auto_compressor_seed_username: compressor-admin + matrix_synapse_auto_compressor_seed_password: molecule_compressor_admin_password_1d3b + + 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_synapse_auto_compressor_role_defaults + + - name: Wait for the Synapse fixture to become active + ansible.builtin.systemd_service: + name: matrix-synapse.service + register: matrix_synapse_auto_compressor_synapse_service + until: matrix_synapse_auto_compressor_synapse_service.status.ActiveState == 'active' + retries: 30 + delay: 5 + failed_when: false + + - name: Request the real Synapse health endpoint + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_synapse_auto_compressor_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_auto_compressor_synapse_health + changed_when: false + until: "'HTTP_STATUS=200' in matrix_synapse_auto_compressor_synapse_health.stdout" + retries: 24 + delay: 5 + failed_when: false + + - name: Assert the real Synapse fixture is healthy and stable + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_synapse_service.status.ActiveState == 'active' + - matrix_synapse_auto_compressor_synapse_service.status.NRestarts is defined + - matrix_synapse_auto_compressor_synapse_service.status.NRestarts | int == 0 + - matrix_synapse_auto_compressor_synapse_health.rc == 0 + - "'HTTP_STATUS=200' in matrix_synapse_auto_compressor_synapse_health.stdout" + - matrix_synapse_auto_compressor_synapse_health.stdout.startswith('OK') + fail_msg: "The real Synapse fixture is not healthy and stable" + success_msg: "The real Synapse fixture is healthy and stable" + + # These tables can only be present after Synapse resolved the configured hostname, + # authenticated to Postgres, and performed its own migrations. + - 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_auto_compressor_synapse_tables + changed_when: false + + - name: Assert Synapse genuinely migrated the compressor database + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_synapse_tables.rc == 0 + - "'schema_version' in matrix_synapse_auto_compressor_synapse_table_names" + - "'rooms' in matrix_synapse_auto_compressor_synapse_table_names" + - "'events' in matrix_synapse_auto_compressor_synapse_table_names" + - "'state_groups' in matrix_synapse_auto_compressor_synapse_table_names" + - "'state_groups_state' in matrix_synapse_auto_compressor_synapse_table_names" + - matrix_synapse_auto_compressor_synapse_table_names | length > 50 + fail_msg: "Synapse did not genuinely migrate the configured Postgres database" + success_msg: "Synapse genuinely migrated the configured Postgres database" + vars: + matrix_synapse_auto_compressor_synapse_table_names: >- + {{ matrix_synapse_auto_compressor_synapse_tables.stdout_lines | select | list }} + + - name: Register the local compressor fixture administrator + ansible.builtin.command: + argv: + - docker + - exec + - matrix-synapse + - register_new_matrix_user + - --config + - /data/homeserver.yaml + - --user + - "{{ matrix_synapse_auto_compressor_seed_username }}" + - --password + - "{{ matrix_synapse_auto_compressor_seed_password }}" + - --admin + - http://localhost:{{ matrix_synapse_container_client_api_port }} + register: matrix_synapse_auto_compressor_registration + changed_when: matrix_synapse_auto_compressor_registration.rc == 0 + failed_when: + - matrix_synapse_auto_compressor_registration.rc != 0 + - "'User ID already taken' not in matrix_synapse_auto_compressor_registration.stdout" + no_log: true + + - name: Log the local fixture administrator in to Synapse + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_synapse_auto_compressor_container_network }} + - "{{ molecule_shared_image_curl }}" + - --fail-with-body + - --silent + - --show-error + - --request + - POST + - --header + - 'Content-Type: application/json' + - --data + - >- + {"type":"m.login.password","identifier":{"type":"m.id.user","user":"{{ matrix_synapse_auto_compressor_seed_username }}"},"password":"{{ matrix_synapse_auto_compressor_seed_password }}"} + - http://matrix-synapse:{{ matrix_synapse_container_client_api_port }}/_matrix/client/v3/login + register: matrix_synapse_auto_compressor_login_response + changed_when: false + no_log: true + + - name: Parse the local fixture login response + ansible.builtin.set_fact: + matrix_synapse_auto_compressor_login: "{{ matrix_synapse_auto_compressor_login_response.stdout | from_json }}" + no_log: true + + - name: Assert Synapse authenticated the local fixture administrator + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_login.access_token is string + - matrix_synapse_auto_compressor_login.access_token | length > 20 + - matrix_synapse_auto_compressor_login.user_id == '@' + matrix_synapse_auto_compressor_seed_username + ':' + matrix_domain + fail_msg: "Synapse did not authenticate the local compressor fixture administrator" + success_msg: "Synapse authenticated the local compressor fixture administrator" + no_log: true + + - name: Create a real Matrix room for compressor state + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_synapse_auto_compressor_container_network }} + - "{{ molecule_shared_image_curl }}" + - --fail-with-body + - --silent + - --show-error + - --request + - POST + - --header + - 'Content-Type: application/json' + - --header + - 'Authorization: Bearer {{ matrix_synapse_auto_compressor_login.access_token }}' + - --data + - '{"preset":"private_chat","name":"Compressor Molecule State"}' + - http://matrix-synapse:{{ matrix_synapse_container_client_api_port }}/_matrix/client/v3/createRoom + register: matrix_synapse_auto_compressor_room_response + changed_when: true + no_log: true + + - name: Parse the real Matrix room response + ansible.builtin.set_fact: + matrix_synapse_auto_compressor_room: "{{ matrix_synapse_auto_compressor_room_response.stdout | from_json }}" + no_log: true + + # A unique state key per request creates enough genuine Synapse state groups for the + # non-default chunk size to consume a complete chunk, without manufacturing database rows. + - name: Populate the room with genuine state transitions through Synapse + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_synapse_auto_compressor_container_network }} + - --env=ACCESS_TOKEN={{ matrix_synapse_auto_compressor_login.access_token }} + - --env=ROOM_ID={{ matrix_synapse_auto_compressor_room.room_id }} + - --entrypoint=/bin/sh + - "{{ molecule_shared_image_curl }}" + - -c + - |- + set -eu; + i=1; + while [ "$i" -le 140 ]; do + curl --fail-with-body --silent --show-error \ + --request PUT \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "{\"sequence\":$i}" \ + "http://matrix-synapse:{{ matrix_synapse_container_client_api_port }}/_matrix/client/v3/rooms/$ROOM_ID/state/com.example.molecule/$i" \ + >/dev/null; + i=$((i + 1)); + done + register: matrix_synapse_auto_compressor_seed_result + changed_when: matrix_synapse_auto_compressor_seed_result.rc == 0 + failed_when: false + no_log: true + + - name: Report a sanitized state-seeding failure + ansible.builtin.debug: + msg: "{{ matrix_synapse_auto_compressor_seed_result.stderr }}" + when: matrix_synapse_auto_compressor_seed_result.rc != 0 + + - name: Assert Synapse accepted every real state transition + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_seed_result.rc == 0 + fail_msg: "Synapse rejected one of the real Matrix state transitions" + success_msg: "Synapse accepted every real Matrix state transition" + + - name: Count genuine state groups for the Matrix room + 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 COUNT(*) FROM state_groups + WHERE room_id = '{{ matrix_synapse_auto_compressor_room.room_id }}' + register: matrix_synapse_auto_compressor_state_group_count + changed_when: false + + - name: Assert Synapse created enough genuine state groups for a complete chunk + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_state_group_count.rc == 0 + - matrix_synapse_auto_compressor_state_group_count.stdout | trim | int >= matrix_synapse_auto_compressor_chunk_size | int + fail_msg: "The Matrix API fixture did not create a complete real compressor chunk" + success_msg: "The Matrix API fixture created a complete real compressor chunk" + + - name: Read the rendered compressor files + ansible.builtin.slurp: + src: "{{ item }}" + loop: + - "{{ matrix_synapse_auto_compressor_base_path }}/env" + - "{{ matrix_synapse_auto_compressor_base_path }}/matrix-synapse-auto-compressor-fix.sh" + - /etc/systemd/system/matrix-synapse-auto-compressor.service + - /etc/systemd/system/matrix-synapse-auto-compressor.timer + register: matrix_synapse_auto_compressor_rendered_files + + - name: Inspect the rendered compressor file identities + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "{{ matrix_synapse_auto_compressor_base_path }}" + - "{{ matrix_synapse_auto_compressor_base_path }}/env" + - "{{ matrix_synapse_auto_compressor_base_path }}/matrix-synapse-auto-compressor-fix.sh" + - /etc/systemd/system/matrix-synapse-auto-compressor.service + - /etc/systemd/system/matrix-synapse-auto-compressor.timer + register: matrix_synapse_auto_compressor_rendered_stats + + - name: Parse the rendered compressor files + ansible.builtin.set_fact: + matrix_synapse_auto_compressor_rendered_env: "{{ matrix_synapse_auto_compressor_rendered_files.results[0].content | b64decode }}" + matrix_synapse_auto_compressor_rendered_script: "{{ matrix_synapse_auto_compressor_rendered_files.results[1].content | b64decode }}" + matrix_synapse_auto_compressor_rendered_service: "{{ matrix_synapse_auto_compressor_rendered_files.results[2].content | b64decode }}" + matrix_synapse_auto_compressor_rendered_timer: "{{ matrix_synapse_auto_compressor_rendered_files.results[3].content | b64decode }}" + + - name: Check the workaround script syntax + ansible.builtin.command: + argv: + - bash + - -n + - "{{ matrix_synapse_auto_compressor_base_path }}/matrix-synapse-auto-compressor-fix.sh" + register: matrix_synapse_auto_compressor_script_syntax + changed_when: false + + - name: Verify systemd parsed the rendered units + ansible.builtin.command: + argv: + - systemd-analyze + - verify + - matrix-synapse-auto-compressor.service + - matrix-synapse-auto-compressor.timer + register: matrix_synapse_auto_compressor_unit_syntax + changed_when: false + + - name: Verify systemd parsed the non-default calendar + ansible.builtin.command: + argv: + - systemd-analyze + - calendar + - "{{ matrix_synapse_auto_compressor_schedule }}" + register: matrix_synapse_auto_compressor_calendar_syntax + changed_when: false + + - name: Assert the rendered environment carries the exact Postgres connection + ansible.builtin.assert: + that: + - "'POSTGRES_LOCATION=' + matrix_synapse_auto_compressor_expected_postgres_location in matrix_synapse_auto_compressor_rendered_env.splitlines()" + - "'PGHOST=' + matrix_synapse_auto_compressor_database_hostname in matrix_synapse_auto_compressor_rendered_env.splitlines()" + - "'PGDATABASE=' + matrix_synapse_auto_compressor_database_name in matrix_synapse_auto_compressor_rendered_env.splitlines()" + - "'PGUSER=' + matrix_synapse_auto_compressor_database_username in matrix_synapse_auto_compressor_rendered_env.splitlines()" + - "'PGPASSWORD=' + matrix_synapse_auto_compressor_database_password in matrix_synapse_auto_compressor_rendered_env.splitlines()" + fail_msg: "The rendered environment does not carry the exact scenario Postgres connection" + success_msg: "The rendered environment carries the exact scenario Postgres connection" + no_log: true + + - name: Assert the rendered workaround script contract + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_script_syntax.rc == 0 + - "'--user=1234:1234' in matrix_synapse_auto_compressor_rendered_script" + - "'--cap-drop=ALL' in matrix_synapse_auto_compressor_rendered_script" + - "'--network=' + matrix_synapse_auto_compressor_container_network in matrix_synapse_auto_compressor_rendered_script" + - molecule_shared_image_postgres in matrix_synapse_auto_compressor_rendered_script + - "'psql -h ' + matrix_synapse_auto_compressor_database_hostname in matrix_synapse_auto_compressor_rendered_script" + - "'DELETE\nFROM state_compressor_state' in matrix_synapse_auto_compressor_rendered_script" + - "'DELETE\nFROM state_compressor_progress' in matrix_synapse_auto_compressor_rendered_script" + fail_msg: "The rendered workaround script does not carry its runtime and cleanup contract" + success_msg: "The rendered workaround script carries its runtime and cleanup contract" + + - name: Assert the rendered oneshot service contract + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_unit_syntax.rc == 0 + - "'Type=oneshot' in matrix_synapse_auto_compressor_rendered_service" + - "'--rm' in matrix_synapse_auto_compressor_rendered_service" + - "'--log-driver=none' in matrix_synapse_auto_compressor_rendered_service" + - "'--cap-drop=ALL' in matrix_synapse_auto_compressor_rendered_service" + - "'--read-only' in matrix_synapse_auto_compressor_rendered_service" + - "'--user=1234:1234' in matrix_synapse_auto_compressor_rendered_service" + - "'--network=' + matrix_synapse_auto_compressor_container_network in matrix_synapse_auto_compressor_rendered_service" + - "'--label=molecule.contract=synapse-auto-compressor' in matrix_synapse_auto_compressor_rendered_service" + - matrix_synapse_auto_compressor_expected_command in matrix_synapse_auto_compressor_rendered_service + - "'network connect synapse-auto-compressor-extra-molecule matrix-synapse-auto-compressor' in matrix_synapse_auto_compressor_rendered_service" + fail_msg: "The rendered service does not carry the exact oneshot runtime contract" + success_msg: "The rendered service carries the exact oneshot runtime contract" + + - name: Assert the rendered non-default timer contract + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_calendar_syntax.rc == 0 + - "'Unit=matrix-synapse-auto-compressor.service' in matrix_synapse_auto_compressor_rendered_timer" + - "'OnCalendar=Mon..Fri *-*-* 03:17:00' in matrix_synapse_auto_compressor_rendered_timer" + - "'RandomizedDelaySec=47min' in matrix_synapse_auto_compressor_rendered_timer" + fail_msg: "The rendered timer does not carry the non-default schedule" + success_msg: "The rendered timer carries the non-default schedule" + + - name: Assert the rendered file identities + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_rendered_stats.results[0].stat.uid | int == matrix_user_uid | int + - matrix_synapse_auto_compressor_rendered_stats.results[0].stat.gid | int == matrix_user_gid | int + - matrix_synapse_auto_compressor_rendered_stats.results[0].stat.mode == '0750' + - matrix_synapse_auto_compressor_rendered_stats.results[1].stat.uid | int == matrix_user_uid | int + - matrix_synapse_auto_compressor_rendered_stats.results[1].stat.gid | int == matrix_user_gid | int + - matrix_synapse_auto_compressor_rendered_stats.results[1].stat.mode == '0640' + - matrix_synapse_auto_compressor_rendered_stats.results[2].stat.uid | int == matrix_user_uid | int + - matrix_synapse_auto_compressor_rendered_stats.results[2].stat.gid | int == matrix_user_gid | int + - matrix_synapse_auto_compressor_rendered_stats.results[2].stat.mode == '0750' + - matrix_synapse_auto_compressor_rendered_stats.results[3].stat.uid | int == 0 + - matrix_synapse_auto_compressor_rendered_stats.results[3].stat.gid | int == 0 + - matrix_synapse_auto_compressor_rendered_stats.results[3].stat.mode == '0644' + - matrix_synapse_auto_compressor_rendered_stats.results[4].stat.uid | int == 0 + - matrix_synapse_auto_compressor_rendered_stats.results[4].stat.gid | int == 0 + - matrix_synapse_auto_compressor_rendered_stats.results[4].stat.mode == '0644' + fail_msg: "The compressor paths and rendered files have the wrong identity or mode" + success_msg: "The compressor paths and rendered files have the expected identities and modes" + + - name: Inspect the enabled compressor timer + ansible.builtin.systemd_service: + name: matrix-synapse-auto-compressor.timer + register: matrix_synapse_auto_compressor_timer + + - name: Assert the compressor timer is enabled and active + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_timer.status.ActiveState == 'active' + - matrix_synapse_auto_compressor_timer.status.SubState == 'waiting' + - matrix_synapse_auto_compressor_timer.status.UnitFileState == 'enabled' + - matrix_synapse_auto_compressor_timer.status.NextElapseUSecRealtime is defined + - matrix_synapse_auto_compressor_timer.status.NextElapseUSecRealtime != 'infinity' + fail_msg: "The compressor timer is not enabled and waiting on its parsed schedule" + success_msg: "The compressor timer is enabled and waiting on its parsed schedule" + + # no_block gives the test a window to inspect this otherwise-transient container. The + # command has already performed genuine compression before its final short sleep. + - name: Start the real compressor asynchronously + ansible.builtin.systemd_service: + name: matrix-synapse-auto-compressor.service + state: started + no_block: true + changed_when: true + + - name: Wait for the transient compressor container to become inspectable + ansible.builtin.command: + argv: + - docker + - container + - inspect + - matrix-synapse-auto-compressor + register: matrix_synapse_auto_compressor_container_inspect + changed_when: false + until: matrix_synapse_auto_compressor_container_inspect.rc == 0 + retries: 30 + delay: 1 + failed_when: false + + - name: Parse the transient compressor container inspection + ansible.builtin.set_fact: + matrix_synapse_auto_compressor_container: >- + {{ (matrix_synapse_auto_compressor_container_inspect.stdout | from_json) | first }} + + - name: Assert the live container uses the exact pinned image + ansible.builtin.assert: + that: + - >- + matrix_synapse_auto_compressor_container.Config.Image == + 'registry.gitlab.com/mb-saces/rust-synapse-tools:' + matrix_synapse_auto_compressor_role_defaults.matrix_synapse_auto_compressor_version + fail_msg: "The live compressor container does not use the image defaults/main.yml pins" + success_msg: "The live compressor container uses the image defaults/main.yml pins" + + - name: Assert the live container process and environment contract + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_container.Config.User == '1234:1234' + - matrix_synapse_auto_compressor_container.Config.Entrypoint == ['/bin/sh'] + - matrix_synapse_auto_compressor_container.Config.Cmd == ['-c', matrix_synapse_auto_compressor_expected_command] + - "'POSTGRES_LOCATION=' + matrix_synapse_auto_compressor_expected_postgres_location in matrix_synapse_auto_compressor_container.Config.Env" + - "'PGHOST=' + matrix_synapse_auto_compressor_database_hostname in matrix_synapse_auto_compressor_container.Config.Env" + - "'PGDATABASE=' + matrix_synapse_auto_compressor_database_name in matrix_synapse_auto_compressor_container.Config.Env" + - "'PGUSER=' + matrix_synapse_auto_compressor_database_username in matrix_synapse_auto_compressor_container.Config.Env" + - "'PGPASSWORD=' + matrix_synapse_auto_compressor_database_password in matrix_synapse_auto_compressor_container.Config.Env" + fail_msg: "The live compressor process does not carry the exact identity, command, and environment" + success_msg: "The live compressor process carries the exact identity, command, and environment" + no_log: true + + - name: Assert the live container security contract + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_container.HostConfig.AutoRemove + - matrix_synapse_auto_compressor_container.HostConfig.ReadonlyRootfs + - "'ALL' in matrix_synapse_auto_compressor_container.HostConfig.CapDrop" + - matrix_synapse_auto_compressor_container.HostConfig.LogConfig.Type == 'none' + - matrix_synapse_auto_compressor_container.Mounts | length == 0 + fail_msg: "The live compressor container does not carry its ephemeral security isolation" + success_msg: "The live compressor container carries its ephemeral security isolation" + + - name: Assert the live container network contract + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_container_network in matrix_synapse_auto_compressor_container.NetworkSettings.Networks + - "'synapse-auto-compressor-extra-molecule' in matrix_synapse_auto_compressor_container.NetworkSettings.Networks" + - matrix_synapse_auto_compressor_container.NetworkSettings.Networks | length == 2 + fail_msg: "The live compressor container does not have exactly its two configured networks" + success_msg: "The live compressor container has exactly its two configured networks" + + - name: Assert the live container publishes no host ports + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_container.HostConfig.PortBindings | default({}, true) | length == 0 + - matrix_synapse_auto_compressor_container.Config.ExposedPorts | default({}, true) | length == 0 + fail_msg: "The live compressor container unexpectedly publishes a host port" + success_msg: "The live compressor container publishes no host ports" + + - name: Assert the live container carries the scenario label + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_container.Config.Labels['molecule.contract'] == 'synapse-auto-compressor' + fail_msg: "The live compressor container does not carry its configured label" + success_msg: "The live compressor container carries its configured label" + + - name: Wait for the oneshot compressor to finish + ansible.builtin.systemd_service: + name: matrix-synapse-auto-compressor.service + register: matrix_synapse_auto_compressor_service + until: matrix_synapse_auto_compressor_service.status.ActiveState in ['inactive', 'failed'] + retries: 60 + delay: 1 + failed_when: false + + - name: Assert the real compressor oneshot completed cleanly + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_service.status.ActiveState == 'inactive' + - matrix_synapse_auto_compressor_service.status.Result == 'success' + - matrix_synapse_auto_compressor_service.status.ExecMainCode == '1' + - matrix_synapse_auto_compressor_service.status.ExecMainStatus | int == 0 + - matrix_synapse_auto_compressor_service.status.NRestarts is defined + - matrix_synapse_auto_compressor_service.status.NRestarts | int == 0 + fail_msg: "The real compressor oneshot did not complete cleanly" + success_msg: "The real compressor oneshot completed cleanly" + + - name: List the compressor-owned Postgres tables + 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' AND tablename LIKE 'state_compressor_%' + register: matrix_synapse_auto_compressor_owned_tables + changed_when: false + + - name: Count rows in the compressor-owned progress tables + 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 + (SELECT COUNT(*) FROM state_compressor_state) || '|' || + (SELECT COUNT(*) FROM state_compressor_progress) || '|' || + (SELECT COUNT(*) FROM state_compressor_total_progress) || '|' || + (SELECT COUNT(*) FROM state_compressor_state + WHERE room_id = '{{ matrix_synapse_auto_compressor_room.room_id }}') || '|' || + (SELECT COUNT(*) FROM state_compressor_progress + WHERE room_id = '{{ matrix_synapse_auto_compressor_room.room_id }}') + register: matrix_synapse_auto_compressor_progress_counts + changed_when: false + + - name: Assert the real compressor created durable database-side progress + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_owned_tables.rc == 0 + - "'state_compressor_state' in matrix_synapse_auto_compressor_owned_table_names" + - "'state_compressor_progress' in matrix_synapse_auto_compressor_owned_table_names" + - "'state_compressor_total_progress' in matrix_synapse_auto_compressor_owned_table_names" + - matrix_synapse_auto_compressor_progress_counts.rc == 0 + - matrix_synapse_auto_compressor_progress_count_values | length == 5 + - matrix_synapse_auto_compressor_progress_count_values[0] | int > 0 + - matrix_synapse_auto_compressor_progress_count_values[1] | int > 0 + - matrix_synapse_auto_compressor_progress_count_values[2] | int > 0 + - matrix_synapse_auto_compressor_progress_count_values[3] | int > 0 + - matrix_synapse_auto_compressor_progress_count_values[4] | int > 0 + fail_msg: "The real compressor did not leave durable progress for the API-created room" + success_msg: "The real compressor left durable progress for the API-created room and globally" + vars: + matrix_synapse_auto_compressor_owned_table_names: >- + {{ matrix_synapse_auto_compressor_owned_tables.stdout_lines | select | list }} + matrix_synapse_auto_compressor_progress_count_values: >- + {{ matrix_synapse_auto_compressor_progress_counts.stdout | trim | split('|') }} + + - name: Confirm the ephemeral compressor container was removed + ansible.builtin.command: + argv: + - docker + - container + - inspect + - matrix-synapse-auto-compressor + register: matrix_synapse_auto_compressor_removed_container + changed_when: false + failed_when: false + + - name: Assert the completed compressor container was removed + ansible.builtin.assert: + that: + - matrix_synapse_auto_compressor_removed_container.rc != 0 + fail_msg: "The completed ephemeral compressor container was not removed" + success_msg: "The completed ephemeral compressor container was removed"