添加jenkins

This commit is contained in:
wwweww
2026-05-03 22:41:40 +08:00
parent b08a3a51e0
commit 13eb299316
3 changed files with 474 additions and 0 deletions
+255
View File
@@ -0,0 +1,255 @@
// ============================================================
// 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 执行完成"
}
}
}