Files
juwan-backend/deploy/jenkins/Jenkinsfile.poll
T
2026-05-03 22:41:40 +08:00

256 lines
11 KiB
Plaintext
Raw 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.
// ============================================================
// juwan-backend CD Pipeline — Harbor 主动轮询模式
// 适用场景:本地开发机无公网,Jenkins 主动检测 Harbor 镜像更新
//
// 工作原理:
// 1. Jenkins 定时(默认每 2 分钟)调用 Harbor Registry API
// 2. 对比每个镜像的最新 digest 与上次记录的 digest
// 3. 发现变化则触发对应服务的 kubectl rollout restart
//
// 前置条件(见 docs/jenkins-cd/01-local-dev-setup.md):
// - Jenkins 凭据:harbor-credentials(用户名/密码)
// - Jenkins 凭据:kubeconfig-devSecret filek3s kubeconfig
// - Jenkins 节点上已安装 kubectl 并可访问 k3s 集群
// ============================================================
pipeline {
agent any
// ── 可调参数 ──────────────────────────────────────────────
parameters {
// Harbor 地址,不含协议前缀
string(
name: 'HARBOR_REGISTRY',
defaultValue: '103.236.53.208:4418',
description: 'Harbor 镜像仓库地址(host:port'
)
string(
name: 'HARBOR_PROJECT',
defaultValue: 'juwan',
description: 'Harbor 项目名'
)
string(
name: 'K8S_NAMESPACE',
defaultValue: 'juwan',
description: 'Kubernetes 命名空间'
)
// 轮询时只检查 latest tag
string(
name: 'IMAGE_TAG',
defaultValue: 'latest',
description: '要监听的镜像 Tag'
)
// 逗号分隔,留空则自动扫描所有服务
string(
name: 'TARGET_SERVICES',
defaultValue: '',
description: '指定要部署的服务(逗号分隔,如 user-api,user-rpc)。留空则部署所有有变化的服务'
)
}
// ── 触发器:每 2 分钟轮询一次 ────────────────────────────
triggers {
// cron 表达式:H/2 * * * * → 每 2 分钟
// 生产环境改用 Webhook,删除此 triggers 块即可
cron('H/2 * * * *')
}
environment {
// Harbor API v2 基础路径
HARBOR_API = "http://${params.HARBOR_REGISTRY}/api/v2.0"
// digest 状态文件存放目录(Jenkins workspace 内持久化)
DIGEST_STATE_DIR = "${WORKSPACE}/.digest-state"
// kubectl 命令(节点上的实际路径)
KUBECTL = 'kubectl'
}
stages {
// ── Stage 1:初始化 ───────────────────────────────────
stage('Init') {
steps {
script {
echo "=== juwan-backend CD Pipeline (Poll Mode) ==="
echo "Harbor: ${params.HARBOR_REGISTRY}/${params.HARBOR_PROJECT}"
echo "Namespace: ${params.K8S_NAMESPACE}"
echo "Tag: ${params.IMAGE_TAG}"
// 创建 digest 状态目录(跨构建持久化)
sh "mkdir -p ${DIGEST_STATE_DIR}"
// 构建要检查的服务列表
if (params.TARGET_SERVICES?.trim()) {
env.SERVICE_LIST = params.TARGET_SERVICES.trim()
} else {
// 自动从 deploy/k8s/service 目录扫描所有 Deployment 名称
// 命名规律:{service}-{api|rpc|mq}
def services = sh(
script: '''
find deploy/k8s/service -name "*.yaml" \
| xargs grep -l "kind: Deployment" \
| xargs -I{} sh -c 'grep "^ name:" {} | head -1 | awk "{print \\$2}"' \
| sort -u \
| tr "\\n" ","
''',
returnStdout: true
).trim().replaceAll(/,$/, '')
env.SERVICE_LIST = services
}
echo "监控服务列表: ${env.SERVICE_LIST}"
}
}
}
// ── Stage 2:检测镜像变化 ─────────────────────────────
stage('Detect Changes') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'harbor-credentials',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)
]) {
def serviceList = env.SERVICE_LIST.split(',').collect { it.trim() }.findAll { it }
def changedServices = []
serviceList.each { svc ->
if (!svc) return
echo "检查镜像: ${svc}:${params.IMAGE_TAG}"
// 调用 Harbor API 获取最新 digest
def digestResult = sh(
script: """
curl -s -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
--connect-timeout 10 \\
--max-time 30 \\
"${HARBOR_API}/projects/${params.HARBOR_PROJECT}/repositories/${svc}/artifacts/${params.IMAGE_TAG}" \\
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('digest',''))" 2>/dev/null || echo ""
""",
returnStdout: true
).trim()
if (!digestResult) {
echo " ⚠️ 无法获取 ${svc} 的 digest,跳过(服务可能尚未推送)"
return
}
// 读取上次记录的 digest
def stateFile = "${DIGEST_STATE_DIR}/${svc}.digest"
def lastDigest = ""
if (fileExists(stateFile)) {
lastDigest = readFile(stateFile).trim()
}
echo " 当前 digest: ${digestResult}"
echo " 上次 digest: ${lastDigest ?: '(首次检测)'}"
if (digestResult != lastDigest) {
echo " ✅ 检测到变化,加入部署队列"
changedServices << svc
// 立即更新 digest 记录,防止重复触发
writeFile file: stateFile, text: digestResult
} else {
echo " — 无变化,跳过"
}
}
// 将变化列表传递给下一个 Stage
env.CHANGED_SERVICES = changedServices.join(',')
echo "需要更新的服务: ${env.CHANGED_SERVICES ?: '(无变化)'}"
}
}
}
}
// ── Stage 3:部署到 k3s ───────────────────────────────
stage('Deploy') {
when {
expression { env.CHANGED_SERVICES?.trim() }
}
steps {
script {
withCredentials([
file(credentialsId: 'kubeconfig-dev', variable: 'KUBECONFIG_FILE')
]) {
def changedList = env.CHANGED_SERVICES.split(',').collect { it.trim() }.findAll { it }
changedList.each { svc ->
echo "=== 部署 ${svc} ==="
// 验证 Deployment 是否存在
def exists = sh(
script: """
KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} get deployment ${svc} \\
-n ${params.K8S_NAMESPACE} \\
--ignore-not-found \\
-o name 2>/dev/null || echo ""
""",
returnStdout: true
).trim()
if (!exists) {
echo " ⚠️ Deployment ${svc} 不存在于 ${params.K8S_NAMESPACE},跳过"
return
}
// 触发滚动重启(imagePullPolicy: Always 会拉取最新 latest 镜像)
sh """
KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout restart deployment/${svc} \\
-n ${params.K8S_NAMESPACE}
"""
// 等待滚动更新完成(超时 3 分钟)
def rolloutStatus = sh(
script: """
KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout status deployment/${svc} \\
-n ${params.K8S_NAMESPACE} \\
--timeout=180s
""",
returnStatus: true
)
if (rolloutStatus == 0) {
echo " ✅ ${svc} 部署成功"
} else {
// 部署失败:回滚并标记失败
echo " ❌ ${svc} 部署超时或失败,执行回滚..."
sh """
KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout undo deployment/${svc} \\
-n ${params.K8S_NAMESPACE}
"""
error("${svc} 部署失败,已回滚")
}
}
}
}
}
}
// ── Stage 4:部署摘要 ─────────────────────────────────
stage('Summary') {
steps {
script {
if (env.CHANGED_SERVICES?.trim()) {
echo "=== 本次部署完成 ==="
echo "已更新服务: ${env.CHANGED_SERVICES}"
} else {
echo "=== 无服务需要更新 ==="
}
}
}
}
}
post {
failure {
echo "❌ Pipeline 失败,请检查上方日志"
}
success {
echo "✅ Pipeline 执行完成"
}
}
}