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

470 строки
24 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. ---
  5. - name: Verify matrix-static-files
  6. hosts: all
  7. become: true
  8. vars_files:
  9. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
  10. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
  11. gather_facts: false
  12. tasks:
  13. # Load the shipped version independently of the scenario. A role version bump
  14. # therefore advances the expected image instead of comparing two test pins.
  15. - name: Load the role's defaults under a separate name
  16. ansible.builtin.include_vars:
  17. file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
  18. name: matrix_static_files_role_defaults
  19. - name: Wait for the matrix-static-files service to become active
  20. ansible.builtin.systemd_service:
  21. name: matrix-static-files.service
  22. register: matrix_static_files_service
  23. until: matrix_static_files_service.status.ActiveState == 'active'
  24. retries: 30
  25. delay: 5
  26. failed_when: false
  27. # Restart=always means ActiveState alone stays green during a crash loop.
  28. - name: Assert the service is active and has not restarted
  29. ansible.builtin.assert:
  30. that:
  31. - matrix_static_files_service.status.ActiveState == 'active'
  32. - matrix_static_files_service.status.NRestarts is defined
  33. - matrix_static_files_service.status.NRestarts | int == 0
  34. fail_msg: >-
  35. matrix-static-files.service is
  36. {{ matrix_static_files_service.status.ActiveState | default('unknown') }}
  37. after {{ matrix_static_files_service.status.NRestarts | default('?') }} restart(s)
  38. success_msg: "matrix-static-files.service is active and has not restarted"
  39. # Probe over the private container network, matching how Traefik reaches the
  40. # service in a deployment. Every document is generated by a different role path.
  41. - name: Fetch the generated JSON documents from the running service
  42. ansible.builtin.command:
  43. argv:
  44. - docker
  45. - run
  46. - --rm
  47. - --network={{ matrix_static_files_container_network }}
  48. - "{{ molecule_shared_image_curl }}"
  49. - --silent
  50. - --show-error
  51. - --write-out
  52. - "\nMOLECULE_HTTP_STATUS=%{http_code}"
  53. - "http://matrix-static-files:{{ matrix_static_files_environment_variable_server_port }}{{ item.path }}"
  54. loop:
  55. - name: client
  56. path: /.well-known/matrix/client
  57. - name: server
  58. path: /.well-known/matrix/server
  59. - name: support
  60. path: /.well-known/matrix/support
  61. - name: mautrix
  62. path: /.well-known/matrix/mautrix
  63. register: matrix_static_files_document_responses
  64. changed_when: false
  65. until: "'MOLECULE_HTTP_STATUS=200' in matrix_static_files_document_responses.stdout"
  66. retries: 24
  67. delay: 5
  68. failed_when: false
  69. - name: Assert every enabled generated document is served successfully
  70. ansible.builtin.assert:
  71. that:
  72. - "'MOLECULE_HTTP_STATUS=200' in item.stdout"
  73. fail_msg: >-
  74. {{ item.item.path }} was not served successfully
  75. ({{ item.stdout | default('no output') }})
  76. success_msg: "{{ item.item.path }} is served successfully"
  77. loop: "{{ matrix_static_files_document_responses.results }}"
  78. loop_control:
  79. label: "{{ item.item.name }}"
  80. - name: Initialize parsed served documents
  81. ansible.builtin.set_fact:
  82. matrix_static_files_served_documents: {}
  83. - name: Parse the served JSON documents
  84. ansible.builtin.set_fact:
  85. matrix_static_files_served_documents: >-
  86. {{
  87. matrix_static_files_served_documents
  88. | combine({item.item.name: item.stdout | regex_replace('\nMOLECULE_HTTP_STATUS=[0-9]+\s*$', '') | from_json})
  89. }}
  90. loop: "{{ matrix_static_files_document_responses.results }}"
  91. loop_control:
  92. label: "{{ item.item.name }}"
  93. # The default server port must be closed. Otherwise a response on the scenario's
  94. # port would not prove SERVER_PORT reached the process.
  95. - name: Probe static-web-server's default port
  96. ansible.builtin.command:
  97. argv:
  98. - docker
  99. - run
  100. - --rm
  101. - --network={{ matrix_static_files_container_network }}
  102. - "{{ molecule_shared_image_curl }}"
  103. - --silent
  104. - --output
  105. - /dev/null
  106. - --write-out
  107. - "MOLECULE_HTTP_STATUS=%{http_code}"
  108. - --connect-timeout
  109. - "2"
  110. - http://matrix-static-files:8080/.well-known/matrix/client
  111. register: matrix_static_files_default_port_response
  112. changed_when: false
  113. failed_when: false
  114. - name: Assert static-web-server's default port is not listening
  115. ansible.builtin.assert:
  116. that:
  117. - "'MOLECULE_HTTP_STATUS=200' not in matrix_static_files_default_port_response.stdout"
  118. fail_msg: >-
  119. Port 8080 answered, so the live probe does not prove the non-default
  120. SERVER_PORT reached static-web-server
  121. success_msg: "Only the configured non-default internal port answers"
  122. - name: Read the generated JSON files from disk
  123. ansible.builtin.slurp:
  124. src: "{{ matrix_static_files_public_path }}/.well-known/matrix/{{ item }}"
  125. loop:
  126. - client
  127. - server
  128. - support
  129. - mautrix
  130. register: matrix_static_files_rendered_document_files
  131. - name: Initialize parsed rendered documents
  132. ansible.builtin.set_fact:
  133. matrix_static_files_rendered_documents: {}
  134. - name: Parse the generated JSON files
  135. ansible.builtin.set_fact:
  136. matrix_static_files_rendered_documents: >-
  137. {{
  138. matrix_static_files_rendered_documents
  139. | combine({item.item: item.content | b64decode | from_json})
  140. }}
  141. loop: "{{ matrix_static_files_rendered_document_files.results }}"
  142. loop_control:
  143. label: "{{ item.item }}"
  144. - name: Assert the running service exposes the exact documents the role generated
  145. ansible.builtin.assert:
  146. that:
  147. - matrix_static_files_served_documents[item] == matrix_static_files_rendered_documents[item]
  148. fail_msg: "The served {{ item }} document differs from the generated file"
  149. success_msg: "The served {{ item }} document is the generated file"
  150. loop:
  151. - client
  152. - server
  153. - support
  154. - mautrix
  155. - name: Assert the client document carries all configured discovery sections
  156. ansible.builtin.assert:
  157. that:
  158. - matrix_static_files_served_documents.client['m.homeserver'].base_url == 'https://homeserver.molecule.invalid'
  159. - matrix_static_files_served_documents.client['m.integrations'].managers[0].api_url == 'https://integrations.molecule.invalid/api'
  160. - matrix_static_files_served_documents.client['m.integrations'].managers[0].ui_url == 'https://integrations.molecule.invalid/ui'
  161. - matrix_static_files_served_documents.client['io.element.jitsi'].preferredDomain == 'jitsi.molecule.local'
  162. - matrix_static_files_served_documents.client['org.matrix.msc2965.authentication'].issuer == 'https://auth.molecule.invalid/'
  163. - matrix_static_files_served_documents.client['org.matrix.msc2965.authentication'].account == 'https://auth.molecule.invalid/account'
  164. - matrix_static_files_served_documents.client['org.matrix.msc3575.proxy'].url == 'https://sync.molecule.invalid'
  165. - matrix_static_files_served_documents.client['m.tile_server'].map_style_url == 'https://maps.molecule.invalid/style.json'
  166. - matrix_static_files_served_documents.client['io.element.e2ee'].default == false
  167. - matrix_static_files_served_documents.client['io.element.e2ee'].force_disable == true
  168. - matrix_static_files_served_documents.client['cc.etke.ketesa'].marker == 'ketesa-custom'
  169. - matrix_static_files_served_documents.client['org.matrix.msc4143.rtc_foci'][0].livekit_service_url == 'https://livekit.molecule.invalid'
  170. fail_msg: "The client document does not carry the configured discovery sections"
  171. success_msg: "The client document carries the configured discovery sections"
  172. - name: Assert recursive extension and omission behavior in the client document
  173. ansible.builtin.assert:
  174. that:
  175. - matrix_static_files_served_documents.client['m.homeserver'].molecule_extension_marker == 'recursive-merge-reached'
  176. - matrix_static_files_served_documents.client['org.example.molecule'].enabled == true
  177. - matrix_static_files_served_documents.client['org.example.molecule'].sequence == 42
  178. - "'m.identity_server' not in matrix_static_files_served_documents.client"
  179. fail_msg: "The client extension was not merged recursively or an omitted section leaked in"
  180. success_msg: "The client extension is merged recursively and disabled identity discovery is omitted"
  181. - name: Assert the server, support and mautrix documents carry configured data and extensions
  182. ansible.builtin.assert:
  183. that:
  184. - matrix_static_files_served_documents.server['m.server'] == 'federation.molecule.local:9448'
  185. - matrix_static_files_served_documents.server['org.example.molecule'] == 'server-extension-reached'
  186. - matrix_static_files_served_documents.support.contacts[0].matrix_id == '@support:molecule.local'
  187. - matrix_static_files_served_documents.support.contacts[0].role == 'm.role.admin'
  188. - matrix_static_files_served_documents.support.support_page == 'https://support.molecule.invalid'
  189. - matrix_static_files_served_documents.support['org.example.molecule'].channel == 'support-extension-reached'
  190. - matrix_static_files_served_documents.mautrix['fi.mau.bridges'] | length == 2
  191. - "'https://telegram.molecule.invalid' in matrix_static_files_served_documents.mautrix['fi.mau.bridges']"
  192. - matrix_static_files_served_documents.mautrix['fi.mau.external_bridge_servers'][0] == 'https://external-bridges.molecule.invalid'
  193. - matrix_static_files_served_documents.mautrix['org.example.molecule'] == 'mautrix-extension-reached'
  194. fail_msg: "One or more auxiliary documents lost configured data or extensions"
  195. success_msg: "Server, support and mautrix documents carry configured data and extensions"
  196. - name: Fetch response headers for every generated JSON document
  197. ansible.builtin.command:
  198. argv:
  199. - docker
  200. - run
  201. - --rm
  202. - --network={{ matrix_static_files_container_network }}
  203. - "{{ molecule_shared_image_curl }}"
  204. - --silent
  205. - --show-error
  206. - --head
  207. - "http://matrix-static-files:{{ matrix_static_files_environment_variable_server_port }}/.well-known/matrix/{{ item }}"
  208. loop:
  209. - client
  210. - server
  211. - support
  212. - mautrix
  213. register: matrix_static_files_document_headers
  214. changed_when: false
  215. - name: Assert every generated JSON document carries the configured headers
  216. ansible.builtin.assert:
  217. that:
  218. - "'content-type: application/json' in item.stdout_lines"
  219. - "'access-control-allow-origin: *' in item.stdout_lines"
  220. - "'cache-control: max-age=25200' in item.stdout_lines"
  221. fail_msg: >-
  222. {{ item.item }} did not carry the configured JSON, CORS and cache headers:
  223. {{ item.stdout }}
  224. success_msg: "{{ item.item }} carries the configured JSON, CORS and cache headers"
  225. loop: "{{ matrix_static_files_document_headers.results }}"
  226. loop_control:
  227. label: "{{ item.item }}"
  228. - name: Fetch the generated base-domain index document
  229. ansible.builtin.command:
  230. argv:
  231. - docker
  232. - run
  233. - --rm
  234. - --network={{ matrix_static_files_container_network }}
  235. - "{{ molecule_shared_image_curl }}"
  236. - --silent
  237. - --show-error
  238. - --write-out
  239. - "\nMOLECULE_HTTP_STATUS=%{http_code}"
  240. - "http://matrix-static-files:{{ matrix_static_files_environment_variable_server_port }}/"
  241. register: matrix_static_files_index_response
  242. changed_when: false
  243. - name: Assert the configured base-domain index is served
  244. ansible.builtin.assert:
  245. that:
  246. - "'MOLECULE_HTTP_STATUS=200' in matrix_static_files_index_response.stdout"
  247. - "'Molecule static-files index marker' in matrix_static_files_index_response.stdout"
  248. fail_msg: >-
  249. The configured base-domain index was not served
  250. ({{ matrix_static_files_index_response.stdout | default('no output') }})
  251. success_msg: "The configured base-domain index is served"
  252. - name: Parse the TOML configuration the role rendered
  253. ansible.builtin.command:
  254. argv:
  255. - python3
  256. - -c
  257. - >-
  258. import json, sys, tomllib;
  259. print(json.dumps(tomllib.load(open(sys.argv[1], 'rb'))))
  260. - "{{ matrix_static_files_config_path }}/config.toml"
  261. register: matrix_static_files_config_command
  262. changed_when: false
  263. - name: Assert the parsed server configuration carries the header contract
  264. ansible.builtin.assert:
  265. that:
  266. - matrix_static_files_config.advanced.headers[0].source == '/.well-known/matrix/*'
  267. - matrix_static_files_config.advanced.headers[0].headers['Content-Type'] == 'application/json'
  268. - matrix_static_files_config.advanced.headers[0].headers['Access-Control-Allow-Origin'] == '*'
  269. - matrix_static_files_config.advanced.headers[0].headers['Cache-Control'] == 'max-age=25200'
  270. fail_msg: "The parsed config.toml does not carry the configured header contract"
  271. success_msg: "The parsed config.toml carries the configured header contract"
  272. vars:
  273. matrix_static_files_config: "{{ matrix_static_files_config_command.stdout | from_json }}"
  274. - name: Read the environment file the role rendered
  275. ansible.builtin.slurp:
  276. src: "{{ matrix_static_files_base_path }}/env"
  277. register: matrix_static_files_env_file
  278. - name: Initialize the parsed environment
  279. ansible.builtin.set_fact:
  280. matrix_static_files_env_parsed: {}
  281. - name: Parse the rendered environment
  282. ansible.builtin.set_fact:
  283. matrix_static_files_env_parsed: >-
  284. {{ matrix_static_files_env_parsed | combine({item.split('=', 1)[0]: item.split('=', 1)[1]}) }}
  285. loop: "{{ (matrix_static_files_env_file.content | b64decode).splitlines() | reject('equalto', '') }}"
  286. when: "'=' in item"
  287. no_log: true
  288. - name: Assert the rendered environment carries the scenario's settings
  289. ansible.builtin.assert:
  290. that:
  291. - matrix_static_files_env_parsed.SERVER_PORT == matrix_static_files_environment_variable_server_port | string
  292. - matrix_static_files_env_parsed.SERVER_LOG_LEVEL == 'debug'
  293. - matrix_static_files_env_parsed.SERVER_LOG_REMOTE_ADDRESS == 'true'
  294. - matrix_static_files_env_parsed.SERVER_IGNORE_HIDDEN_FILES == 'false'
  295. - matrix_static_files_env_parsed.MOLECULE_STATIC_FILES_MARKER == 'environment-reached'
  296. fail_msg: "The parsed environment file does not carry the scenario's settings"
  297. success_msg: "The parsed environment file carries the scenario's settings"
  298. - name: Read the labels file the role rendered
  299. ansible.builtin.slurp:
  300. src: "{{ matrix_static_files_base_path }}/labels"
  301. register: matrix_static_files_labels_file
  302. - name: Initialize the parsed labels
  303. ansible.builtin.set_fact:
  304. matrix_static_files_labels_parsed: {}
  305. - name: Parse the rendered labels
  306. ansible.builtin.set_fact:
  307. matrix_static_files_labels_parsed: >-
  308. {{ matrix_static_files_labels_parsed | combine({item.split('=', 1)[0]: item.split('=', 1)[1]}) }}
  309. loop: "{{ (matrix_static_files_labels_file.content | b64decode).splitlines() | reject('equalto', '') }}"
  310. when:
  311. - "'=' in item"
  312. - not item.startswith('#')
  313. no_log: true
  314. - name: Assert the rendered labels carry both routing contracts
  315. ansible.builtin.assert:
  316. that:
  317. - matrix_static_files_labels_parsed['traefik.enable'] == 'true'
  318. - matrix_static_files_labels_parsed['traefik.docker.network'] == matrix_static_files_container_network
  319. - matrix_static_files_labels_parsed['traefik.http.services.matrix-static-files.loadbalancer.server.port'] == matrix_static_files_environment_variable_server_port | string
  320. - "'Host(`matrix-well-known.molecule.local`)' in matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-well-known-matrix.rule']"
  321. - "'PathPrefix(`/molecule-well-known`)' in matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-well-known-matrix.rule']"
  322. - matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-well-known-matrix.priority'] == '67'
  323. - matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-well-known-matrix.entrypoints'] == 'web'
  324. - matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-well-known-matrix.tls'] == 'false'
  325. - matrix_static_files_labels_parsed['traefik.http.middlewares.matrix-static-files-well-known-matrix-compress.compress.minResponseBodyBytes'] == '321'
  326. - "'Host(`base.molecule.local`)' in matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-base-domain.rule']"
  327. - "'PathPrefix(`/molecule-base`)' in matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-base-domain.rule']"
  328. - matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-base-domain.priority'] == '43'
  329. - matrix_static_files_labels_parsed['traefik.http.routers.matrix-static-files-base-domain.tls'] == 'false'
  330. - matrix_static_files_labels_parsed['molecule.scenario'] == 'matrix-static-files'
  331. fail_msg: "The rendered labels do not carry both routing contracts"
  332. success_msg: "The rendered labels carry both routing contracts"
  333. - name: Inspect the running matrix-static-files container
  334. ansible.builtin.command:
  335. argv:
  336. - docker
  337. - container
  338. - inspect
  339. - matrix-static-files
  340. register: matrix_static_files_container_inspect_command
  341. changed_when: false
  342. - name: Parse the running container inspection
  343. ansible.builtin.set_fact:
  344. matrix_static_files_container: "{{ (matrix_static_files_container_inspect_command.stdout | from_json)[0] }}"
  345. - name: Assert the configured environment reached the running container
  346. ansible.builtin.assert:
  347. that:
  348. - "('SERVER_PORT=' ~ (matrix_static_files_environment_variable_server_port | string)) in matrix_static_files_container.Config.Env"
  349. - "'SERVER_LOG_LEVEL=debug' in matrix_static_files_container.Config.Env"
  350. - "'SERVER_LOG_REMOTE_ADDRESS=true' in matrix_static_files_container.Config.Env"
  351. - "'SERVER_IGNORE_HIDDEN_FILES=false' in matrix_static_files_container.Config.Env"
  352. - "'MOLECULE_STATIC_FILES_MARKER=environment-reached' in matrix_static_files_container.Config.Env"
  353. fail_msg: "The running container environment does not carry the scenario's settings"
  354. success_msg: "The configured environment reached the running container"
  355. - name: Assert the rendered labels reached the running container
  356. ansible.builtin.assert:
  357. that:
  358. - matrix_static_files_container.Config.Labels['traefik.enable'] == 'true'
  359. - matrix_static_files_container.Config.Labels['traefik.docker.network'] == matrix_static_files_container_network
  360. - matrix_static_files_container.Config.Labels['traefik.http.services.matrix-static-files.loadbalancer.server.port'] == matrix_static_files_environment_variable_server_port | string
  361. - matrix_static_files_container.Config.Labels['traefik.http.routers.matrix-static-files-well-known-matrix.priority'] == '67'
  362. - matrix_static_files_container.Config.Labels['traefik.http.routers.matrix-static-files-base-domain.priority'] == '43'
  363. - matrix_static_files_container.Config.Labels['molecule.scenario'] == 'matrix-static-files'
  364. fail_msg: "The running container does not carry the labels the role rendered"
  365. success_msg: "The rendered labels reached the running container"
  366. - name: Assert the running container uses the exact image and version the role pins
  367. ansible.builtin.assert:
  368. that:
  369. - matrix_static_files_container.Config.Image == matrix_static_files_expected_image
  370. fail_msg: >-
  371. The running container uses {{ matrix_static_files_container.Config.Image }},
  372. expected {{ matrix_static_files_expected_image }}
  373. success_msg: "The running container uses the exact image and version defaults/main.yml pins"
  374. vars:
  375. matrix_static_files_expected_tag: >-
  376. {{
  377. 'latest'
  378. if matrix_static_files_role_defaults.matrix_static_files_version == 'latest'
  379. else matrix_static_files_role_defaults.matrix_static_files_version ~ '-alpine'
  380. }}
  381. matrix_static_files_expected_image: >-
  382. {{ matrix_static_files_role_defaults.matrix_static_files_container_image_registry_prefix_upstream_default }}joseluisq/static-web-server:{{ matrix_static_files_expected_tag }}
  383. - name: Assert the running container uses the configured UID and GID
  384. ansible.builtin.assert:
  385. that:
  386. - matrix_static_files_container.Config.User == (matrix_user_uid | string) ~ ':' ~ (matrix_user_gid | string)
  387. fail_msg: >-
  388. The running container uses {{ matrix_static_files_container.Config.User }},
  389. expected {{ matrix_user_uid }}:{{ matrix_user_gid }}
  390. success_msg: "The running container uses the configured UID and GID"
  391. - name: Assert the running container has a read-only root filesystem
  392. ansible.builtin.assert:
  393. that:
  394. - matrix_static_files_container.HostConfig.ReadonlyRootfs == true
  395. fail_msg: "The running container's root filesystem is writable"
  396. success_msg: "The running container has a read-only root filesystem"
  397. - name: Assert the running container drops all capabilities and adds none
  398. ansible.builtin.assert:
  399. that:
  400. - matrix_static_files_container.HostConfig.CapDrop == ['ALL']
  401. - matrix_static_files_container.HostConfig.CapAdd | default([], true) | length == 0
  402. fail_msg: "The running container does not have the expected capability restrictions"
  403. success_msg: "The running container drops all capabilities and adds none"
  404. - name: Assert both bind mounts are read-only
  405. ansible.builtin.assert:
  406. that:
  407. - matrix_static_files_container.Mounts | selectattr('Destination', 'equalto', '/var/public') | list | length == 1
  408. - (matrix_static_files_container.Mounts | selectattr('Destination', 'equalto', '/var/public') | first).RW == false
  409. - matrix_static_files_container.Mounts | selectattr('Destination', 'equalto', '/config') | list | length == 1
  410. - (matrix_static_files_container.Mounts | selectattr('Destination', 'equalto', '/config') | first).RW == false
  411. fail_msg: "The running container is missing a read-only content or configuration mount"
  412. success_msg: "Both bind mounts are present and read-only"
  413. - name: Assert the running container is attached only to its dedicated network
  414. ansible.builtin.assert:
  415. that:
  416. - matrix_static_files_container.NetworkSettings.Networks.keys() | list | length == 1
  417. - matrix_static_files_container_network in matrix_static_files_container.NetworkSettings.Networks
  418. fail_msg: >-
  419. The running container is attached to unexpected networks:
  420. {{ matrix_static_files_container.NetworkSettings.Networks.keys() | list }}
  421. success_msg: "The running container is attached only to its dedicated network"
  422. - name: Assert the running container publishes no host ports
  423. ansible.builtin.assert:
  424. that:
  425. - matrix_static_files_container.HostConfig.PortBindings | default({}, true) | length == 0
  426. fail_msg: "The running container unexpectedly publishes a host port"
  427. success_msg: "The running container publishes no host ports"