9 Commits

Author SHA1 Message Date
96ded68ef4 Refactor DNS handling and add solo record support
- Added 'solo' flag support for A/AAAA, CNAME/MX/TXT, and SRV records in sys-dns-cloudflare-records.
- Simplified sys-svc-dns: removed NS management tasks and CLOUDFLARE_NAMESERVERS default.
- Renamed 03_apex.yml back to 02_apex.yml, adjusted AAAA task name.
- Updated web-app-bluesky DNS tasks: marked critical records with 'solo'.
- Updated web-app-mailu DNS tasks: removed cleanup block, enforced 'solo' on all records.
- Adjusted constructor stage to call domain_mappings with AUTO_BUILD_ALIASES parameter.

Conversation: https://chatgpt.com/share/68cd20d8-9ba8-800f-b070-f7294f072c40
2025-09-19 15:29:11 +02:00
2d8967d559 added www. alias for desktop as default 2025-09-19 14:55:40 +02:00
5e616d3962 web: general domain cleanup (canonical/aliases normalization)
- Normalize domain blocks across apps:
  - Add explicit 'aliases: []' everywhere (no implicit aliases)
  - Standardize canonical subdomains for consistency:
    * Bluesky: web/api under *.bluesky.<PRIMARY_DOMAIN>
    * EspoCRM: espo.crm.<PRIMARY_DOMAIN>
    * Gitea:   tea.git.<PRIMARY_DOMAIN>
    * GitLab:  lab.git.<PRIMARY_DOMAIN>
    * Joomla:  joomla.cms.<PRIMARY_DOMAIN>
    * Magento: magento.shop.<PRIMARY_DOMAIN>
    * OpenProject: open.project.<PRIMARY_DOMAIN>
    * Pretix:  ticket.shop.<PRIMARY_DOMAIN>
    * Taiga:   kanban.project.<PRIMARY_DOMAIN>
  - Remove legacy/duplicate aliases and use empty list instead
  - Fix 'alias' -> 'aliases' where applicable

Context: preparing for AUTO_BUILD_ALIASES=False and deterministic redirect mapping.

Ref: conversation https://chatgpt.com/share/68cd512c-c878-800f-bdf2-81737adf7e0e
2025-09-19 14:51:56 +02:00
0f85d27a4d filter/domain_redirect_mappings: add auto_build_alias parameter
- Extend filter signature with auto_build_alias flag to control automatic
  default→canonical alias creation
- group_vars/all: introduce AUTO_BUILD_ALIASES variable for global toggle
- Update unit tests: adjust calls to new signature and add dedicated
  test cases for auto_build_aliases=False

Ref: conversation https://chatgpt.com/share/68cd512c-c878-800f-bdf2-81737adf7e0e
2025-09-19 14:49:02 +02:00
c6677ca61b tests: ignore Jinja variables inside raw blocks in variable definitions check
- Added regex masking to skip {{ var }} usages inside {% raw %}…{% endraw %} blocks.
- Simplified code by removing redundant comments.
- Cleaned up task file for XWiki role by removing outdated note.

Ref: https://chatgpt.com/share/68cd2558-e92c-800f-a80a-a79d3c81476e
2025-09-19 11:42:01 +02:00
83ce88a048 Solved all open test issues 2025-09-19 11:32:58 +02:00
7d150fa021 DNS & certs refactor:
- Switch certbot flag from MODE_TEST → MODE_DUMMY in dedicated certs
- Add sys-svc-dns defaults for CLOUDFLARE_NAMESERVERS
- Introduce 02_nameservers.yml for NS cleanup + enforce, adjust task ordering (apex now 03_apex.yml)
- Enforce quoting for Bluesky and Mailu TXT records
- Add cleanup of MX/TXT/DMARC/DKIM in Mailu role
- Normalize no_log handling in Nextcloud plugin
- Simplify async conditionals in Collabora role
Conversation: https://chatgpt.com/share/68cd20d8-9ba8-800f-b070-f7294f072c40
2025-09-19 11:22:51 +02:00
2806aab89e Removed deathlock between sys-ctl-bkp-docker-2-loc and sys-ctl-cln-faild-bkps - Timer handles now cleanup exclusively 2025-09-19 11:21:18 +02:00
61772d5916 Solved testing mode bug 2025-09-19 11:18:29 +02:00
77 changed files with 240 additions and 197 deletions

View File

@@ -17,6 +17,7 @@ def run_ansible_playbook(
password_file=None,
verbose=0,
skip_build=False,
skip_tests=False,
logs=False
):
start_time = datetime.datetime.now()
@@ -56,9 +57,8 @@ def run_ansible_playbook(
except subprocess.CalledProcessError:
print("\n❌ Inventory validation failed. Deployment aborted.\n", file=sys.stderr)
sys.exit(1)
# Tests are controlled via MODE_TEST
if modes.get("MODE_TEST", False):
if not skip_tests:
print("\n🧪 Running tests (make messy-test)...\n")
subprocess.run(["make", "messy-test"], check=True)
@@ -255,6 +255,12 @@ def main():
action="store_true",
help="Skip running 'make build' before deployment.",
)
parser.add_argument(
"-t",
"--skip-tests",
action="store_true",
help="Skip running 'make messy-tests' before deployment.",
)
parser.add_argument(
"-i",
"--id",
@@ -301,6 +307,7 @@ def main():
password_file=args.password_file,
verbose=args.verbose,
skip_build=args.skip_build,
skip_tests=args.skip_tests,
logs=args.logs,
)

View File

@@ -7,7 +7,7 @@ class FilterModule(object):
def filters(self):
return {'domain_mappings': self.domain_mappings}
def domain_mappings(self, apps, PRIMARY_DOMAIN):
def domain_mappings(self, apps, primary_domain, auto_build_alias):
"""
Build a flat list of redirect mappings for all apps:
- source: each alias domain
@@ -43,7 +43,7 @@ class FilterModule(object):
domains_cfg = cfg.get('server',{}).get('domains',{})
entry = domains_cfg.get('canonical')
if entry is None:
canonical_map[app_id] = [default_domain(app_id, PRIMARY_DOMAIN)]
canonical_map[app_id] = [default_domain(app_id, primary_domain)]
elif isinstance(entry, dict):
canonical_map[app_id] = list(entry.values())
elif isinstance(entry, list):
@@ -61,11 +61,11 @@ class FilterModule(object):
alias_map[app_id] = []
continue
if isinstance(domains_cfg, dict) and not domains_cfg:
alias_map[app_id] = [default_domain(app_id, PRIMARY_DOMAIN)]
alias_map[app_id] = [default_domain(app_id, primary_domain)]
continue
aliases = parse_entry(domains_cfg, 'aliases', app_id) or []
default = default_domain(app_id, PRIMARY_DOMAIN)
default = default_domain(app_id, primary_domain)
has_aliases = 'aliases' in domains_cfg
has_canonical = 'canonical' in domains_cfg
@@ -74,7 +74,7 @@ class FilterModule(object):
aliases.append(default)
elif has_canonical:
canon = canonical_map.get(app_id, [])
if default not in canon and default not in aliases:
if default not in canon and default not in aliases and auto_build_alias:
aliases.append(default)
alias_map[app_id] = aliases
@@ -84,7 +84,7 @@ class FilterModule(object):
mappings = []
for app_id, sources in alias_map.items():
canon_list = canonical_map.get(app_id, [])
target = canon_list[0] if canon_list else default_domain(app_id, PRIMARY_DOMAIN)
target = canon_list[0] if canon_list else default_domain(app_id, primary_domain)
for src in sources:
if src == target:
# skip self-redirects

View File

@@ -32,8 +32,10 @@ WEBSOCKET_PROTOCOL: "{{ 'wss' if WEB_PROTOCOL == 'https' else 'ws' }}"
# WWW-Redirect to None WWW-Domains enabled
WWW_REDIRECT_ENABLED: "{{ ('web-opt-rdr-www' in group_names) | bool }}"
AUTO_BUILD_ALIASES: False # If enabled it creates an alias domain for each web application by the entity name, recommended to set to false to safge domain space
# Domain
PRIMARY_DOMAIN: "localhost" # Primary Domain of the server
PRIMARY_DOMAIN: "localhost" # Primary Domain of the server
DNS_PROVIDER: cloudflare # The DNS Provider\Registrar for the domain

View File

@@ -1,7 +1,7 @@
# Mode
# The following modes can be combined with each other
MODE_TEST: false # Executes test routines instead of productive routines
MODE_DUMMY: false # Executes dummy/test routines instead of productive routines
MODE_UPDATE: true # Executes updates
MODE_DEBUG: false # This enables debugging in ansible and in the apps, You SHOULD NOT enable this on production servers
MODE_RESET: false # Cleans up all Infinito.Nexus files. It's necessary to run to whole playbook and not particial roles when using this function.

View File

@@ -22,7 +22,7 @@
system_service_on_calendar: "{{ SYS_SCHEDULE_BACKUP_DOCKER_TO_LOCAL }}"
system_service_tpl_exec_start_pre: '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ SYS_SERVICE_GROUP_MANIPULATION | join(" ") }} --ignore {{ SYS_SERVICE_BACKUP_DOCKER_2_LOC }} --timeout "{{ SYS_TIMEOUT_BACKUP_SERVICES }}"'
system_service_tpl_exec_start: "/bin/sh -c '{{ BKP_DOCKER_2_LOC_EXEC }}'"
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }} {{ SYS_SERVICE_CLEANUP_BACKUPS_FAILED }}"
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
# system_service_tpl_exec_start_post: "/usr/bin/systemctl start {{ SYS_SERVICE_CLEANUP_BACKUPS }}" # Not possible to use it because it's a deathlock. Keep this line for documentation purposes
- include_tasks: utils/run_once.yml

View File

@@ -19,7 +19,7 @@
system_service_on_calendar: "{{ SYS_SCHEDULE_CLEANUP_FAILED_BACKUPS }}"
system_service_copy_files: false
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
system_service_tpl_exec_start: '/bin/sh -c "{{ CLEANUP_FAILED_BACKUPS_PKG }} --all --workers {{ CLEANUP_FAILED_BACKUPS_WORKERS }} --yes"'
system_service_tpl_exec_start_pre: '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ SYS_SERVICE_GROUP_MANIPULATION | join(" ") }} --ignore {{ SYS_SERVICE_GROUP_CLEANUP| join(" ") }} --timeout "{{ SYS_TIMEOUT_CLEANUP_SERVICES }}"'
system_service_tpl_exec_start: '/bin/sh -c "{{ CLEANUP_FAILED_BACKUPS_PKG }} --all --workers {{ CLEANUP_FAILED_BACKUPS_WORKERS }} --yes"'
- include_tasks: utils/run_once.yml

View File

@@ -17,6 +17,7 @@
proxied: "{{ item.proxied | default(false) }}"
ttl: "{{ item.ttl | default(1) }}"
state: "{{ item.state | default('present') }}"
solo: "{{ item.solo | default(false) }}"
loop: "{{ cloudflare_records | selectattr('type','in',['A','AAAA']) | list }}"
loop_control: { label: "{{ item.type }} {{ item.name }} -> {{ item.content }}" }
async: "{{ cloudflare_async_enabled | ternary(cloudflare_async_time, omit) }}"
@@ -48,6 +49,7 @@
ttl: "{{ item.ttl | default(1) }}"
priority: "{{ (item.type == 'MX') | ternary(item.priority | default(10), omit) }}"
state: "{{ item.state | default('present') }}"
solo: "{{ item.solo | default(false) }}"
loop: "{{ cloudflare_records | selectattr('type','in',['CNAME','MX','TXT']) | list }}"
loop_control: { label: "{{ item.type }} {{ item.name }} -> {{ item.value }}" }
async: "{{ cloudflare_async_enabled | ternary(cloudflare_async_time, omit) }}"
@@ -83,6 +85,7 @@
value: "{{ item.value }}"
ttl: "{{ item.ttl | default(1) }}"
state: "{{ item.state | default('present') }}"
solo: "{{ item.solo | default(false) }}"
loop: "{{ cloudflare_records | selectattr('type','equalto','SRV') | list }}"
loop_control: { label: "SRV {{ item.service }}.{{ item.proto }} {{ item.name }} -> {{ item.value }}:{{ item.port }}" }
ignore_errors: "{{ item.ignore_errors | default(true) }}"

View File

@@ -1,3 +1,5 @@
# run_once_sys_stk_front_proxy: deactivated
- name: "Load Proxy procedures if Proxy is enabled"
include_tasks: "01_base.yml"
when: SYS_STK_FRONT_PROXY_ENABLED | bool

View File

@@ -18,7 +18,7 @@
{% else %}
--letsencrypt-webroot-path "{{ LETSENCRYPT_WEBROOT_PATH }}"
{% endif %}
{{ '--mode-test' if MODE_TEST | bool else '' }}
{{ '--mode-test' if MODE_DUMMY | bool else '' }}
register: certbundle_result
changed_when: >
('certificate not yet due for renewal' not in (certbundle_result.stdout | lower | default('')))

View File

@@ -24,7 +24,7 @@
{% else %}
-d {{ domain }}
{% endif %}
{{ '--test-cert' if MODE_TEST | bool else '' }}
{{ '--test-cert' if MODE_DUMMY | bool else '' }}
register: certbot_result
changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout"
when: not cert_check.exists

View File

@@ -12,7 +12,7 @@
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
- name: "Ensure AAAA @ for {{ base_domain }} (if IPv6 is global)"
- name: "Ensure AAAA @ for {{ base_domain }} with '{{ networks.internet.ip6 | default('None') }}"
community.general.cloudflare_dns:
api_token: "{{ CLOUDFLARE_API_TOKEN }}"
zone: "{{ base_domain }}"

View File

@@ -13,6 +13,7 @@ server:
domains:
canonical:
- "accounting.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
script-src-elem:

View File

@@ -23,6 +23,7 @@ server:
domains:
canonical:
- baserow.{{ PRIMARY_DOMAIN }}
aliases: []
csp:
whitelist:
worker-src:

View File

@@ -19,6 +19,7 @@ server:
domains:
canonical:
- "meet.{{ PRIMARY_DOMAIN }}"
aliases: []
credentials: {}
docker:

View File

@@ -9,9 +9,10 @@ server:
status_codes:
web: 405
domains:
aliases: []
canonical:
web: "bskyweb.{{ PRIMARY_DOMAIN }}"
api: "bluesky.{{ PRIMARY_DOMAIN }}"
web: "web.bluesky.{{ PRIMARY_DOMAIN }}"
api: "api.bluesky.{{ PRIMARY_DOMAIN }}"
# view: "view.bluesky.{{ PRIMARY_DOMAIN }}"
csp:
whitelist:

View File

@@ -26,6 +26,7 @@
zone: "{{ BLUESKY_API_DOMAIN | to_zone }}"
name: "{{ BLUESKY_API_DOMAIN }}"
content: "{{ networks.internet.ip4 }}"
solo: true
proxied: false
- type: AAAA
@@ -33,16 +34,19 @@
name: "{{ BLUESKY_API_DOMAIN }}"
content: "{{ networks.internet.ip6 | default('') }}"
proxied: false
solo: true
state: "{{ (networks.internet.ip6 is defined and (networks.internet.ip6 | string) | length > 0) | ternary('present','absent') }}"
# 2) Handle verification for primary handle (Apex)
- type: TXT
zone: "{{ PRIMARY_DOMAIN | to_zone }}"
name: "_atproto.{{ PRIMARY_DOMAIN }}"
value: "did=did:web:{{ BLUESKY_API_DOMAIN }}"
value: '"did=did:web:{{ BLUESKY_API_DOMAIN }}"'
solo: true
# 3) Web UI host (only if enabled)
- type: A
solo: true
zone: "{{ BLUESKY_WEB_DOMAIN | to_zone }}"
name: "{{ BLUESKY_WEB_DOMAIN }}"
content: "{{ networks.internet.ip4 }}"
@@ -50,6 +54,7 @@
state: "{{ (BLUESKY_WEB_ENABLED | bool) | ternary('present','absent') }}"
- type: AAAA
solo: true
zone: "{{ BLUESKY_WEB_DOMAIN | to_zone }}"
name: "{{ BLUESKY_WEB_DOMAIN }}"
content: "{{ networks.internet.ip6 | default('') }}"
@@ -58,6 +63,7 @@
# 4) Custom AppView host (only if you actually run one and it's not api.bsky.app)
- type: A
solo: true
zone: "{{ BLUESKY_VIEW_DOMAIN | to_zone }}"
name: "{{ BLUESKY_VIEW_DOMAIN }}"
content: "{{ networks.internet.ip4 }}"
@@ -65,6 +71,7 @@
state: "{{ (BLUESKY_VIEW_ENABLED | bool) and (BLUESKY_VIEW_DOMAIN != 'api.bsky.app') | ternary('present','absent') }}"
- type: AAAA
solo: true
zone: "{{ BLUESKY_VIEW_DOMAIN | to_zone }}"
name: "{{ BLUESKY_VIEW_DOMAIN }}"
content: "{{ networks.internet.ip6 | default('') }}"

View File

@@ -10,6 +10,7 @@ server:
domains:
canonical:
- "bridgyfed.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
whitelist: {}
flags: {}

View File

@@ -30,9 +30,7 @@ server:
domains:
canonical:
- "c.wiki.{{ PRIMARY_DOMAIN }}"
aliases:
- "confluence.{{ PRIMARY_DOMAIN }}"
- "confluence.wiki.{{ PRIMARY_DOMAIN }}"
aliases: []
rbac:
roles: {}
truststore_enabled: false

View File

@@ -29,6 +29,8 @@ server:
domains:
canonical:
- "{{ PRIMARY_DOMAIN }}"
aliases:
- "www.{{ PRIMARY_DOMAIN }}"
docker:
services:
desktop:

View File

@@ -20,6 +20,7 @@ server:
domains:
canonical:
- "forum.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
database:

View File

@@ -4,3 +4,4 @@ server:
domains:
canonical:
- elk.{{ PRIMARY_DOMAIN }}
aliases: []

View File

@@ -23,10 +23,9 @@ server:
frame-src:
- https://s.espocrm.com/
domains:
aliases:
- "crm.{{ PRIMARY_DOMAIN }}"
aliases: []
canonical:
- espocrm.{{ PRIMARY_DOMAIN }}
- espo.crm.{{ PRIMARY_DOMAIN }}
email:
from_name: "Customer Relationship Management ({{ PRIMARY_DOMAIN }})"
docker:

View File

@@ -13,6 +13,7 @@ server:
domains:
canonical:
- "social.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
script-src-elem:

View File

@@ -24,9 +24,7 @@ server:
domains:
canonical:
- "audio.{{ PRIMARY_DOMAIN }}"
aliases:
- "music.{{ PRIMARY_DOMAIN }}"
- "sound.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
style-src:

View File

@@ -35,10 +35,9 @@ server:
manifest-src:
- "data:"
domains:
aliases:
- "git.{{ PRIMARY_DOMAIN }}"
aliases: []
canonical:
- gitea.{{ PRIMARY_DOMAIN }}
- tea.git.{{ PRIMARY_DOMAIN }}
docker:
services:
database:

View File

@@ -22,4 +22,4 @@ docker:
server:
domains:
canonical:
- gitlab.{{ PRIMARY_DOMAIN }}
- lab.git.{{ PRIMARY_DOMAIN }}

View File

@@ -4,3 +4,4 @@ server:
domains:
canonical:
- jenkins.{{ PRIMARY_DOMAIN }}
aliases: []

View File

@@ -31,6 +31,7 @@ server:
domains:
canonical:
- "jira.{{ PRIMARY_DOMAIN }}"
aliases: []
status_codes:
default: 405
rbac:

View File

@@ -10,7 +10,8 @@ features:
server:
domains:
canonical:
- "cms.{{ PRIMARY_DOMAIN }}"
- "joomla.cms.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
style-src:

View File

@@ -29,6 +29,7 @@ server:
domains:
canonical:
- "auth.{{ PRIMARY_DOMAIN }}"
aliases: []
scopes:
nextcloud: nextcloud

View File

@@ -26,8 +26,7 @@ server:
script-src:
unsafe-inline: true
domains:
aliases:
- "ldap.{{ PRIMARY_DOMAIN }}"
aliases: []
canonical:
- lam.{{ PRIMARY_DOMAIN }}

View File

@@ -10,6 +10,7 @@ server:
domains:
canonical:
- "newsletter.{{ PRIMARY_DOMAIN }}"
aliases: []
status_codes:
default: 404
docker:

View File

@@ -12,9 +12,8 @@ server:
whitelist: {}
domains:
canonical:
- "shop.{{ PRIMARY_DOMAIN }}"
aliases:
- "magento.{{ PRIMARY_DOMAIN }}"
- "magento.shop.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
php:

View File

@@ -13,8 +13,7 @@ server:
domains:
canonical:
- "{{ SYSTEM_EMAIL.HOST }}"
alias:
- "mailu.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
style-src:

View File

@@ -3,25 +3,25 @@
name: sys-dns-cloudflare-records
when: DNS_PROVIDER | lower == 'cloudflare'
vars:
cloudflare_async_enabled: "{{ ASYNC_ENABLED | default(false) | bool }}"
cloudflare_async_time: "{{ ASYNC_TIME | default(45) }}"
cloudflare_async_poll: "{{ ASYNC_POLL | default(5) }}"
cloudflare_no_log: "{{ MASK_CREDENTIALS_IN_LOGS | default(true) | bool }}"
cloudflare_async_enabled: "{{ ASYNC_ENABLED | bool }}"
cloudflare_async_time: "{{ ASYNC_TIME }}"
cloudflare_async_poll: "{{ ASYNC_POLL }}"
cloudflare_no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
cloudflare_records:
- { type: A, zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}", name: "{{ MAILU_HOSTNAME }}", content: "{{ MAILU_IP4_PUBLIC }}", proxied: false }
- { type: A, zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}", name: "{{ MAILU_HOSTNAME }}", solo: true, content: "{{ MAILU_IP4_PUBLIC }}", proxied: false }
# - { type: AAAA, zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}", name: "{{ MAILU_HOSTNAME }}", content: "{{ MAILU_IP6_PUBLIC }}", proxied: false }
- { type: CNAME, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "autoconfig.{{ MAILU_DOMAIN_DNS_ZONE }}", value: "{{ MAILU_HOSTNAME }}" }
- { type: MX, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", value: "{{ MAILU_HOSTNAME }}", priority: 10 }
- { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", value: "v=spf1 mx a:{{ MAILU_HOSTNAME }} ~all" }
- { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "_dmarc.{{ MAILU_DOMAIN_DNS_ZONE }}", value: "v=DMARC1; p=reject; ruf=mailto:{{ MAILU_DMARC_RUF }}; adkim=s; aspf=s" }
- { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "dkim._domainkey.{{ MAILU_DOMAIN_DNS_ZONE }}", value: "{{ mailu_dkim_public_key }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_submission", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 587, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_submissions", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 465, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_imaps", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 993, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_imap", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 143, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_pop3s", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 995, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_pop3", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 110, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_autodiscover", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 443, value: "{{ MAILU_HOSTNAME }}" }
- { type: CNAME, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "autoconfig.{{ MAILU_DOMAIN_DNS_ZONE }}", solo: true, value: "{{ MAILU_HOSTNAME }}" }
- { type: MX, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, value: "{{ MAILU_HOSTNAME }}", priority: 10 }
- { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, value: '"v=spf1 mx a:{{ MAILU_HOSTNAME }} ~all"' }
- { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "_dmarc.{{ MAILU_DOMAIN_DNS_ZONE }}", solo: true, value: '"v=DMARC1; p=reject; ruf=mailto:{{ MAILU_DMARC_RUF }}; adkim=s; aspf=s"' }
- { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "dkim._domainkey.{{ MAILU_DOMAIN_DNS_ZONE }}", solo: true, value: '"{{ mailu_dkim_public_key }}"' }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_submission", proto: "_tcp", priority: 20, weight: 1, port: 587, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_submissions", proto: "_tcp", priority: 20, weight: 1, port: 465, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_imaps", proto: "_tcp", priority: 20, weight: 1, port: 993, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_imap", proto: "_tcp", priority: 20, weight: 1, port: 143, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_pop3s", proto: "_tcp", priority: 20, weight: 1, port: 995, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_pop3", proto: "_tcp", priority: 20, weight: 1, port: 110, value: "{{ MAILU_HOSTNAME }}" }
- { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_autodiscover", proto: "_tcp", priority: 20, weight: 1, port: 443, value: "{{ MAILU_HOSTNAME }}" }
- name: "rDNS (Hetzner Cloud) for Mailu"
include_role:

View File

@@ -10,6 +10,7 @@ server:
domains:
canonical:
- "microblog.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
whitelist:
frame-src:

View File

@@ -1,6 +1,6 @@
# General
application_id: "web-app-mastodon"
database_type: "postgres"
application_id: "web-app-mastodon"
database_type: "postgres"
# Mastodon Specific
MASTODON_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.mastodon.version') }}"

View File

@@ -26,10 +26,9 @@ server:
unsafe-inline: true
unsafe-eval: true
domains:
aliases:
- "analytics.{{ PRIMARY_DOMAIN }}"
canonical:
- "matomo.{{ PRIMARY_DOMAIN }}"
aliases: []
excluded_ips: "{{ networks.internet.values() | list }}"
docker:

View File

@@ -3,8 +3,7 @@ server:
domains:
canonical:
- "m.wiki.{{ PRIMARY_DOMAIN }}"
aliases:
- "media.wiki.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
database:

View File

@@ -34,8 +34,7 @@ server:
domains:
canonical:
- "mig.{{ PRIMARY_DOMAIN }}"
aliases:
- "meta-infinite-graph.{{ PRIMARY_DOMAIN }}"
aliases: []
build_data:
# This shouldn't be relevant anymore, because the data is anyhow build async in background

View File

@@ -15,8 +15,8 @@ server:
domains:
canonical:
- "event.{{ PRIMARY_DOMAIN }}"
aliases:
- "events.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
database:

View File

@@ -26,6 +26,7 @@ server:
domains:
canonical:
- "academy.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
database:

View File

@@ -27,3 +27,4 @@ server:
domains:
canonical:
- "slides.{{ PRIMARY_DOMAIN }}"
aliases: []

View File

@@ -18,6 +18,7 @@ server:
domains:
canonical:
- "cloud.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
volumes:
data: nextcloud_data

View File

@@ -34,7 +34,7 @@
failed_when: not ASYNC_ENABLED and config_set_shell.rc != 0
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | default(true) | bool }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: Check if {{ plugin_task_path }} exists
stat:

View File

@@ -10,3 +10,4 @@ server:
domains:
canonical:
- oauth2-proxy.{{ PRIMARY_DOMAIN }}
aliases: []

View File

@@ -30,8 +30,8 @@ server:
- "data:"
domains:
canonical:
- "project.{{ PRIMARY_DOMAIN }}"
- "open.project.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
database:

View File

@@ -24,8 +24,8 @@ server:
domains:
canonical:
- "video.{{ PRIMARY_DOMAIN }}"
aliases:
- "videos.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
redis:

View File

@@ -26,6 +26,7 @@ server:
domains:
canonical:
- pgadmin.{{ PRIMARY_DOMAIN }}
aliases: []
docker:
services:
database:

View File

@@ -15,3 +15,4 @@ server:
domains:
canonical:
- phpldapadmin.{{ PRIMARY_DOMAIN }}
aliases: []

View File

@@ -20,9 +20,7 @@ server:
script-src-elem:
unsafe-inline: true
domains:
aliases:
- "mysql.{{ PRIMARY_DOMAIN }}"
- "mariadb.{{ PRIMARY_DOMAIN }}"
aliases: []
canonical:
- phpmyadmin.{{ PRIMARY_DOMAIN }}
docker:

View File

@@ -23,8 +23,7 @@ server:
domains:
canonical:
- "picture.{{ PRIMARY_DOMAIN }}"
aliases:
- "pictures.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
redis:

View File

@@ -27,9 +27,8 @@ server:
flags: {}
domains:
canonical:
- "ticket.{{ PRIMARY_DOMAIN }}"
aliases:
- "pretix.{{ PRIMARY_DOMAIN }}"
- "ticket.shop.{{ PRIMARY_DOMAIN }}"
aliases: []
rbac:
roles: {}
plugins:

View File

@@ -4,3 +4,4 @@ server:
domains:
canonical:
- "wheel.{{ PRIMARY_DOMAIN }}"
aliases: []

View File

@@ -10,6 +10,7 @@ server:
domains:
canonical:
- "inventory.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
script-src:

View File

@@ -16,3 +16,4 @@ server:
domains:
canonical:
- "docs.{{ PRIMARY_DOMAIN }}"
aliases: []

View File

@@ -17,3 +17,4 @@ server:
domains:
canonical:
- syncope.{{ PRIMARY_DOMAIN }}
aliases: []

View File

@@ -31,4 +31,4 @@ server:
unsafe-eval: true
domains:
canonical:
- "kanban.{{ PRIMARY_DOMAIN }}"
- "kanban.project.{{ PRIMARY_DOMAIN }}"

View File

@@ -41,6 +41,7 @@ server:
domains:
canonical:
- "blog.{{ PRIMARY_DOMAIN }}"
aliases: []
docker:
services:
database:

View File

@@ -35,6 +35,7 @@ server:
domains:
canonical:
- "x.wiki.{{ PRIMARY_DOMAIN }}"
aliases: []
rbac:
roles: {}
ldap:

View File

@@ -17,63 +17,6 @@ _JOB_LOC_RE = re.compile(r"/rest/jobstatus/([^?\s#]+)")
def _join_elements(elems: Iterable[Any]) -> str:
return "/".join(str(x) for x in elems)
def xwiki_job_id(response: Any, default: Optional[str] = None, strict: bool = False) -> Optional[str]:
"""
Extract a XWiki job ID from a typical Ansible `uri` response.
Supports:
- JSON mapping: {"id": {"elements": ["install", "extensions", "123"]}}
- JSON mapping: {"id": "install/extensions/123"}
- Fallback from Location header or URL containing "/rest/jobstatus/<id>"
Args:
response: The registered result from the `uri` task (dict-like).
default: Value to return when no ID can be found (if strict=False).
strict: If True, raise AnsibleFilterError when no ID is found.
Returns:
The job ID string, or `default`/None.
Raises:
AnsibleFilterError: if `strict=True` and no job ID can be determined.
"""
if not isinstance(response, dict):
if strict:
raise AnsibleFilterError("xwiki_job_id: response must be a dict-like Ansible result.")
return default
# 1) Try JSON body
j = response.get("json")
if isinstance(j, dict):
job_id = j.get("id")
if isinstance(job_id, dict):
elems = job_id.get("elements")
if isinstance(elems, list) and elems:
return _join_elements(elems)
if isinstance(job_id, str) and job_id.strip():
return job_id.strip()
# 2) Fallback: Location header (Ansible `uri` exposes it as `location`)
loc = response.get("location")
if isinstance(loc, str) and loc:
m = _JOB_LOC_RE.search(loc)
if m:
return m.group(1)
# 3) As a last resort, try the final `url` (in case server redirected and Ansible captured it)
url = response.get("url")
if isinstance(url, str) and url:
m = _JOB_LOC_RE.search(url)
if m:
return m.group(1)
# Not found
if strict:
raise AnsibleFilterError("xwiki_job_id: could not extract job ID from response.")
return default
def xwiki_extension_status(raw: str) -> int:
"""
Parse the output of the Groovy CheckExtension page.
@@ -103,6 +46,5 @@ class FilterModule(object):
"""Custom filters for XWiki helpers."""
def filters(self):
return {
"xwiki_job_id": xwiki_job_id,
"xwiki_extension_status": xwiki_extension_status,
}

View File

@@ -13,7 +13,6 @@
vars:
xwiki_oidc_enabled_switch: false
xwiki_ldap_enabled_switch: false
xwiki_superadmin_enabled_switch: true
- name: "ASSERT | superadmin can authenticate (needed for installer)"
uri:
@@ -49,6 +48,5 @@
vars:
xwiki_oidc_enabled_switch: "{{ XWIKI_OIDC_ENABLED | bool }}"
xwiki_ldap_enabled_switch: "{{ XWIKI_LDAP_ENABLED | bool }}"
xwiki_superadmin_enabled_switch: false
- include_tasks: utils/run_once.yml

View File

@@ -9,7 +9,6 @@
#
# Notes:
# - We print machine-readable markers so Ansible can assert deterministically.
# - We protect XWiki's {{groovy}} wiki macro from Jinja by using {% raw %}…{% endraw %}.
- name: "XWIKI | Build Groovy installer code from static file (base64 payload)"
vars:

View File

@@ -14,7 +14,6 @@ XWIKI_HOST_PORT: "{{ ports.localhost.http[application_id] }
XWIKI_HOSTNAME: "{{ container_hostname }}"
## Paths
XWIKI_HOST_CONF_PATH: "{{ [docker_compose.directories.config, 'xwiki.cfg'] | path_join }}"
XWIKI_HOST_PROPERTIES_PATH: "{{ [docker_compose.directories.config, 'xwiki.properties'] | path_join }}"
XWIKI_DOCK_DATA_DIR: "/usr/local/xwiki"
@@ -32,7 +31,6 @@ XWIKI_SSO_ENABLED: "{{ (XWIKI_OIDC_ENABLED | bool) or (XWIKI_
# Admin credentials (must be provided via inventory/vault)
XWIKI_ADMIN_USER: "{{ users.administrator.username }}"
XWIKI_ADMIN_PASS: "{{ users.administrator.password }}"
XWIKI_ADMIN_GROUP: "{{ application_id }}-administrator"
# Superadministrator
@@ -43,7 +41,6 @@ XWIKI_SUPERADMIN_USERNAME: "superadmin"
XWIKI_REST_BASE: "{{ ['http://127.0.0.1:'~ XWIKI_HOST_PORT, '/rest/'] | url_join }}"
XWIKI_REST_XWIKI: "{{ [XWIKI_REST_BASE, 'wikis/xwiki'] | url_join }}"
XWIKI_REST_XWIKI_PAGES: "{{ [XWIKI_REST_BASE, 'wikis/xwiki/spaces/XWiki/pages'] | url_join }}"
XWIKI_REST_EXTENSION_INSTALL: "{{ [XWIKI_REST_BASE, 'jobs'] | url_join }}"
# LDAP configuration (mapped to LDAP.* context)
XWIKI_LDAP_SERVER: "{{ LDAP.SERVER.DOMAIN }}"

View File

@@ -17,8 +17,7 @@ server:
domains:
canonical:
- "s.{{ PRIMARY_DOMAIN }}"
aliases:
- "short.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
style-src:

View File

@@ -4,3 +4,4 @@ server:
domains:
canonical:
- asset.{{ PRIMARY_DOMAIN }}
aliases: []

View File

@@ -6,3 +6,4 @@ server:
domains:
canonical:
- "cdn.{{ PRIMARY_DOMAIN }}"
aliases: []

View File

@@ -2,6 +2,7 @@ server:
domains:
canonical:
- "collabora.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
whitelist:
frame-ancestors:

View File

@@ -18,8 +18,8 @@
(not ASYNC_ENABLED | bool )
and
('updated' in (collabora_fonts.stdout | default('')))
async: "{{ ASYNC_TIME if (ASYNC_ENABLED | default(false) | bool) else omit }}"
poll: "{{ ASYNC_POLL if (ASYNC_ENABLED | default(false) | bool) else omit }}"
async: "{{ ASYNC_TIME if (ASYNC_ENABLED | bool) else omit }}"
poll: "{{ ASYNC_POLL if (ASYNC_ENABLED | bool) else omit }}"
when: MODE_UPDATE | bool
- name: Allow Nextcloud host IP for Collabora preview conversion

View File

@@ -6,5 +6,4 @@ server:
domains:
canonical:
- "file.{{ PRIMARY_DOMAIN }}"
alias:
- "files.{{ PRIMARY_DOMAIN }}"
aliases: []

View File

@@ -6,3 +6,4 @@ server:
domains:
canonical:
- "html.{{ PRIMARY_DOMAIN }}"
aliases: []

View File

@@ -8,6 +8,7 @@ server:
domains:
canonical:
- "logout.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
style-src:

View File

@@ -21,6 +21,7 @@ server:
domains:
canonical:
- "icons.{{ PRIMARY_DOMAIN }}"
aliases: []
rbac:
roles:
mail-bot:

View File

@@ -5,3 +5,4 @@ server:
domains:
canonical:
- xmpp.{{ PRIMARY_DOMAIN }}
aliases: []

View File

@@ -76,7 +76,7 @@
redirect_domain_mappings: >-
{{
CURRENT_PLAY_APPLICATIONS |
domain_mappings(PRIMARY_DOMAIN) |
domain_mappings(PRIMARY_DOMAIN, AUTO_BUILD_ALIASES) |
merge_mapping(redirect_domain_mappings, 'source')
}}

View File

@@ -34,6 +34,14 @@ class TestVariableDefinitions(unittest.TestCase):
# File extensions to scan for Jinja usage/inline definitions
self.scan_extensions = {'.yml', '.j2'}
# -----------------------
# Raw-block pattern (ignore any Jinja inside {% raw %}...{% endraw %})
# Supports trimmed variants: {%- raw -%} ... {%- endraw -%}
self.raw_block_re = re.compile(
r'{%\s*-?\s*raw\s*-?\s*%}.*?{%\s*-?\s*endraw\s*-?\s*%}',
re.DOTALL,
)
# -----------------------
# Regex patterns
# -----------------------
@@ -121,18 +129,14 @@ class TestVariableDefinitions(unittest.TestCase):
# Block mapping: keys on subsequent indented lines
in_set_fact = True
set_fact_indent = indent
# continue to next iteration to avoid double-processing this line
continue
if in_set_fact:
# Still inside set_fact child mapping?
if indent > set_fact_indent and stripped.strip():
m = self.mapping_key.match(stripped)
if m:
self.defined.add(m.group(1))
# do not continue; still scan for Jinja defs below
else:
# Leaving the block when indentation decreases or a new key at same level appears
if indent <= set_fact_indent and stripped:
in_set_fact = False
@@ -140,48 +144,37 @@ class TestVariableDefinitions(unittest.TestCase):
if self.ansible_vars_block.match(stripped):
in_vars_block = True
vars_block_indent = indent
# continue to next line to avoid double-processing this line
continue
if in_vars_block:
# Inside vars: collect top-level mapping keys
if indent > vars_block_indent and stripped.strip():
m = self.mapping_key.match(stripped)
if m:
self.defined.add(m.group(1))
# do not continue; still scan for Jinja defs below
else:
# Leaving vars block
if indent <= vars_block_indent and stripped:
in_vars_block = False
# --- Always scan every line (including inside blocks) for Jinja definitions
# {% set var = ... %}
for m in self.jinja_set_def.finditer(line):
self.defined.add(m.group(1))
# {% for x [, y] in ... %}
for m in self.jinja_for_def.finditer(line):
self.defined.add(m.group(1))
if m.group(2):
self.defined.add(m.group(2))
# {% macro name(params...) %}
for m in self.jinja_macro_def.finditer(line):
params_blob = m.group(1)
params = [p.strip() for p in params_blob.split(',')]
for p in params:
if not p:
continue
# Strip * / ** for varargs/kwargs
p = p.lstrip('*')
# Drop default value part: name=...
name = p.split('=', 1)[0].strip()
if re.match(r'^[a-zA-Z_]\w*$', name):
self.defined.add(name)
# --- loop_var and register names
m_loop = self.ansible_loop_var.match(stripped)
if m_loop:
self.defined.add(m_loop.group(1))
@@ -191,7 +184,6 @@ class TestVariableDefinitions(unittest.TestCase):
self.defined.add(m_reg.group(1))
except Exception:
# Ignore unreadable files
pass
def test_all_used_vars_are_defined(self):
@@ -210,29 +202,37 @@ class TestVariableDefinitions(unittest.TestCase):
path = os.path.join(root, fn)
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for lineno, line in enumerate(f, 1):
for m in self.simple_var_pattern.finditer(line):
var = m.group(1)
content = f.read()
# Skip well-known Jinja/Ansible builtins and frequent loop aliases
if var in (
'lookup', 'role_name', 'domains', 'item', 'host_type',
'inventory_hostname', 'role_path', 'playbook_dir',
'ansible_become_password', 'inventory_dir', 'ansible_memtotal_mb', 'omit', 'group_names', 'ansible_processor_vcpus'
):
continue
# Mask {% raw %} ... {% endraw %} blocks
def _mask_raw(m):
s = m.group(0)
return re.sub(r'[^\n]', ' ', s)
# Accept if defined directly or via fallback defaults
if (
var not in self.defined
and f"default_{var}" not in self.defined
and f"defaults_{var}" not in self.defined
):
undefined_uses.append(
f"{path}:{lineno}: '{{{{ {var} }}}}' used but not defined"
)
content_wo_raw = self.raw_block_re.sub(_mask_raw, content)
for lineno, line in enumerate(content_wo_raw.splitlines(True), 1):
for m in self.simple_var_pattern.finditer(line):
var = m.group(1)
if var in (
'lookup', 'role_name', 'domains', 'item', 'host_type',
'inventory_hostname', 'role_path', 'playbook_dir',
'ansible_become_password', 'inventory_dir',
'ansible_memtotal_mb', 'omit', 'group_names',
'ansible_processor_vcpus'
):
continue
if (
var not in self.defined
and f"default_{var}" not in self.defined
and f"defaults_{var}" not in self.defined
):
undefined_uses.append(
f"{path}:{lineno}: '{{{{ {var} }}}}' used but not defined"
)
except Exception:
# Ignore unreadable files
pass
if undefined_uses:

View File

@@ -18,20 +18,20 @@ class TestDomainMappings(unittest.TestCase):
def test_empty_apps(self):
apps = {}
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
self.assertEqual(result, [])
def test_app_without_domains(self):
apps = {'web-app-desktop': {}}
# no domains key → no mappings
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
self.assertEqual(result, [])
def test_empty_domains_cfg(self):
apps = {'web-app-desktop': {'domains': {}}}
default = 'desktop.example.com'
expected = []
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
self.assertEqual(result, expected)
def test_explicit_aliases(self):
@@ -46,7 +46,7 @@ class TestDomainMappings(unittest.TestCase):
expected = [
{'source': 'alias.com', 'target': default},
]
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
# order not important
self.assertCountEqual(result, expected)
@@ -61,7 +61,7 @@ class TestDomainMappings(unittest.TestCase):
expected = [
{'source': 'desktop.example.com', 'target': 'foo.com'}
]
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
self.assertEqual(result, expected)
def test_canonical_dict(self):
@@ -78,7 +78,7 @@ class TestDomainMappings(unittest.TestCase):
expected = [
{'source': 'desktop.example.com', 'target': 'one.com'}
]
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
self.assertEqual(result, expected)
def test_multiple_apps(self):
@@ -94,7 +94,7 @@ class TestDomainMappings(unittest.TestCase):
{'source': 'a1.com', 'target': 'desktop.example.com'},
{'source': 'mastodon.example.com', 'target': 'c2.com'},
]
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
self.assertCountEqual(result, expected)
def test_multiple_aliases(self):
@@ -108,7 +108,7 @@ class TestDomainMappings(unittest.TestCase):
{'source': 'a1.com', 'target': 'desktop.example.com'},
{'source': 'a2.com', 'target': 'desktop.example.com'}
]
result = self.filter.domain_mappings(apps, self.primary)
result = self.filter.domain_mappings(apps, self.primary, True)
self.assertCountEqual(result, expected)
def test_invalid_aliases_type(self):
@@ -116,8 +116,77 @@ class TestDomainMappings(unittest.TestCase):
'web-app-desktop': {'server':{'domains': {'aliases': 123}}}
}
with self.assertRaises(AnsibleFilterError):
self.filter.domain_mappings(apps, self.primary)
self.filter.domain_mappings(apps, self.primary, True)
def test_canonical_not_default_no_autobuild(self):
"""
When only a canonical different from the default exists and auto_build_aliases is False,
we should NOT auto-generate a default alias -> canonical mapping.
"""
apps = {
'web-app-desktop': {
'server': {
'domains': {'canonical': ['foo.com']}
}
}
}
result = self.filter.domain_mappings(apps, self.primary, False)
self.assertEqual(result, []) # no auto-added default alias
def test_aliases_and_canonical_no_autobuild_still_adds_default(self):
"""
If explicit aliases are present, the filter always appends the default domain
to the alias list (to cover 'www'/'root' style defaults), regardless of auto_build_aliases.
With a canonical set, both the explicit alias and the default should point to the canonical.
"""
apps = {
'web-app-desktop': {
'server': {
'domains': {
'aliases': ['alias.com'],
'canonical': ['foo.com']
}
}
}
}
expected = [
{'source': 'alias.com', 'target': 'foo.com'},
{'source': 'desktop.example.com', 'target': 'foo.com'},
]
result = self.filter.domain_mappings(apps, self.primary, False)
self.assertCountEqual(result, expected)
def test_mixed_apps_no_autobuild(self):
"""
One app with only canonical (no aliases) and one app with only aliases:
- The canonical-only app produces no mappings when auto_build_aliases is False.
- The alias-only app maps its aliases to its default domain; default self-mapping is skipped.
"""
apps = {
'web-app-desktop': {
'server': {'domains': {'canonical': ['c1.com']}}
},
'web-app-mastodon': {
'server': {'domains': {'aliases': ['m1.com']}}
},
}
expected = [
{'source': 'm1.com', 'target': 'mastodon.example.com'},
]
result = self.filter.domain_mappings(apps, self.primary, False)
self.assertCountEqual(result, expected)
def test_no_domains_key_no_autobuild(self):
"""
App ohne 'server.domains' erzeugt keine Mappings, unabhängig von auto_build_aliases.
"""
apps = {
'web-app-desktop': {
# no 'server' or 'domains'
}
}
result = self.filter.domain_mappings(apps, self.primary, False)
self.assertEqual(result, [])
if __name__ == "__main__":
unittest.main()