Files
MoShiro/backend/src/MoShiro.Backend.Api/Admin/TaskEndpoints.cs
T
justme 0eda0600cc
ci/woodpecker/pr/verify Pipeline was successful
ci/woodpecker/pr/web-build Pipeline was successful
shared: add independent IPv4 and IPv6 collection [CI]
2026-10-07 11:44:01 +08:00

152 lines
7.2 KiB
C#

using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace MoShiro.Backend.Api;
/// <summary>任务(线路模板、单个 Agent 的覆盖、目标任务)与按需测试。参数按 Kind 的 ParamSpec 校验,并检查带宽预算。</summary>
public static class TaskEndpoints
{
private const string TasksTag = "admin-tasks";
private const string TestsTag = "admin-tests";
public static void Map(RouteGroupBuilder group)
{
group.MapGet("/tasks", ListAsync)
.Api("tasks_list", "Tasks: line templates, per-agent overrides and target tasks", TasksTag).Returns<PageDto<TaskDto>>().ProducesProblems(401, 403);
group.MapPost("/tasks", CreateAsync)
.Api("tasks_create", "Create a task; params are validated against the kind", TasksTag)
.Returns<TaskDto>(StatusCodes.Status201Created).ProducesProblems(400, 401, 403, 404);
group.MapGet("/tasks/{id}", GetAsync)
.Api("tasks_get", "One task", TasksTag).Returns<TaskDto>().ProducesProblems(401, 403, 404);
group.MapPatch("/tasks/{id}", PatchAsync)
.Accepts<PatchTaskRequest>("application/merge-patch+json", "application/json")
.Api("tasks_update", "Change params, schedule, target or enabled (merge-patch, If-Match supported)", TasksTag)
.Returns<TaskDto>().ProducesProblems(400, 401, 403, 404, 412);
group.MapDelete("/tasks/{id}", DeleteAsync)
.Api("tasks_delete", "Delete a task", TasksTag).Returns<object>(StatusCodes.Status204NoContent).ProducesProblems(401, 403);
group.MapPost("/lines/{line_id}/tests", StartTestAsync)
.Api("tests_start", "Start an on-demand test on a line", TestsTag)
.Returns<TestDto>(StatusCodes.Status202Accepted).ProducesProblems(400, 401, 403, 404, 409);
group.MapGet("/lines/{line_id}/tests", ListTestsAsync)
.Api("tests_list", "Recent tests of a line", TestsTag).Returns<PageDto<TestDto>>().ProducesProblems(401, 403);
group.MapPost("/tests/{id}/cancel", CancelAsync)
.Api("tests_cancel", "Cancel a running test", TestsTag).Returns<TestDto>().ProducesProblems(401, 403, 404);
}
private static async Task<IResult> ListAsync(TaskService tasks, string? agent_id, string? scope, CancellationToken ct)
{
IEnumerable<TaskRecord> all = await tasks.ListAsync(ct);
if (agent_id is not null)
{
all = all.Where(t => t.AgentId?.Value == agent_id);
}
if (scope is not null)
{
all = all.Where(t => t.Scope == scope);
}
return TypedResults.Json(new PageDto<TaskDto>([.. all.Select(Mappers.ToDto)], null), ApiJsonContext.Default.PageDtoTaskDto);
}
private static async Task<IResult> CreateAsync(HttpContext http, TaskService tasks, CreateTaskRequest request, CancellationToken ct)
{
Result<TaskRecord> result = await tasks.CreateAsync(
new TaskInput(
request.Scope ?? "", request.Kind ?? "", request.Target, request.Params, request.Schedule, request.Enabled,
request.AgentId is null ? null : new NodeId(request.AgentId), request.BaseTaskId is null ? null : new TaskId(request.BaseTaskId)),
ct);
if (!result.TryGet(out TaskRecord? task, out FleetError? error))
{
return Problems.From(http, error);
}
http.Response.Headers.Location = "/api/admin/tasks/" + task.Id.Value;
Concurrency.SetETag(http, task.RowVersion);
return TypedResults.Json(Mappers.ToDto(task), ApiJsonContext.Default.TaskDto, statusCode: StatusCodes.Status201Created);
}
private static async Task<IResult> GetAsync(HttpContext http, TaskService tasks, string id, CancellationToken ct)
{
TaskRecord? task = await tasks.GetAsync(new TaskId(id), ct);
if (task is null)
{
return Problems.Create(http, MoShiroErrors.TaskNotFound, "task not found");
}
Concurrency.SetETag(http, task.RowVersion);
return TypedResults.Json(Mappers.ToDto(task), ApiJsonContext.Default.TaskDto);
}
private static async Task<IResult> PatchAsync(HttpContext http, TaskService tasks, PatchTaskRequest request, string id, CancellationToken ct)
{
Result<TaskRecord> result = await tasks.UpdateAsync(new TaskId(id), new TaskPatch(request.Params, request.Schedule, request.Enabled, request.Target), Concurrency.ExpectedRowVersion(http), ct);
if (!result.TryGet(out TaskRecord? task, out FleetError? error))
{
return Problems.From(http, error);
}
Concurrency.SetETag(http, task.RowVersion);
return TypedResults.Json(Mappers.ToDto(task), ApiJsonContext.Default.TaskDto);
}
private static async Task<IResult> DeleteAsync(TaskService tasks, string id, CancellationToken ct)
{
await tasks.DeleteAsync(new TaskId(id), ct);
return TypedResults.NoContent();
}
private static async Task<IResult> StartTestAsync(HttpContext http, TestService tests, LineService lines, StartTestRequestDto request, string line_id, CancellationToken ct)
{
Result<LineView> found = await lines.ResolveAsync(line_id, ct);
if (!found.TryGet(out LineView? line, out FleetError? lineError))
{
return Problems.From(http, lineError);
}
Result<TestRecord> result = await tests.StartAsync(line.Key.Agent, line.Key.Probe, new StartTestRequest(request.Plan ?? "", request.Params), "api", line.IpVersion, ct);
if (!result.TryGet(out TestRecord? test, out FleetError? error))
{
return Problems.From(http, error);
}
http.Response.Headers.Location = "/api/public/tests/" + test.Id.Value;
return TypedResults.Json(Mappers.ToDto(test), ApiJsonContext.Default.TestDto, statusCode: StatusCodes.Status202Accepted);
}
private static async Task<IResult> ListTestsAsync(HttpContext http, TestService tests, LineService lines, string line_id, int? limit, CancellationToken ct)
{
// 只给 Agent id(并且有多个 Probe)时列出这个 Agent 上所有 Probe 的测试
NodeId agent;
ProbeId? probe = null;
string? ipVersion = null;
if (LineKey.TryParse(line_id, out LineKey key))
{
agent = key.Agent;
probe = key.Probe;
ipVersion = key.IpVersion;
}
else
{
agent = new NodeId(line_id);
}
return TypedResults.Json(
new PageDto<TestDto>([.. (await tests.ListAsync(agent, probe, Math.Clamp(limit ?? 50, 1, 500), ipVersion, ct)).Select(Mappers.ToDto)], null),
ApiJsonContext.Default.PageDtoTestDto);
}
private static async Task<IResult> CancelAsync(HttpContext http, TestService tests, string id, CancellationToken ct)
{
Result<TestRecord> result = await tests.CancelAsync(new TestId(id), ct);
return result.TryGet(out TestRecord? test, out FleetError? error)
? TypedResults.Json(Mappers.ToDto(test), ApiJsonContext.Default.TestDto)
: Problems.From(http, error);
}
}