hasari-api / services /backend /scripts /entrypoint.sh
erdoganpeker's picture
fix(wave10): 8 P0 bugs from 7-agent prod audit
f297df3
#!/usr/bin/env bash
# services/backend/scripts/entrypoint.sh
# ----------------------------------------------------------------------
# Container entrypoint for hasarui-api / hasarui-worker.
# Responsibilities:
# 1. If models are not already present in $MODEL_DIR, download them
# from S3 ($MODEL_S3_BUCKET / $MODEL_S3_PREFIX).
# Skip entirely when SKIP_MODEL_FETCH=1 (Dockerfile.embedded).
# 2. Run Alembic migrations (web service only — controlled by
# RUN_MIGRATIONS=1; workers should set RUN_MIGRATIONS=0).
# 3. exec "$@" — replace shell with the actual CMD (uvicorn / celery).
# ----------------------------------------------------------------------
set -euo pipefail
MODEL_DIR="${MODEL_DIR:-/app/models}"
REQUIRED_WEIGHTS=("damage_best.pt" "parts_best.pt" "severity_best.pt")
log() { echo "[entrypoint] $*" >&2; }
# -------- 1. Model fetch --------
fetch_models() {
if [[ "${SKIP_MODEL_FETCH:-0}" == "1" ]]; then
log "SKIP_MODEL_FETCH=1 — assuming weights baked into image at ${MODEL_DIR}"
return 0
fi
local missing=0
for w in "${REQUIRED_WEIGHTS[@]}"; do
if [[ ! -f "${MODEL_DIR}/${w}" ]]; then
missing=1
break
fi
done
if [[ "${missing}" == "0" ]]; then
log "All weights already present in ${MODEL_DIR}, skipping fetch."
return 0
fi
# Hugging Face Hub fetch path (public model repo, no auth needed).
# Set HF_MODEL_REPO env to use this branch; otherwise falls back to S3.
if [[ -n "${HF_MODEL_REPO:-}" ]]; then
local hf_branch="${HF_MODEL_BRANCH:-main}"
log "Fetching weights from https://huggingface.co/${HF_MODEL_REPO}/resolve/${hf_branch}/ -> ${MODEL_DIR}/"
mkdir -p "${MODEL_DIR}"
for w in "${REQUIRED_WEIGHTS[@]}"; do
local dst="${MODEL_DIR}/${w}"
if [[ -f "${dst}" ]]; then
log " ${w} already cached, skipping."
continue
fi
local url="https://huggingface.co/${HF_MODEL_REPO}/resolve/${hf_branch}/${w}?download=true"
log " downloading ${url}"
if ! curl --fail --silent --show-error --location \
--retry 5 --retry-delay 3 --max-time 600 \
--output "${dst}.tmp" "${url}"; then
log "ERROR: curl failed for ${url}"
rm -f "${dst}.tmp"
exit 1
fi
mv "${dst}.tmp" "${dst}"
done
log "All weights fetched from Hugging Face."
# ---- Pretrained Ultralytics COCO weights (Ultralytics CDN, public) ----
# PRETRAINED_DIR'a yolo11m-seg.pt indir. is_available() file-exists
# checki bunu bulamayinca pretrained_ultralytics_yolo11m 400 doner.
local pre_dir="${PRETRAINED_DIR:-/app/pretrained}"
local ultra_base="${ULTRALYTICS_BASE:-https://github.com/ultralytics/assets/releases/download/v8.3.0}"
local ultra_weights=("yolo11m-seg.pt")
mkdir -p "${pre_dir}" || true
for w in "${ultra_weights[@]}"; do
local dst="${pre_dir}/${w}"
if [[ -f "${dst}" ]]; then
log " pretrained ${w} cached, skip."
continue
fi
log " downloading pretrained ${ultra_base}/${w}"
if curl --fail --silent --show-error --location \
--retry 3 --retry-delay 3 --max-time 300 \
--output "${dst}.tmp" "${ultra_base}/${w}"; then
mv "${dst}.tmp" "${dst}"
log " pretrained ${w} OK"
else
log "WARN: pretrained ${w} fetch fail; UI will show 'unavailable' but core custom model unaffected"
rm -f "${dst}.tmp" || true
fi
done
return 0
fi
if [[ -z "${MODEL_S3_BUCKET:-}" ]]; then
log "ERROR: HF_MODEL_REPO and MODEL_S3_BUCKET both unset and weights missing."
log " Set HF_MODEL_REPO=owner/repo (preferred) or MODEL_S3_BUCKET."
exit 1
fi
local prefix="${MODEL_S3_PREFIX:-models/full_20260515_044630}"
log "Fetching weights from s3://${MODEL_S3_BUCKET}/${prefix}/ -> ${MODEL_DIR}/"
python - <<PYEOF
import os, sys, boto3
from botocore.config import Config
bucket = os.environ["MODEL_S3_BUCKET"]
prefix = os.environ.get("MODEL_S3_PREFIX", "models/full_20260515_044630").rstrip("/")
target = os.environ.get("MODEL_DIR", "/app/models")
weights = ["damage_best.pt", "parts_best.pt", "severity_best.pt"]
kwargs = {}
if os.environ.get("S3_ENDPOINT"):
kwargs["endpoint_url"] = os.environ["S3_ENDPOINT"]
if os.environ.get("S3_REGION"):
kwargs["region_name"] = os.environ["S3_REGION"]
if os.environ.get("S3_ACCESS_KEY"):
kwargs["aws_access_key_id"] = os.environ["S3_ACCESS_KEY"]
kwargs["aws_secret_access_key"] = os.environ["S3_SECRET_KEY"]
# IMPORTANT: boto3 s3.download_file() and s3.upload_file() call HeadObject
# under the hood. B2 + bucket-scoped Application Keys ("List All Bucket
# Names" disabled) return 403 on HeadObject even when GetObject succeeds.
# Use get_object directly + stream to disk; it skips the HeadObject.
config = Config(
retries={"max_attempts": 5, "mode": "standard"},
signature_version="s3v4",
s3={"addressing_style": "virtual"},
)
s3 = boto3.client("s3", config=config, **kwargs)
os.makedirs(target, exist_ok=True)
for w in weights:
dst = os.path.join(target, w)
if os.path.isfile(dst):
print(f"[entrypoint] {w} already cached, skipping.", file=sys.stderr)
continue
key = f"{prefix}/{w}"
print(f"[entrypoint] downloading s3://{bucket}/{key} -> {dst}", file=sys.stderr)
resp = s3.get_object(Bucket=bucket, Key=key)
tmp = dst + ".tmp"
body = resp["Body"]
with open(tmp, "wb") as f:
for chunk in iter(lambda: body.read(8 * 1024 * 1024), b""):
f.write(chunk)
os.replace(tmp, dst)
print("[entrypoint] All weights fetched.", file=sys.stderr)
PYEOF
}
# -------- 2. DB migrations --------
run_migrations() {
if [[ "${RUN_MIGRATIONS:-0}" != "1" ]]; then
return 0
fi
if [[ ! -f "alembic.ini" ]]; then
log "RUN_MIGRATIONS=1 but no alembic.ini found, skipping."
return 0
fi
log "Running Alembic migrations..."
alembic upgrade head || {
log "Alembic upgrade failed."
exit 1
}
}
# -------- main --------
fetch_models
run_migrations
log "Boot complete. exec: $*"
exec "$@"