fix(base): firewall resolver fails fast on empty/malformed sources; cover hosts: + proto default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4127f8bc6b
commit
7dae93e4e1
2 changed files with 43 additions and 4 deletions
|
|
@ -25,14 +25,24 @@ def _host_cidr(host, hostvars):
|
||||||
|
|
||||||
|
|
||||||
def _resolve_source(frm, catalog, zones, hostvars, groups):
|
def _resolve_source(frm, catalog, zones, hostvars, groups):
|
||||||
"""Resolve a symbolic `from` to a sorted list of source CIDRs."""
|
"""Resolve a symbolic `from` to a sorted, non-empty list of source CIDRs.
|
||||||
|
|
||||||
|
Resolution order: zone -> catalog service -> inventory group -> host. A name present
|
||||||
|
in more than one namespace resolves to the first match in that order. Resolving to
|
||||||
|
zero hosts (e.g. an empty group) is an error, not a silently empty rule.
|
||||||
|
"""
|
||||||
if frm in zones:
|
if frm in zones:
|
||||||
return [zones[frm]]
|
return [zones[frm]]
|
||||||
if frm in catalog:
|
if frm in catalog:
|
||||||
return sorted(_host_cidr(h, hostvars)
|
hosts = _placement_hosts(catalog[frm], groups)
|
||||||
for h in _placement_hosts(catalog[frm], groups))
|
if not hosts:
|
||||||
|
raise ValueError(f"firewall source service '{frm}' resolves to no hosts")
|
||||||
|
return sorted(_host_cidr(h, hostvars) for h in hosts)
|
||||||
if frm in groups:
|
if frm in groups:
|
||||||
return sorted(_host_cidr(h, hostvars) for h in groups[frm])
|
hosts = groups[frm]
|
||||||
|
if not hosts:
|
||||||
|
raise ValueError(f"firewall source group '{frm}' has no members")
|
||||||
|
return sorted(_host_cidr(h, hostvars) for h in hosts)
|
||||||
if frm in hostvars:
|
if frm in hostvars:
|
||||||
return [_host_cidr(frm, hostvars)]
|
return [_host_cidr(frm, hostvars)]
|
||||||
raise ValueError(f"unresolvable firewall source '{frm}'")
|
raise ValueError(f"unresolvable firewall source '{frm}'")
|
||||||
|
|
@ -43,12 +53,15 @@ def resolve_firewall_rules(catalog, zones, inventory_hostname, hostvars, groups)
|
||||||
catalog = catalog or {}
|
catalog = catalog or {}
|
||||||
zones = zones or {}
|
zones = zones or {}
|
||||||
groups = groups or {}
|
groups = groups or {}
|
||||||
|
hostvars = hostvars or {}
|
||||||
|
|
||||||
rules = []
|
rules = []
|
||||||
for _name, entry in sorted(catalog.items()):
|
for _name, entry in sorted(catalog.items()):
|
||||||
if inventory_hostname not in _placement_hosts(entry, groups):
|
if inventory_hostname not in _placement_hosts(entry, groups):
|
||||||
continue
|
continue
|
||||||
for ing in entry.get("ingress", []):
|
for ing in entry.get("ingress", []):
|
||||||
|
if "from" not in ing or "port" not in ing:
|
||||||
|
raise ValueError(f"ingress entry missing 'from' or 'port': {ing!r}")
|
||||||
rules.append({
|
rules.append({
|
||||||
"proto": ing.get("proto", "tcp"),
|
"proto": ing.get("proto", "tcp"),
|
||||||
"port": int(ing["port"]),
|
"port": int(ing["port"]),
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,29 @@ def test_missing_ansible_host_raises():
|
||||||
"ingress": [{"from": "docker02", "port": 80, "proto": "tcp"}]}}
|
"ingress": [{"from": "docker02", "port": 80, "proto": "tcp"}]}}
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
fr.resolve_firewall_rules(cat, ZONES, "docker01", {"docker01": {}, "docker02": {}}, GROUPS)
|
fr.resolve_firewall_rules(cat, ZONES, "docker01", {"docker01": {}, "docker02": {}}, GROUPS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hosts_list_placement():
|
||||||
|
cat = {"svc": {"hosts": ["docker01", "docker02"],
|
||||||
|
"ingress": [{"from": "lan", "port": 9090, "proto": "tcp"}]}}
|
||||||
|
out = fr.resolve_firewall_rules(cat, ZONES, "docker01", HOSTVARS, GROUPS)
|
||||||
|
assert out == [{"proto": "tcp", "port": 9090, "sources": ["10.30.0.0/24"]}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_proto_defaults_to_tcp():
|
||||||
|
cat = {"svc": {"host": "docker01", "ingress": [{"from": "lan", "port": 80}]}}
|
||||||
|
out = fr.resolve_firewall_rules(cat, ZONES, "docker01", HOSTVARS, GROUPS)
|
||||||
|
assert out == [{"proto": "tcp", "port": 80, "sources": ["10.30.0.0/24"]}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_group_source_raises():
|
||||||
|
cat = {"svc": {"host": "docker01",
|
||||||
|
"ingress": [{"from": "empty_grp", "port": 80, "proto": "tcp"}]}}
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
fr.resolve_firewall_rules(cat, ZONES, "docker01", HOSTVARS, {"empty_grp": []})
|
||||||
|
|
||||||
|
|
||||||
|
def test_ingress_missing_port_raises():
|
||||||
|
cat = {"svc": {"host": "docker01", "ingress": [{"from": "lan"}]}}
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
fr.resolve_firewall_rules(cat, ZONES, "docker01", HOSTVARS, GROUPS)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue