28 Commits

Author SHA1 Message Date
6966df2868 Automatyczna zmiana: 1754157020 2025-08-02 19:50:20 +02:00
d33e107efb Automatyczna zmiana: 1754156832 2025-08-02 19:47:13 +02:00
de360b10cc Automatyczna zmiana: 1754156664 2025-08-02 19:44:24 +02:00
e558baa265 Automatyczna zmiana: 1754156522 2025-08-02 19:42:02 +02:00
e8361548ff Automatyczna zmiana: 1754156388 2025-08-02 19:39:48 +02:00
c75e7de053 Automatyczna zmiana: 1754156224 2025-08-02 19:37:04 +02:00
157c451ba4 Automatyczna zmiana: 1754156085 2025-08-02 19:34:45 +02:00
4930bbe45c Automatyczna zmiana: 1754155919 2025-08-02 19:31:59 +02:00
c0469ca0c2 Automatyczna zmiana: 1754155776 2025-08-02 19:29:36 +02:00
77551add1e Automatyczna zmiana: 1754155638 2025-08-02 19:27:18 +02:00
9e4c747df5 Automatyczna zmiana: 1754155458 2025-08-02 19:24:18 +02:00
f28d9dd27f Automatyczna zmiana: 1754155314 2025-08-02 19:21:54 +02:00
4224a6be73 Automatyczna zmiana: 1754155162 2025-08-02 19:19:22 +02:00
f69be74a45 Automatyczna zmiana: 1754155005 2025-08-02 19:16:45 +02:00
d3908d8d66 Automatyczna zmiana: 1754154815 2025-08-02 19:13:35 +02:00
43c168aaf2 Automatyczna zmiana: 1754154641 2025-08-02 19:10:41 +02:00
fa6a653e73 Automatyczna zmiana: 1754154490 2025-08-02 19:08:10 +02:00
be606b6134 Automatyczna zmiana: 1754154324 2025-08-02 19:05:24 +02:00
2c3774ba09 Automatyczna zmiana: 1754154161 2025-08-02 19:02:41 +02:00
e623daad67 Automatyczna zmiana: 1754153922 2025-08-02 18:58:42 +02:00
49d7f8a500 Added missing dev functionalities for Docker files from dev branch 2025-08-02 16:16:05 +02:00
e32403dd86 Added execute permissions to deployment_timer script 2025-08-02 16:14:28 +02:00
64605796b2 Added development functionalities from dev branch 2025-08-02 15:54:42 +02:00
a652a0bddb Added script to trigger and measure deployment time 2025-08-02 15:32:36 +02:00
30ba18cace Corrected event source name 2025-08-02 15:11:20 +02:00
e297121e93 Added second webhook to separate deploy repo 2025-08-02 14:52:42 +02:00
9e4cc61e30 Added permissions for ui user to read workflows in argo-events namespace 2025-08-02 14:22:45 +02:00
90dcd19a46 Updated branch name 2025-08-02 14:22:19 +02:00
13 changed files with 297 additions and 10 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 WORKDIR /app
COPY api . COPY api .
RUN apk add --no-cache curl
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
CMD python3 app.py
CMD python3 app.py

View File

@ -4,7 +4,8 @@ from flask_jwt_extended import JWTManager
from jwt import ExpiredSignatureError from jwt import ExpiredSignatureError
from models import db, RevokedToken from models import db, RevokedToken
import os 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 views import user_bp
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@ -26,6 +27,7 @@ def create_app(config_name="default"):
# Blueprints registration # Blueprints registration
app.register_blueprint(user_bp) app.register_blueprint(user_bp)
app.register_blueprint(tech_bp)
# Database and JWT initialization # Database and JWT initialization
db.init_app(app) 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) # Fill database by initial values (only if we are not testing)
with app.app_context(): with app.app_context():
wait_for_db(max_retries=100)
db.create_all() db.create_all()
if config_name != "testing": if config_name != "testing":
init_db() init_db()

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,17 +2,22 @@ from flask import abort
from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import get_jwt_identity
from models import User, db from models import User, db
import os import os
from sqlalchemy import text
from sqlalchemy.exc import DatabaseError, InterfaceError
import time
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
db_ready = False
def admin_required(user_id, message='Access denied.'): def admin_required(user_id, message='Access denied.'):
"Check if common user try to make administrative action."
user = db.session.get(User, user_id) user = db.session.get(User, user_id)
if user is None or user.role != "Administrator": if user is None or user.role != "Administrator":
abort(403, message) abort(403, message)
def validate_access(owner_id, message='Access denied.'): 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_id = int(get_jwt_identity())
logged_user_role = db.session.get(User, logged_user_id).role logged_user_role = db.session.get(User, logged_user_id).role
if logged_user_role != "Administrator" and logged_user_id != owner_id: if logged_user_role != "Administrator" and logged_user_id != owner_id:
@ -27,6 +32,20 @@ def get_user_or_404(user_id):
return user 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(): def init_db():
"""Create default admin account if database is empty""" """Create default admin account if database is empty"""
with db.session.begin(): with db.session.begin():

View File

@ -2,6 +2,7 @@ from flask import Blueprint, jsonify, request, abort
from flask_jwt_extended import create_access_token, set_access_cookies, jwt_required, \ 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 verify_jwt_in_request, get_jwt_identity, unset_jwt_cookies, get_jwt
from models import db, RevokedToken, User from models import db, RevokedToken, User
import os
from utils import admin_required, validate_access, get_user_or_404 from utils import admin_required, validate_access, get_user_or_404
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
@ -110,3 +111,10 @@ def user_logout():
response = jsonify({"msg": "User logged out successfully."}) response = jsonify({"msg": "User logged out successfully."})
unset_jwt_cookies(response) unset_jwt_cookies(response)
return 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")
})

View File

@ -0,0 +1,23 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: argo-workflow-manager
namespace: argo-events
rules:
- apiGroups: ["argoproj.io"]
resources: ["workflows", "workflowtemplates", "cronworkflows"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argo-ui-user-read-access
namespace: argo-events
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: argo-workflow-manager
subjects:
- kind: ServiceAccount
name: argo-ui-user
namespace: argo

View File

@ -0,0 +1,109 @@
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: webhook-deploy
namespace: argo-events
spec:
template:
serviceAccountName: operate-workflow-sa
dependencies:
- name: gitea-push
eventSourceName: webhook
eventName: user-microservice-deploy
triggers:
- template:
name: deploy-user-microservice
k8s:
operation: create
source:
resource:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: deploy-user-microservice-
spec:
entrypoint: main
serviceAccountName: operate-workflow-sa
volumeClaimTemplates:
- metadata:
name: workspace
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 128Mi
templates:
- name: main
steps:
- - name: checkout
template: checkout
- - name: deploy
template: deploy
- name: checkout
container:
image: alpine/git
command: [sh, -c]
workingDir: /workspace
env:
- name: REPO_URL
value: https://gitea.marcin00.pl/pikram/user-microservice-deploy.git
- name: REPO_BRANCH
value: argo-deploy
args:
- |
git clone --depth 1 --branch "${REPO_BRANCH}" --single-branch "${REPO_URL}" repo
volumeMounts:
- name: workspace
mountPath: /workspace
- name: deploy
container:
image: marcin00.azurecr.io/azure-cli-kubectl:latest
command: [sh, -c]
workingDir: /workspace/repo
env:
- name: CLIENT_ID
value: "c302726f-fafb-4143-94c1-67a70975574a"
- name: CLUSTER_NAME
value: "build"
- name: RESOURCE_GROUP
value: "tst-aks-rg"
- name: DEPLOY_FILES
value: "namespace.yaml secret-store.yaml deploy.yaml ingress.yaml"
- name: DEPLOYMENT
value: "api"
- name: NAMESPACE
value: "user-microservice"
- name: HEALTHCHECK_URL
value: "https://user-microservice.marcin00.pl/health"
args:
- |
echo "===> Logging in to Azure"
az login --identity --client-id $CLIENT_ID
az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --overwrite-existing
kubelogin convert-kubeconfig -l azurecli
echo "===> Applying Kubernetes manifests"
for file in $DEPLOY_FILES; do
kubectl apply -f "$file"
done
echo "===> Waiting for deployment to complete"
kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE --timeout=60s
echo "===> Running health check"
for i in $(seq 1 120); do
if curl -sf $HEALTHCHECK_URL; then
echo "Health check OK"
exit 0
else
echo "Health check failed. Retry $i..."
sleep 5
fi
done
echo "Health check failed"
exit 1
volumeMounts:
- name: workspace
mountPath: /workspace

View File

@ -9,7 +9,7 @@ spec:
dependencies: dependencies:
- name: gitea-push - name: gitea-push
eventSourceName: webhook eventSourceName: webhook
eventName: test-hook eventName: user-microservice
triggers: triggers:
- template: - template:
name: trigger-build-workflow name: trigger-build-workflow
@ -71,7 +71,7 @@ spec:
- name: REPO_URL - name: REPO_URL
value: https://gitea.marcin00.pl/pikram/user-microservice.git value: https://gitea.marcin00.pl/pikram/user-microservice.git
- name: REPO_BRANCH - name: REPO_BRANCH
value: argo-workflow value: argo-workflows
args: args:
- | - |
git clone --depth 1 --branch "${REPO_BRANCH}" --single-branch "${REPO_URL}" repo git clone --depth 1 --branch "${REPO_BRANCH}" --single-branch "${REPO_URL}" repo

View File

@ -9,7 +9,11 @@ spec:
- port: 12000 - port: 12000
targetPort: 12000 targetPort: 12000
webhook: webhook:
test-hook: user-microservice:
endpoint: /gitea-hook endpoint: /user-microservice
method: POST method: POST
port: "12000" port: "12000"
user-microservice-deploy:
endpoint: /user-microservice-deploy
method: POST
port: "12000"

View File

@ -11,7 +11,14 @@ spec:
- host: argo-hook.marcin00.pl - host: argo-hook.marcin00.pl
http: http:
paths: paths:
- path: /gitea-hook - path: /user-microservice
pathType: Prefix
backend:
service:
name: webhook-eventsource-svc
port:
number: 12000
- path: /user-microservice-deploy
pathType: Prefix pathType: Prefix
backend: backend:
service: service:

65
deployment_timer.sh Executable file
View File

@ -0,0 +1,65 @@
#!/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 ===
WAITED=0
echo "[WAIT] Oczekiwanie na nową wersję..."
while true; do
sleep $CHECK_INTERVAL
WAITED=$((WAITED + 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))
# Nadpisujemy linię z licznikiem
printf "\r[INFO] Nowa wersja wdrożona po %d healtcheck próbach: %s\n" "$WAITED" "$NEW_VERSION"
echo "[INFO] Czas wdrożenia: $DURATION sekund"
echo "$START_TIME,$END_TIME,$DURATION,$OLD_VERSION,$NEW_VERSION" >> "$OUTPUT_FILE"
break
else
# Nadpisujemy TYLKO linię z licznikiem
printf "\r[WAIT] Czekam... wykonano %d healtcheck prób" "$WAITED"
fi
done
# Żeby kursor przeszedł do nowej linii po zakończeniu
echo ""

View File

@ -7,9 +7,24 @@ services:
build: . build: .
env_file: env_file:
- api/.env - api/.env
ports:
- 80:80
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
db: db:
container_name: db container_name: db
hostname: db hostname: db
image: mysql:latest image: mysql:latest
env_file: env_file:
- db/.env - db/.env
ports:
- 3306:3306
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5

1
version_marker.txt Normal file
View File

@ -0,0 +1 @@
1754157020