Compare commits

...

6 Commits

Author SHA1 Message Date
e10b51c8e3 Attempt to automate registry images
All checks were successful
CI / Lint & Format (push) Successful in 4s
CI / Tests (Pytest + Alembic + Postgres) (push) Successful in 9m25s
CI / Docker Build (push) Successful in 10m28s
2025-09-30 13:27:35 +01:00
48c5731a8a Updated CI.yml
All checks were successful
CI / Lint & Format (push) Successful in 4s
CI / Tests (Pytest + Alembic + Postgres) (push) Successful in 9m25s
CI / Docker Build (push) Successful in 1m12s
2025-09-30 13:15:40 +01:00
5e79768394 Updated CI.yml
Some checks failed
CI / Lint & Format (push) Successful in 3s
CI / Tests (Pytest + Alembic + Postgres) (push) Successful in 9m31s
CI / Docker Build (push) Failing after 3m14s
2025-09-30 12:48:39 +01:00
ef947d9888 Migrated CI health test to new JSON. Refactored NGINX config
Some checks failed
CI / Lint & Format (push) Successful in 3s
CI / Tests (Pytest + Alembic + Postgres) (push) Failing after 4m44s
CI / Docker Build (push) Has been skipped
2025-09-30 12:39:49 +01:00
7054ba7547 Fixed NGINX Config for new z endpoints
Some checks failed
CI / Lint & Format (push) Successful in 4s
CI / Tests (Pytest + Alembic + Postgres) (push) Failing after 4m45s
CI / Docker Build (push) Has been skipped
2025-09-30 12:24:51 +01:00
3a49d82d04 Added healthcheck, Added readyz, livez endpoints
Some checks failed
CI / Lint & Format (push) Successful in 6s
CI / Docker Build (push) Has been cancelled
CI / Tests (Pytest + Alembic + Postgres) (push) Has been cancelled
2025-09-30 12:20:11 +01:00
6 changed files with 127 additions and 39 deletions

View File

@@ -11,6 +11,7 @@ permissions:
env: env:
PYTHON_VERSION: "3.11" PYTHON_VERSION: "3.11"
IMAGE_NAME: keywarden-api
# Used by tests / alembic; matches docker compose environment # Used by tests / alembic; matches docker compose environment
KEYWARDEN_POSTGRES_USER: postgres KEYWARDEN_POSTGRES_USER: postgres
KEYWARDEN_POSTGRES_PASSWORD: postgres KEYWARDEN_POSTGRES_PASSWORD: postgres
@@ -71,31 +72,69 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/requirements.txt') }} key: pip-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/requirements*.txt', 'pyproject.toml') }}
restore-keys: | restore-keys: |
pip-${{ runner.os }}-${{ env.PYTHON_VERSION }}- pip-${{ runner.os }}-${{ env.PYTHON_VERSION }}-
- name: Install dependencies - name: Install deps
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
# optional: extras/dev
if [ -f pyproject.toml ]; then pip install -e .[dev] || pip install -e . ; fi
- name: Set PYTHONPATH - name: Set PYTHONPATH
run: echo "PYTHONPATH=${GITHUB_WORKSPACE}" >> $GITHUB_ENV run: echo "PYTHONPATH=${GITHUB_WORKSPACE}" >> $GITHUB_ENV
- name: Create .env for tests (optional for app runtime) # Gitea ACT Runner needs this for some reason.
- name: Select Postgres host for runner
run: | run: |
printf "KEYWARDEN_POSTGRES_DSN=%s\nKEYWARDEN_SECRET_KEY=%s\n" \ if [ "${ACT:-}" = "true" ]; then
"${{ env.TEST_POSTGRES_DSN }}" "testsecret" > .env echo "KEYWARDEN_POSTGRES_HOST=postgres" >> "$GITHUB_ENV" # Gitea (act_runner)
else
echo "KEYWARDEN_POSTGRES_HOST=127.0.0.1" >> "$GITHUB_ENV" # GitHub Actions
fi
- name: Echo DB target
run: echo "DB → ${KEYWARDEN_POSTGRES_HOST:-unset}:${{ env.KEYWARDEN_POSTGRES_PORT }}"
# Explicit wait (removes race on act_runner)
- name: Wait for Postgres
run: |
for i in {1..60}; do
python - <<'PY'
import os, socket, sys
h=os.environ.get("KEYWARDEN_POSTGRES_HOST","127.0.0.1"); p=int(os.environ.get("KEYWARDEN_POSTGRES_PORT","5432"))
s=socket.socket(); s.settimeout(1)
try:
s.connect((h,p)); print("Postgres is up:", h,p); sys.exit(0)
except Exception as e:
print("waiting:", e); sys.exit(1)
finally:
s.close()
PY
if [ $? -eq 0 ]; then break; fi
sleep 2
done
- name: Run Alembic migrations - name: Run Alembic migrations
env: env:
KEYWARDEN_POSTGRES_DSN: ${{ env.TEST_POSTGRES_DSN }} KEYWARDEN_POSTGRES_USER: ${{ env.KEYWARDEN_POSTGRES_USER }}
KEYWARDEN_POSTGRES_PASSWORD: ${{ env.KEYWARDEN_POSTGRES_PASSWORD }}
KEYWARDEN_POSTGRES_HOST: ${{ env.KEYWARDEN_POSTGRES_HOST }}
KEYWARDEN_POSTGRES_PORT: ${{ env.KEYWARDEN_POSTGRES_PORT }}
KEYWARDEN_POSTGRES_DB: ${{ env.KEYWARDEN_POSTGRES_DB }}
KEYWARDEN_POSTGRES_SSL: ${{ env.KEYWARDEN_POSTGRES_SSL }}
run: alembic upgrade head run: alembic upgrade head
- name: Pytest - name: Pytest
env: env:
KEYWARDEN_POSTGRES_DSN: ${{ env.TEST_POSTGRES_DSN }} KEYWARDEN_POSTGRES_USER: ${{ env.KEYWARDEN_POSTGRES_USER }}
KEYWARDEN_POSTGRES_PASSWORD: ${{ env.KEYWARDEN_POSTGRES_PASSWORD }}
KEYWARDEN_POSTGRES_HOST: ${{ env.KEYWARDEN_POSTGRES_HOST }}
KEYWARDEN_POSTGRES_PORT: ${{ env.KEYWARDEN_POSTGRES_PORT }}
KEYWARDEN_POSTGRES_DB: ${{ env.KEYWARDEN_POSTGRES_DB }}
KEYWARDEN_POSTGRES_SSL: ${{ env.KEYWARDEN_POSTGRES_SSL }}
run: | run: |
pytest -q tests pytest -q tests
docker-build: docker-build:
@@ -106,15 +145,47 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# Choose Buildx cache backend: gha on GitHub, local on act_runner
- name: Select Buildx cache backend
run: |
if [ "${ACT:-}" = "true" ]; then
echo "CACHE_TO=type=local,dest=/tmp/.buildx-cache,mode=max" >> $GITHUB_ENV
echo "CACHE_FROM=type=local,src=/tmp/.buildx-cache" >> $GITHUB_ENV
else
echo "CACHE_TO=type=gha,mode=max" >> $GITHUB_ENV
echo "CACHE_FROM=type=gha" >> $GITHUB_ENV
fi
- name: Prepare local cache dir (act_runner only)
if: ${{ env.ACT == 'true' }}
run: mkdir -p /tmp/.buildx-cache
- name: Set image reference (Gitea)
run: |
echo "GT_IMAGE=${{ secrets.GITEA_REGISTRY }}/${{ secrets.GITEA_NAMESPACE }}/${{ env.IMAGE_NAME }}" >> $GITHUB_ENV
- name: Set up QEMU (optional)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build image (no push) - name: Set image reference
run: echo "GT_IMAGE=${{ secrets.REGISTRY_HOST }}/${{ secrets.REGISTRY_NAMESPACE }}/${{ env.IMAGE_NAME }}" >> $GITHUB_ENV
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_HOST }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build & push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: false push: true
tags: keywarden:ci tags: |
# speeds up builds by caching layers on GH Actions ${{ env.GT_IMAGE }}:${{ github.ref_name }}
cache-from: type=gha ${{ env.GT_IMAGE }}:sha-${{ github.sha }}
cache-to: type=gha,mode=max ${{ env.GT_IMAGE }}:latest

View File

@@ -1,7 +1,11 @@
from fastapi import FastAPI from fastapi import FastAPI
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from starlette.responses import JSONResponse
from app.api.v1 import auth, keys from app.api.v1 import auth, keys
from app.core.config import settings from app.core.config import settings
from app.db.session import AsyncSessionLocal
app = FastAPI( app = FastAPI(
title=settings.PROJECT_NAME title=settings.PROJECT_NAME
@@ -9,7 +13,25 @@ app = FastAPI(
app.include_router(auth.router, prefix=f"{settings.API_V1_STR}/auth", tags=["auth"]) app.include_router(auth.router, prefix=f"{settings.API_V1_STR}/auth", tags=["auth"])
app.include_router(keys.router, prefix=f"{settings.API_V1_STR}/keys", tags=["keys"]) app.include_router(keys.router, prefix=f"{settings.API_V1_STR}/keys", tags=["keys"])
# Health endpoint (useful for docker, agent and uptime) # Is the API running?
@app.get("/livez")
async def livez():
return {"status": "ok"}
# Is the application ready (including db)?
@app.get("/readyz")
async def readyz():
try:
async with AsyncSessionLocal() as session:
await session.execute(text("SELECT 1"))
return {"status": "ok", "db": "up"}
except SQLAlchemyError:
return JSONResponse(
status_code=503,
content={"status": "degraded", "db": "down"},
)
# Is the application healthy (ready)?
@app.get("/healthz") @app.get("/healthz")
def healthz(): async def healthz():
return {"ok": True} return await readyz() # alias

View File

@@ -43,6 +43,11 @@ services:
# ports: # ports:
# - "8000:8000" # - "8000:8000"
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 command: uvicorn app.main:app --host 0.0.0.0 --port 8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/healthz"]
interval: 10s
timeout: 5s
retries: 5
volumes: volumes:
pgdata: pgdata:

View File

@@ -20,22 +20,7 @@ http {
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;
server_tokens off;
sendfile on;
tcp_nopush on;
keepalive_timeout 60;
tcp_nodelay on;
client_body_timeout 15;
gzip on;
gzip_vary on;
gzip_min_length 1k;
client_max_body_size 10G;
proxy_request_buffering off;
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
types_hash_bucket_size 128; types_hash_bucket_size 128;

View File

@@ -24,12 +24,13 @@ server {
location / { location / {
} }
location /docs {
proxy_pass http://keywarden-api:8000; # NOT FOR PROD vvv
} location ~ ^/(docs|openapi.json)$ {
location /openapi.json {
proxy_pass http://keywarden-api:8000; proxy_pass http://keywarden-api:8000;
} }
## REMOVE IN PRODUCTION BUILDS ^^^
location /api/v1/ { location /api/v1/ {
proxy_pass http://keywarden-api:8000; proxy_pass http://keywarden-api:8000;
proxy_set_header Host $host; proxy_set_header Host $host;
@@ -37,7 +38,11 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
} }
location /healthz { location ~ ^/(healthz|readyz|livez)$ {
proxy_pass http://keywarden-api:8000; proxy_pass http://keywarden-api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
} }

View File

@@ -8,6 +8,6 @@ from app.main import app
def test_healthz(): def test_healthz():
client = TestClient(app) client = TestClient(app)
r = client.get("/healthz") r = client.get("/readyz")
assert r.status_code == 200 assert r.status_code == 200
assert r.json() == {"ok": True} assert r.json() == {"status": "ok", "db": "up"}