mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 10:19:09 +00:00 
			
		
		
		
	refactor(dns): replace sys-dns-parent-hosts with sys-dns-wildcards; emit only *.parent wildcards from CURRENT_PLAY_DOMAINS_ALL
Rename filter parent_build_records→wildcard_records; create only wildcard (*.parent) A/AAAA records (no base/apex); switch to CURRENT_PLAY_DOMAINS_ALL; update vars to SYN_DNS_WILDCARD_RECORDS; adjust role/task names, defaults, and docs; add unittest expecting *.a.b from www.a.b.example.com. See: https://chatgpt.com/share/68c35dc1-7170-800f-8fbe-772e61780597
This commit is contained in:
		| @@ -1,24 +0,0 @@ | ||||
| # sys-dns-parent-hosts | ||||
|  | ||||
| Create Cloudflare DNS A/AAAA records only for **parent hosts** (hosts that have children), | ||||
| and always include the **apex** (SLD.TLD) as a parent. | ||||
|  | ||||
| Examples: | ||||
| - c.wiki.example.com  -> parent: wiki.example.com | ||||
| - a.b.example.com     -> parent: b.example.com | ||||
| - example.com (apex)  -> always included | ||||
|  | ||||
| ## Inputs | ||||
| - parent_dns_domains (list[str], optional): FQDNs to evaluate. If empty, the role flattens CURRENT_PLAY_DOMAINS. | ||||
| - PRIMARY_DOMAIN (apex), defaults_networks.internet.ip4, optional defaults_networks.internet.ip6 | ||||
| - Flags: | ||||
|   - parent_dns_enabled (bool, default: true) | ||||
|   - parent_dns_ipv6_enabled (bool, default: true) | ||||
|   - parent_dns_proxied (bool, default: false) | ||||
|  | ||||
| ## Usage | ||||
| - Include the role once after your constructor stage has set CURRENT_PLAY_DOMAINS. | ||||
|  | ||||
| ## Tests | ||||
| Unit tests: tests/unit/roles/sys-dns-parent-hosts/filter_plugins/test_parent_dns.py | ||||
| Run with: pytest -q | ||||
| @@ -1,2 +0,0 @@ | ||||
| parent_dns_proxied: false | ||||
| parent_dns_domains: [] | ||||
| @@ -1,3 +0,0 @@ | ||||
| - block: | ||||
|   - include_tasks: 01_core.yml | ||||
|   when: run_once_sys_dns_parent_hosts is not defined | ||||
							
								
								
									
										20
									
								
								roles/sys-dns-wildcards/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								roles/sys-dns-wildcards/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| # sys-dns-wildcards | ||||
|  | ||||
| Create Cloudflare DNS **wildcard** A/AAAA records (`*.parent`) only for **parent hosts** (hosts that have children). | ||||
| The **apex** (SLD.TLD) is considered when computing parents, but **no base host** or `*.apex` record is created by this role. | ||||
|  | ||||
| Examples: | ||||
| - c.wiki.example.com  -> parent: wiki.example.com -> creates: `*.wiki.example.com` | ||||
| - a.b.example.com     -> parent: b.example.com    -> creates: `*.b.example.com` | ||||
| - example.com (apex)  -> used to detect parents, but **no** `example.com` or `*.example.com` record is created | ||||
|  | ||||
| ## Inputs | ||||
| - parent_dns_domains (list[str], optional): FQDNs to evaluate. If empty, the role flattens CURRENT_PLAY_DOMAINS_ALL. | ||||
| - PRIMARY_DOMAIN (apex), defaults_networks.internet.ip4, optional defaults_networks.internet.ip6 | ||||
| - Flags: | ||||
|   - parent_dns_enabled (bool, default: true) | ||||
|   - parent_dns_ipv6_enabled (bool, default: true) | ||||
|   - parent_dns_proxied (bool, default: false) | ||||
|  | ||||
| ## Usage | ||||
| - Include the role once after your constructor stage has set CURRENT_PLAY_DOMAINS_ALL. | ||||
							
								
								
									
										3
									
								
								roles/sys-dns-wildcards/defaults/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								roles/sys-dns-wildcards/defaults/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| parent_dns_proxied: true | ||||
| parent_dns_domains: [] | ||||
| parent_dns_ipv6_enabled: true | ||||
| @@ -25,16 +25,12 @@ def _parent_of_child(domain: str, apex: str) -> str | None: | ||||
|     return ".".join(parts[1:])  # drop exactly the left-most label | ||||
| 
 | ||||
| 
 | ||||
| def _flatten_domains(current_play_domains: dict) -> list[str]: | ||||
| def _flatten_domains_any_structure(domains_like) -> list[str]: | ||||
|     """ | ||||
|     Accept CURRENT_PLAY_DOMAINS values as: | ||||
|       - str | ||||
|       - list/tuple/set[str] | ||||
|       - dict -> recurse one level (values must be str or list-like[str]) | ||||
|     Accepts CURRENT_PLAY_DOMAINS_ALL-like structures: | ||||
|       - dict values: str | list/tuple/set[str] | dict (one level deeper) | ||||
|     Returns unique, sorted host list. | ||||
|     """ | ||||
|     if not isinstance(current_play_domains, dict): | ||||
|         raise AnsibleFilterError("CURRENT_PLAY_DOMAINS must be a dict of {app_id: hostnames-or-structures}") | ||||
| 
 | ||||
|     hosts: list[str] = [] | ||||
| 
 | ||||
|     def _add_any(x): | ||||
| @@ -53,17 +49,18 @@ def _flatten_domains(current_play_domains: dict) -> list[str]: | ||||
|             for v in x.values(): | ||||
|                 _add_any(v) | ||||
|             return | ||||
|         raise AnsibleFilterError(f"Unsupported CURRENT_PLAY_DOMAINS value type: {type(x).__name__}") | ||||
|         raise AnsibleFilterError(f"Unsupported value type: {type(x).__name__}") | ||||
| 
 | ||||
|     for v in current_play_domains.values(): | ||||
|     if not isinstance(domains_like, dict): | ||||
|         raise AnsibleFilterError("Expected a dict for CURRENT_PLAY_DOMAINS_ALL") | ||||
|     for v in domains_like.values(): | ||||
|         _add_any(v) | ||||
| 
 | ||||
|     return sorted(set(hosts)) | ||||
| 
 | ||||
| 
 | ||||
| def _parents_from(domains: list[str], apex: str, *, min_child_depth: int, include_apex: bool) -> list[str]: | ||||
| def _parents_from(domains: list[str], apex: str, *, min_child_depth: int) -> list[str]: | ||||
|     _validate(apex) | ||||
|     parents = set([apex]) if include_apex else set() | ||||
|     parents = set() | ||||
|     for d in domains: | ||||
|         _validate(d) | ||||
|         if not d.endswith(apex): | ||||
| @@ -75,21 +72,6 @@ def _parents_from(domains: list[str], apex: str, *, min_child_depth: int, includ | ||||
|     return sorted(parents) | ||||
| 
 | ||||
| 
 | ||||
| def _relative_names(fqdns: list[str], apex: str) -> list[str]: | ||||
|     """FQDN -> relative name; '' represents the apex.""" | ||||
|     _validate(apex) | ||||
|     out: list[str] = [] | ||||
|     for d in fqdns: | ||||
|         _validate(d) | ||||
|         if not d.endswith(apex): | ||||
|             continue | ||||
|         if d == apex: | ||||
|             out.append("") | ||||
|         else: | ||||
|             out.append(d[: -(len(apex) + 1)])  # strip ".apex" | ||||
|     return sorted(set(out)) | ||||
| 
 | ||||
| 
 | ||||
| def _is_global(ip: str) -> bool: | ||||
|     try: | ||||
|         return ipaddress.ip_address(ip).is_global | ||||
| @@ -97,99 +79,79 @@ def _is_global(ip: str) -> bool: | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| def _build_cf_records( | ||||
|     rel_parents: list[str], | ||||
| def _build_wildcard_records( | ||||
|     parents: list[str], | ||||
|     apex: str, | ||||
|     *, | ||||
|     ip4: str, | ||||
|     ip6: str | None, | ||||
|     ipv6_enabled: bool, | ||||
|     proxied: bool, | ||||
|     wildcard_children: bool, | ||||
|     apex_wildcard: bool, | ||||
|     ipv6_enabled: bool, | ||||
| ) -> list[dict]: | ||||
|     if not isinstance(rel_parents, list): | ||||
|         raise AnsibleFilterError("rel_parents must be list[str]") | ||||
|     if not isinstance(parents, list): | ||||
|         raise AnsibleFilterError("parents must be list[str]") | ||||
|     _validate(apex) | ||||
|     if not ip4: | ||||
|         raise AnsibleFilterError("ip4 required") | ||||
| 
 | ||||
|     records: list[dict] = [] | ||||
| 
 | ||||
|     def _add_one(name: str, rtype: str, content: str): | ||||
|     def _add(name: str, rtype: str, content: str): | ||||
|         records.append({ | ||||
|             "zone": apex, | ||||
|             "type": rtype, | ||||
|             "name": name if name else "@", | ||||
|             "name": name, | ||||
|             "content": content, | ||||
|             "proxied": bool(proxied), | ||||
|             "ttl": 1, | ||||
|         }) | ||||
| 
 | ||||
|     for rel in sorted(set(rel_parents)): | ||||
|         # base (parent) host | ||||
|         _add_one(rel, "A", str(ip4)) | ||||
|     for p in sorted(set(parents)): | ||||
|         rel = p[:-len(apex)-1] if p != apex else ""  # relative part; apex shouldn't produce wildcard | ||||
|         if not rel: | ||||
|             # Do NOT create *.apex here (explicitly excluded by requirement) | ||||
|             continue | ||||
|         wc = f"*.{rel}" | ||||
|         _add(wc, "A", str(ip4)) | ||||
|         if ipv6_enabled and ip6 and _is_global(str(ip6)): | ||||
|             _add_one(rel, "AAAA", str(ip6)) | ||||
| 
 | ||||
|         # wildcard children under the parent | ||||
|         if rel and wildcard_children: | ||||
|             wc = f"*.{rel}" | ||||
|             _add_one(wc, "A", str(ip4)) | ||||
|             if ipv6_enabled and ip6 and _is_global(str(ip6)): | ||||
|                 _add_one(wc, "AAAA", str(ip6)) | ||||
| 
 | ||||
|     # optional apex wildcard (*.example.com) | ||||
|     if apex_wildcard: | ||||
|         _add_one("*", "A", str(ip4)) | ||||
|         if ipv6_enabled and ip6 and _is_global(str(ip6)): | ||||
|             _add_one("*", "AAAA", str(ip6)) | ||||
| 
 | ||||
|             _add(wc, "AAAA", str(ip6)) | ||||
|     return records | ||||
| 
 | ||||
| 
 | ||||
| def parent_build_records( | ||||
|     current_play_domains: dict, | ||||
| def wildcard_records( | ||||
|     current_play_domains_all: dict, | ||||
|     apex: str, | ||||
|     ip4: str, | ||||
|     ip6: str | None = None, | ||||
|     proxied: bool = False, | ||||
|     explicit_domains: list[str] | None = None, | ||||
|     include_apex: bool = True, | ||||
|     min_child_depth: int = 2, | ||||
|     wildcard_children: bool = True, | ||||
|     include_apex_wildcard: bool = False, | ||||
|     ipv6_enabled: bool = False, | ||||
|     ipv6_enabled: bool = True, | ||||
| ) -> list[dict]: | ||||
|     """ | ||||
|     Return Cloudflare A/AAAA records for: | ||||
|       - each parent host ('' == apex), | ||||
|       - optionally '*.parent' for wildcard children, | ||||
|       - optionally '*.apex'. | ||||
|     Build only wildcard records for parents: | ||||
|       for each parent 'parent.apex' -> create '*.parent' A/AAAA. | ||||
|       No base parent records and no '*.apex' are created. | ||||
|     """ | ||||
|     # source domains | ||||
|     # Source domains | ||||
|     if explicit_domains and len(explicit_domains) > 0: | ||||
|         domains = sorted(set(explicit_domains)) | ||||
|     else: | ||||
|         domains = _flatten_domains(current_play_domains) | ||||
|         domains = _flatten_domains_any_structure(current_play_domains_all) | ||||
| 
 | ||||
|     parents = _parents_from(domains, apex, min_child_depth=min_child_depth, include_apex=include_apex) | ||||
|     rel_parents = _relative_names(parents, apex) | ||||
| 
 | ||||
|     return _build_cf_records( | ||||
|         rel_parents, | ||||
|     parents = _parents_from(domains, apex, min_child_depth=min_child_depth) | ||||
|     return _build_wildcard_records( | ||||
|         parents, | ||||
|         apex, | ||||
|         ip4=ip4, | ||||
|         ip6=ip6, | ||||
|         ipv6_enabled=ipv6_enabled, | ||||
|         proxied=proxied, | ||||
|         wildcard_children=wildcard_children, | ||||
|         apex_wildcard=include_apex_wildcard, | ||||
|         ipv6_enabled=ipv6_enabled, | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class FilterModule(object): | ||||
|     def filters(self): | ||||
|         return { | ||||
|             "parent_build_records": parent_build_records, | ||||
|             "wildcard_records": wildcard_records, | ||||
|         } | ||||
| @@ -1,6 +1,6 @@ | ||||
| galaxy_info: | ||||
|   author: "Kevin Veen-Birkenbach" | ||||
|   description: "Create Cloudflare DNS records only for parent hosts (and apex)." | ||||
|   description: "Create Cloudflare wildcard DNS records (*.parent) for parent hosts; no base or *.apex records." | ||||
|   license: "Infinito.Nexus NonCommercial License" | ||||
|   min_ansible_version: "2.12" | ||||
|   galaxy_tags: [dns, cloudflare, automation] | ||||
| @@ -3,7 +3,7 @@ | ||||
|   include_role: | ||||
|     name: sys-dns-cloudflare-records | ||||
|   vars: | ||||
|     cloudflare_records: "{{ SYN_DNS_PARENT_HOSTS_RECORDS }}" | ||||
|     cloudflare_records: "{{ SYN_DNS_WILDCARD_RECORDS }}" | ||||
|   when: DNS_PROVIDER == 'cloudflare' | ||||
| 
 | ||||
| - include_tasks: utils/run_once.yml | ||||
							
								
								
									
										3
									
								
								roles/sys-dns-wildcards/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								roles/sys-dns-wildcards/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| - block: | ||||
|   - include_tasks: 01_core.yml | ||||
|   when: run_once_sys_dns_wildcards is not defined | ||||
| @@ -1,12 +1,12 @@ | ||||
| SYN_DNS_PARENT_HOSTS_RECORDS: >- | ||||
| SYN_DNS_WILDCARD_RECORDS: >- | ||||
|   {{ CURRENT_PLAY_DOMAINS_ALL | ||||
|       | parent_build_records( | ||||
|       | wildcard_records( | ||||
|           PRIMARY_DOMAIN, | ||||
|           defaults_networks.internet.ip4, | ||||
|           (defaults_networks.internet.ip6 | default('')), | ||||
|           parent_dns_proxied, | ||||
|           (parent_dns_domains | default([])), | ||||
|           True, | ||||
|           2 | ||||
|           2, | ||||
|           parent_dns_ipv6_enabled | ||||
|         ) | ||||
|   }} | ||||
|   }} | ||||
| @@ -3,7 +3,7 @@ | ||||
| Bootstrap and maintain **DNS prerequisites** for your web stack on Cloudflare. | ||||
|  | ||||
| This role validates credentials and (by default) ensures: | ||||
| - **Parent host A/AAAA records** (incl. the **apex** SLD.TLD) via `sys-dns-parent-hosts` | ||||
| - **Wildcard A/AAAA records** (`*.parent`) for parent hosts via `sys-dns-wildcards` (no base/apex records) | ||||
| - *(Optional)* **CAA** records for Let’s Encrypt (kept as a commented block you can re-enable) | ||||
|  | ||||
| Runs **once per play** and is safe to include in stacks that roll out many domains. | ||||
| @@ -13,9 +13,9 @@ Runs **once per play** and is safe to include in stacks that roll out many domai | ||||
| ## What it does | ||||
|  | ||||
| 1. **Validate `CLOUDFLARE_API_TOKEN`** is present (early fail if missing). | ||||
| 2. **Ensure parent DNS exists** (apex + “parent” FQDNs derived from children): | ||||
|    - Delegates to [`sys-dns-parent-hosts`](../sys-dns-parent-hosts/README.md) | ||||
|    - Creates A (and AAAA if enabled upstream) on the Cloudflare zone, optionally proxied. | ||||
| 2. **Ensure wildcard parent DNS exists** (`*.parent` derived from children): | ||||
|    - Delegates to [`sys-dns-wildcards`](../sys-dns-wildcards/README.md) | ||||
|    - Creates `A` (and `AAAA` if enabled) wildcard records on the Cloudflare zone, optionally proxied.  | ||||
| 3. *(Optional)* **CAA records** for all base SLDs (commented in the tasks; enable if you want CAA managed here). | ||||
|  | ||||
| > Parent hosts example:   | ||||
|   | ||||
| @@ -22,11 +22,11 @@ | ||||
|   async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}" | ||||
|   poll:  "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}" | ||||
|  | ||||
| - name: "Ensure parent DNS (apex + parent FQDNs) exists" | ||||
| - name: "Ensure wildcard parent DNS (*.parent) exists" | ||||
|   include_role: | ||||
|     name: sys-dns-parent-hosts | ||||
|     name: sys-dns-wildcards | ||||
|   vars: | ||||
|     parent_dns_proxied: false | ||||
|   when: run_once_sys_dns_parent_hosts is not defined | ||||
|   when: run_once_sys_dns_wildcards is not defined | ||||
|  | ||||
| - include_tasks: utils/run_once.yml | ||||
		Reference in New Issue
	
	Block a user