blockers 2
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-23 14:54:17 +02:00
parent f47d2d1377
commit cce96f3596
100 changed files with 2758 additions and 1912 deletions

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
namespace StellaOps.Concelier.WebService.Contracts;
public sealed record AdvisorySummaryResponse(
AdvisorySummaryMeta Meta,
IReadOnlyList<AdvisorySummaryItem> Items);
public sealed record AdvisorySummaryMeta(
string Tenant,
int Count,
string? Next,
string Sort);
public sealed record AdvisorySummaryItem(
string AdvisoryKey,
string Source,
string? LinksetId,
double? Confidence,
IReadOnlyList<AdvisorySummaryConflict>? Conflicts,
AdvisorySummaryCounts Counts,
AdvisorySummaryProvenance Provenance,
IReadOnlyList<string> Aliases,
string? ObservedAt);
public sealed record AdvisorySummaryConflict(
string Field,
string Reason,
IReadOnlyList<string>? SourceIds);
public sealed record AdvisorySummaryCounts(
int Observations,
int ConflictFields);
public sealed record AdvisorySummaryProvenance(
IReadOnlyList<string>? ObservationIds,
string? Schema);

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.WebService.Contracts;
namespace StellaOps.Concelier.WebService.Extensions;
internal static class AdvisorySummaryMapper
{
public static AdvisorySummaryItem ToSummary(AdvisoryLinkset linkset)
{
ArgumentNullException.ThrowIfNull(linkset);
var aliases = linkset.Normalized?.Purls ?? Array.Empty<string>();
var conflictFields = linkset.Conflicts?.Select(c => c.Field).Distinct(StringComparer.Ordinal).Count() ?? 0;
var conflicts = linkset.Conflicts?.Select(c => new AdvisorySummaryConflict(
c.Field,
c.Reason,
c.SourceIds?.ToArray()
)).ToArray();
return new AdvisorySummaryItem(
AdvisoryKey: linkset.AdvisoryId,
Source: linkset.Source,
LinksetId: linkset.BuiltByJobId,
Confidence: linkset.Confidence,
Conflicts: conflicts,
Counts: new AdvisorySummaryCounts(
Observations: linkset.ObservationIds.Length,
ConflictFields: conflictFields),
Provenance: new AdvisorySummaryProvenance(
ObservationIds: linkset.ObservationIds.ToArray(),
Schema: "lnm-1.0"),
Aliases: aliases.ToArray(),
ObservedAt: linkset.CreatedAt.UtcDateTime.ToString("O"));
}
public static AdvisorySummaryResponse ToResponse(
string tenant,
IReadOnlyList<AdvisorySummaryItem> items,
string? nextCursor,
string sort)
{
return new AdvisorySummaryResponse(
new AdvisorySummaryMeta(
Tenant: tenant,
Count: items.Count,
Next: nextCursor,
Sort: sort),
items);
}
}

View File

@@ -1,47 +1,49 @@
using System.Diagnostics.Metrics;
using System.Collections.Generic;
namespace StellaOps.Concelier.WebService.Telemetry;
internal sealed class LinksetCacheTelemetry
{
private static readonly Meter Meter = new("StellaOps.Concelier.Linksets");
private readonly Counter<long> _hitTotal;
private readonly Counter<long> _writeTotal;
private readonly Histogram<double> _rebuildMs;
public LinksetCacheTelemetry(IMeterFactory meterFactory)
public LinksetCacheTelemetry()
{
var meter = meterFactory.Create("StellaOps.Concelier.Linksets");
_hitTotal = meter.CreateCounter<long>("lnm.cache.hit_total", unit: "hit", description: "Cache hits for LNM linksets");
_writeTotal = meter.CreateCounter<long>("lnm.cache.write_total", unit: "write", description: "Cache writes for LNM linksets");
_rebuildMs = meter.CreateHistogram<double>("lnm.cache.rebuild_ms", unit: "ms", description: "Synchronous rebuild latency for LNM cache");
_hitTotal = Meter.CreateCounter<long>("lnm.cache.hit_total", unit: "hit", description: "Cache hits for LNM linksets");
_writeTotal = Meter.CreateCounter<long>("lnm.cache.write_total", unit: "write", description: "Cache writes for LNM linksets");
_rebuildMs = Meter.CreateHistogram<double>("lnm.cache.rebuild_ms", unit: "ms", description: "Synchronous rebuild latency for LNM cache");
}
public void RecordHit(string? tenant, string source)
{
var tags = new TagList
var tags = new KeyValuePair<string, object?>[]
{
{ "tenant", tenant ?? string.Empty },
{ "source", source }
new("tenant", tenant ?? string.Empty),
new("source", source)
};
_hitTotal.Add(1, tags);
}
public void RecordWrite(string? tenant, string source)
{
var tags = new TagList
var tags = new KeyValuePair<string, object?>[]
{
{ "tenant", tenant ?? string.Empty },
{ "source", source }
new("tenant", tenant ?? string.Empty),
new("source", source)
};
_writeTotal.Add(1, tags);
}
public void RecordRebuild(string? tenant, string source, double elapsedMs)
{
var tags = new TagList
var tags = new KeyValuePair<string, object?>[]
{
{ "tenant", tenant ?? string.Empty },
{ "source", source }
new("tenant", tenant ?? string.Empty),
new("source", source)
};
_rebuildMs.Record(elapsedMs, tags);
}

View File

@@ -26,12 +26,12 @@ public class AdvisoryChunkBuilderTests
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
fingerprint: "fp",
chunkLimit: 5,
observationLimit: 5,
sectionFilter: ImmutableHashSet.Create("workaround"),
formatFilter: ImmutableHashSet<string>.Empty,
minimumLength: 1);
"fp",
5,
5,
ImmutableHashSet.Create("workaround"),
ImmutableHashSet<string>.Empty,
1);
var builder = new AdvisoryChunkBuilder(_hash);
var result = builder.Build(options, advisory, new[] { observation });
@@ -54,12 +54,12 @@ public class AdvisoryChunkBuilderTests
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
fingerprint: "fp",
chunkLimit: 5,
observationLimit: 5,
sectionFilter: ImmutableHashSet.Create("workaround"),
formatFilter: ImmutableHashSet<string>.Empty,
minimumLength: 1);
"fp",
5,
5,
ImmutableHashSet.Create("workaround"),
ImmutableHashSet<string>.Empty,
1);
var builder = new AdvisoryChunkBuilder(_hash);
var result = builder.Build(options, advisory, new[] { observation });
@@ -115,9 +115,9 @@ public class AdvisoryChunkBuilderTests
fetchedAt: timestamp,
receivedAt: timestamp,
contentHash: "sha256:deadbeef",
signature: new AdvisoryObservationSignature(present: false)),
signature: new AdvisoryObservationSignature(present: false, format: null, keyId: null, signature: null)),
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
rawLinkset: new RawLinkset(),
createdAt: timestamp);
}

View File

@@ -14,13 +14,13 @@ public class AdvisoryChunkCacheKeyTests
public void Create_NormalizesObservationOrdering()
{
var options = new AdvisoryChunkBuildOptions(
AdvisoryKey: "CVE-2025-0001",
Fingerprint: "fp",
ChunkLimit: 10,
ObservationLimit: 10,
SectionFilter: ImmutableHashSet.Create("workaround"),
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 8);
"CVE-2025-0001",
"fp",
10,
10,
ImmutableHashSet.Create("workaround"),
ImmutableHashSet<string>.Empty,
8);
var first = BuildObservation("obs-1", "sha256:one", "2025-11-18T00:00:00Z");
var second = BuildObservation("obs-2", "sha256:two", "2025-11-18T00:05:00Z");
@@ -29,7 +29,6 @@ public class AdvisoryChunkCacheKeyTests
var reversed = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0001", options, new[] { second, first }, "fp");
Assert.Equal(ordered.Value, reversed.Value);
Assert.Equal(ordered.ComputeHash(), reversed.ComputeHash());
}
[Fact]
@@ -37,21 +36,21 @@ public class AdvisoryChunkCacheKeyTests
{
var optionsLower = new AdvisoryChunkBuildOptions(
"CVE-2025-0002",
Fingerprint: "fp",
ChunkLimit: 5,
ObservationLimit: 5,
SectionFilter: ImmutableHashSet.Create("workaround", "fix"),
FormatFilter: ImmutableHashSet.Create("ndjson"),
MinimumLength: 1);
"fp",
5,
5,
ImmutableHashSet.Create("workaround", "fix"),
ImmutableHashSet.Create("ndjson"),
1);
var optionsUpper = new AdvisoryChunkBuildOptions(
"CVE-2025-0002",
Fingerprint: "fp",
ChunkLimit: 5,
ObservationLimit: 5,
SectionFilter: ImmutableHashSet.Create("WorkAround", "FIX"),
FormatFilter: ImmutableHashSet.Create("NDJSON"),
MinimumLength: 1);
"fp",
5,
5,
ImmutableHashSet.Create("WorkAround", "FIX"),
ImmutableHashSet.Create("NDJSON"),
1);
var observation = BuildObservation("obs-3", "sha256:three", "2025-11-18T00:10:00Z");
@@ -59,7 +58,6 @@ public class AdvisoryChunkCacheKeyTests
var upper = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0002", optionsUpper, new[] { observation }, "fp");
Assert.Equal(lower.Value, upper.Value);
Assert.Equal(lower.ComputeHash(), upper.ComputeHash());
}
[Fact]
@@ -67,12 +65,12 @@ public class AdvisoryChunkCacheKeyTests
{
var options = new AdvisoryChunkBuildOptions(
"CVE-2025-0003",
Fingerprint: "fp",
ChunkLimit: 5,
ObservationLimit: 5,
SectionFilter: ImmutableHashSet<string>.Empty,
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 1);
"fp",
5,
5,
ImmutableHashSet<string>.Empty,
ImmutableHashSet<string>.Empty,
1);
var original = BuildObservation("obs-4", "sha256:orig", "2025-11-18T00:15:00Z");
var mutated = BuildObservation("obs-4", "sha256:mut", "2025-11-18T00:15:00Z");
@@ -81,7 +79,6 @@ public class AdvisoryChunkCacheKeyTests
var mutatedKey = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0003", options, new[] { mutated }, "fp");
Assert.NotEqual(originalKey.Value, mutatedKey.Value);
Assert.NotEqual(originalKey.ComputeHash(), mutatedKey.ComputeHash());
}
private static AdvisoryObservation BuildObservation(string id, string contentHash, string timestamp)
@@ -98,9 +95,9 @@ public class AdvisoryChunkCacheKeyTests
fetchedAt: createdAt,
receivedAt: createdAt,
contentHash: contentHash,
signature: new AdvisoryObservationSignature(false)),
signature: new AdvisoryObservationSignature(false, null, null, null)),
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
rawLinkset: new RawLinkset(),
createdAt: createdAt);
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.WebService.Extensions;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests;
public class AdvisorySummaryMapperTests
{
[Fact]
public void Maps_basic_fields()
{
var linkset = new AdvisoryLinkset(
TenantId: "tenant-a",
Source: "nvd",
AdvisoryId: "CVE-2024-1234",
ObservationIds: ImmutableArray.Create("obs1", "obs2"),
Normalized: new AdvisoryLinksetNormalized(
Purls: new[] { "pkg:maven/log4j/log4j@2.17.1" },
Versions: null,
Ranges: null,
Severities: null),
Provenance: null,
Confidence: 0.8,
Conflicts: new[]
{
new AdvisoryLinksetConflict("severity", "severity-mismatch", Array.Empty<string>(), new [] { "nvd", "vendor" })
},
CreatedAt: DateTimeOffset.UnixEpoch,
BuiltByJobId: "job-123");
var summary = AdvisorySummaryMapper.ToSummary(linkset);
Assert.Equal("CVE-2024-1234", summary.AdvisoryKey);
Assert.Equal("nvd", summary.Source);
Assert.Equal(2, summary.Counts.Observations);
Assert.Equal(1, summary.Counts.ConflictFields);
Assert.NotNull(summary.Conflicts);
Assert.Equal("job-123", summary.LinksetId);
Assert.Equal("pkg:maven/log4j/log4j@2.17.1", Assert.Single(summary.Aliases));
}
}

View File

@@ -37,18 +37,17 @@ public sealed class AdvisoryChunkBuilderTests
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
"fingerprint-1",
chunkLimit: 5,
observationLimit: 5,
SectionFilter: ImmutableHashSet<string>.Empty,
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 0);
5,
5,
ImmutableHashSet<string>.Empty,
ImmutableHashSet<string>.Empty,
0);
var result = builder.Build(options, advisory, new[] { observation });
var entry = Assert.Single(result.Response.Entries);
Assert.Equal("/references/0/title", entry.Provenance.ObservationPath);
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
Assert.Equal(new[] { "/references/0/title" }, entry.Provenance.FieldMask);
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0/title"), entry.ChunkId);
}
@@ -69,18 +68,17 @@ public sealed class AdvisoryChunkBuilderTests
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
"fingerprint-2",
chunkLimit: 5,
observationLimit: 5,
SectionFilter: ImmutableHashSet<string>.Empty,
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 0);
5,
5,
ImmutableHashSet<string>.Empty,
ImmutableHashSet<string>.Empty,
0);
var result = builder.Build(options, advisory, new[] { observation });
var entry = Assert.Single(result.Response.Entries);
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
Assert.Equal(new[] { "/references/0" }, entry.Provenance.FieldMask);
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0"), entry.ChunkId);
}

View File

@@ -21,4 +21,6 @@
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
</ItemGroup>
</Project>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
@@ -60,6 +61,8 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
private readonly ITestOutputHelper _output;
private MongoDbRunner _runner = null!;
private Process? _externalMongo;
private string? _externalMongoDataPath;
private ConcelierApplicationFactory _factory = null!;
public WebServiceEndpointsTests(ITestOutputHelper output)
@@ -70,8 +73,15 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
public Task InitializeAsync()
{
PrepareMongoEnvironment();
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
_factory = new ConcelierApplicationFactory(_runner.ConnectionString);
if (TryStartExternalMongo(out var externalConnectionString))
{
_factory = new ConcelierApplicationFactory(externalConnectionString);
}
else
{
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
_factory = new ConcelierApplicationFactory(_runner.ConnectionString);
}
WarmupFactory(_factory);
return Task.CompletedTask;
}
@@ -79,7 +89,30 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
public Task DisposeAsync()
{
_factory.Dispose();
_runner.Dispose();
if (_externalMongo is not null)
{
try
{
if (!_externalMongo.HasExited)
{
_externalMongo.Kill(true);
_externalMongo.WaitForExit(2000);
}
}
catch
{
// ignore cleanup errors in tests
}
if (!string.IsNullOrEmpty(_externalMongoDataPath) && Directory.Exists(_externalMongoDataPath))
{
try { Directory.Delete(_externalMongoDataPath, recursive: true); } catch { /* ignore */ }
}
}
else
{
_runner.Dispose();
}
return Task.CompletedTask;
}
@@ -2605,6 +2638,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Environment.SetEnvironmentVariable("MONGO2GO_CACHE_LOCATION", cacheDir);
Environment.SetEnvironmentVariable("MONGO2GO_DOWNLOADS", cacheDir);
Environment.SetEnvironmentVariable("MONGO2GO_MONGODB_VERSION", "4.4.4");
Environment.SetEnvironmentVariable("MONGO2GO_MONGODB_PLATFORM", "linux");
var opensslPath = Path.Combine(repoRoot, "tests", "native", "openssl-1.1", "linux-x64");
if (Directory.Exists(opensslPath))
@@ -2616,16 +2650,80 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
}
// Also drop the OpenSSL libs next to the mongod binary Mongo2Go will spawn, in case LD_LIBRARY_PATH is ignored.
var mongoBin = Directory.Exists(Path.Combine(repoRoot, ".nuget"))
? Directory.GetFiles(Path.Combine(repoRoot, ".nuget", "packages", "mongo2go"), "mongod", SearchOption.AllDirectories)
var repoNuget = Path.Combine(repoRoot, ".nuget", "packages", "mongo2go");
var homeNuget = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", "mongo2go");
var mongoBin = Directory.Exists(repoNuget)
? Directory.GetFiles(repoNuget, "mongod", SearchOption.AllDirectories)
.FirstOrDefault(path => path.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase))
: null;
// Prefer globally cached Mongo2Go binaries if repo-local cache is missing.
mongoBin ??= Directory.Exists(homeNuget)
? Directory.GetFiles(homeNuget, "mongod", SearchOption.AllDirectories)
.FirstOrDefault(path => path.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase))
: null;
if (mongoBin is not null && File.Exists(mongoBin) && Directory.Exists(opensslPath))
{
var binDir = Path.GetDirectoryName(mongoBin)!;
// Create a tiny wrapper so the loader always gets LD_LIBRARY_PATH even if vstest strips it.
var wrapperPath = Path.Combine(cacheDir, "mongod-wrapper.sh");
Directory.CreateDirectory(cacheDir);
var script = $"#!/usr/bin/env bash\nset -euo pipefail\nexport LD_LIBRARY_PATH=\"{opensslPath}:${{LD_LIBRARY_PATH:-}}\"\nexec \"{mongoBin}\" \"$@\"\n";
File.WriteAllText(wrapperPath, script);
if (OperatingSystem.IsLinux())
{
try
{
File.SetUnixFileMode(wrapperPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
catch
{
// Best-effort; if not supported, chmod will fall back to default permissions.
}
}
// Force Mongo2Go to use the wrapper to avoid downloads and inject OpenSSL search path.
Environment.SetEnvironmentVariable("MONGO2GO_MONGODB_BINARY", wrapperPath);
// Keep direct LD_LIBRARY_PATH/PATH hints for any code paths that still honour parent env.
var existing = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
var combined = string.IsNullOrEmpty(existing) ? binDir : $"{binDir}:{existing}";
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", combined);
Environment.SetEnvironmentVariable("PATH", $"{binDir}:{Environment.GetEnvironmentVariable("PATH")}");
foreach (var libName in new[] { "libssl.so.1.1", "libcrypto.so.1.1" })
{
var target = Path.Combine(binDir, libName);
var source = Path.Combine(opensslPath, libName);
if (File.Exists(source) && !File.Exists(target))
{
File.Copy(source, target);
}
}
// If the Mongo2Go global cache is different from the first hit, add its bin dir too.
var globalBin = Directory.Exists(homeNuget)
? Directory.GetFiles(homeNuget, "mongod", SearchOption.AllDirectories)
.FirstOrDefault(path => path.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase))
: null;
if (mongoBin is not null && File.Exists(mongoBin) && Directory.Exists(opensslPath))
if (globalBin is not null)
{
var binDir = Path.GetDirectoryName(mongoBin)!;
var globalDir = Path.GetDirectoryName(globalBin)!;
var withGlobal = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH") ?? string.Empty;
if (!withGlobal.Split(':', StringSplitOptions.RemoveEmptyEntries).Contains(globalDir))
{
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", $"{globalDir}:{withGlobal}".TrimEnd(':'));
}
Environment.SetEnvironmentVariable("PATH", $"{globalDir}:{Environment.GetEnvironmentVariable("PATH")}");
foreach (var libName in new[] { "libssl.so.1.1", "libcrypto.so.1.1" })
{
var target = Path.Combine(binDir, libName);
var target = Path.Combine(globalDir, libName);
var source = Path.Combine(opensslPath, libName);
if (File.Exists(source) && !File.Exists(target))
{
@@ -2634,29 +2732,142 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
}
}
}
}
private static string? FindRepoRoot()
private bool TryStartExternalMongo(out string? connectionString)
{
connectionString = null;
var repoRoot = FindRepoRoot();
if (repoRoot is null)
{
var current = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(current))
return false;
}
var mongodCandidates = new List<string>();
void AddCandidates(string root)
{
if (Directory.Exists(root))
{
if (File.Exists(Path.Combine(current, "Directory.Build.props")))
{
return current;
}
mongodCandidates.AddRange(Directory.GetFiles(root, "mongod", SearchOption.AllDirectories)
.Where(p => p.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase)));
}
}
var parent = Directory.GetParent(current);
if (parent is null)
{
break;
}
AddCandidates(Path.Combine(repoRoot, ".nuget", "packages", "mongo2go"));
AddCandidates(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", "mongo2go"));
current = parent.FullName;
var mongodPath = mongodCandidates.FirstOrDefault();
if (mongodPath is null)
{
return false;
}
var dataDir = Path.Combine(repoRoot, ".cache", "mongodb-local", $"manual-{Guid.NewGuid():N}");
Directory.CreateDirectory(dataDir);
var opensslPath = Path.Combine(repoRoot, "tests", "native", "openssl-1.1", "linux-x64");
var port = GetEphemeralPort();
var psi = new ProcessStartInfo
{
FileName = mongodPath,
ArgumentList =
{
"--dbpath", dataDir,
"--bind_ip", "127.0.0.1",
"--port", port.ToString(),
"--nojournal",
"--quiet",
"--replSet", "rs0"
},
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true
};
var existingLd = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
var ldCombined = string.IsNullOrEmpty(existingLd) ? opensslPath : $"{opensslPath}:{existingLd}";
psi.Environment["LD_LIBRARY_PATH"] = ldCombined;
psi.Environment["PATH"] = $"{Path.GetDirectoryName(mongodPath)}:{Environment.GetEnvironmentVariable("PATH")}";
_externalMongo = Process.Start(psi);
_externalMongoDataPath = dataDir;
if (_externalMongo is null)
{
return false;
}
// Small ping loop to ensure mongod is ready
var client = new MongoClient($"mongodb://127.0.0.1:{port}");
var sw = System.Diagnostics.Stopwatch.StartNew();
while (sw.Elapsed < TimeSpan.FromSeconds(5))
{
try
{
client.GetDatabase("admin").RunCommand<BsonDocument>("{ ping: 1 }");
// Initiate single-node replica set so features expecting replset work.
client.GetDatabase("admin").RunCommand<BsonDocument>(BsonDocument.Parse("{ replSetInitiate: { _id: \"rs0\", members: [ { _id: 0, host: \"127.0.0.1:" + port + "\" } ] } }"));
// Wait for primary
var readySw = System.Diagnostics.Stopwatch.StartNew();
while (readySw.Elapsed < TimeSpan.FromSeconds(5))
{
var status = client.GetDatabase("admin").RunCommand<BsonDocument>(BsonDocument.Parse("{ replSetGetStatus: 1 }"));
var myState = status["members"].AsBsonArray.FirstOrDefault(x => x["self"].AsBoolean);
if (myState != null && myState["state"].ToInt32() == 1)
{
connectionString = $"mongodb://127.0.0.1:{port}/?replicaSet=rs0";
return true;
}
Thread.Sleep(100);
}
// fallback if primary not reached
connectionString = $"mongodb://127.0.0.1:{port}";
return true;
}
catch
{
Thread.Sleep(100);
}
}
try { _externalMongo.Kill(true); } catch { /* ignore */ }
return false;
}
private static int GetEphemeralPort()
{
var listener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static string? FindRepoRoot()
{
var current = AppContext.BaseDirectory;
string? lastMatch = null;
while (!string.IsNullOrEmpty(current))
{
if (File.Exists(Path.Combine(current, "Directory.Build.props")))
{
lastMatch = current;
}
return null;
var parent = Directory.GetParent(current);
if (parent is null)
{
break;
}
current = parent.FullName;
}
return lastMatch;
}
private static AdvisoryIngestRequest BuildAdvisoryIngestRequest(
string? contentHash,
string upstreamId,