release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Adapts an existing IConnectorPlugin to the unified IPlugin and IFeedCapability interfaces.
|
||||
/// This enables gradual migration of Concelier connectors to the unified plugin architecture.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class FeedPluginAdapter : IPlugin, UnifiedFeedCapability
|
||||
{
|
||||
private readonly IConnectorPlugin _plugin;
|
||||
private readonly FeedType _feedType;
|
||||
private readonly IReadOnlyList<string> _ecosystems;
|
||||
private IPluginContext? _context;
|
||||
private IServiceProvider? _serviceProvider;
|
||||
private LegacyFeedConnector? _connector;
|
||||
private PluginLifecycleState _state = PluginLifecycleState.Discovered;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new adapter for an existing connector plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The existing connector plugin to wrap.</param>
|
||||
/// <param name="feedType">The feed type classification.</param>
|
||||
/// <param name="ecosystems">Ecosystems covered by this feed.</param>
|
||||
public FeedPluginAdapter(
|
||||
IConnectorPlugin plugin,
|
||||
FeedType feedType,
|
||||
IReadOnlyList<string> ecosystems)
|
||||
{
|
||||
_plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
|
||||
_feedType = feedType;
|
||||
_ecosystems = ecosystems ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
#region IPlugin
|
||||
|
||||
/// <inheritdoc />
|
||||
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}");
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginCapabilities Capabilities => PluginCapabilities.Feed | PluginCapabilities.Network;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginLifecycleState State => _state;
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<HealthCheckResult> 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<string, object>
|
||||
{
|
||||
["feedId"] = FeedId,
|
||||
["feedType"] = _feedType.ToString(),
|
||||
["ecosystems"] = string.Join(", ", _ecosystems),
|
||||
["sourceName"] = _connector.SourceName
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ConnectorType => $"feed.{_plugin.Name}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => _plugin.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ConnectionTestResult> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ConnectionInfo> GetConnectionInfoAsync(CancellationToken ct)
|
||||
{
|
||||
return new ConnectionInfo(
|
||||
EndpointUrl: $"feed://{_plugin.Name}",
|
||||
AuthenticatedAs: null,
|
||||
ConnectedSince: null,
|
||||
Metadata: new Dictionary<string, object>
|
||||
{
|
||||
["feedType"] = _feedType.ToString(),
|
||||
["ecosystems"] = _ecosystems
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IFeedCapability
|
||||
|
||||
/// <inheritdoc />
|
||||
public string FeedId => _plugin.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public FeedType FeedType => _feedType;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> CoveredEcosystems => _ecosystems;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FeedMetadata> 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}");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<FeedEntry> 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<FeedEntry>.ThrowNotSupported(
|
||||
$"Direct entry streaming is not supported for {FeedId}. " +
|
||||
"Use the legacy FetchAsync/ParseAsync/MapAsync pipeline via the connector directly.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FeedEntry?> 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.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetRawEntryAsync(string id, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
|
||||
// Not supported by legacy connector
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended Operations
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying connector for extended operations.
|
||||
/// </summary>
|
||||
public LegacyFeedConnector? Connector => _connector;
|
||||
|
||||
/// <summary>
|
||||
/// Executes the fetch phase of the legacy pipeline.
|
||||
/// </summary>
|
||||
public async Task FetchAsync(CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
await _connector!.FetchAsync(_serviceProvider!, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the parse phase of the legacy pipeline.
|
||||
/// </summary>
|
||||
public async Task ParseAsync(CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
await _connector!.ParseAsync(_serviceProvider!, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the map phase of the legacy pipeline.
|
||||
/// </summary>
|
||||
public async Task MapAsync(CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
await _connector!.MapAsync(_serviceProvider!, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the full pipeline (fetch, parse, map).
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for creating async enumerables that throw exceptions.
|
||||
/// </summary>
|
||||
internal static class EmptyAsyncEnumerable<T>
|
||||
{
|
||||
public static IAsyncEnumerable<T> ThrowNotSupported(string message)
|
||||
{
|
||||
return new ThrowingAsyncEnumerable(message);
|
||||
}
|
||||
|
||||
private sealed class ThrowingAsyncEnumerable : IAsyncEnumerable<T>
|
||||
{
|
||||
private readonly string _message;
|
||||
|
||||
public ThrowingAsyncEnumerable(string message) => _message = message;
|
||||
|
||||
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken ct) =>
|
||||
new ThrowingAsyncEnumerator(_message);
|
||||
}
|
||||
|
||||
private sealed class ThrowingAsyncEnumerator : IAsyncEnumerator<T>
|
||||
{
|
||||
private readonly string _message;
|
||||
|
||||
public ThrowingAsyncEnumerator(string message) => _message = message;
|
||||
|
||||
public T Current => throw new NotSupportedException(_message);
|
||||
|
||||
public ValueTask<bool> MoveNextAsync() =>
|
||||
throw new NotSupportedException(_message);
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user