diff --git a/api/app.py b/api/app.py index c4c08bd..051bce1 100644 --- a/api/app.py +++ b/api/app.py @@ -4,7 +4,8 @@ from flask_jwt_extended import JWTManager from jwt import ExpiredSignatureError from models import db, RevokedToken import os -from utils import init_db +from tech_views import tech_bp +from utils import init_db, wait_for_db from views import user_bp from werkzeug.exceptions import HTTPException @@ -26,6 +27,7 @@ def create_app(config_name="default"): # Blueprints registration app.register_blueprint(user_bp) + app.register_blueprint(tech_bp) # Database and JWT initialization db.init_app(app) @@ -53,6 +55,7 @@ def create_app(config_name="default"): # Fill database by initial values (only if we are not testing) with app.app_context(): + wait_for_db(max_retries=100) db.create_all() if config_name != "testing": init_db() diff --git a/api/tech_views.py b/api/tech_views.py new file mode 100644 index 0000000..0f2271c --- /dev/null +++ b/api/tech_views.py @@ -0,0 +1,20 @@ +from flask import Blueprint, jsonify +from models import db +from sqlalchemy import text +from utils import db_ready + +# Blueprint with technical endpoints +tech_bp = Blueprint('tech_bp', __name__) + +@tech_bp.route('/health', methods=['GET']) +def health_check(): + "Check if service works and database is functional" + try: + with db.engine.connect() as connection: + connection.execute(text("SELECT 1")) + return jsonify(status="healthy"), 200 + except Exception: + if db_ready: + return jsonify(status="unhealthy"), 500 + else: + return jsonify(status="starting"), 503 \ No newline at end of file diff --git a/api/utils.py b/api/utils.py index beffffb..2f5d69d 100644 --- a/api/utils.py +++ b/api/utils.py @@ -2,17 +2,22 @@ from flask import abort from flask_jwt_extended import get_jwt_identity from models import User, db import os +from sqlalchemy import text +from sqlalchemy.exc import DatabaseError, InterfaceError +import time from werkzeug.security import generate_password_hash +db_ready = False def admin_required(user_id, message='Access denied.'): + "Check if common user try to make administrative action." user = db.session.get(User, user_id) if user is None or user.role != "Administrator": abort(403, message) def validate_access(owner_id, message='Access denied.'): - # Check if user try to access or edit resource that does not belong to them + "Check if user try to access or edit resource that does not belong to them." logged_user_id = int(get_jwt_identity()) logged_user_role = db.session.get(User, logged_user_id).role if logged_user_role != "Administrator" and logged_user_id != owner_id: @@ -27,6 +32,20 @@ def get_user_or_404(user_id): return user +def wait_for_db(max_retries): + "Try to connect with database times." + global db_ready + for _ in range(max_retries): + try: + with db.engine.connect() as connection: + connection.execute(text("SELECT 1")) + db_ready = True + return + except DatabaseError | InterfaceError: + time.sleep(3) + raise Exception("Failed to connect to database.") + + def init_db(): """Create default admin account if database is empty""" with db.session.begin(): diff --git a/api/views.py b/api/views.py index 4d73b69..65ee144 100644 --- a/api/views.py +++ b/api/views.py @@ -2,6 +2,7 @@ from flask import Blueprint, jsonify, request, abort from flask_jwt_extended import create_access_token, set_access_cookies, jwt_required, \ verify_jwt_in_request, get_jwt_identity, unset_jwt_cookies, get_jwt from models import db, RevokedToken, User +import os from utils import admin_required, validate_access, get_user_or_404 from werkzeug.security import check_password_hash, generate_password_hash @@ -110,3 +111,10 @@ def user_logout(): response = jsonify({"msg": "User logged out successfully."}) unset_jwt_cookies(response) return response + +@user_bp.route('/version', methods=['GET']) +def version(): + return jsonify({ + "version": os.getenv("APP_VERSION", "unknown"), + "build_time": os.getenv("BUILD_DATE", "unknown") + }) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4717f40..dec3e28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,9 +7,24 @@ services: build: . env_file: - api/.env + ports: + - 80:80 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s db: container_name: db hostname: db image: mysql:latest env_file: - db/.env + ports: + - 3306:3306 + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5