Restructure solution layout by module
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.WebService.Constants;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Infrastructure;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.Zastava.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
internal static class RuntimeEndpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public static void MapRuntimeEndpoints(this RouteGroupBuilder apiGroup, string runtimeSegment)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(apiGroup);
|
||||
|
||||
var runtime = apiGroup
|
||||
.MapGroup(NormalizeSegment(runtimeSegment))
|
||||
.WithTags("Runtime");
|
||||
|
||||
runtime.MapPost("/events", HandleRuntimeEventsAsync)
|
||||
.WithName("scanner.runtime.events.ingest")
|
||||
.Produces<RuntimeEventsIngestResponseDto>(StatusCodes.Status202Accepted)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status429TooManyRequests)
|
||||
.RequireAuthorization(ScannerPolicies.RuntimeIngest);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleRuntimeEventsAsync(
|
||||
RuntimeEventsIngestRequestDto request,
|
||||
IRuntimeEventIngestionService ingestionService,
|
||||
IOptions<ScannerWebServiceOptions> options,
|
||||
HttpContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(ingestionService);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var runtimeOptions = options.Value.Runtime ?? new ScannerWebServiceOptions.RuntimeOptions();
|
||||
var validationError = ValidateRequest(request, runtimeOptions, context, out var envelopes);
|
||||
if (validationError is { } problem)
|
||||
{
|
||||
return problem;
|
||||
}
|
||||
|
||||
var result = await ingestionService.IngestAsync(envelopes, request.BatchId, cancellationToken).ConfigureAwait(false);
|
||||
if (result.IsPayloadTooLarge)
|
||||
{
|
||||
var extensions = new Dictionary<string, object?>
|
||||
{
|
||||
["payloadBytes"] = result.PayloadBytes,
|
||||
["maxPayloadBytes"] = result.PayloadLimit
|
||||
};
|
||||
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Runtime event batch too large",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Runtime batch payload exceeds configured budget.",
|
||||
extensions: extensions);
|
||||
}
|
||||
|
||||
if (result.IsRateLimited)
|
||||
{
|
||||
var retryAfterSeconds = Math.Max(1, (int)Math.Ceiling(result.RetryAfter.TotalSeconds));
|
||||
context.Response.Headers.RetryAfter = retryAfterSeconds.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var extensions = new Dictionary<string, object?>
|
||||
{
|
||||
["scope"] = result.RateLimitedScope,
|
||||
["key"] = result.RateLimitedKey,
|
||||
["retryAfterSeconds"] = retryAfterSeconds
|
||||
};
|
||||
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.RateLimited,
|
||||
"Runtime ingestion rate limited",
|
||||
StatusCodes.Status429TooManyRequests,
|
||||
detail: "Runtime ingestion exceeded configured rate limits.",
|
||||
extensions: extensions);
|
||||
}
|
||||
|
||||
var payload = new RuntimeEventsIngestResponseDto
|
||||
{
|
||||
Accepted = result.Accepted,
|
||||
Duplicates = result.Duplicates
|
||||
};
|
||||
|
||||
return Json(payload, StatusCodes.Status202Accepted);
|
||||
}
|
||||
|
||||
private static IResult? ValidateRequest(
|
||||
RuntimeEventsIngestRequestDto request,
|
||||
ScannerWebServiceOptions.RuntimeOptions runtimeOptions,
|
||||
HttpContext context,
|
||||
out IReadOnlyList<RuntimeEventEnvelope> envelopes)
|
||||
{
|
||||
envelopes = request.Events ?? Array.Empty<RuntimeEventEnvelope>();
|
||||
if (envelopes.Count == 0)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "events array must include at least one item.");
|
||||
}
|
||||
|
||||
if (envelopes.Count > runtimeOptions.MaxBatchSize)
|
||||
{
|
||||
var extensions = new Dictionary<string, object?>
|
||||
{
|
||||
["maxBatchSize"] = runtimeOptions.MaxBatchSize,
|
||||
["eventCount"] = envelopes.Count
|
||||
};
|
||||
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "events array exceeds allowed batch size.",
|
||||
extensions: extensions);
|
||||
}
|
||||
|
||||
var seenEventIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < envelopes.Count; i++)
|
||||
{
|
||||
var envelope = envelopes[i];
|
||||
if (envelope is null)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"events[{i}] must not be null.");
|
||||
}
|
||||
|
||||
if (!envelope.IsSupported())
|
||||
{
|
||||
var extensions = new Dictionary<string, object?>
|
||||
{
|
||||
["schemaVersion"] = envelope.SchemaVersion
|
||||
};
|
||||
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Unsupported runtime schema version",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Runtime event schemaVersion is not supported.",
|
||||
extensions: extensions);
|
||||
}
|
||||
|
||||
var runtimeEvent = envelope.Event;
|
||||
if (runtimeEvent is null)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"events[{i}].event must not be null.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(runtimeEvent.EventId))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"events[{i}].eventId is required.");
|
||||
}
|
||||
|
||||
if (!seenEventIds.Add(runtimeEvent.EventId))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"Duplicate eventId detected within batch ('{runtimeEvent.EventId}').");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(runtimeEvent.Tenant))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"events[{i}].tenant is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(runtimeEvent.Node))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"events[{i}].node is required.");
|
||||
}
|
||||
|
||||
if (runtimeEvent.Workload is null)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime ingest request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"events[{i}].workload is required.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeSegment(string segment)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(segment))
|
||||
{
|
||||
return "/runtime";
|
||||
}
|
||||
|
||||
var trimmed = segment.Trim('/');
|
||||
return "/" + trimmed;
|
||||
}
|
||||
|
||||
private static IResult Json<T>(T value, int statusCode)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(value, SerializerOptions);
|
||||
return Results.Content(payload, "application/json", Encoding.UTF8, statusCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user