#!/usr/bin/env bash # ----------------------------------------------------------------------------- # Globale Einstellungen & UI-Theme # ----------------------------------------------------------------------------- set -euo pipefail IFS=$'\n\t' export DIALOGRC=$HOME/.dialogrc if [ ! -f "$DIALOGRC" ]; then cat > "$DIALOGRC" < "$GLOBAL_CONFIG"; fi } get_glob() { jq -r "$1" "$GLOBAL_CONFIG" 2>/dev/null || echo "nano"; } # Sicherer Toggle ohne Sonderzeichen-Probleme toggle_json_bool() { local key=$1; local file=$2 jq --arg k "$key" '.[$k] = if .[$k] == true then false else true end' "$file" > "$file.tmp" && mv "$file.tmp" "$file" } # ----------------------------- # Hilfsfunktion: sichere Strings für dialog # ----------------------------- safe() { echo "$1" | tr -d '\n\r' | sed 's/"/'"'"'/g' } # ----------------------------------------------------------------------------- # System & Registries (Bleiben unverändert) # ----------------------------------------------------------------------------- system_config_menu() { while true; do choice=$(dialog --title " System " --menu "Aktionen:" $W_H $W_W $M_H 1 "Pakete installieren" 2 "Docker/Buildx Setup" 3 "Editor" 0 "Zurueck" 3>&1 1>&2 2>&3) || return case $choice in 1) clear; sudo apt update && sudo apt install -y git jq dialog curl docker.io; read -p "Enter..." ;; 2) clear; sudo docker run --rm --privileged tonistiigi/binfmt --install all; docker buildx create --name multiarch-builder --use || docker buildx use multiarch-builder; docker buildx inspect --bootstrap; sleep 2 ;; 3) ed=$(dialog --inputbox "Editor:" 10 60 "$(get_glob '.editor')" 3>&1 1>&2 2>&3) && jq --arg e "$ed" '.editor=$e' "$GLOBAL_CONFIG" > "$GLOBAL_CONFIG.tmp" && mv "$GLOBAL_CONFIG.tmp" "$GLOBAL_CONFIG" ;; 0) return ;; esac done } registry_menu() { while true; do local regs=$(ls "$REGISTRY_CONFIG_DIR" 2>/dev/null | sed 's/\.json//' || true) local list=(); for r in $regs; do list+=("$r" "Registry"); done choice=$(dialog --title " Registries " --menu "Liste:" $W_H $W_W $M_H "ADD" "[+] Neu" "${list[@]}" "0" "Zurueck" 3>&1 1>&2 2>&3) || return if [ "$choice" = "0" ]; then return; fi if [ "$choice" = "ADD" ]; then local n=$(dialog --inputbox "Name:" 10 60 3>&1 1>&2 2>&3) || continue local url=$(dialog --inputbox "URL (leer f. Dockerhub):" 10 60 3>&1 1>&2 2>&3) || continue local user=$(dialog --inputbox "User:" 10 60 3>&1 1>&2 2>&3) || continue local pass=$(dialog --passwordbox "Pass:" 10 60 3>&1 1>&2 2>&3) || continue echo "{\"name\":\"$n\",\"url\":\"$url\",\"user\":\"$user\",\"pass\":\"$pass\"}" > "$REGISTRY_CONFIG_DIR/$n.json" else sub=$(dialog --menu "Aktion: $choice" 12 60 4 "EDIT" "Bearbeiten" "DEL" "Loeschen" "0" "Abbruch" 3>&1 1>&2 2>&3) || continue if [ "$sub" = "EDIT" ]; then $(get_glob '.editor') "$REGISTRY_CONFIG_DIR/$choice.json"; fi if [ "$sub" = "DEL" ]; then rm "$REGISTRY_CONFIG_DIR/$choice.json"; fi fi done } # ----------------------------------------------------------------------------- # Lokale Docker Images verwalten # ----------------------------------------------------------------------------- local_images_menu() { while true; do # Images auflisten (Repo:Tag) mapfile -t images < <(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '' || true) if [ ${#images[@]} -eq 0 ]; then dialog --msgbox "Keine lokalen Images gefunden." 8 40 return fi img_list=() for img in "${images[@]}"; do arch=$(docker inspect --format '{{.Architecture}}' "$img" 2>/dev/null || echo "unknown") img_list+=("$img" "$arch") done sel_img=$(dialog --title " Lokale Images " \ --menu "Waehle Image:" $W_H $W_W $M_H \ "${img_list[@]}" "0" "Zurueck" \ 3>&1 1>&2 2>&3) || return [ "$sel_img" = "0" ] && return action=$(dialog --menu "Aktion fuer $sel_img:" 12 60 5 \ "INFOS" "Informationen zum Image anzeigen" \ "TAG" "Neuen Tag erstellen" \ "RMTAG" "Diesen Tag loeschen" \ "RMIMG" "Ganzes Image loeschen" \ "0" "Abbruch" \ 3>&1 1>&2 2>&3) || continue case $action in INFOS) clear docker inspect "$sel_img" | less ;; TAG) new_tag=$(dialog --inputbox "Neuer vollstaendiger Tag (repo:tag):" 10 70 \ "$sel_img" 3>&1 1>&2 2>&3) || continue clear docker tag "$sel_img" "$new_tag" && \ dialog --msgbox "Tag erstellt:\n$new_tag" 8 50 ;; RMTAG) if dialog --yesno "Tag wirklich loeschen?\n$sel_img" 8 50; then clear docker rmi "$sel_img" && \ dialog --msgbox "Tag geloescht." 6 30 fi ;; RMIMG) repo="${sel_img%%:*}" if dialog --yesno "ALLE Tags von $repo loeschen?" 8 60; then clear docker images "$repo" --format '{{.Repository}}:{{.Tag}}' \ | xargs -r docker rmi && \ dialog --msgbox "Image $repo entfernt." 6 40 fi ;; 0) ;; esac done } # ----------------------------------------------------------------------------- # Projekt-Verwaltung (DIE FIX-VERSION) # ----------------------------------------------------------------------------- project_config_submenu() { p_name="${1##*/}" # sicherer als basename cfg="$PROJECT_CONFIG_DIR/${p_name}.json" if [ ! -f "$cfg" ]; then dialog --msgbox "Projekt-Konfig nicht gefunden:\n$cfg" 8 60 return fi while true; do if [ ! -f "$cfg" ]; then return; fi # EXPLIZITES EINLESEN OHNE FALLBACKS IN DER VARIABLE # Wir lesen direkt 'true' oder 'false' als String local img ver regs archs img="$(jq -r '.image_name // ""' "$cfg")" ver="$(jq -r '.version // ""' "$cfg")" regs="$(jq -r '.registries // [] | join(",")' "$cfg")" archs="$(jq -r '.architectures // [] | join(",")' "$cfg")" # Hier liegt der Fix: Wir erzwingen die Auswertung des Booleans local cur_push cur_ainc cur_cache cur_latest cur_force_local cur_pull_after cur_push="$(jq -r '.push // false' "$cfg")" cur_ainc="$(jq -r '.auto_inc // false' "$cfg")" cur_cache="$(jq -r '.use_cache // true' "$cfg")" cur_latest="$(jq -r '.use_latest // false' "$cfg")" cur_force_local="$(jq -r '.force_local // false' "$cfg")" cur_pull_after="$(jq -r '.pull_after_push // false' "$cfg")" local MENU_ITEMS=15 local MENU_H=$(( M_H < MENU_ITEMS ? M_H : MENU_ITEMS )) choice=$(dialog --title " Projekt: $(safe "$p_name") " \ --menu "Konfiguration (Werte werden in [ ] angezeigt):" $W_H $W_W $MENU_H \ "1" "Image Name: $(safe "$img")" \ "2" "Version: $(safe "$ver")" \ "3" "Registries: $(safe "$regs")" \ "4" "Architekturen: $(safe "$archs")" \ "5" "Push n. Build: [$cur_push]" \ "6" "Auto-Increment: [$cur_ainc]" \ "7" "Build-Cache: [$cur_cache]" \ "8" "Latest-Tag: [$cur_latest]" \ "9" "Nur lokal bauen: [$cur_force_local]" \ "10" "Nach Push pullen: [$cur_pull_after]" \ "F" "Dateimanager" \ "G" "Git Pull" \ "B" "BUILD STARTEN" \ "D" "Projekt loeschen" \ "0" "Zurueck" \ 3>&1 1>&2 2>&3) || return case $choice in 1) val=$(dialog --inputbox "Name:" 10 60 "$img" 3>&1 1>&2 2>&3) && jq --arg v "$val" '.image_name=$v' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg" ;; 2) val=$(dialog --inputbox "Version:" 10 60 "$ver" 3>&1 1>&2 2>&3) && jq --arg v "$val" '.version=$v' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg" ;; 3) all_regs=$(ls "$REGISTRY_CONFIG_DIR" 2>/dev/null | sed 's/\.json//' || true) reg_list=(); for r in $all_regs; do is_on="OFF"; if [[ "$regs" == *"$r"* ]]; then is_on="ON"; fi; reg_list+=("$r" "Registry" "$is_on"); done sel_regs=$(dialog --separate-output --checklist "Registries:" 20 60 10 "${reg_list[@]}" 3>&1 1>&2 2>&3) || true jq --argjson r "$(echo "$sel_regs" | jq -R . | jq -s .)" '.registries=$r' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg" ;; 4) a_sel=$(dialog --separate-output --checklist "Plattformen:" 12 60 4 "amd64" "x64" ON "arm64" "ARM64" OFF 3>&1 1>&2 2>&3) || true jq --argjson a "$(echo "$a_sel" | jq -R . | jq -s .)" '.architectures=$a' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg" ;; 5) toggle_json_bool "push" "$cfg" ;; 6) toggle_json_bool "auto_inc" "$cfg" ;; 7) toggle_json_bool "use_cache" "$cfg" ;; 8) toggle_json_bool "use_latest" "$cfg" ;; 9) toggle_json_bool "force_local" "$cfg" ;; 10) toggle_json_bool "pull_after_push" "$cfg" ;; F) files=$(find "$PROJECTS_DIR/$p_name" -maxdepth 2 -not -path '*/.*' -type f) f_list=(); for f in $files; do f_list+=("$f" "$(basename "$f")"); done if [ ${#f_list[@]} -gt 0 ]; then sel_f=$(dialog --menu "Datei:" $W_H $W_W $M_H "${f_list[@]}" 3>&1 1>&2 2>&3) && $(get_glob '.editor') "$sel_f"; fi ;; G) clear; cd "$PROJECTS_DIR/$p_name" && git pull || true; sleep 2 ;; B) build_image "$p_name" ;; D) if dialog --yesno "Loeschen?" 8 40; then rm "$cfg"; return; fi ;; 0) return ;; esac done } # ----------------------------------------------------------------------------- # Build & Hauptmenue (Rest bleibt stabil wie v2.9) # ----------------------------------------------------------------------------- build_image() { local force_local=$(jq -r '.force_local' "$cfg") local pull_after_push=$(jq -r '.pull_after_push' "$cfg") local p_name=$1; local cfg="$PROJECT_CONFIG_DIR/$p_name.json" readarray -t selected_regs < <(jq -r '.registries[]' "$cfg") [ ${#selected_regs[@]} -eq 0 ] && dialog --msgbox "Keine Registry gewaehlt!" 8 40 && return local arch_count=$(jq '.architectures | length' "$cfg") local push_enabled=$(jq -r '.push' "$cfg") local platforms=$(jq -r '.architectures | map("linux/" + .) | join(",")' "$cfg") local use_latest=$(jq -r '.use_latest' "$cfg") local push_f="--load" if [ "$force_local" = "true" ]; then push_f="--load" elif [ "$arch_count" -gt 1 ] || [ "$push_enabled" = "true" ]; then push_f="--push" fi clear tag_args=() for r_name in "${selected_regs[@]}"; do r_file="$REGISTRY_CONFIG_DIR/$r_name.json" [ ! -f "$r_file" ] && continue u=$(jq -r '.user' "$r_file"); p=$(jq -r '.pass' "$r_file"); url=$(jq -r '.url' "$r_file") echo "Login bei $r_name..." if [ -z "$url" ] || [ "$url" = "null" ]; then echo "$p" | docker login -u "$u" --password-stdin else echo "$p" | docker login "$url" -u "$u" --password-stdin; fi local raw_img=$(jq -r '.image_name' "$cfg") local tagged_img="$raw_img" if [[ "$raw_img" != *"/"* ]]; then tagged_img="$u/$raw_img"; fi local ver=$(jq -r '.version' "$cfg") if [ -z "$url" ] || [ "$url" = "null" ]; then full_path="$tagged_img" else url_clean=$(echo "$url" | sed 's|https://||; s|http://||; s|/$||'); full_path="$url_clean/$tagged_img"; fi tag_args+=("-t" "$full_path:$ver") [ "$use_latest" = "true" ] && tag_args+=("-t" "$full_path:latest") done cache_f=""; [ "$(jq -r '.use_cache' "$cfg")" = "false" ] && cache_f="--no-cache" logfile="$LOGS_DIR/${p_name}_$(date +%Y%m%d_%H%M).log" if docker buildx build --platform "$platforms" "${tag_args[@]}" $cache_f $push_f "$PROJECTS_DIR/$p_name" 2>&1 | tee "$logfile"; then if [ "$push_f" = "--push" ] && [ "$pull_after_push" = "true" ]; then echo "Pull lokale Images..." for img in "${tag_args[@]}"; do [[ "$img" == "-t" ]] && continue docker pull "$img" || true done fi if [ "$(jq -r '.auto_inc' "$cfg")" = "true" ]; then n_v=$(echo "$ver" | awk -F. '{$NF = $NF + 1;} 1' OFS=.) jq --arg nv "$n_v" '.version=$nv' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg" fi echo -e "\nERFOLG!" else echo -e "\nFEHLER!"; fi read -p "Enter..." } create_project() { n=$(dialog --inputbox "Name:" 10 60 3>&1 1>&2 2>&3) || return [ -z "$n" ] && return git_url="" if ! git_url=$(dialog --inputbox "Git URL (optional):" 10 70 3>&1 1>&2 2>&3); then git_url="" fi mkdir -p "$PROJECTS_DIR/$n" if [ -n "$git_url" ]; then dialog --infobox "Clone Repository...\n$git_url" 6 70 mkdir -p "$PROJECTS_DIR/$n" if ! git clone "$git_url" "$PROJECTS_DIR/$n" >/tmp/git_clone.log 2>&1; then dialog --textbox /tmp/git_clone.log 20 80 return fi sleep 1 reset fi echo "{ \"name\":\"$n\", \"registries\":[], \"image_name\":\"$n\", \"architectures\":[\"amd64\"], \"push\":false, \"version\":\"1.0.0\", \"auto_inc\":false, \"use_cache\":true, \"use_latest\":true, \"force_local\": false, \"pull_after_push\": false }" > "$PROJECT_CONFIG_DIR/$n.json" dialog --msgbox "Projekt '$n' wurde angelegt." 7 50 return } main_menu() { while true; do choice=$(dialog --title " Image Builder v3.0 " --menu "Menue:" $W_H $W_W $M_H 1 "Projekte" 2 "Registries" 3 "System" 4 "Logs" 5 "Lokale Images" 0 "Beenden" 3>&1 1>&2 2>&3) || exit 0 case $choice in 1) projs=$(ls "$PROJECT_CONFIG_DIR" 2>/dev/null | sed 's/\.json//' || true) p_list=(); for p in $projs; do p_list+=("$p" "Projekt"); done if ! p_sel=$(dialog --menu "Waehle:" $W_H $W_W $M_H "NEW" "[+] Neu" "${p_list[@]}" "0" "Zurueck" 3>&1 1>&2 2>&3); then continue fi if [ "$p_sel" = "NEW" ]; then create_project; elif [ "$p_sel" != "0" ]; then project_config_submenu "$p_sel"; fi ;; 2) registry_menu ;; 3) system_config_menu ;; 4) ls_l=$(ls -t "$LOGS_DIR" 2>/dev/null | head -n 10 || true) l_list=(); for l in $ls_l; do l_list+=("$l" "Log"); done if [ ${#l_list[@]} -gt 0 ]; then sel_l=$(dialog --menu "Logs:" $W_H $W_W $M_H "${l_list[@]}" 3>&1 1>&2 2>&3) && $(get_glob '.editor') "$LOGS_DIR/$sel_l"; fi ;; 5) local_images_menu ;; 0) clear; exit 0 ;; esac done } ensure_dirs; main_menu