Matrix Docker Ansible eploy
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 

407 satır
20 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 == '/_matrix/maubot/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. # The role's default port must not answer. Otherwise reaching the custom port would not
  202. # prove that the configured value made it into the running process.
  203. - name: Probe the role's default Maubot port
  204. ansible.builtin.command:
  205. argv:
  206. - docker
  207. - run
  208. - --rm
  209. - --network={{ matrix_bot_maubot_container_network }}
  210. - "{{ molecule_shared_image_curl }}"
  211. - --silent
  212. - --show-error
  213. - --max-time
  214. - '3'
  215. - --output
  216. - /dev/null
  217. - --write-out
  218. - "HTTP_STATUS=%{http_code}"
  219. - http://matrix-bot-maubot:29316/
  220. register: matrix_bot_maubot_default_port_http
  221. changed_when: false
  222. failed_when: false
  223. - name: Assert Maubot is not listening on the role's default port
  224. ansible.builtin.assert:
  225. that:
  226. - matrix_bot_maubot_default_port_http.rc != 0
  227. - "'HTTP_STATUS=200' not in matrix_bot_maubot_default_port_http.stdout"
  228. fail_msg: "Maubot still answers on 29316, so the configured port is not proven"
  229. success_msg: "Maubot does not listen on the role's default port"
  230. # Tables can only appear after hostname resolution, authentication and migrations.
  231. - name: List the tables Maubot created in Postgres
  232. ansible.builtin.command:
  233. argv:
  234. - docker
  235. - exec
  236. - matrix-postgres-molecule
  237. - psql
  238. - --username={{ matrix_bot_maubot_database_username }}
  239. - --dbname={{ matrix_bot_maubot_database_name }}
  240. - --tuples-only
  241. - --no-align
  242. - --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public'
  243. register: matrix_bot_maubot_tables
  244. changed_when: false
  245. - name: Assert Maubot migrated its schema into the configured Postgres database
  246. ansible.builtin.assert:
  247. that:
  248. - matrix_bot_maubot_tables.rc == 0
  249. - "'version' in matrix_bot_maubot_table_names"
  250. - "'client' in matrix_bot_maubot_table_names"
  251. - "'instance' in matrix_bot_maubot_table_names"
  252. - "'database_owner' in matrix_bot_maubot_table_names"
  253. - matrix_bot_maubot_table_names | length > 5
  254. fail_msg: >-
  255. Maubot did not create its schema in {{ matrix_bot_maubot_database_name }}
  256. (found {{ matrix_bot_maubot_table_names | length }} table(s))
  257. success_msg: "Maubot migrated its schema into the configured Postgres database"
  258. vars:
  259. matrix_bot_maubot_table_names: "{{ matrix_bot_maubot_tables.stdout_lines | select | list }}"
  260. - name: Read the labels the role rendered
  261. ansible.builtin.slurp:
  262. src: "{{ matrix_bot_maubot_base_path }}/labels"
  263. register: matrix_bot_maubot_labels_file
  264. - name: Assert the labels carry the configured management exposure
  265. ansible.builtin.assert:
  266. that:
  267. - "'traefik.enable=true' in matrix_bot_maubot_labels_lines"
  268. - "'traefik.docker.network=matrix-bot-maubot-molecule' in matrix_bot_maubot_labels_lines"
  269. - "'traefik.http.services.matrix-bot-maubot.loadbalancer.server.port=29427' in matrix_bot_maubot_labels_lines"
  270. - "'traefik.http.routers.matrix-bot-maubot-management.rule=Host(`maubot.molecule.local`) && PathPrefix(`/molecule-maubot`)' in matrix_bot_maubot_labels_lines"
  271. - "'traefik.http.routers.matrix-bot-maubot-management.priority=719' in matrix_bot_maubot_labels_lines"
  272. - "'traefik.http.middlewares.matrix-bot-maubot-management-strip-prefix.stripprefix.prefixes=/molecule-maubot' in matrix_bot_maubot_labels_lines"
  273. - "'traefik.http.routers.matrix-bot-maubot-management.middlewares=matrix-bot-maubot-management-strip-prefix' in matrix_bot_maubot_labels_lines"
  274. - "'traefik.http.routers.matrix-bot-maubot-management.entrypoints=web' in matrix_bot_maubot_labels_lines"
  275. - "'traefik.http.routers.matrix-bot-maubot-management.tls=false' in matrix_bot_maubot_labels_lines"
  276. - "'molecule.maubot.coverage=enabled' in matrix_bot_maubot_labels_lines"
  277. fail_msg: "The role's labels do not carry the scenario's public management wiring"
  278. success_msg: "The role's labels carry the scenario's public management wiring"
  279. - name: Inspect the running Maubot container
  280. ansible.builtin.command:
  281. argv:
  282. - docker
  283. - container
  284. - inspect
  285. - matrix-bot-maubot
  286. register: matrix_bot_maubot_container_inspect
  287. changed_when: false
  288. - name: Assert the running container uses the exact image pinned by the role
  289. ansible.builtin.assert:
  290. that:
  291. - matrix_bot_maubot_runtime.Config.Image == 'dock.mau.dev/maubot/maubot:' + matrix_bot_maubot_role_defaults.matrix_bot_maubot_version
  292. fail_msg: >-
  293. The running image {{ matrix_bot_maubot_runtime.Config.Image }} is not the exact
  294. dock.mau.dev/maubot/maubot:{{ matrix_bot_maubot_role_defaults.matrix_bot_maubot_version }} pin
  295. success_msg: "The running container uses the exact image pinned by the role"
  296. - name: Assert the running container uses the playbook's matrix identity
  297. ansible.builtin.assert:
  298. that:
  299. - matrix_bot_maubot_runtime.Config.User.split(':')[0] == matrix_user_uid | string
  300. - matrix_bot_maubot_runtime.Config.User.split(':')[1] == matrix_user_gid | string
  301. fail_msg: >-
  302. Maubot runs as {{ matrix_bot_maubot_runtime.Config.User }} instead of
  303. {{ matrix_user_uid }}:{{ matrix_user_gid }}
  304. success_msg: "The running container uses the playbook's matrix UID and GID"
  305. - name: Assert the container root filesystem is read-only
  306. ansible.builtin.assert:
  307. that:
  308. - matrix_bot_maubot_runtime.HostConfig.ReadonlyRootfs is sameas true
  309. fail_msg: "The Maubot container root filesystem is writable"
  310. success_msg: "The Maubot container root filesystem is read-only"
  311. - name: Assert all Linux capabilities are dropped
  312. ansible.builtin.assert:
  313. that:
  314. - matrix_bot_maubot_runtime.HostConfig.CapDrop == ['ALL']
  315. fail_msg: "The Maubot container does not drop all Linux capabilities"
  316. success_msg: "The Maubot container drops all Linux capabilities"
  317. - name: Assert the container has the role's writable temporary filesystem
  318. ansible.builtin.assert:
  319. that:
  320. - matrix_bot_maubot_runtime.HostConfig.Tmpfs['/tmp'] == 'rw,noexec,nosuid,size=1024m'
  321. fail_msg: "The Maubot container is missing its constrained /tmp tmpfs"
  322. success_msg: "The Maubot container has its constrained writable /tmp tmpfs"
  323. - name: Assert the configuration bind mount is read-only
  324. ansible.builtin.assert:
  325. that:
  326. - matrix_bot_maubot_config_mounts | length == 1
  327. - matrix_bot_maubot_config_mounts[0].RW is sameas false
  328. fail_msg: "The Maubot configuration bind mount is missing or writable"
  329. success_msg: "The Maubot configuration bind mount is present and read-only"
  330. - name: Assert the data bind mount is writable
  331. ansible.builtin.assert:
  332. that:
  333. - matrix_bot_maubot_data_mounts | length == 1
  334. - matrix_bot_maubot_data_mounts[0].RW is sameas true
  335. fail_msg: "The Maubot data bind mount is missing or read-only"
  336. success_msg: "The Maubot data bind mount is present and writable"
  337. - name: Assert the container is attached only to its dedicated network
  338. ansible.builtin.assert:
  339. that:
  340. - matrix_bot_maubot_runtime.NetworkSettings.Networks is mapping
  341. - matrix_bot_maubot_runtime.NetworkSettings.Networks | length == 1
  342. - matrix_bot_maubot_container_network in matrix_bot_maubot_runtime.NetworkSettings.Networks
  343. fail_msg: >-
  344. Maubot has unexpected networks:
  345. {{ matrix_bot_maubot_runtime.NetworkSettings.Networks.keys() | list }}
  346. success_msg: "The Maubot container is attached only to its dedicated network"
  347. - name: Ask Docker for Maubot's published ports
  348. ansible.builtin.command:
  349. argv:
  350. - docker
  351. - container
  352. - port
  353. - matrix-bot-maubot
  354. register: matrix_bot_maubot_published_ports
  355. changed_when: false
  356. failed_when: false
  357. - name: Assert the role did not publish a host port
  358. ansible.builtin.assert:
  359. that:
  360. - matrix_bot_maubot_runtime.HostConfig.PortBindings | default({}, true) | length == 0
  361. - matrix_bot_maubot_published_ports.rc == 0
  362. - matrix_bot_maubot_published_ports.stdout | trim | length == 0
  363. fail_msg: >-
  364. Maubot unexpectedly publishes a host port:
  365. {{ matrix_bot_maubot_published_ports.stdout | default('unknown') }}
  366. success_msg: "The role leaves Maubot's management port unpublished"
  367. - name: Assert Docker accepted the role's custom label
  368. ansible.builtin.assert:
  369. that:
  370. - matrix_bot_maubot_runtime.Config.Labels is mapping
  371. - matrix_bot_maubot_runtime.Config.Labels['molecule.maubot.coverage'] == 'enabled'
  372. fail_msg: "Docker did not attach the custom label from the role's label file"
  373. success_msg: "Docker accepted the custom label from the role's label file"