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 的产物名
    // 当前单节点部署固定使用单副本，避免首次初始化和 hostPath 数据共享产生竞争。
    KRA_REPLICAS          = '1'
    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'
    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'
    CRED_JWT_SIGNING_KEY   = 'kra-jwt-signing-key'

    // 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 $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 {
          // gitlabAfter 为 push 事件的目标 commit；手动触发时为空。
          // 注意：不能把 null 赋给 env.*（会被转成字符串 "null"），这里用局部变量。
          def triggerCommit = env.gitlabAfter?.trim()
          if (triggerCommit && !(triggerCommit ==~ /[0-9a-fA-F]{40}/)) {
            error("Invalid GitLab commit SHA: ${triggerCommit}")
          }
          if (triggerCommit && triggerCommit.replace('0', '').isEmpty()) {
            error('GitLab event points to a deleted branch; refusing to deploy')
          }
          if (triggerCommit) {
            env.KRA_TRIGGER_COMMIT = triggerCommit
          }
          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'),
          string(credentialsId: env.CRED_JWT_SIGNING_KEY, variable: 'KRA_JWT_SIGNING_KEY'),
          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 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
            test -n "$KRA_MYSQL_ROOT_PASSWORD" || { echo 'MySQL root password credential is empty' >&2; exit 1; }
            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 -
            test -n "$KRA_JWT_SIGNING_KEY" || { echo 'JWT signing key credential is empty' >&2; exit 1; }
            if [ "$(printf '%s' "$KRA_JWT_SIGNING_KEY" | wc -l)" -ne 0 ]; then
              echo 'JWT signing key credential must be a single line' >&2
              exit 1
            fi
            case "$KRA_JWT_SIGNING_KEY" in
              *"'"*) echo "JWT signing key credential must not contain a single quote" >&2; exit 1 ;;
            esac
            CONFIG_TEMPLATE="$TMP_DIR/config.yaml"
            envsubst '$APP_NAME $KRA_ENV $KRA_JWT_SIGNING_KEY' < deploy/k8s/config.example.yaml > "$CONFIG_TEMPLATE"
            kubectl -n "$KRA_NAMESPACE" create secret generic "${APP_NAME}-config-template" \\
              --from-file=config.yaml="$CONFIG_TEMPLATE" \\
              --dry-run=client -o yaml | kubectl apply -f -
            kubectl -n "$KRA_NAMESPACE" delete configmap "${APP_NAME}-config-template" --ignore-not-found
            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"
            MYSQL_SERVICE_IP=$(kubectl -n "$KRA_NAMESPACE" get service "${APP_NAME}-mysql" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
            if [ -n "$MYSQL_SERVICE_IP" ] && [ "$MYSQL_SERVICE_IP" != 'None' ]; then
              echo "${APP_NAME}-mysql already exists as a ClusterIP Service; delete it during a maintenance window before switching to headless mode" >&2
              exit 1
            fi
            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}')
            PATCH_FILE="$TMP_DIR/mysql-patch.json"
            printf '{"spec":{"template":{"metadata":{"annotations":{"kra.dev/runtime-secret-version":"%s"}}}}}\n' \\
              "$RUNTIME_SECRET_VERSION" > "$PATCH_FILE"
            kubectl -n "$KRA_NAMESPACE" patch statefulset "${APP_NAME}-mysql" --type=merge \\
              --patch-file="$PATCH_FILE"
            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
          '''
        }
      }
    }
  }
}
