From d9997550f9df2128f1fb97a88491aa48148440c9 Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Mon, 31 Aug 2026 06:44:08 +0300 Subject: [PATCH] Test matrix-bot-honoroit with Molecule --- .github/renovate.json | 2 + .../molecule/default/converge.yml | 36 ++ .../molecule/default/molecule.yml | 122 +++++ .../molecule/default/prepare.yml | 88 ++++ .../molecule/default/requirements.yml | 1 + .../molecule/default/verify.yml | 441 ++++++++++++++++++ 6 files changed, 690 insertions(+) create mode 100644 roles/custom/matrix-bot-honoroit/molecule/default/converge.yml create mode 100644 roles/custom/matrix-bot-honoroit/molecule/default/molecule.yml create mode 100644 roles/custom/matrix-bot-honoroit/molecule/default/prepare.yml create mode 120000 roles/custom/matrix-bot-honoroit/molecule/default/requirements.yml create mode 100644 roles/custom/matrix-bot-honoroit/molecule/default/verify.yml diff --git a/.github/renovate.json b/.github/renovate.json index eed19a958..971f2ee27 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -125,6 +125,7 @@ "roles/custom/matrix-authentication-service/defaults/main.yml", "roles/custom/matrix-bot-baibot/defaults/main.yml", "roles/custom/matrix-bot-draupnir/defaults/main.yml", + "roles/custom/matrix-bot-honoroit/defaults/main.yml", "roles/custom/matrix-bot-maubot/defaults/main.yml", "roles/custom/matrix-bot-matrix-reminder-bot/defaults/main.yml", "roles/custom/matrix-bot-meowlnir/defaults/main.yml", @@ -154,6 +155,7 @@ "description": "Automerge minor bumps of explicitly approved components with a weight-bearing Molecule scenario (via branch push - no PR). This list is intentionally narrower than the patch rule: the scenario must exercise enough real behavior to be a useful compatibility gate across a minor release. A failure surfaces as a PR instead. Keep every entry backed by a scenario and the patch rule - bin/check-molecule-automerge-list.py (minor rule) enforces that.", "matchFileNames": [ "roles/custom/matrix-authentication-service/defaults/main.yml", + "roles/custom/matrix-bot-honoroit/defaults/main.yml", "roles/custom/matrix-bot-meowlnir/defaults/main.yml", "roles/custom/matrix-element-admin/defaults/main.yml", "roles/custom/matrix-static-files/defaults/main.yml", diff --git a/roles/custom/matrix-bot-honoroit/molecule/default/converge.yml b/roles/custom/matrix-bot-honoroit/molecule/default/converge.yml new file mode 100644 index 000000000..b57497c64 --- /dev/null +++ b/roles/custom/matrix-bot-honoroit/molecule/default/converge.yml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Include roles for matrix-bot-honoroit Molecule tests + hosts: all + become: true + vars_files: + - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml" + gather_facts: true + tasks: + - name: Include roles for matrix-bot-honoroit 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 + - "custom/{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + loop_control: + loop_var: role_name + +- name: Ensure matrix-bot-honoroit 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 matrix-bot-honoroit systemd service is started + ansible.builtin.systemd_service: + name: matrix-bot-honoroit.service + state: started diff --git a/roles/custom/matrix-bot-honoroit/molecule/default/molecule.yml b/roles/custom/matrix-bot-honoroit/molecule/default/molecule.yml new file mode 100644 index 000000000..2845f4377 --- /dev/null +++ b/roles/custom/matrix-bot-honoroit/molecule/default/molecule.yml @@ -0,0 +1,122 @@ +# 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-bot-honoroit-${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: + # setup.yml normally carries this play-wide switch. + run_setup: true + + matrix_bot_honoroit_hostname: honoroit.molecule.local + matrix_bot_honoroit_path_prefix: /molecule-honoroit + matrix_bot_honoroit_metrics_path: /molecule-metrics + matrix_bot_honoroit_container_port: 18080 + matrix_bot_honoroit_container_network: matrix-bot-honoroit-molecule + matrix_bot_honoroit_container_additional_networks: + - matrix-bot-honoroit-molecule-additional + + # verify.yml is a separate play, where the role's defaults are out of scope. + matrix_bot_honoroit_base_path: /matrix/honoroit + matrix_bot_honoroit_config_path: /matrix/honoroit/config + matrix_bot_honoroit_data_path: /matrix/honoroit/data + + # Traefik itself is not deployed, but the full metrics routing contract is rendered + # with non-default values and compared with the live container labels. + matrix_bot_honoroit_container_labels_traefik_enabled: true + matrix_bot_honoroit_container_labels_traefik_docker_network: "{{ matrix_bot_honoroit_container_network }}" + matrix_bot_honoroit_container_labels_traefik_priority: 710 + matrix_bot_honoroit_container_labels_traefik_entrypoints: molecule-web + matrix_bot_honoroit_container_labels_traefik_tls: false + matrix_bot_honoroit_container_labels_traefik_additional_response_headers_custom: + X-Molecule-Honoroit: scenario + X-Robots-Tag: noindex + matrix_bot_honoroit_container_labels_additional_labels: | + molecule.honoroit.scenario=live-postgres-and-metrics + + # Real deployments use Postgres whenever the playbook's Postgres is enabled. All + # identifiers differ from the role defaults, so a migrated schema proves the role + # constructed and passed the configured DSN. + matrix_bot_honoroit_database_engine: postgres + matrix_bot_honoroit_database_hostname: matrix-postgres-molecule + matrix_bot_honoroit_database_username: molecule_honoroit_user + matrix_bot_honoroit_database_password: molecule_honoroit_password_8c45d1 + matrix_bot_honoroit_database_name: molecule_honoroit_database + matrix_bot_honoroit_database_sslmode: disable + + # These differ from both role and upstream defaults. The shared homeserver stub is + # sufficient for password login, crypto initialization and an idle sync loop. + matrix_bot_honoroit_login: molecule-honoroit + matrix_bot_honoroit_password: molecule_matrix_password_74aa5e + matrix_bot_honoroit_homeserver: http://matrix.molecule.local:8008 + matrix_bot_honoroit_roomid: "!molecule-operators:molecule.local" + matrix_bot_honoroit_prefix: "!molecule-help" + matrix_bot_honoroit_loglevel: DEBUG + matrix_bot_honoroit_cachesize: 317 + matrix_bot_honoroit_no_encryption_warning: true + matrix_bot_honoroit_ignorenothread: true + matrix_bot_honoroit_ignoredrooms: + - "!molecule-ignored-one:molecule.local" + - "!molecule-ignored-two:molecule.local" + matrix_bot_honoroit_allowedusers: + - "@support-*:molecule.local" + - "@operator:molecule.local" + matrix_bot_honoroit_text_prefix_open: "[MOLECULE OPEN]" + matrix_bot_honoroit_text_prefix_done: "[MOLECULE DONE]" + matrix_bot_honoroit_text_greetings: Molecule helpdesk greeting + matrix_bot_honoroit_text_done: Molecule helpdesk completion + + # Live metrics authentication forms the primary compatibility gate: correct + # credentials must work and both absent and wrong credentials must not. + matrix_bot_honoroit_auth_metrics_login: molecule-metrics-user + matrix_bot_honoroit_auth_metrics_password: molecule_metrics_password_c9ed32 + # Keep the generic environment extension observable independently of the role's + # first-class port wiring. + matrix_bot_honoroit_environment_variables_extension: | + MOLECULE_HONOROIT_MARKER=role-rendered-environment + + # Exercise arbitrary runtime arguments and a second private network without making + # anything reachable from the Molecule host. + matrix_bot_honoroit_container_extra_arguments: + - --pids-limit=73 + - --tmpfs=/tmp:rw,noexec,nosuid,size=16m + 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-bot-honoroit/molecule/default/prepare.yml b/roles/custom/matrix-bot-honoroit/molecule/default/prepare.yml new file mode 100644 index 000000000..1827f2cef --- /dev/null +++ b/roles/custom/matrix-bot-honoroit/molecule/default/prepare.yml @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Prepare matrix-bot-honoroit 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 + + - 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" + + - name: Ensure Honoroit's container networks exist + ansible.builtin.command: + argv: + - docker + - network + - create + - "{{ item }}" + loop: "{{ [matrix_bot_honoroit_container_network] + matrix_bot_honoroit_container_additional_networks }}" + register: matrix_bot_honoroit_molecule_networks + changed_when: matrix_bot_honoroit_molecule_networks.rc == 0 + failed_when: + - matrix_bot_honoroit_molecule_networks.rc != 0 + - "'already exists' not in matrix_bot_honoroit_molecule_networks.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_bot_honoroit_container_network }}" + molecule_shared_postgres_database: "{{ matrix_bot_honoroit_database_name }}" + molecule_shared_postgres_username: "{{ matrix_bot_honoroit_database_username }}" + molecule_shared_postgres_password: "{{ matrix_bot_honoroit_database_password }}" + + - name: Ensure the homeserver stub is running + ansible.builtin.include_tasks: + file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" + vars: + molecule_shared_stub_network: "{{ matrix_bot_honoroit_container_network }}" + molecule_shared_stub_server_name: molecule.local + molecule_shared_stub_user_id: "@{{ matrix_bot_honoroit_login }}:molecule.local" + molecule_shared_stub_joined_rooms: + - "{{ matrix_bot_honoroit_roomid }}" diff --git a/roles/custom/matrix-bot-honoroit/molecule/default/requirements.yml b/roles/custom/matrix-bot-honoroit/molecule/default/requirements.yml new file mode 120000 index 000000000..1567a5392 --- /dev/null +++ b/roles/custom/matrix-bot-honoroit/molecule/default/requirements.yml @@ -0,0 +1 @@ +../../../../../molecule-shared/requirements.yml \ No newline at end of file diff --git a/roles/custom/matrix-bot-honoroit/molecule/default/verify.yml b/roles/custom/matrix-bot-honoroit/molecule/default/verify.yml new file mode 100644 index 000000000..16addaa24 --- /dev/null +++ b/roles/custom/matrix-bot-honoroit/molecule/default/verify.yml @@ -0,0 +1,441 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Verify matrix-bot-honoroit + 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" + vars: + matrix_bot_honoroit_health_body: "{{ matrix_bot_honoroit_health.stdout_lines[:-1] | join('\n') }}" + matrix_bot_honoroit_metrics_body: "{{ matrix_bot_honoroit_metrics.stdout_lines[:-1] | join('\n') }}" + matrix_bot_honoroit_metrics_lines: "{{ matrix_bot_honoroit_metrics_body.splitlines() | select | list }}" + matrix_bot_honoroit_rendered_env_lines: >- + {{ (matrix_bot_honoroit_env_file.content | b64decode).splitlines() + | select('match', '^[A-Z0-9_]+=') | list }} + matrix_bot_honoroit_rendered_env: >- + {{ dict(matrix_bot_honoroit_rendered_env_lines | map('split', '=', 1)) }} + matrix_bot_honoroit_rendered_labels_lines: >- + {{ (matrix_bot_honoroit_labels_file.content | b64decode).splitlines() + | select('match', '^[A-Za-z0-9_.-]+=') | list }} + matrix_bot_honoroit_rendered_labels: >- + {{ dict(matrix_bot_honoroit_rendered_labels_lines | map('split', '=', 1)) }} + matrix_bot_honoroit_runtime: "{{ (matrix_bot_honoroit_container_inspect.stdout | from_json) | first }}" + matrix_bot_honoroit_runtime_env: "{{ dict(matrix_bot_honoroit_runtime.Config.Env | map('split', '=', 1)) }}" + matrix_bot_honoroit_data_mounts: >- + {{ matrix_bot_honoroit_runtime.Mounts | selectattr('Destination', 'equalto', '/data') | list }} + gather_facts: false + 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_bot_honoroit_role_defaults + + - name: Wait for the matrix-bot-honoroit service to become active + ansible.builtin.systemd_service: + name: matrix-bot-honoroit.service + register: matrix_bot_honoroit_service + until: matrix_bot_honoroit_service.status.ActiveState == 'active' + retries: 30 + delay: 5 + failed_when: false + + - name: Assert the service is active and has not restarted + ansible.builtin.assert: + that: + - matrix_bot_honoroit_service.status.ActiveState == 'active' + - matrix_bot_honoroit_service.status.NRestarts is defined + - matrix_bot_honoroit_service.status.NRestarts | int == 0 + fail_msg: >- + matrix-bot-honoroit.service is + {{ matrix_bot_honoroit_service.status.ActiveState | default('unknown') }} after + {{ matrix_bot_honoroit_service.status.NRestarts | default('?') }} restart(s) + success_msg: "matrix-bot-honoroit.service is active and has not restarted" + + - name: Wait for Honoroit's health API + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bot_honoroit_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --show-error + - --write-out + - "\nHTTP_STATUS=%{http_code}" + - "http://matrix-bot-honoroit:{{ matrix_bot_honoroit_container_port }}/_health" + register: matrix_bot_honoroit_health + changed_when: false + until: "'HTTP_STATUS=200' in matrix_bot_honoroit_health.stdout" + retries: 24 + delay: 5 + failed_when: false + + - name: Assert the configured health API is live and structurally valid + ansible.builtin.assert: + that: + - matrix_bot_honoroit_health.rc == 0 + - matrix_bot_honoroit_health.stdout_lines[-1] == 'HTTP_STATUS=200' + - "matrix_bot_honoroit_health_body | from_json == {'status': 'ok'}" + fail_msg: >- + Honoroit's health API did not answer with its expected JSON + ({{ matrix_bot_honoroit_health.stdout | default('no output') }}) + success_msg: "Honoroit's live health API answers with its structured status" + + - name: Ask the metrics endpoint without credentials + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bot_honoroit_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --output + - /dev/null + - --write-out + - "HTTP_STATUS=%{http_code}" + - "http://matrix-bot-honoroit:{{ matrix_bot_honoroit_container_port }}/metrics" + register: matrix_bot_honoroit_unauthenticated_metrics + changed_when: false + failed_when: false + + - name: Assert the metrics endpoint requires authentication + ansible.builtin.assert: + that: + - matrix_bot_honoroit_unauthenticated_metrics.rc == 0 + - matrix_bot_honoroit_unauthenticated_metrics.stdout == 'HTTP_STATUS=401' + fail_msg: "Honoroit's metrics endpoint did not reject an unauthenticated request" + success_msg: "Honoroit's metrics endpoint rejects unauthenticated requests" + + - name: Ask the metrics endpoint with incorrect credentials + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bot_honoroit_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --user + - molecule-metrics-user:incorrect-molecule-password + - --output + - /dev/null + - --write-out + - "HTTP_STATUS=%{http_code}" + - "http://matrix-bot-honoroit:{{ matrix_bot_honoroit_container_port }}/metrics" + register: matrix_bot_honoroit_wrong_metrics_credentials + changed_when: false + failed_when: false + + - name: Assert the metrics endpoint rejects incorrect credentials + ansible.builtin.assert: + that: + - matrix_bot_honoroit_wrong_metrics_credentials.rc == 0 + - matrix_bot_honoroit_wrong_metrics_credentials.stdout == 'HTTP_STATUS=401' + fail_msg: "Honoroit's metrics endpoint accepted incorrect credentials" + success_msg: "Honoroit's metrics endpoint rejects incorrect credentials" + + - name: Ask the metrics endpoint with the configured credentials + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bot_honoroit_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --show-error + - --user + - molecule-metrics-user:molecule_metrics_password_c9ed32 + - --write-out + - "\nHTTP_STATUS=%{http_code}" + - "http://matrix-bot-honoroit:{{ matrix_bot_honoroit_container_port }}/metrics" + register: matrix_bot_honoroit_metrics + changed_when: false + failed_when: false + + - name: Assert configured credentials unlock genuine Prometheus metrics + ansible.builtin.assert: + that: + - matrix_bot_honoroit_metrics.rc == 0 + - matrix_bot_honoroit_metrics.stdout_lines[-1] == 'HTTP_STATUS=200' + - matrix_bot_honoroit_metrics_lines | select('match', '^honoroit_messages_operator [0-9]+$') | list | length == 1 + - matrix_bot_honoroit_metrics_lines | select('match', '^honoroit_messages_total [0-9]+$') | list | length == 1 + - matrix_bot_honoroit_metrics_lines | select('match', '^honoroit_request_done [0-9]+$') | list | length == 1 + - matrix_bot_honoroit_metrics_lines | select('match', '^honoroit_request_new [0-9]+$') | list | length == 1 + fail_msg: >- + Configured credentials did not unlock Honoroit's Prometheus metrics + ({{ matrix_bot_honoroit_metrics.stdout | default('no output') }}) + success_msg: "Configured credentials unlock Honoroit's Prometheus metrics" + + - name: Read the environment file the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bot_honoroit_config_path }}/env" + register: matrix_bot_honoroit_env_file + + - name: Assert the parsed environment contains the Matrix and Postgres wiring + ansible.builtin.assert: + that: + - matrix_bot_honoroit_rendered_env.HONOROIT_LOGIN == matrix_bot_honoroit_login + - matrix_bot_honoroit_rendered_env.HONOROIT_PASSWORD == matrix_bot_honoroit_password + - matrix_bot_honoroit_rendered_env.HONOROIT_HOMESERVER == matrix_bot_honoroit_homeserver + - matrix_bot_honoroit_rendered_env.HONOROIT_ROOMID == matrix_bot_honoroit_roomid + - matrix_bot_honoroit_rendered_env.HONOROIT_DB_DIALECT == 'postgres' + - matrix_bot_honoroit_rendered_env.HONOROIT_DB_DSN == 'postgres://molecule_honoroit_user:molecule_honoroit_password_8c45d1@matrix-postgres-molecule:5432/molecule_honoroit_database?sslmode=disable' + fail_msg: "The parsed environment lost the scenario's Matrix or Postgres wiring" + success_msg: "The parsed environment carries the scenario's Matrix and Postgres wiring" + + - name: Assert the parsed environment contains non-default bot behavior + ansible.builtin.assert: + that: + - matrix_bot_honoroit_rendered_env.HONOROIT_PREFIX == '!molecule-help' + - matrix_bot_honoroit_rendered_env.HONOROIT_NOENCRYPTIONWARNING | lower == 'true' + - matrix_bot_honoroit_rendered_env.HONOROIT_LOGLEVEL == 'DEBUG' + - matrix_bot_honoroit_rendered_env.HONOROIT_CACHESIZE == '317' + - matrix_bot_honoroit_rendered_env.HONOROIT_IGNORENOTHREAD | lower == 'true' + - matrix_bot_honoroit_rendered_env.HONOROIT_IGNOREDROOMS == '!molecule-ignored-one:molecule.local !molecule-ignored-two:molecule.local' + - matrix_bot_honoroit_rendered_env.HONOROIT_ALLOWEDUSERS == '@support-*:molecule.local @operator:molecule.local' + - matrix_bot_honoroit_rendered_env.HONOROIT_TEXT_PREFIX_OPEN == '[MOLECULE OPEN]' + - matrix_bot_honoroit_rendered_env.HONOROIT_TEXT_PREFIX_DONE == '[MOLECULE DONE]' + - matrix_bot_honoroit_rendered_env.HONOROIT_TEXT_GREETINGS == 'Molecule helpdesk greeting' + - matrix_bot_honoroit_rendered_env.HONOROIT_TEXT_DONE == 'Molecule helpdesk completion' + fail_msg: "The parsed environment lost non-default Honoroit behavior" + success_msg: "The parsed environment carries non-default Honoroit behavior" + + - name: Assert the parsed environment contains metrics auth and extension values + ansible.builtin.assert: + that: + - matrix_bot_honoroit_rendered_env.HONOROIT_AUTH_METRICS_LOGIN == 'molecule-metrics-user' + - matrix_bot_honoroit_rendered_env.HONOROIT_AUTH_METRICS_PASSWORD == 'molecule_metrics_password_c9ed32' + - matrix_bot_honoroit_rendered_env.HONOROIT_PORT == ':' ~ (matrix_bot_honoroit_container_port | string) + - matrix_bot_honoroit_rendered_env.MOLECULE_HONOROIT_MARKER == 'role-rendered-environment' + fail_msg: "The parsed environment lost the metrics auth or extension values" + success_msg: "The parsed environment carries metrics auth and extension values" + + - name: List the tables Honoroit created in Postgres + ansible.builtin.command: + argv: + - docker + - exec + - matrix-postgres-molecule + - psql + - --username={{ matrix_bot_honoroit_database_username }} + - --dbname={{ matrix_bot_honoroit_database_name }} + - --tuples-only + - --no-align + - --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename + register: matrix_bot_honoroit_tables + changed_when: false + + - name: Assert Honoroit migrated a substantial schema into the configured Postgres database + ansible.builtin.assert: + that: + - matrix_bot_honoroit_tables.rc == 0 + - "'crypto_account' in matrix_bot_honoroit_table_names" + - "'crypto_device' in matrix_bot_honoroit_table_names" + - "'crypto_megolm_inbound_session' in matrix_bot_honoroit_table_names" + - "'crypto_olm_session' in matrix_bot_honoroit_table_names" + - "'crypto_secrets' in matrix_bot_honoroit_table_names" + - "'crypto_version' in matrix_bot_honoroit_table_names" + - "'mx_registrations' in matrix_bot_honoroit_table_names" + - "'mx_room_state' in matrix_bot_honoroit_table_names" + - "'mx_user_profile' in matrix_bot_honoroit_table_names" + - "'mx_version' in matrix_bot_honoroit_table_names" + - matrix_bot_honoroit_table_names | length == 17 + fail_msg: >- + Honoroit did not create its crypto schema in {{ matrix_bot_honoroit_database_name }} + (found {{ matrix_bot_honoroit_table_names | length }} table(s): + {{ matrix_bot_honoroit_table_names | join(', ') }}) + success_msg: "Honoroit migrated its crypto schema into the configured Postgres database" + vars: + matrix_bot_honoroit_table_names: "{{ matrix_bot_honoroit_tables.stdout_lines | select | list }}" + + - name: Read Honoroit's database migration markers + ansible.builtin.command: + argv: + - docker + - exec + - matrix-postgres-molecule + - psql + - --username={{ matrix_bot_honoroit_database_username }} + - --dbname={{ matrix_bot_honoroit_database_name }} + - --tuples-only + - --no-align + - "--command=SELECT 'crypto:' || version || ':' || compat FROM crypto_version UNION ALL SELECT 'state:' || version || ':' || compat FROM mx_version ORDER BY 1" + register: matrix_bot_honoroit_migration_versions + changed_when: false + + - name: Assert Honoroit's Postgres migrations reached the pinned schema versions + ansible.builtin.assert: + that: + - matrix_bot_honoroit_migration_versions.rc == 0 + - matrix_bot_honoroit_migration_versions.stdout_lines == ['crypto:19:15', 'state:10:3'] + fail_msg: >- + Honoroit's database migration markers are missing or unexpected + ({{ matrix_bot_honoroit_migration_versions.stdout | default('no output') | trim }}) + success_msg: "Honoroit's Postgres migrations reached the pinned schema versions" + + - name: Read Honoroit's full systemd journal + ansible.builtin.command: + argv: + - journalctl + - --unit=matrix-bot-honoroit.service + - --no-pager + register: matrix_bot_honoroit_journal + changed_when: false + + - name: Assert the journal records successful initialization and configured debug logging + ansible.builtin.assert: + that: + - "'Honoroit' in matrix_bot_honoroit_journal_plain" + - "'starting bot...' in matrix_bot_honoroit_journal_plain" + - "'client has been started' in matrix_bot_honoroit_journal_plain" + - "'cannot initialize the bot' not in matrix_bot_honoroit_journal_plain" + - "'matrix bot crashed' not in matrix_bot_honoroit_journal_plain" + fail_msg: "Honoroit's journal does not show a clean debug-level startup" + success_msg: "Honoroit's journal records a clean debug-level startup" + vars: + matrix_bot_honoroit_journal_plain: >- + {{ matrix_bot_honoroit_journal.stdout + | regex_replace('\x1B\[[0-?]*[ -/]*[@-~]', '') }} + + - name: Inspect the running Honoroit container + ansible.builtin.command: + argv: + - docker + - container + - inspect + - matrix-bot-honoroit + register: matrix_bot_honoroit_container_inspect + changed_when: false + + - name: Assert the running container uses the exact image pinned by the role + ansible.builtin.assert: + that: + - matrix_bot_honoroit_runtime.Config.Image == matrix_bot_honoroit_expected_image + fail_msg: >- + The running image is {{ matrix_bot_honoroit_runtime.Config.Image }}, expected + {{ matrix_bot_honoroit_expected_image }} from defaults/main.yml + success_msg: "The running container uses the exact image pinned by the role" + vars: + matrix_bot_honoroit_expected_image: >- + {{ matrix_bot_honoroit_role_defaults.matrix_bot_honoroit_container_image_registry_prefix_upstream_default }}etkecc/honoroit:{{ matrix_bot_honoroit_role_defaults.matrix_bot_honoroit_version }} + + - name: Assert the running container preserves the image and systemd lifecycle contract + ansible.builtin.assert: + that: + - matrix_bot_honoroit_runtime.Config.Entrypoint == ['/bin/honoroit'] + - matrix_bot_honoroit_runtime.Config.Cmd is none + - matrix_bot_honoroit_runtime.Config.WorkingDir == '/' + - matrix_bot_honoroit_runtime.HostConfig.RestartPolicy.Name == 'no' + - matrix_bot_honoroit_runtime.HostConfig.AutoRemove is sameas true + - matrix_bot_honoroit_runtime.HostConfig.LogConfig.Type == 'none' + fail_msg: "The running container has an unexpected command or Docker lifecycle contract" + success_msg: "The running container preserves the image command and systemd lifecycle contract" + + - name: Assert the running container uses the configured identity and hardening + ansible.builtin.assert: + that: + - matrix_bot_honoroit_runtime.Config.User == '1234:1234' + - matrix_bot_honoroit_runtime.HostConfig.ReadonlyRootfs is sameas true + - matrix_bot_honoroit_runtime.HostConfig.CapDrop == ['ALL'] + - matrix_bot_honoroit_runtime.HostConfig.PidsLimit == 73 + - matrix_bot_honoroit_runtime.HostConfig.Tmpfs['/tmp'] == 'rw,noexec,nosuid,size=16m' + fail_msg: "The running container lost its configured identity or hardening" + success_msg: "The running container uses the configured identity and hardening" + + - name: Assert the running container has only private network attachments + ansible.builtin.assert: + that: + - matrix_bot_honoroit_runtime.HostConfig.NetworkMode == matrix_bot_honoroit_container_network + - matrix_bot_honoroit_runtime.NetworkSettings.Networks.keys() | sort == ([matrix_bot_honoroit_container_network] + matrix_bot_honoroit_container_additional_networks) | sort + - matrix_bot_honoroit_runtime.HostConfig.PortBindings | length == 0 + fail_msg: "The running container lost a private network or published a host port" + success_msg: "The running container has both private networks and no published host ports" + + - name: Assert the writable data mount is scoped to Honoroit's data directory + ansible.builtin.assert: + that: + - matrix_bot_honoroit_runtime.Mounts | length == 1 + - matrix_bot_honoroit_data_mounts | length == 1 + - matrix_bot_honoroit_data_mounts[0].Source == matrix_bot_honoroit_data_path + - matrix_bot_honoroit_data_mounts[0].RW is sameas true + fail_msg: "The running container's /data mount does not match the role-managed data path" + success_msg: "The running container has the role-managed writable /data mount" + + - name: Assert the running process received the role-rendered environment + ansible.builtin.assert: + that: + - matrix_bot_honoroit_runtime_env.HONOROIT_DB_DSN == matrix_bot_honoroit_rendered_env.HONOROIT_DB_DSN + - matrix_bot_honoroit_runtime_env.HONOROIT_HOMESERVER == matrix_bot_honoroit_rendered_env.HONOROIT_HOMESERVER + - matrix_bot_honoroit_runtime_env.HONOROIT_AUTH_METRICS_LOGIN == matrix_bot_honoroit_rendered_env.HONOROIT_AUTH_METRICS_LOGIN + - matrix_bot_honoroit_runtime_env.HONOROIT_PORT == ':' ~ (matrix_bot_honoroit_container_port | string) + - matrix_bot_honoroit_runtime_env.MOLECULE_HONOROIT_MARKER == 'role-rendered-environment' + fail_msg: "The live container environment differs from the file the role rendered" + success_msg: "The live container received the role-rendered environment" + + - name: Read the labels file the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bot_honoroit_config_path }}/labels" + register: matrix_bot_honoroit_labels_file + + - name: Assert the parsed labels contain the configured metrics router + ansible.builtin.assert: + that: + - matrix_bot_honoroit_rendered_labels['traefik.enable'] == 'true' + - matrix_bot_honoroit_rendered_labels['traefik.docker.network'] == matrix_bot_honoroit_container_network + - matrix_bot_honoroit_rendered_labels['traefik.http.services.matrix-bot-honoroit-metrics.loadbalancer.server.port'] == (matrix_bot_honoroit_container_port | string) + - matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.rule'] == 'Host(`honoroit.molecule.local`) && Path(`/molecule-honoroit/molecule-metrics`)' + - matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.priority'] == '710' + - matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.service'] == 'matrix-bot-honoroit-metrics' + - matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.entrypoints'] == 'molecule-web' + - matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.tls'] == 'false' + - matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.tls.certResolver'] is not defined + fail_msg: "The parsed labels lost the scenario's metrics router contract" + success_msg: "The parsed labels contain the configured metrics router" + + - name: Assert the parsed labels contain the configured middleware chain + ansible.builtin.assert: + that: + - matrix_bot_honoroit_rendered_labels['traefik.http.middlewares.matrix-bot-honoroit-slashless-redirect.redirectregex.regex'] == '(/molecule-honoroit)$' + - matrix_bot_honoroit_rendered_labels['traefik.http.middlewares.matrix-bot-honoroit-slashless-redirect.redirectregex.replacement'] == '${1}/' + - matrix_bot_honoroit_rendered_labels['traefik.http.middlewares.matrix-bot-honoroit-strip-prefix.stripprefix.prefixes'] == '/molecule-honoroit' + - matrix_bot_honoroit_rendered_labels['traefik.http.middlewares.matrix-bot-honoroit-add-headers.headers.customresponseheaders.X-Molecule-Honoroit'] == 'scenario' + - matrix_bot_honoroit_rendered_labels['traefik.http.middlewares.matrix-bot-honoroit-add-headers.headers.customresponseheaders.X-Robots-Tag'] == 'noindex' + - matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.middlewares'] == 'matrix-bot-honoroit-slashless-redirect,matrix-bot-honoroit-strip-prefix,matrix-bot-honoroit-add-headers' + fail_msg: "The parsed labels lost the scenario's path and response-header middlewares" + success_msg: "The parsed labels contain the configured middleware chain" + + - name: Assert the live container received the rendered router and middleware labels + ansible.builtin.assert: + that: + - matrix_bot_honoroit_runtime.Config.Labels['traefik.http.services.matrix-bot-honoroit-metrics.loadbalancer.server.port'] == (matrix_bot_honoroit_container_port | string) + - matrix_bot_honoroit_runtime.Config.Labels['traefik.http.routers.matrix-bot-honoroit-metrics.rule'] == matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.rule'] + - matrix_bot_honoroit_runtime.Config.Labels['traefik.http.routers.matrix-bot-honoroit-metrics.priority'] == matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.priority'] + - matrix_bot_honoroit_runtime.Config.Labels['traefik.http.routers.matrix-bot-honoroit-metrics.middlewares'] == matrix_bot_honoroit_rendered_labels['traefik.http.routers.matrix-bot-honoroit-metrics.middlewares'] + - matrix_bot_honoroit_runtime.Config.Labels['traefik.http.middlewares.matrix-bot-honoroit-add-headers.headers.customresponseheaders.X-Molecule-Honoroit'] == 'scenario' + - matrix_bot_honoroit_runtime.Config.Labels['molecule.honoroit.scenario'] == 'live-postgres-and-metrics' + fail_msg: "The live container labels differ from the rendered routing contract" + success_msg: "The live container received the rendered router and middleware labels" + + - name: Inspect role-rendered support file ownership and permissions + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "{{ matrix_bot_honoroit_config_path }}/env" + - "{{ matrix_bot_honoroit_config_path }}/labels" + register: matrix_bot_honoroit_support_files + + - name: Assert support files are owned by Matrix and not world-readable + ansible.builtin.assert: + that: + - matrix_bot_honoroit_support_files.results | map(attribute='stat.uid') | unique | list == [1234] + - matrix_bot_honoroit_support_files.results | map(attribute='stat.gid') | unique | list == [1234] + - matrix_bot_honoroit_support_files.results | map(attribute='stat.mode') | unique | list == ['0640'] + fail_msg: "Honoroit's support files have unexpected ownership or permissions" + success_msg: "Honoroit's support files are Matrix-owned and mode 0640"