feat: Initialize Zastava Webhook service with TLS and Authority authentication
- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
12
src/StellaOps.Scheduler.Storage.Mongo.Tests/GlobalUsings.cs
Normal file
12
src/StellaOps.Scheduler.Storage.Mongo.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
global using System.Text.Json;
|
||||
global using System.Text.Json.Nodes;
|
||||
global using Microsoft.Extensions.Logging.Abstractions;
|
||||
global using Microsoft.Extensions.Options;
|
||||
global using Mongo2Go;
|
||||
global using MongoDB.Bson;
|
||||
global using MongoDB.Driver;
|
||||
global using StellaOps.Scheduler.Models;
|
||||
global using StellaOps.Scheduler.Storage.Mongo.Internal;
|
||||
global using StellaOps.Scheduler.Storage.Mongo.Migrations;
|
||||
global using StellaOps.Scheduler.Storage.Mongo.Options;
|
||||
global using Xunit;
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Mongo.Tests.Integration;
|
||||
|
||||
public sealed class SchedulerMongoRoundTripTests : IDisposable
|
||||
{
|
||||
private readonly MongoDbRunner _runner;
|
||||
private readonly SchedulerMongoContext _context;
|
||||
|
||||
public SchedulerMongoRoundTripTests()
|
||||
{
|
||||
_runner = MongoDbRunner.Start(additionalMongodArguments: "--quiet");
|
||||
var options = new SchedulerMongoOptions
|
||||
{
|
||||
ConnectionString = _runner.ConnectionString,
|
||||
Database = $"scheduler_roundtrip_{Guid.NewGuid():N}"
|
||||
};
|
||||
|
||||
_context = new SchedulerMongoContext(Microsoft.Extensions.Options.Options.Create(options), NullLogger<SchedulerMongoContext>.Instance);
|
||||
var migrations = new ISchedulerMongoMigration[]
|
||||
{
|
||||
new EnsureSchedulerCollectionsMigration(NullLogger<EnsureSchedulerCollectionsMigration>.Instance),
|
||||
new EnsureSchedulerIndexesMigration()
|
||||
};
|
||||
var runner = new SchedulerMongoMigrationRunner(_context, migrations, NullLogger<SchedulerMongoMigrationRunner>.Instance);
|
||||
runner.RunAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SamplesRoundTripThroughMongoWithoutLosingCanonicalShape()
|
||||
{
|
||||
var samplesRoot = LocateSamplesRoot();
|
||||
|
||||
var scheduleJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "schedule.json"), CancellationToken.None);
|
||||
await AssertRoundTripAsync(
|
||||
scheduleJson,
|
||||
_context.Options.SchedulesCollection,
|
||||
CanonicalJsonSerializer.Deserialize<Schedule>,
|
||||
schedule => schedule.Id);
|
||||
|
||||
var runJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "run.json"), CancellationToken.None);
|
||||
await AssertRoundTripAsync(
|
||||
runJson,
|
||||
_context.Options.RunsCollection,
|
||||
CanonicalJsonSerializer.Deserialize<Run>,
|
||||
run => run.Id);
|
||||
|
||||
var impactJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "impact-set.json"), CancellationToken.None);
|
||||
await AssertRoundTripAsync(
|
||||
impactJson,
|
||||
_context.Options.ImpactSnapshotsCollection,
|
||||
CanonicalJsonSerializer.Deserialize<ImpactSet>,
|
||||
_ => null);
|
||||
|
||||
var auditJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "audit.json"), CancellationToken.None);
|
||||
await AssertRoundTripAsync(
|
||||
auditJson,
|
||||
_context.Options.AuditCollection,
|
||||
CanonicalJsonSerializer.Deserialize<AuditRecord>,
|
||||
audit => audit.Id);
|
||||
}
|
||||
|
||||
private async Task AssertRoundTripAsync<TModel>(
|
||||
string json,
|
||||
string collectionName,
|
||||
Func<string, TModel> deserialize,
|
||||
Func<TModel, string?> resolveId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(deserialize);
|
||||
ArgumentNullException.ThrowIfNull(resolveId);
|
||||
|
||||
var model = deserialize(json);
|
||||
var canonical = CanonicalJsonSerializer.Serialize(model);
|
||||
|
||||
var document = BsonDocument.Parse(canonical);
|
||||
var identifier = resolveId(model);
|
||||
if (!string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
document["_id"] = identifier;
|
||||
}
|
||||
|
||||
var collection = _context.Database.GetCollection<BsonDocument>(collectionName);
|
||||
await collection.InsertOneAsync(document, cancellationToken: CancellationToken.None);
|
||||
|
||||
var filter = identifier is null ? Builders<BsonDocument>.Filter.Empty : Builders<BsonDocument>.Filter.Eq("_id", identifier);
|
||||
var stored = await collection.Find(filter).FirstOrDefaultAsync();
|
||||
Assert.NotNull(stored);
|
||||
|
||||
var sanitized = stored!.DeepClone().AsBsonDocument;
|
||||
sanitized.Remove("_id");
|
||||
|
||||
var storedJson = sanitized.ToJson();
|
||||
|
||||
var parsedExpected = JsonNode.Parse(canonical) ?? throw new InvalidOperationException("Canonical node null.");
|
||||
var parsedActual = JsonNode.Parse(storedJson) ?? throw new InvalidOperationException("Stored node null.");
|
||||
Assert.True(JsonNode.DeepEquals(parsedExpected, parsedActual), "Document changed shape after Mongo round-trip.");
|
||||
}
|
||||
|
||||
private static string LocateSamplesRoot()
|
||||
{
|
||||
var current = AppContext.BaseDirectory;
|
||||
while (!string.IsNullOrEmpty(current))
|
||||
{
|
||||
var candidate = Path.Combine(current, "samples", "api", "scheduler");
|
||||
if (Directory.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var parent = Path.GetDirectoryName(current.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
if (string.Equals(parent, current, StringComparison.Ordinal))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Unable to locate samples/api/scheduler in repository tree.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_runner.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
namespace StellaOps.Scheduler.Storage.Mongo.Tests.Migrations;
|
||||
|
||||
public sealed class SchedulerMongoMigrationTests : IDisposable
|
||||
{
|
||||
private readonly MongoDbRunner _runner;
|
||||
|
||||
public SchedulerMongoMigrationTests()
|
||||
{
|
||||
_runner = MongoDbRunner.Start(additionalMongodArguments: "--quiet");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_CreatesCollectionsAndIndexes()
|
||||
{
|
||||
var options = new SchedulerMongoOptions
|
||||
{
|
||||
ConnectionString = _runner.ConnectionString,
|
||||
Database = $"scheduler_tests_{Guid.NewGuid():N}"
|
||||
};
|
||||
|
||||
var context = new SchedulerMongoContext(Microsoft.Extensions.Options.Options.Create(options), NullLogger<SchedulerMongoContext>.Instance);
|
||||
var migrations = new ISchedulerMongoMigration[]
|
||||
{
|
||||
new EnsureSchedulerCollectionsMigration(NullLogger<EnsureSchedulerCollectionsMigration>.Instance),
|
||||
new EnsureSchedulerIndexesMigration()
|
||||
};
|
||||
|
||||
var runner = new SchedulerMongoMigrationRunner(context, migrations, NullLogger<SchedulerMongoMigrationRunner>.Instance);
|
||||
await runner.RunAsync(CancellationToken.None);
|
||||
|
||||
var cursor = await context.Database.ListCollectionNamesAsync(cancellationToken: CancellationToken.None);
|
||||
var collections = await cursor.ToListAsync();
|
||||
|
||||
Assert.Contains(options.SchedulesCollection, collections);
|
||||
Assert.Contains(options.RunsCollection, collections);
|
||||
Assert.Contains(options.ImpactSnapshotsCollection, collections);
|
||||
Assert.Contains(options.AuditCollection, collections);
|
||||
Assert.Contains(options.LocksCollection, collections);
|
||||
Assert.Contains(options.MigrationsCollection, collections);
|
||||
|
||||
await AssertScheduleIndexesAsync(context, options);
|
||||
await AssertRunIndexesAsync(context, options);
|
||||
await AssertImpactSnapshotIndexesAsync(context, options);
|
||||
await AssertAuditIndexesAsync(context, options);
|
||||
await AssertLockIndexesAsync(context, options);
|
||||
}
|
||||
|
||||
private static async Task AssertScheduleIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
|
||||
{
|
||||
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.SchedulesCollection));
|
||||
Assert.Contains("tenant_enabled", names);
|
||||
Assert.Contains("cron_timezone", names);
|
||||
}
|
||||
|
||||
private static async Task AssertRunIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
|
||||
{
|
||||
var collection = context.Database.GetCollection<BsonDocument>(options.RunsCollection);
|
||||
var indexes = await ListIndexesAsync(collection);
|
||||
|
||||
Assert.Contains(indexes, doc => string.Equals(doc["name"].AsString, "tenant_createdAt_desc", StringComparison.Ordinal));
|
||||
Assert.Contains(indexes, doc => string.Equals(doc["name"].AsString, "state_lookup", StringComparison.Ordinal));
|
||||
Assert.Contains(indexes, doc => string.Equals(doc["name"].AsString, "schedule_createdAt_desc", StringComparison.Ordinal));
|
||||
|
||||
var ttl = indexes.FirstOrDefault(doc => doc.TryGetValue("name", out var name) && name == "finishedAt_ttl");
|
||||
Assert.NotNull(ttl);
|
||||
Assert.Equal(options.CompletedRunRetention.TotalSeconds, ttl!["expireAfterSeconds"].ToDouble());
|
||||
}
|
||||
|
||||
private static async Task AssertImpactSnapshotIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
|
||||
{
|
||||
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.ImpactSnapshotsCollection));
|
||||
Assert.Contains("selector_tenant_scope", names);
|
||||
Assert.Contains("snapshotId_unique", names);
|
||||
}
|
||||
|
||||
private static async Task AssertAuditIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
|
||||
{
|
||||
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.AuditCollection));
|
||||
Assert.Contains("tenant_occurredAt_desc", names);
|
||||
Assert.Contains("correlation_lookup", names);
|
||||
}
|
||||
|
||||
private static async Task AssertLockIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
|
||||
{
|
||||
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.LocksCollection));
|
||||
Assert.Contains("tenant_resource_unique", names);
|
||||
Assert.Contains("expiresAt_ttl", names);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<string>> ListIndexNamesAsync(IMongoCollection<BsonDocument> collection)
|
||||
{
|
||||
var documents = await ListIndexesAsync(collection);
|
||||
return documents.Select(doc => doc["name"].AsString).ToArray();
|
||||
}
|
||||
|
||||
private static async Task<List<BsonDocument>> ListIndexesAsync(IMongoCollection<BsonDocument> collection)
|
||||
{
|
||||
using var cursor = await collection.Indexes.ListAsync();
|
||||
return await cursor.ToListAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_runner.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Scheduler.Storage.Mongo/StellaOps.Scheduler.Storage.Mongo.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scheduler.Models/StellaOps.Scheduler.Models.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mongo2Go" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="../../samples/api/scheduler/*.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user