Files
git.stella-ops.org/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationDependency.cs

275 lines
10 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Infrastructure.Postgres.Migrations;
/// <summary>
/// Represents a dependency between migrations in different modules.
/// </summary>
public sealed record MigrationDependency
{
/// <summary>
/// The module that has the dependency.
/// </summary>
public required string Module { get; init; }
/// <summary>
/// The migration file that has the dependency.
/// </summary>
public required string Migration { get; init; }
/// <summary>
/// The module being depended upon.
/// </summary>
public required string DependsOnModule { get; init; }
/// <summary>
/// The schema being depended upon.
/// </summary>
public required string DependsOnSchema { get; init; }
/// <summary>
/// The specific table or object being depended upon (optional).
/// </summary>
public string? DependsOnObject { get; init; }
/// <summary>
/// Whether this is a soft dependency (FK created conditionally).
/// </summary>
public bool IsSoft { get; init; }
/// <summary>
/// Description of why this dependency exists.
/// </summary>
public string? Description { get; init; }
}
/// <summary>
/// Module schema configuration for dependency resolution.
/// </summary>
public sealed record ModuleSchemaConfig
{
/// <summary>
/// The module name (e.g., "Authority", "Concelier").
/// </summary>
public required string Module { get; init; }
/// <summary>
/// The PostgreSQL schema name (e.g., "auth", "vuln").
/// </summary>
public required string Schema { get; init; }
/// <summary>
/// The WebService that owns this module's migrations.
/// </summary>
public string? OwnerService { get; init; }
/// <summary>
/// The assembly containing migrations for this module.
/// </summary>
public string? MigrationAssembly { get; init; }
}
/// <summary>
/// Registry of module schemas and their dependencies.
/// </summary>
public sealed class ModuleDependencyRegistry
{
private readonly Dictionary<string, ModuleSchemaConfig> _modules = new(StringComparer.OrdinalIgnoreCase);
private readonly List<MigrationDependency> _dependencies = [];
/// <summary>
/// Gets all registered modules.
/// </summary>
public IReadOnlyDictionary<string, ModuleSchemaConfig> Modules => _modules;
/// <summary>
/// Gets all registered dependencies.
/// </summary>
public IReadOnlyList<MigrationDependency> Dependencies => _dependencies;
/// <summary>
/// Registers a module schema configuration.
/// </summary>
public ModuleDependencyRegistry RegisterModule(ModuleSchemaConfig config)
{
ArgumentNullException.ThrowIfNull(config);
_modules[config.Module] = config;
return this;
}
/// <summary>
/// Registers a dependency between modules.
/// </summary>
public ModuleDependencyRegistry RegisterDependency(MigrationDependency dependency)
{
ArgumentNullException.ThrowIfNull(dependency);
_dependencies.Add(dependency);
return this;
}
/// <summary>
/// Gets the schema name for a module.
/// </summary>
public string? GetSchemaForModule(string moduleName)
{
return _modules.TryGetValue(moduleName, out var config) ? config.Schema : null;
}
/// <summary>
/// Gets the module name for a schema.
/// </summary>
public string? GetModuleForSchema(string schemaName)
{
return _modules.Values
.FirstOrDefault(m => string.Equals(m.Schema, schemaName, StringComparison.OrdinalIgnoreCase))
?.Module;
}
/// <summary>
/// Gets dependencies for a specific module.
/// </summary>
public IReadOnlyList<MigrationDependency> GetDependenciesForModule(string moduleName)
{
return _dependencies
.Where(d => string.Equals(d.Module, moduleName, StringComparison.OrdinalIgnoreCase))
.ToList();
}
/// <summary>
/// Gets modules that depend on a specific module.
/// </summary>
public IReadOnlyList<MigrationDependency> GetDependentsOfModule(string moduleName)
{
return _dependencies
.Where(d => string.Equals(d.DependsOnModule, moduleName, StringComparison.OrdinalIgnoreCase))
.ToList();
}
/// <summary>
/// Validates that all dependencies can be satisfied.
/// </summary>
public IReadOnlyList<string> ValidateDependencies()
{
var errors = new List<string>();
foreach (var dep in _dependencies)
{
// Check that the dependent module exists
if (!_modules.ContainsKey(dep.Module))
{
errors.Add($"Unknown module '{dep.Module}' in dependency declaration.");
}
// Check that the target module exists
if (!_modules.ContainsKey(dep.DependsOnModule))
{
errors.Add($"Unknown dependency target module '{dep.DependsOnModule}' from '{dep.Module}'.");
}
// Check that the target schema matches
if (_modules.TryGetValue(dep.DependsOnModule, out var targetConfig))
{
if (!string.Equals(targetConfig.Schema, dep.DependsOnSchema, StringComparison.OrdinalIgnoreCase))
{
errors.Add(
$"Schema mismatch for dependency '{dep.Module}' -> '{dep.DependsOnModule}': " +
$"expected schema '{targetConfig.Schema}', got '{dep.DependsOnSchema}'.");
}
}
}
return errors;
}
/// <summary>
/// Creates the default registry with all StellaOps modules.
/// </summary>
public static ModuleDependencyRegistry CreateDefault()
{
var registry = new ModuleDependencyRegistry();
// Register all modules with their schemas
registry
.RegisterModule(new ModuleSchemaConfig { Module = "Authority", Schema = "auth", OwnerService = "Authority.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Concelier", Schema = "vuln", OwnerService = "Concelier.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Excititor", Schema = "vex", OwnerService = "Excititor.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Policy", Schema = "policy", OwnerService = "Policy.Gateway" })
.RegisterModule(new ModuleSchemaConfig { Module = "Scheduler", Schema = "scheduler", OwnerService = "Scheduler.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Notify", Schema = "notify", OwnerService = "Notify.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Scanner", Schema = "scanner", OwnerService = "Scanner.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Attestor", Schema = "proofchain", OwnerService = "Attestor.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Signer", Schema = "signer", OwnerService = "Signer.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Signals", Schema = "signals", OwnerService = "Signals" })
.RegisterModule(new ModuleSchemaConfig { Module = "EvidenceLocker", Schema = "evidence", OwnerService = "EvidenceLocker.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "ExportCenter", Schema = "export", OwnerService = "ExportCenter.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "IssuerDirectory", Schema = "issuer", OwnerService = "IssuerDirectory.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Orchestrator", Schema = "orchestrator", OwnerService = "Orchestrator.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Findings", Schema = "findings", OwnerService = "Findings.Ledger.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "BinaryIndex", Schema = "binaries", OwnerService = "Scanner.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "VexHub", Schema = "vexhub", OwnerService = "VexHub.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Unknowns", Schema = "unknowns", OwnerService = "Policy.Gateway" });
// Register known cross-module dependencies
registry
.RegisterDependency(new MigrationDependency
{
Module = "Signer",
Migration = "20251214000001_AddKeyManagementSchema.sql",
DependsOnModule = "Attestor",
DependsOnSchema = "proofchain",
DependsOnObject = "trust_anchors",
IsSoft = true,
Description = "Optional FK from signer.key_history to proofchain.trust_anchors"
})
.RegisterDependency(new MigrationDependency
{
Module = "Scanner",
Migration = "N/A",
DependsOnModule = "Concelier",
DependsOnSchema = "vuln",
IsSoft = true,
Description = "Scanner uses Concelier linksets for advisory data"
})
.RegisterDependency(new MigrationDependency
{
Module = "Policy",
Migration = "N/A",
DependsOnModule = "Concelier",
DependsOnSchema = "vuln",
IsSoft = true,
Description = "Policy uses vulnerability data from Concelier"
})
.RegisterDependency(new MigrationDependency
{
Module = "Policy",
Migration = "N/A",
DependsOnModule = "Excititor",
DependsOnSchema = "vex",
IsSoft = true,
Description = "Policy uses VEX data from Excititor"
});
return registry;
}
/// <summary>
/// Serializes the registry to JSON.
/// </summary>
public string ToJson()
{
var data = new
{
modules = _modules.Values.ToList(),
dependencies = _dependencies
};
return JsonSerializer.Serialize(data, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
}