mirror of
https://github.com/opsre/LiteOps.git
synced 2026-05-08 13:07:26 +08:00
✨ feat: 新增gitlab push事件触发webhooks自动构建功能
This commit is contained in:
@@ -226,47 +226,23 @@ class BuildTask(models.Model):
|
||||
stages = models.JSONField(default=list, verbose_name='构建阶段')
|
||||
|
||||
# 构建参数配置
|
||||
parameters = models.JSONField(default=list, verbose_name='构建参数配置', help_text='''
|
||||
[
|
||||
{
|
||||
"name": "MY_SERVICES",
|
||||
"description": "选择要部署的服务",
|
||||
"choices": ["user-service", "order-service", "payment-service"],
|
||||
"default_values": ["user-service"]
|
||||
}
|
||||
]
|
||||
''')
|
||||
parameters = models.JSONField(default=list, verbose_name='构建参数配置')
|
||||
|
||||
# 外部脚本库配置
|
||||
use_external_script = models.BooleanField(default=False, verbose_name='使用外部脚本库')
|
||||
external_script_config = models.JSONField(default=dict, verbose_name='外部脚本库配置', help_text='''
|
||||
{
|
||||
"repo_url": "https://github.com/example/scripts.git", # Git仓库地址
|
||||
"directory": "/data/scripts", # 存放目录
|
||||
"branch": "main", # 分支名称(可选)
|
||||
"token_id": "credential_id" # Git Token凭证ID(私有仓库)
|
||||
}
|
||||
''')
|
||||
external_script_config = models.JSONField(default=dict, verbose_name='外部脚本库配置')
|
||||
|
||||
# 构建时间信息(使用JSON存储)
|
||||
build_time = models.JSONField(default=dict, verbose_name='构建时间信息', help_text='''
|
||||
{
|
||||
"total_duration": "300", # 总耗时(秒)
|
||||
"start_time": "2024-03-06 12:00:00", # 开始时间
|
||||
"end_time": "2024-03-06 12:05:00", # 结束时间
|
||||
"stages_time": [ # 各阶段时间信息
|
||||
{
|
||||
"name": "代码拉取",
|
||||
"start_time": "2024-03-06 12:00:00",
|
||||
"duration": "60" # 耗时(秒)
|
||||
}
|
||||
]
|
||||
}
|
||||
''')
|
||||
build_time = models.JSONField(default=dict, verbose_name='构建时间信息')
|
||||
|
||||
# 构建后操作
|
||||
notification_channels = models.JSONField(default=list, verbose_name='通知方式')
|
||||
|
||||
# 自动构建配置
|
||||
auto_build_enabled = models.BooleanField(default=False, verbose_name='启用自动构建')
|
||||
auto_build_branches = models.JSONField(default=list, verbose_name='自动构建分支')
|
||||
webhook_token = models.CharField(max_length=64, null=True, blank=True, verbose_name='Webhook验证Token')
|
||||
|
||||
# 状态和统计
|
||||
status = models.CharField(max_length=20, default='created', null=True, verbose_name='任务状态') # created, disabled
|
||||
building_status = models.CharField(max_length=20, default='idle', null=True, verbose_name='构建状态') # idle, building
|
||||
@@ -299,26 +275,8 @@ class BuildHistory(models.Model):
|
||||
requirement = models.TextField(null=True, blank=True, verbose_name='构建需求描述')
|
||||
build_log = models.TextField(null=True, blank=True, verbose_name='构建日志')
|
||||
stages = models.JSONField(default=list, verbose_name='构建阶段')
|
||||
parameter_values = models.JSONField(default=dict, verbose_name='构建参数值', help_text='''
|
||||
{
|
||||
"MY_SERVICES": ["user-service", "order-service"],
|
||||
"FEATURE_FLAGS": ["enable-cache"]
|
||||
}
|
||||
''')
|
||||
build_time = models.JSONField(default=dict, verbose_name='构建时间信息', help_text='''
|
||||
{
|
||||
"total_duration": "300", # 总耗时(秒)
|
||||
"start_time": "2024-03-06 12:00:00", # 开始时间
|
||||
"end_time": "2024-03-06 12:05:00", # 结束时间
|
||||
"stages_time": [ # 各阶段时间信息
|
||||
{
|
||||
"name": "代码拉取",
|
||||
"start_time": "2024-03-06 12:00:00",
|
||||
"duration": "60" # 耗时(秒)
|
||||
}
|
||||
]
|
||||
}
|
||||
''')
|
||||
parameter_values = models.JSONField(default=dict, verbose_name='构建参数值')
|
||||
build_time = models.JSONField(default=dict, verbose_name='构建时间信息')
|
||||
|
||||
operator = models.ForeignKey('User', on_delete=models.SET_NULL, to_field='user_id', null=True, verbose_name='构建人')
|
||||
create_time = models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')
|
||||
@@ -329,7 +287,7 @@ class BuildHistory(models.Model):
|
||||
verbose_name = '构建历史'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-create_time']
|
||||
unique_together = ['task', 'build_number'] # 确保任务和构建号的组合唯一
|
||||
unique_together = ['task', 'build_number']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.task.name} #{self.build_number}"
|
||||
|
||||
@@ -154,6 +154,10 @@ class BuildTaskView(View):
|
||||
'external_script_directory': task.external_script_config.get('directory', '') if task.external_script_config else '',
|
||||
'external_script_branch': task.external_script_config.get('branch', '') if task.external_script_config else '',
|
||||
'external_script_token_id': task.external_script_config.get('token_id') if task.external_script_config else None,
|
||||
# 自动构建配置
|
||||
'auto_build_enabled': task.auto_build_enabled,
|
||||
'auto_build_branches': task.auto_build_branches,
|
||||
'webhook_token': task.webhook_token,
|
||||
'status': task.status,
|
||||
'building_status': task.building_status, # 添加构建状态字段
|
||||
'version': task.version,
|
||||
@@ -339,6 +343,11 @@ class BuildTaskView(View):
|
||||
parameters = data.get('parameters', [])
|
||||
notification_channels = data.get('notification_channels', [])
|
||||
|
||||
# 自动构建配置
|
||||
auto_build_enabled = data.get('auto_build_enabled', False)
|
||||
auto_build_branches = data.get('auto_build_branches', [])
|
||||
webhook_token = data.get('webhook_token', '')
|
||||
|
||||
# 外部脚本库配置
|
||||
use_external_script = data.get('use_external_script')
|
||||
external_script_config = None
|
||||
@@ -447,6 +456,11 @@ class BuildTaskView(View):
|
||||
'message': 'GitLab Token凭证不存在'
|
||||
})
|
||||
|
||||
# 如果启用自动构建但没有webhook_token,生成一个
|
||||
if auto_build_enabled and not webhook_token:
|
||||
import secrets
|
||||
webhook_token = secrets.token_urlsafe(32)
|
||||
|
||||
# 创建构建任务
|
||||
creator = User.objects.get(user_id=request.user_id)
|
||||
task = BuildTask.objects.create(
|
||||
@@ -462,6 +476,9 @@ class BuildTaskView(View):
|
||||
notification_channels=notification_channels,
|
||||
use_external_script=use_external_script,
|
||||
external_script_config=external_script_config,
|
||||
auto_build_enabled=auto_build_enabled,
|
||||
auto_build_branches=auto_build_branches,
|
||||
webhook_token=webhook_token,
|
||||
creator=creator
|
||||
)
|
||||
|
||||
@@ -505,6 +522,11 @@ class BuildTaskView(View):
|
||||
notification_channels = data.get('notification_channels')
|
||||
status = data.get('status')
|
||||
|
||||
# 自动构建配置
|
||||
auto_build_enabled = data.get('auto_build_enabled')
|
||||
auto_build_branches = data.get('auto_build_branches')
|
||||
webhook_token = data.get('webhook_token')
|
||||
|
||||
# 外部脚本库配置
|
||||
use_external_script = data.get('use_external_script')
|
||||
external_script_config = None
|
||||
@@ -691,6 +713,25 @@ class BuildTaskView(View):
|
||||
task.use_external_script = use_external_script
|
||||
task.external_script_config = external_script_config
|
||||
|
||||
# 更新自动构建配置
|
||||
if 'auto_build_enabled' in data:
|
||||
task.auto_build_enabled = auto_build_enabled
|
||||
|
||||
if auto_build_enabled:
|
||||
# 如果启用自动构建但没有webhook_token,生成一个
|
||||
if not task.webhook_token and not webhook_token:
|
||||
import secrets
|
||||
task.webhook_token = secrets.token_urlsafe(32)
|
||||
elif webhook_token is not None:
|
||||
task.webhook_token = webhook_token
|
||||
else:
|
||||
# 如果取消自动构建,清除所有相关配置
|
||||
task.auto_build_branches = []
|
||||
task.webhook_token = ''
|
||||
|
||||
if 'auto_build_branches' in data and auto_build_enabled:
|
||||
task.auto_build_branches = auto_build_branches
|
||||
|
||||
task.save()
|
||||
|
||||
return JsonResponse({
|
||||
|
||||
229
backend/apps/views/webhook.py
Normal file
229
backend/apps/views/webhook.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import json
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from ..models import BuildTask, BuildHistory, User
|
||||
from ..utils.builder import Builder
|
||||
import threading
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def execute_auto_build(task, branch, commit_id, commit_message, commit_author):
|
||||
"""执行自动构建任务"""
|
||||
try:
|
||||
# 生成构建号
|
||||
from django.db.models import F
|
||||
from ..views.build import generate_id
|
||||
|
||||
# 更新任务构建号并获取新的构建号
|
||||
task.last_build_number = F('last_build_number') + 1
|
||||
task.total_builds = F('total_builds') + 1
|
||||
task.building_status = 'building'
|
||||
task.save()
|
||||
|
||||
# 重新获取任务以获取更新后的构建号
|
||||
task.refresh_from_db()
|
||||
build_number = task.last_build_number
|
||||
|
||||
# 创建构建历史记录
|
||||
history = BuildHistory.objects.create(
|
||||
history_id=generate_id(),
|
||||
task=task,
|
||||
build_number=build_number,
|
||||
branch=branch,
|
||||
commit_id=commit_id,
|
||||
status='pending',
|
||||
requirement=f"自动构建: {commit_message[:200]} (by {commit_author})", # 使用提交信息作为构建需求
|
||||
parameter_values=get_default_parameter_values(task.parameters), # 使用默认参数值
|
||||
operator=None # 自动构建没有操作人
|
||||
)
|
||||
|
||||
builder = Builder(task, build_number, commit_id, history)
|
||||
builder.execute()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"自动构建执行失败: {str(e)}", exc_info=True)
|
||||
finally:
|
||||
# 无论构建成功、失败或异常,都将构建状态重置为空闲
|
||||
from django.db import transaction
|
||||
with transaction.atomic():
|
||||
BuildTask.objects.filter(task_id=task.task_id).update(building_status='idle')
|
||||
logger.info(f"任务 [{task.task_id}] 自动构建状态已重置为空闲")
|
||||
|
||||
def get_default_parameter_values(parameters):
|
||||
"""获取参数的默认值"""
|
||||
if not parameters:
|
||||
return {}
|
||||
|
||||
default_values = {}
|
||||
for param in parameters:
|
||||
param_name = param.get('name')
|
||||
default_list = param.get('default_values', [])
|
||||
if param_name and default_list:
|
||||
default_values[param_name] = default_list
|
||||
|
||||
return default_values
|
||||
|
||||
def is_branch_matched(branch_name, branch_list):
|
||||
"""检查分支是否在配置的分支列表中"""
|
||||
return branch_name in branch_list
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class GitLabWebhookView(View):
|
||||
"""GitLab Webhook处理视图"""
|
||||
|
||||
def post(self, request, task_id):
|
||||
"""处理GitLab Push Events"""
|
||||
try:
|
||||
# 验证token
|
||||
token = request.GET.get('token')
|
||||
if not token:
|
||||
logger.warning(f"Webhook请求缺少token: task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'error': 'Missing token'
|
||||
}, status=401)
|
||||
|
||||
# 查找对应的构建任务
|
||||
try:
|
||||
task = BuildTask.objects.get(task_id=task_id, webhook_token=token)
|
||||
except BuildTask.DoesNotExist:
|
||||
logger.warning(f"Webhook token验证失败: task_id={task_id}, token={token}")
|
||||
return JsonResponse({
|
||||
'error': 'Invalid task or token'
|
||||
}, status=404)
|
||||
|
||||
# 检查任务是否启用自动构建
|
||||
if not task.auto_build_enabled:
|
||||
logger.info(f"任务[{task_id}]未启用自动构建,忽略webhook")
|
||||
return JsonResponse({
|
||||
'message': 'Auto build is not enabled for this task'
|
||||
})
|
||||
|
||||
# 检查任务状态
|
||||
if task.status == 'disabled':
|
||||
logger.info(f"任务[{task_id}]已禁用,忽略webhook")
|
||||
return JsonResponse({
|
||||
'message': 'Task is disabled'
|
||||
})
|
||||
|
||||
# 是否有正在进行的构建
|
||||
if task.building_status == 'building':
|
||||
logger.info(f"任务[{task_id}]正在构建中,忽略webhook")
|
||||
return JsonResponse({
|
||||
'message': 'Build is already in progress'
|
||||
})
|
||||
|
||||
# 解析webhook数据
|
||||
try:
|
||||
webhook_data = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Webhook数据解析失败: task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'error': 'Invalid JSON data'
|
||||
}, status=400)
|
||||
|
||||
# 是否是push事件
|
||||
event_name = request.headers.get('X-Gitlab-Event', '')
|
||||
if event_name != 'Push Hook':
|
||||
logger.info(f"忽略非Push事件: {event_name}, task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'message': f'Ignored event: {event_name}'
|
||||
})
|
||||
|
||||
# 提取分支信息
|
||||
ref = webhook_data.get('ref', '')
|
||||
if not ref.startswith('refs/heads/'):
|
||||
logger.info(f"忽略非分支推送: {ref}, task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'message': f'Ignored non-branch push: {ref}'
|
||||
})
|
||||
|
||||
branch = ref.replace('refs/heads/', '')
|
||||
|
||||
# 检查分支是否在自动构建配置中
|
||||
if not is_branch_matched(branch, task.auto_build_branches):
|
||||
logger.info(f"分支[{branch}]不在自动构建配置中,忽略webhook: task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'message': f'Branch {branch} is not configured for auto build'
|
||||
})
|
||||
|
||||
# 提取提交信息
|
||||
commits = webhook_data.get('commits', [])
|
||||
if not commits:
|
||||
logger.warning(f"Webhook数据中没有提交信息: task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'error': 'No commits found in webhook data'
|
||||
}, status=400)
|
||||
|
||||
# 使用最新的提交
|
||||
latest_commit = commits[-1]
|
||||
commit_id = latest_commit.get('id', '')
|
||||
commit_message = latest_commit.get('message', '').strip()
|
||||
commit_author = latest_commit.get('author', {}).get('name', 'Unknown')
|
||||
|
||||
if not commit_id:
|
||||
logger.error(f"提交ID为空: task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'error': 'Commit ID is empty'
|
||||
}, status=400)
|
||||
|
||||
env_type = task.environment.type if task.environment else None
|
||||
if env_type not in ['development', 'testing']:
|
||||
logger.warning(f"环境类型[{env_type}]不支持自动构建: task_id={task_id}")
|
||||
return JsonResponse({
|
||||
'message': f'Environment type {env_type} does not support auto build'
|
||||
})
|
||||
|
||||
logger.info(f"触发自动构建: task_id={task_id}, branch={branch}, commit={commit_id[:8]}, author={commit_author}")
|
||||
|
||||
# 在新线程中执行自动构建
|
||||
build_thread = threading.Thread(
|
||||
target=execute_auto_build,
|
||||
args=(task, branch, commit_id, commit_message, commit_author)
|
||||
)
|
||||
build_thread.start()
|
||||
|
||||
return JsonResponse({
|
||||
'message': 'Auto build triggered successfully',
|
||||
'task_id': task_id,
|
||||
'branch': branch,
|
||||
'commit_id': commit_id[:8],
|
||||
'commit_message': commit_message[:100]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Webhook处理失败: {str(e)}", exc_info=True)
|
||||
return JsonResponse({
|
||||
'error': f'Internal server error: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
def get(self, request, task_id):
|
||||
"""用于测试webhook配置"""
|
||||
try:
|
||||
token = request.GET.get('token')
|
||||
if not token:
|
||||
return JsonResponse({
|
||||
'error': 'Missing token'
|
||||
}, status=401)
|
||||
|
||||
try:
|
||||
task = BuildTask.objects.get(task_id=task_id, webhook_token=token)
|
||||
except BuildTask.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'error': 'Invalid task or token'
|
||||
}, status=404)
|
||||
|
||||
return JsonResponse({
|
||||
'message': 'Webhook configuration is valid',
|
||||
'task_name': task.name,
|
||||
'auto_build_enabled': task.auto_build_enabled,
|
||||
'auto_build_branches': task.auto_build_branches
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Webhook测试失败: {str(e)}", exc_info=True)
|
||||
return JsonResponse({
|
||||
'error': f'Internal server error: {str(e)}'
|
||||
}, status=500)
|
||||
Reference in New Issue
Block a user