Nexus仓库自定义镜像清理策略

nexus版本为:3.18.1-01 地址是:http://192.168.54.140:8081 用户是:admin 密码是: 123 仓库名: docker-repo

nexus的web页面可以配置定时清理策略,然后,这里只能指定清理多少天之前的镜像,或最后一次下载时间早于指定天数前的镜像,这不符合我们的要求。


image.png

我们的清理策略是:自动清理apron-repo仓库中所有镜像,每个镜像只保留最新的5个标签版本,并记录详细日志

先建一个task,在“System”->“Tasks”下创建定时任务,选择“Admin - Compact blob store”任务 ,名称为:cleanStore,每天03:00时运行


image.png

再建一个脚本,用来实现我们的清理策略。

$ vi /root/nexus_cleanup.sh
#!/bin/bash
# Nexus3 Docker镜像自动清理脚本(带日志记录)
# 功能:自动清理apron-repo仓库中所有镜像,每个镜像只保留最新的5个标签版本,并记录详细日志

# 配置区 ==============================================
NEXUS_URL="http://192.168.54.140:8081"  # Nexus地址
USERNAME="admin"                        # 用户名
PASSWORD="123"                          # 密码
REPOSITORY="docker-repo"                 # 仓库名
KEEP_LATEST=5                          # 保留最新标签数量
LOG_FILE="/tmp/nexusClean.log"          # 日志文件路径
COMPACT_BLOB_STORE=true                #压缩仓库
TASK_NAME="cleanStore"                 #压缩仓库任务名称 
IMAGE_NAMES=""
CONTINUATION_TOKEN=""

# =====================================================

# 日志函数 ===========================================
function log() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    case $level in
        "INFO")    echo -e "$timestamp [\033[32mINFO\033[0m] $message" | tee -a $LOG_FILE ;;
        "WARNING") echo -e "$timestamp [\033[33mWARNING\033[0m] $message" | tee -a $LOG_FILE ;;
        "ERROR")   echo -e "$timestamp [\033[31mERROR\033[0m] $message" | tee -a $LOG_FILE ;;
        *)         echo -e "$timestamp [UNKNOWN] $message" | tee -a $LOG_FILE ;;
    esac
}


# 日志文件自维护(大于5M则截取最后50行)
[ `du -m /tmp/nexusClean.log  | awk '{print $1}'` -gt 5 ] && tail -50 /tmp/nexusClean.log>/tmp/nexusClean-tmp && mv /tmp/nexusClean-tmp /tmp/nexusClean.log -f

# 初始化日志文件 =====================================
echo "===== Nexus清理脚本执行日志 =====" >> $LOG_FILE
log "INFO" "脚本启动时间: $(date '+%Y-%m-%d %H:%M:%S')"
log "INFO" "Nexus地址: $NEXUS_URL"
log "INFO" "目标仓库: $REPOSITORY"
log "INFO" "保留最新标签数: $KEEP_LATEST"
# =====================================================

# 函数定义 ===========================================
function get_all_images() {
    log "INFO" "正在获取仓库 $REPOSITORY 中的所有镜像..."
    # 获取所有镜像名称(去重)
    # 循环处理分页(兼容初始空值和后续的"null")
    while [ "$CONTINUATION_TOKEN" != "null" ] && { [ -z "$CONTINUATION_TOKEN" ] || [ "$CONTINUATION_TOKEN" != "null" ]; }; do
        # 构造API请求URL(首次请求不带continuationToken参数)
        if [ -z "$CONTINUATION_TOKEN" ]; then
            API_URL="$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY"
        else
            API_URL="$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY&continuationToken=$CONTINUATION_TOKEN"
        fi
    
        # 发送请求并处理响应
        API_RESPONSE=$(curl -s -u "$USERNAME:$PASSWORD" -X GET "$API_URL" -H "accept:application/json")
        
        # 追加当前页结果
        PAGE_NAMES=$(echo "$API_RESPONSE" | jq -r '.items[].name' | sort | uniq)
        IMAGE_NAMES+=$'\n'"$PAGE_NAMES"
        
        # 获取下一页token(处理jq返回的"null"字符串)
        CONTINUATION_TOKEN=$(echo "$API_RESPONSE" | jq -r '.continuationToken // empty')
        if [ -z "$CONTINUATION_TOKEN" ]; then
            CONTINUATION_TOKEN="null"  # 无后续分页时显式设置为"null"
        fi
    done
    
    IMAGE_NAMES=$(echo "$IMAGE_NAMES" | sort | uniq)
        
    if [ -z "$IMAGE_NAMES" ]; then
        log "ERROR" "未获取到任何镜像信息"
        exit 1
    fi
    log "INFO" "找到以下镜像:\n$IMAGE_NAMES"
}

function cleanup_image_tags() {
    local image_name=$1
    log "INFO" "开始处理镜像: $image_name"
    
    # 获取该镜像所有标签(按创建时间倒序排序)
    TAGS=$(curl -s -u "$USERNAME:$PASSWORD" -X GET \
        "$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY&name=$image_name" \
        -H "accept:application/json" | jq -r '.items | sort_by(.created) | reverse | .[].version')
    
    TOTAL_TAGS=$(echo "$TAGS" | wc -l)
    log "INFO" "镜像 $image_name 共有 $TOTAL_TAGS 个标签"
    
    if [ $TOTAL_TAGS -le $KEEP_LATEST ]; then
        log "INFO" "标签数量未超过保留限制($KEEP_LATEST),跳过处理"
        return
    fi
    
    # 计算需要删除的标签数量
    TAGS_TO_DELETE=$((TOTAL_TAGS - KEEP_LATEST))
    log "INFO" "需要删除 $TAGS_TO_DELETE 个旧标签"
    
    # 获取要删除的标签列表(跳过前KEEP_LATEST个)
    DELETE_TAGS=$(echo "$TAGS" | tail -n +$((KEEP_LATEST + 1)))
    
    # 逐个删除旧标签
    for tag in $DELETE_TAGS; do
        log "INFO" "正在删除标签: $tag"
        # 获取组件ID
        COMPONENT_ID=$(curl -s -u "$USERNAME:$PASSWORD" -X GET \
            "$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY&name=$image_name&version=$tag" \
            -H "accept:application/json" | jq -r '.items[0].id')
        
        if [ -z "$COMPONENT_ID" ] || [ "$COMPONENT_ID" == "null" ]; then
            log "WARNING" "未找到 $image_name:$tag 的组件ID"
            continue
        fi
        
        # 执行删除
        RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -u "$USERNAME:$PASSWORD" -X DELETE \
            "$NEXUS_URL/service/rest/v1/components/$COMPONENT_ID" \
            -H "accept:application/json")
        
        if [ "$RESPONSE" -eq 204 ]; then
            log "INFO" "成功删除 $image_name:$tag (组件ID: $COMPONENT_ID)"
        else
            log "ERROR" "删除失败,HTTP状态码: $RESPONSE"
        fi
    done
}
 


function compact_blob_store() {
    log "INFO" "正在触发Blob存储压缩任务..."
    
    # 1. 获取已存在任务的ID
    TASK_ID=$(curl -s -u "$USERNAME:$PASSWORD" \
    "$NEXUS_URL/service/rest/v1/tasks" \
    -H "accept: application/json" | \
    jq -r --arg TASK_NAME "$TASK_NAME" '.items[] | select(.name==$TASK_NAME) | .id')
    

    CURRENT_STATE=$(echo "$TASK_INFO" | jq -r '.currentState')

    # 2. 检查任务是否存在
    if [ -z "$TASK_ID" ]; then
        log "ERROR" "未找到名为'$TASK_NAME'的任务,请先在UI创建"
        return 1
    fi

    # 3. 触发任务执行(如果任务未在运行)
    if [ "$CURRENT_STATE" != "RUNNING" ]; then
        RUN_RESULT=$(curl -s -o /dev/null -w "%{http_code}" -u "$USERNAME:$PASSWORD" -X POST \
            "$NEXUS_URL/service/rest/v1/tasks/$TASK_ID/run")
        
        if [ "$RUN_RESULT" -eq 204 ]; then
            log "INFO" "任务触发成功 (ID: $TASK_ID),等待完成..."
        else
            log "ERROR" "任务触发失败,HTTP状态码: $RUN_RESULT"
            return 1
        fi
    else
        log "WARN" "任务已在运行中 (ID: $TASK_ID)"
    fi

} 
 
 
# =====================================================

# 主执行流程 ==========================================
# 1. 获取所有镜像名称
get_all_images

# 2. 对每个镜像执行清理
for image in $IMAGE_NAMES; do
    cleanup_image_tags "$image"
done

# 3. 可选清理操作
if [ "$COMPACT_BLOB_STORE" = true ]; then
    compact_blob_store
fi

log "INFO" "所有操作已完成!"
log "INFO" "脚本结束时间: $(date '+%Y-%m-%d %H:%M:%S')"
# ===================================================== 


$ chmod +x  /root/nexus_cleanup.sh

说明:

  • 为什么在清理镜像后要进行压缩:因为只删除镜像它只是为镜像打上delete标记,并不会真正删除
  • 当前nexus所支持的api调用,可以在System--API菜单下找到,并测试,也可以在命令行测试:
curl -v -u admin:123   "http://192.168.54.140:8081/service/rest/v1/tasks"
*   Trying 192.168.54.140...
* TCP_NODELAY set
* Connected to 192.168.54.140 (192.168.54.140) port 8081 (#0)
* Server auth using Basic with user 'admin'
> GET /service/rest/v1/tasks HTTP/1.1
> Host: 192.168.54.140:8081
> Authorization: Basic YWRtaW46c3pjYWFjfjE=
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 17 Jun 2025 08:45:47 GMT
< Server: Nexus/3.18.1-01 (OSS)
< X-Content-Type-Options: nosniff
< Content-Type: application/json
< Content-Length: 693
<
{
  "items" : [ {
    "id" : "194ab8d3-b40d-4837-a248-77db54e04f86",
    "name" : "Cleanup service",
    "type" : "repository.cleanup",
    "message" : "Run repository cleanup",
    "currentState" : "WAITING",
    "lastRunResult" : "OK",
    "nextRun" : "2025-06-18T01:00:00.000+0000",
    "lastRun" : "2025-06-17T01:00:00.004+0000"
  }, {
    "id" : "abebd256-fe95-4ee2-a2f3-7b769ba4e5c9",
    "name" : "cleanStore",
    "type" : "blobstore.compact",
    "message" : "Compacting default blob store",
    "currentState" : "WAITING",
    "lastRunResult" : "OK",
    "nextRun" : "2025-06-17T19:00:00.000+0000",
    "lastRun" : "2025-06-17T08:19:20.597+0000"
  } ],
  "continuationToken" : null
* Connection #0 to host 192.168.54.140 left intact
  • 可以看到脚本中删除镜像或调用任务时,都需要先找到它们的id,然后再以id来调用,系统并不支持直接以name来调用

  • 这里docker镜像的标准是yyyymmddHHmmss格式,以便于排序

再将以上脚本配置为定时任务(每周日凌晨2点执行,UTC时间 )

$ (crontab -l | grep "$cron_job"; echo "0 18 * * 0 /root/nexus_cleanup.sh >/dev/null 2>&1") | crontab -
no crontab for root
$  crontab -l  # 查看一下
0 18 * * 0 /root/nexus_cleanup.sh >/dev/null 2>&1

手工测试一下脚本

$ /root/nexus_cleanup.sh
 
2025-06-17 08:58:16 [INFO] 脚本启动时间: 2025-06-17 08:58:16
2025-06-17 08:58:16 [INFO] Nexus地址: http://192.168.54.140:8081
2025-06-17 08:58:16 [INFO] 目标仓库: apron-repo
2025-06-17 08:58:16 [INFO] 保留最新标签数: 5
2025-06-17 08:58:16 [INFO] 正在获取仓库 apron-repo 中的所有镜像...
2025-06-17 08:58:16 [INFO] 找到以下镜像:
app1
dev/app1
helloworld
kuboard
nexus3
2025-06-17 08:58:16 [INFO] 开始处理镜像: app1
2025-06-17 08:58:16 [INFO] 镜像 app1 共有 4 个标签
2025-06-17 08:58:16 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:16 [INFO] 开始处理镜像: dev/app1
2025-06-17 08:58:16 [INFO] 镜像 dev/app1 共有 5 个标签
2025-06-17 08:58:16 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:16 [INFO] 开始处理镜像: helloworld
2025-06-17 08:58:16 [INFO] 镜像 helloworld 共有 6 个标签
2025-06-17 08:58:16 [INFO] 需要删除 1 个旧标签
2025-06-17 08:58:16 [INFO] 正在删除标签: 202506140429
2025-06-17 08:58:17 [INFO] 成功删除 helloworld:202506140429 (组件ID: YXByb24tcmVwbzpkZmJlZjA5ZWZlMTY0NGVhZjA2OTYxMmI1MTg5NjRkMQ)
2025-06-17 08:58:17 [INFO] 开始处理镜像: kuboard
2025-06-17 08:58:17 [INFO] 镜像 kuboard 共有 1 个标签
2025-06-17 08:58:17 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:17 [INFO] 开始处理镜像: nexus3
2025-06-17 08:58:17 [INFO] 镜像 nexus3 共有 1 个标签
2025-06-17 08:58:17 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:17 [INFO] 正在触发Blob存储压缩任务...
2025-06-17 08:58:17 [INFO] 任务触发成功 (ID: abebd256-fe95-4ee2-a2f3-7b769ba4e5c9),等待完成...
2025-06-17 08:58:17 [INFO] 所有操作已完成!
2025-06-17 08:58:17 [INFO] 脚本结束时间: 2025-06-17 08:58:17

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容