mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-08 18:35:11 +00:00
Compare commits
9 Commits
a10ba78a5a
...
96ded68ef4
| Author | SHA1 | Date | |
|---|---|---|---|
| 96ded68ef4 | |||
| 2d8967d559 | |||
| 5e616d3962 | |||
| 0f85d27a4d | |||
| c6677ca61b | |||
| 83ce88a048 | |||
| 7d150fa021 | |||
| 2806aab89e | |||
| 61772d5916 |
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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) }}"
|
||||
|
||||
@@ -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
|
||||
@@ -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('')))
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -13,6 +13,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "accounting.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
script-src-elem:
|
||||
|
||||
@@ -23,6 +23,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- baserow.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
csp:
|
||||
whitelist:
|
||||
worker-src:
|
||||
|
||||
@@ -19,6 +19,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "meet.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
credentials: {}
|
||||
|
||||
docker:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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('') }}"
|
||||
|
||||
@@ -10,6 +10,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "bridgyfed.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
whitelist: {}
|
||||
flags: {}
|
||||
|
||||
@@ -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
|
||||
@@ -29,6 +29,8 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "www.{{ PRIMARY_DOMAIN }}"
|
||||
docker:
|
||||
services:
|
||||
desktop:
|
||||
|
||||
@@ -20,6 +20,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "forum.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -4,3 +4,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- elk.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -13,6 +13,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "social.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
script-src-elem:
|
||||
|
||||
@@ -24,9 +24,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "audio.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "music.{{ PRIMARY_DOMAIN }}"
|
||||
- "sound.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -22,4 +22,4 @@ docker:
|
||||
server:
|
||||
domains:
|
||||
canonical:
|
||||
- gitlab.{{ PRIMARY_DOMAIN }}
|
||||
- lab.git.{{ PRIMARY_DOMAIN }}
|
||||
|
||||
@@ -4,3 +4,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- jenkins.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
|
||||
@@ -31,6 +31,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "jira.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
status_codes:
|
||||
default: 405
|
||||
rbac:
|
||||
|
||||
@@ -10,7 +10,8 @@ features:
|
||||
server:
|
||||
domains:
|
||||
canonical:
|
||||
- "cms.{{ PRIMARY_DOMAIN }}"
|
||||
- "joomla.cms.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
|
||||
@@ -29,6 +29,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "auth.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
scopes:
|
||||
nextcloud: nextcloud
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ server:
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
aliases:
|
||||
- "ldap.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
canonical:
|
||||
- lam.{{ PRIMARY_DOMAIN }}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "newsletter.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
status_codes:
|
||||
default: 404
|
||||
docker:
|
||||
|
||||
@@ -12,9 +12,8 @@ server:
|
||||
whitelist: {}
|
||||
domains:
|
||||
canonical:
|
||||
- "shop.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "magento.{{ PRIMARY_DOMAIN }}"
|
||||
- "magento.shop.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
php:
|
||||
|
||||
@@ -13,8 +13,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "{{ SYSTEM_EMAIL.HOST }}"
|
||||
alias:
|
||||
- "mailu.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,6 +10,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "microblog.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
whitelist:
|
||||
frame-src:
|
||||
|
||||
@@ -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') }}"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -3,8 +3,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "m.wiki.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "media.wiki.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,8 +15,8 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "event.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "events.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -26,6 +26,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "academy.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -27,3 +27,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "slides.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
@@ -18,6 +18,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "cloud.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
volumes:
|
||||
data: nextcloud_data
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,3 +10,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- oauth2-proxy.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
|
||||
@@ -30,8 +30,8 @@ server:
|
||||
- "data:"
|
||||
domains:
|
||||
canonical:
|
||||
- "project.{{ PRIMARY_DOMAIN }}"
|
||||
|
||||
- "open.project.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -24,8 +24,8 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "video.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "videos.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
docker:
|
||||
services:
|
||||
redis:
|
||||
|
||||
@@ -26,6 +26,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- pgadmin.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -15,3 +15,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- phpldapadmin.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -23,8 +23,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "picture.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "pictures.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
redis:
|
||||
|
||||
@@ -27,9 +27,8 @@ server:
|
||||
flags: {}
|
||||
domains:
|
||||
canonical:
|
||||
- "ticket.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "pretix.{{ PRIMARY_DOMAIN }}"
|
||||
- "ticket.shop.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
rbac:
|
||||
roles: {}
|
||||
plugins:
|
||||
|
||||
@@ -4,3 +4,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "wheel.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
@@ -10,6 +10,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "inventory.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
script-src:
|
||||
|
||||
@@ -16,3 +16,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "docs.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
@@ -17,3 +17,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- syncope.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
|
||||
@@ -31,4 +31,4 @@ server:
|
||||
unsafe-eval: true
|
||||
domains:
|
||||
canonical:
|
||||
- "kanban.{{ PRIMARY_DOMAIN }}"
|
||||
- "kanban.project.{{ PRIMARY_DOMAIN }}"
|
||||
|
||||
@@ -41,6 +41,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "blog.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -35,6 +35,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "x.wiki.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
rbac:
|
||||
roles: {}
|
||||
ldap:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -17,8 +17,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "s.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "short.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
|
||||
@@ -4,3 +4,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- asset.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
|
||||
@@ -6,3 +6,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "cdn.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
@@ -2,6 +2,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "collabora.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
whitelist:
|
||||
frame-ancestors:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,5 +6,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "file.{{ PRIMARY_DOMAIN }}"
|
||||
alias:
|
||||
- "files.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
@@ -6,3 +6,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "html.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
@@ -8,6 +8,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "logout.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
|
||||
@@ -21,6 +21,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "icons.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
rbac:
|
||||
roles:
|
||||
mail-bot:
|
||||
|
||||
@@ -5,3 +5,4 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- xmpp.{{ PRIMARY_DOMAIN }}
|
||||
aliases: []
|
||||
@@ -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')
|
||||
}}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user