Compare commits

...

9 Commits

Author SHA1 Message Date
uumas
46c65afd25 v0.1.5 2026-09-22 20:46:40 +03:00
uumas
9bf31e9521 euro_office: Add fonts 2026-09-22 20:44:53 +03:00
uumas
df7662e835 Add Euro-Office role 2026-09-22 04:33:13 +03:00
uumas
3ce5f94e87 v0.1.3 2026-09-21 05:54:01 +03:00
uumas
2d34f7a163 Add mautrix-whatsapp role 2026-09-21 05:53:00 +03:00
uumas
d6518d404e synapse: Add support for appservices and use secrets better 2026-09-21 05:51:48 +03:00
uumas
bb9026bb6a service: Template file improvements 2026-09-21 05:51:32 +03:00
uumas
e980ee0bfc nextcloud: Use correct user for AppAPI handlers 2026-09-19 15:47:56 +03:00
uumas
43f10d744a service: Allow templated files to be mounted as full directory 2026-08-04 20:15:54 +03:00
91 changed files with 522 additions and 105 deletions

View File

@@ -3,7 +3,7 @@ namespace: uumas
name: podman
description: Roles for installing services in podman containers
readme: README.md
version: 0.1.1
version: 0.1.5
repository: "https://git.uumas.fi/uumas/ansible-podman"
license_file: LICENSE
authors:

View File

@@ -0,0 +1 @@
Sets up a Euro Office podman container.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,14 @@
---
argument_specs:
main:
description: Sets up a Euro Office podman container.
options:
euro_office_domains:
description: A list of domains the container should listen on.
type: list
required: true
elements: str
euro_office_jwt_secret:
description: JWT secret, must be at least 32 characters long
type: str
required: true

View File

@@ -0,0 +1,34 @@
---
- name: Euro-Office service
ansible.builtin.import_role:
name: service
vars:
service_name: euro-office
service_container_image: ghcr.io/euro-office/documentserver:latest
service_container_mounts:
- type: volume
source: data
destination: /var/lib/euro-office/documentserver
- type: volume
source: private
destination: /var/www/euro-office/Data
- type: copy
source: fonts/
destination: /usr/share/fonts/custom
service_container_http_port: 80
service_domains: "{{ euro_office_domains }}"
service_database_type: postgres
service_database_secret_type: env
service_database_secret_target: DB_PWD
service_redis: true
service_container_secrets:
- name: jwt
type: env
target: JWT_SECRET
value: "{{ euro_office_jwt_secret }}"
service_container_env:
DB_HOST: postgres
DB_USER: euro_office
DB_NAME: euro_office
DB_PASSWORD__FILE: /run/secrets/postgres
REDIS_SERVER_HOST: redis

View File

@@ -0,0 +1 @@
Sets up a mautrix-whatsapp bridge

View File

@@ -0,0 +1,5 @@
---
mautrix_whatsapp_os_name: Matrix bridge
mautrix_whatsapp_bot_displayname: WhatsApp bridge
mautrix_whatsapp_default_relays: []
mautrix_whatsapp_permissions: {}

View File

@@ -0,0 +1,57 @@
---
argument_specs:
main:
description:
- Sets up a mautrix-whatsapp bridge
options:
mautrix_whatsapp_homeserver_name:
description: Matrix server name of homeserver the bridge is connected to.
type: str
required: true
mautrix_whatsapp_as_token:
description: A secret token that the application service will use to authenticate requests to the homeserver
type: str
required: true
mautrix_whatsapp_hs_token:
description: A secret token that the homeserver will use authenticate requests to the application service
type: str
required: true
mautrix_whatsapp_doublepuppet_token:
description: Double puppeting as token to impersonate matrix users
type: str
required: true
mautrix_whatsapp_os_name:
description: Device name that's shown in the "WhatsApp Web" section in the mobile app
type: str
required: false
default: Matrix bridge
mautrix_whatsapp_bot_displayname:
description: Display name for the bridge bot
type: str
required: false
default: WhatsApp bridge
mautrix_whatsapp_default_relays:
description: List of user login IDs which anyone can set as a relay, as long as the relay user is in the room
type: list
required: false
default: []
elements: str
mautrix_whatsapp_permissions:
description: Permissions for using the bridge. All users are given the relay permission regardless of this.
type: list
required: false
default: []
elements: dict
options:
key:
description: Domain or user id to apply permission value to
type: str
required: true
value:
description: Permission value to apply
type: str
required: true
choices:
- commands
- user
- admin

View File

@@ -0,0 +1,32 @@
---
- name: Mautrix-WhatsApp container
ansible.builtin.import_role:
name: service
vars:
service_name: mautrix-whatsapp
service_container_image: "{{ _mautrix_whatsapp_image }}"
service_container_user: 1337:1337
service_container_command:
- /usr/bin/mautrix-whatsapp
- --no-update
service_database_type: postgres
service_container_additional_networks:
- synapse-mautrix-whatsapp
service_container_mounts:
- type: template
source: config.yaml.j2
destination: /data/config.yaml
service_container_secrets:
- name: as-token
value: "{{ mautrix_whatsapp_as_token }}"
- name: hs-token
value: "{{ mautrix_whatsapp_hs_token }}"
- name: doublepuppet-token
value: as_token:{{ mautrix_whatsapp_doublepuppet_token }}
- name: encryption-pickle-key
service_container_env:
BRIDGE_DATABASE__URI_FILE: /run/secrets/postgres-url
BRIDGE_APPSERVICE__AS_TOKEN_FILE: /run/secrets/as-token
BRIDGE_APPSERVICE__HS_TOKEN_FILE: /run/secrets/hs-token
BRIDGE_ENCRYPTION__PICKLE_KEY: /run/secrets/encryption-pickle-key
"BRIDGE_DOUBLE_PUPPET__SECRETS__{{ mautrix_whatsapp_homeserver_name }}": /run/secrets/doublepuppet-token

View File

@@ -0,0 +1,73 @@
---
# vim:ft=yaml
# {{ ansible_managed }}
network:
os_name: "{{ mautrix_whatsapp_os_name }}"
send_presence_on_typing: true
url_previews: true
extev_polls: true
bridge:
cleanup_on_logout:
enabled: true
manual:
private: nothing
relayed: nothing
shared_no_users: kick
shared_has_users: kick
bad_credentials:
private: nothing
relayed: nothing
shared_no_users: kick
shared_has_users: kick
relay:
enabled: true
admin_only: true
default_relays:
{{ mautrix_whatsapp_default_relays | to_nice_yaml | indent(4) }}
permissions:
"*": relay
{% for item in mautrix_whatsapp_permissions %}
"{{ item.key }}": {{ item.value }}
{% endfor %}
database:
type: postgres
homeserver:
address: http://synapse:8009
domain: {{ mautrix_whatsapp_homeserver_name }}
async_media: true
appservice:
address: http://mautrix-whatsapp:8080
hostname: 0.0.0.0
port: 8080
id: whatsapp
bot:
username: whatsappbot
displayname: {{ mautrix_whatsapp_bot_displayname }}
matrix:
delivery_receipts: true
provisioning:
shared_secret: disable
encryption:
allow: true
default: false
msc4190: true
allow_key_sharing: true
env_config_prefix: BRIDGE_
logging:
min_level: info
writers:
- type: stdout
format: pretty-colored

View File

@@ -0,0 +1,2 @@
---
_mautrix_whatsapp_image: dock.mau.dev/mautrix/whatsapp:latest

View File

@@ -2,6 +2,7 @@
- name: Unregister AppAPI daemon
containers.podman.podman_container_exec:
name: nextcloud
user: www-data
argv:
- /var/www/html/occ
- app_api:daemon:unregister
@@ -14,6 +15,7 @@
- name: Register AppAPI daemon
containers.podman.podman_container_exec:
name: nextcloud
user: www-data
argv:
- /bin/sh
- -c

View File

@@ -163,7 +163,7 @@ argument_specs:
- Mount source.
- If mount type is volume, name of the volume.
- If mount type is bind, host path to bind mount inside the container.
- If mount type is template, the name of the template file, must end in .j2
- If mount type is template, the name of the template file, must end in .j2.
- If mount type is copy, name of the file or directory to copy. Directory name must end in /.
type: str
required: true
@@ -220,9 +220,18 @@ argument_specs:
Command used to verify templated file validity.
Will be run in a temporary container using the same container image as the main service.
File will be available in the same place as in the service container (destination).
Only applicable if mount type is template
Only applicable if mount type is template.
type: str
required: false
template_directory:
description: >-
Whether to create a directory and mount it at the destination file's parent directory.
This is required for container to see templated file changes without restart.
Only applicable if mount type is template.
type: bool
required: false
default: false
service_container_devices:
description: List of devices to be added inside the service main container.
type: list
@@ -326,6 +335,7 @@ argument_specs:
host <service database type> on the default port.
- The database user will be <service name>
- The password will be accessible as secret at /run/secrets/<service database type>
- A postgres url is accessible as secret at /run/secrets/postgres-url
- >
The password will also be available as the
service_podman_secrets['<service name>-<service database type>'] variable.

View File

@@ -13,28 +13,29 @@
- name: Create service template mount directories
ansible.builtin.file:
path: "{{ _service_host_directory }}/mounts/{{ item }}"
path: "{{ item }}"
state: directory
mode: "0700"
loop: "{{ _service_all_template_mount_directories }}"
loop: "{{ _service_all_template_mount_host_files | map('dirname') | unique }}"
- name: Template files for template mounts
ansible.builtin.template:
src: "{{ item[0].source }}"
dest: "{{ item[1] }}"
mode: "{{ item[0].mode | default('0644') }}"
validate: "{{ validate if item[0].template_validate_command is defined else omit }}"
src: "{{ item.source }}"
dest: "{{ item.hostfile }}"
mode: "{{ item.mode | default('0644') }}"
validate: "{{ validate if item.template_validate_command is defined else omit }}"
notify: >-
{{ 'Reload' if service_container_reload_method != 'none' else 'Restart' }}
container service {{ service_name }}
loop: "{{ _service_all_template_mounts | zip(_service_all_template_mount_host_files) }}"
loop: "{{ _service_all_template_mounts_full }}"
vars:
validate: >-
podman run --rm
-v %s:{{ item[0].destination }}:ro
--entrypoint {{ item[0].template_validate_command.split(' ', 1)[0] }}
{{ service_container_image }}
{{ item[0].template_validate_command.split(' ', 1)[1] }}
validate: "{{ _service_template_validate_command }}"
- name: Ensure no legacy templated files present
ansible.builtin.file:
path: "{{ _service_host_directory }}/mounts/{{ item.source | regex_replace('\\.j2$', '') }}"
state: absent
loop: "{{ _service_all_template_mounts_full }}"
- name: Copy files for copy mounts
ansible.builtin.copy:

View File

@@ -25,21 +25,60 @@ _service_container_bind_mounts: >-
service_container_mounts | selectattr('type', '==', 'bind') +
([ _service_container_socket_mount ] if _service_native_socket else [])
}}
_service_container_template_mounts: >-
_service_template_mounts_hostfiles: >-
{{
([{'readonly': true}] * _service_template_mounts | length) |
zip(
_service_template_mounts |
community.general.remove_keys(['mode', 'template_validate_command']),
_service_template_mounts |
map(attribute='source') |
map('regex_replace', '\.j2$', '') |
map('regex_replace', '^', _service_host_directory ~ '/mounts/') |
map('community.general.dict_kv', 'source'),
([{'type': 'bind'}] * _service_template_mounts | length)
_service_template_mounts
| map(attribute='destination')
| map('dirname')
| map('replace', '/', '_')
| map('regex_replace', '^', _service_host_directory ~ '/mounts/')
| zip(
_service_template_mounts
| map(attribute='destination')
| map('basename')
)
| map('path_join')
| map('community.general.dict_kv', 'hostfile')
}}
_service_template_mounts_full: >-
{{
([{'readonly': true, 'template_directory': false}] * _service_template_mounts | length)
| zip(
_service_template_mounts,
_service_template_mounts_hostfiles
)
| map('combine')
}}
_service_template_mounts_plain: "{{ _service_template_mounts_full | rejectattr('template_directory') }}"
_service_container_template_mounts_plain: >-
{{
_service_template_mounts_plain
| community.general.remove_keys(['mode', 'template_validate_command', 'template_directory', 'hostfile'])
| zip(
_service_template_mounts_plain
| map(attribute='hostfile')
| map('community.general.dict_kv', 'source'),
([{'type': 'bind'}] * _service_template_mounts_plain | length)
) |
map('combine')
}}
_service_template_mounts_directory: "{{ _service_template_mounts_full | selectattr('template_directory') }}"
_service_container_template_mounts_directory: >-
{{
_service_template_mounts_directory
| community.general.remove_keys(['mode', 'template_validate_command', 'template_directory', 'hostfile'])
| zip(
_service_template_mounts_directory
| map(attribute='hostfile')
| map('dirname')
| map('community.general.dict_kv', 'source'),
([{'type': 'bind'}] * _service_template_mounts_directory | length)
)
| map('combine')
| unique
}}
_service_container_copy_mounts: >-
{{
([{'readonly': true}] * _service_copy_mounts | length) |
@@ -58,40 +97,49 @@ _service_container_copy_mounts: >-
_service_container_mounts: >-
{{
_service_container_volume_mounts +
_service_container_bind_mounts +
_service_container_template_mounts +
_service_container_copy_mounts
_service_container_volume_mounts
+ _service_container_bind_mounts
+ _service_container_template_mounts_plain
+ _service_container_template_mounts_directory
+ _service_container_copy_mounts
}}
_service_all_template_mounts: >-
{{
(
_service_template_mounts +
(
_service_additional_containers |
map(attribute='mounts', default=[]) |
flatten
_service_template_mounts
+ (
_service_additional_containers
| map(attribute='mounts', default=[])
| flatten
)
) |
selectattr('type', '==', 'template') |
unique
}}
_service_all_template_mount_directories: >-
{{
_service_all_template_mounts |
map(attribute='source') |
map('dirname') |
unique |
select('!=', '')
)
| selectattr('type', '==', 'template')
| unique
}}
_service_all_template_mount_host_files: >-
{{
_service_all_template_mounts |
map(attribute='source') |
map('regex_replace', '\.j2$', '') |
map('regex_replace', '^', _service_host_directory ~ '/mounts/')
_service_all_template_mounts
| map(attribute='destination')
| map('dirname')
| map('replace', '/', '_')
| map('regex_replace', '^', _service_host_directory ~ '/mounts/')
| zip(
_service_all_template_mounts
| map(attribute='destination')
| map('basename')
)
| map('path_join')
}}
_service_all_template_mounts_full: >-
{{
([{'readonly': true, 'template_directory': false}] * _service_all_template_mounts | length)
| zip(
_service_all_template_mounts,
_service_all_template_mount_host_files | map('community.general.dict_kv', 'hostfile')
)
| map('combine')
}}
_service_all_copy_mounts: >-
@@ -109,7 +157,7 @@ _service_all_copy_mounts: >-
}}
_service_all_copy_mount_host_files: >-
{{
_service_all_copy_mounts |
map(attribute='source') |
map('regex_replace', '^', _service_host_directory ~ '/mounts/')
_service_all_copy_mounts
| map(attribute='source')
| map('regex_replace', '^', _service_host_directory ~ '/mounts/')
}}

View File

@@ -2,34 +2,34 @@
_service_container_secrets: >-
{{
service_container_secrets
| map(attribute='name')
| map('community.general.dict_kv', 'target')
| zip(
service_container_secrets,
service_container_secrets
| map(attribute='name')
| map('community.general.dict_kv', 'target')
| zip(
service_container_secrets,
service_container_secrets
| map(attribute='name')
| map('regex_replace', '^', service_name ~ '-')
| map('community.general.dict_kv', 'name')
)
| map('combine')
+ (
[{
'name': _service_database_name,
'type': service_database_secret_type,
'target': service_database_secret_target
}] if _service_setup_database else []
)
+ (
[{
'name': _service_database_name ~ '-url',
'value':
'postgres://'
~ service_name | replace('-', '_')
~ ':' ~ service_podman_secrets[service_name ~ '-postgres']
~ '@postgres/' ~ service_name | replace('-', '_')
~ '?sslmode=disable',
'type': service_database_secret_type,
'target': service_database_secret_target ~ '-url'
}] if service_podman_secrets[service_name ~ '-postgres'] is defined else []
)
| map('regex_replace', '^', service_name ~ '-')
| map('community.general.dict_kv', 'name')
)
| map('combine')
+ (
[{
'name': _service_database_name,
'type': service_database_secret_type,
'target': service_database_secret_target
}] if _service_setup_database else []
)
+ (
[{
'name': _service_database_name ~ '-url',
'value':
'postgres://'
~ service_name | replace('-', '_')
~ ':' ~ service_podman_secrets[service_name ~ '-postgres']
~ '@postgres/' ~ service_name | replace('-', '_')
~ '?sslmode=disable',
'type': service_database_secret_type,
'target': service_database_secret_target ~ '-url'
}] if service_podman_secrets[service_name ~ '-postgres'] is defined else []
)
}}

View File

@@ -0,0 +1,24 @@
---
_service_template_validate_secrets: >-
{{
_service_container_secrets
| map(attribute='name')
| zip(
_service_container_secrets
| community.general.remove_keys(['name', 'value', 'length'])
| map('items')
| map('map', 'join', '=')
| map('join', ',')
)
| map('join', ',')
| map('regex_replace', '^', '--secret ')
| join(' ')
}}
_service_template_validate_command: >-
podman run --rm
-v %s:{{ item.destination }}:ro
--entrypoint {{ item.template_validate_command.split(' ', 1)[0] }}
{{ _service_template_validate_secrets }}
{{ service_container_image }}
{{ item.template_validate_command.split(' ', 1)[1] }}

View File

@@ -19,3 +19,5 @@ synapse_auto_join_rooms: []
synapse_smtp_server: ""
synapse_oidc_provider_client_id: ""
synapse_appservices: []

View File

@@ -116,3 +116,29 @@ argument_specs:
type: str
required: false
default: 16-alpine
synapse_appservices:
description: List of appservices to run with synapse. They will be installed in separate containers.
type: list
required: false
default: []
elements: dict
options:
id:
description: A unique, user-defined ID of the application service which will never change
type: str
required: true
host:
description: Hostname the appservice is accessible to synapse at
type: str
required: true
as_token:
description: A secret token that the application service will use to authenticate requests to the homeserver
type: str
required: true
hs_token:
description: A secret token that the homeserver will use authenticate requests to the application service
type: str
required: true
synapse_doublepuppet_token:
description: Double puppeting token for appservices. Required if synapse_appservices is not empty.

View File

@@ -15,26 +15,11 @@
name: service
vars:
service_name: synapse
service_container_image: "{{ _synapse_image_name }}"
service_container_image: ghcr.io/element-hq/synapse:latest
service_database_type: postgres
service_postgres_tag: "{{ synapse_postgres_tag }}"
service_container_mounts:
- type: template
source: homeserver.yaml.j2
destination: /data/homeserver.yaml
- type: template
source: log.yaml.j2
destination: /data/log.yaml
- type: volume
source: media
destination: /data/media
user: "991"
group: "991"
service_container_secrets:
- name: signing-key
value: "{{ synapse_signing_key }}"
- name: mas-client-secret
- name: mas-homeserver-secret
service_container_mounts: "{{ _synapse_mounts }}"
service_container_secrets: "{{ _synapse_secrets }}"
service_container_env:
SYNAPSE_SERVER_NAME: "{{ synapse_server_name }}"
SYNAPSE_REPORT_STATS: "no"
@@ -47,13 +32,14 @@
proxy_target_socket: /run/matrix-authentication-service-caddy-socket-proxy.sock
service_wants:
- matrix-authentication-service.service
service_container_additional_networks: "{{ _synapse_additional_networks }}"
- name: Matrix authentication service for synapse
ansible.builtin.import_role:
name: matrix_authentication_service
vars:
matrix_authentication_service_additional_networks:
- synapse
- synapse-mas
matrix_authentication_service_secrets: "{{ synapse_mas_secrets }}"
matrix_authentication_service_domain: "{{ synapse_mas_domain }}"
matrix_authentication_service_homeserver_name: "{{ synapse_server_name }}"

View File

@@ -0,0 +1,21 @@
---
# vim:ft=yaml
# {{ ansible_managed }}
{% set id = item.destination.split('/')[-1].split('-')[0] %}
{% set appservice = synapse_appservices | selectattr('id', 'equalto', id) | first %}
id: {{ id }}
url: http://{{ appservice.host }}:8080
as_token: {{ appservice.as_token }}
hs_token: {{ appservice.hs_token }}
sender_localpart: {{ id }}senderlocalpart
rate_limited: false
namespaces:
users:
- regex: ^@{{ id }}bot:{{ synapse_server_name | replace('.', '\.') }}$
exclusive: true
- regex: ^@{{ id }}_.*:{{ synapse_server_name | replace('.', '\.') }}$
exclusive: true
de.sorunome.msc2409.push_ephemeral: true
receive_ephemeral: true
io.element.msc4190: true

View File

@@ -0,0 +1,15 @@
---
# vim:ft=yaml
# {{ ansible_managed }}
id: doublepuppet
url:
as_token: "{{ synapse_doublepuppet_token }}"
hs_token: doublepuppethstoken
sender_localpart: doublepuppetsenderlocalpart
rate_limited: false
namespaces:
users:
- regex: '@.*:{{ synapse_server_name | replace(".", "\.") }}'
exclusive: false

View File

@@ -81,7 +81,7 @@ url_preview_ip_range_blacklist:
turn_uris: {{ synapse_turn_uris }}
{% if synapse_turn_uris | length > 0 %}
turn_shared_secret: {{ synapse_turn_shared_secret }}
turn_shared_secret_file: /run/secrets/turn-shared-secret
{% endif %}
turn_user_lifetime: 1d
turn_allow_guests: false
@@ -109,4 +109,10 @@ autocreate_auto_join_rooms: false
matrix_authentication_service:
enabled: true
endpoint: http://matrix-authentication-service:8080/
secret: "{{ service_podman_secrets['synapse-mas-homeserver-secret'] }}"
secret_path: /run/secrets/mas-homeserver-secret
{% if synapse_appservices | length > 0 %}
app_service_config_files:
- {{ _synapse_doublepuppet_registration_mount.destination }}
{{ _synapse_appservice_registration_mounts | map(attribute='destination') | to_nice_yaml | indent(2) }}
{% endif %}

View File

@@ -1,2 +1,59 @@
---
_synapse_image_name: ghcr.io/element-hq/synapse:latest
_synapse_additional_networks: >-
{{
['synapse-mas']
+ (
synapse_appservices
| map(attribute='host')
| map('regex_replace', '^', 'synapse-')
)
}}
_synapse_main_secrets:
- name: signing-key
value: "{{ synapse_signing_key }}"
- name: mas-client-secret
- name: mas-homeserver-secret
_synapse_turn_secrets:
- name: turn-shared-secret
value: "{{ synapse_turn_shared_secret }}"
_synapse_secrets: >-
{{
_synapse_main_secrets
+ (_synapse_turn_secrets if synapse_turn_uris | length > 0 else [])
}}
_synapse_base_mounts:
- type: template
source: homeserver.yaml.j2
destination: /data/homeserver.yaml
template_validate_command: python -m synapse.config -c /data/homeserver.yaml --no-secrets-in-config
- type: template
source: log.yaml.j2
destination: /data/log.yaml
- type: volume
source: media
destination: /data/media
user: "991"
group: "991"
_synapse_doublepuppet_registration_mount:
type: template
source: doublepuppet-registration.yaml.j2
destination: /data/doublepuppet-registration.yaml
_synapse_appservice_registration_mounts: >-
{{
([{'type': 'template', 'source': 'appservice-registration.yaml.j2'}] * synapse_appservices | length)
| zip(
synapse_appservices
| map(attribute='id')
| map('regex_replace', '^', '/data/appservices/')
| map('regex_replace', '$', '-registration.yaml')
| map('community.general.dict_kv', 'destination')
) | map('combine')
}}
_synapse_mounts: >-
{{
_synapse_base_mounts
+ ([_synapse_doublepuppet_registration_mount] if synapse_appservices | length > 0 else [])
+ _synapse_appservice_registration_mounts
}}