Remove Node.js cache files containing npm vulnerability data for vitest and vite packages
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled

This commit is contained in:
2025-12-15 20:36:06 -05:00
parent 27eed55c9b
commit 68a9b0f390
123 changed files with 13216 additions and 157 deletions
+165
View File
@@ -0,0 +1,165 @@
"""CRUD operations for Docker alerts."""
from datetime import datetime
from typing import List, Optional
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.docker_alert import DockerAlert
class DockerAlertRepository:
"""Repository for Docker alert CRUD operations."""
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, alert_id: int) -> Optional[DockerAlert]:
"""Get an alert by its ID."""
result = await self.session.execute(
select(DockerAlert).where(DockerAlert.id == alert_id)
)
return result.scalar_one_or_none()
async def get_open_alert(self, host_id: str, container_name: str) -> Optional[DockerAlert]:
"""Get an open alert for a specific container."""
result = await self.session.execute(
select(DockerAlert).where(
DockerAlert.host_id == host_id,
DockerAlert.container_name == container_name,
DockerAlert.state == "open"
)
)
return result.scalar_one_or_none()
async def list_alerts(
self,
host_id: Optional[str] = None,
state: Optional[str] = None,
severity: Optional[str] = None,
limit: int = 100,
offset: int = 0
) -> List[DockerAlert]:
"""List alerts with optional filters."""
query = select(DockerAlert)
if host_id:
query = query.where(DockerAlert.host_id == host_id)
if state:
query = query.where(DockerAlert.state == state)
if severity:
query = query.where(DockerAlert.severity == severity)
query = query.order_by(DockerAlert.opened_at.desc()).limit(limit).offset(offset)
result = await self.session.execute(query)
return list(result.scalars().all())
async def count_alerts(
self,
host_id: Optional[str] = None,
state: Optional[str] = None
) -> dict:
"""Count alerts by state."""
base_query = select(DockerAlert)
if host_id:
base_query = base_query.where(DockerAlert.host_id == host_id)
# Count open
open_query = select(func.count(DockerAlert.id)).where(DockerAlert.state == "open")
if host_id:
open_query = open_query.where(DockerAlert.host_id == host_id)
# Count acknowledged
ack_query = select(func.count(DockerAlert.id)).where(DockerAlert.state == "acknowledged")
if host_id:
ack_query = ack_query.where(DockerAlert.host_id == host_id)
# Count total
total_query = select(func.count(DockerAlert.id))
if host_id:
total_query = total_query.where(DockerAlert.host_id == host_id)
open_result = await self.session.execute(open_query)
ack_result = await self.session.execute(ack_query)
total_result = await self.session.execute(total_query)
return {
"total": total_result.scalar() or 0,
"open": open_result.scalar() or 0,
"acknowledged": ack_result.scalar() or 0
}
async def count_open_by_host(self, host_id: str) -> int:
"""Count open alerts for a host."""
result = await self.session.execute(
select(func.count(DockerAlert.id)).where(
DockerAlert.host_id == host_id,
DockerAlert.state == "open"
)
)
return result.scalar() or 0
async def create(
self,
host_id: str,
container_name: str,
severity: str = "warning",
message: Optional[str] = None
) -> DockerAlert:
"""Create a new alert."""
alert = DockerAlert(
host_id=host_id,
container_name=container_name,
severity=severity,
state="open",
message=message,
opened_at=datetime.utcnow()
)
self.session.add(alert)
return alert
async def acknowledge(
self,
alert_id: int,
acknowledged_by: str
) -> Optional[DockerAlert]:
"""Acknowledge an alert."""
alert = await self.get(alert_id)
if alert and alert.state == "open":
alert.state = "acknowledged"
alert.acknowledged_at = datetime.utcnow()
alert.acknowledged_by = acknowledged_by
return alert
return None
async def close(self, alert_id: int) -> Optional[DockerAlert]:
"""Close an alert."""
alert = await self.get(alert_id)
if alert and alert.state in ("open", "acknowledged"):
alert.state = "closed"
alert.closed_at = datetime.utcnow()
return alert
return None
async def close_for_container(self, host_id: str, container_name: str) -> int:
"""Close all open alerts for a container."""
result = await self.session.execute(
select(DockerAlert).where(
DockerAlert.host_id == host_id,
DockerAlert.container_name == container_name,
DockerAlert.state.in_(["open", "acknowledged"])
)
)
alerts = result.scalars().all()
count = 0
for alert in alerts:
alert.state = "closed"
alert.closed_at = datetime.utcnow()
count += 1
return count
async def update_last_notified(self, alert_id: int) -> None:
"""Update the last notified timestamp."""
alert = await self.get(alert_id)
if alert:
alert.last_notified_at = datetime.utcnow()
+140
View File
@@ -0,0 +1,140 @@
"""CRUD operations for Docker containers."""
from datetime import datetime
from typing import List, Optional
from sqlalchemy import case, delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.docker_container import DockerContainer
class DockerContainerRepository:
"""Repository for Docker container CRUD operations."""
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, container_db_id: int) -> Optional[DockerContainer]:
"""Get a container by its database ID."""
result = await self.session.execute(
select(DockerContainer).where(DockerContainer.id == container_db_id)
)
return result.scalar_one_or_none()
async def get_by_container_id(self, host_id: str, container_id: str) -> Optional[DockerContainer]:
"""Get a container by host ID and Docker container ID."""
result = await self.session.execute(
select(DockerContainer).where(
DockerContainer.host_id == host_id,
DockerContainer.container_id == container_id
).limit(1)
)
return result.scalars().first()
async def get_by_name(self, host_id: str, name: str) -> Optional[DockerContainer]:
"""Get a container by host ID and container name."""
result = await self.session.execute(
select(DockerContainer).where(
DockerContainer.host_id == host_id,
DockerContainer.name == name
).limit(1)
)
return result.scalars().first()
async def list_by_host(
self,
host_id: str,
state: Optional[str] = None,
compose_project: Optional[str] = None
) -> List[DockerContainer]:
"""List all containers for a host with optional filters."""
query = select(DockerContainer).where(DockerContainer.host_id == host_id)
if state:
query = query.where(DockerContainer.state == state)
if compose_project:
query = query.where(DockerContainer.compose_project == compose_project)
query = query.order_by(DockerContainer.name)
result = await self.session.execute(query)
return list(result.scalars().all())
async def count_by_host(self, host_id: str) -> dict:
"""Count containers by state for a host."""
result = await self.session.execute(
select(
func.count(DockerContainer.id).label('total'),
func.sum(case((DockerContainer.state == 'running', 1), else_=0)).label('running')
).where(DockerContainer.host_id == host_id)
)
row = result.one()
return {
"total": row.total or 0,
"running": row.running or 0
}
async def upsert(
self,
host_id: str,
container_id: str,
name: str,
image: Optional[str] = None,
state: str = "unknown",
status: Optional[str] = None,
health: Optional[str] = None,
created_at: Optional[datetime] = None,
ports: Optional[dict] = None,
labels: Optional[dict] = None,
compose_project: Optional[str] = None
) -> DockerContainer:
"""Create or update a container."""
existing = await self.get_by_container_id(host_id, container_id)
if existing:
existing.name = name
existing.image = image
existing.state = state
existing.status = status
existing.health = health
existing.created_at = created_at
existing.ports = ports
existing.labels = labels
existing.compose_project = compose_project
existing.last_update_at = datetime.utcnow()
return existing
container = DockerContainer(
host_id=host_id,
container_id=container_id,
name=name,
image=image,
state=state,
status=status,
health=health,
created_at=created_at,
ports=ports,
labels=labels,
compose_project=compose_project
)
self.session.add(container)
return container
async def delete_by_host(self, host_id: str) -> int:
"""Delete all containers for a host."""
result = await self.session.execute(
delete(DockerContainer).where(DockerContainer.host_id == host_id)
)
return result.rowcount
async def delete_stale(self, host_id: str, current_container_ids: List[str]) -> int:
"""Delete containers that no longer exist on the host."""
if not current_container_ids:
return await self.delete_by_host(host_id)
result = await self.session.execute(
delete(DockerContainer).where(
DockerContainer.host_id == host_id,
DockerContainer.container_id.notin_(current_container_ids)
)
)
return result.rowcount
+103
View File
@@ -0,0 +1,103 @@
"""CRUD operations for Docker images."""
from datetime import datetime
from typing import List, Optional
from sqlalchemy import select, delete, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.docker_image import DockerImage
class DockerImageRepository:
"""Repository for Docker image CRUD operations."""
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, image_db_id: int) -> Optional[DockerImage]:
"""Get an image by its database ID."""
result = await self.session.execute(
select(DockerImage).where(DockerImage.id == image_db_id)
)
return result.scalar_one_or_none()
async def get_by_image_id(self, host_id: str, image_id: str) -> Optional[DockerImage]:
"""Get an image by host ID and Docker image ID."""
result = await self.session.execute(
select(DockerImage).where(
DockerImage.host_id == host_id,
DockerImage.image_id == image_id
).limit(1)
)
return result.scalars().first()
async def list_by_host(self, host_id: str) -> List[DockerImage]:
"""List all images for a host."""
result = await self.session.execute(
select(DockerImage)
.where(DockerImage.host_id == host_id)
.order_by(DockerImage.last_update_at.desc())
)
return list(result.scalars().all())
async def count_by_host(self, host_id: str) -> dict:
"""Count images and total size for a host."""
result = await self.session.execute(
select(
func.count(DockerImage.id).label('total'),
func.coalesce(func.sum(DockerImage.size), 0).label('total_size')
).where(DockerImage.host_id == host_id)
)
row = result.one()
return {
"total": row.total or 0,
"total_size": row.total_size or 0
}
async def upsert(
self,
host_id: str,
image_id: str,
repo_tags: Optional[List[str]] = None,
size: Optional[int] = None,
created: Optional[datetime] = None
) -> DockerImage:
"""Create or update an image."""
existing = await self.get_by_image_id(host_id, image_id)
if existing:
existing.repo_tags = repo_tags
existing.size = size
existing.created = created
existing.last_update_at = datetime.utcnow()
return existing
image = DockerImage(
host_id=host_id,
image_id=image_id,
repo_tags=repo_tags,
size=size,
created=created
)
self.session.add(image)
return image
async def delete_by_host(self, host_id: str) -> int:
"""Delete all images for a host."""
result = await self.session.execute(
delete(DockerImage).where(DockerImage.host_id == host_id)
)
return result.rowcount
async def delete_stale(self, host_id: str, current_image_ids: List[str]) -> int:
"""Delete images that no longer exist on the host."""
if not current_image_ids:
return await self.delete_by_host(host_id)
result = await self.session.execute(
delete(DockerImage).where(
DockerImage.host_id == host_id,
DockerImage.image_id.notin_(current_image_ids)
)
)
return result.rowcount
+96
View File
@@ -0,0 +1,96 @@
"""CRUD operations for Docker volumes."""
from datetime import datetime
from typing import List, Optional
from sqlalchemy import select, delete, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.docker_volume import DockerVolume
class DockerVolumeRepository:
"""Repository for Docker volume CRUD operations."""
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, volume_db_id: int) -> Optional[DockerVolume]:
"""Get a volume by its database ID."""
result = await self.session.execute(
select(DockerVolume).where(DockerVolume.id == volume_db_id)
)
return result.scalar_one_or_none()
async def get_by_name(self, host_id: str, name: str) -> Optional[DockerVolume]:
"""Get a volume by host ID and volume name."""
result = await self.session.execute(
select(DockerVolume).where(
DockerVolume.host_id == host_id,
DockerVolume.name == name
).limit(1)
)
return result.scalars().first()
async def list_by_host(self, host_id: str) -> List[DockerVolume]:
"""List all volumes for a host."""
result = await self.session.execute(
select(DockerVolume)
.where(DockerVolume.host_id == host_id)
.order_by(DockerVolume.name)
)
return list(result.scalars().all())
async def count_by_host(self, host_id: str) -> int:
"""Count volumes for a host."""
result = await self.session.execute(
select(func.count(DockerVolume.id)).where(DockerVolume.host_id == host_id)
)
return result.scalar() or 0
async def upsert(
self,
host_id: str,
name: str,
driver: Optional[str] = None,
mountpoint: Optional[str] = None,
scope: Optional[str] = None
) -> DockerVolume:
"""Create or update a volume."""
existing = await self.get_by_name(host_id, name)
if existing:
existing.driver = driver
existing.mountpoint = mountpoint
existing.scope = scope
existing.last_update_at = datetime.utcnow()
return existing
volume = DockerVolume(
host_id=host_id,
name=name,
driver=driver,
mountpoint=mountpoint,
scope=scope
)
self.session.add(volume)
return volume
async def delete_by_host(self, host_id: str) -> int:
"""Delete all volumes for a host."""
result = await self.session.execute(
delete(DockerVolume).where(DockerVolume.host_id == host_id)
)
return result.rowcount
async def delete_stale(self, host_id: str, current_volume_names: List[str]) -> int:
"""Delete volumes that no longer exist on the host."""
if not current_volume_names:
return await self.delete_by_host(host_id)
result = await self.session.execute(
delete(DockerVolume).where(
DockerVolume.host_id == host_id,
DockerVolume.name.notin_(current_volume_names)
)
)
return result.rowcount
+10
View File
@@ -72,3 +72,13 @@ class HostRepository:
)
result = await self.session.execute(stmt)
return result.rowcount > 0
async def list_docker_enabled(self) -> list[Host]:
"""List all hosts with Docker monitoring enabled."""
stmt = (
select(Host)
.where(Host.deleted_at.is_(None), Host.docker_enabled == True)
.order_by(Host.name)
)
result = await self.session.execute(stmt)
return result.scalars().all()