diff --git a/README.md b/README.md index fe5a422..e6fb5d5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ Credentials: 添加您的 Git 凭据 ID Jenkins安装插件: 1、HTTP Request +2、Build User Vars 用于获取构建人信息 +3、SSH server +4、Pipeline +5、DingTalk 钉钉消息案例: diff --git a/resources/templates/jenkins1.html b/resources/templates/jenkins1.html new file mode 100644 index 0000000..3a3aeae --- /dev/null +++ b/resources/templates/jenkins1.html @@ -0,0 +1,391 @@ + + + + + + Jenkins构建成功通知 + + + +
+
+

✅ Jenkins构建成功

+

您的项目已成功构建并部署

+
构建 #${BUILD_NUMBER} 成功
+
+ +
+
+

📊 构建概览

+
+
+
项目名称
+
${JOB_NAME}
+
+
+
构建编号
+
#${BUILD_NUMBER}
+
+
+
分支
+
${BRANCH_NAME}
+
+
+
环境
+
${PROFILES}
+
+
+
构建时间
+
${BUILD_TIMESTAMP}
+
+
+
构建时长
+
${BUILD_DURATION}
+
+
+
+ +
+

⏱️ 构建时间线

+
+
+
+
+
代码检出
+
${SCM_CHECKOUT_TIME}
+
耗时: ${SCM_CHECKOUT_DURATION}
+
+
+
+
+
+
依赖安装
+
${DEPENDENCY_INSTALL_TIME}
+
耗时: ${DEPENDENCY_INSTALL_DURATION}
+
+
+
+
+
+
代码编译
+
${COMPILE_TIME}
+
耗时: ${COMPILE_DURATION}
+
+
+
+
+
+
镜像构建
+
${IMAGE_BUILD_TIME}
+
耗时: ${IMAGE_BUILD_DURATION}
+
+
+
+
+
+
部署执行
+
${DEPLOY_TIME}
+
耗时: ${DEPLOY_DURATION}
+
+
+
+
+ +
+

🚀 部署信息

+
+
服务名称
+
${SERVICE_NAME}
+
+
+
镜像标签
+
${IMAGE_TAG}
+
+
+
部署策略
+
${DEPLOYMENT_STRATEGY}
+
+ +
+
目标服务器
+
    +
  • + 服务器名称 + 状态 +
  • + ${SERVER_LIST} +
+
+
+ + +
+ + +
+ + \ No newline at end of file diff --git a/resources/templates/jenkins2.html b/resources/templates/jenkins2.html new file mode 100644 index 0000000..9edff48 --- /dev/null +++ b/resources/templates/jenkins2.html @@ -0,0 +1,631 @@ + + + + + + Jenkins构建成功通知 + + + +
+
+

✅ Jenkins构建成功

+

您的项目已成功构建并部署到多台服务器

+
构建 #183 成功
+
+ +
+
+

📊 构建概览

+
+
+
项目名称
+
user-service-api
+
+
+
构建编号
+
#183
+
+
+
分支
+
release/v2.1.0
+
+
+
环境
+
生产环境
+
+
+
构建时间
+
2023-10-15 14:30:25
+
+
+
构建时长
+
12分钟 34秒
+
+
+
+ +
+

⏱️ 构建时间线

+
+
+
1
+
+
📥 代码检出
+
开始: 14:30:25 | 结束: 14:31:10
+
耗时: 45秒
+
+
+
+
2
+
+
📦 依赖安装
+
开始: 14:31:10 | 结束: 14:33:25
+
耗时: 2分钟 15秒
+
+
+
+
3
+
+
⚙️ 代码编译
+
开始: 14:33:25 | 结束: 14:37:40
+
耗时: 4分钟 15秒
+
+
+
+
4
+
+
🐳 镜像构建
+
开始: 14:37:40 | 结束: 14:40:15
+
耗时: 2分钟 35秒
+
+
+
+
5
+
+
🚀 部署执行
+
开始: 14:40:15 | 结束: 14:42:59
+
耗时: 2分钟 44秒
+
+
+
+
+ +
+

🌐 多服务器部署

+
+
+
+
web-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.1.101
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
web-server-02
+
部署成功
+
+
+
+
IP地址
+
192.168.1.102
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
api-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.2.101
+
+
+
区域
+
华北-北京
+
+
+
实例类型
+
8核16GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
db-server-01
+
部署中
+
+
+
+
IP地址
+
192.168.3.101
+
+
+
区域
+
华南-广州
+
+
+
实例类型
+
16核32GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
75% 完成
+
+
+
+
+ + +
+ + +
+ + \ No newline at end of file diff --git a/resources/templates/jenkins3.html b/resources/templates/jenkins3.html new file mode 100644 index 0000000..52ac937 --- /dev/null +++ b/resources/templates/jenkins3.html @@ -0,0 +1,724 @@ + + + + + + Jenkins构建成功通知 + + + +
+
+

✅ Jenkins构建成功

+

您的项目已成功构建并部署到多台服务器

+
构建 #183 成功
+
+ +
+
+

📊 构建概览

+
+
+
项目名称
+
user-service-api
+
+
+
构建编号
+
#183
+
+
+
构建节点
+
master-node-01
+
+
+
构建分支
+
release/v2.1.0
+
+
+
构建状态
+
成功
+
+
+
环境
+
生产环境
+
+
+
构建时间
+
2023-10-15 14:30:25
+
+
+
构建时长
+
12分钟 34秒
+
+
+
+ +
+

🔗 Git信息

+
+
+
代码库地址
+
https://github.com/company/user-service-api.git
+
+
+
分支
+
release/v2.1.0
+
+
+
标签
+
v2.1.0-release
+
+
+
提交者
+
张三 (zhangsan@company.com)
+
+
+ +
+
最新提交
+
+ a1b2c3d4 + 修复用户登录验证逻辑问题 + 李四 • 2小时前 +
+
+ e5f6g7h8 + 优化API响应性能 + 王五 • 5小时前 +
+
+
+ +
+

⏱️ 构建时间线

+
+
+
1
+
+
📥 代码检出
+
开始: 14:30:25 | 结束: 14:31:10
+
耗时: 45秒
+
+
+
+
2
+
+
📦 依赖安装
+
开始: 14:31:10 | 结束: 14:33:25
+
耗时: 2分钟 15秒
+
+
+
+
3
+
+
⚙️ 代码编译
+
开始: 14:33:25 | 结束: 14:37:40
+
耗时: 4分钟 15秒
+
+
+
+
4
+
+
🐳 镜像构建
+
开始: 14:37:40 | 结束: 14:40:15
+
耗时: 2分钟 35秒
+
+
+
+
5
+
+
🚀 部署执行
+
开始: 14:40:15 | 结束: 14:42:59
+
耗时: 2分钟 44秒
+
+
+
+
+ +
+

🌐 多服务器部署

+
+
+
+
web-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.1.101
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
web-server-02
+
部署成功
+
+
+
+
IP地址
+
192.168.1.102
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
api-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.2.101
+
+
+
区域
+
华北-北京
+
+
+
实例类型
+
8核16GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
db-server-01
+
部署中
+
+
+
+
IP地址
+
192.168.3.101
+
+
+
区域
+
华南-广州
+
+
+
实例类型
+
16核32GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
75% 完成
+
+
+
+
+ + +
+ + +
+ + \ No newline at end of file diff --git a/resources/templates/jenkins4.html b/resources/templates/jenkins4.html new file mode 100644 index 0000000..11d1532 --- /dev/null +++ b/resources/templates/jenkins4.html @@ -0,0 +1,979 @@ + + + + + + Jenkins构建成功通知 + + + +
+
+

✅ Jenkins构建成功

+

您的项目已成功构建并部署到多台服务器

+
构建 #183 成功
+
+ +
+
+

📊 构建概览

+
+
+
项目名称
+
user-service-api
+
+
+
构建编号
+
#183
+
+
+
构建节点
+
master-node-01
+
+
+
构建分支
+
release/v2.1.0
+
+
+
构建状态
+
成功
+
+
+
环境
+
生产环境
+
+
+
构建时间
+
2023-10-15 14:30:25
+
+
+
构建时长
+
12分钟 34秒
+
+
+
+ + +
+

📦 构建产物

+
+
+
+
Docker镜像
+
镜像
+
+
+
+
镜像名称
+
registry.company.com/user-service-api
+
+
+
镜像标签
+
v2.1.0-183
+
+
+
镜像大小
+
245 MB
+
+
+
构建时间
+
2023-10-15 14:40:15
+
+
+
+ +
+
+
JAR包
+
应用包
+
+
+
+
文件名称
+
user-service-api-2.1.0.jar
+
+
+
文件大小
+
78.5 MB
+
+
+
MD5校验
+
a1b2c3d4e5f6g7h8i9j0
+
+
+
构建时间
+
2023-10-15 14:37:40
+
+
+
+
+
+ + +
+

🚀 部署信息

+
+
+
+
部署策略
+
蓝绿部署
+
+
+
+
部署方式
+
滚动更新
+
+
+
分批数量
+
3批
+
+
+
批次间隔
+
5分钟
+
+
+
失败策略
+
自动回滚
+
+
+
+ +
+
+
健康检查
+
通过
+
+
+
+
检查类型
+
HTTP端点检查
+
+
+
检查路径
+
/health
+
+
+
超时时间
+
30秒
+
+
+
重试次数
+
3次
+
+
+
+
+
+ +
+

🔗 Git信息

+
+
+
代码库地址
+
https://github.com/company/user-service-api.git
+
+
+
分支
+
release/v2.1.0
+
+
+
标签
+
v2.1.0-release
+
+
+
提交者
+
张三 (zhangsan@company.com)
+
+
+ +
+
最新提交
+
+ a1b2c3d4 + 修复用户登录验证逻辑问题 + 李四 • 2小时前 +
+
+ e5f6g7h8 + 优化API响应性能 + 王五 • 5小时前 +
+
+
+ +
+

⏱️ 构建时间线

+
+
+
1
+
+
📥 代码检出
+
开始: 14:30:25 | 结束: 14:31:10
+
耗时: 45秒
+
+
+
+
2
+
+
📦 依赖安装
+
开始: 14:31:10 | 结束: 14:33:25
+
耗时: 2分钟 15秒
+
+
+
+
3
+
+
⚙️ 代码编译
+
开始: 14:33:25 | 结束: 14:37:40
+
耗时: 4分钟 15秒
+
+
+
+
4
+
+
🐳 镜像构建
+
开始: 14:37:40 | 结束: 14:40:15
+
耗时: 2分钟 35秒
+
+
+
+
5
+
+
🚀 部署执行
+
开始: 14:40:15 | 结束: 14:42:59
+
耗时: 2分钟 44秒
+
+
+
+
+ +
+

🌐 多服务器部署

+
+
+
+
web-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.1.101
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
web-server-02
+
部署成功
+
+
+
+
IP地址
+
192.168.1.102
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
api-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.2.101
+
+
+
区域
+
华北-北京
+
+
+
实例类型
+
8核16GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
db-server-01
+
部署中
+
+
+
+
IP地址
+
192.168.3.101
+
+
+
区域
+
华南-广州
+
+
+
实例类型
+
16核32GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
75% 完成
+
+
+
+
+ + +
+ + +
+ + \ No newline at end of file diff --git a/resources/templates/jenkins5.html b/resources/templates/jenkins5.html new file mode 100644 index 0000000..060ca0a --- /dev/null +++ b/resources/templates/jenkins5.html @@ -0,0 +1,1259 @@ + + + + + + Jenkins构建成功通知 + + + +
+
+

✅ Jenkins构建成功

+

您的项目已成功构建并部署到多台服务器

+
构建 #183 成功
+
+ +
+
+

📊 构建概览

+
+
+
项目名称
+
user-service-api
+
+
+
构建编号
+
#183
+
+
+
构建节点
+
master-node-01
+
+
+
构建分支
+
release/v2.1.0
+
+
+
构建状态
+
成功
+
+
+
环境
+
生产环境
+
+
+
构建时间
+
2023-10-15 14:30:25
+
+
+
构建时长
+
12分钟 34秒
+
+
+
+ + +
+

🧪 测试结果

+
+
+
+
单元测试
+
+
+
95.2%
+
通过率
+
+
+
+
+
+
+
+
通过: 238/250
+
+
+ +
+
+
集成测试
+
+
+
100%
+
通过率
+
+
+
+
+
+
+
+
通过: 45/45
+
+
+
+
+ + +
+

🔒 质量门禁

+
+
+
📊
+
87.5%
+
代码覆盖率
+
+
+
⚠️
+
0
+
blocker问题
+
+
+
🔍
+
12
+
代码异味
+
+
+
+ + +
+

📦 构建产物

+
+
+
+
Docker镜像
+
镜像
+
+
+
+
镜像名称
+
registry.company.com/user-service-api
+
+
+
镜像标签
+
v2.1.0-183
+
+
+
镜像大小
+
245 MB
+
+
+
构建时间
+
2023-10-15 14:40:15
+
+
+
+ +
+
+
JAR包
+
应用包
+
+
+
+
文件名称
+
user-service-api-2.1.0.jar
+
+
+
文件大小
+
78.5 MB
+
+
+
MD5校验
+
a1b2c3d4e5f6g7h8i9j0
+
+
+
构建时间
+
2023-10-15 14:37:40
+
+
+
+
+
+ + +
+

💾 资源使用情况

+
+
+
构建资源消耗
+
+
+ CPU使用率 + 78% +
+
+
+
+
+
+
+ 内存使用 + 2.3GB/4GB +
+
+
+
+
+
+ +
+
磁盘与网络
+
+
+ 磁盘I/O + 45MB/s +
+
+
+
+
+
+
+ 网络流量 + 120MB +
+
+
+
+
+
+
+
+ + +
+

🚀 部署信息

+
+
+
+
部署策略
+
蓝绿部署
+
+
+
+
部署方式
+
滚动更新
+
+
+
分批数量
+
3批
+
+
+
批次间隔
+
5分钟
+
+
+
失败策略
+
自动回滚
+
+
+
+ +
+
+
健康检查
+
通过
+
+
+
+
检查类型
+
HTTP端点检查
+
+
+
检查路径
+
/health
+
+
+
超时时间
+
30秒
+
+
+
重试次数
+
3次
+
+
+
+
+
+ +
+

🔗 Git信息

+
+
+
代码库地址
+
https://github.com/company/user-service-api.git
+
+
+
分支
+
release/v2.1.0
+
+
+
标签
+
v2.1.0-release
+
+
+
提交者
+
张三 (zhangsan@company.com)
+
+
+ +
+
最新提交
+
+ a1b2c3d4 + 修复用户登录验证逻辑问题 + 李四 • 2小时前 +
+
+ e5f6g7h8 + 优化API响应性能 + 王五 • 5小时前 +
+
+
+ +
+

⏱️ 构建时间线

+
+
+
1
+
+
📥 代码检出
+
开始: 14:30:25 | 结束: 14:31:10
+
耗时: 45秒
+
+
+
+
2
+
+
📦 依赖安装
+
开始: 14:31:10 | 结束: 14:33:25
+
耗时: 2分钟 15秒
+
+
+
+
3
+
+
⚙️ 代码编译
+
开始: 14:33:25 | 结束: 14:37:40
+
耗时: 4分钟 15秒
+
+
+
+
4
+
+
🧪 测试执行
+
开始: 14:37:40 | 结束: 14:39:20
+
耗时: 1分钟 40秒
+
+
+
+
5
+
+
🐳 镜像构建
+
开始: 14:39:20 | 结束: 14:40:15
+
耗时: 55秒
+
+
+
+
6
+
+
🚀 部署执行
+
开始: 14:40:15 | 结束: 14:42:59
+
耗时: 2分钟 44秒
+
+
+
+
+ +
+

🌐 多服务器部署

+
+
+
+
web-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.1.101
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
web-server-02
+
部署成功
+
+
+
+
IP地址
+
192.168.1.102
+
+
+
区域
+
华东-上海
+
+
+
实例类型
+
4核8GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
api-server-01
+
部署成功
+
+
+
+
IP地址
+
192.168.2.101
+
+
+
区域
+
华北-北京
+
+
+
实例类型
+
8核16GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
已完成
+
+
+ +
+
+
db-server-01
+
部署中
+
+
+
+
IP地址
+
192.168.3.101
+
+
+
区域
+
华南-广州
+
+
+
实例类型
+
16核32GB
+
+
+
部署版本
+
v2.1.0-183
+
+
+
+
+
+
+
75% 完成
+
+
+
+
+ + +
+ + +
+ + \ No newline at end of file diff --git a/resources/templates/jenkins6.html b/resources/templates/jenkins6.html new file mode 100644 index 0000000..9b63713 --- /dev/null +++ b/resources/templates/jenkins6.html @@ -0,0 +1,674 @@ + + + + + + Jenkins资源消耗告警 + + + +
+
+

⚠️ Jenkins资源消耗告警

+

检测到构建过程中的资源使用超过阈值

+
需要管理员关注
+
+ +
+
+

🚨 告警概览

+
+
+
项目名称
+
user-service-api
+
+
+
构建编号
+
#183
+
+
+
告警级别
+
严重
+
+
+
告警时间
+
2023-10-15 14:35:42
+
+
+
触发条件
+
CPU使用率 > 85% 持续5分钟
+
+
+
当前状态
+
持续中
+
+
+
持续时间
+
7分钟 23秒
+
+
+
构建节点
+
master-node-01
+
+
+
+ +
+

📊 资源使用详情

+
+
+
CPU使用情况
+
+
+ 当前使用率 + 92% +
+
+
+
+
+
+
+ 5分钟平均值 + 88% +
+
+
+
+
+
+
+ 阈值设定 + 85% +
+
+
+
+
+
+ +
+
其他资源指标
+
+
+ 内存使用 + 3.7GB/4GB +
+
+
+
+
+
+
+ 磁盘I/O + 78MB/s +
+
+
+
+
+
+
+ 网络流量 + 45MB/s +
+
+
+
+
+
+
+
+ +
+

🔍 告警详情

+
+
+
⚠️
+
CPU使用率持续超过阈值
+
+
+

检测到构建节点 master-node-01 的CPU使用率在构建过程中持续超过设定的阈值(85%),当前峰值达到92%,可能影响构建性能和其他任务的执行。

+
+
+
+
峰值使用率
+
92%
+
+
+
持续时间
+
7分钟 23秒
+
+
+
阈值设定
+
85%
+
+
+
触发时间
+
2023-10-15 14:35:42
+
+
+
+
+ +
+

⏱️ 构建时间线

+
+
+
1
+
+
📥 代码检出
+
开始: 14:30:25 | 结束: 14:31:10
+
耗时: 45秒
+
+
+
+
2
+
+
📦 依赖安装
+
开始: 14:31:10 | 结束: 14:33:25
+
耗时: 2分钟 15秒
+
+ 注意: 此阶段CPU使用率开始上升至75% +
+
+
+
+
3
+
+
⚙️ 代码编译
+
开始: 14:33:25 | 结束: 14:37:40
+
耗时: 4分钟 15秒
+
+ 告警: 此阶段CPU使用率超过阈值,峰值达到92% +
+
+
+
+
4
+
+
🧪 测试执行
+
开始: 14:37:40 | 结束: 14:39:20
+
耗时: 1分钟 40秒
+
+ 注意: 此阶段CPU使用率维持在85%-90% +
+
+
+
+
+ +
+

💡 建议措施

+
+
    +
  • 检查构建节点当前负载,考虑将部分任务迁移到其他节点
  • +
  • 优化构建脚本,减少CPU密集型操作或增加并行处理
  • +
  • 考虑升级构建节点的硬件配置或增加节点数量
  • +
  • 检查是否有异常进程占用大量CPU资源
  • +
  • 评估是否需要调整构建任务的调度策略
  • +
+
+
+ + +
+ + +
+ + \ No newline at end of file diff --git a/src/com/docker/ResolverService.groovy b/src/com/docker/ResolverService.groovy index 0b64695..96800d9 100644 --- a/src/com/docker/ResolverService.groovy +++ b/src/com/docker/ResolverService.groovy @@ -465,7 +465,7 @@ class ResolverService implements Serializable { params.registry = registryConfig.registry ?: "192.168.10.102:8001" params.registryCredential = registryConfig.registryCredential ?: "xiaomayi-hub-credentials" params.registryProject = registryConfig.registryProject ?: "xiaomayi-base" - params.registryType = registryConfig.registryType ?: "harbor" + params.registryType = registryConfig.registryType params.imageFullName = "${params.registry}/${params.registryProject}/${params.imageName}" script.echo "📦 目标镜像仓库: ${params.registry}" diff --git a/src/com/golang/DeploymentService.groovy b/src/com/golang/DeploymentService.groovy new file mode 100644 index 0000000..aef8089 --- /dev/null +++ b/src/com/golang/DeploymentService.groovy @@ -0,0 +1,244 @@ +package com.golang + +import com.golang.stages.BuildService +import com.golang.stages.ContainerBuildService +import com.golang.stages.DeployService +import com.golang.stages.HealthCheckService +import com.golang.stages.RollbackService + +/** + * 部署服务类 - CI/CD流水线核心协调器 + * 负责执行各个CI/CD阶段的业务逻辑,不直接创建Jenkins stage + * 提供模块化的构建、部署、验证功能,作为流水线步骤的抽象层 + * + * 设计原则: + * - 单一职责:每个方法只负责一个特定的功能 + * - 错误隔离:每个阶段的异常不会影响其他阶段 + * - 可重用性:方法可以在不同的流水线中重用 + * - 可扩展性:易于添加新的部署阶段或功能 + * - 可维护性:清晰的代码结构和职责分离 + * + * 主要职责: + * 1. 协调各个部署阶段服务的执行 + * 2. 提供统一的参数传递接口 + * 3. 处理阶段间的依赖关系 + * 4. 提供错误处理和回滚机制 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeploymentService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储环境特定的配置参数,如服务器列表、环境变量等 + * 类型: Map 键值对配置映射 + */ + def envConfig + + // ========================================================================== + // 各个阶段的服务实例(依赖注入) + // ========================================================================== + + /** + * 构建阶段服务实例 + * 负责Maven项目的编译和打包操作 + * 类型: BuildService 专门处理构建逻辑的服务类 + */ + def buildService + + /** + * 容器构建阶段服务实例 + * 负责Docker镜像的构建和推送操作 + * 类型: ContainerBuildService 专门处理容器构建逻辑的服务类 + */ + def containerBuildService + + /** + * 部署阶段服务实例 + * 负责将应用部署到目标服务器 + * 类型: DeployService 专门处理部署逻辑的服务类 + */ + def deployService + + /** + * 健康检查阶段服务实例 + * 负责验证部署的应用是否正常运行 + * 类型: HealthCheckService 专门处理健康检查逻辑的服务类 + */ + def healthCheckService + + /** + * 回滚阶段服务实例 + * 负责在失败时执行回滚操作 + * 类型: RollbackService 专门处理回滚逻辑的服务类 + */ + def rollbackService + + /** + * 构造函数 - 初始化部署服务 + * 创建各个阶段的服务实例,建立服务依赖关系 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在各个服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:服务器列表、环境变量、部署策略等配置 + * - 示例:[ + * servers: [[serverIP: "192.168.1.1", serverPort: 22]], + * envVars: [JAVA_OPTS: "-Xms1g -Xmx1g"], + * deploymentStrategy: "rolling" + * ] + */ + DeploymentService(script, envConfig = [:]) { + // 初始化基础属性 + this.script = script + this.envConfig = envConfig + + // 初始化各个阶段服务(依赖注入模式) + // 通过分离关注点,提高代码的可测试性和可维护性 + this.buildService = new BuildService(script, envConfig) + this.containerBuildService = new ContainerBuildService(script, envConfig) + this.deployService = new DeployService(script, envConfig) + this.healthCheckService = new HealthCheckService(script, envConfig) + this.rollbackService = new RollbackService(script, envConfig) + + script.echo "🔧 DeploymentService 初始化完成" + } + + /** + * 执行构建阶段逻辑 + * 委托给BuildService处理Maven项目的编译和打包 + * 这是CI/CD流水线的第一个阶段,负责源代码到产物的转换 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - MAVEN_SET: String - Maven设置文件ID + * - PROFILES: String - Maven构建环境profiles + * - jarFilePath: String - JAR文件输出路径 + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认true) + * - goals: String - Maven执行目标(默认"clean package") + * - pomFilePath: String - POM文件路径(默认pom.xml) + * + * @throws Exception 当Maven构建失败时抛出异常,中断流水线 + * + * @example 使用示例: + * def params = [ + * MAVEN_SET: "nexus-maven-dev", + * PROFILES: "dev", + * jarFilePath: "target", + * jarFile: "app.jar" + * ] + * deploymentService.executeBuildStage(params) + */ + def executeBuildStage(params) { + // ☕ 开始构建阶段... + buildService.executeBuildStage(params) + } + + /** + * 执行容器构建阶段逻辑 + * 委托给ContainerBuildService处理Docker镜像的构建和推送 + * 这是CI/CD流水线的第二个阶段,负责应用容器化 + * + * @param params 管道参数Map,包含镜像构建所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址 + * - registryCredential: String - Jenkins仓库认证凭证ID + * - registryProject: String - 仓库项目名称 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * 可选字段: + * - buildOptions: String - Docker构建选项 + * - baseImage: String - 基础镜像名称 + * + * @return String 构建的镜像标签(如:dev-123、prod-456) + * @throws Exception 当Docker构建或推送失败时抛出异常 + * + * @example 使用示例: + * def imageTag = deploymentService.executeContainerBuildStage(params) + * echo "镜像标签: ${imageTag}" + */ + def executeContainerBuildStage(params) { + // 🐳 开始容器构建阶段... + return containerBuildService.executeContainerBuildStage(params) + } + + /** + * 执行部署阶段逻辑 + * 委托给DeployService处理应用部署到目标服务器 + * 这是CI/CD流水线的第三个阶段,负责将容器部署到环境 + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servers: List - 服务器列表数组 + * - deployServer: String - 部署服务器引用 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * 可选字段: + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * + * @throws Exception 当部署失败时抛出异常,支持自动回滚 + * + * @note 支持多种部署模式: + * - 单服务器部署:deployServer = "servers[0]" + * - 多服务器部署:deployServer = "servers[0,1,2]" + * - 自动选择:deployServer = "auto" + */ + def executeDeployStage(params) { + // 🚀 开始部署阶段... + deployService.executeDeployStage(params) + } + + /** + * 执行健康检查阶段逻辑 + * 委托给HealthCheckService验证部署的应用是否正常运行 + * 这是CI/CD流水线的第四个阶段,负责部署后验证 + * + * @param params 管道参数Map,包含健康检查所需的所有配置信息,通常包含: + * - serverHosts: List - 服务器列表(可选) + * - serverIP01: String - 单个服务器IP(可选) + * - port: Integer - 服务端口号 + * - healthCheckPath: String - 健康检查端点路径 + * - healthCheckTimeout: Integer - 健康检查超时时间 + * + * @throws Exception 当健康检查失败时抛出异常,触发回滚机制 + * + * @note 健康检查失败时会自动触发回滚操作 + */ + def executeHealthCheckStage(params) { + // ❤️ 开始健康检查阶段... + healthCheckService.executeHealthCheckStage(params) + } + + /** + * 执行回滚操作 + * 委托给RollbackService处理部署失败时的回滚操作 + * 这是CI/CD流水线的安全机制,确保失败时能够恢复到稳定状态 + * + * @param params 管道参数Map,包含回滚所需的所有配置信息,通常包含: + * - serverHosts: List - 需要回滚的服务器列表 + * - serverIP01: String - 单个服务器IP + * - serverPort: Integer - 服务器端口 + * - projectName: String - 项目名称 + * - previousVersion: String - 上一个版本号(可选) + * + * @note 这是一个私有方法,通常在部署或健康检查失败时自动调用 + * @note 回滚操作本身也可能会失败,需要手动介入处理 + */ + def executeRollbackStage(params) { + // ↩️ 开始回滚操作... + rollbackService.executeRollbackStage(params) + } + +} \ No newline at end of file diff --git a/src/com/golang/ResolverService.groovy b/src/com/golang/ResolverService.groovy new file mode 100644 index 0000000..f6fb5c1 --- /dev/null +++ b/src/com/golang/ResolverService.groovy @@ -0,0 +1,857 @@ +package com.golang + +import com.utils.CommonUtils +import com.constants.ConfigConstants +import com.utils.ServerParser + +/** + * JDK API 服务类 + * 负责加载配置、管理分支和服务配置、设置环境参数 + * 适配新的配置文件格式,支持多环境、多服务配置 + * + * 主要功能: + * 1. 加载环境配置和主配置文件 + * 2. 解析分支和服务配置 + * 3. 设置Jenkins流水线参数 + * 4. 提供环境相关的配置获取方法 + * + * @version 2.0.0 + * @since 2025-09-09 + */ +class ResolverService implements Serializable { + + // Jenkins流水线脚本对象,用于执行Jenkins相关操作 + def script + + // 主配置文件内容(解析后的YAML数据) + def config + + // 组织名称 + def orgName + + // 环境配置文件内容 + def envConfig + + // 主配置文件路径 + def jobConfigFile + + /** + * 服务器解析器实例 + * 负责解析服务器引用配置和索引处理 + * 类型: ServerParser 自定义服务器解析工具类 + */ + def serverParser + + /** + * 构造函数 + * 初始化服务类实例 + * + * @param script Jenkins流水线脚本对象,提供echo、error等方法 + * @param orgName 组织名称,用于标识项目归属 + */ + ResolverService(script, orgName) { + this.script = script + this.orgName = orgName + this.config = [:] + this.envConfig = [:] + // 初始化服务器解析器,用于处理服务器引用和索引 + this.serverParser = new ServerParser(script) + } + + /** + * 加载所有配置文件 + * 按顺序加载环境配置和主配置文件 + * + * @throws Exception 如果配置文件加载失败 + */ + void loadConfigurations() { + script.echo "🚀 开始加载配置文件..." + + // 首先加载环境配置文件(.env.yml) + loadEnvConfig() + + // 然后加载主配置文件(根据环境配置中的路径) + loadJobConfig() + + script.echo "✅ 所有配置文件加载完成" + } + + /** + * 加载环境配置文件的方法 + * 从.jenkins/.env.yml读取环境配置,包含主配置文件路径等基本信息 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件存在但解析失败 + */ + def loadEnvConfig() { + try { + // 环境配置文件相对路径 + def envFilePath = '.jenkins/.env.yml' + script.echo "📁 环境配置文件路径: ${envFilePath}" + + if (script.fileExists(envFilePath)) { + // 读取并解析YAML格式的环境配置文件 + envConfig = script.readYaml(file: envFilePath) + + // 从环境配置中获取主配置文件路径 + jobConfigFile = ".pipeline-files/config-file/" + envConfig.jobConfigFile + script.echo "✅ 成功加载环境配置文件" + + // 打印所有环境配置项(调试用) + if (!envConfig.isEmpty()) { + script.echo "📋 环境配置内容:" + envConfig.each { key, value -> + script.echo " ${key}: ${value}" + } + } + } else { + script.echo "⚠️ 警告: 未找到环境配置文件 .env.yml,使用默认配置" + } + } catch (Exception e) { + script.echo "⚠️ 警告: .env.yml 文件无效,使用默认配置: ${e.message}" + // 不抛出异常,允许继续执行 + } + + return this + } + + /** + * 加载主配置文件的方法 + * 读取新的配置文件格式,包含common、branchConfig、serviceConfig等部分 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件不存在或解析失败 + */ + def loadJobConfig() { + try { + // 检查主配置文件是否存在 + if (!script.fileExists(jobConfigFile)) { + script.error("❌ 错误: 配置文件不存在: ${jobConfigFile},请检查文件路径或创建配置文件") + } + + // 读取并解析YAML格式的主配置文件 + this.config = script.readYaml(file: jobConfigFile) + script.echo "✅ 成功加载主配置文件: ${jobConfigFile}" + + // 打印配置详情(调试用) + printConfigSummary() + + } catch (Exception e) { + script.error("❌ 错误: 加载配置文件失败: ${jobConfigFile}. 错误详情: ${e.message}") + } + + return this + } + + /** + * 打印主配置文件的详细内容 + * 格式化输出所有配置信息,便于调试和验证 + */ + def printConfigSummary() { + script.echo "=" * 70 + script.echo "📊 主配置文件详细内容" + script.echo "=" * 70 + + if (!this.config) { + script.echo "⚠️ 配置文件内容为空" + return + } + + // 打印通用配置信息 + if (this.config.common) { + script.echo "🌐 通用配置:" + script.echo "-" * 50 + script.echo " 部署脚本: ${this.config.common.deployScript ?: '未设置'}" + script.echo " 部署路径: ${this.config.common.deployPath ?: '未设置'}" + + // 打印注册表配置 + if (this.config.common.registry) { + script.echo " 注册表配置:" + this.config.common.registry.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + script.echo "" + } + + // 打印环境配置信息 + if (this.config.common?.profiles) { + script.echo "🏭 环境配置 (共 ${this.config.common.profiles.size()} 个环境):" + script.echo "-" * 50 + + this.config.common.profiles.each { envName, envConfig -> + script.echo " ${envName.toUpperCase()} 环境:" + script.echo " 服务器数量: ${envConfig.servers ? envConfig.servers.size() : 0}" + script.echo " 部署策略: ${envConfig.deploymentStrategy ?: 'rolling'}" + script.echo " JVM参数: ${envConfig.envVars?.JAVA_OPTS ?: '未设置'}" + script.echo " 健康检查: ${envConfig.healthCheckPath ?: '/actuator/health'}" + script.echo "" + } + } + + // 打印分支构建配置信息 + if (this.config.branchConfig) { + script.echo "🌿 分支构建配置 (共 ${this.config.branchConfig.size()} 个分支):" + script.echo "-" * 50 + + this.config.branchConfig.each { branchName, branchConfig -> + script.echo " ${branchName} 分支:" + script.echo " Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + } + } + + // 打印服务配置信息 + if (this.config.serviceConfig) { + script.echo "🔧 服务配置 (共 ${this.config.serviceConfig.size()} 个服务):" + script.echo "-" * 50 + + this.config.serviceConfig.each { serviceName, serviceConfig -> + script.echo " ${serviceName} 服务:" + script.echo " 编程语言: ${serviceConfig.programLang ?: '未设置'}" + script.echo " 语言版本: ${serviceConfig.programLangVersion ?: '未设置'}" + script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " 服务端口: ${serviceConfig.port ?: '未设置'}" + } + } + + script.echo "=" * 70 + } + + /** + * 获取当前分支的配置 + * 根据当前Jenkins作业的分支名称查找对应的分支配置 + * + * @return 分支配置对象(Map类型) + * @throws Exception 如果分支配置不存在 + */ + def getBranchConfig() { + // 获取当前Jenkins作业的分支名称 + def branchName = script.env.BRANCH_NAME + + if (!branchName) { + script.error("❌ 错误: 无法获取分支名称,env.BRANCH_NAME 为空。请检查流水线配置") + } + + script.echo "🌿 正在查找分支配置: ${branchName}" + + // 检查分支配置是否存在 + if (!config.branchConfig || config.branchConfig.isEmpty()) { + script.error("❌ 错误: 分支配置列表为空或未定义。请检查配置文件结构") + } + + // 根据分支名称获取配置 + def branchConfig = config.branchConfig[branchName] + + if (branchConfig) { + script.echo "✅ 成功匹配分支配置:" + script.echo " - Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " - 构建参数: ${branchConfig.buildArgs ?: '默认参数'}" + } else { + // 构建友好的错误信息,列出所有可用分支 + def availableBranches = config.branchConfig.keySet().join(', ') + script.error("❌ 错误: 找不到分支 '${branchName}' 的配置。可用分支: ${availableBranches}") + } + + return branchConfig + } + + /** + * 获取指定服务的配置信息 + * 根据服务名称从serviceConfig中查找对应的服务配置 + * + * @param serviceName 服务名称,需要与配置文件中serviceConfig的键匹配 + * @return 服务配置对象(Map类型) + * @throws Exception 如果服务配置不存在 + */ + def getServiceConfig(serviceName) { + if (!serviceName) { + script.error("❌ 错误: 服务名称参数不能为空") + } + + script.echo "🔧 正在查找服务配置: ${serviceName}" + + // 检查服务配置是否存在 + if (!config.serviceConfig || config.serviceConfig.isEmpty()) { + script.error("❌ 错误: 服务配置列表为空或未定义,请检查配置文件结构") + } + + // 根据服务名称获取配置 + def serviceConfig = config.serviceConfig[serviceName] + + if (serviceConfig) { + script.echo "✅ 成功找到服务配置: ${serviceName}" + script.echo " - 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " - 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " - 服务端口: ${serviceConfig.port ?: '未设置'}" + } else { + // 构建友好的错误信息,列出所有可用服务 + def availableServices = config.serviceConfig.keySet().join(', ') + script.error("❌ 错误: 找不到服务 '${serviceName}' 的配置。可用服务: ${availableServices}") + } + + return serviceConfig + } + + /** + * 设置管道参数 + * 整合分支配置、服务配置和环境配置,生成完整的流水线参数 + * + * @return 参数映射表,包含所有必要的部署参数 + */ + def setupParameters() { + script.echo "⚙️ 开始设置管道参数..." + + // 获取当前分支配置 + def branchConfig = getBranchConfig() + + // 从Jenkins作业名称中提取服务名称 + // 格式通常为: "组织名/服务名" 或 "服务名" + def serviceName = script.env.JOB_NAME?.tokenize('/')?.get(0) + script.echo "📦 服务名称: ${serviceName}" + + // 获取服务配置 + def serviceConfig = getServiceConfig(serviceName) + + // 根据分支类型获取环境配置 + def profiles = getBranchProfiles(branchConfig) + script.echo "🌍 环境配置: ${profiles}" + + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = CommonUtils.generateImageTag(script.env.PROFILES, script.env.BUILD_ID) + script.echo "自定义镜像标签:${imageTag}" + + // 构建完整的参数映射表 + def params = [ + // 构建相关参数 + jenkinsAgent : branchConfig.jenkinsAgent, + dockerFile : branchConfig.docker?.dockerFile ?: '.jenkins/Dockerfile', + buildOptions : branchConfig.docker?.buildOptions, + + // 服务相关参数 + goVersion : "${serviceConfig.programLang}:${serviceConfig.programLangVersion}", + projectName : serviceConfig.containerName, + packageName : serviceConfig?.packageName, + servicePath : serviceConfig.servicePath, + mainFile : serviceConfig.mainFile, + goFile : serviceConfig.goFile, + goFilePath : "${serviceConfig.servicePath}", + port : serviceConfig.port, + deployServer : serviceConfig.deployServer, + composeFile : serviceConfig.composeFile, + + // 环境相关参数 + PROFILES : profiles.default_profile, + availableProfiles : profiles.available_profiles, + imageTags : [imageTag], + + + // Git配置 + gitUrl : config.common?.git?.url, + gitCredentialsId : config.common?.git?.credentialsId, + gitBranch : script.env.BRANCH_NAME, + + // 钉钉配置 + dingTalkCredentialsId: config.common?.dingtalk?.credentialsId, + + // 其他参数 + envConfig : this.envConfig, + orgName : this.orgName, + branchConfig : branchConfig, + serviceConfig : serviceConfig + ] + + // 获取分支环境变量配置 + getEnvironmentConfig(params) + + // 设置容器基础镜像 + params.baseImage = "${params.registry}/${params.registryProject}-base/${params.registryImage}" + + script.echo "基础参数:${params}" + + // 设置Jenkins参数(仅对标准流水线) + setupJenkinsParameters(profiles.available_profiles as List) + + script.echo "✅ 管道参数设置完成" + + // 返回参数 + return params + } + + /** + * 获取指定环境的全部配置参数 + * 从通用配置中提取指定环境的所有参数,包括服务器列表、环境变量、部署策略等 + * + * @param params 参数映射表(会被修改),用于存储提取的配置信息 + * @return 包含指定环境全部参数的Map + */ + def getEnvironmentConfig(params) { + try { + // 从参数中获取环境名称,如果没有设置则使用默认值"dev" + // 使用Elvis操作符 ?: 提供默认值,避免空指针异常 + def envName = params.PROFILES ?: "dev" + + // 将环境名称转换为小写,确保与配置中的key匹配(配置通常使用小写) + // 例如:将"DEV"转换为"dev","Prod"转换为"prod" + def envKey = envName.toLowerCase() + + script.echo "🔍 开始获取 ${envKey} 环境的全部配置参数..." + + // ========================================================================== + // 添加通知配置 + // ========================================================================== + if (config.common?.notification) { + params.notification = config.common.notification + script.echo "📧 通知配置已加载" + } + + // ========================================================================== + // 添加短信配置 - 详细配置信息 + // ========================================================================== + if (config.common?.sms) { + // 确保sms配置存在 + params.sms = config.common.sms + + script.echo "📱 短信服务配置详情:" + script.echo " - 服务商: ${params.sms.provider ?: '未设置'}" + script.echo " - 凭据ID: ${params.sms.credentialsId ?: '未设置'}" + script.echo " - 默认签名: ${params.sms.signName ?: '未设置'}" + + // 打印所有模板配置 + if (params.sms.templates) { + script.echo " - 可用模板:" + params.sms.templates.each { templateName, templateConfig -> + def templateCode = templateConfig.templateCode ?: '未设置' + def templateSign = templateConfig.signName ?: params.sms.signName ?: '未设置' + script.echo " 📋 ${templateName}: ${templateCode} (签名: ${templateSign})" + } + } else { + script.echo " - 模板: 未配置" + } + } else { + script.echo "📱 短信服务: 未配置" + } + + // ========================================================================== + // 添加构建策略配置 + // ========================================================================== + if (config.common?.buildStrategy) { + params.buildStrategy = config.common.buildStrategy + script.echo "⚡ 构建策略:" + script.echo " - 重试次数: ${params.buildStrategy.retryCount}" + script.echo " - 超时时间: ${params.buildStrategy.timeoutMinutes}分钟" + script.echo " - 启用缓存: ${params.buildStrategy.enableCaching}" + } + + // 安全地获取环境配置 - 使用Map方式访问 + // 使用安全导航操作符 ?. 避免空指针异常: + // - config.common?:如果config为null则返回null,不会抛出异常 + // - .profiles?:如果common为null则返回null + // - ."${envKey}":动态访问指定环境的配置 + def envConfig = config.common?.profiles?."${envKey}" + if (!envConfig) { + def availableProfiles = config?.common?.profiles?.keySet() ?: [] + throw new Exception("未找到 ${envKey} 环境的配置,可用环境: ${availableProfiles.join(', ')}") + } + + script.echo "✅ 找到 ${envKey} 环境配置,开始提取参数..." + + // ========================================================================== + // 提取服务器配置 + // ========================================================================== + + // 检查环境配置是否存在且包含服务器列表,并且服务器列表不为空 + // envConfig?.servers:安全访问servers属性 + // !envConfig.servers.empty:检查servers列表是否非空 + if (envConfig?.servers && !envConfig.servers.empty) { + // 使用环境配置中的服务器列表更新参数对象 + // 将找到的服务器列表赋值给params.servers,供后续部署使用 + params.servers = envConfig.servers + + // 输出日志信息,显示成功找到服务器配置 + // 使用表情符号增强可读性,显示服务器数量 + script.echo "🖥️ 服务器配置: 共 ${envConfig.servers.size()} 台服务器" + + // 打印服务器详情(用于调试和验证) + // 使用eachWithIndex迭代服务器列表,同时获取索引和服务器对象 + envConfig.servers.eachWithIndex { server, index -> + // 输出每台服务器的详细信息 + // ${index + 1}:显示序号(从1开始) + // ${server.serverName}:服务器名称标识 + // ${server.serverIP}:${server.serverPort}:服务器的IP地址和端口号 + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + // 如果没有找到对应环境的服务器配置或服务器列表为空 + // 输出警告信息,提示用户检查配置 + script.echo "⚠️ 警告: 未找到 ${envName} 环境的服务器配置" + params.servers = [] + + // 注意:这里没有设置params.servers,调用方需要处理这种情况 + // 可能的处理方式:使用默认服务器、终止部署流程或提示用户输入 + } + + // ========================================================================== + // 提取目标服务器配置 + // ========================================================================== + + // 解析部署服务器引用,获取目标服务器列表 + // 使用ServerParser处理复杂的服务器引用表达式 + def targetServers = serverParser.parseTargetServers(params) + params.targetServers = targetServers + script.echo "🎯 部署目标服务器数量: ${targetServers.size()}" + + // ========================================================================== + // 提取环境变量配置 + // ========================================================================== + if (envConfig?.envVars && !envConfig.envVars.empty) { + params.envVars = envConfig.envVars + script.echo "🌡️ 环境变量: 共 ${envConfig.envVars.size()} 个" + envConfig.envVars.each { key, value -> + script.echo " ${key}=${value}" + } + } else { + script.echo "⚠️ 警告: ${envKey} 环境没有配置环境变量" + params.envVars = [:] + } + + // ========================================================================== + // 提取部署策略配置 + // ========================================================================== + if (envConfig?.deploymentStrategy) { + params.deploymentStrategy = envConfig.deploymentStrategy + script.echo "🎯 部署策略: ${envConfig.deploymentStrategy}" + } + + if (envConfig?.deploymentTimeout) { + params.deploymentTimeout = envConfig.deploymentTimeout + script.echo "⏱️ 部署超时: ${envConfig.deploymentTimeout} 秒" + } + + if (envConfig?.maxParallel) { + params.maxParallel = envConfig.maxParallel + script.echo "🔀 最大并行数: ${envConfig.maxParallel}" + } + + if (envConfig?.retryAttempts) { + params.retryAttempts = envConfig.retryAttempts + script.echo "🔄 重试次数: ${envConfig.retryAttempts}" + } + + if (envConfig?.canaryPercentage) { + params.canaryPercentage = envConfig.canaryPercentage + script.echo "📊 金丝雀比例: ${envConfig.canaryPercentage}%" + } + + // ========================================================================== + // 提取健康检查配置 + // ========================================================================== + if (envConfig?.healthCheckEnabled) { + params.healthCheckEnabled = envConfig.healthCheckEnabled + script.echo "❤️ 启用健康检查: ${envConfig.healthCheckEnabled}" + } + + if (envConfig?.healthCheckPath) { + params.healthCheckPath = envConfig.healthCheckPath + script.echo "❤️ 健康检查路径: ${envConfig.healthCheckPath}" + } + + if (envConfig?.healthCheckTimeout) { + params.healthCheckTimeout = envConfig.healthCheckTimeout + script.echo "⏱️ 健康检查超时: ${envConfig.healthCheckTimeout} 秒" + } + + if (envConfig?.healthCheckRetries) { + params.healthCheckRetries = envConfig.healthCheckRetries + script.echo "❤️ 健康检查重试次数: ${envConfig.healthCheckRetries}" + } + + if (envConfig?.healthCheckInterval) { + params.healthCheckInterval = envConfig.healthCheckInterval + script.echo "🔄 健康检查间隔: ${envConfig.healthCheckInterval} 秒" + } + + if (envConfig?.successThreshold) { + params.successThreshold = envConfig.successThreshold + script.echo "✅ 成功阈值: ${envConfig.successThreshold} 次" + } + + // ========================================================================== + // 提取回滚配置(生产环境特有) + // ========================================================================== + if (envConfig?.rollbackEnabled != null) { + params.rollbackEnabled = envConfig.rollbackEnabled + script.echo "↩️ 自动回滚: ${envConfig.rollbackEnabled}" + } + + if (envConfig?.rollbackOnFailure != null) { + params.rollbackOnFailure = envConfig.rollbackOnFailure + script.echo "❌ 失败时回滚: ${envConfig.rollbackOnFailure}" + } + + if (envConfig?.rollbackTimeout) { + params.rollbackTimeout = envConfig.rollbackTimeout + script.echo "⏱️ 回滚超时: ${envConfig.rollbackTimeout} 秒" + } + + // // ========================================================================== + // // 提取通知配置(生产环境特有) + // // ========================================================================== + // if (envConfig?.notifications) { + // params.notifications = envConfig.notifications + // script.echo "📧 通知配置:" + // if (envConfig.notifications.slackChannel) { + // script.echo " Slack频道: ${envConfig.notifications.slackChannel}" + // } + // if (envConfig.notifications.email) { + // script.echo " 邮件地址: ${envConfig.notifications.email}" + // } + // if (envConfig.notifications.recipients) { + // script.echo " 额外收件人: ${envConfig.notifications.recipients.join(', ')}" + // } + // script.echo " 通知时机: 开始=${envConfig.notifications.onStart}, 成功=${envConfig.notifications.onSuccess}, 失败=${envConfig.notifications.onFailure}" + // } + + // ========================================================================== + // 添加通用配置 + // ========================================================================== + params.deployScript = config.common?.deployScript + params.deployPath = config.common?.deployPath + + script.echo "📜 部署脚本: ${params.deployScript}" + script.echo "📁 部署路径: ${params.deployPath}" + + // ========================================================================== + // 添加镜像仓库配置 + // ========================================================================== + if (config.common?.registrys) { + params.registrys = config.common?.registrys + script.echo "🐳 镜像仓库配置:" + params.registrys.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + + // ========================================================================== + // 添加环境分支镜像仓库配置 + // ========================================================================== + def registryConfig = config.common?.registrys?."${envConfig.registry}" + params.registry = registryConfig?.registry + params.registryCredential = registryConfig?.registryCredential + params.registryProject = registryConfig?.registryProject + params.registryImage = registryConfig?.registryImage + params.registryType = registryConfig.registryType + params.imageFullName = "${params.registry}/${params.registryProject}/${params.projectName}" + + script.echo "✅ ${envKey} 环境全部参数获取完成" + + // 返回配置 + return params + } catch (Exception e) { + script.echo "❌ 获取环境配置失败: ${e.message}" + throw e + } + } + + /** + * 设置 Jenkins 作业参数 + * 为标准流水线作业创建环境选择参数和服务器选择参数 + * 根据可用环境配置动态生成下拉选择框,支持环境和服务器的两级选择 + * + * @param availableProfiles 可用的环境配置列表 + * - 类型: List 环境名称列表(如:['dev', 'test', 'prod']) + * - 来源: 通常从配置文件中解析得到的环境配置键值 + * - 要求: 列表不能为空,且必须与配置文件中的环境名称匹配 + * + * @note 在多分支流水线中会自动跳过参数设置(因为多分支通常根据分支自动确定环境) + * @note 在标准流水线中会创建用户可交互的选择参数 + * + * @example 生成的参数: + * - PROFILES: 环境选择下拉框(dev/test/prod) + * - SERVERS: 服务器选择下拉框(根据选择的环境动态显示对应服务器) + */ + private void setupJenkinsParameters(List availableProfiles) { + // 检查可用环境列表是否为空 + if (!availableProfiles || availableProfiles.isEmpty()) { + script.echo "⚠️ 可用环境配置列表为空,跳过参数设置" + return + } + + // 获取当前分支名称,用于判断流水线类型 + def branchName = script.env.BRANCH_NAME ?: '' + + // 判断是否为多分支流水线(分支名通常包含斜杠,如: feature/xxx) + boolean isMultibranchPipeline = branchName.contains('/') + + if (isMultibranchPipeline) { + script.echo "🔧 检测到多分支流水线,跳过参数设置(分支: ${branchName})" + script.echo "💡 多分支流水线通常根据分支名称自动确定目标环境" + return + } + + script.echo "🔧 标准流水线作业,设置部署环境参数" + script.echo "📋 可用环境选项: ${availableProfiles.join(', ')}" + + try { + // ========================================================================== + // 创建环境选择参数 + // ========================================================================== + def profileParameter = script.choice( + choices: availableProfiles.join('\n'), // 下拉选项,每行一个选择项 + description: '请选择要部署的目标环境', // 参数描述,显示在Jenkins界面上 + name: 'PROFILES' // 参数名称,后续通过params.PROFILES访问 + ) + + // ========================================================================== + // 创建服务器选择参数(动态生成所有环境的服务器选项) + // ========================================================================== + def allServers = getAllServersForParameters(availableProfiles) + def serverChoices = generateServerChoices(allServers) + + def serverParameter = script.choice( + choices: serverChoices.join('\n'), // 服务器下拉选项 + description: '请选择要部署的目标服务器(格式:环境-服务器名-IP)', // 参数描述 + name: 'SERVERS' // 参数名称 + ) + + // ========================================================================== + // 设置Jenkins作业参数 + // ========================================================================== + script.properties([ + script.parameters([ + profileParameter, // 环境选择参数 + serverParameter // 服务器选择参数 + ]) + ]) + + script.echo "✅ 成功设置部署参数:" + script.echo " - PROFILES: 环境选择(${availableProfiles.size()} 个选项)" + script.echo " - SERVERS: 服务器选择(${serverChoices.size()} 个选项)" + + } catch (Exception e) { + script.echo "⚠️ 设置 Jenkins 参数失败: ${e.message}" + script.echo "💡 这可能是因为没有权限修改作业配置,或者参数格式不正确" + // 不抛出异常,避免影响后续流程,参数设置失败通常不会阻断部署 + } + } + + /** + * 获取所有环境的服务器信息用于参数生成 + * 从配置文件中提取每个环境的服务器列表,用于构建服务器选择参数 + * 自动过滤空字符串和无效的环境名称 + * + * @param availableProfiles 可用环境列表,可能包含空字符串或无效值 + * @return Map 各环境的服务器列表映射(只包含有效环境) + */ + private Map getAllServersForParameters(List availableProfiles) { + def allServers = [:] + + // 过滤掉空字符串和null值,只处理有效的环境名称 + def validProfiles = availableProfiles.findAll { profile -> + profile != null && profile.trim() != '' + } + + script.echo "🔍 有效环境列表: ${validProfiles.join(', ')}" + script.echo "🗑️ 已过滤空环境: ${availableProfiles.size() - validProfiles.size()} 个" + + validProfiles.each { profile -> + try { + // 安全地获取环境配置中的服务器列表 + // 使用安全导航操作符避免空指针异常 + def envConfig = config?.common?.profiles?."${profile}" + + if (envConfig?.servers) { + allServers[profile] = envConfig.servers + script.echo " 📡 ${profile} 环境: ${envConfig.servers.size()} 台服务器" + + // 可选:输出服务器详情用于调试 + envConfig.servers.eachWithIndex { server, index -> + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + script.echo " ⚠️ ${profile} 环境: 无服务器配置或服务器列表为空" + allServers[profile] = [] + } + } catch (Exception e) { + script.echo " ❌ 获取 ${profile} 环境服务器失败: ${e.message}" + script.echo " 🐛 错误详情: ${e.getStackTrace().find { it.contains('getAllServersForParameters') } ?: '未知位置'}" + allServers[profile] = [] + } + } + + // 检查是否有任何有效环境配置了服务器 + def environmentsWithServers = allServers.count { it.value && !it.value.empty } + script.echo "📊 统计: ${environmentsWithServers} 个环境配置了服务器,${allServers.size() - environmentsWithServers} 个环境无服务器" + + return allServers + } + + /** + * 生成服务器选择选项 + * 创建格式化的服务器选择项,便于用户识别和选择 + * + * @param allServers 所有环境的服务器映射 + * @return List 格式化后的服务器选项列表 + */ + private List generateServerChoices(Map allServers) { + def choices = [] + + // 添加默认选项 + choices.add("auto-自动选择(根据环境默认服务器)") + + // 为每个环境的每台服务器生成选项 + allServers.each { profile, servers -> + if (servers && !servers.empty) { + servers.each { server -> + // 格式化选项:环境-服务器名-IP:端口 + def displayName = "${profile}-${server.serverName}-${server.serverIP}:${server.serverPort}" + choices.add(displayName) + } + } else { + // 如果某个环境没有服务器,添加提示选项 + choices.add("${profile}-无可用服务器") + } + } + + return choices + } + + /** + * 根据分支配置获取对应的环境配置文件和默认配置文件 + * @param branchConfig 分支配置信息,包含分支类型等 + * @return Map 包含可用配置列表和默认配置 + */ + private def getBranchProfiles(branchConfig) { + // 获取当前分支名称 + def branchName = script.env.BRANCH_NAME ?: '' + + // 1. 优先从常量配置中获取该分支类型对应的可用配置 + def availableProfiles = ConfigConstants.BRANCH_PROFILES[branchName] + + // 2. 如果常量配置中没有找到,根据分支类型选择不同的默认配置源 + if (!availableProfiles) { + availableProfiles = (branchName == 'master') ? + envConfig.masterProfiles : // master分支使用特殊配置 + envConfig.Profiles // 其他分支使用通用配置 + } + + // 确保availableProfiles不为null,如果是null则转为空列表 + availableProfiles = availableProfiles ?: [] + + // 3. 获取默认配置,优先从常量配置中获取 + def defaultProfile = ConfigConstants.DEFAULT_PROFILES[branchName] + + // 4. 如果常量配置中没有默认配置,则从可用配置中选择 + if (!defaultProfile) { + defaultProfile = availableProfiles.find { it } ? // 查找第一个非空配置 + availableProfiles[0] : // 如果都为空则取第一个 + null // 如果没有可用配置则返回null + } + + return [ + available_profiles: availableProfiles, + default_profile : defaultProfile + ] + } + +} \ No newline at end of file diff --git a/src/com/golang/stages/BuildService.groovy b/src/com/golang/stages/BuildService.groovy new file mode 100644 index 0000000..0568fad --- /dev/null +++ b/src/com/golang/stages/BuildService.groovy @@ -0,0 +1,314 @@ +package com.golang.stages + +/** + * 构建阶段服务 - 负责Maven项目的编译和打包 + * + * 主要职责: + * - 执行Maven项目的清理、编译、打包操作 + * - 验证构建环境和配置的有效性 + * - 处理构建过程中的异常和错误 + * - 提供构建结果的状态反馈 + * + * 设计原则: + * - 单一职责:专注于Maven构建相关的逻辑 + * - 可重用性:可以在不同的流水线项目中重用 + * - 错误隔离:构建失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class BuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化构建服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:构建环境相关的配置参数 + */ + BuildService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + script.echo "🔧 BuildService 初始化完成" + } + + /** + * 执行构建阶段逻辑 + * 负责Maven项目的编译和打包,是CI/CD流水线的核心构建阶段 + * + * 主要步骤: + * 1. 输出构建开始信息和配置详情 + * 2. 验证环境配置的有效性 + * 3. 执行构建命令 + * 4. 处理构建结果和异常 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - MAVEN_SET: String - Maven设置文件ID(Jenkins Config File ID) + * - PROFILES: String - Maven构建环境profiles(如:dev, test, prod) + * 可选字段: + * - jarFilePath: String - JAR文件输出路径(默认:target) + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认:true) + * - goals: String - Maven执行目标(默认:"clean package") + * - pomFilePath: String - POM文件路径(默认:pom.xml) + * - orgName: String - 组织名称(用于JDK服务验证) + * - availableProfiles: List - 可用的环境profile列表 + * + * @throws Exception 当Maven构建失败时抛出异常,中断流水线 + * 异常信息包含详细的错误原因和建议解决方案 + * + * @example 使用示例: + * def buildParams = [ + * MAVEN_SET: "nexus-maven-dev", + * PROFILES: "dev", + * jarFilePath: "target", + * jarFile: "xiaomayi-admin.jar", + * skipTests: true, + * orgName: "xiaomayi", + * availableProfiles: ["dev", "test", "prod"] + * ] + * buildService.executeBuildStage(buildParams) + */ + def executeBuildStage(params) { + try { + // 输出构建开始信息和配置详情 + script.echo "🏗️ 开始构建阶段..." + + // 验证环境profile的有效性(确保配置的一致性) + validateProfile(params) + + // 执行构建命令 + // 使用buildProject方法执行实际构建 + script.echo "🔨 执行构建命令..." + buildProject(params) + + // 构建成功完成 + script.echo "✅ 代码构建完成" + + } catch (Exception e) { + // 构建失败处理 + script.echo "❌ 构建阶段失败: ${e.message}" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 验证环境profile有效性 + * 检查选择的部署环境是否在可用环境列表中,确保部署配置的正确性 + * + * 主要功能: + * - 验证必要参数的存在性和有效性 + * - 检查所选环境是否在可用环境列表中 + * - 提供详细的错误信息和调试建议 + * + * @param params 参数Map,必须包含以下字段: + * - PROFILES: String - 要验证的环境profile名称 + * - availableProfiles: List - 可用的环境profile列表 + * - orgName: String - 组织名称(用于日志和错误消息) + * + * @throws IllegalArgumentException 当缺少必要参数或参数无效时抛出 + * @throws Exception 当环境验证失败时抛出,包含详细的错误信息 + */ + private def validateProfile(params) { + script.echo "🔍 开始验证环境profile有效性..." + + // ========================================================================== + // 参数完整性检查 + // ========================================================================== + + // 检查PROFILES参数是否存在且不为空 + if (!params.PROFILES) { + def errorMsg = "缺少必要的PROFILES参数,请指定要部署的环境" + script.echo "❌ ${errorMsg}" + throw new IllegalArgumentException(errorMsg) + } + + // 检查可用环境列表是否存在且不为空 + if (!params.availableProfiles || params.availableProfiles.isEmpty()) { + def errorMsg = "缺少可用的环境profile列表,请检查配置文件中环境配置" + script.echo "❌ ${errorMsg}" + throw new IllegalArgumentException(errorMsg) + } + + // 检查组织名称参数(用于更详细的错误消息) + if (!params.orgName) { + script.echo "⚠️ 缺少组织名称参数orgName,使用默认标识" + // 这里不抛出异常,因为orgName不是核心必需参数 + } + + // ========================================================================== + // 环境有效性验证 + // ========================================================================== + + try { + def selectedProfile = params.PROFILES.trim() + def availableProfiles = params.availableProfiles.collect { it.toString().trim() } + + script.echo "📋 验证详情:" + script.echo " 🎯 选择的环境: ${selectedProfile}" + script.echo " 📊 可用环境: ${availableProfiles.join(', ')}" + + // 检查选择的环境是否在可用列表中 + if (!availableProfiles.contains(selectedProfile)) { + def errorMsg = "无效的部署环境: '${selectedProfile}'。可用环境: ${availableProfiles.join(', ')}" + // 使用script.error使构建标记为失败 + script.error("环境验证失败: ${errorMsg}") + } + + // 验证成功 + script.echo "✅ 环境profile验证通过: ${selectedProfile}" + } catch (Exception e) { + // 处理验证过程中的异常 + script.echo "❌ 环境profile验证过程中发生错误: ${e.message}" + script.echo "📋 可用环境列表: ${params.availableProfiles.join(', ')}" + + // 重新抛出异常,确保构建失败 + throw e + } + } + + /** + * 使用Go构建项目 + * 执行Go模块构建命令,包括依赖清理、模块整理、编译等操作,并显示构建结果信息 + * 支持自定义Go环境配置、构建参数等配置 + * + * @param params 构建参数Map,包含以下字段: + * - goVersion: String - 期望的Go版本(用于验证) + * - goProxy: String - Go代理设置(默认https://goproxy.cn,direct) + * - goModule: String - Go模块模式(默认on) + * - goFilePath: String - 可执行文件输出路径 + * - goFile: String - 生成的可执行文件名 + * - mainFile: String - 主程序文件(默认main.go) + * - buildFlags: String - 额外的go build编译标志 + * + * @throws Exception 当Go构建失败时抛出异常 + */ + def buildProject(params) { + // 从参数中获取Go版本,用于验证环境 + def goVersion = params.goVersion ?: "" + // 获取Go代理设置,默认使用国内代理 + def goProxy = params.goProxy ?: "https://goproxy.cn,direct" + // 获取Go模块模式,默认开启 + def goModule = params.goModule ?: "on" + // 获取可执行文件输出目录路径 + def goFilePath = params.goFilePath + // 获取生成的可执行文件名 + def goFile = params.goFile + // 主程序文件,默认为main.go + def mainFile = params.mainFile ?: "main.go" + // 额外的编译标志 + def buildFlags = params.buildFlags ?: "" + + script.echo "🔨 开始Go模块构建项目..." + script.echo "📋 构建参数:" + script.echo " 🔧 Go版本: ${goVersion}" + script.echo " 🌐 Go代理: ${goProxy}" + script.echo " 📦 模块模式: ${goModule}" + script.echo " 📁 输出路径: ${goFilePath}" + script.echo " 🏷️ 可执行文件: ${goFile}" + script.echo " 📄 主程序文件: ${mainFile}" + script.echo " 🚩 编译标志: ${buildFlags}" + + try { + script.echo "🔍 检查Go环境..." + // 检查Go版本 + script.sh "go version" + + // 验证Go版本(如果指定了期望版本) + if (goVersion) { + script.echo "✅ 验证Go版本兼容性..." + def currentVersion = script.sh(script: "go version | awk '{print \$3}'", returnStdout: true).trim() + script.echo " 当前版本: ${currentVersion}, 期望版本: ${goVersion}" + } + + script.echo "⚙️ 配置Go环境..." + // 设置Go模块模式 + script.sh "go env -w GO111MODULE=${goModule}" + + // 设置Go代理 + script.sh "go env -w GOPROXY=${goProxy}" + + // 显示Go环境配置 + script.sh "go env | grep -E '(GO111MODULE|GOPROXY|GOPATH|GOROOT)'" + + script.echo "📦 整理Go模块依赖..." + // 清理并整理依赖 + script.sh "go mod tidy" + + // 可选:验证依赖完整性 + script.sh "go mod verify" + + script.echo "🔨 开始编译Go程序..." + // 执行编译命令 + if (buildFlags) { + script.sh "go build ${buildFlags} -o ${goFile} ${mainFile}" + } else { + script.sh "go build -o ${goFile} ${mainFile}" + } + + // 移动文件到指定目录(如果输出路径不是当前目录) + if (goFilePath && goFilePath != ".") { + script.echo "📂 移动可执行文件到指定目录..." + script.sh "mkdir -p ${goFilePath}" + script.sh "mv ${goFile} ${goFilePath}/" + } + + script.echo "✅ Go构建完成" + + // 显示构建结果信息 + script.echo "📊 构建结果验证:" + + // 显示可执行文件的详细列表信息 + def fullPath = goFilePath ? "${goFilePath}/${goFile}" : goFile + script.sh "ls -lsh ${fullPath}" + + // 计算可执行文件的MD5校验和,用于验证文件完整性 + script.sh "md5sum ${fullPath}" + + // 显示文件大小(人类可读格式) + script.sh "du -h ${fullPath}" + + // 显示最后修改时间 + script.sh "stat ${fullPath}" + + // 可选:验证可执行文件的基本信息 + script.sh "file ${fullPath}" + + // 可选:检查文件类型和架构 + script.sh "ldd ${fullPath} 2>/dev/null || echo '静态编译或无法检查动态库'" + + } catch (Exception e) { + script.echo "❌ Go构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Go环境配置是否正确" + script.echo " - 网络连接是否正常(依赖下载)" + script.echo " - main.go文件路径是否正确" + script.echo " - 代码编译是否有语法错误" + script.echo " - 模块依赖是否完整" + throw e // 重新抛出异常,让调用方处理构建失败 + } + } + +} \ No newline at end of file diff --git a/src/com/golang/stages/ContainerBuildService.groovy b/src/com/golang/stages/ContainerBuildService.groovy new file mode 100644 index 0000000..999d1a2 --- /dev/null +++ b/src/com/golang/stages/ContainerBuildService.groovy @@ -0,0 +1,287 @@ +package com.golang.stages + +/** + * 容器构建阶段服务 - 负责Docker镜像的构建和推送 + * + * 主要职责: + * - 执行Docker镜像的构建、标签生成和推送操作 + * - 管理镜像标签的生命周期和环境变量设置 + * - 处理容器构建过程中的异常和错误 + * - 提供构建结果的状态反馈和镜像信息 + * + * 设计原则: + * - 单一职责:专注于Docker容器构建相关的逻辑 + * - 可重用性:可以在不同的流水线项目中重用 + * - 错误隔离:容器构建失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class ContainerBuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储容器构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化容器构建服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:镜像仓库配置、构建选项等 + */ + ContainerBuildService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + script.echo "🔧 ContainerBuildService 初始化完成" + } + + /** + * 执行容器构建阶段逻辑 + * 负责Docker镜像的构建和推送,是CI/CD流水线的容器化阶段 + * + * 主要步骤: + * 1. 输出容器构建开始信息 + * 2. 调用工具类执行Docker镜像构建和推送 + * 3. 存储镜像标签到环境变量供后续阶段使用 + * 4. 处理构建异常并提供详细的错误信息 + * + * @param params 管道参数Map,包含镜像构建所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) + * - registryCredential: String - Jenkins仓库认证凭证ID + * - registryProject: String - 仓库项目/命名空间名称(如:xiaomayi) + * - projectName: String - 项目名称(如:xiaomayi-admin) + * - PROFILES: String - 环境名称(dev/test/uat/prod) + * 可选字段: + * - buildOptions: String - Docker构建选项(如:--no-cache --pull) + * - baseImage: String - 基础镜像名称 + * - serverPort: String/Integer - 服务端口号 + * - jarFile: String - JAR文件名 + * - jarFilePath: String - JAR文件路径 + * + * @return String 构建的镜像标签(格式:{环境}-{构建ID} 或 {构建ID}) + * @throws Exception 当Docker构建或推送失败时抛出异常,包含详细的错误信息 + * + * @example 使用示例: + * def containerParams = [ + * registry: "192.168.10.102:8001", + * registryCredential: "xiaomayi-hub-credentials", + * registryProject: "xiaomayi", + * projectName: "xiaomayi-admin", + * PROFILES: "dev", + * buildOptions: "--no-cache", + * baseImage: "openjdk:17-jdk-alpine" + * ] + * def imageTag = containerBuildService.executeContainerBuildStage(containerParams) + */ + def executeContainerBuildStage(params) { + try { + script.echo "🐳 开始容器构建阶段..." + script.echo "📋 容器构建配置:" + script.echo " 📦 项目名称: ${params.projectName}" + script.echo " 🌍 目标环境: ${params.PROFILES}" + script.echo " 🏷️ 镜像仓库: ${params.registry}" + script.echo " 📁 命名空间: ${params.registryProject}" + + // 输出详细的构建选项(如果有) + if (params.buildOptions) { + script.echo " ⚙️ 构建选项: ${params.buildOptions}" + } + if (params.baseImage) { + script.echo " 🖼️ 基础镜像: ${params.baseImage}" + } + + // 构建Docker镜像并获取镜像标签 + // 使用buildDockerImage方法执行实际构建 + // 该方法会处理Docker构建、标签生成和镜像推送 + script.echo "🔨 执行Docker镜像构建..." + def imageTag = buildDockerImage(params) + + // 将镜像标签存储到环境变量,供后续部署阶段使用 + // 环境变量可以在整个Pipeline过程中共享使用 + script.env.IMAGE_TAG = imageTag + script.echo "💾 镜像标签已存储到环境变量: IMAGE_TAG=${imageTag}" + + script.echo "✅ 镜像构建完成" + script.echo "🎉 镜像标签: ${imageTag}" + script.echo "🚀 镜像已推送到仓库: ${params.registry}/${params.registryProject}/${params.projectName}:${imageTag}" + + return imageTag + + } catch (Exception e) { + // 容器构建失败处理 + script.echo "❌ 容器构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Dockerfile是否存在且语法正确" + script.echo " - 镜像仓库凭证是否有效" + script.echo " - 网络连接是否正常(镜像推送)" + script.echo " - 基础镜像是否可以访问" + script.echo " - 磁盘空间是否充足" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 构建Docker镜像 + * 使用Docker Pipeline插件构建和推送Docker镜像到指定的镜像仓库 + * 支持自定义构建参数、构建选项和自动清理本地镜像 + * + * @param params 构建参数Map,包含以下必需字段: + * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) + * - registryCredential: String - Jenkins凭证ID,用于仓库认证 + * - registryProject: String - 仓库中的项目/命名空间名称(如:xiaomayi) + * - projectName: String - 项目名称(如:xiaomayi-admin) + * - PROFILES: String - 环境名称(dev/test/uat/prod),用于生成镜像标签 + * 可选字段: + * - buildOptions: String - Docker构建选项(如:--no-cache --pull) + * - baseImage: String - 基础镜像名称,通过--build-arg传递 + * - port: String/Integer - 服务端口号,通过--build-arg传递 + * - jarFile: String - JAR文件名,通过--build-arg传递 + * - jarFilePath: String - JAR文件路径,通过--build-arg传递 + * + * @return String 构建的镜像标签(如:dev-123、prod-456) + * @throws Exception 当Docker构建或推送失败时抛出异常 + */ + def buildDockerImage(params) { + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = generateImageTag(params.PROFILES) + script.echo "🏷️ 生成的镜像标签: ${imageTag}" + + // 获取构建选项,如果未提供则使用空字符串 + def buildOptions = params.buildOptions ?: "" + script.echo "⚙️ 构建选项: ${buildOptions}" + + // 输出构建开始信息和详细参数 + script.echo "🐳 开始构建Docker镜像..." + script.echo " 📦 镜像仓库: ${params.registry}" + script.echo " 📁 命名空间: ${params.registryProject}" + script.echo " 🏷️ 项目名称: ${params.projectName}" + script.echo " 🎯 镜像标签: ${imageTag}" + script.echo " 🔑 仓库凭证: ${params.registryCredential}" + + try { + // 使用Docker Pipeline插件连接到镜像仓库并进行构建推送 + script.docker.withRegistry("https://${params.registry}", params.registryCredential) { + // 构建Docker镜像 + // 镜像名称格式: {命名空间}/{项目名称}:{标签} + def dockerImage = script.docker.build( + // 完整的镜像名称(包含标签) + "${params.registryProject}/${params.projectName}:${imageTag}", + // 构建命令参数组合 + "${buildOptions} " + + // 传递构建参数给Dockerfile + // 环境配置 + "--build-arg VAR_PROFILES=${params.PROFILES} " + + // 基础镜像 + "--build-arg VAR_BASE_IMAGE=${params.baseImage} " + + // 服务端口 + "--build-arg VAR_PORT=${params.port} " + + // JAR文件名 + "--build-arg VAR_JARFILE=${params.jarFile} " + + // JAR文件路径 + "--build-arg VAR_JARFILE_PATH=${params.jarFilePath} " + + // 指定Dockerfile路径和构建上下文,使用项目根目录下的.jenkins/Dockerfile文件 + "-f ${params.dockerFile} ./" + ) + + // 推送镜像到远程仓库 + script.echo "🚀 推送镜像到仓库: ${params.registry}" + dockerImage.push() + + // 清理本地Docker镜像,释放磁盘空间 + script.echo "🧹 开始清理本地镜像..." + cleanupDockerImages(params.registry, params.registryProject, params.projectName, imageTag) + } + + script.echo "✅ Docker镜像构建和推送完成" + return imageTag + + } catch (Exception e) { + script.echo "❌ Docker镜像构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Dockerfile路径是否正确" + script.echo " - 镜像仓库凭证是否有效" + script.echo " - 网络连接是否正常" + throw e // 重新抛出异常,让调用方处理 + } + } + + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + private def generateImageTag(profile) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID + } + + /** + * 清理Docker镜像 + * 删除本地Docker环境中指定的镜像,避免磁盘空间占用和镜像冲突 + * 主要用于构建后清理,移除不再需要的临时镜像 + * + * @param registry 镜像仓库地址(如:192.168.10.102:8001) + * @param registryProject 仓库中的项目/命名空间名称(如:xiaomayi) + * @param projectName 项目名称(如:xiaomayi-admin) + * @param imageTag 镜像标签(由generateImageTag方法生成) + * + * @note 使用"|| true"确保清理失败不会中断构建流程 + * @note 尝试清理两种格式的镜像: + * 1. {registryProject}/{projectName}:{imageTag} + * 2. {registry}/{registryProject}/{projectName}:{imageTag} + */ + private def cleanupDockerImages(registry, registryProject, projectName, imageTag) { + try { + // 输出清理开始信息 + script.echo "🧹 开始清理Docker镜像: ${projectName}:${imageTag}" + + // 清理第一种格式的镜像:{项目}/{应用}:{标签} + // 例如:xiaomayi/xiaomayi-admin:dev-123 + // 使用"|| true"确保命令即使失败也不会抛出异常中断脚本 + script.sh "docker rmi -f ${registryProject}/${projectName}:${imageTag} || true" + + // 清理第二种格式的镜像:{仓库地址}/{项目}/{应用}:{标签} + // 例如:192.168.10.102:8001/xiaomayi/xiaomayi-admin:dev-123 + script.sh "docker rmi -f ${registry}/${registryProject}/${projectName}:${imageTag} || true" + + script.echo "✅ Docker镜像清理完成" + + } catch (Exception e) { + // 捕获并处理异常,输出错误信息但不会中断主流程 + // 镜像清理失败通常不会影响主要功能,所以只是记录日志 + script.echo "⚠️ 清理Docker镜像失败: ${e.message}" + script.echo " 此错误通常可以忽略,不会影响部署流程" + } + } + +} \ No newline at end of file diff --git a/src/com/jdk/utils/PipelineUtils.groovy b/src/com/golang/stages/DeployService.groovy similarity index 51% rename from src/com/jdk/utils/PipelineUtils.groovy rename to src/com/golang/stages/DeployService.groovy index c3015b6..a0492dd 100644 --- a/src/com/jdk/utils/PipelineUtils.groovy +++ b/src/com/golang/stages/DeployService.groovy @@ -1,331 +1,522 @@ -package com.jdk.utils - -import com.constants.ConfigConstants +package com.golang.stages /** - * PipelineUtils 类 - Jenkins Pipeline 工具类 - * 实现 Serializable 接口的 Jenkins 共享库工具类,用于在 Pipeline 中提供可序列化的工具方法 + * 部署阶段服务 - 负责将Docker镜像部署到目标服务器 * - * 这个类设计为在 Jenkins Pipeline 中使用,特别是当需要在多个节点或重启后保持状态时 - * 实现 Serializable 接口是 Jenkins Pipeline 的要求,确保类实例可以在流水线执行过程中被序列化和反序列化 + * 主要职责: + * - 执行单服务器和多服务器的应用部署操作 + * - 解析服务器引用配置,确定部署目标 + * - 管理部署参数和环境配置 + * - 处理部署过程中的异常和错误 + * - 生成部署结果报告和状态反馈 * - * @see Serializable Java 序列化接口,允许对象状态保存和恢复 + * 设计原则: + * - 单一职责:专注于应用部署相关的逻辑 + * - 灵活性:支持单服务器和多服务器部署模式 + * - 错误隔离:部署失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * - 可扩展性:易于添加新的部署策略和功能 * - * 主要用途: - * - 封装常用的 Jenkins Pipeline 操作 - * - 管理环境特定的配置参数 - * - 提供可重用的部署和构建方法 - * - 支持流水线的暂停和恢复 - * - * 注意事项: - * - 所有字段都必须是可序列化的类型 - * - 避免使用非序列化的对象(如 Groovy 的闭包) - * - 确保包含的脚本对象也是可序列化的 + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 */ -class PipelineUtils implements Serializable { +class DeployService implements Serializable { /** - * Jenkins Pipeline 脚本对象引用 - * 用于在类方法中访问 Jenkins DSL(Domain Specific Language)方法 - * - * 类型: Object (通常是 Jenkins pipeline 的 script 对象) - * 序列化: 必须确保 script 对象是可序列化的 - * - * 用途: - * - 执行 shell 命令: script.sh("command") - * - 输出日志: script.echo("message") - * - 错误处理: script.error("error message") - * - 访问环境变量: script.env.VAR_NAME - * - * 示例: 在 Pipeline 中通过 `this` 传递当前脚本上下文 + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) */ def script /** - * 环境配置参数存储 - * 用于保存和管理环境特定的配置信息 - * - * 类型: Map 或具体的配置对象 - * 序列化: 必须使用可序列化的数据类型(String、Number、List、Map 等) - * - * 典型内容: - * - servers: 服务器列表配置 - * - envVars: 环境变量配置 - * - deploymentStrategy: 部署策略 - * - 其他环境相关的参数 - * - * 示例: - * [ - * servers: [[serverIP: "192.168.1.1", serverPort: 22]], - * envVars: [JAVA_OPTS: "-Xms1g -Xmx1g"], - * deploymentStrategy: "rolling" - * ] + * 环境配置信息 + * 存储部署环境特定的配置参数 + * 类型: Map 键值对配置映射 */ def envConfig /** - * PipelineUtils 构造函数 - * 初始化流水线工具类,用于在Jenkins Pipeline中提供各种工具方法和配置管理 - * 这是一个工具类的构造函数,负责初始化脚本对象和环境配置 + * 构造函数 - 初始化部署服务 * - * @param script Jenkins pipeline脚本对象(必填) - * - 类型: Object (通常是Jenkins pipeline的script对象) - * - 用途: 用于在类中访问Jenkins DSL方法,如sh、echo、error等 - * - 示例: 在Pipeline中通过`this`传递当前脚本上下文 + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 * - * @param envConfig 环境配置参数(可选) - * - 类型: Map 键值对配置映射 - * - 默认值: 如果未提供或为空,使用ConfigConstants.DEFAULT_ENV_CONFIG - * - 用途: 存储环境特定的配置信息,如服务器列表、部署参数等 - * - 示例: [servers: [...], envVars: [...], deploymentStrategy: "rolling"] - * - * @throws IllegalArgumentException 如果script参数为null或未提供时可能抛出异常 + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:部署策略、超时设置、健康检查等配置 */ - PipelineUtils(script, envConfig = [:]) { - // 设置脚本对象引用,用于在类方法中访问Jenkins DSL - // this.script 将在所有实例方法中可用,用于执行shell命令、输出日志等 + DeployService(script, envConfig = [:]) { this.script = script + this.envConfig = envConfig - // 设置环境配置,使用提供的配置或默认配置 - // 使用Elvis操作符 ?: 进行空值检查,确保envConfig不为null - // 如果envConfig为空Map或null,则使用默认配置 - this.envConfig = envConfig ?: ConfigConstants.DEFAULT_ENV_CONFIG - - // 输出初始化日志(可选,用于调试) - this.script.echo "🔧 初始化完成" - this.script.echo " 📝 脚本对象: ${script != null ? '已设置' : '未设置'}" - this.script.echo " ⚙️ 环境配置: 包含 ${this.envConfig.size()} 个配置项" + script.echo "🔧 DeployService 初始化完成" + script.echo " 📡 服务器解析器已就绪" } /** - * 使用Maven构建项目 - * 执行Maven构建命令,包括依赖清理、编译、打包等操作,并显示构建结果信息 - * 支持自定义Maven设置、构建环境、跳过测试等配置 + * 执行部署阶段逻辑 + * 负责将Docker镜像部署到目标服务器,支持单服务器和多服务器部署模式 + * 这是CI/CD流水线的核心部署阶段,负责将构建好的镜像部署到目标环境 * - * @param params 构建参数Map,包含以下字段: - * - MAVEN_SET: String - Maven设置文件ID(Jenkins中配置的Config File ID) - * - PROFILES: String - Maven构建环境profiles(如:dev, test, prod) - * - jarFilePath: String - JAR文件输出路径 - * - jarFile: String - 生成的JAR文件名 - * - skipTests: Boolean - 是否跳过测试(默认true,生产环境建议设为false) - * - goals: String - Maven执行目标(默认"clean package") - * - pomFilePath: String - POM文件路径(默认pom.xml) + * 主要步骤: + * 1. 输出部署开始信息和配置验证 + * 2. 解析服务器引用配置,确定部署目标 + * 3. 根据服务器数量选择部署模式(单机/多机) + * 4. 执行具体的部署操作 + * 5. 处理部署结果和异常情况 * - * @throws Exception 当Maven构建失败时抛出异常 + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servers: List - 服务器列表数组,包含所有可用的服务器配置 + * - deployServer: String - 部署服务器引用表达式 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称(dev/test/uat/prod) + * 可选字段: + * - deployScript: String - 部署脚本文件名(默认:deploy.sh) + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - deploymentTimeout: Integer - 部署超时时间(秒) + * - healthCheckPath: String - 健康检查端点路径 + * - healthCheckTimeout: Integer - 健康检查超时时间 + * - stopOnFailure: Boolean - 失败时是否停止部署 + * + * @throws Exception 当部署失败时抛出异常,包含详细的错误信息 + * 异常信息会区分单服务器和多服务器部署的失败情况 + * + * @example 使用示例: + * def deployParams = [ + * servers: config.common.profiles.dev.servers, + * deployServer: "servers[0,2]", // 部署到第1和第3台服务器 + * projectName: "xiaomayi-admin", + * PROFILES: "dev", + * deploymentStrategy: "rolling", + * deploymentTimeout: 3600 + * ] + * deployService.executeDeployStage(deployParams) */ - def buildMavenProject(params) { - // 从参数中获取Maven设置文件ID,用于指定Maven配置 - def mavenSettings = params.MAVEN_SET - // 获取构建环境配置,用于激活对应的Maven profile - def profiles = params.PROFILES - // 获取JAR文件输出目录路径 - def jarFilePath = params.jarFilePath - // 获取生成的JAR文件名 - def jarFile = params.jarFile - // 是否跳过测试,默认true(生产环境建议设置为false) - def skipTests = params.skipTests ?: true - // Maven执行目标,默认执行clean和package - def mavenGoals = params.goals ?: "clean package" - // POM文件路径,默认为项目根目录的pom.xml - def pomFilePath = params.pomFilePath - - script.echo "🔨 开始Maven构建项目..." - script.echo "📋 构建参数:" - script.echo " 📁 Maven配置: ${mavenSettings}" - script.echo " 🌍 环境Profile: ${profiles}" - script.echo " 📦 输出路径: ${jarFilePath}" - script.echo " 🏷️ JAR文件: ${jarFile}" - script.echo " ⚡ 跳过测试: ${skipTests}" - script.echo " 🎯 Maven目标: ${mavenGoals}" - script.echo " 📄 POM文件: ${pomFilePath}" - + def executeDeployStage(params) { try { - // 使用Config File Provider插件提供Maven settings.xml配置文件 - script.configFileProvider([script.configFile(fileId: mavenSettings, variable: 'MAVEN_SETTINGS_XML')]) { - // 执行Maven构建命令 - script.sh """ - /usr/local/maven/bin/mvn --settings ${script.env.MAVEN_SETTINGS_XML} \\ - dependency:purge-local-repository -Dinclude=com.jdk \\ - -f ${pomFilePath} ${mavenGoals} \\ - -P${profiles} \\ - -Dmaven.test.skip=${skipTests} - """ + script.echo "🚀 开始部署阶段..." + script.echo "📋 部署配置:" + script.echo " 📦 项目名称: ${params.projectName}" + script.echo " 🌍 目标环境: ${params.PROFILES}" + script.echo " 📡 服务器引用: ${params.deployServer ?: 'servers[0]'}" + script.echo " 📊 总服务器数: ${params.servers?.size() ?: 0}" + script.echo " 📦 目标服务器: ${params.targetServers}" + + // 目标服务器 + def targetServers = params.targetServers + // 根据目标服务器数量选择部署模式 + if (targetServers.size() > 1) { + script.echo "🔀 多服务器部署模式..." + deployMultipleServers(targetServers, params) + } else if (targetServers.size() == 1) { + script.echo "🔷 单服务器部署模式..." + deploySingleServer(targetServers[0], params) + } else { + throw new Exception("未找到有效的部署目标服务器") } - script.echo "✅ Maven构建完成" - - // 显示构建结果信息 - script.echo "📊 构建结果验证:" - - // 显示JAR文件的详细列表信息(大小、权限等) - script.sh "ls -lsh ${jarFilePath}/${jarFile}" - - // 计算JAR文件的MD5校验和,用于验证文件完整性 - script.sh "md5sum ${jarFilePath}/${jarFile}" - - // 可选:显示文件大小(人类可读格式) - script.sh "du -h ${jarFilePath}/${jarFile}" - - // 可选:显示最后修改时间 - script.sh "stat ${jarFilePath}/${jarFile}" + script.echo "✅ 应用部署完成" } catch (Exception e) { - script.echo "❌ Maven构建失败: ${e.message}" + script.echo "❌ 部署阶段失败: ${e.message}" script.echo "💡 建议检查:" - script.echo " - Maven settings.xml配置是否正确" - script.echo " - 网络连接是否正常(依赖下载)" - script.echo " - POM文件路径是否正确" - script.echo " - 代码编译是否有错误" - throw e // 重新抛出异常,让调用方处理构建失败 - } - } - - - /** - * 构建Docker镜像 - * 使用Docker Pipeline插件构建和推送Docker镜像到指定的镜像仓库 - * 支持自定义构建参数、构建选项和自动清理本地镜像 - * - * @param params 构建参数Map,包含以下必需字段: - * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) - * - registryCredential: String - Jenkins凭证ID,用于仓库认证 - * - registryProject: String - 仓库中的项目/命名空间名称(如:xiaomayi) - * - projectName: String - 项目名称(如:xiaomayi-admin) - * - PROFILES: String - 环境名称(dev/test/uat/prod),用于生成镜像标签 - * 可选字段: - * - buildOptions: String - Docker构建选项(如:--no-cache --pull) - * - baseImage: String - 基础镜像名称,通过--build-arg传递 - * - port: String/Integer - 服务端口号,通过--build-arg传递 - * - jarFile: String - JAR文件名,通过--build-arg传递 - * - jarFilePath: String - JAR文件路径,通过--build-arg传递 - * - * @return String 构建的镜像标签(如:dev-123、prod-456) - * @throws Exception 当Docker构建或推送失败时抛出异常 - */ - def buildDockerImage(params) { - // 生成唯一的镜像标签,格式: {环境}-{构建ID} - def imageTag = generateImageTag(params.PROFILES) - script.echo "🏷️ 生成的镜像标签: ${imageTag}" - - // 获取构建选项,如果未提供则使用空字符串 - def buildOptions = params.buildOptions ?: "" - script.echo "⚙️ 构建选项: ${buildOptions}" - - // 输出构建开始信息和详细参数 - script.echo "🐳 开始构建Docker镜像..." - script.echo " 📦 镜像仓库: ${params.registry}" - script.echo " 📁 命名空间: ${params.registryProject}" - script.echo " 🏷️ 项目名称: ${params.projectName}" - script.echo " 🎯 镜像标签: ${imageTag}" - script.echo " 🔑 仓库凭证: ${params.registryCredential}" - - try { - // 使用Docker Pipeline插件连接到镜像仓库并进行构建推送 - script.docker.withRegistry("https://${params.registry}", params.registryCredential) { - // 构建Docker镜像 - // 镜像名称格式: {命名空间}/{项目名称}:{标签} - def dockerImage = script.docker.build( - // 完整的镜像名称(包含标签) - "${params.registryProject}/${params.projectName}:${imageTag}", - // 构建命令参数组合 - "${buildOptions} " + - // 传递构建参数给Dockerfile - // 环境配置 - "--build-arg VAR_PROFILES=${params.PROFILES} " + - // 基础镜像 - "--build-arg VAR_BASE_IMAGE=${params.baseImage} " + - // 服务端口 - "--build-arg VAR_PORT=${params.port} " + - // JAR文件名 - "--build-arg VAR_JARFILE=${params.jarFile} " + - // JAR文件路径 - "--build-arg VAR_JARFILE_PATH=${params.jarFilePath} " + - // 指定Dockerfile路径和构建上下文,使用项目根目录下的.jenkins/Dockerfile文件 - "-f ${params.dockerFile} ./" - ) - - // 推送镜像到远程仓库 - script.echo "🚀 推送镜像到仓库: ${params.registry}" - dockerImage.push() - - // 清理本地Docker镜像,释放磁盘空间 - script.echo "🧹 开始清理本地镜像..." - cleanupDockerImages(params.registry, params.registryProject, params.projectName, imageTag) - } - - script.echo "✅ Docker镜像构建和推送完成" - return imageTag - - } catch (Exception e) { - script.echo "❌ Docker镜像构建失败: ${e.message}" - script.echo "💡 建议检查:" - script.echo " - Dockerfile路径是否正确" - script.echo " - 镜像仓库凭证是否有效" + script.echo " - 服务器配置是否正确" + script.echo " - SSH密钥凭证是否有效" script.echo " - 网络连接是否正常" - throw e // 重新抛出异常,让调用方处理 + script.echo " - 部署脚本是否存在且可执行" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e } } /** - * 生成Docker镜像标签 - * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 - * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * 部署到单个服务器 + * 负责将应用部署到指定的目标服务器,适用于单实例部署场景 * - * @param profile 环境名称/配置文件(如:dev、test、uat、prod) - * - 用于区分不同环境的镜像 - * - 可以为null或空字符串,此时只使用构建ID - * @return 完整的Docker镜像标签字符串 - * @example - * - generateImageTag("dev") → "dev-123" - * - generateImageTag("prod") → "prod-123" - * - generateImageTag(null) → "123" + * 主要步骤: + * 1. 输出服务器信息和部署详情 + * 2. 构建完整的部署参数Map + * 3. 添加额外的部署配置参数 + * 4. 执行部署操作并处理结果 + * + * @param server 服务器配置对象,包含以下字段: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - SSH端口号(默认22) + * - serverName: String - 服务器名称标识 + * - sshKey: String - Jenkins SSH密钥凭证ID + * - weight: Integer - 负载权重(可选) + * - isPrimary: Boolean - 是否为主节点(可选) + * - priority: Integer - 部署优先级(可选) + * + * @param params 部署参数Map,包含以下字段: + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - 其他部署相关配置 + * + * @throws Exception 部署失败时抛出异常,包含详细的错误信息和诊断建议 + * + * @note 支持敏感信息保护,自动隐藏密钥和密码字段的日志输出 */ - private def generateImageTag(profile) { - // 使用三元运算符判断profile是否存在 - // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 - // 如果profile为空,只返回BUILD_ID - return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID - } + private def deploySingleServer(server, params) { + script.echo "🎯 开始部署到单服务器: ${server.serverName}" + script.echo " 📍 目标地址: ${server.serverIP}:${server.serverPort}" + script.echo " 🔑 SSH凭证: ${server.sshKey}" - /** - * 清理Docker镜像 - * 删除本地Docker环境中指定的镜像,避免磁盘空间占用和镜像冲突 - * 主要用于构建后清理,移除不再需要的临时镜像 - * - * @param registry 镜像仓库地址(如:192.168.10.102:8001) - * @param registryProject 仓库中的项目/命名空间名称(如:xiaomayi) - * @param projectName 项目名称(如:xiaomayi-admin) - * @param imageTag 镜像标签(由generateImageTag方法生成) - * - * @note 使用"|| true"确保清理失败不会中断构建流程 - * @note 尝试清理两种格式的镜像: - * 1. {registryProject}/{projectName}:{imageTag} - * 2. {registry}/{registryProject}/{projectName}:{imageTag} - */ - private def cleanupDockerImages(registry, registryProject, projectName, imageTag) { try { - // 输出清理开始信息 - script.echo "🧹 开始清理Docker镜像: ${projectName}:${imageTag}" + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def deployParams = [ + // serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + // serverPort : server.serverPort, // SSH端口号(默认22) + // serverName : server.serverName, // 服务器名称标识,用于日志和监控 + // sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + profile : params.PROFILES, // 环境名称(dev/test/uat/prod) + buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + composeFile : params.composeFile, + ] - // 清理第一种格式的镜像:{项目}/{应用}:{标签} - // 例如:xiaomayi/xiaomayi-admin:dev-123 - // 使用"|| true"确保命令即使失败也不会抛出异常中断脚本 - script.sh "docker rmi -f ${registryProject}/${projectName}:${imageTag} || true" + // ========================================================================== + // 添加额外部署参数 + // 从主要参数和服务器配置中提取额外的部署参数 + // ========================================================================== + def extraParams = getDeploymentExtraParams(params, server) + if (extraParams) { + // 使用putAll方法合并Map,确保所有参数都传递给部署工具 + deployParams.putAll(extraParams) + } - // 清理第二种格式的镜像:{仓库地址}/{项目}/{应用}:{标签} - // 例如:192.168.10.102:8001/xiaomayi/xiaomayi-admin:dev-123 - script.sh "docker rmi -f ${registry}/${registryProject}/${projectName}:${imageTag} || true" + // 打印部署参数详情(调试用),自动隐藏敏感信息 + script.echo "📋 部署参数详情:" + deployParams.each { key, value -> + if (!key.toLowerCase().contains('key') && !key.toLowerCase().contains('password')) { + script.echo " ${key}: ${value}" + } else { + script.echo " ${key}: ********" // 敏感信息隐藏 + } + } - script.echo "✅ Docker镜像清理完成" + // 执行部署操作 + script.echo "🚀 正在执行部署..." + deployToServer(deployParams) + + script.echo "✅ 单服务器部署完成: ${server.serverName}" } catch (Exception e) { - // 捕获并处理异常,输出错误信息但不会中断主流程 - // 镜像清理失败通常不会影响主要功能,所以只是记录日志 - script.echo "⚠️ 清理Docker镜像失败: ${e.message}" - script.echo " 此错误通常可以忽略,不会影响部署流程" + script.echo "❌ 单服务器部署失败: ${server.serverName}" + script.echo " 💥 错误信息: ${e.message}" + script.echo " 🔍 错误类型: ${e.getClass().name}" + + // 记录详细的错误信息,但不暴露敏感信息 + def errorDetails = "部署到服务器 ${server.serverName} (${server.serverIP}) 失败" + if (e.message?.contains('Authentication')) { + errorDetails += " - 认证失败,请检查SSH密钥" + } else if (e.message?.contains('Connection')) { + errorDetails += " - 连接失败,请检查网络和端口" + } else if (e.message?.contains('Timeout')) { + errorDetails += " - 操作超时,请检查服务器状态" + } else if (e.message?.contains('Permission')) { + errorDetails += " - 权限不足,请检查文件权限" + } + + throw new Exception(errorDetails, e) } } + /** + * 部署到多个服务器 + * 并行或串行地将应用部署到多台目标服务器,支持失败处理和结果报告 + * 适用于集群部署、多节点部署等场景,支持灵活的失败处理策略 + * + * 主要步骤: + * 1. 初始化部署结果跟踪和失败记录 + * 2. 遍历服务器列表,逐台执行部署 + * 3. 根据配置决定失败时是否继续部署 + * 4. 生成部署结果报告和统计信息 + * 5. 处理最终部署结果,决定是否抛出异常 + * + * @param servers 服务器列表,包含每台服务器的配置信息: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - SSH端口号 + * - serverName: String - 服务器名称标识 + * - sshKey: String - Jenkins SSH密钥凭证ID + * - weight: Integer - 负载权重(可选) + * - isPrimary: Boolean - 是否为主节点(可选) + * - priority: Integer - 部署优先级(可选) + * + * @param params 部署参数Map,包含以下字段: + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - stopOnFailure: Boolean - 失败时是否停止部署 + * - 其他部署相关配置 + * + * @throws Exception 当配置了失败停止且有服务器部署失败时抛出异常 + * 异常信息包含失败的服务器列表和详细错误信息 + * + * @note 支持两种失败处理模式: + * - 失败停止:一旦有服务器部署失败就中断整个部署流程 + * - 失败继续:继续部署其他服务器,最后汇总报告所有失败情况 + */ + private def deployMultipleServers(servers, params) { + // 输出部署开始信息,显示目标服务器数量 + script.echo "🔀 开始部署到 ${servers.size()} 台服务器" + script.echo "📊 失败处理策略: ${shouldStopOnFailure(params) ? '失败停止' : '失败继续'}" + + // 初始化部署结果跟踪Map,用于记录每台服务器的部署状态 + def deploymentResults = [:] + // 初始化失败服务器列表,用于收集部署失败的服务器信息 + def failedServers = [] + + // 遍历服务器列表,逐台进行部署 + servers.each { server -> + try { + // 输出当前部署的服务器信息 + script.echo "➡️ 正在部署到: ${server.serverName} (${server.serverIP}:${server.serverPort})" + + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def deployParams = [ + // serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + // serverPort : server.serverPort, // SSH端口号(默认22) + // serverName : server.serverName, // 服务器名称标识,用于日志和监控 + // sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + profile : params.PROFILES, // 环境名称(dev/test/uat/prod) + buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + composeFile : params.composeFile, + ] + + // ========================================================================== + // 添加额外部署参数 + // 从主要参数和服务器配置中提取额外的部署参数 + // ========================================================================== + def extraParams = getDeploymentExtraParams(params, server) + if (extraParams) { + // 使用putAll方法合并Map,确保所有参数都传递给部署工具 + deployParams.putAll(extraParams) + } + + // ========================================================================== + // 执行部署操作 + // 调用部署工具类执行实际的部署命令 + // ========================================================================== + script.echo "🚀 正在执行部署到 ${server.serverName}..." + // 调用部署工具类,传递完整的部署参数 + deployToServer(deployParams) + + // 输出部署成功信息 + script.echo "✅ 部署成功: ${server.serverName}" + + // 记录部署结果 + deploymentResults[server.serverName] = "成功" + + } catch (Exception e) { + // ========================================================================== + // 异常处理:部署失败的情况 + // ========================================================================== + + // 记录失败的部署结果,包含错误信息 + deploymentResults[server.serverName] = "失败: ${e.message}" + // 将失败的服务器添加到失败列表 + failedServers << server.serverName + // 输出详细的错误信息 + script.echo "❌ 部署失败: ${server.serverName} - ${e.message}" + + // ========================================================================== + // 失败处理策略:根据配置决定是否继续部署 + // ========================================================================== + if (shouldStopOnFailure(params)) { + // 如果配置了失败停止,抛出异常中断部署流程 + throw new Exception("部署中断,因为在 ${server.serverName} 上失败且配置了失败停止", e) + } else { + // 如果未配置失败停止,继续部署其他服务器 + script.echo "⏭️ 继续部署其他服务器(配置了失败继续)" + } + } + } + + // ========================================================================== + // 生成部署报告 + // 汇总所有服务器的部署结果,提供清晰的部署状态概览 + // ========================================================================== + generateDeploymentReport(deploymentResults) + + // ========================================================================== + // 最终结果检查 + // 如果有服务器部署失败,抛出异常通知调用方 + // ========================================================================== + if (failedServers) { + // 构建详细的错误信息 + def errorMessage = "多服务器部署完成,但有 ${failedServers.size()} 台服务器失败: ${failedServers.join(', ')}" + // 输出警告信息(不中断流程,但让调用方知道有部分失败) + script.echo "⚠️ ${errorMessage}" + // 抛出异常,让调用方决定如何处理部分失败的情况 + throw new Exception(errorMessage) + } else { + // 所有服务器都部署成功 + script.echo "🎉 所有服务器部署成功!" + } + } + + /** + * 获取部署额外参数 + * 从主要参数和服务器配置中提取并合并部署所需的额外参数 + * 用于构建完整的部署参数Map,确保部署过程拥有所有必要信息 + * + * @param params 主要部署参数Map,包含环境变量、部署策略等通用配置 + * @param server 服务器配置对象,包含服务器特定的属性信息 + * @return 合并后的参数Map,包含所有部署所需的额外参数 + */ + private def getDeploymentExtraParams(params, server) { + // 初始化空的额外参数Map,用于存储合并后的参数 + def extraParams = [:] + + // ========================================================================== + // 添加环境相关参数(从主要参数中提取) + // ========================================================================== + + // 添加环境变量配置(如果存在) + // envVars通常包含应用运行时需要的环境变量,如数据库连接、日志级别等 + if (params?.envVars) { + extraParams.envVars = params.envVars + } + + // 添加部署策略配置(如果存在) + // deploymentStrategy定义部署方式,如rolling(滚动部署)、blue-green(蓝绿部署)等 + if (params?.deploymentStrategy) { + extraParams.deploymentStrategy = params.deploymentStrategy + } + + // 部署超时时间 + if (params?.deploymentStrategy) { + extraParams.deploymentTimeout = params.deploymentTimeout + } + + // 部署失败重试次数 + if (params?.retryAttempts) { + extraParams.retryAttempts = params.retryAttempts + } + + // ========================================================================== + // 添加服务器特定信息(从服务器配置中提取) + // ========================================================================== + + // 将服务器IP地址添加到额外参数Map中 + // serverIP是部署过程中必需的核心连接参数 + if (server?.serverIP) { + extraParams.serverIP = server.serverIP + } + + // 将服务器SSH端口添加到额外参数Map中 + // serverPort指定了SSH服务的监听端口,默认22 + if (server?.serverPort) { + extraParams.serverPort = server.serverPort + } + + // 添加服务器名称标识,用于日志记录和监控 + if (server?.serverName) { + extraParams.serverName = server.serverName + } + + // 添加SSH密钥凭证ID,用于远程服务器认证 + // sshKey是Jenkins中配置的凭证ID,不是实际的密钥内容 + if (server?.sshKey) { + extraParams.sshKey = server.sshKey + } + + // 添加服务器权重配置,用于负载均衡流量分配 + // weight为null时使用默认值50,表示平均分配流量 + if (server?.weight != null) { + extraParams.weight = server.weight ?: 50 // 默认权重为50 + } + + // 添加主节点标识,用于标识服务器是否为主节点 + // isPrimary为null时使用默认值false + if (server?.isPrimary != null) { + extraParams.isPrimary = server.isPrimary ?: false // 默认非主节点 + } + + // 添加部署优先级,数字越小优先级越高 + // 用于控制部署顺序,高优先级服务器优先部署 + if (server?.priority != null) { + extraParams.priority = server.priority + } + + // ========================================================================== + // 添加计算得到的配置参数 + // ========================================================================== + + // 添加部署超时配置,根据环境自动计算合适的超时时间 + // 生产环境通常需要更长的超时时间 + if (params?.deploymentTimeout) { + extraParams.deploymentTimeout = params.deploymentTimeout + } else { + extraParams.deploymentTimeout = getDeploymentTimeout(params) + } + + // ========================================================================== + // 添加健康检查相关配置(如果存在) + // ========================================================================== + + // 添加健康检查端点路径,用于部署后验证应用状态 + if (params.healthCheckPath) { + extraParams.healthCheckPath = params.healthCheckPath + } + + // 添加健康检查超时时间,控制健康检查的最大等待时间 + if (params.healthCheckTimeout) { + extraParams.healthCheckTimeout = params.healthCheckTimeout + } + + // 返回合并后的额外参数Map + // 这个Map将被合并到主要的部署参数中,形成完整的部署配置 + return extraParams + } + + /** + * 辅助方法:获取部署超时时间(根据环境配置) + * 不同环境设置不同的超时时间,生产环境需要更长的部署时间 + * + * @param params 部署参数,包含PROFILES字段指示环境类型 + * @return 超时时间(秒) + */ + private def getDeploymentTimeout(params) { + // 定义各环境的默认超时时间(单位:秒) + def timeouts = [ + 'dev' : 1800, // 开发环境:30分钟(快速失败) + 'test': 2700, // 测试环境:45分钟 + 'uat' : 3600, // UAT环境:60分钟 + 'prod': 7200 // 生产环境:120分钟(谨慎部署) + ] + + // 获取环境类型,默认为开发环境 + def env = params.PROFILES?.toLowerCase() ?: 'dev' + + // 返回对应环境的超时时间,如果没有匹配则使用开发环境超时 + return timeouts[env] ?: 1800 + } + /** * 部署到单个服务器 * 执行远程部署命令,将指定版本的Docker镜像部署到目标服务器 @@ -374,6 +565,7 @@ class PipelineUtils implements Serializable { def retryAttempts = deployParams.retryAttempts ?: 3 // 默认重试3次 def deployPath = deployParams.deployPath // 远程部署路径 def deployScript = deployParams.deployScript // 部署脚本文件名 + def composeFile = deployParams.composeFile // 日志输出部署信息 script.echo "🚀 开始部署到服务器: ${serverIP}:${serverPort}" @@ -386,7 +578,17 @@ class PipelineUtils implements Serializable { def imageTag = generateImageTag(profile) script.echo "🏷️ 生成的镜像标签: ${imageTag}" - // 第一步:传输部署文件到远程服务器 + // // 第一步:检查并创建远程部署路径 + // script.echo "📁 检查远程部署路径..." + // ensureRemotePathExists([ + // serverIP: serverIP, + // serverPort: serverPort, + // sshKey: sshKey, + // remotePath: deployPath, + // timeout: 60 + // ]) + + // 第二步:传输部署文件到远程服务器 // 支持多文件和重命名功能,包括部署脚本和环境配置文件 script.echo "📤 开始文件传输阶段..." @@ -397,7 +599,7 @@ class PipelineUtils implements Serializable { // 环境文件(重命名为 .env) [localPath: ".pipeline-files/scripts-file/docker-env-${profile}.sh", remoteName: ".env", mode: "644"], // Docker执行文件 - [localPath: ".pipeline-files/compose-file/docker-compose-${profile}.yml", remoteName: "docker-compose.yml"], + [localPath: ".pipeline-files/compose-file/${composeFile}.yml", remoteName: "docker-compose.yml"], ] transferDeploymentFiles([ serverIP: serverIP, @@ -408,7 +610,7 @@ class PipelineUtils implements Serializable { timeout: 300 ]) - // 第二步:设置远程文件执行权限 + // 第三步:设置远程文件执行权限 // 确保部署脚本有可执行权限,避免"Permission denied"错误 script.echo "🔧 开始权限设置阶段..." setRemoteFilePermissions([ @@ -420,7 +622,7 @@ class PipelineUtils implements Serializable { timeout: 60 // 权限设置超时时间 ]) - // 第三步:构建部署命令 + // 第四步:构建部署命令 // 使用bash显式执行脚本,双重保障避免权限问题 script.echo "🔨 构建部署命令..." def command = buildDeploymentCommand(deployPath, deployScript, projectName, imageTag, extraArgs) @@ -461,129 +663,116 @@ class PipelineUtils implements Serializable { } } - // /** - // * 传输部署文件到远程服务器 - // * 使用SCP将部署脚本和环境文件传输到目标服务器 - // * - // * @param scpParams SCP传输参数Map,包含以下字段: - // * - serverIP: String - 服务器IP地址 - // * - serverPort: Integer - 服务器SSH端口(默认22) - // * - sshKey: String - SSH密钥凭证ID - // * - localScriptPath: String - 本地部署脚本路径 - // * - localEnvPath: String - 本地环境文件路径 - // * - remotePath: String - 远程服务器目标路径 - // * - remoteEnvFileName: String - 远程环境文件名(默认.env) - // * - timeout: Integer - 超时时间(秒,默认300) - // * - // * @throws Exception 当SCP传输失败时抛出异常 - // * - // * @example - // * scpParams = [ - // * serverIP: "192.168.124.126", - // * sshKey: "jenkins-ssh-205", - // * localScriptPath: ".pipeline-files/scripts/deploy_env.sh", - // * localEnvPath: ".pipeline-files/scripts/docker-env-test.sh", - // * remotePath: "/data/apps/yhsc/", - // * remoteEnvFileName: ".env" - // * ] - // */ - // def transferDeploymentFiles(scpParams) { - // def serverIP = scpParams.serverIP - // def serverPort = scpParams.serverPort ?: 22 - // def sshKey = scpParams.sshKey ?: 'jenkins-ssh-205' - // def localScriptPath = scpParams.localScriptPath - // def localEnvPath = scpParams.localEnvPath - // def remotePath = scpParams.remotePath - // def remoteEnvFileName = scpParams.remoteEnvFileName ?: '.env' - // def timeout = scpParams.timeout ?: 300 + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + private def generateImageTag(profile) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID + } - // script.echo "📤 开始传输部署文件到服务器: ${serverIP}:${serverPort}" - // script.echo "📋 传输参数:" - // script.echo " 📁 本地脚本: ${localScriptPath}" - // script.echo " 📁 本地环境文件: ${localEnvPath}" - // script.echo " 📁 远程路径: ${remotePath}" - // script.echo " 🏷️ 远程环境文件名: ${remoteEnvFileName}" - // script.echo " 🔑 SSH凭证: ${sshKey}" - // script.echo " ⏱️ 超时时间: ${timeout}秒" + /** + * 确保远程路径存在(最简版) + * 使用mkdir -p命令的原子性操作:路径存在时忽略,不存在时创建 + * 适用于大多数场景,代码更简洁 + * + * @param params 参数Map,包含以下字段: + * - serverIP: String - 远程服务器IP地址 + * - serverPort: Integer - SSH端口号 + * - sshKey: String - SSH私钥文件名称 + * - remotePath: String - 要确保存在的远程路径 + */ + def ensureRemotePathExists(params) { + // 输出开始操作的日志 + script.echo "🔍 确保远程路径存在: ${params.remotePath}" + + try { + // 参数解析和默认值设置 + def serverIP = params.serverIP // 服务器IP地址(必需) + def serverPort = params.serverPort ?: 22 // SSH端口,默认22 + def sshKey = params.sshKey ?: 'jenkins-ssh-205' // SSH凭证,默认jenkins-ssh-205 + def remotePath = params.remotePath // 远程目标路径(必需) + def timeout = params.timeout ?: 300 // 超时时间,默认300秒(5分钟) - // try { - // // 使用withCredentials处理SSH密钥 - // script.withCredentials([script.sshUserPrivateKey( - // credentialsId: sshKey, - // keyFileVariable: 'SSH_KEY_FILE', - // usernameVariable: 'SSH_USERNAME' - // )]) { - // // 解析用户名和主机名(支持user@host格式) - // def (username, hostname) = parseServerAddress(serverIP) + // 日志输出传输信息 + script.echo "📤 开始传输部署文件到服务器: ${serverIP}:${serverPort}" + script.echo "📋 传输参数:" + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" - // // 传输部署脚本文件 - // script.echo "📤 传输部署脚本文件..." - // executeSCPCommand( - // localScriptPath, - // "${username}@${hostname}:${remotePath}", - // serverPort, - // sshKey, - // timeout - // ) + // 使用Jenkins凭证管理安全地处理SSH密钥 + // withCredentials确保密钥在使用后自动清理,避免安全风险 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径存储变量 + usernameVariable: 'SSH_USERNAME' // 用户名存储变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 连接用户: ${username}" + script.echo " 🌐 目标主机: ${hostname}" + /** + * 执行SSH命令: + * mkdir -p: 创建目录,-p参数确保: + * - 如果目录已存在,不会报错 + * - 可以创建多级目录(如/a/b/c) + * - 会自动创建不存在的父目录 + * chmod 755: 设置目录权限为755 + */ + def sshCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + 'mkdir -p "${remotePath}" && chmod 755 "${remotePath}"' + """.trim() - // // 传输环境文件(重命名为.env) - // script.echo "📤 传输环境配置文件..." - // executeSCPCommand( - // localEnvPath, - // "${username}@${hostname}:${remotePath}/${remoteEnvFileName}", - // serverPort, - // sshKey, - // timeout - // ) - // } + script.echo "🔧 执行SSH命令" - // script.echo "✅ 文件传输完成" - // script.echo "💡 传输的文件:" - // script.echo " - ${localScriptPath} → ${remotePath}/" - // script.echo " - ${localEnvPath} → ${remotePath}/${remoteEnvFileName}" - - // } catch (Exception e) { - // script.echo "❌ 文件传输失败: ${e.message}" - // script.echo "💡 建议检查:" - // script.echo " - 本地文件路径是否正确" - // script.echo " - 远程目录权限是否足够" - // script.echo " - SSH密钥凭证是否有效" - // script.echo " - 网络连接是否正常" - // throw e - // } - // } - - // /** - // * 执行SCP文件传输命令 - // * 使用SCP协议将文件从本地传输到远程服务器 - // * - // * @param localFile 本地文件路径 - // * @param remoteTarget 远程目标路径(格式: user@host:path) - // * @param serverPort SSH端口号 - // * @param sshKey SSH密钥凭证ID - // * @param timeout 超时时间(秒) - // * - // * @throws Exception 当SCP命令执行失败时抛出异常 - // */ - // private def executeSCPCommand(localFile, remoteTarget, serverPort, sshKey, timeout) { - // script.echo " 📁 传输: ${localFile} → ${remoteTarget}" - - // // 构建SCP命令 - // def scpCommand = """ - // scp -o StrictHostKeyChecking=${ConfigConstants.SSH_CONFIG.strictHostKeyChecking} \ - // -o ConnectTimeout=${ConfigConstants.SSH_CONFIG.connectTimeout} \ - // -o ServerAliveInterval=${ConfigConstants.SSH_CONFIG.serverAliveInterval} \ - // -P ${serverPort} \ - // -i ${script.SSH_KEY_FILE} \ - // ${localFile} \ - // ${remoteTarget} - // """.trim() - - // // 执行SCP命令(带超时设置) - // script.timeout(time: timeout, unit: 'SECONDS') { - // script.sh(script: scpCommand, returnStatus: false) - // } - // } + // 执行SSH命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def exitCode = script.sh( + script: sshCommand, + returnStatus: true + ) + if (exitCode == 0) { + script.echo "✅ 远程路径创建成功: ${remotePath}" + } else { + throw new Exception("SSH命令返回非零状态码: ${exitCode}") + } + } + + // 成功完成后的日志输出 + script.echo "✅ 远程路径准备就绪: ${params.remotePath}" + } + } catch (Exception e) { + /** + * 异常处理: + * 可能的异常原因: + * - SSH连接失败(网络问题、服务器宕机) + * - 认证失败(密钥错误、权限不足) + * - 路径创建失败(磁盘空间不足、权限问题) + */ + script.echo "❌ 远程路径操作失败: ${e.message}" + throw new Exception("无法创建远程路径: ${params.remotePath}", e) + } + } /** * 传输部署文件到远程服务器(支持多文件和重命名) @@ -1101,289 +1290,54 @@ class PipelineUtils implements Serializable { } /** - * 执行应用健康检查 - * 通过HTTP端点检查目标服务的健康状态,支持超时控制和重试机制 - * 用于部署后验证应用是否正常启动和运行 + * 辅助方法:检查是否应该在部署失败时停止 + * 根据部署参数决定单个服务器失败时是否停止整个部署流程 * - * @param serverIP 服务器IP地址 - * - 类型: String - * - 格式: IP地址或主机名(不需要http://前缀) - * - 示例: "192.168.1.100", "example.com", "localhost" - * - 必填: 是 - * - * @param port 服务端口号 - * - 类型: Integer - * - 范围: 1-65535 - * - 示例: 8080, 80, 443 - * - 必填: 是 - * - * @param timeoutMinutes 超时时间(分钟) - * - 类型: Integer - * - 默认值: 5分钟(优先使用参数值,其次环境配置,最后默认5分钟) - * - 建议: 生产环境建议设置较长超时(10-15分钟),开发环境可设置较短(3-5分钟) - * - 可选: 是 - * - * @throws Exception 当健康检查超时或连续失败时抛出异常 - * - * @note 使用Spring Boot Actuator的健康检查端点(/actuator/health) - * @note 支持自动重试机制,直到服务健康或超时 - * @note 包含详细的日志输出和错误诊断信息 - * - * @example 使用示例: - * // 基本使用 - * healthCheck("192.168.1.100", 8080) - * - * // 自定义超时 - * healthCheck("192.168.1.100", 8080, 10) - * - * // 生产环境检查 - * healthCheck("prod-server.example.com", 8080, 15) + * @param params 部署参数Map + * @return boolean true-失败时停止,false-失败时继续 */ - def healthCheck(serverIP, port, timeoutMinutes = null) { - // ========================================================================== - // 参数处理和配置初始化 - // ========================================================================== - - // 确定超时时间(优先级: 参数 > 环境配置 > 默认值) - def timeout = timeoutMinutes ?: envConfig?.healthCheckTimeout ?: 5 - // 健康检查端点路径(Spring Boot Actuator标准端点) - def healthEndpoint = "/actuator/health" - // 构建完整的健康检查URL - def healthCheckUrl = "http://${serverIP}:${port}${healthEndpoint}" - - script.echo "❤️ 开始健康检查..." - script.echo "📋 检查配置:" - script.echo " 📍 目标服务: ${serverIP}:${port}" - script.echo " 🌐 检查地址: ${healthCheckUrl}" - script.echo " ⏱️ 超时时间: ${timeout} 分钟" - script.echo " 🔄 检查机制: 自动重试直到健康或超时" - - // ========================================================================== - // 参数验证 - // ========================================================================== - - // 验证服务器IP地址 - if (!serverIP) { - throw new IllegalArgumentException("服务器IP地址不能为空") - } - - // 验证端口范围 - if (port < 1 || port > 65535) { - throw new IllegalArgumentException("端口号必须在1-65535范围内,当前: ${port}") - } - - // 验证超时时间 - if (timeout < 1) { - script.echo "⚠️ 超时时间过短(${timeout}分钟),使用最小值1分钟" - timeout = 1 - } - - // ========================================================================== - // 健康检查执行 - // ========================================================================== - - try { - // 设置整体超时(防止健康检查无限期等待) - script.timeout(time: timeout, unit: 'MINUTES') { - // 使用waitUntil实现自动重试机制 - script.waitUntil { - try { - script.echo "🔍 执行健康检查请求..." - - // 执行HTTP健康检查(使用curl命令) - // -s: 静默模式,不显示进度信息 - // -o /dev/null: 将响应体输出到空设备 - // -w '%{http_code}': 只输出HTTP状态码 - // --connect-timeout 5: 连接超时5秒 - // --max-time 10: 最大请求时间10秒 - def response = script.sh( - script: "curl -s -o /dev/null -w '%{http_code}' '${healthCheckUrl}' --connect-timeout 5 --max-time 10", - returnStdout: true - ).trim() - - script.echo "📊 健康检查响应: HTTP ${response}" - - // 记录详细的响应信息(用于调试) - if (response != "200") { - // 非200响应时获取更多信息 - try { - def detailResponse = script.sh( - script: "curl -s '${healthCheckUrl}' --connect-timeout 5 --max-time 10", - returnStdout: true - ).trim() - script.echo "📝 详细响应: ${detailResponse.take(200)}..." // 限制输出长度 - } catch (Exception detailError) { - script.echo "⚠️ 获取详细响应失败: ${detailError.message}" - } - } - - // 返回检查结果(200表示健康) - return response == "200" - - } catch (Exception e) { - // 记录检查失败信息 - script.echo "❌ 健康检查请求失败: ${e.message}" - script.echo "💡 可能的原因:" - script.echo " - 应用尚未完全启动" - script.echo " - 网络连接问题" - script.echo " - 防火墙或安全组限制" - script.echo " - 服务配置错误" - - return false // 继续重试 - } - } - } - - // 健康检查成功 - script.echo "✅ 健康检查成功!服务状态: HEALTHY" - script.echo "🎉 应用已正常启动并运行在 ${serverIP}:${port}" - script.echo "💡 建议: 继续执行后续部署验证步骤" - - } catch (Exception e) { - // ========================================================================== - // 健康检查失败处理 - // ========================================================================== - - script.echo "❌ 健康检查失败!服务状态: UNHEALTHY" - script.echo "📋 失败详情:" - script.echo " 🎯 目标服务: ${serverIP}:${port}" - script.echo " 🌐 检查地址: ${healthCheckUrl}" - script.echo " ⏱️ 超时时间: ${timeout} 分钟" - script.echo " 💥 错误信息: ${e.message}" - - script.echo "🔧 故障排除建议:" - script.echo " 1. 📡 检查网络连通性: ping ${serverIP}" - script.echo " 2. 🚪 检查端口访问: telnet ${serverIP} ${port} || nc -zv ${serverIP} ${port}" - script.echo " 3. 📋 检查应用日志: docker logs 或 journalctl -u " - script.echo " 4. 🔍 手动验证: curl -v ${healthCheckUrl}" - script.echo " 5. ⚙️ 检查应用配置: 确认actuator端点已启用" - script.echo " 6. 🐛 查看启动错误: 检查应用启动时的异常信息" - script.echo " 7. 📊 检查资源使用: 内存、CPU、磁盘空间是否充足" - - // 重新抛出异常,让调用方能够处理健康检查失败 - throw new Exception("健康检查失败: 服务未在${timeout}分钟内达到健康状态", e) - } + private def shouldStopOnFailure(params) { + // 生产环境通常应该失败时停止,其他环境可以继续 + def isProduction = params.PROFILES?.toLowerCase() == 'prod' + // 优先使用显式配置,如果没有配置则根据环境决定 + return params.stopOnFailure ?: isProduction } /** - * 执行回滚部署操作 - * 负责将指定项目回退到上一个稳定版本,支持远程服务器操作和完整的错误处理 + * 辅助方法:生成部署报告 + * 创建格式化的部署结果报告,便于查看和分析 * - * @param rollbackParams 回滚参数Map,包含以下字段: - * - timeout: Integer - 操作超时时间(秒) - * - projectName: String - 项目名称(必须) - * - deployScript: String - 部署脚本文件名 - * - deployPath: String - 部署脚本路径 - * - retryAttempts: Integer - 重试次数 - * - * @throws Exception 当回滚操作失败时抛出异常,包含详细的错误信息和诊断建议 - * - * @note 该方法依赖于远程服务器上已部署的回滚脚本和正确的SSH配置 - * @note 支持重试机制,提高在网络不稳定情况下的成功率 + * @param results 部署结果Map,格式:["服务器名称": "状态描述"] */ - def rollbackDeployment(rollbackParams) { - // ========================================================================== - // 参数提取和解构 - // ========================================================================== + private def generateDeploymentReport(results) { + script.echo "=" * 60 + script.echo "📊 多服务器部署结果报告" + script.echo "=" * 60 - // 从服务器配置中提取IP地址(支持user@host格式) - def serverIP = rollbackParams.serverIP - // 从服务器配置中提取SSH端口号(默认22) - def serverPort = rollbackParams.serverPort - // 从服务器配置中提取SSH密钥凭证ID(Jenkins中配置的凭证名称) - def sshKey = rollbackParams.sshKey - // 从回滚参数中提取操作超时时间(秒) - def timeout = rollbackParams.timeout - // 从回滚参数中提取项目名称(用于标识要回滚的具体应用) - def projectName = rollbackParams.projectName - // 从回滚参数中提取部署脚本文件名(默认:deploy.sh) - def deployScript = rollbackParams.deployScript - // 从回滚参数中提取部署脚本所在路径 - def deployPath = rollbackParams.deployPath - // 从回滚参数中提取重试次数(默认:3次) - def retryAttempts = rollbackParams.retryAttempts ?: 3 + // 统计成功和失败数量 + def successCount = results.count { it.value == '成功' } + def failureCount = results.size() - successCount - // ========================================================================== - // 回滚信息输出和验证 - // ========================================================================== - - // 输出回滚操作开始信息,便于调试和审计追踪 - script.echo "🔄 执行回滚操作:" - script.echo " 📍 目标服务器: ${serverIP}:${serverPort}" - script.echo " 📦 项目名称: ${projectName}" - script.echo " 📁 脚本路径: ${deployPath}" - script.echo " 🔑 SSH凭证: ${sshKey}" - script.echo " ⏱️ 超时时间: ${timeout}秒" - script.echo " 🔄 重试次数: ${retryAttempts}次" - - // 验证必要参数是否存在 - if (!serverIP) { - throw new IllegalArgumentException("缺少服务器IP地址参数") - } - if (!projectName) { - throw new IllegalArgumentException("缺少项目名称参数") + // 输出每台服务器的详细结果 + results.each { serverName, status -> + def emoji = status.startsWith('成功') ? '✅' : '❌' + script.echo "${emoji} ${serverName}: ${status}" } - try { - // ========================================================================== - // 回滚命令构建 - // ========================================================================== + script.echo "-" * 60 + // 输出统计信息 + script.echo "📈 统计: 成功 ${successCount}, 失败 ${failureCount}, 总计 ${results.size()}" - // 构建回滚命令字符串 - // 格式: cd {部署脚本路径}; ./{部署脚本} rollback {项目名称} - def command = "cd ${deployPath}; ./${deployScript} rollback ${projectName}" - script.echo " 🖥️ 回滚命令: ${command}" - - // ========================================================================== - // 远程命令执行 - // ========================================================================== - - // 执行远程回滚命令,支持重试机制 - // executeRemoteCommandWithRetry 方法处理SSH连接、命令执行和重试逻辑 - def success = executeRemoteCommandWithRetry( - serverIP, // 目标服务器地址 - serverPort, // SSH端口号 - sshKey, // SSH密钥凭证 - command, // 要执行的命令 - timeout, // 超时时间 - retryAttempts // 重试次数 - ) - - // ========================================================================== - // 成功处理 - // ========================================================================== - - // 回滚成功日志输出 - script.echo "✅ 回滚成功完成: ${projectName} 已回退到上一个稳定版本" - script.echo "💡 建议: 检查应用日志确认回滚状态" - script.echo " 📋 验证步骤:" - script.echo " 1. 检查应用进程状态" - script.echo " 2. 查看应用启动日志" - script.echo " 3. 验证服务健康状态" - script.echo " 4. 确认业务功能正常" - - return success - - } catch (Exception e) { - // ========================================================================== - // 失败处理 - // ========================================================================== - - // 回滚失败错误信息输出 - script.echo "❌ 回滚失败: ${projectName} - ${e.message}" - script.echo "💡 故障排除建议:" - script.echo " 1. 🔍 检查服务器SSH连接是否正常" - script.echo " 2. 📁 确认部署脚本是否存在且可执行" - script.echo " 3. 🏷️ 验证项目名称是否正确" - script.echo " 4. 💾 检查服务器磁盘空间和权限" - script.echo " 5. 🔑 验证SSH密钥凭证是否有效" - script.echo " 6. 🌐 检查网络连接和防火墙设置" - script.echo " 7. ⏱️ 确认超时时间设置是否合理" - - // 重新抛出异常,让调用方能够处理回滚失败 - // 保留原始异常信息以便于调试和根因分析 - throw new Exception("回滚操作失败: ${e.message}", e) + // 根据结果输出不同的总结信息 + if (failureCount == 0) { + script.echo "🎉 所有服务器部署成功!" + } else if (successCount == 0) { + script.echo "💥 所有服务器部署失败!" + } else { + script.echo "⚠️ 部分服务器部署失败,请检查日志" } + + script.echo "=" * 60 } } \ No newline at end of file diff --git a/src/com/golang/stages/HealthCheckService.groovy b/src/com/golang/stages/HealthCheckService.groovy new file mode 100644 index 0000000..4081a6b --- /dev/null +++ b/src/com/golang/stages/HealthCheckService.groovy @@ -0,0 +1,333 @@ +package com.golang.stages + +/** + * 健康检查阶段服务 - 负责验证部署的应用是否正常运行 + * + * 主要职责: + * - 执行部署后的应用健康状态验证 + * - 监控服务可用性和功能性 + * - 在健康检查失败时触发自动回滚机制 + * - 提供详细的健康检查结果和诊断信息 + * + * 设计原则: + * - 单一职责:专注于健康检查相关的逻辑 + * - 安全性:健康检查失败时提供自动回滚保护 + * - 可配置性:支持灵活的健康检查配置和开关 + * - 错误隔离:健康检查失败不会影响其他服务实例 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + * @see rollbackService 依赖回滚服务处理健康检查失败的情况 + */ +class HealthCheckService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储健康检查环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 回滚服务实例 + * 负责在健康检查失败时执行回滚操作 + * 类型: RollbackService 回滚服务类 + */ + def rollbackService + + /** + * 构造函数 - 初始化健康检查服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:健康检查配置、回滚设置等 + * + * @note 自动初始化RollbackService实例用于健康检查失败时的回滚操作 + */ + HealthCheckService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + // 初始化回滚服务,用于健康检查失败时的自动回滚 + this.rollbackService = new RollbackService(script, envConfig) + + script.echo "🔧 HealthCheckService 初始化完成" + script.echo " ❤️ 回滚服务已就绪" + } + + /** + * 执行健康检查阶段逻辑 + * 负责验证部署的应用是否正常运行,是CI/CD流水线的质量保障阶段 + * + * 主要步骤: + * 1. 检查健康检查功能是否启用 + * 2. 确定要检查的目标服务器列表 + * 3. 对每个目标服务器执行健康检查 + * 4. 处理健康检查结果和异常情况 + * 5. 在失败时触发自动回滚机制 + * + * @param params 管道参数Map,包含健康检查所需的所有配置信息,通常包含: + * - serverHosts: List - 服务器列表(多服务器部署时使用) + * - serverIP01: String - 单个服务器IP(单服务器部署时使用) + * - port: Integer - 服务端口号(必须) + * 可选字段: + * - healthCheckPath: String - 健康检查端点路径(默认:/actuator/health) + * - healthCheckTimeout: Integer - 健康检查超时时间(秒) + * - healthCheckInterval: Integer - 健康检查间隔时间(秒) + * - successThreshold: Integer - 成功阈值(连续成功次数) + * - retryAttempts: Integer - 重试次数 + * + * @throws Exception 当健康检查失败时抛出异常,触发回滚机制 + * 异常信息包含健康检查失败的详细原因 + * + * @example 使用示例: + * def healthCheckParams = [ + * serverHosts: ["192.168.1.1", "192.168.1.2"], + * port: 8080, + * healthCheckPath: "/actuator/health", + * healthCheckTimeout: 30, + * retryAttempts: 3 + * ] + * healthCheckService.executeHealthCheckStage(healthCheckParams) + */ + def executeHealthCheckStage(params) { + try { + script.echo "❤️ 开始健康检查..." + script.echo "📋 健康检查配置:" + script.echo " 🚪 服务端口: ${params.serverPort}" + + // 输出健康检查路径(如果配置) + if (params.healthCheckPath) { + script.echo " 📍 健康检查端点: ${params.healthCheckPath}" + } + + // 检查是否启用健康检查功能 + // 支持通过环境配置动态启用/禁用健康检查 + def healthCheckEnabled = envConfig.deployment?.health_check_enabled ?: true + script.echo " ⚙️ 健康检查功能: ${healthCheckEnabled ? '已启用' : '已禁用'}" + + if (healthCheckEnabled) { + // 确定要检查的目标服务器列表 + // 支持多服务器部署和单服务器部署两种模式 + def targets = params.serverHosts ?: [params.serverIP01] + script.echo " 📡 目标服务器数量: ${targets.size()}" + + // 对每个目标服务器执行健康检查 + targets.each { serverIP -> + if (serverIP) { + script.echo " 🔍 对服务器 ${serverIP}:${params.serverPort} 进行健康检查..." + + // 使用healthCheck方法执行实际健康检查 + // 该方法会处理HTTP请求、超时控制、重试机制等 + healthCheck(serverIP, params.port) + } + } + + script.echo "✅ 所有服务器健康检查通过" + } else { + script.echo "⏭️ 健康检查功能已禁用,跳过检查" + } + + script.echo "❤️ 健康检查完成" + + } catch (Exception e) { + // 健康检查失败处理 + script.echo "❌ 健康检查失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - 应用是否正常启动" + script.echo " - 服务端口是否正确暴露" + script.echo " - 健康检查端点是否可以访问" + script.echo " - 网络连接是否正常" + script.echo " - 应用日志是否有错误信息" + + // 健康检查失败时尝试回滚部署 + // 回滚功能可以通过配置启用或禁用 + def rollbackEnabled = envConfig?.rollbackEnabled ?: true + if (rollbackEnabled) { + script.echo "🔄 健康检查失败,触发自动回滚..." + rollbackService.executeRollbackStage(params) + } else { + script.echo "⏭️ 自动回滚功能已禁用,跳过回滚" + } + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 执行应用健康检查 + * 通过HTTP端点检查目标服务的健康状态,支持超时控制和重试机制 + * 用于部署后验证应用是否正常启动和运行 + * + * @param serverIP 服务器IP地址 + * - 类型: String + * - 格式: IP地址或主机名(不需要http://前缀) + * - 示例: "192.168.1.100", "example.com", "localhost" + * - 必填: 是 + * + * @param port 服务端口号 + * - 类型: Integer + * - 范围: 1-65535 + * - 示例: 8080, 80, 443 + * - 必填: 是 + * + * @param timeoutMinutes 超时时间(分钟) + * - 类型: Integer + * - 默认值: 5分钟(优先使用参数值,其次环境配置,最后默认5分钟) + * - 建议: 生产环境建议设置较长超时(10-15分钟),开发环境可设置较短(3-5分钟) + * - 可选: 是 + * + * @throws Exception 当健康检查超时或连续失败时抛出异常 + * + * @note 使用Spring Boot Actuator的健康检查端点(/actuator/health) + * @note 支持自动重试机制,直到服务健康或超时 + * @note 包含详细的日志输出和错误诊断信息 + * + * @example 使用示例: + * // 基本使用 + * healthCheck("192.168.1.100", 8080) + * + * // 自定义超时 + * healthCheck("192.168.1.100", 8080, 10) + * + * // 生产环境检查 + * healthCheck("prod-server.example.com", 8080, 15) + */ + def healthCheck(serverIP, port, timeoutMinutes = null) { + // ========================================================================== + // 参数处理和配置初始化 + // ========================================================================== + + // 确定超时时间(优先级: 参数 > 环境配置 > 默认值) + def timeout = timeoutMinutes ?: envConfig?.healthCheckTimeout ?: 5 + // 健康检查端点路径(Spring Boot Actuator标准端点) + def healthEndpoint = "/actuator/health" + // 构建完整的健康检查URL + def healthCheckUrl = "http://${serverIP}:${port}${healthEndpoint}" + + script.echo "❤️ 开始健康检查..." + script.echo "📋 检查配置:" + script.echo " 📍 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 🔄 检查机制: 自动重试直到健康或超时" + + // ========================================================================== + // 参数验证 + // ========================================================================== + + // 验证服务器IP地址 + if (!serverIP) { + throw new IllegalArgumentException("服务器IP地址不能为空") + } + + // 验证端口范围 + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("端口号必须在1-65535范围内,当前: ${port}") + } + + // 验证超时时间 + if (timeout < 1) { + script.echo "⚠️ 超时时间过短(${timeout}分钟),使用最小值1分钟" + timeout = 1 + } + + // ========================================================================== + // 健康检查执行 + // ========================================================================== + + try { + // 设置整体超时(防止健康检查无限期等待) + script.timeout(time: timeout, unit: 'MINUTES') { + // 使用waitUntil实现自动重试机制 + script.waitUntil { + try { + script.echo "🔍 执行健康检查请求..." + + // 执行HTTP健康检查(使用curl命令) + // -s: 静默模式,不显示进度信息 + // -o /dev/null: 将响应体输出到空设备 + // -w '%{http_code}': 只输出HTTP状态码 + // --connect-timeout 5: 连接超时5秒 + // --max-time 10: 最大请求时间10秒 + def response = script.sh( + script: "curl -s -o /dev/null -w '%{http_code}' '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + + script.echo "📊 健康检查响应: HTTP ${response}" + + // 记录详细的响应信息(用于调试) + if (response != "200") { + // 非200响应时获取更多信息 + try { + def detailResponse = script.sh( + script: "curl -s '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + script.echo "📝 详细响应: ${detailResponse.take(200)}..." // 限制输出长度 + } catch (Exception detailError) { + script.echo "⚠️ 获取详细响应失败: ${detailError.message}" + } + } + + // 返回检查结果(200表示健康) + return response == "200" + + } catch (Exception e) { + // 记录检查失败信息 + script.echo "❌ 健康检查请求失败: ${e.message}" + script.echo "💡 可能的原因:" + script.echo " - 应用尚未完全启动" + script.echo " - 网络连接问题" + script.echo " - 防火墙或安全组限制" + script.echo " - 服务配置错误" + + return false // 继续重试 + } + } + } + + // 健康检查成功 + script.echo "✅ 健康检查成功!服务状态: HEALTHY" + script.echo "🎉 应用已正常启动并运行在 ${serverIP}:${port}" + script.echo "💡 建议: 继续执行后续部署验证步骤" + + } catch (Exception e) { + // ========================================================================== + // 健康检查失败处理 + // ========================================================================== + + script.echo "❌ 健康检查失败!服务状态: UNHEALTHY" + script.echo "📋 失败详情:" + script.echo " 🎯 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 💥 错误信息: ${e.message}" + + script.echo "🔧 故障排除建议:" + script.echo " 1. 📡 检查网络连通性: ping ${serverIP}" + script.echo " 2. 🚪 检查端口访问: telnet ${serverIP} ${port} || nc -zv ${serverIP} ${port}" + script.echo " 3. 📋 检查应用日志: docker logs 或 journalctl -u " + script.echo " 4. 🔍 手动验证: curl -v ${healthCheckUrl}" + script.echo " 5. ⚙️ 检查应用配置: 确认actuator端点已启用" + script.echo " 6. 🐛 查看启动错误: 检查应用启动时的异常信息" + script.echo " 7. 📊 检查资源使用: 内存、CPU、磁盘空间是否充足" + + // 重新抛出异常,让调用方能够处理健康检查失败 + throw new Exception("健康检查失败: 服务未在${timeout}分钟内达到健康状态", e) + } + } + +} \ No newline at end of file diff --git a/src/com/golang/stages/RollbackService.groovy b/src/com/golang/stages/RollbackService.groovy new file mode 100644 index 0000000..883e0ea --- /dev/null +++ b/src/com/golang/stages/RollbackService.groovy @@ -0,0 +1,325 @@ +package com.golang.stages + +import com.utils.ServerParser + +/** + * 回滚阶段服务 - 负责在部署失败时执行回滚操作 + * + * 主要职责: + * - 在部署失败或健康检查失败时执行自动回滚 + * - 将应用回退到上一个稳定版本 + * - 处理回滚过程中的异常和错误 + * - 提供回滚结果的状态反馈和报警通知 + * + * 设计原则: + * - 安全性:确保回滚操作不会造成二次故障 + * - 可靠性:回滚操作本身需要高度可靠 + * - 可追溯性:记录详细的回滚日志和操作记录 + * - 优雅降级:回滚失败时提供明确的处理指引 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class RollbackService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储回滚环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 服务器解析器实例 + * 负责解析服务器引用配置和索引处理 + * 类型: ServerParser 自定义服务器解析工具类 + */ + def serverParser + + /** + * 构造函数 - 初始化回滚服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:回滚策略、超时设置、通知配置等 + */ + RollbackService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + // 初始化服务器解析器,用于处理服务器引用和索引 + this.serverParser = new ServerParser(script) + + script.echo "🔧 RollbackService 初始化完成" + script.echo " 🔄 回滚服务就绪" + } + + /** + * 执行回滚操作 + * 在部署或健康检查失败时回滚到上一个可用版本,是CI/CD流水线的安全机制 + * + * 主要步骤: + * 1. 输出回滚开始信息和配置详情 + * 2. 确定要回滚的目标服务器列表 + * 3. 对每个目标服务器执行回滚操作 + * 4. 处理回滚结果和异常情况 + * 5. 提供回滚完成状态和后续处理指引 + * + * @param params 管道参数Map,包含回滚所需的所有配置信息,通常包含: + * - serverHosts: List - 需要回滚的服务器列表(多服务器部署时使用) + * - serverIP01: String - 单个服务器IP(单服务器部署时使用) + * - serverPort: Integer - 服务器SSH端口号(必须) + * - projectName: String - 项目名称(必须) + * 可选字段: + * - previousVersion: String - 上一个版本号(用于指定回滚版本) + * - rollbackTimeout: Integer - 回滚操作超时时间(秒) + * - rollbackStrategy: String - 回滚策略(如:immediate、graceful) + * - notifyOnRollback: Boolean - 是否发送回滚通知 + * + * @note 回滚操作是安全机制,应该尽可能确保其可靠性 + * @note 回滚失败时需要明确的手动处理指引 + * + * @example 使用示例: + * def rollbackParams = [ + * serverHosts: ["192.168.1.1", "192.168.1.2"], + * serverPort: 22, + * projectName: "xiaomayi-admin", + * previousVersion: "v1.2.0", + * rollbackTimeout: 300 + * ] + * rollbackService.executeRollbackStage(rollbackParams) + */ + private def executeRollbackStage(params) { + try { + script.echo "🔄 开始回滚操作..." + script.echo "📋 回滚配置:" + script.echo " 📦 项目名称: ${params.projectName}" + + // 输出回滚版本信息(如果指定) + if (params.previousVersion) { + script.echo " 📋 回滚版本: ${params.previousVersion}" + } else { + script.echo " 📋 回滚版本: 上一个稳定版本(自动检测)" + + // 获取上一个稳定版本 + params.previousVersion = getPreviousStableVersion() + } + + // 确定要回滚的目标服务器列表 + // 支持多服务器部署和单服务器部署两种模式 + def targetServers = params.targetServers + script.echo " 📡 目标服务器数量: ${targetServers.size()}" + script.echo " 🖥️ 目标服务器列表: ${targetServers.join(', ')}" + + // 对每个目标服务器执行回滚操作 + targetServers.each { server -> + if (server) { + script.echo " 🔄 正在回滚服务器: ${server.serverName} (${server.serverIP}:${server.serverPort}" + + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def rollbackParams = [ + serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + serverPort : server.serverPort, // SSH端口号(默认22) + serverName : server.serverName, // 服务器名称标识,用于日志和监控 + sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + timeout : params.deploymentTimeout, // 部署超时时间 + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + ] + + // 该方法会处理SSH连接、回滚命令执行、版本切换等 + rollbackDeployment(rollbackParams) + } + } + + script.echo "✅ 回滚操作完成" + script.echo "🎉 应用已成功回退到稳定状态" + script.echo "💡 建议: 检查日志确定部署失败原因后再重新部署" + + // 发送回滚通知 + sendRollbackNotification() + + } catch (Exception rollbackError) { + // 回滚操作失败处理 + script.echo "💥 回滚操作失败: ${rollbackError.message}" + script.echo "🚨 需要手动介入处理" + script.echo "💡 手动处理步骤:" + script.echo " 1. 登录服务器检查应用状态" + script.echo " 2. 查看应用日志确定问题原因" + script.echo " 3. 手动执行回滚命令或修复问题" + script.echo " 4. 验证应用是否恢复正常" + script.echo " 5. 联系开发团队分析根本原因" + + // 输出具体的回滚命令示例,便于手动操作 + script.echo "🔧 手动回滚命令示例:" + script.echo " ssh user@${targets[0]} 'cd /app && ./rollback.sh'" + + // 这里不重新抛出异常,因为回滚失败已经是最终状态 + // 需要人工介入处理,避免无限循环的错误处理 + } + } + + /** + * 获取上一个稳定版本的方法示例 + */ + def getPreviousStableVersion() { + // 这里可以实现从版本管理系统中获取上一个稳定版本的逻辑 + // 例如:从Git标签、Docker镜像标签、版本文件中获取 + try { + def previousVersion = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim() + echo "📋 检测到上一个稳定版本: ${previousVersion}" + return previousVersion + } catch (Exception e) { + echo "⚠️ 无法自动检测上一个版本,使用默认回滚策略" + return null // 返回null让工具类使用默认回滚逻辑 + } + } + + /** + * 发送回滚通知的方法示例 + */ + def sendRollbackNotification() { + echo "📧 发送回滚通知..." + // 这里可以实现邮件、Slack、钉钉等通知逻辑 + // 例如: + // slackSend channel: '#deployments', message: "🚨 部署失败已触发回滚" + // emailext subject: '部署回滚通知', body: '应用部署失败已自动回滚' + } + + /** + * 执行回滚部署操作 + * 负责将指定项目回退到上一个稳定版本,支持远程服务器操作和完整的错误处理 + * + * @param rollbackParams 回滚参数Map,包含以下字段: + * - timeout: Integer - 操作超时时间(秒) + * - projectName: String - 项目名称(必须) + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - retryAttempts: Integer - 重试次数 + * + * @throws Exception 当回滚操作失败时抛出异常,包含详细的错误信息和诊断建议 + * + * @note 该方法依赖于远程服务器上已部署的回滚脚本和正确的SSH配置 + * @note 支持重试机制,提高在网络不稳定情况下的成功率 + */ + def rollbackDeployment(rollbackParams) { + // ========================================================================== + // 参数提取和解构 + // ========================================================================== + + // 从服务器配置中提取IP地址(支持user@host格式) + def serverIP = rollbackParams.serverIP + // 从服务器配置中提取SSH端口号(默认22) + def serverPort = rollbackParams.serverPort + // 从服务器配置中提取SSH密钥凭证ID(Jenkins中配置的凭证名称) + def sshKey = rollbackParams.sshKey + // 从回滚参数中提取操作超时时间(秒) + def timeout = rollbackParams.timeout + // 从回滚参数中提取项目名称(用于标识要回滚的具体应用) + def projectName = rollbackParams.projectName + // 从回滚参数中提取部署脚本文件名(默认:deploy.sh) + def deployScript = rollbackParams.deployScript + // 从回滚参数中提取部署脚本所在路径 + def deployPath = rollbackParams.deployPath + // 从回滚参数中提取重试次数(默认:3次) + def retryAttempts = rollbackParams.retryAttempts ?: 3 + + // ========================================================================== + // 回滚信息输出和验证 + // ========================================================================== + + // 输出回滚操作开始信息,便于调试和审计追踪 + script.echo "🔄 执行回滚操作:" + script.echo " 📍 目标服务器: ${serverIP}:${serverPort}" + script.echo " 📦 项目名称: ${projectName}" + script.echo " 📁 脚本路径: ${deployPath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + script.echo " 🔄 重试次数: ${retryAttempts}次" + + // 验证必要参数是否存在 + if (!serverIP) { + throw new IllegalArgumentException("缺少服务器IP地址参数") + } + if (!projectName) { + throw new IllegalArgumentException("缺少项目名称参数") + } + + try { + // ========================================================================== + // 回滚命令构建 + // ========================================================================== + + // 构建回滚命令字符串 + // 格式: cd {部署脚本路径}; ./{部署脚本} rollback {项目名称} + def command = "cd ${deployPath}; ./${deployScript} rollback ${projectName}" + script.echo " 🖥️ 回滚命令: ${command}" + + // ========================================================================== + // 远程命令执行 + // ========================================================================== + + // 执行远程回滚命令,支持重试机制 + // executeRemoteCommandWithRetry 方法处理SSH连接、命令执行和重试逻辑 + def success = executeRemoteCommandWithRetry( + serverIP, // 目标服务器地址 + serverPort, // SSH端口号 + sshKey, // SSH密钥凭证 + command, // 要执行的命令 + timeout, // 超时时间 + retryAttempts // 重试次数 + ) + + // ========================================================================== + // 成功处理 + // ========================================================================== + + // 回滚成功日志输出 + script.echo "✅ 回滚成功完成: ${projectName} 已回退到上一个稳定版本" + script.echo "💡 建议: 检查应用日志确认回滚状态" + script.echo " 📋 验证步骤:" + script.echo " 1. 检查应用进程状态" + script.echo " 2. 查看应用启动日志" + script.echo " 3. 验证服务健康状态" + script.echo " 4. 确认业务功能正常" + + return success + + } catch (Exception e) { + // ========================================================================== + // 失败处理 + // ========================================================================== + + // 回滚失败错误信息输出 + script.echo "❌ 回滚失败: ${projectName} - ${e.message}" + script.echo "💡 故障排除建议:" + script.echo " 1. 🔍 检查服务器SSH连接是否正常" + script.echo " 2. 📁 确认部署脚本是否存在且可执行" + script.echo " 3. 🏷️ 验证项目名称是否正确" + script.echo " 4. 💾 检查服务器磁盘空间和权限" + script.echo " 5. 🔑 验证SSH密钥凭证是否有效" + script.echo " 6. 🌐 检查网络连接和防火墙设置" + script.echo " 7. ⏱️ 确认超时时间设置是否合理" + + // 重新抛出异常,让调用方能够处理回滚失败 + // 保留原始异常信息以便于调试和根因分析 + throw new Exception("回滚操作失败: ${e.message}", e) + } + } + +} \ No newline at end of file diff --git a/src/com/jdk/DeploymentService.groovy b/src/com/jdk/DeploymentService.groovy index 902da69..1b4ea12 100644 --- a/src/com/jdk/DeploymentService.groovy +++ b/src/com/jdk/DeploymentService.groovy @@ -1,6 +1,5 @@ package com.jdk -import com.jdk.utils.PipelineUtils import com.jdk.stages.BuildService import com.jdk.stages.ContainerBuildService import com.jdk.stages.DeployService @@ -36,13 +35,6 @@ class DeploymentService implements Serializable { */ def script - /** - * 管道工具类实例 - * 提供通用的工具方法和工具函数 - * 类型: PipelineUtils 自定义工具类 - */ - def utils - /** * 环境配置信息 * 存储环境特定的配置参数,如服务器列表、环境变量等 @@ -111,15 +103,14 @@ class DeploymentService implements Serializable { // 初始化基础属性 this.script = script this.envConfig = envConfig - this.utils = new PipelineUtils(script, envConfig) // 初始化各个阶段服务(依赖注入模式) // 通过分离关注点,提高代码的可测试性和可维护性 - this.buildService = new BuildService(script, utils, envConfig) - this.containerBuildService = new ContainerBuildService(script, utils, envConfig) - this.deployService = new DeployService(script, utils, envConfig) - this.healthCheckService = new HealthCheckService(script, utils, envConfig) - this.rollbackService = new RollbackService(script, utils, envConfig) + this.buildService = new BuildService(script, envConfig) + this.containerBuildService = new ContainerBuildService(script, envConfig) + this.deployService = new DeployService(script, envConfig) + this.healthCheckService = new HealthCheckService(script, envConfig) + this.rollbackService = new RollbackService(script, envConfig) script.echo "🔧 DeploymentService 初始化完成" } diff --git a/src/com/jdk/ResolverService.groovy b/src/com/jdk/ResolverService.groovy index a671080..f1d4539 100644 --- a/src/com/jdk/ResolverService.groovy +++ b/src/com/jdk/ResolverService.groovy @@ -213,7 +213,6 @@ class ResolverService implements Serializable { script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" script.echo " POM路径: ${serviceConfig.pomFilePath ?: '未设置'}" script.echo " 服务路径: ${serviceConfig.servicePath ?: '未设置'}" - script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" script.echo " JAR包名称: ${serviceConfig.jarName ?: '未设置'}" script.echo " 服务端口: ${serviceConfig.port ?: '未设置'}" } @@ -347,6 +346,7 @@ class ResolverService implements Serializable { port : serviceConfig.port, pomFilePath : serviceConfig.pomFilePath, deployServer : serviceConfig.deployServer, + composeFile : serviceConfig.composeFile, // 环境相关参数 PROFILES : profiles.default_profile, @@ -644,11 +644,11 @@ class ResolverService implements Serializable { // 添加环境分支镜像仓库配置 // ========================================================================== def registryConfig = config.common?.registrys?."${envConfig.registry}" - params.registry = registryConfig?.registry ?: "192.168.10.102:8001" - params.registryCredential = registryConfig?.registryCredential ?: "xiaomayi-hub-credentials" - params.registryProject = registryConfig?.registryProject ?: "xiaomayi" - params.registryImage = registryConfig?.registryImage ?: "jdk:17.0.16-centos8-dos" - params.registryType = registryConfig.registryType ?: "harbor" + params.registry = registryConfig?.registry + params.registryCredential = registryConfig?.registryCredential + params.registryProject = registryConfig?.registryProject + params.registryImage = registryConfig?.registryImage + params.registryType = registryConfig.registryType params.imageFullName = "${params.registry}/${params.registryProject}/${params.projectName}" script.echo "✅ ${envKey} 环境全部参数获取完成" diff --git a/src/com/jdk/stages/BuildService.groovy b/src/com/jdk/stages/BuildService.groovy index ab15cc9..e1f8423 100644 --- a/src/com/jdk/stages/BuildService.groovy +++ b/src/com/jdk/stages/BuildService.groovy @@ -26,13 +26,6 @@ class BuildService implements Serializable { */ def script - /** - * 管道工具类实例 - * 提供通用的工具方法和工具函数,如Maven构建命令执行 - * 类型: PipelineUtils 自定义工具类 - */ - def utils - /** * 环境配置信息 * 存储构建环境特定的配置参数 @@ -47,18 +40,13 @@ class BuildService implements Serializable { * - 用途:用于在服务中访问Jenkins DSL方法 * - 示例:在Pipeline中通过`this`传递当前脚本上下文 * - * @param utils 管道工具类实例(必须) - * - 用途:提供构建相关的工具方法 - * - 示例:执行Maven命令、处理依赖等 - * * @param envConfig 环境配置信息(可选) * - 类型:Map 键值对配置映射 * - 默认值:空Map [:] * - 内容:构建环境相关的配置参数 */ - BuildService(script, utils, envConfig = [:]) { + BuildService(script, envConfig = [:]) { this.script = script - this.utils = utils this.envConfig = envConfig script.echo "🔧 BuildService 初始化完成" @@ -116,9 +104,9 @@ class BuildService implements Serializable { validateProfile(params) // 执行Maven构建命令 - // 使用PipelineUtils中的buildMavenProject方法执行实际构建 + // 使用buildMavenProject方法执行实际构建 script.echo "🔨 执行Maven构建命令..." - utils.buildMavenProject(params) + buildMavenProject(params) // 构建成功完成 script.echo "✅ 代码构建完成" @@ -213,4 +201,87 @@ class BuildService implements Serializable { } } + /** + * 使用Maven构建项目 + * 执行Maven构建命令,包括依赖清理、编译、打包等操作,并显示构建结果信息 + * 支持自定义Maven设置、构建环境、跳过测试等配置 + * + * @param params 构建参数Map,包含以下字段: + * - MAVEN_SET: String - Maven设置文件ID(Jenkins中配置的Config File ID) + * - PROFILES: String - Maven构建环境profiles(如:dev, test, prod) + * - jarFilePath: String - JAR文件输出路径 + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认true,生产环境建议设为false) + * - goals: String - Maven执行目标(默认"clean package") + * - pomFilePath: String - POM文件路径(默认pom.xml) + * + * @throws Exception 当Maven构建失败时抛出异常 + */ + def buildMavenProject(params) { + // 从参数中获取Maven设置文件ID,用于指定Maven配置 + def mavenSettings = params.MAVEN_SET + // 获取构建环境配置,用于激活对应的Maven profile + def profiles = params.PROFILES + // 获取JAR文件输出目录路径 + def jarFilePath = params.jarFilePath + // 获取生成的JAR文件名 + def jarFile = params.jarFile + // 是否跳过测试,默认true(生产环境建议设置为false) + def skipTests = params.skipTests ?: true + // Maven执行目标,默认执行clean和package + def mavenGoals = params.goals ?: "clean package" + // POM文件路径,默认为项目根目录的pom.xml + def pomFilePath = params.pomFilePath + + script.echo "🔨 开始Maven构建项目..." + script.echo "📋 构建参数:" + script.echo " 📁 Maven配置: ${mavenSettings}" + script.echo " 🌍 环境Profile: ${profiles}" + script.echo " 📦 输出路径: ${jarFilePath}" + script.echo " 🏷️ JAR文件: ${jarFile}" + script.echo " ⚡ 跳过测试: ${skipTests}" + script.echo " 🎯 Maven目标: ${mavenGoals}" + script.echo " 📄 POM文件: ${pomFilePath}" + + try { + // 使用Config File Provider插件提供Maven settings.xml配置文件 + script.configFileProvider([script.configFile(fileId: mavenSettings, variable: 'MAVEN_SETTINGS_XML')]) { + // 执行Maven构建命令 + script.sh """ + /usr/local/maven/bin/mvn --settings ${script.env.MAVEN_SETTINGS_XML} \\ + dependency:purge-local-repository -Dinclude=com.yunheng \\ + -f ${pomFilePath} ${mavenGoals} \\ + -P${profiles} \\ + -Dmaven.test.skip=${skipTests} + """ + } + + script.echo "✅ Maven构建完成" + + // 显示构建结果信息 + script.echo "📊 构建结果验证:" + + // 显示JAR文件的详细列表信息(大小、权限等) + script.sh "ls -lsh ${jarFilePath}/${jarFile}" + + // 计算JAR文件的MD5校验和,用于验证文件完整性 + script.sh "md5sum ${jarFilePath}/${jarFile}" + + // 可选:显示文件大小(人类可读格式) + script.sh "du -h ${jarFilePath}/${jarFile}" + + // 可选:显示最后修改时间 + script.sh "stat ${jarFilePath}/${jarFile}" + + } catch (Exception e) { + script.echo "❌ Maven构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Maven settings.xml配置是否正确" + script.echo " - 网络连接是否正常(依赖下载)" + script.echo " - POM文件路径是否正确" + script.echo " - 代码编译是否有错误" + throw e // 重新抛出异常,让调用方处理构建失败 + } + } + } \ No newline at end of file diff --git a/src/com/jdk/stages/ContainerBuildService.groovy b/src/com/jdk/stages/ContainerBuildService.groovy index 5a543f7..5aec6a1 100644 --- a/src/com/jdk/stages/ContainerBuildService.groovy +++ b/src/com/jdk/stages/ContainerBuildService.groovy @@ -26,13 +26,6 @@ class ContainerBuildService implements Serializable { */ def script - /** - * 管道工具类实例 - * 提供通用的工具方法和工具函数,如Docker构建命令执行 - * 类型: PipelineUtils 自定义工具类 - */ - def utils - /** * 环境配置信息 * 存储容器构建环境特定的配置参数 @@ -47,18 +40,13 @@ class ContainerBuildService implements Serializable { * - 用途:用于在服务中访问Jenkins DSL方法 * - 示例:在Pipeline中通过`this`传递当前脚本上下文 * - * @param utils 管道工具类实例(必须) - * - 用途:提供容器构建相关的工具方法 - * - 示例:执行Docker命令、生成镜像标签等 - * * @param envConfig 环境配置信息(可选) * - 类型:Map 键值对配置映射 * - 默认值:空Map [:] * - 内容:镜像仓库配置、构建选项等 */ - ContainerBuildService(script, utils, envConfig = [:]) { + ContainerBuildService(script, envConfig = [:]) { this.script = script - this.utils = utils this.envConfig = envConfig script.echo "🔧 ContainerBuildService 初始化完成" @@ -120,10 +108,10 @@ class ContainerBuildService implements Serializable { } // 构建Docker镜像并获取镜像标签 - // 使用PipelineUtils中的buildDockerImage方法执行实际构建 + // 使用buildDockerImage方法执行实际构建 // 该方法会处理Docker构建、标签生成和镜像推送 script.echo "🔨 执行Docker镜像构建..." - def imageTag = utils.buildDockerImage(params) + def imageTag = buildDockerImage(params) // 将镜像标签存储到环境变量,供后续部署阶段使用 // 环境变量可以在整个Pipeline过程中共享使用 @@ -150,4 +138,150 @@ class ContainerBuildService implements Serializable { throw e } } + + /** + * 构建Docker镜像 + * 使用Docker Pipeline插件构建和推送Docker镜像到指定的镜像仓库 + * 支持自定义构建参数、构建选项和自动清理本地镜像 + * + * @param params 构建参数Map,包含以下必需字段: + * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) + * - registryCredential: String - Jenkins凭证ID,用于仓库认证 + * - registryProject: String - 仓库中的项目/命名空间名称(如:xiaomayi) + * - projectName: String - 项目名称(如:xiaomayi-admin) + * - PROFILES: String - 环境名称(dev/test/uat/prod),用于生成镜像标签 + * 可选字段: + * - buildOptions: String - Docker构建选项(如:--no-cache --pull) + * - baseImage: String - 基础镜像名称,通过--build-arg传递 + * - port: String/Integer - 服务端口号,通过--build-arg传递 + * - jarFile: String - JAR文件名,通过--build-arg传递 + * - jarFilePath: String - JAR文件路径,通过--build-arg传递 + * + * @return String 构建的镜像标签(如:dev-123、prod-456) + * @throws Exception 当Docker构建或推送失败时抛出异常 + */ + def buildDockerImage(params) { + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = generateImageTag(params.PROFILES) + script.echo "🏷️ 生成的镜像标签: ${imageTag}" + + // 获取构建选项,如果未提供则使用空字符串 + def buildOptions = params.buildOptions ?: "" + script.echo "⚙️ 构建选项: ${buildOptions}" + + // 输出构建开始信息和详细参数 + script.echo "🐳 开始构建Docker镜像..." + script.echo " 📦 镜像仓库: ${params.registry}" + script.echo " 📁 命名空间: ${params.registryProject}" + script.echo " 🏷️ 项目名称: ${params.projectName}" + script.echo " 🎯 镜像标签: ${imageTag}" + script.echo " 🔑 仓库凭证: ${params.registryCredential}" + + try { + // 使用Docker Pipeline插件连接到镜像仓库并进行构建推送 + script.docker.withRegistry("https://${params.registry}", params.registryCredential) { + // 构建Docker镜像 + // 镜像名称格式: {命名空间}/{项目名称}:{标签} + def dockerImage = script.docker.build( + // 完整的镜像名称(包含标签) + "${params.registryProject}/${params.projectName}:${imageTag}", + // 构建命令参数组合 + "${buildOptions} " + + // 传递构建参数给Dockerfile + // 环境配置 + "--build-arg VAR_PROFILES=${params.PROFILES} " + + // 基础镜像 + "--build-arg VAR_BASE_IMAGE=${params.baseImage} " + + // 服务端口 + "--build-arg VAR_PORT=${params.port} " + + // JAR文件名 + "--build-arg VAR_JARFILE=${params.jarFile} " + + // JAR文件路径 + "--build-arg VAR_JARFILE_PATH=${params.jarFilePath} " + + // 指定Dockerfile路径和构建上下文,使用项目根目录下的.jenkins/Dockerfile文件 + "-f ${params.dockerFile} ./" + ) + + // 推送镜像到远程仓库 + script.echo "🚀 推送镜像到仓库: ${params.registry}" + dockerImage.push() + + // 清理本地Docker镜像,释放磁盘空间 + script.echo "🧹 开始清理本地镜像..." + cleanupDockerImages(params.registry, params.registryProject, params.projectName, imageTag) + } + + script.echo "✅ Docker镜像构建和推送完成" + return imageTag + + } catch (Exception e) { + script.echo "❌ Docker镜像构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Dockerfile路径是否正确" + script.echo " - 镜像仓库凭证是否有效" + script.echo " - 网络连接是否正常" + throw e // 重新抛出异常,让调用方处理 + } + } + + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + private def generateImageTag(profile) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID + } + + /** + * 清理Docker镜像 + * 删除本地Docker环境中指定的镜像,避免磁盘空间占用和镜像冲突 + * 主要用于构建后清理,移除不再需要的临时镜像 + * + * @param registry 镜像仓库地址(如:192.168.10.102:8001) + * @param registryProject 仓库中的项目/命名空间名称(如:xiaomayi) + * @param projectName 项目名称(如:xiaomayi-admin) + * @param imageTag 镜像标签(由generateImageTag方法生成) + * + * @note 使用"|| true"确保清理失败不会中断构建流程 + * @note 尝试清理两种格式的镜像: + * 1. {registryProject}/{projectName}:{imageTag} + * 2. {registry}/{registryProject}/{projectName}:{imageTag} + */ + private def cleanupDockerImages(registry, registryProject, projectName, imageTag) { + try { + // 输出清理开始信息 + script.echo "🧹 开始清理Docker镜像: ${projectName}:${imageTag}" + + // 清理第一种格式的镜像:{项目}/{应用}:{标签} + // 例如:xiaomayi/xiaomayi-admin:dev-123 + // 使用"|| true"确保命令即使失败也不会抛出异常中断脚本 + script.sh "docker rmi -f ${registryProject}/${projectName}:${imageTag} || true" + + // 清理第二种格式的镜像:{仓库地址}/{项目}/{应用}:{标签} + // 例如:192.168.10.102:8001/xiaomayi/xiaomayi-admin:dev-123 + script.sh "docker rmi -f ${registry}/${registryProject}/${projectName}:${imageTag} || true" + + script.echo "✅ Docker镜像清理完成" + + } catch (Exception e) { + // 捕获并处理异常,输出错误信息但不会中断主流程 + // 镜像清理失败通常不会影响主要功能,所以只是记录日志 + script.echo "⚠️ 清理Docker镜像失败: ${e.message}" + script.echo " 此错误通常可以忽略,不会影响部署流程" + } + } + } \ No newline at end of file diff --git a/src/com/jdk/stages/DeployService.groovy b/src/com/jdk/stages/DeployService.groovy index e36e707..b17dbb5 100644 --- a/src/com/jdk/stages/DeployService.groovy +++ b/src/com/jdk/stages/DeployService.groovy @@ -28,13 +28,6 @@ class DeployService implements Serializable { */ def script - /** - * 管道工具类实例 - * 提供通用的工具方法和工具函数,如部署命令执行 - * 类型: PipelineUtils 自定义工具类 - */ - def utils - /** * 环境配置信息 * 存储部署环境特定的配置参数 @@ -49,18 +42,13 @@ class DeployService implements Serializable { * - 用途:用于在服务中访问Jenkins DSL方法 * - 示例:在Pipeline中通过`this`传递当前脚本上下文 * - * @param utils 管道工具类实例(必须) - * - 用途:提供部署相关的工具方法 - * - 示例:执行部署命令、处理服务器连接等 - * * @param envConfig 环境配置信息(可选) * - 类型:Map 键值对配置映射 * - 默认值:空Map [:] * - 内容:部署策略、超时设置、健康检查等配置 */ - DeployService(script, utils, envConfig = [:]) { + DeployService(script, envConfig = [:]) { this.script = script - this.utils = utils this.envConfig = envConfig script.echo "🔧 DeployService 初始化完成" @@ -198,6 +186,7 @@ class DeployService implements Serializable { buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + composeFile : params.composeFile, ] // ========================================================================== @@ -222,7 +211,7 @@ class DeployService implements Serializable { // 执行部署操作 script.echo "🚀 正在执行部署..." - utils.deployToServer(deployParams) + deployToServer(deployParams) script.echo "✅ 单服务器部署完成: ${server.serverName}" @@ -315,6 +304,7 @@ class DeployService implements Serializable { buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + composeFile : params.composeFile, ] // ========================================================================== @@ -333,7 +323,7 @@ class DeployService implements Serializable { // ========================================================================== script.echo "🚀 正在执行部署到 ${server.serverName}..." // 调用部署工具类,传递完整的部署参数 - utils.deployToServer(deployParams) + deployToServer(deployParams) // 输出部署成功信息 script.echo "✅ 部署成功: ${server.serverName}" @@ -527,6 +517,678 @@ class DeployService implements Serializable { return timeouts[env] ?: 1800 } + /** + * 部署到单个服务器 + * 执行远程部署命令,将指定版本的Docker镜像部署到目标服务器 + * 该方法包含完整的部署流程:文件传输、权限设置、命令执行和结果验证 + * + * @param deployParams 部署参数Map,包含以下必需字段: + * - serverIP: String - 服务器IP地址(支持user@host格式,如esxi@124.222.176.210) + * - serverPort: Integer - 服务器SSH端口号 + * - projectName: String - 项目名称(如xiaomayi-admin) + * - profile: String - 环境profile(dev/test/uat/prod) + * - buildId: String - 构建ID(用于版本追踪) + * - deployPath: String - 远程服务器部署路径(如/usr/local/docker/xiaomayi-web/) + * - deployScript: String - 部署脚本文件名(如deploy_env.sh) + * 可选字段: + * - sshKey: String - SSH密钥凭证ID(默认为jenkins-ssh-205) + * - timeout: Integer - 命令执行超时时间(秒,默认300秒) + * - extraArgs: String - 额外的部署参数 + * - retryAttempts: Integer - 失败重试次数(默认3次) + * + * @throws IllegalArgumentException 参数缺失或无效时抛出 + * @throws Exception SSH连接、文件传输或命令执行失败时抛出 + * + * @example + * deployToServer([ + * serverIP: "esxi@124.222.176.210", + * serverPort: 12422, + * projectName: "xiaomayi-admin", + * profile: "prod", + * buildId: "31", + * deployPath: "/usr/local/docker/xiaomayi-web/", + * deployScript: "deploy_env.sh", + * sshKey: "jenkins-ssh-205", + * timeout: 300 + * ]) + */ + def deployToServer(deployParams) { + // 参数解析和默认值设置 + def serverIP = deployParams.serverIP + def serverPort = deployParams.serverPort ?: 22 // 默认SSH端口 + def projectName = deployParams.projectName + def profile = deployParams.profile + def buildId = deployParams.buildId + def sshKey = deployParams.sshKey ?: 'jenkins-ssh-205' // 默认SSH凭证 + def timeout = deployParams.deploymentTimeout ?: 300 // 默认5分钟超时 + def extraArgs = deployParams.extraArgs ?: '' // 额外参数 + def retryAttempts = deployParams.retryAttempts ?: 3 // 默认重试3次 + def deployPath = deployParams.deployPath // 远程部署路径 + def deployScript = deployParams.deployScript // 部署脚本文件名 + def composeFile = deployParams.composeFile + + // 日志输出部署信息 + script.echo "🚀 开始部署到服务器: ${serverIP}:${serverPort}" + script.echo "📦 项目: ${projectName}, 环境: ${profile}, 构建ID: ${buildId}" + script.echo "📁 部署路径: ${deployPath}" + script.echo "📜 部署脚本: ${deployScript}" + + try { + // 生成镜像标签(包含环境、构建ID和时间戳信息) + def imageTag = generateImageTag(profile) + script.echo "🏷️ 生成的镜像标签: ${imageTag}" + + // 第一步:传输部署文件到远程服务器 + // 支持多文件和重命名功能,包括部署脚本和环境配置文件 + script.echo "📤 开始文件传输阶段..." + + // 构建文件传输列表 + def transferFiles = [ + // 部署脚本(保持原文件名) + [localPath: ".pipeline-files/scripts-file/${deployScript}", mode: "755"], + // 环境文件(重命名为 .env) + [localPath: ".pipeline-files/scripts-file/docker-env-${profile}.sh", remoteName: ".env", mode: "644"], + // Docker执行文件 + [localPath: ".pipeline-files/compose-file/${composeFile}.yml", remoteName: "docker-compose.yml"], + ] + transferDeploymentFiles([ + serverIP: serverIP, + serverPort: serverPort, + sshKey: sshKey, + remotePath: deployPath, + files: transferFiles, // 使用动态文件列表 + timeout: 300 + ]) + + // 第二步:设置远程文件执行权限 + // 确保部署脚本有可执行权限,避免"Permission denied"错误 + script.echo "🔧 开始权限设置阶段..." + setRemoteFilePermissions([ + serverIP: serverIP, + serverPort: serverPort, + sshKey: sshKey, + remotePath: deployPath, // 远程路径 + deployScript: deployScript, // 要设置权限的脚本文件 + timeout: 60 // 权限设置超时时间 + ]) + + // 第三步:构建部署命令 + // 使用bash显式执行脚本,双重保障避免权限问题 + script.echo "🔨 构建部署命令..." + def command = buildDeploymentCommand(deployPath, deployScript, projectName, imageTag, extraArgs) + script.echo "💻 部署命令已构建完成" + + // 第四步:执行远程部署命令(支持重试机制) + // 使用指数退避策略进行重试,提高部署成功率 + script.echo "⚡ 开始执行部署命令..." + def success = executeRemoteCommandWithRetry( + serverIP, + serverPort, + sshKey, + command, + timeout, + retryAttempts + ) + + // 部署成功日志 + script.echo "✅ 部署成功完成: ${projectName} 已部署到 ${serverIP}" + script.echo "🎯 镜像版本: ${imageTag}" + script.echo "📍 部署路径: ${deployPath}" + + // 第五步:生成部署摘要报告 + // 包含部署详情、时间戳、版本信息等 + script.echo "📊 生成部署摘要报告..." + generateDeploymentSummary(deployParams, imageTag, success) + + script.echo "🎉 部署流程全部完成" + + } catch (Exception e) { + // 部署失败处理 + script.echo "❌ 部署失败: ${e.message}" + script.echo "🔍 失败详情: ${e.getClass().getName()}" + script.echo "💡 建议检查网络连接、服务器状态和文件权限" + + // 重新抛出异常,让上层调用者处理 + throw new Exception("部署到服务器 ${serverIP} 失败: ${e.message}", e) + } + } + + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + private def generateImageTag(profile) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID + } + + /** + * 传输部署文件到远程服务器(支持多文件和重命名) + * 使用SCP将多个文件传输到目标服务器,支持自定义远程文件名和权限设置 + * 该方法提供了灵活的文件传输解决方案,适用于复杂的部署场景 + * + * @param scpParams SCP传输参数Map,包含以下字段: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - 服务器SSH端口(默认22) + * - sshKey: String - SSH密钥凭证ID(默认为jenkins-ssh-205) + * - remotePath: String - 远程服务器目标路径(必需) + * - timeout: Integer - 超时时间(秒,默认300) + * - files: List - 文件传输列表,每个文件包含配置信息: + * - localPath: String - 本地文件路径(必需) + * - remoteName: String - 远程文件名(可选,不提供则使用本地文件名) + * - mode: String - 文件权限模式(可选,如"755"、"644") + * + * @throws Exception 当SCP传输失败时抛出异常,包含详细的错误信息 + * + * @example + * // 高级用法:传输多个文件并重命名 + * transferDeploymentFiles([ + * serverIP: "user@192.168.124.126", + * serverPort: 12422, + * sshKey: "jenkins-ssh-205", + * remotePath: "/usr/local/docker/xiaomayi-web/", + * timeout: 600, + * files: [ + * [localPath: ".pipeline-files/scripts/deploy_env.sh", remoteName: "deploy.sh", mode: "755"], + * [localPath: ".pipeline-files/scripts/docker-env-test.sh", remoteName: ".env", mode: "644"], + * [localPath: ".pipeline-files/config/app-config.json", mode: "644"], + * [localPath: ".pipeline-files/certs/ssl.crt", remoteName: "server.crt"], + * [localPath: ".pipeline-files/certs/ssl.key", remoteName: "server.key"] + * ] + * ]) + */ + def transferDeploymentFiles(scpParams) { + // 参数解析和默认值设置 + def serverIP = scpParams.serverIP // 服务器IP地址(必需) + def serverPort = scpParams.serverPort ?: 22 // SSH端口,默认22 + def sshKey = scpParams.sshKey ?: 'jenkins-ssh-205' // SSH凭证,默认jenkins-ssh-205 + def remotePath = scpParams.remotePath // 远程目标路径(必需) + def timeout = scpParams.timeout ?: 300 // 超时时间,默认300秒(5分钟) + def files = scpParams.files ?: [] // 文件列表,默认为空列表 + + // 日志输出传输信息 + script.echo "📤 开始传输部署文件到服务器: ${serverIP}:${serverPort}" + script.echo "📋 传输参数:" + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + script.echo " 📊 文件数量: ${files.size()}" + + // 检查文件列表是否为空,避免不必要的操作 + if (files.isEmpty()) { + script.echo "⚠️ 没有文件需要传输,跳过文件传输阶段" + return // 直接返回,不执行后续操作 + } + + try { + // 使用Jenkins凭证管理安全地处理SSH密钥 + // withCredentials确保密钥在使用后自动清理,避免安全风险 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径存储变量 + usernameVariable: 'SSH_USERNAME' // 用户名存储变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 连接用户: ${username}" + script.echo " 🌐 目标主机: ${hostname}" + + // 遍历文件列表,逐个传输文件 + // eachWithIndex提供索引信息,用于进度显示 + files.eachWithIndex { fileConfig, index -> + // 解析单个文件的配置 + def localPath = fileConfig.localPath // 本地文件路径(必需) + // 远程文件名:如果未指定则使用本地文件名 + def remoteName = fileConfig.remoteName ?: getFileName(localPath) + def mode = fileConfig.mode // 文件权限模式(可选) + + // 输出传输进度信息 + script.echo " 📤 传输文件 (${index + 1}/${files.size()}): ${localPath} → ${remotePath}/${remoteName}" + if (mode) { + script.echo " 🔧 将设置权限: ${mode}" + } + + // 执行SCP文件传输命令 + executeSCPCommand( + localPath, // 本地文件路径 + "${username}@${hostname}:${remotePath}/${remoteName}", // 远程目标路径 + serverPort, // SSH端口 + sshKey, // SSH凭证 + timeout, // 超时时间 + 3 // 重试次数(默认3次) + ) + + // 如果指定了文件权限模式,设置远程文件权限 + if (mode) { + setRemoteFileMode(serverIP, serverPort, sshKey, "${remotePath}/${remoteName}", mode, timeout) + } + + script.echo " ✅ 文件传输完成: ${remoteName}" + } + } + + // 传输完成后的摘要信息 + script.echo "✅ 所有文件传输完成" + script.echo "💡 传输摘要:" + script.echo " =========================================" + files.eachWithIndex { fileConfig, index -> + def localPath = fileConfig.localPath + def remoteName = fileConfig.remoteName ?: getFileName(localPath) + def mode = fileConfig.mode ?: "默认权限" + script.echo " ${index + 1}. ${localPath} → ${remotePath}/${remoteName} (权限: ${mode})" + } + script.echo " =========================================" + + } catch (Exception e) { + // 错误处理:提供详细的错误信息和排查建议 + script.echo "❌ 文件传输失败: ${e.message}" + script.echo "🔍 错误类型: ${e.getClass().getSimpleName()}" + script.echo "💡 排查建议:" + script.echo " - 检查本地文件路径是否正确: ${files.collect{ it.localPath }}" + script.echo " - 验证远程目录是否存在且有写权限: ${remotePath}" + script.echo " - 确认SSH密钥凭证是否有效: ${sshKey}" + script.echo " - 检查网络连接和防火墙设置" + script.echo " - 验证目标服务器SSH服务是否正常运行" + + // 重新抛出异常,让上层调用者处理 + throw new Exception("文件传输到服务器 ${serverIP} 失败: ${e.message}", e) + } + } + + /** + * 从文件路径中提取文件名 + * 用于当未指定远程文件名时,自动使用本地文件名 + * + * @param filePath 文件完整路径(如:/path/to/file.txt 或 relative/path/file.sh) + * @return String 文件名(如:file.txt 或 file.sh) + * + * @example + * getFileName("/opt/apps/deploy.sh") → "deploy.sh" + * getFileName("config/app-config.json") → "app-config.json" + */ + private def getFileName(filePath) { + // 使用Java的File类来提取文件名,处理各种路径格式 + return new File(filePath).getName() + } + + /** + * 设置远程文件权限模式 + * 通过SSH连接到远程服务器执行chmod命令设置文件权限 + * + * @param serverIP 服务器IP地址 + * @param serverPort SSH端口 + * @param sshKey SSH密钥凭证ID + * @param remoteFilePath 远程文件完整路径 + * @param mode 权限模式(如:"755"、"644"、"600") + * @param timeout 超时时间(秒) + * + * @note 该方法设计为容错模式,权限设置失败不会阻止后续操作 + */ + private def setRemoteFileMode(serverIP, serverPort, sshKey, remoteFilePath, mode, timeout) { + try { + // 使用凭证管理确保SSH密钥安全 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, + keyFileVariable: 'SSH_KEY_FILE', + usernameVariable: 'SSH_USERNAME' + )]) { + // 解析服务器地址 + def (username, hostname) = parseServerAddress(serverIP) + + // 构建chmod命令:设置远程文件权限 + def chmodCommand = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=60 -p ${serverPort} -i ${script.SSH_KEY_FILE} ${username}@${hostname} 'chmod ${mode} \"${remoteFilePath}\"'" + + // 执行权限设置命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: chmodCommand, returnStatus: true) + if (result == 0) { + script.echo " ✅ 权限设置成功: ${remoteFilePath} → ${mode}" + } else { + script.echo "⚠️ 权限设置失败,但继续执行: ${remoteFilePath}" + } + } + } + } catch (Exception e) { + // 权限设置失败处理(容错模式) + script.echo "⚠️ 设置文件权限时出错: ${e.message},但继续执行后续操作" + script.echo " 💡 文件仍然可以传输,但可能需要手动设置权限" + } + } + + /** + * 执行SCP文件传输命令(增强版) + * 支持错误重试机制和详细的日志输出,提高传输可靠性 + * + * @param localFile 本地文件路径 + * @param remoteTarget 远程目标路径(格式: user@host:path) + * @param serverPort SSH端口号 + * @param sshKey SSH密钥凭证ID + * @param timeout 单次尝试超时时间(秒) + * @param retries 最大重试次数(默认3次) + * + * @throws Exception 当所有重试尝试都失败后抛出异常 + */ + private def executeSCPCommand(localFile, remoteTarget, serverPort, sshKey, timeout, retries = 3) { + def attempt = 1 + def lastError = null + + // 重试循环:尝试多次传输,提高成功率 + while (attempt <= retries) { + try { + script.echo " 🔄 SCP传输尝试 (${attempt}/${retries}): ${localFile}" + + // 构建SCP命令 + def scpCommand = """ + scp -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -P ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${localFile} \ + ${remoteTarget} + """.trim() + + // 执行SCP命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: scpCommand, returnStatus: true) + if (result != 0) { + throw new Exception("SCP命令返回非零状态码: ${result}") + } + } + + script.echo " ✅ SCP传输成功: ${localFile}" + return // 传输成功,退出方法 + + } catch (Exception e) { + lastError = e + script.echo "❌ SCP传输失败 (尝试 ${attempt}): ${e.message}" + + // 如果不是最后一次尝试,等待后重试 + if (attempt < retries) { + def waitTime = attempt * 5 // 指数退避策略:5, 10, 15秒... + script.echo "⏳ 等待 ${waitTime} 秒后重试..." + script.sleep(waitTime) // 等待一段时间 + } + attempt++ // 增加尝试次数 + } + } + + // 所有重试都失败,抛出异常 + throw new Exception("SCP传输失败,重试 ${retries} 次后仍失败: ${lastError?.message}", lastError) + } + + /** + * 设置远程文件执行权限 + * 确保部署脚本在远程服务器上有执行权限,避免"Permission denied"错误 + * 该方法会尝试设置权限,但即使失败也不会中断部署流程(会尝试使用bash执行) + * + * @param permissionParams 权限设置参数Map,包含以下字段: + * - serverIP: String - 服务器IP地址 + * - serverPort: Integer - 服务器SSH端口(默认22) + * - sshKey: String - SSH密钥凭证ID + * - remotePath: String - 远程服务器路径 + * - deployScript: String - 要设置权限的脚本文件名 + * - timeout: Integer - 超时时间(秒,默认60) + * + * @throws Exception 当SSH连接完全失败时抛出异常 + * + * @note 该方法设计为容错模式,权限设置失败不会阻止后续部署尝试 + */ + def setRemoteFilePermissions(permissionParams) { + // 参数解析和默认值设置 + def serverIP = permissionParams.serverIP + def serverPort = permissionParams.serverPort ?: 22 + def sshKey = permissionParams.sshKey ?: 'jenkins-ssh-205' + def remotePath = permissionParams.remotePath + def deployScript = permissionParams.deployScript + def timeout = permissionParams.timeout ?: 60 + + script.echo "🔧 设置远程文件执行权限..." + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 📜 脚本文件: ${deployScript}" + script.echo " 🔑 SSH凭证: ${sshKey}" + + try { + // 使用Jenkins凭证管理安全地处理SSH密钥 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径环境变量 + usernameVariable: 'SSH_USERNAME' // 用户名环境变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 用户名: ${username}" + script.echo " 🌐 主机名: ${hostname}" + + // ssh -o StrictHostKeyChecking=no \\ # 禁用严格主机密钥检查 + // -o ConnectTimeout=30 \\ # 连接超时30秒 + // -o ServerAliveInterval=60 \\ # 保持连接活跃间隔60秒 + // -p ${serverPort} \\ # SSH端口 + // -i ${script.SSH_KEY_FILE} \\ # SSH密钥文件路径 + // ${username}@${hostname} \\ # 目标服务器 + // 'cd ${remotePath} && \\ # 切换到部署目录 + // chmod +x ${deployScript} && \\ # 设置执行权限 + // ls -la ${deployScript}' # 验证权限设置结果 + + // 构建chmod命令:设置执行权限并验证 + def chmodCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + 'cd ${remotePath} && \ + chmod +x ${deployScript} && \ + ls -la ${deployScript}' + """.trim() + + script.echo " ⚡ 执行权限设置命令..." + + // 执行权限设置命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: chmodCommand, returnStdout: true, returnStatus: true) + + if (result == 0) { + script.echo "✅ 文件权限设置成功" + script.echo " 📋 文件权限已更新,可执行权限已添加" + } else { + script.echo "⚠️ 文件权限设置可能存在问题,但继续执行部署" + script.echo " 💡 将尝试使用bash命令执行脚本" + } + } + } + } catch (Exception e) { + // 权限设置失败处理(容错模式) + script.echo "⚠️ 设置文件权限时出错: ${e.message}" + script.echo " 💡 错误类型: ${e.getClass().getSimpleName()}" + script.echo " 🔄 继续执行部署,将使用bash命令执行脚本" + // 这里不抛出异常,因为即使权限设置失败,我们还可以尝试使用bash执行 + } + } + + /** + * 构建部署命令 + * 创建用于远程执行的部署命令字符串,使用bash显式执行脚本避免权限问题 + * + * @param deployPath 远程部署路径(如/usr/local/docker/xiaomayi-web/) + * @param deployScript 部署脚本文件名(如deploy_env.sh) + * @param projectName 项目名称(如xiaomayi-admin) + * @param imageTag 镜像标签(如prod-31) + * @param extraArgs 额外参数 + * + * @return String 完整的部署命令字符串 + * + * @note 使用bash显式执行脚本,即使脚本没有执行权限也能正常工作 + */ + private def buildDeploymentCommand(deployPath, deployScript, projectName, imageTag, extraArgs) { + script.echo " 🛠️ 构建部署命令..." + script.echo " 📁 路径: ${deployPath}" + script.echo " 📜 脚本: ${deployScript}" + script.echo " 🏷️ 项目: ${projectName}" + script.echo " 🎯 版本: ${imageTag}" + script.echo " ➕ 参数: ${extraArgs}" + + // 构建完整的部署命令 + // 使用bash显式执行脚本,避免"./script.sh"的权限问题 + def command = """ + export PROJECT_NAME=${projectName}; \ + export IMAGE_TAG=${imageTag}; \ + cd ${deployPath}; \ + ./${deployScript} deploy ${projectName} ${imageTag} ${extraArgs} + """.trim().replaceAll("\n", " ") // 去除换行符,转换为单行命令 + + script.echo " ✅ 命令构建完成" + return command + } + + /** + * 执行远程命令(带重试机制) + * 使用指数退避策略执行SSH命令,提高在网络不稳定情况下的成功率 + * + * @param serverIP 服务器IP地址 + * @param serverPort SSH端口 + * @param sshKey SSH凭证ID + * @param command 要执行的远程命令 + * @param timeout 单次执行超时时间(秒) + * @param retryAttempts 最大重试次数 + * + * @return Boolean 命令执行是否成功 + * @throws Exception 所有重试尝试都失败后抛出异常 + */ + private def executeRemoteCommandWithRetry(serverIP, serverPort, sshKey, command, timeout, retryAttempts) { + def attempt = 1 + def lastException = null + + script.echo " 🔄 启用重试机制,最大重试次数: ${retryAttempts}" + + // 重试循环 + while (attempt <= retryAttempts) { + try { + script.echo " 🎯 执行远程命令 (尝试 ${attempt}/${retryAttempts})..." + + // 使用Jenkins凭证安全地执行SSH命令 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, + keyFileVariable: 'SSH_KEY_FILE', + usernameVariable: 'SSH_USERNAME' + )]) { + // 解析服务器地址 + def (username, hostname) = parseServerAddress(serverIP) + + // // 构建SSH命令 + // def sshCommand = """ + // ssh -o StrictHostKeyChecking=no \\ # 禁用严格主机密钥检查 + // -o ConnectTimeout=30 \\ # 连接超时30秒 + // -o ServerAliveInterval=60 \\ # 保持连接活跃间隔60秒 + // -p ${serverPort} \\ # SSH端口 + // -i ${script.SSH_KEY_FILE} \\ # SSH密钥文件 + // ${username}@${hostname} \\ # 目标服务器 + // '${command}' # 要执行的命令 + // """.trim() + + // 构建SSH命令 + def sshCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + '${command}' + """.trim() + + script.echo " ⚡ 执行SSH命令..." + + // 执行命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def output = script.sh(script: sshCommand, returnStdout: true, returnStatus: false) + script.echo " 📋 命令输出:\n${output}" + } + } + + script.echo "✅ 远程命令执行成功" + return true + + } catch (Exception e) { + lastException = e + script.echo "❌ 第 ${attempt} 次尝试失败: ${e.message}" + + // 如果不是最后一次尝试,等待后重试 + if (attempt < retryAttempts) { + def waitTime = attempt * 10 // 指数退避:10, 20, 30秒... + script.echo "⏳ 等待 ${waitTime} 秒后重试..." + script.sleep(waitTime) // 等待一段时间 + } + attempt++ // 增加尝试次数 + } + } + + // 所有重试都失败,抛出异常 + throw new Exception("远程命令执行失败,重试 ${retryAttempts} 次后仍失败: ${lastException?.message}", lastException) + } + + /** + * 解析服务器地址 + * 支持 user@host 格式和单独的host格式,提供灵活的服务器地址解析 + * + * @param serverAddress 服务器地址字符串 + * @return List 包含[用户名, 主机名]的列表 + * + * @example + * parseServerAddress("user@example.com") → ["user", "example.com"] + * parseServerAddress("192.168.1.100") → ["root", "192.168.1.100"] (使用默认用户名) + */ + private def parseServerAddress(serverAddress) { + script.echo " 🔍 解析服务器地址: ${serverAddress}" + + if (serverAddress.contains('@')) { + // 处理 user@host 格式 + def parts = serverAddress.split('@') + def result = [parts[0], parts[1]] + script.echo " ✅ 解析结果: 用户名=${result[0]}, 主机名=${result[1]}" + return result + } else { + // 处理单独的host格式,使用SSH凭证中的用户名或默认root + def username = script.SSH_USERNAME ?: 'root' + def result = [username, serverAddress] + script.echo " ✅ 解析结果: 用户名=${result[0]}(默认), 主机名=${result[1]}" + return result + } + } + + /** + * 生成部署摘要报告 + */ + private def generateDeploymentSummary(deployParams, imageTag, success) { + def summary = """ + ==================== 部署摘要 ==================== + 项目名称: ${deployParams.projectName} + 目标环境: ${deployParams.profile} + 目标服务器: ${deployParams.serverIP}:${deployParams.serverPort} + 镜像标签: ${imageTag} + 构建ID: ${deployParams.buildId} + 部署状态: ${success ? '✅ 成功' : '❌ 失败'} + 时间: ${new Date().format('yyyy-MM-dd HH:mm:ss')} + ================================================ + """ + + script.echo summary + return summary + } + /** * 辅助方法:检查是否应该在部署失败时停止 * 根据部署参数决定单个服务器失败时是否停止整个部署流程 diff --git a/src/com/jdk/stages/HealthCheckService.groovy b/src/com/jdk/stages/HealthCheckService.groovy index 7a4f781..2dd79ac 100644 --- a/src/com/jdk/stages/HealthCheckService.groovy +++ b/src/com/jdk/stages/HealthCheckService.groovy @@ -27,13 +27,6 @@ class HealthCheckService implements Serializable { */ def script - /** - * 管道工具类实例 - * 提供通用的工具方法和工具函数,如健康检查命令执行 - * 类型: PipelineUtils 自定义工具类 - */ - def utils - /** * 环境配置信息 * 存储健康检查环境特定的配置参数 @@ -55,10 +48,6 @@ class HealthCheckService implements Serializable { * - 用途:用于在服务中访问Jenkins DSL方法 * - 示例:在Pipeline中通过`this`传递当前脚本上下文 * - * @param utils 管道工具类实例(必须) - * - 用途:提供健康检查相关的工具方法 - * - 示例:执行健康检查命令、处理HTTP请求等 - * * @param envConfig 环境配置信息(可选) * - 类型:Map 键值对配置映射 * - 默认值:空Map [:] @@ -66,12 +55,11 @@ class HealthCheckService implements Serializable { * * @note 自动初始化RollbackService实例用于健康检查失败时的回滚操作 */ - HealthCheckService(script, utils, envConfig = [:]) { + HealthCheckService(script, envConfig = [:]) { this.script = script - this.utils = utils this.envConfig = envConfig // 初始化回滚服务,用于健康检查失败时的自动回滚 - this.rollbackService = new RollbackService(script, utils, envConfig) + this.rollbackService = new RollbackService(script, envConfig) script.echo "🔧 HealthCheckService 初始化完成" script.echo " ❤️ 回滚服务已就绪" @@ -139,9 +127,9 @@ class HealthCheckService implements Serializable { if (serverIP) { script.echo " 🔍 对服务器 ${serverIP}:${params.serverPort} 进行健康检查..." - // 使用PipelineUtils中的healthCheck方法执行实际健康检查 + // 使用healthCheck方法执行实际健康检查 // 该方法会处理HTTP请求、超时控制、重试机制等 - utils.healthCheck(serverIP, params.port) + healthCheck(serverIP, params.port) } } @@ -177,4 +165,169 @@ class HealthCheckService implements Serializable { } } + /** + * 执行应用健康检查 + * 通过HTTP端点检查目标服务的健康状态,支持超时控制和重试机制 + * 用于部署后验证应用是否正常启动和运行 + * + * @param serverIP 服务器IP地址 + * - 类型: String + * - 格式: IP地址或主机名(不需要http://前缀) + * - 示例: "192.168.1.100", "example.com", "localhost" + * - 必填: 是 + * + * @param port 服务端口号 + * - 类型: Integer + * - 范围: 1-65535 + * - 示例: 8080, 80, 443 + * - 必填: 是 + * + * @param timeoutMinutes 超时时间(分钟) + * - 类型: Integer + * - 默认值: 5分钟(优先使用参数值,其次环境配置,最后默认5分钟) + * - 建议: 生产环境建议设置较长超时(10-15分钟),开发环境可设置较短(3-5分钟) + * - 可选: 是 + * + * @throws Exception 当健康检查超时或连续失败时抛出异常 + * + * @note 使用Spring Boot Actuator的健康检查端点(/actuator/health) + * @note 支持自动重试机制,直到服务健康或超时 + * @note 包含详细的日志输出和错误诊断信息 + * + * @example 使用示例: + * // 基本使用 + * healthCheck("192.168.1.100", 8080) + * + * // 自定义超时 + * healthCheck("192.168.1.100", 8080, 10) + * + * // 生产环境检查 + * healthCheck("prod-server.example.com", 8080, 15) + */ + def healthCheck(serverIP, port, timeoutMinutes = null) { + // ========================================================================== + // 参数处理和配置初始化 + // ========================================================================== + + // 确定超时时间(优先级: 参数 > 环境配置 > 默认值) + def timeout = timeoutMinutes ?: envConfig?.healthCheckTimeout ?: 5 + // 健康检查端点路径(Spring Boot Actuator标准端点) + def healthEndpoint = "/actuator/health" + // 构建完整的健康检查URL + def healthCheckUrl = "http://${serverIP}:${port}${healthEndpoint}" + + script.echo "❤️ 开始健康检查..." + script.echo "📋 检查配置:" + script.echo " 📍 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 🔄 检查机制: 自动重试直到健康或超时" + + // ========================================================================== + // 参数验证 + // ========================================================================== + + // 验证服务器IP地址 + if (!serverIP) { + throw new IllegalArgumentException("服务器IP地址不能为空") + } + + // 验证端口范围 + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("端口号必须在1-65535范围内,当前: ${port}") + } + + // 验证超时时间 + if (timeout < 1) { + script.echo "⚠️ 超时时间过短(${timeout}分钟),使用最小值1分钟" + timeout = 1 + } + + // ========================================================================== + // 健康检查执行 + // ========================================================================== + + try { + // 设置整体超时(防止健康检查无限期等待) + script.timeout(time: timeout, unit: 'MINUTES') { + // 使用waitUntil实现自动重试机制 + script.waitUntil { + try { + script.echo "🔍 执行健康检查请求..." + + // 执行HTTP健康检查(使用curl命令) + // -s: 静默模式,不显示进度信息 + // -o /dev/null: 将响应体输出到空设备 + // -w '%{http_code}': 只输出HTTP状态码 + // --connect-timeout 5: 连接超时5秒 + // --max-time 10: 最大请求时间10秒 + def response = script.sh( + script: "curl -s -o /dev/null -w '%{http_code}' '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + + script.echo "📊 健康检查响应: HTTP ${response}" + + // 记录详细的响应信息(用于调试) + if (response != "200") { + // 非200响应时获取更多信息 + try { + def detailResponse = script.sh( + script: "curl -s '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + script.echo "📝 详细响应: ${detailResponse.take(200)}..." // 限制输出长度 + } catch (Exception detailError) { + script.echo "⚠️ 获取详细响应失败: ${detailError.message}" + } + } + + // 返回检查结果(200表示健康) + return response == "200" + + } catch (Exception e) { + // 记录检查失败信息 + script.echo "❌ 健康检查请求失败: ${e.message}" + script.echo "💡 可能的原因:" + script.echo " - 应用尚未完全启动" + script.echo " - 网络连接问题" + script.echo " - 防火墙或安全组限制" + script.echo " - 服务配置错误" + + return false // 继续重试 + } + } + } + + // 健康检查成功 + script.echo "✅ 健康检查成功!服务状态: HEALTHY" + script.echo "🎉 应用已正常启动并运行在 ${serverIP}:${port}" + script.echo "💡 建议: 继续执行后续部署验证步骤" + + } catch (Exception e) { + // ========================================================================== + // 健康检查失败处理 + // ========================================================================== + + script.echo "❌ 健康检查失败!服务状态: UNHEALTHY" + script.echo "📋 失败详情:" + script.echo " 🎯 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 💥 错误信息: ${e.message}" + + script.echo "🔧 故障排除建议:" + script.echo " 1. 📡 检查网络连通性: ping ${serverIP}" + script.echo " 2. 🚪 检查端口访问: telnet ${serverIP} ${port} || nc -zv ${serverIP} ${port}" + script.echo " 3. 📋 检查应用日志: docker logs 或 journalctl -u " + script.echo " 4. 🔍 手动验证: curl -v ${healthCheckUrl}" + script.echo " 5. ⚙️ 检查应用配置: 确认actuator端点已启用" + script.echo " 6. 🐛 查看启动错误: 检查应用启动时的异常信息" + script.echo " 7. 📊 检查资源使用: 内存、CPU、磁盘空间是否充足" + + // 重新抛出异常,让调用方能够处理健康检查失败 + throw new Exception("健康检查失败: 服务未在${timeout}分钟内达到健康状态", e) + } + } + } \ No newline at end of file diff --git a/src/com/jdk/stages/RollbackService.groovy b/src/com/jdk/stages/RollbackService.groovy index d9a3aaf..9a5a80d 100644 --- a/src/com/jdk/stages/RollbackService.groovy +++ b/src/com/jdk/stages/RollbackService.groovy @@ -28,13 +28,6 @@ class RollbackService implements Serializable { */ def script - /** - * 管道工具类实例 - * 提供通用的工具方法和工具函数,如回滚命令执行 - * 类型: PipelineUtils 自定义工具类 - */ - def utils - /** * 环境配置信息 * 存储回滚环境特定的配置参数 @@ -56,18 +49,13 @@ class RollbackService implements Serializable { * - 用途:用于在服务中访问Jenkins DSL方法 * - 示例:在Pipeline中通过`this`传递当前脚本上下文 * - * @param utils 管道工具类实例(必须) - * - 用途:提供回滚相关的工具方法 - * - 示例:执行回滚命令、版本管理等 - * * @param envConfig 环境配置信息(可选) * - 类型:Map 键值对配置映射 * - 默认值:空Map [:] * - 内容:回滚策略、超时设置、通知配置等 */ - RollbackService(script, utils, envConfig = [:]) { + RollbackService(script, envConfig = [:]) { this.script = script - this.utils = utils this.envConfig = envConfig // 初始化服务器解析器,用于处理服务器引用和索引 @@ -155,7 +143,7 @@ class RollbackService implements Serializable { ] // 该方法会处理SSH连接、回滚命令执行、版本切换等 - utils.rollbackDeployment(rollbackParams) + rollbackDeployment(rollbackParams) } } @@ -213,4 +201,125 @@ class RollbackService implements Serializable { // emailext subject: '部署回滚通知', body: '应用部署失败已自动回滚' } + /** + * 执行回滚部署操作 + * 负责将指定项目回退到上一个稳定版本,支持远程服务器操作和完整的错误处理 + * + * @param rollbackParams 回滚参数Map,包含以下字段: + * - timeout: Integer - 操作超时时间(秒) + * - projectName: String - 项目名称(必须) + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - retryAttempts: Integer - 重试次数 + * + * @throws Exception 当回滚操作失败时抛出异常,包含详细的错误信息和诊断建议 + * + * @note 该方法依赖于远程服务器上已部署的回滚脚本和正确的SSH配置 + * @note 支持重试机制,提高在网络不稳定情况下的成功率 + */ + def rollbackDeployment(rollbackParams) { + // ========================================================================== + // 参数提取和解构 + // ========================================================================== + + // 从服务器配置中提取IP地址(支持user@host格式) + def serverIP = rollbackParams.serverIP + // 从服务器配置中提取SSH端口号(默认22) + def serverPort = rollbackParams.serverPort + // 从服务器配置中提取SSH密钥凭证ID(Jenkins中配置的凭证名称) + def sshKey = rollbackParams.sshKey + // 从回滚参数中提取操作超时时间(秒) + def timeout = rollbackParams.timeout + // 从回滚参数中提取项目名称(用于标识要回滚的具体应用) + def projectName = rollbackParams.projectName + // 从回滚参数中提取部署脚本文件名(默认:deploy.sh) + def deployScript = rollbackParams.deployScript + // 从回滚参数中提取部署脚本所在路径 + def deployPath = rollbackParams.deployPath + // 从回滚参数中提取重试次数(默认:3次) + def retryAttempts = rollbackParams.retryAttempts ?: 3 + + // ========================================================================== + // 回滚信息输出和验证 + // ========================================================================== + + // 输出回滚操作开始信息,便于调试和审计追踪 + script.echo "🔄 执行回滚操作:" + script.echo " 📍 目标服务器: ${serverIP}:${serverPort}" + script.echo " 📦 项目名称: ${projectName}" + script.echo " 📁 脚本路径: ${deployPath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + script.echo " 🔄 重试次数: ${retryAttempts}次" + + // 验证必要参数是否存在 + if (!serverIP) { + throw new IllegalArgumentException("缺少服务器IP地址参数") + } + if (!projectName) { + throw new IllegalArgumentException("缺少项目名称参数") + } + + try { + // ========================================================================== + // 回滚命令构建 + // ========================================================================== + + // 构建回滚命令字符串 + // 格式: cd {部署脚本路径}; ./{部署脚本} rollback {项目名称} + def command = "cd ${deployPath}; ./${deployScript} rollback ${projectName}" + script.echo " 🖥️ 回滚命令: ${command}" + + // ========================================================================== + // 远程命令执行 + // ========================================================================== + + // 执行远程回滚命令,支持重试机制 + // executeRemoteCommandWithRetry 方法处理SSH连接、命令执行和重试逻辑 + def success = executeRemoteCommandWithRetry( + serverIP, // 目标服务器地址 + serverPort, // SSH端口号 + sshKey, // SSH密钥凭证 + command, // 要执行的命令 + timeout, // 超时时间 + retryAttempts // 重试次数 + ) + + // ========================================================================== + // 成功处理 + // ========================================================================== + + // 回滚成功日志输出 + script.echo "✅ 回滚成功完成: ${projectName} 已回退到上一个稳定版本" + script.echo "💡 建议: 检查应用日志确认回滚状态" + script.echo " 📋 验证步骤:" + script.echo " 1. 检查应用进程状态" + script.echo " 2. 查看应用启动日志" + script.echo " 3. 验证服务健康状态" + script.echo " 4. 确认业务功能正常" + + return success + + } catch (Exception e) { + // ========================================================================== + // 失败处理 + // ========================================================================== + + // 回滚失败错误信息输出 + script.echo "❌ 回滚失败: ${projectName} - ${e.message}" + script.echo "💡 故障排除建议:" + script.echo " 1. 🔍 检查服务器SSH连接是否正常" + script.echo " 2. 📁 确认部署脚本是否存在且可执行" + script.echo " 3. 🏷️ 验证项目名称是否正确" + script.echo " 4. 💾 检查服务器磁盘空间和权限" + script.echo " 5. 🔑 验证SSH密钥凭证是否有效" + script.echo " 6. 🌐 检查网络连接和防火墙设置" + script.echo " 7. ⏱️ 确认超时时间设置是否合理" + + // 重新抛出异常,让调用方能够处理回滚失败 + // 保留原始异常信息以便于调试和根因分析 + throw new Exception("回滚操作失败: ${e.message}", e) + } + } + } \ No newline at end of file diff --git a/src/com/nexus/stages/BuildService.groovy b/src/com/nexus/stages/BuildService.groovy index eb8eb89..abf37b7 100644 --- a/src/com/nexus/stages/BuildService.groovy +++ b/src/com/nexus/stages/BuildService.groovy @@ -166,7 +166,7 @@ class BuildService implements Serializable { def goals = [] // 添加依赖清理(针对小蚂蚁依赖) - goals.add("dependency:purge-local-repository -Dinclude=com.xiaomayi") + goals.add("dependency:purge-local-repository -Dinclude=com.yunheng") // 添加标准构建生命周期 goals.add("clean") diff --git a/src/com/node/ResolverService.groovy b/src/com/node/ResolverService.groovy index 5cb7cd3..f2a0a77 100644 --- a/src/com/node/ResolverService.groovy +++ b/src/com/node/ResolverService.groovy @@ -644,11 +644,11 @@ class ResolverService implements Serializable { // 添加环境分支镜像仓库配置 // ========================================================================== def registryConfig = config.common?.registrys?."${envConfig.registry}" - params.registry = registryConfig?.registry ?: "192.168.10.102:8001" - params.registryCredential = registryConfig?.registryCredential ?: "xiaomayi-hub-credentials" - params.registryProject = registryConfig?.registryProject ?: "xiaomayi" - params.registryImage = registryConfig?.registryImage ?: "jdk:17.0.16-centos8-dos" - params.registryType = registryConfig.registryType ?: "harbor" + params.registry = registryConfig?.registry + params.registryCredential = registryConfig?.registryCredential + params.registryProject = registryConfig?.registryProject + params.registryImage = registryConfig?.registryImage + params.registryType = registryConfig.registryType params.imageFullName = "${params.registry}/${params.registryProject}/${params.projectName}" script.echo "✅ ${envKey} 环境全部参数获取完成" diff --git a/src/com/python/DeploymentService.groovy b/src/com/python/DeploymentService.groovy new file mode 100644 index 0000000..e8856db --- /dev/null +++ b/src/com/python/DeploymentService.groovy @@ -0,0 +1,244 @@ +package com.python + +import com.python.stages.BuildService +import com.python.stages.ContainerBuildService +import com.python.stages.DeployService +import com.python.stages.HealthCheckService +import com.python.stages.RollbackService + +/** + * 部署服务类 - CI/CD流水线核心协调器 + * 负责执行各个CI/CD阶段的业务逻辑,不直接创建Jenkins stage + * 提供模块化的构建、部署、验证功能,作为流水线步骤的抽象层 + * + * 设计原则: + * - 单一职责:每个方法只负责一个特定的功能 + * - 错误隔离:每个阶段的异常不会影响其他阶段 + * - 可重用性:方法可以在不同的流水线中重用 + * - 可扩展性:易于添加新的部署阶段或功能 + * - 可维护性:清晰的代码结构和职责分离 + * + * 主要职责: + * 1. 协调各个部署阶段服务的执行 + * 2. 提供统一的参数传递接口 + * 3. 处理阶段间的依赖关系 + * 4. 提供错误处理和回滚机制 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeploymentService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储环境特定的配置参数,如服务器列表、环境变量等 + * 类型: Map 键值对配置映射 + */ + def envConfig + + // ========================================================================== + // 各个阶段的服务实例(依赖注入) + // ========================================================================== + + /** + * 构建阶段服务实例 + * 负责Maven项目的编译和打包操作 + * 类型: BuildService 专门处理构建逻辑的服务类 + */ + def buildService + + /** + * 容器构建阶段服务实例 + * 负责Docker镜像的构建和推送操作 + * 类型: ContainerBuildService 专门处理容器构建逻辑的服务类 + */ + def containerBuildService + + /** + * 部署阶段服务实例 + * 负责将应用部署到目标服务器 + * 类型: DeployService 专门处理部署逻辑的服务类 + */ + def deployService + + /** + * 健康检查阶段服务实例 + * 负责验证部署的应用是否正常运行 + * 类型: HealthCheckService 专门处理健康检查逻辑的服务类 + */ + def healthCheckService + + /** + * 回滚阶段服务实例 + * 负责在失败时执行回滚操作 + * 类型: RollbackService 专门处理回滚逻辑的服务类 + */ + def rollbackService + + /** + * 构造函数 - 初始化部署服务 + * 创建各个阶段的服务实例,建立服务依赖关系 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在各个服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:服务器列表、环境变量、部署策略等配置 + * - 示例:[ + * servers: [[serverIP: "192.168.1.1", serverPort: 22]], + * envVars: [JAVA_OPTS: "-Xms1g -Xmx1g"], + * deploymentStrategy: "rolling" + * ] + */ + DeploymentService(script, envConfig = [:]) { + // 初始化基础属性 + this.script = script + this.envConfig = envConfig + + // 初始化各个阶段服务(依赖注入模式) + // 通过分离关注点,提高代码的可测试性和可维护性 + this.buildService = new BuildService(script, envConfig) + this.containerBuildService = new ContainerBuildService(script, envConfig) + this.deployService = new DeployService(script, envConfig) + this.healthCheckService = new HealthCheckService(script, envConfig) + this.rollbackService = new RollbackService(script, envConfig) + + script.echo "🔧 DeploymentService 初始化完成" + } + + /** + * 执行构建阶段逻辑 + * 委托给BuildService处理Maven项目的编译和打包 + * 这是CI/CD流水线的第一个阶段,负责源代码到产物的转换 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - MAVEN_SET: String - Maven设置文件ID + * - PROFILES: String - Maven构建环境profiles + * - jarFilePath: String - JAR文件输出路径 + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认true) + * - goals: String - Maven执行目标(默认"clean package") + * - pomFilePath: String - POM文件路径(默认pom.xml) + * + * @throws Exception 当Maven构建失败时抛出异常,中断流水线 + * + * @example 使用示例: + * def params = [ + * MAVEN_SET: "nexus-maven-dev", + * PROFILES: "dev", + * jarFilePath: "target", + * jarFile: "app.jar" + * ] + * deploymentService.executeBuildStage(params) + */ + def executeBuildStage(params) { + // ☕ 开始构建阶段... + buildService.executeBuildStage(params) + } + + /** + * 执行容器构建阶段逻辑 + * 委托给ContainerBuildService处理Docker镜像的构建和推送 + * 这是CI/CD流水线的第二个阶段,负责应用容器化 + * + * @param params 管道参数Map,包含镜像构建所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址 + * - registryCredential: String - Jenkins仓库认证凭证ID + * - registryProject: String - 仓库项目名称 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * 可选字段: + * - buildOptions: String - Docker构建选项 + * - baseImage: String - 基础镜像名称 + * + * @return String 构建的镜像标签(如:dev-123、prod-456) + * @throws Exception 当Docker构建或推送失败时抛出异常 + * + * @example 使用示例: + * def imageTag = deploymentService.executeContainerBuildStage(params) + * echo "镜像标签: ${imageTag}" + */ + def executeContainerBuildStage(params) { + // 🐳 开始容器构建阶段... + return containerBuildService.executeContainerBuildStage(params) + } + + /** + * 执行部署阶段逻辑 + * 委托给DeployService处理应用部署到目标服务器 + * 这是CI/CD流水线的第三个阶段,负责将容器部署到环境 + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servers: List - 服务器列表数组 + * - deployServer: String - 部署服务器引用 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * 可选字段: + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * + * @throws Exception 当部署失败时抛出异常,支持自动回滚 + * + * @note 支持多种部署模式: + * - 单服务器部署:deployServer = "servers[0]" + * - 多服务器部署:deployServer = "servers[0,1,2]" + * - 自动选择:deployServer = "auto" + */ + def executeDeployStage(params) { + // 🚀 开始部署阶段... + deployService.executeDeployStage(params) + } + + /** + * 执行健康检查阶段逻辑 + * 委托给HealthCheckService验证部署的应用是否正常运行 + * 这是CI/CD流水线的第四个阶段,负责部署后验证 + * + * @param params 管道参数Map,包含健康检查所需的所有配置信息,通常包含: + * - serverHosts: List - 服务器列表(可选) + * - serverIP01: String - 单个服务器IP(可选) + * - port: Integer - 服务端口号 + * - healthCheckPath: String - 健康检查端点路径 + * - healthCheckTimeout: Integer - 健康检查超时时间 + * + * @throws Exception 当健康检查失败时抛出异常,触发回滚机制 + * + * @note 健康检查失败时会自动触发回滚操作 + */ + def executeHealthCheckStage(params) { + // ❤️ 开始健康检查阶段... + healthCheckService.executeHealthCheckStage(params) + } + + /** + * 执行回滚操作 + * 委托给RollbackService处理部署失败时的回滚操作 + * 这是CI/CD流水线的安全机制,确保失败时能够恢复到稳定状态 + * + * @param params 管道参数Map,包含回滚所需的所有配置信息,通常包含: + * - serverHosts: List - 需要回滚的服务器列表 + * - serverIP01: String - 单个服务器IP + * - serverPort: Integer - 服务器端口 + * - projectName: String - 项目名称 + * - previousVersion: String - 上一个版本号(可选) + * + * @note 这是一个私有方法,通常在部署或健康检查失败时自动调用 + * @note 回滚操作本身也可能会失败,需要手动介入处理 + */ + def executeRollbackStage(params) { + // ↩️ 开始回滚操作... + rollbackService.executeRollbackStage(params) + } + +} \ No newline at end of file diff --git a/src/com/python/ResolverService.groovy b/src/com/python/ResolverService.groovy new file mode 100644 index 0000000..7aed575 --- /dev/null +++ b/src/com/python/ResolverService.groovy @@ -0,0 +1,856 @@ +package com.python + +import com.utils.CommonUtils +import com.constants.ConfigConstants +import com.utils.ServerParser + +/** + * JDK API 服务类 + * 负责加载配置、管理分支和服务配置、设置环境参数 + * 适配新的配置文件格式,支持多环境、多服务配置 + * + * 主要功能: + * 1. 加载环境配置和主配置文件 + * 2. 解析分支和服务配置 + * 3. 设置Jenkins流水线参数 + * 4. 提供环境相关的配置获取方法 + * + * @version 2.0.0 + * @since 2025-09-09 + */ +class ResolverService implements Serializable { + + // Jenkins流水线脚本对象,用于执行Jenkins相关操作 + def script + + // 主配置文件内容(解析后的YAML数据) + def config + + // 组织名称 + def orgName + + // 环境配置文件内容 + def envConfig + + // 主配置文件路径 + def jobConfigFile + + /** + * 服务器解析器实例 + * 负责解析服务器引用配置和索引处理 + * 类型: ServerParser 自定义服务器解析工具类 + */ + def serverParser + + /** + * 构造函数 + * 初始化服务类实例 + * + * @param script Jenkins流水线脚本对象,提供echo、error等方法 + * @param orgName 组织名称,用于标识项目归属 + */ + ResolverService(script, orgName) { + this.script = script + this.orgName = orgName + this.config = [:] + this.envConfig = [:] + // 初始化服务器解析器,用于处理服务器引用和索引 + this.serverParser = new ServerParser(script) + } + + /** + * 加载所有配置文件 + * 按顺序加载环境配置和主配置文件 + * + * @throws Exception 如果配置文件加载失败 + */ + void loadConfigurations() { + script.echo "🚀 开始加载配置文件..." + + // 首先加载环境配置文件(.env.yml) + loadEnvConfig() + + // 然后加载主配置文件(根据环境配置中的路径) + loadJobConfig() + + script.echo "✅ 所有配置文件加载完成" + } + + /** + * 加载环境配置文件的方法 + * 从.jenkins/.env.yml读取环境配置,包含主配置文件路径等基本信息 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件存在但解析失败 + */ + def loadEnvConfig() { + try { + // 环境配置文件相对路径 + def envFilePath = '.jenkins/.env.yml' + script.echo "📁 环境配置文件路径: ${envFilePath}" + + if (script.fileExists(envFilePath)) { + // 读取并解析YAML格式的环境配置文件 + envConfig = script.readYaml(file: envFilePath) + + // 从环境配置中获取主配置文件路径 + jobConfigFile = ".pipeline-files/config-file/" + envConfig.jobConfigFile + script.echo "✅ 成功加载环境配置文件" + + // 打印所有环境配置项(调试用) + if (!envConfig.isEmpty()) { + script.echo "📋 环境配置内容:" + envConfig.each { key, value -> + script.echo " ${key}: ${value}" + } + } + } else { + script.echo "⚠️ 警告: 未找到环境配置文件 .env.yml,使用默认配置" + } + } catch (Exception e) { + script.echo "⚠️ 警告: .env.yml 文件无效,使用默认配置: ${e.message}" + // 不抛出异常,允许继续执行 + } + + return this + } + + /** + * 加载主配置文件的方法 + * 读取新的配置文件格式,包含common、branchConfig、serviceConfig等部分 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件不存在或解析失败 + */ + def loadJobConfig() { + try { + // 检查主配置文件是否存在 + if (!script.fileExists(jobConfigFile)) { + script.error("❌ 错误: 配置文件不存在: ${jobConfigFile},请检查文件路径或创建配置文件") + } + + // 读取并解析YAML格式的主配置文件 + this.config = script.readYaml(file: jobConfigFile) + script.echo "✅ 成功加载主配置文件: ${jobConfigFile}" + + // 打印配置详情(调试用) + printConfigSummary() + + } catch (Exception e) { + script.error("❌ 错误: 加载配置文件失败: ${jobConfigFile}. 错误详情: ${e.message}") + } + + return this + } + + /** + * 打印主配置文件的详细内容 + * 格式化输出所有配置信息,便于调试和验证 + */ + def printConfigSummary() { + script.echo "=" * 70 + script.echo "📊 主配置文件详细内容" + script.echo "=" * 70 + + if (!this.config) { + script.echo "⚠️ 配置文件内容为空" + return + } + + // 打印通用配置信息 + if (this.config.common) { + script.echo "🌐 通用配置:" + script.echo "-" * 50 + script.echo " 部署脚本: ${this.config.common.deployScript ?: '未设置'}" + script.echo " 部署路径: ${this.config.common.deployPath ?: '未设置'}" + + // 打印注册表配置 + if (this.config.common.registry) { + script.echo " 注册表配置:" + this.config.common.registry.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + script.echo "" + } + + // 打印环境配置信息 + if (this.config.common?.profiles) { + script.echo "🏭 环境配置 (共 ${this.config.common.profiles.size()} 个环境):" + script.echo "-" * 50 + + this.config.common.profiles.each { envName, envConfig -> + script.echo " ${envName.toUpperCase()} 环境:" + script.echo " 服务器数量: ${envConfig.servers ? envConfig.servers.size() : 0}" + script.echo " 部署策略: ${envConfig.deploymentStrategy ?: 'rolling'}" + script.echo " JVM参数: ${envConfig.envVars?.JAVA_OPTS ?: '未设置'}" + script.echo " 健康检查: ${envConfig.healthCheckPath ?: '/actuator/health'}" + script.echo "" + } + } + + // 打印分支构建配置信息 + if (this.config.branchConfig) { + script.echo "🌿 分支构建配置 (共 ${this.config.branchConfig.size()} 个分支):" + script.echo "-" * 50 + + this.config.branchConfig.each { branchName, branchConfig -> + script.echo " ${branchName} 分支:" + script.echo " Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + } + } + + // 打印服务配置信息 + if (this.config.serviceConfig) { + script.echo "🔧 服务配置 (共 ${this.config.serviceConfig.size()} 个服务):" + script.echo "-" * 50 + + this.config.serviceConfig.each { serviceName, serviceConfig -> + script.echo " ${serviceName} 服务:" + script.echo " 编程语言: ${serviceConfig.programLang ?: '未设置'}" + script.echo " 语言版本: ${serviceConfig.programLangVersion ?: '未设置'}" + script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " 服务端口: ${serviceConfig.port ?: '未设置'}" + } + } + + script.echo "=" * 70 + } + + /** + * 获取当前分支的配置 + * 根据当前Jenkins作业的分支名称查找对应的分支配置 + * + * @return 分支配置对象(Map类型) + * @throws Exception 如果分支配置不存在 + */ + def getBranchConfig() { + // 获取当前Jenkins作业的分支名称 + def branchName = script.env.BRANCH_NAME + + if (!branchName) { + script.error("❌ 错误: 无法获取分支名称,env.BRANCH_NAME 为空。请检查流水线配置") + } + + script.echo "🌿 正在查找分支配置: ${branchName}" + + // 检查分支配置是否存在 + if (!config.branchConfig || config.branchConfig.isEmpty()) { + script.error("❌ 错误: 分支配置列表为空或未定义。请检查配置文件结构") + } + + // 根据分支名称获取配置 + def branchConfig = config.branchConfig[branchName] + + if (branchConfig) { + script.echo "✅ 成功匹配分支配置:" + script.echo " - Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " - 构建参数: ${branchConfig.buildArgs ?: '默认参数'}" + } else { + // 构建友好的错误信息,列出所有可用分支 + def availableBranches = config.branchConfig.keySet().join(', ') + script.error("❌ 错误: 找不到分支 '${branchName}' 的配置。可用分支: ${availableBranches}") + } + + return branchConfig + } + + /** + * 获取指定服务的配置信息 + * 根据服务名称从serviceConfig中查找对应的服务配置 + * + * @param serviceName 服务名称,需要与配置文件中serviceConfig的键匹配 + * @return 服务配置对象(Map类型) + * @throws Exception 如果服务配置不存在 + */ + def getServiceConfig(serviceName) { + if (!serviceName) { + script.error("❌ 错误: 服务名称参数不能为空") + } + + script.echo "🔧 正在查找服务配置: ${serviceName}" + + // 检查服务配置是否存在 + if (!config.serviceConfig || config.serviceConfig.isEmpty()) { + script.error("❌ 错误: 服务配置列表为空或未定义,请检查配置文件结构") + } + + // 根据服务名称获取配置 + def serviceConfig = config.serviceConfig[serviceName] + + if (serviceConfig) { + script.echo "✅ 成功找到服务配置: ${serviceName}" + script.echo " - 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " - 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " - 服务端口: ${serviceConfig.port ?: '未设置'}" + } else { + // 构建友好的错误信息,列出所有可用服务 + def availableServices = config.serviceConfig.keySet().join(', ') + script.error("❌ 错误: 找不到服务 '${serviceName}' 的配置。可用服务: ${availableServices}") + } + + return serviceConfig + } + + /** + * 设置管道参数 + * 整合分支配置、服务配置和环境配置,生成完整的流水线参数 + * + * @return 参数映射表,包含所有必要的部署参数 + */ + def setupParameters() { + script.echo "⚙️ 开始设置管道参数..." + + // 获取当前分支配置 + def branchConfig = getBranchConfig() + + // 从Jenkins作业名称中提取服务名称 + // 格式通常为: "组织名/服务名" 或 "服务名" + def serviceName = script.env.JOB_NAME?.tokenize('/')?.get(0) + script.echo "📦 服务名称: ${serviceName}" + + // 获取服务配置 + def serviceConfig = getServiceConfig(serviceName) + + // 根据分支类型获取环境配置 + def profiles = getBranchProfiles(branchConfig) + script.echo "🌍 环境配置: ${profiles}" + + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = CommonUtils.generateImageTag(script.env.PROFILES, script.env.BUILD_ID) + script.echo "自定义镜像标签:${imageTag}" + + // 构建完整的参数映射表 + def params = [ + // 构建相关参数 + jenkinsAgent : branchConfig.jenkinsAgent, + dockerFile : branchConfig.docker?.dockerFile ?: '.jenkins/Dockerfile', + buildOptions : branchConfig.docker?.buildOptions, + + // 服务相关参数 + goVersion : "${serviceConfig.programLang}:${serviceConfig.programLangVersion}", + projectName : serviceConfig.containerName, + packageName : serviceConfig?.packageName, + servicePath : serviceConfig.servicePath, + pyFile : serviceConfig.pyFile, + pyFilePath : "${serviceConfig.servicePath}", + port : serviceConfig.port, + deployServer : serviceConfig.deployServer, + composeFile : serviceConfig.composeFile, + + // 环境相关参数 + PROFILES : profiles.default_profile, + availableProfiles : profiles.available_profiles, + imageTags : [imageTag], + + + // Git配置 + gitUrl : config.common?.git?.url, + gitCredentialsId : config.common?.git?.credentialsId, + gitBranch : script.env.BRANCH_NAME, + + // 钉钉配置 + dingTalkCredentialsId: config.common?.dingtalk?.credentialsId, + + // 其他参数 + envConfig : this.envConfig, + orgName : this.orgName, + branchConfig : branchConfig, + serviceConfig : serviceConfig + ] + + // 获取分支环境变量配置 + getEnvironmentConfig(params) + + // 设置容器基础镜像 + params.baseImage = "${params.registry}/${params.registryProject}-base/${params.registryImage}" + + script.echo "基础参数:${params}" + + // 设置Jenkins参数(仅对标准流水线) + setupJenkinsParameters(profiles.available_profiles as List) + + script.echo "✅ 管道参数设置完成" + + // 返回参数 + return params + } + + /** + * 获取指定环境的全部配置参数 + * 从通用配置中提取指定环境的所有参数,包括服务器列表、环境变量、部署策略等 + * + * @param params 参数映射表(会被修改),用于存储提取的配置信息 + * @return 包含指定环境全部参数的Map + */ + def getEnvironmentConfig(params) { + try { + // 从参数中获取环境名称,如果没有设置则使用默认值"dev" + // 使用Elvis操作符 ?: 提供默认值,避免空指针异常 + def envName = params.PROFILES ?: "dev" + + // 将环境名称转换为小写,确保与配置中的key匹配(配置通常使用小写) + // 例如:将"DEV"转换为"dev","Prod"转换为"prod" + def envKey = envName.toLowerCase() + + script.echo "🔍 开始获取 ${envKey} 环境的全部配置参数..." + + // ========================================================================== + // 添加通知配置 + // ========================================================================== + if (config.common?.notification) { + params.notification = config.common.notification + script.echo "📧 通知配置已加载" + } + + // ========================================================================== + // 添加短信配置 - 详细配置信息 + // ========================================================================== + if (config.common?.sms) { + // 确保sms配置存在 + params.sms = config.common.sms + + script.echo "📱 短信服务配置详情:" + script.echo " - 服务商: ${params.sms.provider ?: '未设置'}" + script.echo " - 凭据ID: ${params.sms.credentialsId ?: '未设置'}" + script.echo " - 默认签名: ${params.sms.signName ?: '未设置'}" + + // 打印所有模板配置 + if (params.sms.templates) { + script.echo " - 可用模板:" + params.sms.templates.each { templateName, templateConfig -> + def templateCode = templateConfig.templateCode ?: '未设置' + def templateSign = templateConfig.signName ?: params.sms.signName ?: '未设置' + script.echo " 📋 ${templateName}: ${templateCode} (签名: ${templateSign})" + } + } else { + script.echo " - 模板: 未配置" + } + } else { + script.echo "📱 短信服务: 未配置" + } + + // ========================================================================== + // 添加构建策略配置 + // ========================================================================== + if (config.common?.buildStrategy) { + params.buildStrategy = config.common.buildStrategy + script.echo "⚡ 构建策略:" + script.echo " - 重试次数: ${params.buildStrategy.retryCount}" + script.echo " - 超时时间: ${params.buildStrategy.timeoutMinutes}分钟" + script.echo " - 启用缓存: ${params.buildStrategy.enableCaching}" + } + + // 安全地获取环境配置 - 使用Map方式访问 + // 使用安全导航操作符 ?. 避免空指针异常: + // - config.common?:如果config为null则返回null,不会抛出异常 + // - .profiles?:如果common为null则返回null + // - ."${envKey}":动态访问指定环境的配置 + def envConfig = config.common?.profiles?."${envKey}" + if (!envConfig) { + def availableProfiles = config?.common?.profiles?.keySet() ?: [] + throw new Exception("未找到 ${envKey} 环境的配置,可用环境: ${availableProfiles.join(', ')}") + } + + script.echo "✅ 找到 ${envKey} 环境配置,开始提取参数..." + + // ========================================================================== + // 提取服务器配置 + // ========================================================================== + + // 检查环境配置是否存在且包含服务器列表,并且服务器列表不为空 + // envConfig?.servers:安全访问servers属性 + // !envConfig.servers.empty:检查servers列表是否非空 + if (envConfig?.servers && !envConfig.servers.empty) { + // 使用环境配置中的服务器列表更新参数对象 + // 将找到的服务器列表赋值给params.servers,供后续部署使用 + params.servers = envConfig.servers + + // 输出日志信息,显示成功找到服务器配置 + // 使用表情符号增强可读性,显示服务器数量 + script.echo "🖥️ 服务器配置: 共 ${envConfig.servers.size()} 台服务器" + + // 打印服务器详情(用于调试和验证) + // 使用eachWithIndex迭代服务器列表,同时获取索引和服务器对象 + envConfig.servers.eachWithIndex { server, index -> + // 输出每台服务器的详细信息 + // ${index + 1}:显示序号(从1开始) + // ${server.serverName}:服务器名称标识 + // ${server.serverIP}:${server.serverPort}:服务器的IP地址和端口号 + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + // 如果没有找到对应环境的服务器配置或服务器列表为空 + // 输出警告信息,提示用户检查配置 + script.echo "⚠️ 警告: 未找到 ${envName} 环境的服务器配置" + params.servers = [] + + // 注意:这里没有设置params.servers,调用方需要处理这种情况 + // 可能的处理方式:使用默认服务器、终止部署流程或提示用户输入 + } + + // ========================================================================== + // 提取目标服务器配置 + // ========================================================================== + + // 解析部署服务器引用,获取目标服务器列表 + // 使用ServerParser处理复杂的服务器引用表达式 + def targetServers = serverParser.parseTargetServers(params) + params.targetServers = targetServers + script.echo "🎯 部署目标服务器数量: ${targetServers.size()}" + + // ========================================================================== + // 提取环境变量配置 + // ========================================================================== + if (envConfig?.envVars && !envConfig.envVars.empty) { + params.envVars = envConfig.envVars + script.echo "🌡️ 环境变量: 共 ${envConfig.envVars.size()} 个" + envConfig.envVars.each { key, value -> + script.echo " ${key}=${value}" + } + } else { + script.echo "⚠️ 警告: ${envKey} 环境没有配置环境变量" + params.envVars = [:] + } + + // ========================================================================== + // 提取部署策略配置 + // ========================================================================== + if (envConfig?.deploymentStrategy) { + params.deploymentStrategy = envConfig.deploymentStrategy + script.echo "🎯 部署策略: ${envConfig.deploymentStrategy}" + } + + if (envConfig?.deploymentTimeout) { + params.deploymentTimeout = envConfig.deploymentTimeout + script.echo "⏱️ 部署超时: ${envConfig.deploymentTimeout} 秒" + } + + if (envConfig?.maxParallel) { + params.maxParallel = envConfig.maxParallel + script.echo "🔀 最大并行数: ${envConfig.maxParallel}" + } + + if (envConfig?.retryAttempts) { + params.retryAttempts = envConfig.retryAttempts + script.echo "🔄 重试次数: ${envConfig.retryAttempts}" + } + + if (envConfig?.canaryPercentage) { + params.canaryPercentage = envConfig.canaryPercentage + script.echo "📊 金丝雀比例: ${envConfig.canaryPercentage}%" + } + + // ========================================================================== + // 提取健康检查配置 + // ========================================================================== + if (envConfig?.healthCheckEnabled) { + params.healthCheckEnabled = envConfig.healthCheckEnabled + script.echo "❤️ 启用健康检查: ${envConfig.healthCheckEnabled}" + } + + if (envConfig?.healthCheckPath) { + params.healthCheckPath = envConfig.healthCheckPath + script.echo "❤️ 健康检查路径: ${envConfig.healthCheckPath}" + } + + if (envConfig?.healthCheckTimeout) { + params.healthCheckTimeout = envConfig.healthCheckTimeout + script.echo "⏱️ 健康检查超时: ${envConfig.healthCheckTimeout} 秒" + } + + if (envConfig?.healthCheckRetries) { + params.healthCheckRetries = envConfig.healthCheckRetries + script.echo "❤️ 健康检查重试次数: ${envConfig.healthCheckRetries}" + } + + if (envConfig?.healthCheckInterval) { + params.healthCheckInterval = envConfig.healthCheckInterval + script.echo "🔄 健康检查间隔: ${envConfig.healthCheckInterval} 秒" + } + + if (envConfig?.successThreshold) { + params.successThreshold = envConfig.successThreshold + script.echo "✅ 成功阈值: ${envConfig.successThreshold} 次" + } + + // ========================================================================== + // 提取回滚配置(生产环境特有) + // ========================================================================== + if (envConfig?.rollbackEnabled != null) { + params.rollbackEnabled = envConfig.rollbackEnabled + script.echo "↩️ 自动回滚: ${envConfig.rollbackEnabled}" + } + + if (envConfig?.rollbackOnFailure != null) { + params.rollbackOnFailure = envConfig.rollbackOnFailure + script.echo "❌ 失败时回滚: ${envConfig.rollbackOnFailure}" + } + + if (envConfig?.rollbackTimeout) { + params.rollbackTimeout = envConfig.rollbackTimeout + script.echo "⏱️ 回滚超时: ${envConfig.rollbackTimeout} 秒" + } + + // // ========================================================================== + // // 提取通知配置(生产环境特有) + // // ========================================================================== + // if (envConfig?.notifications) { + // params.notifications = envConfig.notifications + // script.echo "📧 通知配置:" + // if (envConfig.notifications.slackChannel) { + // script.echo " Slack频道: ${envConfig.notifications.slackChannel}" + // } + // if (envConfig.notifications.email) { + // script.echo " 邮件地址: ${envConfig.notifications.email}" + // } + // if (envConfig.notifications.recipients) { + // script.echo " 额外收件人: ${envConfig.notifications.recipients.join(', ')}" + // } + // script.echo " 通知时机: 开始=${envConfig.notifications.onStart}, 成功=${envConfig.notifications.onSuccess}, 失败=${envConfig.notifications.onFailure}" + // } + + // ========================================================================== + // 添加通用配置 + // ========================================================================== + params.deployScript = config.common?.deployScript + params.deployPath = config.common?.deployPath + + script.echo "📜 部署脚本: ${params.deployScript}" + script.echo "📁 部署路径: ${params.deployPath}" + + // ========================================================================== + // 添加镜像仓库配置 + // ========================================================================== + if (config.common?.registrys) { + params.registrys = config.common?.registrys + script.echo "🐳 镜像仓库配置:" + params.registrys.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + + // ========================================================================== + // 添加环境分支镜像仓库配置 + // ========================================================================== + def registryConfig = config.common?.registrys?."${envConfig.registry}" + params.registry = registryConfig?.registry + params.registryCredential = registryConfig?.registryCredential + params.registryProject = registryConfig?.registryProject + params.registryImage = registryConfig?.registryImage + params.registryType = registryConfig.registryType + params.imageFullName = "${params.registry}/${params.registryProject}/${params.projectName}" + + script.echo "✅ ${envKey} 环境全部参数获取完成" + + // 返回配置 + return params + } catch (Exception e) { + script.echo "❌ 获取环境配置失败: ${e.message}" + throw e + } + } + + /** + * 设置 Jenkins 作业参数 + * 为标准流水线作业创建环境选择参数和服务器选择参数 + * 根据可用环境配置动态生成下拉选择框,支持环境和服务器的两级选择 + * + * @param availableProfiles 可用的环境配置列表 + * - 类型: List 环境名称列表(如:['dev', 'test', 'prod']) + * - 来源: 通常从配置文件中解析得到的环境配置键值 + * - 要求: 列表不能为空,且必须与配置文件中的环境名称匹配 + * + * @note 在多分支流水线中会自动跳过参数设置(因为多分支通常根据分支自动确定环境) + * @note 在标准流水线中会创建用户可交互的选择参数 + * + * @example 生成的参数: + * - PROFILES: 环境选择下拉框(dev/test/prod) + * - SERVERS: 服务器选择下拉框(根据选择的环境动态显示对应服务器) + */ + private void setupJenkinsParameters(List availableProfiles) { + // 检查可用环境列表是否为空 + if (!availableProfiles || availableProfiles.isEmpty()) { + script.echo "⚠️ 可用环境配置列表为空,跳过参数设置" + return + } + + // 获取当前分支名称,用于判断流水线类型 + def branchName = script.env.BRANCH_NAME ?: '' + + // 判断是否为多分支流水线(分支名通常包含斜杠,如: feature/xxx) + boolean isMultibranchPipeline = branchName.contains('/') + + if (isMultibranchPipeline) { + script.echo "🔧 检测到多分支流水线,跳过参数设置(分支: ${branchName})" + script.echo "💡 多分支流水线通常根据分支名称自动确定目标环境" + return + } + + script.echo "🔧 标准流水线作业,设置部署环境参数" + script.echo "📋 可用环境选项: ${availableProfiles.join(', ')}" + + try { + // ========================================================================== + // 创建环境选择参数 + // ========================================================================== + def profileParameter = script.choice( + choices: availableProfiles.join('\n'), // 下拉选项,每行一个选择项 + description: '请选择要部署的目标环境', // 参数描述,显示在Jenkins界面上 + name: 'PROFILES' // 参数名称,后续通过params.PROFILES访问 + ) + + // ========================================================================== + // 创建服务器选择参数(动态生成所有环境的服务器选项) + // ========================================================================== + def allServers = getAllServersForParameters(availableProfiles) + def serverChoices = generateServerChoices(allServers) + + def serverParameter = script.choice( + choices: serverChoices.join('\n'), // 服务器下拉选项 + description: '请选择要部署的目标服务器(格式:环境-服务器名-IP)', // 参数描述 + name: 'SERVERS' // 参数名称 + ) + + // ========================================================================== + // 设置Jenkins作业参数 + // ========================================================================== + script.properties([ + script.parameters([ + profileParameter, // 环境选择参数 + serverParameter // 服务器选择参数 + ]) + ]) + + script.echo "✅ 成功设置部署参数:" + script.echo " - PROFILES: 环境选择(${availableProfiles.size()} 个选项)" + script.echo " - SERVERS: 服务器选择(${serverChoices.size()} 个选项)" + + } catch (Exception e) { + script.echo "⚠️ 设置 Jenkins 参数失败: ${e.message}" + script.echo "💡 这可能是因为没有权限修改作业配置,或者参数格式不正确" + // 不抛出异常,避免影响后续流程,参数设置失败通常不会阻断部署 + } + } + + /** + * 获取所有环境的服务器信息用于参数生成 + * 从配置文件中提取每个环境的服务器列表,用于构建服务器选择参数 + * 自动过滤空字符串和无效的环境名称 + * + * @param availableProfiles 可用环境列表,可能包含空字符串或无效值 + * @return Map 各环境的服务器列表映射(只包含有效环境) + */ + private Map getAllServersForParameters(List availableProfiles) { + def allServers = [:] + + // 过滤掉空字符串和null值,只处理有效的环境名称 + def validProfiles = availableProfiles.findAll { profile -> + profile != null && profile.trim() != '' + } + + script.echo "🔍 有效环境列表: ${validProfiles.join(', ')}" + script.echo "🗑️ 已过滤空环境: ${availableProfiles.size() - validProfiles.size()} 个" + + validProfiles.each { profile -> + try { + // 安全地获取环境配置中的服务器列表 + // 使用安全导航操作符避免空指针异常 + def envConfig = config?.common?.profiles?."${profile}" + + if (envConfig?.servers) { + allServers[profile] = envConfig.servers + script.echo " 📡 ${profile} 环境: ${envConfig.servers.size()} 台服务器" + + // 可选:输出服务器详情用于调试 + envConfig.servers.eachWithIndex { server, index -> + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + script.echo " ⚠️ ${profile} 环境: 无服务器配置或服务器列表为空" + allServers[profile] = [] + } + } catch (Exception e) { + script.echo " ❌ 获取 ${profile} 环境服务器失败: ${e.message}" + script.echo " 🐛 错误详情: ${e.getStackTrace().find { it.contains('getAllServersForParameters') } ?: '未知位置'}" + allServers[profile] = [] + } + } + + // 检查是否有任何有效环境配置了服务器 + def environmentsWithServers = allServers.count { it.value && !it.value.empty } + script.echo "📊 统计: ${environmentsWithServers} 个环境配置了服务器,${allServers.size() - environmentsWithServers} 个环境无服务器" + + return allServers + } + + /** + * 生成服务器选择选项 + * 创建格式化的服务器选择项,便于用户识别和选择 + * + * @param allServers 所有环境的服务器映射 + * @return List 格式化后的服务器选项列表 + */ + private List generateServerChoices(Map allServers) { + def choices = [] + + // 添加默认选项 + choices.add("auto-自动选择(根据环境默认服务器)") + + // 为每个环境的每台服务器生成选项 + allServers.each { profile, servers -> + if (servers && !servers.empty) { + servers.each { server -> + // 格式化选项:环境-服务器名-IP:端口 + def displayName = "${profile}-${server.serverName}-${server.serverIP}:${server.serverPort}" + choices.add(displayName) + } + } else { + // 如果某个环境没有服务器,添加提示选项 + choices.add("${profile}-无可用服务器") + } + } + + return choices + } + + /** + * 根据分支配置获取对应的环境配置文件和默认配置文件 + * @param branchConfig 分支配置信息,包含分支类型等 + * @return Map 包含可用配置列表和默认配置 + */ + private def getBranchProfiles(branchConfig) { + // 获取当前分支名称 + def branchName = script.env.BRANCH_NAME ?: '' + + // 1. 优先从常量配置中获取该分支类型对应的可用配置 + def availableProfiles = ConfigConstants.BRANCH_PROFILES[branchName] + + // 2. 如果常量配置中没有找到,根据分支类型选择不同的默认配置源 + if (!availableProfiles) { + availableProfiles = (branchName == 'master') ? + envConfig.masterProfiles : // master分支使用特殊配置 + envConfig.Profiles // 其他分支使用通用配置 + } + + // 确保availableProfiles不为null,如果是null则转为空列表 + availableProfiles = availableProfiles ?: [] + + // 3. 获取默认配置,优先从常量配置中获取 + def defaultProfile = ConfigConstants.DEFAULT_PROFILES[branchName] + + // 4. 如果常量配置中没有默认配置,则从可用配置中选择 + if (!defaultProfile) { + defaultProfile = availableProfiles.find { it } ? // 查找第一个非空配置 + availableProfiles[0] : // 如果都为空则取第一个 + null // 如果没有可用配置则返回null + } + + return [ + available_profiles: availableProfiles, + default_profile : defaultProfile + ] + } + +} \ No newline at end of file diff --git a/src/com/python/stages/BuildService.groovy b/src/com/python/stages/BuildService.groovy new file mode 100644 index 0000000..063d583 --- /dev/null +++ b/src/com/python/stages/BuildService.groovy @@ -0,0 +1,699 @@ +package com.python.stages + +/** + * 构建阶段服务 - 负责Maven项目的编译和打包 + * + * 主要职责: + * - 执行Maven项目的清理、编译、打包操作 + * - 验证构建环境和配置的有效性 + * - 处理构建过程中的异常和错误 + * - 提供构建结果的状态反馈 + * + * 设计原则: + * - 单一职责:专注于Maven构建相关的逻辑 + * - 可重用性:可以在不同的流水线项目中重用 + * - 错误隔离:构建失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class BuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化构建服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:构建环境相关的配置参数 + */ + BuildService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + script.echo "🔧 BuildService 初始化完成" + } + + /** + * 执行构建阶段逻辑 + * 负责Maven项目的编译和打包,是CI/CD流水线的核心构建阶段 + * + * 主要步骤: + * 1. 输出构建开始信息和配置详情 + * 2. 验证环境配置的有效性 + * 3. 执行构建命令 + * 4. 处理构建结果和异常 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - MAVEN_SET: String - Maven设置文件ID(Jenkins Config File ID) + * - PROFILES: String - Maven构建环境profiles(如:dev, test, prod) + * 可选字段: + * - jarFilePath: String - JAR文件输出路径(默认:target) + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认:true) + * - goals: String - Maven执行目标(默认:"clean package") + * - pomFilePath: String - POM文件路径(默认:pom.xml) + * - orgName: String - 组织名称(用于JDK服务验证) + * - availableProfiles: List - 可用的环境profile列表 + * + * @throws Exception 当Maven构建失败时抛出异常,中断流水线 + * 异常信息包含详细的错误原因和建议解决方案 + * + * @example 使用示例: + * def buildParams = [ + * MAVEN_SET: "nexus-maven-dev", + * PROFILES: "dev", + * jarFilePath: "target", + * jarFile: "xiaomayi-admin.jar", + * skipTests: true, + * orgName: "xiaomayi", + * availableProfiles: ["dev", "test", "prod"] + * ] + * buildService.executeBuildStage(buildParams) + */ + def executeBuildStage(params) { + try { + // 输出构建开始信息和配置详情 + script.echo "🏗️ 开始构建阶段..." + + // 验证环境profile的有效性(确保配置的一致性) + validateProfile(params) + + // 执行构建命令 + // 使用buildProject方法执行实际构建 + script.echo "🔨 执行构建命令..." + buildProject(params) + + // 构建成功完成 + script.echo "✅ 代码构建完成" + + } catch (Exception e) { + // 构建失败处理 + script.echo "❌ 构建阶段失败: ${e.message}" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 验证环境profile有效性 + * 检查选择的部署环境是否在可用环境列表中,确保部署配置的正确性 + * + * 主要功能: + * - 验证必要参数的存在性和有效性 + * - 检查所选环境是否在可用环境列表中 + * - 提供详细的错误信息和调试建议 + * + * @param params 参数Map,必须包含以下字段: + * - PROFILES: String - 要验证的环境profile名称 + * - availableProfiles: List - 可用的环境profile列表 + * - orgName: String - 组织名称(用于日志和错误消息) + * + * @throws IllegalArgumentException 当缺少必要参数或参数无效时抛出 + * @throws Exception 当环境验证失败时抛出,包含详细的错误信息 + */ + private def validateProfile(params) { + script.echo "🔍 开始验证环境profile有效性..." + + // ========================================================================== + // 参数完整性检查 + // ========================================================================== + + // 检查PROFILES参数是否存在且不为空 + if (!params.PROFILES) { + def errorMsg = "缺少必要的PROFILES参数,请指定要部署的环境" + script.echo "❌ ${errorMsg}" + throw new IllegalArgumentException(errorMsg) + } + + // 检查可用环境列表是否存在且不为空 + if (!params.availableProfiles || params.availableProfiles.isEmpty()) { + def errorMsg = "缺少可用的环境profile列表,请检查配置文件中环境配置" + script.echo "❌ ${errorMsg}" + throw new IllegalArgumentException(errorMsg) + } + + // 检查组织名称参数(用于更详细的错误消息) + if (!params.orgName) { + script.echo "⚠️ 缺少组织名称参数orgName,使用默认标识" + // 这里不抛出异常,因为orgName不是核心必需参数 + } + + // ========================================================================== + // 环境有效性验证 + // ========================================================================== + + try { + def selectedProfile = params.PROFILES.trim() + def availableProfiles = params.availableProfiles.collect { it.toString().trim() } + + script.echo "📋 验证详情:" + script.echo " 🎯 选择的环境: ${selectedProfile}" + script.echo " 📊 可用环境: ${availableProfiles.join(', ')}" + + // 检查选择的环境是否在可用列表中 + if (!availableProfiles.contains(selectedProfile)) { + def errorMsg = "无效的部署环境: '${selectedProfile}'。可用环境: ${availableProfiles.join(', ')}" + // 使用script.error使构建标记为失败 + script.error("环境验证失败: ${errorMsg}") + } + + // 验证成功 + script.echo "✅ 环境profile验证通过: ${selectedProfile}" + } catch (Exception e) { + // 处理验证过程中的异常 + script.echo "❌ 环境profile验证过程中发生错误: ${e.message}" + script.echo "📋 可用环境列表: ${params.availableProfiles.join(', ')}" + + // 重新抛出异常,确保构建失败 + throw e + } + } + + /** + * 优化版Python项目构建函数 + * 简化流程、增强错误处理、提供更清晰的输出信息 + * + * @param params 构建参数Map,包含以下字段: + * - pythonVersion: String - 期望的Python版本(用于验证) + * - pipIndexUrl: String - PIP镜像源地址(默认http://mirrors.aliyun.com/pypi/simple) + * - trustedHost: String - 信任的主机(默认mirrors.aliyun.com) + * - pyFilePath: String - Python项目文件路径(默认当前目录) + * - pyFile: String - 主Python文件名称(可选,用于语法检查) + * - requirementsFile: String - 依赖文件路径(默认requirements.txt) + * - configurePipMirror: Boolean - 是否配置PIP镜像源(默认true) + * - upgradeTools: Boolean - 是否升级包管理工具(默认true) + * - installDependencies: Boolean - 是否安装依赖(默认true) + * + * @throws Exception 当Python构建失败时抛出异常 + */ + def buildProject(params) { + // 参数默认值设置 + def config = [ + pythonVersion: params.pythonVersion ?: "", + pipIndexUrl: params.pipIndexUrl ?: "http://mirrors.aliyun.com/pypi/simple", + trustedHost: params.trustedHost ?: "mirrors.aliyun.com", + pyFilePath: params.pyFilePath ?: ".", + pyFile: params.pyFile ?: "", + requirementsFile: params.requirementsFile ?: "requirements.txt", + configurePipMirror: params.configurePipMirror != false, // 默认true + upgradeTools: params.upgradeTools != false, // 默认true + installDependencies: params.installDependencies != false // 默认true + ] + + script.echo "🐍 开始Python项目构建..." + script.echo "📋 构建配置: ${config}" + + try { + // 阶段1: 环境验证 + validatePythonEnvironment(config) + + // 阶段2: 工具配置 + configurePythonTools(config) + + // 阶段3: 项目检查 + checkProjectStructure(config) + + // // 阶段4: 依赖管理 + // installProjectDependencies(config) + + // // 阶段5: 最终验证 + // finalValidation(config) + + script.echo "✅ Python项目构建完成" + + } catch (Exception e) { + handleBuildFailure(e, config) + throw e // 重新抛出异常确保构建失败 + } + } + + /** + * 验证Python环境 + */ + private def validatePythonEnvironment(config) { + script.echo "🔍 阶段1: 验证Python环境..." + + // 检查Python3和pip3基础命令 + def pythonVersion = script.sh( + script: "python3 -V", + returnStdout: true + ).trim() + script.echo "📝 ${pythonVersion}" + + def pipVersion = script.sh( + script: "pip3 -V", + returnStdout: true + ).trim() + script.echo "📦 ${pipVersion}" + + // 版本兼容性检查(如果指定了期望版本) + if (config.pythonVersion) { + def currentVersion = script.sh( + script: "python3 -c \"import sys; print('.'.join(map(str, sys.version_info[:2])))\"", + returnStdout: true + ).trim() + + script.echo "🔢 版本检查: 当前v${currentVersion}, 期望v${config.pythonVersion}" + + if (!currentVersion.startsWith(config.pythonVersion.split('\\.')[0])) { + script.echo "⚠️ 警告: Python主版本不匹配,可能影响兼容性" + } + } + } + + /** + * 配置Python工具和镜像 + */ + private def configurePythonTools(config) { + script.echo "⚙️ 阶段2: 配置Python工具..." + + // 配置PIP镜像源(可选) + if (config.configurePipMirror) { + script.sh "pip3 config set global.index-url ${config.pipIndexUrl}" + script.sh "pip3 config set install.trusted-host ${config.trustedHost}" + script.echo "✅ 镜像源配置完成: ${config.pipIndexUrl}" + } + + // 升级包管理工具(可选) + if (config.upgradeTools) { + script.echo "🔄 升级包管理工具..." + script.sh "python3 -m pip install --upgrade pip setuptools wheel --quiet" + script.echo "✅ 工具升级完成" + } + } + + /** + * 检查项目文件结构 + */ + private def checkProjectStructure(config) { + script.echo "📁 阶段3: 检查项目结构..." + + // 显示项目目录内容 + def fileList = script.sh( + script: "ls -la ${config.pyFilePath} | head -20", + returnStdout: true + ).trim() + script.echo "项目文件:\n${fileList}" + + // 检查主Python文件语法(如果指定) + if (config.pyFile) { + def fullPath = getFullPath(config.pyFilePath, config.pyFile) + if (script.fileExists(fullPath)) { + script.sh "python3 -m py_compile ${fullPath} && echo '✅ 语法检查通过'" + } else { + script.echo "⚠️ 主文件不存在: ${fullPath}" + } + } + } + + /** + * 安装项目依赖 + */ + private def installProjectDependencies(config) { + if (!config.installDependencies) { + script.echo "⏭️ 跳过依赖安装" + return + } + + script.echo "📥 阶段4: 安装项目依赖..." + + def requirementsPath = getFullPath(config.pyFilePath, config.requirementsFile) + + if (script.fileExists(requirementsPath)) { + // 安装requirements.txt中的依赖 + script.sh "pip3 install -r ${requirementsPath} --quiet" + script.echo "✅ 依赖安装完成" + + // 验证关键依赖 + validateDependencies(requirementsPath) + + } else { + script.echo "⚠️ 依赖文件不存在: ${requirementsPath}" + + // 尝试使用setup.py作为备选 + def setupPath = getFullPath(config.pyFilePath, "setup.py") + if (script.fileExists(setupPath)) { + script.echo "🔧 使用setup.py安装..." + script.sh "pip3 install -e ${config.pyFilePath} --quiet" + } else { + script.echo "💡 提示: 未找到依赖管理文件,将使用环境现有包" + } + } + } + + // /** + // * 验证安装的依赖 + // */ + // private def validateDependencies(requirementsPath) { + // script.echo "🔍 验证依赖安装..." + + // // 使用单引号避免$符号转义问题,或者正确转义$符号 + // def validationResult = script.sh( + // script: """ + // # 提取前5个主要依赖进行验证 + // grep -v '^[[:space:]]*#' '${requirementsPath}' | \\ + // grep -v '^[[:space:]]*\\$' | \\ + // head -5 | \\ + // while read line; do + // # 清理包名(移除版本说明符) + // pkg_name=\\$(echo "\\$line" | sed 's/[<=>!].*//' | xargs) + // if [ -n "\\$pkg_name" ]; then + // if python3 -c "import \\$pkg_name" 2>/dev/null; then + // echo "✅ \\$pkg_name" + // else + // echo "❌ \\$pkg_name" + // fi + // fi + // done + // """, + // returnStdout: true + // ).trim() + + // if (validationResult) { + // script.echo "依赖验证结果:\n${validationResult}" + // } + // } + + /** + * 更安全的依赖验证方法(替代方案) + */ + private def validateDependencies(requirementsPath) { + script.echo "🔍 验证依赖安装..." + + try { + // 方法1: 使用更简单的验证脚本 + def validationResult = script.sh( + script: """#!/bin/bash + REQUIREMENTS_FILE='${requirementsPath}' + COUNT=0 + if [ -f "\$REQUIREMENTS_FILE" ]; then + while read -r line; do + # 跳过注释和空行 + [[ \$line =~ ^[[:space:]]*# ]] && continue + [[ -z \${line//[[:space:]]/} ]] && continue + + # 提取包名 + pkg_name=\$(echo \$line | sed 's/[<=>!].*//' | xargs) + if [ -n "\$pkg_name" ]; then + if python3 -c "import \$pkg_name" 2>/dev/null; then + echo "✅ \$pkg_name" + else + echo "❌ \$pkg_name" + fi + COUNT=\$((COUNT + 1)) + [ \$COUNT -ge 5 ] && break + fi + done < "\$REQUIREMENTS_FILE" + else + echo "文件不存在: \$REQUIREMENTS_FILE" + fi + """, + returnStdout: true + ).trim() + + if (validationResult) { + script.echo "依赖验证结果:\n${validationResult}" + } + } catch (Exception e) { + script.echo "⚠️ 依赖验证跳过: ${e.message}" + } + } + + /** + * 最终环境验证 + */ + private def finalValidation(config) { + script.echo "🔎 阶段5: 最终验证..." + + // 验证Python环境基本功能 + script.sh "python3 -c \"print('✅ Python环境正常')\"" + + // 显示安装的包数量 + def packageCount = script.sh( + script: "pip3 list --format=json | python3 -c \"import json,sys; print('📦 已安装包:', len(json.load(sys.stdin)))\"", + returnStdout: true + ).trim() + script.echo packageCount + + // 显示项目最终状态 + def fileCount = script.sh( + script: "ls -lh ${config.pyFilePath} | wc -l | xargs echo '文件数量:'", + returnStdout: true + ).trim() + script.echo "📊 项目路径: ${config.pyFilePath}, ${fileCount}" + } + + /** + * 构建失败处理 + */ + private def handleBuildFailure(Exception e, config) { + script.echo "❌ Python项目构建失败: ${e.message}" + script.echo "💡 故障排除建议:" + script.echo " - 检查Python3安装: python3 --version" + script.echo " - 验证网络连接和镜像源可访问性" + script.echo " - 检查项目路径权限: ls -la ${config.pyFilePath}" + script.echo " - 验证requirements.txt格式" + + // 提供诊断信息 + script.echo "🐛 诊断信息:" + script.sh """ + which python3 pip3 || echo "命令未找到" + python3 -c "import sys; print('Python路径:', sys.prefix)" 2>&1 || true + ls -la ${config.pyFilePath} 2>&1 | head -10 || true + """ + } + + /** + * 工具函数: 获取完整文件路径 + */ + private def getFullPath(basePath, fileName) { + return basePath != "." ? "${basePath}/${fileName}" : fileName + } + + // /** + // * 使用Python构建项目 + // * 执行Python环境配置、依赖安装和项目验证,支持自定义镜像源和依赖管理 + // * 包含完整的Python环境检查和配置流程 + // * + // * @param params 构建参数Map,包含以下字段: + // * - pythonVersion: String - 期望的Python版本(用于验证) + // * - pipIndexUrl: String - PIP镜像源地址(默认http://mirrors.aliyun.com/pypi/simple) + // * - trustedHost: String - 信任的主机(默认mirrors.aliyun.com) + // * - pyFilePath: String - Python项目文件路径(默认当前目录) + // * - pyFile: String - 主Python文件名称(可选,用于语法检查) + // * - requirementsFile: String - 依赖文件路径(默认requirements.txt) + // * - configurePipMirror: Boolean - 是否配置PIP镜像源(默认true) + // * - upgradeTools: Boolean - 是否升级包管理工具(默认true) + // * - installDependencies: Boolean - 是否安装依赖(默认true) + // * + // * @throws Exception 当Python构建失败时抛出异常 + // * 特别说明:最终实现的命令 + // * python3 -V + // * pip3 config set global.index-url http://mirrors.aliyun.com/pypi/simple + // * pip3 config set install.trusted-host mirrors.aliyun.com + // * python3 -m pip install --upgrade pip + // * pip3 install --upgrade setuptools wheel + // * pip3 install ez_setup + // * pip3 install -r requirements.txt + // * ls -lsh ${pyFilePath} + // */ + // def buildProject(params) { + // // 从参数中获取Python版本,用于验证环境 + // def pythonVersion = params.programLangVersion ?: "" + // // 获取PIP镜像配置 + // def pipIndexUrl = params.pipIndexUrl ?: "http://mirrors.aliyun.com/pypi/simple" + // def trustedHost = params.trustedHost ?: "mirrors.aliyun.com" + // // 获取Python项目路径 + // def pyFilePath = params.pyFilePath ?: "." + // // 获取主Python文件 + // def pyFile = params.pyFile ?: "" + // // 获取依赖文件路径 + // def requirementsFile = params.requirementsFile ?: "requirements.txt" + // // 是否配置镜像源 + // def configurePipMirror = params.configurePipMirror != false // 默认true + // // 是否升级工具 + // def upgradeTools = params.upgradeTools != false // 默认true + // // 是否安装依赖 + // def installDependencies = params.installDependencies != false // 默认true + + // script.echo "🐍 开始Python项目构建..." + // script.echo "📋 构建参数:" + // script.echo " 🔧 Python版本: ${pythonVersion}" + // script.echo " 🌐 PIP镜像: ${pipIndexUrl}" + // script.echo " 🔒 信任主机: ${trustedHost}" + // script.echo " 📁 项目路径: ${pyFilePath}" + // script.echo " 📄 主文件: ${pyFile}" + // script.echo " 📦 依赖文件: ${requirementsFile}" + // script.echo " ⚙️ 配置镜像: ${configurePipMirror}" + // script.echo " 🔄 升级工具: ${upgradeTools}" + // script.echo " 📥 安装依赖: ${installDependencies}" + + // try { + // script.echo "🔍 检查Python环境..." + // // 检查Python3版本 + // script.sh "python3 -V" + + // // 检查pip3版本 + // script.sh "pip3 -V" + + // // 验证Python版本(如果指定了期望版本) + // if (pythonVersion) { + // script.echo "✅ 验证Python版本兼容性..." + // def currentVersion = script.sh( + // script: "python3 -c \"import sys; print('.'.join(map(str, sys.version_info[:3])))\"", + // returnStdout: true + // ).trim() + // script.echo " 当前版本: ${currentVersion}, 期望版本: ${pythonVersion}" + + // // 简单的版本号前缀匹配验证 + // if (!currentVersion.startsWith(pythonVersion.split('\\.')[0] + '.')) { + // script.echo "⚠️ Python主版本不匹配,可能影响兼容性" + // } + // } + + // script.echo "⚙️ 配置Python环境..." + + // // 配置PIP镜像源(根据参数决定是否配置) + // if (configurePipMirror) { + // script.sh "pip3 config set global.index-url ${pipIndexUrl}" + // script.sh "pip3 config set install.trusted-host ${trustedHost}" + // script.echo "✅ PIP镜像源配置完成" + + // // 验证镜像源配置 + // script.sh "pip3 config list | grep -E '(index-url|trusted-host)'" + // } + + // // 升级Python包管理工具 + // if (upgradeTools) { + // script.echo "📦 升级Python包管理工具..." + // script.sh "python3 -m pip install --upgrade pip" + // script.sh "pip3 install --upgrade setuptools wheel" + // script.sh "pip3 install ez_setup" + // script.echo "✅ 包管理工具升级完成" + // } + + // script.echo "🔍 检查项目文件结构..." + // // 显示项目文件列表 + // script.sh "ls -lsh ${pyFilePath}" + + // // 安装项目依赖 + // if (installDependencies) { + // script.echo "📥 安装项目依赖..." + + // // 检查依赖文件是否存在 + // def requirementsPath = pyFilePath != "." ? "${pyFilePath}/${requirementsFile}" : requirementsFile + // if (script.fileExists(requirementsPath)) { + // script.echo "✅ 找到依赖文件: ${requirementsPath}" + + // // 安装依赖 + // script.sh "pip3 install -r ${requirementsPath}" + + // // 验证安装结果 + // script.echo "🔍 验证依赖安装情况..." + // def dependencyCheck = script.sh( + // script: """ + // if [ -f "${requirementsPath}" ]; then + // # 提取依赖包名(忽略注释和空行,处理版本说明符) + // grep -v '^#' "${requirementsPath}" | grep -v '^$' | while read line; do + // pkg_name=\$(echo "\$line" | sed 's/[<=>].*//' | xargs) + // if [ -n "\$pkg_name" ]; then + // python3 -c "import \$pkg_name" 2>/dev/null && echo "✅ \$pkg_name" || echo "❌ \$pkg_name" + // fi + // done + // else + // echo "依赖文件不存在: ${requirementsPath}" + // fi + // """, + // returnStdout: true + // ).trim() + + // script.echo "依赖验证结果:\n${dependencyCheck}" + + // } else { + // script.echo "⚠️ 依赖文件 ${requirementsPath} 不存在,跳过依赖安装" + + // // 检查是否有setup.py作为备选 + // def setupPath = pyFilePath != "." ? "${pyFilePath}/setup.py" : "setup.py" + // if (script.fileExists(setupPath)) { + // script.echo "🔧 找到setup.py,进行开发模式安装..." + // script.sh "pip3 install -e ${pyFilePath}" + // } + // } + // } + + // script.echo "✅ Python环境配置完成" + + // // 验证Python环境 + // script.echo "🔍 验证Python环境..." + // script.sh "python3 -c \"print('✅ Python环境正常'); import sys; print('Python路径:', sys.path[:3])\"" + + // // 如果有主Python文件,检查语法 + // if (pyFile) { + // def pyFilePathFull = pyFilePath != "." ? "${pyFilePath}/${pyFile}" : pyFile + // if (script.fileExists(pyFilePathFull)) { + // script.echo "🔍 检查Python文件语法..." + // script.sh "python3 -m py_compile ${pyFilePathFull} && echo '✅ Python语法检查通过' || echo '⚠️ 语法检查发现问题'" + + // // 显示文件信息 + // script.echo "📄 主文件信息:" + // script.sh "ls -lsh ${pyFilePathFull}" + // } else { + // script.echo "⚠️ 主Python文件不存在: ${pyFilePathFull}" + // } + // } + + // script.echo "📊 构建结果验证:" + + // // 显示已安装的包 + // script.sh "pip3 list --format=columns | head -20" + // script.echo "...(更多包请查看完整列表)" + + // // 显示项目最终文件状态 + // script.sh "ls -lsh ${pyFilePath}" + + // // 检查Python路径和可访问性 + // script.echo "🔍 检查Python模块导入路径..." + // script.sh "python3 -c \"import site; print('Site-packages:', site.getsitepackages())\"" + + // script.echo "✅ Python项目构建完成" + + // } catch (Exception e) { + // script.echo "❌ Python项目构建失败: ${e.message}" + // script.echo "💡 建议检查:" + // script.echo " - Python3是否正确安装" + // script.echo " - 网络连接是否正常(依赖下载)" + // script.echo " - 项目路径和文件权限是否正确" + // script.echo " - requirements.txt文件格式是否正确" + // script.echo " - 镜像源是否可访问" + + // // 提供调试信息 + // script.echo "🐛 调试信息:" + // script.sh "python3 --version 2>&1 || echo 'Python3未安装'" + // script.sh "which python3 pip3 2>&1" + // script.sh "ls -la ${pyFilePath} 2>&1 || echo '路径不存在'" + + // throw e + // } + // } + +} \ No newline at end of file diff --git a/src/com/python/stages/ContainerBuildService.groovy b/src/com/python/stages/ContainerBuildService.groovy new file mode 100644 index 0000000..0d84b1a --- /dev/null +++ b/src/com/python/stages/ContainerBuildService.groovy @@ -0,0 +1,287 @@ +package com.python.stages + +/** + * 容器构建阶段服务 - 负责Docker镜像的构建和推送 + * + * 主要职责: + * - 执行Docker镜像的构建、标签生成和推送操作 + * - 管理镜像标签的生命周期和环境变量设置 + * - 处理容器构建过程中的异常和错误 + * - 提供构建结果的状态反馈和镜像信息 + * + * 设计原则: + * - 单一职责:专注于Docker容器构建相关的逻辑 + * - 可重用性:可以在不同的流水线项目中重用 + * - 错误隔离:容器构建失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class ContainerBuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储容器构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化容器构建服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:镜像仓库配置、构建选项等 + */ + ContainerBuildService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + script.echo "🔧 ContainerBuildService 初始化完成" + } + + /** + * 执行容器构建阶段逻辑 + * 负责Docker镜像的构建和推送,是CI/CD流水线的容器化阶段 + * + * 主要步骤: + * 1. 输出容器构建开始信息 + * 2. 调用工具类执行Docker镜像构建和推送 + * 3. 存储镜像标签到环境变量供后续阶段使用 + * 4. 处理构建异常并提供详细的错误信息 + * + * @param params 管道参数Map,包含镜像构建所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) + * - registryCredential: String - Jenkins仓库认证凭证ID + * - registryProject: String - 仓库项目/命名空间名称(如:xiaomayi) + * - projectName: String - 项目名称(如:xiaomayi-admin) + * - PROFILES: String - 环境名称(dev/test/uat/prod) + * 可选字段: + * - buildOptions: String - Docker构建选项(如:--no-cache --pull) + * - baseImage: String - 基础镜像名称 + * - serverPort: String/Integer - 服务端口号 + * - jarFile: String - JAR文件名 + * - jarFilePath: String - JAR文件路径 + * + * @return String 构建的镜像标签(格式:{环境}-{构建ID} 或 {构建ID}) + * @throws Exception 当Docker构建或推送失败时抛出异常,包含详细的错误信息 + * + * @example 使用示例: + * def containerParams = [ + * registry: "192.168.10.102:8001", + * registryCredential: "xiaomayi-hub-credentials", + * registryProject: "xiaomayi", + * projectName: "xiaomayi-admin", + * PROFILES: "dev", + * buildOptions: "--no-cache", + * baseImage: "openjdk:17-jdk-alpine" + * ] + * def imageTag = containerBuildService.executeContainerBuildStage(containerParams) + */ + def executeContainerBuildStage(params) { + try { + script.echo "🐳 开始容器构建阶段..." + script.echo "📋 容器构建配置:" + script.echo " 📦 项目名称: ${params.projectName}" + script.echo " 🌍 目标环境: ${params.PROFILES}" + script.echo " 🏷️ 镜像仓库: ${params.registry}" + script.echo " 📁 命名空间: ${params.registryProject}" + + // 输出详细的构建选项(如果有) + if (params.buildOptions) { + script.echo " ⚙️ 构建选项: ${params.buildOptions}" + } + if (params.baseImage) { + script.echo " 🖼️ 基础镜像: ${params.baseImage}" + } + + // 构建Docker镜像并获取镜像标签 + // 使用buildDockerImage方法执行实际构建 + // 该方法会处理Docker构建、标签生成和镜像推送 + script.echo "🔨 执行Docker镜像构建..." + def imageTag = buildDockerImage(params) + + // 将镜像标签存储到环境变量,供后续部署阶段使用 + // 环境变量可以在整个Pipeline过程中共享使用 + script.env.IMAGE_TAG = imageTag + script.echo "💾 镜像标签已存储到环境变量: IMAGE_TAG=${imageTag}" + + script.echo "✅ 镜像构建完成" + script.echo "🎉 镜像标签: ${imageTag}" + script.echo "🚀 镜像已推送到仓库: ${params.registry}/${params.registryProject}/${params.projectName}:${imageTag}" + + return imageTag + + } catch (Exception e) { + // 容器构建失败处理 + script.echo "❌ 容器构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Dockerfile是否存在且语法正确" + script.echo " - 镜像仓库凭证是否有效" + script.echo " - 网络连接是否正常(镜像推送)" + script.echo " - 基础镜像是否可以访问" + script.echo " - 磁盘空间是否充足" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 构建Docker镜像 + * 使用Docker Pipeline插件构建和推送Docker镜像到指定的镜像仓库 + * 支持自定义构建参数、构建选项和自动清理本地镜像 + * + * @param params 构建参数Map,包含以下必需字段: + * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) + * - registryCredential: String - Jenkins凭证ID,用于仓库认证 + * - registryProject: String - 仓库中的项目/命名空间名称(如:xiaomayi) + * - projectName: String - 项目名称(如:xiaomayi-admin) + * - PROFILES: String - 环境名称(dev/test/uat/prod),用于生成镜像标签 + * 可选字段: + * - buildOptions: String - Docker构建选项(如:--no-cache --pull) + * - baseImage: String - 基础镜像名称,通过--build-arg传递 + * - port: String/Integer - 服务端口号,通过--build-arg传递 + * - jarFile: String - JAR文件名,通过--build-arg传递 + * - jarFilePath: String - JAR文件路径,通过--build-arg传递 + * + * @return String 构建的镜像标签(如:dev-123、prod-456) + * @throws Exception 当Docker构建或推送失败时抛出异常 + */ + def buildDockerImage(params) { + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = generateImageTag(params.PROFILES) + script.echo "🏷️ 生成的镜像标签: ${imageTag}" + + // 获取构建选项,如果未提供则使用空字符串 + def buildOptions = params.buildOptions ?: "" + script.echo "⚙️ 构建选项: ${buildOptions}" + + // 输出构建开始信息和详细参数 + script.echo "🐳 开始构建Docker镜像..." + script.echo " 📦 镜像仓库: ${params.registry}" + script.echo " 📁 命名空间: ${params.registryProject}" + script.echo " 🏷️ 项目名称: ${params.projectName}" + script.echo " 🎯 镜像标签: ${imageTag}" + script.echo " 🔑 仓库凭证: ${params.registryCredential}" + + try { + // 使用Docker Pipeline插件连接到镜像仓库并进行构建推送 + script.docker.withRegistry("https://${params.registry}", params.registryCredential) { + // 构建Docker镜像 + // 镜像名称格式: {命名空间}/{项目名称}:{标签} + def dockerImage = script.docker.build( + // 完整的镜像名称(包含标签) + "${params.registryProject}/${params.projectName}:${imageTag}", + // 构建命令参数组合 + "${buildOptions} " + + // 传递构建参数给Dockerfile + // 环境配置 + "--build-arg VAR_PROFILES=${params.PROFILES} " + + // 基础镜像 + "--build-arg VAR_BASE_IMAGE=${params.baseImage} " + + // 服务端口 + "--build-arg VAR_PORT=${params.port} " + + // JAR文件名 + "--build-arg VAR_JARFILE=${params.jarFile} " + + // JAR文件路径 + "--build-arg VAR_JARFILE_PATH=${params.jarFilePath} " + + // 指定Dockerfile路径和构建上下文,使用项目根目录下的.jenkins/Dockerfile文件 + "-f ${params.dockerFile} ./" + ) + + // 推送镜像到远程仓库 + script.echo "🚀 推送镜像到仓库: ${params.registry}" + dockerImage.push() + + // 清理本地Docker镜像,释放磁盘空间 + script.echo "🧹 开始清理本地镜像..." + cleanupDockerImages(params.registry, params.registryProject, params.projectName, imageTag) + } + + script.echo "✅ Docker镜像构建和推送完成" + return imageTag + + } catch (Exception e) { + script.echo "❌ Docker镜像构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Dockerfile路径是否正确" + script.echo " - 镜像仓库凭证是否有效" + script.echo " - 网络连接是否正常" + throw e // 重新抛出异常,让调用方处理 + } + } + + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + private def generateImageTag(profile) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID + } + + /** + * 清理Docker镜像 + * 删除本地Docker环境中指定的镜像,避免磁盘空间占用和镜像冲突 + * 主要用于构建后清理,移除不再需要的临时镜像 + * + * @param registry 镜像仓库地址(如:192.168.10.102:8001) + * @param registryProject 仓库中的项目/命名空间名称(如:xiaomayi) + * @param projectName 项目名称(如:xiaomayi-admin) + * @param imageTag 镜像标签(由generateImageTag方法生成) + * + * @note 使用"|| true"确保清理失败不会中断构建流程 + * @note 尝试清理两种格式的镜像: + * 1. {registryProject}/{projectName}:{imageTag} + * 2. {registry}/{registryProject}/{projectName}:{imageTag} + */ + private def cleanupDockerImages(registry, registryProject, projectName, imageTag) { + try { + // 输出清理开始信息 + script.echo "🧹 开始清理Docker镜像: ${projectName}:${imageTag}" + + // 清理第一种格式的镜像:{项目}/{应用}:{标签} + // 例如:xiaomayi/xiaomayi-admin:dev-123 + // 使用"|| true"确保命令即使失败也不会抛出异常中断脚本 + script.sh "docker rmi -f ${registryProject}/${projectName}:${imageTag} || true" + + // 清理第二种格式的镜像:{仓库地址}/{项目}/{应用}:{标签} + // 例如:192.168.10.102:8001/xiaomayi/xiaomayi-admin:dev-123 + script.sh "docker rmi -f ${registry}/${registryProject}/${projectName}:${imageTag} || true" + + script.echo "✅ Docker镜像清理完成" + + } catch (Exception e) { + // 捕获并处理异常,输出错误信息但不会中断主流程 + // 镜像清理失败通常不会影响主要功能,所以只是记录日志 + script.echo "⚠️ 清理Docker镜像失败: ${e.message}" + script.echo " 此错误通常可以忽略,不会影响部署流程" + } + } + +} \ No newline at end of file diff --git a/src/com/python/stages/DeployService.groovy b/src/com/python/stages/DeployService.groovy new file mode 100644 index 0000000..487d8c7 --- /dev/null +++ b/src/com/python/stages/DeployService.groovy @@ -0,0 +1,1343 @@ +package com.python.stages + +/** + * 部署阶段服务 - 负责将Docker镜像部署到目标服务器 + * + * 主要职责: + * - 执行单服务器和多服务器的应用部署操作 + * - 解析服务器引用配置,确定部署目标 + * - 管理部署参数和环境配置 + * - 处理部署过程中的异常和错误 + * - 生成部署结果报告和状态反馈 + * + * 设计原则: + * - 单一职责:专注于应用部署相关的逻辑 + * - 灵活性:支持单服务器和多服务器部署模式 + * - 错误隔离:部署失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * - 可扩展性:易于添加新的部署策略和功能 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeployService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储部署环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化部署服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:部署策略、超时设置、健康检查等配置 + */ + DeployService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + script.echo "🔧 DeployService 初始化完成" + script.echo " 📡 服务器解析器已就绪" + } + + /** + * 执行部署阶段逻辑 + * 负责将Docker镜像部署到目标服务器,支持单服务器和多服务器部署模式 + * 这是CI/CD流水线的核心部署阶段,负责将构建好的镜像部署到目标环境 + * + * 主要步骤: + * 1. 输出部署开始信息和配置验证 + * 2. 解析服务器引用配置,确定部署目标 + * 3. 根据服务器数量选择部署模式(单机/多机) + * 4. 执行具体的部署操作 + * 5. 处理部署结果和异常情况 + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servers: List - 服务器列表数组,包含所有可用的服务器配置 + * - deployServer: String - 部署服务器引用表达式 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称(dev/test/uat/prod) + * 可选字段: + * - deployScript: String - 部署脚本文件名(默认:deploy.sh) + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - deploymentTimeout: Integer - 部署超时时间(秒) + * - healthCheckPath: String - 健康检查端点路径 + * - healthCheckTimeout: Integer - 健康检查超时时间 + * - stopOnFailure: Boolean - 失败时是否停止部署 + * + * @throws Exception 当部署失败时抛出异常,包含详细的错误信息 + * 异常信息会区分单服务器和多服务器部署的失败情况 + * + * @example 使用示例: + * def deployParams = [ + * servers: config.common.profiles.dev.servers, + * deployServer: "servers[0,2]", // 部署到第1和第3台服务器 + * projectName: "xiaomayi-admin", + * PROFILES: "dev", + * deploymentStrategy: "rolling", + * deploymentTimeout: 3600 + * ] + * deployService.executeDeployStage(deployParams) + */ + def executeDeployStage(params) { + try { + script.echo "🚀 开始部署阶段..." + script.echo "📋 部署配置:" + script.echo " 📦 项目名称: ${params.projectName}" + script.echo " 🌍 目标环境: ${params.PROFILES}" + script.echo " 📡 服务器引用: ${params.deployServer ?: 'servers[0]'}" + script.echo " 📊 总服务器数: ${params.servers?.size() ?: 0}" + script.echo " 📦 目标服务器: ${params.targetServers}" + + // 目标服务器 + def targetServers = params.targetServers + // 根据目标服务器数量选择部署模式 + if (targetServers.size() > 1) { + script.echo "🔀 多服务器部署模式..." + deployMultipleServers(targetServers, params) + } else if (targetServers.size() == 1) { + script.echo "🔷 单服务器部署模式..." + deploySingleServer(targetServers[0], params) + } else { + throw new Exception("未找到有效的部署目标服务器") + } + + script.echo "✅ 应用部署完成" + + } catch (Exception e) { + script.echo "❌ 部署阶段失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - 服务器配置是否正确" + script.echo " - SSH密钥凭证是否有效" + script.echo " - 网络连接是否正常" + script.echo " - 部署脚本是否存在且可执行" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 部署到单个服务器 + * 负责将应用部署到指定的目标服务器,适用于单实例部署场景 + * + * 主要步骤: + * 1. 输出服务器信息和部署详情 + * 2. 构建完整的部署参数Map + * 3. 添加额外的部署配置参数 + * 4. 执行部署操作并处理结果 + * + * @param server 服务器配置对象,包含以下字段: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - SSH端口号(默认22) + * - serverName: String - 服务器名称标识 + * - sshKey: String - Jenkins SSH密钥凭证ID + * - weight: Integer - 负载权重(可选) + * - isPrimary: Boolean - 是否为主节点(可选) + * - priority: Integer - 部署优先级(可选) + * + * @param params 部署参数Map,包含以下字段: + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - 其他部署相关配置 + * + * @throws Exception 部署失败时抛出异常,包含详细的错误信息和诊断建议 + * + * @note 支持敏感信息保护,自动隐藏密钥和密码字段的日志输出 + */ + private def deploySingleServer(server, params) { + script.echo "🎯 开始部署到单服务器: ${server.serverName}" + script.echo " 📍 目标地址: ${server.serverIP}:${server.serverPort}" + script.echo " 🔑 SSH凭证: ${server.sshKey}" + + try { + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def deployParams = [ + // serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + // serverPort : server.serverPort, // SSH端口号(默认22) + // serverName : server.serverName, // 服务器名称标识,用于日志和监控 + // sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + profile : params.PROFILES, // 环境名称(dev/test/uat/prod) + buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + composeFile : params.composeFile, + ] + + // ========================================================================== + // 添加额外部署参数 + // 从主要参数和服务器配置中提取额外的部署参数 + // ========================================================================== + def extraParams = getDeploymentExtraParams(params, server) + if (extraParams) { + // 使用putAll方法合并Map,确保所有参数都传递给部署工具 + deployParams.putAll(extraParams) + } + + // 打印部署参数详情(调试用),自动隐藏敏感信息 + script.echo "📋 部署参数详情:" + deployParams.each { key, value -> + if (!key.toLowerCase().contains('key') && !key.toLowerCase().contains('password')) { + script.echo " ${key}: ${value}" + } else { + script.echo " ${key}: ********" // 敏感信息隐藏 + } + } + + // 执行部署操作 + script.echo "🚀 正在执行部署..." + deployToServer(deployParams) + + script.echo "✅ 单服务器部署完成: ${server.serverName}" + + } catch (Exception e) { + script.echo "❌ 单服务器部署失败: ${server.serverName}" + script.echo " 💥 错误信息: ${e.message}" + script.echo " 🔍 错误类型: ${e.getClass().name}" + + // 记录详细的错误信息,但不暴露敏感信息 + def errorDetails = "部署到服务器 ${server.serverName} (${server.serverIP}) 失败" + if (e.message?.contains('Authentication')) { + errorDetails += " - 认证失败,请检查SSH密钥" + } else if (e.message?.contains('Connection')) { + errorDetails += " - 连接失败,请检查网络和端口" + } else if (e.message?.contains('Timeout')) { + errorDetails += " - 操作超时,请检查服务器状态" + } else if (e.message?.contains('Permission')) { + errorDetails += " - 权限不足,请检查文件权限" + } + + throw new Exception(errorDetails, e) + } + } + + /** + * 部署到多个服务器 + * 并行或串行地将应用部署到多台目标服务器,支持失败处理和结果报告 + * 适用于集群部署、多节点部署等场景,支持灵活的失败处理策略 + * + * 主要步骤: + * 1. 初始化部署结果跟踪和失败记录 + * 2. 遍历服务器列表,逐台执行部署 + * 3. 根据配置决定失败时是否继续部署 + * 4. 生成部署结果报告和统计信息 + * 5. 处理最终部署结果,决定是否抛出异常 + * + * @param servers 服务器列表,包含每台服务器的配置信息: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - SSH端口号 + * - serverName: String - 服务器名称标识 + * - sshKey: String - Jenkins SSH密钥凭证ID + * - weight: Integer - 负载权重(可选) + * - isPrimary: Boolean - 是否为主节点(可选) + * - priority: Integer - 部署优先级(可选) + * + * @param params 部署参数Map,包含以下字段: + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - stopOnFailure: Boolean - 失败时是否停止部署 + * - 其他部署相关配置 + * + * @throws Exception 当配置了失败停止且有服务器部署失败时抛出异常 + * 异常信息包含失败的服务器列表和详细错误信息 + * + * @note 支持两种失败处理模式: + * - 失败停止:一旦有服务器部署失败就中断整个部署流程 + * - 失败继续:继续部署其他服务器,最后汇总报告所有失败情况 + */ + private def deployMultipleServers(servers, params) { + // 输出部署开始信息,显示目标服务器数量 + script.echo "🔀 开始部署到 ${servers.size()} 台服务器" + script.echo "📊 失败处理策略: ${shouldStopOnFailure(params) ? '失败停止' : '失败继续'}" + + // 初始化部署结果跟踪Map,用于记录每台服务器的部署状态 + def deploymentResults = [:] + // 初始化失败服务器列表,用于收集部署失败的服务器信息 + def failedServers = [] + + // 遍历服务器列表,逐台进行部署 + servers.each { server -> + try { + // 输出当前部署的服务器信息 + script.echo "➡️ 正在部署到: ${server.serverName} (${server.serverIP}:${server.serverPort})" + + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def deployParams = [ + // serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + // serverPort : server.serverPort, // SSH端口号(默认22) + // serverName : server.serverName, // 服务器名称标识,用于日志和监控 + // sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + profile : params.PROFILES, // 环境名称(dev/test/uat/prod) + buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + composeFile : params.composeFile, + ] + + // ========================================================================== + // 添加额外部署参数 + // 从主要参数和服务器配置中提取额外的部署参数 + // ========================================================================== + def extraParams = getDeploymentExtraParams(params, server) + if (extraParams) { + // 使用putAll方法合并Map,确保所有参数都传递给部署工具 + deployParams.putAll(extraParams) + } + + // ========================================================================== + // 执行部署操作 + // 调用部署工具类执行实际的部署命令 + // ========================================================================== + script.echo "🚀 正在执行部署到 ${server.serverName}..." + // 调用部署工具类,传递完整的部署参数 + deployToServer(deployParams) + + // 输出部署成功信息 + script.echo "✅ 部署成功: ${server.serverName}" + + // 记录部署结果 + deploymentResults[server.serverName] = "成功" + + } catch (Exception e) { + // ========================================================================== + // 异常处理:部署失败的情况 + // ========================================================================== + + // 记录失败的部署结果,包含错误信息 + deploymentResults[server.serverName] = "失败: ${e.message}" + // 将失败的服务器添加到失败列表 + failedServers << server.serverName + // 输出详细的错误信息 + script.echo "❌ 部署失败: ${server.serverName} - ${e.message}" + + // ========================================================================== + // 失败处理策略:根据配置决定是否继续部署 + // ========================================================================== + if (shouldStopOnFailure(params)) { + // 如果配置了失败停止,抛出异常中断部署流程 + throw new Exception("部署中断,因为在 ${server.serverName} 上失败且配置了失败停止", e) + } else { + // 如果未配置失败停止,继续部署其他服务器 + script.echo "⏭️ 继续部署其他服务器(配置了失败继续)" + } + } + } + + // ========================================================================== + // 生成部署报告 + // 汇总所有服务器的部署结果,提供清晰的部署状态概览 + // ========================================================================== + generateDeploymentReport(deploymentResults) + + // ========================================================================== + // 最终结果检查 + // 如果有服务器部署失败,抛出异常通知调用方 + // ========================================================================== + if (failedServers) { + // 构建详细的错误信息 + def errorMessage = "多服务器部署完成,但有 ${failedServers.size()} 台服务器失败: ${failedServers.join(', ')}" + // 输出警告信息(不中断流程,但让调用方知道有部分失败) + script.echo "⚠️ ${errorMessage}" + // 抛出异常,让调用方决定如何处理部分失败的情况 + throw new Exception(errorMessage) + } else { + // 所有服务器都部署成功 + script.echo "🎉 所有服务器部署成功!" + } + } + + /** + * 获取部署额外参数 + * 从主要参数和服务器配置中提取并合并部署所需的额外参数 + * 用于构建完整的部署参数Map,确保部署过程拥有所有必要信息 + * + * @param params 主要部署参数Map,包含环境变量、部署策略等通用配置 + * @param server 服务器配置对象,包含服务器特定的属性信息 + * @return 合并后的参数Map,包含所有部署所需的额外参数 + */ + private def getDeploymentExtraParams(params, server) { + // 初始化空的额外参数Map,用于存储合并后的参数 + def extraParams = [:] + + // ========================================================================== + // 添加环境相关参数(从主要参数中提取) + // ========================================================================== + + // 添加环境变量配置(如果存在) + // envVars通常包含应用运行时需要的环境变量,如数据库连接、日志级别等 + if (params?.envVars) { + extraParams.envVars = params.envVars + } + + // 添加部署策略配置(如果存在) + // deploymentStrategy定义部署方式,如rolling(滚动部署)、blue-green(蓝绿部署)等 + if (params?.deploymentStrategy) { + extraParams.deploymentStrategy = params.deploymentStrategy + } + + // 部署超时时间 + if (params?.deploymentStrategy) { + extraParams.deploymentTimeout = params.deploymentTimeout + } + + // 部署失败重试次数 + if (params?.retryAttempts) { + extraParams.retryAttempts = params.retryAttempts + } + + // ========================================================================== + // 添加服务器特定信息(从服务器配置中提取) + // ========================================================================== + + // 将服务器IP地址添加到额外参数Map中 + // serverIP是部署过程中必需的核心连接参数 + if (server?.serverIP) { + extraParams.serverIP = server.serverIP + } + + // 将服务器SSH端口添加到额外参数Map中 + // serverPort指定了SSH服务的监听端口,默认22 + if (server?.serverPort) { + extraParams.serverPort = server.serverPort + } + + // 添加服务器名称标识,用于日志记录和监控 + if (server?.serverName) { + extraParams.serverName = server.serverName + } + + // 添加SSH密钥凭证ID,用于远程服务器认证 + // sshKey是Jenkins中配置的凭证ID,不是实际的密钥内容 + if (server?.sshKey) { + extraParams.sshKey = server.sshKey + } + + // 添加服务器权重配置,用于负载均衡流量分配 + // weight为null时使用默认值50,表示平均分配流量 + if (server?.weight != null) { + extraParams.weight = server.weight ?: 50 // 默认权重为50 + } + + // 添加主节点标识,用于标识服务器是否为主节点 + // isPrimary为null时使用默认值false + if (server?.isPrimary != null) { + extraParams.isPrimary = server.isPrimary ?: false // 默认非主节点 + } + + // 添加部署优先级,数字越小优先级越高 + // 用于控制部署顺序,高优先级服务器优先部署 + if (server?.priority != null) { + extraParams.priority = server.priority + } + + // ========================================================================== + // 添加计算得到的配置参数 + // ========================================================================== + + // 添加部署超时配置,根据环境自动计算合适的超时时间 + // 生产环境通常需要更长的超时时间 + if (params?.deploymentTimeout) { + extraParams.deploymentTimeout = params.deploymentTimeout + } else { + extraParams.deploymentTimeout = getDeploymentTimeout(params) + } + + // ========================================================================== + // 添加健康检查相关配置(如果存在) + // ========================================================================== + + // 添加健康检查端点路径,用于部署后验证应用状态 + if (params.healthCheckPath) { + extraParams.healthCheckPath = params.healthCheckPath + } + + // 添加健康检查超时时间,控制健康检查的最大等待时间 + if (params.healthCheckTimeout) { + extraParams.healthCheckTimeout = params.healthCheckTimeout + } + + // 返回合并后的额外参数Map + // 这个Map将被合并到主要的部署参数中,形成完整的部署配置 + return extraParams + } + + /** + * 辅助方法:获取部署超时时间(根据环境配置) + * 不同环境设置不同的超时时间,生产环境需要更长的部署时间 + * + * @param params 部署参数,包含PROFILES字段指示环境类型 + * @return 超时时间(秒) + */ + private def getDeploymentTimeout(params) { + // 定义各环境的默认超时时间(单位:秒) + def timeouts = [ + 'dev' : 1800, // 开发环境:30分钟(快速失败) + 'test': 2700, // 测试环境:45分钟 + 'uat' : 3600, // UAT环境:60分钟 + 'prod': 7200 // 生产环境:120分钟(谨慎部署) + ] + + // 获取环境类型,默认为开发环境 + def env = params.PROFILES?.toLowerCase() ?: 'dev' + + // 返回对应环境的超时时间,如果没有匹配则使用开发环境超时 + return timeouts[env] ?: 1800 + } + + /** + * 部署到单个服务器 + * 执行远程部署命令,将指定版本的Docker镜像部署到目标服务器 + * 该方法包含完整的部署流程:文件传输、权限设置、命令执行和结果验证 + * + * @param deployParams 部署参数Map,包含以下必需字段: + * - serverIP: String - 服务器IP地址(支持user@host格式,如esxi@124.222.176.210) + * - serverPort: Integer - 服务器SSH端口号 + * - projectName: String - 项目名称(如xiaomayi-admin) + * - profile: String - 环境profile(dev/test/uat/prod) + * - buildId: String - 构建ID(用于版本追踪) + * - deployPath: String - 远程服务器部署路径(如/usr/local/docker/xiaomayi-web/) + * - deployScript: String - 部署脚本文件名(如deploy_env.sh) + * 可选字段: + * - sshKey: String - SSH密钥凭证ID(默认为jenkins-ssh-205) + * - timeout: Integer - 命令执行超时时间(秒,默认300秒) + * - extraArgs: String - 额外的部署参数 + * - retryAttempts: Integer - 失败重试次数(默认3次) + * + * @throws IllegalArgumentException 参数缺失或无效时抛出 + * @throws Exception SSH连接、文件传输或命令执行失败时抛出 + * + * @example + * deployToServer([ + * serverIP: "esxi@124.222.176.210", + * serverPort: 12422, + * projectName: "xiaomayi-admin", + * profile: "prod", + * buildId: "31", + * deployPath: "/usr/local/docker/xiaomayi-web/", + * deployScript: "deploy_env.sh", + * sshKey: "jenkins-ssh-205", + * timeout: 300 + * ]) + */ + def deployToServer(deployParams) { + // 参数解析和默认值设置 + def serverIP = deployParams.serverIP + def serverPort = deployParams.serverPort ?: 22 // 默认SSH端口 + def projectName = deployParams.projectName + def profile = deployParams.profile + def buildId = deployParams.buildId + def sshKey = deployParams.sshKey ?: 'jenkins-ssh-205' // 默认SSH凭证 + def timeout = deployParams.deploymentTimeout ?: 300 // 默认5分钟超时 + def extraArgs = deployParams.extraArgs ?: '' // 额外参数 + def retryAttempts = deployParams.retryAttempts ?: 3 // 默认重试3次 + def deployPath = deployParams.deployPath // 远程部署路径 + def deployScript = deployParams.deployScript // 部署脚本文件名 + def composeFile = deployParams.composeFile + + // 日志输出部署信息 + script.echo "🚀 开始部署到服务器: ${serverIP}:${serverPort}" + script.echo "📦 项目: ${projectName}, 环境: ${profile}, 构建ID: ${buildId}" + script.echo "📁 部署路径: ${deployPath}" + script.echo "📜 部署脚本: ${deployScript}" + + try { + // 生成镜像标签(包含环境、构建ID和时间戳信息) + def imageTag = generateImageTag(profile) + script.echo "🏷️ 生成的镜像标签: ${imageTag}" + + // // 第一步:检查并创建远程部署路径 + // script.echo "📁 检查远程部署路径..." + // ensureRemotePathExists([ + // serverIP: serverIP, + // serverPort: serverPort, + // sshKey: sshKey, + // remotePath: deployPath, + // timeout: 60 + // ]) + + // 第二步:传输部署文件到远程服务器 + // 支持多文件和重命名功能,包括部署脚本和环境配置文件 + script.echo "📤 开始文件传输阶段..." + + // 构建文件传输列表 + def transferFiles = [ + // 部署脚本(保持原文件名) + [localPath: ".pipeline-files/scripts-file/${deployScript}", mode: "755"], + // 环境文件(重命名为 .env) + [localPath: ".pipeline-files/scripts-file/docker-env-${profile}.sh", remoteName: ".env", mode: "644"], + // Docker执行文件 + [localPath: ".pipeline-files/compose-file/${composeFile}.yml", remoteName: "docker-compose.yml"], + ] + transferDeploymentFiles([ + serverIP: serverIP, + serverPort: serverPort, + sshKey: sshKey, + remotePath: deployPath, + files: transferFiles, // 使用动态文件列表 + timeout: 300 + ]) + + // 第三步:设置远程文件执行权限 + // 确保部署脚本有可执行权限,避免"Permission denied"错误 + script.echo "🔧 开始权限设置阶段..." + setRemoteFilePermissions([ + serverIP: serverIP, + serverPort: serverPort, + sshKey: sshKey, + remotePath: deployPath, // 远程路径 + deployScript: deployScript, // 要设置权限的脚本文件 + timeout: 60 // 权限设置超时时间 + ]) + + // 第四步:构建部署命令 + // 使用bash显式执行脚本,双重保障避免权限问题 + script.echo "🔨 构建部署命令..." + def command = buildDeploymentCommand(deployPath, deployScript, projectName, imageTag, extraArgs) + script.echo "💻 部署命令已构建完成" + + // 第四步:执行远程部署命令(支持重试机制) + // 使用指数退避策略进行重试,提高部署成功率 + script.echo "⚡ 开始执行部署命令..." + def success = executeRemoteCommandWithRetry( + serverIP, + serverPort, + sshKey, + command, + timeout, + retryAttempts + ) + + // 部署成功日志 + script.echo "✅ 部署成功完成: ${projectName} 已部署到 ${serverIP}" + script.echo "🎯 镜像版本: ${imageTag}" + script.echo "📍 部署路径: ${deployPath}" + + // 第五步:生成部署摘要报告 + // 包含部署详情、时间戳、版本信息等 + script.echo "📊 生成部署摘要报告..." + generateDeploymentSummary(deployParams, imageTag, success) + + script.echo "🎉 部署流程全部完成" + + } catch (Exception e) { + // 部署失败处理 + script.echo "❌ 部署失败: ${e.message}" + script.echo "🔍 失败详情: ${e.getClass().getName()}" + script.echo "💡 建议检查网络连接、服务器状态和文件权限" + + // 重新抛出异常,让上层调用者处理 + throw new Exception("部署到服务器 ${serverIP} 失败: ${e.message}", e) + } + } + + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + private def generateImageTag(profile) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID + } + + /** + * 确保远程路径存在(最简版) + * 使用mkdir -p命令的原子性操作:路径存在时忽略,不存在时创建 + * 适用于大多数场景,代码更简洁 + * + * @param params 参数Map,包含以下字段: + * - serverIP: String - 远程服务器IP地址 + * - serverPort: Integer - SSH端口号 + * - sshKey: String - SSH私钥文件名称 + * - remotePath: String - 要确保存在的远程路径 + */ + def ensureRemotePathExists(params) { + // 输出开始操作的日志 + script.echo "🔍 确保远程路径存在: ${params.remotePath}" + + try { + // 参数解析和默认值设置 + def serverIP = params.serverIP // 服务器IP地址(必需) + def serverPort = params.serverPort ?: 22 // SSH端口,默认22 + def sshKey = params.sshKey ?: 'jenkins-ssh-205' // SSH凭证,默认jenkins-ssh-205 + def remotePath = params.remotePath // 远程目标路径(必需) + def timeout = params.timeout ?: 300 // 超时时间,默认300秒(5分钟) + + // 日志输出传输信息 + script.echo "📤 开始传输部署文件到服务器: ${serverIP}:${serverPort}" + script.echo "📋 传输参数:" + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + + // 使用Jenkins凭证管理安全地处理SSH密钥 + // withCredentials确保密钥在使用后自动清理,避免安全风险 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径存储变量 + usernameVariable: 'SSH_USERNAME' // 用户名存储变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 连接用户: ${username}" + script.echo " 🌐 目标主机: ${hostname}" + /** + * 执行SSH命令: + * mkdir -p: 创建目录,-p参数确保: + * - 如果目录已存在,不会报错 + * - 可以创建多级目录(如/a/b/c) + * - 会自动创建不存在的父目录 + * chmod 755: 设置目录权限为755 + */ + def sshCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + 'mkdir -p "${remotePath}" && chmod 755 "${remotePath}"' + """.trim() + + script.echo "🔧 执行SSH命令" + + // 执行SSH命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def exitCode = script.sh( + script: sshCommand, + returnStatus: true + ) + if (exitCode == 0) { + script.echo "✅ 远程路径创建成功: ${remotePath}" + } else { + throw new Exception("SSH命令返回非零状态码: ${exitCode}") + } + } + + // 成功完成后的日志输出 + script.echo "✅ 远程路径准备就绪: ${params.remotePath}" + } + } catch (Exception e) { + /** + * 异常处理: + * 可能的异常原因: + * - SSH连接失败(网络问题、服务器宕机) + * - 认证失败(密钥错误、权限不足) + * - 路径创建失败(磁盘空间不足、权限问题) + */ + script.echo "❌ 远程路径操作失败: ${e.message}" + throw new Exception("无法创建远程路径: ${params.remotePath}", e) + } + } + + /** + * 传输部署文件到远程服务器(支持多文件和重命名) + * 使用SCP将多个文件传输到目标服务器,支持自定义远程文件名和权限设置 + * 该方法提供了灵活的文件传输解决方案,适用于复杂的部署场景 + * + * @param scpParams SCP传输参数Map,包含以下字段: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - 服务器SSH端口(默认22) + * - sshKey: String - SSH密钥凭证ID(默认为jenkins-ssh-205) + * - remotePath: String - 远程服务器目标路径(必需) + * - timeout: Integer - 超时时间(秒,默认300) + * - files: List - 文件传输列表,每个文件包含配置信息: + * - localPath: String - 本地文件路径(必需) + * - remoteName: String - 远程文件名(可选,不提供则使用本地文件名) + * - mode: String - 文件权限模式(可选,如"755"、"644") + * + * @throws Exception 当SCP传输失败时抛出异常,包含详细的错误信息 + * + * @example + * // 高级用法:传输多个文件并重命名 + * transferDeploymentFiles([ + * serverIP: "user@192.168.124.126", + * serverPort: 12422, + * sshKey: "jenkins-ssh-205", + * remotePath: "/usr/local/docker/xiaomayi-web/", + * timeout: 600, + * files: [ + * [localPath: ".pipeline-files/scripts/deploy_env.sh", remoteName: "deploy.sh", mode: "755"], + * [localPath: ".pipeline-files/scripts/docker-env-test.sh", remoteName: ".env", mode: "644"], + * [localPath: ".pipeline-files/config/app-config.json", mode: "644"], + * [localPath: ".pipeline-files/certs/ssl.crt", remoteName: "server.crt"], + * [localPath: ".pipeline-files/certs/ssl.key", remoteName: "server.key"] + * ] + * ]) + */ + def transferDeploymentFiles(scpParams) { + // 参数解析和默认值设置 + def serverIP = scpParams.serverIP // 服务器IP地址(必需) + def serverPort = scpParams.serverPort ?: 22 // SSH端口,默认22 + def sshKey = scpParams.sshKey ?: 'jenkins-ssh-205' // SSH凭证,默认jenkins-ssh-205 + def remotePath = scpParams.remotePath // 远程目标路径(必需) + def timeout = scpParams.timeout ?: 300 // 超时时间,默认300秒(5分钟) + def files = scpParams.files ?: [] // 文件列表,默认为空列表 + + // 日志输出传输信息 + script.echo "📤 开始传输部署文件到服务器: ${serverIP}:${serverPort}" + script.echo "📋 传输参数:" + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + script.echo " 📊 文件数量: ${files.size()}" + + // 检查文件列表是否为空,避免不必要的操作 + if (files.isEmpty()) { + script.echo "⚠️ 没有文件需要传输,跳过文件传输阶段" + return // 直接返回,不执行后续操作 + } + + try { + // 使用Jenkins凭证管理安全地处理SSH密钥 + // withCredentials确保密钥在使用后自动清理,避免安全风险 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径存储变量 + usernameVariable: 'SSH_USERNAME' // 用户名存储变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 连接用户: ${username}" + script.echo " 🌐 目标主机: ${hostname}" + + // 遍历文件列表,逐个传输文件 + // eachWithIndex提供索引信息,用于进度显示 + files.eachWithIndex { fileConfig, index -> + // 解析单个文件的配置 + def localPath = fileConfig.localPath // 本地文件路径(必需) + // 远程文件名:如果未指定则使用本地文件名 + def remoteName = fileConfig.remoteName ?: getFileName(localPath) + def mode = fileConfig.mode // 文件权限模式(可选) + + // 输出传输进度信息 + script.echo " 📤 传输文件 (${index + 1}/${files.size()}): ${localPath} → ${remotePath}/${remoteName}" + if (mode) { + script.echo " 🔧 将设置权限: ${mode}" + } + + // 执行SCP文件传输命令 + executeSCPCommand( + localPath, // 本地文件路径 + "${username}@${hostname}:${remotePath}/${remoteName}", // 远程目标路径 + serverPort, // SSH端口 + sshKey, // SSH凭证 + timeout, // 超时时间 + 3 // 重试次数(默认3次) + ) + + // 如果指定了文件权限模式,设置远程文件权限 + if (mode) { + setRemoteFileMode(serverIP, serverPort, sshKey, "${remotePath}/${remoteName}", mode, timeout) + } + + script.echo " ✅ 文件传输完成: ${remoteName}" + } + } + + // 传输完成后的摘要信息 + script.echo "✅ 所有文件传输完成" + script.echo "💡 传输摘要:" + script.echo " =========================================" + files.eachWithIndex { fileConfig, index -> + def localPath = fileConfig.localPath + def remoteName = fileConfig.remoteName ?: getFileName(localPath) + def mode = fileConfig.mode ?: "默认权限" + script.echo " ${index + 1}. ${localPath} → ${remotePath}/${remoteName} (权限: ${mode})" + } + script.echo " =========================================" + + } catch (Exception e) { + // 错误处理:提供详细的错误信息和排查建议 + script.echo "❌ 文件传输失败: ${e.message}" + script.echo "🔍 错误类型: ${e.getClass().getSimpleName()}" + script.echo "💡 排查建议:" + script.echo " - 检查本地文件路径是否正确: ${files.collect{ it.localPath }}" + script.echo " - 验证远程目录是否存在且有写权限: ${remotePath}" + script.echo " - 确认SSH密钥凭证是否有效: ${sshKey}" + script.echo " - 检查网络连接和防火墙设置" + script.echo " - 验证目标服务器SSH服务是否正常运行" + + // 重新抛出异常,让上层调用者处理 + throw new Exception("文件传输到服务器 ${serverIP} 失败: ${e.message}", e) + } + } + + /** + * 从文件路径中提取文件名 + * 用于当未指定远程文件名时,自动使用本地文件名 + * + * @param filePath 文件完整路径(如:/path/to/file.txt 或 relative/path/file.sh) + * @return String 文件名(如:file.txt 或 file.sh) + * + * @example + * getFileName("/opt/apps/deploy.sh") → "deploy.sh" + * getFileName("config/app-config.json") → "app-config.json" + */ + private def getFileName(filePath) { + // 使用Java的File类来提取文件名,处理各种路径格式 + return new File(filePath).getName() + } + + /** + * 设置远程文件权限模式 + * 通过SSH连接到远程服务器执行chmod命令设置文件权限 + * + * @param serverIP 服务器IP地址 + * @param serverPort SSH端口 + * @param sshKey SSH密钥凭证ID + * @param remoteFilePath 远程文件完整路径 + * @param mode 权限模式(如:"755"、"644"、"600") + * @param timeout 超时时间(秒) + * + * @note 该方法设计为容错模式,权限设置失败不会阻止后续操作 + */ + private def setRemoteFileMode(serverIP, serverPort, sshKey, remoteFilePath, mode, timeout) { + try { + // 使用凭证管理确保SSH密钥安全 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, + keyFileVariable: 'SSH_KEY_FILE', + usernameVariable: 'SSH_USERNAME' + )]) { + // 解析服务器地址 + def (username, hostname) = parseServerAddress(serverIP) + + // 构建chmod命令:设置远程文件权限 + def chmodCommand = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=60 -p ${serverPort} -i ${script.SSH_KEY_FILE} ${username}@${hostname} 'chmod ${mode} \"${remoteFilePath}\"'" + + // 执行权限设置命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: chmodCommand, returnStatus: true) + if (result == 0) { + script.echo " ✅ 权限设置成功: ${remoteFilePath} → ${mode}" + } else { + script.echo "⚠️ 权限设置失败,但继续执行: ${remoteFilePath}" + } + } + } + } catch (Exception e) { + // 权限设置失败处理(容错模式) + script.echo "⚠️ 设置文件权限时出错: ${e.message},但继续执行后续操作" + script.echo " 💡 文件仍然可以传输,但可能需要手动设置权限" + } + } + + /** + * 执行SCP文件传输命令(增强版) + * 支持错误重试机制和详细的日志输出,提高传输可靠性 + * + * @param localFile 本地文件路径 + * @param remoteTarget 远程目标路径(格式: user@host:path) + * @param serverPort SSH端口号 + * @param sshKey SSH密钥凭证ID + * @param timeout 单次尝试超时时间(秒) + * @param retries 最大重试次数(默认3次) + * + * @throws Exception 当所有重试尝试都失败后抛出异常 + */ + private def executeSCPCommand(localFile, remoteTarget, serverPort, sshKey, timeout, retries = 3) { + def attempt = 1 + def lastError = null + + // 重试循环:尝试多次传输,提高成功率 + while (attempt <= retries) { + try { + script.echo " 🔄 SCP传输尝试 (${attempt}/${retries}): ${localFile}" + + // 构建SCP命令 + def scpCommand = """ + scp -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -P ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${localFile} \ + ${remoteTarget} + """.trim() + + // 执行SCP命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: scpCommand, returnStatus: true) + if (result != 0) { + throw new Exception("SCP命令返回非零状态码: ${result}") + } + } + + script.echo " ✅ SCP传输成功: ${localFile}" + return // 传输成功,退出方法 + + } catch (Exception e) { + lastError = e + script.echo "❌ SCP传输失败 (尝试 ${attempt}): ${e.message}" + + // 如果不是最后一次尝试,等待后重试 + if (attempt < retries) { + def waitTime = attempt * 5 // 指数退避策略:5, 10, 15秒... + script.echo "⏳ 等待 ${waitTime} 秒后重试..." + script.sleep(waitTime) // 等待一段时间 + } + attempt++ // 增加尝试次数 + } + } + + // 所有重试都失败,抛出异常 + throw new Exception("SCP传输失败,重试 ${retries} 次后仍失败: ${lastError?.message}", lastError) + } + + /** + * 设置远程文件执行权限 + * 确保部署脚本在远程服务器上有执行权限,避免"Permission denied"错误 + * 该方法会尝试设置权限,但即使失败也不会中断部署流程(会尝试使用bash执行) + * + * @param permissionParams 权限设置参数Map,包含以下字段: + * - serverIP: String - 服务器IP地址 + * - serverPort: Integer - 服务器SSH端口(默认22) + * - sshKey: String - SSH密钥凭证ID + * - remotePath: String - 远程服务器路径 + * - deployScript: String - 要设置权限的脚本文件名 + * - timeout: Integer - 超时时间(秒,默认60) + * + * @throws Exception 当SSH连接完全失败时抛出异常 + * + * @note 该方法设计为容错模式,权限设置失败不会阻止后续部署尝试 + */ + def setRemoteFilePermissions(permissionParams) { + // 参数解析和默认值设置 + def serverIP = permissionParams.serverIP + def serverPort = permissionParams.serverPort ?: 22 + def sshKey = permissionParams.sshKey ?: 'jenkins-ssh-205' + def remotePath = permissionParams.remotePath + def deployScript = permissionParams.deployScript + def timeout = permissionParams.timeout ?: 60 + + script.echo "🔧 设置远程文件执行权限..." + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 📜 脚本文件: ${deployScript}" + script.echo " 🔑 SSH凭证: ${sshKey}" + + try { + // 使用Jenkins凭证管理安全地处理SSH密钥 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径环境变量 + usernameVariable: 'SSH_USERNAME' // 用户名环境变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 用户名: ${username}" + script.echo " 🌐 主机名: ${hostname}" + + // ssh -o StrictHostKeyChecking=no \\ # 禁用严格主机密钥检查 + // -o ConnectTimeout=30 \\ # 连接超时30秒 + // -o ServerAliveInterval=60 \\ # 保持连接活跃间隔60秒 + // -p ${serverPort} \\ # SSH端口 + // -i ${script.SSH_KEY_FILE} \\ # SSH密钥文件路径 + // ${username}@${hostname} \\ # 目标服务器 + // 'cd ${remotePath} && \\ # 切换到部署目录 + // chmod +x ${deployScript} && \\ # 设置执行权限 + // ls -la ${deployScript}' # 验证权限设置结果 + + // 构建chmod命令:设置执行权限并验证 + def chmodCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + 'cd ${remotePath} && \ + chmod +x ${deployScript} && \ + ls -la ${deployScript}' + """.trim() + + script.echo " ⚡ 执行权限设置命令..." + + // 执行权限设置命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: chmodCommand, returnStdout: true, returnStatus: true) + + if (result == 0) { + script.echo "✅ 文件权限设置成功" + script.echo " 📋 文件权限已更新,可执行权限已添加" + } else { + script.echo "⚠️ 文件权限设置可能存在问题,但继续执行部署" + script.echo " 💡 将尝试使用bash命令执行脚本" + } + } + } + } catch (Exception e) { + // 权限设置失败处理(容错模式) + script.echo "⚠️ 设置文件权限时出错: ${e.message}" + script.echo " 💡 错误类型: ${e.getClass().getSimpleName()}" + script.echo " 🔄 继续执行部署,将使用bash命令执行脚本" + // 这里不抛出异常,因为即使权限设置失败,我们还可以尝试使用bash执行 + } + } + + /** + * 构建部署命令 + * 创建用于远程执行的部署命令字符串,使用bash显式执行脚本避免权限问题 + * + * @param deployPath 远程部署路径(如/usr/local/docker/xiaomayi-web/) + * @param deployScript 部署脚本文件名(如deploy_env.sh) + * @param projectName 项目名称(如xiaomayi-admin) + * @param imageTag 镜像标签(如prod-31) + * @param extraArgs 额外参数 + * + * @return String 完整的部署命令字符串 + * + * @note 使用bash显式执行脚本,即使脚本没有执行权限也能正常工作 + */ + private def buildDeploymentCommand(deployPath, deployScript, projectName, imageTag, extraArgs) { + script.echo " 🛠️ 构建部署命令..." + script.echo " 📁 路径: ${deployPath}" + script.echo " 📜 脚本: ${deployScript}" + script.echo " 🏷️ 项目: ${projectName}" + script.echo " 🎯 版本: ${imageTag}" + script.echo " ➕ 参数: ${extraArgs}" + + // 构建完整的部署命令 + // 使用bash显式执行脚本,避免"./script.sh"的权限问题 + def command = """ + export PROJECT_NAME=${projectName}; \ + export IMAGE_TAG=${imageTag}; \ + cd ${deployPath}; \ + ./${deployScript} deploy ${projectName} ${imageTag} ${extraArgs} + """.trim().replaceAll("\n", " ") // 去除换行符,转换为单行命令 + + script.echo " ✅ 命令构建完成" + return command + } + + /** + * 执行远程命令(带重试机制) + * 使用指数退避策略执行SSH命令,提高在网络不稳定情况下的成功率 + * + * @param serverIP 服务器IP地址 + * @param serverPort SSH端口 + * @param sshKey SSH凭证ID + * @param command 要执行的远程命令 + * @param timeout 单次执行超时时间(秒) + * @param retryAttempts 最大重试次数 + * + * @return Boolean 命令执行是否成功 + * @throws Exception 所有重试尝试都失败后抛出异常 + */ + private def executeRemoteCommandWithRetry(serverIP, serverPort, sshKey, command, timeout, retryAttempts) { + def attempt = 1 + def lastException = null + + script.echo " 🔄 启用重试机制,最大重试次数: ${retryAttempts}" + + // 重试循环 + while (attempt <= retryAttempts) { + try { + script.echo " 🎯 执行远程命令 (尝试 ${attempt}/${retryAttempts})..." + + // 使用Jenkins凭证安全地执行SSH命令 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, + keyFileVariable: 'SSH_KEY_FILE', + usernameVariable: 'SSH_USERNAME' + )]) { + // 解析服务器地址 + def (username, hostname) = parseServerAddress(serverIP) + + // // 构建SSH命令 + // def sshCommand = """ + // ssh -o StrictHostKeyChecking=no \\ # 禁用严格主机密钥检查 + // -o ConnectTimeout=30 \\ # 连接超时30秒 + // -o ServerAliveInterval=60 \\ # 保持连接活跃间隔60秒 + // -p ${serverPort} \\ # SSH端口 + // -i ${script.SSH_KEY_FILE} \\ # SSH密钥文件 + // ${username}@${hostname} \\ # 目标服务器 + // '${command}' # 要执行的命令 + // """.trim() + + // 构建SSH命令 + def sshCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + '${command}' + """.trim() + + script.echo " ⚡ 执行SSH命令..." + + // 执行命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def output = script.sh(script: sshCommand, returnStdout: true, returnStatus: false) + script.echo " 📋 命令输出:\n${output}" + } + } + + script.echo "✅ 远程命令执行成功" + return true + + } catch (Exception e) { + lastException = e + script.echo "❌ 第 ${attempt} 次尝试失败: ${e.message}" + + // 如果不是最后一次尝试,等待后重试 + if (attempt < retryAttempts) { + def waitTime = attempt * 10 // 指数退避:10, 20, 30秒... + script.echo "⏳ 等待 ${waitTime} 秒后重试..." + script.sleep(waitTime) // 等待一段时间 + } + attempt++ // 增加尝试次数 + } + } + + // 所有重试都失败,抛出异常 + throw new Exception("远程命令执行失败,重试 ${retryAttempts} 次后仍失败: ${lastException?.message}", lastException) + } + + /** + * 解析服务器地址 + * 支持 user@host 格式和单独的host格式,提供灵活的服务器地址解析 + * + * @param serverAddress 服务器地址字符串 + * @return List 包含[用户名, 主机名]的列表 + * + * @example + * parseServerAddress("user@example.com") → ["user", "example.com"] + * parseServerAddress("192.168.1.100") → ["root", "192.168.1.100"] (使用默认用户名) + */ + private def parseServerAddress(serverAddress) { + script.echo " 🔍 解析服务器地址: ${serverAddress}" + + if (serverAddress.contains('@')) { + // 处理 user@host 格式 + def parts = serverAddress.split('@') + def result = [parts[0], parts[1]] + script.echo " ✅ 解析结果: 用户名=${result[0]}, 主机名=${result[1]}" + return result + } else { + // 处理单独的host格式,使用SSH凭证中的用户名或默认root + def username = script.SSH_USERNAME ?: 'root' + def result = [username, serverAddress] + script.echo " ✅ 解析结果: 用户名=${result[0]}(默认), 主机名=${result[1]}" + return result + } + } + + /** + * 生成部署摘要报告 + */ + private def generateDeploymentSummary(deployParams, imageTag, success) { + def summary = """ + ==================== 部署摘要 ==================== + 项目名称: ${deployParams.projectName} + 目标环境: ${deployParams.profile} + 目标服务器: ${deployParams.serverIP}:${deployParams.serverPort} + 镜像标签: ${imageTag} + 构建ID: ${deployParams.buildId} + 部署状态: ${success ? '✅ 成功' : '❌ 失败'} + 时间: ${new Date().format('yyyy-MM-dd HH:mm:ss')} + ================================================ + """ + + script.echo summary + return summary + } + + /** + * 辅助方法:检查是否应该在部署失败时停止 + * 根据部署参数决定单个服务器失败时是否停止整个部署流程 + * + * @param params 部署参数Map + * @return boolean true-失败时停止,false-失败时继续 + */ + private def shouldStopOnFailure(params) { + // 生产环境通常应该失败时停止,其他环境可以继续 + def isProduction = params.PROFILES?.toLowerCase() == 'prod' + // 优先使用显式配置,如果没有配置则根据环境决定 + return params.stopOnFailure ?: isProduction + } + + /** + * 辅助方法:生成部署报告 + * 创建格式化的部署结果报告,便于查看和分析 + * + * @param results 部署结果Map,格式:["服务器名称": "状态描述"] + */ + private def generateDeploymentReport(results) { + script.echo "=" * 60 + script.echo "📊 多服务器部署结果报告" + script.echo "=" * 60 + + // 统计成功和失败数量 + def successCount = results.count { it.value == '成功' } + def failureCount = results.size() - successCount + + // 输出每台服务器的详细结果 + results.each { serverName, status -> + def emoji = status.startsWith('成功') ? '✅' : '❌' + script.echo "${emoji} ${serverName}: ${status}" + } + + script.echo "-" * 60 + // 输出统计信息 + script.echo "📈 统计: 成功 ${successCount}, 失败 ${failureCount}, 总计 ${results.size()}" + + // 根据结果输出不同的总结信息 + if (failureCount == 0) { + script.echo "🎉 所有服务器部署成功!" + } else if (successCount == 0) { + script.echo "💥 所有服务器部署失败!" + } else { + script.echo "⚠️ 部分服务器部署失败,请检查日志" + } + + script.echo "=" * 60 + } + +} \ No newline at end of file diff --git a/src/com/python/stages/HealthCheckService.groovy b/src/com/python/stages/HealthCheckService.groovy new file mode 100644 index 0000000..727231b --- /dev/null +++ b/src/com/python/stages/HealthCheckService.groovy @@ -0,0 +1,333 @@ +package com.python.stages + +/** + * 健康检查阶段服务 - 负责验证部署的应用是否正常运行 + * + * 主要职责: + * - 执行部署后的应用健康状态验证 + * - 监控服务可用性和功能性 + * - 在健康检查失败时触发自动回滚机制 + * - 提供详细的健康检查结果和诊断信息 + * + * 设计原则: + * - 单一职责:专注于健康检查相关的逻辑 + * - 安全性:健康检查失败时提供自动回滚保护 + * - 可配置性:支持灵活的健康检查配置和开关 + * - 错误隔离:健康检查失败不会影响其他服务实例 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + * @see rollbackService 依赖回滚服务处理健康检查失败的情况 + */ +class HealthCheckService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储健康检查环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 回滚服务实例 + * 负责在健康检查失败时执行回滚操作 + * 类型: RollbackService 回滚服务类 + */ + def rollbackService + + /** + * 构造函数 - 初始化健康检查服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:健康检查配置、回滚设置等 + * + * @note 自动初始化RollbackService实例用于健康检查失败时的回滚操作 + */ + HealthCheckService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + // 初始化回滚服务,用于健康检查失败时的自动回滚 + this.rollbackService = new RollbackService(script, envConfig) + + script.echo "🔧 HealthCheckService 初始化完成" + script.echo " ❤️ 回滚服务已就绪" + } + + /** + * 执行健康检查阶段逻辑 + * 负责验证部署的应用是否正常运行,是CI/CD流水线的质量保障阶段 + * + * 主要步骤: + * 1. 检查健康检查功能是否启用 + * 2. 确定要检查的目标服务器列表 + * 3. 对每个目标服务器执行健康检查 + * 4. 处理健康检查结果和异常情况 + * 5. 在失败时触发自动回滚机制 + * + * @param params 管道参数Map,包含健康检查所需的所有配置信息,通常包含: + * - serverHosts: List - 服务器列表(多服务器部署时使用) + * - serverIP01: String - 单个服务器IP(单服务器部署时使用) + * - port: Integer - 服务端口号(必须) + * 可选字段: + * - healthCheckPath: String - 健康检查端点路径(默认:/actuator/health) + * - healthCheckTimeout: Integer - 健康检查超时时间(秒) + * - healthCheckInterval: Integer - 健康检查间隔时间(秒) + * - successThreshold: Integer - 成功阈值(连续成功次数) + * - retryAttempts: Integer - 重试次数 + * + * @throws Exception 当健康检查失败时抛出异常,触发回滚机制 + * 异常信息包含健康检查失败的详细原因 + * + * @example 使用示例: + * def healthCheckParams = [ + * serverHosts: ["192.168.1.1", "192.168.1.2"], + * port: 8080, + * healthCheckPath: "/actuator/health", + * healthCheckTimeout: 30, + * retryAttempts: 3 + * ] + * healthCheckService.executeHealthCheckStage(healthCheckParams) + */ + def executeHealthCheckStage(params) { + try { + script.echo "❤️ 开始健康检查..." + script.echo "📋 健康检查配置:" + script.echo " 🚪 服务端口: ${params.serverPort}" + + // 输出健康检查路径(如果配置) + if (params.healthCheckPath) { + script.echo " 📍 健康检查端点: ${params.healthCheckPath}" + } + + // 检查是否启用健康检查功能 + // 支持通过环境配置动态启用/禁用健康检查 + def healthCheckEnabled = envConfig.deployment?.health_check_enabled ?: true + script.echo " ⚙️ 健康检查功能: ${healthCheckEnabled ? '已启用' : '已禁用'}" + + if (healthCheckEnabled) { + // 确定要检查的目标服务器列表 + // 支持多服务器部署和单服务器部署两种模式 + def targets = params.serverHosts ?: [params.serverIP01] + script.echo " 📡 目标服务器数量: ${targets.size()}" + + // 对每个目标服务器执行健康检查 + targets.each { serverIP -> + if (serverIP) { + script.echo " 🔍 对服务器 ${serverIP}:${params.serverPort} 进行健康检查..." + + // 使用healthCheck方法执行实际健康检查 + // 该方法会处理HTTP请求、超时控制、重试机制等 + healthCheck(serverIP, params.port) + } + } + + script.echo "✅ 所有服务器健康检查通过" + } else { + script.echo "⏭️ 健康检查功能已禁用,跳过检查" + } + + script.echo "❤️ 健康检查完成" + + } catch (Exception e) { + // 健康检查失败处理 + script.echo "❌ 健康检查失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - 应用是否正常启动" + script.echo " - 服务端口是否正确暴露" + script.echo " - 健康检查端点是否可以访问" + script.echo " - 网络连接是否正常" + script.echo " - 应用日志是否有错误信息" + + // 健康检查失败时尝试回滚部署 + // 回滚功能可以通过配置启用或禁用 + def rollbackEnabled = envConfig?.rollbackEnabled ?: true + if (rollbackEnabled) { + script.echo "🔄 健康检查失败,触发自动回滚..." + rollbackService.executeRollbackStage(params) + } else { + script.echo "⏭️ 自动回滚功能已禁用,跳过回滚" + } + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 执行应用健康检查 + * 通过HTTP端点检查目标服务的健康状态,支持超时控制和重试机制 + * 用于部署后验证应用是否正常启动和运行 + * + * @param serverIP 服务器IP地址 + * - 类型: String + * - 格式: IP地址或主机名(不需要http://前缀) + * - 示例: "192.168.1.100", "example.com", "localhost" + * - 必填: 是 + * + * @param port 服务端口号 + * - 类型: Integer + * - 范围: 1-65535 + * - 示例: 8080, 80, 443 + * - 必填: 是 + * + * @param timeoutMinutes 超时时间(分钟) + * - 类型: Integer + * - 默认值: 5分钟(优先使用参数值,其次环境配置,最后默认5分钟) + * - 建议: 生产环境建议设置较长超时(10-15分钟),开发环境可设置较短(3-5分钟) + * - 可选: 是 + * + * @throws Exception 当健康检查超时或连续失败时抛出异常 + * + * @note 使用Spring Boot Actuator的健康检查端点(/actuator/health) + * @note 支持自动重试机制,直到服务健康或超时 + * @note 包含详细的日志输出和错误诊断信息 + * + * @example 使用示例: + * // 基本使用 + * healthCheck("192.168.1.100", 8080) + * + * // 自定义超时 + * healthCheck("192.168.1.100", 8080, 10) + * + * // 生产环境检查 + * healthCheck("prod-server.example.com", 8080, 15) + */ + def healthCheck(serverIP, port, timeoutMinutes = null) { + // ========================================================================== + // 参数处理和配置初始化 + // ========================================================================== + + // 确定超时时间(优先级: 参数 > 环境配置 > 默认值) + def timeout = timeoutMinutes ?: envConfig?.healthCheckTimeout ?: 5 + // 健康检查端点路径(Spring Boot Actuator标准端点) + def healthEndpoint = "/actuator/health" + // 构建完整的健康检查URL + def healthCheckUrl = "http://${serverIP}:${port}${healthEndpoint}" + + script.echo "❤️ 开始健康检查..." + script.echo "📋 检查配置:" + script.echo " 📍 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 🔄 检查机制: 自动重试直到健康或超时" + + // ========================================================================== + // 参数验证 + // ========================================================================== + + // 验证服务器IP地址 + if (!serverIP) { + throw new IllegalArgumentException("服务器IP地址不能为空") + } + + // 验证端口范围 + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("端口号必须在1-65535范围内,当前: ${port}") + } + + // 验证超时时间 + if (timeout < 1) { + script.echo "⚠️ 超时时间过短(${timeout}分钟),使用最小值1分钟" + timeout = 1 + } + + // ========================================================================== + // 健康检查执行 + // ========================================================================== + + try { + // 设置整体超时(防止健康检查无限期等待) + script.timeout(time: timeout, unit: 'MINUTES') { + // 使用waitUntil实现自动重试机制 + script.waitUntil { + try { + script.echo "🔍 执行健康检查请求..." + + // 执行HTTP健康检查(使用curl命令) + // -s: 静默模式,不显示进度信息 + // -o /dev/null: 将响应体输出到空设备 + // -w '%{http_code}': 只输出HTTP状态码 + // --connect-timeout 5: 连接超时5秒 + // --max-time 10: 最大请求时间10秒 + def response = script.sh( + script: "curl -s -o /dev/null -w '%{http_code}' '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + + script.echo "📊 健康检查响应: HTTP ${response}" + + // 记录详细的响应信息(用于调试) + if (response != "200") { + // 非200响应时获取更多信息 + try { + def detailResponse = script.sh( + script: "curl -s '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + script.echo "📝 详细响应: ${detailResponse.take(200)}..." // 限制输出长度 + } catch (Exception detailError) { + script.echo "⚠️ 获取详细响应失败: ${detailError.message}" + } + } + + // 返回检查结果(200表示健康) + return response == "200" + + } catch (Exception e) { + // 记录检查失败信息 + script.echo "❌ 健康检查请求失败: ${e.message}" + script.echo "💡 可能的原因:" + script.echo " - 应用尚未完全启动" + script.echo " - 网络连接问题" + script.echo " - 防火墙或安全组限制" + script.echo " - 服务配置错误" + + return false // 继续重试 + } + } + } + + // 健康检查成功 + script.echo "✅ 健康检查成功!服务状态: HEALTHY" + script.echo "🎉 应用已正常启动并运行在 ${serverIP}:${port}" + script.echo "💡 建议: 继续执行后续部署验证步骤" + + } catch (Exception e) { + // ========================================================================== + // 健康检查失败处理 + // ========================================================================== + + script.echo "❌ 健康检查失败!服务状态: UNHEALTHY" + script.echo "📋 失败详情:" + script.echo " 🎯 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 💥 错误信息: ${e.message}" + + script.echo "🔧 故障排除建议:" + script.echo " 1. 📡 检查网络连通性: ping ${serverIP}" + script.echo " 2. 🚪 检查端口访问: telnet ${serverIP} ${port} || nc -zv ${serverIP} ${port}" + script.echo " 3. 📋 检查应用日志: docker logs 或 journalctl -u " + script.echo " 4. 🔍 手动验证: curl -v ${healthCheckUrl}" + script.echo " 5. ⚙️ 检查应用配置: 确认actuator端点已启用" + script.echo " 6. 🐛 查看启动错误: 检查应用启动时的异常信息" + script.echo " 7. 📊 检查资源使用: 内存、CPU、磁盘空间是否充足" + + // 重新抛出异常,让调用方能够处理健康检查失败 + throw new Exception("健康检查失败: 服务未在${timeout}分钟内达到健康状态", e) + } + } + +} \ No newline at end of file diff --git a/src/com/python/stages/RollbackService.groovy b/src/com/python/stages/RollbackService.groovy new file mode 100644 index 0000000..eabf4a2 --- /dev/null +++ b/src/com/python/stages/RollbackService.groovy @@ -0,0 +1,325 @@ +package com.python.stages + +import com.utils.ServerParser + +/** + * 回滚阶段服务 - 负责在部署失败时执行回滚操作 + * + * 主要职责: + * - 在部署失败或健康检查失败时执行自动回滚 + * - 将应用回退到上一个稳定版本 + * - 处理回滚过程中的异常和错误 + * - 提供回滚结果的状态反馈和报警通知 + * + * 设计原则: + * - 安全性:确保回滚操作不会造成二次故障 + * - 可靠性:回滚操作本身需要高度可靠 + * - 可追溯性:记录详细的回滚日志和操作记录 + * - 优雅降级:回滚失败时提供明确的处理指引 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class RollbackService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储回滚环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 服务器解析器实例 + * 负责解析服务器引用配置和索引处理 + * 类型: ServerParser 自定义服务器解析工具类 + */ + def serverParser + + /** + * 构造函数 - 初始化回滚服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:回滚策略、超时设置、通知配置等 + */ + RollbackService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + // 初始化服务器解析器,用于处理服务器引用和索引 + this.serverParser = new ServerParser(script) + + script.echo "🔧 RollbackService 初始化完成" + script.echo " 🔄 回滚服务就绪" + } + + /** + * 执行回滚操作 + * 在部署或健康检查失败时回滚到上一个可用版本,是CI/CD流水线的安全机制 + * + * 主要步骤: + * 1. 输出回滚开始信息和配置详情 + * 2. 确定要回滚的目标服务器列表 + * 3. 对每个目标服务器执行回滚操作 + * 4. 处理回滚结果和异常情况 + * 5. 提供回滚完成状态和后续处理指引 + * + * @param params 管道参数Map,包含回滚所需的所有配置信息,通常包含: + * - serverHosts: List - 需要回滚的服务器列表(多服务器部署时使用) + * - serverIP01: String - 单个服务器IP(单服务器部署时使用) + * - serverPort: Integer - 服务器SSH端口号(必须) + * - projectName: String - 项目名称(必须) + * 可选字段: + * - previousVersion: String - 上一个版本号(用于指定回滚版本) + * - rollbackTimeout: Integer - 回滚操作超时时间(秒) + * - rollbackStrategy: String - 回滚策略(如:immediate、graceful) + * - notifyOnRollback: Boolean - 是否发送回滚通知 + * + * @note 回滚操作是安全机制,应该尽可能确保其可靠性 + * @note 回滚失败时需要明确的手动处理指引 + * + * @example 使用示例: + * def rollbackParams = [ + * serverHosts: ["192.168.1.1", "192.168.1.2"], + * serverPort: 22, + * projectName: "xiaomayi-admin", + * previousVersion: "v1.2.0", + * rollbackTimeout: 300 + * ] + * rollbackService.executeRollbackStage(rollbackParams) + */ + private def executeRollbackStage(params) { + try { + script.echo "🔄 开始回滚操作..." + script.echo "📋 回滚配置:" + script.echo " 📦 项目名称: ${params.projectName}" + + // 输出回滚版本信息(如果指定) + if (params.previousVersion) { + script.echo " 📋 回滚版本: ${params.previousVersion}" + } else { + script.echo " 📋 回滚版本: 上一个稳定版本(自动检测)" + + // 获取上一个稳定版本 + params.previousVersion = getPreviousStableVersion() + } + + // 确定要回滚的目标服务器列表 + // 支持多服务器部署和单服务器部署两种模式 + def targetServers = params.targetServers + script.echo " 📡 目标服务器数量: ${targetServers.size()}" + script.echo " 🖥️ 目标服务器列表: ${targetServers.join(', ')}" + + // 对每个目标服务器执行回滚操作 + targetServers.each { server -> + if (server) { + script.echo " 🔄 正在回滚服务器: ${server.serverName} (${server.serverIP}:${server.serverPort}" + + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def rollbackParams = [ + serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + serverPort : server.serverPort, // SSH端口号(默认22) + serverName : server.serverName, // 服务器名称标识,用于日志和监控 + sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + timeout : params.deploymentTimeout, // 部署超时时间 + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + ] + + // 该方法会处理SSH连接、回滚命令执行、版本切换等 + rollbackDeployment(rollbackParams) + } + } + + script.echo "✅ 回滚操作完成" + script.echo "🎉 应用已成功回退到稳定状态" + script.echo "💡 建议: 检查日志确定部署失败原因后再重新部署" + + // 发送回滚通知 + sendRollbackNotification() + + } catch (Exception rollbackError) { + // 回滚操作失败处理 + script.echo "💥 回滚操作失败: ${rollbackError.message}" + script.echo "🚨 需要手动介入处理" + script.echo "💡 手动处理步骤:" + script.echo " 1. 登录服务器检查应用状态" + script.echo " 2. 查看应用日志确定问题原因" + script.echo " 3. 手动执行回滚命令或修复问题" + script.echo " 4. 验证应用是否恢复正常" + script.echo " 5. 联系开发团队分析根本原因" + + // 输出具体的回滚命令示例,便于手动操作 + script.echo "🔧 手动回滚命令示例:" + script.echo " ssh user@${targets[0]} 'cd /app && ./rollback.sh'" + + // 这里不重新抛出异常,因为回滚失败已经是最终状态 + // 需要人工介入处理,避免无限循环的错误处理 + } + } + + /** + * 获取上一个稳定版本的方法示例 + */ + def getPreviousStableVersion() { + // 这里可以实现从版本管理系统中获取上一个稳定版本的逻辑 + // 例如:从Git标签、Docker镜像标签、版本文件中获取 + try { + def previousVersion = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim() + echo "📋 检测到上一个稳定版本: ${previousVersion}" + return previousVersion + } catch (Exception e) { + echo "⚠️ 无法自动检测上一个版本,使用默认回滚策略" + return null // 返回null让工具类使用默认回滚逻辑 + } + } + + /** + * 发送回滚通知的方法示例 + */ + def sendRollbackNotification() { + echo "📧 发送回滚通知..." + // 这里可以实现邮件、Slack、钉钉等通知逻辑 + // 例如: + // slackSend channel: '#deployments', message: "🚨 部署失败已触发回滚" + // emailext subject: '部署回滚通知', body: '应用部署失败已自动回滚' + } + + /** + * 执行回滚部署操作 + * 负责将指定项目回退到上一个稳定版本,支持远程服务器操作和完整的错误处理 + * + * @param rollbackParams 回滚参数Map,包含以下字段: + * - timeout: Integer - 操作超时时间(秒) + * - projectName: String - 项目名称(必须) + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - retryAttempts: Integer - 重试次数 + * + * @throws Exception 当回滚操作失败时抛出异常,包含详细的错误信息和诊断建议 + * + * @note 该方法依赖于远程服务器上已部署的回滚脚本和正确的SSH配置 + * @note 支持重试机制,提高在网络不稳定情况下的成功率 + */ + def rollbackDeployment(rollbackParams) { + // ========================================================================== + // 参数提取和解构 + // ========================================================================== + + // 从服务器配置中提取IP地址(支持user@host格式) + def serverIP = rollbackParams.serverIP + // 从服务器配置中提取SSH端口号(默认22) + def serverPort = rollbackParams.serverPort + // 从服务器配置中提取SSH密钥凭证ID(Jenkins中配置的凭证名称) + def sshKey = rollbackParams.sshKey + // 从回滚参数中提取操作超时时间(秒) + def timeout = rollbackParams.timeout + // 从回滚参数中提取项目名称(用于标识要回滚的具体应用) + def projectName = rollbackParams.projectName + // 从回滚参数中提取部署脚本文件名(默认:deploy.sh) + def deployScript = rollbackParams.deployScript + // 从回滚参数中提取部署脚本所在路径 + def deployPath = rollbackParams.deployPath + // 从回滚参数中提取重试次数(默认:3次) + def retryAttempts = rollbackParams.retryAttempts ?: 3 + + // ========================================================================== + // 回滚信息输出和验证 + // ========================================================================== + + // 输出回滚操作开始信息,便于调试和审计追踪 + script.echo "🔄 执行回滚操作:" + script.echo " 📍 目标服务器: ${serverIP}:${serverPort}" + script.echo " 📦 项目名称: ${projectName}" + script.echo " 📁 脚本路径: ${deployPath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + script.echo " 🔄 重试次数: ${retryAttempts}次" + + // 验证必要参数是否存在 + if (!serverIP) { + throw new IllegalArgumentException("缺少服务器IP地址参数") + } + if (!projectName) { + throw new IllegalArgumentException("缺少项目名称参数") + } + + try { + // ========================================================================== + // 回滚命令构建 + // ========================================================================== + + // 构建回滚命令字符串 + // 格式: cd {部署脚本路径}; ./{部署脚本} rollback {项目名称} + def command = "cd ${deployPath}; ./${deployScript} rollback ${projectName}" + script.echo " 🖥️ 回滚命令: ${command}" + + // ========================================================================== + // 远程命令执行 + // ========================================================================== + + // 执行远程回滚命令,支持重试机制 + // executeRemoteCommandWithRetry 方法处理SSH连接、命令执行和重试逻辑 + def success = executeRemoteCommandWithRetry( + serverIP, // 目标服务器地址 + serverPort, // SSH端口号 + sshKey, // SSH密钥凭证 + command, // 要执行的命令 + timeout, // 超时时间 + retryAttempts // 重试次数 + ) + + // ========================================================================== + // 成功处理 + // ========================================================================== + + // 回滚成功日志输出 + script.echo "✅ 回滚成功完成: ${projectName} 已回退到上一个稳定版本" + script.echo "💡 建议: 检查应用日志确认回滚状态" + script.echo " 📋 验证步骤:" + script.echo " 1. 检查应用进程状态" + script.echo " 2. 查看应用启动日志" + script.echo " 3. 验证服务健康状态" + script.echo " 4. 确认业务功能正常" + + return success + + } catch (Exception e) { + // ========================================================================== + // 失败处理 + // ========================================================================== + + // 回滚失败错误信息输出 + script.echo "❌ 回滚失败: ${projectName} - ${e.message}" + script.echo "💡 故障排除建议:" + script.echo " 1. 🔍 检查服务器SSH连接是否正常" + script.echo " 2. 📁 确认部署脚本是否存在且可执行" + script.echo " 3. 🏷️ 验证项目名称是否正确" + script.echo " 4. 💾 检查服务器磁盘空间和权限" + script.echo " 5. 🔑 验证SSH密钥凭证是否有效" + script.echo " 6. 🌐 检查网络连接和防火墙设置" + script.echo " 7. ⏱️ 确认超时时间设置是否合理" + + // 重新抛出异常,让调用方能够处理回滚失败 + // 保留原始异常信息以便于调试和根因分析 + throw new Exception("回滚操作失败: ${e.message}", e) + } + } + +} \ No newline at end of file diff --git a/vars/pipelineGolangService.groovy b/vars/pipelineGolangService.groovy index e69de29..a08dfb4 100644 --- a/vars/pipelineGolangService.groovy +++ b/vars/pipelineGolangService.groovy @@ -0,0 +1,364 @@ +// 导入在 Pipeline 中需要使用的共享库类 +// 这些类通常在 Jenkins 的共享库 (Shared Libraries) 中定义,提供了可重用的功能 +import com.golang.ResolverService +import com.golang.DeploymentService +import com.common.StageService +import com.common.NotificationService + +/** + * 主管道入口点 + * 提供完整的CI/CD流水线功能,实现从配置加载到部署的全流程自动化 + * 这是一个 Jenkins 共享库 (Shared Library) 中的全局变量 (Global Variable) 或直接在 Pipeline 脚本中定义的方法 + * + * @param orgName 组织名称,用于标识不同的组织配置和环境。这是调用此流水线时必须传入的主要参数。 + * + * 功能特性: + * - 动态配置加载:支持根据 orgName 加载多环境配置管理 + * - 多阶段流水线:清晰的构建、测试、部署、验证流程 + * - 错误恢复机制:完善的异常处理和回滚功能 (虽然在当前代码示例中未完全展示,但框架支持) + * - 资源清理:自动清理工作空间和临时资源,避免磁盘空间耗尽 + * - 通知机制:构建结果通知(成功、失败、中止、不稳定) + * + * 示例用法: + * @Library ('share-library@master')_ + * orgName = 'xiaomayi' + * pipelineJDKService(orgName) + */ +def call(String orgName) { // Jenkins Pipeline 的入口方法通常命名为 call + // 定义管道参数变量,用于在不同阶段间传递配置信息。初始化为 null。 + def pipelineParams = null + // 声明服务类变量,将在后续阶段初始化 + def resolverService = null // 配置解析服务实例 + def deploymentService = null // 部署服务实例 + def stageService = null // 阶段服务实例 + def notificationService = null // 通知服务实例 + // Jenkins代理标签,指定流水线特定阶段在哪个代理节点上运行 + def jenkinsAgent = '' + + // 启动流水线定义。这是 Declarative Pipeline 的语法开始 + pipeline { + // 指定流水线执行代理,any表示可在任何可用代理上运行。 + // 注意:这里定义的 agent 会被后面 stage 中定义的 agent 覆盖。 + agent any + + // 流水线全局配置选项块 + options { + // 构建历史保留策略,用于管理 Jenkins 服务器磁盘空间 + buildDiscarder(logRotator( + artifactDaysToKeepStr: '', // 构建产物保留天数(空字符串表示不按天数限制,可能使用数量限制) + artifactNumToKeepStr: '2', // 保留最近2个构建的产物(如生成的jar包、docker镜像等) + daysToKeepStr: '7', // 构建记录元数据(日志、状态等)保留7天 + numToKeepStr: '3' // 保留最近3个构建记录(无论天数) + )) + // 全局超时设置:整个流水线最多运行2小时,超时则自动终止,防止无限运行 + timeout(time: 2, unit: 'HOURS') + // 还可以添加其他选项,例如: disableConcurrentBuilds() 防止并行运行等 + } + + // 流水线阶段定义块,包含按顺序执行的一系列阶段 (stages) + stages { + /** + * 第一阶段:初始化配置和环境 + * 加载环境配置、解析参数、初始化服务实例。 + * 此阶段仍在 pipeline 顶部定义的 agent any 上运行。 + */ + stage('Setup Parameters & ENV') { + steps { + // 执行Git检出操作 (checkout SCM),获取流水线配置文件或所需的配置仓库 + checkout scmGit([ // scmGit 是一种 checkout 方式 + // 指定要检出的分支,这里检出 master 分支 + branches: [[name: '*/master']], + // 配置远程仓库信息,包括认证和URL + userRemoteConfigs: [[ + credentialsId: 'c9b5eae2-5df5-485f-bebd-9cd5393b03e1', // 存储在 Jenkins 中的 Git 认证凭据ID + url: 'http://192.168.10.102:3000/cicd/jenkins-pipeline-files.git' // 配置仓库的URL地址 + ]], + // 扩展配置:指定检出到非默认工作目录的子目录中,避免与后续可能检出的应用代码冲突 + extensions: [[ + $class: 'RelativeTargetDirectory', // Jenkins checkout 扩展的类名,用于设置相对目标目录 + relativeTargetDir: '.pipeline-files' // 将配置仓库检出到工作空间下的 .pipeline-files 目录 + ]], + ]) + script { + echo "=== 开始初始化配置 ===" + echo "组织名称: ${orgName}" // 打印传入的组织名称参数 + try { // 使用 try-catch 进行异常处理,确保配置失败时能妥善处理 + // 初始化阶段服务实例,this 代表当前的流水线脚本对象,用于访问 Jenkins 步骤 + stageService = new StageService(this) + // 记录 '初始化配置' 阶段的开始时间 + stageService.recordStageStart('初始化配置') + + // 初始化配置解析服务实例,传入当前脚本对象和组织名称 + resolverService = new ResolverService(this, orgName) + // 调用解析服务的方法,加载所有配置文件(如环境相关的配置和主配置) + resolverService.loadConfigurations() + // 调用解析服务的方法,解析加载的配置并设置管道运行所需的参数集合 + // 返回的 pipelineParams 可能包含 Maven 设置、镜像仓库地址、目标服务器配置、环境变量等 + pipelineParams = resolverService.setupParameters() + + // 处理环境 profile 选择逻辑 + // params.PROFILES 可能是一个通过 Jenkins 输入参数(parameters {} 块)传入的值 + // 如果用户选择了 profile,则使用它;否则使用配置文件中定义的默认 profile (pipelineParams.PROFILES) + def selectedProfile = params.PROFILES ?: pipelineParams.PROFILES + pipelineParams.PROFILES = selectedProfile // 将最终确定的 profile 设置回参数Map中 + + // 设置后续构建和部署阶段使用的 Jenkins 代理标签 + // 优先使用配置文件中指定的代理标签,如果未配置则默认使用 'master' 节点(Jenkins Controller) + jenkinsAgent = pipelineParams?.jenkinsAgent ?: 'master' + echo "本次使用代理节点: ${jenkinsAgent}" + + // 初始化部署服务实例,传入当前脚本对象和从配置中获取的环境配置 + deploymentService = new DeploymentService(this, pipelineParams.envConfig) + + // 初始化消息通知服务实例,传入当前脚本对象、管道参数和阶段服务实例 + notificationService = new NotificationService(this, pipelineParams, stageService) + + // ✅ 关键步骤:将配置文件 stash 起来 + stash name: 'pipeline-files', includes: '.pipeline-files/**' + + // 记录 '初始化配置' 阶段的成功结束 + stageService.recordStageEnd('初始化配置', 'SUCCESS') + + } catch (Exception e) { // 捕获初始化过程中可能抛出的任何异常 + // 打印错误信息到流水线日志 + echo "配置初始化失败: ${e.message}" + // 记录 '初始化配置' 阶段的失败结束 + stageService.recordStageEnd('初始化配置', 'FAILURE') + // 抛出错误,终止整个流水线的执行,并标记为 FAILURE 状态 + error("管道初始化失败: " + e.getMessage()) + } + + echo "=== 结束初始化配置 ===" + } + } + } + + /** + * 第二阶段:代码构建 + * 执行Maven构建(编译、测试、打包),生成可部署的JAR包和Docker镜像。 + * 此阶段在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行,以分担主节点负载或使用特定环境。 + */ + stage('Build') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent // 使用前面计算得到的代理标签 + } + steps { + script { + echo "=== 🔨 开始代码构建 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '构建镜像' 阶段的开始时间 + stageService.recordStageStart('构建镜像') + // 调用部署服务的方法,执行具体的构建逻辑 + // 该方法内部可能包含:mvn clean compile/test/package、docker build/push 等步骤 + deploymentService.executeBuildStage(pipelineParams) + // 记录 '构建镜像' 阶段的成功结束 + stageService.recordStageEnd('构建镜像', 'SUCCESS') + echo "=== 🔨 完成代码构建 ===" + } + } + } + + /** + * 第三阶段:容器构建阶段 + * 负责将JAR包构建为Docker镜像,并推送到镜像仓库 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Container Build') { + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🐳 开始容器构建 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '应用部署' 阶段的开始时间 + stageService.recordStageStart('容器构建') + // 调用部署服务的方法,执行具体的容器构建逻辑 + deploymentService.executeContainerBuildStage(pipelineParams) + // 记录 '应用部署' 阶段的成功结束 + stageService.recordStageEnd('容器构建', 'SUCCESS') + echo "=== 🚀 完成容器构建 ===" + } + } + } + + /** + * 第四阶段:应用部署 + * 将Docker镜像部署到目标服务器(如测试、预生产、生产环境)。 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Deploy') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🚀 开始应用部署 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '应用部署' 阶段的开始时间 + stageService.recordStageStart('应用部署') + // 调用部署服务的方法,执行具体的部署逻辑 + // 该方法内部可能包含:通过 SSH 连接到目标服务器、执行部署脚本、更新容器(docker compose/k8s)等 + deploymentService.executeDeployStage(pipelineParams) + // 记录 '应用部署' 阶段的成功结束 + stageService.recordStageEnd('应用部署', 'SUCCESS') + echo "=== 🚀 完成应用部署 ===" + } + } + } + + /** + * 第五阶段:健康检查 + * 负责验证部署的应用是否正常运行,确保部署的服务的可用性和功能性 + * 这是CI/CD流程中的重要质量关卡,防止有缺陷的部署进入生产环境 + */ + stage('Health Check') { + agent { + // 使用配置文件中指定的代理标签,如果未配置则默认使用'master'节点 + // 健康检查可能需要访问部署环境网络或使用特定工具 + label jenkinsAgent + } + steps { + script { + echo "=== 🏥 开始健康检查 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录健康检查阶段的开始时间和状态 + stageService.recordStageStart('健康检查') + + try { + // 调用部署服务的健康检查方法,执行具体的验证逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 🏗️ 以下是健康检查可能包含的具体操作(示例): + + // 1. 🔗 服务端点连通性测试 + // - 检查应用的健康检查端点(如:/actuator/health)是否返回200状态码 + // - 验证关键API接口是否可访问 + + // 2. 📊 应用状态验证 + // - 检查应用日志是否有启动错误 + // - 验证数据库连接是否正常建立 + // - 确认必要的服务依赖(如Redis、MQ等)是否连接成功 + + // 3. 🔍 功能冒烟测试 + // - 执行一组最基本的业务操作测试,确保核心功能正常 + // - 验证配置是否正确加载(如配置文件、环境变量等) + + // 4. 📈 性能基线检查 + // - 检查应用启动时间是否在可接受范围内 + // - 验证内存占用、线程数等指标是否正常 + + // 5. 🐳 容器运行状态检查 + // - 确认Docker容器处于Running状态 + // - 检查容器资源使用情况(CPU、内存) + + // 调用部署服务的健康检查阶段逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 记录健康检查阶段的成功结束 + stageService.recordStageEnd('健康检查', 'SUCCESS') + + } catch (Exception e) { + // 健康检查失败时的处理 + echo "健康检查失败: ${e.message}" + // 记录健康检查阶段的失败状态 + stageService.recordStageEnd('健康检查', 'FAILURE') + // 可以选择抛出错误使整个流水线失败,或者标记为UNSTABLE + error("健康检查未通过,部署可能存在风险: " + e.getMessage()) + } + + echo "=== 🏥 完成健康检查 ===" + } + } + } + + // 可以在此处添加更多阶段,例如:自动化测试、集成测试、性能测试、安全扫描等 + } + + /** + * 流水线后处理块 (post section) + * 根据流水线的最终状态执行不同的操作,无论构建成功与否都会执行的处理逻辑。 + * 此部分在 pipeline 顶部定义的 agent any 上运行。 + */ + post { + /** + * always:无论构建结果如何(成功、失败、中止、不稳定)都会执行 + * 主要用于资源清理和状态报告等必须完成的操作 + */ + always { + echo "=== 管道执行完成 ===" + echo "完成时间: ${new Date()}" // 打印流水线结束的时间 + // 清理工作空间,删除当前流水线运行产生的所有文件,释放磁盘空间 + cleanWs() + script { + echo "构建完成,状态: ${currentBuild.currentResult}" // 打印最终构建状态 + // 调用阶段服务的方法,收集所有阶段的执行信息(开始时间、持续时间、状态),可能用于通知或报告 + stageService.collectAllStagesInfo() + } + } + + /** + * success:只有当整个流水线的状态为 SUCCESS 时执行 + * 主要用于成功通知和结果报告 + */ + success { + echo "✅ 部署成功!" + script { + // 调用通知服务的方法,发送构建成功的通知(如邮件、钉钉、Slack等) + notificationService.sendBuildSuccess() + } + } + + /** + * failure:只有当整个流水线的状态为 FAILURE 时执行 + * 主要用于错误报告、通知和可能的清理或回滚操作(虽然此处未展示回滚) + */ + failure { + echo "❌ 部署失败!" + script { + // 调用通知服务的方法,发送构建失败的通知,并传递错误信息 + // 这里的 errorMsg 和 extraInfo 是示例,实际可能从捕获的异常中获取 + notificationService.sendBuildFailure( + errorMsg: "单元测试失败", // 示例错误消息 + extraInfo: [stage: "test", errorDetails: "具体错误信息"] // 示例额外信息 + ) + } + } + + /** + * aborted:只有当整个流水线的状态为 ABORTED 时执行(通常由用户手动中止) + * 用于通知构建被中止的情况 + */ + aborted { + echo "❌ 构建中止!" + script { + // 调用通知服务的方法,发送构建中止的通知 + notificationService.sendBuildAborted( + // 传递一些额外信息,例如中止原因和操作人(如果环境变量可用) + extraInfo: [中止原因: '手动中止', 操作人: env.USER_ID] + ) + } + } + + /** + * unstable:只有当整个流水线的状态为 UNSTABLE 时执行(通常由测试失败或有警告导致) + * 主要用于警告通知 + */ + unstable { + echo "⚠️ 管道执行不稳定" + script { + // 调用通知服务的方法,发送构建不稳定的通知 + notificationService.sendBuildUnstable( + extraInfo: ["不稳定原因": "测试未通过或存在警告"] // 示例原因 + ) + } + } + } + } +} \ No newline at end of file diff --git a/vars/pipelineJDKService.groovy b/vars/pipelineJDKService.groovy index fb9f8ad..86c66f6 100644 --- a/vars/pipelineJDKService.groovy +++ b/vars/pipelineJDKService.groovy @@ -71,7 +71,7 @@ def call(String orgName) { // Jenkins Pipeline 的入口方法通常命名为 ca // 配置远程仓库信息,包括认证和URL userRemoteConfigs: [[ credentialsId: 'c9b5eae2-5df5-485f-bebd-9cd5393b03e1', // 存储在 Jenkins 中的 Git 认证凭据ID - url: 'http://192.168.10.102:3000/cicd/jenkins-pipeline-files.git' // 配置仓库的URL地址 + url: 'http://192.168.10.102:3000/cicd/jenkins-pipeline-files2.git' // 配置仓库的URL地址 ]], // 扩展配置:指定检出到非默认工作目录的子目录中,避免与后续可能检出的应用代码冲突 extensions: [[ diff --git a/vars/pipelinePythonService.groovy b/vars/pipelinePythonService.groovy index e69de29..870d553 100644 --- a/vars/pipelinePythonService.groovy +++ b/vars/pipelinePythonService.groovy @@ -0,0 +1,364 @@ +// 导入在 Pipeline 中需要使用的共享库类 +// 这些类通常在 Jenkins 的共享库 (Shared Libraries) 中定义,提供了可重用的功能 +import com.python.ResolverService +import com.python.DeploymentService +import com.common.StageService +import com.common.NotificationService + +/** + * 主管道入口点 + * 提供完整的CI/CD流水线功能,实现从配置加载到部署的全流程自动化 + * 这是一个 Jenkins 共享库 (Shared Library) 中的全局变量 (Global Variable) 或直接在 Pipeline 脚本中定义的方法 + * + * @param orgName 组织名称,用于标识不同的组织配置和环境。这是调用此流水线时必须传入的主要参数。 + * + * 功能特性: + * - 动态配置加载:支持根据 orgName 加载多环境配置管理 + * - 多阶段流水线:清晰的构建、测试、部署、验证流程 + * - 错误恢复机制:完善的异常处理和回滚功能 (虽然在当前代码示例中未完全展示,但框架支持) + * - 资源清理:自动清理工作空间和临时资源,避免磁盘空间耗尽 + * - 通知机制:构建结果通知(成功、失败、中止、不稳定) + * + * 示例用法: + * @Library ('share-library@master')_ + * orgName = 'xiaomayi' + * pipelineJDKService(orgName) + */ +def call(String orgName) { // Jenkins Pipeline 的入口方法通常命名为 call + // 定义管道参数变量,用于在不同阶段间传递配置信息。初始化为 null。 + def pipelineParams = null + // 声明服务类变量,将在后续阶段初始化 + def resolverService = null // 配置解析服务实例 + def deploymentService = null // 部署服务实例 + def stageService = null // 阶段服务实例 + def notificationService = null // 通知服务实例 + // Jenkins代理标签,指定流水线特定阶段在哪个代理节点上运行 + def jenkinsAgent = '' + + // 启动流水线定义。这是 Declarative Pipeline 的语法开始 + pipeline { + // 指定流水线执行代理,any表示可在任何可用代理上运行。 + // 注意:这里定义的 agent 会被后面 stage 中定义的 agent 覆盖。 + agent any + + // 流水线全局配置选项块 + options { + // 构建历史保留策略,用于管理 Jenkins 服务器磁盘空间 + buildDiscarder(logRotator( + artifactDaysToKeepStr: '', // 构建产物保留天数(空字符串表示不按天数限制,可能使用数量限制) + artifactNumToKeepStr: '2', // 保留最近2个构建的产物(如生成的jar包、docker镜像等) + daysToKeepStr: '7', // 构建记录元数据(日志、状态等)保留7天 + numToKeepStr: '3' // 保留最近3个构建记录(无论天数) + )) + // 全局超时设置:整个流水线最多运行2小时,超时则自动终止,防止无限运行 + timeout(time: 2, unit: 'HOURS') + // 还可以添加其他选项,例如: disableConcurrentBuilds() 防止并行运行等 + } + + // 流水线阶段定义块,包含按顺序执行的一系列阶段 (stages) + stages { + /** + * 第一阶段:初始化配置和环境 + * 加载环境配置、解析参数、初始化服务实例。 + * 此阶段仍在 pipeline 顶部定义的 agent any 上运行。 + */ + stage('Setup Parameters & ENV') { + steps { + // 执行Git检出操作 (checkout SCM),获取流水线配置文件或所需的配置仓库 + checkout scmGit([ // scmGit 是一种 checkout 方式 + // 指定要检出的分支,这里检出 master 分支 + branches: [[name: '*/master']], + // 配置远程仓库信息,包括认证和URL + userRemoteConfigs: [[ + credentialsId: 'c9b5eae2-5df5-485f-bebd-9cd5393b03e1', // 存储在 Jenkins 中的 Git 认证凭据ID + url: 'http://192.168.10.102:3000/cicd/jenkins-pipeline-files.git' // 配置仓库的URL地址 + ]], + // 扩展配置:指定检出到非默认工作目录的子目录中,避免与后续可能检出的应用代码冲突 + extensions: [[ + $class: 'RelativeTargetDirectory', // Jenkins checkout 扩展的类名,用于设置相对目标目录 + relativeTargetDir: '.pipeline-files' // 将配置仓库检出到工作空间下的 .pipeline-files 目录 + ]], + ]) + script { + echo "=== 开始初始化配置 ===" + echo "组织名称: ${orgName}" // 打印传入的组织名称参数 + try { // 使用 try-catch 进行异常处理,确保配置失败时能妥善处理 + // 初始化阶段服务实例,this 代表当前的流水线脚本对象,用于访问 Jenkins 步骤 + stageService = new StageService(this) + // 记录 '初始化配置' 阶段的开始时间 + stageService.recordStageStart('初始化配置') + + // 初始化配置解析服务实例,传入当前脚本对象和组织名称 + resolverService = new ResolverService(this, orgName) + // 调用解析服务的方法,加载所有配置文件(如环境相关的配置和主配置) + resolverService.loadConfigurations() + // 调用解析服务的方法,解析加载的配置并设置管道运行所需的参数集合 + // 返回的 pipelineParams 可能包含 Maven 设置、镜像仓库地址、目标服务器配置、环境变量等 + pipelineParams = resolverService.setupParameters() + + // 处理环境 profile 选择逻辑 + // params.PROFILES 可能是一个通过 Jenkins 输入参数(parameters {} 块)传入的值 + // 如果用户选择了 profile,则使用它;否则使用配置文件中定义的默认 profile (pipelineParams.PROFILES) + def selectedProfile = params.PROFILES ?: pipelineParams.PROFILES + pipelineParams.PROFILES = selectedProfile // 将最终确定的 profile 设置回参数Map中 + + // 设置后续构建和部署阶段使用的 Jenkins 代理标签 + // 优先使用配置文件中指定的代理标签,如果未配置则默认使用 'master' 节点(Jenkins Controller) + jenkinsAgent = pipelineParams?.jenkinsAgent ?: 'master' + echo "本次使用代理节点: ${jenkinsAgent}" + + // 初始化部署服务实例,传入当前脚本对象和从配置中获取的环境配置 + deploymentService = new DeploymentService(this, pipelineParams.envConfig) + + // 初始化消息通知服务实例,传入当前脚本对象、管道参数和阶段服务实例 + notificationService = new NotificationService(this, pipelineParams, stageService) + + // ✅ 关键步骤:将配置文件 stash 起来 + stash name: 'pipeline-files', includes: '.pipeline-files/**' + + // 记录 '初始化配置' 阶段的成功结束 + stageService.recordStageEnd('初始化配置', 'SUCCESS') + + } catch (Exception e) { // 捕获初始化过程中可能抛出的任何异常 + // 打印错误信息到流水线日志 + echo "配置初始化失败: ${e.message}" + // 记录 '初始化配置' 阶段的失败结束 + stageService.recordStageEnd('初始化配置', 'FAILURE') + // 抛出错误,终止整个流水线的执行,并标记为 FAILURE 状态 + error("管道初始化失败: " + e.getMessage()) + } + + echo "=== 结束初始化配置 ===" + } + } + } + + /** + * 第二阶段:代码构建 + * 执行Maven构建(编译、测试、打包),生成可部署的JAR包和Docker镜像。 + * 此阶段在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行,以分担主节点负载或使用特定环境。 + */ + stage('Build') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent // 使用前面计算得到的代理标签 + } + steps { + script { + echo "=== 🔨 开始代码构建 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '构建镜像' 阶段的开始时间 + stageService.recordStageStart('构建镜像') + // 调用部署服务的方法,执行具体的构建逻辑 + // 该方法内部可能包含:mvn clean compile/test/package、docker build/push 等步骤 + deploymentService.executeBuildStage(pipelineParams) + // 记录 '构建镜像' 阶段的成功结束 + stageService.recordStageEnd('构建镜像', 'SUCCESS') + echo "=== 🔨 完成代码构建 ===" + } + } + } + + /** + * 第三阶段:容器构建阶段 + * 负责将JAR包构建为Docker镜像,并推送到镜像仓库 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Container Build') { + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🐳 开始容器构建 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '应用部署' 阶段的开始时间 + stageService.recordStageStart('容器构建') + // 调用部署服务的方法,执行具体的容器构建逻辑 + deploymentService.executeContainerBuildStage(pipelineParams) + // 记录 '应用部署' 阶段的成功结束 + stageService.recordStageEnd('容器构建', 'SUCCESS') + echo "=== 🚀 完成容器构建 ===" + } + } + } + + /** + * 第四阶段:应用部署 + * 将Docker镜像部署到目标服务器(如测试、预生产、生产环境)。 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Deploy') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🚀 开始应用部署 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '应用部署' 阶段的开始时间 + stageService.recordStageStart('应用部署') + // 调用部署服务的方法,执行具体的部署逻辑 + // 该方法内部可能包含:通过 SSH 连接到目标服务器、执行部署脚本、更新容器(docker compose/k8s)等 + deploymentService.executeDeployStage(pipelineParams) + // 记录 '应用部署' 阶段的成功结束 + stageService.recordStageEnd('应用部署', 'SUCCESS') + echo "=== 🚀 完成应用部署 ===" + } + } + } + + /** + * 第五阶段:健康检查 + * 负责验证部署的应用是否正常运行,确保部署的服务的可用性和功能性 + * 这是CI/CD流程中的重要质量关卡,防止有缺陷的部署进入生产环境 + */ + stage('Health Check') { + agent { + // 使用配置文件中指定的代理标签,如果未配置则默认使用'master'节点 + // 健康检查可能需要访问部署环境网络或使用特定工具 + label jenkinsAgent + } + steps { + script { + echo "=== 🏥 开始健康检查 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录健康检查阶段的开始时间和状态 + stageService.recordStageStart('健康检查') + + try { + // 调用部署服务的健康检查方法,执行具体的验证逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 🏗️ 以下是健康检查可能包含的具体操作(示例): + + // 1. 🔗 服务端点连通性测试 + // - 检查应用的健康检查端点(如:/actuator/health)是否返回200状态码 + // - 验证关键API接口是否可访问 + + // 2. 📊 应用状态验证 + // - 检查应用日志是否有启动错误 + // - 验证数据库连接是否正常建立 + // - 确认必要的服务依赖(如Redis、MQ等)是否连接成功 + + // 3. 🔍 功能冒烟测试 + // - 执行一组最基本的业务操作测试,确保核心功能正常 + // - 验证配置是否正确加载(如配置文件、环境变量等) + + // 4. 📈 性能基线检查 + // - 检查应用启动时间是否在可接受范围内 + // - 验证内存占用、线程数等指标是否正常 + + // 5. 🐳 容器运行状态检查 + // - 确认Docker容器处于Running状态 + // - 检查容器资源使用情况(CPU、内存) + + // 调用部署服务的健康检查阶段逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 记录健康检查阶段的成功结束 + stageService.recordStageEnd('健康检查', 'SUCCESS') + + } catch (Exception e) { + // 健康检查失败时的处理 + echo "健康检查失败: ${e.message}" + // 记录健康检查阶段的失败状态 + stageService.recordStageEnd('健康检查', 'FAILURE') + // 可以选择抛出错误使整个流水线失败,或者标记为UNSTABLE + error("健康检查未通过,部署可能存在风险: " + e.getMessage()) + } + + echo "=== 🏥 完成健康检查 ===" + } + } + } + + // 可以在此处添加更多阶段,例如:自动化测试、集成测试、性能测试、安全扫描等 + } + + /** + * 流水线后处理块 (post section) + * 根据流水线的最终状态执行不同的操作,无论构建成功与否都会执行的处理逻辑。 + * 此部分在 pipeline 顶部定义的 agent any 上运行。 + */ + post { + /** + * always:无论构建结果如何(成功、失败、中止、不稳定)都会执行 + * 主要用于资源清理和状态报告等必须完成的操作 + */ + always { + echo "=== 管道执行完成 ===" + echo "完成时间: ${new Date()}" // 打印流水线结束的时间 + // 清理工作空间,删除当前流水线运行产生的所有文件,释放磁盘空间 + cleanWs() + script { + echo "构建完成,状态: ${currentBuild.currentResult}" // 打印最终构建状态 + // 调用阶段服务的方法,收集所有阶段的执行信息(开始时间、持续时间、状态),可能用于通知或报告 + stageService.collectAllStagesInfo() + } + } + + /** + * success:只有当整个流水线的状态为 SUCCESS 时执行 + * 主要用于成功通知和结果报告 + */ + success { + echo "✅ 部署成功!" + script { + // 调用通知服务的方法,发送构建成功的通知(如邮件、钉钉、Slack等) + notificationService.sendBuildSuccess() + } + } + + /** + * failure:只有当整个流水线的状态为 FAILURE 时执行 + * 主要用于错误报告、通知和可能的清理或回滚操作(虽然此处未展示回滚) + */ + failure { + echo "❌ 部署失败!" + script { + // 调用通知服务的方法,发送构建失败的通知,并传递错误信息 + // 这里的 errorMsg 和 extraInfo 是示例,实际可能从捕获的异常中获取 + notificationService.sendBuildFailure( + errorMsg: "单元测试失败", // 示例错误消息 + extraInfo: [stage: "test", errorDetails: "具体错误信息"] // 示例额外信息 + ) + } + } + + /** + * aborted:只有当整个流水线的状态为 ABORTED 时执行(通常由用户手动中止) + * 用于通知构建被中止的情况 + */ + aborted { + echo "❌ 构建中止!" + script { + // 调用通知服务的方法,发送构建中止的通知 + notificationService.sendBuildAborted( + // 传递一些额外信息,例如中止原因和操作人(如果环境变量可用) + extraInfo: [中止原因: '手动中止', 操作人: env.USER_ID] + ) + } + } + + /** + * unstable:只有当整个流水线的状态为 UNSTABLE 时执行(通常由测试失败或有警告导致) + * 主要用于警告通知 + */ + unstable { + echo "⚠️ 管道执行不稳定" + script { + // 调用通知服务的方法,发送构建不稳定的通知 + notificationService.sendBuildUnstable( + extraInfo: ["不稳定原因": "测试未通过或存在警告"] // 示例原因 + ) + } + } + } + } +} \ No newline at end of file