Compare commits

..

23 Commits

Author SHA1 Message Date
Marcin-Ramotowski
a79ae2d50f Apply new features from branch 'dev' into jenkins-pipeline 2025-06-12 18:42:28 +00:00
Marcin-Ramotowski
cd4ab3fd27 Handled more errors during db initialization 2025-06-12 18:42:07 +00:00
Marcin-Ramotowski
301cf5922e Changed docker image base to Alpine and added curl 2025-06-11 22:15:37 +00:00
Marcin-Ramotowski
479ec4f917 Added healthcheck 2025-06-11 22:04:35 +00:00
Marcin-Ramotowski
3f40a6126c Added more descriptions of functions 2025-06-11 20:04:04 +00:00
Marcin-Ramotowski
dd9e9ce110 Improved function body 2025-06-11 19:57:15 +00:00
Marcin-Ramotowski
d9fe927832 Removed deprecated label option from pipeline 2025-06-11 17:51:55 +00:00
Marcin-Ramotowski
99cfdfddd0 Added annotation required to run Sysbox on pod 2025-06-11 16:52:16 +00:00
Marcin-Ramotowski
f579e440f8 Updated path to pod template yaml file 2025-06-11 16:43:22 +00:00
Marcin-Ramotowski
ba69728c81 Changed pod runtime to sysbox 2025-06-11 16:32:12 +00:00
Marcin-Ramotowski
5366e313c5 Moved Jenkinsfile and pod agent template to separate directory 2025-06-11 16:29:03 +00:00
Marcin-Ramotowski
283be1a1ec Deleted Goss 2025-06-11 16:28:08 +00:00
Marcin-Ramotowski
1b7204c2ba Changed name of variable to store ACR name instead of ACR username 2025-06-10 20:17:25 +00:00
Marcin-Ramotowski
8f9aed299d Added managed identity client id 2025-06-10 19:32:40 +00:00
Marcin-Ramotowski
6522977280 Changed basic auth to managed identity 2025-06-10 18:50:37 +00:00
Marcin-Ramotowski
c707974a2e Corrected agent declaration in Jenkinsfile 2025-06-08 16:56:35 +00:00
Marcin-Ramotowski
cc2f224d60 Moved pod agent code from Jenkins master to YAML file in repo 2025-06-08 16:45:05 +00:00
Marcin-Ramotowski
b14e6cf873 Restored dind container usage 2025-06-07 21:44:24 +00:00
Marcin-Ramotowski
87e3c0df80 Removed Goss tests 2025-06-07 21:14:40 +00:00
Marcin-Ramotowski
aea09a6081 Added bash installation 2025-06-07 15:08:58 +00:00
Marcin-Ramotowski
d05cede409 The command curl is replaced with wget 2025-06-07 15:06:19 +00:00
Marcin-Ramotowski
17162027b6 Removed unnecessary post cleanup 2025-06-07 14:57:35 +00:00
Marcin-Ramotowski
8887f1b2bd Updated Jenkins pipeline to use in Kubernetes 2025-06-07 13:31:03 +00:00
7 changed files with 150 additions and 13 deletions

49
.jenkins/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,49 @@
pipeline {
agent {
kubernetes {
yamlFile '.jenkins/podTemplate.yaml'
}
}
environment {
ACR_NAME = 'marcin00'
CLIENT_ID = 'c302726f-fafb-4143-94c1-67a70975574a'
DOCKER_REGISTRY_URL = 'marcin00.azurecr.io'
DOCKER_IMAGE = "${DOCKER_REGISTRY_URL}/user-microservice:${GIT_COMMIT}"
}
stages {
stage('Code Tests') {
steps {
container('python') {
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 & Push Docker') {
steps {
container('docker') {
sh '''
docker build -t ${DOCKER_IMAGE} .
az login --identity --client-id ${CLIENT_ID}
az acr login --name ${ACR_NAME}
docker push ${DOCKER_IMAGE}
'''
}
}
}
}
}

50
.jenkins/podTemplate.yaml Normal file
View File

@ -0,0 +1,50 @@
apiVersion: v1
kind: Pod
metadata:
annotations:
io.kubernetes.cri-o.userns-mode: "auto:size=65536"
labels:
jenkins: "slave"
jenkins/label: "kubernetes-agent"
spec:
runtimeClassName: sysbox-runc
containers:
- name: jnlp
image: jenkins/inbound-agent:alpine
tty: false
workingDir: /home/jenkins/agent
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
env:
- name: JENKINS_WEB_SOCKET
value: "true"
- name: REMOTING_OPTS
value: "-noReconnectAfter 1d"
- name: python
image: python:3.11.7-alpine
command:
- cat
tty: true
workingDir: /home/jenkins/agent
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: docker
image: marcin00.azurecr.io/azure-cli-docker:slim-bookworm
tty: true
workingDir: /home/jenkins/agent
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
nodeSelector:
kubernetes.io/os: linux
restartPolicy: Never
volumes:
- name: workspace-volume
emptyDir: {}

View File

@ -1,5 +1,6 @@
FROM python:3.11.7-slim-bookworm FROM python:3.11.7-alpine
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,6 +4,7 @@ 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 tech_views import tech_bp
from utils import init_db, wait_for_db 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,7 +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() 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

@ -3,19 +3,21 @@ 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 import text
from sqlalchemy.exc import DatabaseError from sqlalchemy.exc import DatabaseError, InterfaceError
import time 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:
@ -30,20 +32,18 @@ def get_user_or_404(user_id):
return user return user
MAX_RETRIES = 100 def wait_for_db(max_retries):
"Try to connect with database <max_retries> times."
def wait_for_db(): global db_ready
for retries in range(MAX_RETRIES): for _ in range(max_retries):
try: try:
with db.engine.connect() as connection: with db.engine.connect() as connection:
connection.execute(text("SELECT 1")) connection.execute(text("SELECT 1"))
print("Successfully connected with database.") db_ready = True
return return
except DatabaseError: except DatabaseError | InterfaceError:
print(f"Waiting for database... (retry {retries + 1})")
time.sleep(3) time.sleep(3)
print("Failed to connect to database.") raise Exception("Failed to connect to database.")
raise Exception("Database not ready after multiple retries.")
def init_db(): def init_db():

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