init
This commit is contained in:
66
src/app/Dockerfile
Normal file
66
src/app/Dockerfile
Normal file
@@ -0,0 +1,66 @@
|
||||
# ==========================
|
||||
# STAGE 1 : Construction
|
||||
# ==========================
|
||||
FROM python:3.11-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Installer les dépendances système
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Créer un environnement virtuel
|
||||
RUN python -m venv /venv
|
||||
ENV PATH="/venv/bin:$PATH"
|
||||
|
||||
# Installer les dépendances
|
||||
COPY src/app/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
# ==========================
|
||||
# STAGE 2 : Production
|
||||
# ==========================
|
||||
FROM python:3.11-slim AS runtime
|
||||
|
||||
# Définir le répertoire de travail
|
||||
WORKDIR /app
|
||||
|
||||
# Installer les bibliothèques système
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 🔥 Installe curl ici
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# Créer un utilisateur non-root
|
||||
RUN useradd -m -u 1000 devopsuser
|
||||
|
||||
# Copier l'environnement virtuel
|
||||
COPY --from=builder /venv /venv
|
||||
ENV PATH="/venv/bin:$PATH"
|
||||
|
||||
# Copier le code
|
||||
COPY src/app /app
|
||||
|
||||
# Changer les droits
|
||||
RUN chown -R devopsuser:devopsuser /app
|
||||
USER devopsuser
|
||||
|
||||
# Variables d'environnement
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV ENV=production
|
||||
ENV HOSTNAME=devops-container
|
||||
ENV PYTHONPATH="${PYTHONPATH}:/app"
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Lancer l'application
|
||||
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]
|
||||
0
src/app/__init__.py
Normal file
0
src/app/__init__.py
Normal file
11
src/app/database.py
Normal file
11
src/app/database.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
def get_db_connection():
|
||||
conn = psycopg2.connect(
|
||||
host="db",
|
||||
database="devopsdb",
|
||||
user="devopsuser",
|
||||
password="devopspass"
|
||||
)
|
||||
return conn
|
||||
153
src/app/main.py
Normal file
153
src/app/main.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# ==========================
|
||||
# IMPORTS DES DÉPENDANCES
|
||||
# ==========================
|
||||
from fastapi import FastAPI, Request, HTTPException, Response
|
||||
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
|
||||
import time
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
# Import du routeur pour /users
|
||||
from routes import router as routes_app
|
||||
|
||||
# ==========================
|
||||
# CONFIGURATION DE L'APPLICATION
|
||||
# ==========================
|
||||
# Configuration du logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Création de l'application FastAPI
|
||||
app = FastAPI(
|
||||
title="DevOps Windows API",
|
||||
description="Application de démonstration DevOps sous Windows",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Inclusion du routeur (doit être après la création de 'app')
|
||||
app.include_router(routes_app, prefix="/users", tags=["users"])
|
||||
|
||||
# ==========================
|
||||
# MÉTRIQUES PROMETHEUS
|
||||
# ==========================
|
||||
REQUEST_COUNT = Counter(
|
||||
'http_requests_total',
|
||||
'Total des requêtes HTTP',
|
||||
['method', 'endpoint', 'status']
|
||||
)
|
||||
|
||||
REQUEST_LATENCY = Histogram(
|
||||
'http_request_duration_seconds',
|
||||
'Temps de réponse HTTP',
|
||||
['method', 'endpoint']
|
||||
)
|
||||
|
||||
# ==========================
|
||||
# MIDDLEWARE DE MONITORING
|
||||
# ==========================
|
||||
@app.middleware("http")
|
||||
async def monitor_requests(request: Request, call_next):
|
||||
"""Middleware pour suivre les requêtes et mesurer la latence."""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
status_code = str(response.status_code)
|
||||
except Exception as e:
|
||||
status_code = "500"
|
||||
response = Response(
|
||||
content=f"Erreur serveur : {str(e)}",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# Enregistrement des métriques
|
||||
REQUEST_COUNT.labels(
|
||||
method=request.method,
|
||||
endpoint=request.url.path,
|
||||
status=status_code
|
||||
).inc()
|
||||
|
||||
REQUEST_LATENCY.labels(
|
||||
method=request.method,
|
||||
endpoint=request.url.path
|
||||
).observe(process_time)
|
||||
|
||||
# Ajout du temps de traitement dans les headers
|
||||
response.headers["X-Process-Time"] = f"{process_time:.3f}s"
|
||||
|
||||
return response
|
||||
|
||||
# ==========================
|
||||
# ENDPOINTS PRINCIPAUX
|
||||
# ==========================
|
||||
|
||||
@app.get("/")
|
||||
async def home():
|
||||
"""Endpoint racine - Statut de l'application."""
|
||||
return {
|
||||
"message": "🚀 DevOps Stack Windows - Fonctionnel !",
|
||||
"environment": os.getenv("ENV", "development"),
|
||||
"status": "running",
|
||||
"hostname": os.getenv("HOSTNAME", "windows-devops"),
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check pour Kubernetes."""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": time.time(),
|
||||
"service": "devops-app"
|
||||
}
|
||||
|
||||
@app.get("/metrics")
|
||||
async def metrics():
|
||||
"""Endpoint pour Prometheus."""
|
||||
try:
|
||||
data = generate_latest()
|
||||
return Response(
|
||||
content=data,
|
||||
media_type=CONTENT_TYPE_LATEST,
|
||||
headers={"Cache-Control": "no-cache"}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération des métriques : {e}")
|
||||
raise HTTPException(status_code=500, detail="Échec de la génération des métriques")
|
||||
|
||||
@app.get("/info")
|
||||
async def info():
|
||||
"""Informations système."""
|
||||
return {
|
||||
"python_version": "3.11",
|
||||
"platform": "windows",
|
||||
"service": "FastAPI DevOps",
|
||||
"features": ["docker", "kubernetes", "monitoring", "ci-cd"]
|
||||
}
|
||||
|
||||
@app.get("/env")
|
||||
async def show_env():
|
||||
"""Affiche les variables d'environnement (sécurisées)."""
|
||||
safe_env = {
|
||||
"ENV": os.getenv("ENV", "not-set"),
|
||||
"HOSTNAME": os.getenv("HOSTNAME", "not-set"),
|
||||
"PYTHON_VERSION": os.getenv("PYTHON_VERSION", "not-set")
|
||||
}
|
||||
return safe_env
|
||||
|
||||
# ==========================
|
||||
# LANCEMENT EN DÉVELOPPEMENT
|
||||
# ==========================
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
logger.info("Démarrage du serveur FastAPI...")
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
log_level="info",
|
||||
reload=True # Auto-reload en dev
|
||||
)
|
||||
8
src/app/requirements.txt
Normal file
8
src/app/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
prometheus-client==0.19.0
|
||||
python-dotenv==1.0.0
|
||||
pytest==7.4.3
|
||||
httpx==0.25.1
|
||||
psycopg2==2.9.9
|
||||
23
src/app/routes.py
Normal file
23
src/app/routes.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from database import get_db_connection
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/users")
|
||||
async def get_users():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
cursor.execute("SELECT * FROM users")
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
@router.post("/users")
|
||||
async def add_user(name: str, email: str):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s) ON CONFLICT (email) DO NOTHING", (name, email))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"status": "success"}
|
||||
44
src/app/test_main.py
Normal file
44
src/app/test_main.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def test_home_endpoint():
|
||||
"""Test de l'endpoint principal"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "message" in data
|
||||
assert "status" in data
|
||||
assert data["status"] == "running"
|
||||
|
||||
def test_health_endpoint():
|
||||
"""Test du health check"""
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
assert "service" in data
|
||||
|
||||
def test_metrics_endpoint():
|
||||
"""Test des métriques Prometheus"""
|
||||
response = client.get("/metrics")
|
||||
assert response.status_code == 200
|
||||
assert "text/plain" in response.headers["content-type"]
|
||||
assert "http_requests_total" in response.text
|
||||
|
||||
def test_info_endpoint():
|
||||
"""Test des informations"""
|
||||
response = client.get("/info")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "python_version" in data
|
||||
assert "platform" in data
|
||||
|
||||
def test_env_endpoint():
|
||||
"""Test des variables d'environnement"""
|
||||
response = client.get("/env")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "ENV" in data
|
||||
Reference in New Issue
Block a user