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:
2025-09-12 01:40:06 +02:00
parent feee3fd71f
commit 206b3eadbc
16 changed files with 289 additions and 250 deletions

View File

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

View File

@@ -1,2 +0,0 @@
parent_dns_proxied: false
parent_dns_domains: []

View File

@@ -1,3 +0,0 @@
- block:
- include_tasks: 01_core.yml
when: run_once_sys_dns_parent_hosts is not defined

View 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.

View File

@@ -0,0 +1,3 @@
parent_dns_proxied: true
parent_dns_domains: []
parent_dns_ipv6_enabled: true

View File

@@ -25,16 +25,12 @@ def _parent_of_child(domain: str, apex: str) -> str | None:
return ".".join(parts[1:]) # drop exactly the left-most label 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: Accepts CURRENT_PLAY_DOMAINS_ALL-like structures:
- str - dict values: str | list/tuple/set[str] | dict (one level deeper)
- list/tuple/set[str] Returns unique, sorted host list.
- dict -> recurse one level (values must be str or list-like[str])
""" """
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] = [] hosts: list[str] = []
def _add_any(x): def _add_any(x):
@@ -53,17 +49,18 @@ def _flatten_domains(current_play_domains: dict) -> list[str]:
for v in x.values(): for v in x.values():
_add_any(v) _add_any(v)
return 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) _add_any(v)
return sorted(set(hosts)) 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) _validate(apex)
parents = set([apex]) if include_apex else set() parents = set()
for d in domains: for d in domains:
_validate(d) _validate(d)
if not d.endswith(apex): 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) 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: def _is_global(ip: str) -> bool:
try: try:
return ipaddress.ip_address(ip).is_global return ipaddress.ip_address(ip).is_global
@@ -97,99 +79,79 @@ def _is_global(ip: str) -> bool:
return False return False
def _build_cf_records( def _build_wildcard_records(
rel_parents: list[str], parents: list[str],
apex: str, apex: str,
*, *,
ip4: str, ip4: str,
ip6: str | None, ip6: str | None,
ipv6_enabled: bool,
proxied: bool, proxied: bool,
wildcard_children: bool, ipv6_enabled: bool,
apex_wildcard: bool,
) -> list[dict]: ) -> list[dict]:
if not isinstance(rel_parents, list): if not isinstance(parents, list):
raise AnsibleFilterError("rel_parents must be list[str]") raise AnsibleFilterError("parents must be list[str]")
_validate(apex) _validate(apex)
if not ip4: if not ip4:
raise AnsibleFilterError("ip4 required") raise AnsibleFilterError("ip4 required")
records: list[dict] = [] records: list[dict] = []
def _add_one(name: str, rtype: str, content: str): def _add(name: str, rtype: str, content: str):
records.append({ records.append({
"zone": apex, "zone": apex,
"type": rtype, "type": rtype,
"name": name if name else "@", "name": name,
"content": content, "content": content,
"proxied": bool(proxied), "proxied": bool(proxied),
"ttl": 1, "ttl": 1,
}) })
for rel in sorted(set(rel_parents)): for p in sorted(set(parents)):
# base (parent) host rel = p[:-len(apex)-1] if p != apex else "" # relative part; apex shouldn't produce wildcard
_add_one(rel, "A", str(ip4)) if not rel:
if ipv6_enabled and ip6 and _is_global(str(ip6)): # Do NOT create *.apex here (explicitly excluded by requirement)
_add_one(rel, "AAAA", str(ip6)) continue
# wildcard children under the parent
if rel and wildcard_children:
wc = f"*.{rel}" wc = f"*.{rel}"
_add_one(wc, "A", str(ip4)) _add(wc, "A", str(ip4))
if ipv6_enabled and ip6 and _is_global(str(ip6)): if ipv6_enabled and ip6 and _is_global(str(ip6)):
_add_one(wc, "AAAA", str(ip6)) _add(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))
return records return records
def parent_build_records( def wildcard_records(
current_play_domains: dict, current_play_domains_all: dict,
apex: str, apex: str,
ip4: str, ip4: str,
ip6: str | None = None, ip6: str | None = None,
proxied: bool = False, proxied: bool = False,
explicit_domains: list[str] | None = None, explicit_domains: list[str] | None = None,
include_apex: bool = True,
min_child_depth: int = 2, min_child_depth: int = 2,
wildcard_children: bool = True, ipv6_enabled: bool = True,
include_apex_wildcard: bool = False,
ipv6_enabled: bool = False,
) -> list[dict]: ) -> list[dict]:
""" """
Return Cloudflare A/AAAA records for: Build only wildcard records for parents:
- each parent host ('' == apex), for each parent 'parent.apex' -> create '*.parent' A/AAAA.
- optionally '*.parent' for wildcard children, No base parent records and no '*.apex' are created.
- optionally '*.apex'.
""" """
# source domains # Source domains
if explicit_domains and len(explicit_domains) > 0: if explicit_domains and len(explicit_domains) > 0:
domains = sorted(set(explicit_domains)) domains = sorted(set(explicit_domains))
else: 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) parents = _parents_from(domains, apex, min_child_depth=min_child_depth)
rel_parents = _relative_names(parents, apex) return _build_wildcard_records(
parents,
return _build_cf_records(
rel_parents,
apex, apex,
ip4=ip4, ip4=ip4,
ip6=ip6, ip6=ip6,
ipv6_enabled=ipv6_enabled,
proxied=proxied, proxied=proxied,
wildcard_children=wildcard_children, ipv6_enabled=ipv6_enabled,
apex_wildcard=include_apex_wildcard,
) )
class FilterModule(object): class FilterModule(object):
def filters(self): def filters(self):
return { return {
"parent_build_records": parent_build_records, "wildcard_records": wildcard_records,
} }

View File

@@ -1,6 +1,6 @@
galaxy_info: galaxy_info:
author: "Kevin Veen-Birkenbach" 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" license: "Infinito.Nexus NonCommercial License"
min_ansible_version: "2.12" min_ansible_version: "2.12"
galaxy_tags: [dns, cloudflare, automation] galaxy_tags: [dns, cloudflare, automation]

View File

@@ -3,7 +3,7 @@
include_role: include_role:
name: sys-dns-cloudflare-records name: sys-dns-cloudflare-records
vars: vars:
cloudflare_records: "{{ SYN_DNS_PARENT_HOSTS_RECORDS }}" cloudflare_records: "{{ SYN_DNS_WILDCARD_RECORDS }}"
when: DNS_PROVIDER == 'cloudflare' when: DNS_PROVIDER == 'cloudflare'
- include_tasks: utils/run_once.yml - include_tasks: utils/run_once.yml

View File

@@ -0,0 +1,3 @@
- block:
- include_tasks: 01_core.yml
when: run_once_sys_dns_wildcards is not defined

View File

@@ -1,12 +1,12 @@
SYN_DNS_PARENT_HOSTS_RECORDS: >- SYN_DNS_WILDCARD_RECORDS: >-
{{ CURRENT_PLAY_DOMAINS_ALL {{ CURRENT_PLAY_DOMAINS_ALL
| parent_build_records( | wildcard_records(
PRIMARY_DOMAIN, PRIMARY_DOMAIN,
defaults_networks.internet.ip4, defaults_networks.internet.ip4,
(defaults_networks.internet.ip6 | default('')), (defaults_networks.internet.ip6 | default('')),
parent_dns_proxied, parent_dns_proxied,
(parent_dns_domains | default([])), (parent_dns_domains | default([])),
True, 2,
2 parent_dns_ipv6_enabled
) )
}} }}

View File

@@ -3,7 +3,7 @@
Bootstrap and maintain **DNS prerequisites** for your web stack on Cloudflare. Bootstrap and maintain **DNS prerequisites** for your web stack on Cloudflare.
This role validates credentials and (by default) ensures: 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 Lets Encrypt (kept as a commented block you can re-enable) - *(Optional)* **CAA** records for Lets 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. 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 ## What it does
1. **Validate `CLOUDFLARE_API_TOKEN`** is present (early fail if missing). 1. **Validate `CLOUDFLARE_API_TOKEN`** is present (early fail if missing).
2. **Ensure parent DNS exists** (apex + “parent” FQDNs derived from children): 2. **Ensure wildcard parent DNS exists** (`*.parent` derived from children):
- Delegates to [`sys-dns-parent-hosts`](../sys-dns-parent-hosts/README.md) - Delegates to [`sys-dns-wildcards`](../sys-dns-wildcards/README.md)
- Creates A (and AAAA if enabled upstream) on the Cloudflare zone, optionally proxied. - 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). 3. *(Optional)* **CAA records** for all base SLDs (commented in the tasks; enable if you want CAA managed here).
> Parent hosts example: > Parent hosts example:

View File

@@ -22,11 +22,11 @@
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}" async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL 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: include_role:
name: sys-dns-parent-hosts name: sys-dns-wildcards
vars: vars:
parent_dns_proxied: false 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 - include_tasks: utils/run_once.yml

View File

@@ -1,131 +0,0 @@
import os
import sys
import unittest
# Make the filter plugin importable
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))
FILTER_PATH = os.path.join(ROOT, "roles", "sys-dns-parent-hosts", "filter_plugins")
if FILTER_PATH not in sys.path:
sys.path.insert(0, FILTER_PATH)
from parent_dns import parent_build_records # noqa: E402
def has_record(records, rtype, name, zone):
"""True if an exact (type, name, zone) record exists."""
return any(
r.get("type") == rtype and r.get("name") == name and r.get("zone") == zone
for r in records
)
def has_name_or_wildcard(records, rtype, label, zone):
"""True if either <label> or *.<label> exists for (type, zone)."""
return has_record(records, rtype, label, zone) or has_record(
records, rtype, f"*.{label}", zone
)
class TestParentDNS(unittest.TestCase):
def test_end_to_end_with_ipv6(self):
current = {
"web-app-foo": [
"example.com",
"wiki.example.com",
"c.wiki.example.com",
"a.b.example.com",
],
"web-app-bar": ["foo.other.com"], # different apex -> ignored
}
recs = parent_build_records(
current_play_domains=current,
apex="example.com",
ip4="192.0.2.10",
ip6="2001:db8::10", # AAAA may or may not be emitted by role; treat as optional
proxied=True,
explicit_domains=None,
include_apex=True,
min_child_depth=2,
)
# Apex must resolve
self.assertTrue(has_record(recs, "A", "@", "example.com"))
# Parents may be plain or wildcard (or both)
self.assertTrue(has_name_or_wildcard(recs, "A", "wiki", "example.com"))
self.assertTrue(has_name_or_wildcard(recs, "A", "b", "example.com"))
# AAAA optional: if present, at least apex AAAA must exist
if any(r.get("type") == "AAAA" for r in recs):
self.assertTrue(has_record(recs, "AAAA", "@", "example.com"))
# Proxied flag is propagated
self.assertTrue(all(r.get("proxied") is True for r in recs if r["type"] in ("A", "AAAA")))
def test_explicit_domains_without_ipv6(self):
explicit = ["example.com", "c.wiki.example.com", "x.y.example.com"]
recs = parent_build_records(
current_play_domains={"ignore": ["foo.example.com"]},
apex="example.com",
ip4="198.51.100.5",
ip6="", # No IPv6 -> no AAAA expected
proxied=False,
explicit_domains=explicit,
include_apex=True,
min_child_depth=2,
)
# Apex must resolve
self.assertTrue(has_record(recs, "A", "@", "example.com"))
# Parents may be plain or wildcard
self.assertTrue(has_name_or_wildcard(recs, "A", "wiki", "example.com"))
self.assertTrue(has_name_or_wildcard(recs, "A", "y", "example.com"))
# No IPv6 supplied -> there should be no AAAA records
self.assertFalse(any(r.get("type") == "AAAA" for r in recs))
def test_current_play_domains_may_contain_dicts(self):
# Dict values with strings and lists inside must be accepted and flattened.
current = {
"web-app-foo": {
"prod": "wiki.example.com",
"preview": ["c.wiki.example.com"],
},
"web-app-bar": ["irrelevant.other.com"], # different apex, ignored
}
recs = parent_build_records(
current_play_domains=current,
apex="example.com",
ip4="203.0.113.7",
ip6=None,
proxied=False,
explicit_domains=None,
include_apex=True,
min_child_depth=2,
)
self.assertTrue(has_record(recs, "A", "@", "example.com"))
self.assertTrue(has_name_or_wildcard(recs, "A", "wiki", "example.com"))
def test_invalid_inputs_raise(self):
with self.assertRaises(Exception):
parent_build_records(
current_play_domains={"ok": ["example.com"]},
apex="", # invalid apex
ip4="192.0.2.1",
)
with self.assertRaises(Exception):
parent_build_records(
current_play_domains={"ok": ["example.com"]},
apex="example.com",
ip4="", # required
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,211 @@
# tests/unit/roles/sys-dns-wildcards/filter_plugins/test_wildcard_dns.py
import unittest
import importlib.util
from pathlib import Path
def _load_module():
"""
Load the wildcard_dns filter plugin from:
roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py
"""
here = Path(__file__).resolve()
# Go up to repository root (…/tests/unit/roles/… → 5 levels up)
repo_root = here.parents[5] if len(here.parents) >= 6 else here.parents[0]
path = repo_root / "roles" / "sys-dns-wildcards" / "filter_plugins" / "wildcard_dns.py"
if not path.exists():
raise FileNotFoundError(f"Could not find {path}")
spec = importlib.util.spec_from_file_location("wildcard_dns", path)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod) # type: ignore[attr-defined]
return mod
_wildcard_dns = _load_module()
def _get_filter():
"""Return the wildcard_records filter function from the plugin."""
fm = _wildcard_dns.FilterModule()
filters = fm.filters()
if "wildcard_records" not in filters:
raise AssertionError("wildcard_records filter not found")
return filters["wildcard_records"]
def _as_set(records):
"""Normalize records for order-independent comparison."""
return {
(r.get("type"), r.get("name"), r.get("content"), bool(r.get("proxied")))
for r in records
}
class TestWildcardDNS(unittest.TestCase):
def setUp(self):
self.wildcard_records = _get_filter()
def test_only_wildcards_no_apex_or_base(self):
apex = "example.com"
cpda = {
"svc-a": ["c.wiki.example.com", "a.b.example.com"],
"svc-b": {"extra": ["www.a.b.example.com"]},
"svc-c": "example.com",
}
recs = self.wildcard_records(
current_play_domains_all=cpda,
apex=apex,
ip4="203.0.113.10",
ip6="2606:4700:4700::1111",
proxied=True,
explicit_domains=None,
min_child_depth=2,
ipv6_enabled=True,
)
got = _as_set(recs)
expected = {
("A", "*.wiki", "203.0.113.10", True),
("AAAA", "*.wiki", "2606:4700:4700::1111", True),
("A", "*.b", "203.0.113.10", True),
("AAAA", "*.b", "2606:4700:4700::1111", True),
# now included because www.a.b.example.com promotes a.b.example.com as a parent
("A", "*.a.b", "203.0.113.10", True),
("AAAA", "*.a.b", "2606:4700:4700::1111", True),
}
self.assertEqual(got, expected)
def test_min_child_depth_prevents_apex_wildcard(self):
apex = "example.com"
cpda = {"svc": ["x.example.com"]} # depth = 1
recs = self.wildcard_records(
current_play_domains_all=cpda,
apex=apex,
ip4="198.51.100.42",
ip6="2606:4700:4700::1111",
proxied=False,
explicit_domains=None,
min_child_depth=2, # requires >= 2 → no parent derived
ipv6_enabled=True,
)
self.assertEqual(recs, [])
def test_ipv6_disabled_and_private_ipv6_filtered(self):
apex = "example.com"
cpda = {"svc": ["a.b.example.com"]}
# IPv6 disabled → only A record
recs1 = self.wildcard_records(
current_play_domains_all=cpda,
apex=apex,
ip4="203.0.113.9",
ip6="2606:4700:4700::1111",
proxied=False,
explicit_domains=None,
min_child_depth=2,
ipv6_enabled=False,
)
self.assertEqual(_as_set(recs1), {("A", "*.b", "203.0.113.9", False)})
# IPv6 enabled but ULA (not global) → skip AAAA
recs2 = self.wildcard_records(
current_play_domains_all=cpda,
apex=apex,
ip4="203.0.113.9",
ip6="fd00::1",
proxied=False,
explicit_domains=None,
min_child_depth=2,
ipv6_enabled=True,
)
self.assertEqual(_as_set(recs2), {("A", "*.b", "203.0.113.9", False)})
def test_proxied_flag_true_is_set(self):
recs = self.wildcard_records(
current_play_domains_all={"svc": ["a.b.example.com"]},
apex="example.com",
ip4="203.0.113.7",
ip6=None,
proxied=True,
explicit_domains=None,
min_child_depth=2,
ipv6_enabled=True,
)
self.assertTrue(all(r.get("proxied") is True for r in recs))
self.assertEqual(_as_set(recs), {("A", "*.b", "203.0.113.7", True)})
def test_explicit_domains_override_source(self):
cpda = {"svc": ["ignore.me.example.com", "a.b.example.com"]}
explicit = ["c.wiki.example.com"]
recs = self.wildcard_records(
current_play_domains_all=cpda,
apex="example.com",
ip4="203.0.113.5",
ip6="2606:4700:4700::1111",
proxied=False,
explicit_domains=explicit,
min_child_depth=2,
ipv6_enabled=True,
)
self.assertEqual(
_as_set(recs),
{
("A", "*.wiki", "203.0.113.5", False),
("AAAA", "*.wiki", "2606:4700:4700::1111", False),
},
)
def test_nested_structures_flattened_correctly(self):
cpda = {
"svc1": {
"primary": ["c.wiki.example.com"],
"extra": {"alt": ["a.b.example.com"]},
},
"svc2": "www.a.b.example.com",
"svc3": ["x.example.net"], # wrong apex → ignored
}
recs = self.wildcard_records(
current_play_domains_all=cpda,
apex="example.com",
ip4="203.0.113.21",
ip6="2606:4700:4700::1111",
proxied=False,
explicit_domains=None,
min_child_depth=2,
ipv6_enabled=True,
)
got = _as_set(recs)
expected = {
("A", "*.wiki", "203.0.113.21", False),
("AAAA", "*.wiki", "2606:4700:4700::1111", False),
("A", "*.b", "203.0.113.21", False),
("AAAA", "*.b", "2606:4700:4700::1111", False),
# now included because www.a.b.example.com promotes a.b.example.com as a parent
("A", "*.a.b", "203.0.113.21", False),
("AAAA", "*.a.b", "2606:4700:4700::1111", False),
}
self.assertEqual(got, expected)
def test_error_on_missing_ip4(self):
with self.assertRaises(Exception):
self.wildcard_records(
current_play_domains_all={"svc": ["a.b.example.com"]},
apex="example.com",
ip4="", # must not be empty
ip6=None,
proxied=False,
explicit_domains=None,
min_child_depth=2,
ipv6_enabled=True,
)
if __name__ == "__main__":
unittest.main()