Matrix Docker Ansible eploy
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

364 linhas
17 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. ---
  5. # Proves Hookshot starts on the config.yml and registration.yml the role rendered,
  6. # and opens exactly the HTTP listeners that configuration described - on the ports
  7. # the role put there, and not on the ones it did not.
  8. #
  9. # Deliberately does NOT configure GitHub, GitLab, Jira or Figma. Each needs an account
  10. # and a credential on a third-party service, which is where a scenario stops testing this
  11. # repository and starts testing a fake (see docs/molecule-testing.md). The generic webhooks
  12. # listener needs no credential from anyone, so it is the one exercised live.
  13. - name: Verify hookshot
  14. hosts: all
  15. become: true
  16. vars_files:
  17. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
  18. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
  19. gather_facts: false
  20. tasks:
  21. # From the role's own defaults rather than pinned in molecule.yml, so the version
  22. # assertion compares the running image against what the role ships, not against the
  23. # scenario. The default ports come from here for the same reason: "these ports stay
  24. # closed" is only meaningful against the ports the role would otherwise have used.
  25. - name: Load the role's defaults under a separate name
  26. ansible.builtin.include_vars:
  27. file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
  28. name: hookshot_role_defaults
  29. - name: Wait for the hookshot service to become active
  30. ansible.builtin.systemd_service:
  31. name: matrix-hookshot.service
  32. register: hookshot_service
  33. until: hookshot_service.status.ActiveState == 'active'
  34. retries: 30
  35. delay: 5
  36. failed_when: false
  37. # `Restart=always` means a bridge crash-looping on unreadable config still reports
  38. # `active`, so the restart counter is checked too. Asserted `is defined` because
  39. # `| int` turns a missing property into 0 and would pass vacuously.
  40. - name: Assert the service is active and has not been restarting
  41. ansible.builtin.assert:
  42. that:
  43. - hookshot_service.status.ActiveState == 'active'
  44. - hookshot_service.status.NRestarts is defined
  45. - hookshot_service.status.NRestarts | int == 0
  46. fail_msg: >-
  47. matrix-hookshot.service is
  48. {{ hookshot_service.status.ActiveState | default('unknown') }}
  49. after {{ hookshot_service.status.NRestarts | default('?') }}
  50. automatic restart(s)
  51. success_msg: "matrix-hookshot.service is active and has not restarted"
  52. # ------------------------------------------------------------------
  53. # The rendered configuration
  54. # ------------------------------------------------------------------
  55. - name: Read the configuration the role rendered
  56. ansible.builtin.slurp:
  57. src: "{{ matrix_bridge_hookshot_base_path }}/config.yml"
  58. register: hookshot_config_file
  59. - name: Parse the rendered configuration
  60. ansible.builtin.set_fact:
  61. hookshot_config: "{{ hookshot_config_file.content | b64decode | from_yaml }}"
  62. # Each differs from what Hookshot would use on its own AND from the role's defaults,
  63. # so finding them here rules out a coincidence.
  64. - name: Assert the rendered configuration carries this scenario's values
  65. ansible.builtin.assert:
  66. that:
  67. - hookshot_config.bridge.domain == matrix_domain
  68. - hookshot_config.bridge.url == matrix_bridge_hookshot_homeserver_address
  69. - hookshot_config.bridge.port | int == matrix_bridge_hookshot_appservice_port | int
  70. - hookshot_config.generic.userIdPrefix == matrix_bridge_hookshot_generic_userIdPrefix
  71. - hookshot_config.feeds.pollIntervalSeconds | int == matrix_bridge_hookshot_feeds_pollIntervalSeconds | int
  72. - hookshot_config.logging.level == matrix_bridge_hookshot_logging_level
  73. - hookshot_config.metrics.enabled | bool
  74. fail_msg: "The rendered configuration does not carry the scenario's values"
  75. success_msg: "The rendered configuration carries the scenario's values"
  76. # The `listeners` list is the role's own construction, assembled from a handful of
  77. # independent switches. Getting it wrong is invisible in a "did it start" test,
  78. # hence asserting the whole shape rather than key by key.
  79. - name: Assert the role rendered exactly the listeners the scenario asked for
  80. ansible.builtin.assert:
  81. that:
  82. - hookshot_config.listeners | length == 2
  83. - hookshot_listener_ports == [matrix_bridge_hookshot_webhook_port | int, matrix_bridge_hookshot_metrics_port | int]
  84. - hookshot_config.listeners | map(attribute='resources') | flatten == ['webhooks', 'metrics']
  85. fail_msg: >-
  86. The rendered listeners are {{ hookshot_config.listeners }}, not the
  87. webhooks and metrics listeners this scenario configured
  88. success_msg: "The role rendered exactly the webhooks and metrics listeners"
  89. vars:
  90. hookshot_listener_ports: "{{ hookshot_config.listeners | map(attribute='port') | map('int') | list }}"
  91. # GitLab is the interesting one: the role turns it ON by default, so its absence
  92. # proves the scenario's switch reached the template.
  93. - name: Assert no third-party service section was rendered
  94. ansible.builtin.assert:
  95. that:
  96. - "'gitlab' not in hookshot_config"
  97. - "'github' not in hookshot_config"
  98. - "'jira' not in hookshot_config"
  99. - "'figma' not in hookshot_config"
  100. fail_msg: >-
  101. The rendered configuration contains a third-party service section
  102. ({{ hookshot_config.keys() | list }}); this scenario configures none
  103. success_msg: "No third-party service section was rendered"
  104. - name: Assert no widgets section was rendered while widgets are disabled
  105. ansible.builtin.assert:
  106. that:
  107. - "'widgets' not in hookshot_config"
  108. fail_msg: >-
  109. A widgets section was rendered even though
  110. matrix_bridge_hookshot_widgets_enabled is false
  111. success_msg: "No widgets section was rendered while widgets are disabled"
  112. # ------------------------------------------------------------------
  113. # The rendered registration
  114. # ------------------------------------------------------------------
  115. # The role generates the registration; Hookshot only consumes it. Worth checking
  116. # on its own, as it is the half of the handshake the homeserver reads.
  117. - name: Read the appservice registration the role rendered
  118. ansible.builtin.slurp:
  119. src: "{{ matrix_bridge_hookshot_base_path }}/registration.yml"
  120. register: hookshot_registration_file
  121. - name: Parse the rendered registration
  122. ansible.builtin.set_fact:
  123. hookshot_registration: "{{ hookshot_registration_file.content | b64decode | from_yaml }}"
  124. # `url` is built from the container name and the appservice port. It has to agree
  125. # with `bridge.port` in config.yml, or the two halves silently disagree.
  126. - name: Assert the registration carries the scenario's tokens, bot and callback URL
  127. ansible.builtin.assert:
  128. that:
  129. - hookshot_registration.as_token == matrix_bridge_hookshot_appservice_token
  130. - hookshot_registration.hs_token == matrix_bridge_hookshot_homeserver_token
  131. - hookshot_registration.sender_localpart == matrix_bridge_hookshot_bot_localpart
  132. - hookshot_registration.url == 'http://' + matrix_bridge_hookshot_identifier + ':' + (matrix_bridge_hookshot_appservice_port | string)
  133. fail_msg: "The appservice registration does not carry the scenario's tokens, bot and callback URL"
  134. success_msg: "The appservice registration carries the scenario's tokens, bot and callback URL"
  135. # The user namespace derives from the generic webhook prefix, and the GitLab namespace
  136. # is conditional on that service being enabled. Checks both switches reach the
  137. # registration, not just config.yml.
  138. - name: Assert the registration namespaces follow the enabled services
  139. ansible.builtin.assert:
  140. that:
  141. - hookshot_registration_user_regexes | select('search', matrix_bridge_hookshot_generic_userIdPrefix) | list | length == 1
  142. - hookshot_registration_user_regexes | select('search', '_gitlab_') | list | length == 0
  143. fail_msg: >-
  144. The registration's user namespaces are
  145. {{ hookshot_registration_user_regexes }}, which do not follow the
  146. services this scenario enabled
  147. success_msg: "The registration's user namespaces follow the enabled services"
  148. vars:
  149. hookshot_registration_user_regexes: "{{ hookshot_registration.namespaces.users | map(attribute='regex') | list }}"
  150. # ------------------------------------------------------------------
  151. # The listeners, live
  152. # ------------------------------------------------------------------
  153. # A helper container, because the role publishes no host port - exactly as in a real
  154. # deployment. See docs/molecule-testing.md.
  155. - name: Wait for the webhooks listener to answer on the port the role configured
  156. ansible.builtin.command:
  157. argv:
  158. - docker
  159. - run
  160. - --rm
  161. - --network={{ matrix_bridge_hookshot_container_network }}
  162. - "{{ molecule_shared_image_curl }}"
  163. - --silent
  164. - --max-time
  165. - "5"
  166. - --request
  167. - POST
  168. - --header
  169. - "Content-Type: application/json"
  170. - --data
  171. - "{}"
  172. - --write-out
  173. - "|HTTP_STATUS=%{http_code}"
  174. - "http://{{ matrix_bridge_hookshot_identifier }}:{{ matrix_bridge_hookshot_webhook_port }}/webhook/molecule-no-such-hook"
  175. register: hookshot_webhooks_probe
  176. changed_when: false
  177. until: "'HTTP_STATUS=000' not in hookshot_webhooks_probe.stdout"
  178. retries: 24
  179. delay: 5
  180. failed_when: false
  181. # An unknown webhook id draws a JSON body from the generic-webhook handler that no
  182. # other component would produce. An Express "Cannot POST" page would mean the port is
  183. # Hookshot's but the generic webhooks service was never mounted on it; a refused
  184. # connection would mean the listener was never opened at all.
  185. - name: Assert the generic webhooks service is mounted on that listener
  186. ansible.builtin.assert:
  187. that:
  188. - "'\"ok\":false' in hookshot_webhooks_probe.stdout"
  189. - "'Webhook not found' in hookshot_webhooks_probe.stdout"
  190. - "'HTTP_STATUS=404' in hookshot_webhooks_probe.stdout"
  191. fail_msg: >-
  192. Port {{ matrix_bridge_hookshot_webhook_port }} did not answer as
  193. Hookshot's generic webhooks service
  194. ({{ hookshot_webhooks_probe.stdout | default('no output') }})
  195. success_msg: "The generic webhooks service answers on the port the role configured"
  196. # Metrics are OFF in the role's defaults, so this listener exists only because the
  197. # scenario asked for it.
  198. - name: Probe the metrics listener on the port the role configured
  199. ansible.builtin.command:
  200. argv:
  201. - docker
  202. - run
  203. - --rm
  204. - --network={{ matrix_bridge_hookshot_container_network }}
  205. - "{{ molecule_shared_image_curl }}"
  206. - --silent
  207. - --max-time
  208. - "5"
  209. - --write-out
  210. - "|HTTP_STATUS=%{http_code}"
  211. - "http://{{ matrix_bridge_hookshot_identifier }}:{{ matrix_bridge_hookshot_metrics_port }}/metrics"
  212. register: hookshot_metrics_probe
  213. changed_when: false
  214. failed_when: false
  215. - name: Assert the metrics listener serves Hookshot's own metrics
  216. ansible.builtin.assert:
  217. that:
  218. - "'HTTP_STATUS=200' in hookshot_metrics_probe.stdout"
  219. - "'hookshot_webhooks_http_request' in hookshot_metrics_probe.stdout"
  220. fail_msg: >-
  221. Port {{ matrix_bridge_hookshot_metrics_port }} did not serve Hookshot's
  222. metrics ({{ hookshot_metrics_probe.stdout | default('no output') | truncate(200) }})
  223. success_msg: "The metrics listener serves Hookshot's own metrics"
  224. # The appservice port is not in `listeners`; it comes from `bridge.port`.
  225. # A separate socket, opened by a separate part of the config.
  226. - name: Probe the appservice port the role configured
  227. ansible.builtin.command:
  228. argv:
  229. - docker
  230. - run
  231. - --rm
  232. - --network={{ matrix_bridge_hookshot_container_network }}
  233. - "{{ molecule_shared_image_curl }}"
  234. - --silent
  235. - --max-time
  236. - "5"
  237. - --write-out
  238. - "|HTTP_STATUS=%{http_code}"
  239. - "http://{{ matrix_bridge_hookshot_identifier }}:{{ matrix_bridge_hookshot_appservice_port }}/_matrix/app/v1/ping"
  240. register: hookshot_appservice_probe
  241. changed_when: false
  242. failed_when: false
  243. - name: Assert the appservice API answers on the port the role configured
  244. ansible.builtin.assert:
  245. that:
  246. - "'HTTP_STATUS=000' not in hookshot_appservice_probe.stdout"
  247. - "'errcode' in hookshot_appservice_probe.stdout"
  248. fail_msg: >-
  249. Port {{ matrix_bridge_hookshot_appservice_port }} did not answer as a
  250. Matrix appservice
  251. ({{ hookshot_appservice_probe.stdout | default('no output') }})
  252. success_msg: "The appservice API answers on the port the role configured"
  253. # The other half of the story. Every port above is one the scenario chose; these are
  254. # the ones the role and Hookshot would have used had the scenario's configuration never
  255. # reached the process. If any of them answers, the probes above prove much less than
  256. # they appear to.
  257. - name: Probe the ports the role's defaults would have used
  258. ansible.builtin.command:
  259. argv:
  260. - docker
  261. - run
  262. - --rm
  263. - --network={{ matrix_bridge_hookshot_container_network }}
  264. - "{{ molecule_shared_image_curl }}"
  265. - --silent
  266. - --max-time
  267. - "5"
  268. - --output
  269. - /dev/null
  270. - --write-out
  271. - "HTTP_STATUS=%{http_code}"
  272. - "http://{{ matrix_bridge_hookshot_identifier }}:{{ item.port }}/"
  273. register: hookshot_closed_probes
  274. changed_when: false
  275. failed_when: false
  276. loop:
  277. - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_appservice_port }}"
  278. what: the appservice port the role defaults to
  279. - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_webhook_port }}"
  280. what: the webhooks port the role defaults to
  281. - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_metrics_port }}"
  282. what: the metrics port the role defaults to
  283. - port: "{{ hookshot_role_defaults.matrix_bridge_hookshot_widgets_port }}"
  284. what: the widgets port, whose listener this scenario disabled
  285. loop_control:
  286. label: "{{ item.port }} - {{ item.what }}"
  287. - name: Assert nothing listens on the ports the role's defaults would have used
  288. ansible.builtin.assert:
  289. that:
  290. - hookshot_closed_probes.results | rejectattr('stdout', 'search', 'HTTP_STATUS=000') | list | length == 0
  291. fail_msg: >-
  292. Something answered on
  293. {{ hookshot_closed_probes.results | rejectattr('stdout', 'search', 'HTTP_STATUS=000') | map(attribute='item') | list }},
  294. so the ports this scenario configured are not the only ones Hookshot
  295. is listening on
  296. success_msg: >-
  297. Nothing listens on the ports the role's defaults would have used
  298. ({{ hookshot_closed_probes.results | map(attribute='item.port') | list | join(', ') }})
  299. # ------------------------------------------------------------------
  300. # The container the role started
  301. # ------------------------------------------------------------------
  302. - name: Read the image of the running container
  303. ansible.builtin.command:
  304. argv:
  305. - docker
  306. - container
  307. - inspect
  308. - "{{ matrix_bridge_hookshot_identifier }}"
  309. - --format
  310. - "{{ '{{' }} .Config.Image {{ '}}' }}"
  311. register: hookshot_image
  312. changed_when: false
  313. - name: Assert the running container is the version defaults/main.yml pins
  314. ansible.builtin.assert:
  315. that:
  316. - hookshot_role_defaults.matrix_bridge_hookshot_version | string in hookshot_image.stdout
  317. fail_msg: >-
  318. The running container is {{ hookshot_image.stdout }}, which does not
  319. carry the pinned version
  320. {{ hookshot_role_defaults.matrix_bridge_hookshot_version }}
  321. success_msg: "The running container is the version defaults/main.yml pins"
  322. - name: Read the labels the role rendered
  323. ansible.builtin.slurp:
  324. src: "{{ matrix_bridge_hookshot_base_path }}/labels"
  325. register: hookshot_labels
  326. - name: Assert no Traefik labels are emitted while Traefik support is disabled
  327. ansible.builtin.assert:
  328. that:
  329. - "'traefik.' not in (hookshot_labels.content | b64decode)"
  330. fail_msg: >-
  331. Traefik labels were emitted even though
  332. matrix_bridge_hookshot_container_labels_traefik_enabled is false
  333. success_msg: "No Traefik labels are emitted while Traefik support is disabled"