Files
ljwx-k8s/cache-image.sh
2026-02-20 22:50:45 +08:00

543 lines
17 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Kubernetes 镜像缓存工具
# 用途:通过 EU Harbor 拉取镜像并缓存到本地 Harbor
#
# 使用方式:
# ./cache-image.sh nginx:alpine # 缓存单个镜像
# ./cache-image.sh nginx:alpine mysql:8.0 # 缓存多个镜像
# ./cache-image.sh -f images.txt # 从文件批量缓存
# ./cache-image.sh --update-deployment namespace/deployment # 自动更新部署
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# 日志函数
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[✓]${NC} $1"; }
log_error() { echo -e "${RED}[✗]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[!]${NC} $1"; }
log_step() { echo -e "${CYAN}[→]${NC} $1"; }
# Harbor 配置
EU_HARBOR="harbor.eu.lingjingwanxiang.cn"
LOCAL_HARBOR="harbor.omniverseai.net"
HARBOR_PROJECT="library"
# 统计变量
TOTAL_IMAGES=0
SUCCESS_COUNT=0
FAILED_COUNT=0
FAILED_IMAGES=()
# 显示使用说明
show_usage() {
cat << EOF
Kubernetes 镜像缓存工具
用途:
通过 EU Harbor 拉取镜像并缓存到本地 Harbor,解决镜像拉取超时问题
使用方式:
$0 <image[:tag]> # 缓存单个镜像
$0 <image1> <image2> ... # 缓存多个镜像
$0 -f <file> # 从文件批量缓存(每行一个镜像)
$0 --update-deployment <ns/deploy> # 缓存镜像并更新 Deployment
$0 --from-pod <namespace/pod> # 从 Pod 获取镜像并缓存
选项:
-f, --file <file> 从文件读取镜像列表
-p, --project <project> 指定本地 Harbor 项目(默认: library)
-u, --update-deployment <ns/deploy> 自动更新 Deployment 使用本地镜像
--from-pod <ns/pod> 从指定 Pod 获取所有镜像
--dry-run 仅显示操作,不实际执行
-h, --help 显示帮助信息
示例:
# 缓存单个镜像
$0 nginx:alpine
# 缓存多个镜像
$0 nginx:alpine redis:7 mysql:8.0
# 从文件批量缓存
$0 -f images.txt
# 缓存并更新 Deployment
$0 --update-deployment kubernetes-dashboard/kubernetes-dashboard
# 从失败的 Pod 获取镜像并缓存
$0 --from-pod kubernetes-dashboard/kubernetes-dashboard-xxx
镜像格式支持:
- nginx:alpine → harbor.omniverseai.net/library/nginx:alpine
- redis → harbor.omniverseai.net/library/redis:latest
- mysql/mysql-server:8.0 → harbor.omniverseai.net/library/mysql-server:8.0
- registry.k8s.io/metrics-server/metrics-server:tag → harbor.omniverseai.net/library/metrics-server-metrics-server:tag
- gcr.io/project/image:tag → harbor.omniverseai.net/library/image:tag
- quay.io/repo/image:tag → harbor.omniverseai.net/library/repo-image:tag
EOF
}
# 登录 Harbor
login_harbor() {
log_step "登录 Harbor 仓库..."
# 登录 EU Harbor(忽略警告信息)
if ! echo 'Harbor12345' | docker login "$EU_HARBOR" -u admin --password-stdin > /dev/null 2>&1; then
log_error "EU Harbor 登录失败"
return 1
fi
# 登录本地 Harbor(忽略警告信息)
if ! echo 'admin123' | docker login "$LOCAL_HARBOR" -u admin --password-stdin > /dev/null 2>&1; then
log_error "本地 Harbor 登录失败"
return 1
fi
log_success "Harbor 登录成功"
return 0
}
# 解析镜像名称
parse_image_name() {
local input_image="$1"
local image_name=""
local image_tag="latest"
# 移除 registry 前缀(如果有)
# gcr.io/project/image:tag -> image:tag
# docker.io/library/nginx:alpine -> nginx:alpine
if [[ "$input_image" =~ ^[^/]+\.[^/]+/ ]]; then
input_image=$(echo "$input_image" | sed -E 's|^[^/]+\.[^/]+/||')
fi
# 分离镜像名和标签
if [[ "$input_image" =~ : ]]; then
image_name="${input_image%:*}"
image_tag="${input_image##*:}"
else
image_name="$input_image"
fi
# 保留完整路径(包括组织名)
# aquasec/trivy-operator -> aquasec/trivy-operator (保留)
# nginx -> nginx (保留)
echo "${image_name}:${image_tag}"
}
# 转换镜像名为本地存储格式(扁平化,避免 Harbor 路径问题)
normalize_for_local_storage() {
local image="$1"
# 将 / 替换为 -,避免 Harbor 项目路径问题
# aquasec/trivy-operator:0.29.0 -> aquasec-trivy-operator:0.29.0
echo "$image" | sed 's|/|-|g'
}
# 缓存单个镜像
cache_single_image() {
local input_image="$1"
local dry_run="${2:-false}"
# 解析镜像名(保留组织前缀)
local normalized_image=$(parse_image_name "$input_image")
local image_name="${normalized_image%:*}"
local image_tag="${normalized_image##*:}"
# 生成本地存储的扁平化镜像名
local local_image_name=$(normalize_for_local_storage "$normalized_image")
log_info "=== 缓存镜像: $input_image → $normalized_image ==="
log_info " 本地存储为: $local_image_name"
if [[ "$dry_run" == "true" ]]; then
log_warning "[DRY-RUN] 将缓存: $input_image"
log_info " 源: ${EU_HARBOR}/dockerhub-proxy/${normalized_image}"
log_info " 目标: ${LOCAL_HARBOR}/${HARBOR_PROJECT}/${local_image_name}"
return 0
fi
# 尝试从 EU Harbor 拉取
local src_image=""
log_step "[1/5] 从 EU Harbor 拉取镜像(300秒超时)..."
# 检查镜像来源并选择对应的代理
if [[ "$input_image" =~ ^registry\.k8s\.io/ ]]; then
log_info "检测到 registry.k8s.io 镜像,使用 k8s-proxy..."
if timeout 300 docker pull "${EU_HARBOR}/k8s-proxy/${normalized_image}" &> /dev/null; then
src_image="${EU_HARBOR}/k8s-proxy/${normalized_image}"
log_success "从 EU Harbor k8s-proxy 拉取成功"
else
log_error "k8s-proxy 拉取失败: $input_image"
return 1
fi
elif [[ "$input_image" =~ ^quay\.io/ ]]; then
log_info "检测到 quay.io 镜像,使用 quay-proxy..."
if timeout 300 docker pull "${EU_HARBOR}/quay-proxy/${normalized_image}" &> /dev/null; then
src_image="${EU_HARBOR}/quay-proxy/${normalized_image}"
log_success "从 EU Harbor quay-proxy 拉取成功"
else
log_error "quay-proxy 拉取失败: $input_image"
return 1
fi
elif [[ "$input_image" =~ ^ghcr\.io/ ]]; then
log_info "检测到 ghcr.io 镜像,使用 ghcr-proxy..."
if timeout 300 docker pull "${EU_HARBOR}/ghcr-proxy/${normalized_image}" &> /dev/null; then
src_image="${EU_HARBOR}/ghcr-proxy/${normalized_image}"
log_success "从 EU Harbor ghcr-proxy 拉取成功"
else
log_error "ghcr-proxy 拉取失败: $input_image"
return 1
fi
elif timeout 300 docker pull "${EU_HARBOR}/library/${normalized_image}" &> /dev/null; then
src_image="${EU_HARBOR}/library/${normalized_image}"
log_success "从 EU Harbor library 拉取成功"
else
log_warning "library 没有此镜像或超时,尝试 dockerhub-proxy..."
# 尝试从 dockerhub-proxy 拉取
# 保留完整路径,包括组织名
local docker_image="$normalized_image"
if [[ ! "$docker_image" =~ / ]]; then
docker_image="library/${docker_image}"
fi
if timeout 300 docker pull "${EU_HARBOR}/dockerhub-proxy/${docker_image}" &> /dev/null; then
src_image="${EU_HARBOR}/dockerhub-proxy/${docker_image}"
log_success "从 EU Harbor dockerhub-proxy 拉取成功"
else
log_error "所有源都拉取失败(超时或网络问题): $input_image"
log_warning "提示:如果是网络问题,请在 EU Harbor 服务器上执行此脚本"
return 1
fi
fi
# 标记为本地 Harbor 镜像(使用扁平化名称)
local dest_image="${LOCAL_HARBOR}/${HARBOR_PROJECT}/${local_image_name}"
log_step "[2/5] 标记镜像..."
if ! docker tag "$src_image" "$dest_image"; then
log_error "标记失败"
return 1
fi
log_success "标记成功"
# 推送到本地 Harbor
log_step "[3/5] 推送到本地 Harbor..."
if ! docker push "$dest_image"; then
log_error "推送失败"
return 1
fi
log_success "推送成功"
# 验证
log_step "[4/5] 验证从本地 Harbor 拉取..."
docker rmi "$dest_image" &> /dev/null || true
if ! docker pull "$dest_image" &> /dev/null; then
log_error "验证失败"
return 1
fi
log_success "验证成功"
# 检查架构
log_step "[5/5] 检查镜像架构..."
local arch=$(docker inspect "$dest_image" --format '{{.Architecture}}' 2>/dev/null || echo "unknown")
log_success "架构: $arch"
echo ""
log_success "✅ 镜像缓存完成!"
echo ""
echo " 原始镜像: $input_image"
echo " 本地路径: $dest_image"
echo " 架构: $arch"
echo ""
echo " K8s 使用: image: $dest_image"
echo ""
return 0
}
# 从文件批量缓存
cache_from_file() {
local file="$1"
local dry_run="${2:-false}"
if [[ ! -f "$file" ]]; then
log_error "文件不存在: $file"
return 1
fi
log_info "从文件读取镜像列表: $file"
echo ""
local images=()
while IFS= read -r line || [[ -n "$line" ]]; do
# 跳过空行和注释
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
images+=("$line")
done < "$file"
if [[ ${#images[@]} -eq 0 ]]; then
log_warning "文件中没有有效的镜像"
return 0
fi
log_info "找到 ${#images[@]} 个镜像"
echo ""
cache_multiple_images "${images[@]}" "$dry_run"
}
# 缓存多个镜像
cache_multiple_images() {
local dry_run="false"
local images=()
# 解析参数
for arg in "$@"; do
if [[ "$arg" == "true" || "$arg" == "false" ]]; then
dry_run="$arg"
else
images+=("$arg")
fi
done
TOTAL_IMAGES=${#images[@]}
SUCCESS_COUNT=0
FAILED_COUNT=0
FAILED_IMAGES=()
log_info "=== 批量缓存 ${TOTAL_IMAGES} 个镜像 ==="
echo ""
for i in "${!images[@]}"; do
local image="${images[$i]}"
local current=$((i + 1))
echo ""
log_info "进度: [$current/$TOTAL_IMAGES] 处理: $image"
echo "───────────────────────────────────────────────"
if cache_single_image "$image" "$dry_run"; then
((SUCCESS_COUNT++))
else
((FAILED_COUNT++))
FAILED_IMAGES+=("$image")
fi
echo "───────────────────────────────────────────────"
done
# 显示总结
echo ""
echo "═══════════════════════════════════════════════"
log_info "批量缓存完成统计"
echo "═══════════════════════════════════════════════"
echo " 总计: $TOTAL_IMAGES"
echo " 成功: $SUCCESS_COUNT"
echo " 失败: $FAILED_COUNT"
if [[ $FAILED_COUNT -gt 0 ]]; then
echo ""
log_error "失败的镜像:"
for img in "${FAILED_IMAGES[@]}"; do
echo " ✗ $img"
done
fi
echo "═══════════════════════════════════════════════"
echo ""
return $FAILED_COUNT
}
# 从 Pod 获取镜像
get_images_from_pod() {
local pod_ref="$1"
local namespace="${pod_ref%/*}"
local pod_name="${pod_ref##*/}"
if [[ -z "$namespace" || -z "$pod_name" || "$namespace" == "$pod_name" ]]; then
log_error "Pod 格式错误,应为: namespace/pod-name"
return 1
fi
log_info "从 Pod 获取镜像: $namespace/$pod_name"
local images=$(kubectl get pod "$pod_name" -n "$namespace" -o jsonpath='{.spec.containers[*].image}' 2>/dev/null)
if [[ -z "$images" ]]; then
log_error "无法获取 Pod 镜像,请检查 Pod 是否存在"
return 1
fi
echo "$images"
}
# 更新 Deployment
update_deployment() {
local deploy_ref="$1"
local namespace="${deploy_ref%/*}"
local deploy_name="${deploy_ref##*/}"
if [[ -z "$namespace" || -z "$deploy_name" || "$namespace" == "$deploy_name" ]]; then
log_error "Deployment 格式错误,应为: namespace/deployment-name"
return 1
fi
log_info "获取 Deployment 镜像: $namespace/$deploy_name"
# 获取所有容器镜像
local containers=$(kubectl get deployment "$deploy_name" -n "$namespace" -o json 2>/dev/null | \
jq -r '.spec.template.spec.containers[] | "\(.name):\(.image)"')
if [[ -z "$containers" ]]; then
log_error "无法获取 Deployment 信息"
return 1
fi
echo ""
log_info "找到以下容器:"
echo "$containers"
echo ""
# 缓存所有镜像
local images=()
while IFS=: read -r container_name image; do
images+=("$image")
done <<< "$containers"
if ! cache_multiple_images "${images[@]}"; then
log_error "镜像缓存失败,不更新 Deployment"
return 1
fi
# 更新 Deployment
log_info "更新 Deployment 使用本地 Harbor 镜像..."
echo ""
while IFS=: read -r container_name original_image; do
local normalized_image=$(parse_image_name "$original_image")
local local_image_name=$(normalize_for_local_storage "$normalized_image")
local new_image="${LOCAL_HARBOR}/${HARBOR_PROJECT}/${local_image_name}"
log_step "更新容器 '$container_name': $new_image"
if kubectl set image deployment/"$deploy_name" \
"$container_name=$new_image" \
-n "$namespace"; then
log_success "容器 '$container_name' 更新成功"
else
log_error "容器 '$container_name' 更新失败"
fi
done <<< "$containers"
echo ""
log_success "✅ Deployment 更新完成"
echo ""
log_info "等待 Pod 重启..."
sleep 3
kubectl get pods -n "$namespace" | grep "$deploy_name"
}
# 主函数
main() {
local dry_run=false
local from_file=""
local update_deploy=""
local from_pod=""
local images=()
# 解析参数
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_usage
exit 0
;;
-f|--file)
from_file="$2"
shift 2
;;
-p|--project)
HARBOR_PROJECT="$2"
shift 2
;;
-u|--update-deployment)
update_deploy="$2"
shift 2
;;
--from-pod)
from_pod="$2"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
-*)
log_error "未知选项: $1"
show_usage
exit 1
;;
*)
images+=("$1")
shift
;;
esac
done
# 验证参数
if [[ -z "$from_file" && -z "$update_deploy" && -z "$from_pod" && ${#images[@]} -eq 0 ]]; then
log_error "请提供镜像名称或使用 -f 选项"
echo ""
show_usage
exit 1
fi
# 登录 Harbor
if [[ "$dry_run" != "true" ]]; then
if ! login_harbor; then
exit 1
fi
echo ""
fi
# 执行操作
if [[ -n "$from_pod" ]]; then
# 从 Pod 获取镜像
local pod_images=$(get_images_from_pod "$from_pod")
if [[ -z "$pod_images" ]]; then
exit 1
fi
cache_multiple_images $pod_images "$dry_run"
elif [[ -n "$update_deploy" ]]; then
# 更新 Deployment
update_deployment "$update_deploy"
elif [[ -n "$from_file" ]]; then
# 从文件批量缓存
cache_from_file "$from_file" "$dry_run"
else
# 缓存指定镜像
if [[ ${#images[@]} -eq 1 ]]; then
cache_single_image "${images[0]}" "$dry_run"
else
cache_multiple_images "${images[@]}" "$dry_run"
fi
fi
}
# 执行主函数
main "$@"