stela ops usage fixes roles propagation and timoeut, one account to support multi tenants, migrations consolidation, search to support documentation, doctor and open api vector db search
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
namespace StellaOps.Platform.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Plug-in contract for one consolidated migration module per web service.
|
||||
/// </summary>
|
||||
public interface IMigrationModulePlugin
|
||||
{
|
||||
MigrationModuleInfo Module { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace StellaOps.Platform.Database;
|
||||
|
||||
internal static class MigrationModulePluginDiscovery
|
||||
{
|
||||
private static readonly string[] PluginDirectoryEnvironmentVariables =
|
||||
[
|
||||
"STELLAOPS_MIGRATION_PLUGIN_DIR",
|
||||
"STELLAOPS_MIGRATIONS_PLUGIN_DIR",
|
||||
"STELLAOPS_PLUGIN_MIGRATIONS_DIR"
|
||||
];
|
||||
|
||||
public static IReadOnlyList<MigrationModuleInfo> DiscoverModules()
|
||||
{
|
||||
LoadExternalPluginAssemblies();
|
||||
|
||||
var modulesByName = new Dictionary<string, MigrationModuleInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var plugin in DiscoverPlugins().OrderBy(static plugin => plugin.Module.Name, StringComparer.Ordinal))
|
||||
{
|
||||
var module = plugin.Module;
|
||||
if (string.IsNullOrWhiteSpace(module.Name))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid migration module plugin '{plugin.GetType().FullName}': module name is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(module.SchemaName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid migration module plugin '{plugin.GetType().FullName}': schema name is required.");
|
||||
}
|
||||
|
||||
if (!modulesByName.TryAdd(module.Name, module))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Duplicate migration module plug-in registration for module '{module.Name}'.");
|
||||
}
|
||||
}
|
||||
|
||||
var modules = modulesByName.Values
|
||||
.OrderBy(static module => module.Name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (modules.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No migration module plug-ins were discovered. " +
|
||||
"Register at least one IMigrationModulePlugin implementation.");
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
private static IEnumerable<IMigrationModulePlugin> DiscoverPlugins()
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().OrderBy(static a => a.FullName, StringComparer.Ordinal))
|
||||
{
|
||||
if (assembly.IsDynamic)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var type in GetLoadableTypes(assembly))
|
||||
{
|
||||
if (!typeof(IMigrationModulePlugin).IsAssignableFrom(type) ||
|
||||
type.IsAbstract ||
|
||||
type.IsInterface ||
|
||||
type.GetConstructor(Type.EmptyTypes) is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Activator.CreateInstance(type) is IMigrationModulePlugin plugin)
|
||||
{
|
||||
yield return plugin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
return ex.Types.Where(static type => type is not null)!;
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadExternalPluginAssemblies()
|
||||
{
|
||||
foreach (var directory in ResolvePluginDirectories())
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var dllPath in Directory.EnumerateFiles(directory, "*.dll", SearchOption.TopDirectoryOnly)
|
||||
.OrderBy(static path => path, StringComparer.Ordinal))
|
||||
{
|
||||
TryLoadAssembly(dllPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryLoadAssembly(string assemblyPath)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(assemblyPath);
|
||||
if (IsAssemblyAlreadyLoaded(fullPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
AssemblyLoadContext.Default.LoadFromAssemblyPath(fullPath);
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
// Ignore native/non-.NET binaries in plugin directories.
|
||||
}
|
||||
catch (FileLoadException)
|
||||
{
|
||||
// Ignore already loaded or load-conflict assemblies.
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAssemblyAlreadyLoaded(string fullPath) =>
|
||||
AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(static assembly => !assembly.IsDynamic)
|
||||
.Select(static assembly => assembly.Location)
|
||||
.Any(location => string.Equals(location, fullPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static IReadOnlyList<string> ResolvePluginDirectories()
|
||||
{
|
||||
var directories = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "plugins", "migrations"))
|
||||
};
|
||||
|
||||
foreach (var variable in PluginDirectoryEnvironmentVariables)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(variable);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var candidate in value.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
directories.Add(Path.GetFullPath(candidate));
|
||||
}
|
||||
}
|
||||
|
||||
return directories.OrderBy(static directory => directory, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using StellaOps.AirGap.Persistence.Postgres;
|
||||
using StellaOps.Authority.Persistence.Postgres;
|
||||
using StellaOps.Concelier.Persistence.Postgres;
|
||||
using StellaOps.Excititor.Persistence.Postgres;
|
||||
using StellaOps.Notify.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using StellaOps.Scheduler.Persistence.Postgres;
|
||||
using StellaOps.TimelineIndexer.Infrastructure;
|
||||
|
||||
namespace StellaOps.Platform.Database;
|
||||
|
||||
public sealed class AirGapMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "AirGap",
|
||||
SchemaName: "airgap",
|
||||
MigrationsAssembly: typeof(AirGapDataSource).Assembly);
|
||||
}
|
||||
|
||||
public sealed class AuthorityMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Authority",
|
||||
SchemaName: "authority",
|
||||
MigrationsAssembly: typeof(AuthorityDataSource).Assembly,
|
||||
ResourcePrefix: "StellaOps.Authority.Persistence.Migrations");
|
||||
}
|
||||
|
||||
public sealed class SchedulerMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Scheduler",
|
||||
SchemaName: "scheduler",
|
||||
MigrationsAssembly: typeof(SchedulerDataSource).Assembly,
|
||||
ResourcePrefix: "StellaOps.Scheduler.Persistence.Migrations");
|
||||
}
|
||||
|
||||
public sealed class ConcelierMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Concelier",
|
||||
SchemaName: "vuln",
|
||||
MigrationsAssembly: typeof(ConcelierDataSource).Assembly,
|
||||
ResourcePrefix: "StellaOps.Concelier.Persistence.Migrations");
|
||||
}
|
||||
|
||||
public sealed class PolicyMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Policy",
|
||||
SchemaName: "policy",
|
||||
MigrationsAssembly: typeof(PolicyDataSource).Assembly,
|
||||
ResourcePrefix: "StellaOps.Policy.Persistence.Migrations");
|
||||
}
|
||||
|
||||
public sealed class NotifyMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Notify",
|
||||
SchemaName: "notify",
|
||||
MigrationsAssembly: typeof(NotifyDataSource).Assembly,
|
||||
ResourcePrefix: "StellaOps.Notify.Persistence.Migrations");
|
||||
}
|
||||
|
||||
public sealed class ExcititorMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Excititor",
|
||||
SchemaName: "vex",
|
||||
MigrationsAssembly: typeof(ExcititorDataSource).Assembly,
|
||||
ResourcePrefix: "StellaOps.Excititor.Persistence.Migrations");
|
||||
}
|
||||
|
||||
public sealed class PlatformMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Platform",
|
||||
SchemaName: "release",
|
||||
MigrationsAssembly: typeof(ReleaseMigrationRunner).Assembly,
|
||||
ResourcePrefix: "StellaOps.Platform.Database.Migrations.Release");
|
||||
}
|
||||
|
||||
public sealed class ScannerMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "Scanner",
|
||||
SchemaName: "scanner",
|
||||
MigrationsAssembly: typeof(ScannerDataSource).Assembly);
|
||||
}
|
||||
|
||||
public sealed class TimelineIndexerMigrationModulePlugin : IMigrationModulePlugin
|
||||
{
|
||||
public MigrationModuleInfo Module { get; } = new(
|
||||
Name: "TimelineIndexer",
|
||||
SchemaName: "timeline",
|
||||
MigrationsAssembly: typeof(TimelineIndexerDataSource).Assembly,
|
||||
ResourcePrefix: "StellaOps.TimelineIndexer.Infrastructure.Db.Migrations");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
|
||||
namespace StellaOps.Platform.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a PostgreSQL module with migration metadata.
|
||||
/// </summary>
|
||||
public sealed record MigrationModuleInfo(
|
||||
string Name,
|
||||
string SchemaName,
|
||||
Assembly MigrationsAssembly,
|
||||
string? ResourcePrefix = null);
|
||||
|
||||
/// <summary>
|
||||
/// Canonical PostgreSQL migration module registry owned by Platform.
|
||||
/// </summary>
|
||||
public static class MigrationModuleRegistry
|
||||
{
|
||||
private static readonly Lazy<IReadOnlyList<MigrationModuleInfo>> _modules =
|
||||
new(MigrationModulePluginDiscovery.DiscoverModules, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered modules.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<MigrationModuleInfo> Modules => _modules.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets module names for completion and validation.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> ModuleNames => Modules.Select(static module => module.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a module by name (case-insensitive).
|
||||
/// </summary>
|
||||
public static MigrationModuleInfo? FindModule(string name) =>
|
||||
Modules.FirstOrDefault(module =>
|
||||
string.Equals(module.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Gets modules matching the filter, or all modules when null/empty/all.
|
||||
/// </summary>
|
||||
public static IEnumerable<MigrationModuleInfo> GetModules(string? moduleFilter)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(moduleFilter) || moduleFilter.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Modules;
|
||||
}
|
||||
|
||||
var module = FindModule(moduleFilter);
|
||||
return module is null ? [] : [module];
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\AirGap\__Libraries\StellaOps.AirGap.Persistence\StellaOps.AirGap.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Authority\__Libraries\StellaOps.Authority.Persistence\StellaOps.Authority.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Persistence\StellaOps.Concelier.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Persistence\StellaOps.Excititor.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Notify\__Libraries\StellaOps.Notify.Persistence\StellaOps.Notify.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Policy\__Libraries\StellaOps.Policy.Persistence\StellaOps.Policy.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.Storage\StellaOps.Scanner.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\..\Scheduler\__Libraries\StellaOps.Scheduler.Persistence\StellaOps.Scheduler.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\TimelineIndexer\StellaOps.TimelineIndexer\StellaOps.TimelineIndexer.Infrastructure\StellaOps.TimelineIndexer.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| SPRINT_20260222_051-MGC-04-W1 | DONE | Added platform-owned `MigrationModuleRegistry` canonical module catalog for migration runner entrypoint consolidation; CLI now consumes this registry instead of owning module metadata. |
|
||||
| SPRINT_20260222_051-MGC-04-W1-PLUGINS | DONE | Replaced hardcoded module catalog with auto-discovered migration plugins (`IMigrationModulePlugin`) so one consolidated plugin descriptor per web service feeds both CLI and Platform API migration execution paths. |
|
||||
| B22-01-DB | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added release migration `047_GlobalContextAndFilters.sql` with `platform.context_regions`, `platform.context_environments`, and `platform.ui_context_preferences`. |
|
||||
| B22-02-DB | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added release migration `048_ReleaseReadModels.sql` with release list/activity/approvals projection tables, correlation keys, and deterministic ordering indexes. |
|
||||
| B22-03-DB | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added release migration `049_TopologyInventory.sql` with normalized topology inventory projection tables and sync-watermark indexes. |
|
||||
|
||||
Reference in New Issue
Block a user