343 lines
10 KiB
Bash
Executable File
343 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
LOCAL_DIR="${ORBSTACK_LOCAL_DIR:-$HOME/.orbstack/local}"
|
|
ENV_FILE="${HARBOR_MIRROR_ENV_FILE:-$LOCAL_DIR/harbor-mirror.env}"
|
|
PROXY_SRC="$ROOT_DIR/scripts/dockerhub-mirror-auth-proxy.py"
|
|
PROXY_DST="$LOCAL_DIR/dockerhub_mirror_auth_proxy.py"
|
|
PROXY_CONTAINER="${HARBOR_MIRROR_CONTAINER:-dockerhub-mirror-auth}"
|
|
PROXY_IMAGE="${HARBOR_MIRROR_PROXY_IMAGE:-python:3.11-alpine}"
|
|
PROXY_PORT="${HARBOR_MIRROR_PORT:-5002}"
|
|
PROXY_CONTAINER_PORT="${HARBOR_MIRROR_CONTAINER_PORT:-5001}"
|
|
HARBOR_NETWORK="${HARBOR_NETWORK:-harbor_core}"
|
|
MIRROR_URL="http://127.0.0.1:${PROXY_PORT}"
|
|
DOCKER_CONFIG_FILES=("$HOME/.orbstack/config/docker.json" "$HOME/.orbstack/docker")
|
|
|
|
log() { printf '[%s] %s\n' "$1" "$2"; }
|
|
info() { log INFO "$1"; }
|
|
ok() { log OK "$1"; }
|
|
warn() { log WARN "$1"; }
|
|
fail() { log ERROR "$1"; exit 1; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") <command>
|
|
|
|
Commands:
|
|
init-env Create $ENV_FILE template
|
|
install Copy proxy, start container, configure OrbStack Docker
|
|
ensure Keep proxy container running; warn if Docker mirror is missing
|
|
start-proxy Recreate the local auth proxy container
|
|
configure-docker Add $MIRROR_URL to OrbStack Docker config files
|
|
restart-docker Restart OrbStack Docker
|
|
verify-docker Pull a standard Docker Hub image and show proxy evidence
|
|
verify-k3s Pull a standard Docker Hub image from k3s and show Harbor evidence
|
|
doctor Run status + Docker verification, then k3s verification when kubectl exists
|
|
status Show proxy, Docker mirror, and k3s registry state
|
|
logs Show proxy + Harbor nginx/registry evidence logs
|
|
install-launchd Install a user LaunchAgent that runs "ensure" every 5 minutes
|
|
EOF
|
|
}
|
|
|
|
load_env() {
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
# shellcheck disable=SC1090
|
|
set -a
|
|
. "$ENV_FILE"
|
|
set +a
|
|
fi
|
|
|
|
if [[ -z "${HARBOR_HOST:-}" ]]; then
|
|
HARBOR_HOST="$(docker inspect "$PROXY_CONTAINER" \
|
|
--format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
|
|
| sed -n 's/^HARBOR_HOST=//p' | head -1 || true)"
|
|
fi
|
|
|
|
HARBOR_PROJECT="${HARBOR_PROJECT:-dockerhub}"
|
|
HARBOR_VERIFY_TLS="${HARBOR_VERIFY_TLS:-false}"
|
|
HARBOR_NETWORK="${HARBOR_NETWORK:-harbor_core}"
|
|
PROXY_PORT="${HARBOR_MIRROR_PORT:-$PROXY_PORT}"
|
|
MIRROR_URL="http://127.0.0.1:${PROXY_PORT}"
|
|
|
|
if [[ -z "${HARBOR_HOST:-}" ]]; then
|
|
fail "HARBOR_HOST is not set. Run: $0 init-env, then edit $ENV_FILE"
|
|
fi
|
|
}
|
|
|
|
init_env() {
|
|
mkdir -p "$LOCAL_DIR"
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
warn "env file already exists: $ENV_FILE"
|
|
return 0
|
|
fi
|
|
local detected_host
|
|
detected_host="$(docker inspect "$PROXY_CONTAINER" \
|
|
--format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
|
|
| sed -n 's/^HARBOR_HOST=//p' | head -1 || true)"
|
|
detected_host="${detected_host:-REPLACE_WITH_HARBOR_HOST}"
|
|
|
|
cat >"$ENV_FILE" <<'EOF'
|
|
# Local-only settings for OrbStack -> Harbor mirror.
|
|
# Do not commit this file.
|
|
EOF
|
|
{
|
|
printf 'HARBOR_HOST=%s\n' "$detected_host"
|
|
cat <<'EOF'
|
|
HARBOR_PROJECT=dockerhub
|
|
HARBOR_VERIFY_TLS=false
|
|
HARBOR_NETWORK=harbor_core
|
|
HARBOR_MIRROR_PORT=5002
|
|
EOF
|
|
} >>"$ENV_FILE"
|
|
ok "created $ENV_FILE"
|
|
}
|
|
|
|
copy_proxy() {
|
|
mkdir -p "$LOCAL_DIR"
|
|
install -m 0644 "$PROXY_SRC" "$PROXY_DST"
|
|
ok "proxy script installed: $PROXY_DST"
|
|
}
|
|
|
|
start_proxy() {
|
|
load_env
|
|
copy_proxy
|
|
|
|
docker network inspect "$HARBOR_NETWORK" >/dev/null 2>&1 \
|
|
|| fail "Docker network not found: $HARBOR_NETWORK"
|
|
|
|
if docker ps -a --format '{{.Names}}' | grep -qx "$PROXY_CONTAINER"; then
|
|
docker rm -f "$PROXY_CONTAINER" >/dev/null
|
|
fi
|
|
|
|
docker run -d \
|
|
--name "$PROXY_CONTAINER" \
|
|
--restart unless-stopped \
|
|
--network "$HARBOR_NETWORK" \
|
|
-p "127.0.0.1:${PROXY_PORT}:${PROXY_CONTAINER_PORT}" \
|
|
-e "HARBOR_HOST=${HARBOR_HOST}" \
|
|
-e "HARBOR_PROJECT=${HARBOR_PROJECT}" \
|
|
-e "HARBOR_VERIFY_TLS=${HARBOR_VERIFY_TLS}" \
|
|
-e "LISTEN=0.0.0.0" \
|
|
-e "PORT=${PROXY_CONTAINER_PORT}" \
|
|
-v "${PROXY_DST}:/app/proxy.py:ro" \
|
|
"$PROXY_IMAGE" python /app/proxy.py >/dev/null
|
|
|
|
ok "proxy container running: ${PROXY_CONTAINER} on ${MIRROR_URL}"
|
|
}
|
|
|
|
merge_docker_config() {
|
|
local file="$1"
|
|
local mirror="$2"
|
|
mkdir -p "$(dirname "$file")"
|
|
|
|
if [[ -f "$file" ]]; then
|
|
cp "$file" "${file}.bak.harbor-mirror.$(date +%Y%m%d%H%M%S)"
|
|
fi
|
|
|
|
python3 - "$file" "$mirror" "$PROXY_PORT" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
path, mirror, port = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
except FileNotFoundError:
|
|
data = {}
|
|
except json.JSONDecodeError as exc:
|
|
raise SystemExit(f"{path}: invalid JSON: {exc}")
|
|
|
|
def prepend_unique(values, item):
|
|
values = list(values or [])
|
|
return [item] + [v for v in values if v != item]
|
|
|
|
data["registry-mirrors"] = prepend_unique(data.get("registry-mirrors"), mirror)
|
|
data["insecure-registries"] = prepend_unique(data.get("insecure-registries"), f"127.0.0.1:{port}")
|
|
data["insecure-registries"] = prepend_unique(data.get("insecure-registries"), f"localhost:{port}")
|
|
|
|
tmp = f"{path}.tmp"
|
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
json.dump(data, fh, indent=2, ensure_ascii=False)
|
|
fh.write("\n")
|
|
os.replace(tmp, path)
|
|
PY
|
|
ok "updated Docker config: $file"
|
|
}
|
|
|
|
configure_docker() {
|
|
for file in "${DOCKER_CONFIG_FILES[@]}"; do
|
|
merge_docker_config "$file" "$MIRROR_URL"
|
|
done
|
|
warn "restart OrbStack Docker to apply the mirror: $0 restart-docker"
|
|
}
|
|
|
|
restart_docker() {
|
|
if [[ -x "$HOME/.orbstack/bin/orbctl" ]]; then
|
|
"$HOME/.orbstack/bin/orbctl" restart docker
|
|
else
|
|
fail "orbctl not found at $HOME/.orbstack/bin/orbctl"
|
|
fi
|
|
}
|
|
|
|
docker_mirror_configured() {
|
|
local docker_info
|
|
docker_info="$(docker info 2>/dev/null || true)"
|
|
printf '%s\n' "$docker_info" | grep -Fq "$MIRROR_URL/" \
|
|
|| printf '%s\n' "$docker_info" | grep -Fq "$MIRROR_URL"
|
|
}
|
|
|
|
verify_docker() {
|
|
load_env
|
|
docker ps --format '{{.Names}}' | grep -qx "$PROXY_CONTAINER" \
|
|
|| fail "proxy container is not running"
|
|
docker_mirror_configured || fail "Docker registry mirror is not configured: $MIRROR_URL"
|
|
|
|
local image="${1:-testcontainers/ryuk:0.12.0}"
|
|
info "pulling standard Docker Hub image through Docker mirror: $image"
|
|
docker pull "$image"
|
|
ok "Docker pull succeeded: $image"
|
|
docker logs --tail 80 "$PROXY_CONTAINER" 2>&1 | grep -E 'manifests|blobs' | tail -10 || true
|
|
}
|
|
|
|
verify_k3s() {
|
|
load_env
|
|
command -v kubectl >/dev/null || fail "kubectl not found"
|
|
|
|
local image="${1:-testcontainers/ryuk:0.11.0}"
|
|
local pod="k3s-harbor-pull-test-$(date +%s)"
|
|
trap 'kubectl delete pod "$pod" --ignore-not-found --wait=false >/dev/null 2>&1 || true' RETURN
|
|
|
|
info "pulling standard image from k3s: $image"
|
|
kubectl delete pod "$pod" --ignore-not-found >/dev/null 2>&1 || true
|
|
kubectl run "$pod" \
|
|
--image="$image" \
|
|
--image-pull-policy=Always \
|
|
--restart=Never \
|
|
--command -- /bin/sh -c 'sleep 30' >/dev/null
|
|
kubectl wait --for=condition=Ready "pod/$pod" --timeout=120s
|
|
kubectl describe pod "$pod" | sed -n '/Events:/,$p'
|
|
|
|
local repo="${image%%:*}"
|
|
if [[ "$repo" != */* ]]; then
|
|
repo="library/$repo"
|
|
fi
|
|
local encoded_repo="${repo//\//%2F}"
|
|
|
|
info "Harbor nginx evidence:"
|
|
docker logs --since 5m nginx 2>&1 \
|
|
| grep -E "dockerhub/${repo}|repository%3Adockerhub%2F${encoded_repo}%3Apull" \
|
|
| tail -20 || true
|
|
}
|
|
|
|
status() {
|
|
info "proxy container"
|
|
docker ps -a --filter "name=^/${PROXY_CONTAINER}$" \
|
|
--format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' || true
|
|
echo
|
|
info "Docker registry mirrors"
|
|
docker info 2>/dev/null | sed -n '/Registry Mirrors:/,/Live Restore Enabled:/p' || true
|
|
echo
|
|
info "k3s registries.yaml evidence"
|
|
kubectl get node >/dev/null 2>&1 && kubectl get node -o wide || true
|
|
}
|
|
|
|
logs() {
|
|
load_env
|
|
info "proxy logs"
|
|
docker logs --tail 80 "$PROXY_CONTAINER" 2>&1 || true
|
|
echo
|
|
info "Harbor nginx Docker Hub evidence"
|
|
docker logs --since 15m nginx 2>&1 | grep -E 'dockerhub/|containerd/v|Docker-Client' | tail -80 || true
|
|
echo
|
|
info "Harbor registry Docker Hub evidence"
|
|
docker logs --since 15m registry 2>&1 | grep -E 'dockerhub/' | tail -80 || true
|
|
}
|
|
|
|
ensure() {
|
|
load_env
|
|
copy_proxy
|
|
if ! docker ps --format '{{.Names}}' | grep -qx "$PROXY_CONTAINER"; then
|
|
warn "proxy container is not running; recreating"
|
|
start_proxy
|
|
fi
|
|
if ! docker_mirror_configured; then
|
|
warn "Docker mirror is missing from OrbStack config: $MIRROR_URL"
|
|
fi
|
|
ok "ensure completed"
|
|
}
|
|
|
|
doctor() {
|
|
status
|
|
verify_docker
|
|
if command -v kubectl >/dev/null && kubectl get node >/dev/null 2>&1; then
|
|
verify_k3s
|
|
else
|
|
warn "kubectl/k3s unavailable; skipped k3s verification"
|
|
fi
|
|
}
|
|
|
|
install_launchd() {
|
|
local plist="$HOME/Library/LaunchAgents/com.orbstack.harbor-mirror.ensure.plist"
|
|
mkdir -p "$HOME/Library/LaunchAgents" "$ROOT_DIR/logs"
|
|
cat >"$plist" <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>com.orbstack.harbor-mirror.ensure</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/bin/bash</string>
|
|
<string>${ROOT_DIR}/scripts/orbstack-harbor-mirrorctl.sh</string>
|
|
<string>ensure</string>
|
|
</array>
|
|
<key>StartInterval</key>
|
|
<integer>300</integer>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>StandardOutPath</key>
|
|
<string>${ROOT_DIR}/logs/orbstack-harbor-mirror-stdout.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>${ROOT_DIR}/logs/orbstack-harbor-mirror-stderr.log</string>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>PATH</key>
|
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
launchctl unload "$plist" >/dev/null 2>&1 || true
|
|
launchctl load "$plist"
|
|
ok "LaunchAgent installed: $plist"
|
|
}
|
|
|
|
install_all() {
|
|
load_env
|
|
start_proxy
|
|
configure_docker
|
|
restart_docker
|
|
verify_docker
|
|
}
|
|
|
|
cmd="${1:-}"
|
|
case "$cmd" in
|
|
init-env) init_env ;;
|
|
install) install_all ;;
|
|
ensure) ensure ;;
|
|
start-proxy) start_proxy ;;
|
|
configure-docker) configure_docker ;;
|
|
restart-docker) restart_docker ;;
|
|
verify-docker) shift; verify_docker "$@" ;;
|
|
verify-k3s) shift; verify_k3s "$@" ;;
|
|
doctor) doctor ;;
|
|
status) status ;;
|
|
logs) logs ;;
|
|
install-launchd) install_launchd ;;
|
|
-h|--help|help|"") usage ;;
|
|
*) usage; exit 2 ;;
|
|
esac
|