Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added MongoPackRunApprovalStore for managing approval states with MongoDB.
- Introduced MongoPackRunArtifactUploader for uploading and storing artifacts.
- Created MongoPackRunLogStore to handle logging of pack run events.
- Developed MongoPackRunStateStore for persisting and retrieving pack run states.
- Implemented unit tests for MongoDB stores to ensure correct functionality.
- Added MongoTaskRunnerTestContext for setting up MongoDB test environment.
- Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
master
2025-11-07 10:01:35 +02:00
parent e5ffcd6535
commit a1ce3f74fa
122 changed files with 8730 additions and 914 deletions

View File

@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading.RateLimiting;
@@ -9,10 +10,13 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Caching;
using StellaOps.AdvisoryAI.Diagnostics;
using StellaOps.AdvisoryAI.Hosting;
using StellaOps.AdvisoryAI.Metrics;
using StellaOps.AdvisoryAI.Outputs;
using StellaOps.AdvisoryAI.Orchestration;
using StellaOps.AdvisoryAI.Queue;
using StellaOps.AdvisoryAI.WebService.Contracts;
var builder = WebApplication.CreateBuilder(args);
@@ -72,6 +76,9 @@ app.MapPost("/v1/advisory-ai/pipeline/{taskType}", HandleSinglePlan)
app.MapPost("/v1/advisory-ai/pipeline:batch", HandleBatchPlans)
.RequireRateLimiting("advisory-ai");
app.MapGet("/v1/advisory-ai/outputs/{cacheKey}", HandleGetOutput)
.RequireRateLimiting("advisory-ai");
app.Run();
static async Task<IResult> HandleSinglePlan(
@@ -85,6 +92,10 @@ static async Task<IResult> HandleSinglePlan(
AdvisoryPipelineMetrics pipelineMetrics,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.plan_request", ActivityKind.Server);
activity?.SetTag("advisory.task_type", taskType);
activity?.SetTag("advisory.advisory_key", request.AdvisoryKey);
if (!Enum.TryParse<AdvisoryTaskType>(taskType, ignoreCase: true, out var parsedType))
{
return Results.BadRequest(new { error = $"Unknown task type '{taskType}'." });
@@ -103,6 +114,7 @@ static async Task<IResult> HandleSinglePlan(
var normalizedRequest = request with { TaskType = parsedType };
var taskRequest = normalizedRequest.ToTaskRequest();
var plan = await orchestrator.CreatePlanAsync(taskRequest, cancellationToken).ConfigureAwait(false);
activity?.SetTag("advisory.plan_cache_key", plan.CacheKey);
await planCache.SetAsync(plan.CacheKey, plan, cancellationToken).ConfigureAwait(false);
await taskQueue.EnqueueAsync(new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request), cancellationToken).ConfigureAwait(false);
@@ -125,6 +137,9 @@ static async Task<IResult> HandleBatchPlans(
AdvisoryPipelineMetrics pipelineMetrics,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.plan_batch", ActivityKind.Server);
activity?.SetTag("advisory.batch_size", batchRequest.Requests.Count);
if (batchRequest.Requests.Count == 0)
{
return Results.BadRequest(new { error = "At least one request must be supplied." });
@@ -153,6 +168,12 @@ static async Task<IResult> HandleBatchPlans(
var normalizedRequest = item with { TaskType = parsedType };
var taskRequest = normalizedRequest.ToTaskRequest();
var plan = await orchestrator.CreatePlanAsync(taskRequest, cancellationToken).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("advisory.plan.created", tags: new ActivityTagsCollection
{
{ "advisory.task_type", plan.Request.TaskType.ToString() },
{ "advisory.advisory_key", plan.Request.AdvisoryKey },
{ "advisory.plan_cache_key", plan.CacheKey }
}));
await planCache.SetAsync(plan.CacheKey, plan, cancellationToken).ConfigureAwait(false);
await taskQueue.EnqueueAsync(new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request), cancellationToken).ConfigureAwait(false);
@@ -167,6 +188,37 @@ static async Task<IResult> HandleBatchPlans(
return Results.Ok(results);
}
static async Task<IResult> HandleGetOutput(
HttpContext httpContext,
string cacheKey,
string taskType,
string? profile,
IAdvisoryOutputStore outputStore,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(outputStore);
if (!Enum.TryParse<AdvisoryTaskType>(taskType, ignoreCase: true, out var parsedTaskType))
{
return Results.BadRequest(new { error = $"Unknown task type '{taskType}'." });
}
if (!EnsureAuthorized(httpContext, parsedTaskType))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
var resolvedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile!.Trim();
var output = await outputStore.TryGetAsync(cacheKey, parsedTaskType, resolvedProfile, cancellationToken)
.ConfigureAwait(false);
if (output is null)
{
return Results.NotFound(new { error = "Output not found." });
}
return Results.Ok(AdvisoryOutputResponse.FromDomain(output));
}
static bool EnsureAuthorized(HttpContext context, AdvisoryTaskType taskType)
{
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))