427 lines
15 KiB
C#
427 lines
15 KiB
C#
|
|
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;
|
|
using System.Collections.Concurrent;
|
|
using System.Text.Json;
|
|
|
|
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();
|
|
}
|
|
}
|