481 lines
11 KiB
Bash
481 lines
11 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
# Script Name: docker-image-manager.v1.sh
|
|
# Beschreibung: Docker Image Manager - verwaltet und inspiziert Docker Images
|
|
# - Alle Images auflisten
|
|
# - Ungenutzte Images anzeigen
|
|
# - Zeigt zugeordnete Container pro Image
|
|
# - Images interaktiv loeschen
|
|
# Autor: Patrick Asmus
|
|
# Web: https://www.cleveradmin.de
|
|
# Git-Reposit.: https://git.techniverse.net/scriptos/docker-image-manager
|
|
# Version: 1.0.0
|
|
# Datum: 18.02.2026
|
|
# Modifikation: Initialer Release (v1.0.0)
|
|
#####################################################
|
|
|
|
LAST_IDS=()
|
|
LAST_LABELS=()
|
|
declare -A IMAGE_TO_CONTAINERS=()
|
|
SELECTED_IDS=()
|
|
SELECTED_LABELS=()
|
|
|
|
require_docker() {
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "Docker CLI nicht gefunden. Bitte Docker installieren und PATH pruefen." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! docker info >/dev/null 2>&1; then
|
|
echo "Docker Daemon nicht erreichbar. Bitte Docker starten." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
load_container_map() {
|
|
IMAGE_TO_CONTAINERS=()
|
|
|
|
local -a lines
|
|
mapfile -t lines < <(docker ps -a -q | xargs -r docker inspect --format '{{.Id}}|{{.Image}}|{{.Name}}')
|
|
|
|
if [[ ${#lines[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local line image_id name
|
|
for line in "${lines[@]}"; do
|
|
IFS='|' read -r _ image_id name <<< "$line"
|
|
name=${name#/}
|
|
if [[ -n "${IMAGE_TO_CONTAINERS[$image_id]:-}" ]]; then
|
|
IMAGE_TO_CONTAINERS[$image_id]="${IMAGE_TO_CONTAINERS[$image_id]},${name}"
|
|
else
|
|
IMAGE_TO_CONTAINERS[$image_id]="$name"
|
|
fi
|
|
done
|
|
}
|
|
|
|
print_images_table() {
|
|
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
|
|
echo "Keine Images gefunden."
|
|
return 0
|
|
fi
|
|
|
|
load_container_map
|
|
|
|
printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "Nr." "REPOSITORY" "TAG" "IMAGE ID" "SIZE" "CREATED" "CONTAINERS"
|
|
printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "----" "------------------------------" "--------------------" "---------------" "------------" "--------------------" "------------------------------"
|
|
|
|
for i in "${!LAST_IDS[@]}"; do
|
|
IFS='|' read -r repo tag short_id size created <<< "${LAST_LABELS[$i]}"
|
|
local containers="${IMAGE_TO_CONTAINERS[${LAST_IDS[$i]}]:-}"
|
|
if [[ -z "$containers" ]]; then
|
|
containers="-"
|
|
fi
|
|
printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "$((i + 1))" "$repo" "$tag" "$short_id" "$size" "$created" "$containers"
|
|
done
|
|
}
|
|
|
|
load_images_from_lines() {
|
|
local -a lines=("$@")
|
|
LAST_IDS=()
|
|
LAST_LABELS=()
|
|
|
|
if [[ ${#lines[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local line id repo tag created size short_id
|
|
for line in "${lines[@]}"; do
|
|
IFS='|' read -r id repo tag created size <<< "$line"
|
|
short_id=${id:0:12}
|
|
LAST_IDS+=("$id")
|
|
LAST_LABELS+=("$repo|$tag|$short_id|$size|$created")
|
|
done
|
|
}
|
|
|
|
list_all_images() {
|
|
local -a lines
|
|
mapfile -t lines < <(docker images --no-trunc --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.CreatedSince}}|{{.Size}}')
|
|
load_images_from_lines "${lines[@]}"
|
|
print_images_table
|
|
}
|
|
|
|
list_unused_images() {
|
|
local used_file
|
|
used_file=$(mktemp)
|
|
|
|
if docker ps -a -q >/dev/null 2>&1; then
|
|
docker ps -a -q | xargs -r docker inspect --format '{{.Image}}' | sort -u > "$used_file"
|
|
fi
|
|
|
|
local -a lines
|
|
mapfile -t lines < <(docker images --no-trunc --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.CreatedSince}}|{{.Size}}')
|
|
|
|
LAST_IDS=()
|
|
LAST_LABELS=()
|
|
|
|
local line id repo tag created size short_id
|
|
for line in "${lines[@]}"; do
|
|
IFS='|' read -r id repo tag created size <<< "$line"
|
|
if [[ ! -s "$used_file" ]] || ! grep -Fxq "$id" "$used_file"; then
|
|
short_id=${id:0:12}
|
|
LAST_IDS+=("$id")
|
|
LAST_LABELS+=("$repo|$tag|$short_id|$size|$created")
|
|
fi
|
|
done
|
|
|
|
rm -f "$used_file"
|
|
print_images_table
|
|
}
|
|
|
|
confirm_action() {
|
|
local prompt="$1"
|
|
local answer
|
|
read -r -e -p "$prompt [y/N]: " answer
|
|
case "$answer" in
|
|
y|Y|yes|YES) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
select_images_by_numbers() {
|
|
local -a numbers=()
|
|
local input="${1:-}"
|
|
|
|
SELECTED_IDS=()
|
|
SELECTED_LABELS=()
|
|
|
|
if [[ -z "$input" ]]; then
|
|
read -r -e -p "Nummern der Images (z.B. 1,3,5) oder Enter fuer Abbruch: " input
|
|
fi
|
|
|
|
if [[ -z "$input" ]]; then
|
|
echo "Abgebrochen."
|
|
return 1
|
|
fi
|
|
|
|
input=${input//,/ }
|
|
for token in $input; do
|
|
if [[ "$token" =~ ^[0-9]+$ ]]; then
|
|
numbers+=("$token")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#numbers[@]} -eq 0 ]]; then
|
|
echo "Keine gueltigen Nummern angegeben."
|
|
return 1
|
|
fi
|
|
|
|
local idx
|
|
for idx in "${numbers[@]}"; do
|
|
if (( idx < 1 || idx > ${#LAST_IDS[@]} )); then
|
|
echo "Nummer $idx ist ungueltig."
|
|
return 1
|
|
fi
|
|
SELECTED_IDS+=("${LAST_IDS[$((idx - 1))]}")
|
|
SELECTED_LABELS+=("${LAST_LABELS[$((idx - 1))]}")
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
print_selected_images() {
|
|
if [[ ${#SELECTED_IDS[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "Nr." "REPOSITORY" "TAG" "IMAGE ID" "SIZE" "CREATED"
|
|
printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "----" "------------------------------" "--------------------" "---------------" "------------" "--------------------"
|
|
|
|
local i
|
|
for i in "${!SELECTED_IDS[@]}"; do
|
|
IFS='|' read -r repo tag short_id size created <<< "${SELECTED_LABELS[$i]}"
|
|
printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "$((i + 1))" "$repo" "$tag" "$short_id" "$size" "$created"
|
|
done
|
|
}
|
|
|
|
remove_image_by_id() {
|
|
local id="$1"
|
|
local -a used
|
|
mapfile -t used < <(docker ps -a --filter "ancestor=$id" --format '{{.ID}}|{{.Names}}')
|
|
if [[ ${#used[@]} -gt 0 ]]; then
|
|
echo "Ueberspringe Image (in Benutzung durch Container): $id"
|
|
local entry
|
|
for entry in "${used[@]}"; do
|
|
echo "- ${entry#*|} (${entry%%|*})"
|
|
done
|
|
return
|
|
fi
|
|
|
|
local -a tags=()
|
|
mapfile -t tags < <(docker image inspect "$id" --format '{{range .RepoTags}}{{println .}}{{end}}')
|
|
|
|
if [[ ${#tags[@]} -eq 0 ]]; then
|
|
echo "Loesche Image: $id"
|
|
docker rmi "$id"
|
|
return
|
|
fi
|
|
|
|
local tag
|
|
local -a tag_list=()
|
|
for tag in "${tags[@]}"; do
|
|
if [[ "$tag" == "<none>:<none>" || -z "$tag" ]]; then
|
|
continue
|
|
fi
|
|
tag_list+=("$tag")
|
|
done
|
|
|
|
if [[ ${#tag_list[@]} -eq 0 ]]; then
|
|
echo "Loesche Image: $id"
|
|
docker rmi "$id"
|
|
return
|
|
fi
|
|
|
|
for tag in "${tag_list[@]}"; do
|
|
echo "Loesche Tag: $tag"
|
|
docker rmi "$tag"
|
|
done
|
|
|
|
if docker image inspect "$id" >/dev/null 2>&1; then
|
|
echo "Loesche Image: $id"
|
|
docker rmi "$id"
|
|
fi
|
|
}
|
|
|
|
delete_selected_images() {
|
|
local -A seen=()
|
|
local id
|
|
for id in "${SELECTED_IDS[@]}"; do
|
|
if [[ -n "${seen[$id]:-}" ]]; then
|
|
continue
|
|
fi
|
|
seen[$id]=1
|
|
remove_image_by_id "$id"
|
|
done
|
|
}
|
|
|
|
inspect_image_by_number() {
|
|
local num="${1:-}"
|
|
if [[ -z "$num" ]]; then
|
|
read -r -e -p "Nummer des Images: " num
|
|
fi
|
|
|
|
if ! [[ "$num" =~ ^[0-9]+$ ]]; then
|
|
echo "Ungueltige Nummer."
|
|
return 1
|
|
fi
|
|
|
|
if (( num < 1 || num > ${#LAST_IDS[@]} )); then
|
|
echo "Nummer $num ist ungueltig."
|
|
return 1
|
|
fi
|
|
|
|
local id
|
|
id="${LAST_IDS[$((num - 1))]}"
|
|
|
|
docker image inspect "$id" --format \
|
|
$'ID: {{.Id}}\nRepoTags: {{.RepoTags}}\nCreated: {{.Created}}\nSize: {{.Size}}\nArchitecture: {{.Architecture}}\nOS: {{.Os}}\nLabels: {{json .Config.Labels}}'
|
|
|
|
echo
|
|
echo "Containers:"
|
|
local -a lines
|
|
mapfile -t lines < <(docker ps -a --filter "ancestor=$id" --format '{{.ID}}|{{.Names}}|{{.Status}}')
|
|
if [[ ${#lines[@]} -eq 0 ]]; then
|
|
echo "- keine"
|
|
return 0
|
|
fi
|
|
|
|
local line cid name status
|
|
for line in "${lines[@]}"; do
|
|
IFS='|' read -r cid name status <<< "$line"
|
|
echo "- $name ($cid) - $status"
|
|
done
|
|
}
|
|
|
|
purge_images() {
|
|
local mode="$1"
|
|
local force_yes="$2"
|
|
|
|
case "$mode" in
|
|
unused)
|
|
list_unused_images
|
|
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
;;
|
|
*)
|
|
echo "Unbekannter Modus: $mode"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$force_yes" != "true" ]]; then
|
|
if ! confirm_action "Alle aufgelisteten Images loeschen?"; then
|
|
echo "Abgebrochen."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local -A seen=()
|
|
local id
|
|
for id in "${LAST_IDS[@]}"; do
|
|
if [[ -n "${seen[$id]:-}" ]]; then
|
|
continue
|
|
fi
|
|
seen[$id]=1
|
|
remove_image_by_id "$id"
|
|
done
|
|
}
|
|
|
|
print_help() {
|
|
echo "Docker Image Manager"
|
|
echo
|
|
echo "Verwendung:"
|
|
echo " bash ./docker-image-manager.v1.sh [BEFEHL] [OPTIONS]"
|
|
echo
|
|
echo "Befehle:"
|
|
echo " list - Alle Docker Images auflisten"
|
|
echo " unused-images - Images anzeigen, die von keinem Container genutzt werden"
|
|
echo " inspect - Details zu einem Image anzeigen (inkl. Containerliste)"
|
|
echo " delete - Einzelne Images nach Nummer loeschen"
|
|
echo " purge-unused - Alle ungenutzten Images loeschen"
|
|
echo " help, -h, --help - Diese Hilfe anzeigen"
|
|
echo
|
|
echo "Optionen:"
|
|
echo " --yes - Loeschen ohne Rueckfrage"
|
|
}
|
|
|
|
interactive_menu() {
|
|
while true; do
|
|
echo
|
|
echo "Docker Image Manager"
|
|
echo "1) Images anzeigen + Details"
|
|
echo "2) Ungenutzte Images anzeigen"
|
|
echo "3) Image loeschen (per Nummer)"
|
|
echo "4) Alle ungenutzten Images loeschen"
|
|
echo "0) Beenden"
|
|
read -r -e -p "Auswahl: " choice
|
|
|
|
case "$choice" in
|
|
1)
|
|
list_all_images
|
|
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
|
|
continue
|
|
fi
|
|
read -r -e -p "Nummer fuer Details (Enter fuer zurueck): " detail_num
|
|
if [[ -n "$detail_num" ]]; then
|
|
inspect_image_by_number "$detail_num"
|
|
fi
|
|
;;
|
|
2)
|
|
list_unused_images
|
|
;;
|
|
3)
|
|
list_all_images
|
|
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
|
|
continue
|
|
fi
|
|
if ! select_images_by_numbers ""; then
|
|
continue
|
|
fi
|
|
echo "Ausgewaehlte Images:"
|
|
print_selected_images
|
|
if confirm_action "Ausgewaehlte Images loeschen?"; then
|
|
delete_selected_images
|
|
else
|
|
echo "Abgebrochen."
|
|
fi
|
|
;;
|
|
4)
|
|
purge_images "unused" "false"
|
|
;;
|
|
0)
|
|
echo "Tschuess."
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Ungueltige Auswahl."
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
main() {
|
|
require_docker
|
|
|
|
local cmd=""
|
|
local force_yes="false"
|
|
local -a extra_args=()
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--yes)
|
|
force_yes="true"
|
|
;;
|
|
-h|--help|help)
|
|
cmd="help"
|
|
;;
|
|
*)
|
|
if [[ -z "$cmd" ]]; then
|
|
cmd="$arg"
|
|
else
|
|
extra_args+=("$arg")
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$cmd" ]]; then
|
|
interactive_menu
|
|
return 0
|
|
fi
|
|
|
|
case "$cmd" in
|
|
list)
|
|
list_all_images
|
|
;;
|
|
unused-images)
|
|
list_unused_images
|
|
;;
|
|
inspect)
|
|
list_all_images
|
|
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
inspect_image_by_number "${extra_args[0]:-}"
|
|
;;
|
|
delete)
|
|
list_all_images
|
|
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
if ! select_images_by_numbers "${extra_args[*]:-}"; then
|
|
return 1
|
|
fi
|
|
echo "Ausgewaehlte Images:"
|
|
print_selected_images
|
|
if [[ "$force_yes" != "true" ]]; then
|
|
if ! confirm_action "Ausgewaehlte Images loeschen?"; then
|
|
echo "Abgebrochen."
|
|
return 0
|
|
fi
|
|
fi
|
|
delete_selected_images
|
|
;;
|
|
purge-unused)
|
|
purge_images "unused" "$force_yes"
|
|
;;
|
|
help|*)
|
|
print_help
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|