From aab311dc86c07fd86bc54c64532ec2b2e9b224f2 Mon Sep 17 00:00:00 2001 From: florian Date: Sun, 5 Oct 2025 20:27:30 +0200 Subject: [PATCH] Deployment configuration --- Dockerfile | 11 ++++ db.py | 42 ------------- hvac_handler.py | 14 ----- rabbitmq.md | 19 +++--- rabbitmq_handler.py | 21 ------- requirements.txt | 24 +++++++ src/db.py | 62 +++++++++++++++++++ src/hvac_handler.py | 44 +++++++++++++ logger_handler.py => src/logger_handler.py | 0 main.py => src/main.py | 4 +- src/rabbitmq_handler.py | 56 +++++++++++++++++ .../uvicorn_logging_config.py | 0 validator.py => src/validator.py | 0 13 files changed, 206 insertions(+), 91 deletions(-) create mode 100644 Dockerfile delete mode 100644 db.py delete mode 100644 hvac_handler.py delete mode 100644 rabbitmq_handler.py create mode 100644 requirements.txt create mode 100644 src/db.py create mode 100644 src/hvac_handler.py rename logger_handler.py => src/logger_handler.py (100%) rename main.py => src/main.py (96%) create mode 100644 src/rabbitmq_handler.py rename uvicorn_logging_config.py => src/uvicorn_logging_config.py (100%) rename validator.py => src/validator.py (100%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dbe199e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-slim + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +WORKDIR /app + +COPY src/ /app/ + +ENTRYPOINT ["sh", "-c", "sleep 10 && python main.py"] \ No newline at end of file diff --git a/db.py b/db.py deleted file mode 100644 index 672f529..0000000 --- a/db.py +++ /dev/null @@ -1,42 +0,0 @@ -import mysql.connector -from mysql.connector import pooling -import threading - -MYSQL_CONFIG = { - "host": "localhost", - "user": "florian", - "password": "password123++", - "database": "app" -} - -# Lock to ensure thread-safe pool creation -_pool_lock = threading.Lock() -_connection_pool = None - -def get_connection_pool(): - global _connection_pool - with _pool_lock: - if _connection_pool is None: - _connection_pool = mysql.connector.pooling.MySQLConnectionPool( - pool_name="mypool", - pool_size=5, - pool_reset_session=True, - **MYSQL_CONFIG - ) - return _connection_pool - -# Dependency for FastAPI -def get_db(): - pool = get_connection_pool() - conn = pool.get_connection() - try: - yield conn - finally: - conn.close() - -if __name__ == "__main__": - # Manual test - for conn in get_db(): - cursor = conn.cursor(dictionary=True) - cursor.execute("SELECT NOW() AS ts") - print(cursor.fetchone()) diff --git a/hvac_handler.py b/hvac_handler.py deleted file mode 100644 index 8db9769..0000000 --- a/hvac_handler.py +++ /dev/null @@ -1,14 +0,0 @@ -import hvac -import base64 -import os - -HVAC_AGENT_URL = os.getenv("HVAC_AGENT_URL","http://vault-agent:8201") -client = hvac.Client(url=HVAC_AGENT_URL) - -def decrypt_token(ciphertext: str) -> str: - response = client.secrets.transit.decrypt_data( - name="push-tokens", - ciphertext=ciphertext - ) - plaintext_b64 = response["data"]["plaintext"] - return base64.b64decode(plaintext_b64).decode() diff --git a/rabbitmq.md b/rabbitmq.md index df91c04..1cbccd3 100644 --- a/rabbitmq.md +++ b/rabbitmq.md @@ -1,15 +1,12 @@ rabbitmqctl add_vhost app_notifications -rabbitmqctl add_user notifier strongpassword -rabbitmqctl set_user_tags notifier management -rabbitmqctl set_permissions -p app_notifications notifier ".*" ".*" ".*" -rabbitmqadmin --username "admin" --password "admin" declare exchange --vhost "app_notifications" --name "app_notifications" --type "topic" --durable "true" -rabbitmqadmin --username "admin" --password "admin" declare queue --vhost "app_notifications" --name "notifications_retry" - --durable "true" - rabbitmqadmin --username "admin" --password "admin" declare queue --vhost "app_notifications" --name "notifications_dlq" - --durable "true" -rabbitmqadmin --username "admin" --password "admin" declare queue --vhost "app_notifications" --name "notifications" - --durable "true" - rabbitmqadmin --username "admin" --password "admin" declare binding --vhost "app_notifications" --source "app_notifications" --destination "notifications" --destination-type "queue" --routing-key "notify.*" +rabbitmqctl add_user backend-api-internal strongpassword +rabbitmqctl set_user_tags backend-api-internal management +rabbitmqctl set_permissions -p app_notifications backend-api-internal ".*" ".*" ".*" +rabbitmqadmin declare exchange --vhost "app_notifications" --name "app_notifications" --type "topic" --durable "true" +rabbitmqadmin declare queue --vhost "app_notifications" --name "notifications_retry" --durable "true" +rabbitmqadmin declare queue --vhost "app_notifications" --name "notifications_dlq" --durable "true" +rabbitmqadmin declare queue --vhost "app_notifications" --name "notifications" --durable "true" +rabbitmqadmin declare binding --vhost "app_notifications" --source "app_notifications" --destination "notifications" --destination-type "queue" --routing-key "notify.#" # Retry policy: messages stay for 30s before going back to main queue rabbitmqctl set_policy \ diff --git a/rabbitmq_handler.py b/rabbitmq_handler.py deleted file mode 100644 index d617614..0000000 --- a/rabbitmq_handler.py +++ /dev/null @@ -1,21 +0,0 @@ -import pika -from typing import Dict -import json - -def send_message_to_rmq(user_id: int, message: Dict): - credentials = pika.credentials.PlainCredentials(username="notifier",password="strongpassword") - conn_params = pika.ConnectionParameters(host="localhost", - credentials=credentials,virtual_host="app_notifications") - connection = pika.BlockingConnection(conn_params) - #connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) - channel = connection.channel() - channel.confirm_delivery() - - channel.basic_publish(exchange='app_notifications', - routing_key=f"notify.user.{user_id}", - body=json.dumps(message), - properties=pika.BasicProperties( - content_type="application/json", - delivery_mode=2 - )) - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1378026 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +annotated-types==0.7.0 +anyio==4.11.0 +argon2-cffi==25.1.0 +argon2-cffi-bindings==25.1.0 +certifi==2025.10.5 +cffi==2.0.0 +charset-normalizer==3.4.3 +click==8.3.0 +fastapi==0.118.0 +h11==0.16.0 +hvac==2.3.0 +idna==3.10 +mysql-connector-python==9.4.0 +pika==1.3.2 +pycparser==2.23 +pydantic==2.11.10 +pydantic_core==2.33.2 +requests==2.32.5 +sniffio==1.3.1 +starlette==0.48.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 +uvicorn==0.37.0 diff --git a/src/db.py b/src/db.py new file mode 100644 index 0000000..1e04d6a --- /dev/null +++ b/src/db.py @@ -0,0 +1,62 @@ +import mysql.connector +from mysql.connector import pooling +import threading +from hvac_handler import get_secret +import os +import time +import sys + + +db_username = get_secret("secret/api-internal/db", "username") +db_password = get_secret("secret/api-internal/db", "password") +db_host = os.getenv("BACKEND_API_INTERNAL_DB_HOST","localhost") +db_database = os.getenv("BACKEND_API_INTERNAL_DB_DATABASE","app") + +MAX_RETRIES = 5 +RETRY_DELAY = 5 + +MYSQL_CONFIG = { + "host": db_host, + "user": db_username, + "password": db_password, + "database": db_database +} + +_pool_lock = threading.Lock() +_connection_pool = None + +def create_connection_pool(): + global _connection_pool + for attempt in range(1, MAX_RETRIES+1): + try: + print(f"[MySQL] Attempt {attempt} to connect...") + _connection_pool = mysql.connector.pooling.MySQLConnectionPool( + pool_name="mypool", + pool_size=5, + pool_reset_session=True, + **MYSQL_CONFIG + ) + print("[MySQL] Connection pool created successfully.") + return + except mysql.connector.Error as e: + print(f"[MySQL] Attempt {attempt} failed: {e}") + if attempt < MAX_RETRIES: + time.sleep(RETRY_DELAY) + print(f"[MySQL] Failed to connect after {MAX_RETRIES} attempts — exiting.") + sys.exit(1) + +def get_connection_pool(): + global _connection_pool + with _pool_lock: + if _connection_pool is None: + create_connection_pool + return _connection_pool + +def get_db(): + pool = get_connection_pool() + conn = pool.get_connection() + try: + yield conn + finally: + conn.close() + diff --git a/src/hvac_handler.py b/src/hvac_handler.py new file mode 100644 index 0000000..06ddd79 --- /dev/null +++ b/src/hvac_handler.py @@ -0,0 +1,44 @@ +import hvac +import base64 +import os +import time +import sys + +HVAC_AGENT_URL = os.getenv("HVAC_AGENT_URL","http://vault-agent:8201") + +MAX_RETRIES = 5 +BACKOFF = 5 + +def get_client(): + for attempt in range(1, MAX_RETRIES+1): + try: + client = hvac.Client(url=HVAC_AGENT_URL) + if client.is_authenticated(): + return client + raise Exception("Not authenticated") + except Exception as e: + print(f"Vault connection failed (attempt {attempt}/{MAX_RETRIES}): {e}") + time.sleep(BACKOFF * attempt) + print("Vault unreachable after retries. Exiting.") + sys.exit(1) + +client = get_client() + +def get_secret(path:str, key:str): + try: + secret = client.secrets.kv.v2.read_secret_version( + mount_point="kv", + path=path + ) + return secret["data"]["data"][key] + except Exception as e: + print(f"Failed to fetch secret '{path}:{key}': {e}") + sys.exit(1) + +def decrypt_token(ciphertext: str) -> str: + response = client.secrets.transit.decrypt_data( + name="push-tokens", + ciphertext=ciphertext + ) + plaintext_b64 = response["data"]["plaintext"] + return base64.b64decode(plaintext_b64).decode() diff --git a/logger_handler.py b/src/logger_handler.py similarity index 100% rename from logger_handler.py rename to src/logger_handler.py diff --git a/main.py b/src/main.py similarity index 96% rename from main.py rename to src/main.py index 2b5860d..d36c4f5 100644 --- a/main.py +++ b/src/main.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, Query, Depends, HTTPException, Header +from fastapi import FastAPI, Depends, HTTPException from fastapi.responses import JSONResponse from fastapi.security.api_key import APIKeyHeader from starlette.exceptions import HTTPException as StarletteHTTPException @@ -11,8 +11,6 @@ from rabbitmq_handler import send_message_to_rmq import uvicorn from uvicorn_logging_config import LOGGING_CONFIG - - logger = setup_logger(__name__) api_key_header_internal = APIKeyHeader(name="X-API-Key-Internal") diff --git a/src/rabbitmq_handler.py b/src/rabbitmq_handler.py new file mode 100644 index 0000000..73a4894 --- /dev/null +++ b/src/rabbitmq_handler.py @@ -0,0 +1,56 @@ +import pika +from typing import Dict +import ssl +from hvac_handler import get_secret +import json +import time +import sys + +rmq_username = get_secret("secret/api-internal/rmq", "username") +rmq_password = get_secret("secret/api-internal/rmq", "password") + +MAX_RETRIES = 5 +RETRY_DELAY = 5 + +def send_message_to_rmq(user_id: int, message: Dict): + credentials = pika.PlainCredentials(username=rmq_username, password=rmq_password) + context = ssl.create_default_context() + context.check_hostname = False + ssl_options = pika.SSLOptions(context) + conn_params = pika.ConnectionParameters( + host="localhost", + port=5671, + ssl_options=ssl_options, + credentials=credentials, + virtual_host="app_notifications" + ) + + for attempt in range(1, MAX_RETRIES + 1): + try: + connection = pika.BlockingConnection(conn_params) + channel = connection.channel() + channel.exchange_declare(exchange="app_notifications", exchange_type="topic", durable=True) + channel.confirm_delivery() + channel.basic_publish( + exchange='app_notifications', + routing_key=f"notify.user.{user_id}", + body=json.dumps(message), + properties=pika.BasicProperties( + content_type="application/json", + delivery_mode=2 + ), + mandatory=True + ) + connection.close() + return + + except Exception as e: + print(f"[RMQ] Attempt {attempt} failed: {e}") + if attempt < MAX_RETRIES: + time.sleep(RETRY_DELAY) + else: + print("[RMQ] Failed to connect after maximum retries — exiting.") + sys.exit(1) + +if __name__ == "__main__": + send_message_to_rmq(1, {"type": "notification", "content": "Vault TLS cert reloaded successfully."}) \ No newline at end of file diff --git a/uvicorn_logging_config.py b/src/uvicorn_logging_config.py similarity index 100% rename from uvicorn_logging_config.py rename to src/uvicorn_logging_config.py diff --git a/validator.py b/src/validator.py similarity index 100% rename from validator.py rename to src/validator.py