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:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user