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
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:
@@ -18,7 +18,8 @@ using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
||||
using StellaOps.Cli.Services.Models.Transport;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
@@ -30,10 +31,12 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
private static readonly IReadOnlyDictionary<string, object?> EmptyMetadata =
|
||||
new ReadOnlyDictionary<string, object?>(new Dictionary<string, object?>(0, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
private const string OperatorReasonParameterName = "operator_reason";
|
||||
private const string OperatorTicketParameterName = "operator_ticket";
|
||||
private const string BackfillReasonParameterName = "backfill_reason";
|
||||
private const string BackfillTicketParameterName = "backfill_ticket";
|
||||
private const string OperatorReasonParameterName = "operator_reason";
|
||||
private const string OperatorTicketParameterName = "operator_ticket";
|
||||
private const string BackfillReasonParameterName = "backfill_reason";
|
||||
private const string BackfillTicketParameterName = "backfill_ticket";
|
||||
private const string AdvisoryScopesHeader = "X-StellaOps-Scopes";
|
||||
private const string AdvisoryRunScope = "advisory:run";
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly StellaOpsCliOptions _options;
|
||||
@@ -885,13 +888,122 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
throw new InvalidOperationException("EntryTrace response payload was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(
|
||||
AdvisoryAiTaskType taskType,
|
||||
AdvisoryPipelinePlanRequestModel request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var taskSegment = taskType.ToString().ToLowerInvariant();
|
||||
var relative = $"v1/advisory-ai/pipeline/{taskSegment}";
|
||||
|
||||
var payload = new AdvisoryPipelinePlanRequestModel
|
||||
{
|
||||
TaskType = taskType,
|
||||
AdvisoryKey = string.IsNullOrWhiteSpace(request.AdvisoryKey) ? string.Empty : request.AdvisoryKey.Trim(),
|
||||
ArtifactId = string.IsNullOrWhiteSpace(request.ArtifactId) ? null : request.ArtifactId!.Trim(),
|
||||
ArtifactPurl = string.IsNullOrWhiteSpace(request.ArtifactPurl) ? null : request.ArtifactPurl!.Trim(),
|
||||
PolicyVersion = string.IsNullOrWhiteSpace(request.PolicyVersion) ? null : request.PolicyVersion!.Trim(),
|
||||
Profile = string.IsNullOrWhiteSpace(request.Profile) ? "default" : request.Profile!.Trim(),
|
||||
PreferredSections = request.PreferredSections is null
|
||||
? null
|
||||
: request.PreferredSections
|
||||
.Where(static section => !string.IsNullOrWhiteSpace(section))
|
||||
.Select(static section => section.Trim())
|
||||
.ToArray(),
|
||||
ForceRefresh = request.ForceRefresh
|
||||
};
|
||||
|
||||
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
|
||||
ApplyAdvisoryAiEndpoint(httpRequest, taskType);
|
||||
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(failure);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var plan = await response.Content.ReadFromJsonAsync<AdvisoryPipelinePlanResponseModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (plan is null)
|
||||
{
|
||||
throw new InvalidOperationException("Advisory AI plan response was empty.");
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = response.Content is null
|
||||
? string.Empty
|
||||
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse advisory plan response. {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AdvisoryPipelineOutputModel?> TryGetAdvisoryPipelineOutputAsync(
|
||||
string cacheKey,
|
||||
AdvisoryAiTaskType taskType,
|
||||
string profile,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cacheKey))
|
||||
{
|
||||
throw new ArgumentException("Cache key is required.", nameof(cacheKey));
|
||||
}
|
||||
|
||||
var encodedKey = Uri.EscapeDataString(cacheKey);
|
||||
var taskSegment = Uri.EscapeDataString(taskType.ToString().ToLowerInvariant());
|
||||
var resolvedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile.Trim();
|
||||
var relative = $"v1/advisory-ai/outputs/{encodedKey}?taskType={taskSegment}&profile={Uri.EscapeDataString(resolvedProfile)}";
|
||||
|
||||
using var request = CreateRequest(HttpMethod.Get, relative);
|
||||
ApplyAdvisoryAiEndpoint(request, taskType);
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(failure);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<AdvisoryPipelineOutputModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = response.Content is null
|
||||
? string.Empty
|
||||
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse advisory output response. {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = includeDisabled ? "?includeDisabled=true" : string.Empty;
|
||||
using var request = CreateRequest(HttpMethod.Get, $"excititor/providers{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
@@ -1778,7 +1890,44 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(HttpMethod method, string relativeUri)
|
||||
private void ApplyAdvisoryAiEndpoint(HttpRequestMessage request, AdvisoryAiTaskType taskType)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
var requestUri = request.RequestUri ?? throw new InvalidOperationException("Request URI was not initialized.");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_options.AdvisoryAiUrl) &&
|
||||
Uri.TryCreate(_options.AdvisoryAiUrl, UriKind.Absolute, out var advisoryBase))
|
||||
{
|
||||
if (!requestUri.IsAbsoluteUri)
|
||||
{
|
||||
request.RequestUri = new Uri(advisoryBase, requestUri.ToString());
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(_options.AdvisoryAiUrl))
|
||||
{
|
||||
throw new InvalidOperationException($"Advisory AI URL '{_options.AdvisoryAiUrl}' is not a valid absolute URI.");
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
}
|
||||
|
||||
var taskScope = $"advisory:{taskType.ToString().ToLowerInvariant()}";
|
||||
var combined = $"{AdvisoryRunScope} {taskScope}";
|
||||
|
||||
if (request.Headers.Contains(AdvisoryScopesHeader))
|
||||
{
|
||||
request.Headers.Remove(AdvisoryScopesHeader);
|
||||
}
|
||||
|
||||
request.Headers.TryAddWithoutValidation(AdvisoryScopesHeader, combined);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(HttpMethod method, string relativeUri)
|
||||
{
|
||||
if (!Uri.TryCreate(relativeUri, UriKind.RelativeOrAbsolute, out var requestUri))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user