diff --git a/molecule-shared/playbook-context.yml b/molecule-shared/playbook-context.yml index 4e56480e6..18f4d874c 100644 --- a/molecule-shared/playbook-context.yml +++ b/molecule-shared/playbook-context.yml @@ -38,6 +38,16 @@ matrix_user_gid: 1234 # scenario wants. matrix_admin: '' +# --- Host commands (matrix-base) ------------------------------------------- +# +# Some roles shell out to a host binary through this indirection instead of +# naming it directly (matrix-bridge-hookshot and matrix-bridge-appservice-irc +# both generate a key with it). matrix-base's defaults are what supplies the +# value in a real run; those roles install the binary themselves, by including +# matrix-base's `ensure_openssl_installed` tasks. + +matrix_host_command_openssl: "/usr/bin/env openssl" + # --- Bridge-wide switches (matrix-base) ------------------------------------ # # Every bridge role reads these, so they live here rather than in each bridge's diff --git a/roles/custom/matrix-bridge-hookshot/molecule/default/converge.yml b/roles/custom/matrix-bridge-hookshot/molecule/default/converge.yml new file mode 100644 index 000000000..1865d1b98 --- /dev/null +++ b/roles/custom/matrix-bridge-hookshot/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 hookshot 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 hookshot 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 hookshot is started + hosts: all + become: true + gather_facts: false + tasks: + - name: Ensure systemd daemon is reloaded + ansible.builtin.systemd_service: + daemon_reload: true + + - name: Ensure the hookshot systemd service is started + ansible.builtin.systemd_service: + name: matrix-hookshot.service + state: started diff --git a/roles/custom/matrix-bridge-hookshot/molecule/default/molecule.yml b/roles/custom/matrix-bridge-hookshot/molecule/default/molecule.yml new file mode 100644 index 000000000..d259ab296 --- /dev/null +++ b/roles/custom/matrix-bridge-hookshot/molecule/default/molecule.yml @@ -0,0 +1,123 @@ +# 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: hookshot-${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 + options: + # The role's `tasks/main.yml` opens with a block tagged + # `reset-hookshot-encryption`, and Ansible runs tagged tasks unless tags are + # actually selected - so a tagless play (which is what Molecule runs) hits + # it. It stops `matrix-hookshot.service` before that service exists, which + # fails on a fresh host. Skipping the tag is what the playbook effectively + # does by always running with `--tags=setup-all,start`. + # + # `molecule-notest,notest` are Molecule's own defaults, repeated here + # because this key replaces them rather than adding to them. + skip-tags: molecule-notest,notest,reset-hookshot-encryption + inventory: + group_vars: + all: + matrix_bridge_hookshot_container_network: hookshot-molecule + + # The stub prepare.yml stands up. Hookshot contacts it while starting; + # it is not a real homeserver and nothing is asserted about it. + matrix_bridge_hookshot_homeserver_address: http://matrix.molecule.local:8008 + + # Appservice tokens. These are what Hookshot and the homeserver would + # authenticate to each other with; here they only have to reach the + # rendered configuration and the registration file. + matrix_bridge_hookshot_appservice_token: molecule_as_token_4f2a91 + matrix_bridge_hookshot_homeserver_token: molecule_hs_token_9b3e77 + + # Deliberately different from the role's default (`hookshot`), so the + # registration's sender_localpart can only have come from the role. + # prepare.yml tells the homeserver stub to claim this same user id. + matrix_bridge_hookshot_bot_localpart: molecule-hookshot + + # Hookshot's HTTP surface is the point of this scenario. Every port + # below differs from BOTH the role's default and Hookshot's own, so an + # answer on one of them can only mean the role's configuration reached + # the process. verify.yml also asserts that nothing answers on the + # defaults these replace (9993 appservice, 9000 webhooks, 9001 metrics). + matrix_bridge_hookshot_appservice_port: 9772 + matrix_bridge_hookshot_webhook_port: 9741 + + # Off in the role's defaults. Enabling it makes the role render a second + # entry in Hookshot's `listeners` list, which is the cheapest listener to + # assert *content* on: /metrics answers in a format nothing else would. + matrix_bridge_hookshot_metrics_enabled: true + matrix_bridge_hookshot_metrics_port: 9752 + + # On in the role's defaults. Turned off here so that the absence of a + # listener can be asserted too - the widgets port staying closed is what + # tells "the role rendered the listener list" apart from "Hookshot binds + # everything anyway". + matrix_bridge_hookshot_widgets_enabled: false + + # No third-party service is configured in this scenario - see the header + # of verify.yml. GitLab is the one the role enables by default, so it is + # explicitly switched off and its absence from the rendered config and + # registration is asserted. + matrix_bridge_hookshot_gitlab_enabled: false + + # The generic webhooks listener is the only part of Hookshot that needs + # no account anywhere, so it is what this scenario exercises live. The + # prefix differs from the role's default (`_webhooks_`). + matrix_bridge_hookshot_generic_userIdPrefix: _molecule_hook_ # noqa var-naming + + # Neither the role's default (600) nor Hookshot's own (600). + matrix_bridge_hookshot_feeds_pollIntervalSeconds: 907 # noqa var-naming + + # The role defaults to `warn`; Hookshot itself defaults to `info`. This + # is a third value, so finding it in config.yml cannot be a coincidence. + matrix_bridge_hookshot_logging_level: debug + + # Traefik is not deployed here, so the labels the role would render for + # it are switched off and their absence is asserted instead. + matrix_bridge_hookshot_container_labels_traefik_enabled: false + + # verify.yml runs as its own play, where role defaults are out of scope, + # so what it reads is pinned here. These two match the role's own + # defaults on purpose - they name things (a path, a container) rather + # than configure them, and nothing is asserted *about* them. + matrix_bridge_hookshot_base_path: /matrix/hookshot + matrix_bridge_hookshot_identifier: matrix-hookshot + 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/roles +scenario: + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - idempotence + - verify + - cleanup + - destroy +verifier: + name: ansible diff --git a/roles/custom/matrix-bridge-hookshot/molecule/default/prepare.yml b/roles/custom/matrix-bridge-hookshot/molecule/default/prepare.yml new file mode 100644 index 000000000..4c38489ca --- /dev/null +++ b/roles/custom/matrix-bridge-hookshot/molecule/default/prepare.yml @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Prepare hookshot 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_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 + + # The role's file tasks set owner/group by name and Ansible resolves those + # through the passwd database, so they have to exist first. matrix-base + # creates them in a real deployment. + - 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 the container network the role attaches to exists + ansible.builtin.command: + argv: + - docker + - network + - create + - "{{ matrix_bridge_hookshot_container_network }}" + register: hookshot_molecule_network + changed_when: hookshot_molecule_network.rc == 0 + failed_when: + - hookshot_molecule_network.rc != 0 + - "'already exists' not in hookshot_molecule_network.stderr" + + # Hookshot contacts the homeserver as it starts, and - being an appservice - + # refuses to run if /whoami answers with a user other than the bot it was + # configured as, so the stub is told to claim the localpart the scenario set. + # It is not being asked to bridge anything, and nothing is asserted about it; + # see molecule-shared/homeserver-stub.py. + - 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_bridge_hookshot_container_network }}" + molecule_shared_stub_server_name: molecule.local + molecule_shared_stub_user_id: "@{{ matrix_bridge_hookshot_bot_localpart }}:molecule.local" diff --git a/roles/custom/matrix-bridge-hookshot/molecule/default/requirements.yml b/roles/custom/matrix-bridge-hookshot/molecule/default/requirements.yml new file mode 120000 index 000000000..1567a5392 --- /dev/null +++ b/roles/custom/matrix-bridge-hookshot/molecule/default/requirements.yml @@ -0,0 +1 @@ +../../../../../molecule-shared/requirements.yml \ No newline at end of file diff --git a/roles/custom/matrix-bridge-hookshot/molecule/default/verify.yml b/roles/custom/matrix-bridge-hookshot/molecule/default/verify.yml new file mode 100644 index 000000000..bb3a3f774 --- /dev/null +++ b/roles/custom/matrix-bridge-hookshot/molecule/default/verify.yml @@ -0,0 +1,373 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +# What this proves: Hookshot starts, accepts the config.yml and registration.yml +# the role rendered, and opens exactly the HTTP listeners that configuration +# described - on the ports the role put there, and not on the ones it did not. +# +# What it deliberately does NOT do: configure GitHub, GitLab, Jira or Figma. +# Every one of those needs an account and a credential on a third-party service, +# which is the line where a scenario stops testing this repository and starts +# testing a fake (see docs/molecule-testing.md). The generic webhooks listener +# needs no credential from anyone, so it is the one that gets exercised live. +- name: Verify hookshot + 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 + + tasks: + # Read from the role's own defaults rather than pinned in molecule.yml, so + # the version assertion below compares the running image against what the + # role ships instead of against the scenario itself. The default ports come + # from here for the same reason: the "these ports stay closed" assertion is + # only meaningful against the ports the role would otherwise have used. + - name: Load the role's defaults under a separate name + ansible.builtin.include_vars: + file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" + name: hookshot_role_defaults + + - name: Wait for the hookshot service to become active + ansible.builtin.systemd_service: + name: matrix-hookshot.service + register: hookshot_service + until: hookshot_service.status.ActiveState == 'active' + retries: 30 + delay: 5 + failed_when: false + + # `Restart=always` means a bridge crash-looping on a configuration it cannot + # read still reports `active`, so the restart counter is checked too. It is + # asserted `is defined` because `| int` turns a missing property into 0 and + # would pass vacuously. + - name: Assert the service is active and has not been restarting + ansible.builtin.assert: + that: + - hookshot_service.status.ActiveState == 'active' + - hookshot_service.status.NRestarts is defined + - hookshot_service.status.NRestarts | int == 0 + fail_msg: >- + matrix-hookshot.service is + {{ hookshot_service.status.ActiveState | default('unknown') }} + after {{ hookshot_service.status.NRestarts | default('?') }} + automatic restart(s) + success_msg: "matrix-hookshot.service is active and has not restarted" + + # ------------------------------------------------------------------ + # The rendered configuration + # ------------------------------------------------------------------ + + - name: Read the configuration the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bridge_hookshot_base_path }}/config.yml" + register: hookshot_config_file + + - name: Parse the rendered configuration + ansible.builtin.set_fact: + hookshot_config: "{{ hookshot_config_file.content | b64decode | from_yaml }}" + + # Each of these differs from what Hookshot would use on its own AND from + # what the role defaults to, so finding them here means the role's + # configuration is what Hookshot is running on rather than a coincidence. + - name: Assert the rendered configuration carries this scenario's values + ansible.builtin.assert: + that: + - hookshot_config.bridge.domain == matrix_domain + - hookshot_config.bridge.url == matrix_bridge_hookshot_homeserver_address + - hookshot_config.bridge.port | int == matrix_bridge_hookshot_appservice_port | int + - hookshot_config.generic.userIdPrefix == matrix_bridge_hookshot_generic_userIdPrefix + - hookshot_config.feeds.pollIntervalSeconds | int == matrix_bridge_hookshot_feeds_pollIntervalSeconds | int + - hookshot_config.logging.level == matrix_bridge_hookshot_logging_level + - hookshot_config.metrics.enabled | bool + fail_msg: "The rendered configuration does not carry the scenario's values" + success_msg: "The rendered configuration carries the scenario's values" + + # Hookshot's `listeners` list is the role's own construction: it decides + # which resources get a port at all, from a handful of independent switches. + # Getting this wrong is invisible in a "did it start" test, which is why it + # is asserted as a whole rather than key by key. + - name: Assert the role rendered exactly the listeners the scenario asked for + ansible.builtin.assert: + that: + - hookshot_config.listeners | length == 2 + - hookshot_listener_ports == [matrix_bridge_hookshot_webhook_port | int, matrix_bridge_hookshot_metrics_port | int] + - hookshot_config.listeners | map(attribute='resources') | flatten == ['webhooks', 'metrics'] + fail_msg: >- + The rendered listeners are {{ hookshot_config.listeners }}, not the + webhooks and metrics listeners this scenario configured + success_msg: "The role rendered exactly the webhooks and metrics listeners" + vars: + hookshot_listener_ports: "{{ hookshot_config.listeners | map(attribute='port') | map('int') | list }}" + + # No third-party service is configured here, so none of their sections may + # appear. GitLab is the interesting one: the role turns it ON by default, so + # its absence is what proves the scenario's switch reached the template. + - name: Assert no third-party service section was rendered + ansible.builtin.assert: + that: + - "'gitlab' not in hookshot_config" + - "'github' not in hookshot_config" + - "'jira' not in hookshot_config" + - "'figma' not in hookshot_config" + fail_msg: >- + The rendered configuration contains a third-party service section + ({{ hookshot_config.keys() | list }}); this scenario configures none + success_msg: "No third-party service section was rendered" + + - name: Assert no widgets section was rendered while widgets are disabled + ansible.builtin.assert: + that: + - "'widgets' not in hookshot_config" + fail_msg: >- + A widgets section was rendered even though + matrix_bridge_hookshot_widgets_enabled is false + success_msg: "No widgets section was rendered while widgets are disabled" + + # ------------------------------------------------------------------ + # The rendered registration + # ------------------------------------------------------------------ + + # The registration file is the half of the appservice handshake the + # homeserver reads. The role generates it, Hookshot only consumes it, so it + # is worth checking on its own. + - name: Read the appservice registration the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bridge_hookshot_base_path }}/registration.yml" + register: hookshot_registration_file + + - name: Parse the rendered registration + ansible.builtin.set_fact: + hookshot_registration: "{{ hookshot_registration_file.content | b64decode | from_yaml }}" + + # `url` is where the homeserver would push transactions, and the role builds + # it out of the container name and the appservice port. It has to agree with + # `bridge.port` in config.yml or the two halves would silently disagree. + - name: Assert the registration carries the scenario's tokens, bot and callback URL + ansible.builtin.assert: + that: + - hookshot_registration.as_token == matrix_bridge_hookshot_appservice_token + - hookshot_registration.hs_token == matrix_bridge_hookshot_homeserver_token + - hookshot_registration.sender_localpart == matrix_bridge_hookshot_bot_localpart + - hookshot_registration.url == 'http://' + matrix_bridge_hookshot_identifier + ':' + (matrix_bridge_hookshot_appservice_port | string) + fail_msg: "The appservice registration does not carry the scenario's tokens, bot and callback URL" + success_msg: "The appservice registration carries the scenario's tokens, bot and callback URL" + + # The user namespace is derived from the generic webhook prefix, and the + # GitLab namespace is conditional on the service being enabled - so this + # checks that the two switches reach the registration, not just config.yml. + - name: Assert the registration namespaces follow the enabled services + ansible.builtin.assert: + that: + - hookshot_registration_user_regexes | select('search', matrix_bridge_hookshot_generic_userIdPrefix) | list | length == 1 + - hookshot_registration_user_regexes | select('search', '_gitlab_') | list | length == 0 + fail_msg: >- + The registration's user namespaces are + {{ hookshot_registration_user_regexes }}, which do not follow the + services this scenario enabled + success_msg: "The registration's user namespaces follow the enabled services" + vars: + hookshot_registration_user_regexes: "{{ hookshot_registration.namespaces.users | map(attribute='regex') | list }}" + + # ------------------------------------------------------------------ + # The listeners, live + # ------------------------------------------------------------------ + + # A helper container is used because the role publishes no host port, exactly + # as in a real deployment; see docs/molecule-testing.md. + - name: Wait for the webhooks listener to answer on the port the role configured + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bridge_hookshot_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --max-time + - "5" + - --request + - POST + - --header + - "Content-Type: application/json" + - --data + - "{}" + - --write-out + - "|HTTP_STATUS=%{http_code}" + - "http://{{ matrix_bridge_hookshot_identifier }}:{{ matrix_bridge_hookshot_webhook_port }}/webhook/molecule-no-such-hook" + register: hookshot_webhooks_probe + changed_when: false + until: "'HTTP_STATUS=000' not in hookshot_webhooks_probe.stdout" + retries: 24 + delay: 5 + failed_when: false + + # Hookshot answers an unknown webhook id from its generic-webhook handler, + # with a JSON body no other component would produce. An Express "Cannot POST" + # page here would mean the port is Hookshot's but the generic webhooks + # service was never mounted on it; a refused connection would mean the + # listener the role described was never opened at all. + - name: Assert the generic webhooks service is mounted on that listener + ansible.builtin.assert: + that: + - "'\"ok\":false' in hookshot_webhooks_probe.stdout" + - "'Webhook not found' in hookshot_webhooks_probe.stdout" + - "'HTTP_STATUS=404' in hookshot_webhooks_probe.stdout" + fail_msg: >- + Port {{ matrix_bridge_hookshot_webhook_port }} did not answer as + Hookshot's generic webhooks service + ({{ hookshot_webhooks_probe.stdout | default('no output') }}) + success_msg: "The generic webhooks service answers on the port the role configured" + + # Metrics are OFF in the role's defaults, so this listener exists only + # because the scenario asked for it - and /metrics answers in a format + # nothing else on that port could have produced. + - name: Probe the metrics listener on the port the role configured + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bridge_hookshot_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --max-time + - "5" + - --write-out + - "|HTTP_STATUS=%{http_code}" + - "http://{{ matrix_bridge_hookshot_identifier }}:{{ matrix_bridge_hookshot_metrics_port }}/metrics" + register: hookshot_metrics_probe + changed_when: false + failed_when: false + + - name: Assert the metrics listener serves Hookshot's own metrics + ansible.builtin.assert: + that: + - "'HTTP_STATUS=200' in hookshot_metrics_probe.stdout" + - "'hookshot_webhooks_http_request' in hookshot_metrics_probe.stdout" + fail_msg: >- + Port {{ matrix_bridge_hookshot_metrics_port }} did not serve Hookshot's + metrics ({{ hookshot_metrics_probe.stdout | default('no output') | truncate(200) }}) + success_msg: "The metrics listener serves Hookshot's own metrics" + + # The appservice port is not in `listeners` - it comes from `bridge.port` - + # so it is a separate socket, opened by a separate part of the config. + - name: Probe the appservice port the role configured + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bridge_hookshot_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --max-time + - "5" + - --write-out + - "|HTTP_STATUS=%{http_code}" + - "http://{{ matrix_bridge_hookshot_identifier }}:{{ matrix_bridge_hookshot_appservice_port }}/_matrix/app/v1/ping" + register: hookshot_appservice_probe + changed_when: false + failed_when: false + + - name: Assert the appservice API answers on the port the role configured + ansible.builtin.assert: + that: + - "'HTTP_STATUS=000' not in hookshot_appservice_probe.stdout" + - "'errcode' in hookshot_appservice_probe.stdout" + fail_msg: >- + Port {{ matrix_bridge_hookshot_appservice_port }} did not answer as a + Matrix appservice + ({{ hookshot_appservice_probe.stdout | default('no output') }}) + success_msg: "The appservice API answers on the port the role configured" + + # The other half of the story. Every port above is one the scenario chose; + # these are the ones the role and Hookshot would have used if the scenario's + # configuration had never reached the process. If any of them answers, then + # a passing probe above proves much less than it looks like it does. + - name: Probe the ports the role's defaults would have used + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bridge_hookshot_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --max-time + - "5" + - --output + - /dev/null + - --write-out + - "HTTP_STATUS=%{http_code}" + - "http://{{ matrix_bridge_hookshot_identifier }}:{{ item.port }}/" + register: hookshot_closed_probes + changed_when: false + failed_when: false + loop: + - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_appservice_port }}" + what: the appservice port the role defaults to + - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_webhook_port }}" + what: the webhooks port the role defaults to + - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_metrics_port }}" + what: the metrics port the role defaults to + - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_widgets_port }}" + what: the widgets port, whose listener this scenario disabled + loop_control: + label: "{{ item.port }} - {{ item.what }}" + + - name: Assert nothing listens on the ports the role's defaults would have used + ansible.builtin.assert: + that: + - hookshot_closed_probes.results | rejectattr('stdout', 'search', 'HTTP_STATUS=000') | list | length == 0 + fail_msg: >- + Something answered on + {{ hookshot_closed_probes.results | rejectattr('stdout', 'search', 'HTTP_STATUS=000') | map(attribute='item') | list }}, + so the ports this scenario configured are not the only ones Hookshot + is listening on + success_msg: >- + Nothing listens on the ports the role's defaults would have used + ({{ hookshot_closed_probes.results | map(attribute='item.port') | list | join(', ') }}) + + # ------------------------------------------------------------------ + # The container the role started + # ------------------------------------------------------------------ + + - name: Read the image of the running container + ansible.builtin.command: + argv: + - docker + - container + - inspect + - "{{ matrix_bridge_hookshot_identifier }}" + - --format + - "{{ '{{' }} .Config.Image {{ '}}' }}" + register: hookshot_image + changed_when: false + + - name: Assert the running container is the version defaults/main.yml pins + ansible.builtin.assert: + that: + - hookshot_role_defaults.matrix_bridge_hookshot_version | string in hookshot_image.stdout + fail_msg: >- + The running container is {{ hookshot_image.stdout }}, which does not + carry the pinned version + {{ hookshot_role_defaults.matrix_bridge_hookshot_version }} + success_msg: "The running container is the version defaults/main.yml pins" + + - name: Read the labels the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bridge_hookshot_base_path }}/labels" + register: hookshot_labels + + - name: Assert no Traefik labels are emitted while Traefik support is disabled + ansible.builtin.assert: + that: + - "'traefik.' not in (hookshot_labels.content | b64decode)" + fail_msg: >- + Traefik labels were emitted even though + matrix_bridge_hookshot_container_labels_traefik_enabled is false + success_msg: "No Traefik labels are emitted while Traefik support is disabled"