216 lines
7.6 KiB
C#
216 lines
7.6 KiB
C#
using StellaOps.Auth.Abstractions;
|
|
using StellaOps.Auth.ServerIntegration.Tenancy;
|
|
using StellaOps.Signals.Hosting;
|
|
using StellaOps.Signals.Options;
|
|
using StellaOps.Signals.Routing;
|
|
|
|
namespace StellaOps.Signals;
|
|
|
|
internal static class CompatibilityApiV1Endpoints
|
|
{
|
|
private static readonly CompatibilitySignalRecord[] Signals =
|
|
[
|
|
new(
|
|
"sig-001",
|
|
"ci_build",
|
|
"gitea",
|
|
"completed",
|
|
new Dictionary<string, object?>
|
|
{
|
|
["host"] = "build-agent-01",
|
|
["runtime"] = "ebpf",
|
|
["probeStatus"] = "healthy",
|
|
["latencyMs"] = 41
|
|
},
|
|
"corr-001",
|
|
"sha256:001",
|
|
new[] { "update-runtime-health" },
|
|
"2026-03-09T08:10:00Z",
|
|
"2026-03-09T08:10:02Z",
|
|
null),
|
|
new(
|
|
"sig-002",
|
|
"ci_deploy",
|
|
"internal",
|
|
"processing",
|
|
new Dictionary<string, object?>
|
|
{
|
|
["host"] = "deploy-stage-02",
|
|
["runtime"] = "etw",
|
|
["probeStatus"] = "degraded",
|
|
["latencyMs"] = 84
|
|
},
|
|
"corr-002",
|
|
"sha256:002",
|
|
new[] { "refresh-rollout-state" },
|
|
"2026-03-09T08:12:00Z",
|
|
null,
|
|
null),
|
|
new(
|
|
"sig-003",
|
|
"registry_push",
|
|
"harbor",
|
|
"failed",
|
|
new Dictionary<string, object?>
|
|
{
|
|
["host"] = "registry-sync-01",
|
|
["runtime"] = "dyld",
|
|
["probeStatus"] = "failed",
|
|
["latencyMs"] = 132
|
|
},
|
|
"corr-003",
|
|
"sha256:003",
|
|
new[] { "retry-mirror" },
|
|
"2026-03-09T08:13:00Z",
|
|
"2026-03-09T08:13:05Z",
|
|
"Registry callback timed out."),
|
|
new(
|
|
"sig-004",
|
|
"scan_complete",
|
|
"internal",
|
|
"completed",
|
|
new Dictionary<string, object?>
|
|
{
|
|
["host"] = "scanner-03",
|
|
["runtime"] = "ebpf",
|
|
["probeStatus"] = "healthy",
|
|
["latencyMs"] = 58
|
|
},
|
|
"corr-004",
|
|
"sha256:004",
|
|
new[] { "refresh-risk-snapshot" },
|
|
"2026-03-09T08:16:00Z",
|
|
"2026-03-09T08:16:01Z",
|
|
null),
|
|
new(
|
|
"sig-005",
|
|
"policy_eval",
|
|
"internal",
|
|
"received",
|
|
new Dictionary<string, object?>
|
|
{
|
|
["host"] = "policy-runner-01",
|
|
["runtime"] = "unknown",
|
|
["probeStatus"] = "degraded",
|
|
["latencyMs"] = 73
|
|
},
|
|
"corr-005",
|
|
"sha256:005",
|
|
new[] { "await-policy-evaluation" },
|
|
"2026-03-09T08:18:00Z",
|
|
null,
|
|
null)
|
|
];
|
|
|
|
public static IEndpointRouteBuilder MapSignalsCompatibilityEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup("/api/v1/signals").RequireTenant();
|
|
|
|
group.MapGet("", (
|
|
HttpContext context,
|
|
SignalsOptions options,
|
|
SignalsSealedModeMonitor sealedModeMonitor,
|
|
string? type,
|
|
string? status,
|
|
string? provider,
|
|
int? limit,
|
|
string? cursor) =>
|
|
{
|
|
if (!Program.TryAuthorizeAny(context, [SignalsPolicies.Read, StellaOpsScopes.OrchRead], options.Authority.AllowAnonymousFallback, out var authFailure))
|
|
{
|
|
return authFailure ?? Results.Unauthorized();
|
|
}
|
|
|
|
if (!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
|
|
{
|
|
return sealedFailure ?? Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
}
|
|
|
|
var filtered = ApplyFilters(type, status, provider);
|
|
var offset = ParseCursor(cursor);
|
|
var pageSize = Math.Clamp(limit ?? 50, 1, 200);
|
|
var items = filtered.Skip(offset).Take(pageSize).ToArray();
|
|
var nextCursor = offset + pageSize < filtered.Length ? (offset + pageSize).ToString() : null;
|
|
|
|
return Results.Ok(new
|
|
{
|
|
items,
|
|
total = filtered.Length,
|
|
cursor = nextCursor
|
|
});
|
|
});
|
|
|
|
group.MapGet("/stats", (
|
|
HttpContext context,
|
|
SignalsOptions options,
|
|
SignalsSealedModeMonitor sealedModeMonitor) =>
|
|
{
|
|
if (!Program.TryAuthorizeAny(context, [SignalsPolicies.Read, StellaOpsScopes.OrchRead], options.Authority.AllowAnonymousFallback, out var authFailure))
|
|
{
|
|
return authFailure ?? Results.Unauthorized();
|
|
}
|
|
|
|
if (!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
|
|
{
|
|
return sealedFailure ?? Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
}
|
|
|
|
return Results.Ok(BuildStats(Signals));
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
private static CompatibilitySignalRecord[] ApplyFilters(string? type, string? status, string? provider) =>
|
|
Signals
|
|
.Where(signal => string.IsNullOrWhiteSpace(type) || string.Equals(signal.Type, type, StringComparison.OrdinalIgnoreCase))
|
|
.Where(signal => string.IsNullOrWhiteSpace(status) || string.Equals(signal.Status, status, StringComparison.OrdinalIgnoreCase))
|
|
.Where(signal => string.IsNullOrWhiteSpace(provider) || string.Equals(signal.Provider, provider, StringComparison.OrdinalIgnoreCase))
|
|
.ToArray();
|
|
|
|
private static int ParseCursor(string? cursor) =>
|
|
int.TryParse(cursor, out var offset) && offset >= 0 ? offset : 0;
|
|
|
|
private static object BuildStats(IReadOnlyCollection<CompatibilitySignalRecord> signals)
|
|
{
|
|
var byType = signals
|
|
.GroupBy(signal => signal.Type, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
|
|
var byStatus = signals
|
|
.GroupBy(signal => signal.Status, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
|
|
var byProvider = signals
|
|
.GroupBy(signal => signal.Provider, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
|
|
var successful = signals.Count(signal => string.Equals(signal.Status, "completed", StringComparison.OrdinalIgnoreCase));
|
|
var latencySamples = signals
|
|
.Select(signal => signal.Payload.TryGetValue("latencyMs", out var value) ? value : null)
|
|
.OfType<int>()
|
|
.ToArray();
|
|
|
|
return new
|
|
{
|
|
total = signals.Count,
|
|
byType,
|
|
byStatus,
|
|
byProvider,
|
|
lastHourCount = signals.Count,
|
|
successRate = signals.Count == 0 ? 100 : Math.Round((successful / (double)signals.Count) * 100, 2),
|
|
avgProcessingMs = latencySamples.Length == 0 ? 0 : Math.Round(latencySamples.Average(), 2)
|
|
};
|
|
}
|
|
|
|
internal sealed record CompatibilitySignalRecord(
|
|
string Id,
|
|
string Type,
|
|
string Provider,
|
|
string Status,
|
|
IReadOnlyDictionary<string, object?> Payload,
|
|
string? CorrelationId,
|
|
string? ArtifactRef,
|
|
IReadOnlyCollection<string> TriggeredActions,
|
|
string ReceivedAt,
|
|
string? ProcessedAt,
|
|
string? Error);
|
|
}
|