Jenkinsfile 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. pipeline {
  2. agent any
  3. parameters {
  4. choice(name: 'env', choices: ['dev', 'test', 'prod'], description: '部署环境')
  5. string(name: 'NAMESPACE', defaultValue: 'portal-frontend', description: 'K8s 命名空间')
  6. string(name: 'DOMAIN', defaultValue: 'radar-power.asia', description: 'Ingress 域名(留空则不创建 Ingress)')
  7. string(name: 'TLS_SECRET', defaultValue: 'portal-tls', description: 'TLS Secret 名称(仅在 DOMAIN 非空时使用)')
  8. }
  9. environment {
  10. PROJECT_NAME = 'portal-frontend'
  11. BUILD_DIR = 'dist'
  12. NODE_ENV = 'production'
  13. HARBOR_HOST = '8.130.28.21:81'
  14. KUBECONFIG_PATH = '/root/.kube/config'
  15. HARBOR_USER = 'admin'
  16. HARBOR_PASS = 'Hfln@1024'
  17. HARBOR_RETENTION_ID = '1'
  18. }
  19. stages {
  20. stage('🧬 初始化环境') {
  21. steps {
  22. script {
  23. env.HARBOR_PROJECT = params.env
  24. env.IMAGE_TAG = "${HARBOR_HOST}/${env.HARBOR_PROJECT}/${PROJECT_NAME}:${BUILD_NUMBER}"
  25. echo ">>> 环境:${params.env}, Harbor项目:${env.HARBOR_PROJECT}, K8s命名空间:${params.NAMESPACE}"
  26. if (params.DOMAIN?.trim()) {
  27. echo ">>> 域名:${params.DOMAIN}, TLS Secret:${params.TLS_SECRET}"
  28. echo ">>> 注意:TLS Secret 应该已经存在,请确保命名空间一致"
  29. }
  30. }
  31. }
  32. }
  33. stage('📥 拉取代码') {
  34. steps {
  35. checkout scm
  36. echo "✅ 代码拉取成功"
  37. }
  38. }
  39. stage('🔧 构建 Docker 镜像') {
  40. steps {
  41. script {
  42. sh """
  43. docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} ${HARBOR_HOST}
  44. docker build --build-arg ENV=${params.env} -t ${IMAGE_TAG} .
  45. """
  46. echo "✅ 镜像构建成功:${IMAGE_TAG}"
  47. }
  48. }
  49. }
  50. stage('🚀 推送镜像到 Harbor') {
  51. steps {
  52. script {
  53. sh """
  54. docker push ${IMAGE_TAG}
  55. docker rmi ${IMAGE_TAG}
  56. """
  57. echo "✅ 镜像推送并本地清理完成"
  58. }
  59. }
  60. }
  61. stage('�� 验证 TLS Secret') {
  62. when {
  63. expression { params.DOMAIN?.trim() }
  64. }
  65. steps {
  66. script {
  67. sh """
  68. export KUBECONFIG=${KUBECONFIG_PATH}
  69. # 验证 TLS Secret 是否存在
  70. if ! kubectl get secret ${params.TLS_SECRET} -n ${params.NAMESPACE} >/dev/null 2>&1; then
  71. echo "❌ TLS Secret '${params.TLS_SECRET}' 在命名空间 '${params.NAMESPACE}' 中不存在!"
  72. echo "请先创建 TLS Secret:"
  73. echo "kubectl create secret tls ${params.TLS_SECRET} --cert=fullchain.pem --key=privkey.pem -n ${params.NAMESPACE}"
  74. exit 1
  75. fi
  76. echo "✅ TLS Secret '${params.TLS_SECRET}' 验证成功"
  77. """
  78. }
  79. }
  80. }
  81. stage('�� Kubernetes 部署') {
  82. steps {
  83. script {
  84. def domain = params.DOMAIN?.trim()
  85. sh """
  86. export KUBECONFIG=${KUBECONFIG_PATH}
  87. # 确保命名空间存在
  88. kubectl get ns ${params.NAMESPACE} >/dev/null 2>&1 || kubectl create ns ${params.NAMESPACE}
  89. # 如果 Deployment 存在,更新镜像
  90. if kubectl get deployment ${PROJECT_NAME} -n ${params.NAMESPACE} >/dev/null 2>&1; then
  91. echo ">>> Deployment 已存在,更新镜像..."
  92. kubectl set image deployment/${PROJECT_NAME} ${PROJECT_NAME}=${IMAGE_TAG} -n ${params.NAMESPACE}
  93. kubectl rollout status deployment/${PROJECT_NAME} -n ${params.NAMESPACE} --timeout=120s
  94. # 更新 Ingress(如果提供了域名)
  95. if [ -n "${domain}" ]; then
  96. echo ">>> 更新 Ingress 配置..."
  97. kubectl apply -n ${params.NAMESPACE} -f - <<EOF
  98. apiVersion: networking.k8s.io/v1
  99. kind: Ingress
  100. metadata:
  101. name: ${PROJECT_NAME}-ingress
  102. annotations:
  103. kubernetes.io/ingress.class: nginx
  104. nginx.ingress.kubernetes.io/ssl-redirect: "true"
  105. nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
  106. nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
  107. nginx.ingress.kubernetes.io/ssl-passthrough: "false"
  108. nginx.ingress.kubernetes.io/proxy-body-size: "8m"
  109. nginx.ingress.kubernetes.io/rewrite-target: /
  110. spec:
  111. tls:
  112. - hosts:
  113. - ${domain}
  114. secretName: ${params.TLS_SECRET}
  115. rules:
  116. - host: ${domain}
  117. http:
  118. paths:
  119. - path: /
  120. pathType: Prefix
  121. backend:
  122. service:
  123. name: ${PROJECT_NAME}
  124. port:
  125. number: 80
  126. EOF
  127. fi
  128. else
  129. # 创建新的 Deployment 和 Service
  130. echo ">>> 创建新的 Deployment 和 Service..."
  131. kubectl apply -n ${params.NAMESPACE} -f - <<EOF
  132. apiVersion: apps/v1
  133. kind: Deployment
  134. metadata:
  135. name: ${PROJECT_NAME}
  136. spec:
  137. replicas: 2
  138. selector:
  139. matchLabels:
  140. app: ${PROJECT_NAME}
  141. template:
  142. metadata:
  143. labels:
  144. app: ${PROJECT_NAME}
  145. spec:
  146. containers:
  147. - name: ${PROJECT_NAME}
  148. image: ${IMAGE_TAG}
  149. ports:
  150. - containerPort: 80
  151. ---
  152. apiVersion: v1
  153. kind: Service
  154. metadata:
  155. name: ${PROJECT_NAME}
  156. spec:
  157. type: ClusterIP
  158. selector:
  159. app: ${PROJECT_NAME}
  160. ports:
  161. - port: 80
  162. targetPort: 80
  163. EOF
  164. # 创建 Ingress(如果提供了域名)
  165. if [ -n "${domain}" ]; then
  166. echo ">>> 创建新的 Ingress..."
  167. kubectl apply -n ${params.NAMESPACE} -f - <<EOF
  168. apiVersion: networking.k8s.io/v1
  169. kind: Ingress
  170. metadata:
  171. name: ${PROJECT_NAME}-ingress
  172. annotations:
  173. kubernetes.io/ingress.class: nginx
  174. nginx.ingress.kubernetes.io/ssl-redirect: "true"
  175. nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
  176. nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
  177. nginx.ingress.kubernetes.io/ssl-passthrough: "false"
  178. nginx.ingress.kubernetes.io/proxy-body-size: "8m"
  179. nginx.ingress.kubernetes.io/rewrite-target: /
  180. spec:
  181. tls:
  182. - hosts:
  183. - ${domain}
  184. secretName: ${params.TLS_SECRET}
  185. rules:
  186. - host: ${domain}
  187. http:
  188. paths:
  189. - path: /
  190. pathType: Prefix
  191. backend:
  192. service:
  193. name: ${PROJECT_NAME}
  194. port:
  195. number: 80
  196. EOF
  197. fi
  198. fi
  199. # 等待部署完成
  200. echo ">>> 等待部署完成..."
  201. kubectl wait --for=condition=available --timeout=300s deployment/${PROJECT_NAME} -n ${params.NAMESPACE}
  202. # 显示部署状态
  203. echo ">>> 部署状态:"
  204. kubectl get all -n ${params.NAMESPACE}
  205. kubectl get ingress -n ${params.NAMESPACE} || echo ">>> 未配置 Ingress"
  206. # 如果配置了域名,显示访问信息
  207. if [ -n "${domain}" ]; then
  208. echo ">>> 应用部署完成!"
  209. echo ">>> 访问地址:https://${domain}"
  210. echo ">>> 注意:确保域名 ${domain} 已正确解析到集群公网IP"
  211. else
  212. echo ">>> 应用部署完成!"
  213. echo ">>> 注意:未配置域名,请手动配置 Ingress 或使用 NodePort 访问"
  214. fi
  215. """
  216. }
  217. }
  218. }
  219. stage('🧹 清理本地旧镜像(保留最新3个)') {
  220. steps {
  221. script {
  222. def baseImage = "${HARBOR_HOST}/${env.HARBOR_PROJECT}/${PROJECT_NAME}"
  223. sh """
  224. docker images ${baseImage} --format "{{.Repository}}:{{.Tag}}" \\
  225. | grep -v latest \\
  226. | sort -r -t ':' -k2 \\
  227. | tail -n +4 \\
  228. | xargs -r docker rmi || true
  229. """
  230. echo "✅ 本地旧镜像清理完成"
  231. }
  232. }
  233. }
  234. stage(' 清理悬空镜像 <none>') {
  235. steps {
  236. script {
  237. sh """
  238. docker images -f "dangling=true" -q | xargs -r docker rmi || true
  239. """
  240. echo "✅ 悬空镜像(<none>)清理完成"
  241. }
  242. }
  243. }
  244. stage(' 触发 Harbor 镜像保留策略') {
  245. steps {
  246. script {
  247. sh """
  248. curl -u ${HARBOR_USER}:${HARBOR_PASS} -X POST \\
  249. "http://${HARBOR_HOST}/api/v2.0/retentions/${HARBOR_RETENTION_ID}/executions"
  250. """
  251. echo "✅ Harbor 镜像保留策略已触发"
  252. }
  253. }
  254. }
  255. }
  256. post {
  257. success {
  258. echo "✅ 构建 & 部署成功!"
  259. script {
  260. if (params.DOMAIN?.trim()) {
  261. echo "🌐 应用可通过 https://${params.DOMAIN} 访问"
  262. }
  263. }
  264. }
  265. failure {
  266. echo "❌ 构建或部署失败,请检查日志"
  267. }
  268. always {
  269. cleanWs()
  270. }
  271. }
  272. }