// ============================================================ // 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-dev(Secret file,k3s 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 执行完成" } } }