337 lines
14 KiB
Bash
337 lines
14 KiB
Bash
#!/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 <nama>] [--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() { # <file> [--env-file <f>] [--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() { # <envfile>
|
||
|
|
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 <host>:/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 "$@"
|