homelab_automation/app/crud/docker_container.py

186 lines
6.9 KiB
Python

"""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
async def list_all(
self,
state: Optional[str] = None,
compose_project: Optional[str] = None,
health: Optional[str] = None,
host_ids: Optional[List[str]] = None
) -> List[DockerContainer]:
"""List all containers across all hosts with optional filters."""
query = select(DockerContainer)
if state:
query = query.where(DockerContainer.state == state)
if compose_project:
query = query.where(DockerContainer.compose_project == compose_project)
if health:
query = query.where(DockerContainer.health == health)
if host_ids:
query = query.where(DockerContainer.host_id.in_(host_ids))
query = query.order_by(DockerContainer.host_id, DockerContainer.name)
result = await self.session.execute(query)
return list(result.scalars().all())
async def count_all(self) -> dict:
"""Count all containers by state across all hosts."""
result = await self.session.execute(
select(
func.count(DockerContainer.id).label('total'),
func.sum(case((DockerContainer.state == 'running', 1), else_=0)).label('running'),
func.sum(case((DockerContainer.state == 'exited', 1), else_=0)).label('stopped'),
func.sum(case((DockerContainer.state == 'paused', 1), else_=0)).label('paused'),
func.count(func.distinct(DockerContainer.host_id)).label('hosts_count'),
func.max(DockerContainer.last_update_at).label('last_update')
)
)
row = result.one()
return {
"total": row.total or 0,
"running": row.running or 0,
"stopped": row.stopped or 0,
"paused": row.paused or 0,
"hosts_count": row.hosts_count or 0,
"last_update": row.last_update
}