Matrix Docker Ansible eploy
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

374 строки
18 KiB

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