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

440 строки
22 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. ---
  5. # Maubot starts without a Matrix client configured. Its real management API and Postgres
  6. # migrations are the weight-bearing evidence, so this needs no homeserver or credentials.
  7. - name: Verify matrix-bot-maubot
  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. matrix_bot_maubot_config: "{{ matrix_bot_maubot_config_file.content | b64decode | from_yaml }}"
  15. matrix_bot_maubot_ui_body: "{{ matrix_bot_maubot_ui_http.stdout_lines[:-1] | join('\n') }}"
  16. matrix_bot_maubot_paths_body: "{{ matrix_bot_maubot_paths_http.stdout_lines[:-1] | join('\n') }}"
  17. matrix_bot_maubot_paths: "{{ matrix_bot_maubot_paths_body | from_json }}"
  18. matrix_bot_maubot_login_body: "{{ matrix_bot_maubot_login_http.stdout_lines[:-1] | join('\n') }}"
  19. matrix_bot_maubot_login_response: "{{ matrix_bot_maubot_login_body | from_json }}"
  20. matrix_bot_maubot_expected_database_uri: >-
  21. 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 }}
  22. matrix_bot_maubot_labels_lines: "{{ (matrix_bot_maubot_labels_file.content | b64decode).splitlines() }}"
  23. matrix_bot_maubot_runtime: "{{ (matrix_bot_maubot_container_inspect.stdout | from_json) | first }}"
  24. matrix_bot_maubot_config_mounts: "{{ matrix_bot_maubot_runtime.Mounts | selectattr('Destination', 'equalto', '/config') | list }}"
  25. matrix_bot_maubot_data_mounts: "{{ matrix_bot_maubot_runtime.Mounts | selectattr('Destination', 'equalto', '/data') | list }}"
  26. gather_facts: false
  27. tasks:
  28. # Read the version from the role itself, so a role pin bump changes what is expected.
  29. - name: Load the role's defaults under a separate name
  30. ansible.builtin.include_vars:
  31. file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
  32. name: matrix_bot_maubot_role_defaults
  33. - name: Wait for the matrix-bot-maubot service to become active
  34. ansible.builtin.systemd_service:
  35. name: matrix-bot-maubot.service
  36. register: matrix_bot_maubot_service
  37. until: matrix_bot_maubot_service.status.ActiveState == 'active'
  38. retries: 30
  39. delay: 5
  40. failed_when: false
  41. # Restart=always can hide a crash loop behind ActiveState=active.
  42. - name: Assert the service is active and has not restarted
  43. ansible.builtin.assert:
  44. that:
  45. - matrix_bot_maubot_service.status.ActiveState == 'active'
  46. - matrix_bot_maubot_service.status.NRestarts is defined
  47. - matrix_bot_maubot_service.status.NRestarts | int == 0
  48. fail_msg: >-
  49. matrix-bot-maubot.service is
  50. {{ matrix_bot_maubot_service.status.ActiveState | default('unknown') }} after
  51. {{ matrix_bot_maubot_service.status.NRestarts | default('?') }} restart(s)
  52. success_msg: "matrix-bot-maubot.service is active and has not restarted"
  53. - name: Read the configuration file the role rendered
  54. ansible.builtin.slurp:
  55. src: "{{ matrix_bot_maubot_config_path }}/config.yaml"
  56. register: matrix_bot_maubot_config_file
  57. - name: Assert the parsed server configuration carries the scenario's values
  58. ansible.builtin.assert:
  59. that:
  60. - matrix_bot_maubot_config.server.hostname == '0.0.0.0'
  61. - matrix_bot_maubot_config.server.port == matrix_bot_maubot_server_port
  62. - matrix_bot_maubot_config.server.public_url == 'https://maubot.molecule.local/molecule-maubot'
  63. - matrix_bot_maubot_config.server.ui_base_path == '/'
  64. - matrix_bot_maubot_config.server.plugin_base_path == '/plugin/'
  65. - "'base_path' not in matrix_bot_maubot_config.server"
  66. - "'appservice_base_path' not in matrix_bot_maubot_config.server"
  67. - matrix_bot_maubot_config.server.unshared_secret == matrix_bot_maubot_unshared_secret
  68. fail_msg: "The parsed server block does not carry the scenario's non-default values"
  69. success_msg: "The parsed server block carries the scenario's non-default values"
  70. - name: Assert the parsed database configuration carries the scenario's Postgres URI
  71. ansible.builtin.assert:
  72. that:
  73. - matrix_bot_maubot_config.database == matrix_bot_maubot_expected_database_uri
  74. - matrix_bot_maubot_config.database.startswith('postgres://')
  75. - matrix_bot_maubot_database_username in matrix_bot_maubot_config.database
  76. - matrix_bot_maubot_database_password in matrix_bot_maubot_config.database
  77. - matrix_bot_maubot_database_hostname in matrix_bot_maubot_config.database
  78. - matrix_bot_maubot_database_name in matrix_bot_maubot_config.database
  79. fail_msg: "The parsed configuration does not point at the scenario's Postgres database"
  80. success_msg: "The parsed configuration points at the scenario's Postgres database"
  81. - name: Assert the parsed homeserver preset carries the scenario's values
  82. ansible.builtin.assert:
  83. that:
  84. - matrix_bot_maubot_homeserver_name in matrix_bot_maubot_config.homeservers
  85. - matrix_bot_maubot_config.homeservers[matrix_bot_maubot_homeserver_name].url == matrix_bot_maubot_homeserver_url
  86. - matrix_bot_maubot_config.homeservers[matrix_bot_maubot_homeserver_name].secret == matrix_bot_maubot_homeserver_secret
  87. fail_msg: "The parsed homeserver preset does not carry the scenario's values"
  88. success_msg: "The parsed homeserver preset carries the scenario's values"
  89. - name: Assert the parsed management configuration carries scenario values
  90. ansible.builtin.assert:
  91. that:
  92. - matrix_bot_maubot_config.admins['molecule-admin'] == 'molecule_admin_password_61bd93'
  93. - matrix_bot_maubot_config.admins.root == ''
  94. - matrix_bot_maubot_config.api_features.values() | select('equalto', true) | list | length == matrix_bot_maubot_config.api_features | length
  95. fail_msg: "The parsed management configuration lost the admin or API features"
  96. success_msg: "The parsed management configuration carries the admin and API features"
  97. - name: Assert the parsed logging configuration carries the scenario's level
  98. ansible.builtin.assert:
  99. that:
  100. - matrix_bot_maubot_config.logging.loggers.maubot.level == matrix_bot_maubot_logging_level
  101. - matrix_bot_maubot_config.logging.loggers.mau.level == matrix_bot_maubot_logging_level
  102. - matrix_bot_maubot_config.logging.loggers.aiohttp.level == matrix_bot_maubot_logging_level
  103. - matrix_bot_maubot_config.logging.root.level == matrix_bot_maubot_logging_level
  104. fail_msg: "The parsed logging configuration does not carry the scenario's level"
  105. success_msg: "The parsed logging configuration carries the scenario's level"
  106. # Traefik strips the configured public prefix before proxying, so the role deliberately
  107. # configures the process UI at /. Probe that exact internal contract over the network.
  108. - name: Wait for Maubot's management UI on the configured internal port and path
  109. ansible.builtin.command:
  110. argv:
  111. - docker
  112. - run
  113. - --rm
  114. - --network={{ matrix_bot_maubot_container_network }}
  115. - "{{ molecule_shared_image_curl }}"
  116. - --silent
  117. - --show-error
  118. - --write-out
  119. - "\nHTTP_STATUS=%{http_code}"
  120. - "http://matrix-bot-maubot:{{ matrix_bot_maubot_server_port }}/"
  121. register: matrix_bot_maubot_ui_http
  122. changed_when: false
  123. until: "'HTTP_STATUS=200' in matrix_bot_maubot_ui_http.stdout"
  124. retries: 24
  125. delay: 5
  126. failed_when: false
  127. - name: Assert Maubot serves its real management UI at the configured internal address
  128. ansible.builtin.assert:
  129. that:
  130. - matrix_bot_maubot_ui_http.rc == 0
  131. - matrix_bot_maubot_ui_http.stdout_lines[-1] == 'HTTP_STATUS=200'
  132. - matrix_bot_maubot_ui_body | trim | length > 100
  133. - "'<!doctype html' in matrix_bot_maubot_ui_body | lower"
  134. - "'maubot' in matrix_bot_maubot_ui_body | lower"
  135. fail_msg: "Maubot did not serve its management UI on the configured port at /"
  136. success_msg: "Maubot serves its real management UI on the configured port at /"
  137. # Maubot derives this browser-facing API location from server.public_url. It is live
  138. # process evidence that the public prefix reached Maubot and that the frontend will send
  139. # requests through the Traefik prefix which the role's labels strip below.
  140. - name: Fetch Maubot's live frontend path metadata
  141. ansible.builtin.command:
  142. argv:
  143. - docker
  144. - run
  145. - --rm
  146. - --network={{ matrix_bot_maubot_container_network }}
  147. - "{{ molecule_shared_image_curl }}"
  148. - --silent
  149. - --show-error
  150. - --write-out
  151. - "\nHTTP_STATUS=%{http_code}"
  152. - "http://matrix-bot-maubot:{{ matrix_bot_maubot_server_port }}/paths.json"
  153. register: matrix_bot_maubot_paths_http
  154. changed_when: false
  155. failed_when: false
  156. - name: Assert Maubot advertises the API under the configured public prefix
  157. ansible.builtin.assert:
  158. that:
  159. - matrix_bot_maubot_paths_http.rc == 0
  160. - matrix_bot_maubot_paths_http.stdout_lines[-1] == 'HTTP_STATUS=200'
  161. - matrix_bot_maubot_paths is mapping
  162. - matrix_bot_maubot_paths.api_path == '/molecule-maubot/_matrix/maubot/v1'
  163. fail_msg: >-
  164. Maubot did not derive the prefixed browser API path from server.public_url
  165. ({{ matrix_bot_maubot_paths_http.stdout | default('no output') }})
  166. success_msg: "Maubot advertises its browser API under the configured public prefix"
  167. # This authenticates against the live management API with the non-default admin from
  168. # the rendered YAML. It proves both the API base path and account reached the process.
  169. - name: Log in to Maubot's management API
  170. ansible.builtin.command:
  171. argv:
  172. - docker
  173. - run
  174. - --rm
  175. - --network={{ matrix_bot_maubot_container_network }}
  176. - "{{ molecule_shared_image_curl }}"
  177. - --silent
  178. - --show-error
  179. - --header
  180. - 'Content-Type: application/json'
  181. - --data
  182. - '{"username":"molecule-admin","password":"molecule_admin_password_61bd93"}'
  183. - --write-out
  184. - "\nHTTP_STATUS=%{http_code}"
  185. - "http://matrix-bot-maubot:{{ matrix_bot_maubot_server_port }}/_matrix/maubot/v1/auth/login"
  186. register: matrix_bot_maubot_login_http
  187. changed_when: false
  188. failed_when: false
  189. - name: Assert the management API authenticated the configured admin
  190. ansible.builtin.assert:
  191. that:
  192. - matrix_bot_maubot_login_http.rc == 0
  193. - matrix_bot_maubot_login_http.stdout_lines[-1] == 'HTTP_STATUS=200'
  194. - matrix_bot_maubot_login_response is mapping
  195. - matrix_bot_maubot_login_response.token is string
  196. - matrix_bot_maubot_login_response.token | length > 20
  197. fail_msg: >-
  198. Maubot's live management API rejected the admin the role configured
  199. ({{ matrix_bot_maubot_login_http.stdout | default('no output') }})
  200. success_msg: "Maubot's live management API authenticated the configured admin"
  201. # Traefik strips the public prefix, so plugin webhooks reach Maubot at the relative
  202. # plugin_base_path. Maubot's plugin router answers unknown instances with an empty 404,
  203. # while aiohttp's default 404 carries a body, so the empty body proves the router is mounted.
  204. - name: Probe Maubot's plugin endpoint router
  205. ansible.builtin.command:
  206. argv:
  207. - docker
  208. - run
  209. - --rm
  210. - --network={{ matrix_bot_maubot_container_network }}
  211. - "{{ molecule_shared_image_curl }}"
  212. - --silent
  213. - --show-error
  214. - --write-out
  215. - "\nHTTP_STATUS=%{http_code}"
  216. - "http://matrix-bot-maubot:{{ matrix_bot_maubot_server_port }}/plugin/molecule-probe"
  217. register: matrix_bot_maubot_plugin_http
  218. changed_when: false
  219. failed_when: false
  220. - name: Assert Maubot's plugin router handles the configured plugin base path
  221. ansible.builtin.assert:
  222. that:
  223. - matrix_bot_maubot_plugin_http.rc == 0
  224. - matrix_bot_maubot_plugin_http.stdout_lines[-1] == 'HTTP_STATUS=404'
  225. - matrix_bot_maubot_plugin_http.stdout_lines[:-1] | join('\n') | trim | length == 0
  226. fail_msg: >-
  227. Maubot's plugin router did not answer on the configured plugin base path
  228. ({{ matrix_bot_maubot_plugin_http.stdout | default('no output') }})
  229. success_msg: "Maubot's plugin router answers on the configured plugin base path"
  230. # The role's default port must not answer. Otherwise reaching the custom port would not
  231. # prove that the configured value made it into the running process.
  232. - name: Probe the role's default Maubot port
  233. ansible.builtin.command:
  234. argv:
  235. - docker
  236. - run
  237. - --rm
  238. - --network={{ matrix_bot_maubot_container_network }}
  239. - "{{ molecule_shared_image_curl }}"
  240. - --silent
  241. - --show-error
  242. - --max-time
  243. - '3'
  244. - --output
  245. - /dev/null
  246. - --write-out
  247. - "HTTP_STATUS=%{http_code}"
  248. - http://matrix-bot-maubot:29316/
  249. register: matrix_bot_maubot_default_port_http
  250. changed_when: false
  251. failed_when: false
  252. - name: Assert Maubot is not listening on the role's default port
  253. ansible.builtin.assert:
  254. that:
  255. - matrix_bot_maubot_default_port_http.rc != 0
  256. - "'HTTP_STATUS=200' not in matrix_bot_maubot_default_port_http.stdout"
  257. fail_msg: "Maubot still answers on 29316, so the configured port is not proven"
  258. success_msg: "Maubot does not listen on the role's default port"
  259. # Tables can only appear after hostname resolution, authentication and migrations.
  260. - name: List the tables Maubot created in Postgres
  261. ansible.builtin.command:
  262. argv:
  263. - docker
  264. - exec
  265. - matrix-postgres-molecule
  266. - psql
  267. - --username={{ matrix_bot_maubot_database_username }}
  268. - --dbname={{ matrix_bot_maubot_database_name }}
  269. - --tuples-only
  270. - --no-align
  271. - --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public'
  272. register: matrix_bot_maubot_tables
  273. changed_when: false
  274. - name: Assert Maubot migrated its schema into the configured Postgres database
  275. ansible.builtin.assert:
  276. that:
  277. - matrix_bot_maubot_tables.rc == 0
  278. - "'version' in matrix_bot_maubot_table_names"
  279. - "'client' in matrix_bot_maubot_table_names"
  280. - "'instance' in matrix_bot_maubot_table_names"
  281. - "'database_owner' in matrix_bot_maubot_table_names"
  282. - matrix_bot_maubot_table_names | length > 5
  283. fail_msg: >-
  284. Maubot did not create its schema in {{ matrix_bot_maubot_database_name }}
  285. (found {{ matrix_bot_maubot_table_names | length }} table(s))
  286. success_msg: "Maubot migrated its schema into the configured Postgres database"
  287. vars:
  288. matrix_bot_maubot_table_names: "{{ matrix_bot_maubot_tables.stdout_lines | select | list }}"
  289. - name: Read the labels the role rendered
  290. ansible.builtin.slurp:
  291. src: "{{ matrix_bot_maubot_base_path }}/labels"
  292. register: matrix_bot_maubot_labels_file
  293. - name: Assert the labels carry the configured management exposure
  294. ansible.builtin.assert:
  295. that:
  296. - "'traefik.enable=true' in matrix_bot_maubot_labels_lines"
  297. - "'traefik.docker.network=matrix-bot-maubot-molecule' in matrix_bot_maubot_labels_lines"
  298. - "'traefik.http.services.matrix-bot-maubot.loadbalancer.server.port=29427' in matrix_bot_maubot_labels_lines"
  299. - "'traefik.http.routers.matrix-bot-maubot-management.rule=Host(`maubot.molecule.local`) && PathPrefix(`/molecule-maubot`)' in matrix_bot_maubot_labels_lines"
  300. - "'traefik.http.routers.matrix-bot-maubot-management.priority=719' in matrix_bot_maubot_labels_lines"
  301. - "'traefik.http.middlewares.matrix-bot-maubot-management-slashless-redirect.redirectregex.regex=(/molecule-maubot)$' in matrix_bot_maubot_labels_lines"
  302. - "'traefik.http.middlewares.matrix-bot-maubot-management-slashless-redirect.redirectregex.replacement=${1}/' in matrix_bot_maubot_labels_lines"
  303. - "'traefik.http.middlewares.matrix-bot-maubot-management-strip-prefix.stripprefix.prefixes=/molecule-maubot' in matrix_bot_maubot_labels_lines"
  304. - "'traefik.http.routers.matrix-bot-maubot-management.middlewares=matrix-bot-maubot-management-slashless-redirect,matrix-bot-maubot-management-strip-prefix' in matrix_bot_maubot_labels_lines"
  305. - "'traefik.http.routers.matrix-bot-maubot-management.entrypoints=web' in matrix_bot_maubot_labels_lines"
  306. - "'traefik.http.routers.matrix-bot-maubot-management.tls=false' in matrix_bot_maubot_labels_lines"
  307. - "'molecule.maubot.coverage=enabled' in matrix_bot_maubot_labels_lines"
  308. fail_msg: "The role's labels do not carry the scenario's public management wiring"
  309. success_msg: "The role's labels carry the scenario's public management wiring"
  310. - name: Inspect the running Maubot container
  311. ansible.builtin.command:
  312. argv:
  313. - docker
  314. - container
  315. - inspect
  316. - matrix-bot-maubot
  317. register: matrix_bot_maubot_container_inspect
  318. changed_when: false
  319. - name: Assert the running container uses the exact image pinned by the role
  320. ansible.builtin.assert:
  321. that:
  322. - matrix_bot_maubot_runtime.Config.Image == 'dock.mau.dev/maubot/maubot:' + matrix_bot_maubot_role_defaults.matrix_bot_maubot_version
  323. fail_msg: >-
  324. The running image {{ matrix_bot_maubot_runtime.Config.Image }} is not the exact
  325. dock.mau.dev/maubot/maubot:{{ matrix_bot_maubot_role_defaults.matrix_bot_maubot_version }} pin
  326. success_msg: "The running container uses the exact image pinned by the role"
  327. - name: Assert the running container uses the playbook's matrix identity
  328. ansible.builtin.assert:
  329. that:
  330. - matrix_bot_maubot_runtime.Config.User.split(':')[0] == matrix_user_uid | string
  331. - matrix_bot_maubot_runtime.Config.User.split(':')[1] == matrix_user_gid | string
  332. fail_msg: >-
  333. Maubot runs as {{ matrix_bot_maubot_runtime.Config.User }} instead of
  334. {{ matrix_user_uid }}:{{ matrix_user_gid }}
  335. success_msg: "The running container uses the playbook's matrix UID and GID"
  336. - name: Assert the container root filesystem is read-only
  337. ansible.builtin.assert:
  338. that:
  339. - matrix_bot_maubot_runtime.HostConfig.ReadonlyRootfs is sameas true
  340. fail_msg: "The Maubot container root filesystem is writable"
  341. success_msg: "The Maubot container root filesystem is read-only"
  342. - name: Assert all Linux capabilities are dropped
  343. ansible.builtin.assert:
  344. that:
  345. - matrix_bot_maubot_runtime.HostConfig.CapDrop == ['ALL']
  346. fail_msg: "The Maubot container does not drop all Linux capabilities"
  347. success_msg: "The Maubot container drops all Linux capabilities"
  348. - name: Assert the container has the role's writable temporary filesystem
  349. ansible.builtin.assert:
  350. that:
  351. - matrix_bot_maubot_runtime.HostConfig.Tmpfs['/tmp'] == 'rw,noexec,nosuid,size=1024m'
  352. fail_msg: "The Maubot container is missing its constrained /tmp tmpfs"
  353. success_msg: "The Maubot container has its constrained writable /tmp tmpfs"
  354. - name: Assert the configuration bind mount is read-only
  355. ansible.builtin.assert:
  356. that:
  357. - matrix_bot_maubot_config_mounts | length == 1
  358. - matrix_bot_maubot_config_mounts[0].RW is sameas false
  359. fail_msg: "The Maubot configuration bind mount is missing or writable"
  360. success_msg: "The Maubot configuration bind mount is present and read-only"
  361. - name: Assert the data bind mount is writable
  362. ansible.builtin.assert:
  363. that:
  364. - matrix_bot_maubot_data_mounts | length == 1
  365. - matrix_bot_maubot_data_mounts[0].RW is sameas true
  366. fail_msg: "The Maubot data bind mount is missing or read-only"
  367. success_msg: "The Maubot data bind mount is present and writable"
  368. - name: Assert the container is attached only to its dedicated network
  369. ansible.builtin.assert:
  370. that:
  371. - matrix_bot_maubot_runtime.NetworkSettings.Networks is mapping
  372. - matrix_bot_maubot_runtime.NetworkSettings.Networks | length == 1
  373. - matrix_bot_maubot_container_network in matrix_bot_maubot_runtime.NetworkSettings.Networks
  374. fail_msg: >-
  375. Maubot has unexpected networks:
  376. {{ matrix_bot_maubot_runtime.NetworkSettings.Networks.keys() | list }}
  377. success_msg: "The Maubot container is attached only to its dedicated network"
  378. - name: Ask Docker for Maubot's published ports
  379. ansible.builtin.command:
  380. argv:
  381. - docker
  382. - container
  383. - port
  384. - matrix-bot-maubot
  385. register: matrix_bot_maubot_published_ports
  386. changed_when: false
  387. failed_when: false
  388. - name: Assert the role did not publish a host port
  389. ansible.builtin.assert:
  390. that:
  391. - matrix_bot_maubot_runtime.HostConfig.PortBindings | default({}, true) | length == 0
  392. - matrix_bot_maubot_published_ports.rc == 0
  393. - matrix_bot_maubot_published_ports.stdout | trim | length == 0
  394. fail_msg: >-
  395. Maubot unexpectedly publishes a host port:
  396. {{ matrix_bot_maubot_published_ports.stdout | default('unknown') }}
  397. success_msg: "The role leaves Maubot's management port unpublished"
  398. - name: Assert Docker accepted the role's custom label
  399. ansible.builtin.assert:
  400. that:
  401. - matrix_bot_maubot_runtime.Config.Labels is mapping
  402. - matrix_bot_maubot_runtime.Config.Labels['molecule.maubot.coverage'] == 'enabled'
  403. fail_msg: "Docker did not attach the custom label from the role's label file"
  404. success_msg: "Docker accepted the custom label from the role's label file"