CI/CD
Objectifs
- Estimer son travail
- Ajouter des tests unitaires en Python
- Créer une CI/CD pipeline sur GitLab
Rendu
- Rapport individuel en PDF sur Cyberlearn
- Nom du fichier:
lab04-cicd-{nom}.pdf
- Délai: 2 semaines
- Nom du fichier:
- Mettez tout votre travail sur une branche
feature/04-cicd
et faites une merge request (MR) surmain
en m'ajoutant comme reviewer - Ajoutez un lien vers le commit dans votre rapport
Tâches
Estimer son travail
- Estimez le temps total nécessaire pour réaliser ce laboratoire
- Découpez le travail en tâches pour faciliter l'estimation
- A la fin du rapport, comparez le temps estimé avec le temps réellement passé:
Tâche Temps estimé Temps réel Commentaire ... 30m 45m ... ... ... ... ... Total 2h 1h30 ...
Tester le backend
- Ajoutez les dépendances de développement
poetry add -G dev pytest pytest-cov httpx
- Une dépendance de développement est une dépendance qui n'est pas nécessaire en production, par exemple uniquement pour les tests
pytest
est le framework de testpytest-cov
permet de générer un rapport de couverture de codehttpx
permet de faire des requêtes HTTP dans les tests
- Ajoutez/modifier les fichiers suivants (inspiré de cette documentation) :
from os import getenv
from sys import modules // [!code focus]
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from . import models, schemas
from .database import SessionLocal, engine
if "pytest" not in modules: // [!code focus]
models.Base.metadata.create_all(bind=engine) // [!code focus]
app = FastAPI(root_path=getenv("ROOT_PATH"))
...
from random import choices, uniform
from string import ascii_letters
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from backend.database import Base
from backend.main import app, get_db
DATABASE_URL = "sqlite://"
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def random_string(n=32):
return "".join(choices(ascii_letters, k=n))
def random_double():
return round(uniform(0.0, 100.0), 2)
product = {
"name": random_string(),
"description": random_string(512),
"price": random_double(),
}
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"Hello": "World"}
def test_read_empty_products():
response = client.get("/products/")
assert response.status_code == 200
assert response.json() == []
def test_create_product():
response = client.post(
"/products/",
json=product,
)
assert response.status_code == 200
assert response.json() == {"id": 1, **product}
def test_read_product():
response = client.get("/products/1")
assert response.status_code == 200
assert response.json() == {"id": 1, **product}
def test_read_products():
response = client.get("/products/")
assert response.status_code == 200
assert response.json() == [{"id": 1, **product}]
def test_delete_product():
response = client.delete("/products/1")
assert response.status_code == 200
assert response.json() == {"id": 1, **product}
response = client.get("/products/1")
assert response.status_code == 404
def test_read_deleted_empty_products():
response = client.get("/products/")
assert response.status_code == 200
assert response.json() == []
- Pour lancer les tests :
poetry run pytest --cov
GitLab CI/CD
Créez une pipeline sur GitLab CI/CD qui :
- a les 3 stages :
- build : vérifie que le code compile
- test :
- vérifie que les tests (du backend) passent
- Unit Test Reports
- Code Coverage
- Code Quality
- Dependency Scanning
- SAST
- Container Scanning
- deploy : met à jour les images Docker sur le registry
- est déclenchée à chaque push sur n'importe quelle branche
- le stage
deploy
n'est exécuté que surmain
- le stage
- Le frontend et le backend doivent être dans des jobs séparés et en parallèle
- Chacun est exécuté uniquement lorsqu'il y a des changements dans son dossier
Étapes proposées :
Vous allez devoir tester beaucoup de changements sur la pipeline, une manière d'éviter d'avoir plein de commit est d'en utiliser qu'un seul (ne pas le faire sur
main
oudevelop
!) :git commit --amend --all --no-edit && git push --force-with-lease
Commencez par le frontend (commencez vos scripts par `cd frontend/)
- Le job
build-frontend
utilise l'imagenode:lts
, exécutenpm ci
etnpm run build
- Le résultat du build est gardé dans un artifact pour être utilisé par le job
deploy-frontend
- Ajoutez le cache
- Le résultat du build est gardé dans un artifact pour être utilisé par le job
- Le job
deploy-frontend
utilise l'imagedocker
avec le servicedocker:dind
, exécutedocker build -t ${CI_REGISTRY_IMAGE}/frontend:latest .
etdocker push ${CI_REGISTRY_IMAGE}/frontend:latest
- Docker in Docker
- Docker login
echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
- Docker Layer Caching
Solution
.gitlab-ci.yml
yamlbuild-frontend: stage: build image: node:lts cache: key: files: - frontend/package-lock.json paths: - frontend/.npm/ before_script: - cd frontend/ script: - npm ci --cache .npm --prefer-offline - npm run build artifacts: paths: - frontend/dist/ deploy-frontend: stage: deploy image: docker services: - docker:dind dependencies: - build-frontend variables: REGISTRY_IMAGE: ${CI_REGISTRY_IMAGE}/frontend before_script: - cd frontend/ - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin script: - docker pull $REGISTRY_IMAGE:latest || true - docker build --cache-from $REGISTRY_IMAGE:latest -t $REGISTRY_IMAGE:latest . - docker push $REGISTRY_IMAGE:latest
- Le job
Puis le backend (similairement au frontend)
Le job
build-backend
utilise l'imagepython:3.11
, installe Poetry et les dépendances en les cachant pour les prochains jobsyamlbuild-backend: stage: build image: python:3.11 variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" cache: paths: - .cache/pip - backend/.venv/ before_script: - cd backend/ - pip install poetry - poetry config virtualenvs.in-project true script: - poetry install
Le job
test-backend
reprend le cache du jobbuild-backend
et exécutepoetry run pytest --cov
yamltest-backend: stage: test image: python:3.11 variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" cache: paths: - .cache/pip - backend/.venv/ before_script: - cd backend/ - pip install poetry - poetry config virtualenvs.in-project true script: - poetry run pytest --cov
- Ajoutez les éléments suivants
Le job
deploy-backend
est très similaire au jobdeploy-frontend
Solution
.gitlab-ci.yml
yamlbuild-backend: stage: build image: python:3.11 variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" cache: paths: - .cache/pip - backend/.venv/ before_script: - cd backend/ - pip install poetry - poetry config virtualenvs.in-project true script: - poetry install test-backend: stage: test image: python:3.11 variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" cache: paths: - .cache/pip - backend/.venv/ before_script: - cd backend/ - pip install poetry - poetry config virtualenvs.in-project true script: - poetry run pytest --cov --junitxml="rspec.xml" --cov-report term --cov-report xml:coverage.xml coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: paths: - backend/rspec.xml reports: junit: backend/rspec.xml coverage_report: coverage_format: cobertura path: backend/coverage.xml deploy-backend: stage: deploy image: docker services: - docker:dind variables: REGISTRY_IMAGE: ${CI_REGISTRY_IMAGE}/backend before_script: - cd backend/ - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin script: - docker pull $REGISTRY_IMAGE:latest || true - docker build --cache-from $REGISTRY_IMAGE:latest -t $REGISTRY_IMAGE:latest . - docker push $REGISTRY_IMAGE:latest
N'effectuez le stage
deploy
uniquement sur la branchemain
Transformez votre pipeline en Directed Acyclic Graph Pipelines
Transformez votre pipeline en Parent-child pipelines
Ajoutez et configurez