3
0
hr~ vor 1 Monat
Ursprung
Commit
d8e3f18d28

+ 30 - 0
ahx-oidc/DEV/Jenkinsfile

@@ -0,0 +1,30 @@
+properties([
+    parameters([
+        // New preferred params:
+        string(name: 'baseBranch', defaultValue: 'dev', description: '分支名(如 master, dev)'),
+        choice(name: 'namespace', choices: ['default', 'test'], description: 'K8S namespace(如 default)'),
+        choice(name: 'action', choices: ['upgrade', 'upgrade:selected'], description: '选择操作类型')
+        // Backward compatible legacy param:
+        // string(name: 'actionParameter', defaultValue: 'refs/heads/dev', description: '参数路径(如 refs/heads/dev)')
+    ])
+])
+
+node {
+    checkout scm
+    def deployAction = params.action ?: 'upgrade'
+    def deployBaseBranch = params.baseBranch ?: 'dev'
+    def deployNamespace = params.namespace ?: 'default'
+    def yaml2Map = load "./sharedLibs/yaml2Map.groovy"
+    def stages = load "./sharedLibs/stages.k8s.groovy"
+    def pipeline = load "./sharedLibs/pipelines.k8s.java.groovy"
+    def GLOBAL_CONFIG = yaml2Map.read('global.yaml').project
+
+    def WORKING_SPACE = 'ahx-oidc/DEV'
+    dir(WORKING_SPACE) {
+        def PROJECT_CONFIG = yaml2Map.read('cfg.yaml').project
+        def CONFIG = yaml2Map.merge(PROJECT_CONFIG, GLOBAL_CONFIG)
+        CONFIG.pipeline = PROJECT_CONFIG.pipeline
+        println 'configs: ' + CONFIG
+        pipeline.run(CONFIG, deployAction, stages, deployBaseBranch, deployNamespace)
+    }
+}

+ 21 - 0
ahx-oidc/DEV/cfg.yaml

@@ -0,0 +1,21 @@
+project:
+  git:
+    address: http://git.hobbystocks.cn/mangoo/Oidc.git
+  service:
+    name: ahx-oidc
+    module: ''
+    version: ''
+    jar: OidcService-1.0-SNAPSHOT.jar
+  pipeline:
+    jdkTool: openJDK17
+    baseImage: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openjdk:17-jdk-alpine
+    includeBuildNumber: true
+    args:
+      - -Djava.security.egd=file:/dev/./urandom
+      - -Xmx256m
+      - -Dserver.port=80
+      - --spring.profiles.active=dev
+    deployMode: configmapEnvOnly
+    k8sBackups: []
+  k3s:
+    kubeconfig: k3s/k3s-eastern.yaml

+ 33 - 0
ahx-oidc/DEV/configmap-env.ini

@@ -0,0 +1,33 @@
+CONSOLE_ENABLED=true
+
+# FLUENTD
+FLUENTD_ENABLED=true
+FLUENTD_HOST=fluentd-aggr
+FLUENTD_PORT=24224
+
+# PG
+POSTGRES_HOST=192.168.50.8
+POSTGRES_PORT=5432
+POSTGRES_USERNAME=postgres
+POSTGRES_PASSWORD=123456
+POSTGRES_DATABASE=ahx_app
+
+
+# Redis
+REDIS_HOST=192.168.50.8
+REDIS_PORT=6379
+REDIS_PASSWORD=Pass2010
+REDIS_DATABASE=15
+
+ADMIN_BASEURI=http://b2b-user-service/
+ENDUSER_BASEURI=http://b2b-user-service/
+MERCHANTUSER_BASEURI=http://b2b-user-service/
+LDAPUSER_BASEURI=http://b2b-user-service/
+
+
+HS_RPC_WECHAT_DUMMY=false
+HS_RPC_WECHAT_APPID=wx3a5928a461ce03f5
+HS_RPC_WECHAT_SECRET=7cf12ce9c612de6836ddd8a6a9bafeaf
+HS_RPC_TOTP_DUMMY=true
+
+TZ=Asia/Shanghai

+ 30 - 0
ahx-oidc/DEV/deployment.yaml

@@ -0,0 +1,30 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: <service_name><service_version>
+spec:
+  selector:
+    matchLabels:
+      app: <service_name><service_version>
+  template:
+    metadata:
+      labels:
+        app: <service_name><service_version>
+      annotations:
+        commit-sha: "<COMMIT_SHA>"
+        prometheus.io/path: /actuator/prometheus
+        prometheus.io/scrape: "true"
+    spec:
+      imagePullSecrets:
+        - name: <imagePullSecret>
+      containers:
+        - name: <service_name>
+          image: <docker_image>
+          command: ["java"]
+          args: <java_args>
+          ports:
+            - name: http
+              containerPort: 80
+          envFrom:
+            - configMapRef:
+                name: <configmap_env_name>

+ 11 - 0
ahx-oidc/DEV/service.yaml

@@ -0,0 +1,11 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: <service_name><service_version>
+spec:
+  selector:
+    app: <service_name><service_version>
+  ports:
+    - port: 80
+      targetPort: 80

+ 2 - 0
global.yaml

@@ -1,4 +1,6 @@
 project:
+  git:
+    credentialId: gogs.hobbystocks.cn
   docker:
     #registry: registry.azure.hobbystocks.cn
     #push_credentialId: docker.registry

+ 58 - 0
k3s-INFRA/DEV/traefik-rules/ahx-oidc/IngressRoute.yaml

@@ -0,0 +1,58 @@
+apiVersion: traefik.io/v1alpha1
+kind: IngressRoute
+metadata:
+  name: ahx-oidc.https
+spec:
+  entryPoints:
+    - websecure
+  routes:
+    - kind: Rule
+      match: >-
+        Host(`dev.ahxpm.com`) && PathPrefix(`/ahx-oidc`)
+      middlewares:
+        - name: ahx-oidc-strip
+        - name: ahx-oidc.cors-header
+      priority: 200
+      services:
+        - name: ahx-oidc
+          port: 80
+    - kind: Rule
+      match: Host(`dev.ahxpm.com`) && Path(`/ahx-oidc/api/external/authn/token/a2`)
+      services:
+        - name: ahx-oidc
+          port: 80
+    - kind: Rule
+      match: Host(`dev.ahxpm.com`) && Path(`/ahx-oidc/api/external/ldap/password`)
+      middlewares:
+        - name: ahx-oidc.authn-app
+        - name: ahx-oidc-strip
+        - name: ahx-oidc.cors-header
+      services:
+        - name: ahx-oidc
+          port: 80
+    - kind: Rule
+      match: Host(`dev.ahxpm.com`) && Path(`/ahx-oidc/oauth2/token`)
+      middlewares:
+        - name: ahx-oidc-strip
+        - name: ahx-oidc.cors-header
+      services:
+        - name: ahx-oidc
+          port: 80
+    - kind: Rule
+      match: Host(`dev.ahxpm.com`) && Path(`/ahx-oidc/api/internal/authz/permission`)
+      middlewares:
+        - name: ahx-oidc-strip
+        - name: ahx-oidc.cors-header
+      services:
+        - name: ahx-oidc
+          port: 80
+    - kind: Rule
+      match: Host(`dev.ahxpm.com`) && Path(`/ahx-oidc/api/totp/permission/send`)
+      middlewares:
+        - name: ahx-oidc-strip
+        - name: ahx-oidc.cors-header
+      services:
+        - name: ahx-oidc
+          port: 80
+  tls:
+    secretName: dev.ahxpm.com

+ 65 - 0
k3s-INFRA/DEV/traefik-rules/ahx-oidc/Middleware.yaml

@@ -0,0 +1,65 @@
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: ahx-oidc-strip
+spec:
+  stripPrefix:
+    prefixes:
+      - /ahx-oidc
+---
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: ahx-oidc.cors-header
+spec:
+  headers:
+    accessControlAllowHeaders:
+      - '*'
+    accessControlAllowMethods:
+      - '*'
+    accessControlMaxAge: 100
+    addVaryHeader: true
+---
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: ahx-oidc.authn-app
+spec:
+  forwardAuth:
+    address: http://ahx-oidc/api/internal/authn/token/app
+    authResponseHeaders:
+      - X-USER-BASE64
+    trustForwardHeader: true
+---
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: ahx-oidc.authn-optional-app
+spec:
+  forwardAuth:
+    address: http://ahx-oidc/api/internal/authn/token/optional/app
+    authResponseHeaders:
+      - X-USER-BASE64
+    trustForwardHeader: true
+---
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: ahx-oidc.authn-partner
+spec:
+  forwardAuth:
+    address: http://ahx-oidc/api/internal/authn/token/partner
+    authResponseHeaders:
+      - X-USER-BASE64
+    trustForwardHeader: true
+---
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: ahx-oidc.authn-optional-partner
+spec:
+  forwardAuth:
+    address: http://ahx-oidc/api/internal/authn/token/optional/partner
+    authResponseHeaders:
+      - X-USER-BASE64
+    trustForwardHeader: true

+ 0 - 5
sharedLibs/imageBuilder.groovy

@@ -1,9 +1,7 @@
-// 获取镜像URL
 generateFullImageUri = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
     return "${DOCKER.registry}/${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
 }
 
-// 获取镜像名
 generateImageName = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
     return "${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
 }
@@ -20,12 +18,10 @@ EOF
     return pathOfDockerfile
 }
 
-// 项目自带Dockerfile的时候,只需返回项目路径
 generateNodeJsDockerFile = { ... args ->
     return 'projdir'
 }
 
-// Maven工程生成.jar
 def buildJar(String path='projdir') {
     
     echo "-----> Building java package ..."
@@ -43,7 +39,6 @@ def cleanJar(String path='projdir') {
     return path
 }
 
-// 构建Docker镜像
 def buildDockerImage(Object SERVICE, Object DOCKER, String base_branch, Closure dockerfile=null, String filter2Remove=null) {
 
     def imageUri = generateFullImageUri(DOCKER, base_branch, BUILD_NUMBER)

+ 81 - 20
sharedLibs/k3sUtil.groovy

@@ -1,8 +1,4 @@
-// 配置configMap Environment
-//   - SERVICE: service配置
-//   - K3S: k3s配置
-//   - configMapPath: 指定configMapPath,默认使用"projdir/configmap-env"
-def applyConfigMapEnv(Object SERVICE, Object K3S, String configMapPath='projdir/configmap-env.ini', String defaultNS='ahxpm') {
+def applyConfigMapEnv(Object SERVICE, Object K3S, String configMapPath='projdir/configmap-env.ini', String defaultNS='default') {
 
   println "applying k3s configmap environment from ${configMapPath}"
   configmap_env_name = "${SERVICE.name}${SERVICE.version}-env"
@@ -12,11 +8,21 @@ def applyConfigMapEnv(Object SERVICE, Object K3S, String configMapPath='projdir/
   return configmap_env_name
 }
 
-// 配置configMap
-//   - SERVICE: service配置
-//   - K3S: k3s配置
-//   - configMapPath: 指定configMapPath,默认使用./configmap/
-def applyConfigMapConfig(Object SERVICE, Object K3S, String configMapPath = './configmap/', String defaultNS='ahxpm') {
+def applyCanaryConfigMapEnv(Object SERVICE, Object K3S, String version, 
+    String configMapPath='projdir/configmap-env.ini', String defaultNS='default') {
+
+  println "applying k3s configmap environment from ${configMapPath}"
+  def configmap_env_name = "${SERVICE.name}${SERVICE.version}-${version}-env"
+  def configmap_env = "${SERVICE.name}${SERVICE.version}-${version}-env.yaml"
+  println "create configmap ${configmap_env_name} > ${configmap_env}"
+  sh "/k3s/kubectl create configmap ${configmap_env_name} --dry-run=client --from-env-file=${configMapPath} -o yaml > ${configmap_env}"
+  sh "/k3s/kubectl --kubeconfig ${env.WORKSPACE}/${K3S.kubeconfig} apply -f ${configmap_env} -n ${defaultNS}"
+
+  print "return configmap_env_name: ${configmap_env_name}"
+  return configmap_env_name
+}
+
+def applyConfigMapConfig(Object SERVICE, Object K3S, String configMapPath = './configmap/', String defaultNS='default') {
 
   println "applying k3s configmap config from ${configMapPath}"
   configmap_name = "${SERVICE.name}${SERVICE.version}-config"
@@ -26,9 +32,17 @@ def applyConfigMapConfig(Object SERVICE, Object K3S, String configMapPath = './c
   return configmap_name
 }
 
-// 生成deployment.yaml
-//   - template: deployment(template).yaml模板文件路径
-//   - kvs: key/value替换deployment(template)
+def applyCanaryConfigMapConfig(Object SERVICE, Object K3S, String version, 
+    String configMapPath = './configmap/', String defaultNS='default') {
+
+  println "applying k3s configmap config from ${configMapPath}"
+  configmap_name = "${SERVICE.name}${SERVICE.version}-${version}-config"
+  configmap_file = "${SERVICE.name}-${version}-configmap.yaml"
+  sh "/k3s/kubectl create configmap ${configmap_name} --dry-run=client --from-file=${configMapPath} -o yaml > ${configmap_file}"
+  sh "/k3s/kubectl --kubeconfig ${env.WORKSPACE}/${K3S.kubeconfig} apply -f ${configmap_file} -n ${defaultNS}"
+  return configmap_name
+}
+
 def generateDeployment(Map kvs, String template="deployment.yaml") {
   
   if (!fileExists(template)) {
@@ -39,7 +53,6 @@ def generateDeployment(Map kvs, String template="deployment.yaml") {
   println "generating k8s deployment... replace Key/Value in file: ${template}."
   kvs.each {
     def key, val -> 
-      print(val)
       value = val
         .replace('/', '\\/')
         .replace('"', '\\"')
@@ -48,9 +61,33 @@ def generateDeployment(Map kvs, String template="deployment.yaml") {
   }
 }
 
-// 生成service.yaml
-//   - template: service(template).yaml模板文件路径
-//   - kvs: key/value替换service(template).yaml
+def generateCanaryDeployment(Map kvs, String version, String template="deployment.yaml") {
+  /**
+  * 从$template生成deployment-${version}.yaml
+  */
+  
+  if (!fileExists(template)) {
+    println "skip generating deployment.yaml, file ${template} not exists" 
+    return
+  }
+
+  def outputFile = "deployment-${version}.yaml"
+  println "generating k8s deployment(${version}) from ${template} -> ${outputFile}"
+
+  sh "cp ${template} ${outputFile}"
+  println "generating k8s deployment(${version})... replace Key/Value in file: ${template}."
+  kvs.each {
+    def key, val -> 
+      value = val
+        .replace('/', '\\/')
+        .replace('"', '\\"')
+      
+      sh "sed -i \"s/${key}/${value}/g\" ${outputFile}"
+  }
+
+  return outputFile
+}
+
 def generateService(Map kvs, String template="service.yaml") {
 
   if (!fileExists(template)) {
@@ -70,7 +107,32 @@ def generateService(Map kvs, String template="service.yaml") {
   }
 }
 
-// 配置deployment.yaml
+def generateCanaryService(Map kvs, String version, String template="service.yaml") {
+
+  /**
+  * 从$template生成service-${version}.yaml
+  */
+
+  if (!fileExists(template)) {
+    println "skip generating service.yaml, file ${template} not exists" 
+    return
+  }
+
+  def outputFile = "service-${version}.yaml"
+  sh "cp ${template} ${outputFile}"
+  println "generating k8s service-${version}.yaml... replace Key/Value in file: ${template}."
+  kvs.each {
+    def key, val -> 
+      print(val)
+      value = val
+        .replace('/', '\\/')
+        .replace('"', '\\"')
+      
+      sh "sed -i \"s/${key}/${value}/g\" ${outputFile}"
+  }
+  return outputFile
+}
+
 def applyDeployment(Object K3S, String deployment="deployment.yaml") {
 
   if (!fileExists(deployment)) {
@@ -82,7 +144,6 @@ def applyDeployment(Object K3S, String deployment="deployment.yaml") {
   sh "/k3s/kubectl --kubeconfig ${env.WORKSPACE}/${K3S.kubeconfig} apply -f ${deployment}"
 }
 
-// 配置service.yaml
 def applyService(K3S, String service="service.yaml") {
 
   if (!fileExists(service)) {
@@ -91,7 +152,7 @@ def applyService(K3S, String service="service.yaml") {
   }
 
   println "applying k8s deployment file: ${service} ..."
-  sh "/k3s/kubectl --kubeconfig ${env.WORKSPACE}/${K3S.kubeconfig} apply -f ${service} -n ahxpm"
+  sh "/k3s/kubectl --kubeconfig ${env.WORKSPACE}/${K3S.kubeconfig} apply -f ${service}"
 }
 
 return this

+ 248 - 0
sharedLibs/pipelines.k8s.java.groovy

@@ -0,0 +1,248 @@
+/**
+ * Unified Java + Docker + K3s pipeline runner for this repo.
+ *
+ * Goal: keep service Jenkinsfiles thin by moving per-service differences into cfg.yaml:
+ *   project.pipeline: { jdkTool, baseImage, enableSkywalking, deployMode, k8sBackups, ... }
+ *
+ * This file is designed to be backward compatible: if `CONFIG.pipeline` is missing,
+ * defaults match the most common existing Jenkinsfile patterns in this repo.
+ */
+
+def _get(Map m, String k, def dflt=null) {
+    return (m != null && m.containsKey(k) && m[k] != null) ? m[k] : dflt
+}
+
+def _asStringArray(def v) {
+    if (v == null) return [] as String[]
+    if (v instanceof String[]) return v
+    if (v instanceof List) return (v.collect { it.toString() }) as String[]
+    return [v.toString()] as String[]
+}
+
+def _asList(def v) {
+    if (v == null) return []
+    if (v instanceof List) return v
+    return [v]
+}
+
+def _normalizeModulePath(def module) {
+    def moduleText = module == null ? '' : module.toString().trim()
+    if (!moduleText) return ''
+    return moduleText.endsWith('/') ? moduleText : moduleText + '/'
+}
+
+def _pipelineArgs(def pipeline) {
+    return pipeline instanceof Map ? _get(pipeline, "args", null) : null
+}
+
+def _skywalkingArgs(Map CONFIG) {
+    def p = CONFIG.pipeline ?: [:]
+    if (_get(p, "addSkywalkingArgs", false) != true) return []
+
+    return [
+            "-javaagent:/app/skywalking-agent/skywalking-agent.jar",
+            "-Dskywalking.agent.service_name=${CONFIG.service.name}",
+            "-Dskywalking.collector.backend_service=${CONFIG.skywalking.address}"
+    ]
+}
+
+def _splitJavaArgs(String[] rawArgs) {
+    def jvmArgs = []
+    def appArgs = []
+
+    rawArgs.each { arg ->
+        def text = arg == null ? '' : arg.toString().trim()
+        if (!text) return
+
+        if (text.startsWith('--')) {
+            appArgs << text
+            return
+        }
+
+        jvmArgs << text
+    }
+
+    return [jvmArgs, appArgs]
+}
+
+def _buildJavaArgs(Map CONFIG) {
+    def p = CONFIG.pipeline ?: [:]
+    def args = _pipelineArgs(p)
+    def buildNumberArg = _get(p, "includeBuildNumber", false) ? ["-DBuild.number=${BUILD_NUMBER}"] : []
+
+    if (args instanceof List || args instanceof String[]) {
+        def directArgs = _asStringArray(args)
+        if (directArgs.length > 0) {
+            def (jvmArgs, appArgs) = _splitJavaArgs(directArgs)
+            return (jvmArgs + _skywalkingArgs(CONFIG) + buildNumberArg + ["-jar", "/app/target/${CONFIG.service.jar}"] + appArgs) as String[]
+        }
+    }
+
+    if (args instanceof String && args.toString().trim()) {
+        def (jvmArgs, appArgs) = _splitJavaArgs([args.toString().trim()] as String[])
+        return (jvmArgs + _skywalkingArgs(CONFIG) + buildNumberArg + ["-jar", "/app/target/${CONFIG.service.jar}"] + appArgs) as String[]
+    }
+
+    if (args instanceof Map) {
+        def explicitArgs = _asStringArray(_get(args, "javaArgs", null))
+        if (explicitArgs.length > 0) {
+            def (jvmArgs, appArgs) = _splitJavaArgs(explicitArgs)
+            return (jvmArgs + _skywalkingArgs(CONFIG) + buildNumberArg + ["-jar", "/app/target/${CONFIG.service.jar}"] + appArgs) as String[]
+        }
+    }
+
+    def explicitArgs = _asStringArray(_get(p, "javaArgs", null))
+    if (explicitArgs.length > 0) return explicitArgs
+
+    def jvmArgs = ["-Djava.security.egd=file:/dev/./urandom"]
+    jvmArgs.addAll(_skywalkingArgs(CONFIG))
+    jvmArgs.addAll(buildNumberArg)
+    def xmx = args instanceof Map ? _get(args, "xmx", _get(p, "xmx", null)) : _get(p, "xmx", null)
+    if (xmx != null && xmx.toString().trim()) {
+        jvmArgs << "-Xmx${xmx.toString().trim()}"
+    }
+
+    def serverPort = args instanceof Map ? _get(args, "serverPort", _get(p, "serverPort", null)) : _get(p, "serverPort", null)
+    if (serverPort != null && serverPort.toString().trim()) {
+        jvmArgs << "-Dserver.port=${serverPort.toString().trim()}"
+    }
+
+    def appArgs = []
+    def springProfile = args instanceof Map ? _get(args, "springProfile", _get(p, "springProfile", null)) : _get(p, "springProfile", null)
+    if (springProfile != null && springProfile.toString().trim()) {
+        appArgs << "--spring.profiles.active=${springProfile.toString().trim()}"
+    }
+
+    return (jvmArgs + ["-jar", "/app/target/${CONFIG.service.jar}"] + appArgs) as String[]
+}
+
+def _generateDockerfile(Map CONFIG, String pathOfDockerfile='projdir') {
+    def p = CONFIG.pipeline ?: [:]
+    def module = _normalizeModulePath(CONFIG.service.module)
+
+    def baseImage = _get(p, "baseImage", "swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openjdk:17-jdk-alpine")
+    def addSkywalking = _get(p, "enableSkywalking", false)
+
+    def jar = CONFIG.service.jar
+    def skywalkingAddLine = addSkywalking ? "ADD skywalking-agent /app/skywalking-agent" : ""
+
+    sh """cat > ${pathOfDockerfile}/Dockerfile<<EOF
+FROM ${baseImage}
+${skywalkingAddLine}
+ADD ${module}target/${jar} /app/target/${jar}
+EOF
+"""
+
+    return pathOfDockerfile
+}
+
+def _maybeCopySkywalking(Map CONFIG) {
+    def p = CONFIG.pipeline ?: [:]
+    if (!_get(p, "enableSkywalking", false)) return
+    def srcRel = _get(p, "skywalkingAgentSrc", "../../skywalking-agent-9.0.0")
+    sh "cp -r ${srcRel} projdir/skywalking-agent"
+}
+
+def _maybeCleanupSkywalking(Map CONFIG) {
+    def p = CONFIG.pipeline ?: [:]
+    if (!_get(p, "enableSkywalking", false)) return
+    sh "rm -rf projdir/skywalking-agent"
+}
+
+def _deployAdditionalConfigmapEnvOnly(def stages, Map CONFIG, String baseBranch, String[] javaArgs, String configmapEnv, String namespace='default') {
+    def p = CONFIG.pipeline ?: [:]
+    def additionalDeployments = _asList(_get(p, "additionalConfigmapEnvOnlyDeployments", []))
+
+    additionalDeployments.each { deployment ->
+        if (!(deployment instanceof Map)) {
+            error("pipeline.additionalConfigmapEnvOnlyDeployments entries must be maps")
+        }
+
+        def deploymentTemplate = _get(deployment, "deploymentTemplate", "")
+        def serviceTemplate = _get(deployment, "serviceTemplate", "")
+        if (!deploymentTemplate || !serviceTemplate) {
+            error("additional configmap-env-only deployments require deploymentTemplate and serviceTemplate")
+        }
+
+        stages.deployWithConfigmapEnvOnlyTemplate(
+                CONFIG.service,
+                CONFIG.k3s,
+                baseBranch,
+                javaArgs,
+                configmapEnv,
+                deploymentTemplate,
+                serviceTemplate,
+                namespace
+        )
+    }
+}
+
+def run(Map CONFIG, String action, def stages, String baseBranch, String namespace='default', String rollbackBuildNumber='') {
+    // Ensure docker image name is formatted consistently with existing Jenkinsfiles.
+    // 装备docker镜像路径
+    CONFIG.docker.image = String.format(CONFIG.docker.image, CONFIG.service.name)
+
+    // 选择jdk环境
+    def p = CONFIG.pipeline ?: [:]
+    def jdkTool = _get(p, "jdkTool", null)
+    if (jdkTool != null && jdkTool.toString().trim()) {
+        jdk = tool name: jdkTool
+        env.JAVA_HOME = "${jdk}"
+    }
+
+    def base_branch = baseBranch
+    String[] JAVA_ARGS = _buildJavaArgs(CONFIG)
+
+    String[] K8S_BACKUPS = _asStringArray(_get(p, "k8sBackups", []))
+    def deployMode = _get(p, "deployMode", "configmapEnvOnly")
+
+    // Optional per-service file names; keep defaults matching existing code.
+    def configmapEnv = _get(p, "configmapEnvFile", "./configmap-env.ini")
+    def configmapConf = _get(p, "configmapConfFile", "./configmap")
+
+    if ("upgrade" == action || "upgrade:selected" == action) {
+        stages.upgrade(CONFIG, base_branch, K8S_BACKUPS, [
+                'POST_GITCLONE': { _maybeCopySkywalking(CONFIG) },
+                'GENERATEDOCKERFILE': { _generateDockerfile(CONFIG) },
+                'K3SDEPLOY': {
+                    if (deployMode == "configmapEnvOnly") {
+                        stages.deployWithConfigmapEnvOnly(CONFIG.service, CONFIG.k3s, base_branch, JAVA_ARGS, configmapEnv, namespace)
+                        _deployAdditionalConfigmapEnvOnly(stages, CONFIG, base_branch, JAVA_ARGS, configmapEnv, namespace)
+                        return
+                    }
+
+                    if (deployMode == "configmaps") {
+                        stages.deployWithConfigmaps(CONFIG.service, CONFIG.k3s, base_branch, JAVA_ARGS, configmapEnv, configmapConf, namespace)
+                        return
+                    }
+
+                    // Fallback to repo default (env-only).
+                    stages.deployWithConfigmapEnvOnly(CONFIG.service, CONFIG.k3s, base_branch, JAVA_ARGS, configmapEnv, namespace)
+                }
+        ])
+        _maybeCleanupSkywalking(CONFIG)
+        return
+    }
+
+    if ("rollback" == action) {
+        stages.rollback(CONFIG, rollbackBuildNumber, [
+                'K3SDEPLOY': {
+                    if (deployMode == "configmapEnvOnly") {
+                        stages.deployWithConfigmapEnvOnly_rollback(CONFIG.service, CONFIG.k3s, rollbackBuildNumber, JAVA_ARGS, configmapEnv, namespace)
+                        return
+                    }
+
+                    if (deployMode == "configmaps") {
+                        stages.deployWithConfigmaps_rollback(CONFIG.service, CONFIG.k3s, rollbackBuildNumber, JAVA_ARGS, configmapEnv, configmapConf, namespace)
+                        return
+                    }
+
+                    stages.deployWithConfigmapEnvOnly_rollback(CONFIG.service, CONFIG.k3s, rollbackBuildNumber, JAVA_ARGS, configmapEnv, namespace)
+                }
+        ])
+        return
+    }
+
+}
+
+return this

+ 0 - 7
sharedLibs/stages.docker.groovy

@@ -71,13 +71,6 @@ def dockerRun(Object CONFIG, String docker_image_uri) {
     sh "$EXEC"
 }
 
-// Jenkins pipeline stages
-// closures = [
-//    GITCLONE: { -> },
-//    GENERATEDOCKERFILE: { -> },
-//    BUILDIMAGE: { -> },
-//    DOCKERRUN: { -> } 
-//]
 def execute(CONFIG, base_branch, closures=[:]) {
 
     stage('source code check out') {

+ 0 - 1
sharedLibs/stages.docker.nodejs.groovy

@@ -64,7 +64,6 @@ def cleanDockerImage(String dockerImgName) {
 }
 
 
-// Jenkins pipeline stages
 def execute(CONFIG, base_branch, closures=[:]) {
 
     stage('source code check out') {

+ 0 - 1
sharedLibs/stages.groovy

@@ -73,7 +73,6 @@ def deployWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args,
     k3sUtils.applyDeployment(K3S)
 }
 
-// Jenkins pipeline stages
 def execute(CONFIG, base_branch, closures=[:]) {
 
     stage('source code check out') {

+ 753 - 0
sharedLibs/stages.k8s-prod.groovy

@@ -0,0 +1,753 @@
+sharedLibsPath = "${env.WORKSPACE}/sharedLibs"
+backup_destination = '/var/jenkins_home/backups'
+backup_targetFile = 'javaDockerImage.tar'
+BACKUP_MAX = 2
+
+generateFullImageUri = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
+    return "${DOCKER.registry}/${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
+}
+
+generateImageName = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
+    return "${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
+}
+
+def processString(String content) {
+    return content
+}
+
+def buildJar(Object SERVICE, String path='projdir') {
+    
+    echo "-----> Building project package ..."
+    dir(path) {
+        if (SERVICE.module != null && SERVICE.module.trim())
+            sh "$MAVEN_HOME/bin/mvn clean package -pl \"${SERVICE.module}\" -am -Dmaven.test.skip=true"
+        else
+            sh "$MAVEN_HOME/bin/mvn clean package -Dmaven.test.skip=true"
+    }
+    return path
+}
+
+@Deprecated
+def backup_jar(Object SERVICE, String srcPath, String workingPath='projdir') {
+
+    echo "-----> Backup project artifact ..."
+    def module = SERVICE.module == null ? '' : (SERVICE.module[-1] == '/' ? SERVICE.module : SERVICE.module + '/')
+    path = "${backup_destination}/${JOB_NAME}/${BUILD_NUMBER}/target"
+    sh """if [ -d ${path} ];
+    then
+        echo \"${path} already exists\"
+    else
+        mkdir -p ${path}
+    fi
+    cp ${workingPath}/${module}${srcPath} ${path}
+    """
+}
+
+def cleanDockerImage(String dockerImgName) {
+    sh "docker rmi ${dockerImgName}"
+}
+
+def backupDockerImage(String dockerImgName) {
+
+    echo "-----> Backup artifact(docker image : ${dockerImgName}) ..."
+    path = "${backup_destination}/${JOB_NAME}/${BUILD_NUMBER}"
+    sh """
+    docker save ${dockerImgName} -o ${backup_targetFile}
+    if [ -d ${path} ];
+    then
+        echo \"${path} already exists\"
+    else
+        mkdir -p ${path}
+        mv ${backup_targetFile} ${path}
+    fi
+    """
+}
+
+def backupK8sConfigs(String[] files2backup) {
+
+    path = "${backup_destination}/${JOB_NAME}/${BUILD_NUMBER}/k8s"
+    sh """if [ -d ${path} ];
+    then
+        echo \"${path} already exists!\"
+    else
+        mkdir -p ${path}
+    fi
+    """
+
+    for(file in files2backup) {
+        sh "cp -r ${file} ${path}"
+    }
+}
+
+def cleanJar(String path='projdir') {
+    echo "-----> Cleaning project package ..."
+    dir(path){
+        sh '$MAVEN_HOME/bin/mvn clean'
+    }
+    return path
+}
+
+def generateDockerfile(Object SERVICE, String pathOfDockerfile='projdir') {
+    
+    def module = SERVICE.module == null ? '' : (SERVICE.module[-1] == '/' ? SERVICE.module : SERVICE.module + '/')
+    echo "-----> Generating Dockerfile: ${pathOfDockerfile}/Dockerfile ..."
+    sh """cat > ${pathOfDockerfile}/Dockerfile<<EOF
+FROM openjdk:17-alpine as final
+ADD ${module}target/${SERVICE.jar} /app/target/${SERVICE.jar}
+EOF
+"""
+    return pathOfDockerfile
+}
+
+def buildDockerImage(Object SERVICE, Object DOCKER, String base_branch, projDir='projdir', String filter2Remove=null) {
+
+    def imageUri = generateFullImageUri(DOCKER, base_branch, BUILD_NUMBER)
+    echo "-----> Building docker image: ${imageUri} ..."
+    dir(projDir) {
+        docker.withRegistry("http://${DOCKER.registry}", "${DOCKER.push_credentialId}") {
+            def imageName = generateImageName(DOCKER, base_branch, BUILD_NUMBER)
+            def image = docker.build(imageName)
+            image.push()
+            sh "docker rmi ${imageName}"
+            if(filter2Remove != null)
+                sh "docker image prune --filter label=${filter2Remove} -f"
+        }
+    }
+    return imageUri
+}
+
+def restoreDockerImage(Object DOCKER, String workspace, String base_branch, String rollbackBuildNumber) {
+
+    dir(workspace) {
+
+        imageUri = generateFullImageUri(DOCKER, base_branch, rollbackBuildNumber)
+        sh "docker load -i ${backup_targetFile}"
+        docker.withRegistry("http://${DOCKER.registry}", "${DOCKER.push_credentialId}") {
+            def image = docker.image(imageUri)
+            image.push()
+        }
+    }
+    return imageUri
+}
+
+// 灰度发布: 部署Canary版本pod
+def deployCanaryWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args, namespace='default') {
+
+        print "loading ${sharedLibsPath}/k3sUtil.groovy ..."
+        def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+        def version = 'canary'
+
+        def configmap_env_name = k3sUtils.applyCanaryConfigMapEnv(SERVICE, K3S, version, "./configmap-env.ini", namespace)
+        print "get configmap_env_name: ${configmap_env_name}"
+        
+        def java_args = '['
+            args.each{item -> java_args += "\"$item\", "}
+        java_args = java_args[0..-2] + ']'
+
+        def kvs = [:]
+        kvs.put('<namespace>', namespace)
+        kvs.put('<service_name>', SERVICE.name)
+        kvs.put('<service_version>', SERVICE.version)
+        kvs.put('<gray_version>', version)
+        kvs.put('<release>', base_branch)
+        kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+        kvs.put('<imagePullSecret>', K3S.pull_secretId)
+        kvs.put('<docker_image>', imageUri)
+        kvs.put('<java_args>', java_args)
+        if (SERVICE.health != null)
+            kvs.put('<service_health>', SERVICE.health)
+        kvs.put('<configmap_env_name>', configmap_env_name)
+        def canaryDeploy = k3sUtils.generateCanaryDeployment(kvs, version, "./deployment.yaml")
+        k3sUtils.applyDeployment(K3S, canaryDeploy)
+
+        if (!fileExists('./service.yaml')) {
+            print "skip applying service. './service.yaml' file does not exists" 
+            return
+        }
+        def canaryService = k3sUtils.generateCanaryService(kvs, version, './service.yaml')
+        k3sUtils.applyService(K3S, canaryService)
+}
+
+def deployWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv, namespace)
+    def java_args = '['
+        args.each{item -> java_args += "\"$item\","}
+    java_args = java_args[0..-2] + ']'
+
+    kvs = [:]
+    kvs.put('<service_name>', SERVICE.name)
+    kvs.put('<service_version>', SERVICE.version)
+    kvs.put('<release>', base_branch)
+    kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+    kvs.put('<imagePullSecret>', K3S.pull_secretId)
+    kvs.put('<docker_image>', imageUri)
+    kvs.put('<java_args>', java_args)
+    if (SERVICE.health != null)
+        kvs.put('<service_health>', SERVICE.health)
+    kvs.put('<configmap_env_name>', configmap_env_name)
+    k3sUtils.generateDeployment(kvs, 'deployment.yaml')
+    k3sUtils.applyDeployment(K3S, 'deployment.yaml')
+
+    k3sUtils.generateService(kvs, 'service.yaml')
+    k3sUtils.applyService(K3S, 'service.yaml')
+}
+
+def deployWithConfigmapEnvOnly_rollback(SERVICE, K3S, String rollbackBuildNumber, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    dir("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/k8s") {
+
+        def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv, namespace)
+        def java_args = '['
+            args.each{item -> java_args += "\"$item\","}
+        java_args = java_args[0..-2] + ']'
+
+        kvs = [:]
+        kvs.put('<service_name>', SERVICE.name)
+        kvs.put('<service_version>', SERVICE.version)
+        kvs.put('<release>', BASE_BRANCH)
+        kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+        kvs.put('<imagePullSecret>', K3S.pull_secretId)
+        kvs.put('<docker_image>', imageUri)
+        kvs.put('<java_args>', java_args)
+        if (SERVICE.health != null)
+            kvs.put('<service_health>', SERVICE.health)
+        kvs.put('<configmap_env_name>', configmap_env_name)
+        k3sUtils.generateDeployment(kvs, 'deployment.yaml')
+        k3sUtils.applyDeployment(K3S, 'deployment.yaml')
+
+        k3sUtils.generateService(kvs, 'service.yaml')
+        k3sUtils.applyService(K3S, 'service.yaml')
+    }
+}
+
+def deployWithConfigmapEnvOnly2nd(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv, namespace)
+    def java_args = '['
+        args.each{item -> java_args += "\"$item\","}
+    java_args = java_args[0..-2] + ']'
+
+    kvs = [:]
+    kvs.put('<service_name>', SERVICE.name)
+    kvs.put('<service_version>', SERVICE.version)
+    kvs.put('<release>', base_branch)
+    kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+    kvs.put('<imagePullSecret>', K3S.pull_secretId)
+    kvs.put('<docker_image>', imageUri)
+    kvs.put('<java_args>', java_args)
+    if (SERVICE.health != null)
+        kvs.put('<service_health>', SERVICE.health)
+    kvs.put('<configmap_env_name>', configmap_env_name)
+    k3sUtils.generateDeployment(kvs, 'deployment2.yaml')
+    k3sUtils.applyDeployment(K3S, 'deployment2.yaml')
+
+    k3sUtils.generateService(kvs, 'service2.yaml')
+    k3sUtils.applyService(K3S, 'service2.yaml')
+}
+
+def deployWithConfigmapEnvOnly2nd_rollback(SERVICE, K3S, String rollbackBuildNumber, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    dir("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/k8s") {
+
+        def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv, namespace)
+        def java_args = '['
+            args.each{item -> java_args += "\"$item\","}
+        java_args = java_args[0..-2] + ']'
+
+        kvs = [:]
+        kvs.put('<service_name>', SERVICE.name)
+        kvs.put('<service_version>', SERVICE.version)
+        kvs.put('<release>', BASE_BRANCH)
+        kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+        kvs.put('<imagePullSecret>', K3S.pull_secretId)
+        kvs.put('<docker_image>', imageUri)
+        kvs.put('<java_args>', java_args)
+        if (SERVICE.health != null)
+            kvs.put('<service_health>', SERVICE.health)
+        kvs.put('<configmap_env_name>', configmap_env_name)
+        k3sUtils.generateDeployment(kvs, 'deployment2.yaml')
+        k3sUtils.applyDeployment(K3S, 'deployment2.yaml')
+
+        k3sUtils.generateService(kvs, 'service2.yaml')
+        k3sUtils.applyService(K3S, 'service2.yaml')
+    }
+}
+
+def deployWithConfigmaps_rollback(SERVICE, K3S, String rollbackBuildNumber, String[] args, configmapEnv="./configmap-env.ini", configmapConf="./configmap") {
+    
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    dir("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/k8s") {
+        def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv)
+        def configmap_conf_name = k3sUtils.applyConfigMapConfig(SERVICE, K3S, configmapConf)
+        def java_args = '['
+            args.each{item -> java_args += "\"$item\","}
+        java_args = java_args[0..-2] + ']'
+
+        kvs = [:]
+        kvs.put('<service_name>', SERVICE.name)
+        kvs.put('<service_version>', SERVICE.version)
+        kvs.put('<release>', BASE_BRANCH)
+        kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+        kvs.put('<imagePullSecret>', K3S.pull_secretId)
+        kvs.put('<docker_image>', imageUri)
+        kvs.put('<java_args>', java_args)
+        if (SERVICE.health != null)
+            kvs.put('<service_health>', SERVICE.health)
+        kvs.put('<configmap_env_name>', configmap_env_name)
+        kvs.put('<configmap_conf_name>', configmap_conf_name)
+        k3sUtils.generateDeployment(kvs, 'deployment.yaml')
+        k3sUtils.applyDeployment(K3S, 'deployment.yaml')
+
+        k3sUtils.generateService(kvs, 'service.yaml')
+        k3sUtils.applyService(K3S, 'service.yaml')
+    }
+}
+
+// 灰度发布: 部署Canary版本pod
+def deployCanaryWithConfigmaps(SERVICE, K3S, String base_branch, String[] args, namespace = "default") {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+    def version = 'canary'
+
+    def configmap_env_name = k3sUtils.applyCanaryConfigMapEnv(SERVICE, K3S, version, "./configmap-env.ini", namespace)
+    def configmap_conf_name = k3sUtils.applyCanaryConfigMapConfig(SERVICE, K3S, version, "./configmap")
+    def java_args = '['
+        args.each{item -> java_args += "\"$item\","}
+    java_args = java_args[0..-2] + ']'
+
+    def kvs = [:]
+    kvs.put('<namespace>', namespace)
+    kvs.put('<service_name>', SERVICE.name)
+    kvs.put('<service_version>', SERVICE.version)
+    kvs.put('<gray_version>', version)
+    kvs.put('<release>', base_branch)
+    kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+    kvs.put('<imagePullSecret>', K3S.pull_secretId)
+    kvs.put('<docker_image>', imageUri)
+    kvs.put('<java_args>', java_args)
+    if (SERVICE.health != null)
+        kvs.put('<service_health>', SERVICE.health)
+    kvs.put('<configmap_env_name>', configmap_env_name)
+    kvs.put('<configmap_conf_name>', configmap_conf_name)
+
+    def canaryDeploy = k3sUtils.generateCanaryDeployment(kvs, version, "./deployment.yaml")
+    print 'Generated ${canaryDeploy}. skip applying deployment.yaml'
+    k3sUtils.applyDeployment(K3S, canaryDeploy)
+
+    if (!fileExists('./service.yaml')) {
+        print "skip applying service. './service.yaml' file does not exists" 
+        return
+    }
+    def canaryService = k3sUtils.generateCanaryService(kvs, version, './service.yaml')
+    k3sUtils.applyService(K3S, canaryService)
+}
+
+def deployWithConfigmaps(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", configmapConf="./configmap") {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv)
+    def configmap_conf_name = k3sUtils.applyConfigMapConfig(SERVICE, K3S, configmapConf)
+    def java_args = '['
+        args.each{item -> java_args += "\"$item\","}
+    java_args = java_args[0..-2] + ']'
+
+    kvs = [:]
+    kvs.put('<service_name>', SERVICE.name)
+    kvs.put('<service_version>', SERVICE.version)
+    kvs.put('<release>', base_branch)
+    kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+    kvs.put('<imagePullSecret>', K3S.pull_secretId)
+    kvs.put('<docker_image>', imageUri)
+    kvs.put('<java_args>', java_args)
+    if (SERVICE.health != null)
+        kvs.put('<service_health>', SERVICE.health)
+    kvs.put('<configmap_env_name>', configmap_env_name)
+    kvs.put('<configmap_conf_name>', configmap_conf_name)
+    k3sUtils.generateDeployment(kvs, 'deployment.yaml')
+    k3sUtils.applyDeployment(K3S, 'deployment.yaml')
+
+    k3sUtils.generateService(kvs, 'service.yaml')
+    k3sUtils.applyService(K3S, 'service.yaml')
+}
+
+def deployWithConfigmaps2nd(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", configmapConf="./configmap") {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv)
+    def configmap_conf_name = k3sUtils.applyConfigMapConfig(SERVICE, K3S, configmapConf)
+    def java_args = '['
+    args.each{item -> java_args += "\"$item\","}
+    java_args = java_args[0..-2] + ']'
+
+    kvs = [:]
+    kvs.put('<service_name>', SERVICE.name)
+    kvs.put('<service_version>', SERVICE.version)
+    kvs.put('<release>', base_branch)
+    kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+    kvs.put('<imagePullSecret>', K3S.pull_secretId)
+    kvs.put('<docker_image>', imageUri)
+    kvs.put('<java_args>', java_args)
+    if (SERVICE.health != null)
+        kvs.put('<service_health>', SERVICE.health)
+    kvs.put('<configmap_env_name>', configmap_env_name)
+    kvs.put('<configmap_conf_name>', configmap_conf_name)
+    k3sUtils.generateDeployment(kvs, 'deployment2.yaml')
+    k3sUtils.applyDeployment(K3S, 'deployment2.yaml')
+
+    k3sUtils.generateService(kvs, 'service2.yaml')
+    k3sUtils.applyService(K3S, 'service2.yaml')
+}
+
+def execute(CONFIG, base_branch, closures=[:]) {                    //配置文件,当前分支,自定义闭包
+
+    stage('source code check out') {
+        
+        println "loading ${sharedLibsPath}/gitUtil.groovy..."     //打印信息共享库路径,变量路径,存储加载的录像本身地址,脚本名称
+        def gitUtils = load "${sharedLibsPath}/gitUtil.groovy"    //load方法加载对象,赋值给gitUtils
+
+        COMMIT_SHA = closures.GITCLONE == null ? gitUtils.clone(CONFIG.git, base_branch) : closures.GITCLONE()  //定义变量,为空则调用git配置中git仓库信息branch当前分支,不空调用自定义闭包
+    }
+
+    stage("docker image build") {
+
+        def projDir = 'projdir'                                 //定义变量,项目目录
+        if (closures.BUILDJAR != null)                          //检查是否自定义闭包,有就使用,没有就使用默认方法
+            projDir = closures.BUILDJAR()
+        else
+            projDir = buildJar(CONFIG.service, projDir) //default
+
+        if (closures.BACKUP != null)
+            closures.BACKUP()
+
+        if (closures.GENERATEDOCKERFILE != null)
+            projDir = closures.GENERATEDOCKERFILE()
+        else
+            projDir = generateDockerfile(CONFIG.service) //default
+
+        if (closures.BUILDIMAGE != null)
+            imageUri = closures.BUILDIMAGE()
+        else
+            imageUri = buildDockerImage(CONFIG.service, CONFIG.docker, base_branch, projDir)
+
+        if (closures.CLEANJAR != null)
+            closures.CLEANJAR()
+        else
+            cleanJar(projDir)
+    }
+    stage("notify when build") {
+        
+        //notifywhenbuild(base_branch, CONFIG.service.name)
+    }
+    stage("apply K8S ConfigMaps && Deployment") {
+        
+        if (closures.K3SDEPLOY != null) {
+            closures.K3SDEPLOY()
+            return
+        }
+    }
+}
+
+// 灰度发布: 部署stable版本pod
+def deployStableWithConfigmaps(SERVICE, K3S, String BASE_BRANCH, String rollbackBuildNumber, String[] args, namespace='default') {
+    
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+    def release = 'stable'
+
+    dir("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/k8s") {
+        def configmap_env_name = k3sUtils.applyCanaryConfigMapEnv(SERVICE, K3S, release, "./configmap-env.ini", namespace)
+        def configmap_conf_name = k3sUtils.applyCanaryConfigMapConfig(SERVICE, K3S, release, "./configmap")
+        def java_args = '['
+            args.each{item -> java_args += "\"$item\","}
+        java_args = java_args[0..-2] + ']'
+
+        def kvs = [:]
+        kvs.put('<namespace>', namespace)
+        kvs.put('<service_name>', SERVICE.name)
+        kvs.put('<service_version>', SERVICE.version)
+        kvs.put('<gray_version>', release)
+        kvs.put('<release>', BASE_BRANCH)
+        kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+        kvs.put('<imagePullSecret>', K3S.pull_secretId)
+        kvs.put('<docker_image>', imageUri)
+        kvs.put('<java_args>', java_args)
+        if (SERVICE.health != null)
+            kvs.put('<service_health>', SERVICE.health)
+        kvs.put('<configmap_env_name>', configmap_env_name)
+        kvs.put('<configmap_conf_name>', configmap_conf_name)
+        def stableDeploy = k3sUtils.generateCanaryDeployment(kvs, release, './deployment.yaml')
+        k3sUtils.applyDeployment(K3S, stableDeploy)
+
+        if (!fileExists('./service.yaml')) {
+            print "skip applying service. './service.yaml' file does not exists" 
+            return
+        }
+        def stableService = k3sUtils.generateCanaryService(kvs, release, './service.yaml')
+        k3sUtils.applyService(K3S, stableService)
+    }
+}
+
+// 灰度发布: 部署stable版本pod
+def deployStableWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args, namespace='default') {
+
+        print "loading ${sharedLibsPath}/k3sUtil.groovy ..."
+        def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+        def release = 'stable'
+
+        dir("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/k8s") {
+
+            def configmap_env_name = k3sUtils.applyCanaryConfigMapEnv(SERVICE, K3S, release, "./configmap-env.ini", namespace)
+            def java_args = '['
+                args.each{item -> java_args += "\"$item\", "}
+            java_args = java_args[0..-2] + ']'
+
+            def kvs = [:]
+            kvs.put('<namespace>', namespace)
+            kvs.put('<service_name>', SERVICE.name)
+            kvs.put('<service_version>', SERVICE.version)
+            kvs.put('<gray_version>', release)
+            kvs.put('<release>', base_branch)
+            kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+            kvs.put('<imagePullSecret>', K3S.pull_secretId)
+            kvs.put('<docker_image>', imageUri)
+            kvs.put('<java_args>', java_args)
+            if (SERVICE.health != null)
+                kvs.put('<service_health>', SERVICE.health)
+            kvs.put('<configmap_env_name>', configmap_env_name)
+            def canaryDeploy = k3sUtils.generateCanaryDeployment(kvs, release, "./deployment.yaml")
+            k3sUtils.applyDeployment(K3S, canaryDeploy)
+
+            if (!fileExists(canaryService)) {
+                print "skip applying service. './service.yaml' file does not exists" 
+                return
+            }
+            def canaryService = k3sUtils.generateCanaryService(kvs, release, './service.yaml')
+            k3sUtils.applyService(K3S, canaryService)
+        }
+}
+
+// 灰度发布: 升级stable版本
+def upgrade_stable(Object CONFIG, String base_branch, String rollbackBuildNumber, closures = [:]) {
+    
+    // stage('find latest Canary version') {
+
+    //     def basePath = "${backup_destination}/${JOB_NAME}"
+    //     def baseDir = new File(basePath)
+    //     if (!baseDir.exists() || !baseDir.isDirectory()) {
+    //         error "Path does not exist or is not a directory: ${basePath}"
+    //         return
+    //     }
+
+    //     latestDir = baseDir.listFiles()
+    //         ?.findAll { f ->
+    //             f.isDirectory() && (f.name ==~ /^\d+$/)
+    //         }
+    //         ?.max { it.lastModified() }
+
+    //     rollbackBuildNumber = latestDir.name
+    // }
+
+    // 2. docker image build
+    stage("docker image restore") {
+
+        def envFile = new File("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/env")
+        def lines = envFile.readLines()
+        COMMIT_SHA = lines[0]
+        BASE_BRANCH = lines[1]
+
+        def path = "${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}"
+        print("restore docker image from rollbackBuildNo. #${rollbackBuildNumber}")
+        // imageUri = restoreDockerImage(CONFIG.docker, path, BASE_BRANCH, rollbackBuildNumber)
+        imageUri = generateFullImageUri(CONFIG.docker, BASE_BRANCH, rollbackBuildNumber)
+        print "restore docker image: ${imageUri}"
+        // cleanDockerImage(imageUri)
+    }
+
+    stage("apply Release(Canary:${rollbackBuildNumber}) K8S ConfigMaps && Deployment") {
+
+        if (closures.K3SDEPLOY != null)
+            closures.K3SDEPLOY()
+    }
+    
+
+    stage("Post Execution Script") {
+
+        // print "Clear rollback workspace"
+        // 清理rollback工作目录
+        // dir("${backup_destination}/${JOB_NAME}") {
+        //     sh "rm -rf *@tmp"
+        // }
+    }
+}
+
+// 灰度发布: 升级canary版本
+def upgrade(Object CONFIG, String base_branch, String[] k8sBackupFiles, closures=[:]) {
+    
+    stage('source code check out') {
+        
+        println "loading ${sharedLibsPath}/gitUtil.groovy..."
+        def gitUtils = load "${sharedLibsPath}/gitUtil.groovy"
+
+        COMMIT_SHA = closures.GITCLONE == null ? gitUtils.clone(CONFIG.git, base_branch) : closures.GITCLONE()
+
+        if (closures.POST_GITCLONE != null)
+            closures.POST_GITCLONE()
+    }
+
+    stage("docker image build") {
+
+        def projDir = 'projdir'
+        if (closures.BUILDJAR != null)
+            projDir = closures.BUILDJAR()
+        else
+            projDir = buildJar(CONFIG.service, projDir) //default
+
+        if (closures.GENERATEDOCKERFILE != null)
+            projDir = closures.GENERATEDOCKERFILE()
+        else
+            projDir = generateDockerfile(CONFIG.service) //default
+
+        if (closures.BUILDIMAGE != null)
+            imageUri = closures.BUILDIMAGE()
+        else
+            imageUri = buildDockerImage(CONFIG.service, CONFIG.docker, base_branch, projDir)
+
+        if (closures.CLEANJAR != null)
+            closures.CLEANJAR()
+        else
+            cleanJar(projDir)
+
+        if (k8sBackupFiles != null && k8sBackupFiles.size() > 0) {
+            backupDockerImage(imageUri)
+            envFile = new File("${backup_destination}/${JOB_NAME}/${BUILD_NUMBER}/env")
+            envFile << "${COMMIT_SHA}\n"
+            envFile << "${base_branch}\n"
+        }
+
+        cleanDockerImage(imageUri)
+    }
+
+    stage("apply K8S ConfigMaps && Deployment") {
+        
+        if (k8sBackupFiles != null && k8sBackupFiles.size() > 0)
+            backupK8sConfigs(k8sBackupFiles)
+
+        if (closures.K3SDEPLOY != null) {
+            closures.K3SDEPLOY()
+            return
+        }
+    }
+
+    stage("notify after build") {
+        if (closures.BUILDINFO != null) {
+            def buildInfo = closures.ALLCHANGES()
+            if (buildInfo.length() > 800) {
+                buildInfo = buildInfo.substring(buildInfo.length() - 800)
+            }
+            def title = closures.BUILDINFO()+"正在发布"
+            def content = "变更:"+buildInfo+"\\n"+
+                        "分支:"+base_branch+"\\n"+
+                        "版本:"+BUILD_NUMBER+"\\n"
+            sendGroupNotification(title, content)
+        }
+    }
+
+    stage("Post Execution Script") {
+
+        print "Clear older backup version"
+        // 清理旧的备份
+        dir("${backup_destination}/${JOB_NAME}") {
+            sh """
+            save_file=`ls -ltr | tail -${BACKUP_MAX} | awk '{print \$NF}'`
+            ls | grep -v "\$save_file" | xargs rm -rf
+            """
+        }
+    }
+}
+
+
+def rollback(CONFIG, rollbackBuildNumber, closures=[:]) {
+    // 1. restore specified version
+    path = "${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}"
+    if (fileExists(path)) {
+
+        stage('source code check out') {
+            print("souce code check out. Skipped!")
+        }
+
+
+        envFile = new File("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/env")
+        def lines = envFile.readLines()
+        COMMIT_SHA = lines[0]
+        BASE_BRANCH = lines[1]
+
+        // 2. docker image build
+        stage("docker image build") {
+            print("restore docker image from rollbackNo. #${rollbackBuildNumber}")
+            imageUri = restoreDockerImage(CONFIG.docker, path, BASE_BRANCH, rollbackBuildNumber)
+            cleanDockerImage(imageUri)
+        }
+
+        // 3. k8s configmap && deployment apply
+        stage("apply K8S ConfigMaps && Deployment") {
+            if (closures.K3SDEPLOY != null)
+                closures.K3SDEPLOY()
+        }
+
+        stage("Post Execution Script") {
+
+            print "Clear rollback workspace"
+            // 清理rollback工作目录
+            dir("${backup_destination}/${JOB_NAME}") {
+                sh "rm -rf *@tmp"
+            }
+        }
+    }
+    
+}
+
+
+def sendGroupNotification(String title, String content) {
+
+    WECHAT_ROBOT_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=92059925-97db-4830-8438-1514a246a143"
+    stage('Send Message') {
+       
+                        // 构建要发送的企业微信消息
+                        def message = """
+                        {
+                            "msgtype": "text",
+                            "text": {
+                                "content": "【${title}】\n${content}"
+                            }
+                        }
+                        """
+                        // 发送消息到企业微信
+                        sh """
+                        curl -X POST -H 'Content-Type: application/json' -d '${message}' ${WECHAT_ROBOT_URL}
+                        """
+    }   
+
+}    
+
+        
+return this

+ 200 - 21
sharedLibs/stages.k8s.groovy

@@ -1,4 +1,3 @@
-// can be called locally, without "def"
 myUtil = { -> 
     println "hello myUtil"
 }
@@ -6,14 +5,12 @@ myUtil = { ->
 sharedLibsPath = "${env.WORKSPACE}/sharedLibs"
 backup_destination = '/var/jenkins_home/backups'
 backup_targetFile = 'javaDockerImage.tar'
-BACKUP_MAX = 3
+BACKUP_MAX = 2
 
-// 获取镜像URL
 generateFullImageUri = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
     return "${DOCKER.registry}/${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
 }
 
-// 获取镜像名
 generateImageName = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
     return "${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
 }
@@ -57,7 +54,6 @@ def backupDockerImage(String dockerImgName) {
 
     echo "-----> Backup artifact(docker image : ${dockerImgName}) ..."
     path = "${backup_destination}/${JOB_NAME}/${BUILD_NUMBER}"
-    // docker save ${dockerImgName} | gzip > ${backup_targetFile}.tar.gz
     sh """
     docker save ${dockerImgName} -o ${backup_targetFile}
     if [ -d ${path} ];
@@ -100,6 +96,7 @@ def generateDockerfile(Object SERVICE, String pathOfDockerfile='projdir') {
     echo "-----> Generating Dockerfile: ${pathOfDockerfile}/Dockerfile ..."
     sh """cat > ${pathOfDockerfile}/Dockerfile<<EOF
 FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openjdk:17-jdk-alpine as final
+# ADD skywalking-agent /app/skywalking-agent
 ADD ${module}target/${SERVICE.jar} /app/target/${SERVICE.jar}
 EOF
 """
@@ -116,7 +113,6 @@ def buildDockerImage(Object SERVICE, Object DOCKER, String base_branch, projDir=
             def image = docker.build(imageName)
             image.push()
             sh "docker rmi ${imageName}"
-            // sh "docker rmi ${imageUri}"
             if(filter2Remove != null)
                 sh "docker image prune --filter label=${filter2Remove} -f"
         }
@@ -129,9 +125,7 @@ def restoreDockerImage(Object DOCKER, String workspace, String base_branch, Stri
     dir(workspace) {
 
         imageUri = generateFullImageUri(DOCKER, base_branch, rollbackBuildNumber)
-        // gunzip ${backup_targetFile}.tar.gz | docker load
         sh "docker load -i ${backup_targetFile}"
-        // in case, docker image doesn't exist in remote registry
         docker.withRegistry("http://${DOCKER.registry}", "${DOCKER.push_credentialId}") {
             def image = docker.image(imageUri)
             image.push()
@@ -140,7 +134,41 @@ def restoreDockerImage(Object DOCKER, String workspace, String base_branch, Stri
     return imageUri
 }
 
-def deployWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", namespace='ahxpm') {
+// 灰度发布: 部署Canary版本pod
+// version: release or canary
+def deployCanaryWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args, String version, namespace='default') {
+
+        print "loading ${sharedLibsPath}/k3sUtil.groovy ..."
+        def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+        def configmap_env_name = k3sUtils.applyCanaryConfigMapEnv(SERVICE, K3S, version, "./configmap-env.ini", namespace)
+        print "get configmap_env_name: ${configmap_env_name}"
+        
+        def java_args = '['
+        args.each{item -> java_args += "\"$item\", "}
+        java_args = java_args[0..-2] + ']'
+
+        kvs = [:]
+        kvs.put('<namespace>', namespace)
+        kvs.put('<service_name>', SERVICE.name)
+        kvs.put('<service_version>', SERVICE.version)
+        kvs.put('<gray_version>', version)
+        kvs.put('<release>', base_branch)
+        kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+        kvs.put('<imagePullSecret>', K3S.pull_secretId)
+        kvs.put('<docker_image>', imageUri)
+        kvs.put('<java_args>', java_args)
+        if (SERVICE.health != null)
+            kvs.put('<service_health>', SERVICE.health)
+        kvs.put('<configmap_env_name>', configmap_env_name)
+        def canaryDeploy = k3sUtils.generateCanaryDeployment(kvs, version, "./deployment.yaml")
+        k3sUtils.applyDeployment(K3S, canaryDeploy)
+
+        def canaryService = k3sUtils.generateCanaryService(kvs, version, "service.yaml")
+        k3sUtils.applyService(K3S, canaryService)
+}
+
+def deployWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
 
     print "loading ${sharedLibsPath}/k3sUtil.groovy..."
     def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
@@ -168,7 +196,7 @@ def deployWithConfigmapEnvOnly(SERVICE, K3S, String base_branch, String[] args,
     k3sUtils.applyService(K3S, 'service.yaml')
 }
 
-def deployWithConfigmapEnvOnly_rollback(SERVICE, K3S, String rollbackBuildNumber, String[] args, configmapEnv="./configmap-env.ini", namespace='ahxpm') {
+def deployWithConfigmapEnvOnly_rollback(SERVICE, K3S, String rollbackBuildNumber, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
 
     print "loading ${sharedLibsPath}/k3sUtil.groovy..."
     def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
@@ -199,7 +227,7 @@ def deployWithConfigmapEnvOnly_rollback(SERVICE, K3S, String rollbackBuildNumber
     }
 }
 
-def deployWithConfigmapEnvOnly2nd(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", namespace='ahxpm') {
+def deployWithConfigmapEnvOnly2nd(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
 
     print "loading ${sharedLibsPath}/k3sUtil.groovy..."
     def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
@@ -227,7 +255,7 @@ def deployWithConfigmapEnvOnly2nd(SERVICE, K3S, String base_branch, String[] arg
     k3sUtils.applyService(K3S, 'service2.yaml')
 }
 
-def deployWithConfigmapEnvOnly2nd_rollback(SERVICE, K3S, String rollbackBuildNumber, String[] args, configmapEnv="./configmap-env.ini", namespace='ahxpm') {
+def deployWithConfigmapEnvOnly2nd_rollback(SERVICE, K3S, String rollbackBuildNumber, String[] args, configmapEnv="./configmap-env.ini", namespace='default') {
 
     print "loading ${sharedLibsPath}/k3sUtil.groovy..."
     def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
@@ -290,6 +318,41 @@ def deployWithConfigmaps_rollback(SERVICE, K3S, String rollbackBuildNumber, Stri
     }
 }
 
+// 灰度发布: 部署Canary版本pod
+// version: release or canary
+def deployCanaryWithConfigmaps(SERVICE, K3S, String base_branch, String[] args, String version, namespace = "default") {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    def configmap_env_name = k3sUtils.applyCanaryConfigMapEnv(SERVICE, K3S, version, "./configmap-env.ini")
+    def configmap_conf_name = k3sUtils.applyCanaryConfigMapConfig(SERVICE, K3S, version, "./configmap")
+    def java_args = '['
+        args.each{item -> java_args += "\"$item\","}
+    java_args = java_args[0..-2] + ']'
+
+    def kvs = [:]
+    kvs.put('<namespace>', namespace)
+    kvs.put('<service_name>', SERVICE.name)
+    kvs.put('<service_version>', SERVICE.version)
+    kvs.put('<gray_version>', version)
+    kvs.put('<release>', base_branch)
+    kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+    kvs.put('<imagePullSecret>', K3S.pull_secretId)
+    kvs.put('<docker_image>', imageUri)
+    kvs.put('<java_args>', java_args)
+    if (SERVICE.health != null)
+        kvs.put('<service_health>', SERVICE.health)
+    kvs.put('<configmap_env_name>', configmap_env_name)
+    kvs.put('<configmap_conf_name>', configmap_conf_name)
+
+    def canaryDeploy = k3sUtils.generateCanaryDeployment(kvs, version, "./deployment.yaml")
+    k3sUtils.applyDeployment(K3S, canaryDeploy)
+
+    def canaryService = k3sUtils.generateCanaryService(kvs, version, "service.yaml")
+    k3sUtils.applyService(K3S, canaryService)
+}
+
 def deployWithConfigmaps(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", configmapConf="./configmap") {
 
     print "loading ${sharedLibsPath}/k3sUtil.groovy..."
@@ -320,6 +383,38 @@ def deployWithConfigmaps(SERVICE, K3S, String base_branch, String[] args, config
     k3sUtils.applyService(K3S, 'service.yaml')
 }
 
+
+def deployWithConfigmapsWithNameSpace(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", configmapConf="./configmap", namespace = "default") {
+
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    def configmap_env_name = k3sUtils.applyConfigMapEnv(SERVICE, K3S, configmapEnv)
+    def configmap_conf_name = k3sUtils.applyConfigMapConfig(SERVICE, K3S, configmapConf)
+    def java_args = '['
+        args.each{item -> java_args += "\"$item\","}
+    java_args = java_args[0..-2] + ']'
+
+    kvs = [:]
+    kvs.put('<namespace>', namespace)
+    kvs.put('<service_name>', SERVICE.name)
+    kvs.put('<service_version>', SERVICE.version)
+    kvs.put('<release>', base_branch)
+    kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+    kvs.put('<imagePullSecret>', K3S.pull_secretId)
+    kvs.put('<docker_image>', imageUri)
+    kvs.put('<java_args>', java_args)
+    if (SERVICE.health != null)
+        kvs.put('<service_health>', SERVICE.health)
+    kvs.put('<configmap_env_name>', configmap_env_name)
+    kvs.put('<configmap_conf_name>', configmap_conf_name)
+    k3sUtils.generateDeployment(kvs, 'deployment.yaml')
+    k3sUtils.applyDeployment(K3S, 'deployment.yaml')
+
+    k3sUtils.generateService(kvs, 'service.yaml')
+    k3sUtils.applyService(K3S, 'service.yaml')
+}
+
 def deployWithConfigmaps2nd(SERVICE, K3S, String base_branch, String[] args, configmapEnv="./configmap-env.ini", configmapConf="./configmap") {
 
     print "loading ${sharedLibsPath}/k3sUtil.groovy..."
@@ -350,7 +445,6 @@ def deployWithConfigmaps2nd(SERVICE, K3S, String base_branch, String[] args, con
     k3sUtils.applyService(K3S, 'service2.yaml')
 }
 
-// Jenkins pipeline stages
 def execute(CONFIG, base_branch, closures=[:]) {                    //配置文件,当前分支,自定义闭包
 
     stage('source code check out') {
@@ -367,7 +461,7 @@ def execute(CONFIG, base_branch, closures=[:]) {                    //配置文
         if (closures.BUILDJAR != null)                          //检查是否自定义闭包,有就使用,没有就使用默认方法
             projDir = closures.BUILDJAR()
         else
-            projDir = buildJar(CONFIG.service, projDir) //ahxpm
+            projDir = buildJar(CONFIG.service, projDir) //default
 
         if (closures.BACKUP != null)
             closures.BACKUP()
@@ -375,7 +469,7 @@ def execute(CONFIG, base_branch, closures=[:]) {                    //配置文
         if (closures.GENERATEDOCKERFILE != null)
             projDir = closures.GENERATEDOCKERFILE()
         else
-            projDir = generateDockerfile(CONFIG.service) //ahxpm
+            projDir = generateDockerfile(CONFIG.service) //default
 
         if (closures.BUILDIMAGE != null)
             imageUri = closures.BUILDIMAGE()
@@ -400,6 +494,94 @@ def execute(CONFIG, base_branch, closures=[:]) {                    //配置文
     }
 }
 
+// 灰度发布: 部署release版本pod
+def deployReleaseWithConfigmaps(SERVICE, K3S, String BASE_BRANCH, String[] args, namespace='default') {
+    
+    print "loading ${sharedLibsPath}/k3sUtil.groovy..."
+    def k3sUtils = load "${sharedLibsPath}/k3sUtil.groovy"
+
+    dir("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/k8s") {
+        def configmap_env_name = k3sUtils.applyCanaryConfigMapEnv(SERVICE, K3S, 'release', "./configmap-env.ini")
+        def configmap_conf_name = k3sUtils.applyCanaryConfigMapConfig(SERVICE, K3S, 'release', "./configmap")
+        def java_args = '['
+        args.each{item -> java_args += "\"$item\","}
+        java_args = java_args[0..-2] + ']'
+
+        def kvs = [:]
+        kvs.put('<namespace>', namespace)
+        kvs.put('<service_name>', SERVICE.name)
+        kvs.put('<service_version>', SERVICE.version)
+        kvs.put('<gray_version>', 'release')
+        kvs.put('<release>', BASE_BRANCH)
+        kvs.put('<COMMIT_SHA>', COMMIT_SHA)
+        kvs.put('<imagePullSecret>', K3S.pull_secretId)
+        kvs.put('<docker_image>', imageUri)
+        kvs.put('<java_args>', java_args)
+        if (SERVICE.health != null)
+            kvs.put('<service_health>', SERVICE.health)
+        kvs.put('<configmap_env_name>', configmap_env_name)
+        kvs.put('<configmap_conf_name>', configmap_conf_name)
+        def releaseDeploy = k3sUtils.generateCanaryDeployment(kvs, 'release', 'deployment.yaml')
+        k3sUtils.applyDeployment(K3S, releaseDeploy)
+
+        def releaseService = k3sUtils.generateCanaryService(kvs, 'release', 'service.yaml')
+        k3sUtils.applyService(K3S, releaseService)
+    }
+}
+
+def upgrade_release(Object CONFIG, String base_branch, closures = [:]) {
+    
+    stage('find latest Canary version') {
+
+        def basePath = "${backup_destination}/${JOB_NAME}"
+        def baseDir = new File(basePath)
+        if (!baseDir.exists() || !baseDir.isDirectory()) {
+            error "Path does not exist or is not a directory: ${basePath}"
+            return
+        }
+
+        latestDir = baseDir
+            .listFiles()
+            ?.findAll { f ->
+                f.isDirectory() && (f.name ==~ /^\d+$/)
+            }
+            ?.max { it.lastModified() }
+
+        rollbackBuildNumber = latestDir.name
+    }
+
+    // 2. docker image build
+    stage("docker image restore") {
+
+        envFile = new File("${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}/env")
+        def lines = envFile.readLines()
+        COMMIT_SHA = lines[0]
+        BASE_BRANCH = lines[1]
+
+        def path = "${backup_destination}/${JOB_NAME}/${rollbackBuildNumber}"
+        print("restore docker image from rollbackBuildNo. #${rollbackBuildNumber}")
+        imageUri = restoreDockerImage(CONFIG.docker, path, BASE_BRANCH, rollbackBuildNumber)
+        // cleanDockerImage(imageUri)
+    }
+
+    stage("apply Release(Canary:${rollbackBuildNumber}) K8S ConfigMaps && Deployment") {
+
+        if (closures.K3SDEPLOY != null)
+            closures.K3SDEPLOY()
+    }
+    
+    
+
+    stage("Post Execution Script") {
+
+        // print "Clear rollback workspace"
+        // 清理rollback工作目录
+        // dir("${backup_destination}/${JOB_NAME}") {
+        //     sh "rm -rf *@tmp"
+        // }
+    }
+}
+
 def upgrade(Object CONFIG, String base_branch, String[] k8sBackupFiles, closures=[:]) {
     
     stage('source code check out') {
@@ -419,12 +601,12 @@ def upgrade(Object CONFIG, String base_branch, String[] k8sBackupFiles, closures
         if (closures.BUILDJAR != null)
             projDir = closures.BUILDJAR()
         else
-            projDir = buildJar(CONFIG.service, projDir) //ahxpm
+            projDir = buildJar(CONFIG.service, projDir) //default
 
         if (closures.GENERATEDOCKERFILE != null)
             projDir = closures.GENERATEDOCKERFILE()
         else
-            projDir = generateDockerfile(CONFIG.service) //ahxpm
+            projDir = generateDockerfile(CONFIG.service) //default
 
         if (closures.BUILDIMAGE != null)
             imageUri = closures.BUILDIMAGE()
@@ -436,30 +618,27 @@ def upgrade(Object CONFIG, String base_branch, String[] k8sBackupFiles, closures
         else
             cleanJar(projDir)
 
-        // stage("Docker Image backup") {
         if (k8sBackupFiles != null && k8sBackupFiles.size() > 0) {
             backupDockerImage(imageUri)
             envFile = new File("${backup_destination}/${JOB_NAME}/${BUILD_NUMBER}/env")
             envFile << "${COMMIT_SHA}\n"
             envFile << "${base_branch}\n"
         }
-        // }
 
         cleanDockerImage(imageUri)
     }
 
     stage("apply K8S ConfigMaps && Deployment") {
         
-        // stage("K8s configs backup") {
         if (k8sBackupFiles != null && k8sBackupFiles.size() > 0)
             backupK8sConfigs(k8sBackupFiles)
-        // }
 
         if (closures.K3SDEPLOY != null) {
             closures.K3SDEPLOY()
             return
         }
     }
+
     stage("notify when build") {
         if (closures.BUILDINFO != null) {
             def buildInfo = closures.ALLCHANGES()

+ 1 - 3
sharedLibs/stages.k8s.python.groovy

@@ -1,5 +1,4 @@
-// -------------
-// python on K8S
+
 sharedLibsPath = "${env.WORKSPACE}/sharedLibs"
 backup_destination = '/var/jenkins_home/backups'
 backup_targetFile = 'dockerImage.tar'
@@ -224,7 +223,6 @@ def deployWithConfigmaps(SERVICE, K3S, String base_branch, String[] args, config
     k3sUtils.applyService(K3S, 'service.yaml')
 }
 
-// Jenkins pipeline stages
 def upgrade(Object CONFIG, String base_branch, String[] k8sBackupFiles, closures=[:]) {
     
     stage('Source code check out') {

+ 0 - 6
sharedLibs/stages.nodejs.groovy

@@ -2,12 +2,10 @@ sharedLibsPath = "${env.WORKSPACE}/sharedLibs"
 backup_destination = '/var/jenkins_home/backups'
 backup_targetFile = 'dockerImage.tar'
 
-// 获取镜像URL
 generateFullImageUri = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
     return "${DOCKER.registry}/${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
 }
 
-// 获取镜像名
 generateImageName = {Map DOCKER, String base_branch, String BUILD_NUMBER ->
     return "${DOCKER.image}:${base_branch}-${BUILD_NUMBER}"
 }
@@ -65,7 +63,6 @@ def restoreDockerImage(Object DOCKER, String workspace, String base_branch, Stri
 
         imageUri = generateFullImageUri(DOCKER, base_branch, rollbackBuildNumber)
         sh "docker load -i ${backup_targetFile}"
-        // in case, docker image doesn't exist in remote registry
         docker.withRegistry("http://${DOCKER.registry}", "${DOCKER.push_credentialId}") {
             def image = docker.image(imageUri)
             image.push()
@@ -129,7 +126,6 @@ def applyDeployment(SERVICE, K3S, String base_branch) {
     k3sUtils.applyService(K3S)
 }
 
-// Jenkins pipeline stages
 def execute(CONFIG, base_branch, closures=[:]) {
 
     stage('source code check out') {
@@ -212,7 +208,6 @@ def upgrade(Object CONFIG, String base_branch, String[] k8sBackupFiles, closures
     }
 
     stage("Clear older backup version") {
-        // 清理旧的备份
         dir("${backup_destination}/${JOB_NAME}") {
             echo "-----> Clear older backup version ..."
             sh """
@@ -257,7 +252,6 @@ def rollback(Object CONFIG, String rollbackBuildNumber, closures=[:]) {
         }
 
         stage("Clear rollback workspace") {
-            // 清理rollback工作目录
             dir("${backup_destination}/${JOB_NAME}") {
                 echo "-----> Clear rollback workspace ..."
                 sh "rm -rf *@tmp"

+ 0 - 4
sharedLibs/stages.ssh.groovy

@@ -69,8 +69,6 @@ def upgrade(Object CONFIG, String base_branch, closures=[:]) {
                 timeoutSec: 60
             ]
 
-            // 6.2 启动服务
-            echo '🚀 启动 Python 服务...'
             try {
                 sshCommand remote: remote,
                     command: """
@@ -83,9 +81,7 @@ def upgrade(Object CONFIG, String base_branch, closures=[:]) {
                 echo "✅ Python(${gitTargetPath}) 服务已启动 (目录: ${CONFIG.ssh.remotePath}/${gitTargetPath})"
             } catch (Exception e) {
                 echo "❌ 命令执行失败: ${e.getMessage()}"
-                // 处理失败情况
             }
-            // if (startStatus != 0) error "❌ 服务启动失败"
         }
     }
 }

+ 9 - 17
sharedLibs/yaml2Map.groovy

@@ -1,26 +1,18 @@
-// file -> Map
 def read(String filePath) {
   def pipelineCfg = readYaml(file: filePath)
   return pipelineCfg
 }
 
-// 用project 覆盖 global
 def merge(Object project, Object global) {
-  global.k3s = global.k3s + (project.k3s == null ? [:] : project.k3s)
-  global.docker = global.docker + (project.docker == null ? [:] : project.docker)
-  global.git = (global.git == null ? [:] : global.git) + (project.git == null ? [:] : project.git)
-  global.service = project.service
-  global.skywalking = project.skywalking
-
-  if (project.ssh != null) {
-    global.ssh = project.ssh
-  }
-
-  if (project.files != null) {
-    global.files = project.files
+  def result = global.clone()
+  project.each { key, value ->
+    if (value instanceof Map && result[key] instanceof Map) {
+      result[key] = merge(value, result[key])
+    } else {
+      result[key] = value
+    }
   }
-  
-  return global
+  return result
 }
 
-return this
+return this