feat(marker): add project-level marker definition inheritance

Project-level marker definitions fan out to every link (existing and new).
A definition is stored once on Project._marker_definitions; when applied to
a link the marker is named "global-{def_name}" — the "global" prefix was
pre-reserved in the schema, so inherited and per-link markers can never
collide, nor will their registry keys.

Key behavior:
- POST /projects/{pid}/marker-definitions → fan out to all existing links
- PUT  /projects/{pid}/marker-definitions/{name} → sync all inherited copies
- DELETE → remove every inherited copy from every link
- New links auto-inherit all active defs (hook in UDPLink.create)
- Per-link DELETE/PUT of a "global-*" marker is rejected (409)
- Inherited markers are NOT persisted in the topology; they are re-created
  from _marker_definitions on project load
- Compute side is untouched — the marker reaches uBridge via the existing
  start_marker→update→NIO→ubridge pipeline

Files:
- controller/project.py — _marker_definitions + CRUD + fanout + topology load
- controller/link.py — Link.inherit_marker() + asdict() filter
- controller/udp_link.py — guards on stop/update + create() inheritance hook
- controller/topology.py — persist marker_definitions in project topology
- schemas/controller/links.py — MarkerDefinitionCreate schema
- api/routes/controller/projects.py — REST endpoints (marker-definitions)
This commit is contained in:
YueGuobin committed 2026-07-16 00:39:52 +08:00
1 parent 369badc6c2
commit 31991fe359
6 files changed
+291 -4

No files matched your search

+24 -2
View File
@@ -145,6 +145,9 @@ class UDPLink(Link):
await node1.delete(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", timeout=120)
raise e
self._created = True
# New links automatically inherit every active project-level marker
# definition so the user doesn't have to reconfigure.
self._project.apply_defs_to_new_link(self)
async def update(self):
"""
@@ -319,7 +322,7 @@ class UDPLink(Link):
# explicitly deletes a marker via the REST API, and a marker is torn
# down automatically only when its link is deleted.
async def start_marker(self, name, bpf, tag=None, color=None):
async def start_marker(self, name, bpf, tag=None, color=None, inherited_from=None):
"""
Attach a traffic-insight marker to this link.
@@ -334,6 +337,8 @@ class UDPLink(Link):
:param tag: optional correlation id
:param color: optional hex color for the Web UI (e.g. '#ff5722'); stored
with the link and persisted in the topology, never sent to uBridge
:param inherited_from: def name when this marker is a project-level
inheritance copy; set automatically, never exposed to REST callers
"""
if name in self._markers:
@@ -344,13 +349,16 @@ class UDPLink(Link):
raise ControllerError(f"Invalid BPF expression: {result.get('error', 'unknown error')}")
marker_side = self._choose_marker_side()
self._markers[name] = {
marker_entry = {
"bpf": bpf,
"tag": tag,
"enabled": True,
"color": color,
"capture_node_id": marker_side["node"].id,
}
if inherited_from:
marker_entry["inherited_from"] = inherited_from
self._markers[name] = marker_entry
if self._created:
await self.update()
self._project.emit_notification("link.updated", self.asdict())
@@ -370,6 +378,13 @@ class UDPLink(Link):
if name not in self._markers:
raise ControllerNotFoundError(f"Marker '{name}' not found on link {self._id}")
if self._markers[name].get("inherited_from"):
raise ControllerError(
f"Marker '{name}' is inherited from the project-level "
f"definition '{self._markers[name]['inherited_from']}'. "
"Delete or update it via the marker-definitions API instead."
)
del self._markers[name]
if self._created:
await self.update()
@@ -393,6 +408,13 @@ class UDPLink(Link):
if not marker_info:
raise ControllerNotFoundError(f"Marker '{name}' not found on link {self._id}")
if marker_info.get("inherited_from"):
raise ControllerError(
f"Marker '{name}' is inherited from the project-level "
f"definition '{marker_info['inherited_from']}'. "
"Update it via the marker-definitions API instead."
)
if bpf is not None and bpf != marker_info["bpf"]:
result = validate_bpf_syntax(bpf)
if not result.get("valid"):