完善任务部分成功状态与运行概览
补充证书部署部分成功的任务状态、HTTP响应和前端任务展示,并增加对应测试。
This commit is contained in:
parent
a9f17c740a
commit
2b9d82df2f
@ -51,14 +51,14 @@ func (w *certificateTaskWorkflow) run(ctx context.Context, id, taskType, stage s
|
||||
finishCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
||||
defer cancel()
|
||||
finished := time.Now().UTC()
|
||||
status, message := certificateTaskOutcome(resultErr)
|
||||
status, message := certificateTaskOutcome(result, resultErr)
|
||||
auditErr := w.audit.CreateAuditLog(finishCtx, repository.AuditLog{
|
||||
TaskID: task.ID,
|
||||
Action: taskType, ResourceType: "domain", ResourceID: id,
|
||||
Status: status, Message: message,
|
||||
})
|
||||
if auditErr != nil {
|
||||
status, message = certificateTaskOutcome(auditErr)
|
||||
status, message = certificateTaskOutcome(result, auditErr)
|
||||
stage = "record"
|
||||
}
|
||||
taskErr := w.tasks.UpdateTask(finishCtx, task.ID, status, stage, message, &started, &finished)
|
||||
@ -95,7 +95,7 @@ func (w *certificateTaskWorkflow) run(ctx context.Context, id, taskType, stage s
|
||||
cancel()
|
||||
finishCtx, finishCancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
||||
finished := time.Now().UTC()
|
||||
status, message := certificateTaskOutcome(resultErr)
|
||||
status, message := certificateTaskOutcome(result, resultErr)
|
||||
recordErr := w.tasks.FinishAttempt(finishCtx, attempt.ID, status, message, &finished)
|
||||
finishCancel()
|
||||
if recordErr != nil {
|
||||
@ -114,14 +114,17 @@ func (w *certificateTaskWorkflow) run(ctx context.Context, id, taskType, stage s
|
||||
return result, resultErr
|
||||
}
|
||||
|
||||
func certificateTaskOutcome(err error) (string, string) {
|
||||
func certificateTaskOutcome(result CertificateWorkflowResult, err error) (string, string) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return "cancelled", "证书工作流已取消"
|
||||
return "cancelled", "\u8bc1\u4e66\u5de5\u4f5c\u6d41\u5df2\u53d6\u6d88"
|
||||
}
|
||||
if result.Deployment.Status == "partial" {
|
||||
return "partial_success", "\u8bc1\u4e66\u5de5\u4f5c\u6d41\u5df2\u5b8c\u6210\u90e8\u5206\u90e8\u7f72"
|
||||
}
|
||||
if err != nil {
|
||||
return "failed", "证书工作流未完成,请检查受保护的服务日志"
|
||||
return "failed", "\u8bc1\u4e66\u5de5\u4f5c\u6d41\u672a\u5b8c\u6210\uff0c\u8bf7\u68c0\u67e5\u53d7\u4fdd\u62a4\u7684\u670d\u52a1\u65e5\u5fd7"
|
||||
}
|
||||
return "success", "证书工作流执行完成"
|
||||
return "success", "\u8bc1\u4e66\u5de5\u4f5c\u6d41\u6267\u884c\u5b8c\u6210"
|
||||
}
|
||||
|
||||
var _ certificate.CertificateIssuer = (*certificateTaskWorkflow)(nil)
|
||||
|
||||
@ -258,3 +258,16 @@ func TestCertificateTerminalPersistenceFailureIsReturnedWithoutReissue(t *testin
|
||||
t.Fatalf("calls=%d error=%v logged=%v", calls, err, logged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificateTasksRecordPartialDeploymentFailure(t *testing.T) {
|
||||
w, a := newCertificateTaskFixture(t)
|
||||
_, err := w.run(t.Context(), "task-domain", "renewal_run", "certificate_issue", 1, 0, func(context.Context, string) (CertificateWorkflowResult, error) {
|
||||
return CertificateWorkflowResult{
|
||||
Deployment: deployment.Report{Total: 2, Success: 1, Failed: 1, Status: "partial"},
|
||||
}, errors.New("partial deployment")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("partial deployment error should remain visible to the caller")
|
||||
}
|
||||
assertCertificateTask(t, a, "partial_success", "partial_success")
|
||||
}
|
||||
|
||||
@ -148,10 +148,14 @@ func (s *Server) executeCertificateIssueTask(c *gin.Context, task repository.Tas
|
||||
status := "success"
|
||||
message := "证书申请和部署完成"
|
||||
cancelled := false
|
||||
partial := issueErr == nil && deploymentReport.Status == "partial"
|
||||
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)
|
||||
}
|
||||
@ -181,7 +185,7 @@ func (s *Server) executeCertificateIssueTask(c *gin.Context, task repository.Tas
|
||||
task.UpdatedAt = finished
|
||||
if s.audit != nil {
|
||||
auditStatus := status
|
||||
if issueErr != nil && status != "cancelled" {
|
||||
if issueErr != nil && status != "cancelled" && status != "partial_success" {
|
||||
auditStatus = "error"
|
||||
}
|
||||
_ = s.audit.CreateAuditLog(context.Background(), repository.AuditLog{
|
||||
@ -198,6 +202,8 @@ func (s *Server) executeCertificateIssueTask(c *gin.Context, task repository.Tas
|
||||
responseStatus := http.StatusBadGateway
|
||||
if status == "cancelled" {
|
||||
responseStatus = http.StatusConflict
|
||||
} else if status == "partial_success" {
|
||||
responseStatus = http.StatusMultiStatus
|
||||
} else if errors.Is(issueErr, context.DeadlineExceeded) {
|
||||
responseStatus = http.StatusGatewayTimeout
|
||||
}
|
||||
|
||||
@ -563,3 +563,30 @@ func TestTaskAPIRecordsPartialDeploymentSuccess(t *testing.T) {
|
||||
t.Fatalf("partial audit status: %+v %v", logs, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskAPIRecordsPartialDeploymentFailureAsPartialSuccess(t *testing.T) {
|
||||
server, taskRepo, auditRepo, closeDB := newTaskAPITestServer(t, func(_ context.Context, id string) (certificate.IssueResult, deployment.Report, error) {
|
||||
return certificate.IssueResult{DomainID: id}, deployment.Report{
|
||||
Total: 2, Success: 1, Failed: 1, Status: "partial",
|
||||
}, errors.New("deployment partially failed")
|
||||
})
|
||||
defer closeDB()
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
||||
if response.Code != http.StatusMultiStatus {
|
||||
t.Fatalf("partial failure response: %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
items, err := taskRepo.ListTasks(context.Background(), "domain-1", 10)
|
||||
if err != nil || len(items) != 1 || items[0].Status != "partial_success" {
|
||||
t.Fatalf("partial failure task status: %+v %v", items, err)
|
||||
}
|
||||
attempts, err := taskRepo.ListAttempts(context.Background(), items[0].ID)
|
||||
if err != nil || len(attempts) != 1 || attempts[0].Status != "partial_success" {
|
||||
t.Fatalf("partial failure attempt status: %+v %v", attempts, err)
|
||||
}
|
||||
logs, err := auditRepo.ListAuditLogs(context.Background(), 10)
|
||||
if err != nil || len(logs) != 1 || logs[0].Status != "partial_success" {
|
||||
t.Fatalf("partial failure audit status: %+v %v", logs, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,6 +163,9 @@ export function useDashboard() {
|
||||
success: "已完成",
|
||||
failed: "失败",
|
||||
cancelled: "已取消",
|
||||
cancelling: "\u53d6\u6d88\u4e2d",
|
||||
retry_wait: "\u7b49\u5f85\u91cd\u8bd5",
|
||||
partial_success: "\u90e8\u5206\u6210\u529f",
|
||||
};
|
||||
taskStatusLabels.error = taskStatusLabels.failed;
|
||||
const taskTypeLabels = {
|
||||
@ -216,6 +219,9 @@ export function useDashboard() {
|
||||
success: "已完成",
|
||||
failed: "失败",
|
||||
cancelled: "已取消",
|
||||
cancelling: "\u53d6\u6d88\u4e2d",
|
||||
retry_wait: "\u7b49\u5f85\u91cd\u8bd5",
|
||||
partial_success: "\u90e8\u5206\u6210\u529f",
|
||||
};
|
||||
const domainStatusLabels = {
|
||||
pending: "等待申请",
|
||||
|
||||
@ -341,6 +341,8 @@ test("管理续期的任务类型、审计和阶段使用中文", async (t) => {
|
||||
assert.equal(state.auditActionLabel("management_renewal"), "管理证书续期");
|
||||
assert.equal(state.taskStageLabel("activate"), "激活管理证书");
|
||||
assert.equal(state.taskStageLabel("retry_wait"), "等待续期重试");
|
||||
assert.equal(state.taskStatusLabels.partial_success, "\u90e8\u5206\u6210\u529f");
|
||||
assert.equal(state.attemptStatusLabels.retry_wait, "\u7b49\u5f85\u91cd\u8bd5");
|
||||
});
|
||||
|
||||
test("已保存凭据不变可保存,输入新凭据必须重新验证", async (t) => {
|
||||
@ -946,12 +948,12 @@ test("runtimeOverview.handlesEmptyDataThresholdsAndTaskOrdering", () => {
|
||||
for (const key of ["total", "valid", "expiring", "expired", "missing", "autoRenew", "configuredServers", "boundDomains", "activeTasks", "failedTasks"]) assert.equal(empty[key], 0);
|
||||
for (const value of [0, -1, "invalid"]) assert.equal(summarizeRuntime({ settings: { acme: { renewal_near_expiry_days: value } } }).threshold, 10);
|
||||
assert.equal(summarizeRuntime({ settings: { acme: { renewal_near_expiry_days: 30 } } }).threshold, 30);
|
||||
const tasks = ["queued", "running", "failed", "error", "success", "cancelled"].map((status, index) => ({ id: index, status, created_at: `2026-09-25T12:0${index}:00Z` }));
|
||||
const tasks = ["queued", "running", "retry_wait", "cancelling", "failed", "error", "success", "cancelled"].map((status, index) => ({ id: index, status, created_at: `2026-09-25T12:0${index}:00Z` }));
|
||||
const original = structuredClone(tasks);
|
||||
const result = summarizeRuntime({ tasks });
|
||||
assert.equal(result.activeTasks, 2);
|
||||
assert.equal(result.activeTasks, 4);
|
||||
assert.equal(result.failedTasks, 2);
|
||||
assert.deepEqual(result.tasks.map((item) => item.id), [5, 4, 3, 2, 1, 0]);
|
||||
assert.deepEqual(result.tasks.map((item) => item.id), [7, 6, 5, 4, 3, 2, 1, 0]);
|
||||
assert.deepEqual(tasks, original);
|
||||
assert.equal(runtimeTime(null), runtimeTime("invalid"));
|
||||
assert.doesNotMatch(runtimeTime("2026-09-25T12:00:00Z"), /Invalid/);
|
||||
|
||||
@ -53,7 +53,7 @@ export function summarizeRuntime(data, now = Date.now()) {
|
||||
autoRenew: domains.filter((item) => item.auto_renew === true).length,
|
||||
configuredServers: (data.servers || []).length,
|
||||
boundDomains: domains.filter((item) => (data.domainServers?.[item.id] || []).length > 0).length,
|
||||
activeTasks: tasks.filter((item) => item.status === "queued" || item.status === "running").length,
|
||||
activeTasks: tasks.filter((item) => ["queued", "running", "retry_wait", "cancelling"].includes(item.status)).length,
|
||||
failedTasks: tasks.filter((item) => item.status === "failed" || item.status === "error").length,
|
||||
tasks,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user