fix(resource): enforce DAG guard via service layer
CI / helm (pull_request) Successful in 24s
CI / push-validation (pull_request) Successful in 27s
CI / build (pull_request) Successful in 33s
CI / lint (pull_request) Failing after 35s
CI / quality (pull_request) Successful in 47s
CI / typecheck (pull_request) Successful in 55s
CI / security (pull_request) Successful in 55s
CI / coverage (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 3m6s
CI / integration_tests (pull_request) Successful in 4m3s
CI / unit_tests (pull_request) Failing after 4m56s
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been cancelled
CI / benchmark-regression (pull_request) Has been cancelled

ISSUES CLOSED: #6329
This commit is contained in:
2026-04-11 02:39:35 +00:00
parent f1d1f8ee6a
commit 3f26c112af
2 changed files with 82 additions and 66 deletions
@@ -25,6 +25,8 @@ from cleveragents.application.services._resource_registry_data import (
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.resource import PhysVirt, Resource
from cleveragents.infrastructure.database.models import (
ResourceEdgeModel,
ResourceLinkModel,
ResourceModel,
ResourceTypeModel,
)
@@ -249,3 +251,78 @@ class ResourceInstanceMixin:
return db_resource_to_domain(row)
finally:
session.close()
def remove_resource(
self: RegistryHost,
name_or_id: str,
*,
force: bool = False,
) -> None:
"""Remove a resource from the registry.
Args:
name_or_id: Namespaced name or ULID of the resource to remove.
force: When ``True``, cascade removal of DAG links before
deleting the resource (used by ``--update`` workflows).
Raises:
ValidationError: If DAG links still reference the resource and
``force`` is ``False``.
NotFoundError: If the resource does not exist.
"""
resource = self.show_resource(name_or_id)
session = self._session()
try:
link_filter = (ResourceLinkModel.parent_id == resource.resource_id) | (
ResourceLinkModel.child_id == resource.resource_id
)
link_query = session.query(ResourceLinkModel).filter(link_filter)
link_count = link_query.count()
edge_filter = (ResourceEdgeModel.parent_id == resource.resource_id) | (
ResourceEdgeModel.child_id == resource.resource_id
)
edge_count = session.query(ResourceEdgeModel).filter(edge_filter).count()
remaining_edges = max(link_count, edge_count)
if remaining_edges > 0 and not force:
display_name = resource.name or resource.resource_id
raise ValidationError(
message=(
f"Cannot remove resource '{display_name}': "
f"{remaining_edges} edge(s) still reference it."
),
details={
"resource_id": resource.resource_id,
"link_count": link_count,
"edge_count": edge_count,
},
)
if link_count > 0:
link_query.delete(synchronize_session="fetch")
if edge_count > 0:
session.query(ResourceEdgeModel).filter(edge_filter).delete(
synchronize_session="fetch"
)
row = (
session.query(ResourceModel)
.filter_by(resource_id=resource.resource_id)
.first()
)
if row is not None:
session.delete(row)
session.commit()
except ValidationError:
session.rollback()
raise
except Exception:
session.rollback()
raise
finally:
session.close()
+5 -66
View File
@@ -714,34 +714,7 @@ def resource_add(
# --update: remove existing resource before re-registering
if update:
try:
existing = service.show_resource(name)
# Remove the existing resource so we can re-register
session = service._session()
try:
from cleveragents.infrastructure.database.models import (
ResourceEdgeModel,
ResourceLinkModel,
ResourceModel,
)
# Remove links and edges first to avoid FK violations
session.query(ResourceLinkModel).filter(
(ResourceLinkModel.parent_id == existing.resource_id)
| (ResourceLinkModel.child_id == existing.resource_id)
).delete(synchronize_session="fetch")
session.query(ResourceEdgeModel).filter(
(ResourceEdgeModel.parent_id == existing.resource_id)
| (ResourceEdgeModel.child_id == existing.resource_id)
).delete(synchronize_session="fetch")
session.query(ResourceModel).filter_by(
resource_id=existing.resource_id
).delete(synchronize_session="fetch")
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
service.remove_resource(name, force=True)
except NotFoundError:
# Resource doesn't exist yet — proceed with normal add
pass
@@ -1378,45 +1351,11 @@ def resource_remove(
console.print("[yellow]Aborted.[/yellow]")
raise typer.Abort()
# Delete via session
session = service._session()
try:
from cleveragents.infrastructure.database.models import (
ResourceLinkModel,
ResourceModel,
)
# Check for edges
edge_count: int = (
session.query(ResourceLinkModel)
.filter(
(ResourceLinkModel.parent_id == res.resource_id)
| (ResourceLinkModel.child_id == res.resource_id)
)
.count()
)
if edge_count > 0:
console.print(
f"[red]Cannot remove resource '{res.name or res.resource_id}': "
f"{edge_count} edge(s) still reference it.[/red]"
)
raise typer.Abort()
row = (
session.query(ResourceModel)
.filter_by(resource_id=res.resource_id)
.first()
)
if row is not None:
session.delete(row)
session.commit()
except typer.Abort:
raise
except Exception:
session.rollback()
raise
finally:
session.close()
service.remove_resource(res.resource_id)
except ValidationError as exc:
console.print(f"[red]{exc.message}[/red]")
raise typer.Abort() from exc
console.print(f"[green]Removed resource:[/green] {res.name or res.resource_id}")