Rename Feedser to Concelier

This commit is contained in:
2025-10-18 20:04:15 +03:00
parent 7e1b10d3b2
commit 0137856fdb
1208 changed files with 4370 additions and 4370 deletions

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Concelier.Exporter.Json/StellaOps.Concelier.Exporter.Json.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Exporter.TrivyDb/StellaOps.Concelier.Exporter.TrivyDb.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,86 @@
using System;
using StellaOps.Concelier.Exporter.TrivyDb;
using StellaOps.Concelier.Storage.Mongo.Exporting;
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
public sealed class TrivyDbExportPlannerTests
{
[Fact]
public void CreatePlan_ReturnsFullWhenStateMissing()
{
var planner = new TrivyDbExportPlanner();
var manifest = new[] { new ExportFileRecord("path.json", 10, "sha256:a") };
var plan = planner.CreatePlan(existingState: null, treeDigest: "sha256:abcd", manifest);
Assert.Equal(TrivyDbExportMode.Full, plan.Mode);
Assert.Equal("sha256:abcd", plan.TreeDigest);
Assert.Null(plan.BaseExportId);
Assert.Null(plan.BaseManifestDigest);
Assert.True(plan.ResetBaseline);
Assert.Equal(manifest, plan.Manifest);
}
[Fact]
public void CreatePlan_ReturnsSkipWhenCursorMatches()
{
var planner = new TrivyDbExportPlanner();
var existingManifest = new[] { new ExportFileRecord("path.json", 10, "sha256:a") };
var state = new ExportStateRecord(
Id: TrivyDbFeedExporter.ExporterId,
BaseExportId: "20240810T000000Z",
BaseDigest: "sha256:base",
LastFullDigest: "sha256:base",
LastDeltaDigest: null,
ExportCursor: "sha256:unchanged",
TargetRepository: "concelier/trivy",
ExporterVersion: "1.0",
UpdatedAt: DateTimeOffset.UtcNow,
Files: existingManifest);
var plan = planner.CreatePlan(state, "sha256:unchanged", existingManifest);
Assert.Equal(TrivyDbExportMode.Skip, plan.Mode);
Assert.Equal("sha256:unchanged", plan.TreeDigest);
Assert.Equal("20240810T000000Z", plan.BaseExportId);
Assert.Equal("sha256:base", plan.BaseManifestDigest);
Assert.False(plan.ResetBaseline);
Assert.Empty(plan.ChangedFiles);
Assert.Empty(plan.RemovedPaths);
}
[Fact]
public void CreatePlan_ReturnsFullWhenCursorDiffers()
{
var planner = new TrivyDbExportPlanner();
var manifest = new[] { new ExportFileRecord("path.json", 10, "sha256:a") };
var state = new ExportStateRecord(
Id: TrivyDbFeedExporter.ExporterId,
BaseExportId: "20240810T000000Z",
BaseDigest: "sha256:base",
LastFullDigest: "sha256:base",
LastDeltaDigest: null,
ExportCursor: "sha256:old",
TargetRepository: "concelier/trivy",
ExporterVersion: "1.0",
UpdatedAt: DateTimeOffset.UtcNow,
Files: manifest);
var newManifest = new[] { new ExportFileRecord("path.json", 10, "sha256:b") };
var plan = planner.CreatePlan(state, "sha256:new", newManifest);
Assert.Equal(TrivyDbExportMode.Delta, plan.Mode);
Assert.Equal("sha256:new", plan.TreeDigest);
Assert.Equal("20240810T000000Z", plan.BaseExportId);
Assert.Equal("sha256:base", plan.BaseManifestDigest);
Assert.False(plan.ResetBaseline);
Assert.Single(plan.ChangedFiles);
var deltaState = state with { LastDeltaDigest = "sha256:delta" };
var deltaPlan = planner.CreatePlan(deltaState, "sha256:newer", newManifest);
Assert.Equal(TrivyDbExportMode.Full, deltaPlan.Mode);
Assert.True(deltaPlan.ResetBaseline);
Assert.Equal(deltaPlan.Manifest, deltaPlan.ChangedFiles);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Concelier.Storage.Mongo.Exporting;
using Xunit;
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
public sealed class TrivyDbOciWriterTests : IDisposable
{
private readonly string _root;
public TrivyDbOciWriterTests()
{
_root = Directory.CreateTempSubdirectory("trivy-writer-tests").FullName;
}
[Fact]
public async Task WriteAsync_ReusesBlobsFromBaseLayout_WhenDigestMatches()
{
var baseLayout = Path.Combine(_root, "base");
Directory.CreateDirectory(Path.Combine(baseLayout, "blobs", "sha256"));
var configBytes = Encoding.UTF8.GetBytes("base-config");
var configDigest = ComputeDigest(configBytes);
WriteBlob(baseLayout, configDigest, configBytes);
var layerBytes = Encoding.UTF8.GetBytes("base-layer");
var layerDigest = ComputeDigest(layerBytes);
WriteBlob(baseLayout, layerDigest, layerBytes);
var manifest = CreateManifest(configDigest, layerDigest);
var manifestBytes = SerializeManifest(manifest);
var manifestDigest = ComputeDigest(manifestBytes);
WriteBlob(baseLayout, manifestDigest, manifestBytes);
var plan = new TrivyDbExportPlan(
TrivyDbExportMode.Delta,
TreeDigest: "sha256:tree",
BaseExportId: "20241101T000000Z",
BaseManifestDigest: manifestDigest,
ResetBaseline: false,
Manifest: Array.Empty<ExportFileRecord>(),
ChangedFiles: new[] { new ExportFileRecord("data.json", 1, "sha256:data") },
RemovedPaths: Array.Empty<string>());
var configDescriptor = new OciDescriptor(TrivyDbMediaTypes.TrivyConfig, configDigest, configBytes.Length);
var layerDescriptor = new OciDescriptor(TrivyDbMediaTypes.TrivyLayer, layerDigest, layerBytes.Length);
var package = new TrivyDbPackage(
manifest,
new TrivyConfigDocument(
TrivyDbMediaTypes.TrivyConfig,
DateTimeOffset.Parse("2024-11-01T00:00:00Z"),
"20241101T000000Z",
layerDigest,
layerBytes.Length),
new Dictionary<string, TrivyDbBlob>(StringComparer.Ordinal)
{
[configDigest] = CreateThrowingBlob(),
[layerDigest] = CreateThrowingBlob(),
},
JsonSerializer.SerializeToUtf8Bytes(new { mode = "delta" }));
var writer = new TrivyDbOciWriter();
var destination = Path.Combine(_root, "delta");
await writer.WriteAsync(package, destination, reference: "example/trivy:delta", plan, baseLayout, CancellationToken.None);
var reusedConfig = File.ReadAllBytes(GetBlobPath(destination, configDigest));
Assert.Equal(configBytes, reusedConfig);
var reusedLayer = File.ReadAllBytes(GetBlobPath(destination, layerDigest));
Assert.Equal(layerBytes, reusedLayer);
}
private static TrivyDbBlob CreateThrowingBlob()
{
var ctor = typeof(TrivyDbBlob).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
binder: null,
new[] { typeof(Func<CancellationToken, ValueTask<Stream>>), typeof(long) },
modifiers: null)
?? throw new InvalidOperationException("Unable to access TrivyDbBlob constructor.");
Func<CancellationToken, ValueTask<Stream>> factory = _ => throw new InvalidOperationException("Blob should have been reused from base layout.");
return (TrivyDbBlob)ctor.Invoke(new object[] { factory, 0L });
}
private static OciManifest CreateManifest(string configDigest, string layerDigest)
{
var configDescriptor = new OciDescriptor(TrivyDbMediaTypes.TrivyConfig, configDigest, 0);
var layerDescriptor = new OciDescriptor(TrivyDbMediaTypes.TrivyLayer, layerDigest, 0);
return new OciManifest(
SchemaVersion: 2,
MediaType: TrivyDbMediaTypes.OciManifest,
Config: configDescriptor,
Layers: new[] { layerDescriptor });
}
private static byte[] SerializeManifest(OciManifest manifest)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false,
};
return JsonSerializer.SerializeToUtf8Bytes(manifest, options);
}
private static void WriteBlob(string layoutRoot, string digest, byte[] payload)
{
var path = GetBlobPath(layoutRoot, digest);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllBytes(path, payload);
}
private static string GetBlobPath(string layoutRoot, string digest)
{
var fileName = digest[7..];
return Path.Combine(layoutRoot, "blobs", "sha256", fileName);
}
private static string ComputeDigest(byte[] payload)
{
var hash = SHA256.HashData(payload);
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
}
public void Dispose()
{
try
{
if (Directory.Exists(_root))
{
Directory.Delete(_root, recursive: true);
}
}
catch
{
// best effort cleanup
}
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Concelier.Exporter.TrivyDb;
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
public sealed class TrivyDbPackageBuilderTests
{
[Fact]
public void BuildsOciManifestWithExpectedMediaTypes()
{
var metadata = Encoding.UTF8.GetBytes("{\"generatedAt\":\"2024-07-15T12:00:00Z\"}");
var archive = Enumerable.Range(0, 256).Select(static b => (byte)b).ToArray();
var archivePath = Path.GetTempFileName();
File.WriteAllBytes(archivePath, archive);
var archiveDigest = ComputeDigest(archive);
try
{
var request = new TrivyDbPackageRequest(
metadata,
archivePath,
archiveDigest,
archive.LongLength,
DateTimeOffset.Parse("2024-07-15T12:00:00Z"),
"2024.07.15");
var builder = new TrivyDbPackageBuilder();
var package = builder.BuildPackage(request);
Assert.Equal(TrivyDbMediaTypes.OciManifest, package.Manifest.MediaType);
Assert.Equal(TrivyDbMediaTypes.TrivyConfig, package.Manifest.Config.MediaType);
var layer = Assert.Single(package.Manifest.Layers);
Assert.Equal(TrivyDbMediaTypes.TrivyLayer, layer.MediaType);
var configBytes = JsonSerializer.SerializeToUtf8Bytes(package.Config, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var expectedConfigDigest = ComputeDigest(configBytes);
Assert.Equal(expectedConfigDigest, package.Manifest.Config.Digest);
Assert.Equal(archiveDigest, layer.Digest);
Assert.True(package.Blobs.ContainsKey(archiveDigest));
Assert.Equal(archive.LongLength, package.Blobs[archiveDigest].Length);
Assert.True(package.Blobs.ContainsKey(expectedConfigDigest));
Assert.Equal(metadata, package.MetadataJson.ToArray());
}
finally
{
if (File.Exists(archivePath))
{
File.Delete(archivePath);
}
}
}
[Fact]
public void ThrowsWhenMetadataMissing()
{
var builder = new TrivyDbPackageBuilder();
var archivePath = Path.GetTempFileName();
var archiveBytes = new byte[] { 1, 2, 3 };
File.WriteAllBytes(archivePath, archiveBytes);
var digest = ComputeDigest(archiveBytes);
try
{
Assert.Throws<ArgumentException>(() => builder.BuildPackage(new TrivyDbPackageRequest(
ReadOnlyMemory<byte>.Empty,
archivePath,
digest,
archiveBytes.LongLength,
DateTimeOffset.UtcNow,
"1")));
}
finally
{
if (File.Exists(archivePath))
{
File.Delete(archivePath);
}
}
}
private static string ComputeDigest(ReadOnlySpan<byte> payload)
{
var hash = SHA256.HashData(payload);
var hex = Convert.ToHexString(hash);
return "sha256:" + hex.ToLowerInvariant();
}
}