kra-new/deploy/Jenkinsfile

203 lines
11 KiB
Groovy
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.

pipeline {
agent any
options {
disableConcurrentBuilds()
}
environment {
// ══════════════════════════════════════════════════════════════════
// 项目级参数 —— 基于 kra 模板的新项目只需修改本块。
// 这些变量自动 export 到 shell由 envsubst 白名单渲染进 k8s 清单。
// ══════════════════════════════════════════════════════════════════
APP_NAME = 'kra' // 派生:服务名/Secret名/库名/存储声明/镜像后缀
IMAGE_REPO = 'library/kra' // Harbor 中镜像仓库路径
REGISTRY = '192.168.200.132'
BIN_NAME = 'kratos-admin' // Makefile build 的产物名
KRA_REPLICAS = '3'
KRA_FRONTEND_NODEPORT = '30080'
KRA_BACKEND_NODEPORT = '30081'
KRA_MYSQL_IMAGE = 'docker.m.daocloud.io/library/mysql:8.4'
KRA_REDIS_IMAGE = 'docker.m.daocloud.io/library/redis:7-alpine'
KRA_SHARED_STORAGE_CLASS = 'longhorn' // 必须支持 ReadWriteMany按目标集群实际存储类修改
GO_IMAGE = 'docker.m.daocloud.io/golang:1.25'
NODE_IMAGE = 'docker.m.daocloud.io/node:20-slim'
PNPM_VERSION = '10.15.1'
CACHE_DIR = '/var/lib/kra-ci-cache' // Jenkins 宿主机依赖缓存根目录
// Jenkins 凭据 ID新项目若沿用同一套凭据可不改
CRED_HARBOR_PUSH = 'harbor-library-push'
CRED_KUBECONFIG_DEV = 'kra-rke2-128-kubeconfig'
CRED_KUBECONFIG_PROD = 'kra-rke2-129-kubeconfig'
CRED_MYSQL_PASSWORD = 'kra-mysql-root-password'
// k8s 清单渲染白名单:只替换列出的变量,避免误伤清单内 shell 变量(如 probe 里的 $MYSQL_ROOT_PASSWORD
K8S_RENDER_VARS = '$KRA_APP $KRA_NAMESPACE $KRA_ENV $KRA_BACKEND_IMAGE $KRA_FRONTEND_IMAGE $KRA_REPLICAS $KRA_FRONTEND_NODEPORT $KRA_BACKEND_NODEPORT $KRA_MYSQL_IMAGE $KRA_REDIS_IMAGE $KRA_SHARED_STORAGE_CLASS $BIN_NAME'
}
stages {
stage('Select environment') {
steps {
script {
// 分支来源优先级:
// 1) env.gitlabBranch —— GitLab Jenkins 集成/webhook 触发时由 gitlab-plugin 注入push payload 的 ref
// 2) env.BRANCH_NAME —— 多分支流水线场景
// 3) JOB_NAME —— 手动触发兜底:名字以 -dev 结尾按 dev 部署,否则默认 main
def branch = env.gitlabBranch ?: env.BRANCH_NAME
if (!branch) {
branch = env.JOB_NAME.endsWith('-dev') ? 'dev' : 'main'
}
branch = branch.trim().replaceFirst(/^refs\/heads\//, '')
// The backend NodePort service has the longest generated name:
// <APP_NAME>-backend-nodeport, which must stay within 63 characters.
if (!(env.APP_NAME ==~ /^[a-z0-9](?:[a-z0-9-]{0,44}[a-z0-9])?$/)) {
error("APP_NAME must be a lowercase Kubernetes name with at most 46 characters: ${env.APP_NAME}")
}
echo "gitlabBranch='${env.gitlabBranch ?: ''}' BRANCH_NAME='${env.BRANCH_NAME ?: ''}' -> resolved branch='${branch}'"
env.KRA_BRANCH = branch
if (branch == 'dev') {
env.KRA_NAMESPACE = "${env.APP_NAME}-dev"
env.KRA_ENV = 'development'
env.KUBECONFIG_CREDENTIALS = env.CRED_KUBECONFIG_DEV
} else if (branch == 'main') {
env.KRA_NAMESPACE = "${env.APP_NAME}-prod"
env.KRA_ENV = 'production'
env.KUBECONFIG_CREDENTIALS = env.CRED_KUBECONFIG_PROD
} else {
error("Unsupported branch: ${branch}. Only dev and main deploy.")
}
}
}
}
stage('Checkout triggered commit') {
steps {
// job 的 SCM branch spec 固定(如 */main集成触发时按实际推送分支重新检出
// 保证单 job 同时服务 dev/main 两个环境。
script {
env.KRA_TRIGGER_COMMIT = env.gitlabAfter?.trim()
if (env.KRA_TRIGGER_COMMIT && !(env.KRA_TRIGGER_COMMIT ==~ /[0-9a-fA-F]{40}/)) {
error("Invalid GitLab commit SHA: ${env.KRA_TRIGGER_COMMIT}")
}
if (env.KRA_TRIGGER_COMMIT && env.KRA_TRIGGER_COMMIT.replace('0', '').isEmpty()) {
error('GitLab event points to a deleted branch; refusing to deploy')
}
checkout([$class: 'GitSCM',
branches: [[name: "*/${env.KRA_BRANCH}"]],
userRemoteConfigs: [[credentialsId: 'gitlab-root-http', url: 'http://192.168.200.130/root/kra.git']]])
if (env.KRA_TRIGGER_COMMIT) {
sh "git fetch --no-tags origin ${env.KRA_TRIGGER_COMMIT}"
sh "git checkout --detach ${env.KRA_TRIGGER_COMMIT}"
}
env.KRA_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
if (env.KRA_TRIGGER_COMMIT && env.KRA_COMMIT != env.KRA_TRIGGER_COMMIT) {
error("Checked out ${env.KRA_COMMIT}, expected GitLab commit ${env.KRA_TRIGGER_COMMIT}")
}
echo "building commit ${env.KRA_COMMIT}"
}
}
}
stage('Test') {
steps {
sh '''
set -eu
HOST_WORKSPACE=$(printf '%s' "$WORKSPACE" | sed 's#/var/jenkins_home#/opt/jenkins#')
docker run --rm -e GOPROXY=https://goproxy.cn,direct \\
-v "$HOST_WORKSPACE:/src" \\
-v "$CACHE_DIR/go-mod:/go/pkg/mod" \\
-v "$CACHE_DIR/go-build:/root/.cache/go-build" \\
-w /src "$GO_IMAGE" sh -c "git config --global --add safe.directory /src && go test ./... && make build"
'''
sh '''
set -eu
HOST_WORKSPACE=$(printf '%s' "$WORKSPACE" | sed 's#/var/jenkins_home#/opt/jenkins#')
docker run --rm \\
-e COREPACK_NPM_REGISTRY=https://registry.npmmirror.com \\
-v "$HOST_WORKSPACE/web:/app" \\
-v "$CACHE_DIR/pnpm-store:/root/.local/share/pnpm/store" \\
-v "$CACHE_DIR/corepack:/root/.cache/node/corepack" \\
-w /app "$NODE_IMAGE" bash -lc "corepack enable && corepack prepare pnpm@$PNPM_VERSION --activate && pnpm install --frozen-lockfile && pnpm build"
# 前端 Nginx 配置模板渲染proxy_pass 指向 ${APP_NAME}-backend 服务)。
# KRA_APP 必须在此显式导出envsubst 会把未设置的变量替换为空串。
export KRA_APP="$APP_NAME"
envsubst '$KRA_APP' < web/.docker-compose/nginx/conf.d/my.conf > web/nginx.k8s.conf
'''
}
}
stage('Build and push images') {
steps {
script {
env.IMAGE_TAG = "${env.KRA_BRANCH}-${env.KRA_COMMIT.take(12)}"
}
withCredentials([usernamePassword(credentialsId: env.CRED_HARBOR_PUSH, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD')]) {
sh '''
set -eu
umask 077
DOCKER_CONFIG_DIR=$(mktemp -d)
trap 'rm -rf "$DOCKER_CONFIG_DIR"' EXIT
echo "$HARBOR_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login "$REGISTRY" --username "$HARBOR_USER" --password-stdin >/dev/null
docker --config "$DOCKER_CONFIG_DIR" build --build-arg BIN_NAME="$BIN_NAME" -t "$REGISTRY/$IMAGE_REPO-backend:$IMAGE_TAG" .
docker --config "$DOCKER_CONFIG_DIR" build -t "$REGISTRY/$IMAGE_REPO-frontend:$IMAGE_TAG" web
docker --config "$DOCKER_CONFIG_DIR" push "$REGISTRY/$IMAGE_REPO-backend:$IMAGE_TAG"
docker --config "$DOCKER_CONFIG_DIR" push "$REGISTRY/$IMAGE_REPO-frontend:$IMAGE_TAG"
docker --config "$DOCKER_CONFIG_DIR" logout "$REGISTRY" >/dev/null 2>&1 || true
'''
}
}
}
stage('Deploy to Rancher cluster') {
steps {
withCredentials([file(credentialsId: "${env.KUBECONFIG_CREDENTIALS}", variable: 'KUBECONFIG_FILE'),
string(credentialsId: env.CRED_MYSQL_PASSWORD, variable: 'KRA_MYSQL_ROOT_PASSWORD'),
usernamePassword(credentialsId: env.CRED_HARBOR_PUSH, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD')]) {
sh '''
set -eu
export KUBECONFIG="$KUBECONFIG_FILE"
umask 077
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
kubectl get storageclass "$KRA_SHARED_STORAGE_CLASS" >/dev/null
kubectl create namespace "$KRA_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
DOCKER_CONFIG_DIR="$TMP_DIR/docker"
mkdir -p "$DOCKER_CONFIG_DIR"
printf '%s' "$HARBOR_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login "$REGISTRY" --username "$HARBOR_USER" --password-stdin >/dev/null
kubectl -n "$KRA_NAMESPACE" create secret generic harbor-registry \\
--type=kubernetes.io/dockerconfigjson \\
--from-file=.dockerconfigjson="$DOCKER_CONFIG_DIR/config.json" \\
--dry-run=client -o yaml | kubectl apply -f -
docker --config "$DOCKER_CONFIG_DIR" logout "$REGISTRY" >/dev/null 2>&1 || true
printf '%s' "$KRA_MYSQL_ROOT_PASSWORD" > "$TMP_DIR/mysql-root-password"
kubectl -n "$KRA_NAMESPACE" create secret generic "${APP_NAME}-runtime" \\
--from-file=mysql-root-password="$TMP_DIR/mysql-root-password" \\
--dry-run=client -o yaml | kubectl apply -f -
CONFIG_TEMPLATE="$TMP_DIR/config.yaml"
envsubst '$APP_NAME $KRA_ENV' < deploy/k8s/config.example.yaml > "$CONFIG_TEMPLATE"
kubectl -n "$KRA_NAMESPACE" create configmap "${APP_NAME}-config-template" \\
--from-file=config.yaml="$CONFIG_TEMPLATE" \\
--dry-run=client -o yaml | kubectl apply -f -
export KRA_APP="$APP_NAME"
export KRA_BACKEND_IMAGE="$REGISTRY/$IMAGE_REPO-backend:$IMAGE_TAG"
export KRA_FRONTEND_IMAGE="$REGISTRY/$IMAGE_REPO-frontend:$IMAGE_TAG"
RENDERED_MANIFEST="$TMP_DIR/manifest.yaml"
envsubst "$K8S_RENDER_VARS" < deploy/k8s/kra.yaml > "$RENDERED_MANIFEST"
kubectl apply --dry-run=server -f "$RENDERED_MANIFEST"
kubectl apply -f "$RENDERED_MANIFEST"
RUNTIME_SECRET_VERSION=$(kubectl -n "$KRA_NAMESPACE" get secret "${APP_NAME}-runtime" -o jsonpath='{.metadata.resourceVersion}')
kubectl -n "$KRA_NAMESPACE" patch statefulset "${APP_NAME}-mysql" --type=merge \\
-p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kra.dev/runtime-secret-version\":\"$RUNTIME_SECRET_VERSION\"}}}}}"
kubectl -n "$KRA_NAMESPACE" wait --for=jsonpath='{.status.phase}'=Bound \\
"pvc/${APP_NAME}-config" "pvc/${APP_NAME}-uploads" --timeout=5m
kubectl -n "$KRA_NAMESPACE" rollout status statefulset/${APP_NAME}-mysql --timeout=10m
kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-redis --timeout=10m
kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-backend --timeout=10m
kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-frontend --timeout=10m
'''
}
}
}
}
}