generated from coulomb/repo-seed
Refine runtime entity taxonomy
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from datetime import datetime, timezone
|
||||
from re import sub
|
||||
from typing import Any
|
||||
@@ -10,6 +11,10 @@ DISPLAY_STATES = ("show", "blur", "hide", "highlight", "remove")
|
||||
LAYER_ORDER = (
|
||||
"repository",
|
||||
"server",
|
||||
"runtime_service",
|
||||
"application",
|
||||
"network",
|
||||
"domain",
|
||||
"deployment",
|
||||
"service",
|
||||
"capability",
|
||||
@@ -22,6 +27,10 @@ LAYER_ORDER = (
|
||||
_KIND_LAYER = {
|
||||
"Repository": "repository",
|
||||
"Server": "server",
|
||||
"RuntimeService": "runtime_service",
|
||||
"ApplicationEndpoint": "application",
|
||||
"NetworkPort": "network",
|
||||
"DomainName": "domain",
|
||||
"Deployment": "deployment",
|
||||
"ServiceDeclaration": "service",
|
||||
"CapabilityDeclaration": "capability",
|
||||
@@ -34,6 +43,10 @@ _KIND_LAYER = {
|
||||
_LAYER_COLORS = {
|
||||
"repository": "#475569",
|
||||
"server": "#334155",
|
||||
"runtime_service": "#0369a1",
|
||||
"application": "#0f766e",
|
||||
"network": "#64748b",
|
||||
"domain": "#9333ea",
|
||||
"deployment": "#16a34a",
|
||||
"service": "#0f766e",
|
||||
"capability": "#2563eb",
|
||||
@@ -51,6 +64,13 @@ _EDGE_STRENGTH = {
|
||||
"uses_interface": "medium",
|
||||
"declares": "weak",
|
||||
"deployed_as": "medium",
|
||||
"exposes_port": "strong",
|
||||
"listens_on": "strong",
|
||||
"names_endpoint": "medium",
|
||||
"opens_port": "strong",
|
||||
"routes_to_port": "medium",
|
||||
"routes_to_service": "strong",
|
||||
"resolves_to": "medium",
|
||||
"runs_on": "strong",
|
||||
"owns_deployment": "weak",
|
||||
}
|
||||
@@ -287,12 +307,13 @@ def fabric_graph_explorer_payload(
|
||||
node_id = str(node.get("id", ""))
|
||||
if not node_id:
|
||||
continue
|
||||
kind = str(node.get("kind", ""))
|
||||
if kind == "Repository":
|
||||
source_kind = str(node.get("kind", ""))
|
||||
if source_kind == "Repository":
|
||||
continue
|
||||
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||
kind = _presentation_kind(source_kind, attributes)
|
||||
layer = _layer_for_kind(kind)
|
||||
is_unresolved = node_id in unresolved
|
||||
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||
review_state = str(attributes.get("discovery_review_state") or "accepted")
|
||||
confidence = attributes.get("discovery_confidence")
|
||||
elements.append(
|
||||
@@ -318,7 +339,7 @@ def fabric_graph_explorer_payload(
|
||||
),
|
||||
"visualSize": 34 if layer == "binding" else 46 if is_unresolved else 50,
|
||||
"ownership": str(attributes.get("owner") or attributes.get("discovery_origin") or "repo"),
|
||||
"attributes": attributes,
|
||||
"attributes": {**attributes, "source_kind": source_kind} if source_kind != kind else attributes,
|
||||
"discovery": {
|
||||
"stableKey": attributes.get("discovery_stable_key", ""),
|
||||
"origin": attributes.get("discovery_origin", ""),
|
||||
@@ -342,6 +363,7 @@ def fabric_graph_explorer_payload(
|
||||
|
||||
node_layers = _node_data_index(elements, "layer")
|
||||
node_repos = _node_data_index(elements, "repo")
|
||||
node_kinds = _node_data_index(elements, "kind")
|
||||
edge_index = _append_infrastructure_elements(
|
||||
source_nodes,
|
||||
elements,
|
||||
@@ -353,7 +375,7 @@ def fabric_graph_explorer_payload(
|
||||
for edge in source_edges:
|
||||
source = source_repository_node_ids.get(str(edge.get("from", "")), str(edge.get("from", "")))
|
||||
target = source_repository_node_ids.get(str(edge.get("to", "")), str(edge.get("to", "")))
|
||||
edge_type = str(edge.get("type", ""))
|
||||
edge_type = _presentation_edge_type(str(edge.get("type", "")), source, target, node_kinds)
|
||||
if not source or not target:
|
||||
continue
|
||||
elements.append(_edge_element(edge_index, source, target, edge_type, node_layers, node_repos))
|
||||
@@ -422,6 +444,30 @@ def _layer_for_kind(kind: str) -> str:
|
||||
return _KIND_LAYER.get(kind, kind.lower() or "unknown")
|
||||
|
||||
|
||||
def _presentation_kind(kind: str, attributes: dict[str, Any]) -> str:
|
||||
if kind != "Server":
|
||||
return kind
|
||||
host = str(attributes.get("host") or "").strip().lower()
|
||||
runtime_type = str(attributes.get("runtime_target_type") or attributes.get("server_type") or "")
|
||||
if runtime_type == "kubernetes-service-dns" or host.endswith(".svc.cluster.local"):
|
||||
return "RuntimeService"
|
||||
if runtime_type in {"declared-endpoint", "ingress-host"} and _looks_like_domain(host):
|
||||
return "ApplicationEndpoint"
|
||||
return kind
|
||||
|
||||
|
||||
def _presentation_edge_type(edge_type: str, source: str, target: str, node_kinds: dict[str, str]) -> str:
|
||||
if edge_type == "resolves_to":
|
||||
target_kind = node_kinds.get(target, "")
|
||||
if target_kind == "ApplicationEndpoint":
|
||||
return "names_endpoint"
|
||||
if target_kind == "RuntimeService":
|
||||
return "routes_to_service"
|
||||
if edge_type == "opens_port" and node_kinds.get(source, "") in {"ApplicationEndpoint", "RuntimeService"}:
|
||||
return "listens_on"
|
||||
return edge_type
|
||||
|
||||
|
||||
def _edge_strength(edge_type: str) -> str:
|
||||
if edge_type.startswith("binds:"):
|
||||
status = edge_type.split(":", 1)[1]
|
||||
@@ -448,6 +494,26 @@ def _node_data_index(elements: list[dict[str, Any]], field: str) -> dict[str, st
|
||||
return index
|
||||
|
||||
|
||||
def _runtime_node_indexes(source_nodes: list[dict[str, Any]]) -> tuple[dict[str, str], dict[str, str]]:
|
||||
servers_by_host: dict[str, str] = {}
|
||||
ports_by_endpoint: dict[str, str] = {}
|
||||
for node in source_nodes:
|
||||
node_id = str(node.get("id") or "")
|
||||
if not node_id:
|
||||
continue
|
||||
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||
kind = _presentation_kind(str(node.get("kind") or ""), attributes)
|
||||
host = _normalize_endpoint_host(str(attributes.get("host") or ""))
|
||||
if kind == "Server" and host:
|
||||
servers_by_host.setdefault(host, node_id)
|
||||
if kind == "NetworkPort" and host:
|
||||
port = _int_value(attributes.get("port"))
|
||||
protocol = _normalize_protocol(str(attributes.get("protocol") or "tcp"))
|
||||
if port is not None:
|
||||
ports_by_endpoint.setdefault(_endpoint_key(host, port, protocol), node_id)
|
||||
return servers_by_host, ports_by_endpoint
|
||||
|
||||
|
||||
def _append_infrastructure_elements(
|
||||
source_nodes: list[dict[str, Any]],
|
||||
elements: list[dict[str, Any]],
|
||||
@@ -457,7 +523,19 @@ def _append_infrastructure_elements(
|
||||
) -> int:
|
||||
edge_index = 0
|
||||
endpoints_by_service = _endpoints_by_service(source_nodes)
|
||||
server_ids_by_host: dict[str, str] = {}
|
||||
server_ids_by_host, port_ids_by_endpoint = _runtime_node_indexes(source_nodes)
|
||||
generated_edge_keys: set[tuple[str, str, str]] = set()
|
||||
|
||||
def append_edge(source: str, target: str, edge_type: str) -> None:
|
||||
nonlocal edge_index
|
||||
if not source or not target:
|
||||
return
|
||||
key = (source, edge_type, target)
|
||||
if key in generated_edge_keys:
|
||||
return
|
||||
generated_edge_keys.add(key)
|
||||
elements.append(_edge_element(edge_index, source, target, edge_type, node_layers, node_repos))
|
||||
edge_index += 1
|
||||
|
||||
service_nodes = sorted(
|
||||
(node for node in source_nodes if node.get("kind") == "ServiceDeclaration"),
|
||||
@@ -516,31 +594,35 @@ def _append_infrastructure_elements(
|
||||
node_layers[deployment_id] = "deployment"
|
||||
node_repos[deployment_id] = repo
|
||||
|
||||
elements.append(_edge_element(edge_index, service_id, deployment_id, "deployed_as", node_layers, node_repos))
|
||||
edge_index += 1
|
||||
append_edge(service_id, deployment_id, "deployed_as")
|
||||
if repo and repo in repo_slugs:
|
||||
repo_id = f"repo:{repo}"
|
||||
elements.append(_edge_element(edge_index, repo_id, deployment_id, "owns_deployment", node_layers, node_repos))
|
||||
edge_index += 1
|
||||
append_edge(repo_id, deployment_id, "owns_deployment")
|
||||
|
||||
for endpoint in matching_endpoints:
|
||||
server_id = server_ids_by_host.get(endpoint["host"])
|
||||
if server_id is None:
|
||||
server_id = _server_id(endpoint["host"])
|
||||
server_ids_by_host[endpoint["host"]] = server_id
|
||||
host = endpoint["host"]
|
||||
port = endpoint["port"]
|
||||
protocol = endpoint["protocol"]
|
||||
server_id = server_ids_by_host.get(host)
|
||||
endpoint_key = _endpoint_key(host, port, protocol)
|
||||
port_id = port_ids_by_endpoint.get(endpoint_key)
|
||||
port_was_generated = port_id is None
|
||||
if server_id is None and _looks_like_machine_address(host):
|
||||
server_id = _server_id(host)
|
||||
server_ids_by_host[host] = server_id
|
||||
server_data = {
|
||||
"id": server_id,
|
||||
"stableKey": server_id,
|
||||
"kind": "Server",
|
||||
"layer": "server",
|
||||
"label": endpoint["host"],
|
||||
"name": endpoint["host"],
|
||||
"label": host,
|
||||
"name": host,
|
||||
"description": f"Server inferred from endpoint {endpoint['url']}.",
|
||||
"repo": "",
|
||||
"domain": str(service.get("domain") or ""),
|
||||
"lifecycle": "active",
|
||||
"environment": environment,
|
||||
"serverHost": endpoint["host"],
|
||||
"serverHost": host,
|
||||
"reviewState": "accepted",
|
||||
"freshnessState": "current",
|
||||
"unresolved": False,
|
||||
@@ -548,7 +630,7 @@ def _append_infrastructure_elements(
|
||||
"visualSize": 48,
|
||||
"ownership": "inferred",
|
||||
"attributes": {
|
||||
"host": endpoint["host"],
|
||||
"host": host,
|
||||
"source_interface_id": endpoint["interface_id"],
|
||||
"endpoint_url": endpoint["url"],
|
||||
},
|
||||
@@ -561,8 +643,49 @@ def _append_infrastructure_elements(
|
||||
elements.append({"data": server_data, "classes": "server accepted inferred"})
|
||||
node_layers[server_id] = "server"
|
||||
node_repos[server_id] = ""
|
||||
elements.append(_edge_element(edge_index, deployment_id, server_id, "runs_on", node_layers, node_repos))
|
||||
edge_index += 1
|
||||
if port_id is None:
|
||||
port_id = _port_id(host, port, protocol)
|
||||
port_ids_by_endpoint[endpoint_key] = port_id
|
||||
port_data = {
|
||||
"id": port_id,
|
||||
"stableKey": port_id,
|
||||
"kind": "NetworkPort",
|
||||
"layer": "network",
|
||||
"label": f"{host}:{port}/{protocol}",
|
||||
"name": f"{host}:{port}/{protocol}",
|
||||
"description": f"Port inferred from endpoint {endpoint['url']}.",
|
||||
"repo": "",
|
||||
"domain": str(service.get("domain") or ""),
|
||||
"lifecycle": "active",
|
||||
"environment": environment,
|
||||
"serverHost": host,
|
||||
"reviewState": "accepted",
|
||||
"freshnessState": "current",
|
||||
"unresolved": False,
|
||||
"confidence": 0.7,
|
||||
"visualSize": 42,
|
||||
"ownership": "inferred",
|
||||
"attributes": {
|
||||
"host": host,
|
||||
"port": port,
|
||||
"protocol": protocol,
|
||||
"source_interface_id": endpoint["interface_id"],
|
||||
"endpoint_url": endpoint["url"],
|
||||
},
|
||||
"displayState": "show",
|
||||
"visibilitySource": "default",
|
||||
"visibilityReason": "default",
|
||||
"sourceReferences": [{"label": "Endpoint interface", "ref": endpoint["interface_id"]}],
|
||||
"deepLinks": {"interface": f"/graph/nodes/{endpoint['interface_id']}"},
|
||||
}
|
||||
elements.append({"data": port_data, "classes": "network accepted inferred"})
|
||||
node_layers[port_id] = "network"
|
||||
node_repos[port_id] = ""
|
||||
if server_id:
|
||||
append_edge(deployment_id, server_id, "runs_on")
|
||||
if port_was_generated:
|
||||
append_edge(server_id, port_id, "opens_port")
|
||||
append_edge(deployment_id, port_id, "exposes_port")
|
||||
return edge_index
|
||||
|
||||
|
||||
@@ -574,13 +697,16 @@ def _endpoints_by_service(source_nodes: list[dict[str, Any]]) -> dict[str, list[
|
||||
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||
endpoint = attributes.get("endpoint") if isinstance(attributes.get("endpoint"), dict) else {}
|
||||
url = str(endpoint.get("url") or "").strip()
|
||||
host = _endpoint_host(url)
|
||||
parsed = _parse_endpoint_url(url)
|
||||
service_id = str(attributes.get("service_id") or "")
|
||||
if not service_id or not host:
|
||||
if not service_id or not parsed:
|
||||
continue
|
||||
host, port, protocol = parsed
|
||||
endpoints.setdefault(service_id, []).append(
|
||||
{
|
||||
"host": host,
|
||||
"port": port,
|
||||
"protocol": protocol,
|
||||
"url": url,
|
||||
"interface_id": str(node.get("id") or ""),
|
||||
"environments": _environments(attributes),
|
||||
@@ -607,11 +733,70 @@ def _environment_matches(deployment_environment: str, endpoint_environments: lis
|
||||
|
||||
|
||||
def _endpoint_host(url: str) -> str:
|
||||
parsed = _parse_endpoint_url(url)
|
||||
return parsed[0] if parsed else ""
|
||||
|
||||
|
||||
def _parse_endpoint_url(url: str) -> tuple[str, int, str] | None:
|
||||
if not url:
|
||||
return ""
|
||||
return None
|
||||
parsed = urlparse(url)
|
||||
host = parsed.netloc or parsed.path.split("/", 1)[0]
|
||||
return host.strip().lower()
|
||||
host = _normalize_endpoint_host(parsed.hostname or parsed.netloc or parsed.path.split("/", 1)[0])
|
||||
try:
|
||||
port = parsed.port
|
||||
except ValueError:
|
||||
port = None
|
||||
scheme = str(parsed.scheme or "").lower()
|
||||
port = port or {"http": 80, "https": 443, "postgres": 5432}.get(scheme)
|
||||
if not host or port is None:
|
||||
return None
|
||||
return host, port, "tcp"
|
||||
|
||||
|
||||
def _normalize_endpoint_host(host: str) -> str:
|
||||
value = str(host or "").strip().lower().strip("[]")
|
||||
if value in {"0.0.0.0", "::"}:
|
||||
return "localhost"
|
||||
return value
|
||||
|
||||
|
||||
def _endpoint_key(host: str, port: int, protocol: str) -> str:
|
||||
return f"{_normalize_endpoint_host(host)}:{port}/{_normalize_protocol(protocol)}"
|
||||
|
||||
|
||||
def _normalize_protocol(protocol: str) -> str:
|
||||
return str(protocol or "tcp").strip().lower() or "tcp"
|
||||
|
||||
|
||||
def _int_value(value: object) -> int | None:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
text = str(value or "").strip()
|
||||
if text.isdecimal():
|
||||
return int(text)
|
||||
return None
|
||||
|
||||
|
||||
def _looks_like_domain(host: str) -> bool:
|
||||
value = _normalize_endpoint_host(host)
|
||||
if not value or value == "localhost":
|
||||
return False
|
||||
if all(part.isdecimal() for part in value.split(".") if part):
|
||||
return False
|
||||
return "." in value
|
||||
|
||||
|
||||
def _looks_like_machine_address(host: str) -> bool:
|
||||
value = _normalize_endpoint_host(host)
|
||||
if value in {"localhost", "127.0.0.1"}:
|
||||
return True
|
||||
try:
|
||||
ipaddress.ip_address(value)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _server_id(host: str) -> str:
|
||||
@@ -619,6 +804,11 @@ def _server_id(host: str) -> str:
|
||||
return f"server:{key or 'unknown'}"
|
||||
|
||||
|
||||
def _port_id(host: str, port: int, protocol: str) -> str:
|
||||
key = sub(r"[^A-Za-z0-9._:+-]+", "-", _endpoint_key(host, port, protocol)).strip("-")
|
||||
return f"port:{key or 'unknown'}"
|
||||
|
||||
|
||||
def _edge_element(
|
||||
edge_index: int,
|
||||
source: str,
|
||||
@@ -744,6 +934,17 @@ def _node_description(kind: str, attributes: object) -> str:
|
||||
)
|
||||
if kind == "BindingAssertion":
|
||||
return str(attributes.get("status", ""))
|
||||
if kind == "RuntimeService":
|
||||
return str(attributes.get("runtime_target_type") or attributes.get("service_type") or "runtime service")
|
||||
if kind == "ApplicationEndpoint":
|
||||
return str(attributes.get("endpoint_url") or attributes.get("domain") or "application endpoint")
|
||||
if kind == "NetworkPort":
|
||||
host = str(attributes.get("host") or "")
|
||||
port = str(attributes.get("port") or "")
|
||||
protocol = str(attributes.get("protocol") or "")
|
||||
return " ".join(part for part in (host, port, protocol) if part)
|
||||
if kind == "DomainName":
|
||||
return str(attributes.get("domain") or "")
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@@ -38,9 +38,12 @@ PATH_SCOPED_NODE_KINDS = {
|
||||
}
|
||||
EVIDENCE_AGGREGATE_EDGE_TYPES = {
|
||||
"exposes_port",
|
||||
"listens_on",
|
||||
"names_endpoint",
|
||||
"opens_port",
|
||||
"resolves_to",
|
||||
"routes_to_port",
|
||||
"routes_to_service",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import ipaddress
|
||||
import re
|
||||
import subprocess
|
||||
import tomllib
|
||||
@@ -1118,10 +1119,11 @@ def _add_runtime_endpoint(
|
||||
if not host or port_number is None:
|
||||
return ""
|
||||
|
||||
server_key = discovery_stable_key(context.repo_slug, "Server", host)
|
||||
target_kind = _runtime_target_kind(host, server_type)
|
||||
target_key = discovery_stable_key(context.repo_slug, target_kind, host)
|
||||
context.accumulator.add_node(
|
||||
stable_key=server_key,
|
||||
kind="Server",
|
||||
stable_key=target_key,
|
||||
kind=target_kind,
|
||||
label=host,
|
||||
replacement_scope=scope,
|
||||
provenance=provenance,
|
||||
@@ -1129,7 +1131,7 @@ def _add_runtime_endpoint(
|
||||
aliases=[host],
|
||||
attributes={
|
||||
"host": host,
|
||||
"server_type": server_type,
|
||||
"runtime_target_type": server_type,
|
||||
**(attributes or {}),
|
||||
},
|
||||
confidence=confidence,
|
||||
@@ -1154,8 +1156,8 @@ def _add_runtime_endpoint(
|
||||
confidence=confidence,
|
||||
)
|
||||
context.accumulator.add_edge(
|
||||
edge_type="opens_port",
|
||||
source_key=server_key,
|
||||
edge_type="opens_port" if target_kind == "Server" else "listens_on",
|
||||
source_key=target_key,
|
||||
target_key=port_key,
|
||||
replacement_scope=scope,
|
||||
provenance=provenance,
|
||||
@@ -1174,9 +1176,31 @@ def _add_runtime_endpoint(
|
||||
)
|
||||
route_domain = _normalize_domain(domain)
|
||||
if route_domain:
|
||||
_add_domain_route(context, scope, provenance, anchor, route_domain, port_key, host, confidence=confidence)
|
||||
_add_domain_route(
|
||||
context,
|
||||
scope,
|
||||
provenance,
|
||||
anchor,
|
||||
route_domain,
|
||||
port_key,
|
||||
host,
|
||||
runtime_target_key=target_key,
|
||||
runtime_target_kind=target_kind,
|
||||
confidence=confidence,
|
||||
)
|
||||
elif _looks_like_domain(host):
|
||||
_add_domain_route(context, scope, provenance, anchor, host, port_key, host, confidence=confidence)
|
||||
_add_domain_route(
|
||||
context,
|
||||
scope,
|
||||
provenance,
|
||||
anchor,
|
||||
host,
|
||||
port_key,
|
||||
host,
|
||||
runtime_target_key=target_key,
|
||||
runtime_target_kind=target_kind,
|
||||
confidence=confidence,
|
||||
)
|
||||
return port_key
|
||||
|
||||
|
||||
@@ -1225,13 +1249,14 @@ def _add_domain_route(
|
||||
port_key: str,
|
||||
server_host: str,
|
||||
*,
|
||||
runtime_target_key: str = "",
|
||||
runtime_target_kind: str = "",
|
||||
confidence: float,
|
||||
) -> None:
|
||||
domain_value = _normalize_domain(domain)
|
||||
if not domain_value:
|
||||
return
|
||||
domain_key = discovery_stable_key(context.repo_slug, "DomainName", domain_value)
|
||||
server_key = discovery_stable_key(context.repo_slug, "Server", _normalize_host(server_host))
|
||||
context.accumulator.add_node(
|
||||
stable_key=domain_key,
|
||||
kind="DomainName",
|
||||
@@ -1253,7 +1278,23 @@ def _add_domain_route(
|
||||
source_anchor=anchor,
|
||||
confidence=confidence,
|
||||
)
|
||||
if server_host:
|
||||
if runtime_target_key:
|
||||
edge_type = {
|
||||
"ApplicationEndpoint": "names_endpoint",
|
||||
"RuntimeService": "routes_to_service",
|
||||
"Server": "resolves_to",
|
||||
}.get(runtime_target_kind, "routes_to")
|
||||
context.accumulator.add_edge(
|
||||
edge_type=edge_type,
|
||||
source_key=domain_key,
|
||||
target_key=runtime_target_key,
|
||||
replacement_scope=scope,
|
||||
provenance=provenance,
|
||||
source_anchor=anchor,
|
||||
confidence=confidence,
|
||||
)
|
||||
elif server_host:
|
||||
server_key = discovery_stable_key(context.repo_slug, "Server", _normalize_host(server_host))
|
||||
context.accumulator.add_edge(
|
||||
edge_type="resolves_to",
|
||||
source_key=domain_key,
|
||||
@@ -1509,6 +1550,28 @@ def _looks_like_domain(host: str) -> bool:
|
||||
return "." in value
|
||||
|
||||
|
||||
def _runtime_target_kind(host: str, runtime_target_type: str) -> str:
|
||||
value = _normalize_host(host)
|
||||
if _looks_like_machine_address(value):
|
||||
return "Server"
|
||||
if runtime_target_type == "kubernetes-service-dns" or value.endswith(".svc.cluster.local"):
|
||||
return "RuntimeService"
|
||||
if runtime_target_type == "ingress-host" or _looks_like_domain(value):
|
||||
return "ApplicationEndpoint"
|
||||
return "RuntimeService"
|
||||
|
||||
|
||||
def _looks_like_machine_address(host: str) -> bool:
|
||||
value = _normalize_host(host)
|
||||
if value in {"localhost", "127.0.0.1"}:
|
||||
return True
|
||||
try:
|
||||
ipaddress.ip_address(value)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _int_value(value: object) -> int | None:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
|
||||
@@ -42,7 +42,14 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
|
||||
assert manifest["profile_persistence"] == "local"
|
||||
assert manifest["shareable_state"]["profile_id"] is True
|
||||
assert set(manifest["filter"]["actions"]) >= {"show", "hide", "blur", "highlight", "remove"}
|
||||
assert {layer["id"] for layer in manifest["layers"]} >= {"server", "deployment"}
|
||||
assert {layer["id"] for layer in manifest["layers"]} >= {
|
||||
"server",
|
||||
"runtime_service",
|
||||
"application",
|
||||
"network",
|
||||
"domain",
|
||||
"deployment",
|
||||
}
|
||||
filter_labels = {field["id"]: field["label"] for field in manifest["filter"]["fields"]}
|
||||
assert filter_labels["layer"] == "Node Type"
|
||||
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
|
||||
@@ -50,9 +57,11 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
|
||||
registered_only = next(
|
||||
element for element in nodes if element["data"]["id"] == "repo:registered-only"
|
||||
)
|
||||
deployment = next(element for element in nodes if element["data"]["kind"] == "Deployment")
|
||||
server = next(element for element in nodes if element["data"]["kind"] == "Server")
|
||||
nodes_by_id = {element["data"]["id"]: element for element in nodes}
|
||||
runs_on = next(edge for edge in edges if edge["data"]["edgeType"] == "runs_on")
|
||||
deployment = nodes_by_id[runs_on["data"]["source"]]
|
||||
server = nodes_by_id[runs_on["data"]["target"]]
|
||||
network_port = next(element for element in nodes if element["data"]["kind"] == "NetworkPort")
|
||||
same_repo_edge = next(edge for edge in edges if edge["data"].get("sameRepo") is True)
|
||||
cross_repo_edge = next(edge for edge in edges if edge["data"].get("layoutAffinity") == "cross-repo")
|
||||
|
||||
@@ -60,6 +69,21 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
|
||||
assert registered_only["data"]["unresolved"] is True
|
||||
assert deployment["data"]["layer"] == "deployment"
|
||||
assert server["data"]["layer"] == "server"
|
||||
assert ":" not in server["data"]["label"]
|
||||
assert network_port["data"]["layer"] == "network"
|
||||
assert network_port["data"]["label"].endswith("/tcp")
|
||||
assert (
|
||||
len(
|
||||
[
|
||||
edge
|
||||
for edge in edges
|
||||
if edge["data"]["edgeType"] == "runs_on"
|
||||
and edge["data"]["source"] == deployment["data"]["id"]
|
||||
and edge["data"]["target"] == server["data"]["id"]
|
||||
]
|
||||
)
|
||||
== 1
|
||||
)
|
||||
assert runs_on["data"]["layoutIdealLength"] < cross_repo_edge["data"]["layoutIdealLength"]
|
||||
assert runs_on["data"]["layoutElasticity"] > cross_repo_edge["data"]["layoutElasticity"]
|
||||
assert same_repo_edge["data"]["layoutIdealLength"] < cross_repo_edge["data"]["layoutIdealLength"]
|
||||
@@ -123,6 +147,90 @@ def test_graph_explorer_collapses_discovered_repository_nodes() -> None:
|
||||
assert declares_package["data"]["target"] == "discovery:fixture-repo:library:fixture-service"
|
||||
|
||||
|
||||
def test_graph_explorer_presents_legacy_server_nodes_as_runtime_entities() -> None:
|
||||
graph = {
|
||||
"apiVersion": "railiance.fabric/v1alpha1",
|
||||
"kind": "FabricGraphExport",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "fixture.server.gitea.example.test",
|
||||
"kind": "Server",
|
||||
"name": "gitea.example.test",
|
||||
"repo": "fixture-repo",
|
||||
"domain": "testing",
|
||||
"lifecycle": "active",
|
||||
"attributes": {"host": "gitea.example.test", "server_type": "ingress-host"},
|
||||
},
|
||||
{
|
||||
"id": "fixture.server.gitea.default.svc.cluster.local",
|
||||
"kind": "Server",
|
||||
"name": "gitea.default.svc.cluster.local",
|
||||
"repo": "fixture-repo",
|
||||
"domain": "testing",
|
||||
"lifecycle": "active",
|
||||
"attributes": {"host": "gitea.default.svc.cluster.local", "server_type": "kubernetes-service-dns"},
|
||||
},
|
||||
{
|
||||
"id": "fixture.domain.gitea.example.test",
|
||||
"kind": "DomainName",
|
||||
"name": "gitea.example.test",
|
||||
"repo": "fixture-repo",
|
||||
"domain": "testing",
|
||||
"lifecycle": "active",
|
||||
"attributes": {"domain": "gitea.example.test"},
|
||||
},
|
||||
{
|
||||
"id": "fixture.port.gitea.default.svc.cluster.local-3000-tcp",
|
||||
"kind": "NetworkPort",
|
||||
"name": "gitea.default.svc.cluster.local:3000/tcp",
|
||||
"repo": "fixture-repo",
|
||||
"domain": "testing",
|
||||
"lifecycle": "active",
|
||||
"attributes": {"host": "gitea.default.svc.cluster.local", "port": 3000, "protocol": "tcp"},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"from": "fixture.domain.gitea.example.test",
|
||||
"to": "fixture.server.gitea.example.test",
|
||||
"type": "resolves_to",
|
||||
},
|
||||
{
|
||||
"from": "fixture.server.gitea.default.svc.cluster.local",
|
||||
"to": "fixture.port.gitea.default.svc.cluster.local-3000-tcp",
|
||||
"type": "opens_port",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
payload = fabric_graph_explorer_payload(graph, [{"slug": "fixture-repo", "name": "Fixture Repo"}], {"fixture-repo"})
|
||||
nodes_by_id = {
|
||||
element["data"]["id"]: element["data"]
|
||||
for element in payload["elements"]
|
||||
if "source" not in element["data"]
|
||||
}
|
||||
|
||||
assert nodes_by_id["fixture.server.gitea.example.test"]["kind"] == "ApplicationEndpoint"
|
||||
assert nodes_by_id["fixture.server.gitea.example.test"]["layer"] == "application"
|
||||
assert nodes_by_id["fixture.server.gitea.default.svc.cluster.local"]["kind"] == "RuntimeService"
|
||||
assert nodes_by_id["fixture.server.gitea.default.svc.cluster.local"]["layer"] == "runtime_service"
|
||||
edge_types = {
|
||||
(element["data"]["source"], element["data"]["target"]): element["data"]["edgeType"]
|
||||
for element in payload["elements"]
|
||||
if "source" in element["data"]
|
||||
}
|
||||
assert edge_types[("fixture.domain.gitea.example.test", "fixture.server.gitea.example.test")] == "names_endpoint"
|
||||
assert (
|
||||
edge_types[
|
||||
(
|
||||
"fixture.server.gitea.default.svc.cluster.local",
|
||||
"fixture.port.gitea.default.svc.cluster.local-3000-tcp",
|
||||
)
|
||||
]
|
||||
== "listens_on"
|
||||
)
|
||||
|
||||
|
||||
def test_cli_exports_graph_explorer_payload(capsys) -> None:
|
||||
assert cli_main(["export", "--format", "graph-explorer"]) == 0
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
|
||||
@@ -40,9 +40,15 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
|
||||
assert nodes_by_label[("ScoreWorkload", "fixture-api")]["attributes"]["container_count"] == 1
|
||||
assert nodes_by_label[("Lockfile", "package-lock.json")]["attributes"]["path"] == "package-lock.json"
|
||||
assert nodes_by_label[("ServiceConfig", "application.yaml")]["attributes"]["format"] == "yaml"
|
||||
assert nodes_by_label[("Server", "127.0.0.1")]["attributes"]["server_type"] == "compose-host"
|
||||
assert nodes_by_label[("Server", "fixture-api.testing.svc.cluster.local")]["attributes"]["server_type"] == "kubernetes-service-dns"
|
||||
assert nodes_by_label[("Server", "declared.fixture.test")]["attributes"]["server_type"] == "declared-endpoint"
|
||||
assert nodes_by_label[("Server", "127.0.0.1")]["attributes"]["runtime_target_type"] == "compose-host"
|
||||
assert (
|
||||
nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["attributes"]["runtime_target_type"]
|
||||
== "kubernetes-service-dns"
|
||||
)
|
||||
assert (
|
||||
nodes_by_label[("ApplicationEndpoint", "declared.fixture.test")]["attributes"]["runtime_target_type"]
|
||||
== "declared-endpoint"
|
||||
)
|
||||
assert nodes_by_label[("NetworkPort", "127.0.0.1:8080/tcp")]["attributes"]["target_port"] == 8080
|
||||
assert nodes_by_label[("NetworkPort", "fixture-api.testing.svc.cluster.local:8080/tcp")]["attributes"]["service_port"] == 8080
|
||||
assert nodes_by_label[("NetworkPort", "declared.fixture.test:9443/tcp")]["attributes"]["scheme"] == "https"
|
||||
@@ -63,8 +69,11 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
|
||||
"provides",
|
||||
"exposes",
|
||||
"opens_port",
|
||||
"listens_on",
|
||||
"names_endpoint",
|
||||
"exposes_port",
|
||||
"routes_to_port",
|
||||
"routes_to_service",
|
||||
"resolves_to",
|
||||
}
|
||||
assert {attribute["name"] for attribute in candidates["attributes"]} >= {
|
||||
|
||||
122
workplans/RAIL-FAB-WP-0015-runtime-entity-taxonomy.md
Normal file
122
workplans/RAIL-FAB-WP-0015-runtime-entity-taxonomy.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
id: RAIL-FAB-WP-0015
|
||||
type: workplan
|
||||
title: "Runtime Entity Taxonomy Refinement"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
planning_order: 15
|
||||
created: "2026-05-21"
|
||||
updated: "2026-05-21"
|
||||
state_hub_workstream_id: "958559cc-c640-40d6-afe3-467ee0e9e973"
|
||||
---
|
||||
|
||||
# RAIL-FAB-WP-0015 - Runtime Entity Taxonomy Refinement
|
||||
|
||||
## Goal
|
||||
|
||||
Refine runtime topology discovery and graph rendering so servers, services,
|
||||
applications, ports, and domains are distinct entities instead of variants of
|
||||
the old catch-all `Server` node.
|
||||
|
||||
## Background
|
||||
|
||||
The first runtime topology pass intentionally used conservative `Server`,
|
||||
`NetworkPort`, and `DomainName` nodes. After projecting all candidates into the
|
||||
graph, this proved too broad:
|
||||
|
||||
- `127.0.0.1:8765` is a port endpoint, not a server.
|
||||
- `gitea.coulomb.social` is an application-facing endpoint, not a machine.
|
||||
- `pink-account.coulomb.social` should be connected through the privacyIDEA
|
||||
service/application topology instead of floating as a repo-declared server.
|
||||
|
||||
## Target Model
|
||||
|
||||
- `Server` is reserved for concrete machine or host addresses such as IPs and
|
||||
localhost loopback addresses.
|
||||
- `RuntimeService` represents a running service target such as a Kubernetes
|
||||
Service DNS name or a declared service endpoint.
|
||||
- `ApplicationEndpoint` represents user-facing application endpoints exposed
|
||||
through domains, ingress hosts, or declared HTTP URLs.
|
||||
- `NetworkPort` remains the separate port/protocol binding node.
|
||||
- `DomainName` remains the DNS name and routes to the port/service/application
|
||||
it describes.
|
||||
|
||||
## Tasks
|
||||
|
||||
### T01 - Update Runtime Scanner Taxonomy
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0015-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c19f55e2-dd68-4cb9-8c21-fd9c61a3ab25"
|
||||
```
|
||||
|
||||
Emit refined runtime candidates from Compose, Kubernetes, and Fabric endpoint
|
||||
evidence without using `Server` for domain names or service DNS targets.
|
||||
|
||||
### T02 - Update Graph Explorer Runtime Projection
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0015-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b5caa75d-04bf-4eb3-9b61-f28eb85fe9d4"
|
||||
```
|
||||
|
||||
Render legacy accepted `Server` candidates as the refined presentation kind
|
||||
where their attributes make the intent clear, and stop inferring `host:port`
|
||||
values as server nodes.
|
||||
|
||||
### T03 - Preserve Port And Service Relationships
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0015-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "06b19102-d5f9-4707-af86-7fb248126374"
|
||||
```
|
||||
|
||||
Keep existing port/domain relationships while adding explicit relationships
|
||||
from runtime services and application endpoints to their ports. Deduplicate
|
||||
inferred deployment `runs_on` edges.
|
||||
|
||||
### T04 - Verify Fixtures And Live Graph Shape
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0015-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "d14e85b4-4323-4ce6-9e25-bc92293dc351"
|
||||
```
|
||||
|
||||
Update scanner and graph explorer tests, then verify the live graph no longer
|
||||
classifies application domains or `host:port` endpoints as servers.
|
||||
|
||||
Verification result:
|
||||
|
||||
- `python3 -m pytest tests/test_graph_explorer.py tests/test_scanner.py -q`
|
||||
passed with 8 tests.
|
||||
- `python3 -m pytest` passed with 35 tests.
|
||||
- Restarted the local registry on port 8765.
|
||||
- Live graph explorer export now shows:
|
||||
- `127.0.0.1:8765` as a `NetworkPort`.
|
||||
- `gitea.coulomb.social` and `pink-account.coulomb.social` as
|
||||
`ApplicationEndpoint` nodes where legacy discovery previously rendered
|
||||
them as `Server`.
|
||||
- `privacyidea.mfa.svc.cluster.local` as a `RuntimeService`.
|
||||
- 0 `Server` nodes whose label contains `host:port`.
|
||||
- 1 `runs_on` edge from `deployment:railiance-fabric.registry:dev`.
|
||||
|
||||
## Close Criteria
|
||||
|
||||
- Scanner tests distinguish `Server`, `RuntimeService`, `ApplicationEndpoint`,
|
||||
`NetworkPort`, and `DomainName`.
|
||||
- Graph explorer tests validate endpoint parsing and duplicate `runs_on`
|
||||
removal.
|
||||
- The local graph export shows application domains as applications, service DNS
|
||||
names as runtime services, and `127.0.0.1:8765` as a port.
|
||||
Reference in New Issue
Block a user