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
 |