save progress
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.FailureSignatures;
|
||||
|
||||
internal static class FailureSignatureEndpoints
|
||||
{
|
||||
private const string ReadScope = "scheduler.runs.read";
|
||||
|
||||
public static IEndpointRouteBuilder MapFailureSignatureEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/v1/scheduler/failure-signatures");
|
||||
|
||||
group.MapGet("/best-match", GetBestMatchAsync);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetBestMatchAsync(
|
||||
HttpContext httpContext,
|
||||
[FromQuery] string? scopeType,
|
||||
[FromQuery] string? scopeId,
|
||||
[FromQuery] string? toolchainHash,
|
||||
[FromServices] ITenantContextAccessor tenantAccessor,
|
||||
[FromServices] IScopeAuthorizer scopeAuthorizer,
|
||||
[FromServices] IServiceProvider serviceProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
scopeAuthorizer.EnsureScope(httpContext, ReadScope);
|
||||
var tenant = tenantAccessor.GetTenant(httpContext);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(scopeType))
|
||||
{
|
||||
throw new ValidationException("scopeType must be provided.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(scopeId))
|
||||
{
|
||||
throw new ValidationException("scopeId must be provided.");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<FailureSignatureScopeType>(scopeType.Trim(), ignoreCase: true, out var parsedScopeType))
|
||||
{
|
||||
throw new ValidationException($"scopeType '{scopeType}' is not valid.");
|
||||
}
|
||||
|
||||
var repository = serviceProvider.GetService<IFailureSignatureRepository>();
|
||||
if (repository is null)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "Failure signature storage is not configured.",
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
var match = await repository
|
||||
.GetBestMatchAsync(
|
||||
tenant.TenantId,
|
||||
parsedScopeType,
|
||||
scopeId.Trim(),
|
||||
string.IsNullOrWhiteSpace(toolchainHash) ? null : toolchainHash.Trim(),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (match is null)
|
||||
{
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
return Results.Ok(new FailureSignatureBestMatchResponse(match));
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or ValidationException)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record FailureSignatureBestMatchResponse
|
||||
{
|
||||
public FailureSignatureBestMatchResponse(FailureSignatureEntity signature)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(signature);
|
||||
|
||||
SignatureId = signature.SignatureId;
|
||||
ScopeType = signature.ScopeType.ToString().ToLowerInvariant();
|
||||
ScopeId = signature.ScopeId;
|
||||
ToolchainHash = signature.ToolchainHash;
|
||||
ErrorCode = signature.ErrorCode;
|
||||
ErrorCategory = signature.ErrorCategory?.ToString().ToLowerInvariant();
|
||||
PredictedOutcome = signature.PredictedOutcome.ToString().ToLowerInvariant();
|
||||
ConfidenceScore = signature.ConfidenceScore;
|
||||
OccurrenceCount = signature.OccurrenceCount;
|
||||
FirstSeenAt = signature.FirstSeenAt;
|
||||
LastSeenAt = signature.LastSeenAt;
|
||||
}
|
||||
|
||||
public Guid SignatureId { get; }
|
||||
public string ScopeType { get; }
|
||||
public string ScopeId { get; }
|
||||
public string ToolchainHash { get; }
|
||||
public string? ErrorCode { get; }
|
||||
public string? ErrorCategory { get; }
|
||||
public string PredictedOutcome { get; }
|
||||
public decimal? ConfidenceScore { get; }
|
||||
public int OccurrenceCount { get; }
|
||||
public DateTimeOffset FirstSeenAt { get; }
|
||||
public DateTimeOffset LastSeenAt { get; }
|
||||
}
|
||||
}
|
||||
@@ -245,8 +245,8 @@ internal static class PolicySimulationEndpointExtensions
|
||||
|
||||
var preview = new
|
||||
{
|
||||
candidates = inputs.Targets?.Count ?? 0,
|
||||
estimatedRuns = inputs.Targets?.Count ?? 0,
|
||||
candidates = inputs.SbomSet.Length,
|
||||
estimatedRuns = inputs.SbomSet.Length,
|
||||
message = "preview pending execution; actual diff will be available once job starts"
|
||||
};
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ using StellaOps.Plugin.DependencyInjection;
|
||||
using StellaOps.Plugin.Hosting;
|
||||
using StellaOps.Scheduler.WebService.Hosting;
|
||||
using StellaOps.Scheduler.ImpactIndex;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.WebService;
|
||||
using StellaOps.Scheduler.WebService.Auth;
|
||||
using StellaOps.Scheduler.WebService.EventWebhooks;
|
||||
using StellaOps.Scheduler.WebService.FailureSignatures;
|
||||
using StellaOps.Scheduler.WebService.GraphJobs;
|
||||
using StellaOps.Scheduler.WebService.GraphJobs.Events;
|
||||
using StellaOps.Scheduler.WebService.Schedules;
|
||||
@@ -213,6 +215,7 @@ app.MapGraphJobEndpoints();
|
||||
ResolverJobEndpointExtensions.MapResolverJobEndpoints(app);
|
||||
app.MapScheduleEndpoints();
|
||||
app.MapRunEndpoints();
|
||||
app.MapFailureSignatureEndpoints();
|
||||
app.MapPolicyRunEndpoints();
|
||||
app.MapPolicySimulationEndpoints();
|
||||
app.MapSchedulerEventWebhookEndpoints();
|
||||
|
||||
@@ -94,6 +94,17 @@ internal sealed class InMemoryRunRepository : IRunRepository
|
||||
query = query.Where(run => run.CreatedAt > createdAfter);
|
||||
}
|
||||
|
||||
if (options.Cursor is { } cursor)
|
||||
{
|
||||
query = options.SortAscending
|
||||
? query.Where(run => run.CreatedAt > cursor.CreatedAt ||
|
||||
(run.CreatedAt == cursor.CreatedAt &&
|
||||
string.Compare(run.Id, cursor.RunId, StringComparison.Ordinal) > 0))
|
||||
: query.Where(run => run.CreatedAt < cursor.CreatedAt ||
|
||||
(run.CreatedAt == cursor.CreatedAt &&
|
||||
string.Compare(run.Id, cursor.RunId, StringComparison.Ordinal) < 0));
|
||||
}
|
||||
|
||||
query = options.SortAscending
|
||||
? query.OrderBy(run => run.CreatedAt).ThenBy(run => run.Id, StringComparer.Ordinal)
|
||||
: query.OrderByDescending(run => run.CreatedAt).ThenByDescending(run => run.Id, StringComparer.Ordinal);
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Scheduler.ImpactIndex;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.WebService.Auth;
|
||||
using StellaOps.Scheduler.WebService.Schedules;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Runs;
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Schedules;
|
||||
|
||||
public interface ISchedulerAuditService
|
||||
{
|
||||
Task<AuditRecord> WriteAsync(SchedulerAuditEvent auditEvent, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record SchedulerAuditEvent(
|
||||
string TenantId,
|
||||
string Category,
|
||||
string Action,
|
||||
AuditActor Actor,
|
||||
DateTimeOffset? OccurredAt = null,
|
||||
string? AuditId = null,
|
||||
string? EntityId = null,
|
||||
string? ScheduleId = null,
|
||||
string? RunId = null,
|
||||
string? CorrelationId = null,
|
||||
IReadOnlyDictionary<string, string>? Metadata = null,
|
||||
string? Message = null);
|
||||
6
src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md
Normal file
6
src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Active Tasks
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Notes |
|
||||
|----|--------|----------|------------|-------------|-------|
|
||||
| SCHED-WS-TTFS-0341-T4 | DONE (2025-12-18) | Agent | `docs/implplan/SPRINT_0341_0001_0001_ttfs_enhancements.md` | Add failure signature best-match endpoint to support TTFS FirstSignal enrichment. | `GET /api/v1/scheduler/failure-signatures/best-match` + deterministic endpoint tests. |
|
||||
|
||||
Reference in New Issue
Block a user