Initial commit
This commit is contained in:
commit
85d261e987
18 files changed
+709
No files matched your search
@@ -0,0 +1,63 @@
|
|||||||
|
name: Build & Publish to GHCR via local build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
GHCR_ORG: gansejunge
|
||||||
|
IMAGE_NAME: app-notifications-service-GENERIC
|
||||||
|
IMAGE_PATH: ghcr.io/gansejunge/app-notifications-service-GENERIC
|
||||||
|
OPS_ORG: notifier
|
||||||
|
OPS_REPO: ops-deployment
|
||||||
|
OPS_BRANCH_BASE: main
|
||||||
|
OPS_PATH: service-GENERIC/deployment.yaml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: local
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ secrets.GHCR_USERNAME }} --password-stdin
|
||||||
|
|
||||||
|
- name: Get Latest Tag
|
||||||
|
id: get_tag
|
||||||
|
run: |
|
||||||
|
BASE64_TOKEN=$(echo "${{ secrets.GHCR_TOKEN }}" | base64)
|
||||||
|
LATEST_TAG=$(curl -s -H "Authorization: Bearer $BASE64_TOKEN" \
|
||||||
|
https://ghcr.io/v2/${{ env.GHCR_ORG }}/${{ env.IMAGE_NAME }}/tags/list \
|
||||||
|
| jq -r '.tags | map(select(test("^[0-9]+$"))) | map(tonumber) | max // 0')
|
||||||
|
NEXT_TAG=$((LATEST_TAG + 1))
|
||||||
|
echo "latest=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "next=$NEXT_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--build-arg COMMIT_ID=$(echo "${{ github.sha }}" | cut -c1-7) \
|
||||||
|
-t ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }} .
|
||||||
|
docker push ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }}
|
||||||
|
|
||||||
|
- name: Clone ops-deployment repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: notifier/ops-deployment
|
||||||
|
path: ./ops-deployment
|
||||||
|
token: ${{ secrets.BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update GENERIC deployment
|
||||||
|
working-directory: ./ops-deployment
|
||||||
|
run: |
|
||||||
|
NEW_TAG=${{ steps.get_tag.outputs.next }}
|
||||||
|
NEW_IMAGE="${{ env.IMAGE_PATH }}:$NEW_TAG"
|
||||||
|
git config user.name "automation-bot"
|
||||||
|
git config user.email "dev@gansejunge.com"
|
||||||
|
|
||||||
|
sed -i "s|ghcr.io/$GHCR_ORG/$IMAGE_NAME:[0-9]\+|$NEW_IMAGE|g" "$OPS_PATH"
|
||||||
|
|
||||||
|
COMMIT_URL="https://git.gansejunge.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
|
||||||
|
git add $OPS_PATH
|
||||||
|
git commit -m "Update GENERIC image to version $NEW_TAG" -m "Linked build commit: $COMMIT_URL"
|
||||||
|
git push origin $OPS_BRANCH_BASE
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
name: Build & Publish to GHCR
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
GHCR_ORG: gansejunge
|
||||||
|
IMAGE_NAME: app-notifications-service-GENERIC
|
||||||
|
IMAGE_PATH: ghcr.io/gansejunge/app-notifications-service-GENERIC
|
||||||
|
OPS_ORG: notifier
|
||||||
|
OPS_REPO: ops-deployment
|
||||||
|
OPS_BRANCH_BASE: main
|
||||||
|
OPS_PATH: service-GENERIC/deployment.yaml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ secrets.GHCR_USERNAME }} --password-stdin
|
||||||
|
|
||||||
|
- name: Get Latest Tag
|
||||||
|
id: get_tag
|
||||||
|
run: |
|
||||||
|
BASE64_TOKEN=$(echo "${{ secrets.GHCR_TOKEN }}" | base64)
|
||||||
|
LATEST_TAG=$(curl -s -H "Authorization: Bearer $BASE64_TOKEN" \
|
||||||
|
https://ghcr.io/v2/${{ env.GHCR_ORG }}/${{ env.IMAGE_NAME }}/tags/list \
|
||||||
|
| jq -r '.tags | map(select(test("^[0-9]+$"))) | map(tonumber) | max // 0')
|
||||||
|
NEXT_TAG=$((LATEST_TAG + 1))
|
||||||
|
echo "latest=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "next=$NEXT_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--build-arg COMMIT_ID=$(echo "${{ github.sha }}" | cut -c1-7) \
|
||||||
|
-t ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }} .
|
||||||
|
docker push ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }}
|
||||||
|
|
||||||
|
- name: Clone ops-deployment repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: notifier/ops-deployment
|
||||||
|
path: ./ops-deployment
|
||||||
|
token: ${{ secrets.BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update GENERIC deployment
|
||||||
|
working-directory: ./ops-deployment
|
||||||
|
run: |
|
||||||
|
NEW_TAG=${{ steps.get_tag.outputs.next }}
|
||||||
|
NEW_IMAGE="${{ env.IMAGE_PATH }}:$NEW_TAG"
|
||||||
|
git config user.name "automation-bot"
|
||||||
|
git config user.email "dev@gansejunge.com"
|
||||||
|
|
||||||
|
sed -i "s|ghcr.io/$GHCR_ORG/$IMAGE_NAME:[0-9]\+|$NEW_IMAGE|g" "$OPS_PATH"
|
||||||
|
|
||||||
|
COMMIT_URL="https://git.gansejunge.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
|
||||||
|
git add $OPS_PATH
|
||||||
|
git commit -m "Update GENERIC image to version $NEW_TAG" -m "Linked build commit: $COMMIT_URL"
|
||||||
|
git push origin $OPS_BRANCH_BASE
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: pip-audit
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "35 1 * * *"
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: [requirements.txt, scripts/filter_requirements.py]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
report_b64: ${{ steps.audit.outputs.report_b64 }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Audit dependencies
|
||||||
|
id: audit
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pip install pip-audit
|
||||||
|
python3 scripts/filter_requirements.py requirements.txt requirements.audit.txt
|
||||||
|
|
||||||
|
# Don't let a non-zero exit abort the step, so the report is saved first
|
||||||
|
set +e
|
||||||
|
pip-audit -r requirements.audit.txt --no-deps --disable-pip > audit.txt 2>&1
|
||||||
|
echo $? > audit.exit
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cat audit.txt
|
||||||
|
echo "report_b64=$(head -c 100000 audit.txt | base64 -w0)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Fail if the audit failed
|
||||||
|
shell: bash
|
||||||
|
run: exit "$(cat audit.exit)"
|
||||||
|
|
||||||
|
notify:
|
||||||
|
needs: audit
|
||||||
|
if: ${{ always() && needs.audit.result == 'failure' }}
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: Send mail via local postfix
|
||||||
|
env:
|
||||||
|
MAIL_FROM: ${{ vars.MAIL_FROM }}
|
||||||
|
MAIL_TO: ${{ vars.MAIL_TO }}
|
||||||
|
REPO: ${{ gitea.repository }}
|
||||||
|
RUN_URL: ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_number }}
|
||||||
|
REPORT_B64: ${{ needs.audit.outputs.report_b64 }}
|
||||||
|
run: |
|
||||||
|
python3 - <<'EOF'
|
||||||
|
import base64, os, smtplib
|
||||||
|
from email.message import EmailMessage
|
||||||
|
|
||||||
|
b64 = os.environ.get("REPORT_B64", "")
|
||||||
|
report = (
|
||||||
|
base64.b64decode(b64).decode("utf-8", errors="replace")
|
||||||
|
if b64 else
|
||||||
|
"(no audit output, the job failed before pip-audit finished)"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg["Subject"] = f"[pip-audit] Problem in {os.environ['REPO']}"
|
||||||
|
msg["From"] = os.environ["MAIL_FROM"]
|
||||||
|
msg["To"] = os.environ["MAIL_TO"]
|
||||||
|
msg.set_content(
|
||||||
|
f"pip-audit result for {os.environ['REPO']}:\n\n"
|
||||||
|
f"{report}\n\n"
|
||||||
|
f"Run: {os.environ['RUN_URL']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
with smtplib.SMTP("127.0.0.1", 25, timeout=30) as s:
|
||||||
|
s.send_message(msg)
|
||||||
|
EOF
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
# ---> Python
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Builder stage
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
WORKDIR /install
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
|
||||||
|
WORKDIR /app
|
||||||
|
COPY src/ /app/
|
||||||
|
|
||||||
|
# Runtime image
|
||||||
|
FROM nvcr.io/nvidia/distroless/python:3.12-v4.2.0
|
||||||
|
COPY --from=builder /install /usr/local
|
||||||
|
COPY --from=builder /app /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG COMMIT_ID=unknown
|
||||||
|
ENV COMMIT_ID=$COMMIT_ID
|
||||||
|
|
||||||
|
ENTRYPOINT ["python", "main.py"]
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
target-version = "py312"
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", "F", # pycodestyle errors, pyflakes (unused imports/variables, undefined names)
|
||||||
|
"I", # import sorting (replaces isort)
|
||||||
|
"B", # bugbear: likely bugs
|
||||||
|
"ASYNC", # async mistakes
|
||||||
|
"RUF006", # un-referenced asyncio.create_task (tasks can be garbage-collected)
|
||||||
|
]
|
||||||
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["db_module", "secret_manager", "service_auth", "shared_models",
|
||||||
|
"simple_logger_handler", "send_notifications", "send_notifications_webhook",]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
indent-style = "tab"
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
|
extend-immutable-calls = [
|
||||||
|
"fastapi.Depends",
|
||||||
|
"fastapi.Query",
|
||||||
|
"fastapi.Header",
|
||||||
|
"fastapi.Body",
|
||||||
|
"fastapi.Security",
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ruff
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
--extra-index-url https://git.gansejunge.com/api/packages/notifier/pypi/simple/
|
||||||
|
fastapi
|
||||||
|
lib-db-module
|
||||||
|
lib-secret-manager
|
||||||
|
lib-send-notifications
|
||||||
|
lib-service-auth
|
||||||
|
lib-shared-models
|
||||||
|
prometheus_client
|
||||||
|
pydantic
|
||||||
|
simple-logger-handler
|
||||||
|
uvicorn
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Write a copy of requirements.txt without the self-hosted packages.
|
||||||
|
|
||||||
|
Usage: filter_requirements.py <input> <output>
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
SKIP = {
|
||||||
|
"lib-db-module",
|
||||||
|
"lib-secret-manager",
|
||||||
|
"lib-send-notifications",
|
||||||
|
"send_notifications_webhook",
|
||||||
|
"lib-service-auth",
|
||||||
|
"lib-shared-models",
|
||||||
|
"simple-logger-handler",
|
||||||
|
}
|
||||||
|
|
||||||
|
NAME_RE = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)")
|
||||||
|
|
||||||
|
|
||||||
|
def main(src: str, dst: str) -> None:
|
||||||
|
skipped = []
|
||||||
|
|
||||||
|
with open(src) as fin, open(dst, "w") as fout:
|
||||||
|
for line in fin:
|
||||||
|
match = NAME_RE.match(line)
|
||||||
|
if match and match.group(1) in SKIP:
|
||||||
|
skipped.append(match.group(1))
|
||||||
|
continue
|
||||||
|
fout.write(line)
|
||||||
|
|
||||||
|
print(f"Skipped {len(skipped)} self-hosted package(s): {', '.join(skipped) or '-'}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
main(sys.argv[1], sys.argv[2])
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from service_auth import KeySource, ServiceAuth
|
||||||
|
|
||||||
|
|
||||||
|
class Caller(StrEnum):
|
||||||
|
GATEWAY = "gateway"
|
||||||
|
SCHEDULER = "scheduler"
|
||||||
|
|
||||||
|
auth = ServiceAuth({
|
||||||
|
Caller.GATEWAY: KeySource("GATEWAY_KEY", "/etc/secrets/gateway_key"),
|
||||||
|
Caller.SCHEDULER: KeySource("SCHEDULER_KEY", "/etc/secrets/scheduler_key"),
|
||||||
|
})
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
from metrics_server import REQUEST_COUNTER
|
||||||
|
from simple_logger_handler import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
|
async def custom_http_exception_handler(request,exc):
|
||||||
|
logger.debug(f"Handling HTTP exception: {exc}")
|
||||||
|
if exc.status_code == 404:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=403,
|
||||||
|
content={"detail": "Unauthorized"}
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={"detail": exc.detail}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def prometheus_middleware(request: Request, call_next):
|
||||||
|
logger.debug(
|
||||||
|
"Incoming request: %s %s",
|
||||||
|
request.method, request.url.path,
|
||||||
|
)
|
||||||
|
status = 500
|
||||||
|
try:
|
||||||
|
response = await call_next(request)
|
||||||
|
status = response.status_code
|
||||||
|
logger.debug("Request processed with status: %s", status)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("[FastAPI] Exception during request processing: %s", e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
REQUEST_COUNTER.labels(request.method, request.url.path, status).inc()
|
||||||
|
logger.debug(
|
||||||
|
"[FastAPI] Prometheus counter incremented: %s %s %s",
|
||||||
|
request.method, request.url.path, status,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def register_handlers(app: FastAPI) -> None:
|
||||||
|
"""Attach the error handlers and the metrics middleware to the app."""
|
||||||
|
app.add_exception_handler(StarletteHTTPException, custom_http_exception_handler)
|
||||||
|
app.add_middleware(BaseHTTPMiddleware, dispatch=prometheus_middleware)
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
|
||||||
|
from auth_config import Caller, auth
|
||||||
|
from db_module import (
|
||||||
|
close_connection_pool,
|
||||||
|
create_connection_pool,
|
||||||
|
start_healthcheck_thread,
|
||||||
|
)
|
||||||
|
from handlers import register_handlers
|
||||||
|
from metrics_server import metrics_api
|
||||||
|
from routers.scheduler import scheduler_router
|
||||||
|
from routers.user import user_router
|
||||||
|
from secret_manager import cleanup_secret_files
|
||||||
|
from send_notifications import NotificationClient
|
||||||
|
from simple_logger_handler import LOG_LEVEL, setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
logger.info("[App] Starting application...")
|
||||||
|
app.state.notifier = NotificationClient()
|
||||||
|
logger.info("[DB] Creating MySQL connection pool...")
|
||||||
|
create_connection_pool("seadexPool")
|
||||||
|
|
||||||
|
start_healthcheck_thread()
|
||||||
|
logger.info("[DB] MySQL healthcheck thread started.")
|
||||||
|
|
||||||
|
SECRET_PATHS = frozenset({
|
||||||
|
"/etc/secrets/api_key",
|
||||||
|
"/etc/secrets/db_username",
|
||||||
|
"/etc/secrets/db_password"
|
||||||
|
})
|
||||||
|
cleanup_secret_files(SECRET_PATHS)
|
||||||
|
|
||||||
|
yield
|
||||||
|
logger.info("[App] Closing MySQL connection pool...")
|
||||||
|
close_connection_pool()
|
||||||
|
logger.info("[App] Shutdown complete.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
api = FastAPI(
|
||||||
|
title="Service GENERIC",
|
||||||
|
description="GENERICGENERICGENERICGENERICGENERIC",
|
||||||
|
version="1.0.0",
|
||||||
|
lifespan=lifespan,
|
||||||
|
dependencies=[Depends(auth.authenticate)],
|
||||||
|
)
|
||||||
|
|
||||||
|
register_handlers(api)
|
||||||
|
api.include_router(user_router)
|
||||||
|
api.include_router(scheduler_router)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_servers():
|
||||||
|
logger.info("[Server] Starting main API (port 5000) and metrics server (port 9000)...")
|
||||||
|
logger.info("[Server] Build commit id=%s", os.getenv("COMMIT_ID", "unknown"))
|
||||||
|
|
||||||
|
auth.load_keys(api, Caller.GATEWAY, Caller.SCHEDULER, Caller.ADMIN)
|
||||||
|
cleanup_secret_files(auth.key_paths(Caller.GATEWAY, Caller.SCHEDULER, Caller.ADMIN))
|
||||||
|
|
||||||
|
config_main = uvicorn.Config(api, host="0.0.0.0", port=5000, log_level=LOG_LEVEL.lower(),log_config=None)
|
||||||
|
config_metrics = uvicorn.Config(metrics_api, host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(),log_config=None)
|
||||||
|
|
||||||
|
server_main = uvicorn.Server(config_main)
|
||||||
|
server_metrics = uvicorn.Server(config_metrics)
|
||||||
|
|
||||||
|
await asyncio.gather(server_main.serve(), server_metrics.serve())
|
||||||
|
logger.info("[Server] Both servers have stopped.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(start_servers())
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from fastapi import FastAPI, Response
|
||||||
|
from prometheus_client import CONTENT_TYPE_LATEST, Counter, generate_latest
|
||||||
|
|
||||||
|
metrics_api = FastAPI(title="Metrics Server", description="Prometheus metrics endpoint")
|
||||||
|
|
||||||
|
REQUEST_COUNTER = Counter("http_requests_total", "Total HTTP Requests", ["method", "endpoint", "status"])
|
||||||
|
|
||||||
|
@metrics_api.get("/metrics")
|
||||||
|
async def metrics():
|
||||||
|
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, computed_field
|
||||||
|
|
||||||
|
|
||||||
|
class GenericEntry(BaseModel):
|
||||||
|
name: str
|
||||||
|
custom_name: str | None = None
|
||||||
|
published: datetime | None = None
|
||||||
|
|
||||||
|
class Counts(BaseModel):
|
||||||
|
active: int
|
||||||
|
inactive: int
|
||||||
|
|
||||||
|
class GenericList(BaseModel):
|
||||||
|
active: list[GenericEntry] = Field(default_factory=list)
|
||||||
|
inactive: list[GenericEntry] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def counts(self) -> Counts:
|
||||||
|
return Counts(active=len(self.active),inactive=len(self.inactive))
|
||||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
|
||||||
|
from auth_config import Caller, auth
|
||||||
|
from db_module import (
|
||||||
|
get_db,
|
||||||
|
)
|
||||||
|
from simple_logger_handler import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
|
scheduler_router = APIRouter(dependencies=[Depends(auth.require(Caller.SCHEDULER))])
|
||||||
|
|
||||||
|
@scheduler_router.post("/update")
|
||||||
|
def check_for_updates(request:Request, db=Depends(get_db)):
|
||||||
|
return
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from auth_config import Caller, auth
|
||||||
|
from db_module import (
|
||||||
|
get_db,
|
||||||
|
)
|
||||||
|
from models import GenericList
|
||||||
|
from service_auth import UserId
|
||||||
|
from simple_logger_handler import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
|
user_router = APIRouter(dependencies=[Depends(auth.require(Caller.GATEWAY))])
|
||||||
|
|
||||||
|
@user_router.post("/entry/add")
|
||||||
|
def add_entry(
|
||||||
|
user_id: UserId,
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
@user_router.post("/entry/delete")
|
||||||
|
def remove_anime(
|
||||||
|
user_id: UserId,
|
||||||
|
db=Depends(get_db)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
@user_router.get("/entry/list", response_model=GenericList)
|
||||||
|
def list_active_and_inactive_anime(
|
||||||
|
user_id: UserId,
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
@user_router.post("/entry/rename")
|
||||||
|
def rename_entry(
|
||||||
|
user_id: UserId,
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
return
|
||||||
Reference in new issue
Block a user