Add Policy DSL Validator, Schema Exporter, and Simulation Smoke tools

- Implemented PolicyDslValidator with command-line options for strict mode and JSON output.
- Created PolicySchemaExporter to generate JSON schemas for policy-related models.
- Developed PolicySimulationSmoke tool to validate policy simulations against expected outcomes.
- Added project files and necessary dependencies for each tool.
- Ensured proper error handling and usage instructions across tools.
This commit is contained in:
master
2025-10-27 08:00:11 +02:00
parent 2b7b88ca77
commit 799f787de2
712 changed files with 49449 additions and 6124 deletions

View File

@@ -0,0 +1,23 @@
using MongoDB.Bson;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Mongo.Serialization;
internal static class AuditRecordDocumentMapper
{
public static BsonDocument ToBsonDocument(AuditRecord record)
{
ArgumentNullException.ThrowIfNull(record);
var json = CanonicalJsonSerializer.Serialize(record);
var document = BsonDocument.Parse(json);
document["_id"] = record.Id;
return document;
}
public static AuditRecord FromBsonDocument(BsonDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var node = document.ToCanonicalJsonNode();
return CanonicalJsonSerializer.Deserialize<AuditRecord>(node.ToCanonicalJson());
}
}

View File

@@ -0,0 +1,144 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using MongoDB.Bson;
using MongoDB.Bson.IO;
namespace StellaOps.Scheduler.Storage.Mongo.Serialization;
internal static class BsonDocumentJsonExtensions
{
public static JsonNode ToCanonicalJsonNode(this BsonDocument document, params string[] fieldsToRemove)
{
ArgumentNullException.ThrowIfNull(document);
var clone = document.DeepClone().AsBsonDocument;
clone.Remove("_id");
if (fieldsToRemove is { Length: > 0 })
{
foreach (var field in fieldsToRemove)
{
clone.Remove(field);
}
}
var json = clone.ToJson(new JsonWriterSettings
{
OutputMode = JsonOutputMode.RelaxedExtendedJson,
Indent = false,
});
var node = JsonNode.Parse(json) ?? throw new InvalidOperationException("Unable to parse BSON document JSON.");
return NormalizeExtendedJson(node);
}
private static JsonNode NormalizeExtendedJson(JsonNode node)
{
if (node is JsonObject obj)
{
if (TryConvertExtendedDate(obj, out var replacement))
{
return replacement;
}
foreach (var property in obj.ToList())
{
if (property.Value is null)
{
continue;
}
var normalized = NormalizeExtendedJson(property.Value);
if (!ReferenceEquals(normalized, property.Value))
{
obj[property.Key] = normalized;
}
}
return obj;
}
if (node is JsonArray array)
{
for (var i = 0; i < array.Count; i++)
{
if (array[i] is null)
{
continue;
}
var normalized = NormalizeExtendedJson(array[i]!);
if (!ReferenceEquals(normalized, array[i]))
{
array[i] = normalized;
}
}
return array;
}
return node;
}
private static bool TryConvertExtendedDate(JsonObject obj, out JsonNode replacement)
{
replacement = obj;
if (obj.Count != 1 || !obj.TryGetPropertyValue("$date", out var value) || value is null)
{
return false;
}
if (value is JsonValue directValue)
{
if (directValue.TryGetValue(out string? dateString) && TryParseIso(dateString, out var iso))
{
replacement = JsonValue.Create(iso);
return true;
}
if (directValue.TryGetValue(out long epochMilliseconds))
{
replacement = JsonValue.Create(DateTimeOffset.FromUnixTimeMilliseconds(epochMilliseconds).ToString("O"));
return true;
}
}
else if (value is JsonObject nested &&
nested.TryGetPropertyValue("$numberLong", out var numberNode) &&
numberNode is JsonValue numberValue &&
numberValue.TryGetValue(out string? numberString) &&
long.TryParse(numberString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ms))
{
replacement = JsonValue.Create(DateTimeOffset.FromUnixTimeMilliseconds(ms).ToString("O"));
return true;
}
return false;
}
private static bool TryParseIso(string? value, out string iso)
{
iso = string.Empty;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed))
{
iso = parsed.ToUniversalTime().ToString("O");
return true;
}
return false;
}
public static string ToCanonicalJson(this JsonNode node)
{
ArgumentNullException.ThrowIfNull(node);
return node.ToJsonString(new JsonSerializerOptions
{
WriteIndented = false
});
}
}

View File

@@ -0,0 +1,125 @@
using MongoDB.Bson;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Mongo.Serialization;
internal static class GraphJobDocumentMapper
{
private const string PayloadField = "payload";
public static BsonDocument ToBsonDocument(GraphBuildJob job)
{
ArgumentNullException.ThrowIfNull(job);
var payloadJson = CanonicalJsonSerializer.Serialize(job);
var payloadDocument = BsonDocument.Parse(payloadJson);
var document = new BsonDocument
{
["_id"] = job.Id,
["tenantId"] = job.TenantId,
["kind"] = "build",
["status"] = job.Status.ToString().ToLowerInvariant(),
["createdAt"] = job.CreatedAt.UtcDateTime,
["attempts"] = job.Attempts,
[PayloadField] = payloadDocument
};
if (!string.IsNullOrWhiteSpace(job.GraphSnapshotId))
{
document["graphSnapshotId"] = job.GraphSnapshotId;
}
if (!string.IsNullOrWhiteSpace(job.CorrelationId))
{
document["correlationId"] = job.CorrelationId;
}
if (job.StartedAt is { } startedAt)
{
document["startedAt"] = startedAt.UtcDateTime;
}
if (job.CompletedAt is { } completedAt)
{
document["completedAt"] = completedAt.UtcDateTime;
}
if (!string.IsNullOrWhiteSpace(job.Error))
{
document["error"] = job.Error;
}
return document;
}
public static BsonDocument ToBsonDocument(GraphOverlayJob job)
{
ArgumentNullException.ThrowIfNull(job);
var payloadJson = CanonicalJsonSerializer.Serialize(job);
var payloadDocument = BsonDocument.Parse(payloadJson);
var document = new BsonDocument
{
["_id"] = job.Id,
["tenantId"] = job.TenantId,
["kind"] = "overlay",
["status"] = job.Status.ToString().ToLowerInvariant(),
["createdAt"] = job.CreatedAt.UtcDateTime,
["attempts"] = job.Attempts,
[PayloadField] = payloadDocument
};
document["graphSnapshotId"] = job.GraphSnapshotId;
document["overlayKind"] = job.OverlayKind.ToString().ToLowerInvariant();
document["overlayKey"] = job.OverlayKey;
if (!string.IsNullOrWhiteSpace(job.BuildJobId))
{
document["buildJobId"] = job.BuildJobId;
}
if (!string.IsNullOrWhiteSpace(job.CorrelationId))
{
document["correlationId"] = job.CorrelationId;
}
if (job.StartedAt is { } startedAt)
{
document["startedAt"] = startedAt.UtcDateTime;
}
if (job.CompletedAt is { } completedAt)
{
document["completedAt"] = completedAt.UtcDateTime;
}
if (!string.IsNullOrWhiteSpace(job.Error))
{
document["error"] = job.Error;
}
return document;
}
public static GraphBuildJob ToGraphBuildJob(BsonDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var payloadDocument = document[PayloadField].AsBsonDocument;
var json = payloadDocument.ToJson();
var job = CanonicalJsonSerializer.Deserialize<GraphBuildJob>(json);
return job;
}
public static GraphOverlayJob ToGraphOverlayJob(BsonDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var payloadDocument = document[PayloadField].AsBsonDocument;
var json = payloadDocument.ToJson();
var job = CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(json);
return job;
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Security.Cryptography;
using System.Text;
using MongoDB.Bson;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Mongo.Serialization;
internal static class ImpactSetDocumentMapper
{
private const string SelectorHashPrefix = "selector::";
public static BsonDocument ToBsonDocument(ImpactSet impactSet)
{
ArgumentNullException.ThrowIfNull(impactSet);
var json = CanonicalJsonSerializer.Serialize(impactSet);
var document = BsonDocument.Parse(json);
document["_id"] = ComputeDocumentId(impactSet);
document["selectorDigest"] = ComputeSelectorDigest(impactSet);
return document;
}
public static ImpactSet FromBsonDocument(BsonDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var node = document.ToCanonicalJsonNode();
return CanonicalJsonSerializer.Deserialize<ImpactSet>(node.ToCanonicalJson());
}
private static string ComputeDocumentId(ImpactSet impactSet)
{
if (!string.IsNullOrWhiteSpace(impactSet.SnapshotId))
{
return impactSet.SnapshotId!;
}
var selectorJson = CanonicalJsonSerializer.Serialize(impactSet.Selector);
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(selectorJson));
return SelectorHashPrefix + Convert.ToHexString(hash).ToLowerInvariant();
}
private static string ComputeSelectorDigest(ImpactSet impactSet)
{
return ComputeSelectorDigest(impactSet.Selector);
}
public static string ComputeSelectorDigest(Selector selector)
{
ArgumentNullException.ThrowIfNull(selector);
var selectorJson = CanonicalJsonSerializer.Serialize(selector);
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(selectorJson));
return Convert.ToHexString(hash).ToLowerInvariant();
}
}

View File

@@ -0,0 +1,23 @@
using MongoDB.Bson;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Mongo.Serialization;
internal static class RunDocumentMapper
{
public static BsonDocument ToBsonDocument(Run run)
{
ArgumentNullException.ThrowIfNull(run);
var json = CanonicalJsonSerializer.Serialize(run);
var document = BsonDocument.Parse(json);
document["_id"] = run.Id;
return document;
}
public static Run FromBsonDocument(BsonDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var node = document.ToCanonicalJsonNode();
return CanonicalJsonSerializer.Deserialize<Run>(node.ToCanonicalJson());
}
}

View File

@@ -0,0 +1,25 @@
using MongoDB.Bson;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Mongo.Serialization;
internal static class ScheduleDocumentMapper
{
private static readonly string[] IgnoredFields = { "deletedAt", "deletedBy" };
public static BsonDocument ToBsonDocument(Schedule schedule)
{
ArgumentNullException.ThrowIfNull(schedule);
var json = CanonicalJsonSerializer.Serialize(schedule);
var document = BsonDocument.Parse(json);
document["_id"] = schedule.Id;
return document;
}
public static Schedule FromBsonDocument(BsonDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var node = document.ToCanonicalJsonNode(IgnoredFields);
return CanonicalJsonSerializer.Deserialize<Schedule>(node.ToCanonicalJson());
}
}