fix(core): audit matches split *_HOST/*_PORT env pairs
The consumption audit only matched host:port inside a single value (a URL, DATABASE_URL), missing the very common split-var shape (X_HOST=localhost + X_PORT=1883, e.g. castle-api's mqtt config). Resolve those pairs too — factored into a shared _resolve helper. A bare *_PORT with no *_HOST is ignored so a deployment's own listen port isn't mistaken for a dependency. Adds test_audit.py.
This commit is contained in:
@@ -35,12 +35,43 @@ class Suggestion:
|
||||
protocol: str # the provider's socket protocol
|
||||
|
||||
|
||||
def _resolve(
|
||||
host: str,
|
||||
port: int,
|
||||
*,
|
||||
consumer: str,
|
||||
port_provider: dict[int, tuple[str, str]],
|
||||
declared: set[str],
|
||||
proposed: set[str],
|
||||
env_var: str,
|
||||
out: list[Suggestion],
|
||||
) -> None:
|
||||
"""Resolve a (host, port) to a provider and record a suggestion if it's a new,
|
||||
undeclared, local match. Only resolve local hosts (this node's providers) or a
|
||||
host that names the provider — avoids matching a coincidental external host that
|
||||
happens to share a port number."""
|
||||
prov = port_provider.get(port)
|
||||
if not prov:
|
||||
return
|
||||
pname, proto = prov
|
||||
if host not in _LOCAL_HOSTS and host != pname:
|
||||
return
|
||||
if pname == consumer or pname in declared or pname in proposed:
|
||||
return
|
||||
proposed.add(pname)
|
||||
out.append(Suggestion(consumer, pname, env_var, f"{host}:{port}", proto))
|
||||
|
||||
|
||||
def suggest_consumption(config: CastleConfig) -> list[Suggestion]:
|
||||
"""Undeclared consumption suggestions, derived from env endpoint values.
|
||||
|
||||
For each deployment, scan its ``defaults.env`` values for ``host:port`` (or a
|
||||
URL). When the port resolves to a *local* provider's socket and the consumer
|
||||
doesn't already declare it, propose the edge. Deduped per (consumer, provider)."""
|
||||
Two shapes are recognized in a deployment's ``defaults.env``:
|
||||
(a) ``host:port`` inside a single value (a URL, a ``DATABASE_URL``); and
|
||||
(b) a split ``X_HOST`` + ``X_PORT`` pair (e.g. ``CASTLE_API_MQTT_HOST`` +
|
||||
``CASTLE_API_MQTT_PORT``). When the port resolves to a *local* provider's socket
|
||||
and the consumer doesn't already declare it, propose the edge — deduped per
|
||||
(consumer, provider). A bare ``*_PORT`` with no ``*_HOST`` is ignored: without an
|
||||
explicit host it can't be told apart from the deployment's own listen port."""
|
||||
model = build_model(config, check=False)
|
||||
# port -> (provider name, protocol); ports are unique per host, so a port match
|
||||
# against a local host is a confident resolution.
|
||||
@@ -54,21 +85,33 @@ def suggest_consumption(config: CastleConfig) -> list[Suggestion]:
|
||||
env = dict(dep.defaults.env) if (dep.defaults and dep.defaults.env) else {}
|
||||
declared = {r.ref for r in getattr(dep, "requires", [])}
|
||||
proposed: set[str] = set()
|
||||
|
||||
def resolve(host: str, port: int, env_var: str) -> None:
|
||||
_resolve(
|
||||
host,
|
||||
port,
|
||||
consumer=name,
|
||||
port_provider=port_provider,
|
||||
declared=declared,
|
||||
proposed=proposed,
|
||||
env_var=env_var,
|
||||
out=out,
|
||||
)
|
||||
|
||||
# (a) host:port inside a single value.
|
||||
for var, val in env.items():
|
||||
for m in _HOSTPORT.finditer(str(val)):
|
||||
host = m.group("host")
|
||||
port = int(m.group("port"))
|
||||
prov = port_provider.get(port)
|
||||
if not prov:
|
||||
continue
|
||||
pname, proto = prov
|
||||
# Only resolve when the value points at a local host (this node's
|
||||
# providers) or names the provider directly — avoids matching a
|
||||
# coincidental external host that happens to share a port number.
|
||||
if host not in _LOCAL_HOSTS and host != pname:
|
||||
continue
|
||||
if pname == name or pname in declared or pname in proposed:
|
||||
continue
|
||||
proposed.add(pname)
|
||||
out.append(Suggestion(name, pname, var, f"{host}:{port}", proto))
|
||||
resolve(m.group("host"), int(m.group("port")), var)
|
||||
# (b) split X_HOST + X_PORT pair.
|
||||
for var, val in env.items():
|
||||
if not var.endswith("_HOST"):
|
||||
continue
|
||||
pvar = var[:-5] + "_PORT" # X_HOST -> X_PORT
|
||||
if pvar not in env:
|
||||
continue
|
||||
try:
|
||||
port = int(str(env[pvar]).strip())
|
||||
except ValueError:
|
||||
continue
|
||||
resolve(str(val).strip(), port, f"{var}+{pvar}")
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user