generated from coulomb/repo-seed
Refine runtime entity taxonomy
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from re import sub
|
from re import sub
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -10,6 +11,10 @@ DISPLAY_STATES = ("show", "blur", "hide", "highlight", "remove")
|
|||||||
LAYER_ORDER = (
|
LAYER_ORDER = (
|
||||||
"repository",
|
"repository",
|
||||||
"server",
|
"server",
|
||||||
|
"runtime_service",
|
||||||
|
"application",
|
||||||
|
"network",
|
||||||
|
"domain",
|
||||||
"deployment",
|
"deployment",
|
||||||
"service",
|
"service",
|
||||||
"capability",
|
"capability",
|
||||||
@@ -22,6 +27,10 @@ LAYER_ORDER = (
|
|||||||
_KIND_LAYER = {
|
_KIND_LAYER = {
|
||||||
"Repository": "repository",
|
"Repository": "repository",
|
||||||
"Server": "server",
|
"Server": "server",
|
||||||
|
"RuntimeService": "runtime_service",
|
||||||
|
"ApplicationEndpoint": "application",
|
||||||
|
"NetworkPort": "network",
|
||||||
|
"DomainName": "domain",
|
||||||
"Deployment": "deployment",
|
"Deployment": "deployment",
|
||||||
"ServiceDeclaration": "service",
|
"ServiceDeclaration": "service",
|
||||||
"CapabilityDeclaration": "capability",
|
"CapabilityDeclaration": "capability",
|
||||||
@@ -34,6 +43,10 @@ _KIND_LAYER = {
|
|||||||
_LAYER_COLORS = {
|
_LAYER_COLORS = {
|
||||||
"repository": "#475569",
|
"repository": "#475569",
|
||||||
"server": "#334155",
|
"server": "#334155",
|
||||||
|
"runtime_service": "#0369a1",
|
||||||
|
"application": "#0f766e",
|
||||||
|
"network": "#64748b",
|
||||||
|
"domain": "#9333ea",
|
||||||
"deployment": "#16a34a",
|
"deployment": "#16a34a",
|
||||||
"service": "#0f766e",
|
"service": "#0f766e",
|
||||||
"capability": "#2563eb",
|
"capability": "#2563eb",
|
||||||
@@ -51,6 +64,13 @@ _EDGE_STRENGTH = {
|
|||||||
"uses_interface": "medium",
|
"uses_interface": "medium",
|
||||||
"declares": "weak",
|
"declares": "weak",
|
||||||
"deployed_as": "medium",
|
"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",
|
"runs_on": "strong",
|
||||||
"owns_deployment": "weak",
|
"owns_deployment": "weak",
|
||||||
}
|
}
|
||||||
@@ -287,12 +307,13 @@ def fabric_graph_explorer_payload(
|
|||||||
node_id = str(node.get("id", ""))
|
node_id = str(node.get("id", ""))
|
||||||
if not node_id:
|
if not node_id:
|
||||||
continue
|
continue
|
||||||
kind = str(node.get("kind", ""))
|
source_kind = str(node.get("kind", ""))
|
||||||
if kind == "Repository":
|
if source_kind == "Repository":
|
||||||
continue
|
continue
|
||||||
|
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||||
|
kind = _presentation_kind(source_kind, attributes)
|
||||||
layer = _layer_for_kind(kind)
|
layer = _layer_for_kind(kind)
|
||||||
is_unresolved = node_id in unresolved
|
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")
|
review_state = str(attributes.get("discovery_review_state") or "accepted")
|
||||||
confidence = attributes.get("discovery_confidence")
|
confidence = attributes.get("discovery_confidence")
|
||||||
elements.append(
|
elements.append(
|
||||||
@@ -318,7 +339,7 @@ def fabric_graph_explorer_payload(
|
|||||||
),
|
),
|
||||||
"visualSize": 34 if layer == "binding" else 46 if is_unresolved else 50,
|
"visualSize": 34 if layer == "binding" else 46 if is_unresolved else 50,
|
||||||
"ownership": str(attributes.get("owner") or attributes.get("discovery_origin") or "repo"),
|
"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": {
|
"discovery": {
|
||||||
"stableKey": attributes.get("discovery_stable_key", ""),
|
"stableKey": attributes.get("discovery_stable_key", ""),
|
||||||
"origin": attributes.get("discovery_origin", ""),
|
"origin": attributes.get("discovery_origin", ""),
|
||||||
@@ -342,6 +363,7 @@ def fabric_graph_explorer_payload(
|
|||||||
|
|
||||||
node_layers = _node_data_index(elements, "layer")
|
node_layers = _node_data_index(elements, "layer")
|
||||||
node_repos = _node_data_index(elements, "repo")
|
node_repos = _node_data_index(elements, "repo")
|
||||||
|
node_kinds = _node_data_index(elements, "kind")
|
||||||
edge_index = _append_infrastructure_elements(
|
edge_index = _append_infrastructure_elements(
|
||||||
source_nodes,
|
source_nodes,
|
||||||
elements,
|
elements,
|
||||||
@@ -353,7 +375,7 @@ def fabric_graph_explorer_payload(
|
|||||||
for edge in source_edges:
|
for edge in source_edges:
|
||||||
source = source_repository_node_ids.get(str(edge.get("from", "")), str(edge.get("from", "")))
|
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", "")))
|
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:
|
if not source or not target:
|
||||||
continue
|
continue
|
||||||
elements.append(_edge_element(edge_index, source, target, edge_type, node_layers, node_repos))
|
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")
|
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:
|
def _edge_strength(edge_type: str) -> str:
|
||||||
if edge_type.startswith("binds:"):
|
if edge_type.startswith("binds:"):
|
||||||
status = edge_type.split(":", 1)[1]
|
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
|
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(
|
def _append_infrastructure_elements(
|
||||||
source_nodes: list[dict[str, Any]],
|
source_nodes: list[dict[str, Any]],
|
||||||
elements: list[dict[str, Any]],
|
elements: list[dict[str, Any]],
|
||||||
@@ -457,7 +523,19 @@ def _append_infrastructure_elements(
|
|||||||
) -> int:
|
) -> int:
|
||||||
edge_index = 0
|
edge_index = 0
|
||||||
endpoints_by_service = _endpoints_by_service(source_nodes)
|
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(
|
service_nodes = sorted(
|
||||||
(node for node in source_nodes if node.get("kind") == "ServiceDeclaration"),
|
(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_layers[deployment_id] = "deployment"
|
||||||
node_repos[deployment_id] = repo
|
node_repos[deployment_id] = repo
|
||||||
|
|
||||||
elements.append(_edge_element(edge_index, service_id, deployment_id, "deployed_as", node_layers, node_repos))
|
append_edge(service_id, deployment_id, "deployed_as")
|
||||||
edge_index += 1
|
|
||||||
if repo and repo in repo_slugs:
|
if repo and repo in repo_slugs:
|
||||||
repo_id = f"repo:{repo}"
|
repo_id = f"repo:{repo}"
|
||||||
elements.append(_edge_element(edge_index, repo_id, deployment_id, "owns_deployment", node_layers, node_repos))
|
append_edge(repo_id, deployment_id, "owns_deployment")
|
||||||
edge_index += 1
|
|
||||||
|
|
||||||
for endpoint in matching_endpoints:
|
for endpoint in matching_endpoints:
|
||||||
server_id = server_ids_by_host.get(endpoint["host"])
|
host = endpoint["host"]
|
||||||
if server_id is None:
|
port = endpoint["port"]
|
||||||
server_id = _server_id(endpoint["host"])
|
protocol = endpoint["protocol"]
|
||||||
server_ids_by_host[endpoint["host"]] = server_id
|
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 = {
|
server_data = {
|
||||||
"id": server_id,
|
"id": server_id,
|
||||||
"stableKey": server_id,
|
"stableKey": server_id,
|
||||||
"kind": "Server",
|
"kind": "Server",
|
||||||
"layer": "server",
|
"layer": "server",
|
||||||
"label": endpoint["host"],
|
"label": host,
|
||||||
"name": endpoint["host"],
|
"name": host,
|
||||||
"description": f"Server inferred from endpoint {endpoint['url']}.",
|
"description": f"Server inferred from endpoint {endpoint['url']}.",
|
||||||
"repo": "",
|
"repo": "",
|
||||||
"domain": str(service.get("domain") or ""),
|
"domain": str(service.get("domain") or ""),
|
||||||
"lifecycle": "active",
|
"lifecycle": "active",
|
||||||
"environment": environment,
|
"environment": environment,
|
||||||
"serverHost": endpoint["host"],
|
"serverHost": host,
|
||||||
"reviewState": "accepted",
|
"reviewState": "accepted",
|
||||||
"freshnessState": "current",
|
"freshnessState": "current",
|
||||||
"unresolved": False,
|
"unresolved": False,
|
||||||
@@ -548,7 +630,7 @@ def _append_infrastructure_elements(
|
|||||||
"visualSize": 48,
|
"visualSize": 48,
|
||||||
"ownership": "inferred",
|
"ownership": "inferred",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"host": endpoint["host"],
|
"host": host,
|
||||||
"source_interface_id": endpoint["interface_id"],
|
"source_interface_id": endpoint["interface_id"],
|
||||||
"endpoint_url": endpoint["url"],
|
"endpoint_url": endpoint["url"],
|
||||||
},
|
},
|
||||||
@@ -561,8 +643,49 @@ def _append_infrastructure_elements(
|
|||||||
elements.append({"data": server_data, "classes": "server accepted inferred"})
|
elements.append({"data": server_data, "classes": "server accepted inferred"})
|
||||||
node_layers[server_id] = "server"
|
node_layers[server_id] = "server"
|
||||||
node_repos[server_id] = ""
|
node_repos[server_id] = ""
|
||||||
elements.append(_edge_element(edge_index, deployment_id, server_id, "runs_on", node_layers, node_repos))
|
if port_id is None:
|
||||||
edge_index += 1
|
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
|
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 {}
|
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||||
endpoint = attributes.get("endpoint") if isinstance(attributes.get("endpoint"), dict) else {}
|
endpoint = attributes.get("endpoint") if isinstance(attributes.get("endpoint"), dict) else {}
|
||||||
url = str(endpoint.get("url") or "").strip()
|
url = str(endpoint.get("url") or "").strip()
|
||||||
host = _endpoint_host(url)
|
parsed = _parse_endpoint_url(url)
|
||||||
service_id = str(attributes.get("service_id") or "")
|
service_id = str(attributes.get("service_id") or "")
|
||||||
if not service_id or not host:
|
if not service_id or not parsed:
|
||||||
continue
|
continue
|
||||||
|
host, port, protocol = parsed
|
||||||
endpoints.setdefault(service_id, []).append(
|
endpoints.setdefault(service_id, []).append(
|
||||||
{
|
{
|
||||||
"host": host,
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
"protocol": protocol,
|
||||||
"url": url,
|
"url": url,
|
||||||
"interface_id": str(node.get("id") or ""),
|
"interface_id": str(node.get("id") or ""),
|
||||||
"environments": _environments(attributes),
|
"environments": _environments(attributes),
|
||||||
@@ -607,11 +733,70 @@ def _environment_matches(deployment_environment: str, endpoint_environments: lis
|
|||||||
|
|
||||||
|
|
||||||
def _endpoint_host(url: str) -> str:
|
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:
|
if not url:
|
||||||
return ""
|
return None
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
host = parsed.netloc or parsed.path.split("/", 1)[0]
|
host = _normalize_endpoint_host(parsed.hostname or parsed.netloc or parsed.path.split("/", 1)[0])
|
||||||
return host.strip().lower()
|
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:
|
def _server_id(host: str) -> str:
|
||||||
@@ -619,6 +804,11 @@ def _server_id(host: str) -> str:
|
|||||||
return f"server:{key or 'unknown'}"
|
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(
|
def _edge_element(
|
||||||
edge_index: int,
|
edge_index: int,
|
||||||
source: str,
|
source: str,
|
||||||
@@ -744,6 +934,17 @@ def _node_description(kind: str, attributes: object) -> str:
|
|||||||
)
|
)
|
||||||
if kind == "BindingAssertion":
|
if kind == "BindingAssertion":
|
||||||
return str(attributes.get("status", ""))
|
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 ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,12 @@ PATH_SCOPED_NODE_KINDS = {
|
|||||||
}
|
}
|
||||||
EVIDENCE_AGGREGATE_EDGE_TYPES = {
|
EVIDENCE_AGGREGATE_EDGE_TYPES = {
|
||||||
"exposes_port",
|
"exposes_port",
|
||||||
|
"listens_on",
|
||||||
|
"names_endpoint",
|
||||||
"opens_port",
|
"opens_port",
|
||||||
"resolves_to",
|
"resolves_to",
|
||||||
"routes_to_port",
|
"routes_to_port",
|
||||||
|
"routes_to_service",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import ipaddress
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import tomllib
|
import tomllib
|
||||||
@@ -1118,10 +1119,11 @@ def _add_runtime_endpoint(
|
|||||||
if not host or port_number is None:
|
if not host or port_number is None:
|
||||||
return ""
|
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(
|
context.accumulator.add_node(
|
||||||
stable_key=server_key,
|
stable_key=target_key,
|
||||||
kind="Server",
|
kind=target_kind,
|
||||||
label=host,
|
label=host,
|
||||||
replacement_scope=scope,
|
replacement_scope=scope,
|
||||||
provenance=provenance,
|
provenance=provenance,
|
||||||
@@ -1129,7 +1131,7 @@ def _add_runtime_endpoint(
|
|||||||
aliases=[host],
|
aliases=[host],
|
||||||
attributes={
|
attributes={
|
||||||
"host": host,
|
"host": host,
|
||||||
"server_type": server_type,
|
"runtime_target_type": server_type,
|
||||||
**(attributes or {}),
|
**(attributes or {}),
|
||||||
},
|
},
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
@@ -1154,8 +1156,8 @@ def _add_runtime_endpoint(
|
|||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
)
|
)
|
||||||
context.accumulator.add_edge(
|
context.accumulator.add_edge(
|
||||||
edge_type="opens_port",
|
edge_type="opens_port" if target_kind == "Server" else "listens_on",
|
||||||
source_key=server_key,
|
source_key=target_key,
|
||||||
target_key=port_key,
|
target_key=port_key,
|
||||||
replacement_scope=scope,
|
replacement_scope=scope,
|
||||||
provenance=provenance,
|
provenance=provenance,
|
||||||
@@ -1174,9 +1176,31 @@ def _add_runtime_endpoint(
|
|||||||
)
|
)
|
||||||
route_domain = _normalize_domain(domain)
|
route_domain = _normalize_domain(domain)
|
||||||
if route_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):
|
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
|
return port_key
|
||||||
|
|
||||||
|
|
||||||
@@ -1225,13 +1249,14 @@ def _add_domain_route(
|
|||||||
port_key: str,
|
port_key: str,
|
||||||
server_host: str,
|
server_host: str,
|
||||||
*,
|
*,
|
||||||
|
runtime_target_key: str = "",
|
||||||
|
runtime_target_kind: str = "",
|
||||||
confidence: float,
|
confidence: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
domain_value = _normalize_domain(domain)
|
domain_value = _normalize_domain(domain)
|
||||||
if not domain_value:
|
if not domain_value:
|
||||||
return
|
return
|
||||||
domain_key = discovery_stable_key(context.repo_slug, "DomainName", domain_value)
|
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(
|
context.accumulator.add_node(
|
||||||
stable_key=domain_key,
|
stable_key=domain_key,
|
||||||
kind="DomainName",
|
kind="DomainName",
|
||||||
@@ -1253,7 +1278,23 @@ def _add_domain_route(
|
|||||||
source_anchor=anchor,
|
source_anchor=anchor,
|
||||||
confidence=confidence,
|
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(
|
context.accumulator.add_edge(
|
||||||
edge_type="resolves_to",
|
edge_type="resolves_to",
|
||||||
source_key=domain_key,
|
source_key=domain_key,
|
||||||
@@ -1509,6 +1550,28 @@ def _looks_like_domain(host: str) -> bool:
|
|||||||
return "." in value
|
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:
|
def _int_value(value: object) -> int | None:
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -42,7 +42,14 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
|
|||||||
assert manifest["profile_persistence"] == "local"
|
assert manifest["profile_persistence"] == "local"
|
||||||
assert manifest["shareable_state"]["profile_id"] is True
|
assert manifest["shareable_state"]["profile_id"] is True
|
||||||
assert set(manifest["filter"]["actions"]) >= {"show", "hide", "blur", "highlight", "remove"}
|
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"]}
|
filter_labels = {field["id"]: field["label"] for field in manifest["filter"]["fields"]}
|
||||||
assert filter_labels["layer"] == "Node Type"
|
assert filter_labels["layer"] == "Node Type"
|
||||||
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
|
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(
|
registered_only = next(
|
||||||
element for element in nodes if element["data"]["id"] == "repo:registered-only"
|
element for element in nodes if element["data"]["id"] == "repo:registered-only"
|
||||||
)
|
)
|
||||||
deployment = next(element for element in nodes if element["data"]["kind"] == "Deployment")
|
nodes_by_id = {element["data"]["id"]: element for element in nodes}
|
||||||
server = next(element for element in nodes if element["data"]["kind"] == "Server")
|
|
||||||
runs_on = next(edge for edge in edges if edge["data"]["edgeType"] == "runs_on")
|
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)
|
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")
|
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 registered_only["data"]["unresolved"] is True
|
||||||
assert deployment["data"]["layer"] == "deployment"
|
assert deployment["data"]["layer"] == "deployment"
|
||||||
assert server["data"]["layer"] == "server"
|
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"]["layoutIdealLength"] < cross_repo_edge["data"]["layoutIdealLength"]
|
||||||
assert runs_on["data"]["layoutElasticity"] > cross_repo_edge["data"]["layoutElasticity"]
|
assert runs_on["data"]["layoutElasticity"] > cross_repo_edge["data"]["layoutElasticity"]
|
||||||
assert same_repo_edge["data"]["layoutIdealLength"] < cross_repo_edge["data"]["layoutIdealLength"]
|
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"
|
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:
|
def test_cli_exports_graph_explorer_payload(capsys) -> None:
|
||||||
assert cli_main(["export", "--format", "graph-explorer"]) == 0
|
assert cli_main(["export", "--format", "graph-explorer"]) == 0
|
||||||
payload = json.loads(capsys.readouterr().out)
|
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[("ScoreWorkload", "fixture-api")]["attributes"]["container_count"] == 1
|
||||||
assert nodes_by_label[("Lockfile", "package-lock.json")]["attributes"]["path"] == "package-lock.json"
|
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[("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", "127.0.0.1")]["attributes"]["runtime_target_type"] == "compose-host"
|
||||||
assert nodes_by_label[("Server", "fixture-api.testing.svc.cluster.local")]["attributes"]["server_type"] == "kubernetes-service-dns"
|
assert (
|
||||||
assert nodes_by_label[("Server", "declared.fixture.test")]["attributes"]["server_type"] == "declared-endpoint"
|
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", "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", "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"
|
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",
|
"provides",
|
||||||
"exposes",
|
"exposes",
|
||||||
"opens_port",
|
"opens_port",
|
||||||
|
"listens_on",
|
||||||
|
"names_endpoint",
|
||||||
"exposes_port",
|
"exposes_port",
|
||||||
"routes_to_port",
|
"routes_to_port",
|
||||||
|
"routes_to_service",
|
||||||
"resolves_to",
|
"resolves_to",
|
||||||
}
|
}
|
||||||
assert {attribute["name"] for attribute in candidates["attributes"]} >= {
|
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