29 Commits

Author SHA1 Message Date
6ed3b50110 Changed decimal separator to comma 2025-08-07 19:51:01 +02:00
c1b07499c2 Made more space about bars to place numbers 2025-08-06 23:35:28 +02:00
46079135b7 Added more colors; added values above the bars 2025-08-06 23:30:24 +02:00
72e38fab08 Added script to generate charts with mean values 2025-08-06 23:22:15 +02:00
e6c68f839a Added script to trigger and measure deployment time 2025-07-20 09:54:38 +02:00
fcb93a5cf8 Added build args to Dockerfile 2025-07-20 09:54:31 +02:00
c9b1dac864 Added endpoint to fetch app version 2025-07-03 22:47:21 +02:00
cd4ab3fd27 Handled more errors during db initialization 2025-06-12 18:42:07 +00:00
301cf5922e Changed docker image base to Alpine and added curl 2025-06-11 22:15:37 +00:00
479ec4f917 Added healthcheck 2025-06-11 22:04:35 +00:00
3f40a6126c Added more descriptions of functions 2025-06-11 20:04:04 +00:00
dd9e9ce110 Improved function body 2025-06-11 19:57:15 +00:00
d3d3c98f99 Moved wait_for_db function to utils module 2025-06-11 19:48:58 +00:00
9e010ed389 Implemented waiting for db readiness 2025-06-11 19:43:47 +00:00
636a382cf5 Deleted jenkins pipeline from main branch 2025-06-11 17:13:27 +00:00
76a351710f Added variable APP_PORT to customize application port 2025-05-04 16:42:43 +00:00
c1f0da4a9c Extended Goss sleep 2025-05-04 15:37:47 +00:00
eefc952ff0 Updated app port in Goss YAML config 2025-05-04 15:26:21 +00:00
8c35b3bd8c Changed development server to production 2025-05-04 15:23:05 +00:00
60011b1c72 Added GOSS_SLEEP flag to wait for container full start before tests 2025-05-04 11:03:12 +00:00
859a962c12 Corrected python command name in Goss config YAML 2025-05-04 11:02:09 +00:00
0e9df4f859 Corrected command to run tests in Goss 2025-05-04 10:14:36 +00:00
1554404657 Corrected port to check in Goss YAML config 2025-05-04 10:14:18 +00:00
925af7d314 Corrected commands to test python app 2025-05-04 06:55:10 +00:00
fb260a0f6d Corrected directory in jenkins pipeline 2025-05-03 20:21:02 +00:00
dcd9a39b46 Corrected shell commands in jenkins pipeline 2025-05-03 20:05:32 +00:00
8194e3e9fe Added Jenkins pipeline to test code and container 2025-05-03 19:47:27 +00:00
0006044ae4 Added goss tests 2025-05-03 19:45:40 +00:00
74a58879ce Refactored code responsible for finding user in database 2025-04-02 20:02:34 +00:00
9 changed files with 218 additions and 12 deletions

View File

@ -1,5 +1,18 @@
FROM python:3.11.7-slim-bookworm
FROM python:3.11.7-alpine
# Wersja i data builda jako build-arg
ARG APP_VERSION=unknown
ARG BUILD_DATE=unknown
# Ustawiamy zmienne w ENV, by były dostępne w kontenerze
ENV APP_VERSION=$APP_VERSION
ENV BUILD_DATE=$BUILD_DATE
WORKDIR /app
COPY api .
RUN apk add --no-cache curl
RUN pip install -r requirements.txt
CMD python3 app.py

View File

@ -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()
@ -61,5 +64,7 @@ def create_app(config_name="default"):
# Server start only if we run app directly
if __name__ == "__main__":
from waitress import serve
app = create_app()
app.run(host="0.0.0.0")
port = os.getenv("APP_PORT", "80")
serve(app, host="0.0.0.0", port=port)

View File

@ -11,4 +11,5 @@ mysql-connector-python==9.2.0
python-dotenv==1.0.0
SQLAlchemy==2.0.23
typing_extensions==4.8.0
waitress==3.0.2
Werkzeug==3.0.1

20
api/tech_views.py Normal file
View File

@ -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

View File

@ -2,23 +2,50 @@ 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:
abort(403, message)
def get_user_or_404(user_id):
"Get user from database or abort 404"
user = db.session.get(User, user_id)
if user is None:
abort(404, "User not found")
return user
def wait_for_db(max_retries):
"Try to connect with database <max_retries> 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():

View File

@ -2,7 +2,8 @@ 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
from utils import admin_required, validate_access
import os
from utils import admin_required, validate_access, get_user_or_404
from werkzeug.security import check_password_hash, generate_password_hash
user_bp = Blueprint('user_bp', __name__)
@ -23,9 +24,7 @@ def get_all_users():
@jwt_required()
def get_user(user_id):
validate_access(user_id) # check if user tries to read other user account details
user = db.session.get(User, user_id)
if user is None:
abort(404, "User not found.")
user = get_user_or_404(user_id)
return jsonify(user.to_dict())
@ -59,7 +58,7 @@ def edit_user(user_id):
if request_fields != editable_fields:
abort(400, "Invalid request data structure.")
user_to_update = User.query.get_or_404(user_id)
user_to_update = get_user_or_404(user_id)
for field_name in editable_fields:
requested_value = request_data.get(field_name)
if requested_value is None:
@ -75,9 +74,7 @@ def edit_user(user_id):
@jwt_required()
def remove_user(user_id):
validate_access(user_id) # Only admin can remove other users accounts
user_to_delete = db.session.get(User, user_id)
if user_to_delete is None:
abort(404, "User not found.")
user_to_delete = get_user_or_404(user_id)
db.session.delete(user_to_delete)
db.session.commit()
return jsonify({"msg": "User removed successfully."})
@ -114,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")
})

67
data_analysis.py Normal file
View File

@ -0,0 +1,67 @@
import matplotlib.pyplot as plt # type: ignore
import os
import statistics
# Dane
data = {
'Jenkins + Jenkins': (165, 158, 217, 164, 136, 135, 147, 145, 138, 134, 137, 129, 136, 142, 125, 138, 133, 136, 128, 131),
'Jenkins + ArgoCD': (181, 111, 115, 121, 128, 105, 108, 119, 112, 109, 110, 108, 111, 106, 113, 117, 113, 120, 113, 107),
'Jenkins + FluxCD' : (167, 119, 113, 110, 102, 126, 111, 113, 118, 106, 111, 104, 101, 105, 104, 106, 102, 105, 107, 103),
'Woodpecker + Woodpecker': (340, 348, 334, 363, 350, 339, 331, 354, 357, 351, 356, 347, 354, 341, 357, 352, 368, 336, 331, 340),
'Woodpecker + ArgoCD': (355, 360, 354, 344, 318, 353, 328, 305, 331, 324, 328, 349, 337, 328, 349, 350, 344, 344, 344, 341),
'Woodpecker + FluxCD' : (326, 344, 325, 337, 343, 358, 339, 341, 335, 354, 342, 355, 345, 334, 356, 346, 338, 342, 330, 333),
'Argo Workflows + Argo-Workflows': (190, 190, 169, 211, 172, 198, 207, 192, 212, 181, 168, 199, 216, 213, 220, 209, 192, 210, 196, 165),
'Argo Workflows + ArgoCD': (145, 159, 163, 148, 169, 185, 153, 148, 139, 176, 133, 140, 161, 135, 161, 130, 139, 164, 183, 183),
'Argo Workflows + FluxCD': (161, 136, 181, 157, 141, 139, 157, 149, 151, 139, 139, 148, 152, 142, 136, 149, 160, 145, 173, 161)
}
# Wyliczenie średnich
labels = list(data.keys())
means = [statistics.mean(data[k]) for k in labels]
# Grupy indeksów do porównań
groupings = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8]
]
# Kolory z palety 'tab10'
color_palette = plt.get_cmap('tab10')
# Folder wyjściowy
output_folder = "plots"
os.makedirs(output_folder, exist_ok=True)
# Generowanie wykresów
for i, group in enumerate(groupings):
group_labels = [labels[j] for j in group]
group_means = [means[j] for j in group]
colors = [color_palette(j % 10) for j in group] # różne kolory
plt.figure()
bars = plt.bar(group_labels, group_means, color=colors)
# Oblicz maksymalną wartość, by zwiększyć wysokość osi Y
max_val = max(group_means)
plt.ylim(0, max_val * 1.15) # dodaj 15% zapasu na tekst
plt.ylabel("Średni czas wdrożenia (sek)")
plt.title(f"Porównanie średnich czasów wdrożenia")
plt.xticks(rotation=45)
# Dodanie wartości nad słupkami
for bar in bars:
yval = bar.get_height()
label = f'{yval:.1f}'.replace('.', ',') # <-- tutaj zamiana kropki na przecinek
plt.text(bar.get_x() + bar.get_width()/2.0, yval + max_val * 0.02, label,
ha='center', va='bottom', fontsize=9)
plt.tight_layout()
plt.savefig(f"{output_folder}/mean_times_{i}.png")
plt.close()
print("Wszystkie wykresy wygenerowane z dodatkowymi marginesami!")

54
deployment_timer.sh Executable file
View File

@ -0,0 +1,54 @@
#!/bin/bash
# === KONFIGURACJA ===
APP_URL="https://user-microservice.marcin00.pl/version"
MARKER_FILE="version_marker.txt"
OUTPUT_FILE="deployment_times.csv"
CHECK_INTERVAL=1 # sekundy
# === POBRANIE AKTUALNEJ WERSJI APLIKACJI ===
echo "[INFO] Pobieranie aktualnej wersji z /version..."
OLD_VERSION=$(curl -s "$APP_URL" | jq -r '.version')
if [[ -z "$OLD_VERSION" ]]; then
echo "[ERROR] Nie udało się pobrać aktualnej wersji aplikacji."
exit 1
fi
echo "[INFO] Aktualna wersja: $OLD_VERSION"
# === Modyfikacja pliku, commit i push ===
TIMESTAMP=$(date +%s)
echo "$TIMESTAMP" > "$MARKER_FILE"
git add "$MARKER_FILE"
git commit -m "Automatyczna zmiana: $TIMESTAMP"
START_TIME=$(date +%s)
echo "[INFO] Wykonuję git push..."
git push
if [[ $? -ne 0 ]]; then
echo "[ERROR] Push nie powiódł się."
exit 1
fi
echo "[INFO] Oczekiwanie na wdrożenie nowej wersji..."
# === Odpytywanie endpointa /version ===
while true; do
sleep $CHECK_INTERVAL
NEW_VERSION=$(curl -s "$APP_URL" | jq -r '.version')
if [[ "$NEW_VERSION" != "$OLD_VERSION" ]]; then
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "[INFO] Nowa wersja wdrożona: $NEW_VERSION"
echo "[INFO] Czas wdrożenia: $DURATION sekund"
echo "$START_TIME,$END_TIME,$DURATION,$OLD_VERSION,$NEW_VERSION" >> "$OUTPUT_FILE"
break
else
echo "[WAIT] Czekam... ($NEW_VERSION)"
fi
done

View File

@ -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