Files
jenkins-pipeline/SCM/构建镜像/build_image_lessie_agents_v2.groovy
2025-12-03 15:31:02 +08:00

309 lines
14 KiB
Groovy
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.

// --- 辅助函数:深拷贝对象以确保可序列化 ---
def deepCopyForSerialization(obj) {
if (obj instanceof Map) {
// 创建新的 LinkedHashMap递归拷贝值
return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] }
} else if (obj instanceof List) {
// 创建新的 ArrayList递归拷贝元素
return obj.collect { item -> deepCopyForSerialization(item) }
} else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) {
return obj
} else {
return obj.toString()
}
}
// --- 结束辅助函数 ---
pipeline {
agent any
parameters {
gitParameter(
branchFilter: 'origin/(.*)',
defaultValue: 'dxin',
name: 'Code_branch',
type: 'PT_BRANCH_TAG',
selectedValue: 'DEFAULT',
sortMode: 'NONE',
description: '选择代码分支:',
quickFilterEnabled: true,
tagFilter: '*',
listSize: "1"
)
choice(
name: 'NAME_SPACES',
choices: ['sit', 'test', 'prod'],
description: '选择存放镜像的仓库命名空间:'
)
string(
name: 'CUSTOM_TAG',
defaultValue: '',
description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”'
)
}
environment {
REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址
NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间
IMAGE_NAME = "lessie-sourcing-agents" // 镜像名(固定前缀)
CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID
}
stages {
stage('拉取代码') {
steps {
// 拉取指定分支代码(通过参数 params.Code_branch 动态指定)
git branch: "${params.Code_branch}",
credentialsId: 'fly_gitlab_auth',
url: 'http://172.24.16.20/python/lessie-sourcing-agents.git'
}
}
stage('获取信息') {
steps {
script {
env.Code_branch = "${params.Code_branch}"
env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an', returnStdout: true).trim()
env.GIT_COMMIT_TIME = sh(script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', returnStdout: true).trim()
env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim()
def buildNumber = env.BUILD_NUMBER
def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
def formattedBranch = branchName.replace('/', '-').replace('_', '-')
def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim()
def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}"
def customTag = params.CUSTOM_TAG?.trim()
def tagPattern = ~/^[a-zA-Z0-9._-]+$/
if (customTag && customTag ==~ tagPattern) {
echo "✅ 使用自定义镜像 Tag: ${customTag}"
env.IMAGE_TAG = customTag
} else if (customTag) {
echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}"
def confirmed = true
timeout(time: 1, unit: 'MINUTES') {
try {
input(
message: """⚠️ Tag 命名不规范:
${customTag}
将使用自动生成的 Tag
${defaultTag}
是否继续构建?""",
ok: '确认'
)
} catch (err) {
// 用户点击“取消”或中断
echo "🚫 用户取消构建"
confirmed = false
}
}
if (confirmed) {
echo "✅ 用户确认使用自动生成的 Tag${defaultTag}"
env.IMAGE_TAG = defaultTag
} else {
error("流水线已终止。")
}
} else {
env.IMAGE_TAG = defaultTag
echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}"
}
}
}
}
stage('登录仓库') {
steps {
withCredentials([usernamePassword(
credentialsId: env.CREDENTIALS_ID,
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PWD'
)]) {
sh '''
echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin
'''
}
}
}
stage('构建镜像') {
steps {
script {
// 构建镜像,添加标签信息
sh """
docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \
--label "git-branch='${Code_branch}'" \
--label "git-commit='${GIT_COMMIT_SHORT}'" \
--label "git-author='${GIT_AUTHOR}'" \
--label "git-message='${GIT_COMMIT_MSG}'" \
--label "build-time='${GIT_COMMIT_TIME}'" \
.
"""
}
}
}
stage('推送镜像') {
steps {
script {
sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}"
}
}
}
}
post {
always {
script {
def keepCount = 3
echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本"
def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}"
// 获取所有镜像(按创建时间排序,越新的越前)
// 格式Repository:Tag ImageID CreatedAt
def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim()
if (!allImagesRaw) {
echo "未找到任何镜像,无需清理"
return
}
def allImages = allImagesRaw.split('\n')
if (allImages.size() <= keepCount) {
echo "当前镜像数未超过 ${keepCount} 个,无需清理"
return
}
def oldImages = allImages.drop(keepCount)
echo "发现 ${oldImages.size()} 个旧镜像需要清理"
oldImages.each { line ->
echo " ${line}"
}
oldImages.each { line ->
def parts = line.split(' ')
def imageTag = parts[0]
def imageId = parts.size() > 1 ? parts[1] : ""
// 对于标签为<none>的无效镜像使用镜像ID删除
if (imageTag.contains("<none>") && imageId) {
echo "删除无效镜像: ${imageId}"
sh(returnStatus: true, script: "docker rmi -f ${imageId} || true")
} else if (imageId) {
// 对于有标签的有效镜像优先使用镜像ID删除
echo "删除旧镜像: ${imageTag} (${imageId})"
sh(returnStatus: true, script: "docker rmi -f ${imageId} || true")
} else {
// 兜底方案,使用标签删除
echo "删除旧镜像: ${imageTag}"
sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true")
}
}
echo "清理完成,当前镜像状态:"
sh """
docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}'
"""
sh "docker logout ${REGISTRY}"
echo "容器仓库已登出,本地凭证已清理"
}
}
success {
script {
// 1. 准备元数据 (转换所有环境变量为 String)
def metadataDir = '/var/lib/jenkins/metadata'
def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名
def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}"
// --- 转换为 String ---
def registry = env.REGISTRY as String
def namespace = env.NAMESPACE as String
def imageName = env.IMAGE_NAME as String
def imageTag = env.IMAGE_TAG as String
def codeBranch = params.Code_branch as String // 使用 params因为 Code_branch 是参数
def gitCommit = env.GIT_COMMIT_LONG as String
def gitAuthor = env.GIT_AUTHOR as String
def gitCommitMsg = env.GIT_COMMIT_MSG as String
def gitCommitTime = env.GIT_COMMIT_TIME as String
def buildNumber = env.BUILD_NUMBER as String
// --- 转换为 String ---
// 2. 准备新数据
def newImageData = [
image_tag: imageTag, // 使用转换后的变量
full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量
labels: [
"git-branch": codeBranch,
"git-commit": gitCommit,
"git-author": gitAuthor,
"git-message": gitCommitMsg,
"build-time": gitCommitTime
],
build_job_number: buildNumber,
build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间
]
// 2. 读取现有数据(如果文件存在)
def existingDataList = []
try {
// 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题)
def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 []
// --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) ---
if (rawExistingData instanceof List) {
existingDataList = rawExistingData.collect { item ->
if (item instanceof Map) {
// 递归深拷贝 Map (使用辅助函数)
return deepCopyForSerialization(item)
} else {
return item
}
}
} else {
echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。"
existingDataList = []
}
// --- 结束修复 ---
} catch (Exception e) {
// readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常
echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。"
// 确保目录存在
sh "mkdir -p ${metadataDir}"
existingDataList = [] // 重置为新列表
}
// 3. 将新数据添加到列表开头(最新的在前)
existingDataList.add(0, newImageData)
// 4. 限制列表大小为 20
if (existingDataList.size() > 20) {
existingDataList = existingDataList.take(20)
}
// 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化)
writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进)
echo "镜像元数据已存储到: ${fullMetadataPath}"
// 输出构建结果
echo """
镜像地址:${registry}/${namespace}/${imageName}:${imageTag}
对应代码提交哈希:${gitCommit}
对应代码分支:${codeBranch}
代码提交者:${gitAuthor}
提交备注:${gitCommitMsg}
""".stripIndent()
}
}
failure {
// 输出构建结果
echo "有失败步骤!"
}
}
}