release orchestrator v1 draft and build fixes
This commit is contained in:
425
src/Plugin/StellaOps.Plugin.Registry/InMemoryPluginRegistry.cs
Normal file
425
src/Plugin/StellaOps.Plugin.Registry/InMemoryPluginRegistry.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Execution;
|
||||
using StellaOps.Plugin.Abstractions.Health;
|
||||
using StellaOps.Plugin.Abstractions.Lifecycle;
|
||||
using StellaOps.Plugin.Registry.Models;
|
||||
|
||||
namespace StellaOps.Plugin.Registry;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of the plugin registry for testing and development.
|
||||
/// </summary>
|
||||
public sealed class InMemoryPluginRegistry : IPluginRegistry
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, PluginRecord> _plugins = new();
|
||||
private readonly ConcurrentDictionary<Guid, PluginCapabilityRecord> _capabilities = new();
|
||||
private readonly ConcurrentDictionary<Guid, PluginInstanceRecord> _instances = new();
|
||||
private readonly ConcurrentDictionary<Guid, List<PluginHealthRecord>> _healthHistory = new();
|
||||
private readonly ILogger<InMemoryPluginRegistry> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new in-memory plugin registry instance.
|
||||
/// </summary>
|
||||
public InMemoryPluginRegistry(
|
||||
ILogger<InMemoryPluginRegistry> logger,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PluginRecord> RegisterAsync(LoadedPlugin plugin, CancellationToken ct)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
// Get current health status from last check result if available
|
||||
var healthStatus = plugin.LastHealthCheckResult?.Status ?? HealthStatus.Unknown;
|
||||
|
||||
var record = new PluginRecord
|
||||
{
|
||||
Id = id,
|
||||
PluginId = plugin.Info.Id,
|
||||
Name = plugin.Info.Name,
|
||||
Version = plugin.Info.Version,
|
||||
Vendor = plugin.Info.Vendor,
|
||||
Description = plugin.Info.Description,
|
||||
LicenseId = plugin.Info.LicenseId,
|
||||
TrustLevel = plugin.TrustLevel,
|
||||
Capabilities = plugin.Capabilities,
|
||||
Status = plugin.State,
|
||||
HealthStatus = healthStatus,
|
||||
Source = "installed",
|
||||
AssemblyPath = plugin.Manifest?.AssemblyPath,
|
||||
EntryPoint = plugin.Manifest?.EntryPoint,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
LoadedAt = plugin.LoadedAt
|
||||
};
|
||||
|
||||
_plugins[id] = record;
|
||||
_logger.LogDebug("Registered plugin {PluginId} with ID {Id}", plugin.Info.Id, id);
|
||||
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateStatusAsync(string pluginId, PluginLifecycleState status, string? message = null, CancellationToken ct = default)
|
||||
{
|
||||
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
if (record != null)
|
||||
{
|
||||
var updated = new PluginRecord
|
||||
{
|
||||
Id = record.Id,
|
||||
PluginId = record.PluginId,
|
||||
Name = record.Name,
|
||||
Version = record.Version,
|
||||
Vendor = record.Vendor,
|
||||
Description = record.Description,
|
||||
LicenseId = record.LicenseId,
|
||||
TrustLevel = record.TrustLevel,
|
||||
Capabilities = record.Capabilities,
|
||||
Status = status,
|
||||
StatusMessage = message,
|
||||
HealthStatus = record.HealthStatus,
|
||||
LastHealthCheck = record.LastHealthCheck,
|
||||
HealthCheckFailures = record.HealthCheckFailures,
|
||||
Source = record.Source,
|
||||
AssemblyPath = record.AssemblyPath,
|
||||
EntryPoint = record.EntryPoint,
|
||||
Manifest = record.Manifest,
|
||||
CreatedAt = record.CreatedAt,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
LoadedAt = record.LoadedAt
|
||||
};
|
||||
_plugins[record.Id] = updated;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateHealthAsync(string pluginId, HealthStatus status, HealthCheckResult? result = null, CancellationToken ct = default)
|
||||
{
|
||||
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
if (record != null)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = new PluginRecord
|
||||
{
|
||||
Id = record.Id,
|
||||
PluginId = record.PluginId,
|
||||
Name = record.Name,
|
||||
Version = record.Version,
|
||||
Vendor = record.Vendor,
|
||||
Description = record.Description,
|
||||
LicenseId = record.LicenseId,
|
||||
TrustLevel = record.TrustLevel,
|
||||
Capabilities = record.Capabilities,
|
||||
Status = record.Status,
|
||||
StatusMessage = record.StatusMessage,
|
||||
HealthStatus = status,
|
||||
LastHealthCheck = now,
|
||||
HealthCheckFailures = status == HealthStatus.Healthy ? 0 : record.HealthCheckFailures + 1,
|
||||
Source = record.Source,
|
||||
AssemblyPath = record.AssemblyPath,
|
||||
EntryPoint = record.EntryPoint,
|
||||
Manifest = record.Manifest,
|
||||
CreatedAt = record.CreatedAt,
|
||||
UpdatedAt = now,
|
||||
LoadedAt = record.LoadedAt
|
||||
};
|
||||
_plugins[record.Id] = updated;
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
RecordHealthCheckAsync(pluginId, result, ct).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnregisterAsync(string pluginId, CancellationToken ct)
|
||||
{
|
||||
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
if (record != null)
|
||||
{
|
||||
_plugins.TryRemove(record.Id, out _);
|
||||
|
||||
// Remove capabilities
|
||||
var capsToRemove = _capabilities.Values.Where(c => c.PluginId == record.Id).ToList();
|
||||
foreach (var cap in capsToRemove)
|
||||
{
|
||||
_capabilities.TryRemove(cap.Id, out _);
|
||||
}
|
||||
|
||||
// Remove instances
|
||||
var instancesToRemove = _instances.Values.Where(i => i.PluginId == record.Id).ToList();
|
||||
foreach (var instance in instancesToRemove)
|
||||
{
|
||||
_instances.TryRemove(instance.Id, out _);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Unregistered plugin {PluginId}", pluginId);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PluginRecord?> GetAsync(string pluginId, CancellationToken ct)
|
||||
{
|
||||
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginRecord>> GetAllAsync(CancellationToken ct)
|
||||
{
|
||||
var result = _plugins.Values.OrderBy(p => p.Name).ToList();
|
||||
return Task.FromResult<IReadOnlyList<PluginRecord>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginRecord>> GetByStatusAsync(PluginLifecycleState status, CancellationToken ct)
|
||||
{
|
||||
var result = _plugins.Values
|
||||
.Where(p => p.Status == status)
|
||||
.OrderBy(p => p.Name)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<PluginRecord>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginRecord>> GetByCapabilityAsync(PluginCapabilities capability, CancellationToken ct)
|
||||
{
|
||||
var result = _plugins.Values
|
||||
.Where(p => (p.Capabilities & capability) != 0 && p.Status == PluginLifecycleState.Active)
|
||||
.OrderBy(p => p.Name)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<PluginRecord>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginRecord>> GetByCapabilityTypeAsync(string capabilityType, string? capabilityId = null, CancellationToken ct = default)
|
||||
{
|
||||
var matchingCaps = _capabilities.Values
|
||||
.Where(c => c.CapabilityType == capabilityType && c.IsEnabled)
|
||||
.Where(c => capabilityId == null || c.CapabilityId == capabilityId);
|
||||
|
||||
var pluginIds = matchingCaps.Select(c => c.PluginId).ToHashSet();
|
||||
|
||||
var result = _plugins.Values
|
||||
.Where(p => pluginIds.Contains(p.Id) && p.Status == PluginLifecycleState.Active)
|
||||
.OrderBy(p => p.Name)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<PluginRecord>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RegisterCapabilitiesAsync(Guid pluginDbId, IEnumerable<PluginCapabilityRecord> capabilities, CancellationToken ct)
|
||||
{
|
||||
foreach (var cap in capabilities)
|
||||
{
|
||||
var record = cap with { PluginId = pluginDbId };
|
||||
_capabilities[cap.Id] = record;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginCapabilityRecord>> GetCapabilitiesAsync(string pluginId, CancellationToken ct)
|
||||
{
|
||||
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
if (plugin == null)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<PluginCapabilityRecord>>([]);
|
||||
}
|
||||
|
||||
var result = _capabilities.Values
|
||||
.Where(c => c.PluginId == plugin.Id)
|
||||
.OrderBy(c => c.CapabilityType)
|
||||
.ThenBy(c => c.CapabilityId)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<PluginCapabilityRecord>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PluginInstanceRecord> CreateInstanceAsync(CreatePluginInstanceRequest request, CancellationToken ct)
|
||||
{
|
||||
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == request.PluginId)
|
||||
?? throw new InvalidOperationException($"Plugin {request.PluginId} not found");
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
var record = new PluginInstanceRecord
|
||||
{
|
||||
Id = id,
|
||||
PluginId = plugin.Id,
|
||||
PluginStringId = request.PluginId,
|
||||
TenantId = request.TenantId,
|
||||
InstanceName = request.InstanceName,
|
||||
Config = request.Config,
|
||||
SecretsPath = request.SecretsPath,
|
||||
ResourceLimits = request.ResourceLimits,
|
||||
Enabled = true,
|
||||
Status = "pending",
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
_instances[id] = record;
|
||||
_logger.LogDebug("Created instance {InstanceId} for plugin {PluginId}", id, request.PluginId);
|
||||
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PluginInstanceRecord?> GetInstanceAsync(Guid instanceId, CancellationToken ct)
|
||||
{
|
||||
_instances.TryGetValue(instanceId, out var record);
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginInstanceRecord>> GetInstancesForTenantAsync(Guid tenantId, CancellationToken ct)
|
||||
{
|
||||
var result = _instances.Values
|
||||
.Where(i => i.TenantId == tenantId)
|
||||
.OrderBy(i => i.CreatedAt)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<PluginInstanceRecord>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginInstanceRecord>> GetInstancesForPluginAsync(string pluginId, CancellationToken ct)
|
||||
{
|
||||
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
if (plugin == null)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<PluginInstanceRecord>>([]);
|
||||
}
|
||||
|
||||
var result = _instances.Values
|
||||
.Where(i => i.PluginId == plugin.Id)
|
||||
.OrderBy(i => i.CreatedAt)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<PluginInstanceRecord>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateInstanceConfigAsync(Guid instanceId, JsonDocument config, CancellationToken ct)
|
||||
{
|
||||
if (_instances.TryGetValue(instanceId, out var existing))
|
||||
{
|
||||
var updated = existing with
|
||||
{
|
||||
Config = config,
|
||||
UpdatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
_instances[instanceId] = updated;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetInstanceEnabledAsync(Guid instanceId, bool enabled, CancellationToken ct)
|
||||
{
|
||||
if (_instances.TryGetValue(instanceId, out var existing))
|
||||
{
|
||||
var updated = existing with
|
||||
{
|
||||
Enabled = enabled,
|
||||
UpdatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
_instances[instanceId] = updated;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteInstanceAsync(Guid instanceId, CancellationToken ct)
|
||||
{
|
||||
_instances.TryRemove(instanceId, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RecordHealthCheckAsync(string pluginId, HealthCheckResult result, CancellationToken ct)
|
||||
{
|
||||
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
if (plugin == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var healthRecord = new PluginHealthRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PluginId = plugin.Id,
|
||||
PluginStringId = pluginId,
|
||||
CheckedAt = now,
|
||||
Status = result.Status,
|
||||
ResponseTimeMs = result.Duration.HasValue ? (int)result.Duration.Value.TotalMilliseconds : null,
|
||||
ErrorMessage = result.Message,
|
||||
CreatedAt = now
|
||||
};
|
||||
|
||||
_healthHistory.AddOrUpdate(
|
||||
plugin.Id,
|
||||
_ => [healthRecord],
|
||||
(_, list) =>
|
||||
{
|
||||
list.Add(healthRecord);
|
||||
return list;
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PluginHealthRecord>> GetHealthHistoryAsync(
|
||||
string pluginId,
|
||||
DateTimeOffset since,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
|
||||
if (plugin == null || !_healthHistory.TryGetValue(plugin.Id, out var history))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<PluginHealthRecord>>([]);
|
||||
}
|
||||
|
||||
var result = history
|
||||
.Where(h => h.CheckedAt >= since)
|
||||
.OrderByDescending(h => h.CheckedAt)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<PluginHealthRecord>>(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all data from the registry. For testing purposes.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_plugins.Clear();
|
||||
_capabilities.Clear();
|
||||
_instances.Clear();
|
||||
_healthHistory.Clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user