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

388 строки
21 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. ---
  5. # Proves the bridge starts without a Google account or paired device, accepts the role-rendered
  6. # configuration and registration, initializes real Postgres, and exposes its live appservice.
  7. - name: Verify mautrix-gmessages
  8. hosts: all
  9. become: true
  10. vars_files:
  11. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
  12. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
  13. vars:
  14. mautrix_gmessages_config: "{{ mautrix_gmessages_config_file.content | b64decode | from_yaml }}"
  15. mautrix_gmessages_registration: "{{ mautrix_gmessages_registration_file.content | b64decode | from_yaml }}"
  16. mautrix_gmessages_labels_rendered: "{{ mautrix_gmessages_labels_file.content | b64decode }}"
  17. mautrix_gmessages_expected_public_address: http://gmessages-api.molecule.local/bridges/gmessages-api
  18. gather_facts: false
  19. tasks:
  20. # The version remains sourced from the role so a Renovate bump changes the expectation.
  21. - name: Load the role's defaults under a separate name
  22. ansible.builtin.include_vars:
  23. file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
  24. name: mautrix_gmessages_role_defaults
  25. - name: Wait for the mautrix-gmessages service to become active
  26. ansible.builtin.systemd_service:
  27. name: matrix-mautrix-gmessages.service
  28. register: mautrix_gmessages_service
  29. until: mautrix_gmessages_service.status.ActiveState == 'active'
  30. retries: 30
  31. delay: 5
  32. failed_when: false
  33. # Restart=always means ActiveState alone cannot distinguish a healthy service from a loop.
  34. - name: Assert the service is active and has not been restarting
  35. ansible.builtin.assert:
  36. that:
  37. - mautrix_gmessages_service.status.ActiveState == 'active'
  38. - mautrix_gmessages_service.status.NRestarts is defined
  39. - mautrix_gmessages_service.status.NRestarts | int == 0
  40. fail_msg: >-
  41. matrix-mautrix-gmessages.service is
  42. {{ mautrix_gmessages_service.status.ActiveState | default('unknown') }} after
  43. {{ mautrix_gmessages_service.status.NRestarts | default('?') }} restart(s)
  44. success_msg: "matrix-mautrix-gmessages.service is active and has not restarted"
  45. - name: Wait for the bridge liveness endpoint
  46. ansible.builtin.command:
  47. argv:
  48. - docker
  49. - run
  50. - --rm
  51. - --network={{ matrix_bridge_mautrix_gmessages_container_network }}
  52. - "{{ molecule_shared_image_curl }}"
  53. - --silent
  54. - --output
  55. - /dev/null
  56. - --write-out
  57. - "HTTP_STATUS=%{http_code}"
  58. - http://matrix-mautrix-gmessages:8080/_matrix/mau/live
  59. register: mautrix_gmessages_live
  60. changed_when: false
  61. until: mautrix_gmessages_live.rc == 0 and mautrix_gmessages_live.stdout == 'HTTP_STATUS=200'
  62. retries: 24
  63. delay: 5
  64. failed_when: false
  65. - name: Wait for the bridge readiness endpoint
  66. ansible.builtin.command:
  67. argv:
  68. - docker
  69. - run
  70. - --rm
  71. - --network={{ matrix_bridge_mautrix_gmessages_container_network }}
  72. - "{{ molecule_shared_image_curl }}"
  73. - --silent
  74. - --output
  75. - /dev/null
  76. - --write-out
  77. - "HTTP_STATUS=%{http_code}"
  78. - http://matrix-mautrix-gmessages:8080/_matrix/mau/ready
  79. register: mautrix_gmessages_ready
  80. changed_when: false
  81. until: mautrix_gmessages_ready.rc == 0 and mautrix_gmessages_ready.stdout == 'HTTP_STATUS=200'
  82. retries: 24
  83. delay: 5
  84. failed_when: false
  85. - name: Assert the bridge is live and ready on the configured appservice port
  86. ansible.builtin.assert:
  87. that:
  88. - mautrix_gmessages_live.rc == 0
  89. - mautrix_gmessages_live.stdout == 'HTTP_STATUS=200'
  90. - mautrix_gmessages_ready.rc == 0
  91. - mautrix_gmessages_ready.stdout == 'HTTP_STATUS=200'
  92. fail_msg: >-
  93. Health probes returned live={{ mautrix_gmessages_live.stdout | default('none') }}
  94. (rc={{ mautrix_gmessages_live.rc | default('?') }}) and
  95. ready={{ mautrix_gmessages_ready.stdout | default('none') }}
  96. (rc={{ mautrix_gmessages_ready.rc | default('?') }})
  97. success_msg: "The bridge is live and ready on the configured appservice port"
  98. # This authenticated endpoint exercises the provisioning middleware and the network
  99. # connector's login-flow API without starting a Google login or carrying credentials.
  100. - name: Query the authenticated provisioning login flows
  101. ansible.builtin.command:
  102. argv:
  103. - docker
  104. - run
  105. - --rm
  106. - --network={{ matrix_bridge_mautrix_gmessages_container_network }}
  107. - "{{ molecule_shared_image_curl }}"
  108. - --silent
  109. - --show-error
  110. - --fail-with-body
  111. - --header
  112. - "Authorization: Bearer {{ matrix_bridge_mautrix_gmessages_provisioning_shared_secret }}"
  113. - "http://matrix-mautrix-gmessages:8080/_matrix/provision/v3/login/flows?user_id=%40alice%3Amolecule.local"
  114. register: mautrix_gmessages_login_flows
  115. changed_when: false
  116. - name: Query the provisioning API with an invalid secret
  117. ansible.builtin.command:
  118. argv:
  119. - docker
  120. - run
  121. - --rm
  122. - --network={{ matrix_bridge_mautrix_gmessages_container_network }}
  123. - "{{ molecule_shared_image_curl }}"
  124. - --silent
  125. - --output
  126. - /dev/null
  127. - --write-out
  128. - "HTTP_STATUS=%{http_code}"
  129. - --header
  130. - "Authorization: Bearer definitely-wrong-molecule-secret"
  131. - "http://matrix-mautrix-gmessages:8080/_matrix/provision/v3/login/flows?user_id=%40alice%3Amolecule.local"
  132. register: mautrix_gmessages_login_flows_unauthorized
  133. changed_when: false
  134. failed_when: false
  135. - name: Assert the live provisioning API enforces auth and advertises login flows
  136. ansible.builtin.assert:
  137. that:
  138. - mautrix_gmessages_login_flows.rc == 0
  139. - (mautrix_gmessages_login_flows.stdout | from_json).flows is sequence
  140. - (mautrix_gmessages_login_flows.stdout | from_json).flows | length > 0
  141. - mautrix_gmessages_login_flows_unauthorized.rc == 0
  142. - mautrix_gmessages_login_flows_unauthorized.stdout == 'HTTP_STATUS=401'
  143. fail_msg: "The provisioning API did not enforce its secret or return Google login flows"
  144. success_msg: "The provisioning API enforces its secret and returns Google login flows"
  145. - name: Read the configuration the role rendered
  146. ansible.builtin.slurp:
  147. src: "{{ matrix_bridge_mautrix_gmessages_config_path }}/config.yaml"
  148. register: mautrix_gmessages_config_file
  149. - name: Assert the rendered configuration carries the appservice identity
  150. ansible.builtin.assert:
  151. that:
  152. - mautrix_gmessages_config.homeserver.address == matrix_bridge_mautrix_gmessages_homeserver_address
  153. - mautrix_gmessages_config.homeserver.domain == matrix_bridge_mautrix_gmessages_homeserver_domain
  154. - mautrix_gmessages_config.appservice.id == 'gmessages'
  155. - mautrix_gmessages_config.appservice.address == 'http://matrix-mautrix-gmessages:8080'
  156. - mautrix_gmessages_config.appservice.public_address == mautrix_gmessages_expected_public_address
  157. - mautrix_gmessages_config.appservice.bot.username == matrix_bridge_mautrix_gmessages_appservice_bot_username
  158. - mautrix_gmessages_config.appservice.as_token == matrix_bridge_mautrix_gmessages_appservice_token
  159. - mautrix_gmessages_config.appservice.hs_token == matrix_bridge_mautrix_gmessages_homeserver_token
  160. - mautrix_gmessages_config.appservice.username_template == matrix_bridge_mautrix_gmessages_appservice_username_template
  161. fail_msg: "The rendered configuration does not carry the scenario's appservice identity"
  162. success_msg: "The rendered configuration carries the scenario's appservice identity"
  163. - name: Assert the rendered configuration carries non-default bridge behavior
  164. ansible.builtin.assert:
  165. that:
  166. - mautrix_gmessages_config.bridge.command_prefix == matrix_bridge_mautrix_gmessages_command_prefix
  167. - not mautrix_gmessages_config.bridge.personal_filtering_spaces
  168. - not mautrix_gmessages_config.matrix.federate_rooms
  169. - mautrix_gmessages_config.network.displayname_template == matrix_bridge_mautrix_gmessages_network_displayname_template
  170. - mautrix_gmessages_config.provisioning.shared_secret == matrix_bridge_mautrix_gmessages_provisioning_shared_secret
  171. - mautrix_gmessages_config.logging.min_level == matrix_bridge_mautrix_gmessages_logging_level
  172. fail_msg: "The rendered configuration does not carry the scenario's bridge behavior"
  173. success_msg: "The rendered configuration carries the scenario's bridge behavior"
  174. - name: Assert the rendered configuration carries the non-default backfill policy
  175. ansible.builtin.assert:
  176. that:
  177. - not mautrix_gmessages_config.backfill.enabled
  178. - mautrix_gmessages_config.backfill.max_initial_messages == 17
  179. - mautrix_gmessages_config.backfill.max_catchup_messages == 29
  180. - mautrix_gmessages_config.backfill.unread_hours_threshold == 61
  181. - mautrix_gmessages_config.backfill.threads.max_initial_messages == 7
  182. fail_msg: "The rendered configuration does not carry the scenario's backfill policy"
  183. success_msg: "The rendered configuration carries the scenario's backfill policy"
  184. - name: Assert the configuration points at the scenario's Postgres database
  185. ansible.builtin.assert:
  186. that:
  187. - mautrix_gmessages_config.database.type == matrix_bridge_mautrix_gmessages_database_engine
  188. - matrix_bridge_mautrix_gmessages_database_username in mautrix_gmessages_config.database.uri
  189. - matrix_bridge_mautrix_gmessages_database_hostname in mautrix_gmessages_config.database.uri
  190. - matrix_bridge_mautrix_gmessages_database_name in mautrix_gmessages_config.database.uri
  191. fail_msg: >-
  192. database.uri is {{ mautrix_gmessages_config.database.uri | default('unset') }}, which
  193. was not composed from the scenario's connection settings
  194. success_msg: "The configuration points at the scenario's Postgres database"
  195. - name: Read the appservice registration the role rendered
  196. ansible.builtin.slurp:
  197. src: "{{ matrix_bridge_mautrix_gmessages_config_path }}/registration.yaml"
  198. register: mautrix_gmessages_registration_file
  199. - name: Assert the registration carries the configured handshake values
  200. ansible.builtin.assert:
  201. that:
  202. - mautrix_gmessages_registration.id == 'gmessages'
  203. - mautrix_gmessages_registration.as_token == matrix_bridge_mautrix_gmessages_appservice_token
  204. - mautrix_gmessages_registration.hs_token == matrix_bridge_mautrix_gmessages_homeserver_token
  205. - mautrix_gmessages_registration.sender_localpart == '_bot_' + matrix_bridge_mautrix_gmessages_appservice_bot_username
  206. - mautrix_gmessages_registration.url == 'http://matrix-mautrix-gmessages:8080'
  207. fail_msg: "The registration does not carry the configured handshake values"
  208. success_msg: "The registration carries the configured handshake values"
  209. - name: Assert the registration namespaces cover only GMessages ghosts and the bridge bot
  210. ansible.builtin.assert:
  211. that:
  212. - mautrix_gmessages_ghost_regex | length > 0
  213. - mautrix_gmessages_bot_regex | length > 0
  214. - mautrix_gmessages_ghost_mxid is match(mautrix_gmessages_ghost_regex)
  215. - mautrix_gmessages_wrong_ghost_mxid is not match(mautrix_gmessages_ghost_regex)
  216. - mautrix_gmessages_bot_mxid is match(mautrix_gmessages_bot_regex)
  217. fail_msg: "The registration namespaces do not cover only GMessages ghosts and the bot"
  218. success_msg: "The registration namespaces cover only GMessages ghosts and the bot"
  219. vars:
  220. mautrix_gmessages_user_regexes: "{{ mautrix_gmessages_registration.namespaces.users | map(attribute='regex') | list }}"
  221. mautrix_gmessages_ghost_regex: "{{ mautrix_gmessages_user_regexes | select('search', 'gmessages_') | first | default('') }}"
  222. mautrix_gmessages_bot_regex: "{{ mautrix_gmessages_user_regexes | reject('search', 'gmessages_') | first | default('') }}"
  223. mautrix_gmessages_ghost_mxid: "@gmessages_15551234567:{{ matrix_bridge_mautrix_gmessages_homeserver_domain }}"
  224. mautrix_gmessages_wrong_ghost_mxid: "@signal_15551234567:{{ matrix_bridge_mautrix_gmessages_homeserver_domain }}"
  225. mautrix_gmessages_bot_mxid: "@{{ matrix_bridge_mautrix_gmessages_appservice_bot_username }}:{{ matrix_bridge_mautrix_gmessages_homeserver_domain }}"
  226. # Tables can only appear after DNS resolution, authentication and genuine migrations.
  227. - name: List the tables the bridge created in Postgres
  228. ansible.builtin.command:
  229. argv:
  230. - docker
  231. - exec
  232. - matrix-postgres-molecule
  233. - psql
  234. - --username={{ matrix_bridge_mautrix_gmessages_database_username }}
  235. - --dbname={{ matrix_bridge_mautrix_gmessages_database_name }}
  236. - --tuples-only
  237. - --no-align
  238. - "--command=SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
  239. register: mautrix_gmessages_tables
  240. changed_when: false
  241. - name: Assert the bridge migrated its schema into the configured database
  242. ansible.builtin.assert:
  243. that:
  244. - mautrix_gmessages_tables.rc == 0
  245. - "'version' in mautrix_gmessages_table_names"
  246. - "'portal' in mautrix_gmessages_table_names"
  247. - "'user_login' in mautrix_gmessages_table_names"
  248. - "'message' in mautrix_gmessages_table_names"
  249. - "'gmessages_version' in mautrix_gmessages_table_names"
  250. - "'gmessages_login_prefix' in mautrix_gmessages_table_names"
  251. - mautrix_gmessages_table_names | length > 12
  252. fail_msg: >-
  253. The bridge did not migrate its schema into {{ matrix_bridge_mautrix_gmessages_database_name }}
  254. (found {{ mautrix_gmessages_table_names | length }} table(s))
  255. success_msg: "The bridge migrated its schema into the configured database"
  256. vars:
  257. mautrix_gmessages_table_names: "{{ mautrix_gmessages_tables.stdout_lines | select | list }}"
  258. - name: Read the labels the role rendered
  259. ansible.builtin.slurp:
  260. src: "{{ matrix_bridge_mautrix_gmessages_base_path }}/labels"
  261. register: mautrix_gmessages_labels_file
  262. - name: Assert the labels route the public endpoint to the appservice port
  263. ansible.builtin.assert:
  264. that:
  265. - "'traefik.enable=true' in mautrix_gmessages_labels_rendered"
  266. - "'traefik.docker.network=' + matrix_bridge_mautrix_gmessages_container_network in mautrix_gmessages_labels_rendered"
  267. - "'traefik.http.services.matrix-mautrix-gmessages-exposure.loadbalancer.server.port=8080' in mautrix_gmessages_labels_rendered"
  268. - "'traefik.http.routers.matrix-mautrix-gmessages-exposure.rule=Host(`gmessages-api.molecule.local`) && PathPrefix(`/bridges/gmessages-api`)' in mautrix_gmessages_labels_rendered"
  269. - "'traefik.http.middlewares.matrix-mautrix-gmessages-exposure-strip-prefix.stripprefix.prefixes=/bridges/gmessages-api' in mautrix_gmessages_labels_rendered"
  270. - "'traefik.http.routers.matrix-mautrix-gmessages-exposure.entrypoints=web' in mautrix_gmessages_labels_rendered"
  271. - "'traefik.http.routers.matrix-mautrix-gmessages-exposure.tls=false' in mautrix_gmessages_labels_rendered"
  272. - "'molecule.gmessages.coverage=enabled' in mautrix_gmessages_labels_rendered"
  273. fail_msg: "The rendered labels do not carry the scenario's exposure configuration"
  274. success_msg: "The rendered labels carry the scenario's exposure configuration"
  275. - name: Inspect the running bridge container
  276. ansible.builtin.command:
  277. argv:
  278. - docker
  279. - container
  280. - inspect
  281. - matrix-mautrix-gmessages
  282. register: mautrix_gmessages_container_inspect
  283. changed_when: false
  284. - name: Parse the running bridge container inspection
  285. ansible.builtin.set_fact:
  286. mautrix_gmessages_container: "{{ (mautrix_gmessages_container_inspect.stdout | from_json) | first }}"
  287. - name: Assert the running container uses the exact image pinned by the role
  288. ansible.builtin.assert:
  289. that:
  290. - mautrix_gmessages_container.Config.Image == mautrix_gmessages_expected_image
  291. fail_msg: >-
  292. The running container uses {{ mautrix_gmessages_container.Config.Image }}, expected
  293. {{ mautrix_gmessages_expected_image }}
  294. success_msg: "The running container uses the exact image pinned by the role"
  295. vars:
  296. mautrix_gmessages_expected_image: >-
  297. {{ mautrix_gmessages_role_defaults.matrix_bridge_mautrix_gmessages_container_image_registry_prefix_upstream_default }}mautrix/gmessages:{{ mautrix_gmessages_role_defaults.matrix_bridge_mautrix_gmessages_version }}
  298. - name: Assert the running container uses the playbook-supplied runtime
  299. ansible.builtin.assert:
  300. that:
  301. - mautrix_gmessages_container.Config.User == (matrix_user_uid | string) + ':' + (matrix_user_gid | string)
  302. - mautrix_gmessages_container.Config.WorkingDir == '/data'
  303. - mautrix_gmessages_container.Config.Cmd == ['/usr/bin/mautrix-gmessages', '-c', '/config/config.yaml', '-r', '/config/registration.yaml', '--no-update']
  304. - mautrix_gmessages_container.HostConfig.RestartPolicy.Name == 'no'
  305. - mautrix_gmessages_container.HostConfig.AutoRemove
  306. - mautrix_gmessages_container.HostConfig.LogConfig.Type == 'none'
  307. fail_msg: "The running container does not use the role's exact identity, command and workdir"
  308. success_msg: "The running container uses the role's exact identity, command and workdir"
  309. - name: Assert the running container has the intended privilege isolation
  310. ansible.builtin.assert:
  311. that:
  312. - "'ALL' in mautrix_gmessages_container.HostConfig.CapDrop"
  313. - not mautrix_gmessages_container.HostConfig.Privileged
  314. fail_msg: "The running container does not have the intended privilege isolation"
  315. success_msg: "The running container drops all capabilities and is unprivileged"
  316. - name: Assert the running container carries the role's exact bind mounts
  317. ansible.builtin.assert:
  318. that:
  319. - mautrix_gmessages_config_mount | length > 0
  320. - mautrix_gmessages_config_mount.Source == matrix_bridge_mautrix_gmessages_config_path
  321. - not mautrix_gmessages_config_mount.RW
  322. - mautrix_gmessages_data_mount | length > 0
  323. - mautrix_gmessages_data_mount.Source == matrix_bridge_mautrix_gmessages_data_path
  324. - mautrix_gmessages_data_mount.RW
  325. - mautrix_gmessages_container.Mounts | length == 2
  326. fail_msg: "The running container does not carry the role's exact config and data mounts"
  327. success_msg: "The running container carries read-only config and writable data mounts"
  328. vars:
  329. mautrix_gmessages_config_mount: >-
  330. {{ mautrix_gmessages_container.Mounts
  331. | selectattr('Destination', 'equalto', '/config')
  332. | first | default({}) }}
  333. mautrix_gmessages_data_mount: >-
  334. {{ mautrix_gmessages_container.Mounts
  335. | selectattr('Destination', 'equalto', '/data')
  336. | first | default({}) }}
  337. - name: Assert the rendered labels are attached to the running container
  338. ansible.builtin.assert:
  339. that:
  340. - mautrix_gmessages_container.Config.Labels['traefik.enable'] == 'true'
  341. - mautrix_gmessages_container.Config.Labels['traefik.docker.network'] == matrix_bridge_mautrix_gmessages_container_network
  342. - mautrix_gmessages_container.Config.Labels['traefik.http.services.matrix-mautrix-gmessages-exposure.loadbalancer.server.port'] == '8080'
  343. - mautrix_gmessages_container.Config.Labels['molecule.gmessages.coverage'] == 'enabled'
  344. fail_msg: "The running container does not carry the labels the role rendered"
  345. success_msg: "The running container carries the labels the role rendered"
  346. - name: Assert the running container has the exact network and port exposure
  347. ansible.builtin.assert:
  348. that:
  349. - mautrix_gmessages_container.HostConfig.NetworkMode == matrix_bridge_mautrix_gmessages_container_network
  350. - matrix_bridge_mautrix_gmessages_container_network in mautrix_gmessages_container.NetworkSettings.Networks
  351. - mautrix_gmessages_container.NetworkSettings.Networks | length == 1
  352. - mautrix_gmessages_container.HostConfig.PortBindings | default({}, true) | length == 0
  353. fail_msg: >-
  354. The container has unexpected networks or host ports:
  355. networks={{ mautrix_gmessages_container.NetworkSettings.Networks.keys() | list }},
  356. ports={{ mautrix_gmessages_container.HostConfig.PortBindings | default({}) }}
  357. success_msg: "The container uses only its dedicated network and publishes no host ports"