Rename Feedser to Concelier
This commit is contained in:
		| @@ -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> | ||||
| @@ -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
											
										
									
								
							| @@ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user