up
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Policy.Engine.ReachabilityFacts;
|
||||
|
||||
/// <summary>
|
||||
/// Store interface for reachability facts persistence.
|
||||
/// </summary>
|
||||
public interface IReachabilityFactsStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a single reachability fact by key.
|
||||
/// </summary>
|
||||
Task<ReachabilityFact?> GetAsync(
|
||||
string tenantId,
|
||||
string componentPurl,
|
||||
string advisoryId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple reachability facts by keys.
|
||||
/// </summary>
|
||||
Task<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>> GetBatchAsync(
|
||||
IReadOnlyList<ReachabilityFactKey> keys,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Queries reachability facts with filtering.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<ReachabilityFact>> QueryAsync(
|
||||
ReachabilityFactsQuery query,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves or updates a reachability fact.
|
||||
/// </summary>
|
||||
Task SaveAsync(
|
||||
ReachabilityFact fact,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves multiple reachability facts.
|
||||
/// </summary>
|
||||
Task SaveBatchAsync(
|
||||
IReadOnlyList<ReachabilityFact> facts,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a reachability fact.
|
||||
/// </summary>
|
||||
Task DeleteAsync(
|
||||
string tenantId,
|
||||
string componentPurl,
|
||||
string advisoryId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of facts for a tenant.
|
||||
/// </summary>
|
||||
Task<long> CountAsync(
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of the reachability facts store for development and testing.
|
||||
/// </summary>
|
||||
public sealed class InMemoryReachabilityFactsStore : IReachabilityFactsStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<ReachabilityFactKey, ReachabilityFact> _facts = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryReachabilityFactsStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<ReachabilityFact?> GetAsync(
|
||||
string tenantId,
|
||||
string componentPurl,
|
||||
string advisoryId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId);
|
||||
_facts.TryGetValue(key, out var fact);
|
||||
return Task.FromResult(fact);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>> GetBatchAsync(
|
||||
IReadOnlyList<ReachabilityFactKey> keys,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Dictionary<ReachabilityFactKey, ReachabilityFact>();
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (_facts.TryGetValue(key, out var fact))
|
||||
{
|
||||
result[key] = fact;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>>(result);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ReachabilityFact>> QueryAsync(
|
||||
ReachabilityFactsQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var results = _facts.Values
|
||||
.Where(f => f.TenantId == query.TenantId)
|
||||
.Where(f => query.ComponentPurls == null || query.ComponentPurls.Contains(f.ComponentPurl))
|
||||
.Where(f => query.AdvisoryIds == null || query.AdvisoryIds.Contains(f.AdvisoryId))
|
||||
.Where(f => query.States == null || query.States.Contains(f.State))
|
||||
.Where(f => !query.MinConfidence.HasValue || f.Confidence >= query.MinConfidence.Value)
|
||||
.Where(f => query.IncludeExpired || !f.ExpiresAt.HasValue || f.ExpiresAt > now)
|
||||
.OrderByDescending(f => f.ComputedAt)
|
||||
.Skip(query.Skip)
|
||||
.Take(query.Limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<ReachabilityFact>>(results);
|
||||
}
|
||||
|
||||
public Task SaveAsync(ReachabilityFact fact, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId);
|
||||
_facts[key] = fact;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveBatchAsync(IReadOnlyList<ReachabilityFact> facts, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var fact in facts)
|
||||
{
|
||||
var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId);
|
||||
_facts[key] = fact;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(
|
||||
string tenantId,
|
||||
string componentPurl,
|
||||
string advisoryId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId);
|
||||
_facts.TryRemove(key, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<long> CountAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var count = _facts.Values.Count(f => f.TenantId == tenantId);
|
||||
return Task.FromResult((long)count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Index definitions for MongoDB reachability_facts collection.
|
||||
/// </summary>
|
||||
public static class ReachabilityFactsIndexes
|
||||
{
|
||||
/// <summary>
|
||||
/// Primary compound index for efficient lookups.
|
||||
/// </summary>
|
||||
public const string PrimaryIndex = "tenant_component_advisory";
|
||||
|
||||
/// <summary>
|
||||
/// Index for querying by tenant and state.
|
||||
/// </summary>
|
||||
public const string TenantStateIndex = "tenant_state_computed";
|
||||
|
||||
/// <summary>
|
||||
/// Index for TTL expiration.
|
||||
/// </summary>
|
||||
public const string ExpirationIndex = "expires_at_ttl";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index definitions for creating MongoDB indexes.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ReachabilityIndexDefinition> GetIndexDefinitions()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ReachabilityIndexDefinition(
|
||||
PrimaryIndex,
|
||||
new[] { "tenant_id", "component_purl", "advisory_id" },
|
||||
Unique: true),
|
||||
new ReachabilityIndexDefinition(
|
||||
TenantStateIndex,
|
||||
new[] { "tenant_id", "state", "computed_at" },
|
||||
Unique: false),
|
||||
new ReachabilityIndexDefinition(
|
||||
ExpirationIndex,
|
||||
new[] { "expires_at" },
|
||||
Unique: false,
|
||||
ExpireAfterSeconds: 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Index definition for MongoDB collection.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityIndexDefinition(
|
||||
string Name,
|
||||
IReadOnlyList<string> Fields,
|
||||
bool Unique,
|
||||
int? ExpireAfterSeconds = null);
|
||||
Reference in New Issue
Block a user