mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-03 19:58:14 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			1126765da2
			...
			14e868a644
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 14e868a644 | |||
| 2a1a956739 | |||
| bd2dde3af6 | 
@@ -10,12 +10,12 @@
 | 
			
		||||
## Helper Variables:
 | 
			
		||||
_oidc_client_realm:         "{{ OIDC.CLIENT.REALM if OIDC.CLIENT is defined and OIDC.CLIENT.REALM is defined else SOFTWARE_NAME | lower }}"
 | 
			
		||||
_oidc_url:                  "{{ 
 | 
			
		||||
                                (OIDC.URL 
 | 
			
		||||
                                  if (oidc is defined and OIDC.URL is defined) 
 | 
			
		||||
                                ( OIDC.URL
 | 
			
		||||
                                  if (OIDC is defined and OIDC.URL is defined) 
 | 
			
		||||
                                  else WEB_PROTOCOL ~ '://' ~ (domains | get_domain('web-app-keycloak'))
 | 
			
		||||
                                ) 
 | 
			
		||||
                                ).rstrip('/') 
 | 
			
		||||
                            }}"
 | 
			
		||||
_oidc_client_issuer_url:    "{{ _oidc_url }}/realms/{{_oidc_client_realm}}/"
 | 
			
		||||
_oidc_client_issuer_url:    "{{ _oidc_url ~ '/realms/' ~ _oidc_client_realm }}"
 | 
			
		||||
_oidc_client_id:            "{{ OIDC.CLIENT.ID if OIDC.CLIENT is defined and OIDC.CLIENT.ID is defined else SOFTWARE_NAME | lower }}"
 | 
			
		||||
 | 
			
		||||
defaults_oidc:
 | 
			
		||||
@@ -23,16 +23,16 @@ defaults_oidc:
 | 
			
		||||
  CLIENT:
 | 
			
		||||
    ID:                   "{{ _oidc_client_id }}"                                                  # Client identifier, typically matching your primary domain
 | 
			
		||||
#   secret:                                                                                        # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters
 | 
			
		||||
    REALM:                "{{_oidc_client_realm}}"                                          # The realm to which the client belongs in the OIDC provider
 | 
			
		||||
    ISSUER_URL:           "{{_oidc_client_issuer_url}}"                                     # Base URL of the OIDC provider (issuer)
 | 
			
		||||
    DISCOVERY_DOCUMENT:   "{{_oidc_client_issuer_url}}/.well-known/openid-configuration"    # URL for fetching the provider's configuration details
 | 
			
		||||
    AUTHORIZE_URL:        "{{_oidc_client_issuer_url}}/protocol/openid-connect/auth"        # Endpoint to start the authorization process
 | 
			
		||||
    TOKEN_URL:            "{{_oidc_client_issuer_url}}/protocol/openid-connect/token"       # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url')
 | 
			
		||||
    USER_INFO_URL:        "{{_oidc_client_issuer_url}}/protocol/openid-connect/userinfo"    # Endpoint to retrieve user information
 | 
			
		||||
    LOGOUT_URL:           "{{_oidc_client_issuer_url}}/protocol/openid-connect/logout"      # Endpoint to log out the user
 | 
			
		||||
    CHANGE_CREDENTIALS:   "{{_oidc_client_issuer_url}}account/account-security/signing-in"  # URL for managing or changing user credentials
 | 
			
		||||
    CERTS:                "{{_oidc_client_issuer_url}}/protocol/openid-connect/certs"       # JSON Web Key Set (JWKS)
 | 
			
		||||
    RESET_CREDENTIALS:    "{{_oidc_client_issuer_url}}/login-actions/reset-credentials?client_id={{ _oidc_client_id }}" # Password reset url
 | 
			
		||||
    REALM:                "{{ _oidc_client_realm }}"                                               # The realm to which the client belongs in the OIDC provider
 | 
			
		||||
    ISSUER_URL:           "{{ _oidc_client_issuer_url }}"                                          # Base URL of the OIDC provider (issuer)
 | 
			
		||||
    DISCOVERY_DOCUMENT:   "{{ _oidc_client_issuer_url ~ '/.well-known/openid-configuration' }}"    # URL for fetching the provider's configuration details
 | 
			
		||||
    AUTHORIZE_URL:        "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/auth' }}"        # Endpoint to start the authorization process
 | 
			
		||||
    TOKEN_URL:            "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/token' }}"       # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url')
 | 
			
		||||
    USER_INFO_URL:        "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/userinfo' }}"    # Endpoint to retrieve user information
 | 
			
		||||
    LOGOUT_URL:           "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/logout' }}"      # Endpoint to log out the user
 | 
			
		||||
    CHANGE_CREDENTIALS:   "{{ _oidc_client_issuer_url ~ '/account/account-security/signing-in' }}" # URL for managing or changing user credentials
 | 
			
		||||
    CERTS:                "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/certs' }}"       # JSON Web Key Set (JWKS)
 | 
			
		||||
    RESET_CREDENTIALS:    "{{ _oidc_client_issuer_url ~ '/login-actions/reset-credentials?client_id=' ~ _oidc_client_id }}" # Password reset url
 | 
			
		||||
  BUTTON_TEXT:            "SSO Login ({{ PRIMARY_DOMAIN | upper }})"                               # Default button text
 | 
			
		||||
  ATTRIBUTES:
 | 
			
		||||
    # Attribut to identify the user
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
# Cloudflare DNS Records
 | 
			
		||||
 | 
			
		||||
## Description
 | 
			
		||||
 | 
			
		||||
This Ansible role automates the management of DNS A-records in Cloudflare zones. It uses the [community.general.cloudflare_dns](https://docs.ansible.com/ansible/latest/collections/community/general/cloudflare_dns_module.html) module to create or update A-records for a list of domains, automatically detects the correct zone for each record, and supports configurable proxy settings.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
Looping over a provided list of domains (`cloudflare_domains`), this role:
 | 
			
		||||
- Determines the zone name by extracting the last two labels of each domain.
 | 
			
		||||
- Ensures an A-record for each domain points to the specified IP (`cloudflare_target_ip`).
 | 
			
		||||
- Honors the `proxied` flag to switch between DNS-only and Cloudflare-proxied modes.
 | 
			
		||||
- Provides an optional debug task (`MODE_DEBUG`) to output the domain list before changes.
 | 
			
		||||
 | 
			
		||||
Ideal for environments where bulk or dynamic DNS updates are needed, this role abstracts away the complexity of Cloudflare’s zone and record API.
 | 
			
		||||
 | 
			
		||||
## Purpose
 | 
			
		||||
 | 
			
		||||
Cloudflare DNS Records delivers an idempotent, scalable solution for managing A-records across multiple Cloudflare zones. Whether you need to onboard hundreds of domains or toggle proxy settings in CI/CD pipelines, this role handles the orchestration and ensures consistency.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- **Automatic Zone Detection:** Parses each domain to derive its zone (`example.com`) without manual intervention.  
 | 
			
		||||
- **Bulk Record Management:** Creates or updates A-records for all entries in `cloudflare_domains`.  
 | 
			
		||||
- **Proxy Toggle:** Configure `proxied: true` or `false` per record to switch between DNS-only and proxied modes.  
 | 
			
		||||
- **Debug Support:** Enable `MODE_DEBUG` to print the domain list for validation before execution.  
 | 
			
		||||
- **Flexible Authentication:** Supports both API token (`api_token`) and Global API key + email.  
 | 
			
		||||
- **Low-TTL Option:** Use `ttl: 1` for rapid DNS propagation during dynamic updates.
 | 
			
		||||
 | 
			
		||||
## Author
 | 
			
		||||
 | 
			
		||||
Kevin Veen-Birkenbach
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
Infinito.Nexus NonCommercial License  
 | 
			
		||||
<https://s.infinito.nexus/license>
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
galaxy_info:
 | 
			
		||||
  author: "Kevin Veen-Birkenbach"
 | 
			
		||||
  description: "Manages DNS A-records in Cloudflare zones."
 | 
			
		||||
  license: "Infinito.Nexus NonCommercial License"
 | 
			
		||||
  license_url: "https://s.infinito.nexus/license"
 | 
			
		||||
  company: |
 | 
			
		||||
    Kevin Veen-Birkenbach
 | 
			
		||||
    Consulting & Coaching Solutions
 | 
			
		||||
    https://www.veen.world
 | 
			
		||||
  min_ansible_version: "2.9"
 | 
			
		||||
  platforms:
 | 
			
		||||
    - name: "All"
 | 
			
		||||
      versions:
 | 
			
		||||
        - "all"
 | 
			
		||||
  galaxy_tags:
 | 
			
		||||
    - "cloudflare"
 | 
			
		||||
    - "dns"
 | 
			
		||||
    - "records"
 | 
			
		||||
    - "ansible"
 | 
			
		||||
    - "network"
 | 
			
		||||
    - "automation"
 | 
			
		||||
  repository: "https://s.infinito.nexus/code"
 | 
			
		||||
  issue_tracker_url: "https://s.infinito.nexus/issues"
 | 
			
		||||
  documentation: "https://docs.infinito.nexus"
 | 
			
		||||
dependencies: []
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
# run_once_srv_web_7_7_dns_records: deactivated
 | 
			
		||||
 | 
			
		||||
- name: Create or update Cloudflare A-record for {{ item }}
 | 
			
		||||
  community.general.cloudflare_dns:
 | 
			
		||||
    api_token: "{{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
    zone: "{{ item.split('.')[-2:] | join('.') }}"
 | 
			
		||||
    state: present
 | 
			
		||||
    type: A
 | 
			
		||||
    name: "{{ item }}"
 | 
			
		||||
    content: "{{ cloudflare_target_ip }}"
 | 
			
		||||
    ttl: 1
 | 
			
		||||
    proxied: "{{ cloudflare_proxied | int }}"
 | 
			
		||||
  loop: "{{ cloudflare_domains }}"
 | 
			
		||||
  loop_control:
 | 
			
		||||
    label: "{{ item }}"
 | 
			
		||||
  async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
 | 
			
		||||
  poll:  "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
 | 
			
		||||
@@ -58,10 +58,12 @@
 | 
			
		||||
 | 
			
		||||
- name: Include DNS role to register Gitea domain(s)
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-web-7-7-dns-records
 | 
			
		||||
    name: sys-dns-cloudflare-records
 | 
			
		||||
  vars:
 | 
			
		||||
    CLOUDFLARE_API_TOKEN:   "{{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
    cloudflare_domains:     "{{ [ domains | get_domain(application_id) ] }}"
 | 
			
		||||
    cloudflare_target_ip:   "{{ networks.internet.ip4 }}"
 | 
			
		||||
    cloudflare_proxied:     false
 | 
			
		||||
    cloudflare_records:
 | 
			
		||||
      - zone:     "{{ domains | get_domain(application_id) | to_zone }}"
 | 
			
		||||
        type:     A
 | 
			
		||||
        name:    "{{ domains | get_domain(application_id) }}"
 | 
			
		||||
        content: "{{ networks.internet.ip4 }}"
 | 
			
		||||
        proxied: false  # Necessary for SSH port
 | 
			
		||||
  when: DNS_PROVIDER == 'cloudflare'
 | 
			
		||||
							
								
								
									
										2
									
								
								roles/web-opt-rdr-www/Todo.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								roles/web-opt-rdr-www/Todo.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
# To-dos
 | 
			
		||||
- Test flavor 'edge'
 | 
			
		||||
							
								
								
									
										5
									
								
								roles/web-opt-rdr-www/config/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								roles/web-opt-rdr-www/config/main.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# The following defines the redirect flavor
 | 
			
		||||
# Possible options 
 | 
			
		||||
#  - 'edge' for configuration via cloudflare
 | 
			
		||||
#  - 'origin' via native nginx 
 | 
			
		||||
prefered_flavor: "origin"
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
galaxy_info:
 | 
			
		||||
  author: "Kevin Veen-Birkenbach"
 | 
			
		||||
  description: "An Ansible role to redirect www subdomains to non-www domains in Nginx"
 | 
			
		||||
  description: "An Ansible role to redirect www subdomains to bare domains (apex). Supports Cloudflare edge redirects or local Nginx redirects."
 | 
			
		||||
  license: "Infinito.Nexus NonCommercial License"
 | 
			
		||||
  license_url: "https://s.infinito.nexus/license"
 | 
			
		||||
  company: |
 | 
			
		||||
@@ -9,14 +9,14 @@ galaxy_info:
 | 
			
		||||
    https://www.veen.world
 | 
			
		||||
  min_ansible_version: "2.9"
 | 
			
		||||
  platforms:
 | 
			
		||||
  - name: Archlinux
 | 
			
		||||
    - name: "Archlinux"
 | 
			
		||||
      versions:
 | 
			
		||||
    - rolling
 | 
			
		||||
        - "rolling"
 | 
			
		||||
  galaxy_tags:
 | 
			
		||||
    - nginx
 | 
			
		||||
    - redirect
 | 
			
		||||
    - www
 | 
			
		||||
  - wildcard
 | 
			
		||||
    - cloudflare
 | 
			
		||||
    - seo
 | 
			
		||||
  repository: "https://s.infinito.nexus/code"
 | 
			
		||||
  issue_tracker_url: "https://s.infinito.nexus/issues"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										134
									
								
								roles/web-opt-rdr-www/tasks/cloudflare_redirect_rule.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								roles/web-opt-rdr-www/tasks/cloudflare_redirect_rule.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
# This task file ensures that a Cloudflare "Dynamic Redirect Rule" exists
 | 
			
		||||
# which redirects www.example.com → https://example.com{uri} with 301.
 | 
			
		||||
 | 
			
		||||
# Required vars to pass in:
 | 
			
		||||
#   www_fqdn:   "www.example.com"
 | 
			
		||||
#   apex_url:   "https://example.com"
 | 
			
		||||
#   CLOUDFLARE_API_TOKEN
 | 
			
		||||
 | 
			
		||||
- name: "Derive zone and apex from www_fqdn"
 | 
			
		||||
  set_fact:
 | 
			
		||||
    cf_zone_name: "{{ www_fqdn | to_zone }}"                       # e.g. "cymais.cloud"
 | 
			
		||||
    domain_apex: "{{ www_fqdn | regex_replace('^www\\.', '') }}"   # e.g. "academy.cymais.cloud"
 | 
			
		||||
 | 
			
		||||
- name: "Cloudflare: Lookup zone id for {{ cf_zone_name }}"
 | 
			
		||||
  ansible.builtin.uri:
 | 
			
		||||
    url: "https://api.cloudflare.com/client/v4/zones?name={{ cf_zone_name }}"
 | 
			
		||||
    method: GET
 | 
			
		||||
    headers:
 | 
			
		||||
      Authorization: "Bearer {{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
      Content-Type: "application/json"
 | 
			
		||||
    return_content: true
 | 
			
		||||
  register: cf_zone_lookup
 | 
			
		||||
  failed_when: >
 | 
			
		||||
    (cf_zone_lookup.status != 200) or
 | 
			
		||||
    (cf_zone_lookup.json.result | length) == 0
 | 
			
		||||
 | 
			
		||||
- name: "Set fact: zone_id"
 | 
			
		||||
  set_fact:
 | 
			
		||||
    cf_zone_id: "{{ (cf_zone_lookup.json.result | first).id }}"
 | 
			
		||||
 | 
			
		||||
- name: "Remove conflicting A/AAAA record for {{ www_fqdn }}"
 | 
			
		||||
  community.general.cloudflare_dns:
 | 
			
		||||
    api_token: "{{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
    zone: "{{ cf_zone_name }}"
 | 
			
		||||
    type: "A"
 | 
			
		||||
    name: "{{ www_fqdn }}"
 | 
			
		||||
    state: absent
 | 
			
		||||
  ignore_errors: true
 | 
			
		||||
 | 
			
		||||
- name: "Cloudflare DNS: ensure {{ www_fqdn }} is proxied (CNAME)"
 | 
			
		||||
  community.general.cloudflare_dns:
 | 
			
		||||
    api_token: "{{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
    zone: "{{ cf_zone_name }}"
 | 
			
		||||
    type: "CNAME"
 | 
			
		||||
    name: "{{ www_fqdn }}"
 | 
			
		||||
    value: "{{ domain_apex }}"
 | 
			
		||||
    proxied: true
 | 
			
		||||
    ttl: 1
 | 
			
		||||
    state: present
 | 
			
		||||
    
 | 
			
		||||
# 1) Fetch existing redirect rulesets
 | 
			
		||||
- name: "Cloudflare Rulesets: list dynamic redirect rulesets"
 | 
			
		||||
  ansible.builtin.uri:
 | 
			
		||||
    url: "https://api.cloudflare.com/client/v4/zones/{{ cf_zone_id }}/rulesets?phase=http_request_dynamic_redirect"
 | 
			
		||||
    method: GET
 | 
			
		||||
    headers:
 | 
			
		||||
      Authorization: "Bearer {{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
      Content-Type: "application/json"
 | 
			
		||||
    return_content: true
 | 
			
		||||
  register: cf_rulesets
 | 
			
		||||
 | 
			
		||||
- name: "Pick existing custom ruleset (if any)"
 | 
			
		||||
  set_fact:
 | 
			
		||||
    cf_redirect_ruleset: >-
 | 
			
		||||
      {{
 | 
			
		||||
        (cf_rulesets.json.result | default([]))
 | 
			
		||||
        | selectattr('kind','equalto','zone')
 | 
			
		||||
        | list
 | 
			
		||||
        | first
 | 
			
		||||
      }}
 | 
			
		||||
    cf_redirect_ruleset_id: "{{ (cf_redirect_ruleset.id | default('')) }}"
 | 
			
		||||
 | 
			
		||||
# Desired redirect rule object
 | 
			
		||||
- name: "Build desired redirect rule object"
 | 
			
		||||
  set_fact:
 | 
			
		||||
    desired_redirect_rule:
 | 
			
		||||
      ref: "redirect_www_to_apex"
 | 
			
		||||
      description: "Redirect www → apex (301)"
 | 
			
		||||
      expression: "http.host eq \"{{ www_fqdn }}\""
 | 
			
		||||
      action: "redirect"
 | 
			
		||||
      action_parameters:
 | 
			
		||||
        from_value:
 | 
			
		||||
          target_url:
 | 
			
		||||
            expression: "\"{{ apex_url }}\" + http.request.uri.path + (len(http.request.uri.query) > 0 ? (\"?\" + http.request.uri.query) : \"\")"
 | 
			
		||||
          status_code: 301
 | 
			
		||||
          preserve_query_string: true
 | 
			
		||||
 | 
			
		||||
# 2) Update existing ruleset (PUT)
 | 
			
		||||
- name: "Update existing dynamic redirect ruleset (PUT)"
 | 
			
		||||
  when: cf_redirect_ruleset_id | length > 0
 | 
			
		||||
  vars:
 | 
			
		||||
    existing_rules: "{{ cf_redirect_ruleset.rules | default([]) }}"
 | 
			
		||||
    merged_rules: >-
 | 
			
		||||
      {{
 | 
			
		||||
        (
 | 
			
		||||
          (existing_rules | rejectattr('ref','equalto', desired_redirect_rule.ref) | list)
 | 
			
		||||
          + [ desired_redirect_rule ]
 | 
			
		||||
        )
 | 
			
		||||
      }}
 | 
			
		||||
  ansible.builtin.uri:
 | 
			
		||||
    url: "https://api.cloudflare.com/client/v4/zones/{{ cf_zone_id }}/rulesets/{{ cf_redirect_ruleset_id }}"
 | 
			
		||||
    method: PUT
 | 
			
		||||
    headers:
 | 
			
		||||
      Authorization: "Bearer {{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
      Content-Type: "application/json"
 | 
			
		||||
    status_code: 200
 | 
			
		||||
    body_format: json
 | 
			
		||||
    body:
 | 
			
		||||
      name: "{{ cf_redirect_ruleset.name | default('www-to-apex') }}"
 | 
			
		||||
      kind: "zone"
 | 
			
		||||
      phase: "http_request_dynamic_redirect"
 | 
			
		||||
      rules: "{{ merged_rules }}"
 | 
			
		||||
  register: cf_ruleset_update
 | 
			
		||||
  changed_when: cf_ruleset_update.status in [200]
 | 
			
		||||
 | 
			
		||||
# 3) Create new ruleset (POST)
 | 
			
		||||
- name: "Create dynamic redirect ruleset (POST)"
 | 
			
		||||
  when: cf_redirect_ruleset_id | length == 0
 | 
			
		||||
  ansible.builtin.uri:
 | 
			
		||||
    url: "https://api.cloudflare.com/client/v4/zones/{{ cf_zone_id }}/rulesets"
 | 
			
		||||
    method: POST
 | 
			
		||||
    headers:
 | 
			
		||||
      Authorization: "Bearer {{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
      Content-Type: "application/json"
 | 
			
		||||
    status_code: 200
 | 
			
		||||
    body_format: json
 | 
			
		||||
    body:
 | 
			
		||||
      name: "www-to-apex"
 | 
			
		||||
      kind: "zone"
 | 
			
		||||
      phase: "http_request_dynamic_redirect"
 | 
			
		||||
      rules:
 | 
			
		||||
        - "{{ desired_redirect_rule }}"
 | 
			
		||||
  register: cf_ruleset_create
 | 
			
		||||
  changed_when: cf_ruleset_create.status in [200]
 | 
			
		||||
@@ -6,22 +6,42 @@
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_web_opt_rdr_www is not defined
 | 
			
		||||
 | 
			
		||||
- name: Filter www-prefixed domains from current_play_domains_all
 | 
			
		||||
  set_fact:
 | 
			
		||||
    www_domains: "{{ current_play_domains_all | select('match', '^www\\.') | list }}"
 | 
			
		||||
 | 
			
		||||
- name: Include web-opt-rdr-domains role for www-to-bare redirects
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: web-opt-rdr-domains
 | 
			
		||||
  vars:
 | 
			
		||||
    domain_mappings: "{{ www_domains | map('regex_replace', '^www\\.(.+)$', '{ source: \"www.\\1\", target: \"\\1\" }') | map('from_yaml') | list }}"
 | 
			
		||||
    domain_mappings: "{{ REDIRECT_WWW_DOMAINS | map('regex_replace', '^www\\.(.+)$', '{ source: \"www.\\1\", target: \"\\1\" }') | map('from_yaml') | list }}"
 | 
			
		||||
  when: REDIRECT_WWW_FLAVOR == 'origin'
 | 
			
		||||
 | 
			
		||||
- name: Include DNS role to set redirects
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-web-7-7-dns-records
 | 
			
		||||
    name: sys-dns-cloudflare-records
 | 
			
		||||
  vars:
 | 
			
		||||
    CLOUDFLARE_API_TOKEN: "{{ CLOUDFLARE_API_TOKEN }}"
 | 
			
		||||
    cloudflare_domains: "{{ www_domains }}"
 | 
			
		||||
    cloudflare_target_ip: "{{ networks.internet.ip4 }}"
 | 
			
		||||
    cloudflare_proxied: false
 | 
			
		||||
  when: DNS_PROVIDER == 'cloudflare'
 | 
			
		||||
    cloudflare_records: |
 | 
			
		||||
      {%- set bare = REDIRECT_WWW_DOMAINS | map('regex_replace', '^www\\.(.+)$', '\\1') | list -%}
 | 
			
		||||
      [
 | 
			
		||||
      {%- for d in bare -%}
 | 
			
		||||
        {
 | 
			
		||||
          "type": "A",
 | 
			
		||||
          "zone": "{{ d | to_zone }}",
 | 
			
		||||
          "name": "{{ d }}",
 | 
			
		||||
          "content": "{{ networks.internet.ip4 }}",
 | 
			
		||||
          "proxied": {{ REDIRECT_WWW_FLAVOR == 'edge' }},
 | 
			
		||||
          "ttl": 1
 | 
			
		||||
        }{{ "," if not loop.last else "" }}
 | 
			
		||||
      {%- endfor -%}
 | 
			
		||||
      ]
 | 
			
		||||
  when: 
 | 
			
		||||
    - DNS_PROVIDER == 'cloudflare'
 | 
			
		||||
    - REDIRECT_WWW_FLAVOR == 'origin'
 | 
			
		||||
 | 
			
		||||
- name: Include Cloudflare redirect rule to enforce www → apex
 | 
			
		||||
  include_tasks: cloudflare_redirect_rule.yml
 | 
			
		||||
  vars:
 | 
			
		||||
    domain: "{{ item | regex_replace('^www\\.', '') }}"
 | 
			
		||||
    www_fqdn: "{{ item }}"
 | 
			
		||||
    apex_url: "{{ WEB_PROTOCOL }}://{{ item | regex_replace('^www\\.', '') }}"
 | 
			
		||||
  loop: "{{ REDIRECT_WWW_DOMAINS }}"
 | 
			
		||||
  when: REDIRECT_WWW_FLAVOR == 'edge'
 | 
			
		||||
    
 | 
			
		||||
  
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,6 @@
 | 
			
		||||
# General
 | 
			
		||||
application_id:       "web-opt-rdr-www"
 | 
			
		||||
 | 
			
		||||
# Redirect WWW
 | 
			
		||||
REDIRECT_WWW_FLAVOR:  "{{ applications | get_app_conf(application_id, 'prefered_flavor') if DNS_PROVIDER == 'cloudflare' else 'origin' }}"
 | 
			
		||||
REDIRECT_WWW_DOMAINS: "{{ current_play_domains_all | select('match', '^www\\.') | list }}"
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import unittest
 | 
			
		||||
import yaml
 | 
			
		||||
import glob
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
class TestWebRolesDomains(unittest.TestCase):
 | 
			
		||||
    def test_canonical_domains_present_and_not_empty(self):
 | 
			
		||||
        """
 | 
			
		||||
        Check all roles/web-*/config/main.yml files:
 | 
			
		||||
        - must have domains.canonical defined
 | 
			
		||||
        - domains.canonical must not be empty dict, empty list, or empty string
 | 
			
		||||
        """
 | 
			
		||||
        role_config_paths = glob.glob("roles/web-*/config/main.yml")
 | 
			
		||||
        self.assertTrue(role_config_paths, "No roles/web-*/config/main.yml files found.")
 | 
			
		||||
 | 
			
		||||
        for path in role_config_paths:
 | 
			
		||||
            with self.subTest(role_config=path):
 | 
			
		||||
                with open(path, "r") as f:
 | 
			
		||||
                    data = yaml.safe_load(f)
 | 
			
		||||
 | 
			
		||||
                self.assertIsInstance(data, dict, f"YAML root is not a dict in {path}")
 | 
			
		||||
 | 
			
		||||
                domains = data.get('server',{}).get('domains',{})
 | 
			
		||||
                self.assertIsNotNone(domains, f"'domains' section missing in {path}")
 | 
			
		||||
                self.assertIsInstance(domains, dict, f"'domains' must be a dict in {path}")
 | 
			
		||||
 | 
			
		||||
                canonical = domains.get("canonical")
 | 
			
		||||
                self.assertIsNotNone(canonical, f"'server.domains.canonical' missing in {path}")
 | 
			
		||||
 | 
			
		||||
                # Check for emptiness
 | 
			
		||||
                empty_values = [{}, [], ""]
 | 
			
		||||
                self.assertNotIn(canonical, empty_values,
 | 
			
		||||
                    f"'server.domains.canonical' in {path} must not be empty dict, list, or empty string")
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    unittest.main()
 | 
			
		||||
		Reference in New Issue
	
	Block a user