save checkpoint
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user