namespace StellaOps.Concelier.Plugin.Unified; using System.Runtime.CompilerServices; using StellaOps.Plugin; using StellaOps.Plugin.Abstractions; using StellaOps.Plugin.Abstractions.Capabilities; using StellaOps.Plugin.Abstractions.Context; using StellaOps.Plugin.Abstractions.Health; using StellaOps.Plugin.Abstractions.Lifecycle; using UnifiedFeedCapability = StellaOps.Plugin.Abstractions.Capabilities.IFeedCapability; using LegacyFeedConnector = StellaOps.Plugin.IFeedConnector; /// /// Adapts an existing IConnectorPlugin to the unified IPlugin and IFeedCapability interfaces. /// This enables gradual migration of Concelier connectors to the unified plugin architecture. /// /// /// The adapter bridges the Concelier-specific IConnectorPlugin/IFeedConnector interfaces to /// the Plugin.Abstractions IFeedCapability interface. The underlying connector operations /// are delegated to the wrapped connector. /// public sealed class FeedPluginAdapter : IPlugin, UnifiedFeedCapability { private readonly IConnectorPlugin _plugin; private readonly FeedType _feedType; private readonly IReadOnlyList _ecosystems; private IPluginContext? _context; private IServiceProvider? _serviceProvider; private LegacyFeedConnector? _connector; private PluginLifecycleState _state = PluginLifecycleState.Discovered; /// /// Creates a new adapter for an existing connector plugin. /// /// The existing connector plugin to wrap. /// The feed type classification. /// Ecosystems covered by this feed. public FeedPluginAdapter( IConnectorPlugin plugin, FeedType feedType, IReadOnlyList ecosystems) { _plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); _feedType = feedType; _ecosystems = ecosystems ?? Array.Empty(); } #region IPlugin /// public PluginInfo Info => new( Id: $"com.stellaops.feed.{_plugin.Name}", Name: _plugin.Name, Version: "1.0.0", Vendor: "Stella Ops", Description: $"Vulnerability feed connector for {_plugin.Name}"); /// public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn; /// public PluginCapabilities Capabilities => PluginCapabilities.Feed | PluginCapabilities.Network; /// public PluginLifecycleState State => _state; /// public async Task InitializeAsync(IPluginContext context, CancellationToken ct) { _context = context; _state = PluginLifecycleState.Initializing; try { // Get service provider from context // Note: This would require IPluginContext to expose the service provider // For now we create a minimal service provider _serviceProvider = CreateServiceProvider(context); if (!_plugin.IsAvailable(_serviceProvider)) { _state = PluginLifecycleState.Failed; throw new InvalidOperationException( $"Feed connector '{_plugin.Name}' is not available."); } // Create the actual connector _connector = _plugin.Create(_serviceProvider); _state = PluginLifecycleState.Active; context.Logger.Info("Feed connector adapter initialized for {FeedId}", _plugin.Name); } catch (Exception ex) { _state = PluginLifecycleState.Failed; context.Logger.Error(ex, "Failed to initialize feed connector {FeedId}", _plugin.Name); throw; } await Task.CompletedTask; } /// public async Task HealthCheckAsync(CancellationToken ct) { try { if (_state != PluginLifecycleState.Active) { return HealthCheckResult.Unhealthy($"Connector is in state {_state}"); } if (_connector == null) { return HealthCheckResult.Unhealthy("Connector not initialized"); } return HealthCheckResult.Healthy() .WithDetails(new Dictionary { ["feedId"] = FeedId, ["feedType"] = _feedType.ToString(), ["ecosystems"] = string.Join(", ", _ecosystems), ["sourceName"] = _connector.SourceName }); } catch (Exception ex) { return HealthCheckResult.Unhealthy(ex); } } /// public ValueTask DisposeAsync() { if (_connector is IAsyncDisposable asyncDisposable) { return asyncDisposable.DisposeAsync(); } if (_connector is IDisposable disposable) { disposable.Dispose(); } _connector = null; _state = PluginLifecycleState.Stopped; return ValueTask.CompletedTask; } #endregion #region IConnectorCapability /// public string ConnectorType => $"feed.{_plugin.Name}"; /// public string DisplayName => _plugin.Name; /// public async Task TestConnectionAsync(CancellationToken ct) { if (_state != PluginLifecycleState.Active) { return ConnectionTestResult.Failed($"Connector is in state {_state}"); } try { // For feed connectors, we can't easily test connection without a full fetch // Return success if the connector is initialized return ConnectionTestResult.Succeeded(); } catch (Exception ex) { return ConnectionTestResult.Failed(ex.Message, ex); } } /// public async Task GetConnectionInfoAsync(CancellationToken ct) { return new ConnectionInfo( EndpointUrl: $"feed://{_plugin.Name}", AuthenticatedAs: null, ConnectedSince: null, Metadata: new Dictionary { ["feedType"] = _feedType.ToString(), ["ecosystems"] = _ecosystems }); } #endregion #region IFeedCapability /// public string FeedId => _plugin.Name; /// public FeedType FeedType => _feedType; /// public IReadOnlyList CoveredEcosystems => _ecosystems; /// public async Task GetMetadataAsync(CancellationToken ct) { EnsureActive(); // The legacy IFeedConnector doesn't have direct metadata access // Return basic metadata from the plugin return new FeedMetadata( FeedId: FeedId, LastModified: null, TotalEntries: null, SchemaVersion: null, Description: $"Vulnerability feed from {FeedId}"); } /// public IAsyncEnumerable FetchEntriesAsync( DateTimeOffset? since, CancellationToken ct) { EnsureActive(); // The legacy IFeedConnector uses FetchAsync/ParseAsync/MapAsync pipeline // which doesn't directly stream entries. We document this limitation. return EmptyAsyncEnumerable.ThrowNotSupported( $"Direct entry streaming is not supported for {FeedId}. " + "Use the legacy FetchAsync/ParseAsync/MapAsync pipeline via the connector directly."); } /// public async Task GetEntryAsync(string id, CancellationToken ct) { EnsureActive(); // The legacy connector doesn't support individual entry lookup throw new NotSupportedException( $"Individual entry lookup is not supported for {FeedId}. " + "Use the legacy pipeline for bulk data processing."); } /// public async Task GetRawEntryAsync(string id, CancellationToken ct) { EnsureActive(); // Not supported by legacy connector return null; } #endregion #region Extended Operations /// /// Gets the underlying connector for extended operations. /// public LegacyFeedConnector? Connector => _connector; /// /// Executes the fetch phase of the legacy pipeline. /// public async Task FetchAsync(CancellationToken ct) { EnsureActive(); await _connector!.FetchAsync(_serviceProvider!, ct); } /// /// Executes the parse phase of the legacy pipeline. /// public async Task ParseAsync(CancellationToken ct) { EnsureActive(); await _connector!.ParseAsync(_serviceProvider!, ct); } /// /// Executes the map phase of the legacy pipeline. /// public async Task MapAsync(CancellationToken ct) { EnsureActive(); await _connector!.MapAsync(_serviceProvider!, ct); } /// /// Executes the full pipeline (fetch, parse, map). /// public async Task RunPipelineAsync(CancellationToken ct) { await FetchAsync(ct); await ParseAsync(ct); await MapAsync(ct); } #endregion #region Helpers private void EnsureActive() { if (_state != PluginLifecycleState.Active || _connector == null) { throw new InvalidOperationException( $"Feed connector '{_plugin.Name}' is not active (state: {_state})"); } } private IServiceProvider CreateServiceProvider(IPluginContext context) { // The adapter needs to provide services that the legacy connector expects // For now, return a minimal provider that wraps the context return new MinimalServiceProvider(context); } private sealed class MinimalServiceProvider : IServiceProvider { private readonly IPluginContext _context; public MinimalServiceProvider(IPluginContext context) { _context = context; } public object? GetService(Type serviceType) { if (serviceType == typeof(IPluginContext)) return _context; if (serviceType == typeof(IPluginLogger)) return _context.Logger; return null; } } #endregion } /// /// Helper for creating async enumerables that throw exceptions. /// internal static class EmptyAsyncEnumerable { public static IAsyncEnumerable ThrowNotSupported(string message) { return new ThrowingAsyncEnumerable(message); } private sealed class ThrowingAsyncEnumerable : IAsyncEnumerable { private readonly string _message; public ThrowingAsyncEnumerable(string message) => _message = message; public IAsyncEnumerator GetAsyncEnumerator(CancellationToken ct) => new ThrowingAsyncEnumerator(_message); } private sealed class ThrowingAsyncEnumerator : IAsyncEnumerator { private readonly string _message; public ThrowingAsyncEnumerator(string message) => _message = message; public T Current => throw new NotSupportedException(_message); public ValueTask MoveNextAsync() => throw new NotSupportedException(_message); public ValueTask DisposeAsync() => ValueTask.CompletedTask; } }