#!/usr/bin/env bash # Script: deploy-img.sh # Description: Pousse l'image Docker locale vers un registre HTTP (insecure), gère le versioning semver et la rétention # Date: 2025-04-18 # Mise à jour: 2025-09-19 - Vérification stricte du daemon (insecure-registries), compatibilité Windows/Linux set -euo pipefail ##################################### # CONFIG UTILISATEUR # ##################################### IMAGE_NAME="newtube-angular" REGISTRY_HOST="docker-registry.dev.home" REGISTRY_PORT="5000" MAX_VERSIONS=5 # nombre de versions (semver) à conserver LOCAL_TAG="latest" # tag local source PUSH_LATEST="yes" # yes/no : pousser aussi :latest ##################################### # DÉDUCTIONS & CONSTANTES # ##################################### REGISTRY="${REGISTRY_HOST}:${REGISTRY_PORT}" REMOTE_REPO="${REGISTRY}/${IMAGE_NAME}" LOCAL_IMAGE="${IMAGE_NAME}:${LOCAL_TAG}" TEMP_DIR="/tmp/docker-deploy-$(date +%s)" CURL="curl -fsSL" JQ_BIN="${JQ_BIN:-jq}" # possibilité de surcharger via env # Headers pour obtenir le digest (manifest v2) 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 is_windows_shell() { # Git Bash / MSYS / CYGWIN ou variable OS=Windows_NT if [[ "${OS:-}" == "Windows_NT" ]] || uname -s | grep -qiE 'mingw|msys|cygwin'; then return 0 fi return 1 } 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 # Vérifier l'image locale 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. Construis-la d'abord (docker build …)." exit 1 fi # Vérifier config daemon: insecure-registries info "Vérification de la config du daemon Docker (insecure-registries)…" if ! docker info >/dev/null 2>&1; then error "Impossible de contacter le daemon Docker. Est-il démarré ?" exit 1 fi if ! docker info 2>/dev/null \ | tr -d '\r' \ | awk '/Insecure Registries:/,/^$/ {print}' \ | grep -q -E "(^|[[:space:]])${REGISTRY_HOST}:${REGISTRY_PORT}([[:space:]]|$)"; then error "Le daemon Docker n'a PAS '${REGISTRY}' dans insecure-registries." if is_windows_shell; then info "Sous Windows (Docker Desktop) : Settings → Docker Engine, ajoute dans le JSON :" printf '%s\n' ' "insecure-registries": ["'"${REGISTRY}"'"]' info "Puis clique 'Apply & Restart' et relance ce script." else info "Sous Linux : édite /etc/docker/daemon.json et ajoute par ex. :" printf '%s\n' '{ "insecure-registries": ["'"${REGISTRY}"'"] }' info "Puis : sudo systemctl restart docker" fi exit 1 fi # Ping HTTP direct du registre (API v2) info "Vérification HTTP directe du registre (http://${REGISTRY}/v2/)…" if ! ${CURL} "http://${REGISTRY}/v2/" >/dev/null 2>&1; then error "Impossible de joindre http://${REGISTRY}/v2/. Le registre écoute-t-il bien en HTTP sur :${REGISTRY_PORT} ?" exit 1 fi # Auth facultative via env (DOCKER_USERNAME / DOCKER_PASSWORD) if [[ -n "${DOCKER_USERNAME:-}" && -n "${DOCKER_PASSWORD:-}" ]]; then info "Authentification au registre (docker login)…" # NB: docker login respecte le mode HTTP si daemon configuré en insecure-registries echo "${DOCKER_PASSWORD}" | docker login "${REGISTRY}" --username "${DOCKER_USERNAME}" --password-stdin >/dev/null else info "Aucun identifiant Docker fourni (DOCKER_USERNAME/DOCKER_PASSWORD). Tentative sans authentification." fi mkdir -p "${TEMP_DIR}" ##################################### # RÉCUPÉRATION DES TAGS # ##################################### get_all_tags() { # Renvoie la liste des tags (un par ligne) ou rien si vide/erreur # Utilise jq si dispo, sinon un parseur simple. 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 # fallback très simple (non robuste à tous les cas, mais suffisant ici) echo "${tags_json}" \ | tr -d '\n' \ | sed -n 's/.*"tags":[[]\([^]]*\)[]].*/\1/p' \ | tr -d '"' \ | tr ',' '\n' \ | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' fi } filter_semver() { # Garde uniquement X.Y.Z (numériques) grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' || true } increment_patch() { # Lit un tag semver X.Y.Z et augmente Z 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="1.0.0" else LATEST_TAG="$(printf '%s\n' "${SEMVER_TAGS}" | tail -n1)" NEW_TAG="$(increment_patch "${LATEST_TAG}")" fi info "Tag retenu pour cette release : ${NEW_TAG}" ##################################### # TAG + PUSH (HTTP via daemon) ##################################### info "Taggage local → ${REMOTE_REPO}:${NEW_TAG}" docker tag "${LOCAL_IMAGE}" "${REMOTE_REPO}:${NEW_TAG}" if [[ "${PUSH_LATEST}" == "yes" ]]; then info "Taggage local → ${REMOTE_REPO}:latest" docker tag "${LOCAL_IMAGE}" "${REMOTE_REPO}:latest" fi info "Push de ${REMOTE_REPO}:${NEW_TAG} (HTTP via insecure-registries)…" docker push --disable-content-trust "${REMOTE_REPO}:${NEW_TAG}" if [[ "${PUSH_LATEST}" == "yes" ]]; then info "Push de ${REMOTE_REPO}:latest…" docker push --disable-content-trust "${REMOTE_REPO}:latest" fi ##################################### # RÉTENTION: SUPPRIMER ANCIENS # ##################################### delete_by_tag() { # Supprime un manifest par son tag en récupérant le digest via HEAD local tag="$1" local digest # On demande le digest via HEAD + Accept manifest v2 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} (manifest v2 absent ?). Skip." return 0 fi info "Suppression manifest ${tag} (digest: ${digest})…" # La delete API ne renvoie rien en cas de succès (204) if ! curl -fsS -X DELETE "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${digest}" >/dev/null; then warn "Échec suppression manifest pour ${tag} (digest: ${digest})." fi } # Mettre à jour la liste après push 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 )) # On supprime les plus anciennes OLDEST="$(printf '%s\n' "${SEMVER_TAGS}" | head -n "${TO_DELETE_COUNT}")" while IFS= read -r old_tag; do [[ -z "${old_tag}" ]] && continue # Ne jamais supprimer le tag que l'on vient de pousser (sécurité) if [[ "${old_tag}" == "${NEW_TAG}" ]]; then continue fi delete_by_tag "${old_tag}" done <<< "${OLDEST}" fi fi ##################################### # NETTOYAGE LOCAL OPTIONNEL # ##################################### info "Nettoyage des 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}" exit 0