Adapted roles to new architecture

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-17 15:39:31 +02:00
parent 9469452275
commit ad449c3b6a
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
41 changed files with 665 additions and 101 deletions

113
cli/build/inventory/full.py Normal file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env python3
# cli/build/inventory/full.py
import argparse
import sys
import os
try:
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
except ImportError:
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
import yaml
import json
def build_group_inventory(apps, host):
"""
Builds a group-based Ansible inventory: each app is a group containing the host.
"""
groups = {app: {"hosts": [host]} for app in apps}
inventory = {
"all": {
"hosts": [host],
"children": {app: {} for app in apps},
},
**groups
}
return inventory
def build_hostvar_inventory(apps, host):
"""
Alternative: Builds an inventory where all invokables are set as hostvars (as a list).
"""
return {
"all": {
"hosts": [host],
},
"_meta": {
"hostvars": {
host: {
"invokable_applications": apps
}
}
}
}
def main():
parser = argparse.ArgumentParser(
description='Build a dynamic Ansible inventory for a given host with all invokable applications.'
)
parser.add_argument(
'--host',
required=True,
help='Hostname to assign to all invokable application groups'
)
parser.add_argument(
'-f', '--format',
choices=['json', 'yaml'],
default='yaml',
help='Output format (yaml [default], json)'
)
parser.add_argument(
'--inventory-style',
choices=['group', 'hostvars'],
default='group',
help='Inventory style: group (default, one group per app) or hostvars (list as hostvar)'
)
parser.add_argument(
'-c', '--categories-file',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles', 'categories.yml')),
help='Path to roles/categories.yml (default: roles/categories.yml at project root)'
)
parser.add_argument(
'-r', '--roles-dir',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles')),
help='Path to roles/ directory (default: roles/ at project root)'
)
parser.add_argument(
'-o', '--output',
help='Write output to file instead of stdout'
)
args = parser.parse_args()
try:
apps = get_all_invokable_apps(
categories_file=args.categories_file,
roles_dir=args.roles_dir
)
except Exception as e:
sys.stderr.write(f"Error: {e}\n")
sys.exit(1)
# Select inventory style
if args.inventory_style == 'group':
inventory = build_group_inventory(apps, args.host)
else:
inventory = build_hostvar_inventory(apps, args.host)
# Output in chosen format
if args.format == 'json':
output = json.dumps(inventory, indent=2)
else:
output = yaml.safe_dump(inventory, default_flow_style=False)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
else:
print(output)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
# cli/meta/applications/invokable.py
import argparse
import sys
import os
# Import filter plugin for get_all_invokable_apps
try:
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
except ImportError:
# Try to adjust sys.path if running outside Ansible
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
try:
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
except ImportError:
sys.stderr.write("Could not import filter_plugins.get_all_invokable_apps. Check your PYTHONPATH.\n")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description='List all invokable applications (application_ids) based on invokable paths from categories.yml and available roles.'
)
parser.add_argument(
'-c', '--categories-file',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles', 'categories.yml')),
help='Path to roles/categories.yml (default: roles/categories.yml at project root)'
)
parser.add_argument(
'-r', '--roles-dir',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles')),
help='Path to roles/ directory (default: roles/ at project root)'
)
args = parser.parse_args()
try:
result = get_all_invokable_apps(
categories_file=args.categories_file,
roles_dir=args.roles_dir
)
except Exception as e:
sys.stderr.write(f"Error: {e}\n")
sys.exit(1)
for app_id in result:
print(app_id)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,54 @@
import os
import yaml
def get_all_invokable_apps(
categories_file=None,
roles_dir=None
):
"""
Return all application_ids (or role names) for roles whose directory names match invokable paths from categories.yml.
:param categories_file: Path to categories.yml (default: roles/categories.yml at project root)
:param roles_dir: Path to roles directory (default: roles/ at project root)
:return: List of application_ids (or role names)
"""
# Resolve defaults
here = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(here, '..'))
if not categories_file:
categories_file = os.path.join(project_root, 'roles', 'categories.yml')
if not roles_dir:
roles_dir = os.path.join(project_root, 'roles')
# Get invokable paths
from filter_plugins.invokable_paths import get_invokable_paths
invokable_paths = get_invokable_paths(categories_file)
if not invokable_paths:
return []
result = []
if not os.path.isdir(roles_dir):
return []
for role in sorted(os.listdir(roles_dir)):
role_path = os.path.join(roles_dir, role)
if not os.path.isdir(role_path):
continue
if any(role == p or role.startswith(p + '-') for p in invokable_paths):
vars_file = os.path.join(role_path, 'vars', 'main.yml')
if os.path.isfile(vars_file):
try:
with open(vars_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f) or {}
app_id = data.get('application_id', role)
except Exception:
app_id = role
else:
app_id = role
result.append(app_id)
return sorted(result)
class FilterModule(object):
def filters(self):
return {
'get_all_invokable_apps': get_all_invokable_apps
}

View File

@ -0,0 +1,55 @@
import os
import yaml
def _load_categories_tree(categories_file):
with open(categories_file, 'r', encoding='utf-8') as f:
categories = yaml.safe_load(f)['roles']
return categories
def _flatten_categories(tree, prefix=''):
"""Flattens nested category tree to all possible category paths."""
result = []
for k, v in tree.items():
current = f"{prefix}-{k}" if prefix else k
result.append(current)
if isinstance(v, dict):
for sk, sv in v.items():
if isinstance(sv, dict):
result.extend(_flatten_categories({sk: sv}, current))
return result
def get_entity_name(role_name):
"""
Automatically get the entity name from a role name by removing the
longest matching category path from categories.yml.
"""
possible_locations = [
os.path.join(os.getcwd(), 'roles', 'categories.yml'),
os.path.join(os.path.dirname(__file__), '..', 'roles', 'categories.yml'),
'roles/categories.yml',
]
categories_file = None
for loc in possible_locations:
if os.path.exists(loc):
categories_file = loc
break
if not categories_file:
return role_name
categories_tree = _load_categories_tree(categories_file)
all_category_paths = _flatten_categories(categories_tree)
role_name_lc = role_name.lower()
all_category_paths = [cat.lower() for cat in all_category_paths]
for cat in sorted(all_category_paths, key=len, reverse=True):
if role_name_lc.startswith(cat + "-"):
return role_name[len(cat) + 1:]
if role_name_lc == cat:
return ""
return role_name
class FilterModule(object):
def filters(self):
return {
'get_entity_name': get_entity_name,
}

View File

@ -27,7 +27,7 @@ ports:
web-app-mediawiki: 8004 web-app-mediawiki: 8004
web-app-mybb: 8005 web-app-mybb: 8005
yourls: 8006 yourls: 8006
mailu: 8007 web-app-mailu: 8007
web-app-elk: 8008 web-app-elk: 8008
web-app-mastodon: 8009 web-app-mastodon: 8009
web-app-pixelfed: 8010 web-app-pixelfed: 8010

View File

@ -42,7 +42,7 @@ defaults_networks:
subnet: 192.168.101.240/28 subnet: 192.168.101.240/28
web-app-matrix: web-app-matrix:
subnet: 192.168.102.0/28 subnet: 192.168.102.0/28
mailu: web-app-mailu:
# Use one of the last container ips for dns resolving so that it isn't used # Use one of the last container ips for dns resolving so that it isn't used
dns: 192.168.102.29 dns: 192.168.102.29
subnet: 192.168.102.16/28 subnet: 192.168.102.16/28

View File

@ -19,7 +19,7 @@ defaults_service_provider:
bluesky: >- bluesky: >-
{{ ('@' ~ users.contact.username ~ '.' ~ domains.bluesky.api) {{ ('@' ~ users.contact.username ~ '.' ~ domains.bluesky.api)
if 'bluesky' in group_names else '' }} if 'bluesky' in group_names else '' }}
email: "{{ users.contact.username ~ '@' ~ primary_domain if 'mailu' in group_names else '' }}" email: "{{ users.contact.username ~ '@' ~ primary_domain if 'web-app-mailu' in group_names else '' }}"
mastodon: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-mastodon') if 'web-app-mastodon' in group_names else '' }}" mastodon: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-mastodon') if 'web-app-mastodon' in group_names else '' }}"
matrix: "{{ '@' ~ users.contact.username ~ ':' ~ domains['web-app-matrix'].synapse if 'web-app-matrix' in group_names else '' }}" matrix: "{{ '@' ~ users.contact.username ~ ':' ~ domains['web-app-matrix'].synapse if 'web-app-matrix' in group_names else '' }}"
peertube: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-peertube') if 'web-app-peertube' in group_names else '' }}" peertube: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-peertube') if 'web-app-peertube' in group_names else '' }}"

View File

@ -13,9 +13,9 @@
- database:/var/lib/mysql - database:/var/lib/mysql
healthcheck: healthcheck:
test: [ "CMD", "sh", "-c", "/usr/bin/mariadb --user=$$MYSQL_USER --password=$$MYSQL_PASSWORD --execute 'SHOW DATABASES;'" ] test: [ "CMD", "sh", "-c", "/usr/bin/mariadb --user=$$MYSQL_USER --password=$$MYSQL_PASSWORD --execute 'SHOW DATABASES;'" ]
interval: 3s interval: 10s
timeout: 1s timeout: 5s
retries: 5 retries: 18
networks: networks:
- default - default
{% endif %} {% endif %}

View File

@ -1,14 +1,17 @@
# Helper variables # Helper variables
_database_id: "svc-db-{{ database_type }}" _database_id: "svc-db-{{ database_type }}"
_database_central_name: "{{ applications | get_app_conf( _database_id, 'docker.services.' ~ database_type ~ '.name') }}" _database_central_name: "{{ applications | get_app_conf( _database_id, 'docker.services.' ~ database_type ~ '.name') }}"
_database_consumer_public_id: "{{ database_application_id | get_public_id }}"
_database_central_enabled: "{{ applications | get_app_conf(database_application_id, 'features.central_database', False) }}"
# Definition # Definition
database_name: "{{ applications | get_app_conf( database_application_id, 'database.name', false, database_application_id | get_public_id ) }}" # The overwritte configuration is needed by bigbluebutton database_name: "{{ applications | get_app_conf( database_application_id, 'database.name', false, _database_consumer_public_id ) }}" # The overwritte configuration is needed by bigbluebutton
database_instance: "{{ _database_central_name if applications | get_app_conf(database_application_id, 'features.central_database', False) else database_name }}" # This could lead to bugs at dedicated database @todo cleanup database_instance: "{{ _database_central_name if _database_central_enabled else database_name }}" # This could lead to bugs at dedicated database @todo cleanup
database_host: "{{ _database_central_name if applications | get_app_conf(database_application_id, 'features.central_database', False) else 'database' }}" # This could lead to bugs at dedicated database @todo cleanup database_host: "{{ _database_central_name if _database_central_enabled else 'database' }}" # This could lead to bugs at dedicated database @todo cleanup
database_username: "{{ applications | get_app_conf(database_application_id, 'database.username', false, database_application_id | get_public_id)}}" # The overwritte configuration is needed by bigbluebutton database_username: "{{ applications | get_app_conf(database_application_id, 'database.username', false, _database_consumer_public_id)}}" # The overwritte configuration is needed by bigbluebutton
database_password: "{{ applications | get_app_conf(database_application_id, 'credentials.database_password', true) }}" database_password: "{{ applications | get_app_conf(database_application_id, 'credentials.database_password', true) }}"
database_port: "{{ ports.localhost.database[ _database_id ] }}" database_port: "{{ ports.localhost.database[ _database_id ] }}"
database_env: "{{docker_compose.directories.env}}{{database_type}}.env" database_env: "{{docker_compose.directories.env}}{{database_type}}.env"
database_url_jdbc: "jdbc:{{ database_type if database_type == 'mariadb' else 'postgresql' }}://{{ database_host }}:{{ database_port }}/{{ database_name }}" database_url_jdbc: "jdbc:{{ database_type if database_type == 'mariadb' else 'postgresql' }}://{{ database_host }}:{{ database_port }}/{{ database_name }}"
database_url_full: "{{database_type}}://{{database_username}}:{{database_password}}@{{database_host}}:{{database_port}}/{{ database_name }}" database_url_full: "{{database_type}}://{{database_username}}:{{database_password}}@{{database_host}}:{{database_port}}/{{ database_name }}"
database_volume: "{{ _database_consumer_public_id ~ '_' if not _database_central_enabled }}{{ database_host }}"

View File

@ -2,5 +2,6 @@
{% if not applications | get_app_conf(application_id, 'features.central_database', False)%} {% if not applications | get_app_conf(application_id, 'features.central_database', False)%}
volumes: volumes:
database: database:
name: {{ database_volume }}
{% endif %} {% endif %}
{{ "\n" }} {{ "\n" }}

View File

@ -2,5 +2,6 @@
volumes: volumes:
{% if not applications | get_app_conf(application_id, 'features.central_database', False)%} {% if not applications | get_app_conf(application_id, 'features.central_database', False)%}
database: database:
name: {{ database_volume }}
{% endif %} {% endif %}
{{ "\n" }} {{ "\n" }}

View File

@ -0,0 +1,2 @@
# Todos
- Add cleanup service for docker system prune -f

View File

@ -24,9 +24,9 @@
restart_policy: "{{ docker_restart_policy }}" restart_policy: "{{ docker_restart_policy }}"
healthcheck: healthcheck:
test: "/usr/bin/mariadb --user=root --password={{ mariadb_root_pwd }} --execute \"SHOW DATABASES;\"" test: "/usr/bin/mariadb --user=root --password={{ mariadb_root_pwd }} --execute \"SHOW DATABASES;\""
interval: 3s interval: 10s
timeout: 1s timeout: 5s
retries: 5 retries: 18
when: run_once_docker_mariadb is not defined when: run_once_docker_mariadb is not defined
register: setup_mariadb_container_result register: setup_mariadb_container_result

View File

@ -3,7 +3,7 @@
{% set redis_version = applications | get_app_conf('svc-db-redis', 'docker.services.redis.version')%} {% set redis_version = applications | get_app_conf('svc-db-redis', 'docker.services.redis.version')%}
redis: redis:
image: "{{ redis_image }}:{{ redis_version }}" image: "{{ redis_image }}:{{ redis_version }}"
container_name: {{ application_id }}-redis container_name: {{ application_id | get_public_id }}-redis
restart: {{ docker_restart_policy }} restart: {{ docker_restart_policy }}
logging: logging:
driver: journald driver: journald

View File

@ -43,7 +43,7 @@ for filename in os.listdir(config_path):
url = f"{{ web_protocol }}://{domain}" url = f"{{ web_protocol }}://{domain}"
redirected_domains = [domain['source'] for domain in {{ current_play_domain_mappings_redirect}}] redirected_domains = [domain['source'] for domain in {{ current_play_domain_mappings_redirect}}]
redirected_domains.append("{{domains | get_domain('mailu')}}") redirected_domains.append("{{domains | get_domain('web-app-mailu')}}")
expected_statuses = get_expected_statuses(domain, parts, redirected_domains) expected_statuses = get_expected_statuses(domain, parts, redirected_domains)

View File

@ -43,7 +43,7 @@ if __name__ == "__main__":
if os.path.isfile(docker_compose_file): if os.path.isfile(docker_compose_file):
print(f"Found docker-compose.yml in {dir_path}.") print(f"Found docker-compose.yml in {dir_path}.")
if dir_name == "mailu": if dir_name == "web-app-mailu":
print(f"Directory {dir_name} detected. Performing hard restart...") print(f"Directory {dir_name} detected. Performing hard restart...")
hard_restart_docker_services(dir_path) hard_restart_docker_services(dir_path)
else: else:

View File

@ -10,7 +10,7 @@
domain: "{{ item }}" domain: "{{ item }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
loop: loop:
- "{{ domains | get_domain('mailu') }}" - "{{ domains | get_domain('web-app-mailu') }}"
- "{{ domain }}" - "{{ domain }}"
- name: "For '{{ application_id }}': configure {{domains | get_domain(application_id)}}.conf" - name: "For '{{ application_id }}': configure {{domains | get_domain(application_id)}}.conf"

View File

@ -0,0 +1,32 @@
- name: Check if config.php exists in EspoCRM
command: docker exec --user root {{ espocrm_name }} test -f {{ espocrm_config_file }}
register: config_file_exists
changed_when: false
failed_when: false
- name: Patch EspoCRM config.php with updated DB credentials
when: config_file_exists.rc == 0
block:
- name: Update DB host
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'host' => .*/'host' => '{{ database_host }}',/" {{ espocrm_config_file }}
notify: docker compose up
- name: Update DB name
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'dbname' => .*/'dbname' => '{{ database_name }}',/" {{ espocrm_config_file }}
notify: docker compose up
- name: Update DB user
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'user' => .*/'user' => '{{ database_username }}',/" {{ espocrm_config_file }}
notify: docker compose up
- name: Update DB password
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'password' => .*/'password' => '{{ database_password }}',/" {{ espocrm_config_file }}
notify: docker compose up

View File

@ -3,6 +3,13 @@
include_role: include_role:
name: cmp-db-docker-proxy name: cmp-db-docker-proxy
- name: Update database credentials
include_tasks: database.yml
- name: Flush handlers to make DB available before password reset
meta: flush_handlers
when: docker_compose_flush_handlers | bool
- name: Set OIDC scopes in EspoCRM config (inside web container) - name: Set OIDC scopes in EspoCRM config (inside web container)
ansible.builtin.shell: | ansible.builtin.shell: |
docker compose exec -T web php -r ' docker compose exec -T web php -r '

View File

@ -9,3 +9,4 @@ espocrm_version: "{{ applications | get_app_conf(application_id,
espocrm_image: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.image', True) }}" espocrm_image: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.image', True) }}"
espocrm_name: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.name', True) }}" espocrm_name: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.name', True) }}"
espocrm_volume: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}" espocrm_volume: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}"
espocrm_config_file: "/var/www/html/data/config-internal.php"

View File

@ -0,0 +1,2 @@
# Todos
- Implement hard restart into Backup for mailu

View File

@ -1,4 +1,3 @@
version: "2024.06" # Docker Image Version
oidc: oidc:
email_by_username: true # If true, then the mail is set by the username. If wrong then the OIDC user email is used email_by_username: true # If true, then the mail is set by the username. If wrong then the OIDC user email is used
enable_user_creation: true # Users will be created if not existing enable_user_creation: true # Users will be created if not existing
@ -31,3 +30,6 @@ docker:
enabled: true enabled: true
database: database:
enabled: true enabled: true
mailu:
version: "2024.06" # Docker Image Version
name: mailu

View File

@ -19,7 +19,7 @@
mailu_compose_dir: "{{ docker_compose.directories.instance }}" mailu_compose_dir: "{{ docker_compose.directories.instance }}"
mailu_domain: "{{ primary_domain }}" mailu_domain: "{{ primary_domain }}"
mailu_api_base_url: "http://127.0.0.1:8080/api/v1" mailu_api_base_url: "http://127.0.0.1:8080/api/v1"
mailu_global_api_token: "{{ applications.mailu.credentials.api_token }}" mailu_global_api_token: "{{ applications | get_app_conf(application_id, 'credentials.api_token') }}"
mailu_action: >- mailu_action: >-
{{ {{
( (

View File

@ -2,13 +2,15 @@
# Core services # Core services
resolver: resolver:
image: {{docker_source}}/unbound:{{applications.mailu.version}} image: {{docker_source}}/unbound:{{ mailu_version }}
container_name: {{mailu_name}}_resolver
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
ipv4_address: {{networks.local.mailu.dns}} ipv4_address: {{networks.local['web-app-mailu'].dns}}
front: front:
image: {{docker_source}}/nginx:{{applications.mailu.version}} container_name: {{mailu_name}}_front
image: {{docker_source}}/nginx:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
ports: ports:
- "127.0.0.1:{{ports.localhost.http[application_id]}}:80" - "127.0.0.1:{{ports.localhost.http[application_id]}}:80"
@ -30,10 +32,11 @@
webmail: webmail:
radicale: radicale:
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
admin: admin:
image: {{docker_source}}/admin:{{applications.mailu.version}} container_name: {{mailu_name}}_admin
image: {{docker_source}}/admin:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "admin_data:/data" - "admin_data:/data"
@ -44,11 +47,12 @@
front: front:
condition: service_started condition: service_started
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
imap: imap:
image: {{docker_source}}/dovecot:{{applications.mailu.version}} container_name: {{mailu_name}}_imap
image: {{docker_source}}/dovecot:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "dovecot_mail:/mail" - "dovecot_mail:/mail"
@ -57,11 +61,12 @@
- front - front
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
smtp: smtp:
image: {{docker_source}}/postfix:{{applications.mailu.version}} container_name: {{mailu_name}}_smtp
image: {{docker_source}}/postfix:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro" - "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
@ -70,22 +75,24 @@
- front - front
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
oletools: oletools:
image: {{docker_source}}/oletools:{{applications.mailu.version}} container_name: {{mailu_name}}_oletools
image: {{docker_source}}/oletools:{{ mailu_version }}
hostname: oletools hostname: oletools
restart: {{docker_restart_policy}} restart: {{docker_restart_policy}}
depends_on: depends_on:
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
noinet: noinet:
antispam: antispam:
image: {{docker_source}}/rspamd:{{applications.mailu.version}} container_name: {{mailu_name}}_antispam
image: {{docker_source}}/rspamd:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "filter:/var/lib/rspamd" - "filter:/var/lib/rspamd"
@ -97,13 +104,14 @@
- antivirus - antivirus
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
noinet: noinet:
# Optional services # Optional services
antivirus: antivirus:
container_name: {{mailu_name}}_antivirus
image: clamav/clamav-debian:latest image: clamav/clamav-debian:latest
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
@ -111,23 +119,25 @@
depends_on: depends_on:
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
webdav: webdav:
image: {{docker_source}}/radicale:{{applications.mailu.version}} container_name: {{mailu_name}}_webdav
image: {{docker_source}}/radicale:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "webdav_data:/data" - "webdav_data:/data"
depends_on: depends_on:
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
radicale: radicale:
fetchmail: fetchmail:
image: {{docker_source}}/fetchmail:{{applications.mailu.version}} container_name: {{mailu_name}}_fetchmail
image: {{docker_source}}/fetchmail:{{ mailu_version }}
volumes: volumes:
- "admin_data:/data" - "admin_data:/data"
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
@ -137,11 +147,12 @@
- imap - imap
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
webmail: webmail:
image: {{docker_source}}/webmail:{{applications.mailu.version}} container_name: {{mailu_name}}_webmail
image: {{docker_source}}/webmail:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "webmail_data:/data" - "webmail_data:/data"
@ -151,19 +162,27 @@
- front - front
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
webmail: webmail:
{% include 'roles/docker-compose/templates/volumes.yml.j2' %} {% include 'roles/docker-compose/templates/volumes.yml.j2' %}
smtp_queue: smtp_queue:
name: {{ mailu_smtp_queue }}
admin_data: admin_data:
name: {{ mailu_admin_data }}
webdav_data: webdav_data:
name: {{ mailu_webdav_data }}
webmail_data: webmail_data:
name: {{ mailu_webmail_data }}
filter: filter:
name: {{ mailu_filter }}
dkim: dkim:
name: {{ mailu_dkim }}
dovecot_mail: dovecot_mail:
name: {{ mailu_dovecot_mail }}
redis: redis:
name: {{ mailu_redis }}
{% include 'roles/docker-compose/templates/networks.yml.j2' %} {% include 'roles/docker-compose/templates/networks.yml.j2' %}
radicale: radicale:

View File

@ -11,13 +11,13 @@
LD_PRELOAD=/usr/lib/libhardened_malloc.so LD_PRELOAD=/usr/lib/libhardened_malloc.so
# Set to a randomly generated 16 bytes string # Set to a randomly generated 16 bytes string
SECRET_KEY={{applications.mailu.credentials.secret_key}} SECRET_KEY={{applications | get_app_conf(application_id,'credentials.secret_key')}}
# Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!) # Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)
SUBNET={{networks.local.mailu.subnet}} SUBNET={{networks.local['web-app-mailu'].subnet}}
# Main mail domain # Main mail domain
DOMAIN={{applications.mailu.domain}} DOMAIN={{ applications | get_app_conf(application_id,'domain') }}
# Hostnames for this server, separated with comas # Hostnames for this server, separated with comas
HOSTNAMES={{domains | get_domain(application_id)}} HOSTNAMES={{domains | get_domain(application_id)}}
@ -151,7 +151,7 @@ SQLALCHEMY_DATABASE_URI=mysql+mysqlconnector://{{database_username}}:{{database_
API=true API=true
WEB_API=/api WEB_API=/api
# Configures the authentication token. The minimum length is 3 characters. This token must be passed as request header to the API as authentication token. This is a mandatory setting for using the RESTful API. # Configures the authentication token. The minimum length is 3 characters. This token must be passed as request header to the API as authentication token. This is a mandatory setting for using the RESTful API.
API_TOKEN={{applications.mailu.credentials.api_token}} API_TOKEN={{ applications | get_app_conf(application_id, 'credentials.api_token')}}
# Activated https://mailu.io/master/configuration.html#advanced-settings # Activated https://mailu.io/master/configuration.html#advanced-settings

View File

@ -1,7 +1,7 @@
application_id: "mailu" application_id: "web-app-mailu"
# Database Configuration # Database Configuration
database_password: "{{applications.mailu.credentials.database_password}}" database_password: "{{ applications | get_app_conf(application_id, ' credentials.database_password') }}"
database_type: "mariadb" database_type: "mariadb"
cert_mount_directory: "{{docker_compose.directories.volumes}}certs/" cert_mount_directory: "{{docker_compose.directories.volumes}}certs/"
@ -13,3 +13,13 @@ docker_source: "{{ 'ghcr.io/heviat' if applications | get_app_conf(a
domain: "{{ domains | get_domain(application_id) }}" domain: "{{ domains | get_domain(application_id) }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
proxy_extra_configuration: "client_max_body_size 31M;" proxy_extra_configuration: "client_max_body_size 31M;"
mailu_version: "{{ applications | get_app_conf(application_id, 'docker.services.mailu.version', True) }}"
mailu_name: "{{ applications | get_app_conf(application_id, 'docker.services.mailu.name', True) }}"
mailu_smtp_queue: "mailu_smtp_queue"
mailu_admin_data: "mailu_admin_data"
mailu_webdav_data: "mailu_webdav_data"
mailu_webmail_data: "mailu_webmail_data"
mailu_filter: "mailu_filter"
mailu_dkim: "mailu_dkim"
mailu_dovecot_mail: "mailu_dovecot_mail"
mailu_redis: "mailu_redis"

View File

@ -2,7 +2,7 @@
include_role: include_role:
name: cmp-db-docker-proxy name: cmp-db-docker-proxy
- name: "Update database credentials" - name: "Patch Matomo config.ini.php with updated DB credentials"
include_tasks: database.yml include_tasks: database.yml
- name: flush docker service - name: flush docker service

View File

@ -1,25 +1,16 @@
- name: Backup config.ini.php before patching - name: Update DB host
command: >
docker cp {{ matomo_name }}:{{ matomo_config }} {{ matomo_backup_file }}
- name: Patch Matomo config.ini.php with updated DB credentials
block:
- name: Update DB host
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}
sed -i "s/^host *=.*/host = {{ database_host }}/" {{ matomo_config }} sed -i "s/^host *=.*/host = {{ database_host }}/" {{ matomo_config }}
- name: Update DB name
- name: Update DB name
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}
sed -i "s/^dbname *=.*/dbname = {{ database_name }}/" {{ matomo_config }} sed -i "s/^dbname *=.*/dbname = {{ database_name }}/" {{ matomo_config }}
- name: Update DB user
- name: Update DB user
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}
sed -i "s/^username *=.*/username = {{ database_username }}/" {{ matomo_config }} sed -i "s/^username *=.*/username = {{ database_username }}/" {{ matomo_config }}
- name: Update DB password
- name: Update DB password
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}
sed -i "s/^password *=.*/password = {{ database_password }}/" {{ matomo_config }} sed -i "s/^password *=.*/password = {{ database_password }}/" {{ matomo_config }}

View File

@ -8,7 +8,6 @@ matomo_version: "{{ applications | get_app_conf(application_id, 'docker.se
matomo_image: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.image', True) }}" matomo_image: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.image', True) }}"
matomo_name: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.name', True) }}" matomo_name: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.name', True) }}"
matomo_data: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}" matomo_data: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}"
matomo_backup_file: "{{ docker_compose.directories.instance }}/config.ini.php.bak"
matomo_config: "/var/www/html/config/config.ini.php" matomo_config: "/var/www/html/config/config.ini.php"
# I don't know if this is still necessary # I don't know if this is still necessary

View File

@ -13,10 +13,6 @@
state: directory state: directory
mode: "0755" mode: "0755"
- name: Copy config.php from container to host
command: >
docker cp {{ moodle_container }}:{{ moodle_config }} {{ moodle_backup_file }}
- name: Check if config.php exists - name: Check if config.php exists
command: docker exec --user root {{ moodle_container }} test -f {{ moodle_config }} command: docker exec --user root {{ moodle_container }} test -f {{ moodle_config }}
register: config_file_exists register: config_file_exists

View File

@ -10,7 +10,6 @@ bitnami_user_group: "{{ bitnami_user }}:{{ bitnami_user }}"
docker_compose_flush_handlers: false # Wait for env update docker_compose_flush_handlers: false # Wait for env update
moodle_backup_file: "{{ docker_compose.directories.instance }}/config.ini.php.bak"
moodle_config: "/bitnami/moodle/config.php" moodle_config: "/bitnami/moodle/config.php"
moodle_version: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.version', True) }}" moodle_version: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.version', True) }}"
moodle_image: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.image', True) }}" moodle_image: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.image', True) }}"

View File

@ -10,9 +10,12 @@ csp:
- "data:" - "data:"
domains: domains:
canonical: canonical:
nextcloud: "cloud.{{ primary_domain }}" - "cloud.{{ primary_domain }}"
# nextcloud: "cloud.{{ primary_domain }}"
# talk: "talk.{{ primary_domain }}" @todo needs to be activated # talk: "talk.{{ primary_domain }}" @todo needs to be activated
docker: docker:
volumes:
data: nextcloud_data
services: services:
redis: redis:
enabled: true enabled: true
@ -21,7 +24,7 @@ docker:
nextcloud: nextcloud:
name: "nextcloud" name: "nextcloud"
image: "nextcloud" image: "nextcloud"
version: "latest-fpm-alpine" version: "production-fpm-alpine"
backup: backup:
no_stop_required: true no_stop_required: true
proxy: proxy:

View File

@ -3,16 +3,27 @@
- name: Add dynamic config merging from Jinja template - name: Add dynamic config merging from Jinja template
template: template:
src: include.php.j2 src: include.php.j2
dest: "{{nextcloud_host_include_instructions_file}}" dest: "{{ nextcloud_host_include_instructions_file }}"
notify: docker compose restart notify: docker compose restart
- name: Flush handlers so Nextcloud container is restarted and ready
meta: flush_handlers
- name: "Wait until Nextcloud is reachable on port {{ports.localhost.http[application_id]}}"
wait_for:
host: 127.0.0.1
port: "{{ports.localhost.http[application_id]}}"
timeout: 120
delay: 2
state: started
- name: Copy include instructions to the container - name: Copy include instructions to the container
command: > command: >
docker cp {{ nextcloud_host_include_instructions_file }} {{ nextcloud_name }}:{{nextcloud_docker_include_instructions_file}} docker cp {{ nextcloud_host_include_instructions_file }} {{ nextcloud_container }}:{{ nextcloud_docker_include_instructions_file }}
- name: Append generated config to config.php only if not present - name: Append generated config to config.php only if not present
command: > command: >
docker exec -u {{nextcloud_docker_user}} {{ nextcloud_name }} sh -c " docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_container }} sh -c "
grep -q '{{ nextcloud_docker_config_additives_directory }}' {{ nextcloud_docker_config_file }} || grep -q '{{ nextcloud_docker_config_additives_directory }}' {{ nextcloud_docker_config_file }} ||
cat {{nextcloud_docker_include_instructions_file}} >> {{ nextcloud_docker_config_file }}" cat {{ nextcloud_docker_include_instructions_file }} >> {{ nextcloud_docker_config_file }}"
notify: docker compose restart notify: docker compose restart

View File

@ -65,7 +65,7 @@
- name: Ensure Nextcloud administrator is in the 'admin' group - name: Ensure Nextcloud administrator is in the 'admin' group
command: > command: >
docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_name }} docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_container }}
php occ group:adduser admin {{ nextcloud_administrator_username }} php occ group:adduser admin {{ nextcloud_administrator_username }}
register: add_admin_to_group register: add_admin_to_group
changed_when: "'Added user' in add_admin_to_group.stdout" changed_when: "'Added user' in add_admin_to_group.stdout"

View File

@ -2,7 +2,7 @@
application: application:
image: "{{ nextcloud_image }}:{{ nextcloud_version }}" image: "{{ nextcloud_image }}:{{ nextcloud_version }}"
container_name: {{ nextcloud_name }} container_name: {{ nextcloud_container }}
volumes: volumes:
- data:{{nextcloud_docker_work_directory}} - data:{{nextcloud_docker_work_directory}}
- {{nextcloud_host_config_additives_directory}}:{{nextcloud_docker_config_additives_directory}}:ro - {{nextcloud_host_config_additives_directory}}:{{nextcloud_docker_config_additives_directory}}:ro
@ -70,6 +70,7 @@
{% include 'roles/docker-compose/templates/volumes.yml.j2' %} {% include 'roles/docker-compose/templates/volumes.yml.j2' %}
data: data:
name: {{ nextcloud_volume }}
redis: redis:
{% include 'roles/docker-compose/templates/networks.yml.j2' %} {% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@ -20,16 +20,18 @@ nextcloud_control_node_plugin_tasks_directory: "{{role_path}}/tasks/plugins/"
# Host # Host
## Host Paths ## Host Paths
nextcloud_host_config_additives_directory: "{{docker_compose.directories.volumes}}cymais/" # This folder is the path to which the additive configurations will be copied nextcloud_host_config_additives_directory: "{{ docker_compose.directories.volumes }}cymais/" # This folder is the path to which the additive configurations will be copied
nextcloud_host_include_instructions_file: "{{docker_compose.directories.volumes}}includes.php" # Path to the instruction file on the host. Responsible for loading the additional configurations nextcloud_host_include_instructions_file: "{{ docker_compose.directories.volumes }}includes.php" # Path to the instruction file on the host. Responsible for loading the additional configurations
nextcloud_domains: "{{ domains[application_id].nextcloud }}" nextcloud_domains: "{{ domains | get_domain(application_id) }}" # This is wrong and should be optimized @todo implement support for multiple domains
# Docker # Docker
nextcloud_volume: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}"
nextcloud_version: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.version', True) }}" nextcloud_version: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.version', True) }}"
nextcloud_image: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.image', True) }}" nextcloud_image: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.image', True) }}"
nextcloud_name: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.name', True) }}" nextcloud_container: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.name', True) }}"
nextcloud_proxy_name: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.name', True) }}" nextcloud_proxy_name: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.name', True) }}"
nextcloud_proxy_image: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.image', True) }}" nextcloud_proxy_image: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.image', True) }}"
@ -58,5 +60,5 @@ nextcloud_docker_config_additives_directory: "{{nextcloud_docker_config_direc
nextcloud_docker_include_instructions_file: "/tmp/includes.php" # Path to the temporary file which will be included to the config.php to load the additional configurations nextcloud_docker_include_instructions_file: "/tmp/includes.php" # Path to the temporary file which will be included to the config.php to load the additional configurations
## Execution ## Execution
nextcloud_docker_exec: "docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_name }}" # General execute composition nextcloud_docker_exec: "docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_container }}" # General execute composition
nextcloud_docker_exec_occ: "{{nextcloud_docker_exec}} {{ nextcloud_docker_work_directory }}occ" # Execute docker occ command nextcloud_docker_exec_occ: "{{nextcloud_docker_exec}} {{ nextcloud_docker_work_directory }}occ" # Execute docker occ command

View File

@ -3,7 +3,7 @@ followus:
description: Follow us to stay up to recieve the newest CyMaIS updates description: Follow us to stay up to recieve the newest CyMaIS updates
icon: icon:
class: fas fa-newspaper class: fas fa-newspaper
{% if ["mastodon", "bluesky"] | any_in(group_names) %} {% if ["web-app-mastodon", "web-app-bluesky"] | any_in(group_names) %}
children: children:
{% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %} {% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %}
- name: Mastodon - name: Mastodon

View File

@ -6,7 +6,7 @@
- name: "Include role srv-proxy-6-6-domain for {{ application_id }}" - name: "Include role srv-proxy-6-6-domain for {{ application_id }}"
include_role: include_role:
name: srv-proxy-6-6-domain name: srv-proxy-6-6-domain
loop: "{{ applications | get_app_conf(application_id, 'domain', True)s.canonical }}" loop: "{{ applications | get_app_conf(application_id, 'domains.canonical', True) }}"
loop_control: loop_control:
loop_var: domain loop_var: domain
vars: vars:

View File

@ -0,0 +1,118 @@
import os
import shutil
import tempfile
import yaml
import unittest
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
class TestGetAllInvokableApps(unittest.TestCase):
def setUp(self):
"""Create a temporary roles/ directory with categories.yml and some example roles."""
self.test_dir = tempfile.mkdtemp(prefix="invokable_apps_test_")
self.roles_dir = os.path.join(self.test_dir, "roles")
os.makedirs(self.roles_dir, exist_ok=True)
self.categories_file = os.path.join(self.roles_dir, "categories.yml")
# Write a categories.yml with nested invokable/non-invokable paths
categories = {
"roles": {
"web": {
"title": "Web",
"invokable": False,
"app": {
"title": "Applications",
"invokable": True
},
"svc": {
"title": "Services",
"invokable": False
}
},
"update": {
"title": "Update",
"invokable": True
},
"util": {
"title": "Utils",
"invokable": False,
"desk": {
"title": "Desktop Utils",
"invokable": True
}
}
}
}
with open(self.categories_file, 'w') as f:
yaml.safe_dump(categories, f)
# Create roles: some should match invokable paths, some shouldn't
roles = [
('web-app-nextcloud', 'web-app-nextcloud'),
('web-app-matomo', 'matomo-app'), # application_id differs
('web-svc-nginx', None), # should NOT match any invokable path
('update', None), # exact match to invokable path
('util-desk-custom', None) # matches util-desk
]
for rolename, appid in roles:
role_dir = os.path.join(self.roles_dir, rolename)
os.makedirs(os.path.join(role_dir, 'vars'), exist_ok=True)
vars_path = os.path.join(role_dir, 'vars', 'main.yml')
data = {}
if appid:
data['application_id'] = appid
with open(vars_path, 'w') as f:
yaml.safe_dump(data, f)
def tearDown(self):
"""Clean up the temporary test directory after each test."""
shutil.rmtree(self.test_dir)
def test_get_all_invokable_apps(self):
"""Should return only applications whose role paths match invokable paths."""
result = get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
expected = sorted([
'web-app-nextcloud', # application_id from role
'matomo-app', # application_id from role
'update', # role directory name
'util-desk-custom' # role directory name
])
self.assertEqual(sorted(result), expected)
def test_empty_when_no_invokable(self):
"""Should return an empty list if there are no invokable paths in categories.yml."""
with open(self.categories_file, 'w') as f:
yaml.safe_dump({"roles": {"foo": {"invokable": False}}}, f)
result = get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
self.assertEqual(result, [])
def test_empty_when_no_roles(self):
"""Should return an empty list if there are no roles, but categories.yml exists."""
shutil.rmtree(self.roles_dir)
os.makedirs(self.roles_dir, exist_ok=True)
# Recreate categories.yml after removing roles_dir
with open(self.categories_file, 'w') as f:
yaml.safe_dump({"roles": {"web": {"app": {"invokable": True}}}}, f)
result = get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
self.assertEqual(result, [])
def test_error_when_no_categories_file(self):
"""Should raise FileNotFoundError if categories.yml is missing."""
os.remove(self.categories_file)
with self.assertRaises(FileNotFoundError):
get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,93 @@
import unittest
import tempfile
import shutil
import os
import sys
import yaml
class TestGetEntityNameFilter(unittest.TestCase):
def setUp(self):
# Create a temporary directory for roles and categories.yml
self.temp_dir = tempfile.mkdtemp()
self.roles_dir = os.path.join(self.temp_dir, 'roles')
os.makedirs(self.roles_dir)
self.categories_file = os.path.join(self.roles_dir, 'categories.yml')
# Minimal categories.yml for tests
categories = {
'roles': {
'web': {
'app': {
'title': "Applications",
'invokable': True
},
'svc': {
'title': "Services",
'invokable': True
}
},
'util': {
'desk': {
'dev': {
'title': "Dev Utilities",
'invokable': True
}
}
},
'sys': {
'bkp': {
'title': "Backup",
'invokable': True
},
'hlth': {
'title': "Health",
'invokable': True
}
}
}
}
with open(self.categories_file, 'w', encoding='utf-8') as f:
yaml.safe_dump(categories, f, default_flow_style=False)
# Patch working directory so plugin finds the test categories.yml
self._cwd = os.getcwd()
os.chdir(self.temp_dir)
# Make sure filter_plugins directory is on sys.path
plugin_path = os.path.join(self._cwd, "filter_plugins")
if plugin_path not in sys.path and os.path.isdir(plugin_path):
sys.path.insert(0, plugin_path)
# Import plugin fresh each time
global get_entity_name
from filter_plugins.get_entity_name import get_entity_name
self.get_entity_name = get_entity_name
def tearDown(self):
os.chdir(self._cwd)
shutil.rmtree(self.temp_dir)
def test_entity_name_web_app(self):
self.assertEqual(self.get_entity_name("web-app-snipe-it"), "snipe-it")
self.assertEqual(self.get_entity_name("web-app-nextcloud"), "nextcloud")
self.assertEqual(self.get_entity_name("web-svc-file"), "file")
def test_entity_name_util_desk_dev(self):
self.assertEqual(self.get_entity_name("util-desk-dev-arduino"), "arduino")
self.assertEqual(self.get_entity_name("util-desk-dev-shell"), "shell")
def test_entity_name_sys_bkp(self):
self.assertEqual(self.get_entity_name("sys-bkp-directory-validator"), "directory-validator")
def test_entity_name_sys_hlth(self):
self.assertEqual(self.get_entity_name("sys-hlth-btrfs"), "btrfs")
def test_no_category_match(self):
# Unknown category, should return input
self.assertEqual(self.get_entity_name("foobar-role"), "foobar-role")
def test_exact_category_match(self):
self.assertEqual(self.get_entity_name("web-app"), "")
if __name__ == "__main__":
unittest.main()