consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,379 @@
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.JobEngine.Core.Domain;
using StellaOps.JobEngine.Infrastructure.Postgres;
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 quota management.
/// </summary>
public static class QuotaEndpoints
{
/// <summary>
/// Maps quota endpoints to the route builder.
/// </summary>
public static RouteGroupBuilder MapQuotaEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/jobengine/quotas")
.WithTags("Orchestrator Quotas")
.RequireAuthorization(JobEnginePolicies.Quota)
.RequireTenant();
// Quota CRUD operations
group.MapGet(string.Empty, ListQuotas)
.WithName("Orchestrator_ListQuotas")
.WithDescription(_t("orchestrator.quota.list_description"));
group.MapGet("{quotaId:guid}", GetQuota)
.WithName("Orchestrator_GetQuota")
.WithDescription(_t("orchestrator.quota.get_description"));
group.MapPost(string.Empty, CreateQuota)
.WithName("Orchestrator_CreateQuota")
.WithDescription(_t("orchestrator.quota.create_description"));
group.MapPut("{quotaId:guid}", UpdateQuota)
.WithName("Orchestrator_UpdateQuota")
.WithDescription(_t("orchestrator.quota.update_description"));
group.MapDelete("{quotaId:guid}", DeleteQuota)
.WithName("Orchestrator_DeleteQuota")
.WithDescription(_t("orchestrator.quota.delete_description"));
// Quota control operations
group.MapPost("{quotaId:guid}/pause", PauseQuota)
.WithName("Orchestrator_PauseQuota")
.WithDescription(_t("orchestrator.quota.pause_description"));
group.MapPost("{quotaId:guid}/resume", ResumeQuota)
.WithName("Orchestrator_ResumeQuota")
.WithDescription(_t("orchestrator.quota.resume_description"));
// Quota summary
group.MapGet("summary", GetQuotaSummary)
.WithName("Orchestrator_GetQuotaSummary")
.WithDescription(_t("orchestrator.quota.reset_description"));
return group;
}
private static async Task<IResult> ListQuotas(
HttpContext context,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
[FromQuery] string? jobType = null,
[FromQuery] bool? paused = 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 quotas = await repository.ListAsync(
tenantId,
jobType,
paused,
effectiveLimit,
offset,
cancellationToken).ConfigureAwait(false);
var responses = quotas.Select(QuotaResponse.FromDomain).ToList();
var nextCursor = EndpointHelpers.CreateNextCursor(offset, effectiveLimit, responses.Count);
return Results.Ok(new QuotaListResponse(responses, nextCursor));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> GetQuota(
HttpContext context,
[FromRoute] Guid quotaId,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var quota = await repository.GetByIdAsync(tenantId, quotaId, cancellationToken).ConfigureAwait(false);
if (quota is null)
{
return Results.NotFound();
}
return Results.Ok(QuotaResponse.FromDomain(quota));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> CreateQuota(
HttpContext context,
[FromBody] CreateQuotaRequest request,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
// Validate request
if (request.MaxActive <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.max_active_positive") });
if (request.MaxPerHour <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.max_per_hour_positive") });
if (request.BurstCapacity <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.burst_capacity_positive") });
if (request.RefillRate <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.refill_rate_positive") });
var now = DateTimeOffset.UtcNow;
var quota = new Quota(
QuotaId: Guid.NewGuid(),
TenantId: tenantId,
JobType: request.JobType,
MaxActive: request.MaxActive,
MaxPerHour: request.MaxPerHour,
BurstCapacity: request.BurstCapacity,
RefillRate: request.RefillRate,
CurrentTokens: request.BurstCapacity,
LastRefillAt: now,
CurrentActive: 0,
CurrentHourCount: 0,
CurrentHourStart: new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, 0, 0, now.Offset),
Paused: false,
PauseReason: null,
QuotaTicket: null,
CreatedAt: now,
UpdatedAt: now,
UpdatedBy: actorId);
await repository.CreateAsync(quota, cancellationToken).ConfigureAwait(false);
return Results.Created($"/api/v1/jobengine/quotas/{quota.QuotaId}", QuotaResponse.FromDomain(quota));
}
catch (DuplicateQuotaException ex)
{
return Results.Conflict(new { error = ex.Message });
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> UpdateQuota(
HttpContext context,
[FromRoute] Guid quotaId,
[FromBody] UpdateQuotaRequest request,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var quota = await repository.GetByIdAsync(tenantId, quotaId, cancellationToken).ConfigureAwait(false);
if (quota is null)
{
return Results.NotFound();
}
// Validate request
if (request.MaxActive.HasValue && request.MaxActive <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.max_active_positive") });
if (request.MaxPerHour.HasValue && request.MaxPerHour <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.max_per_hour_positive") });
if (request.BurstCapacity.HasValue && request.BurstCapacity <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.burst_capacity_positive") });
if (request.RefillRate.HasValue && request.RefillRate <= 0)
return Results.BadRequest(new { error = _t("orchestrator.quota.error.refill_rate_positive") });
var updated = quota with
{
MaxActive = request.MaxActive ?? quota.MaxActive,
MaxPerHour = request.MaxPerHour ?? quota.MaxPerHour,
BurstCapacity = request.BurstCapacity ?? quota.BurstCapacity,
RefillRate = request.RefillRate ?? quota.RefillRate,
UpdatedAt = DateTimeOffset.UtcNow,
UpdatedBy = actorId
};
await repository.UpdateAsync(updated, cancellationToken).ConfigureAwait(false);
return Results.Ok(QuotaResponse.FromDomain(updated));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> DeleteQuota(
HttpContext context,
[FromRoute] Guid quotaId,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var deleted = await repository.DeleteAsync(tenantId, quotaId, cancellationToken).ConfigureAwait(false);
if (!deleted)
{
return Results.NotFound();
}
return Results.NoContent();
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> PauseQuota(
HttpContext context,
[FromRoute] Guid quotaId,
[FromBody] PauseQuotaRequest request,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var quota = await repository.GetByIdAsync(tenantId, quotaId, cancellationToken).ConfigureAwait(false);
if (quota is null)
{
return Results.NotFound();
}
if (string.IsNullOrWhiteSpace(request.Reason))
{
return Results.BadRequest(new { error = _t("orchestrator.quota.error.pause_reason_required") });
}
await repository.PauseAsync(tenantId, quotaId, request.Reason, request.Ticket, actorId, cancellationToken)
.ConfigureAwait(false);
var updated = await repository.GetByIdAsync(tenantId, quotaId, cancellationToken).ConfigureAwait(false);
return Results.Ok(QuotaResponse.FromDomain(updated!));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> ResumeQuota(
HttpContext context,
[FromRoute] Guid quotaId,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var quota = await repository.GetByIdAsync(tenantId, quotaId, cancellationToken).ConfigureAwait(false);
if (quota is null)
{
return Results.NotFound();
}
await repository.ResumeAsync(tenantId, quotaId, actorId, cancellationToken).ConfigureAwait(false);
var updated = await repository.GetByIdAsync(tenantId, quotaId, cancellationToken).ConfigureAwait(false);
return Results.Ok(QuotaResponse.FromDomain(updated!));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> GetQuotaSummary(
HttpContext context,
[FromServices] TenantResolver tenantResolver,
[FromServices] IQuotaRepository repository,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
// Get all quotas for the tenant
var quotas = await repository.ListAsync(tenantId, null, null, 1000, 0, cancellationToken)
.ConfigureAwait(false);
var totalQuotas = quotas.Count;
var pausedQuotas = quotas.Count(q => q.Paused);
// Calculate utilization for each quota
var utilizationItems = quotas.Select(q =>
{
var tokenUtilization = q.BurstCapacity > 0
? 1.0 - (q.CurrentTokens / q.BurstCapacity)
: 0.0;
var concurrencyUtilization = q.MaxActive > 0
? (double)q.CurrentActive / q.MaxActive
: 0.0;
var hourlyUtilization = q.MaxPerHour > 0
? (double)q.CurrentHourCount / q.MaxPerHour
: 0.0;
return new QuotaUtilizationResponse(
QuotaId: q.QuotaId,
JobType: q.JobType,
TokenUtilization: Math.Round(tokenUtilization, 4),
ConcurrencyUtilization: Math.Round(concurrencyUtilization, 4),
HourlyUtilization: Math.Round(hourlyUtilization, 4),
Paused: q.Paused);
}).ToList();
var avgTokenUtilization = utilizationItems.Count > 0
? utilizationItems.Average(u => u.TokenUtilization)
: 0.0;
var avgConcurrencyUtilization = utilizationItems.Count > 0
? utilizationItems.Average(u => u.ConcurrencyUtilization)
: 0.0;
return Results.Ok(new QuotaSummaryResponse(
TotalQuotas: totalQuotas,
PausedQuotas: pausedQuotas,
AverageTokenUtilization: Math.Round(avgTokenUtilization, 4),
AverageConcurrencyUtilization: Math.Round(avgConcurrencyUtilization, 4),
Quotas: utilizationItems));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
}