Files
git.stella-ops.org/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/RunEndpoints.cs

186 lines
6.5 KiB
C#

using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.JobEngine.Infrastructure.Repositories;
using StellaOps.JobEngine.WebService.Contracts;
using StellaOps.JobEngine.WebService.Services;
using static StellaOps.Localization.T;
namespace StellaOps.JobEngine.WebService.Endpoints;
/// <summary>
/// REST API endpoints for runs (batch executions).
/// </summary>
public static class RunEndpoints
{
/// <summary>
/// Maps run endpoints to the route builder.
/// </summary>
public static RouteGroupBuilder MapRunEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/jobengine/runs")
.WithTags("Orchestrator Runs")
.RequireAuthorization(JobEnginePolicies.Read)
.RequireTenant();
group.MapGet(string.Empty, ListRuns)
.WithName("Orchestrator_ListRuns")
.WithDescription(_t("orchestrator.run.list_description"));
group.MapGet("{runId:guid}", GetRun)
.WithName("Orchestrator_GetRun")
.WithDescription(_t("orchestrator.run.get_description"));
group.MapGet("{runId:guid}/jobs", GetRunJobs)
.WithName("Orchestrator_GetRunJobs")
.WithDescription(_t("orchestrator.run.get_jobs_description"));
group.MapGet("{runId:guid}/summary", GetRunSummary)
.WithName("Orchestrator_GetRunSummary")
.WithDescription(_t("orchestrator.run.get_summary_description"));
return group;
}
private static async Task<IResult> ListRuns(
HttpContext context,
[FromServices] TenantResolver tenantResolver,
[FromServices] IRunRepository repository,
[FromQuery] Guid? sourceId = null,
[FromQuery] string? runType = null,
[FromQuery] string? status = null,
[FromQuery] string? projectId = null,
[FromQuery] string? createdAfter = null,
[FromQuery] string? createdBefore = null,
[FromQuery] int? limit = null,
[FromQuery] string? cursor = null,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var effectiveLimit = EndpointHelpers.GetLimit(limit);
var offset = EndpointHelpers.ParseCursorOffset(cursor);
var parsedStatus = EndpointHelpers.TryParseRunStatus(status);
var parsedCreatedAfter = EndpointHelpers.TryParseDateTimeOffset(createdAfter);
var parsedCreatedBefore = EndpointHelpers.TryParseDateTimeOffset(createdBefore);
var runs = await repository.ListAsync(
tenantId,
sourceId,
runType,
parsedStatus,
projectId,
parsedCreatedAfter,
parsedCreatedBefore,
effectiveLimit,
offset,
cancellationToken).ConfigureAwait(false);
var responses = runs.Select(RunResponse.FromDomain).ToList();
var nextCursor = EndpointHelpers.CreateNextCursor(offset, effectiveLimit, responses.Count);
return Results.Ok(new RunListResponse(responses, nextCursor));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> GetRun(
HttpContext context,
[FromRoute] Guid runId,
[FromServices] TenantResolver tenantResolver,
[FromServices] IRunRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var run = await repository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false);
if (run is null)
{
return Results.NotFound();
}
return Results.Ok(RunResponse.FromDomain(run));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> GetRunJobs(
HttpContext context,
[FromRoute] Guid runId,
[FromServices] TenantResolver tenantResolver,
[FromServices] IRunRepository runRepository,
[FromServices] IJobRepository jobRepository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
// Verify run exists
var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false);
if (run is null)
{
return Results.NotFound();
}
var jobs = await jobRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false);
var responses = jobs.Select(JobResponse.FromDomain).ToList();
return Results.Ok(new JobListResponse(responses, null));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> GetRunSummary(
HttpContext context,
[FromRoute] Guid runId,
[FromServices] TenantResolver tenantResolver,
[FromServices] IRunRepository runRepository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false);
if (run is null)
{
return Results.NotFound();
}
// Return the aggregate counts from the run itself
var summary = new
{
runId = run.RunId,
status = run.Status.ToString().ToLowerInvariant(),
totalJobs = run.TotalJobs,
completedJobs = run.CompletedJobs,
succeededJobs = run.SucceededJobs,
failedJobs = run.FailedJobs,
pendingJobs = run.TotalJobs - run.CompletedJobs,
createdAt = run.CreatedAt,
startedAt = run.StartedAt,
completedAt = run.CompletedAt
};
return Results.Ok(summary);
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
}