From 2bf818aeb5e80ab3eae5237884d2565369e87df8 Mon Sep 17 00:00:00 2001 From: Foxly Date: Mon, 13 Jul 2026 11:05:34 +0200 Subject: [PATCH 1/2] Modernize Docker installer and add CI tests --- .github/workflows/ci.yml | 22 + README.md | 326 +++++----- dockerinstall.sh | 1212 +++++++++++++------------------------- tests/integration.sh | 177 ++++++ tests/mock-command.sh | 59 ++ 5 files changed, 837 insertions(+), 959 deletions(-) create mode 100644 .github/workflows/ci.yml mode change 100644 => 100755 dockerinstall.sh create mode 100755 tests/integration.sh create mode 100755 tests/mock-command.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc9c402 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + shell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install ShellCheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + - name: Check Bash syntax + run: bash -n dockerinstall.sh tests/integration.sh tests/mock-command.sh + - name: Run integration simulations + run: bash tests/integration.sh + - name: Run ShellCheck + run: shellcheck dockerinstall.sh tests/integration.sh tests/mock-command.sh diff --git a/README.md b/README.md index 91d4b39..fad7b05 100644 --- a/README.md +++ b/README.md @@ -1,237 +1,231 @@ -# 🐳 Foxly dockerinstall — Docker Installer, Upgrader & Uninstaller +# 🐳 Foxly dockerinstall -[🇩🇪 Deutsch](#-deutsch) | [🇬🇧 English](#-english) +Single-file manager for installing, upgrading, checking, backing up, and uninstalling Docker Engine on supported Debian and Ubuntu hosts. -[![License: MIT](https://img.shields.io/badge/License-MIT-informational?style=for-the-badge)](LICENSE) -[![Stars](https://img.shields.io/github/stars/foxly-it/dockerinstall?style=for-the-badge)](https://github.com/foxly-it/dockerinstall/stargazers) -[![Issues](https://img.shields.io/github/issues/foxly-it/dockerinstall?style=for-the-badge)](https://github.com/foxly-it/dockerinstall/issues) -![Bash](https://img.shields.io/badge/language-Bash-blue?style=for-the-badge) -[![Made by Foxly](https://img.shields.io/badge/made%20by-Foxly%20IT-orange?style=for-the-badge)](https://foxly.de) +[Deutsch](#deutsch) · [English](#english) · [MIT License](LICENSE) ---- +## Deutsch -## 🇩🇪 Deutsch +### Unterstützte Systeme -### 🧩 Übersicht +- Debian 11 (Bullseye), 12 (Bookworm) und 13 (Trixie) +- Ubuntu 22.04 LTS, 24.04 LTS, 25.10 und 26.04 LTS +- systemd und eine von Docker unterstützte Architektur -**dockerinstall.sh** ist ein eigenständiges **Single-File-Tool zur Verwaltung von Docker** auf Linux-Systemen. +Der Installer lehnt nicht unterstützte Distributionen und Versionen ab. Der Support-Stand orientiert sich an den offiziellen Anleitungen für [Debian](https://docs.docker.com/engine/install/debian/) und [Ubuntu](https://docs.docker.com/engine/install/ubuntu/). -Es deckt den kompletten Lebenszyklus ab: +### Eigenschaften -- Installation von Docker Engine & Plugins -- sauberes Upgrade bestehender Installationen -- vollständige Deinstallation (optional inkl. Daten) -- integrierter Self-Check und Backup-Funktion -- interaktive **und** nicht-interaktive Nutzung +- offizielles Docker-APT-Repository im aktuellen Deb822-Format (`docker.sources`) +- offizieller Docker-Signaturschlüssel (`docker.asc`) +- Docker Engine, CLI, containerd, Buildx und Compose v2 +- sichere, validierte CLI für interaktive und automatisierte Nutzung +- getrennte Logs für Installation, Self-Check und Deinstallation +- optionales Host-Backup bei angehaltenem Docker-Dienst +- Datenlöschung nur nach explizitem `--remove-data` oder Bestätigung -Kein Framework, keine Abhängigkeiten – **ein Skript, volle Kontrolle**. +> Mitglieder der Gruppe `docker` besitzen root-ähnliche Rechte. Füge nur vertrauenswürdige Benutzer hinzu. ---- +### Installation -### 🖥️ Unterstützte Systeme +```bash +git clone https://github.com/foxly-it/dockerinstall.git +cd dockerinstall +sudo ./dockerinstall.sh install +``` -Getestet und produktiv genutzt auf: +Interaktives Menü: -- Debian 12 (Bookworm) -- Debian 13 (Trixie) -- Ubuntu 22.04 / 24.04 LTS +```bash +sudo ./dockerinstall.sh +``` -Distribution und Codename werden automatisch über `/etc/os-release` erkannt. +Automatisierte Installation: ---- +```bash +sudo ./dockerinstall.sh install --non-interactive --no-hello +``` -### ✨ Features (v2.x) +### Upgrade -**Installation** -- entfernt alte oder konfliktbehaftete Docker-Pakete -- richtet das offizielle Docker-APT-Repository inkl. GPG-Key ein -- installiert Docker Engine, CLI, Buildx & Compose Plugin -- aktiviert systemd-Dienste -- optional: Benutzer zur `docker`-Gruppe hinzufügen +```bash +sudo ./dockerinstall.sh upgrade +sudo ./dockerinstall.sh upgrade --backup-data +sudo ./dockerinstall.sh upgrade --force-upgrade +``` -**Upgrade** -- prüft und repariert Repository-Konfiguration -- reinstalliert Docker & Plugins sauber -- optional: Backup, Force-Upgrade, non-interactive +Ohne `--force-upgrade` aktualisiert APT nur Pakete, für die eine neuere Version verfügbar ist. Mit der Option werden die Docker-Pakete zusätzlich reinstalliert. -**Uninstall** -- entfernt Docker-Pakete, Repo & GPG-Keys -- optional: Docker-Daten behalten oder löschen -- systemd-Bereinigung +### Deinstallation -**Self-Check** -- Root-Rechte -- APT-Status & Locks -- curl / gpg -- systemd -- Schreibrechte unter `/var/lib` -- Docker-Status +Docker-Pakete entfernen und Daten behalten: -**Backup** -- `.tar.gz`-Backup von: - - `/var/lib/docker` - - `/var/lib/containerd` +```bash +sudo ./dockerinstall.sh uninstall --non-interactive +``` +Vorher sichern und Daten anschließend ausdrücklich löschen: ---- +```bash +sudo ./dockerinstall.sh uninstall --backup-data --remove-data --non-interactive +``` -### 🚀 Nutzung +`--backup-data` löscht niemals automatisch Daten. Ohne `--remove-data` bleiben `/var/lib/docker` und `/var/lib/containerd` erhalten. -Repository klonen: +### Optionen - git clone https://github.com/foxly-it/dockerinstall.git - cd dockerinstall +```text +--add-user=USER USER zur docker-Gruppe hinzufügen +--add-user USER Alternative Schreibweise +--no-hello hello-world-Prüfung überspringen +--no-clear Terminal nicht leeren +--no-color Farben deaktivieren +--log-file=PATH eigenen Logpfad verwenden +--non-interactive niemals Eingaben abfragen +--backup-data gestopptes Host-Backup erstellen +--remove-data Docker-Daten bei uninstall löschen +--keep-data Docker-Daten bei uninstall behalten (Standard) +--force-upgrade Docker-Pakete beim Upgrade reinstallieren +--self-check System nur prüfen +-h, --help Hilfe anzeigen +--version Skriptversion anzeigen +``` -Interaktiv starten: +### Backup-Hinweis - sudo ./dockerinstall.sh +Das Backup archiviert vorhandene Verzeichnisse unter `/var/lib/docker`, `/var/lib/containerd` und `/etc/docker` nach `/var/backups`. Docker und containerd werden dafür kurz angehalten und anschließend wieder gestartet, falls Docker vorher lief. -Menü: +Das ist ein Host-Level-Notfallbackup. Für Datenbanken und produktive Anwendungen sollten zusätzlich anwendungskonsistente Dumps und getestete Volume-Backups verwendet werden. Bind-Mount-Daten außerhalb dieser Pfade sind nicht enthalten. - 1) Installieren - 2) Upgrade - 9) Deinstallieren +### Logs ---- +- Installation und Upgrade: `/var/log/docker-install.log` +- Deinstallation: `/var/log/docker-uninstall.log` +- Self-Check: `/var/log/docker-self-check.log` -### ⚙️ CLI-Modus & Optionen +### Entwicklung und Tests - sudo ./dockerinstall.sh [install|upgrade|uninstall] [OPTIONS] +```bash +bash -n dockerinstall.sh +bash tests/integration.sh +shellcheck dockerinstall.sh tests/integration.sh tests/mock-command.sh +``` -Optionen: +Die Integrationstests verwenden einen isolierten Test-Root und gemockte Systembefehle. Sie verändern weder Docker noch APT auf dem Entwicklungsrechner. Ein Release sollte zusätzlich auf frischen VMs aller als getestet ausgewiesenen Distributionen geprüft werden. - --add-user=USER Benutzer zur docker-Gruppe hinzufügen - --no-hello Hello-World-Test überspringen - --no-clear Terminal nicht löschen - --log-file=PATH Eigenen Logpfad setzen - --non-interactive Keine Prompts, kein Menü - --backup-data Backup vor Upgrade/Uninstall - --force-upgrade Upgrade erzwingen - --self-check Nur Systemcheck ausführen +## English -Beispiele: +### Supported systems - sudo ./dockerinstall.sh install --no-hello - sudo ./dockerinstall.sh upgrade --backup-data --non-interactive +- Debian 11 (Bullseye), 12 (Bookworm), and 13 (Trixie) +- Ubuntu 22.04 LTS, 24.04 LTS, 25.10, and 26.04 LTS +- systemd and a Docker-supported architecture ---- +The installer rejects unsupported distributions and releases. The support matrix follows Docker's official installation documentation for [Debian](https://docs.docker.com/engine/install/debian/) and [Ubuntu](https://docs.docker.com/engine/install/ubuntu/). -### 🧾 Logging +### Features - /var/log/docker-install.log - /var/log/docker-uninstall.log +- Docker's official APT repository using the current Deb822 format (`docker.sources`) +- Docker's official signing key (`docker.asc`) +- Docker Engine, CLI, containerd, Buildx, and Compose v2 +- safe, validated CLI for interactive and automated operation +- separate logs for installation, self-check, and uninstallation +- optional host-level backup while the Docker service is stopped +- data removal only after an explicit `--remove-data` option or confirmation ---- +> Membership in the `docker` group grants root-level privileges. Only add trusted users. -### 🛡️ Sicherheit +### Installation -Mitglieder der `docker`-Gruppe besitzen **root-ähnliche Rechte**. -Nur vertrauenswürdige Benutzer hinzufügen. +```bash +git clone https://github.com/foxly-it/dockerinstall.git +cd dockerinstall +sudo ./dockerinstall.sh install +``` -Docker Security Best Practices: -https://docs.docker.com/security/ +Open the interactive menu: ---- +```bash +sudo ./dockerinstall.sh +``` -## 🇬🇧 English +Run an automated installation: -### 🧩 Overview +```bash +sudo ./dockerinstall.sh install --non-interactive --no-hello +``` -**dockerinstall.sh** is a standalone **single-file tool for managing Docker** on Linux systems. +### Upgrade -It covers the complete Docker lifecycle: +```bash +sudo ./dockerinstall.sh upgrade +sudo ./dockerinstall.sh upgrade --backup-data +sudo ./dockerinstall.sh upgrade --force-upgrade +``` -- installation of Docker Engine & plugins -- clean upgrades of existing installations -- full removal (optionally including data) -- integrated self-check and backup -- interactive **and** non-interactive operation +Without `--force-upgrade`, APT only updates packages for which a newer version is available. With this option, the Docker packages are reinstalled as well. -No frameworks, no dependencies — **one script, full control**. +### Uninstallation ---- +Remove Docker packages while retaining all Docker data: -### 🖥️ Supported Systems +```bash +sudo ./dockerinstall.sh uninstall --non-interactive +``` -Tested and used in production on: +Create a backup first and then explicitly remove the data: -- Debian 12 (Bookworm) -- Debian 13 (Trixie) -- Ubuntu 22.04 / 24.04 LTS +```bash +sudo ./dockerinstall.sh uninstall --backup-data --remove-data --non-interactive +``` -Distribution and codename are detected automatically via `/etc/os-release`. +`--backup-data` never removes data automatically. Unless `--remove-data` is supplied, `/var/lib/docker` and `/var/lib/containerd` are retained. ---- +### Options -### ✨ Features (v2.x) +```text +--add-user=USER Add USER to the docker group +--add-user USER Alternative syntax +--no-hello Skip the hello-world verification +--no-clear Do not clear the terminal +--no-color Disable colored output +--log-file=PATH Use a custom log file +--non-interactive Never prompt for input +--backup-data Create a stopped host-level backup +--remove-data Remove Docker data during uninstall +--keep-data Retain Docker data during uninstall (default) +--force-upgrade Reinstall Docker packages during upgrade +--self-check Validate the system without installing anything +-h, --help Show command help +--version Show the script version +``` -**Installation** -- removes conflicting Docker packages -- sets up the official Docker APT repository incl. GPG key -- installs Docker Engine, CLI, Buildx & Compose plugin -- enables systemd services -- optional docker group user setup +### Backup notes -**Upgrade** -- verifies and fixes repository configuration -- clean reinstall of Docker & plugins -- optional backup, force-upgrade, non-interactive mode +The backup archives existing directories under `/var/lib/docker`, `/var/lib/containerd`, and `/etc/docker` to `/var/backups`. Docker and containerd are stopped briefly and started again afterward if Docker was running before the backup. -**Uninstall** -- removes Docker packages, repo and GPG keys -- optional data removal -- systemd cleanup +This is an emergency host-level backup. Databases and production applications should additionally use application-consistent dumps and tested volume backups. Bind-mounted data outside the listed paths is not included. -**Self-Check** -- root privileges -- APT status & locks -- curl / gpg -- systemd -- write access under `/var/lib` -- Docker status +### Logs -**Backup** -- creates a `.tar.gz` backup of: - - `/var/lib/docker` - - `/var/lib/containerd` +- Installation and upgrade: `/var/log/docker-install.log` +- Uninstallation: `/var/log/docker-uninstall.log` +- Self-check: `/var/log/docker-self-check.log` +### Development and testing ---- +```bash +bash -n dockerinstall.sh +bash tests/integration.sh +shellcheck dockerinstall.sh tests/integration.sh tests/mock-command.sh +``` -### 🚀 Usage +The integration tests use an isolated test root and mocked system commands. They do not modify Docker or APT on the development machine. Before publishing a release, the complete workflow should additionally be tested on clean virtual machines for every distribution advertised as tested. -Clone the repository: +Run `./dockerinstall.sh --help` to display the built-in command reference. - git clone https://github.com/foxly-it/dockerinstall.git - cd dockerinstall +## License -Run interactively: - - sudo ./dockerinstall.sh - ---- - -### ⚙️ CLI Mode & Options - - sudo ./dockerinstall.sh [install|upgrade|uninstall] [OPTIONS] - ---- - -### 🛡️ Security Note - -Members of the `docker` group have **root-like privileges**. -Only add trusted users. - ---- - -## 📜 License - -MIT License -See [LICENSE](LICENSE). - ---- - -Foxly IT -Reboot required — but planned. +[MIT](LICENSE) © Foxly IT ![Alt](https://repobeats.axiom.co/api/embed/49f262ca5e7653bb4718e0b5c55547018a7ce48b.svg "Repobeats analytics image") diff --git a/dockerinstall.sh b/dockerinstall.sh old mode 100644 new mode 100755 index 4d03f83..662e83d --- a/dockerinstall.sh +++ b/dockerinstall.sh @@ -1,928 +1,554 @@ #!/usr/bin/env bash -# ============================================================ -# dockerinstall.sh – Foxly Edition -# Single-File Docker Manager -# Install | Upgrade | Uninstall | Self-Check | Backup -# Fully integrated visuals.sh + compose_pull_compact -# Compatible: Debian 12 & 13, Ubuntu 22.04+ -# by Mark “Foxly IT” Schenk – foxly.de -# Version: 2.0 -# ============================================================ +# Foxly dockerinstall - Docker Engine lifecycle manager +# Supported: Debian 11/12/13 and Ubuntu 22.04/24.04/25.10/26.04 +# Version: 2.1.0 set -Eeuo pipefail -# ============================================================ -# VISUALS.SH – vollständig integriert -# ============================================================ - -SCRIPT_ANIM="${SCRIPT_ANIM:-1}" +VERSION="2.1.0" +ACTION="" +ADD_USER="" +HELLO=1 +CLEAR=1 +NON_INTERACTIVE=0 +FORCE_UPGRADE=0 +BACKUP_DATA=0 +REMOVE_DATA=0 +KEEP_DATA=0 NO_COLOR="${NO_COLOR:-0}" -FORCE_NO_COLOR="${FORCE_NO_COLOR:-0}" - -# ---------- TTY / Colors ---------- -_is_tty() { [ -t 1 ]; } -_term_cols() { tput cols 2>/dev/null || echo 80; } - -if [ "$NO_COLOR" = "1" ] || [ "$FORCE_NO_COLOR" = "1" ] || ! _is_tty; then - C_RESET=""; C_DIM=""; C_BOLD="" - C_TEXT=""; C_WATER=""; C_FOX=""; C_ACCENT="" -else - C_RESET="$(printf '\033[0m')" - C_DIM="$(printf '\033[2m')" - C_BOLD="$(printf '\033[1m')" - C_TEXT="$(printf '\033[38;5;250m')" - C_WATER="$(printf '\033[38;5;45m')" - C_FOX="$(printf '\033[38;5;214m')" - C_ACCENT="$(printf '\033[38;5;223m')" -fi - -# ---------- Center helpers ---------- -_center_print() { - local cols pad line plain width left - cols="$(_term_cols)" - while IFS= read -r line; do - plain="${line//\033\[[0-9;]*m/}" - width=${#plain} - if [ "$width" -lt "$cols" ]; then - pad=$(( (cols - width) / 2 )) - left="$(printf '%*s' "$pad" '')" - else - left="" - fi - printf "%s%s\n" "$left" "$line" - done +LOG_FILE="" +LOG_FILE_EXPLICIT=0 +TEST_MODE="${DOCKERINSTALL_TEST_MODE:-0}" +SYSTEM_ROOT="${DOCKERINSTALL_ROOT:-}" +BACKUP_DIR="${DOCKERINSTALL_BACKUP_DIR:-}" +PROG_ACTIVE=0 + +repo_file="" +key_file="" +OS_ID="" +OS_VERSION="" +OS_CODENAME="" + +usage() { + cat <<'EOF' +Foxly dockerinstall - install, upgrade, or uninstall Docker Engine + +Usage: + sudo ./dockerinstall.sh [install|upgrade|uninstall] [OPTIONS] + sudo ./dockerinstall.sh --self-check + +Options: + --add-user=USER Add USER to the docker group + --add-user USER Same as above + --no-hello Skip the hello-world verification + --no-clear Do not clear the terminal + --no-color Disable colored output + --log-file=PATH Write output to a custom log file + --non-interactive Never prompt for input + --backup-data Create a stopped, host-level Docker data backup + --remove-data Delete Docker data during uninstall + --keep-data Keep Docker data during uninstall (the default) + --force-upgrade Reinstall Docker packages even when already current + --self-check Validate the host without changing it + -h, --help Show this help + --version Show the script version + +Data is never deleted unless --remove-data is supplied or deletion is +explicitly confirmed in interactive uninstall mode. +EOF } -_center_block() { - local lines=() line plain width max=0 - while IFS= read -r line; do - lines+=("$line") - plain="${line//\033\[[0-9;]*m/}" - width=${#plain} - (( width > max )) && max=$width - done - local cols pad left; cols="$(_term_cols)" - if (( max < cols )); then - pad=$(( (cols - max) / 2 )) - left="$(printf '%*s' "$pad" '')" - else - left="" - fi - for line in "${lines[@]}"; do - printf "%s%s\n" "$left" "$line" - done +die() { + log_err "$1" + exit "${2:-1}" } -# ---------- Tiny bubbles ---------- -_bubbles_frame() { - case "$1" in - 0) printf " · \n";; - 1) printf " · \n";; - 2) printf " · \n";; - 3) printf " · \n";; - esac +system_path() { + printf '%s%s' "$SYSTEM_ROOT" "$1" } -parse_no_color_flag() { - for a in "$@"; do - [ "$a" = "--no-color" ] && FORCE_NO_COLOR=1 - done +is_tty() { + [ -t 1 ] } -# ---------- Foxly ASCII ---------- -_fox_art() { - local mode="${1:-install}" - local E="^_^" M="^" - if [ "$mode" = "uninstall" ]; then - E="x_x"; M="_" - elif [ "$mode" = "upgrade" ]; then - E="o_o"; M="^" +init_colors() { + if [ "$NO_COLOR" = "1" ] || ! is_tty; then + C_RESET="" C_BOLD="" C_DIM="" C_INFO="" C_WARN="" C_ERR="" C_OK="" + else + C_RESET=$'\033[0m' + C_BOLD=$'\033[1m' + C_DIM=$'\033[2m' + C_INFO=$'\033[38;5;45m' + C_WARN=$'\033[38;5;214m' + C_ERR=$'\033[38;5;196m' + C_OK=$'\033[38;5;82m' fi - - cat < ${C_ACCENT}${M}${C_FOX} <~~${C_RESET} - ${C_FOX}\\ \\|/ /${C_RESET} - ${C_FOX}\`-----'${C_RESET} - ${C_WATER}[ ][ ][ ]${C_RESET} - ${C_WATER}[ ][ ][ ]${C_RESET} - ${C_WATER}\`-----'${C_RESET} -EOF } -# ---------- Banner ---------- -whale_banner() { - local mode="${1:-install}" - shift || true +log_info() { printf '%s[INFO]%s %s\n' "$C_INFO" "$C_RESET" "$1"; } +log_warn() { printf '%s[WARN]%s %s\n' "$C_WARN" "$C_RESET" "$1"; } +log_err() { printf '%s[FAIL]%s %s\n' "$C_ERR" "$C_RESET" "$1" >&2; } +log_ok() { printf '%s[ OK ]%s %s\n' "$C_OK" "$C_RESET" "$1"; } - for arg in "$@"; do [ "$arg" = "--no-color" ] && FORCE_NO_COLOR=1; done - - local title - case "$mode" in - install) title="${C_BOLD}${C_TEXT}Install dockerinstall – Foxly is on duty!${C_RESET}";; - upgrade) title="${C_BOLD}${C_TEXT}Upgrade dockerinstall – Polishing the fleet…${C_RESET}";; - uninstall) title="${C_BOLD}${C_TEXT}Uninstall dockerinstall – The Fox takes a bow.${C_RESET}";; - *) title="${C_BOLD}${C_TEXT}dockerinstall – Foxly Tool${C_RESET}";; - esac - - printf "%s\n" "$title" | _center_print +banner() { + local mode="${1:-dockerinstall}" + printf '\n%s%sFoxly dockerinstall%s %s- %s%s\n\n' \ + "$C_BOLD" "$C_INFO" "$C_RESET" "$C_DIM" "$mode" "$C_RESET" +} - if [ "$SCRIPT_ANIM" = "1" ] && _is_tty; then - for f in 0 1 2 3; do - _bubbles_frame "$f" | _center_print - sleep 0.05 - done +progress_cleanup() { + if [ "$PROG_ACTIVE" -eq 1 ]; then + if is_tty; then + printf '\n' + tput cnorm 2>/dev/null || true + fi + PROG_ACTIVE=0 fi - - _fox_art "$mode" | _center_block - printf "%s\n" "${C_DIM}(Foxly ready – systems online.)${C_RESET}" | _center_print } -# ============================================================ -# Mini-Progressbar -# ============================================================ - -PROG_TOTAL=0 -PROG_CUR=0 -PROG_ACTIVE=0 - -progress__hide_cursor() { _is_tty && tput civis 2>/dev/null || true; } -progress__show_cursor() { _is_tty && tput cnorm 2>/dev/null || true; } - -progress__draw() { - local p="${1:-0}" label="${2:-}" cols barw filled empty - cols="$(_term_cols)" - barw=40 - [ "$cols" -lt 70 ] && barw=28 - [ "$cols" -lt 50 ] && barw=20 - - local filled_len=$(( (p * barw) / 100 )) - (( filled_len > barw )) && filled_len="$barw" - local empty_len=$(( barw - filled_len )) - - filled="$(printf '%*s' "$filled_len" '' | tr ' ' '#')" - empty="$(printf '%*s' "$empty_len" '' | tr ' ' '-')" - - printf "\r%s ${C_WATER}[${C_RESET}%s%s${C_WATER}]${C_RESET} ${C_TEXT}%3d%%${C_RESET} ${C_DIM}%s${C_RESET}" \ - "${C_BOLD}${C_TEXT}▶${C_RESET}" "$filled" "$empty" "$p" "$label" +on_error() { + local status line + status="${1:-1}" + line="${2:-unknown}" + progress_cleanup + log_err "Abbruch in Zeile ${line} (Exit ${status}). Siehe Logdatei für Details." + exit "$status" } progress_init() { - PROG_TOTAL="${1:-0}" - PROG_CUR=0 PROG_ACTIVE=1 - if _is_tty; then - progress__hide_cursor - printf "%s\n" "${C_BOLD}${C_TEXT}${2:-Progress}${C_RESET}" - progress__draw 0 "Startet …" - else - echo "[INFO] ${2:-Progress}" + log_info "$1" + if is_tty; then + tput civis 2>/dev/null || true fi - trap 'progress_cleanup' EXIT } progress_step() { - local label="${1:-}" - PROG_CUR=$(( PROG_CUR + 1 )) - local pct=$(( (PROG_CUR * 100) / PROG_TOTAL )) - (( pct > 100 )) && pct=100 - - if _is_tty; then - progress__draw "$pct" "$label" - else - echo "[${PROG_CUR}/${PROG_TOTAL}] $label" - fi -} - -progress_set() { - local pct="${1:-0}" label="${2:-}" - (( pct < 0 )) && pct=0 - (( pct > 100 )) && pct=100 - - if _is_tty; then - progress__draw "$pct" "$label" - else - echo "[$pct%%] $label" - fi + log_info "$1" } progress_done() { - local label="${1:-Fertig}" - if _is_tty; then - progress__draw 100 "$label" - printf "\n" - else - echo "[DONE] $label" - fi - progress__show_cursor - PROG_ACTIVE=0 - trap - EXIT + progress_cleanup + log_ok "$1" } -progress_cleanup() { - if [ "$PROG_ACTIVE" -eq 1 ]; then - _is_tty && printf "\n" - progress__show_cursor - PROG_ACTIVE=0 +set_action() { + local requested="$1" + if [ -n "$ACTION" ] && [ "$ACTION" != "$requested" ]; then + die "Mehrere Aktionen angegeben: '$ACTION' und '$requested'." fi + ACTION="$requested" } -# ============================================================ -# compose_pull_compact (3-line calm output) -# ============================================================ - -_COMPACT_NAMES=() -_COMPACT_TOTALS=() -_COMPACT_CURS=() -_COMPACT_DONE=() -_COMPACT_IDXMAP="" -_COMPACT_LINES=3 +require_value() { + local option="$1" value="${2:-}" + [ -n "$value" ] || die "Option '$option' benötigt einen Wert." + printf '%s' "$value" +} -_compact__index_of() { - local nm="$1" pair - for pair in $_COMPACT_IDXMAP; do - case "$pair" in "$nm":*) echo "${pair##*:}"; return 0;; +parse_arguments() { + while [ "$#" -gt 0 ]; do + case "$1" in + install|upgrade|uninstall) set_action "$1" ;; + --self-check) set_action "selfcheck" ;; + --add-user=*) ADD_USER="$(require_value --add-user "${1#*=}")" ;; + --add-user) + [ "$#" -ge 2 ] || die "Option '--add-user' benötigt einen Wert." + ADD_USER="$(require_value --add-user "$2")" + shift + ;; + --no-hello) HELLO=0 ;; + --no-clear) CLEAR=0 ;; + --no-color) NO_COLOR=1 ;; + --backup-data) BACKUP_DATA=1 ;; + --remove-data) REMOVE_DATA=1 ;; + --keep-data) KEEP_DATA=1 ;; + --non-interactive) NON_INTERACTIVE=1 ;; + --force-upgrade) FORCE_UPGRADE=1 ;; + --log-file=*) + LOG_FILE="$(require_value --log-file "${1#*=}")" + LOG_FILE_EXPLICIT=1 + ;; + -h|--help) usage; exit 0 ;; + --version) printf '%s\n' "$VERSION"; exit 0 ;; + --) shift; [ "$#" -eq 0 ] || die "Unerwartete Argumente: $*"; break ;; + -*) die "Unbekannte Option: $1" ;; + *) die "Unbekanntes Argument: $1" ;; esac + shift done - echo "-1" -} - -_compact__fmt_bytes() { - local b="${1:-0}" - if [ "$b" -lt 1024 ]; then printf "%dB" "$b"; return; fi - local kb=$(( b / 1024 )) - if [ "$kb" -lt 1024 ]; then printf "%dKB" "$kb"; return; fi - local mb=$(( kb / 1024 )) - if [ "$mb" -lt 1024 ]; then printf "%dMB" "$mb"; return; fi - local gb=$(( mb / 1024 )) - printf "%dGB" "$gb" -} - -_compact__bar() { - local p=$1 w=$2 - (( p < 0 )) && p=0 - (( p > 100 )) && p=100 - (( w < 10 )) && w=10 - local f=$(( (p * w) / 100 )) - (( f > w )) && f=$w - local e=$(( w - f )) - printf "[%s%s]" \ - "$(printf '%*s' "$f" '' | tr ' ' '=')" \ - "$(printf '%*s' "$e" '' | tr ' ' ' ')" -} - -_compact__line() { - local i="$1" - local nm="${_COMPACT_NAMES[$i]}" - local tot="${_COMPACT_TOTALS[$i]}" - local cur="${_COMPACT_CURS[$i]}" - local dn="${_COMPACT_DONE[$i]}" - - local mark bar pct right cols text - cols="$(_term_cols)" - - if [ "$dn" -eq 1 ]; then - mark="✓" - bar="$(_compact__bar 100 22)" - right="done" - else - mark="⣿" - if [ "$tot" -gt 0 ]; then - pct=$(( (100 * cur) / tot )) - (( pct > 100 )) && pct=100 - bar="$(_compact__bar "$pct" 22)" - right="$(printf "%s/%s (%3d%%)" "$(_compact__fmt_bytes "$cur")" "$(_compact__fmt_bytes "$tot")" "$pct")" - else - bar="$(_compact__bar 0 22)" - right="waiting" - fi - fi - - text=$(printf " %s %-18s ${C_WATER}%s${C_RESET} %s" "$mark" "$nm" "$bar" "$right") - printf "%-*s\n" "$cols" "$text" -} - -compose_pull_compact_init() { - _COMPACT_NAMES=(); _COMPACT_TOTALS=(); _COMPACT_CURS=(); _COMPACT_DONE=() - _COMPACT_IDXMAP="" - for i in 1 2 3; do - nm="${1:-item$i}" - tot="${2:-0}" - shift 2 || true - _COMPACT_NAMES+=("$nm") - _COMPACT_TOTALS+=("$tot") - _COMPACT_CURS+=(0) - _COMPACT_DONE+=(0) - done - for i in 0 1 2; do - _COMPACT_IDXMAP="$_COMPACT_IDXMAP ${_COMPACT_NAMES[$i]}:$i" - done - _compact__line 0 - _compact__line 1 - _compact__line 2 -} - -compose_pull_compact_set() { - local nm="$1" cur="${2:-0}" idx - idx="$(_compact__index_of "$nm")" - [ "$idx" -lt 0 ] && return 0 - _COMPACT_CURS[$idx]="$cur" - tput cuu "$_COMPACT_LINES" 2>/dev/null || printf "\033[%dA" "$_COMPACT_LINES" - _compact__line 0 - _compact__line 1 - _compact__line 2 -} - -compose_pull_compact_done() { - local nm="$1" idx - idx="$(_compact__index_of "$nm")" - [ "$idx" -lt 0 ] && return 0 - _COMPACT_DONE[$idx]=1 - _COMPACT_CURS[$idx]="${_COMPACT_TOTALS[$idx]}" - tput cuu "$_COMPACT_LINES" 2>/dev/null || printf "\033[%dA" "$_COMPACT_LINES" - _compact__line 0 - _compact__line 1 - _compact__line 2 -} - -compose_pull_compact_finish() { printf "\n"; } -# ============================================================ -# BLOCK 2 – CORE FUNCTIONS (Backup, Repo, Self-Check, Status) -# ============================================================ - -# ------------------------------------------------------------ -# GLOBAL FLAGS (für Block 3 initialisiert) -# ------------------------------------------------------------ -ACTION="" -ADD_USER="" -HELLO=1 -CLEAR=1 -LOG_FILE="/var/log/docker-install.log" -NON_INTERACTIVE=0 -FORCE_UPGRADE=0 -BACKUP_DATA=0 - - -# ------------------------------------------------------------ -# PRINT HELPERS -# ------------------------------------------------------------ -log_info() { printf "${C_WATER}[INFO]${C_RESET} %s\n" "$1"; } -log_warn() { printf "${C_ACCENT}[WARN]${C_RESET} %s\n" "$1"; } -log_err() { printf "${C_FOX}[FAIL]${C_RESET} %s\n" "$1"; } -log_ok() { printf "${C_TEXT}[ OK ]${C_RESET} %s\n" "$1"; } - - -# ------------------------------------------------------------ -# SELF-CHECK – prüft alles bevor Install/Upgrade läuft -# ------------------------------------------------------------ -self_check() { - whale_banner "install" - - printf "\n${C_BOLD}${C_TEXT}Self-Check gestartet …${C_RESET}\n\n" - - local errors=0 - - # Root? - if [ "${EUID:-$(id -u)}" -ne 0 ]; then - log_err "Script muss mit root/sudo ausgeführt werden." - errors=$((errors+1)) - else - log_ok "Root-Rechte vorhanden" - fi - - # apt vorhanden? - if ! command -v apt-get >/dev/null 2>&1; then - log_err "Dies ist kein Debian-/Ubuntu-System (apt-get fehlt)." - errors=$((errors+1)) - else - log_ok "APT-System erkannt" - fi - - # curl - if ! command -v curl >/dev/null 2>&1; then - log_err "curl fehlt" - errors=$((errors+1)) - else - log_ok "curl installiert" - fi - # gpg - if ! command -v gpg >/dev/null 2>&1; then - log_err "gpg fehlt" - errors=$((errors+1)) - else - log_ok "gpg installiert" + if [ "$REMOVE_DATA" -eq 1 ] && [ "$KEEP_DATA" -eq 1 ]; then + die "--remove-data und --keep-data schließen sich gegenseitig aus." fi - - # systemd? - if ! pidof systemd >/dev/null 2>&1; then - log_err "Systemd wird nicht ausgeführt – Docker benötigt systemd." - errors=$((errors+1)) - else - log_ok "systemd läuft" - fi - - # docker vorhanden? - if command -v docker >/dev/null 2>&1; then - log_ok "Docker CLI vorhanden" - else - log_warn "Docker CLI fehlt (nicht schlimm für Installation)" + if [ "$ACTION" != "uninstall" ] && { [ "$REMOVE_DATA" -eq 1 ] || [ "$KEEP_DATA" -eq 1 ]; }; then + die "--remove-data und --keep-data sind nur bei uninstall erlaubt." fi - - # /var/lib/docker beschreibbar? - if [ -d /var/lib ] && [ ! -w /var/lib ]; then - log_err "/var/lib ist nicht beschreibbar." - errors=$((errors+1)) - else - log_ok "/var/lib beschreibbar" + if [ "$FORCE_UPGRADE" -eq 1 ] && [ "$ACTION" != "upgrade" ]; then + die "--force-upgrade ist nur bei upgrade erlaubt." fi - - # apt-Lock prüfen - if fuser /var/lib/dpkg/lock >/dev/null 2>&1 || \ - fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || \ - fuser /var/cache/apt/archives/lock >/dev/null 2>&1; then - log_err "APT-Lock aktiv – anderer Prozess installiert/updated gerade!" - errors=$((errors+1)) - else - log_ok "APT-Lock frei" + if [ "$BACKUP_DATA" -eq 1 ] && [ "$ACTION" != "upgrade" ] && [ "$ACTION" != "uninstall" ]; then + die "--backup-data ist nur bei upgrade oder uninstall erlaubt." fi - - # Ergebnis - if [ "$errors" -gt 0 ]; then - printf "\n${C_FOX}Self-Check hat %d Fehler gefunden – Abbruch.${C_RESET}\n" "$errors" - exit 1 + if [ -n "$ADD_USER" ] && [ "$ACTION" != "install" ]; then + die "--add-user ist nur bei install erlaubt." fi - - printf "\n${C_WATER}Self-Check erfolgreich!${C_RESET}\n" - exit 0 } - -# ------------------------------------------------------------ -# BACKUP (/var/lib/docker → tar.gz) -# ------------------------------------------------------------ -perform_backup() { - local TS - TS="$(date +%Y%m%d-%H%M%S)" - local OUT="/var/backups/docker-backup-${TS}.tar.gz" - - mkdir -p /var/backups - - whale_banner "upgrade" - - log_info "Backup erstellt → $OUT" - progress_init 3 "Backup läuft …" - - progress_step "Vorbereitung" - - tar -czf "$OUT" /var/lib/docker /var/lib/containerd 2>/dev/null || { - progress_done "Fehler" - log_err "Backup fehlgeschlagen" - exit 1 - } - - progress_step "Daten komprimiert" - progress_step "Abschluss" - - progress_done "Backup abgeschlossen" - log_ok "Backup gespeichert in: $OUT" +show_menu_and_pick_action() { + banner "Menü" + printf '1) Docker installieren\n2) Docker aktualisieren\n9) Docker deinstallieren\n\nAuswahl: ' + local choice + read -r choice + case "$choice" in + 1) ACTION="install" ;; + 2) ACTION="upgrade" ;; + 9) ACTION="uninstall" ;; + *) die "Ungültige Auswahl: $choice" ;; + esac } - -# ------------------------------------------------------------ -# DOCKER STATUS – läuft der Dienst? -# ------------------------------------------------------------ -docker_is_running() { - if systemctl is-active --quiet docker 2>/dev/null; then - return 0 - fi - if systemctl is-active --quiet containerd 2>/dev/null; then - return 0 +require_root() { + if [ "$TEST_MODE" != "1" ] && [ "$(id -u)" -ne 0 ]; then + die "Dieses Skript muss mit root/sudo ausgeführt werden." fi - return 1 } -docker_status_report() { - if docker_is_running; then - log_ok "Docker läuft" +detect_os() { + local os_release + os_release="$(system_path /etc/os-release)" + [ -r "$os_release" ] || die "Kann $os_release nicht lesen." + + ID="" VERSION_ID="" VERSION_CODENAME="" UBUNTU_CODENAME="" + # /etc/os-release is a trusted, root-owned system file in normal operation. + # shellcheck disable=SC1090 + . "$os_release" + + OS_ID="${ID:-}" + OS_VERSION="${VERSION_ID:-}" + if [ "$OS_ID" = "ubuntu" ]; then + OS_CODENAME="${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}" else - log_warn "Docker läuft NICHT" + OS_CODENAME="${VERSION_CODENAME:-}" fi -} + [ -n "$OS_VERSION" ] && [ -n "$OS_CODENAME" ] || \ + die "Distribution oder Codename konnte nicht erkannt werden." -# ------------------------------------------------------------ -# DEBIAN/UBUNTU Versionsdaten -# ------------------------------------------------------------ -detect_os() { - . /etc/os-release - OS_ID="$ID" - OS_CODENAME="$VERSION_CODENAME" - OS_VERSION="$VERSION_ID" -} + case "${OS_ID}:${OS_VERSION}" in + debian:11|debian:12|debian:13|ubuntu:22.04|ubuntu:24.04|ubuntu:25.10|ubuntu:26.04) ;; + *) die "Nicht unterstütztes System: ${OS_ID} ${OS_VERSION} (${OS_CODENAME})." ;; + esac -# ------------------------------------------------------------ -# Versions-Helper (installierte vs. Repo-Version) -# ------------------------------------------------------------ -docker_current_version() { - docker --version 2>/dev/null | awk '{print $3}' | sed 's/,//' + repo_file="$(system_path /etc/apt/sources.list.d/docker.sources)" + key_file="$(system_path /etc/apt/keyrings/docker.asc)" } -docker_repo_version() { - apt-cache policy docker-ce 2>/dev/null | awk '/Candidate:/ {print $2}' +require_commands() { + local missing=() command + for command in apt-get dpkg dpkg-query systemctl tee tar; do + command -v "$command" >/dev/null 2>&1 || missing+=("$command") + done + [ "${#missing[@]}" -eq 0 ] || die "Fehlende Programme: ${missing[*]}" } -docker_needs_upgrade() { - local cur latest - cur="$(docker_current_version)" - latest="$(docker_repo_version)" - if [ -z "$cur" ]; then - return 0 - fi - if [ -z "$latest" ] || [ "$latest" = "(none)" ]; then - return 0 - fi - dpkg --compare-versions "$latest" gt "$cur" +check_apt_locks() { + command -v fuser >/dev/null 2>&1 || return 0 + local lock + for lock in /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock \ + /var/lib/apt/lists/lock /var/cache/apt/archives/lock; do + if fuser "$(system_path "$lock")" >/dev/null 2>&1; then + die "APT-Lock aktiv: $lock" + fi + done } +validate_environment() { + require_root + detect_os + require_commands + check_apt_locks + log_ok "Unterstütztes System: ${OS_ID} ${OS_VERSION} (${OS_CODENAME})" +} -repo_file="/etc/apt/sources.list.d/docker.list" -key_file="/etc/apt/keyrings/docker.gpg" - - -# ------------------------------------------------------------ -# ERKENNE Falsche/alte Docker-Repos -# ------------------------------------------------------------ -repo_is_correct() { - if [[ ! -f "$repo_file" ]]; then - return 1 +configure_logging() { + if [ "$LOG_FILE_EXPLICIT" -eq 0 ]; then + case "$ACTION" in + uninstall) LOG_FILE="$(system_path /var/log/docker-uninstall.log)" ;; + selfcheck) LOG_FILE="$(system_path /var/log/docker-self-check.log)" ;; + *) LOG_FILE="$(system_path /var/log/docker-install.log)" ;; + esac fi - if grep -q "$OS_CODENAME" "$repo_file"; then - return 0 + mkdir -p "$(dirname "$LOG_FILE")" + if [ "$TEST_MODE" = "1" ]; then + exec >> "$LOG_FILE" 2>&1 + else + exec > >(tee -a "$LOG_FILE") 2>&1 fi - return 1 } - -# ------------------------------------------------------------ -# Entferne alle alten Docker-Repos -# ------------------------------------------------------------ -remove_old_repos() { - rm -f /etc/apt/sources.list.d/docker*.list - rm -f /etc/apt/keyrings/docker.gpg +self_check() { + banner "Self-Check" + validate_environment + command -v curl >/dev/null 2>&1 || die "curl fehlt." + local data_dir + data_dir="$(system_path /var/lib)" + [ -d "$data_dir" ] || die "$data_dir existiert nicht." + [ -w "$data_dir" ] || die "$data_dir ist nicht beschreibbar." + log_ok "Self-Check erfolgreich." +} + +remove_repository_files() { + local path + for path in \ + /etc/apt/sources.list.d/docker.list \ + /etc/apt/sources.list.d/docker.sources \ + /etc/apt/keyrings/docker.gpg \ + /etc/apt/keyrings/docker.asc; do + rm -f -- "$(system_path "$path")" + done } - -# ------------------------------------------------------------ -# INSTALLIEREN des korrekten Docker-Repos -# ------------------------------------------------------------ install_correct_repo() { - detect_os - - mkdir -p /etc/apt/keyrings - - curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" \ - | gpg --dearmor -o "$key_file" - - chmod a+r "$key_file" - - echo \ -"deb [arch=$(dpkg --print-architecture) signed-by=$key_file] \ -https://download.docker.com/linux/${OS_ID} ${OS_CODENAME} stable" \ - > "$repo_file" + local keyring_dir source_dir arch key_tmp repo_tmp + keyring_dir="$(system_path /etc/apt/keyrings)" + source_dir="$(system_path /etc/apt/sources.list.d)" + arch="$(dpkg --print-architecture)" + key_tmp="${key_file}.tmp.$$" + repo_tmp="${repo_file}.tmp.$$" + + mkdir -p "$keyring_dir" "$source_dir" + curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" -o "$key_tmp" + chmod a+r "$key_tmp" + + { + printf 'Types: deb\n' + printf 'URIs: https://download.docker.com/linux/%s\n' "$OS_ID" + printf 'Suites: %s\n' "$OS_CODENAME" + printf 'Components: stable\n' + printf 'Architectures: %s\n' "$arch" + printf 'Signed-By: %s\n' "$key_file" + } > "$repo_tmp" + + remove_repository_files + mv -f -- "$key_tmp" "$key_file" + mv -f -- "$repo_tmp" "$repo_file" +} + +installed_packages() { + local package status + for package in "$@"; do + status="$(dpkg-query -W -f='${db:Status-Abbrev}' "$package" 2>/dev/null || true)" + [[ "$status" == ii* ]] && printf '%s\n' "$package" + done + return 0 } - -# ------------------------------------------------------------ -# Sicherer Prompt (übersprungen bei NON_INTERACTIVE=1) -# ------------------------------------------------------------ -safe_prompt() { - local question="$1" - local default="$2" - - if [ "$NON_INTERACTIVE" -eq 1 ]; then - echo "$default" - return +remove_conflicting_packages() { + local packages=() + # Package names cannot contain whitespace; word splitting is intentional. + # shellcheck disable=SC2207 + packages=($(installed_packages \ + docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc)) + if [ "${#packages[@]}" -gt 0 ]; then + apt-get remove -y "${packages[@]}" + else + log_info "Keine konfliktbehafteten Pakete gefunden." fi - - echo -n "$question " - read -r answer - echo "${answer:-$default}" } - -# ------------------------------------------------------------ -# WÄHLE AKTION über Menü (falls nicht non-interactive) -# ------------------------------------------------------------ -show_menu_and_pick_action() { - whale_banner "install" - echo "" - echo " Was möchtest du tun?" - echo "" - echo " 1) Docker installieren" - echo " 2) Docker Upgrade (Repo fix + Reinstall)" - echo " 9) Docker deinstallieren" - echo "" - printf " Auswahl: " - - read -r choice - - case "$choice" in - 1) ACTION="install";; - 2) ACTION="upgrade";; - 9) ACTION="uninstall";; - *) echo "Ungültige Eingabe."; exit 1;; - esac +docker_was_running() { + systemctl is-active --quiet docker.service } +stop_docker_for_backup() { + local was_running=1 + docker_was_running || was_running=0 + systemctl stop docker.service docker.socket containerd.service 2>/dev/null || true + printf '%s' "$was_running" +} -# ------------------------------------------------------------ -# SETUP LOGGING -# ------------------------------------------------------------ -setup_logging() { - mkdir -p "$(dirname "$LOG_FILE")" - exec > >(tee -a "$LOG_FILE") 2>&1 +start_docker_after_backup() { + local was_running="$1" + if [ "$was_running" -eq 1 ]; then + systemctl start containerd.service docker.service + fi } +perform_backup() { + local timestamp output was_running source relative + local sources=() + timestamp="$(date +%Y%m%d-%H%M%S)" + [ -n "$BACKUP_DIR" ] || BACKUP_DIR="$(system_path /var/backups)" + mkdir -p "$BACKUP_DIR" + output="${BACKUP_DIR}/docker-backup-${timestamp}.tar.gz" + + was_running="$(stop_docker_for_backup)" + for relative in var/lib/docker var/lib/containerd etc/docker; do + source="$(system_path "/$relative")" + [ -e "$source" ] && sources+=("$relative") + done + if [ "${#sources[@]}" -eq 0 ]; then + start_docker_after_backup "$was_running" + die "Keine Docker-Daten für ein Backup gefunden." + fi -# ------------------------------------------------------------ -# PARSE ARGUMENTS (für Block 3 relevant) -# ------------------------------------------------------------ -parse_arguments() { - for arg in "$@"; do - case "$arg" in - install) ACTION="install";; - upgrade) ACTION="upgrade";; - uninstall) ACTION="uninstall";; + log_info "Docker ist für ein konsistenteres Host-Backup angehalten." + if ! tar -czf "$output" -C "${SYSTEM_ROOT:-/}" "${sources[@]}"; then + start_docker_after_backup "$was_running" + die "Backup fehlgeschlagen." + fi + start_docker_after_backup "$was_running" + log_ok "Backup gespeichert: $output" +} - --add-user=*) ADD_USER="${arg#*=}";; - --add-user) shift; ADD_USER="${1:-}";; +verify_docker_installation() { + docker --version + docker compose version + docker buildx version + docker info >/dev/null + if [ "$HELLO" -eq 1 ]; then + docker run --rm hello-world + fi + log_ok "Docker Engine, Compose und Buildx wurden verifiziert." +} - --no-hello) HELLO=0;; - --no-clear) CLEAR=0;; +install_docker_packages() { + local install_args=(-y) + if [ "$FORCE_UPGRADE" -eq 1 ]; then + install_args+=(--reinstall) + fi + apt-get install "${install_args[@]}" \ + docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +} - --backup-data) BACKUP_DATA=1;; - --non-interactive) NON_INTERACTIVE=1;; - --force-upgrade) FORCE_UPGRADE=1;; +perform_install() { + banner "Installation" + validate_environment + progress_init "Docker-Installation gestartet." - --self-check) ACTION="selfcheck";; - --log-file=*) LOG_FILE="${arg#*=}";; + remove_repository_files + apt-get update + apt-get install -y ca-certificates curl + command -v curl >/dev/null 2>&1 || die "curl konnte nicht installiert werden." + progress_step "Voraussetzungen installiert." - --no-color) : ;; # handled by visuals - *) ;; - esac - done -} -# ============================================================ -# BLOCK 3 – INSTALL / UPGRADE / UNINSTALL / MAIN DISPATCH -# ============================================================ + remove_conflicting_packages + progress_step "Konfliktpakete geprüft." -# ------------------------------------------------------------ -# INSTALL DOCKER -# ------------------------------------------------------------ -perform_install() { - whale_banner "install" - - PHASES=( - "System-Check & Prereqs" - "Konfliktpakete entfernen" - "Docker-Repo einbinden" - "Docker Engine & Tools installieren" - "Services aktivieren" - "User/Gruppen (optional)" - "Hello-World Test (optional)" - "Cleanup" - ) - - progress_init "${#PHASES[@]}" "Install dockerinstall – Setup läuft" - - # PHASE 1 - apt-get update -y - apt-get install -y ca-certificates curl gnupg lsb-release - install -m 0755 -d /etc/apt/keyrings - progress_step "${PHASES[0]}" - - # PHASE 2 - apt-get remove -y docker docker.io docker-engine docker-doc podman-docker \ - docker-compose docker-compose-plugin 2>/dev/null || true - - apt-get purge -y docker-ce docker-ce-cli containerd.io \ - docker-buildx-plugin docker-compose-plugin 2>/dev/null || true - - progress_step "${PHASES[1]}" - - # PHASE 3 – Repo schreiben - remove_old_repos install_correct_repo - apt-get update -y - progress_step "${PHASES[2]}" + apt-get update + progress_step "Offizielles Docker-Repository eingerichtet." - # PHASE 4 – Pakete - apt-get install -y docker-ce docker-ce-cli containerd.io \ - docker-buildx-plugin docker-compose-plugin - progress_step "${PHASES[3]}" + install_docker_packages + systemctl enable --now containerd.service docker.service + progress_step "Docker Engine und Plugins installiert." - # PHASE 5 – Dienste aktivieren - systemctl enable --now docker - systemctl enable --now containerd - progress_step "${PHASES[4]}" - - # PHASE 6 – Optionale Gruppe if [ -n "$ADD_USER" ]; then - if id "$ADD_USER" >/dev/null 2>&1; then - usermod -aG docker "$ADD_USER" - log_ok "User '$ADD_USER' zur docker-Gruppe hinzugefügt" - else - log_warn "User '$ADD_USER' existiert nicht – übersprungen" - fi + id "$ADD_USER" >/dev/null 2>&1 || die "Benutzer '$ADD_USER' existiert nicht." + usermod -aG docker "$ADD_USER" + log_warn "Gruppenänderung für '$ADD_USER' wird nach erneutem Login aktiv." fi - progress_step "${PHASES[5]}" - # PHASE 7 – Hello World - if [ "$HELLO" -eq 1 ]; then - docker run --rm hello-world || true - fi - progress_step "${PHASES[6]}" - - # PHASE 8 – Cleanup - apt-get autoremove -y >/dev/null 2>&1 || true - progress_step "${PHASES[7]}" - - progress_done "Installation abgeschlossen" - log_ok "Docker erfolgreich installiert" + verify_docker_installation + apt-get autoremove -y + progress_done "Docker wurde erfolgreich installiert." } - -# ------------------------------------------------------------ -# UPGRADE DOCKER -# ------------------------------------------------------------ perform_upgrade() { - whale_banner "upgrade" - - if docker_is_running; then - log_info "Docker läuft – Upgrade kann im Livebetrieb erfolgen." - else - log_warn "Docker läuft nicht – trotzdem Upgrade möglich." - fi + banner "Upgrade" + validate_environment + progress_init "Docker-Upgrade gestartet." if [ "$BACKUP_DATA" -eq 1 ]; then perform_backup fi - - PHASES=( - "Entferne alte Repos & Keys" - "Neues Docker-Repo schreiben" - "APT aktualisieren" - "Docker neu installieren" - "Services laden" - "Cleanup" - ) - - progress_init "${#PHASES[@]}" "Upgrade dockerinstall – Update läuft" - - # PHASE 1 - remove_old_repos - progress_step "${PHASES[0]}" - - # PHASE 2 + remove_repository_files + apt-get update + apt-get install -y ca-certificates curl + command -v curl >/dev/null 2>&1 || die "curl konnte nicht installiert werden." install_correct_repo - progress_step "${PHASES[1]}" - - # PHASE 3 - apt-get update -y - progress_step "${PHASES[2]}" - - # PHASE 4 - apt-get install --reinstall -y docker-ce docker-ce-cli containerd.io \ - docker-buildx-plugin docker-compose-plugin - progress_step "${PHASES[3]}" - - # PHASE 5 - systemctl enable --now docker - systemctl enable --now containerd - progress_step "${PHASES[4]}" - - # PHASE 6 - apt-get autoremove -y >/dev/null 2>&1 || true - progress_step "${PHASES[5]}" - - progress_done "Upgrade abgeschlossen" - log_ok "Docker erfolgreich aktualisiert" + apt-get update + install_docker_packages + systemctl enable --now containerd.service docker.service + verify_docker_installation + apt-get autoremove -y + progress_done "Docker wurde erfolgreich aktualisiert." } +confirm_data_removal() { + if [ "$REMOVE_DATA" -eq 1 ]; then + return 0 + fi + if [ "$KEEP_DATA" -eq 1 ] || [ "$NON_INTERACTIVE" -eq 1 ]; then + return 1 + fi + local answer + printf 'Docker-Daten endgültig löschen? Tippe DELETE zum Bestätigen: ' + read -r answer + [ "$answer" = "DELETE" ] +} -# ------------------------------------------------------------ -# UNINSTALL DOCKER -# ------------------------------------------------------------ perform_uninstall() { - whale_banner "uninstall" - - PHASES=( - "Dienste stoppen" - "Pakete entfernen" - "Repo & Key entfernen" - "Datenverzeichnisse (optional)" - "Gruppen-Hinweis" - "Cleanup" - ) - - progress_init "${#PHASES[@]}" "Uninstall dockerinstall – Entferne Komponenten" - - # PHASE 1 - systemctl stop docker 2>/dev/null || true - systemctl stop containerd 2>/dev/null || true - progress_step "${PHASES[0]}" - - # PHASE 2 - apt-get purge -y docker-ce docker-ce-cli containerd.io \ - docker-buildx-plugin docker-compose-plugin docker-compose 2>/dev/null || true - - apt-get autoremove -y --purge 2>/dev/null || true - progress_step "${PHASES[1]}" + banner "Deinstallation" + validate_environment + progress_init "Docker-Deinstallation gestartet." - # PHASE 3 - remove_old_repos - apt-get update -y - progress_step "${PHASES[2]}" - - # PHASE 4 – Daten + systemctl stop docker.service docker.socket containerd.service 2>/dev/null || true if [ "$BACKUP_DATA" -eq 1 ]; then perform_backup fi - # Daten ggf. löschen - if [ "$NON_INTERACTIVE" -eq 1 ]; then - KEEP=$( [ "$BACKUP_DATA" -eq 1 ] && echo "n" || echo "y" ) + local packages=() + # Package names cannot contain whitespace; word splitting is intentional. + # shellcheck disable=SC2207 + packages=($(installed_packages docker-ce docker-ce-cli containerd.io \ + docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras)) + if [ "${#packages[@]}" -gt 0 ]; then + apt-get purge -y "${packages[@]}" + apt-get autoremove -y --purge else - KEEP=$(safe_prompt "Docker-Daten behalten? (y/n)" "y") + log_info "Keine offiziellen Docker-Pakete gefunden." fi - if [ "$KEEP" = "n" ]; then - rm -rf /var/lib/docker /var/lib/containerd - log_warn "Docker-Daten gelöscht" + remove_repository_files + if confirm_data_removal; then + rm -rf -- "$(system_path /var/lib/docker)" "$(system_path /var/lib/containerd)" + log_warn "Docker-Daten wurden gelöscht." else - log_info "Docker-Daten behalten" - fi - - progress_step "${PHASES[3]}" - - # PHASE 5 – Hinweis - if getent group docker >/dev/null 2>&1; then - log_info "Gruppe 'docker' existiert evtl. noch. Entfernen falls leer: groupdel docker" + log_info "Docker-Daten wurden behalten." fi - progress_step "${PHASES[4]}" - # PHASE 6 – Cleanup - rm -f /etc/systemd/system/docker.service.d/* 2>/dev/null || true - systemctl daemon-reload || true - progress_step "${PHASES[5]}" - - progress_done "Deinstallation abgeschlossen" - log_ok "Docker erfolgreich entfernt" + systemctl daemon-reload + progress_done "Docker wurde erfolgreich deinstalliert." } - -# ------------------------------------------------------------ -# MAIN -# ------------------------------------------------------------ main() { + init_colors parse_arguments "$@" - setup_logging - - # Screen clear - [ "$CLEAR" -eq 1 ] && tput clear 2>/dev/null || true + init_colors - detect_os - - # Self-check - if [ "$ACTION" = "selfcheck" ]; then - self_check - fi - - # No action → Menü oder non-interactive Fehler if [ -z "$ACTION" ]; then if [ "$NON_INTERACTIVE" -eq 1 ]; then - log_err "Kein Modus gewählt und --non-interactive gesetzt." - exit 1 + die "Keine Aktion gewählt und --non-interactive gesetzt." fi show_menu_and_pick_action fi - + require_root + configure_logging + if [ "$CLEAR" -eq 1 ] && is_tty; then + tput clear 2>/dev/null || true + fi - # Execute case "$ACTION" in - install) perform_install ;; - upgrade) perform_upgrade ;; + install) perform_install ;; + upgrade) perform_upgrade ;; uninstall) perform_uninstall ;; - *) log_err "Unbekannte Aktion: $ACTION"; exit 1 ;; + selfcheck) self_check ;; esac - - docker_status_report } -main "$@" \ No newline at end of file +trap 'on_error "$?" "$LINENO"' ERR +trap progress_cleanup EXIT +main "$@" diff --git a/tests/integration.sh b/tests/integration.sh new file mode 100755 index 0000000..a1c71c1 --- /dev/null +++ b/tests/integration.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1; pwd)" +SCRIPT="$REPO_DIR/dockerinstall.sh" +MOCK_SOURCE="$REPO_DIR/tests/mock-command.sh" +TEST_DIR="$(mktemp -d)" +MOCK_BIN="$TEST_DIR/bin" +MOCK_LOG="$TEST_DIR/commands.log" + +cleanup() { + if [ "${KEEP_TEST_DIR:-0}" = "1" ]; then + printf 'Test directory retained: %s\n' "$TEST_DIR" >&2 + else + rm -rf "$TEST_DIR" + fi +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_file() { + [ -f "$1" ] || fail "Expected file: $1" +} + +assert_contains() { + grep -Fq "$2" "$1" || fail "Expected '$2' in $1" +} + +assert_not_contains() { + if grep -Fq "$2" "$1"; then + fail "Did not expect '$2' in $1" + fi +} + +make_mocks() { + mkdir -p "$MOCK_BIN" + local command + for command in apt-get curl dpkg dpkg-query systemctl docker usermod; do + ln -s "$MOCK_SOURCE" "$MOCK_BIN/$command" + done + : > "$MOCK_LOG" +} + +make_root() { + local root="$1" id="$2" version="$3" codename="$4" + mkdir -p "$root/etc/apt/sources.list.d" "$root/etc/apt/keyrings" \ + "$root/etc/docker" "$root/var/lib/docker" "$root/var/lib/containerd" \ + "$root/var/log" "$root/var/backups" + printf 'ID=%s\nVERSION_ID="%s"\nVERSION_CODENAME=%s\n' \ + "$id" "$version" "$codename" > "$root/etc/os-release" + printf '%s\n' 'legacy' > "$root/etc/apt/sources.list.d/docker.list" + printf '%s\n' 'legacy-key' > "$root/etc/apt/keyrings/docker.gpg" +} + +run_script() { + local root="$1" + shift + DOCKERINSTALL_TEST_MODE=1 \ + DOCKERINSTALL_ROOT="$root" \ + DOCKERINSTALL_BACKUP_DIR="$root/var/backups" \ + MOCK_LOG="$MOCK_LOG" \ + PATH="$MOCK_BIN:$PATH" \ + "$SCRIPT" "$@" --no-clear --no-color +} + +expect_failure() { + if "$@" >/dev/null 2>&1; then + fail "Command unexpectedly succeeded: $*" + fi +} + +test_cli_validation() { + expect_failure "$SCRIPT" install --unknown-option + expect_failure "$SCRIPT" install upgrade + expect_failure "$SCRIPT" uninstall --remove-data --keep-data + expect_failure "$SCRIPT" install --add-user + expect_failure "$SCRIPT" install --backup-data + expect_failure "$SCRIPT" upgrade --add-user nobody + [ "$($SCRIPT --version)" = "2.1.0" ] || fail 'Unexpected version output' +} + +test_supported_os_matrix() { + local specification id version codename root + for specification in \ + 'debian 11 bullseye' \ + 'debian 12 bookworm' \ + 'debian 13 trixie' \ + 'ubuntu 22.04 jammy' \ + 'ubuntu 24.04 noble' \ + 'ubuntu 25.10 questing' \ + 'ubuntu 26.04 resolute'; do + # Fields are controlled test data and intentionally split on spaces. + # shellcheck disable=SC2086 + set -- $specification + id="$1" version="$2" codename="$3" + root="$TEST_DIR/matrix-${id}-${version}" + make_root "$root" "$id" "$version" "$codename" + run_script "$root" --self-check + done + + root="$TEST_DIR/unsupported" + make_root "$root" debian 10 buster + expect_failure run_script "$root" --self-check +} + +test_debian_install() { + local root="$TEST_DIR/debian" + make_root "$root" debian 13 trixie + run_script "$root" install --non-interactive + + assert_file "$root/etc/apt/sources.list.d/docker.sources" + assert_file "$root/etc/apt/keyrings/docker.asc" + [ ! -e "$root/etc/apt/sources.list.d/docker.list" ] || fail 'Legacy source was not removed' + [ ! -e "$root/etc/apt/keyrings/docker.gpg" ] || fail 'Legacy key was not removed' + assert_contains "$root/etc/apt/sources.list.d/docker.sources" 'URIs: https://download.docker.com/linux/debian' + assert_contains "$root/etc/apt/sources.list.d/docker.sources" 'Suites: trixie' + assert_contains "$MOCK_LOG" 'apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin' + assert_contains "$MOCK_LOG" 'docker run --rm hello-world' +} + +test_ubuntu_install_and_user_argument() { + local root="$TEST_DIR/ubuntu" + local current_user + current_user="$(id -un)" + make_root "$root" ubuntu 24.04 noble + run_script "$root" install --non-interactive --no-hello --add-user "$current_user" + + assert_contains "$root/etc/apt/sources.list.d/docker.sources" 'URIs: https://download.docker.com/linux/ubuntu' + assert_contains "$root/etc/apt/sources.list.d/docker.sources" 'Suites: noble' + assert_contains "$MOCK_LOG" "usermod -aG docker $current_user" + assert_not_contains "$MOCK_LOG" 'docker run --rm hello-world' +} + +test_upgrade_backup_and_force() { + local root="$TEST_DIR/upgrade" + make_root "$root" debian 12 bookworm + printf '%s\n' data > "$root/var/lib/docker/marker" + run_script "$root" upgrade --backup-data --force-upgrade --non-interactive --no-hello + + find "$root/var/backups" -name 'docker-backup-*.tar.gz' -type f | grep -q . || \ + fail 'Backup archive was not created' + assert_contains "$MOCK_LOG" 'systemctl stop docker.service docker.socket containerd.service' + assert_contains "$MOCK_LOG" 'apt-get install -y --reinstall docker-ce' +} + +test_uninstall_data_safety() { + local keep_root="$TEST_DIR/uninstall-keep" + local remove_root="$TEST_DIR/uninstall-remove" + make_root "$keep_root" debian 13 trixie + printf '%s\n' keep > "$keep_root/var/lib/docker/marker" + MOCK_INSTALLED_PACKAGES='docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras' \ + run_script "$keep_root" uninstall --non-interactive + assert_file "$keep_root/var/lib/docker/marker" + assert_contains "$MOCK_LOG" 'docker-ce-rootless-extras' + + make_root "$remove_root" debian 13 trixie + printf '%s\n' remove > "$remove_root/var/lib/docker/marker" + run_script "$remove_root" uninstall --remove-data --non-interactive + [ ! -e "$remove_root/var/lib/docker" ] || fail 'Docker data was not removed explicitly' +} + +make_mocks +test_cli_validation +test_supported_os_matrix +test_debian_install +: > "$MOCK_LOG" +test_ubuntu_install_and_user_argument +: > "$MOCK_LOG" +test_upgrade_backup_and_force +: > "$MOCK_LOG" +test_uninstall_data_safety + +printf '%s\n' 'All integration tests passed.' diff --git a/tests/mock-command.sh b/tests/mock-command.sh new file mode 100755 index 0000000..464190c --- /dev/null +++ b/tests/mock-command.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +command_name="" +command_name="$(basename "$0")" +printf '%s %s\n' "$command_name" "$*" >> "${MOCK_LOG:?MOCK_LOG is required}" + +case "$command_name" in + apt-get) + exit 0 + ;; + curl) + output="" + while [ "$#" -gt 0 ]; do + case "$1" in + -o) + shift + output="${1:-}" + ;; + esac + shift + done + [ -n "$output" ] || exit 2 + printf '%s\n' 'mock docker signing key' > "$output" + ;; + dpkg) + [ "${1:-}" = "--print-architecture" ] || exit 2 + printf '%s\n' "${MOCK_ARCH:-amd64}" + ;; + dpkg-query) + package="${*: -1}" + case " ${MOCK_INSTALLED_PACKAGES:-} " in + *" $package "*) printf 'ii '\''\n' ;; + *) exit 1 ;; + esac + ;; + systemctl) + if [ "${1:-}" = "is-active" ]; then + [ "${MOCK_DOCKER_RUNNING:-1}" = "1" ] + fi + ;; + docker) + case "${1:-}" in + --version) printf '%s\n' 'Docker version 29.0.0, build mock' ;; + compose) printf '%s\n' 'Docker Compose version v2.mock' ;; + buildx) printf '%s\n' 'github.com/docker/buildx v0.mock' ;; + info) exit 0 ;; + run) printf '%s\n' 'Hello from Docker!' ;; + *) exit 2 ;; + esac + ;; + usermod) + exit 0 + ;; + *) + printf 'Unsupported mock command: %s\n' "$command_name" >&2 + exit 2 + ;; +esac From f38a676459c69e579a97130dce8a5ef5e8103ecf Mon Sep 17 00:00:00 2001 From: Foxly Date: Mon, 13 Jul 2026 11:32:19 +0200 Subject: [PATCH 2/2] Fix ShellCheck OS validation warning --- dockerinstall.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dockerinstall.sh b/dockerinstall.sh index 662e83d..fd0aeea 100755 --- a/dockerinstall.sh +++ b/dockerinstall.sh @@ -232,8 +232,9 @@ detect_os() { OS_CODENAME="${VERSION_CODENAME:-}" fi - [ -n "$OS_VERSION" ] && [ -n "$OS_CODENAME" ] || \ + if [ -z "$OS_VERSION" ] || [ -z "$OS_CODENAME" ]; then die "Distribution oder Codename konnte nicht erkannt werden." + fi case "${OS_ID}:${OS_VERSION}" in debian:11|debian:12|debian:13|ubuntu:22.04|ubuntu:24.04|ubuntu:25.10|ubuntu:26.04) ;;