543 lines
17 KiB
Bash
Executable File
543 lines
17 KiB
Bash
Executable File
#!/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 "$@"
|