Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Exporter.Json/StellaOps.Concelier.Exporter.Json.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Exporter.TrivyDb/StellaOps.Concelier.Exporter.TrivyDb.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/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