#!/usr/bin/env bash # ============================================================================= # prefetch-images.sh — tarik SEMUA image Docker lebih dulu (agar install offline-ish) # ----------------------------------------------------------------------------- # Daftar image TIDAK di-hardcode: diambil dari `docker compose config --images`, # sehingga selalu sinkron dengan docker-compose.yml (termasuk image yang berada di # balik profil, mis. dremio). # # CATATAN PENTING — satu image dibangun LOKAL: # sicabe/airflow:3.3.2-om203-ol dibangun dari docker/airflow/Dockerfile.332 # (base apache/airflow:slim-3.3.2-python3.11). Ia TIDAK ada di registry publik, # sehingga `pull` dari Docker Hub tidak mungkin. # ✅ SEJAK 2026-10-02 image ini TERSEDIA di REGISTRY INTERNAL: # registry.sicabe.com/airflow:3.3.2-om203-ol # Script ini OTOMATIS ikut menariknya bila `AIRFLOW_IMAGE` menunjuk REGISTRY # (host memuat titik, mis. `registry.sicabe.com/airflow:…`) — cukup set # AIRFLOW_IMAGE=registry.sicabe.com/airflow:3.3.2-om203-ol # di .env. Bila `AIRFLOW_IMAGE` dibiarkan nama LOKAL polos (`sicabe/airflow:…`), # image DIKECUALIKAN dari pull dan wajib dibangun/di-retag manual (alur di bawah). # Untuk host TANPA akses registry, tetap ada alur build-sekali-salin: # 1) di SATU host: ./scripts/prefetch-images.sh # docker build -t sicabe/airflow:3.3.2-om203-ol \ # -f docker/airflow/Dockerfile.332 docker/airflow # ./scripts/prefetch-images.sh --save-airflow /tmp/airflow.tar # 2) salin: scp /tmp/airflow.tar host2:/tmp/ ; scp … host3:/tmp/ # 3) di host lain: ./scripts/prefetch-images.sh --load-airflow /tmp/airflow.tar # # Pemakaian: # ./scripts/prefetch-images.sh [--all | --host ] [--list] [--no-base] # ./scripts/prefetch-images.sh --save-airflow /tmp/airflow.tar # ./scripts/prefetch-images.sh --load-airflow /tmp/airflow.tar # ============================================================================= set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" RED=$'\e[31m'; GRN=$'\e[32m'; YLW=$'\e[33m'; BLD=$'\e[1m'; NC=$'\e[0m' # PENTING: semua diagnostik ke STDERR. stdout hanya untuk DATA (daftar image), # karena collect_images() ditangkap dengan $( ) — pesan info yang ikut tertangkap # akan dianggap nama image (bug yang ditemukan saat menguji --list). ok() { echo -e " ${GRN}[OK]${NC} $*" >&2; } inf() { echo -e " ${YLW}[..]${NC} $*" >&2; } warn() { echo -e " ${YLW}[WARN]${NC} $*" >&2; } err() { echo -e " ${RED}[!!]${NC} $*" >&2; } MODE="all" # all | host HOST_NAME="" LIST_ONLY=0 # Dremio (opt-in di compose), image build lokal, dan base build Airflow DEFAULT # DIKELUARKAN dari daftar prefetch. Ketiganya diaktifkan lewat flag bila memang # diperlukan (mis. deployment yang menjalankan dremio / akan build Airflow). WITH_BASE=0 WITH_DREMIO=0 SAVE_AIRFLOW="" LOAD_AIRFLOW="" usage() { sed -n '2,28p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0; } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --all) MODE="all" ;; --host) MODE="host"; HOST_NAME="${2:-}"; shift ;; --list) LIST_ONLY=1 ;; --no-base) WITH_BASE=0 ;; # dipertahankan: kini defaultnya sudah tanpa base --with-base) WITH_BASE=1 ;; --with-dremio) WITH_DREMIO=1 ;; --save-airflow) SAVE_AIRFLOW="${2:-}"; shift ;; --load-airflow) LOAD_AIRFLOW="${2:-}"; shift ;; -h|--help) usage ;; *) err "Argumen tidak dikenal: $1"; exit 2 ;; esac shift done } require_docker() { if ! command -v docker &>/dev/null; then err "docker tidak ditemukan — jalankan ./scripts/prepare-host.sh lebih dulu." exit 1 fi } # ─── Daftar image (tidak di-hardcode) ─────────────────────────────────────── compose_images() { # [--env-file ] [--profile …]… local file="$1"; shift local out err_file # stderr JANGAN ditelan: kalau kosong, tampilkan penyebabnya (pelajaran dari gate # Fase 5 yang sempat menyembunyikan error compose sehingga gagal sulit dilacak). # # Catatan bug: versi pertama memakai `local err` + `trap … RETURN`. Trap RETURN # berjalan SETELAH variabel local dibersihkan sehingga ${err} menjadi unbound, dan # nama `err` juga menutupi FUNGSI err(). Kini tanpa trap + bernama err_file. err_file="$(mktemp)" out="$(docker compose -f "${file}" "$@" config --images 2>"${err_file}" | sort -u)" if [[ -z "${out}" && -s "${err_file}" ]]; then warn "docker compose gagal mendaftar image:" sed 's/^/ /' "${err_file}" >&2 fi rm -f "${err_file}" printf '%s\n' "${out}" } # Isi ENV_ARGS (variabel global) dengan argumen env untuk compose. # Bila .env BELUM ADA (server masih bersih — .env dibuat install.sh saat instalasi), # pakai default dari config/environment.defaults.sh: ke-16 variabel yang diwajibkan # compose (:?err) semuanya ada di sana, dan nilainya TIDAK memengaruhi daftar image # (image di-pin di docker-compose.yml). # # PENTING: fungsi ini HARUS dipanggil di shell saat ini — bukan di dalam $( ) — supaya # `set -a; source defaults` benar-benar berlaku untuk proses compose kita. Versi pertama # saya memanggilnya dalam command substitution (subshell), jadi sourcing-nya sia-sia. ENV_ARGS=() resolve_env() { # local envfile="$1" if [[ -f "${envfile}" ]]; then ENV_ARGS=(--env-file "${envfile}") return 0 fi warn "berkas ${envfile} belum ada — memakai default config/environment.defaults.sh" echo " (.env dibuat oleh install.sh saat instalasi; tidak memengaruhi daftar image)" >&2 set -a # shellcheck disable=SC1091 . "${DEPLOY_DIR}/config/environment.defaults.sh" set +a ENV_ARGS=() } collect_images() { local file envfile if [[ "${MODE}" == "host" ]]; then [[ -z "${HOST_NAME}" ]] && { err "--host memerlukan nama host."; exit 2; } file="${DEPLOY_DIR}/generated/host-${HOST_NAME}/docker-compose.yml" if [[ ! -f "${file}" ]]; then err "Bundle host belum ada: ${file}" echo " Jalankan dulu: ./install.sh --generate (atau --generate --inventory cluster.yml)" exit 1 fi envfile="${DEPLOY_DIR}/generated/host-${HOST_NAME}/.env" [[ -f "${envfile}" ]] || envfile="${DEPLOY_DIR}/.env" inf "sumber daftar image: bundle host '${HOST_NAME}'" resolve_env "${envfile}" compose_images "${file}" ${ENV_ARGS[@]+"${ENV_ARGS[@]}"} --profile all else local extra=() if [[ ${WITH_DREMIO} -eq 1 ]]; then extra=(--profile dremio) inf "sumber daftar image: docker-compose.yml (profil all + dremio)" else inf "sumber daftar image: docker-compose.yml (profil all; dremio dikeluarkan)" fi resolve_env "${DEPLOY_DIR}/.env" compose_images "${DEPLOY_DIR}/docker-compose.yml" ${ENV_ARGS[@]+"${ENV_ARGS[@]}"} \ --profile all ${extra[@]+"${extra[@]}"} fi } # Base image Airflow diambil dari Dockerfile agar tidak ter-hardcode di sini. airflow_base_image() { local df="${DEPLOY_DIR}/docker/airflow/Dockerfile.slim" [[ -f "${df}" ]] || return 1 grep -m1 '^FROM ' "${df}" | awk '{print $2}' } airflow_built_image() { # shellcheck disable=SC1091 ( set -a; [[ -f "${DEPLOY_DIR}/config/environment.defaults.sh" ]] && . "${DEPLOY_DIR}/config/environment.defaults.sh" [[ -f "${DEPLOY_DIR}/.env" ]] && . "${DEPLOY_DIR}/.env" echo "${AIRFLOW_IMAGE:-sicabe/airflow:3.3.2-om203-ol}" ) 2>/dev/null | tail -1 } # Host registry dari nama image — untuk petunjuk diagnosa saat pull gagal. # Image tanpa '/' atau dengan komponen pertama tanpa titik berasal dari Docker Hub # (endpoint API-nya registry-1.docker.io). registry_host() { # Dipecah DUA baris: `local x="$1" y="${x%%/*}"` mengevaluasi RHS `y` SEBELUM `x` # di-assign → "unbound variable" di bawah `set -u` (bug laten yang baru muncul saat # fungsi ini dipanggil di jalur normal). local img="$1" local first="${img%%/*}" # WAJIB ada '/' dulu: tanpa itu, "postgres:15" membuat ${img%%/*} = seluruh string # dan tanda ':' adalah TAG, bukan port registry (bug yang ditemukan saat menguji). if [[ "${img}" == */* && ( "${first}" == *.* || "${first}" == *:* || "${first}" == "localhost" ) ]]; then echo "${first}" else echo "registry-1.docker.io" fi } image_size_mb() { docker image inspect -f '{{.Size}}' "$1" 2>/dev/null | awk '{printf "%d", $1/1024/1024}' } # ─── Aksi: simpan / muat image Airflow yang dibangun lokal ────────────────── do_save_airflow() { local built; built="$(airflow_built_image)" if ! docker image inspect "${built}" &>/dev/null; then err "Image '${built}' belum ada — bangun dulu:" echo " docker build -t ${built} -f docker/airflow/Dockerfile.332 docker/airflow" exit 1 fi inf "menyimpan '${built}' ke ${SAVE_AIRFLOW} (berisi seluruh layer, termasuk base)" docker save -o "${SAVE_AIRFLOW}" "${built}" && \ ok "tersimpan ($(du -h "${SAVE_AIRFLOW}" | cut -f1)) — salin ke host lain lalu pakai --load-airflow" } do_load_airflow() { [[ -f "${LOAD_AIRFLOW}" ]] || { err "berkas tidak ditemukan: ${LOAD_AIRFLOW}"; exit 1; } inf "memuat image dari ${LOAD_AIRFLOW}" docker load -i "${LOAD_AIRFLOW}" && ok "image dimuat" local built; built="$(airflow_built_image)" docker image inspect "${built}" &>/dev/null && ok "'${built}' tersedia" || warn "image '${built}' belum terlihat" } # ─── Aksi: tarik image ────────────────────────────────────────────────────── do_pull() { local images base built built_local images="$(collect_images)" if [[ -z "${images}" ]]; then err "daftar image kosong — periksa docker-compose.yml / bundle host." exit 1 fi base="$(airflow_base_image || true)" built="$(airflow_built_image)" # Image Airflow: dikecualikan HANYA bila namanya menunjuk LOKAL polos # (`sicabe/airflow:…`, tidak ada host registry). Bila `AIRFLOW_IMAGE` menunjuk # REGISTRY (mis. `registry.sicabe.com/airflow:…`, host memuat titik), image # IKUT di-pull seperti image lain — sejak image dipublikasikan ke registry internal. # Deteksi memakai registry_host(): nama Docker Hub polos -> "registry-1.docker.io". built_local=0 if [[ -n "${built}" && "$(registry_host "${built}")" == "registry-1.docker.io" ]]; then built_local=1 # Keluarkan dari daftar (bukan sekadar dilewati) supaya daftar = yang benar-benar diunduh. images="$(printf '%s\n' "${images}" | grep -vxF "${built}" || true)" fi echo echo -e "${BLD}Daftar image (${MODE})${NC}" printf '%s\n' "${images}" | sed 's/^/ - /' [[ -n "${base}" && ${WITH_BASE} -eq 1 ]] && echo " - ${base} (base build Airflow)" if [[ -n "${built}" && ${built_local} -eq 1 ]]; then echo " (dikecualikan: ${built} — dibangun lokal, tidak ada di Docker Hub; tersedia di registry internal)" fi [[ ${WITH_DREMIO} -eq 0 ]] && echo " (dikecualikan: dremio/dremio-oss — opt-in, aktifkan dengan --with-dremio)" if [[ ${LIST_ONLY} -eq 1 ]]; then echo inf "--list: tidak ada yang ditarik." return 0 fi echo echo -e "${BLD}Menarik image${NC}" local failed=() okc=0 while IFS= read -r img; do [[ -z "${img}" ]] && continue # Image yang dibangun lokal tidak ada di registry — jangan dicoba di-pull. if [[ ${built_local} -eq 1 && "${img}" == "${built}" ]]; then inf "${img} — dibangun lokal, dilewati (lihat alur build-sekali-salin di bawah)" continue fi if docker image inspect "${img}" &>/dev/null && [[ "${img}" != *":latest" ]]; then ok "${img} (sudah ada)" okc=$((okc + 1)) continue fi inf "pull ${img} …" # Output pull ditangkap ke berkas, BUKAN dibuang: tanpa ini kegagalan hanya # berbunyi "GAGAL" tanpa sebab (dialami saat minio/quay.io gagal di server). local perr; perr="$(mktemp)" if docker pull "${img}" >"${perr}" 2>&1; then ok "${img}" okc=$((okc + 1)) else warn "GAGAL: ${img}" tail -3 "${perr}" | sed 's/^/ /' >&2 local reg; reg="$(registry_host "${img}")" echo " hint: periksa akses ke registry '${reg}' → curl -sI https://${reg}/v2/" >&2 failed+=("${img}") fi rm -f "${perr}" done <<< "${images}" if [[ -n "${base}" && ${WITH_BASE} -eq 1 ]]; then if docker image inspect "${base}" &>/dev/null; then ok "${base} (sudah ada)" elif docker pull -q "${base}" >/dev/null 2>&1; then ok "${base} (base build Airflow)" else warn "GAGAL: ${base} — build Airflow lokal akan mengunduh ulang" failed+=("${base}") fi fi echo echo -e "${BLD}Ringkasan${NC}" echo " berhasil/sudah ada : ${okc}" if [[ ${#failed[@]} -gt 0 ]]; then echo " gagal : ${#failed[@]}" printf ' - %s\n' "${failed[@]}" fi # Ukuran total image yang terdaftar (perkiraan kebutuhan disk) local total=0 size while IFS= read -r img; do size="$(image_size_mb "${img}")" [[ -n "${size}" ]] && total=$((total + size)) done <<< "${images}" echo " ukuran total : ~${total} MB (image yang terdaftar)" if [[ ${built_local} -eq 1 ]] && ! docker image inspect "${built}" &>/dev/null; then echo warn "'${built}' BELUM ada dan ia TIDAK dapat di-pull (dibangun lokal)." echo " Alur build-sekali-salin (opsi b):" echo " di SATU host : docker build -t ${built} -f docker/airflow/Dockerfile.332 docker/airflow" echo " ./scripts/prefetch-images.sh --save-airflow /tmp/airflow.tar" echo " host lain : scp /tmp/airflow.tar :/tmp/" echo " ./scripts/prefetch-images.sh --load-airflow /tmp/airflow.tar" fi [[ ${#failed[@]} -eq 0 ]] } main() { parse_args "$@" require_docker if [[ -n "${SAVE_AIRFLOW}" ]]; then do_save_airflow; exit $?; fi if [[ -n "${LOAD_AIRFLOW}" ]]; then do_load_airflow; exit $?; fi echo -e "${BLD}Prefetch image SiCABE${NC}" do_pull } main "$@"