diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c975cbd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,56 @@ +# ── Python ── +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ +.eggs/ +*.whl + +# ── Environnement virtuel ── +venv/ +env/ +.venv/ +.env +.env.* +!.env.example + +# ── Git ── +.git/ +.gitignore +.gitattributes + +# ── IDE ── +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# ── Node (imago-admin) ── +imago-admin/node_modules/ +imago-admin/dist/ + +# ── Données locales ── +data/ +*.db +*.sqlite + +# ── Tests & CI ── +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +htmlcov/ +.coverage +*.log + +# ── Docker ── +docker-compose.override.yml +docker-compose.production.yml + +# ── OS ── +.DS_Store +Thumbs.db diff --git a/docker/build-img.ps1 b/docker/build-img.ps1 new file mode 100644 index 0000000..b13bc7b --- /dev/null +++ b/docker/build-img.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS + Construit l'image Docker Imago backend via WSL Debian. +.DESCRIPTION + Build l'image Docker pour le backend Imago en utilisant Docker + sous WSL Debian avec BuildKit activé. +.PARAMETRES + -full : Construction complète sans cache (--no-cache). +.EXEMPLE + .\build-img.ps1 # Build avec cache + .\build-img.ps1 -full # Build complet sans cache +#> + +param( + [switch]$full +) + +$ErrorActionPreference = 'Stop' + +try { + $scriptDir = $PSScriptRoot + $projectRoot = (Get-Item (Join-Path $scriptDir '..')).FullName + + # Convertir chemin Windows vers WSL + $wslProjectRoot = $projectRoot -replace '^([A-Za-z]):\\', '/mnt/$1/' -replace '\\', '/' + $wslProjectRoot = $wslProjectRoot.ToLower() + + # Vérifier Docker dans WSL Debian + Write-Host "Vérification de Docker dans WSL Debian..." -ForegroundColor Cyan + $dockerCheck = wsl -d Debian bash -c "docker --version" 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Docker n'est pas disponible dans WSL Debian" + exit 1 + } + Write-Host "Docker détecté" -ForegroundColor Green + + $noCache = if ($full) { '--no-cache' } else { '' } + + # Commande de build + $cdCmd = "cd `"$wslProjectRoot`"" + $dockerCmd = "docker build $noCache --progress=plain -t imago-backend:latest -f Dockerfile ." + $innerCmd = "export DOCKER_BUILDKIT=1 && $cdCmd && $dockerCmd" + + Write-Host "Construction de l'image imago-backend:latest..." -ForegroundColor Cyan + $buildResult = wsl -d Debian bash -lc $innerCmd 2>&1 + $buildResult | ForEach-Object { Write-Host $_ } + + if ($LASTEXITCODE -ne 0) { + Write-Error "Erreur lors de la construction de l'image (code: $LASTEXITCODE)" + exit 1 + } + + Write-Host "Image imago-backend:latest construite avec succès." -ForegroundColor Green +} +catch { + Write-Error "Erreur : $_" + exit 1 +} diff --git a/docker/deploy-img.ps1 b/docker/deploy-img.ps1 new file mode 100644 index 0000000..c374cec --- /dev/null +++ b/docker/deploy-img.ps1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS + Exécute deploy-img.sh dans WSL Debian pour pousser Imago vers le registre. +.DESCRIPTION + Lance le script Bash deploy-img.sh sous WSL Debian qui tague et pousse + l'image vers dev.lab.home:5000 avec versioning semver. +.EXEMPLE + .\deploy-img.ps1 +#> + +param() + +$ErrorActionPreference = 'Stop' + +try { + $scriptDir = $PSScriptRoot + $wslScriptDir = $scriptDir -replace '^([A-Za-z]):\\', '/mnt/$1/' -replace '\\', '/' + $wslScriptDir = $wslScriptDir.ToLower() + + Write-Host "Déploiement de l'image Imago vers le registre..." -ForegroundColor Cyan + + # Rendre le script exécutable + wsl -d Debian -- chmod +x "$wslScriptDir/deploy-img.sh" + + # Exécuter + $innerCmd = "cd '$wslScriptDir' && ./deploy-img.sh" + wsl -d Debian bash -lc $innerCmd + + if ($LASTEXITCODE -ne 0) { + throw "Le déploiement a échoué (code: $LASTEXITCODE)" + } + + Write-Host "Déploiement terminé avec succès." -ForegroundColor Green +} +catch { + Write-Error "Erreur : $_" + exit 1 +} diff --git a/docker/deploy-img.sh b/docker/deploy-img.sh new file mode 100644 index 0000000..5397ec7 --- /dev/null +++ b/docker/deploy-img.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# Script: deploy-img.sh +# Description: Pousse l'image Docker Imago vers le registre HTTP local, +# gère le versioning semver et la rétention. +# Adapté de: homelab-automation-api/docker/deploy-img.sh + +set -euo pipefail + +##################################### +# CONFIG # +##################################### +IMAGE_NAME="imago-backend" +REGISTRY_HOST="dev.lab.home" +REGISTRY_PORT="5000" +MAX_VERSIONS=5 +LOCAL_TAG="latest" +PUSH_LATEST="yes" + +##################################### +# DÉDUCTIONS # +##################################### +REGISTRY="${REGISTRY_HOST}:${REGISTRY_PORT}" +REMOTE_REPO="${REGISTRY}/${IMAGE_NAME}" +LOCAL_IMAGE="${IMAGE_NAME}:${LOCAL_TAG}" +TEMP_DIR="/tmp/imago-deploy-$(date +%s)" +CURL="curl -fsSL" +JQ_BIN="${JQ_BIN:-jq}" +ACCEPT_MANIFEST='application/vnd.docker.distribution.manifest.v2+json' + +##################################### +# UTILITAIRES # +##################################### +info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } +warn() { echo -e "\033[1;33m[AVERT]\033[0m $*"; } +error() { echo -e "\033[1;31m[ERREUR]\033[0m $*" >&2; } +success() { echo -e "\033[1;32m[SUCCÈS]\033[0m $*"; } + +cleanup() { + set +e + [[ -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR" +} +trap cleanup EXIT + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { error "Commande requise manquante: $1"; exit 1; } +} + +##################################### +# PRÉ-VÉRIFICATIONS # +##################################### +require_cmd docker +require_cmd curl +require_cmd sed +require_cmd grep +require_cmd awk +require_cmd sort + +info "Vérification de l'image locale '${LOCAL_IMAGE}'…" +if ! docker image inspect "${LOCAL_IMAGE}" >/dev/null 2>&1; then + error "L'image locale ${LOCAL_IMAGE} n'existe pas. Lance build-img.ps1 d'abord." + exit 1 +fi + +info "Vérification du registre (http://${REGISTRY}/v2/)…" +if ! ${CURL} "http://${REGISTRY}/v2/" >/dev/null 2>&1; then + error "Registre injoignable à http://${REGISTRY}/v2/" + exit 1 +fi + +mkdir -p "${TEMP_DIR}" + +##################################### +# RÉCUPÉRATION DES TAGS # +##################################### +get_all_tags() { + local tags_json + if ! tags_json="$(${CURL} "http://${REGISTRY}/v2/${IMAGE_NAME}/tags/list" 2>/dev/null)"; then + return 0 + fi + if command -v "${JQ_BIN}" >/dev/null 2>&1; then + echo "${tags_json}" | ${JQ_BIN} -r '.tags[]?' 2>/dev/null || true + else + echo "${tags_json}" \ + | tr -d '\n' \ + | sed -n 's/.*"tags":[[]\([^]]*\)[]].*/\1/p' \ + | tr -d '"' \ + | tr ',' '\n' \ + | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' + fi +} + +filter_semver() { + grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' || true +} + +increment_patch() { + local tag="$1" + local major minor patch + IFS='.' read -r major minor patch <<< "${tag}" + echo "${major}.${minor}.$((patch+1))" +} + +info "Récupération des tags existants…" +ALL_TAGS="$(get_all_tags || true)" +SEMVER_TAGS="$(printf '%s\n' "${ALL_TAGS}" | filter_semver | sort -V || true)" + +if [[ -z "${SEMVER_TAGS}" ]]; then + NEW_TAG="2.0.0" +else + LATEST_TAG="$(printf '%s\n' "${SEMVER_TAGS}" | tail -n1)" + NEW_TAG="$(increment_patch "${LATEST_TAG}")" +fi +info "Tag retenu : ${NEW_TAG}" + +##################################### +# TAG + PUSH # +##################################### +info "Taggage ${LOCAL_IMAGE} → ${REMOTE_REPO}:${NEW_TAG}" +docker tag "${LOCAL_IMAGE}" "${REMOTE_REPO}:${NEW_TAG}" + +if [[ "${PUSH_LATEST}" == "yes" ]]; then + info "Taggage ${LOCAL_IMAGE} → ${REMOTE_REPO}:latest" + docker tag "${LOCAL_IMAGE}" "${REMOTE_REPO}:latest" +fi + +info "Push ${REMOTE_REPO}:${NEW_TAG}…" +docker push --disable-content-trust "${REMOTE_REPO}:${NEW_TAG}" + +if [[ "${PUSH_LATEST}" == "yes" ]]; then + info "Push ${REMOTE_REPO}:latest…" + docker push --disable-content-trust "${REMOTE_REPO}:latest" +fi + +##################################### +# RÉTENTION # +##################################### +delete_by_tag() { + local tag="$1" + local digest + digest="$(curl -fsSI -H "Accept: ${ACCEPT_MANIFEST}" "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${tag}" \ + | tr -d '\r' \ + | awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}' \ + | tail -n1)" + if [[ -z "${digest}" ]]; then + warn "Digest introuvable pour ${tag}. Skip." + return 0 + fi + info "Suppression ${tag} (digest: ${digest})…" + curl -fsS -X DELETE "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${digest}" >/dev/null || true +} + +ALL_TAGS="$(get_all_tags || true)" +SEMVER_TAGS="$(printf '%s\n' "${ALL_TAGS}" | filter_semver | sort -V || true)" + +if [[ -n "${SEMVER_TAGS}" ]]; then + COUNT="$(printf '%s\n' "${SEMVER_TAGS}" | wc -l | awk '{print $1}')" + if (( COUNT > MAX_VERSIONS )); then + TO_DELETE_COUNT=$(( COUNT - MAX_VERSIONS )) + OLDEST="$(printf '%s\n' "${SEMVER_TAGS}" | head -n "${TO_DELETE_COUNT}")" + while IFS= read -r old_tag; do + [[ -z "${old_tag}" ]] && continue + [[ "${old_tag}" == "${NEW_TAG}" ]] && continue + delete_by_tag "${old_tag}" + done <<< "${OLDEST}" + fi +fi + +##################################### +# NETTOYAGE LOCAL # +##################################### +info "Nettoyage tags locaux temporaires…" +set +e +docker rmi "${REMOTE_REPO}:${NEW_TAG}" >/dev/null 2>&1 +[[ "${PUSH_LATEST}" == "yes" ]] && docker rmi "${REMOTE_REPO}:latest" >/dev/null 2>&1 +set -e + +success "Déploiement terminé. Version publiée : ${NEW_TAG}" +success "Image disponible : ${REMOTE_REPO}:${NEW_TAG}" +success " : ${REMOTE_REPO}:latest" +exit 0