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="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
|