save checkpoint

This commit is contained in:
master
2026-02-11 01:32:14 +02:00
parent 5593212b41
commit cf5b72974f
2316 changed files with 68799 additions and 3808 deletions

View File

@@ -2,9 +2,9 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using StellaOps.SbomService.Lineage.Domain;
using StellaOps.SbomService.Lineage.Services;
namespace StellaOps.SbomService.Controllers;
@@ -59,7 +59,7 @@ public sealed class LineageStreamController : ControllerBase
var digestList = string.IsNullOrWhiteSpace(watchDigests)
? null
: watchDigests.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
: (IReadOnlyList<string>)watchDigests.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
try
{
@@ -67,7 +67,6 @@ public sealed class LineageStreamController : ControllerBase
{
var eventData = System.Text.Json.JsonSerializer.Serialize(new
{
id = evt.EventId,
type = evt.EventType.ToString(),
digest = evt.AffectedDigest,
parentDigest = evt.ParentDigest,
@@ -139,52 +138,40 @@ public sealed class LineageStreamController : ControllerBase
if (fullResult.Graph.Nodes.Count == 0)
return NotFound(new { error = "LINEAGE_NOT_FOUND", artifactDigest });
// Convert to optimizer format
var allNodes = fullResult.Graph.Nodes
.Select(n => new LineageNode(n.Digest, n.Name, n.Version, n.ComponentCount))
.ToImmutableArray();
var allEdges = fullResult.Graph.Edges
.Select(e => new LineageEdge(e.FromDigest, e.ToDigest))
.ToImmutableArray();
var request = new LineageOptimizationRequest
{
TenantId = tenantId,
CenterDigest = artifactDigest,
AllNodes = allNodes,
AllEdges = allEdges,
MaxDepth = maxDepth,
PageSize = pageSize,
PageNumber = pageNumber,
Offset = pageNumber * pageSize,
Limit = pageSize,
SearchTerm = searchTerm
};
var optimized = _optimizer.Optimize(request);
var optimized = _optimizer.Optimize(fullResult.Graph, request);
return Ok(new OptimizedLineageGraphDto
{
CenterDigest = artifactDigest,
Nodes = optimized.Nodes.Select(n => new LineageNodeDto
{
Digest = n.Digest,
Name = n.Name,
Version = n.Version,
ComponentCount = n.ComponentCount
Digest = n.ArtifactDigest,
Name = n.Metadata?.ImageReference ?? n.ArtifactDigest,
Version = n.Metadata?.Tag ?? string.Empty,
ComponentCount = 0
}).ToList(),
Edges = optimized.Edges.Select(e => new LineageEdgeDto
{
FromDigest = e.FromDigest,
ToDigest = e.ToDigest
FromDigest = e.ParentDigest,
ToDigest = e.ChildDigest
}).ToList(),
BoundaryNodes = optimized.BoundaryNodes.Select(b => new BoundaryNodeDto
{
Digest = b.Digest,
HiddenChildrenCount = b.HiddenChildrenCount,
HiddenParentsCount = b.HiddenParentsCount
HiddenChildrenCount = b.HiddenChildCount,
HiddenParentsCount = b.HiddenParentCount
}).ToList(),
TotalNodes = optimized.TotalNodes,
HasMorePages = optimized.HasMorePages,
TotalNodes = optimized.TotalNodeCount,
HasMorePages = optimized.HasMore,
PageNumber = pageNumber,
PageSize = pageSize
});
@@ -219,7 +206,7 @@ public sealed class LineageStreamController : ControllerBase
return;
}
if (!Enum.TryParse<TraversalDirection>(direction, ignoreCase: true, out var traversalDir))
if (!Enum.TryParse<TraversalDirection>(direction, ignoreCase: true, out _))
{
Response.StatusCode = 400;
await Response.WriteAsync("Invalid direction. Use: Children, Parents, or Center");
@@ -243,28 +230,41 @@ public sealed class LineageStreamController : ControllerBase
return;
}
var allNodes = fullResult.Graph.Nodes
.Select(n => new LineageNode(n.Digest, n.Name, n.Version, n.ComponentCount))
.ToImmutableArray();
// Build lookup maps from the full graph for the callback functions
var nodesByDigest = fullResult.Graph.Nodes.ToDictionary(n => n.ArtifactDigest, StringComparer.Ordinal);
var childrenByDigest = fullResult.Graph.Edges
.GroupBy(e => e.ParentDigest, StringComparer.Ordinal)
.ToDictionary(
g => g.Key,
g => (IReadOnlyList<LineageNode>)g
.Select(e => nodesByDigest.GetValueOrDefault(e.ChildDigest))
.Where(n => n is not null)
.ToList()!,
StringComparer.Ordinal);
var parentsByDigest = fullResult.Graph.Edges
.GroupBy(e => e.ChildDigest, StringComparer.Ordinal)
.ToDictionary(
g => g.Key,
g => (IReadOnlyList<LineageNode>)g
.Select(e => nodesByDigest.GetValueOrDefault(e.ParentDigest))
.Where(n => n is not null)
.ToList()!,
StringComparer.Ordinal);
var allEdges = fullResult.Graph.Edges
.Select(e => new LineageEdge(e.FromDigest, e.ToDigest))
.ToImmutableArray();
Task<IReadOnlyList<LineageNode>> GetChildren(string digest, CancellationToken token) =>
Task.FromResult(childrenByDigest.GetValueOrDefault(digest, (IReadOnlyList<LineageNode>)Array.Empty<LineageNode>()));
Task<IReadOnlyList<LineageNode>> GetParents(string digest, CancellationToken token) =>
Task.FromResult(parentsByDigest.GetValueOrDefault(digest, (IReadOnlyList<LineageNode>)Array.Empty<LineageNode>()));
await foreach (var level in _optimizer.TraverseLevelsAsync(
artifactDigest, allNodes, allEdges, traversalDir, maxDepth, ct))
artifactDigest, GetChildren, GetParents, maxDepth, ct))
{
var levelData = System.Text.Json.JsonSerializer.Serialize(new
{
depth = level.Depth,
nodes = level.Nodes.Select(n => new
{
digest = n.Digest,
name = n.Name,
version = n.Version,
componentCount = n.ComponentCount
}),
isComplete = level.IsComplete
depth = level.Level,
direction = level.Direction.ToString(),
nodeDigests = level.NodeDigests
});
await Response.WriteAsync($"event: level\n", ct);
@@ -315,16 +315,22 @@ public sealed class LineageStreamController : ControllerBase
if (fullResult.Graph.Nodes.Count == 0)
return NotFound(new { error = "LINEAGE_NOT_FOUND", artifactDigest });
var allNodes = fullResult.Graph.Nodes
.Select(n => new LineageNode(n.Digest, n.Name, n.Version, n.ComponentCount))
.ToImmutableArray();
var allEdges = fullResult.Graph.Edges
.Select(e => new LineageEdge(e.FromDigest, e.ToDigest))
.ToImmutableArray();
var metadata = await _optimizer.GetOrComputeMetadataAsync(
tenantId, artifactDigest, allNodes, allEdges, ct);
artifactDigest,
tenantId,
async innerCt =>
{
// Compute metadata from the full graph
return new LineageGraphMetadata
{
ArtifactDigest = artifactDigest,
TotalNodes = fullResult.Graph.Nodes.Count,
TotalEdges = fullResult.Graph.Edges.Count,
MaxDepth = ComputeMaxDepth(fullResult.Graph, artifactDigest),
LastUpdated = DateTimeOffset.UtcNow
};
},
ct);
return Ok(new LineageGraphMetadataDto
{
@@ -332,7 +338,7 @@ public sealed class LineageStreamController : ControllerBase
TotalNodes = metadata.TotalNodes,
TotalEdges = metadata.TotalEdges,
MaxDepth = metadata.MaxDepth,
ComputedAt = metadata.ComputedAt
ComputedAt = metadata.LastUpdated
});
}
catch (Exception ex)
@@ -362,7 +368,7 @@ public sealed class LineageStreamController : ControllerBase
if (tenantId == Guid.Empty)
return Unauthorized();
await _optimizer.InvalidateCacheAsync(tenantId, artifactDigest, ct);
await _optimizer.InvalidateCacheAsync(artifactDigest, tenantId, ct);
return NoContent();
}
@@ -371,6 +377,35 @@ public sealed class LineageStreamController : ControllerBase
// TODO: Extract from claims or headers
return Guid.Parse("00000000-0000-0000-0000-000000000001");
}
private static int ComputeMaxDepth(LineageGraph graph, string centerDigest)
{
if (graph.Nodes.Count == 0)
return 0;
var childrenMap = graph.Edges
.GroupBy(e => e.ParentDigest)
.ToDictionary(g => g.Key, g => g.Select(e => e.ChildDigest).ToList());
var visited = new HashSet<string>(StringComparer.Ordinal);
var maxDepth = 0;
void Dfs(string digest, int depth)
{
if (!visited.Add(digest))
return;
if (depth > maxDepth)
maxDepth = depth;
if (childrenMap.TryGetValue(digest, out var children))
{
foreach (var child in children)
Dfs(child, depth + 1);
}
}
Dfs(centerDigest, 0);
return maxDepth;
}
}
// DTOs for API responses