Scert/backend/internal/httpapi/task_api.go
xuans a8e7de7568 保存证书恢复后台任务接入准备
保存恢复任务依赖注入和任务配置检查的中间状态,便于继续完成异步恢复链路。
2026-10-03 01:42:49 +08:00

745 lines
28 KiB
Go

package httpapi
import (
"context"
"database/sql"
"errors"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"git.pcux.cn/xuans/Scert/backend/internal/repository"
)
func (s *Server) issueDomainAsTask(c *gin.Context) {
s.submitDomainTask(c, "certificate_issue")
}
func (s *Server) submitDomainTask(c *gin.Context, taskType string) {
s.submitPreparedDomainTask(c, taskType, nil)
}
type preparedCertificateTask struct {
operation func(context.Context, string) (certificate.IssueResult, deployment.Report, error)
cleanup func()
}
func (s *Server) submitPreparedDomainTask(c *gin.Context, taskType string, prepared *preparedCertificateTask) {
transferred := false
defer func() {
if !transferred && prepared != nil && prepared.cleanup != nil {
prepared.cleanup()
}
}()
if (taskType == "certificate_issue" && s.issueCertificate == nil) ||
(taskType == "certificate_deploy" && s.deployCertificateTask == nil) ||
(taskType == "certificate_validate" && s.validateCertificateTask == nil) ||
(taskType == "certificate_restore" && s.restoreVersionTask == nil) {
s.writeError(c, http.StatusNotImplemented, "证书申请接口尚未配置", errors.New("证书申请工作流未配置"))
return
}
if taskRepositoryUnavailable(s.tasks) {
s.writeError(c, http.StatusNotImplemented, "证书任务接口尚未配置", errors.New("任务仓储未配置"))
return
}
if s.projectResources == nil || s.projectMembers == nil {
s.writeError(c, http.StatusServiceUnavailable, "证书任务权限服务尚未就绪", errors.New("项目资源或项目权限服务未配置"))
return
}
domainID := strings.TrimSpace(c.Param("id"))
resource, err := s.projectResources.GetVisibleDomain(c.Request.Context(), c.GetString(authenticatedActorKey), domainID)
if err != nil {
if errors.Is(err, repository.ErrProjectAccessDenied) || errors.Is(err, sql.ErrNoRows) {
s.writeError(c, http.StatusForbidden, "无权管理此项目的证书任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "读取域名失败", err)
return
}
authorizer, ok := s.projectMembers.(repository.ProjectAuthorizer)
if !ok {
s.writeError(c, http.StatusServiceUnavailable, "证书任务权限服务尚未就绪", errors.New("项目授权服务未配置"))
return
}
if taskType == "certificate_issue" || taskType == "certificate_import" {
if err := authorizer.AuthorizeProject(c.Request.Context(), c.GetString(authenticatedActorKey), resource.ResourceScope.Normalize().ProjectID, domain.PermissionCertificateManage); err != nil {
if errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusForbidden, "无权管理此项目的证书任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "检查证书任务权限失败", err)
return
}
}
permission := domain.PermissionDeploymentExecute
if taskType == "certificate_validate" {
permission = domain.PermissionPrivateKeyRead
}
if err := authorizer.AuthorizeProject(c.Request.Context(), c.GetString(authenticatedActorKey), resource.ResourceScope.Normalize().ProjectID, permission); err != nil {
if errors.Is(err, repository.ErrProjectAccessDenied) {
message := "无权执行此项目的证书部署任务"
if taskType == "certificate_validate" {
message = "无权读取此项目的私钥进行证书校验"
}
s.writeError(c, http.StatusForbidden, message, nil)
return
}
s.writeError(c, http.StatusInternalServerError, "检查证书部署权限失败", err)
return
}
if taskType == "certificate_issue" && !s.authorizeDNSCredentialUse(c, resource.ResourceScope.Normalize().ProjectID) {
return
}
s.issueTaskMu.Lock()
idempotencyKey := strings.TrimSpace(c.GetHeader("Idempotency-Key"))
if idempotencyKey != "" {
if !validTaskIdempotencyKey(idempotencyKey) {
s.issueTaskMu.Unlock()
s.writeError(c, http.StatusBadRequest, "幂等键格式无效", errors.New("幂等键长度或字符无效"))
return
}
if existing, err := s.tasks.FindTaskByIdempotencyKey(c.Request.Context(), domainID, idempotencyKey); err == nil {
if existing.TaskType != taskType {
s.issueTaskMu.Unlock()
s.writeError(c, http.StatusConflict, "幂等键已用于另一种操作", nil)
return
}
status := http.StatusOK
if isActiveTaskStatus(existing.Status) {
status = http.StatusConflict
}
c.JSON(status, gin.H{"task": existing, "replayed": true})
s.issueTaskMu.Unlock()
return
} else if !errors.Is(err, sql.ErrNoRows) {
s.issueTaskMu.Unlock()
s.writeError(c, http.StatusInternalServerError, "检查幂等任务失败", err)
return
}
}
if active, err := s.tasks.FindActiveTaskByDomain(c.Request.Context(), domainID, taskType); err == nil {
s.issueTaskMu.Unlock()
c.JSON(http.StatusConflict, gin.H{"error": "该域名已有正在执行的证书任务", "task": active})
return
} else if !errors.Is(err, sql.ErrNoRows) {
s.issueTaskMu.Unlock()
s.writeError(c, http.StatusInternalServerError, "检查正在执行的任务失败", err)
return
}
now := time.Now().UTC()
task := repository.Task{
HistoryScope: repository.HistoryScope{ScopeType: repository.ScopeProject, ProjectID: resource.ResourceScope.Normalize().ProjectID},
OperatorID: s.currentOperatorID(c),
ID: domain.NewID(), DomainID: domainID, TaskType: taskType, Status: "queued", Stage: "queued",
Message: "等待执行证书任务", IdempotencyKey: idempotencyKey, CreatedAt: now, UpdatedAt: now,
}
if err := s.tasks.CreateTask(c.Request.Context(), task); err != nil {
if idempotencyKey != "" {
if existing, lookupErr := s.tasks.FindTaskByIdempotencyKey(c.Request.Context(), domainID, idempotencyKey); lookupErr == nil {
if existing.TaskType != taskType {
s.issueTaskMu.Unlock()
s.writeError(c, http.StatusConflict, "幂等键已用于另一种操作", nil)
return
}
status := http.StatusOK
if isActiveTaskStatus(existing.Status) {
status = http.StatusConflict
}
c.JSON(status, gin.H{"task": existing, "replayed": true})
s.issueTaskMu.Unlock()
return
}
}
s.issueTaskMu.Unlock()
if errors.Is(err, repository.ErrTaskAlreadyRunning) {
c.JSON(http.StatusConflict, gin.H{"error": "该域名已有正在执行的同类证书任务"})
return
}
s.writeError(c, http.StatusInternalServerError, "创建证书任务失败", err)
return
}
s.issueTaskMu.Unlock()
attempt := repository.TaskAttempt{
ID: domain.NewID(), TaskID: task.ID, AttemptNumber: 1, Stage: taskType, Status: "running",
Message: "等待执行证书任务", StartedAt: &now, CreatedAt: now,
}
if err := s.tasks.CreateAttempt(c.Request.Context(), attempt); err != nil {
_ = s.tasks.UpdateTask(context.Background(), task.ID, "failed", taskType, "创建任务尝试失败", &now, &now)
s.writeError(c, http.StatusInternalServerError, "创建任务尝试失败", err)
return
}
if prepared != nil {
transferred = s.enqueueCertificateTask(c, task, attempt, taskType, prepared.operation,
taskType, "正在导入证书并部署", "证书导入和部署完成", prepared.cleanup)
if transferred {
writeAcceptedCertificateTask(c, task)
}
return
}
if taskType == "certificate_deploy" {
s.executeCertificateDeployTask(c, task, attempt, taskType)
return
}
if taskType == "certificate_validate" {
s.executeCertificateValidationTask(c, task, attempt)
return
}
s.executeCertificateIssueTask(c, task, attempt, taskType)
}
func (s *Server) executeCertificateIssueTask(c *gin.Context, task repository.Task, attempt repository.TaskAttempt, auditAction string) {
if s.enqueueCertificateTask(c, task, attempt, auditAction, s.issueCertificate, "certificate_issue", "正在申请证书并部署", "证书申请和部署完成", nil) {
writeAcceptedCertificateTask(c, task)
}
}
func (s *Server) executeCertificateDeployTask(c *gin.Context, task repository.Task, attempt repository.TaskAttempt, auditAction string) {
if s.enqueueCertificateTask(c, task, attempt, auditAction, s.deployCertificateTask, "certificate_deploy", "正在部署证书", "证书部署完成", nil) {
writeAcceptedCertificateTask(c, task)
}
}
func (s *Server) runCertificateTask(ctx context.Context, task repository.Task, attempt repository.TaskAttempt, auditAction string, operation func(context.Context, string) (certificate.IssueResult, deployment.Report, error), stage, runningMessage, successMessage string) {
s.runCertificateOperation(ctx, task, attempt, auditAction, func(ctx context.Context, id string) (certificateTaskResult, error) {
result, report, err := operation(ctx, id)
return certificateTaskResult{Certificate: result, Deployment: &report}, err
}, stage, runningMessage, successMessage)
}
type certificateTaskResult struct {
Certificate certificate.IssueResult
Deployment *deployment.Report
Validation *certificate.ValidationResult
}
func (s *Server) runCertificateOperation(ctx context.Context, task repository.Task, attempt repository.TaskAttempt, auditAction string, operation func(context.Context, string) (certificateTaskResult, error), stage, runningMessage, successMessage string) {
executionCtx := deployment.WithTransactionContext(ctx, deployment.TransactionContext{
TaskID: task.ID, AttemptID: attempt.ID, AttemptNumber: attempt.AttemptNumber,
})
now := time.Now().UTC()
if err := executionCtx.Err(); err != nil {
s.finishTaskContext(task, attempt, err)
return
}
if err := s.tasks.UpdateTask(executionCtx, task.ID, "running", stage, runningMessage, &now, nil); err != nil {
failureMessage := "任务启动失败"
if s.finishCancelledTask(task, attempt, executionCtx) {
return
}
attempt.Stage = stage
s.finishTaskBeforeExecution(task, attempt, "failed", failureMessage)
return
}
if s.audit != nil {
if err := s.audit.CreateAuditLog(executionCtx, repository.AuditLog{
TaskID: task.ID, OperatorID: task.OperatorID,
Action: auditAction, ResourceType: "domain", ResourceID: task.DomainID,
Status: "running", Message: runningMessage,
}); err != nil {
failureMessage := "任务启动审计记录失败"
if s.finishCancelledTask(task, attempt, executionCtx) {
return
}
attempt.Stage = "record"
s.finishTaskBeforeExecution(task, attempt, "failed", failureMessage)
return
}
}
if s.finishCancelledTask(task, attempt, executionCtx) {
return
}
executionCtx = certificate.WithProgressReporter(executionCtx, func(progressCtx context.Context, progressStage, progressMessage string) error {
return s.tasks.UpdateTask(progressCtx, task.ID, "running", progressStage, progressMessage, &now, nil)
})
if err := s.authorizeCertificateTaskExecution(executionCtx, task, attempt); err != nil {
if s.finishCancelledTask(task, attempt, executionCtx) {
return
}
attempt.Stage = "authorize"
s.finishTaskBeforeExecution(task, attempt, "failed", "任务执行前权限或资源归属校验未通过,未执行证书操作")
return
}
if s.finishCancelledTask(task, attempt, executionCtx) {
return
}
result, issueErr := operation(executionCtx, task.DomainID)
certificateResult := result.Certificate
var deploymentReport deployment.Report
if result.Deployment != nil {
deploymentReport = *result.Deployment
}
if task.TaskType == "certificate_import" {
deploymentReport.CertificateImported = issueErr == nil || (certificateResult.DomainID == task.DomainID &&
certificateResult.Saved.CertificatePath != "" && certificateResult.Saved.FullChainPath != "" && certificateResult.Saved.PrivateKeyPath != "")
}
finishCtx, finishCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer finishCancel()
deploymentReport = sanitizeDeploymentReport(deploymentReport)
finished := time.Now().UTC()
status := "success"
message := successMessage
partial := deploymentReport.Status == "partial"
if issueErr != nil {
status = "failed"
message = certificateTaskErrorMessage(issueErr)
if partial {
status = "partial_success"
message = "\u8bc1\u4e66\u5df2\u7b7e\u53d1\uff0c\u4f46\u4ec5\u90e8\u5206\u670d\u52a1\u5668\u90e8\u7f72\u6210\u529f"
}
if s.logger != nil {
s.logger.Error("证书任务执行失败", "task_id", task.ID, "domain_id", task.DomainID, "error", issueErr)
}
if errors.Is(issueErr, context.Canceled) && !partial && deploymentReport.RecoveryStatus != "unconfirmed" && deploymentReport.RecoveryStatus != "failed" {
status = "cancelled"
}
} else if partial {
status = "partial_success"
}
if task.TaskType == "certificate_import" && issueErr != nil {
message = "证书导入失败"
if deploymentReport.CertificateImported {
message = "证书已导入,但部署未完成。请检查部署结果后重试部署,无需重新导入。"
}
}
if task.TaskType == "certificate_validate" && issueErr != nil {
message = "证书校验失败,请检查当前证书、私钥匹配及证书链"
if status == "cancelled" {
message = "证书校验已取消"
}
if errors.Is(issueErr, context.DeadlineExceeded) {
message = "证书校验超时"
}
}
s.taskMu.Lock()
defer s.taskMu.Unlock()
if persisted, err := s.tasks.GetTask(finishCtx, task.ID); err != nil {
status, message = "failed", "无法确认任务收尾状态,请核查部署结果后再重试"
issueErr = errors.Join(issueErr, err)
} else if persisted.Status == "cancelled" || persisted.Status == "cancelling" {
// 取消请求只是意图,不能覆盖已经完成的外部操作,也不能隐藏失败及部分成功。
if issueErr == nil && !partial {
message = "任务已完成,取消请求未撤销已完成的操作"
} else if status == "cancelled" {
status = "cancelled"
message = "任务已停止;已执行操作是否回滚请查看部署结果"
if task.TaskType == "certificate_validate" {
message = "证书校验已停止,未保存通过结果"
}
}
}
completion := repository.TaskCompletion{
TaskID: task.ID, AttemptID: attempt.ID, Status: status, Stage: stage,
Message: message, StartedAt: &now, FinishedAt: finished,
AuditAction: auditAction,
}
if result.Deployment != nil {
completion.Report = &deploymentReport
}
if status == "success" {
completion.Validation = result.Validation
}
if err := s.tasks.FinishTaskAttempt(finishCtx, completion); err != nil && s.logger != nil {
s.logger.Error("保存任务及尝试终态失败,保留中断恢复状态", "task_id", task.ID, "error", err)
}
}
func (s *Server) finishCancelledTask(task repository.Task, attempt repository.TaskAttempt, ctx context.Context) bool {
if ctx.Err() != nil {
s.finishTaskContext(task, attempt, ctx.Err())
return true
}
checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
persisted, err := s.tasks.GetTask(checkCtx, task.ID)
if err != nil {
s.finishTaskBeforeExecution(task, attempt, "failed", "无法确认任务状态,未执行外部操作")
return true
}
if persisted.Status == "cancelled" || persisted.Status == "cancelling" {
s.finishTaskContext(task, attempt, context.Canceled)
return true
}
return false
}
const certificateTaskTimeout = 30 * time.Minute
func (s *Server) registerTaskCancel(taskID string, cancel context.CancelFunc) {
s.taskMu.Lock()
defer s.taskMu.Unlock()
if s.taskCancels == nil {
s.taskCancels = make(map[string]context.CancelFunc)
}
s.taskCancels[taskID] = cancel
}
func (s *Server) unregisterTaskCancel(taskID string) {
s.taskMu.Lock()
defer s.taskMu.Unlock()
delete(s.taskCancels, taskID)
delete(s.asyncTasks, taskID)
}
func (s *Server) taskWorkflowContext(c *gin.Context) context.Context {
ctx := repository.WithTaskCancellationHooks(c.Request.Context(), repository.TaskCancellationHooks{
Register: s.registerTaskCancel,
Unregister: s.unregisterTaskCancel,
})
return repository.WithTaskOperator(ctx, s.currentOperatorID(c))
}
func taskRepositoryUnavailable(tasks repository.TaskRepository) bool {
if tasks == nil {
return true
}
value := reflect.ValueOf(tasks)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return value.IsNil()
default:
return false
}
}
func (s *Server) projectTaskReader(c *gin.Context) (repository.ProjectTaskReader, bool) {
if taskRepositoryUnavailable(s.tasks) {
s.writeError(c, http.StatusNotImplemented, "任务管理接口尚未配置", errors.New("任务仓储未配置"))
return nil, false
}
reader, ok := s.tasks.(repository.ProjectTaskReader)
if !ok {
s.writeError(c, http.StatusNotImplemented, "任务权限接口尚未配置", errors.New("项目任务读取服务未配置"))
return nil, false
}
return reader, true
}
func (s *Server) cancelTask(c *gin.Context) {
reader, ok := s.projectTaskReader(c)
if !ok {
return
}
taskID := strings.TrimSpace(c.Param("id"))
item, err := reader.GetVisibleTask(c.Request.Context(), c.GetString(authenticatedActorKey), taskID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusForbidden, "无权访问此任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "读取任务失败", err)
return
}
if !isCancellableTaskStatus(item.Status) {
c.JSON(http.StatusConflict, gin.H{"error": "任务当前无法取消", "task": item})
return
}
if item.TaskType == "renewal_run" {
c.JSON(http.StatusConflict, gin.H{"error": "该任务由证书工作流控制,不能单独修改取消状态", "task": item})
return
}
if item.TaskType == "management_renewal" {
c.JSON(http.StatusConflict, gin.H{"error": "管理证书任务由续期调度控制,不能单独修改取消状态", "task": item})
return
}
if item.TaskType == "renewal_check" {
if err := s.authorizeRenewalOperator(c.Request.Context(), s.currentOperatorID(c)); err != nil {
s.writeError(c, http.StatusForbidden, "只有当前有效的超级管理员可以取消全局续期检查", nil)
return
}
} else if !s.authorizeProjectPermissions(c, item.ProjectID, domain.PermissionTaskCancel) {
return
}
s.taskMu.Lock()
if s.asyncTasks[taskID] {
if err := reader.RequestCancelVisibleTask(c.Request.Context(), c.GetString(authenticatedActorKey), taskID); err != nil {
s.taskMu.Unlock()
s.writeError(c, http.StatusConflict, "任务状态已变化,无法取消", err)
return
}
s.taskCancels[taskID]()
s.taskMu.Unlock()
item.Status = "cancelling"
item.Message = "正在取消任务并等待部署收尾"
if s.audit != nil {
_ = s.audit.CreateAuditLog(c.Request.Context(), repository.AuditLog{
TaskID: taskID, OperatorID: s.currentOperatorID(c), Action: "task_cancel",
ResourceType: "task", ResourceID: taskID, Status: "success", Message: item.Message,
})
}
c.JSON(http.StatusAccepted, gin.H{"task": item})
return
}
s.taskMu.Unlock()
finished := time.Now().UTC()
if err := reader.CancelVisibleTask(c.Request.Context(), c.GetString(authenticatedActorKey), taskID, "任务已由用户取消", &finished); err != nil {
if errors.Is(err, repository.ErrProjectAccessDenied) || errors.Is(err, sql.ErrNoRows) {
s.writeError(c, http.StatusForbidden, "无权访问此任务", nil)
return
}
s.writeError(c, http.StatusConflict, "取消任务失败", err)
return
}
s.taskMu.Lock()
cancel := s.taskCancels[taskID]
s.taskMu.Unlock()
if cancel != nil {
cancel()
}
item.Status = "cancelled"
item.Message = "任务已由用户取消"
item.FinishedAt = &finished
item.UpdatedAt = finished
if s.audit != nil {
_ = s.audit.CreateAuditLog(context.Background(), repository.AuditLog{
TaskID: taskID, OperatorID: s.currentOperatorID(c),
Action: "task_cancel", ResourceType: "task", ResourceID: taskID, Status: "success", Message: item.Message,
})
}
c.JSON(http.StatusOK, gin.H{"task": item})
}
func (s *Server) retryTask(c *gin.Context) {
reader, ok := s.projectTaskReader(c)
if !ok {
return
}
actorID := c.GetString(authenticatedActorKey)
taskID := strings.TrimSpace(c.Param("id"))
item, err := reader.GetVisibleTask(c.Request.Context(), actorID, taskID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusForbidden, "无权访问此任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "读取任务失败", err)
return
}
s.taskMu.Lock()
workerActive := s.asyncTasks[taskID]
s.taskMu.Unlock()
if workerActive {
s.writeError(c, http.StatusConflict, "任务仍在执行或收尾,暂时不能重试", nil)
return
}
if item.TaskType != "certificate_issue" && item.TaskType != "certificate_deploy" {
c.JSON(http.StatusConflict, gin.H{"error": "当前任务类型不支持手动重试", "task": item})
return
}
retryable := item.Status == "failed" || item.Status == "cancelled"
if item.TaskType == "certificate_deploy" && item.Status == "partial_success" {
retryable = true
}
if !retryable {
c.JSON(http.StatusConflict, gin.H{"error": "仅失败、已取消或部分成功的证书任务可以重试", "task": item})
return
}
var runTask func(*gin.Context, repository.Task, repository.TaskAttempt, string)
if item.TaskType == "certificate_deploy" {
if s.deployCertificateTask == nil {
s.writeError(c, http.StatusNotImplemented, "证书部署接口尚未配置", errors.New("证书部署工作流未配置"))
return
}
runTask = s.executeCertificateDeployTask
} else {
if s.issueCertificate == nil {
s.writeError(c, http.StatusNotImplemented, "证书申请接口尚未配置", errors.New("证书申请工作流未配置"))
return
}
runTask = s.executeCertificateIssueTask
}
authorizer, ok := s.projectMembers.(repository.ProjectAuthorizer)
if !ok {
s.writeError(c, http.StatusServiceUnavailable, "任务重试权限服务尚未就绪", errors.New("项目授权服务未配置"))
return
}
projectID := item.ProjectID
for _, permission := range []domain.ProjectPermission{
domain.PermissionTaskRetry,
domain.PermissionCertificateManage,
domain.PermissionDeploymentExecute,
} {
if err := authorizer.AuthorizeProject(c.Request.Context(), actorID, projectID, permission); err != nil {
if errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusForbidden, "无权重试此项目的证书任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "检查任务重试权限失败", err)
return
}
}
if item.TaskType == "certificate_issue" && !s.authorizeDNSCredentialUse(c, projectID) {
return
}
startedAt := time.Now().UTC()
retryTask, attempt, err := reader.RetryVisibleTask(c.Request.Context(), actorID, taskID, &startedAt)
if err != nil {
switch {
case errors.Is(err, repository.ErrProjectAccessDenied), errors.Is(err, sql.ErrNoRows):
s.writeError(c, http.StatusForbidden, "无权重试此任务", nil)
case errors.Is(err, repository.ErrTaskNotRetryable):
s.writeError(c, http.StatusConflict, "任务状态已变化,当前无法重试", nil)
default:
s.writeError(c, http.StatusInternalServerError, "创建任务重试失败", err)
}
return
}
runTask(c, retryTask, attempt, item.TaskType+"_retry")
}
func validTaskIdempotencyKey(value string) bool {
if len(value) < 8 || len(value) > 128 {
return false
}
for _, char := range value {
if (char < 'a' || char > 'z') && (char < 'A' || char > 'Z') && (char < '0' || char > '9') && char != '-' && char != '_' && char != '.' {
return false
}
}
return true
}
func isActiveTaskStatus(status string) bool {
switch status {
case "queued", "running", "retry_wait", "cancelling":
return true
default:
return false
}
}
func isCancellableTaskStatus(status string) bool {
switch status {
case "queued", "running", "retry_wait", "cancelling":
return true
default:
return false
}
}
func (s *Server) listTasks(c *gin.Context) {
reader, ok := s.projectTaskReader(c)
if !ok {
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
items, err := reader.ListVisibleTasks(c.Request.Context(), c.GetString(authenticatedActorKey), c.Query("domain_id"), limit)
if err != nil {
s.writeError(c, http.StatusInternalServerError, "查询任务失败", err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (s *Server) getTask(c *gin.Context) {
reader, ok := s.projectTaskReader(c)
if !ok {
return
}
item, err := reader.GetVisibleTask(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("id"))
if err != nil {
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusForbidden, "无权访问此任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "读取任务失败", err)
return
}
c.JSON(http.StatusOK, item)
}
func (s *Server) listTaskAttempts(c *gin.Context) {
reader, ok := s.projectTaskReader(c)
if !ok {
return
}
actorID := c.GetString(authenticatedActorKey)
if _, err := reader.GetVisibleTask(c.Request.Context(), actorID, c.Param("id")); err != nil {
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusForbidden, "无权访问此任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "读取任务失败", err)
return
}
items, err := reader.ListVisibleTaskAttempts(c.Request.Context(), actorID, c.Param("id"))
if err != nil {
s.writeError(c, http.StatusInternalServerError, "查询任务尝试失败", err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (s *Server) listTaskDeploymentResults(c *gin.Context) {
reader, ok := s.projectTaskReader(c)
if !ok {
return
}
actorID := c.GetString(authenticatedActorKey)
if _, err := reader.GetVisibleTask(c.Request.Context(), actorID, c.Param("id")); err != nil {
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusForbidden, "无权访问此任务", nil)
return
}
s.writeError(c, http.StatusInternalServerError, "读取任务失败", err)
return
}
items, err := reader.ListVisibleTaskDeploymentResults(c.Request.Context(), actorID, c.Param("id"))
if err != nil {
s.writeError(c, http.StatusInternalServerError, "查询任务部署结果失败", err)
return
}
for index := range items {
if items[index].Status == "failed" {
items[index].Message = deployment.PublicStoredFailureMessage(items[index].Message)
}
}
reports, err := reader.ListVisibleTaskDeploymentReports(c.Request.Context(), actorID, c.Param("id"))
if err != nil {
s.writeError(c, http.StatusInternalServerError, "查询完整部署报告失败", err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items, "reports": reports})
}
func taskDeploymentResults(report deployment.Report) []repository.TaskDeploymentResult {
if len(report.Results) == 0 {
return nil
}
items := make([]repository.TaskDeploymentResult, 0, len(report.Results))
for index, item := range report.Results {
item = deployment.PublicResult(item)
items = append(items, repository.TaskDeploymentResult{
ResultIndex: index,
ServerID: item.ServerID,
Server: item.Server,
Status: item.Status,
Message: item.Message,
BackupID: item.BackupID,
})
}
return items
}
func (s *Server) listAuditLogs(c *gin.Context) {
if !s.requireProjectResourceReader(c) {
return
}
reader, ok := s.projectResources.(repository.ProjectAuditReader)
if !ok {
s.writeError(c, http.StatusNotImplemented, "审计日志查询接口尚未配置", errors.New("审计日志读取服务未配置"))
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
items, err := reader.ListVisibleAuditLogs(c.Request.Context(), c.GetString(authenticatedActorKey), limit)
if err != nil {
s.writeError(c, http.StatusInternalServerError, "查询审计日志失败", err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}