diff --git a/.github/renovate.json b/.github/renovate.json index d5dc8b350..57b4f923f 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -85,6 +85,7 @@ "matchFileNames": [ "roles/custom/matrix-alertmanager-receiver/defaults/main.yml", "roles/custom/matrix-bot-baibot/defaults/main.yml", + "roles/custom/matrix-bot-maubot/defaults/main.yml", "roles/custom/matrix-bot-matrix-reminder-bot/defaults/main.yml", "roles/custom/matrix-bridge-heisenbridge/defaults/main.yml", "roles/custom/matrix-bridge-hookshot/defaults/main.yml", diff --git a/roles/custom/matrix-bot-maubot/molecule/default/converge.yml b/roles/custom/matrix-bot-maubot/molecule/default/converge.yml new file mode 100644 index 000000000..2d82b926c --- /dev/null +++ b/roles/custom/matrix-bot-maubot/molecule/default/converge.yml @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Include roles for matrix-bot-maubot 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-maubot 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 + +# Component roles install their unit; the complete playbook's systemd service manager starts +# it. Keep that boundary explicit rather than pulling the manager into this role test. +- name: Ensure matrix-bot-maubot 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-maubot systemd service is started + ansible.builtin.systemd_service: + name: matrix-bot-maubot.service + state: started diff --git a/roles/custom/matrix-bot-maubot/molecule/default/molecule.yml b/roles/custom/matrix-bot-maubot/molecule/default/molecule.yml new file mode 100644 index 000000000..56d654924 --- /dev/null +++ b/roles/custom/matrix-bot-maubot/molecule/default/molecule.yml @@ -0,0 +1,100 @@ +# 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-maubot-${MOLECULE_DISTRO:-ubuntu2604}-default + image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2604}-ansible:latest" + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + pre_build_image: true +provisioner: + name: ansible + config_options: + defaults: + callback_result_format: yaml + inventory: + group_vars: + all: + matrix_bot_maubot_container_network: matrix-bot-maubot-molecule + + # verify.yml runs as a separate play, where role defaults are out of scope. Pin + # paths it reads to the values the role derives during converge. + matrix_bot_maubot_base_path: /matrix/maubot + matrix_bot_maubot_config_path: /matrix/maubot/config + matrix_bot_maubot_data_path: /matrix/maubot/data + + # Postgres is the normal playbook path. The non-default database and user mean + # successful migrations prove that the role assembled and Maubot consumed this URI. + matrix_bot_maubot_database_engine: postgres + matrix_bot_maubot_database_hostname: matrix-postgres-molecule + matrix_bot_maubot_database_name: molecule_maubot + matrix_bot_maubot_database_username: molecule_maubot + matrix_bot_maubot_database_password: molecule_pg_password_27c541 + matrix_bot_maubot_database_sslmode: disable + + # Both the port and public path differ from the role and Maubot defaults. The scenario + # proves the internal endpoints and the public-prefix/StripPrefix mapping separately. + matrix_bot_maubot_server_port: 29427 + matrix_bot_maubot_scheme: https + matrix_bot_maubot_hostname: maubot.molecule.local + matrix_bot_maubot_path_prefix: /molecule-maubot + matrix_bot_maubot_unshared_secret: molecule_unshared_secret_b9317a + + # The homeserver is only recorded as a convenient management-interface preset. + # Maubot has no clients or instances yet, so it does not contact this deliberately + # nonexistent service during startup and no external Matrix credentials are needed. + matrix_bot_maubot_homeserver_name: molecule-homeserver + matrix_bot_maubot_homeserver_url: https://matrix-no-such-service.invalid + matrix_bot_maubot_homeserver_secret: molecule_registration_secret_98e127 + matrix_bot_maubot_initial_password: molecule_matrix_password_unused_62aa11 + + # This account belongs only to Maubot's management API. Logging in below proves the + # running process loaded it, rather than merely proving that a YAML file exists. + matrix_bot_maubot_admins: + molecule-admin: molecule_admin_password_61bd93 + matrix_bot_maubot_logging_level: DEBUG + + # No Traefik is deployed. Non-default values still let the scenario verify the role's + # public-routing contract in both the label file and Docker's accepted labels. + matrix_bot_maubot_container_labels_traefik_enabled: true + matrix_bot_maubot_container_labels_traefik_docker_network: matrix-bot-maubot-molecule + matrix_bot_maubot_container_labels_management_enabled: true + matrix_bot_maubot_container_labels_management_hostname: maubot.molecule.local + matrix_bot_maubot_container_labels_management_traefik_priority: 719 + matrix_bot_maubot_container_labels_management_traefik_entrypoints: web + matrix_bot_maubot_container_labels_management_traefik_tls: false + matrix_bot_maubot_container_labels_additional_labels: | + molecule.maubot.coverage=enabled + + # Keep the production-like no-host-port path: probes join the container network. + matrix_bot_maubot_container_management_interface_http_bind_port: '' + 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-maubot/molecule/default/prepare.yml b/roles/custom/matrix-bot-maubot/molecule/default/prepare.yml new file mode 100644 index 000000000..8058a5364 --- /dev/null +++ b/roles/custom/matrix-bot-maubot/molecule/default/prepare.yml @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Prepare matrix-bot-maubot 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 + + # matrix-base creates this identity in the complete playbook. The component role's file + # tasks resolve owner/group by name, so a role-scoped scenario supplies that side effect. + - 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" + + # The role also creates this during converge, but Postgres must join it first. + - name: Ensure the container network the role attaches to exists + ansible.builtin.command: + argv: + - docker + - network + - create + - "{{ matrix_bot_maubot_container_network }}" + register: matrix_bot_maubot_molecule_network + changed_when: matrix_bot_maubot_molecule_network.rc == 0 + failed_when: + - matrix_bot_maubot_molecule_network.rc != 0 + - "'already exists' not in matrix_bot_maubot_molecule_network.stderr" + + - name: Ensure Postgres is running + ansible.builtin.include_tasks: + file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/postgres.yml" + vars: + molecule_shared_postgres_network: "{{ matrix_bot_maubot_container_network }}" + molecule_shared_postgres_database: "{{ matrix_bot_maubot_database_name }}" + molecule_shared_postgres_username: "{{ matrix_bot_maubot_database_username }}" + molecule_shared_postgres_password: "{{ matrix_bot_maubot_database_password }}" diff --git a/roles/custom/matrix-bot-maubot/molecule/default/requirements.yml b/roles/custom/matrix-bot-maubot/molecule/default/requirements.yml new file mode 120000 index 000000000..1567a5392 --- /dev/null +++ b/roles/custom/matrix-bot-maubot/molecule/default/requirements.yml @@ -0,0 +1 @@ +../../../../../molecule-shared/requirements.yml \ No newline at end of file diff --git a/roles/custom/matrix-bot-maubot/molecule/default/verify.yml b/roles/custom/matrix-bot-maubot/molecule/default/verify.yml new file mode 100644 index 000000000..a5e88bf7a --- /dev/null +++ b/roles/custom/matrix-bot-maubot/molecule/default/verify.yml @@ -0,0 +1,406 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +# Maubot starts without a Matrix client configured. Its real management API and Postgres +# migrations are the weight-bearing evidence, so this needs no homeserver or credentials. +- name: Verify matrix-bot-maubot + 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_maubot_config: "{{ matrix_bot_maubot_config_file.content | b64decode | from_yaml }}" + matrix_bot_maubot_ui_body: "{{ matrix_bot_maubot_ui_http.stdout_lines[:-1] | join('\n') }}" + matrix_bot_maubot_paths_body: "{{ matrix_bot_maubot_paths_http.stdout_lines[:-1] | join('\n') }}" + matrix_bot_maubot_paths: "{{ matrix_bot_maubot_paths_body | from_json }}" + matrix_bot_maubot_login_body: "{{ matrix_bot_maubot_login_http.stdout_lines[:-1] | join('\n') }}" + matrix_bot_maubot_login_response: "{{ matrix_bot_maubot_login_body | from_json }}" + matrix_bot_maubot_expected_database_uri: >- + postgres://{{ matrix_bot_maubot_database_username }}:{{ matrix_bot_maubot_database_password }}@{{ matrix_bot_maubot_database_hostname }}:5432/{{ matrix_bot_maubot_database_name }}?sslmode={{ matrix_bot_maubot_database_sslmode }} + matrix_bot_maubot_labels_lines: "{{ (matrix_bot_maubot_labels_file.content | b64decode).splitlines() }}" + matrix_bot_maubot_runtime: "{{ (matrix_bot_maubot_container_inspect.stdout | from_json) | first }}" + matrix_bot_maubot_config_mounts: "{{ matrix_bot_maubot_runtime.Mounts | selectattr('Destination', 'equalto', '/config') | list }}" + matrix_bot_maubot_data_mounts: "{{ matrix_bot_maubot_runtime.Mounts | selectattr('Destination', 'equalto', '/data') | list }}" + gather_facts: false + tasks: + # Read the version from the role itself, so a role pin bump changes what is expected. + - 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_maubot_role_defaults + + - name: Wait for the matrix-bot-maubot service to become active + ansible.builtin.systemd_service: + name: matrix-bot-maubot.service + register: matrix_bot_maubot_service + until: matrix_bot_maubot_service.status.ActiveState == 'active' + retries: 30 + delay: 5 + failed_when: false + + # Restart=always can hide a crash loop behind ActiveState=active. + - name: Assert the service is active and has not restarted + ansible.builtin.assert: + that: + - matrix_bot_maubot_service.status.ActiveState == 'active' + - matrix_bot_maubot_service.status.NRestarts is defined + - matrix_bot_maubot_service.status.NRestarts | int == 0 + fail_msg: >- + matrix-bot-maubot.service is + {{ matrix_bot_maubot_service.status.ActiveState | default('unknown') }} after + {{ matrix_bot_maubot_service.status.NRestarts | default('?') }} restart(s) + success_msg: "matrix-bot-maubot.service is active and has not restarted" + + - name: Read the configuration file the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bot_maubot_config_path }}/config.yaml" + register: matrix_bot_maubot_config_file + + - name: Assert the parsed server configuration carries the scenario's values + ansible.builtin.assert: + that: + - matrix_bot_maubot_config.server.hostname == '0.0.0.0' + - matrix_bot_maubot_config.server.port == matrix_bot_maubot_server_port + - matrix_bot_maubot_config.server.public_url == 'https://maubot.molecule.local/molecule-maubot' + - matrix_bot_maubot_config.server.ui_base_path == '/' + - matrix_bot_maubot_config.server.plugin_base_path == '/_matrix/maubot/plugin/' + - "'base_path' not in matrix_bot_maubot_config.server" + - "'appservice_base_path' not in matrix_bot_maubot_config.server" + - matrix_bot_maubot_config.server.unshared_secret == matrix_bot_maubot_unshared_secret + fail_msg: "The parsed server block does not carry the scenario's non-default values" + success_msg: "The parsed server block carries the scenario's non-default values" + + - name: Assert the parsed database configuration carries the scenario's Postgres URI + ansible.builtin.assert: + that: + - matrix_bot_maubot_config.database == matrix_bot_maubot_expected_database_uri + - matrix_bot_maubot_config.database.startswith('postgres://') + - matrix_bot_maubot_database_username in matrix_bot_maubot_config.database + - matrix_bot_maubot_database_password in matrix_bot_maubot_config.database + - matrix_bot_maubot_database_hostname in matrix_bot_maubot_config.database + - matrix_bot_maubot_database_name in matrix_bot_maubot_config.database + fail_msg: "The parsed configuration does not point at the scenario's Postgres database" + success_msg: "The parsed configuration points at the scenario's Postgres database" + + - name: Assert the parsed homeserver preset carries the scenario's values + ansible.builtin.assert: + that: + - matrix_bot_maubot_homeserver_name in matrix_bot_maubot_config.homeservers + - matrix_bot_maubot_config.homeservers[matrix_bot_maubot_homeserver_name].url == matrix_bot_maubot_homeserver_url + - matrix_bot_maubot_config.homeservers[matrix_bot_maubot_homeserver_name].secret == matrix_bot_maubot_homeserver_secret + fail_msg: "The parsed homeserver preset does not carry the scenario's values" + success_msg: "The parsed homeserver preset carries the scenario's values" + + - name: Assert the parsed management configuration carries scenario values + ansible.builtin.assert: + that: + - matrix_bot_maubot_config.admins['molecule-admin'] == 'molecule_admin_password_61bd93' + - matrix_bot_maubot_config.admins.root == '' + - matrix_bot_maubot_config.api_features.values() | select('equalto', true) | list | length == matrix_bot_maubot_config.api_features | length + fail_msg: "The parsed management configuration lost the admin or API features" + success_msg: "The parsed management configuration carries the admin and API features" + + - name: Assert the parsed logging configuration carries the scenario's level + ansible.builtin.assert: + that: + - matrix_bot_maubot_config.logging.loggers.maubot.level == matrix_bot_maubot_logging_level + - matrix_bot_maubot_config.logging.loggers.mau.level == matrix_bot_maubot_logging_level + - matrix_bot_maubot_config.logging.loggers.aiohttp.level == matrix_bot_maubot_logging_level + - matrix_bot_maubot_config.logging.root.level == matrix_bot_maubot_logging_level + fail_msg: "The parsed logging configuration does not carry the scenario's level" + success_msg: "The parsed logging configuration carries the scenario's level" + + # Traefik strips the configured public prefix before proxying, so the role deliberately + # configures the process UI at /. Probe that exact internal contract over the network. + - name: Wait for Maubot's management UI on the configured internal port and path + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bot_maubot_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --show-error + - --write-out + - "\nHTTP_STATUS=%{http_code}" + - "http://matrix-bot-maubot:{{ matrix_bot_maubot_server_port }}/" + register: matrix_bot_maubot_ui_http + changed_when: false + until: "'HTTP_STATUS=200' in matrix_bot_maubot_ui_http.stdout" + retries: 24 + delay: 5 + failed_when: false + + - name: Assert Maubot serves its real management UI at the configured internal address + ansible.builtin.assert: + that: + - matrix_bot_maubot_ui_http.rc == 0 + - matrix_bot_maubot_ui_http.stdout_lines[-1] == 'HTTP_STATUS=200' + - matrix_bot_maubot_ui_body | trim | length > 100 + - "'- + Maubot did not derive the prefixed browser API path from server.public_url + ({{ matrix_bot_maubot_paths_http.stdout | default('no output') }}) + success_msg: "Maubot advertises its browser API under the configured public prefix" + + # This authenticates against the live management API with the non-default admin from + # the rendered YAML. It proves both the API base path and account reached the process. + - name: Log in to Maubot's management API + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bot_maubot_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --show-error + - --header + - 'Content-Type: application/json' + - --data + - '{"username":"molecule-admin","password":"molecule_admin_password_61bd93"}' + - --write-out + - "\nHTTP_STATUS=%{http_code}" + - "http://matrix-bot-maubot:{{ matrix_bot_maubot_server_port }}/_matrix/maubot/v1/auth/login" + register: matrix_bot_maubot_login_http + changed_when: false + failed_when: false + + - name: Assert the management API authenticated the configured admin + ansible.builtin.assert: + that: + - matrix_bot_maubot_login_http.rc == 0 + - matrix_bot_maubot_login_http.stdout_lines[-1] == 'HTTP_STATUS=200' + - matrix_bot_maubot_login_response is mapping + - matrix_bot_maubot_login_response.token is string + - matrix_bot_maubot_login_response.token | length > 20 + fail_msg: >- + Maubot's live management API rejected the admin the role configured + ({{ matrix_bot_maubot_login_http.stdout | default('no output') }}) + success_msg: "Maubot's live management API authenticated the configured admin" + + # The role's default port must not answer. Otherwise reaching the custom port would not + # prove that the configured value made it into the running process. + - name: Probe the role's default Maubot port + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bot_maubot_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --show-error + - --max-time + - '3' + - --output + - /dev/null + - --write-out + - "HTTP_STATUS=%{http_code}" + - http://matrix-bot-maubot:29316/ + register: matrix_bot_maubot_default_port_http + changed_when: false + failed_when: false + + - name: Assert Maubot is not listening on the role's default port + ansible.builtin.assert: + that: + - matrix_bot_maubot_default_port_http.rc != 0 + - "'HTTP_STATUS=200' not in matrix_bot_maubot_default_port_http.stdout" + fail_msg: "Maubot still answers on 29316, so the configured port is not proven" + success_msg: "Maubot does not listen on the role's default port" + + # Tables can only appear after hostname resolution, authentication and migrations. + - name: List the tables Maubot created in Postgres + ansible.builtin.command: + argv: + - docker + - exec + - matrix-postgres-molecule + - psql + - --username={{ matrix_bot_maubot_database_username }} + - --dbname={{ matrix_bot_maubot_database_name }} + - --tuples-only + - --no-align + - --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public' + register: matrix_bot_maubot_tables + changed_when: false + + - name: Assert Maubot migrated its schema into the configured Postgres database + ansible.builtin.assert: + that: + - matrix_bot_maubot_tables.rc == 0 + - "'version' in matrix_bot_maubot_table_names" + - "'client' in matrix_bot_maubot_table_names" + - "'instance' in matrix_bot_maubot_table_names" + - "'database_owner' in matrix_bot_maubot_table_names" + - matrix_bot_maubot_table_names | length > 5 + fail_msg: >- + Maubot did not create its schema in {{ matrix_bot_maubot_database_name }} + (found {{ matrix_bot_maubot_table_names | length }} table(s)) + success_msg: "Maubot migrated its schema into the configured Postgres database" + vars: + matrix_bot_maubot_table_names: "{{ matrix_bot_maubot_tables.stdout_lines | select | list }}" + + - name: Read the labels the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bot_maubot_base_path }}/labels" + register: matrix_bot_maubot_labels_file + + - name: Assert the labels carry the configured management exposure + ansible.builtin.assert: + that: + - "'traefik.enable=true' in matrix_bot_maubot_labels_lines" + - "'traefik.docker.network=matrix-bot-maubot-molecule' in matrix_bot_maubot_labels_lines" + - "'traefik.http.services.matrix-bot-maubot.loadbalancer.server.port=29427' in matrix_bot_maubot_labels_lines" + - "'traefik.http.routers.matrix-bot-maubot-management.rule=Host(`maubot.molecule.local`) && PathPrefix(`/molecule-maubot`)' in matrix_bot_maubot_labels_lines" + - "'traefik.http.routers.matrix-bot-maubot-management.priority=719' in matrix_bot_maubot_labels_lines" + - "'traefik.http.middlewares.matrix-bot-maubot-management-strip-prefix.stripprefix.prefixes=/molecule-maubot' in matrix_bot_maubot_labels_lines" + - "'traefik.http.routers.matrix-bot-maubot-management.middlewares=matrix-bot-maubot-management-strip-prefix' in matrix_bot_maubot_labels_lines" + - "'traefik.http.routers.matrix-bot-maubot-management.entrypoints=web' in matrix_bot_maubot_labels_lines" + - "'traefik.http.routers.matrix-bot-maubot-management.tls=false' in matrix_bot_maubot_labels_lines" + - "'molecule.maubot.coverage=enabled' in matrix_bot_maubot_labels_lines" + fail_msg: "The role's labels do not carry the scenario's public management wiring" + success_msg: "The role's labels carry the scenario's public management wiring" + + - name: Inspect the running Maubot container + ansible.builtin.command: + argv: + - docker + - container + - inspect + - matrix-bot-maubot + register: matrix_bot_maubot_container_inspect + changed_when: false + + - name: Assert the running container uses the exact image pinned by the role + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.Config.Image == 'dock.mau.dev/maubot/maubot:' + matrix_bot_maubot_role_defaults.matrix_bot_maubot_version + fail_msg: >- + The running image {{ matrix_bot_maubot_runtime.Config.Image }} is not the exact + dock.mau.dev/maubot/maubot:{{ matrix_bot_maubot_role_defaults.matrix_bot_maubot_version }} pin + success_msg: "The running container uses the exact image pinned by the role" + + - name: Assert the running container uses the playbook's matrix identity + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.Config.User.split(':')[0] == matrix_user_uid | string + - matrix_bot_maubot_runtime.Config.User.split(':')[1] == matrix_user_gid | string + fail_msg: >- + Maubot runs as {{ matrix_bot_maubot_runtime.Config.User }} instead of + {{ matrix_user_uid }}:{{ matrix_user_gid }} + success_msg: "The running container uses the playbook's matrix UID and GID" + + - name: Assert the container root filesystem is read-only + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.HostConfig.ReadonlyRootfs is sameas true + fail_msg: "The Maubot container root filesystem is writable" + success_msg: "The Maubot container root filesystem is read-only" + + - name: Assert all Linux capabilities are dropped + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.HostConfig.CapDrop == ['ALL'] + fail_msg: "The Maubot container does not drop all Linux capabilities" + success_msg: "The Maubot container drops all Linux capabilities" + + - name: Assert the container has the role's writable temporary filesystem + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.HostConfig.Tmpfs['/tmp'] == 'rw,noexec,nosuid,size=1024m' + fail_msg: "The Maubot container is missing its constrained /tmp tmpfs" + success_msg: "The Maubot container has its constrained writable /tmp tmpfs" + + - name: Assert the configuration bind mount is read-only + ansible.builtin.assert: + that: + - matrix_bot_maubot_config_mounts | length == 1 + - matrix_bot_maubot_config_mounts[0].RW is sameas false + fail_msg: "The Maubot configuration bind mount is missing or writable" + success_msg: "The Maubot configuration bind mount is present and read-only" + + - name: Assert the data bind mount is writable + ansible.builtin.assert: + that: + - matrix_bot_maubot_data_mounts | length == 1 + - matrix_bot_maubot_data_mounts[0].RW is sameas true + fail_msg: "The Maubot data bind mount is missing or read-only" + success_msg: "The Maubot data bind mount is present and writable" + + - name: Assert the container is attached only to its dedicated network + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.NetworkSettings.Networks is mapping + - matrix_bot_maubot_runtime.NetworkSettings.Networks | length == 1 + - matrix_bot_maubot_container_network in matrix_bot_maubot_runtime.NetworkSettings.Networks + fail_msg: >- + Maubot has unexpected networks: + {{ matrix_bot_maubot_runtime.NetworkSettings.Networks.keys() | list }} + success_msg: "The Maubot container is attached only to its dedicated network" + + - name: Ask Docker for Maubot's published ports + ansible.builtin.command: + argv: + - docker + - container + - port + - matrix-bot-maubot + register: matrix_bot_maubot_published_ports + changed_when: false + failed_when: false + + - name: Assert the role did not publish a host port + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.HostConfig.PortBindings | default({}, true) | length == 0 + - matrix_bot_maubot_published_ports.rc == 0 + - matrix_bot_maubot_published_ports.stdout | trim | length == 0 + fail_msg: >- + Maubot unexpectedly publishes a host port: + {{ matrix_bot_maubot_published_ports.stdout | default('unknown') }} + success_msg: "The role leaves Maubot's management port unpublished" + + - name: Assert Docker accepted the role's custom label + ansible.builtin.assert: + that: + - matrix_bot_maubot_runtime.Config.Labels is mapping + - matrix_bot_maubot_runtime.Config.Labels['molecule.maubot.coverage'] == 'enabled' + fail_msg: "Docker did not attach the custom label from the role's label file" + success_msg: "Docker accepted the custom label from the role's label file"