#!/usr/bin/env bash # docker_offline_tool.sh # Assistant interactif (whiptail) pour préparer un package Docker + docker-compose + images + dockerfiles pour usage hors-ligne. # Usage: chmod +x docker_offline_tool.sh && ./docker_offline_tool.sh set -euo pipefail # ---------- Configuration ---------- WORKDIR="${PWD}/docker_offline_package" TMPDIR="${WORKDIR}/tmp" OUT_TARBALL="${PWD}/docker_offline_bundle_$(date +%Y%m%d_%H%M%S).tar.gz" WHIPTAIL="$(command -v whiptail || true)" CURL="$(command -v curl || true)" TAR="$(command -v tar || true)" DOCKER_CMD="$(command -v docker || true)" mkdir -p "$WORKDIR" "$TMPDIR" "$WORKDIR/images" "$WORKDIR/dockerfiles" "$WORKDIR/docker_binaries" if [ -z "$WHIPTAIL" ]; then echo "Le paquet 'whiptail' est requis. Installez-le (ex: apt install whiptail) puis relancez." exit 1 fi if [ -z "$CURL" ] || [ -z "$TAR" ]; then "$WHIPTAIL" --msgbox "Le script requiert 'curl' et 'tar'." 10 60 exit 1 fi # ---------- Helpers ---------- info_box() { "$WHIPTAIL" --title "${2:-Info}" --msgbox "$1" 12 70; } error_box() { "$WHIPTAIL" --title "Erreur" --msgbox "$1" 12 70; } detect_arch() { arch=$(uname -m) case "$arch" in x86_64) echo "x86_64";; aarch64|arm64) echo "aarch64";; armv7l|armv7) echo "armv7";; ppc64le) echo "ppc64le";; s390x) echo "s390x";; *) echo "$arch";; esac } # ---------- Dockerfile templates ---------- create_dockerfile_menu() { CHOICE=$("$WHIPTAIL" --title "Dockerfile" --menu "Choisir un template ou créer personnalisé" 18 70 8 \ 1 "nginx (site statique)" \ 2 "node (express sample)" \ 3 "python (wheels offline)" \ 4 "java (jar)" \ 5 "personnalisé" 3>&1 1>&2 2>&3) || return DST="$WORKDIR/dockerfiles/$(date +%Y%m%d_%H%M%S)_$CHOICE" mkdir -p "$DST" case "$CHOICE" in 1) cat >"$DST/Dockerfile" <<'EOF' FROM nginx:stable-alpine COPY ./html /usr/share/nginx/html:ro EXPOSE 80 CMD ["nginx","-g","daemon off;"] EOF ;; 2) cat >"$DST/Dockerfile" <<'EOF' FROM node:18-alpine WORKDIR /app COPY package*.json ./ # place npm tarballs or node_modules in ./npm_offline for offline use RUN npm ci --production || true COPY . . EXPOSE 3000 CMD ["node","server.js"] EOF ;; 3) cat >"$DST/Dockerfile" <<'EOF' FROM python:3.11-slim WORKDIR /app # place wheels in ./wheels COPY ./wheels ./wheels COPY requirements.txt . RUN pip install --no-index --find-links=./wheels -r requirements.txt COPY . . CMD ["python","app.py"] EOF ;; 4) cat >"$DST/Dockerfile" <<'EOF' FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY app.jar ./app.jar EXPOSE 8080 CMD ["java","-jar","/app/app.jar"] EOF ;; 5) BASE=$("$WHIPTAIL" --inputbox "Image de base (ex: debian:bookworm)" 10 60 "debian:bookworm" 3>&1 1>&2 2>&3) RUNS=$("$WHIPTAIL" --inputbox "Commandes RUN (séparées par &&) - ex: apt update && apt install -y curl" 12 60 "apt update && apt install -y curl" 3>&1 1>&2 2>&3) START=$("$WHIPTAIL" --inputbox "Commande de démarrage (CMD) - ex: /bin/bash" 8 60 "/bin/bash" 3>&1 1>&2 2>&3) cat >"$DST/Dockerfile" <&1 1>&2 2>&3) || return # collect Dockerfile choice DF_CHOICES=() for d in "$WORKDIR"/dockerfiles/*; do [ -d "$d" ] || continue DF_CHOICES+=("$d" "$(basename "$d")") done DF_CHOICES+=("Autre" "Choisir un dossier manuellement") DF_SELECTED=$("$WHIPTAIL" --title "Sélection Dockerfile" --menu "Choisir" 18 70 12 "${DF_CHOICES[@]}" 3>&1 1>&2 2>&3) || return if [ "$DF_SELECTED" = "Autre" ]; then DF_SELECTED=$("$WHIPTAIL" --fselect "$PWD/" 20 70 3>&1 1>&2 2>&3) || return fi if [ ! -f "$DF_SELECTED/Dockerfile" ]; then error_box "Aucun Dockerfile trouvé dans $DF_SELECTED" return fi IMAGE_NAME=$("$WHIPTAIL" --inputbox "Nom de l'image (ex: myapp:offline)" 10 60 "myapp:offline" 3>&1 1>&2 2>&3) || return # include extra dependencies (local or URL) if "$WHIPTAIL" --yesno "Inclure des fichiers/dépendances dans le contexte (wheels, npm tarballs, .deb/.rpm, binaires) ?" 10 60; then EXTRA_FILES=() while true; do entry=$("$WHIPTAIL" --inputbox "Chemin local ou URL (laisser vide pour terminer)" 10 70 "" 3>&1 1>&2 2>&3) || break [ -z "$entry" ] && break EXTRA_FILES+=("$entry") done else EXTRA_FILES=() fi # prepare context CONTEXT_DIR="$TMPDIR/build_ctx_$(date +%s)" rm -rf "$CONTEXT_DIR" mkdir -p "$CONTEXT_DIR" cp -r "$DF_SELECTED"/* "$CONTEXT_DIR/" 2>/dev/null || true # download/copy extras with gauge for f in "${EXTRA_FILES[@]:-}"; do if [[ "$f" =~ ^https?:// ]]; then BASENAME="$(basename "$f")" # download with curl and show progress in gauge by polling bytes TEMP_DL="$CONTEXT_DIR/${BASENAME}" # Use curl --progress-bar and parse percentages - simpler to show a small gauge animation ( echo "0"; echo "# Téléchargement: $BASENAME" if ! curl -L --fail -o "$TEMP_DL" "$f" 2>/tmp/curl_err; then echo "0"; echo "# Erreur téléchargement: $BASENAME (voir /tmp/curl_err)"; sleep 1 else echo "100"; echo "# Téléchargement terminé: $BASENAME" fi ) | "$WHIPTAIL" --gauge "Téléchargement..." 10 70 0 else if [ -e "$f" ]; then cp -r "$f" "$CONTEXT_DIR/" || true else (echo "0"; echo "# Fichier introuvable: $f") | "$WHIPTAIL" --gauge "Attention" 8 60 0 fi fi done # check docker present for local build if [ -z "$DOCKER_CMD" ]; then if ! "$WHIPTAIL" --yesno "docker n'est pas installé sur la machine préparatrice. Voulez-vous seulement préparer le contexte et sauvegarder (sans builder) ?" 10 70; then info_box "Le build local nécessite docker. Installez docker ou choisissez 'préparer seulement'." return else # just prepare context and return info_box "Contexte préparé dans $CONTEXT_DIR. Vous pouvez transférer ce dossier vers une machine avec docker pour build." return fi fi LOG="$TMPDIR/docker_build_$(date +%s).log" : >"$LOG" # run docker build in background # capture build progress by writing to logfile ( echo "=== Build started: $(date) ===" >>"$LOG" if ! docker build -t "$IMAGE_NAME" "$CONTEXT_DIR" >>"$LOG" 2>&1; then echo "=== Build FAILED at $(date) ===" >>"$LOG" exit 2 fi echo "=== Build success: $(date) ===" >>"$LOG" echo "=== Saving image to tar ===" >>"$LOG" mkdir -p "$WORKDIR/images" if ! docker save -o "$WORKDIR/images/$(echo "$IMAGE_NAME" | tr '/:' '__').tar" "$IMAGE_NAME" >>"$LOG" 2>&1; then echo "=== Save FAILED: $(date) ===" >>"$LOG" exit 3 fi echo "=== Image saved ===" >>"$LOG" ) & BUILD_PID=$! # show progress or console if [ "$DISPLAY" = "progress" ]; then # staged progress: 0->70 build, 70->95 save, 95->100 finalize pct=0 while kill -0 "$BUILD_PID" 2>/dev/null; do # estimate: if logfile contains "Step X/X", try to infer progress steps_done=$(grep -oE "Step [0-9]+/[0-9]+" "$LOG" | tail -n1 | grep -oE "[0-9]+/[0-9]+" || true) if [ -n "$steps_done" ]; then cur=$(echo "$steps_done" | cut -d/ -f1) tot=$(echo "$steps_done" | cut -d/ -f2) # scale to 0..70 pct=$(( 1 + (cur * 69 / (tot == 0 ? 1 : tot)) )) else # fallback: increase slowly to show activity pct=$(( (pct + 3) % 65 + 1 )) fi # show last lines as message tail_msg=$(tail -n 6 "$LOG" 2>/dev/null || true) { echo "$pct" echo "# Build en cours..." echo "$tail_msg" } | "$WHIPTAIL" --gauge "Construction: $IMAGE_NAME" 15 70 "$pct" sleep 1 done # After process ends, show final gauge to 95..100 while saving maybe wait "$BUILD_PID" || RC=$? || true # finalize { echo "95"; echo "# Finalisation..." sleep 1 echo "100"; echo "# Terminé" } | "$WHIPTAIL" --gauge "Finalisation..." 8 60 0 # show full log "$WHIPTAIL" --title "Log build" --textbox "$LOG" 25 90 else # console mode: show last lines live inside a repeated tailbox (keeps whiptail visible) # We'll show a live-updating gauge that contains last lines (works similarly to console). while kill -0 "$BUILD_PID" 2>/dev/null; do tail_msg=$(tail -n 20 "$LOG" 2>/dev/null || true) # compute a visual percentage heuristic: presence of "Saving" or "Successfully built" if grep -q "Saving" "$LOG" 2>/dev/null || grep -q "Image is up to date" "$LOG" 2>/dev/null; then pct=85 elif grep -q "Successfully built" "$LOG" 2>/dev/null || grep -q "BUILD SUCCESS" "$LOG" 2>/dev/null; then pct=70 else pct=30 fi { echo "$pct" echo "# Logs (mise à jour automatique):" echo "$tail_msg" } | "$WHIPTAIL" --gauge "Build live: $IMAGE_NAME (mode console)" 20 90 "$pct" sleep 1 done wait "$BUILD_PID" || true "$WHIPTAIL" --title "Log build complet" --textbox "$LOG" 25 90 fi # check final result if grep -q "Build FAILED" "$LOG" 2>/dev/null || grep -q "FAILED" "$LOG" 2>/dev/null; then error_box "Le build a échoué. Consultez le log complet." else info_box "Image buildée et sauvegardée dans : $WORKDIR/images" fi } # ---------- Prepare offline docker + compose package ---------- prepare_docker_offline_installer() { arch_default=$(detect_arch) ARCH=$("$WHIPTAIL" --inputbox "Architecture cible (détectée: $arch_default)" 10 60 "$arch_default" 3>&1 1>&2 2>&3) || return # ask Docker static version or latest DOCKER_VER=$("$WHIPTAIL" --inputbox "Version Docker static (laisser vide pour latest disponible sur download.docker.com)" 10 70 "" 3>&1 1>&2 2>&3) || return BIN_DIR="$WORKDIR/docker_binaries" mkdir -p "$BIN_DIR" # fetch docker static tarball index_url="https://download.docker.com/linux/static/stable/${ARCH}/" if [ -z "$DOCKER_VER" ]; then # try to fetch listing and get latest docker-*.tgz listing=$($CURL -fsSL "$index_url" || true) # find docker-.tgz - fallback to first match docker_tgz=$(echo "$listing" | grep -oE 'docker-[0-9]+\.[0-9]+\.[0-9]+\.tgz' | sort -V | tail -n1 || true) if [ -z "$docker_tgz" ]; then # fallback generic error_box "Impossible de détecter automatiquement la version Docker sur $index_url. Indique manuellement le nom du fichier ou une version." DOCKER_VER=$("$WHIPTAIL" --inputbox "Spécifie le nom exact du tarball (ex: docker-24.0.5.tgz)" 10 70 "" 3>&1 1>&2 2>&3) || return docker_tgz="$DOCKER_VER" fi else docker_tgz="$DOCKER_VER" fi docker_url="${index_url}${docker_tgz}" # download with progress gauge ( echo "0"; echo "# Téléchargement $docker_tgz ..." if $CURL -fSL -o "$BIN_DIR/$docker_tgz" "$docker_url" 2>/tmp/docker_dl_err; then echo "80"; echo "# Téléchargement terminé" else echo "0"; echo "# Erreur téléchargement: $docker_url (voir /tmp/docker_dl_err)" "$WHIPTAIL" --msgbox "Erreur téléchargement Docker static. Vérifiez l'URL: $docker_url\nVoir /tmp/docker_dl_err" 12 70 return fi echo "90"; echo "# Extraction test..." sleep 1 echo "100"; echo "# OK" ) | "$WHIPTAIL" --gauge "Téléchargement des binaires Docker..." 12 70 0 # docker-compose plugin (CLI) - ask version or latest COMPOSE_VER=$("$WHIPTAIL" --inputbox "Version docker-compose plugin (ex: v2.24.1) - laisser vide pour latest" 10 70 "" 3>&1 1>&2 2>&3) || return if [ -z "$COMPOSE_VER" ]; then # query GitHub API for latest release assets api_json=$($CURL -fsSL "https://api.github.com/repos/docker/compose/releases/latest" || true) compose_asset=$( echo "$api_json" | grep -oP '"browser_download_url":\s*"\K[^"]*' | grep "docker-compose-linux-${ARCH}" | head -n1 || true ) if [ -z "$compose_asset" ]; then # try generic naming compose_asset=$( echo "$api_json" | grep -oP '"browser_download_url":\s*"\K[^"]*' | grep "docker-compose-linux" | head -n1 || true ) fi else # build download url case "$ARCH" in x86_64) arch_name="x86_64";; aarch64) arch_name="aarch64";; armv7) arch_name="armv7";; *) arch_name="$ARCH";; esac compose_asset="https://github.com/docker/compose/releases/download/${COMPOSE_VER}/docker-compose-linux-${arch_name}" fi if [ -z "$compose_asset" ]; then error_box "Impossible de déterminer l'asset docker-compose à télécharger. Donne une version manuellement." return fi ( echo "0"; echo "# Téléchargement docker-compose..." if $CURL -fSL -o "$BIN_DIR/docker-compose" "$compose_asset" 2>/tmp/compose_dl_err; then chmod +x "$BIN_DIR/docker-compose" echo "100"; echo "# docker-compose OK" else echo "0"; echo "# Erreur téléchargement docker-compose (voir /tmp/compose_dl_err)" "$WHIPTAIL" --msgbox "Erreur téléchargement docker-compose: $compose_asset\nVoir /tmp/compose_dl_err" 12 70 return fi ) | "$WHIPTAIL" --gauge "Téléchargement docker-compose..." 10 70 0 # create installer script for offline server INSTALLER="$WORKDIR/install_docker_offline.sh" cat >"$INSTALLER" <<'INSTALLER_EOF' #!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" # Install static docker binaries included in this package if [ "$(id -u)" -ne 0 ]; then echo "Exécutez en root: sudo ./install_docker_offline.sh" exit 1 fi echo "Installation des binaires Docker depuis le package..." # find docker-*.tgz shopt -s nullglob for t in docker_binaries/docker-*.tgz; do echo "Extraction $t ..." tar xzf "$t" -C /tmp/docker_offline_extract || true # copy binaries (best-effort) DIR=$(ls -d /tmp/docker_offline_extract/* 2>/dev/null | head -n1 || true) if [ -n "$DIR" ]; then cp "$DIR"/* /usr/bin/ 2>/dev/null || true chmod +x /usr/bin/docker* /usr/bin/dockerd* /usr/bin/containerd* 2>/dev/null || true fi done # install docker-compose plugin if present if [ -f docker_binaries/docker-compose ]; then mkdir -p /usr/libexec/docker/cli-plugins 2>/dev/null || true cp docker_binaries/docker-compose /usr/libexec/docker/cli-plugins/docker-compose 2>/dev/null || true chmod +x /usr/libexec/docker/cli-plugins/docker-compose 2>/dev/null || true fi # Load saved images if [ -d images ]; then for img in images/*.tar; do [ -f "$img" ] || continue echo "Chargement image $img ..." docker load -i "$img" || true done fi echo "Installation offline terminée. Vérifiez 'docker --version' et 'docker compose version'." INSTALLER_EOF chmod +x "$INSTALLER" # assemble lightweight package structure PKG_DIR="$WORKDIR/offline_bundle" rm -rf "$PKG_DIR" mkdir -p "$PKG_DIR/docker_binaries" "$PKG_DIR/images" "$PKG_DIR/dockerfiles" cp -a "$WORKDIR/docker_binaries/"* "$PKG_DIR/docker_binaries/" 2>/dev/null || true cp -a "$WORKDIR/images/"* "$PKG_DIR/images/" 2>/dev/null || true cp -a "$WORKDIR/dockerfiles/"* "$PKG_DIR/dockerfiles/" 2>/dev/null || true cp "$INSTALLER" "$PKG_DIR/" # create tarball ( echo "0"; echo "# Compression du bundle..." sleep 1 tar czf "$OUT_TARBALL" -C "$PKG_DIR" . echo "100"; echo "# Bundle créé" ) | "$WHIPTAIL" --gauge "Création du bundle offline..." 10 70 0 info_box "Bundle prêt" "Bundle créé : $OUT_TARBALL\nTransférez-le sur la machine hors-ligne et exécutez install_docker_offline.sh en root." } # ---------- Assemble full bundle (if needed) ---------- assemble_bundle() { PKG_DIR="$WORKDIR/offline_bundle" if [ ! -d "$PKG_DIR" ]; then error_box "Aucun bundle préparé. Lancez d'abord la préparation (Préparer Docker offline)." return fi tar czf "$OUT_TARBALL" -C "$PKG_DIR" . info_box "Archive créée" "Archive: $OUT_TARBALL" } # ---------- Menu principal ---------- main_menu() { while true; do CHOICE=$("$WHIPTAIL" --title "Docker Offline Tool" --menu "Que voulez-vous faire ?" 18 70 10 \ 1 "Créer / builder image Docker (préparer offline)" \ 2 "Préparer package d'installation Docker + Compose (offline)" \ 3 "Créer Dockerfile (templates ou perso)" \ 4 "Assembler bundle final (.tar.gz)" \ 5 "Quitter" 3>&1 1>&2 2>&3) || break case "$CHOICE" in 1) build_image_prepare_offline ;; 2) prepare_docker_offline_installer ;; 3) create_dockerfile_menu ;; 4) assemble_bundle ;; 5) break ;; *) error_box "Choix invalide" ;; esac done } main_menu