Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Obtiene información sobre el deploy:
curl http://localhost:8000/
```

#### POST /up|down|stop
#### POST /up|down|kill|stop
Controla el deploy:

```bash
Expand Down Expand Up @@ -103,6 +103,12 @@ Lista el estado de todos los contenedores:
curl http://localhost:8000/containers
```

#### GET SSE /containers/events
Información en tiempo real sobre los cambios de estado de los contenedores (SSE):
```bash
curl -N http://localhost:8000/containers/events
```

#### POST /containers/{name}/start|stop|restart
Controla un contenedor específico:

Expand All @@ -121,13 +127,6 @@ ws.onmessage = (event) => {
};
```

#### GET /ping
Health check del servicio:

```bash
curl http://localhost:8000/ping
```

## Documentación Interactiva

FastAPI genera automáticamente documentación interactiva:
Expand Down
40 changes: 39 additions & 1 deletion data/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ services:
- mongodbdata:/data/configdb
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
healthcheck:
test: ["CMD-SHELL", "mongosh --quiet --eval 'db.getMongo().getDBNames().indexOf(\"open5gs\")' || exit 1"]
interval: 5s
timeout: 3s
start_period: 5s
expose:
- "27017/udp"
- "27017/tcp"
ports:
- "27017:27017/tcp"
networks:
default:
ipv4_address: ${MONGO_IP}
Expand All @@ -29,6 +36,10 @@ services:
- "9999/tcp"
ports:
- "9999:9999/tcp"
healthcheck:
test: ["CMD", "pidof", "npm run dev"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${WEBUI_IP}
Expand All @@ -53,6 +64,10 @@ services:
- "3868/sctp"
- "5868/tcp"
- "5868/sctp"
healthcheck:
test: ["CMD", "pidof", "open5gs-hssd"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${HSS_IP}
Expand All @@ -73,6 +88,10 @@ services:
expose:
- "2123/udp"
- "8805/udp"
healthcheck:
test: ["CMD", "pidof", "open5gs-sgwcd"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${SGWC_IP}
Expand All @@ -96,6 +115,10 @@ services:
- "2152/udp"
ports:
- "${SGWU_ADVERTISE_IP}:2152:2152/udp"
healthcheck:
test: ["CMD", "pidof", "open5gs-sgwud"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${SGWU_IP}
Expand Down Expand Up @@ -132,6 +155,10 @@ services:
- "2123/udp"
- "7777/tcp"
- "9091/tcp"
healthcheck:
test: ["CMD", "pidof", "open5gs-smfd"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${SMF_IP}
Expand Down Expand Up @@ -165,6 +192,10 @@ services:
sysctls:
- net.ipv4.ip_forward=1
- net.ipv6.conf.all.disable_ipv6=0
healthcheck:
test: ["CMD", "pidof", "open5gs-upfd"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${UPF_IP}
Expand All @@ -189,7 +220,6 @@ services:
- SGWC_IP=${SGWC_IP}
- SMF_IP=${SMF_IP}
- HSS_IP=${HSS_IP}

volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
Expand All @@ -203,6 +233,10 @@ services:
- "9091/tcp"
ports:
- "36412:36412/sctp"
healthcheck:
test: ["CMD", "pidof", "open5gs-mmed"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${MME_IP}
Expand Down Expand Up @@ -231,6 +265,10 @@ services:
ports:
- "${PCRF_BIND_PORT}:${PCRF_BIND_PORT}/sctp"
- "${PCRF_BIND_PORT}:${PCRF_BIND_PORT}/tcp"
healthcheck:
test: ["CMD", "pidof", "open5gs-pcrfd"]
interval: 5s
timeout: 3s
networks:
default:
ipv4_address: ${PCRF_IP}
Expand Down
1 change: 0 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
app = create_app(
compose_file=COMPOSE_FILE,
env_file=ENV_FILE,
include_routers=True,
)

if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "base-deployment-controller"
version = "0.1.0"
version = "0.2.0"
description = "REST API to control the basic operations of a deployment"
readme = "README.md"
requires-python = ">=3.8"
Expand Down
51 changes: 40 additions & 11 deletions src/base_deployment_controller/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
"""Base Deployment Controller package entry point."""
import asyncio
from contextlib import asynccontextmanager

from fastapi import FastAPI

from .services.config import ConfigService
from .services.task_manager import TaskManager
from .routers.api import APIRoutes
from .routers.environment import EnvRoutes
from .routers.container import ContainerRoutes
from .services.status_event_manager import StatusEventManager
from .routers.deployment import DeploymentRoutes
from .builder import AppBuilder


def create_app(
compose_file: str = "compose.yaml",
env_file: str = ".env",
include_routers: bool = True,
title: str = "Base Deployment Controller",
description: str = "REST API to control the basic operations of a deployment",
version: str = "1.0.0",
Expand All @@ -22,35 +27,59 @@ def create_app(
Args:
compose_file: Path to compose.yaml file.
env_file: Path to .env file.
include_routers: If True, registers base routers (envs, containers, deployment).
title: FastAPI application title.
description: FastAPI application description.
version: Application version string.

Returns:
FastAPI app ready to use or extend.
"""
config_service = ConfigService(compose_file, env_file)
task_manager = TaskManager(ttl=3600) # 1 hour TTL for completed tasks

@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: start cleanup task
cleanup_task = asyncio.create_task(_cleanup_loop(task_manager))
yield
# Shutdown: cancel cleanup task
cleanup_task.cancel()
try:
await cleanup_task
except asyncio.CancelledError:
pass

async def _cleanup_loop(task_manager: TaskManager):
"""Background loop for cleaning up old tasks."""
while True:
await asyncio.sleep(300) # Run every 5 minutes
await task_manager.cleanup_old_tasks()

app = FastAPI(
title=title,
description=description,
version=version,
lifespan=lifespan,
)

api_routes = APIRoutes()
env_routes = EnvRoutes(config_service, task_manager)
status_events = StatusEventManager(config_service)
container_routes = ContainerRoutes(config_service, task_manager, status_events)
deployment_routes = DeploymentRoutes(config_service, task_manager)

if include_routers:
config_service = ConfigService(compose_file, env_file)
env_routes = EnvRoutes(config_service)
container_routes = ContainerRoutes(config_service)
deployment_routes = DeploymentRoutes(config_service)

app.include_router(env_routes.router)
app.include_router(container_routes.router)
app.include_router(deployment_routes.router)
app.include_router(api_routes.router)
app.include_router(env_routes.router)
app.include_router(container_routes.router)
app.include_router(deployment_routes.router)

return app


__all__ = [
"ConfigService",
"TaskManager",
"APIRoutes",
"EnvRoutes",
"ContainerRoutes",
"DeploymentRoutes",
Expand Down
16 changes: 12 additions & 4 deletions src/base_deployment_controller/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from fastapi import APIRouter, FastAPI

from .services.config import ConfigService
from .services.task_manager import TaskManager
from .services.status_event_manager import StatusEventManager
from .routers.api import APIRoutes
from .routers.environment import EnvRoutes
from .routers.container import ContainerRoutes
from .routers.deployment import DeploymentRoutes
Expand Down Expand Up @@ -76,10 +79,15 @@ def build(self) -> FastAPI:
)

config_service = ConfigService(self.compose_file, self.env_file)
env_routes = EnvRoutes(config_service)
container_routes = ContainerRoutes(config_service)
deployment_routes = DeploymentRoutes(config_service)

task_manager = TaskManager(ttl=3600)
status_events = StatusEventManager(config_service)

api_routes = APIRoutes()
env_routes = EnvRoutes(config_service, task_manager)
container_routes = ContainerRoutes(config_service, task_manager, status_events)
deployment_routes = DeploymentRoutes(config_service, task_manager)

app.include_router(api_routes.router)
app.include_router(env_routes.router)
app.include_router(container_routes.router)
app.include_router(deployment_routes.router)
Expand Down
9 changes: 3 additions & 6 deletions src/base_deployment_controller/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
"""Pydantic models for the Base Deployment Controller."""

from .api import APIInfoResponse
from .environment import (
EnvVariable,
EnvVariablesResponse,
BulkEnvUpdateRequest,
EnvUpdateResponse,
)
from .container import ContainerInfo, ContainersInfoResponse, ContainerControlResponse
from .container import ContainerInfo, ContainersInfoResponse
from .deployment import (
DeploymentStatus,
DeploymentMetadata,
DeploymentInfoResponse,
DeploymentPingResponse,
DeploymentActionResponse,
)
from .compose import ComposeActionResponse

__all__ = [
"APIInfoResponse",
"EnvVariable",
"EnvVariablesResponse",
"BulkEnvUpdateRequest",
"EnvUpdateResponse",
"ContainerInfo",
"ContainersInfoResponse",
"ContainerControlResponse",
"DeploymentStatus",
"DeploymentMetadata",
"DeploymentInfoResponse",
"DeploymentPingResponse",
"DeploymentActionResponse",
"ComposeActionResponse",
]
10 changes: 10 additions & 0 deletions src/base_deployment_controller/models/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""API root endpoint models."""
from pydantic import BaseModel, Field


class APIInfoResponse(BaseModel):
"""Response with general API information."""

name: str = Field(..., description="API name")
status: str = Field(..., description="API status")
message: str = Field(..., description="Status message")
9 changes: 0 additions & 9 deletions src/base_deployment_controller/models/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,3 @@ class ContainersInfoResponse(BaseModel):
"""Response with list of containers and their current status."""

containers: list[ContainerInfo] = Field(..., description="List of containers")


class ContainerControlResponse(BaseModel):
"""Response after executing a control action on a container."""

success: bool = Field(..., description="Action success status")
container: str = Field(..., description="Container name")
action: str = Field(..., description="Action performed")
message: str = Field(..., description="Status message")
8 changes: 0 additions & 8 deletions src/base_deployment_controller/models/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,3 @@ class DeploymentPingResponse(BaseModel):

success: bool = Field(..., description="Ping success status")
message: str = Field(..., description="Ping message")


class DeploymentActionResponse(BaseModel):
"""Response after performing a deployment action."""

success: bool = Field(..., description="Action success status")
action: str = Field(..., description="Action performed")
message: str = Field(..., description="Status message")
34 changes: 34 additions & 0 deletions src/base_deployment_controller/models/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Event models for container status streaming via SSE.
"""
from datetime import datetime
from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field


class ServiceState(str, Enum):
"""State of a service/container for status events."""

NOT_STARTED = "not_started"
PULLING = "pulling"
PULLED = "pulled"
CREATING = "creating"
STARTING = "starting"
STARTED = "started"
STOPPING = "stopping"
STOPPED = "stopped"
REMOVING = "removing"
REMOVED = "removed"
ERROR = "error"


class ContainerStatusEvent(BaseModel):
"""Container status change event for SSE streaming."""

container_name: str = Field(..., description="Docker container name")
state: ServiceState = Field(..., description="New state")
prev_state: Optional[ServiceState] = Field(None, description="Previous state if known")
action: str = Field(..., description="Docker event action that triggered the state change")
timestamp: datetime = Field(..., description="Event timestamp")
Loading
Loading