Files
juwan-backend/deploy/jenkins/Jenkinsfile.poll
T

273 lines
12 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)。留空则自动从 Harbor 查询实际存在的仓库'
)
booleanParam(
name: 'FORCE_DEPLOY',
defaultValue: false,
description: '强制部署所有服务(忽略 digest 对比,用于手动触发全量更新)'
)
}
// ── 触发器:每 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()
echo "使用指定服务列表: ${env.SERVICE_LIST}"
} else {
// 直接查询 Harbor API,只监控实际存在的仓库
// 避免扫描 k8s yaml 导致检查大量不存在的镜像
withCredentials([
usernamePassword(
credentialsId: 'harbor-credentials',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)
]) {
def services = sh(
script: """
curl -s -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
--connect-timeout 10 --max-time 30 \\
"${HARBOR_API}/projects/${params.HARBOR_PROJECT}/repositories?page_size=100" \\
| python3 -c "
import sys, json
repos = json.load(sys.stdin)
names = [r['name'].split('/')[-1] for r in repos if isinstance(repos, list)]
print(','.join(sorted(names)))
" 2>/dev/null || echo ""
""",
returnStdout: true
).trim()
if (!services) {
error("无法从 Harbor 获取仓库列表,请检查 harbor-credentials 凭据和 Harbor 地址")
}
env.SERVICE_LIST = services
}
echo "从 Harbor 自动发现服务列表: ${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}"
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
}
def stateFile = "${DIGEST_STATE_DIR}/${svc}.digest"
def lastDigest = fileExists(stateFile) ? readFile(stateFile).trim() : ""
echo " 当前 digest: ${digestResult}"
echo " 上次 digest: ${lastDigest ?: '(首次检测)'}"
if (params.FORCE_DEPLOY || digestResult != lastDigest) {
if (params.FORCE_DEPLOY) {
echo " 🔄 强制部署模式,加入部署队列"
} else {
echo " ✅ 检测到变化,加入部署队列"
}
changedServices << svc
writeFile file: stateFile, text: digestResult
} else {
echo " — 无变化,跳过"
}
}
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 执行完成"
}
}
}