diff --git a/.github/workflows/docker-image-api.yaml b/.github/workflows/docker-image-api.yaml new file mode 100644 index 0000000..abc9c24 --- /dev/null +++ b/.github/workflows/docker-image-api.yaml @@ -0,0 +1,44 @@ +name: docker-image-api + +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - 'api/**' + +env: + IMAGE_TAG_OWNER: opensource-deadlock-tools + IMAGE_TAG_NAME: devlock + +permissions: + contents: read + packages: write + +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push Compose + uses: docker/build-push-action@v6 + with: + push: true + tags: ghcr.io/${{ env.IMAGE_TAG_OWNER }}/${{ env.IMAGE_TAG_NAME }}/api:latest + context: api diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..b8b78dc --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12 + +WORKDIR /app + +COPY requirements.txt requirements.txt + +RUN pip install --no-cache-dir uvicorn && \ + pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "devlock_api.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/devlock_api/__init__.py b/api/devlock_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/devlock_api/datarepo.py b/api/devlock_api/datarepo.py new file mode 100644 index 0000000..a6f4262 --- /dev/null +++ b/api/devlock_api/datarepo.py @@ -0,0 +1,12 @@ +import os + +import clickhouse_connect + +ch_host = os.environ["CLICKHOUSE_HOST"] +ch_port = int(os.environ["CLICKHOUSE_HTTP_PORT"]) +ch_user = os.environ["CLICKHOUSE_USER"] +ch_pass = os.environ["CLICKHOUSE_PASSWORD"] + +repo = clickhouse_connect.get_client( + host=ch_host, port=ch_port, username=ch_user, password=ch_pass +) diff --git a/api/devlock_api/limiter.py b/api/devlock_api/limiter.py new file mode 100644 index 0000000..6c6faa4 --- /dev/null +++ b/api/devlock_api/limiter.py @@ -0,0 +1,4 @@ +from slowapi import Limiter +from slowapi.util import get_remote_address + +limiter = Limiter(key_func=get_remote_address, key_style="endpoint") diff --git a/api/devlock_api/main.py b/api/devlock_api/main.py new file mode 100644 index 0000000..2fe726b --- /dev/null +++ b/api/devlock_api/main.py @@ -0,0 +1,28 @@ +from devlock_api.routes import v1 +from fastapi import FastAPI +from prometheus_fastapi_instrumentator import Instrumentator +from starlette.middleware.gzip import GZipMiddleware +from starlette.responses import RedirectResponse + +app = FastAPI() +app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5) + +Instrumentator().instrument(app).expose(app, include_in_schema=False) + +app.include_router(v1.router) + + +@app.get("/", include_in_schema=False) +def root(): + return RedirectResponse(url="/docs") + + +@app.get("/health", include_in_schema=False) +def health(): + return {"status": "ok"} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/api/devlock_api/routes/__init__.py b/api/devlock_api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/devlock_api/routes/v1.py b/api/devlock_api/routes/v1.py new file mode 100644 index 0000000..467fe7b --- /dev/null +++ b/api/devlock_api/routes/v1.py @@ -0,0 +1,32 @@ +from devlock_api.datarepo import repo +from devlock_api.limiter import limiter +from fastapi import APIRouter, Request + +router = APIRouter(prefix="/v1", tags=["V1"]) + + +@router.get("/matches") +@limiter.limit("100/minute") +async def match_list(request: Request, skip: int = 0): + query_result = repo.query( + "SELECT * FROM match_info LIMIT 20 OFFSET %(offset)s", {"offset": skip} + ) + return query_result.named_results() + + +@router.get("/matches/{match_id}/meta") +@limiter.limit("100/minute") +async def match_meta(request: Request, match_id: str): + query_result = repo.query( + "SELECT * FROM match_info WHERE match_id = %(match_id)s", {"match_id": match_id} + ) + return query_result.first_item + + +@router.get("/active-matches") +@limiter.limit("100/minute") +async def active_matches(request: Request, skip: int = 0): + query_result = repo.query( + "SELECT * FROM summary_active_matches LIMIT 500 OFFSET %(skip)s", {"skip": skip} + ) + return query_result.named_results() diff --git a/api/docker-compose.yaml b/api/docker-compose.yaml new file mode 100644 index 0000000..9dea4e4 --- /dev/null +++ b/api/docker-compose.yaml @@ -0,0 +1,29 @@ +services: + api: + image: ghcr.io/opensource-deadlock-tools/devlock/api + build: . + restart: always + env_file: ../.env + environment: + VIRTUAL_HOST: api.devlock.net + VIRTUAL_PORT: 8080 + LETSENCRYPT_HOST: api.devlock.net + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 5 + ports: + - "8080:8080" + networks: + - webserver + - clickhouse + - monitoring + +networks: + clickhouse: + external: true + webserver: + external: true + monitoring: + external: true diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..9edb647 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,7 @@ +fastapi[standard]>=0.115.1 +clickhouse-connect>=0.8.3 +prometheus-client>=0.21.0 +slowapi>=0.1.9 +prometheus_fastapi_instrumentator>=7.0.0 +starlette>=0.40.0 +uvicorn>=0.32.0 diff --git a/monitoring/prometheus.yaml b/monitoring/prometheus.yaml index 8cfa7d3..77bca4f 100644 --- a/monitoring/prometheus.yaml +++ b/monitoring/prometheus.yaml @@ -9,6 +9,9 @@ scrape_configs: - job_name: node static_configs: - targets: ['node-exporter:9100'] +- job_name: api + static_configs: + - targets: ['api:8080'] - job_name: Clickhouse static_configs: - targets: ['clickhouse:9363']