|
@@ -46,7 +46,9 @@
|
|
|
* 与预生产「每服务一子目录」约定一致。生产 Docker 挂载 jar 时请指向 PROD 路径。
|
|
* 与预生产「每服务一子目录」约定一致。生产 Docker 挂载 jar 时请指向 PROD 路径。
|
|
|
* 若 compose 将生产 jar 挂为「../java:/app_deploy」(相对 compose 目录),则 Jenkins 内应设 PROD_DEPLOY_ROOT=/app_deploy,对应宿主机为 compose 上级目录下的 java/。
|
|
* 若 compose 将生产 jar 挂为「../java:/app_deploy」(相对 compose 目录),则 Jenkins 内应设 PROD_DEPLOY_ROOT=/app_deploy,对应宿主机为 compose 上级目录下的 java/。
|
|
|
* 【必配】若 Job 使用 PROD_DEPLOY_ROOT=/alien_produ/java 且 **未** 配置 PROD_SSH_TARGET(本机发版):Jenkins 容器必须 volume 挂载宿主机 /alien_produ/java 到同路径(见 docker-compose-jenkins-host-network.yml),否则 cp 写在 Jenkins 私有层、生产侧进程读宿主机文件时,unzip 通过仍 corrupt jarfile。
|
|
* 【必配】若 Job 使用 PROD_DEPLOY_ROOT=/alien_produ/java 且 **未** 配置 PROD_SSH_TARGET(本机发版):Jenkins 容器必须 volume 挂载宿主机 /alien_produ/java 到同路径(见 docker-compose-jenkins-host-network.yml),否则 cp 写在 Jenkins 私有层、生产侧进程读宿主机文件时,unzip 通过仍 corrupt jarfile。
|
|
|
- * 【环境隔离】预生产仍在 39.106.135.88(UAT_DEPLOY_ROOT 读该机上制品);生产默认发到 **39.105.153.68**:设置 PROD_SSH_TARGET=alien_store@39.105.153.68(可改)后,制品先写入 Jenkins 工作区下暂存目录再 rsync 到目标机 PROD_DEPLOY_ROOT,docker/docker compose 经 SSH 在目标机执行(须 Jenkins 到目标机免密 sudo docker)。留空 PROD_SSH_TARGET 则保持「Jenkins 与生产 dockerd 同机」旧行为。
|
|
|
|
|
|
|
+ * 【环境隔离】预生产仍在 39.106.135.88(UAT_DEPLOY_ROOT 读该机上制品);生产默认发到 **39.105.153.68**:设置 PROD_SSH_TARGET=alien_store@39.105.153.68(可改)后,制品先写入 Jenkins 工作区下暂存目录再 rsync 到目标机 PROD_DEPLOY_ROOT,docker/docker compose 经 SSH 在目标机执行(须目标机 sudo 免密 docker)。留空 PROD_SSH_TARGET 则保持「Jenkins 与生产 dockerd 同机」旧行为。
|
|
|
|
|
+ * 【SSH】远程发版须 Jenkins 能登录 PROD_SSH_TARGET:填写 PROD_SSH_CREDENTIALS_ID(Jenkins「SSH Username with private key」凭据,需 SSH Agent 插件)或将私钥配在运行节点默认 ssh(~/.ssh)。否则 ssh 报 Permission denied (publickey)。
|
|
|
|
|
+ * 【凭据参数仍为空】已存在 Job 常不继承 Jenkinsfile 里 parameters 的新 defaultValue,运行时 params 仍为空串;脚本用 effectiveProdSshCredentialsId() 在「已配置 PROD_SSH_TARGET」时回退 DEFAULT_PROD_SSH_CREDENTIALS_ID,仍走 sshagent。换凭据后改脚本内该常量或显式填参数。
|
|
|
*
|
|
*
|
|
|
* 生产容器须由 <PROD_DEPLOY_ROOT>/docker-compose.yml(或 PROD_COMPOSE_FILE)通过 docker compose 管理;与 docker restart / docker run 的「独立容器」不共享 Compose 元数据,见 Job 头「docker compose 启动方式」说明。
|
|
* 生产容器须由 <PROD_DEPLOY_ROOT>/docker-compose.yml(或 PROD_COMPOSE_FILE)通过 docker compose 管理;与 docker restart / docker run 的「独立容器」不共享 Compose 元数据,见 Job 头「docker compose 启动方式」说明。
|
|
|
* 若 PROD 目录下出现 alien-xxx-1.0.0.jar「目录」且为空:多为历史上 docker -v 指向不存在的文件时 Docker 在宿主机误建同名目录;须 rm -rf 该目录后再拷贝真 jar。
|
|
* 若 PROD 目录下出现 alien-xxx-1.0.0.jar「目录」且为空:多为历史上 docker -v 指向不存在的文件时 Docker 在宿主机误建同名目录;须 rm -rf 该目录后再拷贝真 jar。
|
|
@@ -153,6 +155,22 @@ def jasyptParamToPlain(def raw) {
|
|
|
return raw.toString().trim()
|
|
return raw.toString().trim()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 与 parameters 中 PROD_SSH_CREDENTIALS_ID 的 defaultValue 对齐;已存在 Job 运行时 params 常仍为空,须脚本层回退。 */
|
|
|
|
|
+def DEFAULT_PROD_SSH_CREDENTIALS_ID = 'e611a045-2fdc-4613-babd-a72d69bf9814'
|
|
|
|
|
+
|
|
|
|
|
+/** 仅当 PROD_SSH_TARGET 非空时才需要凭据:参数非空用参数,否则用内置 ID;本机发版(无 SSH 目标)返回空串。 */
|
|
|
|
|
+def effectiveProdSshCredentialsId(String prodSshTargetTrimmed) {
|
|
|
|
|
+ def sshT = (prodSshTargetTrimmed ?: '').toString().trim()
|
|
|
|
|
+ if (!sshT) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+ def p = (params.PROD_SSH_CREDENTIALS_ID != null) ? params.PROD_SSH_CREDENTIALS_ID.toString().trim() : ''
|
|
|
|
|
+ if (p) {
|
|
|
|
|
+ return p
|
|
|
|
|
+ }
|
|
|
|
|
+ return DEFAULT_PROD_SSH_CREDENTIALS_ID
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
def filterServices(List services) {
|
|
def filterServices(List services) {
|
|
|
def selected = []
|
|
def selected = []
|
|
|
def mode = params.DEPLOY_MODE
|
|
def mode = params.DEPLOY_MODE
|
|
@@ -304,7 +322,13 @@ pipeline {
|
|
|
name: 'PROD_SSH_TARGET',
|
|
name: 'PROD_SSH_TARGET',
|
|
|
defaultValue: 'alien_store@39.105.153.68',
|
|
defaultValue: 'alien_store@39.105.153.68',
|
|
|
trim: true,
|
|
trim: true,
|
|
|
- description: '生产 Docker 所在主机 SSH 目标(如 alien_store@39.105.153.68)。留空=旧行为:在本机 PROD_DEPLOY_ROOT 写文件并用本机 docker compose。非空时:先写入工作区 .jenkins_remote_prod_staging 再 rsync 到对方 PROD_DEPLOY_ROOT,且 docker 命令经「ssh 目标 sudo docker」执行;须配置免密登录与目标机 sudo 免密 docker'
|
|
|
|
|
|
|
+ description: '生产 Docker 所在主机 SSH 目标(如 alien_store@39.105.153.68)。留空=旧行为:在本机 PROD_DEPLOY_ROOT 写文件并用本机 docker compose。非空时:先写入工作区 .jenkins_remote_prod_staging 再 rsync 到对方 PROD_DEPLOY_ROOT,且 docker 经「ssh 目标 sudo docker」;须目标机 sudo 免密 docker,且 Jenkins 能 SSH 登录(见 PROD_SSH_CREDENTIALS_ID)'
|
|
|
|
|
+ )
|
|
|
|
|
+ string(
|
|
|
|
|
+ name: 'PROD_SSH_CREDENTIALS_ID',
|
|
|
|
|
+ defaultValue: 'e611a045-2fdc-4613-babd-a72d69bf9814',
|
|
|
|
|
+ trim: true,
|
|
|
|
|
+ description: 'Jenkins「SSH Username with private key」凭据 ID(需 SSH Agent 插件);非空时 Verify/rsync/scp/远程 docker 在 sshagent 内执行。默认值为当前环境 alien_store 凭据 ID。注意:已创建的旧 Job 可能仍得到空串,脚本在配置 PROD_SSH_TARGET 时会回退脚本内 DEFAULT_PROD_SSH_CREDENTIALS_ID;换凭据后请改参数或改脚本常量'
|
|
|
)
|
|
)
|
|
|
string(
|
|
string(
|
|
|
name: 'PROD_PAY_CERT_HOST',
|
|
name: 'PROD_PAY_CERT_HOST',
|
|
@@ -354,6 +378,7 @@ pipeline {
|
|
|
stage('Announce deploy plan') {
|
|
stage('Announce deploy plan') {
|
|
|
steps {
|
|
steps {
|
|
|
script {
|
|
script {
|
|
|
|
|
+ echo '>>> [Jenkinsfile] prod-ssh-cred-fallback-20260213:远程时参数 PROD_SSH_CREDENTIALS_ID 为空仍走 sshagent(effectiveProdSshCredentialsId)。若本行缺失,说明本 Job 未执行仓库中当前 docs/jenkins/Jenkinsfile-prod-promote-from-uat.groovy(请检查 Pipeline 来源:SCM 分支/路径或是否仍为「粘贴脚本」旧副本)'
|
|
|
def services = getServiceDefinitions()
|
|
def services = getServiceDefinitions()
|
|
|
def selected = filterServices(services)
|
|
def selected = filterServices(services)
|
|
|
def names = selected.collect { it.prodDir }.join(', ')
|
|
def names = selected.collect { it.prodDir }.join(', ')
|
|
@@ -370,6 +395,13 @@ pipeline {
|
|
|
if (_ssh) {
|
|
if (_ssh) {
|
|
|
echo ">>> PROD_SSH_TARGET=${_ssh}(远程生产:制品 rsync 到对方 PROD_DEPLOY_ROOT,docker compose 在目标机执行)"
|
|
echo ">>> PROD_SSH_TARGET=${_ssh}(远程生产:制品 rsync 到对方 PROD_DEPLOY_ROOT,docker compose 在目标机执行)"
|
|
|
echo ">>> 本地暂存目录(写入后同步): ${env.WORKSPACE}/.jenkins_remote_prod_staging"
|
|
echo ">>> 本地暂存目录(写入后同步): ${env.WORKSPACE}/.jenkins_remote_prod_staging"
|
|
|
|
|
+ def _paramCred = (params.PROD_SSH_CREDENTIALS_ID != null) ? params.PROD_SSH_CREDENTIALS_ID.toString().trim() : ''
|
|
|
|
|
+ def _sc = effectiveProdSshCredentialsId(_ssh)
|
|
|
|
|
+ if (_paramCred) {
|
|
|
|
|
+ echo ">>> PROD_SSH_CREDENTIALS_ID=${_sc}(来自构建参数,SSH Agent)"
|
|
|
|
|
+ } else {
|
|
|
|
|
+ echo ">>> PROD_SSH_CREDENTIALS_ID: 构建参数为空,已用脚本内置回退 ${_sc}(SSH Agent;旧 Job 常不继承 parameters 的 defaultValue)"
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
echo ">>> PROD_SSH_TARGET: (空)本机生产路径 + 本机 Docker"
|
|
echo ">>> PROD_SSH_TARGET: (空)本机生产路径 + 本机 Docker"
|
|
|
}
|
|
}
|
|
@@ -440,22 +472,41 @@ pipeline {
|
|
|
def prodImg = params.PROD_JRE_IMAGE ?: 'my-openjdk8-ffmpeg:v1'
|
|
def prodImg = params.PROD_JRE_IMAGE ?: 'my-openjdk8-ffmpeg:v1'
|
|
|
def prodSshV = (params.PROD_SSH_TARGET ?: '').trim()
|
|
def prodSshV = (params.PROD_SSH_TARGET ?: '').trim()
|
|
|
def prodRemote = (params.PROD_DEPLOY_ROOT ?: env.PROD_DEPLOY_ROOT ?: '/alien_produ/java').trim()
|
|
def prodRemote = (params.PROD_DEPLOY_ROOT ?: env.PROD_DEPLOY_ROOT ?: '/alien_produ/java').trim()
|
|
|
|
|
+ def prodSshCredV = effectiveProdSshCredentialsId(prodSshV)
|
|
|
if (prodSshV) {
|
|
if (prodSshV) {
|
|
|
- sh """
|
|
|
|
|
|
|
+ def verifyRemote = {
|
|
|
|
|
+ def prSq = prodRemote.replace("'", "'\\''")
|
|
|
|
|
+ def imgSq = prodImg.replace("'", "'\\''")
|
|
|
|
|
+ def sshSq = prodSshV.replace("'", "'\\''")
|
|
|
|
|
+ def remoteBody = """set -e
|
|
|
|
|
+rm -f '${prSq}/.jenkins_jar_promote_mount_probe' 2>/dev/null || true
|
|
|
|
|
+printf ok | sudo tee '${prSq}/.jenkins_jar_promote_mount_probe' >/dev/null
|
|
|
|
|
+OUT=\$(sudo docker run --rm --entrypoint cat -v '${prSq}:/__prod:ro' '${imgSq}' '/__prod/.jenkins_jar_promote_mount_probe' 2>/dev/null || true)
|
|
|
|
|
+sudo rm -f '${prSq}/.jenkins_jar_promote_mount_probe' 2>/dev/null || true
|
|
|
|
|
+printf '%s' "\$OUT"
|
|
|
|
|
+"""
|
|
|
|
|
+ def rpath = "${env.WORKSPACE}/.jenkins_remote_mount_verify.sh".replace('\\', '/')
|
|
|
|
|
+ writeFile file: rpath, text: remoteBody, encoding: 'UTF-8'
|
|
|
|
|
+ def rpathSh = rpath.replace("'", "'\\''")
|
|
|
|
|
+ sh """
|
|
|
set -e
|
|
set -e
|
|
|
- SSH='${prodSshV.replace("'", "'\\''")}'
|
|
|
|
|
- PROD='${prodRemote.replace("'", "'\\''")}'
|
|
|
|
|
- IMG='${prodImg.replace("'", "'\\''")}'
|
|
|
|
|
- PFILE=".jenkins_jar_promote_mount_probe"
|
|
|
|
|
- ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "\$SSH" "rm -f \"\$PROD/\$PFILE\" 2>/dev/null; printf ok | sudo tee \"\$PROD/\$PFILE\" >/dev/null"
|
|
|
|
|
- OUT=\$(ssh -o BatchMode=yes "\$SSH" "sudo docker run --rm --entrypoint cat -v \"\$PROD:/__prod:ro\" \"\$IMG\" \"/__prod/\$PFILE\" 2>/dev/null" || true)
|
|
|
|
|
- ssh -o BatchMode=yes "\$SSH" "sudo rm -f \"\$PROD/\$PFILE\" 2>/dev/null || true"
|
|
|
|
|
|
|
+ SSH='${sshSq}'
|
|
|
|
|
+ OUT=\$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "\$SSH" 'bash -s' < '${rpathSh}')
|
|
|
if [ "\$OUT" != "ok" ]; then
|
|
if [ "\$OUT" != "ok" ]; then
|
|
|
- echo "ERROR: 远程 \\\$SSH 上路径 \\\$PROD 无法被该机的 docker 挂载读取(请核对目录权限、docker 与 ssh 用户)。"
|
|
|
|
|
|
|
+ echo "ERROR: 远程 \\\$SSH 上路径 ${prodRemote} 无法被该机的 docker 挂载读取(请核对目录权限、docker、sudo 免密、镜像 ${prodImg})。"
|
|
|
|
|
+ echo "若同一日志中更早出现 Permission denied(publickey):属 SSH 认证失败(凭据私钥须与生产机 alien_store 的 authorized_keys 中公钥为同一对);与 docker 挂载校验无关。"
|
|
|
exit 1
|
|
exit 1
|
|
|
fi
|
|
fi
|
|
|
- echo ">>> 远程 PROD 与 Docker 挂载校验通过: \$SSH:\$PROD"
|
|
|
|
|
|
|
+ echo ">>> 远程 PROD 与 Docker 挂载校验通过: \$SSH:${prodRemote}"
|
|
|
"""
|
|
"""
|
|
|
|
|
+ }
|
|
|
|
|
+ if (prodSshCredV) {
|
|
|
|
|
+ sshagent([prodSshCredV]) {
|
|
|
|
|
+ verifyRemote()
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ verifyRemote()
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
sh """
|
|
sh """
|
|
|
set -e
|
|
set -e
|
|
@@ -522,24 +573,26 @@ pipeline {
|
|
|
def selected = filterServices(services)
|
|
def selected = filterServices(services)
|
|
|
def mode = params.BOOTSTRAP_SYNC_MODE
|
|
def mode = params.BOOTSTRAP_SYNC_MODE
|
|
|
def dryRunSh = dryRunForShell()
|
|
def dryRunSh = dryRunForShell()
|
|
|
|
|
+ def bootSshCred = effectiveProdSshCredentialsId(prodSsh)
|
|
|
|
|
|
|
|
- for (def s in selected) {
|
|
|
|
|
- def jar = "${s.module}-1.0.0.jar"
|
|
|
|
|
- def srcJar = "${env.UAT_DEPLOY_ROOT}/${s.uatDir}/${(jar)}"
|
|
|
|
|
- def dstDir = "${prodLocalRoot}/${s.prodDir}/config"
|
|
|
|
|
- def dstFile = "${dstDir}/bootstrap-prod.yml"
|
|
|
|
|
- def srcGit = "${WORKSPACE}/${s.bootstrapRel}"
|
|
|
|
|
- def dstDirSh = dstDir.replace("'", "'\\''")
|
|
|
|
|
- def rsyncBootstrapBlock = ''
|
|
|
|
|
- if (prodSsh) {
|
|
|
|
|
- rsyncBootstrapBlock = """
|
|
|
|
|
|
|
+ def bootstrapLoop = {
|
|
|
|
|
+ for (def s in selected) {
|
|
|
|
|
+ def jar = "${s.module}-1.0.0.jar"
|
|
|
|
|
+ def srcJar = "${env.UAT_DEPLOY_ROOT}/${s.uatDir}/${(jar)}"
|
|
|
|
|
+ def dstDir = "${prodLocalRoot}/${s.prodDir}/config"
|
|
|
|
|
+ def dstFile = "${dstDir}/bootstrap-prod.yml"
|
|
|
|
|
+ def srcGit = "${WORKSPACE}/${s.bootstrapRel}"
|
|
|
|
|
+ def dstDirSh = dstDir.replace("'", "'\\''")
|
|
|
|
|
+ def rsyncBootstrapBlock = ''
|
|
|
|
|
+ if (prodSsh) {
|
|
|
|
|
+ rsyncBootstrapBlock = """
|
|
|
ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${sshQE}' "mkdir -p '${remQE}/${s.prodDir}/config'"
|
|
ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${sshQE}' "mkdir -p '${remQE}/${s.prodDir}/config'"
|
|
|
rsync -az '${dstDirSh}/' '${sshQE}:${remQE}/${s.prodDir}/config/'
|
|
rsync -az '${dstDirSh}/' '${sshQE}:${remQE}/${s.prodDir}/config/'
|
|
|
"""
|
|
"""
|
|
|
- }
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (mode == 'from_jar') {
|
|
|
|
|
- sh """
|
|
|
|
|
|
|
+ if (mode == 'from_jar') {
|
|
|
|
|
+ sh """
|
|
|
set -e
|
|
set -e
|
|
|
JENKINS_DRY_RUN=${dryRunSh}
|
|
JENKINS_DRY_RUN=${dryRunSh}
|
|
|
echo ">>> [bootstrap from jar] ${s.module}"
|
|
echo ">>> [bootstrap from jar] ${s.module}"
|
|
@@ -571,8 +624,8 @@ pipeline {
|
|
|
${rsyncBootstrapBlock}
|
|
${rsyncBootstrapBlock}
|
|
|
fi
|
|
fi
|
|
|
"""
|
|
"""
|
|
|
- } else {
|
|
|
|
|
- sh """
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sh """
|
|
|
set -e
|
|
set -e
|
|
|
JENKINS_DRY_RUN=${dryRunSh}
|
|
JENKINS_DRY_RUN=${dryRunSh}
|
|
|
echo ">>> [bootstrap from git] ${s.module}"
|
|
echo ">>> [bootstrap from git] ${s.module}"
|
|
@@ -593,7 +646,15 @@ pipeline {
|
|
|
${rsyncBootstrapBlock}
|
|
${rsyncBootstrapBlock}
|
|
|
fi
|
|
fi
|
|
|
"""
|
|
"""
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (prodSsh && bootSshCred) {
|
|
|
|
|
+ sshagent([bootSshCred]) {
|
|
|
|
|
+ bootstrapLoop()
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ bootstrapLoop()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -614,6 +675,7 @@ pipeline {
|
|
|
def prodLocalRoot = useRemoteProd ? "${env.WORKSPACE}/.jenkins_remote_prod_staging".replace('\\', '/') : (env.PROD_DEPLOY_ROOT ?: '/alien_produ/java')
|
|
def prodLocalRoot = useRemoteProd ? "${env.WORKSPACE}/.jenkins_remote_prod_staging".replace('\\', '/') : (env.PROD_DEPLOY_ROOT ?: '/alien_produ/java')
|
|
|
def sshQE = prodSsh.replace("'", "'\\''")
|
|
def sshQE = prodSsh.replace("'", "'\\''")
|
|
|
def remQE = prodRemoteRoot.replace("'", "'\\''")
|
|
def remQE = prodRemoteRoot.replace("'", "'\\''")
|
|
|
|
|
+ def prodSshCred = effectiveProdSshCredentialsId(prodSsh)
|
|
|
|
|
|
|
|
def hostEnvFile = (params.PROD_DOCKER_ENV_FILE ?: '').trim()
|
|
def hostEnvFile = (params.PROD_DOCKER_ENV_FILE ?: '').trim()
|
|
|
def jpwStr = jasyptParamToPlain(params.JASYPT_ENCRYPTOR_PASSWORD)
|
|
def jpwStr = jasyptParamToPlain(params.JASYPT_ENCRYPTOR_PASSWORD)
|
|
@@ -624,28 +686,29 @@ pipeline {
|
|
|
writeFile file: writtenJasyptEnv, text: "JASYPT_ENCRYPTOR_PASSWORD=${(jpwStr)}\n", encoding: 'UTF-8'
|
|
writeFile file: writtenJasyptEnv, text: "JASYPT_ENCRYPTOR_PASSWORD=${(jpwStr)}\n", encoding: 'UTF-8'
|
|
|
effDockerEnvFile = writtenJasyptEnv
|
|
effDockerEnvFile = writtenJasyptEnv
|
|
|
}
|
|
}
|
|
|
- if (useRemoteProd && hostEnvFile && !params.DRY_RUN) {
|
|
|
|
|
- def pulledEnv = "${env.WORKSPACE}/.jenkins_remote_pulled_env_${System.currentTimeMillis()}"
|
|
|
|
|
- def hfEsc = hostEnvFile.replace("'", "'\\''")
|
|
|
|
|
- sh """
|
|
|
|
|
|
|
+ def runPromoteRemoteSteps = {
|
|
|
|
|
+ if (useRemoteProd && hostEnvFile && !params.DRY_RUN) {
|
|
|
|
|
+ def pulledEnv = "${env.WORKSPACE}/.jenkins_remote_pulled_env_${System.currentTimeMillis()}"
|
|
|
|
|
+ def hfEsc = hostEnvFile.replace("'", "'\\''")
|
|
|
|
|
+ sh """
|
|
|
set -e
|
|
set -e
|
|
|
scp -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${sshQE}:${hfEsc}' '${pulledEnv}'
|
|
scp -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${sshQE}:${hfEsc}' '${pulledEnv}'
|
|
|
"""
|
|
"""
|
|
|
- effDockerEnvFile = pulledEnv
|
|
|
|
|
- }
|
|
|
|
|
- def jpfTrim = (params.PROD_JASYPT_PASSWORD_FILE ?: '').trim()
|
|
|
|
|
- def jasyptDefaultPath = useRemoteProd ? "${prodRemoteRoot}/.jasypt-encryptor-password" : "${env.PROD_DEPLOY_ROOT}/.jasypt-encryptor-password"
|
|
|
|
|
- // 须在 for/sh 外定义:Jenkins CPS 对 sh 字符串插值只认 script 块顶层/同层 Binding 中的变量
|
|
|
|
|
- def jasyptHostFile = jpfTrim ? jpfTrim : jasyptDefaultPath
|
|
|
|
|
- def hasJpwPlain = jpwStr.length() > 0 ? '1' : '0'
|
|
|
|
|
- def jasyptExplicit = jpfTrim ? '1' : '0'
|
|
|
|
|
- def prodJreForJasypt = params.PROD_JRE_IMAGE ?: 'my-openjdk8-ffmpeg:v1'
|
|
|
|
|
|
|
+ effDockerEnvFile = pulledEnv
|
|
|
|
|
+ }
|
|
|
|
|
+ def jpfTrim = (params.PROD_JASYPT_PASSWORD_FILE ?: '').trim()
|
|
|
|
|
+ def jasyptDefaultPath = useRemoteProd ? "${prodRemoteRoot}/.jasypt-encryptor-password" : "${env.PROD_DEPLOY_ROOT}/.jasypt-encryptor-password"
|
|
|
|
|
+ // 须在 for/sh 外定义:Jenkins CPS 对 sh 字符串插值只认 script 块顶层/同层 Binding 中的变量
|
|
|
|
|
+ def jasyptHostFile = jpfTrim ? jpfTrim : jasyptDefaultPath
|
|
|
|
|
+ def hasJpwPlain = jpwStr.length() > 0 ? '1' : '0'
|
|
|
|
|
+ def jasyptExplicit = jpfTrim ? '1' : '0'
|
|
|
|
|
+ def prodJreForJasypt = params.PROD_JRE_IMAGE ?: 'my-openjdk8-ffmpeg:v1'
|
|
|
|
|
|
|
|
- if (!params.DRY_RUN) {
|
|
|
|
|
- def effEsc = (effDockerEnvFile ?: '').replace("'", "'\\''")
|
|
|
|
|
- def jhfEsc = jasyptHostFile.toString().replace("'", "'\\''")
|
|
|
|
|
- if (useRemoteProd) {
|
|
|
|
|
- sh """
|
|
|
|
|
|
|
+ if (!params.DRY_RUN) {
|
|
|
|
|
+ def effEsc = (effDockerEnvFile ?: '').replace("'", "'\\''")
|
|
|
|
|
+ def jhfEsc = jasyptHostFile.toString().replace("'", "'\\''")
|
|
|
|
|
+ if (useRemoteProd) {
|
|
|
|
|
+ sh """
|
|
|
set +e
|
|
set +e
|
|
|
JENKINS_HAS_JPW=${hasJpwPlain}
|
|
JENKINS_HAS_JPW=${hasJpwPlain}
|
|
|
OK=0
|
|
OK=0
|
|
@@ -667,8 +730,8 @@ pipeline {
|
|
|
echo ">>> Jasypt 来源校验通过(远程)"
|
|
echo ">>> Jasypt 来源校验通过(远程)"
|
|
|
exit 0
|
|
exit 0
|
|
|
"""
|
|
"""
|
|
|
- } else {
|
|
|
|
|
- sh """
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sh """
|
|
|
set +e
|
|
set +e
|
|
|
JENKINS_HAS_JPW=${hasJpwPlain}
|
|
JENKINS_HAS_JPW=${hasJpwPlain}
|
|
|
OK=0
|
|
OK=0
|
|
@@ -706,178 +769,178 @@ pipeline {
|
|
|
echo ">>> Jasypt 来源校验通过(env-file / 构建密码 / 密钥文件至少其一有效)"
|
|
echo ">>> Jasypt 来源校验通过(env-file / 构建密码 / 密钥文件至少其一有效)"
|
|
|
exit 0
|
|
exit 0
|
|
|
"""
|
|
"""
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- def composeFileForJob = (params.PROD_COMPOSE_FILE ?: '').trim() ?: "${prodRemoteRoot}/docker-compose.yml"
|
|
|
|
|
- def jicForJob = (params.PROD_JAR_IN_CONTAINER ?: 'app.jar').trim() ?: 'app.jar'
|
|
|
|
|
- def effEnvPathForSh = (effDockerEnvFile ?: '')
|
|
|
|
|
- def dcpExplicitPath = (params.PROD_DOCKER_COMPOSE_PATH ?: '').trim()
|
|
|
|
|
- def dcpShellQuoted = dcpExplicitPath.replace("'", "'\\''")
|
|
|
|
|
- def dcpV2PluginPath = (params.PROD_DOCKER_COMPOSE_V2_PLUGIN ?: '').trim()
|
|
|
|
|
- def dcpV2ShellQuoted = dcpV2PluginPath.replace("'", "'\\''")
|
|
|
|
|
- def dcpDindT = (params.PROD_DOCKER_COMPOSE_DIND_IMAGE != null) ? (params.PROD_DOCKER_COMPOSE_DIND_IMAGE ?: 'docker:24.0.9-cli').toString().trim() : 'docker:24.0.9-cli'
|
|
|
|
|
- def dcpDindParam = dcpDindT ? dcpDindT : 'docker:24.0.9-cli'
|
|
|
|
|
- def dcpDindOff = dcpDindParam.equalsIgnoreCase('off') || dcpDindParam == '0'
|
|
|
|
|
- def dcpDindSh = dcpDindOff ? '' : dcpDindParam.replace("'", "'\\''")
|
|
|
|
|
- if (useRemoteProd) {
|
|
|
|
|
- dcpDindSh = ''
|
|
|
|
|
- }
|
|
|
|
|
- def extNRaw = params.PROD_DOCKER_CREATE_EXTERNAL_NETWORKS
|
|
|
|
|
- def extNOff = extNRaw != null && (extNRaw.toString().trim().equalsIgnoreCase('off') || extNRaw.toString().trim() == '0' || extNRaw.toString().trim().equalsIgnoreCase('none'))
|
|
|
|
|
- def extNList = extNOff ? '' : ((extNRaw == null || extNRaw.toString().trim() == '') ? 'common-network-produ' : extNRaw.toString().trim())
|
|
|
|
|
- def extNetsSh = extNOff ? '' : extNList.replace("'", "'\\''")
|
|
|
|
|
|
|
|
|
|
- for (def s in selected) {
|
|
|
|
|
- def jar = "${s.module}-1.0.0.jar"
|
|
|
|
|
- def srcJar = "${env.UAT_DEPLOY_ROOT}/${s.uatDir}/${(jar)}"
|
|
|
|
|
- def dstDir = "${prodLocalRoot}/${s.prodDir}"
|
|
|
|
|
- def logHostDir = useRemoteProd ? "${prodRemoteRoot}/logs/${s.prodDir}" : "${env.PROD_DEPLOY_ROOT}/logs/${s.prodDir}"
|
|
|
|
|
- def srcLib = "${env.UAT_DEPLOY_ROOT}/${s.uatDir}/lib"
|
|
|
|
|
- def dstLib = "${dstDir}/lib"
|
|
|
|
|
- def cPrimary = s.container
|
|
|
|
|
- def cFallback = s.prodDir
|
|
|
|
|
- def composeSvc = s.composeService
|
|
|
|
|
- def jarHostPath = "${dstDir}/${(jar)}"
|
|
|
|
|
- def jarOnProdServer = "${prodRemoteRoot}/${s.prodDir}/${(jar)}"
|
|
|
|
|
- def jarOnProdServerSh = jarOnProdServer.replace("'", "'\\''")
|
|
|
|
|
- def jarHostPathSh = jarHostPath.replace("'", "'\\''")
|
|
|
|
|
- def srcLibSh = srcLib.toString().replace("'", "'\\''")
|
|
|
|
|
- def dstLibSh = dstLib.toString().replace("'", "'\\''")
|
|
|
|
|
- def jarInContainerPath = "/app/${(jicForJob)}"
|
|
|
|
|
- def jarInContainerAlt = "/app/${(jar)}"
|
|
|
|
|
- def jcpPrimaryEscaped = jarInContainerPath.replace("'", "'\\''")
|
|
|
|
|
- def jcpAltEscaped = (jarInContainerPath == jarInContainerAlt) ? '' : jarInContainerAlt.replace("'", "'\\''")
|
|
|
|
|
- def jreImage = params.PROD_JRE_IMAGE ?: 'my-openjdk8-ffmpeg:v1'
|
|
|
|
|
- def jreImageSh = jreImage.toString().replace("'", "'\\''")
|
|
|
|
|
- def autoCreate = params.AUTO_CREATE_CONTAINER_IF_MISSING ? 'true' : 'false'
|
|
|
|
|
- def autoRecreate = params.AUTO_RECREATE_IF_MOUNT_MISMATCH ? 'true' : 'false'
|
|
|
|
|
- def removeStaleJarDir = params.REMOVE_STALE_JAR_DIR_IF_DIRECTORY ? 'true' : 'false'
|
|
|
|
|
- def payCertHost = (params.PROD_PAY_CERT_HOST ?: '').trim()
|
|
|
|
|
- def payCertProdDirs = ['store', 'store-platform', 'lawyer', 'dining'] as Set
|
|
|
|
|
- def payCertSvc = payCertProdDirs.contains(s.prodDir) ? '1' : '0'
|
|
|
|
|
- def dstPayCertDirInspect = useRemoteProd ? "${prodRemoteRoot}/${s.prodDir}/alien/aliPayCert" : "${dstDir}/alien/aliPayCert"
|
|
|
|
|
- // payCertHost 不得写进 promoteShell 的 GString(含 PAY_MOUNT="..."),CPS 会报 illegal $;内层 if/elif 整段在 Groovy 中拼接
|
|
|
|
|
- def payCertHostShellEsc = (payCertHost ?: '').toString().replace('"', '\\"')
|
|
|
|
|
- def payCertPayMountInner
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ def composeFileForJob = (params.PROD_COMPOSE_FILE ?: '').trim() ?: "${prodRemoteRoot}/docker-compose.yml"
|
|
|
|
|
+ def jicForJob = (params.PROD_JAR_IN_CONTAINER ?: 'app.jar').trim() ?: 'app.jar'
|
|
|
|
|
+ def effEnvPathForSh = (effDockerEnvFile ?: '')
|
|
|
|
|
+ def dcpExplicitPath = (params.PROD_DOCKER_COMPOSE_PATH ?: '').trim()
|
|
|
|
|
+ def dcpShellQuoted = dcpExplicitPath.replace("'", "'\\''")
|
|
|
|
|
+ def dcpV2PluginPath = (params.PROD_DOCKER_COMPOSE_V2_PLUGIN ?: '').trim()
|
|
|
|
|
+ def dcpV2ShellQuoted = dcpV2PluginPath.replace("'", "'\\''")
|
|
|
|
|
+ def dcpDindT = (params.PROD_DOCKER_COMPOSE_DIND_IMAGE != null) ? (params.PROD_DOCKER_COMPOSE_DIND_IMAGE ?: 'docker:24.0.9-cli').toString().trim() : 'docker:24.0.9-cli'
|
|
|
|
|
+ def dcpDindParam = dcpDindT ? dcpDindT : 'docker:24.0.9-cli'
|
|
|
|
|
+ def dcpDindOff = dcpDindParam.equalsIgnoreCase('off') || dcpDindParam == '0'
|
|
|
|
|
+ def dcpDindSh = dcpDindOff ? '' : dcpDindParam.replace("'", "'\\''")
|
|
|
if (useRemoteProd) {
|
|
if (useRemoteProd) {
|
|
|
- payCertPayMountInner = " if [ -n \"" + payCertHostShellEsc + "\" ]; then\n" +
|
|
|
|
|
- " PAY_MOUNT=\"" + payCertHostShellEsc + "\"\n" +
|
|
|
|
|
- ' elif [ -n "$JENKINS_PROD_SSH" ] && ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "test -d \"$JENKINS_PAY_CERT_DIR\""; then\n' +
|
|
|
|
|
- ' PAY_MOUNT="$JENKINS_PAY_CERT_DIR"\n' +
|
|
|
|
|
- ' elif [ -n "$JENKINS_PROD_SSH" ] && ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "test -f \"$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/alien/apiclient_key.pem\""; then\n' +
|
|
|
|
|
- ' PAY_MOUNT="$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/alien"\n' +
|
|
|
|
|
- ' echo ">>> WARN: 证书在远端 $PROMOTE_PROD_DIR/alien/ 根目录,将该目录挂为容器内 /usr/local/alien/aliPayCert"\n' +
|
|
|
|
|
- ' fi\n'
|
|
|
|
|
- } else {
|
|
|
|
|
- payCertPayMountInner = " if [ -n \"" + payCertHostShellEsc + "\" ]; then\n" +
|
|
|
|
|
- " PAY_MOUNT=\"" + payCertHostShellEsc + "\"\n" +
|
|
|
|
|
- ' elif [ -d "$JENKINS_PAY_CERT_DIR" ] && [ -n "$(ls -A "$JENKINS_PAY_CERT_DIR" 2>/dev/null)" ]; then\n' +
|
|
|
|
|
- ' PAY_MOUNT="$JENKINS_PAY_CERT_DIR"\n' +
|
|
|
|
|
- ' elif [ -f "$PROMOTE_DST_DIR/alien/apiclient_key.pem" ]; then\n' +
|
|
|
|
|
- ' PAY_MOUNT="$PROMOTE_DST_DIR/alien"\n' +
|
|
|
|
|
- ' echo ">>> WARN: 证书在 $PROMOTE_DST_DIR/alien/ 根目录(无 alien/aliPayCert 子目录),将该目录挂为容器内 /usr/local/alien/aliPayCert"\n' +
|
|
|
|
|
- ' fi\n'
|
|
|
|
|
|
|
+ dcpDindSh = ''
|
|
|
}
|
|
}
|
|
|
- def dryRunPayCertEcho = ' echo "[DRY_RUN] 证书卷:优先 PROD_PAY_CERT_HOST=' + (payCertHost ? payCertHost : '(未填)') + ',否则若目录存在非空则 $JENKINS_PAY_CERT_DIR 到 /usr/local/alien/aliPayCert:ro"'
|
|
|
|
|
- // jasyptHostFile / effDockerEnvFile 在 promoteShell 的 GString 中接中文全角「(」等时 CPS 报 illegal $
|
|
|
|
|
- def jhPath = jasyptHostFile.toString()
|
|
|
|
|
- def jhPathSh = jhPath.replace("'", "'\\''")
|
|
|
|
|
- def jhDq = jhPath.replace('\\', '\\\\').replace('"', '\\"')
|
|
|
|
|
- def effPath = (effDockerEnvFile ?: '').toString()
|
|
|
|
|
- def effPathN = effPath.replace('"', '\\"')
|
|
|
|
|
- def dryRunEffIfFi = " if [ -n \"" + effPathN + "\" ]; then\n" +
|
|
|
|
|
- " echo \"[DRY_RUN] 将在 compose 前对 shell set -a source: " + effPath + "(yml 中 \${JASYPT_*} 等插值用)\"\n" +
|
|
|
|
|
- " fi\n"
|
|
|
|
|
- def jasyptDryHfDBlock = " JHF_D='" + jhPathSh + "'\n" +
|
|
|
|
|
- " if [ -f \"\$JHF_D\" ]; then\n" +
|
|
|
|
|
- " echo \"[DRY_RUN] 将生成临时 Jasypt env 并在 compose 前 source: " + jhDq + "\"\n" +
|
|
|
|
|
- " elif command -v docker >/dev/null 2>&1; then\n" +
|
|
|
|
|
- " _JD=\$(dirname \"\$JHF_D\")\n" +
|
|
|
|
|
- " _JB=\$(basename \"\$JHF_D\")\n" +
|
|
|
|
|
- " if docker run --rm --entrypoint sh -v \"\$_JD:/__jd:ro\" \"\$JRE_IMG\" -c \"test -f /__jd/\$_JB\" 2>/dev/null; then\n" +
|
|
|
|
|
- " echo \"[DRY_RUN] 密钥文件仅 Docker 宿主机可见: " + jhDq + "\"\n" +
|
|
|
|
|
- " fi\n" +
|
|
|
|
|
- " fi\n"
|
|
|
|
|
- def envFileOptIfElifFi = " if [ -n \"" + effPathN + "\" ] && [ -f \"" + effPathN + "\" ]; then\n" +
|
|
|
|
|
- " ENV_FILE_OPT=\"--env-file " + effPath + "\"\n" +
|
|
|
|
|
- " elif [ -n \"" + effPathN + "\" ]; then\n" +
|
|
|
|
|
- " echo \">>> WARN: docker --env-file 路径不可读或不存在: " + effPath + "(Jasypt 等可能无法注入;检查 Jenkins/宿主机挂载或改用构建密码参数)\"\n" +
|
|
|
|
|
- " fi\n"
|
|
|
|
|
- def jasyptLineJhf = " JHF='" + jhPathSh + "'"
|
|
|
|
|
- def jasyptWarnExplJpfUnread = ' echo ">>> WARN: 显式 PROD_JASYPT_PASSWORD_FILE 不可读: ' + jhDq + '(已依赖其它来源通过晋升前校验)"'
|
|
|
|
|
- // 避免 promoteShell 中 sh "\$JCPR" 触发 CPS 对 \$\J 的误解析,用 (char)36 生成字面量 $
|
|
|
|
|
- def dlr = (char) 36
|
|
|
|
|
- def lineDockerExecOdJcpr = " if docker exec \"${dlr}CNAME\" sh -c 'od -An -N2 -tx1 \"${dlr}1\"' sh \"${dlr}JCPR\" 2>/dev/null | grep -qE '50[[:space:]]+4b'; then"
|
|
|
|
|
|
|
+ def extNRaw = params.PROD_DOCKER_CREATE_EXTERNAL_NETWORKS
|
|
|
|
|
+ def extNOff = extNRaw != null && (extNRaw.toString().trim().equalsIgnoreCase('off') || extNRaw.toString().trim() == '0' || extNRaw.toString().trim().equalsIgnoreCase('none'))
|
|
|
|
|
+ def extNList = extNOff ? '' : ((extNRaw == null || extNRaw.toString().trim() == '') ? 'common-network-produ' : extNRaw.toString().trim())
|
|
|
|
|
+ def extNetsSh = extNOff ? '' : extNList.replace("'", "'\\''")
|
|
|
|
|
|
|
|
- def jenkinsHomeSh = (env.JENKINS_HOME ?: '/var/jenkins_home').replace("'", "'\\''")
|
|
|
|
|
- def promoteProdDirSh = s.prodDir.replace("'", "'\\''")
|
|
|
|
|
- def logHostDirSh = logHostDir.replace("'", "'\\''")
|
|
|
|
|
- def jarBasenameSh = jar.replace("'", "'\\''")
|
|
|
|
|
- def cPrimarySh = cPrimary.toString().replace("'", "'\\''")
|
|
|
|
|
- def cFallbackSh = cFallback.toString().replace("'", "'\\''")
|
|
|
|
|
- def dstPayCertDirSh = dstPayCertDirInspect.replace("'", "'\\''")
|
|
|
|
|
- def dstDirSh = dstDir.replace("'", "'\\''")
|
|
|
|
|
- def uatDeployRoot = env.UAT_DEPLOY_ROOT ?: ''
|
|
|
|
|
- def uatDeployRootSh = uatDeployRoot.toString().replace("'", "'\\''")
|
|
|
|
|
- def uatUatServiceDirSh = "${(uatDeployRoot)}/${(s.uatDir)}".toString().replace("'", "'\\''")
|
|
|
|
|
- def uatDiagLsSvc = "ls -la '" + uatUatServiceDirSh + "' 2>&1"
|
|
|
|
|
- def uatDiagLsRoot = "ls -la '" + uatDeployRootSh + "' 2>&1"
|
|
|
|
|
- def missingJarDiag = " echo \"诊断 UAT_DEPLOY_ROOT: " + uatDeployRoot + ":\"\n " + uatDiagLsSvc + "\n " + uatDiagLsRoot
|
|
|
|
|
- def srcJarSh = srcJar.toString().replace("'", "'\\''")
|
|
|
|
|
- def ifNoUatJar = " if [ ! -f '" + srcJarSh + "' ]; then"
|
|
|
|
|
- def errEchoUatMissing = " echo \"ERROR: 当前 Jenkins 进程内找不到 UAT 制品: " + srcJar + "\""
|
|
|
|
|
- // 须输出 shell 的 $PROMOTE_DST_DIR;Groovy 中勿用 '$$'(在 bash 里会展开为 PID+字面量,如 17253PROMOTE)
|
|
|
|
|
- def dryRunCpEcho = " echo \"[DRY_RUN] cp -f " + srcJar + " \$PROMOTE_DST_DIR/\""
|
|
|
|
|
- def cpSrcToDst = " cp -f '" + srcJarSh + "' " + "\$" + "PROMOTE_DST_DIR/"
|
|
|
|
|
- def echoJarLine = ' echo ">>> [jar] ' + s.module + ': ' + s.uatDir + ' -> ' + s.prodDir + '(compose 服务名: $COMPOSE_SVC, 容器内 jar: $JCP)"'
|
|
|
|
|
- def dryRunLogDirEcho = useRemoteProd ?
|
|
|
|
|
- ' echo "[DRY_RUN] mkdir -p 本地: $(dirname $PROMOTE_DST_DIR)/logs/$PROMOTE_PROD_DIR -> rsync -> 生产: ' + logHostDir + '"' :
|
|
|
|
|
- " echo \"[DRY_RUN] mkdir -p " + logHostDir + "(日志目录,与 compose ./logs/" + s.prodDir + ":/app/logs 一致)\""
|
|
|
|
|
- def jasyptExplLine = " JENKINS_JASYPT_EXPL=" + jasyptExplicit
|
|
|
|
|
- // 无引号 JENKINS_*= 赋值不得写进 promoteShell 的 GString,否则如 ${autoCreate} 会触发非法 $ 转义
|
|
|
|
|
- def jenkinsEnvBools = " JENKINS_DRY_RUN=" + dryRunSh + "\n" +
|
|
|
|
|
- " JENKINS_PAY_CERT_SVC=" + payCertSvc + "\n" +
|
|
|
|
|
- " JENKINS_STALE_JAR_RM=" + removeStaleJarDir + "\n" +
|
|
|
|
|
- " JENKINS_AUTO_RECREATE=" + autoRecreate + "\n" +
|
|
|
|
|
- " JENKINS_AUTO_CREATE=" + autoCreate
|
|
|
|
|
- // DCP_*='...' 含 dcpShellQuoted 类变量名时,CPS 对大 GString 仍报 “illegal $”;整段在 Groovy 中拼接
|
|
|
|
|
- def dcpThreeLines = " DCP_EXPLICIT='" + dcpShellQuoted + "'\n" +
|
|
|
|
|
- " DCP_V2_PLUGIN='" + dcpV2ShellQuoted + "'\n" +
|
|
|
|
|
- " DCP_DIND_IMAGE='" + dcpDindSh + "'"
|
|
|
|
|
- def dcpExtNetsLine = " DCP_EXT_NETS='" + extNetsSh + "'"
|
|
|
|
|
- def jarOnProdExportSh = useRemoteProd ? jarOnProdServerSh : ''
|
|
|
|
|
- def sshExport = useRemoteProd ? sshQE : ''
|
|
|
|
|
- def remExport = useRemoteProd ? remQE : ''
|
|
|
|
|
- // export 与环境变量行整段用 + 拼接;大 GString 中 ANY 'VAR=''${...}'' 均可能被 CPS 报 “illegal $”(与 eff* / ext* 等标识符相关)
|
|
|
|
|
- def promoteExportBlock = " export JENKINS_HOME='" + jenkinsHomeSh + "'\n" +
|
|
|
|
|
- " COMPOSE_FILE='" + composeFileForJob + "'\n" +
|
|
|
|
|
- " COMPOSE_SVC='" + composeSvc + "'\n" +
|
|
|
|
|
- " PROMOTE_PROD_DIR='" + promoteProdDirSh + "'\n" +
|
|
|
|
|
- " PROMOTE_DST_DIR='" + dstDirSh + "'\n" +
|
|
|
|
|
- " JAR_HOST='" + jarHostPathSh + "'\n" +
|
|
|
|
|
- " JAR_ON_PROD_SERVER='" + jarOnProdExportSh + "'\n" +
|
|
|
|
|
- " JENKINS_PROD_SSH='" + sshExport + "'\n" +
|
|
|
|
|
- " JENKINS_REMOTE_PROD_ROOT='" + remExport + "'\n" +
|
|
|
|
|
- " PROMOTE_SRC_LIB='" + srcLibSh + "'\n" +
|
|
|
|
|
- " PROMOTE_DST_LIB='" + dstLibSh + "'\n" +
|
|
|
|
|
- " JAR_BASENAME='" + jarBasenameSh + "'\n" +
|
|
|
|
|
- " C_PRIMARY='" + cPrimarySh + "'\n" +
|
|
|
|
|
- " C_FALLBACK='" + cFallbackSh + "'\n" +
|
|
|
|
|
- " JENKINS_LOG_DIR='" + logHostDirSh + "'\n" +
|
|
|
|
|
- " JENKINS_PAY_CERT_DIR='" + dstPayCertDirSh + "'\n" +
|
|
|
|
|
- " JRE_IMG='" + jreImageSh + "'\n" +
|
|
|
|
|
- " JCP='" + jcpPrimaryEscaped + "'\n" +
|
|
|
|
|
- " JCP_ALT='" + jcpAltEscaped + "'\n" +
|
|
|
|
|
- " JENKINS_PROD_ENV_FILE='" + effEnvPathForSh + "'\n"
|
|
|
|
|
|
|
+ for (def s in selected) {
|
|
|
|
|
+ def jar = "${s.module}-1.0.0.jar"
|
|
|
|
|
+ def srcJar = "${env.UAT_DEPLOY_ROOT}/${s.uatDir}/${(jar)}"
|
|
|
|
|
+ def dstDir = "${prodLocalRoot}/${s.prodDir}"
|
|
|
|
|
+ def logHostDir = useRemoteProd ? "${prodRemoteRoot}/logs/${s.prodDir}" : "${env.PROD_DEPLOY_ROOT}/logs/${s.prodDir}"
|
|
|
|
|
+ def srcLib = "${env.UAT_DEPLOY_ROOT}/${s.uatDir}/lib"
|
|
|
|
|
+ def dstLib = "${dstDir}/lib"
|
|
|
|
|
+ def cPrimary = s.container
|
|
|
|
|
+ def cFallback = s.prodDir
|
|
|
|
|
+ def composeSvc = s.composeService
|
|
|
|
|
+ def jarHostPath = "${dstDir}/${(jar)}"
|
|
|
|
|
+ def jarOnProdServer = "${prodRemoteRoot}/${s.prodDir}/${(jar)}"
|
|
|
|
|
+ def jarOnProdServerSh = jarOnProdServer.replace("'", "'\\''")
|
|
|
|
|
+ def jarHostPathSh = jarHostPath.replace("'", "'\\''")
|
|
|
|
|
+ def srcLibSh = srcLib.toString().replace("'", "'\\''")
|
|
|
|
|
+ def dstLibSh = dstLib.toString().replace("'", "'\\''")
|
|
|
|
|
+ def jarInContainerPath = "/app/${(jicForJob)}"
|
|
|
|
|
+ def jarInContainerAlt = "/app/${(jar)}"
|
|
|
|
|
+ def jcpPrimaryEscaped = jarInContainerPath.replace("'", "'\\''")
|
|
|
|
|
+ def jcpAltEscaped = (jarInContainerPath == jarInContainerAlt) ? '' : jarInContainerAlt.replace("'", "'\\''")
|
|
|
|
|
+ def jreImage = params.PROD_JRE_IMAGE ?: 'my-openjdk8-ffmpeg:v1'
|
|
|
|
|
+ def jreImageSh = jreImage.toString().replace("'", "'\\''")
|
|
|
|
|
+ def autoCreate = params.AUTO_CREATE_CONTAINER_IF_MISSING ? 'true' : 'false'
|
|
|
|
|
+ def autoRecreate = params.AUTO_RECREATE_IF_MOUNT_MISMATCH ? 'true' : 'false'
|
|
|
|
|
+ def removeStaleJarDir = params.REMOVE_STALE_JAR_DIR_IF_DIRECTORY ? 'true' : 'false'
|
|
|
|
|
+ def payCertHost = (params.PROD_PAY_CERT_HOST ?: '').trim()
|
|
|
|
|
+ def payCertProdDirs = ['store', 'store-platform', 'lawyer', 'dining'] as Set
|
|
|
|
|
+ def payCertSvc = payCertProdDirs.contains(s.prodDir) ? '1' : '0'
|
|
|
|
|
+ def dstPayCertDirInspect = useRemoteProd ? "${prodRemoteRoot}/${s.prodDir}/alien/aliPayCert" : "${dstDir}/alien/aliPayCert"
|
|
|
|
|
+ // payCertHost 不得写进 promoteShell 的 GString(含 PAY_MOUNT="..."),CPS 会报 illegal $;内层 if/elif 整段在 Groovy 中拼接
|
|
|
|
|
+ def payCertHostShellEsc = (payCertHost ?: '').toString().replace('"', '\\"')
|
|
|
|
|
+ def payCertPayMountInner
|
|
|
|
|
+ if (useRemoteProd) {
|
|
|
|
|
+ payCertPayMountInner = " if [ -n \"" + payCertHostShellEsc + "\" ]; then\n" +
|
|
|
|
|
+ " PAY_MOUNT=\"" + payCertHostShellEsc + "\"\n" +
|
|
|
|
|
+ ' elif [ -n "$JENKINS_PROD_SSH" ] && ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "test -d \"$JENKINS_PAY_CERT_DIR\""; then\n' +
|
|
|
|
|
+ ' PAY_MOUNT="$JENKINS_PAY_CERT_DIR"\n' +
|
|
|
|
|
+ ' elif [ -n "$JENKINS_PROD_SSH" ] && ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "test -f \"$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/alien/apiclient_key.pem\""; then\n' +
|
|
|
|
|
+ ' PAY_MOUNT="$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/alien"\n' +
|
|
|
|
|
+ ' echo ">>> WARN: 证书在远端 $PROMOTE_PROD_DIR/alien/ 根目录,将该目录挂为容器内 /usr/local/alien/aliPayCert"\n' +
|
|
|
|
|
+ ' fi\n'
|
|
|
|
|
+ } else {
|
|
|
|
|
+ payCertPayMountInner = " if [ -n \"" + payCertHostShellEsc + "\" ]; then\n" +
|
|
|
|
|
+ " PAY_MOUNT=\"" + payCertHostShellEsc + "\"\n" +
|
|
|
|
|
+ ' elif [ -d "$JENKINS_PAY_CERT_DIR" ] && [ -n "$(ls -A "$JENKINS_PAY_CERT_DIR" 2>/dev/null)" ]; then\n' +
|
|
|
|
|
+ ' PAY_MOUNT="$JENKINS_PAY_CERT_DIR"\n' +
|
|
|
|
|
+ ' elif [ -f "$PROMOTE_DST_DIR/alien/apiclient_key.pem" ]; then\n' +
|
|
|
|
|
+ ' PAY_MOUNT="$PROMOTE_DST_DIR/alien"\n' +
|
|
|
|
|
+ ' echo ">>> WARN: 证书在 $PROMOTE_DST_DIR/alien/ 根目录(无 alien/aliPayCert 子目录),将该目录挂为容器内 /usr/local/alien/aliPayCert"\n' +
|
|
|
|
|
+ ' fi\n'
|
|
|
|
|
+ }
|
|
|
|
|
+ def dryRunPayCertEcho = ' echo "[DRY_RUN] 证书卷:优先 PROD_PAY_CERT_HOST=' + (payCertHost ? payCertHost : '(未填)') + ',否则若目录存在非空则 $JENKINS_PAY_CERT_DIR 到 /usr/local/alien/aliPayCert:ro"'
|
|
|
|
|
+ // jasyptHostFile / effDockerEnvFile 在 promoteShell 的 GString 中接中文全角「(」等时 CPS 报 illegal $
|
|
|
|
|
+ def jhPath = jasyptHostFile.toString()
|
|
|
|
|
+ def jhPathSh = jhPath.replace("'", "'\\''")
|
|
|
|
|
+ def jhDq = jhPath.replace('\\', '\\\\').replace('"', '\\"')
|
|
|
|
|
+ def effPath = (effDockerEnvFile ?: '').toString()
|
|
|
|
|
+ def effPathN = effPath.replace('"', '\\"')
|
|
|
|
|
+ def dryRunEffIfFi = " if [ -n \"" + effPathN + "\" ]; then\n" +
|
|
|
|
|
+ " echo \"[DRY_RUN] 将在 compose 前对 shell set -a source: " + effPath + "(yml 中 \${JASYPT_*} 等插值用)\"\n" +
|
|
|
|
|
+ " fi\n"
|
|
|
|
|
+ def jasyptDryHfDBlock = " JHF_D='" + jhPathSh + "'\n" +
|
|
|
|
|
+ " if [ -f \"\$JHF_D\" ]; then\n" +
|
|
|
|
|
+ " echo \"[DRY_RUN] 将生成临时 Jasypt env 并在 compose 前 source: " + jhDq + "\"\n" +
|
|
|
|
|
+ " elif command -v docker >/dev/null 2>&1; then\n" +
|
|
|
|
|
+ " _JD=\$(dirname \"\$JHF_D\")\n" +
|
|
|
|
|
+ " _JB=\$(basename \"\$JHF_D\")\n" +
|
|
|
|
|
+ " if docker run --rm --entrypoint sh -v \"\$_JD:/__jd:ro\" \"\$JRE_IMG\" -c \"test -f /__jd/\$_JB\" 2>/dev/null; then\n" +
|
|
|
|
|
+ " echo \"[DRY_RUN] 密钥文件仅 Docker 宿主机可见: " + jhDq + "\"\n" +
|
|
|
|
|
+ " fi\n" +
|
|
|
|
|
+ " fi\n"
|
|
|
|
|
+ def envFileOptIfElifFi = " if [ -n \"" + effPathN + "\" ] && [ -f \"" + effPathN + "\" ]; then\n" +
|
|
|
|
|
+ " ENV_FILE_OPT=\"--env-file " + effPath + "\"\n" +
|
|
|
|
|
+ " elif [ -n \"" + effPathN + "\" ]; then\n" +
|
|
|
|
|
+ " echo \">>> WARN: docker --env-file 路径不可读或不存在: " + effPath + "(Jasypt 等可能无法注入;检查 Jenkins/宿主机挂载或改用构建密码参数)\"\n" +
|
|
|
|
|
+ " fi\n"
|
|
|
|
|
+ def jasyptLineJhf = " JHF='" + jhPathSh + "'"
|
|
|
|
|
+ def jasyptWarnExplJpfUnread = ' echo ">>> WARN: 显式 PROD_JASYPT_PASSWORD_FILE 不可读: ' + jhDq + '(已依赖其它来源通过晋升前校验)"'
|
|
|
|
|
+ // 避免 promoteShell 中 sh "\$JCPR" 触发 CPS 对 \$\J 的误解析,用 (char)36 生成字面量 $
|
|
|
|
|
+ def dlr = (char) 36
|
|
|
|
|
+ def lineDockerExecOdJcpr = " if docker exec \"${dlr}CNAME\" sh -c 'od -An -N2 -tx1 \"${dlr}1\"' sh \"${dlr}JCPR\" 2>/dev/null | grep -qE '50[[:space:]]+4b'; then"
|
|
|
|
|
|
|
|
- def remoteDockerShimBlock = useRemoteProd ? '\n if [ -n "$JENKINS_PROD_SSH" ]; then\n docker() { ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" sudo docker "$@"; }\n fi\n' : '\n'
|
|
|
|
|
- def rsyncRemoteBlock = useRemoteProd ? '\n if [ -n "$JENKINS_PROD_SSH" ]; then\n echo ">>> rsync 制品到 $JENKINS_PROD_SSH:$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/"\n ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "sudo mkdir -p $JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR $JENKINS_REMOTE_PROD_ROOT/logs/$PROMOTE_PROD_DIR"\n rsync -az "$PROMOTE_DST_DIR/" "$JENKINS_PROD_SSH:$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/"\n rsync -az "$(dirname "$PROMOTE_DST_DIR")/logs/$PROMOTE_PROD_DIR/" "$JENKINS_PROD_SSH:$JENKINS_REMOTE_PROD_ROOT/logs/$PROMOTE_PROD_DIR/" 2>/dev/null || true\n fi\n' : '\n'
|
|
|
|
|
- def composeExistsBlockSh = useRemoteProd ? '\n if ! ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "test -f $COMPOSE_FILE"; then\n echo "ERROR: 远程未找到 docker compose 文件: $COMPOSE_FILE"\n exit 1\n fi\n' : '\n if [ ! -f "$COMPOSE_FILE" ]; then\n echo "ERROR: 未找到 docker compose 文件: $COMPOSE_FILE。请将 PROD_COMPOSE_FILE/PROD_DEPLOY_ROOT 与现网 yml 对齐,例如 /alien_produ/java/docker-compose.yml"\n exit 1\n fi\n'
|
|
|
|
|
- def composeDirBlockSh = useRemoteProd ? ' COMPOSE_DIR=$(dirname "$COMPOSE_FILE")' : ' COMPOSE_DIR=$(cd "$(dirname "$COMPOSE_FILE")" && pwd)'
|
|
|
|
|
|
|
+ def jenkinsHomeSh = (env.JENKINS_HOME ?: '/var/jenkins_home').replace("'", "'\\''")
|
|
|
|
|
+ def promoteProdDirSh = s.prodDir.replace("'", "'\\''")
|
|
|
|
|
+ def logHostDirSh = logHostDir.replace("'", "'\\''")
|
|
|
|
|
+ def jarBasenameSh = jar.replace("'", "'\\''")
|
|
|
|
|
+ def cPrimarySh = cPrimary.toString().replace("'", "'\\''")
|
|
|
|
|
+ def cFallbackSh = cFallback.toString().replace("'", "'\\''")
|
|
|
|
|
+ def dstPayCertDirSh = dstPayCertDirInspect.replace("'", "'\\''")
|
|
|
|
|
+ def dstDirSh = dstDir.replace("'", "'\\''")
|
|
|
|
|
+ def uatDeployRoot = env.UAT_DEPLOY_ROOT ?: ''
|
|
|
|
|
+ def uatDeployRootSh = uatDeployRoot.toString().replace("'", "'\\''")
|
|
|
|
|
+ def uatUatServiceDirSh = "${(uatDeployRoot)}/${(s.uatDir)}".toString().replace("'", "'\\''")
|
|
|
|
|
+ def uatDiagLsSvc = "ls -la '" + uatUatServiceDirSh + "' 2>&1"
|
|
|
|
|
+ def uatDiagLsRoot = "ls -la '" + uatDeployRootSh + "' 2>&1"
|
|
|
|
|
+ def missingJarDiag = " echo \"诊断 UAT_DEPLOY_ROOT: " + uatDeployRoot + ":\"\n " + uatDiagLsSvc + "\n " + uatDiagLsRoot
|
|
|
|
|
+ def srcJarSh = srcJar.toString().replace("'", "'\\''")
|
|
|
|
|
+ def ifNoUatJar = " if [ ! -f '" + srcJarSh + "' ]; then"
|
|
|
|
|
+ def errEchoUatMissing = " echo \"ERROR: 当前 Jenkins 进程内找不到 UAT 制品: " + srcJar + "\""
|
|
|
|
|
+ // 须输出 shell 的 $PROMOTE_DST_DIR;Groovy 中勿用 '$$'(在 bash 里会展开为 PID+字面量,如 17253PROMOTE)
|
|
|
|
|
+ def dryRunCpEcho = " echo \"[DRY_RUN] cp -f " + srcJar + " \$PROMOTE_DST_DIR/\""
|
|
|
|
|
+ def cpSrcToDst = " cp -f '" + srcJarSh + "' " + "\$" + "PROMOTE_DST_DIR/"
|
|
|
|
|
+ def echoJarLine = ' echo ">>> [jar] ' + s.module + ': ' + s.uatDir + ' -> ' + s.prodDir + '(compose 服务名: $COMPOSE_SVC, 容器内 jar: $JCP)"'
|
|
|
|
|
+ def dryRunLogDirEcho = useRemoteProd ?
|
|
|
|
|
+ ' echo "[DRY_RUN] mkdir -p 本地: $(dirname $PROMOTE_DST_DIR)/logs/$PROMOTE_PROD_DIR -> rsync -> 生产: ' + logHostDir + '"' :
|
|
|
|
|
+ " echo \"[DRY_RUN] mkdir -p " + logHostDir + "(日志目录,与 compose ./logs/" + s.prodDir + ":/app/logs 一致)\""
|
|
|
|
|
+ def jasyptExplLine = " JENKINS_JASYPT_EXPL=" + jasyptExplicit
|
|
|
|
|
+ // 无引号 JENKINS_*= 赋值不得写进 promoteShell 的 GString,否则如 ${autoCreate} 会触发非法 $ 转义
|
|
|
|
|
+ def jenkinsEnvBools = " JENKINS_DRY_RUN=" + dryRunSh + "\n" +
|
|
|
|
|
+ " JENKINS_PAY_CERT_SVC=" + payCertSvc + "\n" +
|
|
|
|
|
+ " JENKINS_STALE_JAR_RM=" + removeStaleJarDir + "\n" +
|
|
|
|
|
+ " JENKINS_AUTO_RECREATE=" + autoRecreate + "\n" +
|
|
|
|
|
+ " JENKINS_AUTO_CREATE=" + autoCreate
|
|
|
|
|
+ // DCP_*='...' 含 dcpShellQuoted 类变量名时,CPS 对大 GString 仍报 “illegal $”;整段在 Groovy 中拼接
|
|
|
|
|
+ def dcpThreeLines = " DCP_EXPLICIT='" + dcpShellQuoted + "'\n" +
|
|
|
|
|
+ " DCP_V2_PLUGIN='" + dcpV2ShellQuoted + "'\n" +
|
|
|
|
|
+ " DCP_DIND_IMAGE='" + dcpDindSh + "'"
|
|
|
|
|
+ def dcpExtNetsLine = " DCP_EXT_NETS='" + extNetsSh + "'"
|
|
|
|
|
+ def jarOnProdExportSh = useRemoteProd ? jarOnProdServerSh : ''
|
|
|
|
|
+ def sshExport = useRemoteProd ? sshQE : ''
|
|
|
|
|
+ def remExport = useRemoteProd ? remQE : ''
|
|
|
|
|
+ // export 与环境变量行整段用 + 拼接;大 GString 中 ANY 'VAR=''${...}'' 均可能被 CPS 报 “illegal $”(与 eff* / ext* 等标识符相关)
|
|
|
|
|
+ def promoteExportBlock = " export JENKINS_HOME='" + jenkinsHomeSh + "'\n" +
|
|
|
|
|
+ " COMPOSE_FILE='" + composeFileForJob + "'\n" +
|
|
|
|
|
+ " COMPOSE_SVC='" + composeSvc + "'\n" +
|
|
|
|
|
+ " PROMOTE_PROD_DIR='" + promoteProdDirSh + "'\n" +
|
|
|
|
|
+ " PROMOTE_DST_DIR='" + dstDirSh + "'\n" +
|
|
|
|
|
+ " JAR_HOST='" + jarHostPathSh + "'\n" +
|
|
|
|
|
+ " JAR_ON_PROD_SERVER='" + jarOnProdExportSh + "'\n" +
|
|
|
|
|
+ " JENKINS_PROD_SSH='" + sshExport + "'\n" +
|
|
|
|
|
+ " JENKINS_REMOTE_PROD_ROOT='" + remExport + "'\n" +
|
|
|
|
|
+ " PROMOTE_SRC_LIB='" + srcLibSh + "'\n" +
|
|
|
|
|
+ " PROMOTE_DST_LIB='" + dstLibSh + "'\n" +
|
|
|
|
|
+ " JAR_BASENAME='" + jarBasenameSh + "'\n" +
|
|
|
|
|
+ " C_PRIMARY='" + cPrimarySh + "'\n" +
|
|
|
|
|
+ " C_FALLBACK='" + cFallbackSh + "'\n" +
|
|
|
|
|
+ " JENKINS_LOG_DIR='" + logHostDirSh + "'\n" +
|
|
|
|
|
+ " JENKINS_PAY_CERT_DIR='" + dstPayCertDirSh + "'\n" +
|
|
|
|
|
+ " JRE_IMG='" + jreImageSh + "'\n" +
|
|
|
|
|
+ " JCP='" + jcpPrimaryEscaped + "'\n" +
|
|
|
|
|
+ " JCP_ALT='" + jcpAltEscaped + "'\n" +
|
|
|
|
|
+ " JENKINS_PROD_ENV_FILE='" + effEnvPathForSh + "'\n"
|
|
|
|
|
|
|
|
- def promoteShell = """
|
|
|
|
|
|
|
+ def remoteDockerShimBlock = useRemoteProd ? '\n if [ -n "$JENKINS_PROD_SSH" ]; then\n docker() { ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" sudo docker "$@"; }\n fi\n' : '\n'
|
|
|
|
|
+ def rsyncRemoteBlock = useRemoteProd ? '\n if [ -n "$JENKINS_PROD_SSH" ]; then\n echo ">>> rsync 制品到 $JENKINS_PROD_SSH:$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/"\n ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "sudo mkdir -p $JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR $JENKINS_REMOTE_PROD_ROOT/logs/$PROMOTE_PROD_DIR"\n rsync -az "$PROMOTE_DST_DIR/" "$JENKINS_PROD_SSH:$JENKINS_REMOTE_PROD_ROOT/$PROMOTE_PROD_DIR/"\n rsync -az "$(dirname "$PROMOTE_DST_DIR")/logs/$PROMOTE_PROD_DIR/" "$JENKINS_PROD_SSH:$JENKINS_REMOTE_PROD_ROOT/logs/$PROMOTE_PROD_DIR/" 2>/dev/null || true\n fi\n' : '\n'
|
|
|
|
|
+ def composeExistsBlockSh = useRemoteProd ? '\n if ! ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$JENKINS_PROD_SSH" "test -f $COMPOSE_FILE"; then\n echo "ERROR: 远程未找到 docker compose 文件: $COMPOSE_FILE"\n exit 1\n fi\n' : '\n if [ ! -f "$COMPOSE_FILE" ]; then\n echo "ERROR: 未找到 docker compose 文件: $COMPOSE_FILE。请将 PROD_COMPOSE_FILE/PROD_DEPLOY_ROOT 与现网 yml 对齐,例如 /alien_produ/java/docker-compose.yml"\n exit 1\n fi\n'
|
|
|
|
|
+ def composeDirBlockSh = useRemoteProd ? ' COMPOSE_DIR=$(dirname "$COMPOSE_FILE")' : ' COMPOSE_DIR=$(cd "$(dirname "$COMPOSE_FILE")" && pwd)'
|
|
|
|
|
+
|
|
|
|
|
+ def promoteShell = """
|
|
|
set -e
|
|
set -e
|
|
|
#JENKINS_PLACE_EXPORT_ENV
|
|
#JENKINS_PLACE_EXPORT_ENV
|
|
|
#JENKINS_PLACE_REMOTE_DOCKER_SHIM
|
|
#JENKINS_PLACE_REMOTE_DOCKER_SHIM
|
|
@@ -1487,37 +1550,45 @@ pipeline {
|
|
|
fi
|
|
fi
|
|
|
fi
|
|
fi
|
|
|
"""
|
|
"""
|
|
|
- sh( promoteShell.toString()
|
|
|
|
|
- // replaceAll 的替换串含 shell 的 $CNAME 等,Java 会当「组引用」;须 quoteReplacement
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_EXPORT_ENV\n/, java.util.regex.Matcher.quoteReplacement("\n" + promoteExportBlock))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_REMOTE_DOCKER_SHIM\n/, java.util.regex.Matcher.quoteReplacement(remoteDockerShimBlock))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_RSYNC_REMOTE\n/, java.util.regex.Matcher.quoteReplacement(rsyncRemoteBlock))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_COMPOSE_EXISTS\n/, java.util.regex.Matcher.quoteReplacement(composeExistsBlockSh))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_COMPOSE_DIR\n/, java.util.regex.Matcher.quoteReplacement("\n" + composeDirBlockSh + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_PAYCERT_ECHO\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunPayCertEcho + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_EFF\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunEffIfFi + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_JHF_D\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptDryHfDBlock + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_PAY_CERT_MOUNT_INNER\n/, java.util.regex.Matcher.quoteReplacement("\n" + payCertPayMountInner + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DCP_THREE\n/, java.util.regex.Matcher.quoteReplacement("\n" + dcpThreeLines + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DCP_EXT_NETS\n/, java.util.regex.Matcher.quoteReplacement("\n" + dcpExtNetsLine + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ENV_BOOLS\n/, java.util.regex.Matcher.quoteReplacement("\n" + jenkinsEnvBools + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_JASYPT_EXPL\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptExplLine + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ECHO_JARLINE\n/, java.util.regex.Matcher.quoteReplacement("\n" + echoJarLine + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_IF_NO_UAT_JAR\n/, java.util.regex.Matcher.quoteReplacement("\n" + ifNoUatJar + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ECHO_ERR_UAT_JAR\n/, java.util.regex.Matcher.quoteReplacement("\n" + errEchoUatMissing + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_MISSING_JAR_DIAG\n/, java.util.regex.Matcher.quoteReplacement("\n" + missingJarDiag + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_LOGDIR_ECHO\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunLogDirEcho + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_CP\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunCpEcho + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_CP_PROD\n/, java.util.regex.Matcher.quoteReplacement("\n" + cpSrcToDst + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ENV_FILE_OPT\n/, java.util.regex.Matcher.quoteReplacement("\n" + envFileOptIfElifFi + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_JHF\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptLineJhf + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_JASYPT_WARN_EXPL_UNREAD\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptWarnExplJpfUnread + "\n"))
|
|
|
|
|
- .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DOCKER_EXEC_OD_JCPR\n/, java.util.regex.Matcher.quoteReplacement("\n" + lineDockerExecOdJcpr + "\n")) )
|
|
|
|
|
|
|
+ sh( promoteShell.toString()
|
|
|
|
|
+ // replaceAll 的替换串含 shell 的 $CNAME 等,Java 会当「组引用」;须 quoteReplacement
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_EXPORT_ENV\n/, java.util.regex.Matcher.quoteReplacement("\n" + promoteExportBlock))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_REMOTE_DOCKER_SHIM\n/, java.util.regex.Matcher.quoteReplacement(remoteDockerShimBlock))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_RSYNC_REMOTE\n/, java.util.regex.Matcher.quoteReplacement(rsyncRemoteBlock))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_COMPOSE_EXISTS\n/, java.util.regex.Matcher.quoteReplacement(composeExistsBlockSh))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_COMPOSE_DIR\n/, java.util.regex.Matcher.quoteReplacement("\n" + composeDirBlockSh + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_PAYCERT_ECHO\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunPayCertEcho + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_EFF\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunEffIfFi + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_JHF_D\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptDryHfDBlock + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_PAY_CERT_MOUNT_INNER\n/, java.util.regex.Matcher.quoteReplacement("\n" + payCertPayMountInner + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DCP_THREE\n/, java.util.regex.Matcher.quoteReplacement("\n" + dcpThreeLines + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DCP_EXT_NETS\n/, java.util.regex.Matcher.quoteReplacement("\n" + dcpExtNetsLine + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ENV_BOOLS\n/, java.util.regex.Matcher.quoteReplacement("\n" + jenkinsEnvBools + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_JASYPT_EXPL\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptExplLine + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ECHO_JARLINE\n/, java.util.regex.Matcher.quoteReplacement("\n" + echoJarLine + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_IF_NO_UAT_JAR\n/, java.util.regex.Matcher.quoteReplacement("\n" + ifNoUatJar + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ECHO_ERR_UAT_JAR\n/, java.util.regex.Matcher.quoteReplacement("\n" + errEchoUatMissing + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_MISSING_JAR_DIAG\n/, java.util.regex.Matcher.quoteReplacement("\n" + missingJarDiag + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_LOGDIR_ECHO\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunLogDirEcho + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DRYRUN_CP\n/, java.util.regex.Matcher.quoteReplacement("\n" + dryRunCpEcho + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_CP_PROD\n/, java.util.regex.Matcher.quoteReplacement("\n" + cpSrcToDst + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_ENV_FILE_OPT\n/, java.util.regex.Matcher.quoteReplacement("\n" + envFileOptIfElifFi + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_JHF\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptLineJhf + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_JASYPT_WARN_EXPL_UNREAD\n/, java.util.regex.Matcher.quoteReplacement("\n" + jasyptWarnExplJpfUnread + "\n"))
|
|
|
|
|
+ .replaceAll(~/\n[ \t]*#JENKINS_PLACE_DOCKER_EXEC_OD_JCPR\n/, java.util.regex.Matcher.quoteReplacement("\n" + lineDockerExecOdJcpr + "\n")) )
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (writtenJasyptEnv) {
|
|
|
|
|
+ sh "rm -f \"${writtenJasyptEnv}\" 2>/dev/null || true"
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- } finally {
|
|
|
|
|
- if (writtenJasyptEnv) {
|
|
|
|
|
- sh "rm -f \"${writtenJasyptEnv}\" 2>/dev/null || true"
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ if (useRemoteProd && prodSshCred) {
|
|
|
|
|
+ sshagent([prodSshCred]) {
|
|
|
|
|
+ runPromoteRemoteSteps()
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ runPromoteRemoteSteps()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1535,7 +1606,7 @@ pipeline {
|
|
|
failure {
|
|
failure {
|
|
|
echo '>>> 流水线失败:优先检查 UAT 是否已发版、UAT_DEPLOY_ROOT/PROD_DEPLOY_ROOT、compose 路径、Docker 与挂载'
|
|
echo '>>> 流水线失败:优先检查 UAT 是否已发版、UAT_DEPLOY_ROOT/PROD_DEPLOY_ROOT、compose 路径、Docker 与挂载'
|
|
|
echo '>>> 制品与 jar 来自 UAT 目录(UAT_DEPLOY_ROOT/<uatDir>/),不依赖 Git;仅当「BOOTSTRAP_SYNC_MODE=from_git」才会拉取 Git 取 bootstrap-prod.yml。默认 from_jar=从 UAT jar 解压,无需 Git'
|
|
echo '>>> 制品与 jar 来自 UAT 目录(UAT_DEPLOY_ROOT/<uatDir>/),不依赖 Git;仅当「BOOTSTRAP_SYNC_MODE=from_git」才会拉取 Git 取 bootstrap-prod.yml。默认 from_jar=从 UAT jar 解压,无需 Git'
|
|
|
- echo '>>> 若 Jenkins 与生产 Docker 不在同一台机器:须配置 PROD_SSH_TARGET(免密 ssh + 目标机 sudo docker);制品经 rsync 到对方 PROD_DEPLOY_ROOT;留空 PROD_SSH_TARGET 则仍为本机 docker compose'
|
|
|
|
|
|
|
+ echo '>>> 若 Jenkins 与生产 Docker 不在同一台机器:须 PROD_SSH_TARGET;凭据用 PROD_SSH_CREDENTIALS_ID(SSH Agent + 私钥凭据)。界面参数仍为空时脚本会回退 DEFAULT_PROD_SSH_CREDENTIALS_ID;若报 Permission denied 检查凭据 ID/私钥是否匹配目标机'
|
|
|
echo '>>> 若 source / compose 报 invalid env file / invalid utf8:将 .env.produ-docker 改为 UTF-8,或用 env.produ-docker.example;Jasypt 需进入容器 env,检查 compose、.env 与 dcp 前 set -a、AUTO_RECREATE'
|
|
echo '>>> 若 source / compose 报 invalid env file / invalid utf8:将 .env.produ-docker 改为 UTF-8,或用 env.produ-docker.example;Jasypt 需进入容器 env,检查 compose、.env 与 dcp 前 set -a、AUTO_RECREATE'
|
|
|
echo '>>> 若报 unknown shorthand flag: f 或本机无 compose:默认已用 PROD_DOCKER_COMPOSE_DIND_IMAGE(如 docker:24.0.9-cli)通过 docker run 回退;失败则检查能 pull 该镜像、/var/run/docker.sock、或设 PROD_DOCKER_COMPOSE_PATH/PROD_DOCKER_COMPOSE_V2_PLUGIN/挂 cli-plugins'
|
|
echo '>>> 若报 unknown shorthand flag: f 或本机无 compose:默认已用 PROD_DOCKER_COMPOSE_DIND_IMAGE(如 docker:24.0.9-cli)通过 docker run 回退;失败则检查能 pull 该镜像、/var/run/docker.sock、或设 PROD_DOCKER_COMPOSE_PATH/PROD_DOCKER_COMPOSE_V2_PLUGIN/挂 cli-plugins'
|
|
|
echo '>>> 若报 network ... declared as external, but could not be found:设 PROD_DOCKER_CREATE_EXTERNAL_NETWORKS 为 yml 中网名(默认预建 common-network-produ),或宿主机 docker network create'
|
|
echo '>>> 若报 network ... declared as external, but could not be found:设 PROD_DOCKER_CREATE_EXTERNAL_NETWORKS 为 yml 中网名(默认预建 common-network-produ),或宿主机 docker network create'
|