Compare commits

...

3 Commits

78 changed files with 488 additions and 187 deletions

View File

@@ -1,3 +1,4 @@
[defaults] [defaults]
lookup_plugins = ./lookup_plugins lookup_plugins = ./lookup_plugins
filter_plugins = ./filter_plugins filter_plugins = ./filter_plugins
module_utils = ./module_utils

144
cli/create_docker_role.py Normal file
View File

@@ -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 applications 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()

View File

@@ -97,8 +97,6 @@ Now that you have defined the application settings, domain, and application ID,
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
``` ```
3. **`docker-compose.yml.j2`**: 3. **`docker-compose.yml.j2`**:

View File

@@ -1,2 +0,0 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@@ -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}

View File

@@ -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"
)

27
filter_plugins/get_url.py Normal file
View File

@@ -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) }"

View File

@@ -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"
)

View File

@@ -1,5 +1,5 @@
# You should change this to match your reverse proxy DNS name and protocol # 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 }} 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) # 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)

View File

@@ -9,5 +9,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -290,6 +290,6 @@ DEFAULT_REGISTRATION=invite
OPENID_CONNECT_CLIENT_ID={{oidc.client.id}} OPENID_CONNECT_CLIENT_ID={{oidc.client.id}}
OPENID_CONNECT_CLIENT_SECRET={{oidc.client.secret}} OPENID_CONNECT_CLIENT_SECRET={{oidc.client.secret}}
OPENID_CONNECT_ISSUER={{oidc.client.issuer_url}} 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 # OPENID_CONNECT_UID_FIELD=sub default
{% endif %} {% endif %}

View File

@@ -11,3 +11,5 @@ domain: "{{ domains | get_domain(application_id) }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
bbb_env_file_link: "{{ docker_compose.directories.instance }}.env" bbb_env_file_link: "{{ docker_compose.directories.instance }}.env"
bbb_env_file_origine: "{{ bbb_repository_directory }}.env" bbb_env_file_origine: "{{ bbb_repository_directory }}.env"
docker_compose_skipp_file_creation: true # Skipp creation of docker-compose.yml file

View File

@@ -46,5 +46,3 @@
dest: "{{social_app_path}}" dest: "{{social_app_path}}"
version: "main" version: "main"
notify: docker compose up notify: docker compose up
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -16,5 +16,3 @@
- subnet: "{{ networks.local.collabora.subnet }}" - subnet: "{{ networks.local.collabora.subnet }}"
when: run_once_docker_mariadb is not defined when: run_once_docker_mariadb is not defined
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -0,0 +1 @@
docker_compose_skipp_file_creation: false # If set to true the file creation will be skipped

View File

@@ -1,23 +1,27 @@
- name: "Create (optional) '{{ docker_compose.files.dockerfile }}'" - name: Create (optional) Dockerfile
template: template:
src: "{{ playbook_dir }}/roles/{{ role_name }}/templates/Dockerfile.j2" src: "{{ item }}"
dest: "{{ docker_compose.files.dockerfile }}" dest: "{{ docker_compose.files.dockerfile }}"
notify: docker compose up with_first_found:
ignore_errors: false - "{{ playbook_dir }}/roles/{{ role_name }}/templates/Dockerfile.j2"
register: create_dockerfile_result - "{{ playbook_dir }}/roles/{{ role_name }}/files/Dockerfile"
notify: docker compose up
register: create_dockerfile_result
failed_when: failed_when:
- create_dockerfile_result is failed - create_dockerfile_result is failed
- "'Could not find or access' not in create_dockerfile_result.msg" - "'Could not find or access' not in create_dockerfile_result.msg"
- name: "Create (optional) '{{ docker_compose.files.env }}'" - name: "Create (optional) '{{ docker_compose.files.env }}'"
template: template:
src: "env.j2" src: "{{ item }}"
dest: "{{ docker_compose.files.env }}" dest: "{{ docker_compose.files.env }}"
mode: '770' mode: '770'
force: yes force: yes
notify: docker compose up notify: docker compose up
register: env_template 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: failed_when:
- env_template is failed - env_template is failed
- "'Could not find or access' not in env_template.msg" - "'Could not find or access' not in env_template.msg"

View File

@@ -16,11 +16,5 @@
mode: '0755' mode: '0755'
with_dict: "{{ docker_compose.directories }}" with_dict: "{{ docker_compose.directories }}"
- name: flush docker service - include_tasks: "create-files.yml"
meta: flush_handlers when: not docker_compose_skipp_file_creation | bool
when: run_once_docker_compose is not defined
- name: run the docker tasks once
set_fact:
run_once_docker_compose: true
when: run_once_docker_compose is not defined

View File

@@ -9,5 +9,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -14,8 +14,6 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- 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

@@ -23,7 +23,7 @@ ESPOCRM_ADMIN_USERNAME={{ applications[application_id].users.administrator.usern
ESPOCRM_ADMIN_PASSWORD={{ applications[application_id].credentials.administrator_password }} ESPOCRM_ADMIN_PASSWORD={{ applications[application_id].credentials.administrator_password }}
# Public base URL of the EspoCRM instance # 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 # General UI & locale settings

View File

@@ -21,8 +21,6 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: Build friendica_addons based on features - name: Build friendica_addons based on features
set_fact: set_fact:
friendica_addons: >- friendica_addons: >-

View File

@@ -10,6 +10,4 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -8,5 +8,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -10,8 +10,6 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: Wait for Gitea HTTP endpoint - name: Wait for Gitea HTTP endpoint
wait_for: wait_for:
host: "127.0.0.1" host: "127.0.0.1"

View File

@@ -4,7 +4,7 @@
# General # General
DOMAIN={{domains | get_domain(application_id)}} DOMAIN={{domains | get_domain(application_id)}}
RUN_MODE="{{ 'dev' if (CYMAIS_ENVIRONMENT | lower) == 'development' else 'prod' }}" 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 }}" APP_NAME="{{ applications[application_id].title }}"
USER_UID=1000 USER_UID=1000
USER_GID=1000 USER_GID=1000

View File

@@ -9,5 +9,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -12,5 +12,3 @@
loop: "{{ domains }}" loop: "{{ domains }}"
loop_control: loop_control:
loop_var: domain loop_var: domain
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -10,8 +10,6 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: "create directory {{import_directory_host}}" - name: "create directory {{import_directory_host}}"
file: file:
path: "{{import_directory_host}}" path: "{{import_directory_host}}"

View File

@@ -517,7 +517,7 @@
"/realms/{{ keycloak_realm }}/account/*" "/realms/{{ keycloak_realm }}/account/*"
], ],
"webOrigins": [ "webOrigins": [
"{{ web_protocol }}://{{domains | get_domain('keycloak')}}" "{{ domains | get_url('keycloak', web_protocol) }}"
], ],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,

View File

@@ -8,5 +8,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -23,8 +23,6 @@
ipam_config: ipam_config:
- subnet: "{{ networks.local.central_ldap.subnet }}" - subnet: "{{ networks.local.central_ldap.subnet }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: "Reset LDAP admin passwords" - name: "Reset LDAP admin passwords"
include_tasks: reset_admin_passwords.yml include_tasks: reset_admin_passwords.yml
when: applications[application_id].network.local when: applications[application_id].network.local

View File

@@ -25,8 +25,6 @@
dest: "{{docker_compose.directories.config}}config.toml" dest: "{{docker_compose.directories.config}}config.toml"
notify: docker compose up notify: docker compose up
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: Check if listmonk database is already initialized - name: Check if listmonk database is already initialized
command: docker compose exec -T {{database_host}} psql -U {{database_username}} -d {{database_name}} -c "\dt" command: docker compose exec -T {{database_host}} psql -U {{database_username}} -d {{database_name}} -c "\dt"
register: db_tables register: db_tables

View File

@@ -3,7 +3,7 @@ database_type: "postgres"
listmonk_settings: listmonk_settings:
- key: "app.root_url" - key: "app.root_url"
value: '"{{ web_protocol }}://{{ domains | get_domain(application_id) }}"' value: '"{{ domains | get_url(application_id, web_protocol) }}"'
- key: "app.notify_emails" - key: "app.notify_emails"
value: "{{ [ users.administrator.email ] | to_json }}" value: "{{ [ users.administrator.email ] | to_json }}"

View File

@@ -18,9 +18,6 @@
name: nginx-docker-cert-deploy name: nginx-docker-cert-deploy
when: run_once_docker_mailu is not defined when: run_once_docker_mailu is not defined
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
when: run_once_docker_mailu is not defined
- name: Flush docker service handlers - name: Flush docker service handlers
meta: flush_handlers meta: flush_handlers
when: run_once_docker_mailu is not defined when: run_once_docker_mailu is not defined

View File

@@ -16,8 +16,6 @@
client_max_body_size: "80m" client_max_body_size: "80m"
vhost_flavour: "ws_generic" vhost_flavour: "ws_generic"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: flush docker service - name: flush docker service
meta: flush_handlers meta: flush_handlers
when: applications.mastodon.setup |bool when: applications.mastodon.setup |bool

View File

@@ -12,9 +12,6 @@
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
when: run_once_docker_matomo is not defined when: run_once_docker_matomo is not defined
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
when: run_once_docker_matomo is not defined
- name: run the docker matomo tasks once - name: run the docker matomo tasks once
set_fact: set_fact:
run_once_docker_matomo: true run_once_docker_matomo: true

View File

@@ -2,7 +2,7 @@
application_id: "matomo" application_id: "matomo"
database_type: "mariadb" database_type: "mariadb"
matomo_excluded_ips: "{{ applications.matomo.excluded_ips }}" 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 }}" matomo_auth_token: "{{ applications.matomo.credentials.auth_token }}"

View File

@@ -15,5 +15,3 @@
src: "config.exs.j2" src: "config.exs.j2"
dest: "{{ mobilizon_host_conf_exs_file }}" dest: "{{ mobilizon_host_conf_exs_file }}"
notify: docker compose up notify: docker compose up
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -3,6 +3,6 @@ application_id: mobilizon
database_type: "postgres" database_type: "postgres"
database_gis_enabled: true 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_exposed_docker_port: 4000
mobilizon_host_conf_exs_file: "{{docker_compose.directories.config}}config.exs" mobilizon_host_conf_exs_file: "{{docker_compose.directories.config}}config.exs"

View File

@@ -10,8 +10,6 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: Wait until the Moodle container is healthy - name: Wait until the Moodle container is healthy
shell: docker inspect --format '{% raw %}{{.State.Health.Status}}{% endraw %}' {{ container_name }} shell: docker inspect --format '{% raw %}{{.State.Health.Status}}{% endraw %}' {{ container_name }}
register: health_check register: health_check

View File

@@ -39,7 +39,7 @@
- { name: "field_lock_lastname", value: "locked" } - { name: "field_lock_lastname", value: "locked" }
- { name: "field_map_email", value: "locked" } - { name: "field_map_email", value: "locked" }
#- { name: "showloginform", value: 0 } # Deactivate if OIDC is active #- { 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: loop_control:
label: "{{ item.name }}" label: "{{ item.name }}"
command: > command: >

View File

@@ -35,8 +35,6 @@
dest: "{{docker_compose.directories.volumes}}nginx.conf" dest: "{{docker_compose.directories.volumes}}nginx.conf"
notify: restart nextcloud nginx service notify: restart nextcloud nginx service
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: Flush all handlers immediately so that occ can be used - name: Flush all handlers immediately so that occ can be used
meta: flush_handlers meta: flush_handlers

View File

@@ -32,7 +32,7 @@ NEXTCLOUD_ADMIN_PASSWORD= "{{applications[application_id].credentials.admi
NEXTCLOUD_TRUSTED_DOMAINS= "{{domains | get_domain(application_id)}}" NEXTCLOUD_TRUSTED_DOMAINS= "{{domains | get_domain(application_id)}}"
# Whitelist local docker gateway in Nextcloud to prevent brute-force throtteling # Whitelist local docker gateway in Nextcloud to prevent brute-force throtteling
TRUSTED_PROXIES= "{{ networks.internet.values() | select | join(',') }}" 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" OVERWRITEPROTOCOL= "https"
# Redis Configuration # Redis Configuration

View File

@@ -4,4 +4,4 @@ plugin_configuration:
configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}" configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}"
- appid: "bbb" - appid: "bbb"
configkey: "api.url" configkey: "api.url"
configvalue: "{{ web_protocol }}://{{domains | get_domain('bigbluebutton')}}{{applications.bigbluebutton.api_suffix}}" configvalue: "{{ domains | get_url('bigbluebutton', web_protocol) }}{{applications.bigbluebutton.api_suffix}}"

View File

@@ -18,4 +18,4 @@ nextcloud_system_config:
value: "{{domains | get_domain(application_id)}}" value: "{{domains | get_domain(application_id)}}"
- parameter: "overwrite.cli.url" - parameter: "overwrite.cli.url"
value: "{{ web_protocol }}://{{domains | get_domain(application_id)}}" value: "{{ domains | get_url(application_id, web_protocol) }}"

View File

@@ -33,8 +33,6 @@
state: directory state: directory
mode: 0755 mode: 0755
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: flush docker service - name: flush docker service
meta: flush_handlers meta: flush_handlers

View File

@@ -11,8 +11,6 @@
vars: vars:
http: "{{ ports.localhost.http[application_id] }}" http: "{{ ports.localhost.http[application_id] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: "Install and activate auth-openid-connect plugin if OIDC is enabled" - name: "Install and activate auth-openid-connect plugin if OIDC is enabled"
include_tasks: enable-oidc.yml include_tasks: enable-oidc.yml
when: applications | is_feature_enabled('oidc',application_id) when: applications | is_feature_enabled('oidc',application_id)

View File

@@ -13,5 +13,3 @@
- name: "configure pgadmin servers" - name: "configure pgadmin servers"
include_tasks: configuration.yml include_tasks: configuration.yml
when: applications[application_id].server_mode | bool when: applications[application_id].server_mode | bool
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -8,5 +8,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -1,3 +1,3 @@
# @See https://github.com/leenooks/phpLDAPadmin/wiki/Docker-Container # @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}} LDAP_HOST= {{ldap.server.domain}}

View File

@@ -9,5 +9,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -9,5 +9,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -5,7 +5,7 @@ APP_KEY={{applications[application_id].credentials.app_key}}
APP_NAME="{{applications.pixelfed.titel}}" APP_NAME="{{applications.pixelfed.titel}}"
APP_ENV={{ CYMAIS_ENVIRONMENT | lower }} APP_ENV={{ CYMAIS_ENVIRONMENT | lower }}
APP_DEBUG={{enable_debug | string | 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)}}" APP_DOMAIN="{{domains | get_domain(application_id)}}"
ADMIN_DOMAIN="{{domains | get_domain(application_id)}}" ADMIN_DOMAIN="{{domains | get_domain(application_id)}}"
SESSION_DOMAIN="{{domains | get_domain(application_id)}}" SESSION_DOMAIN="{{domains | get_domain(application_id)}}"

View File

@@ -25,5 +25,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -11,5 +11,3 @@
notify: docker compose up notify: docker compose up
become: true become: true
ignore_errors: true ignore_errors: true
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -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"]

View File

View File

@@ -0,0 +1,9 @@
{
"name": "simpleicons-server",
"type": "module",
"dependencies": {
"express": "^4.18.2",
"simple-icons": "^9.0.0",
"sharp": "^0.32.0"
}
}

View File

@@ -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' %}

View File

@@ -0,0 +1,62 @@
import express from 'express';
import * as icons from 'simple-icons';
import sharp from 'sharp';
const app = express();
const port = process.env.PORT || 3000;
// Helper: convert 'nextcloud' → 'siNextcloud'
function getExportName(slug) {
return 'si' + slug
.split('-')
.map(part => part[0].toUpperCase() + part.slice(1))
.join('');
}
// 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];
if (!icon) {
return res.status(404).send('Icon not found');
}
res.type('image/svg+xml');
res.send(icon.svg);
});
// 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);
const icon = icons[exportName];
if (!icon) {
return res.status(404).send('Icon not found');
}
try {
const pngBuffer = await sharp(Buffer.from(icon.svg))
.resize(size, size)
.png()
.toBuffer();
res.type('image/png');
res.send(pngBuffer);
} catch (err) {
console.error('PNG generation error:', err);
res.status(500).send('PNG generation error');
}
});
app.listen(port, () => {
console.log(`Simple-Icons server listening at http://0.0.0.0:${port}`);
});

View File

@@ -0,0 +1 @@
application_id: simpleicons

View File

@@ -10,7 +10,6 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: "Configure Snipe-IT LDAP settings" - name: "Configure Snipe-IT LDAP settings"
import_tasks: ldap.yml import_tasks: ldap.yml

View File

@@ -1,4 +1,4 @@
application_id: "snipe-it" application_id: "snipe-it"
database_password: "{{ applications[application_id].credentials.database_password }}" database_password: "{{ applications[application_id].credentials.database_password }}"
database_type: "mariadb" database_type: "mariadb"
snipe_it_url: "{{ web_protocol }}://{{domains | get_domain(application_id)}}" snipe_it_url: "{{ domains | get_url(application_id, web_protocol) }}"

View File

@@ -21,5 +21,3 @@
vars: vars:
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -27,4 +27,3 @@
force: yes force: yes
notify: docker compose up notify: docker compose up
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -13,7 +13,7 @@ services:
environment: environment:
SPRING_PROFILES_ACTIVE: docker,postgresql,saml2 SPRING_PROFILES_ACTIVE: docker,postgresql,saml2
OPENJPA_REMOTE_COMMIT: sjvm 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 # database variablen auslesen
console: console:
@@ -25,7 +25,7 @@ services:
restart: always restart: always
environment: environment:
SPRING_PROFILES_ACTIVE: docker,saml2 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: enduser:
depends_on: depends_on:
@@ -36,5 +36,5 @@ services:
restart: always restart: always
environment: environment:
SPRING_PROFILES_ACTIVE: docker,saml2 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]}}/

View File

@@ -28,4 +28,3 @@
dest: "{{docker_compose_init}}" dest: "{{docker_compose_init}}"
notify: docker compose up notify: docker compose up
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -0,0 +1,2 @@
# Docker Role Template
This folder contains a template to setup docker roles

View File

@@ -0,0 +1 @@
application_id: template

View File

@@ -2,7 +2,7 @@
command: > command: >
docker-compose exec -T -u www-data application docker-compose exec -T -u www-data application
wp core install wp core install
--url="{{ web_protocol }}://{{ domains | get_domain(application_id) }}" --url="{{ domains | get_url(application_id, web_protocol) }}"
--title="{{ applications[application_id].title }}" --title="{{ applications[application_id].title }}"
--admin_user="{{ applications[application_id].users.administrator.username }}" --admin_user="{{ applications[application_id].users.administrator.username }}"
--admin_password="{{ applications[application_id].credentials.administrator_password }}" --admin_password="{{ applications[application_id].credentials.administrator_password }}"

View File

@@ -25,7 +25,6 @@
dest: "{{ host_msmtp_conf }}" dest: "{{ host_msmtp_conf }}"
notify: docker compose up notify: docker compose up
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: "Install wordpress" - name: "Install wordpress"
include_tasks: install.yml include_tasks: install.yml

View File

@@ -11,7 +11,7 @@ discourse_settings:
- name: discourse_connect - name: discourse_connect
key: url key: url
value: "{{ web_protocol }}://{{ domains | get_domain('discourse') }}" value: "{{ domains | get_url('discourse', web_protocol) }}"
- name: discourse_connect - name: discourse_connect
key: api-key key: api-key
value: "{{ vault_discourse_api_key }}" value: "{{ vault_discourse_api_key }}"

View File

@@ -10,4 +10,3 @@
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] }}"
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"

View File

@@ -2,7 +2,7 @@ YOURLS_DB_HOST: "{{database_host}}"
YOURLS_DB_USER: "{{database_username}}" YOURLS_DB_USER: "{{database_username}}"
YOURLS_DB_PASS: "{{database_password}}" YOURLS_DB_PASS: "{{database_password}}"
YOURLS_DB_NAME: "{{database_name}}" 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_USER: "{{applications.yourls.users.administrator.username}}"
YOURLS_PASS: "{{applications[application_id].credentials.administrator_password}}" YOURLS_PASS: "{{applications[application_id].credentials.administrator_password}}"
# The following deactivates the login mask for admins, if the oauth2 proxy is activated # The following deactivates the login mask for admins, if the oauth2 proxy is activated

View File

@@ -1,4 +1,4 @@
base_domain: "{{ domain | regex_replace('^(?:.*\\.)?(.+\\..+)$', '\\1') }}" 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_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 }}" matomo_verification_url: "{{ matomo_index_php_url }}?module=API&method=SitesManager.getSitesIdFromSiteUrl&url=https://{{ base_domain }}&format=json&token_auth={{ matomo_auth_token }}"

View File

@@ -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 - name: Merge variables
block: block:
- name: Merge users - name: Merge users

View File

@@ -1,17 +1,8 @@
# tests/unit/test_get_domain_filter.py # tests/unit/test_get_domain.py
import unittest import unittest
import sys import sys
import os import os
from filter_plugins.get_domain import FilterModule
# 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 ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
class TestGetDomainFilter(unittest.TestCase): class TestGetDomainFilter(unittest.TestCase):

View File

@@ -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()