From 4bc643fcd195db8c8d3dbceed57445ea6152206c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E6=A0=8B=E6=A2=81?= Date: Sun, 22 Jun 2025 14:46:32 +0800 Subject: [PATCH] init --- Dockerfile | 19 ++++ Jenkinsfile | 211 +++++++++++++++++++++++++++++++++++++++++++++ nginx.conf.gateway | 191 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+) create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 nginx.conf.gateway diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95d2691 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# --- 网关运行阶段 --- +FROM nginx:stable-alpine@sha256:5d453a6883d39e524c678b46d9a42a0b03442f66a46046b3c2b219999a094451 AS gateway + +# 复制自定义的 Nginx 配置文件 +# 假设 Nginx 配置在 gateway 项目的根目录,名为 nginx.conf.gateway +COPY nginx.conf.gateway /etc/nginx/nginx.conf + +# 复制 SSL 证书到 Nginx 容器内 +# 假设你的证书在 gateway 项目的 certs/ 文件夹 +COPY certs/server.crt /etc/nginx/certs/server.crt +COPY certs/server.key /etc/nginx/certs/server.key + +# 暴露 Nginx 监听的端口 +# 在 nginx.conf.gateway 中配置了 8000 和 8001 端口 +EXPOSE 8000 +EXPOSE 8001 + +# 启动 Nginx 服务器 +CMD ["nginx", "-g", "daemon off;"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..31c75d8 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,211 @@ +// Groovy 辅助函数,用于发送钉钉通知。(保持不变) +@NonCPS +def sendDingTalkNotification(Map config) { + def message = config.get('message', '来自 Jenkins 的通知') + def webhookEnvVarName = config.get('webhookEnvVarName') // 存储 Webhook URL 的环境变量名称 + def author = config.get('author', '未知用户') + def jobName = config.get('jobName', env.JOB_NAME ?: 'N/A') + def buildNumber = config.get('buildNumber', env.BUILD_NUMBER ?: 'N/A') + def enabled = config.get('enabled', false) + + if (enabled && webhookEnvVarName) { + def webhookUrl = env[webhookEnvVarName] + if (!webhookUrl) { + echo "钉钉 Webhook URL 未通过环境变量 ${webhookEnvVarName} 找到。跳过通知。" + return + } + def finalMessage = "BZPT.发布 (${jobName}#${buildNumber}):\n${message}" + if (author && author != "未知用户" && author.trim() != "") { + finalMessage += "\n@${author.trim()}" + } + def payload = groovy.json.JsonOutput.toJson([msgtype: "text", text: [content: finalMessage]]) + def curlResult = sh script: """ + echo "正在发送钉钉通知..." + curl -X POST -H 'Content-Type: application/json' -d '${payload}' '${webhookUrl}' --silent --show-error --connect-timeout 10 --max-time 15 + """, returnStatus: true + if (curlResult != 0) { + echo "警告:钉钉通知可能发送失败 (curl 退出码: ${curlResult})。" + } else { + echo "钉钉通知发送成功。" + } + } else { + echo "钉钉通知已跳过 (可能已禁用、未设置 Webhook 凭证或未找到 Webhook URL 的环境变量)。" + } +} + +pipeline { + agent any + + // triggers 块现在会使用在 Jenkins UI 中配置的 SCM 信息进行轮询 + triggers { + pollSCM('H/5 * * * *') + } + + parameters { + // Git 参数现在主要用于 UI 显示和分支选择,实际 SCM 配置在 Job UI 中 + string(name: 'GIT_REPO_URL', defaultValue: 'http://111.230.114.47:3000/yidongliang/gateway.git', description: 'Git 仓库 URL (仅供参考,实际配置在Job的SCM部分)') + string(name: 'GIT_BRANCH', defaultValue: 'stage', description: '要拉取的 Git 分支 (例如:develop, stage, master)') + credentials(name: 'GIT_CREDENTIALS_ID', defaultValue: 'jenkins', description: 'Git 凭证 ID', required: true) + + // Docker 构建参数 (保持不变) + string(name: 'DOCKERFILE_PATH_IN_REPO', defaultValue: 'Dockerfile', description: '仓库中 Dockerfile 的路径') + string(name: 'DOCKER_REGISTRY_URL', defaultValue: 'https://106.52.199.114:5000', description: 'Docker 镜像仓库 URL。留空则不推送。') + string(name: 'DOCKER_IMAGE_NAME', defaultValue: 'bzpt.gateway', description: 'Docker 镜像名称') + string(name: 'IMAGE_BASE_TAG', defaultValue: '1.0', description: '镜像标签的基础部分') + credentials(name: 'DOCKER_CREDENTIALS_ID', defaultValue: 'dockerregister', description: 'Docker 镜像仓库凭证 ID', required: false) + booleanParam(name: 'PUSH_LATEST_TAG', defaultValue: true, description: '是否同时创建并推送 "latest" 标签?') + + // 钉钉通知参数 (保持不变) + booleanParam(name: 'SEND_DINGTALK_NOTIFICATIONS', defaultValue: true, description: '是否发送钉钉通知?') + credentials(name: 'DINGTALK_WEBHOOK_CREDENTIAL_ID', defaultValue: 'stage-publish-dingding', description: '存储钉钉 Webhook URL 的凭证 ID', required: false) + } + + environment { + LAST_COMMIT_AUTHOR = "gateway-stage" + DINGTALK_WEBHOOK_ENV_VAR_NAME = 'DINGTALK_WEBHOOK_URL_FROM_CREDS' + } + + stages { + // ========================================================================= + // **核心改动:不再需要“拉取代码”阶段。** + // 代码已由 Jenkins 根据 UI 配置自动检出。 + // 第一个阶段直接开始进行初始化。 + // ========================================================================= + stage('0. 初始化和准备') { + steps { + // 清理工作空间是好习惯,但注意它会删除所有文件,包括 Jenkins 自动检出的代码。 + // 如果需要重新检出,可以使用 checkout scm。但通常在此场景下不需要 cleanWs。 + // 我们暂时保留它,因为它在您的原始脚本中。 + cleanWs() + + // **重要**:由于 cleanWs 删除了所有内容,我们需要再次检出代码。 + // `checkout scm` 是一个特殊的步骤,它会使用在 Jenkins UI 中配置的 SCM 信息。 + echo "重新检出代码以确保工作空间内容最新..." + checkout scm + + script { + echo "代码已检出。开始初始化构建环境..." + + // 构造带 registry 的完整镜像名 + def preparedImageNameWithRegistry = params.DOCKER_IMAGE_NAME + env.PREPARED_IMAGE_NAME = preparedImageNameWithRegistry + echo "构建的镜像全名 (不含标签): ${env.PREPARED_IMAGE_NAME}" + + // 现在可以安全地执行 git 命令 + def shortCommit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + env.IMAGE_TAG = "${params.IMAGE_BASE_TAG}.${BUILD_NUMBER}-${shortCommit}" + echo "生成的 IMAGE_TAG: ${env.IMAGE_TAG}" + + try { + env.LAST_COMMIT_AUTHOR = sh(script: 'git log -1 --pretty=format:"%an"', returnStdout: true).trim() + } catch (e) { + echo "警告:无法获取最后提交的作者。 ${e.getMessage()}" + env.LAST_COMMIT_AUTHOR = "未知用户" + } + echo "最后提交的作者: ${env.LAST_COMMIT_AUTHOR}" + } + } + } + + // 后续阶段保持不变,仅序号变更 + stage('1. 构建 Docker 镜像') { + steps { + script { + def dockerfilePath = params.DOCKERFILE_PATH_IN_REPO + if (!fileExists(dockerfilePath)) { + error "在工作空间相对路径下未找到 Dockerfile: ${dockerfilePath}" + } + if (!env.PREPARED_IMAGE_NAME || !env.IMAGE_TAG) { + error "构建 Docker 镜像所需的 PREPARED_IMAGE_NAME 或 IMAGE_TAG 未设置。" + } + + def fullImageNameWithTag = "${env.PREPARED_IMAGE_NAME}:${env.IMAGE_TAG}" + docker.build(fullImageNameWithTag, "-f \"${dockerfilePath}\" .") + echo "Docker 镜像 ${fullImageNameWithTag} 构建成功。" + + if (params.PUSH_LATEST_TAG) { + def fullImageNameLatest = "${env.PREPARED_IMAGE_NAME}:latest" + sh "docker tag ${fullImageNameWithTag} ${fullImageNameLatest}" + echo "成功将镜像标记为: ${fullImageNameLatest}" + } + } + } + } + + stage('2. 推送 Docker 镜像 (可选)') { + when { expression { params.DOCKER_REGISTRY_URL != "" } } + steps { + script { + def fullImageNameWithTag = "${env.PREPARED_IMAGE_NAME}:${env.IMAGE_TAG}" + def fullImageNameLatest = "${env.PREPARED_IMAGE_NAME}:latest" + + docker.withRegistry(params.DOCKER_REGISTRY_URL, params.DOCKER_CREDENTIALS_ID) { + echo "正在推送镜像: ${fullImageNameWithTag}" + docker.image(fullImageNameWithTag).push() + echo "镜像 ${fullImageNameWithTag} 推送成功。" + + if (params.PUSH_LATEST_TAG) { + echo "正在推送 latest 镜像: ${fullImageNameLatest}" + docker.image(fullImageNameLatest).push() + echo "镜像 ${fullImageNameLatest} 推送成功。" + } + } + } + } + } + } + // post 块定义无需任何修改,保持原样 + post { + always { + echo "流水线结束。最终状态: ${currentBuild.result ?: 'IN PROGRESS'}" + } + success { + script { + if (params.SEND_DINGTALK_NOTIFICATIONS && params.DINGTALK_WEBHOOK_CREDENTIAL_ID) { + withCredentials([string(credentialsId: params.DINGTALK_WEBHOOK_CREDENTIAL_ID, variable: env.DINGTALK_WEBHOOK_ENV_VAR_NAME)]) { + sendDingTalkNotification( + message: "${params.DOCKER_IMAGE_NAME} 构建和推送成功。镜像: ${env.PREPARED_IMAGE_NAME}:${env.IMAGE_TAG}", + webhookEnvVarName: env.DINGTALK_WEBHOOK_ENV_VAR_NAME, + author: env.LAST_COMMIT_AUTHOR ?: '未知用户', + jobName: env.JOB_NAME, + buildNumber: env.BUILD_NUMBER, + enabled: params.SEND_DINGTALK_NOTIFICATIONS + ) + } + } + } + } + failure { + script { + if (params.SEND_DINGTALK_NOTIFICATIONS && params.DINGTALK_WEBHOOK_CREDENTIAL_ID) { + withCredentials([string(credentialsId: params.DINGTALK_WEBHOOK_CREDENTIAL_ID, variable: env.DINGTALK_WEBHOOK_ENV_VAR_NAME)]) { + sendDingTalkNotification( + message: "${params.DOCKER_IMAGE_NAME} 构建失败。请检查控制台: ${env.BUILD_URL}console", + webhookEnvVarName: env.DINGTALK_WEBHOOK_ENV_VAR_NAME, + author: env.LAST_COMMIT_AUTHOR ?: '未知用户', + jobName: env.JOB_NAME, + buildNumber: env.BUILD_NUMBER, + enabled: params.SEND_DINGTALK_NOTIFICATIONS + ) + } + } + } + } + aborted { + script { + if (params.SEND_DINGTALK_NOTIFICATIONS && params.DINGTALK_WEBHOOK_CREDENTIAL_ID) { + withCredentials([string(credentialsId: params.DINGTALK_WEBHOOK_CREDENTIAL_ID, variable: env.DINGTALK_WEBHOOK_ENV_VAR_NAME)]) { + sendDingTalkNotification( + message: "${params.DOCKER_IMAGE_NAME} 构建已中止。请检查控制台: ${env.BUILD_URL}console", + webhookEnvVarName: env.DINGTALK_WEBHOOK_ENV_VAR_NAME, + author: env.LAST_COMMIT_AUTHOR ?: '未知用户', + jobName: env.JOB_NAME, + buildNumber: env.BUILD_NUMBER, + enabled: params.SEND_DINGTALK_NOTIFICATIONS + ) + } + } + } + } + } +} diff --git a/nginx.conf.gateway b/nginx.conf.gateway new file mode 100644 index 0000000..45a5cac --- /dev/null +++ b/nginx.conf.gateway @@ -0,0 +1,191 @@ +# 这是网关 Nginx 的配置文件 +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + keepalive_timeout 30s; + keepalive_requests 1000; + + # 安全响应头 + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff; + # 对于 X-Frame-Options,根据你的需求设置 SAMEORIGIN 或 DENY + add_header X-Frame-Options SAMEORIGIN; + # add_header Referrer-Policy "strict-origin-when-cross-origin"; # 可选 + + # --- SSL 配置 --- + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # --- 后端服务定义 (在 Docker 网络中通常使用服务名) --- + # 如果 sys-ui 和 lmg-ui 的服务名是 "sys-ui-container" 和 "lmg-ui-container" + # 它们需要在同一个 Docker 网络中运行,并且可以通过这些服务名访问到它们的 80 端口 + upstream sys_ui_backend { + server sys-ui-container:80; + } + upstream lmg_ui_backend { + server lmg-ui-container:80; + } + # 你的后端 API 服务 + upstream sys_api { + server sys_backend_service:19902; # 替换为你的实际后端服务名或IP + } + upstream lmg_api { + server lmg_backend_service:19904; # 替换为你的实际后端服务名或IP + } + # 认证服务 + upstream auth_api { + server auth_backend_service:19902; # 替换为你的实际后端服务名或IP + } + + # --- 网关服务配置 --- + + # sys-ui Server Block + server { + listen 8000 ssl reuseport; + server_name your-domain.com; # 替换为你的域名或IP + + # SSL 证书配置 (如果与 http 块重复,server 块配置会覆盖) + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # 统一代理头配置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # --- sys-ui 静态资源服务 --- + location / { + proxy_pass http://sys_ui_backend; # 将所有请求代理到 sys-ui 容器 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # 如果 sys-ui 的 Nginx 配置了 try_files,这里不需要再配置 + # 理论上 Nginx 是直接代理到 sys-ui 的 80 端口,所以 sys-ui 内部的 Nginx 会处理 try_files + # 如果你想让网关直接提供静态文件,可以像之前一样复制 dist 文件到网关目录,但这样就不是单独部署 UI 容器了 + } + + # --- sys-ui 后端 API 代理 --- + location /api/sys/ { + proxy_pass https://sys_api/api/; + proxy_ssl_server_name on; + proxy_ssl_session_reuse off; + proxy_ssl_verify off; # 生产环境建议开启验证 + + proxy_set_header Host $proxy_host; + proxy_set_header Authorization $http_authorization; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + + proxy_redirect https://sys_api/ /api/sys/; # 重写 Location 头 + } + + # --- 认证服务代理 --- + location /auth/ { + proxy_pass https://auth_api/auth/; + proxy_ssl_server_name on; + proxy_ssl_session_reuse off; + proxy_ssl_verify off; + + proxy_set_header Host $proxy_host; + proxy_set_header Authorization $http_authorization; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + + proxy_redirect https://auth_api/ /auth/; # 重写 Location 头 + } + } + + # lmg-ui Server Block + server { + listen 8001 ssl reuseport; + server_name your-domain.com; # 替换为你的域名或IP + + # SSL 配置 + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # 统一代理头配置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # --- lmg-ui 静态资源服务 --- + location / { + proxy_pass http://lmg_ui_backend; # 将所有请求代理到 lmg-ui 容器 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # --- lmg-ui 后端 API 代理 --- + location /api/lmg/ { + proxy_pass https://lmg_api/api/; + proxy_ssl_server_name on; + proxy_ssl_session_reuse off; + proxy_ssl_verify off; + + proxy_set_header Host $proxy_host; + proxy_set_header Authorization $http_authorization; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + + proxy_redirect https://lmg_api/ /api/lmg/; # 重写 Location 头 + } + + # --- 跨系统访问 Sys API (如果 lmg-ui 需要访问 sys API) --- + location /api/sys/ { + proxy_pass https://sys_api/api/; + proxy_ssl_server_name on; + proxy_ssl_session_reuse off; + proxy_ssl_verify off; + + proxy_set_header Host $proxy_host; + proxy_set_header Authorization $http_authorization; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + + proxy_redirect https://sys_api/ /api/sys/; # 重写 Location 头 + } + } +}