diff --git a/ansible.cfg b/ansible.cfg index 1465cf11..9b28e89d 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,3 +1,4 @@ [defaults] lookup_plugins = ./lookup_plugins -filter_plugins = ./filter_plugins \ No newline at end of file +filter_plugins = ./filter_plugins +module_utils = ./module_utils \ No newline at end of file diff --git a/cli/create_docker_role.py b/cli/create_docker_role.py new file mode 100644 index 00000000..6adef5f7 --- /dev/null +++ b/cli/create_docker_role.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +import argparse +import os +import shutil +import yaml +import ipaddress +from jinja2 import Environment, FileSystemLoader + +# Paths to the group-vars files +PORTS_FILE = './group_vars/all/09_ports.yml' +NETWORKS_FILE = './group_vars/all/10_networks.yml' +ROLE_TEMPLATE_DIR = './docker-template' +ROLES_DIR = './roles' + + +def load_yaml(path): + with open(path) as f: + return yaml.safe_load(f) + + +def dump_yaml(data, path): + with open(path, 'w') as f: + yaml.safe_dump(data, f, sort_keys=False) + + +def get_next_network(networks_dict, prefixlen): + # Collect all local subnets matching the given prefix length + nets = [] + for name, info in networks_dict['defaults_networks']['local'].items(): + net = ipaddress.ip_network(info['subnet']) + if net.prefixlen == prefixlen: + nets.append(net) + # Sort by network address and return the first one + nets.sort(key=lambda n: int(n.network_address)) + return nets[0] + + +def get_next_port(ports_dict, category, service): + used = set() + # Gather already taken ports under localhost.category + for svc, port in ports_dict['ports']['localhost'].get(category, {}).items(): + used.add(int(port)) + # Start searching from port 1 upwards + candidate = 1 + while candidate in used: + candidate += 1 + return candidate + + +def render_template(src_dir, dst_dir, context): + env = Environment( + loader=FileSystemLoader(src_dir), + keep_trailing_newline=True, + autoescape=False, + ) + for root, _, files in os.walk(src_dir): + rel_path = os.path.relpath(root, src_dir) + target_path = os.path.join(dst_dir, rel_path) + os.makedirs(target_path, exist_ok=True) + for filename in files: + template = env.get_template(os.path.join(rel_path, filename)) + rendered = template.render(**context) + out_name = filename[:-3] if filename.endswith('.j2') else filename + with open(os.path.join(target_path, out_name), 'w') as f: + f.write(rendered) + + +def main(): + parser = argparse.ArgumentParser( + description="Create a Docker Ansible role with Jinja2 templates, and assign network and ports" + ) + parser.add_argument( + '--application-id', '-a', required=True, + help="Unique ID of the application (used in the role name)" + ) + parser.add_argument( + '--network', '-n', choices=['24', '28'], required=True, + help="Network prefix length to assign (/24 or /28)" + ) + parser.add_argument( + '--ports', '-p', nargs='+', metavar="CATEGORY.SERVICE", required=True, + help="List of ports in the format category.service (e.g. http.nextcloud)" + ) + args = parser.parse_args() + + app_id = args.application_id + role_name = f"docker-{app_id}" + + # 1) Create the role from the template + role_dir = os.path.join(ROLES_DIR, role_name) + if os.path.exists(role_dir): + parser.error(f"Role {role_name} already exists at {role_dir}") + render_template(ROLE_TEMPLATE_DIR, role_dir, { + 'application_id': app_id, + 'role_name': role_name, + }) + print(f"→ Role {role_name} created at {role_dir}") + + # 2) Assign network + networks = load_yaml(NETWORKS_FILE) + prefix = int(args.network) + chosen_net = get_next_network(networks, prefix) + out_net = { + 'defaults_networks': { + 'application': { + app_id: str(chosen_net) + } + } + } + net_file = f'./group_vars/{app_id}_network.yml' + dump_yaml(out_net, net_file) + print(f"→ Assigned network {chosen_net} (/{prefix}) and wrote to {net_file}") + + # 3) Assign ports + ports_yaml = load_yaml(PORTS_FILE) + assigned = {} + for entry in args.ports: + try: + category, service = entry.split('.', 1) + except ValueError: + parser.error(f"Invalid port spec: {entry}. Must be CATEGORY.SERVICE") + port = get_next_port(ports_yaml, category, service) + # Insert into the in-memory ports data under localhost + ports_yaml['ports']['localhost'].setdefault(category, {})[service] = port + assigned[entry] = port + + # Backup and write updated all/09_ports.yml + backup_file = PORTS_FILE + '.bak' + shutil.copy(PORTS_FILE, backup_file) + dump_yaml(ports_yaml, PORTS_FILE) + print(f"→ Assigned ports: {assigned}. Updated {PORTS_FILE} (backup at {backup_file})") + + # Also write ports to the application’s own vars file + out_ports = {'ports': {'localhost': {}}} + for entry, port in assigned.items(): + category, service = entry.split('.', 1) + out_ports['ports']['localhost'].setdefault(category, {})[service] = port + ports_file = f'./group_vars/{app_id}_ports.yml' + dump_yaml(out_ports, ports_file) + print(f"→ Wrote assigned ports to {ports_file}") + + +if __name__ == '__main__': + main() diff --git a/filter_plugins/__init__.py b/filter_plugins/__init__.py deleted file mode 100644 index 0bfb5a62..00000000 --- a/filter_plugins/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) \ No newline at end of file diff --git a/filter_plugins/get_domain.py b/filter_plugins/get_domain.py new file mode 100644 index 00000000..2672e221 --- /dev/null +++ b/filter_plugins/get_domain.py @@ -0,0 +1,21 @@ +#!/usr/bin/python +import os +import sys +from ansible.errors import AnsibleFilterError + +class FilterModule(object): + def filters(self): + # module_utils-Verzeichnis ermitteln und zum Import-Pfad hinzufügen + plugin_dir = os.path.dirname(__file__) + project_root = os.path.dirname(plugin_dir) + module_utils = os.path.join(project_root, 'module_utils') + if module_utils not in sys.path: + sys.path.append(module_utils) + + # jetzt kannst Du domain_utils importieren + try: + from domain_utils import get_domain + except ImportError as e: + raise AnsibleFilterError(f"could not import domain_utils: {e}") + + return {'get_domain': get_domain} diff --git a/filter_plugins/get_domain_filter.py b/filter_plugins/get_domain_filter.py deleted file mode 100644 index 37470733..00000000 --- a/filter_plugins/get_domain_filter.py +++ /dev/null @@ -1,60 +0,0 @@ -from ansible.errors import AnsibleFilterError - -class FilterModule(object): - '''Ansible filter plugin to retrieve the correct domain for a given application_id.''' - - def filters(self): - return { - 'get_domain': self.get_domain, - } - - def get_domain(self, domains, application_id): - """ - Return the domain for application_id from the domains mapping: - - If value is a string, return it. - - If value is a dict, return its first value. - - If value is a list, return its first element. - - Otherwise, raise an error. - """ - # Ensure domains is a mapping - if not isinstance(domains, dict): - raise AnsibleFilterError(f"'domains' must be a dict, got {type(domains).__name__}") - - if application_id not in domains: - raise AnsibleFilterError(f"application_id '{application_id}' not found in domains mapping") - - val = domains[application_id] - - # String case - if isinstance(val, str): - if not val: - raise AnsibleFilterError(f"domains['{application_id}'] is an empty string") - return val - - # Dict case - if isinstance(val, dict): - try: - first_val = next(iter(val.values())) - except StopIteration: - raise AnsibleFilterError(f"domains['{application_id}'] dict is empty") - if not isinstance(first_val, str) or not first_val: - raise AnsibleFilterError( - f"first value of domains['{application_id}'] must be a non-empty string, got {first_val!r}" - ) - return first_val - - # List case - if isinstance(val, list): - if not val: - raise AnsibleFilterError(f"domains['{application_id}'] list is empty") - first = val[0] - if not isinstance(first, str) or not first: - raise AnsibleFilterError( - f"first element of domains['{application_id}'] must be a non-empty string, got {first!r}" - ) - return first - - # Other types - raise AnsibleFilterError( - f"domains['{application_id}'] has unsupported type {type(val).__name__}, must be str, dict or list" - ) \ No newline at end of file diff --git a/filter_plugins/get_url.py b/filter_plugins/get_url.py new file mode 100644 index 00000000..2304b856 --- /dev/null +++ b/filter_plugins/get_url.py @@ -0,0 +1,27 @@ +#!/usr/bin/python +import os +import sys +from ansible.errors import AnsibleFilterError + +class FilterModule(object): + def filters(self): + return {'get_url': self.get_url} + + def get_url(self, domains, application_id, protocol): + # 1) module_utils-Verzeichnis in den Pfad aufnehmen + plugin_dir = os.path.dirname(__file__) + project_root = os.path.dirname(plugin_dir) + module_utils = os.path.join(project_root, 'module_utils') + if module_utils not in sys.path: + sys.path.append(module_utils) + + # 2) jetzt domain_utils importieren + try: + from domain_utils import get_domain + except ImportError as e: + raise AnsibleFilterError(f"could not import domain_utils: {e}") + + # 3) Validierung und Aufruf + if not isinstance(protocol, str): + raise AnsibleFilterError("Protocol must be a string") + return f"{protocol}://{ get_domain(domains, application_id) }" diff --git a/module_utils/domain_utils.py b/module_utils/domain_utils.py new file mode 100644 index 00000000..2812b136 --- /dev/null +++ b/module_utils/domain_utils.py @@ -0,0 +1,52 @@ +# filter_plugins/domain_utils.py +from ansible.errors import AnsibleFilterError + +def get_domain(domains, application_id): + """ + Return the domain for application_id from the domains mapping: + - If value is a string, return it. + - If value is a dict, return its first value. + - If value is a list, return its first element. + - Otherwise, raise an error. + """ + if not isinstance(domains, dict): + raise AnsibleFilterError(f"'domains' must be a dict, got {type(domains).__name__}") + + if application_id not in domains: + raise AnsibleFilterError(f"application_id '{application_id}' not found in domains mapping") + + val = domains[application_id] + + # String case + if isinstance(val, str): + if not val: + raise AnsibleFilterError(f"domains['{application_id}'] is an empty string") + return val + + # Dict case + if isinstance(val, dict): + try: + first_val = next(iter(val.values())) + except StopIteration: + raise AnsibleFilterError(f"domains['{application_id}'] dict is empty") + if not isinstance(first_val, str) or not first_val: + raise AnsibleFilterError( + f"first value of domains['{application_id}'] must be a non-empty string, got {first_val!r}" + ) + return first_val + + # List case + if isinstance(val, list): + if not val: + raise AnsibleFilterError(f"domains['{application_id}'] list is empty") + first = val[0] + if not isinstance(first, str) or not first: + raise AnsibleFilterError( + f"first element of domains['{application_id}'] must be a non-empty string, got {first!r}" + ) + return first + + # Unsupported type + raise AnsibleFilterError( + f"domains['{application_id}'] has unsupported type {type(val).__name__}, must be str, dict or list" + ) diff --git a/roles/docker-akaunting/templates/env.j2 b/roles/docker-akaunting/templates/env.j2 index 9c15d7f8..b317903f 100644 --- a/roles/docker-akaunting/templates/env.j2 +++ b/roles/docker-akaunting/templates/env.j2 @@ -1,5 +1,5 @@ # You should change this to match your reverse proxy DNS name and protocol -APP_URL={{ web_protocol }}://{{domains | get_domain(application_id)}} +APP_URL={{ domains | get_url(application_id, web_protocol) }} LOCALE={{ HOST_LL }} # Don't change this unless you rename your database container or use rootless podman, in case of using rootless podman you should set it to 127.0.0.1 (NOT localhost) diff --git a/roles/docker-bigbluebutton/templates/env.j2 b/roles/docker-bigbluebutton/templates/env.j2 index a3241f41..83d7405e 100644 --- a/roles/docker-bigbluebutton/templates/env.j2 +++ b/roles/docker-bigbluebutton/templates/env.j2 @@ -290,6 +290,6 @@ DEFAULT_REGISTRATION=invite OPENID_CONNECT_CLIENT_ID={{oidc.client.id}} OPENID_CONNECT_CLIENT_SECRET={{oidc.client.secret}} OPENID_CONNECT_ISSUER={{oidc.client.issuer_url}} -OPENID_CONNECT_REDIRECT=https://{{domains | get_domain(application_id)}} +OPENID_CONNECT_REDIRECT={{ domains | get_url(application_id, web_protocol) }} # OPENID_CONNECT_UID_FIELD=sub default {% endif %} \ No newline at end of file diff --git a/roles/docker-compose/tasks/create-files.yml b/roles/docker-compose/tasks/create-files.yml index 0496d163..54fda4fd 100644 --- a/roles/docker-compose/tasks/create-files.yml +++ b/roles/docker-compose/tasks/create-files.yml @@ -1,23 +1,27 @@ -- name: "Create (optional) '{{ docker_compose.files.dockerfile }}'" +- name: Create (optional) Dockerfile template: - src: "{{ playbook_dir }}/roles/{{ role_name }}/templates/Dockerfile.j2" - dest: "{{ docker_compose.files.dockerfile }}" - notify: docker compose up - ignore_errors: false - register: create_dockerfile_result + src: "{{ item }}" + dest: "{{ docker_compose.files.dockerfile }}" + with_first_found: + - "{{ playbook_dir }}/roles/{{ role_name }}/templates/Dockerfile.j2" + - "{{ playbook_dir }}/roles/{{ role_name }}/files/Dockerfile" + notify: docker compose up + register: create_dockerfile_result failed_when: - create_dockerfile_result is failed - "'Could not find or access' not in create_dockerfile_result.msg" - name: "Create (optional) '{{ docker_compose.files.env }}'" template: - src: "env.j2" + src: "{{ item }}" dest: "{{ docker_compose.files.env }}" mode: '770' force: yes notify: docker compose up register: env_template - ignore_errors: false + with_first_found: + - "{{ playbook_dir }}/roles/{{ role_name }}/templates/env.j2" + - "{{ playbook_dir }}/roles/{{ role_name }}/files/env" failed_when: - env_template is failed - "'Could not find or access' not in env_template.msg" diff --git a/roles/docker-espocrm/templates/env.j2 b/roles/docker-espocrm/templates/env.j2 index 225c4127..587ca37c 100644 --- a/roles/docker-espocrm/templates/env.j2 +++ b/roles/docker-espocrm/templates/env.j2 @@ -23,7 +23,7 @@ ESPOCRM_ADMIN_USERNAME={{ applications[application_id].users.administrator.usern ESPOCRM_ADMIN_PASSWORD={{ applications[application_id].credentials.administrator_password }} # Public base URL of the EspoCRM instance -ESPOCRM_SITE_URL={{ web_protocol }}://{{ domains | get_domain(application_id) }} +ESPOCRM_SITE_URL={{ domains | get_url(application_id, web_protocol) }} # ------------------------------------------------ # General UI & locale settings diff --git a/roles/docker-gitea/templates/env.j2 b/roles/docker-gitea/templates/env.j2 index af7685e5..70602e5a 100644 --- a/roles/docker-gitea/templates/env.j2 +++ b/roles/docker-gitea/templates/env.j2 @@ -4,7 +4,7 @@ # General DOMAIN={{domains | get_domain(application_id)}} RUN_MODE="{{ 'dev' if (CYMAIS_ENVIRONMENT | lower) == 'development' else 'prod' }}" -ROOT_URL="{{ web_protocol }}://{{domains | get_domain(application_id)}}/" +ROOT_URL="{{ domains | get_url(application_id, web_protocol) }}/" APP_NAME="{{ applications[application_id].title }}" USER_UID=1000 USER_GID=1000 diff --git a/roles/docker-keycloak/templates/import/realm.json.j2 b/roles/docker-keycloak/templates/import/realm.json.j2 index 715e8953..51e95a5a 100644 --- a/roles/docker-keycloak/templates/import/realm.json.j2 +++ b/roles/docker-keycloak/templates/import/realm.json.j2 @@ -517,7 +517,7 @@ "/realms/{{ keycloak_realm }}/account/*" ], "webOrigins": [ - "{{ web_protocol }}://{{domains | get_domain('keycloak')}}" + "{{ domains | get_url('keycloak', web_protocol) }}" ], "notBefore": 0, "bearerOnly": false, diff --git a/roles/docker-listmonk/vars/main.yml b/roles/docker-listmonk/vars/main.yml index a41ba31c..7e87e355 100644 --- a/roles/docker-listmonk/vars/main.yml +++ b/roles/docker-listmonk/vars/main.yml @@ -3,7 +3,7 @@ database_type: "postgres" listmonk_settings: - key: "app.root_url" - value: '"{{ web_protocol }}://{{ domains | get_domain(application_id) }}"' + value: '"{{ domains | get_url(application_id, web_protocol) }}"' - key: "app.notify_emails" value: "{{ [ users.administrator.email ] | to_json }}" diff --git a/roles/docker-matomo/vars/main.yml b/roles/docker-matomo/vars/main.yml index a83e7828..2518430e 100644 --- a/roles/docker-matomo/vars/main.yml +++ b/roles/docker-matomo/vars/main.yml @@ -2,7 +2,7 @@ application_id: "matomo" database_type: "mariadb" matomo_excluded_ips: "{{ applications.matomo.excluded_ips }}" -matomo_index_php_url: "{{ web_protocol }}://{{ domains | get_domain('matomo') }}/index.php" +matomo_index_php_url: "{{ domains | get_url('matomo', web_protocol) }}/index.php" matomo_auth_token: "{{ applications.matomo.credentials.auth_token }}" diff --git a/roles/docker-mobilizon/vars/main.yml b/roles/docker-mobilizon/vars/main.yml index 366c722e..12b0d590 100644 --- a/roles/docker-mobilizon/vars/main.yml +++ b/roles/docker-mobilizon/vars/main.yml @@ -3,6 +3,6 @@ application_id: mobilizon database_type: "postgres" database_gis_enabled: true -mobilizon_oidc_callback_url: "{{ web_protocol }}://{{ domains | get_domain(application_id) }}/auth/openid_connect/callback" +mobilizon_oidc_callback_url: "{{ domains | get_url(application_id, web_protocol) }}/auth/openid_connect/callback" mobilizon_exposed_docker_port: 4000 mobilizon_host_conf_exs_file: "{{docker_compose.directories.config}}config.exs" \ No newline at end of file diff --git a/roles/docker-moodle/tasks/oidc.yml b/roles/docker-moodle/tasks/oidc.yml index 07adaa22..1d0025e2 100644 --- a/roles/docker-moodle/tasks/oidc.yml +++ b/roles/docker-moodle/tasks/oidc.yml @@ -39,7 +39,7 @@ - { name: "field_lock_lastname", value: "locked" } - { name: "field_map_email", value: "locked" } #- { name: "showloginform", value: 0 } # Deactivate if OIDC is active - - { name: "alternateloginurl", value: "{{ web_protocol }}://{{ domains | get_domain(application_id) }}/auth/oidc/" } + - { name: "alternateloginurl", value: "{{ domains | get_url(application_id, web_protocol) }}/auth/oidc/" } loop_control: label: "{{ item.name }}" command: > diff --git a/roles/docker-nextcloud/templates/env.j2 b/roles/docker-nextcloud/templates/env.j2 index 0165b8bf..6b69fb44 100644 --- a/roles/docker-nextcloud/templates/env.j2 +++ b/roles/docker-nextcloud/templates/env.j2 @@ -32,7 +32,7 @@ NEXTCLOUD_ADMIN_PASSWORD= "{{applications[application_id].credentials.admi NEXTCLOUD_TRUSTED_DOMAINS= "{{domains | get_domain(application_id)}}" # Whitelist local docker gateway in Nextcloud to prevent brute-force throtteling TRUSTED_PROXIES= "{{ networks.internet.values() | select | join(',') }}" -OVERWRITECLIURL= "{{ web_protocol }}://{{domains | get_domain(application_id)}}" +OVERWRITECLIURL= "{{ domains | get_url(application_id, web_protocol) }}" OVERWRITEPROTOCOL= "https" # Redis Configuration diff --git a/roles/docker-nextcloud/vars/plugins/bbb.yml b/roles/docker-nextcloud/vars/plugins/bbb.yml index dbe74cb7..75fadbd2 100644 --- a/roles/docker-nextcloud/vars/plugins/bbb.yml +++ b/roles/docker-nextcloud/vars/plugins/bbb.yml @@ -4,4 +4,4 @@ plugin_configuration: configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}" - appid: "bbb" configkey: "api.url" - configvalue: "{{ web_protocol }}://{{domains | get_domain('bigbluebutton')}}{{applications.bigbluebutton.api_suffix}}" \ No newline at end of file + configvalue: "{{ domains | get_url('bigbluebutton', web_protocol) }}{{applications.bigbluebutton.api_suffix}}" \ No newline at end of file diff --git a/roles/docker-nextcloud/vars/system.yml b/roles/docker-nextcloud/vars/system.yml index 48e5b71b..02aec9e4 100644 --- a/roles/docker-nextcloud/vars/system.yml +++ b/roles/docker-nextcloud/vars/system.yml @@ -18,4 +18,4 @@ nextcloud_system_config: value: "{{domains | get_domain(application_id)}}" - parameter: "overwrite.cli.url" - value: "{{ web_protocol }}://{{domains | get_domain(application_id)}}" \ No newline at end of file + value: "{{ domains | get_url(application_id, web_protocol) }}" \ No newline at end of file diff --git a/roles/docker-phpldapadmin/templates/env.j2 b/roles/docker-phpldapadmin/templates/env.j2 index 76f83b7f..c7c8ef6c 100644 --- a/roles/docker-phpldapadmin/templates/env.j2 +++ b/roles/docker-phpldapadmin/templates/env.j2 @@ -1,3 +1,3 @@ # @See https://github.com/leenooks/phpLDAPadmin/wiki/Docker-Container -APP_URL= {{ web_protocol }}://{{domains | get_domain(application_id)}} +APP_URL= {{ domains | get_url(application_id, web_protocol) }} LDAP_HOST= {{ldap.server.domain}} \ No newline at end of file diff --git a/roles/docker-pixelfed/templates/env.j2 b/roles/docker-pixelfed/templates/env.j2 index bf606b3a..4a832d03 100644 --- a/roles/docker-pixelfed/templates/env.j2 +++ b/roles/docker-pixelfed/templates/env.j2 @@ -5,7 +5,7 @@ APP_KEY={{applications[application_id].credentials.app_key}} APP_NAME="{{applications.pixelfed.titel}}" APP_ENV={{ CYMAIS_ENVIRONMENT | lower }} APP_DEBUG={{enable_debug | string | lower }} -APP_URL={{ web_protocol }}://{{domains | get_domain(application_id)}} +APP_URL={{ domains | get_url(application_id, web_protocol) }} APP_DOMAIN="{{domains | get_domain(application_id)}}" ADMIN_DOMAIN="{{domains | get_domain(application_id)}}" SESSION_DOMAIN="{{domains | get_domain(application_id)}}" diff --git a/roles/docker-simpleicons/files/Dockerfile b/roles/docker-simpleicons/files/Dockerfile new file mode 100644 index 00000000..b10d1dd9 --- /dev/null +++ b/roles/docker-simpleicons/files/Dockerfile @@ -0,0 +1,16 @@ +FROM node:latest AS builder + +WORKDIR /app +COPY package*.json ./ + +RUN npm install + +FROM node:latest + +WORKDIR /app +COPY --from=builder /app/node_modules ./node_modules +COPY server.js . + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/roles/docker-simpleicons/files/env b/roles/docker-simpleicons/files/env new file mode 100644 index 00000000..e69de29b diff --git a/roles/docker-simpleicons/templates/package.json b/roles/docker-simpleicons/files/package.json similarity index 100% rename from roles/docker-simpleicons/templates/package.json rename to roles/docker-simpleicons/files/package.json diff --git a/roles/docker-simpleicons/templates/Dockerfile b/roles/docker-simpleicons/templates/Dockerfile deleted file mode 100644 index 5efaebf5..00000000 --- a/roles/docker-simpleicons/templates/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -# ---- Builder Stage ---- -FROM node:latest AS builder - -WORKDIR /app -# Nur package.json und package-lock.json kopieren für schnellere Caching-Layers -COPY package*.json ./ - -# simple-icons installieren -RUN npm install - -# ---- Runtime Stage ---- -FROM node:latest - -WORKDIR /app -# Nur node_modules aus dem Builder übernehmen -COPY --from=builder /app/node_modules ./node_modules -# Kopiere den Server-Code -COPY server.js . - -# Port, auf dem der Server lauscht -ENV PORT=3000 -EXPOSE 3000 - -# Startbefehl -CMD ["node", "server.js"] diff --git a/roles/docker-simpleicons/templates/docker-compose.yml b/roles/docker-simpleicons/templates/docker-compose.yml deleted file mode 100644 index fde6408f..00000000 --- a/roles/docker-simpleicons/templates/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: '3.8' - -services: - icons: - build: - context: . - dockerfile: Dockerfile - image: simpleicons-server:latest - container_name: simpleicons-server - ports: - - "3000:3000" - environment: - - PORT=3000 - restart: unless-stopped diff --git a/roles/docker-simpleicons/templates/docker-compose.yml.j2 b/roles/docker-simpleicons/templates/docker-compose.yml.j2 new file mode 100644 index 00000000..7154c5a8 --- /dev/null +++ b/roles/docker-simpleicons/templates/docker-compose.yml.j2 @@ -0,0 +1,14 @@ +services: + application: + build: + context: . + dockerfile: Dockerfile + image: simpleicons-server:latest + container_name: simpleicons-server + ports: + - "{{ports.localhost.http[application_id]}}:3000" +{% include 'roles/docker-compose/templates/services/base.yml.j2' %} +{% include 'templates/docker/container/networks.yml.j2' %} + +{% include 'templates/docker/compose/networks.yml.j2' %} + diff --git a/roles/docker-simpleicons/templates/server.js b/roles/docker-simpleicons/templates/server.js.j2 similarity index 67% rename from roles/docker-simpleicons/templates/server.js rename to roles/docker-simpleicons/templates/server.js.j2 index 6db151ec..b5a61a13 100644 --- a/roles/docker-simpleicons/templates/server.js +++ b/roles/docker-simpleicons/templates/server.js.j2 @@ -5,7 +5,7 @@ import sharp from 'sharp'; const app = express(); const port = process.env.PORT || 3000; -// Helper: turn 'nextcloud' → 'siNextcloud' +// Helper: convert 'nextcloud' → 'siNextcloud' function getExportName(slug) { return 'si' + slug .split('-') @@ -13,8 +13,13 @@ function getExportName(slug) { .join(''); } -// GET /icons/:slug.svg -app.get('/icons/:slug.svg', (req, res) => { +// Root: redirect to your documentation +app.get('/', (req, res) => { + res.redirect('https://docs.cymais.cloud/roles/docker-{{ application_id }}/README.html'); +}); + +// GET /:slug.svg +app.get('/:slug.svg', (req, res) => { const slug = req.params.slug.toLowerCase(); const exportName = getExportName(slug); const icon = icons[exportName]; @@ -23,11 +28,12 @@ app.get('/icons/:slug.svg', (req, res) => { return res.status(404).send('Icon not found'); } - res.type('image/svg+xml').send(icon.svg); + res.type('image/svg+xml'); + res.send(icon.svg); }); -// GET /icons/:slug.png?size=... -app.get('/icons/:slug.png', async (req, res) => { +// GET /:slug.png?size=... +app.get('/:slug.png', async (req, res) => { const slug = req.params.slug.toLowerCase(); const size = parseInt(req.query.size, 10) || 128; const exportName = getExportName(slug); @@ -38,12 +44,13 @@ app.get('/icons/:slug.png', async (req, res) => { } try { - const png = await sharp(Buffer.from(icon.svg)) + const pngBuffer = await sharp(Buffer.from(icon.svg)) .resize(size, size) .png() .toBuffer(); - res.type('image/png').send(png); + res.type('image/png'); + res.send(pngBuffer); } catch (err) { console.error('PNG generation error:', err); res.status(500).send('PNG generation error'); diff --git a/roles/docker-simpleicons/vars/main.yml b/roles/docker-simpleicons/vars/main.yml new file mode 100644 index 00000000..e2a49383 --- /dev/null +++ b/roles/docker-simpleicons/vars/main.yml @@ -0,0 +1 @@ +application_id: simpleicons \ No newline at end of file diff --git a/roles/docker-snipe-it/vars/main.yml b/roles/docker-snipe-it/vars/main.yml index 1baf7652..cd58a298 100644 --- a/roles/docker-snipe-it/vars/main.yml +++ b/roles/docker-snipe-it/vars/main.yml @@ -1,4 +1,4 @@ application_id: "snipe-it" database_password: "{{ applications[application_id].credentials.database_password }}" database_type: "mariadb" -snipe_it_url: "{{ web_protocol }}://{{domains | get_domain(application_id)}}" \ No newline at end of file +snipe_it_url: "{{ domains | get_url(application_id, web_protocol) }}" \ No newline at end of file diff --git a/roles/docker-syncope/templates/docker-compose.yml.j2 b/roles/docker-syncope/templates/docker-compose.yml.j2 index 94df8482..1c621f74 100644 --- a/roles/docker-syncope/templates/docker-compose.yml.j2 +++ b/roles/docker-syncope/templates/docker-compose.yml.j2 @@ -13,7 +13,7 @@ services: environment: SPRING_PROFILES_ACTIVE: docker,postgresql,saml2 OPENJPA_REMOTE_COMMIT: sjvm - SERVICE_DISCOVERY_ADDRESS: {{ web_protocol }}://{{ domains | get_domain(application_id) }}/{{syncope_paths[rest]}}/ + SERVICE_DISCOVERY_ADDRESS: {{ domains | get_url(application_id, web_protocol) }}/{{syncope_paths[rest]}}/ # database variablen auslesen console: @@ -25,7 +25,7 @@ services: restart: always environment: SPRING_PROFILES_ACTIVE: docker,saml2 - SERVICE_DISCOVERY_ADDRESS: {{ web_protocol }}://{{ domains | get_domain(application_id) }}/{{syncope_paths[console]}}/ + SERVICE_DISCOVERY_ADDRESS: {{ domains | get_url(application_id, web_protocol) }}/{{syncope_paths[console]}}/ enduser: depends_on: @@ -36,5 +36,5 @@ services: restart: always environment: SPRING_PROFILES_ACTIVE: docker,saml2 - SERVICE_DISCOVERY_ADDRESS: {{ web_protocol }}://{{ domains | get_domain(application_id) }}/{{syncope_paths[enduser]}}/ + SERVICE_DISCOVERY_ADDRESS: {{ domains | get_url(application_id, web_protocol) }}/{{syncope_paths[enduser]}}/ \ No newline at end of file diff --git a/roles/docker-template/README.md b/roles/docker-template/README.md new file mode 100644 index 00000000..073ed83b --- /dev/null +++ b/roles/docker-template/README.md @@ -0,0 +1,2 @@ +# Docker Role Template +This folder contains a template to setup docker roles \ No newline at end of file diff --git a/roles/docker-template/vars/main.yml b/roles/docker-template/vars/main.yml new file mode 100644 index 00000000..fd346dc9 --- /dev/null +++ b/roles/docker-template/vars/main.yml @@ -0,0 +1 @@ +application_id: template \ No newline at end of file diff --git a/roles/docker-wordpress/tasks/install.yml b/roles/docker-wordpress/tasks/install.yml index 18c1cfe0..3b937ffa 100644 --- a/roles/docker-wordpress/tasks/install.yml +++ b/roles/docker-wordpress/tasks/install.yml @@ -2,7 +2,7 @@ command: > docker-compose exec -T -u www-data application wp core install - --url="{{ web_protocol }}://{{ domains | get_domain(application_id) }}" + --url="{{ domains | get_url(application_id, web_protocol) }}" --title="{{ applications[application_id].title }}" --admin_user="{{ applications[application_id].users.administrator.username }}" --admin_password="{{ applications[application_id].credentials.administrator_password }}" diff --git a/roles/docker-wordpress/vars/discourse.yml b/roles/docker-wordpress/vars/discourse.yml index f811ba8b..37c3f9f0 100644 --- a/roles/docker-wordpress/vars/discourse.yml +++ b/roles/docker-wordpress/vars/discourse.yml @@ -11,7 +11,7 @@ discourse_settings: - name: discourse_connect key: url - value: "{{ web_protocol }}://{{ domains | get_domain('discourse') }}" + value: "{{ domains | get_url('discourse', web_protocol) }}" - name: discourse_connect key: api-key value: "{{ vault_discourse_api_key }}" diff --git a/roles/docker-yourls/templates/env.j2 b/roles/docker-yourls/templates/env.j2 index 957e0aa9..51711fa8 100644 --- a/roles/docker-yourls/templates/env.j2 +++ b/roles/docker-yourls/templates/env.j2 @@ -2,7 +2,7 @@ YOURLS_DB_HOST: "{{database_host}}" YOURLS_DB_USER: "{{database_username}}" YOURLS_DB_PASS: "{{database_password}}" YOURLS_DB_NAME: "{{database_name}}" -YOURLS_SITE: "{{ web_protocol }}://{{domains | get_domain(application_id)}}" +YOURLS_SITE: "{{ domains | get_url(application_id, web_protocol) }}" YOURLS_USER: "{{applications.yourls.users.administrator.username}}" YOURLS_PASS: "{{applications[application_id].credentials.administrator_password}}" # The following deactivates the login mask for admins, if the oauth2 proxy is activated diff --git a/roles/nginx-modifier-matomo/vars/main.yml b/roles/nginx-modifier-matomo/vars/main.yml index cf61511e..fbbd97fc 100644 --- a/roles/nginx-modifier-matomo/vars/main.yml +++ b/roles/nginx-modifier-matomo/vars/main.yml @@ -1,4 +1,4 @@ base_domain: "{{ domain | regex_replace('^(?:.*\\.)?(.+\\..+)$', '\\1') }}" -matomo_index_php_url: "{{ web_protocol }}://{{ domains | get_domain('matomo') }}/index.php" +matomo_index_php_url: "{{ domains | get_url('matomo', web_protocol) }}/index.php" matomo_auth_token: "{{ applications.matomo.credentials.auth_token }}" matomo_verification_url: "{{ matomo_index_php_url }}?module=API&method=SitesManager.getSitesIdFromSiteUrl&url=https://{{ base_domain }}&format=json&token_auth={{ matomo_auth_token }}" \ No newline at end of file diff --git a/tasks/plays/01_constructor.yml b/tasks/plays/01_constructor.yml index c4090065..e4e83963 100644 --- a/tasks/plays/01_constructor.yml +++ b/tasks/plays/01_constructor.yml @@ -1,4 +1,17 @@ --- + +- name: Show effective filter_plugins setting + shell: ansible-config dump --only-changed | grep -i filter_plugins || echo "using default" + register: filter_cfg + +- name: Debug filter_plugins config + debug: + msg: "{{ filter_cfg.stdout_lines }}" + +- name: "Debug: show which ansible.cfg was used" + debug: + msg: "{{ ansible_config_file }}" + - name: Merge variables block: - name: Merge users diff --git a/tests/unit/filter_plugins/test_get_domain_filter.py b/tests/unit/filter_plugins/test_get_domain.py similarity index 88% rename from tests/unit/filter_plugins/test_get_domain_filter.py rename to tests/unit/filter_plugins/test_get_domain.py index ed635a1e..dbd11078 100644 --- a/tests/unit/filter_plugins/test_get_domain_filter.py +++ b/tests/unit/filter_plugins/test_get_domain.py @@ -1,17 +1,8 @@ -# tests/unit/test_get_domain_filter.py +# tests/unit/test_get_domain.py import unittest import sys import os - -# Ensure filter_plugins directory is on the path -sys.path.insert( - 0, - os.path.abspath( - os.path.join(os.path.dirname(__file__), '../../../filter_plugins') - ) -) - -from get_domain_filter import FilterModule +from filter_plugins.get_domain import FilterModule from ansible.errors import AnsibleFilterError class TestGetDomainFilter(unittest.TestCase): diff --git a/tests/unit/filter_plugins/test_get_url.py b/tests/unit/filter_plugins/test_get_url.py new file mode 100644 index 00000000..d446e7fa --- /dev/null +++ b/tests/unit/filter_plugins/test_get_url.py @@ -0,0 +1,77 @@ +# tests/unit/filter_plugins/test_get_url.py +import unittest +import sys +import os + +# Ensure filter_plugins directory is on the path +sys.path.insert( + 0, + os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../../filter_plugins') + ) +) + +from get_url import FilterModule +from ansible.errors import AnsibleFilterError + +class TestGetUrlFilter(unittest.TestCase): + def setUp(self): + # Retrieve the get_url filter function + self.get_url = FilterModule().filters()['get_url'] + + def test_string_domain(self): + domains = {'app': 'example.com'} + url = self.get_url(domains, 'app', 'https') + self.assertEqual(url, 'https://example.com') + + def test_dict_domain(self): + domains = {'app': {'primary': 'primary.com', 'secondary': 'secondary.com'}} + url = self.get_url(domains, 'app', 'http') + self.assertEqual(url, 'http://primary.com') + + def test_list_domain(self): + domains = {'app': ['first.com', 'second.com']} + url = self.get_url(domains, 'app', 'ftp') + self.assertEqual(url, 'ftp://first.com') + + def test_missing_application_id(self): + domains = {'app': 'example.com'} + with self.assertRaises(AnsibleFilterError): + self.get_url(domains, 'missing', 'https') + + def test_domains_not_dict(self): + with self.assertRaises(AnsibleFilterError): + self.get_url(['not', 'a', 'dict'], 'app', 'https') + + def test_empty_string_domain(self): + domains = {'app': ''} + with self.assertRaises(AnsibleFilterError): + self.get_url(domains, 'app', 'https') + + def test_empty_dict_domain(self): + domains = {'app': {}} + with self.assertRaises(AnsibleFilterError): + self.get_url(domains, 'app', 'https') + + def test_empty_list_domain(self): + domains = {'app': []} + with self.assertRaises(AnsibleFilterError): + self.get_url(domains, 'app', 'https') + + def test_non_string_in_dict_domain(self): + domains = {'app': {'key': 123}} + with self.assertRaises(AnsibleFilterError): + self.get_url(domains, 'app', 'https') + + def test_non_string_in_list_domain(self): + domains = {'app': [123]} + with self.assertRaises(AnsibleFilterError): + self.get_url(domains, 'app', 'https') + + def test_protocol_not_string(self): + domains = {'app': 'example.com'} + with self.assertRaises(AnsibleFilterError): + self.get_url(domains, 'app', 123) + +if __name__ == '__main__': + unittest.main()