From 636a382cf5dd81218886caba00ea9dbc9ae68a40 Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Wed, 11 Jun 2025 17:13:27 +0000 Subject: [PATCH 1/8] Deleted jenkins pipeline from main branch --- Jenkinsfile | 72 ----------------------------------------------------- goss.yaml | 8 ------ 2 files changed, 80 deletions(-) delete mode 100644 Jenkinsfile delete mode 100644 goss.yaml diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 2aa9b93..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,72 +0,0 @@ -pipeline { - agent any - environment { - DOCKER_REGISTRY_URL = 'marcin00.azurecr.io' - DOCKER_IMAGE = "${DOCKER_REGISTRY_URL}/user-microservice:${GIT_COMMIT}" - ACR_NAME = 'marcin00' - } - stages { - stage('Checkout') { - steps { - checkout scm - } - } - stage('Test python app') { - steps { - script { - dir('api') { - sh ''' - python3 -m venv env - source env/bin/activate - pip install -r requirements.txt pytest - python3 -m pytest --junit-xml=pytest_junit.xml - ''' - } - } - } - post { - always { - junit testResults: '**/*pytest_junit.xml' - } - } - } - stage('Build & test docker image') { - steps { - script { - appImage = docker.build("${DOCKER_IMAGE}") - - sh label: 'Install dgoss', script: ''' - curl -s -L https://github.com/aelsabbahy/goss/releases/latest/download/goss-linux-amd64 -o goss - curl -s -L https://github.com/aelsabbahy/goss/releases/latest/download/dgoss -o dgoss - chmod +rx *goss - ''' - - withEnv(['GOSS_OPTS=-f junit', 'GOSS_PATH=./goss', 'GOSS_SLEEP=3', 'SQLALCHEMY_DATABASE_URI=sqlite:///:memory:']) { - sh label: 'run image tests', script: './dgoss run -e SQLALCHEMY_DATABASE_URI=sqlite:///:memory: ${DOCKER_IMAGE} > goss_junit.xml' - } - } - } - post { - always { - junit testResults: '**/*goss_junit.xml' - } - } - } - stage('Deploy') { - steps { - script { - sh ''' - az login --identity - az acr login --name ${ACR_NAME} - docker push ${DOCKER_IMAGE} - ''' - } - } - } - } - post { - cleanup { - script { cleanWs() } - } - } -} diff --git a/goss.yaml b/goss.yaml deleted file mode 100644 index b59e9af..0000000 --- a/goss.yaml +++ /dev/null @@ -1,8 +0,0 @@ -port: - tcp:80: - listening: true - ip: - - 0.0.0.0 -process: - python3: - running: true From 9e010ed389a353fbaa8e9198fb56106f2662e75f Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Wed, 11 Jun 2025 19:43:47 +0000 Subject: [PATCH 2/8] Implemented waiting for db readiness --- api/app.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/app.py b/api/app.py index c4c08bd..5374030 100644 --- a/api/app.py +++ b/api/app.py @@ -4,10 +4,29 @@ from flask_jwt_extended import JWTManager from jwt import ExpiredSignatureError from models import db, RevokedToken import os +from sqlalchemy import text +from sqlalchemy.exc import DatabaseError +import time from utils import init_db from views import user_bp from werkzeug.exceptions import HTTPException +MAX_RETRIES = 100 + +def wait_for_db(): + for retries in range(MAX_RETRIES): + try: + with db.engine.connect() as connection: + connection.execute(text("SELECT 1")) + print("Successfully connected with database.") + return + except DatabaseError: + print(f"Waiting for database... (retry {retries + 1})") + time.sleep(3) + print("Failed to connect to database.") + raise Exception("Database not ready after multiple retries.") + + def create_app(config_name="default"): """Creates and returns a new instance of Flask app.""" load_dotenv() @@ -53,6 +72,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() db.create_all() if config_name != "testing": init_db() From d3d3c98f997f5c4731f90b484e01f2fdae000705 Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Wed, 11 Jun 2025 19:48:58 +0000 Subject: [PATCH 3/8] Moved wait_for_db function to utils module --- api/app.py | 21 +-------------------- api/utils.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/api/app.py b/api/app.py index 5374030..8f3c1a2 100644 --- a/api/app.py +++ b/api/app.py @@ -4,29 +4,10 @@ from flask_jwt_extended import JWTManager from jwt import ExpiredSignatureError from models import db, RevokedToken import os -from sqlalchemy import text -from sqlalchemy.exc import DatabaseError -import time -from utils import init_db +from utils import init_db, wait_for_db from views import user_bp from werkzeug.exceptions import HTTPException -MAX_RETRIES = 100 - -def wait_for_db(): - for retries in range(MAX_RETRIES): - try: - with db.engine.connect() as connection: - connection.execute(text("SELECT 1")) - print("Successfully connected with database.") - return - except DatabaseError: - print(f"Waiting for database... (retry {retries + 1})") - time.sleep(3) - print("Failed to connect to database.") - raise Exception("Database not ready after multiple retries.") - - def create_app(config_name="default"): """Creates and returns a new instance of Flask app.""" load_dotenv() diff --git a/api/utils.py b/api/utils.py index beffffb..26eda1d 100644 --- a/api/utils.py +++ b/api/utils.py @@ -2,6 +2,9 @@ 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 +import time from werkzeug.security import generate_password_hash @@ -27,6 +30,22 @@ def get_user_or_404(user_id): return user +MAX_RETRIES = 100 + +def wait_for_db(): + for retries in range(MAX_RETRIES): + try: + with db.engine.connect() as connection: + connection.execute(text("SELECT 1")) + print("Successfully connected with database.") + return + except DatabaseError: + print(f"Waiting for database... (retry {retries + 1})") + time.sleep(3) + print("Failed to connect to database.") + raise Exception("Database not ready after multiple retries.") + + def init_db(): """Create default admin account if database is empty""" with db.session.begin(): From dd9e9ce11048f8a02af8e8d800b5561d4aeee74c Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Wed, 11 Jun 2025 19:57:15 +0000 Subject: [PATCH 4/8] Improved function body --- api/app.py | 2 +- api/utils.py | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/api/app.py b/api/app.py index 8f3c1a2..8ca7625 100644 --- a/api/app.py +++ b/api/app.py @@ -53,7 +53,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() + wait_for_db(max_retries=100) db.create_all() if config_name != "testing": init_db() diff --git a/api/utils.py b/api/utils.py index 26eda1d..e1256a4 100644 --- a/api/utils.py +++ b/api/utils.py @@ -30,20 +30,15 @@ def get_user_or_404(user_id): return user -MAX_RETRIES = 100 - -def wait_for_db(): - for retries in range(MAX_RETRIES): +def wait_for_db(max_retries): + for _ in range(max_retries): try: with db.engine.connect() as connection: connection.execute(text("SELECT 1")) - print("Successfully connected with database.") return except DatabaseError: - print(f"Waiting for database... (retry {retries + 1})") time.sleep(3) - print("Failed to connect to database.") - raise Exception("Database not ready after multiple retries.") + raise Exception("Failed to connect to database.") def init_db(): From 3f40a6126c1ed723b4b2a1348ef31a79842bb39c Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Wed, 11 Jun 2025 20:04:04 +0000 Subject: [PATCH 5/8] Added more descriptions of functions --- api/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/utils.py b/api/utils.py index e1256a4..2a1261e 100644 --- a/api/utils.py +++ b/api/utils.py @@ -9,13 +9,14 @@ from werkzeug.security import generate_password_hash 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: @@ -31,6 +32,7 @@ def get_user_or_404(user_id): def wait_for_db(max_retries): + "Try to connect with database times." for _ in range(max_retries): try: with db.engine.connect() as connection: From 479ec4f917700e37aba44b539482248718726618 Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Wed, 11 Jun 2025 22:04:35 +0000 Subject: [PATCH 6/8] Added healthcheck --- api/app.py | 2 ++ api/tech_views.py | 20 ++++++++++++++++++++ api/utils.py | 3 +++ docker-compose.yml | 15 +++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 api/tech_views.py diff --git a/api/app.py b/api/app.py index 8ca7625..051bce1 100644 --- a/api/app.py +++ b/api/app.py @@ -4,6 +4,7 @@ from flask_jwt_extended import JWTManager from jwt import ExpiredSignatureError from models import db, RevokedToken import os +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) 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 2a1261e..3d51eb3 100644 --- a/api/utils.py +++ b/api/utils.py @@ -7,6 +7,7 @@ from sqlalchemy.exc import DatabaseError 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." @@ -33,10 +34,12 @@ def get_user_or_404(user_id): 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: time.sleep(3) 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 From 301cf5922e247f23f48ee266da0ca8b80e101314 Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Wed, 11 Jun 2025 22:15:37 +0000 Subject: [PATCH 7/8] Changed docker image base to Alpine and added curl --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index aafa859..396a672 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ -FROM python:3.11.7-slim-bookworm +FROM python:3.11.7-alpine WORKDIR /app COPY api . +RUN apk add --no-cache curl RUN pip install -r requirements.txt CMD python3 app.py From cd4ab3fd27b3ce91575e487c450a95737ea99351 Mon Sep 17 00:00:00 2001 From: Marcin-Ramotowski Date: Thu, 12 Jun 2025 18:42:07 +0000 Subject: [PATCH 8/8] Handled more errors during db initialization --- api/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/utils.py b/api/utils.py index 3d51eb3..2f5d69d 100644 --- a/api/utils.py +++ b/api/utils.py @@ -3,7 +3,7 @@ from flask_jwt_extended import get_jwt_identity from models import User, db import os from sqlalchemy import text -from sqlalchemy.exc import DatabaseError +from sqlalchemy.exc import DatabaseError, InterfaceError import time from werkzeug.security import generate_password_hash @@ -41,7 +41,7 @@ def wait_for_db(max_retries): connection.execute(text("SELECT 1")) db_ready = True return - except DatabaseError: + except DatabaseError | InterfaceError: time.sleep(3) raise Exception("Failed to connect to database.")