Files
LiteOps/backend/apps/views/build.py

1038 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import uuid
import hashlib
import logging
import threading
import time
from datetime import datetime
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 django.db import transaction
from django.db.models import Q, F
from ..models import BuildTask, BuildHistory, Project, Environment, GitlabTokenCredential, User, NotificationRobot
from ..utils.auth import jwt_auth_required
from ..utils.builder import Builder
from ..utils.permissions import get_user_permissions
logger = logging.getLogger('apps')
def generate_id():
"""生成唯一ID"""
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
def execute_build(task, build_number, commit_id, history):
"""执行构建任务"""
try:
builder = Builder(task, build_number, commit_id, history)
builder.execute()
finally:
# 无论构建成功、失败或异常,都将构建状态重置为空闲
from django.db import transaction
with transaction.atomic():
# 重新获取任务对象,确保获取最新状态
from ..models import BuildTask
BuildTask.objects.filter(task_id=task.task_id).update(building_status='idle')
logger.info(f"任务 [{task.task_id}] 构建状态已重置为空闲")
@method_decorator(csrf_exempt, name='dispatch')
class BuildTaskView(View):
@method_decorator(jwt_auth_required)
def get(self, request, task_id=None):
"""获取构建任务列表或单个任务详情"""
try:
# 获取当前用户的权限信息
user_permissions = get_user_permissions(request.user_id)
data_permissions = user_permissions.get('data', {})
# 检查用户是否有构建任务查看权限
function_permissions = user_permissions.get('function', {})
build_permissions = function_permissions.get('build_task', [])
if 'view' not in build_permissions:
logger.warning(f'用户[{request.user_id}]没有构建任务查看权限')
return JsonResponse({
'code': 403,
'message': '没有权限查看构建任务'
}, status=403)
# 如果请求参数中包含get_robots=true则返回通知机器人列表
if request.GET.get('get_robots') == 'true':
robots = NotificationRobot.objects.all()
robot_list = []
for robot in robots:
robot_list.append({
'robot_id': robot.robot_id,
'type': robot.type,
'name': robot.name,
})
return JsonResponse({
'code': 200,
'message': '获取通知机器人列表成功',
'data': robot_list
})
# 如果提供了task_id则返回单个任务详情
if task_id:
try:
task = BuildTask.objects.select_related(
'project',
'environment',
'git_token',
'creator'
).get(task_id=task_id)
# 检查是否有权限查看该任务(项目权限和环境权限)
# 项目权限检查
project_scope = data_permissions.get('project_scope', 'all')
if project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if task.project and task.project.project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{task.project.project_id}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限查看该项目的构建任务'
}, status=403)
# 环境权限检查
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_scope == 'custom':
permitted_environment_types = data_permissions.get('environment_types', [])
if task.environment and task.environment.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境类型[{task.environment.type}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限查看该环境的构建任务'
}, status=403)
# 获取最新的构建历史
latest_build = BuildHistory.objects.filter(task=task).order_by('-build_number').first()
# 获取通知机器人详情
notification_robots = []
if task.notification_channels:
robots = NotificationRobot.objects.filter(robot_id__in=task.notification_channels)
for robot in robots:
notification_robots.append({
'robot_id': robot.robot_id,
'type': robot.type,
'name': robot.name
})
return JsonResponse({
'code': 200,
'message': '获取任务详情成功',
'data': {
'task_id': task.task_id,
'name': task.name,
'project': {
'project_id': task.project.project_id,
'name': task.project.name,
'repository': task.project.repository
} if task.project else None,
'environment': {
'environment_id': task.environment.environment_id,
'name': task.environment.name,
'type': task.environment.type
} if task.environment else None,
'description': task.description,
'requirement': task.requirement,
'branch': task.branch,
'git_token': {
'credential_id': task.git_token.credential_id,
'name': task.git_token.name
} if task.git_token else None,
'stages': task.stages,
'parameters': task.parameters,
'notification_channels': task.notification_channels,
'notification_robots': notification_robots,
# 外部脚本库配置
'use_external_script': task.use_external_script,
'external_script_repo_url': task.external_script_config.get('repo_url', '') if task.external_script_config else '',
'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,
'last_build_number': task.last_build_number,
'total_builds': task.total_builds,
'success_builds': task.success_builds,
'failure_builds': task.failure_builds,
'last_build': {
'id': latest_build.history_id,
'number': latest_build.build_number,
'status': latest_build.status,
'time': latest_build.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'duration': '未完成' if latest_build.status in ['pending', 'running'] else str(latest_build.build_time.get('total_duration', 0)) + ''
} if latest_build else None,
'creator': {
'user_id': task.creator.user_id,
'name': task.creator.name
} if task.creator else None,
'create_time': task.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'update_time': task.update_time.strftime('%Y-%m-%d %H:%M:%S')
}
})
except BuildTask.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '任务不存在'
})
# 获取查询参数
project_id = request.GET.get('project_id')
environment_id = request.GET.get('environment_id')
name = request.GET.get('name')
# 构建查询条件
query = Q()
# 应用项目权限过滤
project_scope = data_permissions.get('project_scope', 'all')
if project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if not permitted_project_ids:
# 如果设置了自定义项目权限但列表为空,意味着没有权限查看任何项目
logger.info(f'用户[{request.user_id}]没有权限查看任何项目的构建任务')
return JsonResponse({
'code': 200,
'message': '获取任务列表成功',
'data': []
})
# 用户只能查看有权限的项目
if project_id and project_id != 'all':
# 如果指定了项目,检查是否有该项目的权限
if project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{project_id}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限查看该项目的构建任务'
}, status=403)
query &= Q(project__project_id=project_id)
else:
# 如果没有指定项目或选择了全部,则限制为有权限的项目
query &= Q(project__project_id__in=permitted_project_ids)
else:
# 如果有全部项目权限并且指定了项目ID
if project_id and project_id != 'all':
query &= Q(project__project_id=project_id)
# 应用环境权限过滤
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_scope == 'custom':
permitted_environment_types = data_permissions.get('environment_types', [])
if not permitted_environment_types:
# 如果设置了自定义环境权限但列表为空,意味着没有权限查看任何环境
logger.info(f'用户[{request.user_id}]没有权限查看任何环境的构建任务')
return JsonResponse({
'code': 200,
'message': '获取任务列表成功',
'data': []
})
if environment_id and environment_id != 'all':
# 如果指定了环境,需要检查是否在有权限的环境类型中
try:
env = Environment.objects.get(environment_id=environment_id)
if env.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境[{environment_id}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限查看该环境的构建任务'
}, status=403)
query &= Q(environment__environment_id=environment_id)
except Environment.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '环境不存在'
}, status=404)
else:
# 如果没有指定环境或选择了全部,则限制为有权限的环境类型
query &= Q(environment__type__in=permitted_environment_types)
else:
# 如果有全部环境权限并且指定了环境ID
if environment_id and environment_id != 'all':
query &= Q(environment__environment_id=environment_id)
# 添加其他查询条件
if name:
query &= Q(name__icontains=name)
# 查询任务列表
tasks = BuildTask.objects.select_related(
'project',
'environment',
'creator'
).filter(query)
task_list = []
for task in tasks:
# 获取最新的构建历史
latest_build = BuildHistory.objects.filter(task=task).order_by('-build_number').first()
task_list.append({
'task_id': task.task_id,
'name': task.name,
'project': {
'project_id': task.project.project_id,
'name': task.project.name
} if task.project else None,
'environment': {
'environment_id': task.environment.environment_id,
'name': task.environment.name,
'type': task.environment.type
} if task.environment else None,
'description': task.description,
'branch': task.branch,
'status': task.status,
'building_status': task.building_status,
'parameters': task.parameters,
'version': task.version,
'last_build_number': task.last_build_number,
'total_builds': task.total_builds,
'success_builds': task.success_builds,
'failure_builds': task.failure_builds,
'last_build': {
'id': latest_build.history_id,
'number': latest_build.build_number,
'status': latest_build.status,
'time': latest_build.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'duration': '未完成' if latest_build.status in ['pending', 'running'] else str(latest_build.build_time.get('total_duration', 0)) + ''
} if latest_build else None,
'creator': {
'user_id': task.creator.user_id,
'name': task.creator.name
} if task.creator else None,
'create_time': task.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'update_time': task.update_time.strftime('%Y-%m-%d %H:%M:%S')
})
return JsonResponse({
'code': 200,
'message': '获取任务列表成功',
'data': task_list
})
except Exception as e:
logger.error(f'获取构建任务失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(jwt_auth_required)
def post(self, request):
"""创建构建任务"""
try:
with transaction.atomic():
data = json.loads(request.body)
name = data.get('name')
project_id = data.get('project_id')
environment_id = data.get('environment_id')
description = data.get('description')
branch = data.get('branch', 'main')
git_token_id = data.get('git_token_id')
stages = data.get('stages', [])
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
if 'use_external_script' in data:
if use_external_script:
repo_url = data.get('external_script_repo_url', '').strip()
directory = data.get('external_script_directory', '').strip()
external_script_branch = data.get('external_script_branch', '').strip()
token_id = data.get('external_script_token_id')
# 验证外部脚本库必填字段
if not repo_url:
return JsonResponse({
'code': 400,
'message': '外部脚本库仓库地址不能为空'
})
if not directory:
return JsonResponse({
'code': 400,
'message': '外部脚本库存放目录不能为空'
})
if not external_script_branch:
return JsonResponse({
'code': 400,
'message': '外部脚本库分支名称不能为空'
})
external_script_config = {
'repo_url': repo_url,
'directory': directory,
'branch': external_script_branch,
'token_id': token_id
}
else:
external_script_config = {}
# 验证必要字段
if not all([name, project_id, environment_id]):
return JsonResponse({
'code': 400,
'message': '任务名称、项目和环境不能为空'
})
# 检查任务名称是否已存在
if BuildTask.objects.filter(name=name).exists():
return JsonResponse({
'code': 400,
'message': f'任务名称 "{name}" 已存在,请使用其他名称'
})
# 验证参数配置格式
if parameters:
import re
for param in parameters:
if not param.get('name') or not param.get('choices'):
return JsonResponse({
'code': 400,
'message': '参数名称和可选值不能为空'
})
# 验证参数名格式(大写字母、数字、下划线)
if not re.match(r'^[A-Z_][A-Z0-9_]*$', param['name']):
return JsonResponse({
'code': 400,
'message': f'参数名"{param["name"]}"格式不正确,只能包含大写字母、数字和下划线,且必须以字母或下划线开头'
})
# 验证通知机器人是否存在
if notification_channels:
existing_robots = set(NotificationRobot.objects.filter(
robot_id__in=notification_channels
).values_list('robot_id', flat=True))
invalid_robots = set(notification_channels) - existing_robots
if invalid_robots:
return JsonResponse({
'code': 400,
'message': f'以下机器人不存在: {", ".join(invalid_robots)}'
})
# 检查项目是否存在
try:
project = Project.objects.get(project_id=project_id)
except Project.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '项目不存在'
})
# 检查环境是否存在
try:
environment = Environment.objects.get(environment_id=environment_id)
except Environment.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '环境不存在'
})
# 检查GitLab Token凭证是否存在
git_token = None
if git_token_id:
try:
git_token = GitlabTokenCredential.objects.get(credential_id=git_token_id)
except GitlabTokenCredential.DoesNotExist:
return JsonResponse({
'code': 404,
'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(
task_id=generate_id(),
name=name,
project=project,
environment=environment,
description=description,
branch=branch,
git_token=git_token,
stages=stages,
parameters=parameters,
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
)
return JsonResponse({
'code': 200,
'message': '创建构建任务成功',
'data': {
'task_id': task.task_id,
'name': task.name
}
})
except Exception as e:
logger.error(f'创建构建任务失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(jwt_auth_required)
def put(self, request):
"""更新构建任务"""
try:
# 获取当前用户的权限信息
user_permissions = get_user_permissions(request.user_id)
data_permissions = user_permissions.get('data', {})
function_permissions = user_permissions.get('function', {})
build_permissions = function_permissions.get('build_task', [])
with transaction.atomic():
data = json.loads(request.body)
task_id = data.get('task_id')
name = data.get('name')
project_id = data.get('project_id')
environment_id = data.get('environment_id')
description = data.get('description')
branch = data.get('branch')
git_token_id = data.get('git_token_id')
stages = data.get('stages')
parameters = data.get('parameters')
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
if 'use_external_script' in data:
if use_external_script:
repo_url = data.get('external_script_repo_url', '').strip()
directory = data.get('external_script_directory', '').strip()
external_script_branch = data.get('external_script_branch', '').strip()
token_id = data.get('external_script_token_id')
# 验证外部脚本库必填字段
if not repo_url:
return JsonResponse({
'code': 400,
'message': '外部脚本库仓库地址不能为空'
})
if not directory:
return JsonResponse({
'code': 400,
'message': '外部脚本库存放目录不能为空'
})
if not external_script_branch:
return JsonResponse({
'code': 400,
'message': '外部脚本库分支名称不能为空'
})
external_script_config = {
'repo_url': repo_url,
'directory': directory,
'branch': external_script_branch,
'token_id': token_id
}
else:
external_script_config = {}
# 验证参数配置格式
if parameters:
import re
for param in parameters:
if not param.get('name') or not param.get('choices'):
return JsonResponse({
'code': 400,
'message': '参数名称和可选值不能为空'
})
# 验证参数名格式(大写字母、数字、下划线)
if not re.match(r'^[A-Z_][A-Z0-9_]*$', param['name']):
return JsonResponse({
'code': 400,
'message': f'参数名"{param["name"]}"格式不正确,只能包含大写字母、数字和下划线,且必须以字母或下划线开头'
})
if not task_id:
return JsonResponse({
'code': 400,
'message': '任务ID不能为空'
})
try:
task = BuildTask.objects.get(task_id=task_id)
except BuildTask.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '任务不存在'
})
# 如果只修改状态,需要检查是否有禁用权限
if status and len(data) == 2 and 'task_id' in data and 'status' in data:
if 'disable' not in build_permissions:
logger.warning(f'用户[{request.user_id}]没有禁用/启用任务权限')
return JsonResponse({
'code': 403,
'message': '没有权限禁用/启用任务'
}, status=403)
else:
# 否则检查是否有编辑权限
if 'edit' not in build_permissions:
logger.warning(f'用户[{request.user_id}]没有编辑任务权限')
return JsonResponse({
'code': 403,
'message': '没有权限编辑任务'
}, status=403)
# 项目权限检查
project_scope = data_permissions.get('project_scope', 'all')
if project_id and project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试编辑无权限的项目[{project_id}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限编辑该项目的构建任务'
}, status=403)
# 环境权限检查
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_id and environment_scope == 'custom':
try:
env = Environment.objects.get(environment_id=environment_id)
permitted_environment_types = data_permissions.get('environment_types', [])
if env.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试编辑无权限的环境类型[{env.type}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限编辑该环境的构建任务'
}, status=403)
except Environment.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '环境不存在'
})
# 更新项目关联
if project_id:
try:
project = Project.objects.get(project_id=project_id)
task.project = project
except Project.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '项目不存在'
})
# 更新环境关联
if environment_id:
try:
environment = Environment.objects.get(environment_id=environment_id)
task.environment = environment
except Environment.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '环境不存在'
})
# 更新GitLab Token凭证关联
if 'git_token_id' in data:
if git_token_id:
try:
git_token = GitlabTokenCredential.objects.get(credential_id=git_token_id)
task.git_token = git_token
except GitlabTokenCredential.DoesNotExist:
return JsonResponse({
'code': 404,
'message': 'GitLab Token凭证不存在'
})
else:
task.git_token = None
# 更新其他字段
if 'name' in data:
# 检查任务名称是否已存在
if BuildTask.objects.filter(name=name).exclude(task_id=task_id).exists():
return JsonResponse({
'code': 400,
'message': f'任务名称 "{name}" 已存在,请使用其他名称'
})
task.name = name
if 'description' in data:
task.description = description
if 'branch' in data:
task.branch = branch
if 'stages' in data:
task.stages = stages
if 'parameters' in data:
task.parameters = parameters
if 'notification_channels' in data:
# 验证通知机器人是否存在
existing_robots = set(NotificationRobot.objects.filter(
robot_id__in=notification_channels
).values_list('robot_id', flat=True))
invalid_robots = set(notification_channels) - existing_robots
if invalid_robots:
return JsonResponse({
'code': 400,
'message': f'以下机器人不存在: {", ".join(invalid_robots)}'
})
task.notification_channels = notification_channels
if 'status' in data:
task.status = status
# 更新外部脚本库配置
if 'use_external_script' in data:
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({
'code': 200,
'message': '更新构建任务成功'
})
except Exception as e:
logger.error(f'更新构建任务失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(jwt_auth_required)
def delete(self, request):
"""删除构建任务"""
try:
with transaction.atomic():
data = json.loads(request.body)
task_id = data.get('task_id')
if not task_id:
return JsonResponse({
'code': 400,
'message': '任务ID不能为空'
})
try:
task = BuildTask.objects.get(task_id=task_id)
task.delete()
return JsonResponse({
'code': 200,
'message': '删除构建任务成功'
})
except BuildTask.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '任务不存在'
})
except Exception as e:
logger.error(f'删除构建任务失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(csrf_exempt, name='dispatch')
class BuildExecuteView(View):
@method_decorator(jwt_auth_required)
def post(self, request):
"""执行构建"""
try:
# 获取当前用户的权限信息
user_permissions = get_user_permissions(request.user_id)
data_permissions = user_permissions.get('data', {})
# 检查用户是否有执行构建权限
function_permissions = user_permissions.get('function', {})
build_permissions = function_permissions.get('build_task', [])
if 'execute' not in build_permissions:
logger.warning(f'用户[{request.user_id}]没有执行构建权限')
return JsonResponse({
'code': 403,
'message': '没有权限执行构建任务'
}, status=403)
data = json.loads(request.body)
task_id = data.get('task_id')
branch = data.get('branch') # 获取用户选择的分支
commit_id = data.get('commit_id')
version = data.get('version')
requirement = data.get('requirement')
parameter_values = data.get('parameter_values', {})
if not task_id:
return JsonResponse({
'code': 400,
'message': '任务ID不能为空'
})
try:
task = BuildTask.objects.select_related('project', 'environment').get(task_id=task_id)
except BuildTask.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '任务不存在'
})
# 检查是否有权限执行该任务
# 项目权限检查
project_scope = data_permissions.get('project_scope', 'all')
if project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if task.project and task.project.project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试执行无权限的项目[{task.project.project_id}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限执行该项目的构建任务'
}, status=403)
# 环境权限检查
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_scope == 'custom':
permitted_environment_types = data_permissions.get('environment_types', [])
if task.environment and task.environment.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试执行无权限的环境类型[{task.environment.type}]的构建任务')
return JsonResponse({
'code': 403,
'message': '没有权限执行该环境的构建任务'
}, status=403)
# 根据环境类型检查必要参数
env_type = task.environment.type if task.environment else None
if env_type in ['development', 'testing']:
# 开发环境和测试环境需要分支和commit_id
if not branch:
return JsonResponse({
'code': 400,
'message': '分支不能为空'
})
if not commit_id:
return JsonResponse({
'code': 400,
'message': 'Commit ID不能为空'
})
elif env_type in ['staging', 'production']:
if not version:
return JsonResponse({
'code': 400,
'message': '版本号不能为空'
})
parts = version.split('_')
if len(parts) == 2 and len(parts[1]) >= 8:
commit_id = parts[1]
else:
return JsonResponse({
'code': 400,
'message': '版本号格式不正确应为YYYYMMDDHHmmSS_commitId'
})
if not requirement:
return JsonResponse({
'code': 400,
'message': '构建需求描述不能为空'
})
# 验证参数值是否合法
if task.parameters and parameter_values:
task_parameters = {p['name']: p['choices'] for p in task.parameters}
for param_name, selected_values in parameter_values.items():
if param_name not in task_parameters:
return JsonResponse({
'code': 400,
'message': f'未定义的参数: {param_name}'
})
for value in selected_values:
if value not in task_parameters[param_name]:
return JsonResponse({
'code': 400,
'message': f'参数{param_name}的值"{value}"不在可选范围内'
})
# 检查任务的构建状态
if task.building_status == 'building':
return JsonResponse({
'code': 400,
'message': '当前任务正在构建中,请等待构建完成后再试'
})
running_build = BuildHistory.objects.filter(
task_id=task_id,
status__in=['pending', 'running']
).exists()
if running_build:
# 如果有正在进行的构建但building_status不是building则修正状态
BuildTask.objects.filter(task_id=task_id).update(building_status='building')
return JsonResponse({
'code': 400,
'message': '当前任务有正在进行的构建,请等待构建完成后再试'
})
if task.status == 'disabled':
return JsonResponse({
'code': 400,
'message': '任务已禁用'
})
# 生成构建号
build_number = task.last_build_number + 1
# 创建构建历史记录
history = BuildHistory.objects.create(
history_id=generate_id(),
task=task,
build_number=build_number,
branch=branch if branch else '', # 对于预发布和生产环境,分支为空
commit_id=commit_id,
version=version if version else None, # 对于预发布和生产环境,使用传入的版本号
status='pending', # 初始状态为等待中
requirement=requirement,
parameter_values=parameter_values,
operator=User.objects.get(user_id=request.user_id) # 记录构建人
)
# 更新任务状态、构建号和构建状态
BuildTask.objects.filter(task_id=task_id).update(
last_build_number=build_number,
total_builds=F('total_builds') + 1,
building_status='building' # 设置为构建中状态
)
# 在新线程中执行构建
build_thread = threading.Thread(
target=execute_build,
args=(task, build_number, commit_id, history)
)
build_thread.start()
return JsonResponse({
'code': 200,
'message': '开始构建',
'data': {
'build_number': build_number,
'history_id': history.history_id
}
})
except Exception as e:
logger.error(f'执行构建失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(jwt_auth_required)
def put(self, request):
"""停止构建"""
try:
data = json.loads(request.body)
history_id = data.get('history_id')
if not history_id:
return JsonResponse({
'code': 400,
'message': '历史ID不能为空'
})
try:
history = BuildHistory.objects.get(history_id=history_id)
except BuildHistory.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '构建历史不存在'
})
# 只有进行中的构建可以停止
if history.status not in ['pending', 'running']:
return JsonResponse({
'code': 400,
'message': '只能停止进行中的构建'
})
# 更新构建状态为terminated
history.status = 'terminated'
# 如果构建日志存在,追加终止消息
if history.build_log:
history.build_log += "\n[系统] 构建被手动终止\n"
else:
history.build_log = "[系统] 构建被手动终止\n"
# 更新构建时间
if not history.build_time:
history.build_time = {}
if 'start_time' in history.build_time and 'total_duration' not in history.build_time:
# 计算从开始到现在的持续时间
start_time = datetime.strptime(history.build_time['start_time'], '%Y-%m-%d %H:%M:%S')
duration = int((datetime.now() - start_time).total_seconds())
history.build_time['total_duration'] = str(duration)
history.build_time['end_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
history.save()
# 更新任务统计信息和构建状态
BuildTask.objects.filter(task_id=history.task.task_id).update(
building_status='idle' # 重置构建状态为空闲
)
return JsonResponse({
'code': 200,
'message': '构建已终止'
})
except Exception as e:
logger.error(f'停止构建失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})