241 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
			
		
		
	
	
			241 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
| #!/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="obsiviewer-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
 |