脚本

https://github.xyh.moe:8888/xian.yuheng/docker-push

#! /bin/bash

# 禁止 ^C 显示
stty -echoctl
#stty echoctl
################################################################################
# 检测 docker 是否存在
command -v docker 1>/dev/null 2>/dev/null || { echo -e "\e[31m[错误] 未检测到 Docker。\e[0m"; exit 1; }
# 检测是否有 docker 运行权限
docker version    1>/dev/null 2>/dev/null || { echo -e "\e[31m[错误] 无 Docker 运行权限。\e[0m"; exit 1; }
################################################################################
# 帮助文档
help_str=$(cat << EOF
参数说明:
    -h, --help               帮助文档
    -f, --file               指定镜像列表文件
    -r, --registry           自定义推送仓库地址
    -a, --arch               指定架构 "linux/amd64" "linux/arm64"
    -t, --force-tag          强制重拉推tag
    --delete                 删除已推送本地镜像
    CTRL + C                 跳过当前镜像
    CTRL + \                 退出脚本程序
EOF
)
#################################################################################
# 函数
function generate_horizontal_line()
{   # 创建一条和终端等宽的 分割线 [horizontal line]
    printf "%0.s${mark:-=}" $(seq $(tput cols)) && printf "\n"
}

skip_signal=false
function handle_skip()
{   # 跳过信号
    skip_signal=true
}
trap handle_skip SIGINT     # Ctrl+C -> 跳过

quit_signal=false
function handle_quit()
{   # 退出信号
    echo -e "❌ 收到退出信号,正在终止脚本..."
    quit_signal=true
}
trap handle_quit SIGQUIT    # Ctrl+\ -> 退出

function check_signals()
{   # 检查跳过和终止信号
    local image="$1"
    if [[ "$quit_signal" == true ]]; then
        exit 0
    fi

    if [[ "$skip_signal" == true ]]; then
        echo -e "\e[33m[跳过镜像] $image\e[0m"
        skip_signal=false
        return 1   # 返回 1 表示跳过当前镜像
    fi

    return 0       # 返回 0 表示继续处理
}

function check_remote_image()
{   # 判断 远程镜像 是否存在
    # ${1} 一个 镜像名 形如 registry/repo/image:tag
    
    #   存在 返回 0
    # 不存在 返回 1
    
    docker manifest inspect "$1" 1>/dev/null 2>/dev/null
}

function check_force_tag()
{   # 判断是否为强制拉推tag
    local tag="$1"
    shift
    local force_tags=("$@")
    for t in "${force_tags[@]}"; do
        [[ "$tag" == "$t" ]] && return 0
    done
    return 1
}

function read_image_file()
{   # 处理 输入文件
    # 排除 # 开头的行
    # 排除 > 开头的行
    # 排除 空白字符行

    local input_file="$1"
    local image_list=()
    while IFS= read -r line; do
        [[ "$line" =~ ^\s*$ ]] && continue
        [[ "$line" =~ ^[#\>] ]] && continue
        image_list+=("$line")
    done < "$input_file"
    echo "${image_list[@]}"
}

function format_image_name()
{   # 格式化镜像名
    local image="$1"
    local fallback_registry="$2"

    local tag name

    # 判断是否带tag,是否带:,且冒号后是否不带 /。
    if [[ "$image" == *:* && "${image##*:}" != */* ]]; then
        tag="${image##*:}"
        name="${image%:*}"
    else
        tag="latest"
        name="$image"
    fi

    IFS='/' read -ra parts <<< "$name"

    local registry repo_parts image_name repo

    if [[ "${#parts[@]}" -ge 2 && ( "${parts[0]}" == *.* || "${parts[0]}" == *:* || "${parts[0]}" == "localhost" ) ]]; then
        registry="$fallback_registry"
        image_name="${parts[${#parts[@]}-1]}"
        repo_parts=("${parts[@]:1:${#parts[@]}-2}")
    else
        registry="$fallback_registry"
        image_name="${parts[${#parts[@]}-1]}"
        repo_parts=("${parts[@]:0:${#parts[@]}-1}")
        [[ "${#repo_parts[@]}" -eq 0 ]] && repo_parts=("library")
    fi

    repo="$(IFS=/; echo "${repo_parts[*]}")"

    if [[ -n "$repo" ]]; then
        echo "${registry}/${repo}/${image_name}:${tag}"
    else
        echo "${registry}/library/${image_name}:${tag}"
    fi
}

################################################################################
# 默认参数
# 强制tag
force_tags=("latest" "stable" "LTS" "lts" "master")
# 指定架构
architecture=""
################################################################################
# 参数判断
ARGS=` \
    getopt \
    -o hf:r:t:a: \
    -l help,file:,registry:,arch,force-tag:,delete \
    -n "$0" \
    -- "$@" \
`
[ $? != 0 ] && echo -e "$help_str" && exit 1;
eval set -- "${ARGS}"

while true; do
    case "$1" in
        -h | --help)             echo -e "$help_str"; exit  0 ;;
        -f | --file)                       file="$2"; shift 2 ;;
        -r | --registry)        custom_registry="$2"; shift 2 ;;
        -a | --arch)               architecture="$2"; shift 2 ;;
        --delete)                  remove_local=true; shift   ;;
        -t | --force-tag)         # 支持逗号分隔多个tag,或者多次使用此参数都加进去
            IFS=',' read -ra extra_tags <<< "$2"
            for t in "${extra_tags[@]}"; do
                force_tags+=("$t")
            done
            shift 2
            ;;
        --) shift; break;;
        *) echo "未知错误"; exit 1 ;;
    esac
done

# 推送源
push_registry="${custom_registry:-docker.io}"
################################################################################
if [[ -n "$file" ]]; then
    images=($(read_image_file "$file"))
else
    images=($(docker images --format '{{.Repository}}:{{.Tag}}'))
fi

generate_horizontal_line
printf "%-24s: %s\n" 推送仓库地址 "$push_registry"
printf "%-24s: %s\n" 镜像列表文件 "${file:-[使用本地镜像]}"
generate_horizontal_line

# 输出 镜像 列表
index=1
for image in "${images[@]}"; do
    printf "%5d. %s\n" "$index" "$image"
    ((index++))
done
generate_horizontal_line
################################################################################
# 推送镜像
for image in "${images[@]}";do
    check_signals "$image" || continue

    formated_image_name=$(format_image_name "$image" "$push_registry")

    # 获取 tag
    tag="${image##*:}"
    force_tag=false

    if check_force_tag "$tag" "${force_tags[@]}" || ! check_remote_image "$formated_image_name"; then
        # 拉取镜像
        if [[ -n "$architecture" ]]; then
            docker pull --platform "$architecture" "$image" 1>/dev/null 2>/dev/null
        else
            docker pull                            "$image" 1>/dev/null 2>/dev/null
        fi

        if [[ $? == 0 ]]; then
            echo -e "\e[32m[拉取成功] $image\e[0m"
            check_signals "$image" || continue
            docker tag "$image" "$formated_image_name"
            docker push "$formated_image_name" 1>/dev/null
            if [[ $? == 0 ]]; then
                cleaning_list+=("$image")
                echo -e "\e[32m[推送成功] $formated_image_name\e[0m"
            else
                echo -e "\e[31m[推送失败] $formated_image_name\e[0m"
            fi
            docker rmi "$formated_image_name" 1>/dev/null 2>/dev/null
        else
            check_signals "$image" || continue
            echo -e "\e[31m[拉取失败] $image\e[0m"
        fi
    else
        echo -e "\e[34m[镜像存在] $formated_image_name\e[0m"
        cleaning_list+=("$image")
    fi
done
################################################################################
# 清理镜像
if [[ "$remove_local" == true ]]; then
    #generate_horizontal_line
    #echo "开始删除本地镜像..."
    for image in "${cleaning_list[@]}"; do
        docker rmi "$image" 1>/dev/null 2>/dev/null
        [ $? -eq 0 ] && echo -e "\e[32m[清理镜像] $image\e[0m"
    done
    #echo "本地镜像删除完成"
    #generate_horizontal_line
fi
################################################################################

exit 0

镜像文件格式

# Nginx
docker.io/library/nginx:1.25-alpine
docker.io/library/nginx:1.25.0-alpine
docker.io/library/nginx:1.25.1-alpine
docker.io/library/nginx:1.25.2-alpine