Matrix Docker Ansible eploy
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 

266 wiersze
14 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. ---
  5. # Proves Postmoogle logs in as the bot the role configured, migrates the Postgres database
  6. # the role pointed it at, and serves SMTP on the non-default port the role rendered. The SMTP
  7. # probe stops after EHLO: this scenario deliberately never bridges real mail.
  8. - name: Verify Postmoogle
  9. hosts: all
  10. become: true
  11. vars_files:
  12. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
  13. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
  14. gather_facts: false
  15. vars:
  16. matrix_bridge_postmoogle_molecule_user_id: "@{{ matrix_bridge_postmoogle_login }}:molecule.local"
  17. matrix_bridge_postmoogle_molecule_database_dsn: >-
  18. postgres://{{ matrix_bridge_postmoogle_database_username }}:{{ matrix_bridge_postmoogle_database_password }}@{{ matrix_bridge_postmoogle_database_hostname }}:5432/{{ matrix_bridge_postmoogle_database_name }}?sslmode=disable
  19. matrix_bridge_postmoogle_container: "{{ (matrix_bridge_postmoogle_container_inspect.stdout | from_json) | first }}"
  20. tasks:
  21. # From the role's defaults rather than pinned in molecule.yml, so the image assertion
  22. # compares the running image against what the role ships, not the scenario itself.
  23. - name: Load the role's defaults under a separate name
  24. ansible.builtin.include_vars:
  25. file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
  26. name: matrix_bridge_postmoogle_role_defaults
  27. - name: Wait for the Postmoogle service to become active
  28. ansible.builtin.systemd_service:
  29. name: matrix-postmoogle.service
  30. register: matrix_bridge_postmoogle_service
  31. until: matrix_bridge_postmoogle_service.status.ActiveState == 'active'
  32. retries: 30
  33. delay: 5
  34. failed_when: false
  35. # Restart=always makes a crash-looping container appear active. The counter is therefore
  36. # part of the same assertion, and `is defined` prevents a missing counter becoming 0.
  37. - name: Assert the service is active and has not been restarting
  38. ansible.builtin.assert:
  39. that:
  40. - matrix_bridge_postmoogle_service.status.ActiveState == 'active'
  41. - matrix_bridge_postmoogle_service.status.NRestarts is defined
  42. - matrix_bridge_postmoogle_service.status.NRestarts | int == 0
  43. fail_msg: >-
  44. matrix-postmoogle.service is
  45. {{ matrix_bridge_postmoogle_service.status.ActiveState | default('unknown') }}
  46. after {{ matrix_bridge_postmoogle_service.status.NRestarts | default('?') }}
  47. automatic restart(s)
  48. success_msg: "matrix-postmoogle.service is active and has not restarted"
  49. # The unit attaches the container output to journald. Strip its ANSI colours and filter
  50. # the whole journal: the login line is among the oldest and would disappear from a tail.
  51. - name: Wait for Postmoogle to report Matrix login and SMTP startup
  52. ansible.builtin.shell:
  53. cmd: >-
  54. set -o pipefail;
  55. journalctl --unit=matrix-postmoogle.service --no-pager --output=cat --lines=all
  56. | sed -r 's/\x1B\[[0-9;]*[mK]//g'
  57. | grep -E 'Stored credentials after login|Starting SMTP server|cannot initialize matrix bot'
  58. | head -n 30 || true
  59. executable: /bin/bash
  60. register: matrix_bridge_postmoogle_journal
  61. changed_when: false
  62. until:
  63. - "'Stored credentials after login' in matrix_bridge_postmoogle_journal.stdout"
  64. - "'Starting SMTP server' in matrix_bridge_postmoogle_journal.stdout"
  65. retries: 24
  66. delay: 5
  67. failed_when: false
  68. # "Stored credentials after login" is emitted after the Matrix /login response has been
  69. # accepted. Requiring the returned MXID proves the URL, localpart and password were good
  70. # enough for a login round-trip, rather than only appearing in a file on disk.
  71. - name: Assert Postmoogle logged in as the configured bot
  72. ansible.builtin.assert:
  73. that:
  74. - "'Stored credentials after login' in matrix_bridge_postmoogle_journal.stdout"
  75. - "'user_id=' ~ matrix_bridge_postmoogle_molecule_user_id in matrix_bridge_postmoogle_journal.stdout"
  76. - "'cannot initialize matrix bot' not in matrix_bridge_postmoogle_journal.stdout"
  77. fail_msg: >-
  78. Postmoogle did not log in as {{ matrix_bridge_postmoogle_molecule_user_id }}
  79. success_msg: "Postmoogle completed Matrix login as the configured bot"
  80. - name: Assert Postmoogle opened its configured SMTP port
  81. ansible.builtin.assert:
  82. that:
  83. - "'Starting SMTP server port=' ~ matrix_bridge_postmoogle_port in matrix_bridge_postmoogle_journal.stdout"
  84. fail_msg: "Postmoogle did not start SMTP on port {{ matrix_bridge_postmoogle_port }}"
  85. success_msg: "Postmoogle started its configured SMTP listener"
  86. # This is an actual SMTP readiness exchange over the container network, not only a TCP
  87. # connect. It sends EHLO and stops there, without supplying an envelope or message body.
  88. - name: Exchange an SMTP banner and EHLO with Postmoogle
  89. ansible.builtin.command:
  90. argv:
  91. - docker
  92. - run
  93. - --rm
  94. - --network={{ matrix_bridge_postmoogle_container_network }}
  95. - "{{ molecule_shared_image_python }}"
  96. - python3
  97. - -c
  98. - >-
  99. import socket;
  100. s=socket.create_connection(("matrix-postmoogle", {{ matrix_bridge_postmoogle_port | int }}), 5);
  101. s.settimeout(5);
  102. banner=s.recv(4096).decode();
  103. s.sendall(b"EHLO molecule.local\r\n");
  104. reply=s.recv(4096).decode();
  105. print("BANNER=" + repr(banner));
  106. print("EHLO=" + repr(reply));
  107. s.close()
  108. register: matrix_bridge_postmoogle_smtp
  109. changed_when: false
  110. retries: 24
  111. delay: 5
  112. until: matrix_bridge_postmoogle_smtp.rc == 0
  113. failed_when: false
  114. - name: Assert Postmoogle completed the SMTP readiness exchange
  115. ansible.builtin.assert:
  116. that:
  117. - matrix_bridge_postmoogle_smtp.rc == 0
  118. - "\"BANNER='220\" in matrix_bridge_postmoogle_smtp.stdout"
  119. - "\"EHLO='250\" in matrix_bridge_postmoogle_smtp.stdout"
  120. fail_msg: >-
  121. Postmoogle did not return a 220 banner and 250 EHLO response on port
  122. {{ matrix_bridge_postmoogle_port }}
  123. ({{ matrix_bridge_postmoogle_smtp.stdout | default('no output') }})
  124. success_msg: "Postmoogle completes an SMTP banner and EHLO exchange"
  125. - name: Read the environment file the role rendered
  126. ansible.builtin.slurp:
  127. src: "{{ matrix_bridge_postmoogle_config_path }}/env"
  128. register: matrix_bridge_postmoogle_env_file
  129. - name: Read the environment file's ownership and mode
  130. ansible.builtin.stat:
  131. path: "{{ matrix_bridge_postmoogle_config_path }}/env"
  132. register: matrix_bridge_postmoogle_env_stat
  133. # Parse the key=value document instead of substring-matching it, including the DSN's own
  134. # equals sign by splitting each line only once.
  135. - name: Parse the rendered Postmoogle environment
  136. ansible.builtin.set_fact:
  137. matrix_bridge_postmoogle_env: >-
  138. {{ matrix_bridge_postmoogle_env | default({}) | combine(dict([item.split('=', 1)])) }}
  139. loop: "{{ (matrix_bridge_postmoogle_env_file.content | b64decode).splitlines() }}"
  140. when: item is search('=')
  141. no_log: true
  142. - name: Assert the parsed environment carries this scenario's configuration
  143. ansible.builtin.assert:
  144. that:
  145. - matrix_bridge_postmoogle_actual_env == matrix_bridge_postmoogle_expected_env
  146. fail_msg: "The rendered environment does not carry the scenario's values"
  147. success_msg: "The parsed environment carries the scenario's values"
  148. vars:
  149. matrix_bridge_postmoogle_expected_env:
  150. POSTMOOGLE_LOGIN: "{{ matrix_bridge_postmoogle_login }}"
  151. POSTMOOGLE_PASSWORD: "{{ matrix_bridge_postmoogle_password }}"
  152. POSTMOOGLE_HOMESERVER: "{{ matrix_bridge_postmoogle_homeserver }}"
  153. POSTMOOGLE_DOMAINS: "{{ matrix_bridge_postmoogle_domains | join(' ') }}"
  154. POSTMOOGLE_PORT: "{{ matrix_bridge_postmoogle_port }}"
  155. POSTMOOGLE_DB_DSN: "{{ matrix_bridge_postmoogle_molecule_database_dsn | trim }}"
  156. POSTMOOGLE_DB_DIALECT: postgres
  157. POSTMOOGLE_PREFIX: "{{ matrix_bridge_postmoogle_prefix }}"
  158. POSTMOOGLE_MAXSIZE: "{{ matrix_bridge_postmoogle_maxsize }}"
  159. POSTMOOGLE_LOGLEVEL: "{{ matrix_bridge_postmoogle_loglevel }}"
  160. POSTMOOGLE_ADMINS: "{{ matrix_bridge_postmoogle_admins | join(' ') }}"
  161. POSTMOOGLE_DATA_SECRET: "{{ matrix_bridge_postmoogle_data_secret }}"
  162. POSTMOOGLE_PROXIES: "{{ matrix_bridge_postmoogle_proxies | join(' ') }}"
  163. POSTMOOGLE_MAILBOXES_FORWARDED: "{{ matrix_bridge_postmoogle_mailboxes_forwarded | join(' ') }}"
  164. POSTMOOGLE_MAILBOXES_RESERVED: "{{ matrix_bridge_postmoogle_mailboxes_reserved | join(' ') }}"
  165. matrix_bridge_postmoogle_actual_env: >-
  166. {{ matrix_bridge_postmoogle_env | dict2items
  167. | selectattr('key', 'in', matrix_bridge_postmoogle_expected_env.keys())
  168. | items2dict }}
  169. - name: Assert the environment file has the playbook-supplied identity and private mode
  170. ansible.builtin.assert:
  171. that:
  172. - matrix_bridge_postmoogle_env_stat.stat.uid | int == matrix_user_uid | int
  173. - matrix_bridge_postmoogle_env_stat.stat.gid | int == matrix_user_gid | int
  174. - matrix_bridge_postmoogle_env_stat.stat.mode == '0640'
  175. fail_msg: "The environment file does not have the expected identity or mode"
  176. success_msg: "The environment file has the playbook-supplied identity and private mode"
  177. # These tables can only exist after Postmoogle has resolved the non-default hostname,
  178. # authenticated with the non-default credentials and run its Matrix/crypto migrations.
  179. - name: List the tables Postmoogle created in Postgres
  180. ansible.builtin.command:
  181. argv:
  182. - docker
  183. - exec
  184. - matrix-postgres-molecule
  185. - psql
  186. - --username={{ matrix_bridge_postmoogle_database_username }}
  187. - --dbname={{ matrix_bridge_postmoogle_database_name }}
  188. - --tuples-only
  189. - --no-align
  190. - --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public'
  191. register: matrix_bridge_postmoogle_tables
  192. changed_when: false
  193. - name: Assert Postmoogle migrated its schema into the configured database
  194. ansible.builtin.assert:
  195. that:
  196. - matrix_bridge_postmoogle_tables.rc == 0
  197. - "'mx_version' in matrix_bridge_postmoogle_table_names"
  198. - "'crypto_version' in matrix_bridge_postmoogle_table_names"
  199. - matrix_bridge_postmoogle_table_names | length > 10
  200. fail_msg: >-
  201. Postmoogle did not create its Matrix and crypto schema in
  202. {{ matrix_bridge_postmoogle_database_name }}
  203. success_msg: "Postmoogle migrated its schema into the configured Postgres database"
  204. vars:
  205. matrix_bridge_postmoogle_table_names: "{{ matrix_bridge_postmoogle_tables.stdout_lines | select | list }}"
  206. - name: Inspect the running Postmoogle container
  207. ansible.builtin.command:
  208. argv:
  209. - docker
  210. - container
  211. - inspect
  212. - matrix-postmoogle
  213. register: matrix_bridge_postmoogle_container_inspect
  214. changed_when: false
  215. - name: Assert the running container uses the exact pinned image
  216. ansible.builtin.assert:
  217. that:
  218. - matrix_bridge_postmoogle_container.Config.Image == 'ghcr.io/etkecc/postmoogle:' ~ matrix_bridge_postmoogle_role_defaults.matrix_bridge_postmoogle_version
  219. fail_msg: "The running container does not use the exact image the role pins"
  220. success_msg: "The running container uses the exact image the role pins"
  221. - name: Assert the running container uses the playbook identity and a read-only root filesystem
  222. ansible.builtin.assert:
  223. that:
  224. - matrix_bridge_postmoogle_container.Config.User == matrix_user_uid ~ ':' ~ matrix_user_gid
  225. - matrix_bridge_postmoogle_container.HostConfig.ReadonlyRootfs
  226. fail_msg: "The running container does not use the expected identity or read-only root filesystem"
  227. success_msg: "The running container uses the playbook identity and a read-only root filesystem"
  228. - name: Assert the running container is attached to the configured network
  229. ansible.builtin.assert:
  230. that:
  231. - matrix_bridge_postmoogle_container_network in matrix_bridge_postmoogle_container.NetworkSettings.Networks
  232. fail_msg: "The running container is not attached to {{ matrix_bridge_postmoogle_container_network }}"
  233. success_msg: "The running container is attached to the configured network"
  234. # The high host binding is read back from Docker rather than merely from the unit file.
  235. # Docker could not have started the container if that binding were invalid.
  236. - name: Assert Docker published only the configured SMTP port
  237. ansible.builtin.assert:
  238. that:
  239. - matrix_bridge_postmoogle_port ~ '/tcp' in matrix_bridge_postmoogle_container.HostConfig.PortBindings
  240. - matrix_bridge_postmoogle_container.HostConfig.PortBindings[matrix_bridge_postmoogle_port ~ '/tcp'][0].HostPort == matrix_bridge_postmoogle_smtp_host_bind_port
  241. - "'25/tcp' not in matrix_bridge_postmoogle_container.HostConfig.PortBindings"
  242. fail_msg: "Docker does not carry the configured high SMTP binding, or also publishes port 25"
  243. success_msg: "Docker published only the configured high SMTP binding"