diff --git a/roles/svc-db-memcached/README.md b/roles/svc-db-memcached/README.md new file mode 100644 index 00000000..1d633f8e --- /dev/null +++ b/roles/svc-db-memcached/README.md @@ -0,0 +1,11 @@ +# Memcached + +## Description + +This Ansible role provides a Jinja2 snippet to inject a Memcached service definition into your Docker Compose setup. + +## Further Resources + +- [Official Memcached Docker image on Docker Hub](https://hub.docker.com/_/memcached) +- [Memcached official documentation](https://memcached.org/) +- [Docker Compose reference](https://docs.docker.com/compose/compose-file/) diff --git a/roles/svc-db-memcached/config/main.yml b/roles/svc-db-memcached/config/main.yml new file mode 100644 index 00000000..085fa5fd --- /dev/null +++ b/roles/svc-db-memcached/config/main.yml @@ -0,0 +1,7 @@ +docker: + services: + memcached: + image: memcached + version: latest + backup: + enabled: false \ No newline at end of file diff --git a/roles/svc-db-memcached/meta/main.yml b/roles/svc-db-memcached/meta/main.yml new file mode 100644 index 00000000..54a483c5 --- /dev/null +++ b/roles/svc-db-memcached/meta/main.yml @@ -0,0 +1,17 @@ +galaxy_info: + author: "Kevin Veen-Birkenbach" + description: "Provides a Docker Compose snippet for a Memcached service (`memcached`) with optional volume, healthcheck, and logging." + license: "CyMaIS NonCommercial License (CNCL)" + license_url: "https://s.veen.world/cncl" + company: | + Kevin Veen-Birkenbach + Consulting & Coaching Solutions + https://www.veen.world + galaxy_tags: + - memcached + - docker + - cache + repository: "https://github.com/kevinveenbirkenbach/cymais" + issue_tracker_url: "https://github.com/kevinveenbirkenbach/cymais/issues" + documentation: "https://github.com/kevinveenbirkenbach/cymais/tree/main/roles/svc-db-memcached" +dependencies: [] diff --git a/roles/svc-db-memcached/vars/main.yml b/roles/svc-db-memcached/vars/main.yml new file mode 100644 index 00000000..5aa39898 --- /dev/null +++ b/roles/svc-db-memcached/vars/main.yml @@ -0,0 +1 @@ +application_id: svc-db-memcached \ No newline at end of file diff --git a/roles/svc-db-redis/README.md b/roles/svc-db-redis/README.md index bfcd4e2d..d77ffa93 100644 --- a/roles/svc-db-redis/README.md +++ b/roles/svc-db-redis/README.md @@ -1,4 +1,4 @@ -# Role: svc-db-redis +# Redis ## Description diff --git a/roles/svc-db-redis/config/main.yml b/roles/svc-db-redis/config/main.yml new file mode 100644 index 00000000..dddc5645 --- /dev/null +++ b/roles/svc-db-redis/config/main.yml @@ -0,0 +1,7 @@ +docker: + services: + redis: + image: redis + version: alpine + backup: + enabled: false \ No newline at end of file diff --git a/roles/svc-db-redis/templates/service.yml.j2 b/roles/svc-db-redis/templates/service.yml.j2 index b725d3f5..e97a13be 100644 --- a/roles/svc-db-redis/templates/service.yml.j2 +++ b/roles/svc-db-redis/templates/service.yml.j2 @@ -1,8 +1,10 @@ # This template needs to be included in docker-compose.yml, which depend on redis +{% set redis_image = applications | get_app_conf('svc-db-redis', 'docker.services.redis.image') %} +{% set redis_version = applications | get_app_conf('svc-db-redis', 'docker.services.redis.version')%} redis: - image: redis:alpine - container_name: {{application_id}}-redis - restart: {{docker_restart_policy}} + image: "{{ redis_image }}:{{ redis_version }}" + container_name: {{ application_id }}-redis + restart: {{ docker_restart_policy }} logging: driver: journald volumes: diff --git a/roles/svc-db-redis/vars/main.yml b/roles/svc-db-redis/vars/main.yml index 555e1016..16adfcf3 100644 --- a/roles/svc-db-redis/vars/main.yml +++ b/roles/svc-db-redis/vars/main.yml @@ -1 +1 @@ -application_id: redis \ No newline at end of file +application_id: svc-db-redis \ No newline at end of file diff --git a/roles/web-app-akaunting/config/main.yml b/roles/web-app-akaunting/config/main.yml index bc45b9c6..2be2a1d3 100644 --- a/roles/web-app-akaunting/config/main.yml +++ b/roles/web-app-akaunting/config/main.yml @@ -15,10 +15,11 @@ docker: database: enabled: true akaunting: - no_stop_required: true - image: docker.io/akaunting/akaunting + backup: + no_stop_required: true + image: docker.io/akaunting/akaunting version: latest - name: akaunting + name: akaunting volumes: data: akaunting_data credentials: {} diff --git a/roles/web-app-baserow/config/main.yml b/roles/web-app-baserow/config/main.yml index 281f0b99..60755f7d 100644 --- a/roles/web-app-baserow/config/main.yml +++ b/roles/web-app-baserow/config/main.yml @@ -10,9 +10,10 @@ docker: database: enabled: true baserow: - no_stop_required: true - image: "baserow/baserow" - version: "latest" - name: "baserow" + backup: + no_stop_required: true + image: "baserow/baserow" + version: "latest" + name: "baserow" volumes: - data: "baserow_data" + data: "baserow_data" diff --git a/roles/web-app-discourse/config/main.yml b/roles/web-app-discourse/config/main.yml index e2a96915..dde561a7 100644 --- a/roles/web-app-discourse/config/main.yml +++ b/roles/web-app-discourse/config/main.yml @@ -29,7 +29,9 @@ docker: # @todo check this out and repair it if necessary discourse: name: "discourse" - no_stop_required: true + image: "local_discourse/discourse_application" # Necessary to define this for the docker 2 loc backup + backup: + no_stop_required: true volumes: data: discourse_data network: discourse diff --git a/roles/web-app-gitea/config/main.yml b/roles/web-app-gitea/config/main.yml index 02671ed8..e329b5db 100644 --- a/roles/web-app-gitea/config/main.yml +++ b/roles/web-app-gitea/config/main.yml @@ -42,7 +42,8 @@ docker: gitea: image: "gitea/gitea" version: "latest" - no_stop_required: true + backup: + no_stop_required: true port: 3000 name: "gitea" volumes: diff --git a/roles/web-app-listmonk/config/main.yml b/roles/web-app-listmonk/config/main.yml index 09cfe399..52874dad 100644 --- a/roles/web-app-listmonk/config/main.yml +++ b/roles/web-app-listmonk/config/main.yml @@ -15,6 +15,7 @@ docker: listmonk: image: listmonk/listmonk version: latest - no_stop_required: true + backup: + no_stop_required: true name: listmonk port: 9000 \ No newline at end of file diff --git a/roles/web-app-mastodon/config/main.yml b/roles/web-app-mastodon/config/main.yml index cfe97d49..0552e0a7 100644 --- a/roles/web-app-mastodon/config/main.yml +++ b/roles/web-app-mastodon/config/main.yml @@ -22,7 +22,8 @@ docker: mastodon: image: "ghcr.io/mastodon/mastodon" version: latest - no_stop_required: true + backup: + no_stop_required: true name: "mastodon" streaming: image: "ghcr.io/mastodon/mastodon-streaming" diff --git a/roles/web-app-matomo/config/main.yml b/roles/web-app-matomo/config/main.yml index d0ea4169..fcc4f30d 100644 --- a/roles/web-app-matomo/config/main.yml +++ b/roles/web-app-matomo/config/main.yml @@ -36,7 +36,8 @@ docker: image: "matomo" version: "latest" name: "matomo" - no_stop_required: true + backup: + no_stop_required: true database: enabled: true redis: diff --git a/roles/web-app-matrix/config/main.yml b/roles/web-app-matrix/config/main.yml index 424b9a66..6a32f4cc 100644 --- a/roles/web-app-matrix/config/main.yml +++ b/roles/web-app-matrix/config/main.yml @@ -6,7 +6,8 @@ docker: version: latest image: matrixdotorg/synapse name: matrix-synapse - no_stop_required: true + backup: + no_stop_required: true element: version: latest image: vectorim/element-web diff --git a/roles/web-app-mediawiki/config/main.yml b/roles/web-app-mediawiki/config/main.yml index ba6873e1..71315403 100644 --- a/roles/web-app-mediawiki/config/main.yml +++ b/roles/web-app-mediawiki/config/main.yml @@ -6,7 +6,8 @@ docker: mediawiki: image: mediawiki version: latest - no_stop_required: true + backup: + no_stop_required: true name: mediawiki volumes: data: mediawiki_data \ No newline at end of file diff --git a/roles/web-app-nextcloud/config/main.yml b/roles/web-app-nextcloud/config/main.yml index 265a18c9..129273b8 100644 --- a/roles/web-app-nextcloud/config/main.yml +++ b/roles/web-app-nextcloud/config/main.yml @@ -22,7 +22,8 @@ docker: name: "nextcloud" image: "nextcloud" version: "latest-fpm-alpine" - no_stop_required: true + backup: + no_stop_required: true proxy: name: "nextcloud-proxy" image: "nginx" diff --git a/roles/web-app-openproject/config/main.yml b/roles/web-app-openproject/config/main.yml index 7242c8a2..32becf5a 100644 --- a/roles/web-app-openproject/config/main.yml +++ b/roles/web-app-openproject/config/main.yml @@ -13,7 +13,7 @@ ldap: features: matomo: true css: false # Temporary deactivated. Needs to be optimized for production use. - port-ui-desktop: true + port-ui-desktop: true ldap: true central_database: true oauth2: true @@ -34,8 +34,9 @@ docker: web: name: openproject-web image: openproject/community - version: "13" # Update when available. Sadly no rolling release implemented - no_stop_required: true + version: "13" # Update when available. No rolling release implemented + backup: + no_stop_required: true seeder: name: openproject-seeder cron: @@ -44,6 +45,10 @@ docker: name: openproject-worker proxy: name: openproject-proxy + cache: + name: openproject-cache + image: "" # If need a specific memcached image you have to define it here, otherwise the version from svc-db-memcached will be used + version: "" # If need a specific memcached version you have to define it here, otherwise the version from svc-db-memcached will be used volumes: data: "openproject_data" \ No newline at end of file diff --git a/roles/web-app-openproject/templates/docker-compose.yml.j2 b/roles/web-app-openproject/templates/docker-compose.yml.j2 index b454041e..2285a9ca 100644 --- a/roles/web-app-openproject/templates/docker-compose.yml.j2 +++ b/roles/web-app-openproject/templates/docker-compose.yml.j2 @@ -10,8 +10,8 @@ x-op-app: &app {% include 'roles/docker-compose/templates/base.yml.j2' %} cache: - image: memcached - container_name: openproject-memcached + image: "{{ openproject_cache_image}}:{{openproject_cache_version }}" + container_name: {{ openproject_cache_name }} {% include 'roles/docker-container/templates/base.yml.j2' %} proxy: diff --git a/roles/web-app-openproject/vars/main.yml b/roles/web-app-openproject/vars/main.yml index c27e385e..155a8f18 100644 --- a/roles/web-app-openproject/vars/main.yml +++ b/roles/web-app-openproject/vars/main.yml @@ -10,6 +10,22 @@ openproject_seeder_name: "{{ applications | get_app_conf(application_id, 'd openproject_cron_name: "{{ applications | get_app_conf(application_id, 'docker.services.cron.name', True) }}" openproject_proxy_name: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.name', True) }}" openproject_worker_name: "{{ applications | get_app_conf(application_id, 'docker.services.worker.name', True) }}" + +openproject_cache_name: "{{ applications | get_app_conf(application_id, 'docker.services.cache.name', True) }}" +openproject_cache_image: >- + {{ applications + | get_app_conf(application_id, 'docker.services.cache.image') + or applications + | get_app_conf('svc-db-memcached', 'docker.services.memcached.image') + }} + +openproject_cache_version: >- + {{ applications + | get_app_conf(application_id, 'docker.services.cache.version') + or applications + | get_app_conf('svc-db-memcached', 'docker.services.memcached.version') + }} + openproject_plugins_folder: "{{docker_compose.directories.volumes}}plugins/" diff --git a/roles/web-app-peertube/config/main.yml b/roles/web-app-peertube/config/main.yml index 7387c1aa..23461f5e 100644 --- a/roles/web-app-peertube/config/main.yml +++ b/roles/web-app-peertube/config/main.yml @@ -34,6 +34,7 @@ docker: name: "peertube" version: "production-bookworm" image: "chocobozzz/peertube" - no_stop_required: true + backup: + no_stop_required: true volumes: data: peertube_data \ No newline at end of file diff --git a/roles/web-app-pixelfed/config/main.yml b/roles/web-app-pixelfed/config/main.yml index 1c555083..10d41926 100644 --- a/roles/web-app-pixelfed/config/main.yml +++ b/roles/web-app-pixelfed/config/main.yml @@ -30,7 +30,8 @@ docker: image: "zknt/pixelfed" version: "latest" name: "pixelfed" - no_stop_required: true + backup: + no_stop_required: true worker: name: "pixelfed_worker" volumes: diff --git a/roles/web-app-wordpress/config/main.yml b/roles/web-app-wordpress/config/main.yml index f3fec704..3003b510 100644 --- a/roles/web-app-wordpress/config/main.yml +++ b/roles/web-app-wordpress/config/main.yml @@ -46,7 +46,8 @@ docker: version: latest image: wordpress name: wordpress - no_stop_required: true + backup: + no_stop_required: true volumes: data: wordpress_data rbac: diff --git a/templates/roles/web-app/config/main.yml.j2 b/templates/roles/web-app/config/main.yml.j2 index e6392d07..5071baa4 100644 --- a/templates/roles/web-app/config/main.yml.j2 +++ b/templates/roles/web-app/config/main.yml.j2 @@ -6,7 +6,8 @@ docker: database: enabled: false # Enable the database {{ application_id }}: - no_stop_required: true + backup: + no_stop_required: true image: "" version: "latest" name: "web-app-{{ application_id }}" diff --git a/tests/integration/backups/__init__.py b/tests/integration/backups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/backups/test_enabled.py b/tests/integration/backups/test_enabled.py new file mode 100644 index 00000000..a974dc20 --- /dev/null +++ b/tests/integration/backups/test_enabled.py @@ -0,0 +1,51 @@ +import unittest +import os +import yaml + +class TestBackupsEnabledIntegrity(unittest.TestCase): + def setUp(self): + # Path to the roles directory + self.roles_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../../roles') + ) + + def test_backups_enabled_image_consistency(self): + """ + Ensure that if `backups.enabled` is set for any docker.services[*]: + - it's a boolean value + - the containing service dict has an `image` entry at the same level + """ + for role in os.listdir(self.roles_dir): + docker_config_path = os.path.join( + self.roles_dir, role, 'config', 'main.yml' + ) + if not os.path.isfile(docker_config_path): + continue + + with open(docker_config_path, 'r') as f: + try: + config = yaml.safe_load(f) or {} + except yaml.YAMLError as e: + self.fail(f"YAML parsing failed for {docker_config_path}: {e}") + continue + + services = (config.get('docker', {}) or {}).get('services', {}) or {} + + for service_key, service in services.items(): + if not isinstance(service, dict): + continue + + backups_cfg = service.get('backups', {}) or {} + if 'enabled' in backups_cfg: + with self.subTest(role=role, service=service_key): + self.assertIsInstance( + backups_cfg['enabled'], bool, + f"`backups.enabled` in role '{role}', service '{service_key}' must be a boolean." + ) + self.assertIn( + 'image', service, + f"`image` is required in role '{role}', service '{service_key}' when `backups.enabled` is defined." + ) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/backups/test_no_stop_required.py b/tests/integration/backups/test_no_stop_required.py new file mode 100644 index 00000000..fa543bd6 --- /dev/null +++ b/tests/integration/backups/test_no_stop_required.py @@ -0,0 +1,55 @@ +import unittest +import os +import yaml + +class TestNoStopRequiredIntegrity(unittest.TestCase): + def setUp(self): + # Path to the roles directory + self.roles_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../../roles') + ) + + def test_backup_no_stop_required_consistency(self): + """ + Ensure that if `backup.no_stop_required: true` is set for any docker.services[*]: + - it's a boolean value + - the containing service dict has an `image` entry at the same level + """ + for role in os.listdir(self.roles_dir): + docker_config_path = os.path.join( + self.roles_dir, role, 'config', 'main.yml' + ) + if not os.path.isfile(docker_config_path): + continue + + with open(docker_config_path, 'r') as f: + try: + # Ensure config is at least an empty dict if YAML is empty or null + config = yaml.safe_load(f) or {} + except yaml.YAMLError as e: + self.fail(f"YAML parsing failed for {docker_config_path}: {e}") + continue + + # Safely get services dict + services = (config.get('docker', {}) or {}).get('services', {}) or {} + + for service_key, service in services.items(): + if not isinstance(service, dict): + continue + backup_cfg = service.get('backup', {}) or {} + # Check if no_stop_required is explicitly True + if backup_cfg.get('no_stop_required') is True: + with self.subTest(role=role, service=service_key): + # Must be a boolean + self.assertIsInstance( + backup_cfg['no_stop_required'], bool, + f"`backup.no_stop_required` in role '{role}', service '{service_key}' must be a boolean." + ) + # Must have `image` defined at the service level + self.assertIn( + 'image', service, + f"`image` is required in role '{role}', service '{service_key}' when `backup.no_stop_required` is set to True." + ) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_no_stop_required.py b/tests/integration/test_no_stop_required.py deleted file mode 100644 index e94e36eb..00000000 --- a/tests/integration/test_no_stop_required.py +++ /dev/null @@ -1,52 +0,0 @@ -import unittest -import os -import yaml - -class TestNoStopRequiredIntegrity(unittest.TestCase): - def setUp(self): - self.roles_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../roles')) - - def test_no_stop_required_consistency(self): - """ - This test ensures that if 'no_stop_required' is defined in any - docker.services[*] entry, it must: - - be a boolean value (True/False) - - have a 'name' entry defined on the same level - - This is critical for the role 'sys-bkp-docker-2-loc', which uses the - 'no_stop_required' flag to determine which container names should be excluded - from stopping during backup operations. - - The logic for processing this flag is implemented in: - https://github.com/kevinveenbirkenbach/backup-docker-to-local - """ - for role in os.listdir(self.roles_dir): - docker_config_path = os.path.join(self.roles_dir, role, 'config', 'main.yml') - if not os.path.isfile(docker_config_path): - continue - - with open(docker_config_path, 'r') as f: - try: - config = yaml.safe_load(f) - except yaml.YAMLError as e: - self.fail(f"YAML parsing failed for {docker_config_path}: {e}") - continue - - docker_services = ( - config.get('docker', {}).get('services', {}) if config else {} - ) - - for service_key, service in docker_services.items(): - if isinstance(service, dict) and 'no_stop_required' in service: - with self.subTest(role=role, service=service_key): - self.assertIsInstance( - service['no_stop_required'], bool, - f"'no_stop_required' in role '{role}', service '{service_key}' must be a boolean." - ) - self.assertIn( - 'name', service, - f"'name' is required in role '{role}', service '{service_key}' when 'no_stop_required' is set." - ) - -if __name__ == '__main__': - unittest.main()