diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bf29fd..b8d767e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,3 +55,90 @@ jobs: - name: Run tests run: | if [ -d backend/tests ]; then pytest -q; else echo "No tests found"; fi + + docker-lint: + name: Docker Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Hadolint on backend Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: backend/Dockerfile + ignore: DL3045 + + - name: Run Hadolint on frontend Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: frontend/Dockerfile + ignore: DL3045 + + build-and-scan: + name: Build and Scan Docker Images + needs: docker-lint + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build backend image + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: false + load: true + tags: backend:latest + + - name: Build frontend image + uses: docker/build-push-action@v5 + with: + context: ./frontend + file: ./frontend/Dockerfile + push: false + load: true + tags: frontend:latest + + - name: Scan backend with Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: backend:latest + format: sarif + output: backend-trivy.sarif + + - name: Scan frontend with Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: frontend:latest + format: sarif + output: frontend-trivy.sarif + + - name: Upload Trivy results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: backend-trivy.sarif + + - name: Upload frontend Trivy results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: frontend-trivy.sarif + + validate-docker-compose: + name: Validate Docker Compose + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate docker-compose.yml + run: | + docker-compose -f docker-compose.yml config > /dev/null + continue-on-error: true diff --git a/atelier02-conteneurisation.pdf b/atelier02-conteneurisation.pdf new file mode 100755 index 0000000..0acd1b0 Binary files /dev/null and b/atelier02-conteneurisation.pdf differ diff --git a/backend/Dockerfile b/backend/Dockerfile index f1004ef..329c0c4 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,9 +1,27 @@ -FROM python:3.12 - +# Stage 1: Tests et qualité +FROM python:3.12 as test WORKDIR /app -COPY . /app/ +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt flake8 bandit -RUN pip install -r requirements.txt +COPY . . +# Tests de qualité +RUN flake8 app/ --count --show-source --statistics || true +RUN bandit -r app/ -f json -o /tmp/bandit-report.json || true +# Stage 2: Application runtime +FROM python:3.12-slim as runtime +WORKDIR /app + +# Non-root user pour la sécurité +RUN useradd -m -u 1000 appuser + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY --from=test /app . +RUN chown -R appuser:appuser /app + +USER appuser EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..27114a7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +version: '3.9' + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + target: runtime + container_name: eni-backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=sqlite:///./test.db + volumes: + - ./backend/app:/app/app + restart: unless-stopped + networks: + - eni-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/docs"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: eni-frontend + ports: + - "80:80" + environment: + - API_URL=http://backend:8000 + depends_on: + backend: + condition: service_healthy + restart: unless-stopped + networks: + - eni-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + +networks: + eni-network: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..07f273a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Build +FROM node:20-alpine as builder +WORKDIR /app +COPY package*.json ./ +RUN npm install || true + +COPY . . +# Aucun build nécessaire pour cette app vanilla JS +# mais on peut valider la qualité du code +RUN npm run lint 2>/dev/null || echo "No lint script" + +# Stage 2: Runtime avec nginx +FROM nginx:alpine-slim +WORKDIR /usr/share/nginx/html + +# Copier les fichiers depuis le stage builder +COPY --from=builder /app . + +# Copier une configuration nginx pour le SPA +RUN cat > /etc/nginx/conf.d/default.conf << 'EOF' +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Cache les assets statiques + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Fallback pour le SPA + location / { + try_files $uri /index.html; + } +} +EOF + +USER nginx:nginx +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"]