Skip to content

Commit 0545f2a

Browse files
authored
Merge pull request #21 from DevelApp-ai/copilot/fix-220572152-310773785-588774bd-232a-469f-afa4-329fbca131e0
Expose async module-based plugin loading types for Kubernetes/remote sources (v2.1.0)
2 parents 5ad8adf + 8102bfc commit 0545f2a

File tree

5 files changed

+133
-29
lines changed

5 files changed

+133
-29
lines changed

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ All notable changes to the RuntimePluggableClassFactory project will be document
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.1.0] - 2025-11-03
9+
10+
### Added - Async Module-Based Plugin Loading Support
11+
12+
#### Expanded Async Loading Capabilities
13+
- **HybridPluginFactory<T>**: Officially exposed factory for working with both traditional and containerized plugins
14+
- **PluginInfo**: Public type for plugin metadata including execution mode and container information
15+
- **PluginExecutionMode**: Enum for specifying plugin execution mode (Auto, Traditional, Containerized)
16+
- **HybridPluginFactoryOptions**: Configuration options for hybrid plugin loading
17+
- Moved hybrid plugin types from Examples namespace to main public API namespace
18+
19+
#### Module Identification Types (Already Public, Now Documented)
20+
- **PluginIdentifier**: Identifies plugins by namespace, name, and version
21+
- **ContainerizedPluginInfo**: Information about deployed containerized plugins
22+
- **ContainerizedPluginLoader<T>**: Async loader for containerized plugins
23+
- **ContainerizedPluginProxy<T>**: Proxy for bridging traditional and containerized plugin execution
24+
- **ContainerizedPluginLoaderOptions**: Configuration for containerized plugin loading
25+
26+
### Changed
27+
- Moved `HybridPluginFactory` and related types from `.Examples` namespace to `.Containerized` namespace
28+
- Elevated hybrid plugin loading from example code to officially supported public API
29+
30+
### Use Cases Enabled
31+
- **Dynamic Module Loading from Kubernetes/Remote Sources**: Full support for async loading of plugins from containerized environments
32+
- **Hybrid Plugin Architectures**: Seamlessly mix traditional in-process plugins with containerized plugins
33+
- **Custom Async Plugin Loaders**: All types needed for implementing custom module-based async plugin loading are now publicly exposed
34+
835
## [2.0.0] - 2025-07-24
936

1037
### Added - TDS Implementation Complete

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,71 @@ All performance targets are validated by automated tests:
205205
- **TypedPluginClassFactory**: Type-safe plugin execution
206206
- **PluginExecutionSandbox**: Isolated execution environment
207207
- **DefaultPluginSecurityValidator**: Comprehensive security validation
208+
- **HybridPluginFactory** (v2.1.0+): Hybrid factory for both traditional and containerized plugins
209+
- **ContainerizedPluginLoader** (v2.1.0+): Async loader for containerized plugins from Kubernetes/remote sources
208210

209211
### Key Interfaces
210212
- `IPluginClass`: Basic plugin interface
211213
- `ITypedPluginClass<TInput, TOutput>`: Type-safe plugin interface
212214
- `IPluginLoader<T>`: Plugin loading interface
213215
- `IPluginSecurityValidator`: Security validation interface
216+
- `IContainerizedPluginOrchestrator` (v2.1.0+): Interface for containerized plugin orchestration
217+
218+
## Async Module-Based Plugin Loading (v2.1.0+)
219+
220+
### Hybrid Plugin Loading
221+
222+
The `HybridPluginFactory` enables seamless mixing of traditional in-process plugins with containerized plugins loaded from Kubernetes or other remote sources:
223+
224+
```csharp
225+
using DevelApp.RuntimePluggableClassFactory.Containerized;
226+
using DevelApp.RuntimePluggableClassFactory.Containerized.Interfaces;
227+
228+
// Setup traditional plugin loader
229+
var traditionalLoader = new FilePluginLoader<IMyPlugin>(pluginDirectory, securityValidator);
230+
var traditionalFactory = new PluginClassFactory<IMyPlugin>(traditionalLoader);
231+
232+
// Setup containerized plugin orchestrator (e.g., Kubernetes)
233+
var containerizedOrchestrator = new KubernetesPluginOrchestrator(/* ... */);
234+
235+
// Create hybrid factory
236+
var hybridFactory = new HybridPluginFactory<IMyPlugin>(
237+
traditionalFactory,
238+
containerizedOrchestrator,
239+
logger,
240+
new HybridPluginFactoryOptions
241+
{
242+
PreferContainerized = true,
243+
AutoDeployContainerized = false
244+
});
245+
246+
// Load plugins from either source
247+
var plugin = await hybridFactory.GetPluginAsync(
248+
new NamespaceString("MyCompany.Plugins"),
249+
new IdentifierString("DataProcessor"),
250+
version: new SemanticVersionNumber(1, 0, 0),
251+
executionMode: PluginExecutionMode.Auto);
252+
253+
// List all available plugins
254+
var availablePlugins = await hybridFactory.ListAvailablePluginsAsync();
255+
foreach (var pluginInfo in availablePlugins)
256+
{
257+
Console.WriteLine($"{pluginInfo.ModuleName}.{pluginInfo.PluginName} ({pluginInfo.ExecutionMode})");
258+
}
259+
```
260+
261+
### Module Identification Types
262+
263+
The following types are now exposed in the public API for implementing custom async module-based plugin loading:
264+
265+
- **PluginIdentifier**: Identifies plugins by namespace, name, and version
266+
- **PluginInfo**: Plugin metadata including execution mode and container information
267+
- **PluginExecutionMode**: Enum values - Auto, Traditional, Containerized
268+
- **ContainerizedPluginInfo**: Metadata for deployed containerized plugins
269+
- **ContainerizedPluginLoader<T>**: Async loader for containerized plugins
270+
- **ContainerizedPluginLoaderOptions**: Configuration for async loading
271+
272+
These types enable dynamic module loading from Kubernetes/remote sources as an alternative to traditional directory-based scanning.
214273

215274
## Documentation
216275

RuntimePluggableClassFactory.Containerized/Examples/HybridPluginFactory.cs renamed to RuntimePluggableClassFactory.Containerized/HybridPluginFactory.cs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
using System;
77
using System.Collections.Generic;
88
using System.Linq;
9+
using System.Threading;
910
using System.Threading.Tasks;
1011

11-
namespace DevelApp.RuntimePluggableClassFactory.Containerized.Examples
12+
namespace DevelApp.RuntimePluggableClassFactory.Containerized
1213
{
1314
/// <summary>
14-
/// Example factory that can use both traditional (in-process) and containerized plugins
15-
/// This demonstrates how the CRPCF can coexist with the existing RuntimePluggableClassFactory
15+
/// Factory that can use both traditional (in-process) and containerized plugins.
16+
/// Enables async module-based plugin loading from Kubernetes/remote sources and local directories.
17+
/// This allows the CRPCF to coexist with the existing RuntimePluggableClassFactory.
1618
/// </summary>
1719
/// <typeparam name="T">Plugin interface type</typeparam>
1820
public class HybridPluginFactory<T> where T : IPluginClass
@@ -46,12 +48,14 @@ public HybridPluginFactory(
4648
/// <param name="pluginName">Plugin name</param>
4749
/// <param name="version">Plugin version (optional)</param>
4850
/// <param name="executionMode">Preferred execution mode</param>
51+
/// <param name="cancellationToken">Cancellation token</param>
4952
/// <returns>Plugin instance or null if not found</returns>
5053
public async Task<T?> GetPluginAsync(
5154
NamespaceString moduleName,
5255
IdentifierString pluginName,
5356
SemanticVersionNumber? version = null,
54-
PluginExecutionMode executionMode = PluginExecutionMode.Auto)
57+
PluginExecutionMode executionMode = PluginExecutionMode.Auto,
58+
CancellationToken cancellationToken = default)
5559
{
5660
_logger.LogDebug("Getting plugin {ModuleName}.{PluginName} with execution mode {ExecutionMode}",
5761
moduleName, pluginName, executionMode);
@@ -64,14 +68,14 @@ public HybridPluginFactory(
6468
return await GetTraditionalPluginAsync(moduleName, pluginName, version);
6569

6670
case PluginExecutionMode.Containerized:
67-
return await GetContainerizedPluginAsync(moduleName, pluginName, version);
71+
return await GetContainerizedPluginAsync(moduleName, pluginName, version, cancellationToken);
6872

6973
case PluginExecutionMode.Auto:
7074
default:
7175
// Try preferred mode first, then fallback
7276
if (_options.PreferContainerized)
7377
{
74-
var containerized = await GetContainerizedPluginAsync(moduleName, pluginName, version);
78+
var containerized = await GetContainerizedPluginAsync(moduleName, pluginName, version, cancellationToken);
7579
if (containerized != null) return containerized;
7680

7781
return await GetTraditionalPluginAsync(moduleName, pluginName, version);
@@ -81,7 +85,7 @@ public HybridPluginFactory(
8185
var traditional = await GetTraditionalPluginAsync(moduleName, pluginName, version);
8286
if (traditional != null) return traditional;
8387

84-
return await GetContainerizedPluginAsync(moduleName, pluginName, version);
88+
return await GetContainerizedPluginAsync(moduleName, pluginName, version, cancellationToken);
8589
}
8690
}
8791
}
@@ -95,8 +99,10 @@ public HybridPluginFactory(
9599
/// <summary>
96100
/// Lists all available plugins from both traditional and containerized sources
97101
/// </summary>
102+
/// <param name="cancellationToken">Cancellation token</param>
98103
/// <returns>Available plugins with their execution modes</returns>
99-
public async Task<IEnumerable<PluginInfo>> ListAvailablePluginsAsync()
104+
public async Task<IEnumerable<PluginInfo>> ListAvailablePluginsAsync(
105+
CancellationToken cancellationToken = default)
100106
{
101107
var plugins = new List<PluginInfo>();
102108

@@ -120,7 +126,7 @@ public async Task<IEnumerable<PluginInfo>> ListAvailablePluginsAsync()
120126
// Get containerized plugins
121127
if (_containerizedOrchestrator != null)
122128
{
123-
var containerizedPlugins = await _containerizedOrchestrator.ListPluginsAsync();
129+
var containerizedPlugins = await _containerizedOrchestrator.ListPluginsAsync(null, cancellationToken);
124130
plugins.AddRange(containerizedPlugins.Select(p => new PluginInfo
125131
{
126132
ModuleName = p.PluginId.Namespace,
@@ -150,8 +156,11 @@ public async Task<IEnumerable<PluginInfo>> ListAvailablePluginsAsync()
150156
/// Deploys a NuGet package as a containerized plugin
151157
/// </summary>
152158
/// <param name="request">Deployment request</param>
159+
/// <param name="cancellationToken">Cancellation token</param>
153160
/// <returns>Deployment result</returns>
154-
public async Task<PluginDeploymentResult> DeployContainerizedPluginAsync(PluginDeploymentRequest request)
161+
public async Task<PluginDeploymentResult> DeployContainerizedPluginAsync(
162+
PluginDeploymentRequest request,
163+
CancellationToken cancellationToken = default)
155164
{
156165
if (_containerizedOrchestrator == null)
157166
{
@@ -162,7 +171,7 @@ public async Task<PluginDeploymentResult> DeployContainerizedPluginAsync(PluginD
162171

163172
try
164173
{
165-
return await _containerizedOrchestrator.DeployPluginAsync(request);
174+
return await _containerizedOrchestrator.DeployPluginAsync(request, cancellationToken);
166175
}
167176
catch (Exception ex)
168177
{
@@ -236,7 +245,11 @@ public void AllowTraditionalPlugin(NamespaceString moduleName, IdentifierString
236245
}
237246
}
238247

239-
private async Task<T?> GetContainerizedPluginAsync(NamespaceString moduleName, IdentifierString pluginName, SemanticVersionNumber? version)
248+
private async Task<T?> GetContainerizedPluginAsync(
249+
NamespaceString moduleName,
250+
IdentifierString pluginName,
251+
SemanticVersionNumber? version,
252+
CancellationToken cancellationToken = default)
240253
{
241254
if (_containerizedOrchestrator == null)
242255
{
@@ -250,10 +263,10 @@ public void AllowTraditionalPlugin(NamespaceString moduleName, IdentifierString
250263
{
251264
Namespace = moduleName,
252265
Name = pluginName,
253-
Version = version ?? new SemanticVersionNumber(0, 0, 0) // Will match latest if version not specified
266+
Version = version ?? new SemanticVersionNumber(0, 0, 0) // Use 0.0.0 as placeholder when version not specified
254267
};
255268

256-
var pluginInfo = await _containerizedOrchestrator.GetPluginInfoAsync(pluginId);
269+
var pluginInfo = await _containerizedOrchestrator.GetPluginInfoAsync(pluginId, cancellationToken);
257270
if (pluginInfo != null)
258271
{
259272
var proxy = ContainerizedPluginProxyFactory.Create<T>(_containerizedOrchestrator, pluginInfo);

RuntimePluggableClassFactory.Containerized/Implementations/ContainerizedPluginProxy.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public ContainerizedPluginProxy(
5959
/// <param name="context">Execution context</param>
6060
/// <param name="input">Input data</param>
6161
/// <returns>Execution result</returns>
62-
public PluginExecutionResult<object> ExecuteTyped(IPluginExecutionContext context, object input)
62+
public Interface.PluginExecutionResult<object> ExecuteTyped(IPluginExecutionContext context, object input)
6363
{
6464
try
6565
{
@@ -68,7 +68,7 @@ public PluginExecutionResult<object> ExecuteTyped(IPluginExecutionContext contex
6868
catch (Exception ex)
6969
{
7070
_logger?.LogError(ex, "Error executing containerized plugin {PluginId}", _pluginId);
71-
return PluginExecutionResult<object>.CreateFailure($"Plugin execution failed: {ex.Message}", ex);
71+
return Interface.PluginExecutionResult<object>.CreateFailure($"Plugin execution failed: {ex.Message}", ex);
7272
}
7373
}
7474

@@ -78,7 +78,7 @@ public PluginExecutionResult<object> ExecuteTyped(IPluginExecutionContext contex
7878
/// <param name="context">Execution context</param>
7979
/// <param name="input">Input data</param>
8080
/// <returns>Execution result</returns>
81-
public async Task<PluginExecutionResult<object>> ExecuteAsync(IPluginExecutionContext context, object input)
81+
public async Task<Interface.PluginExecutionResult<object>> ExecuteAsync(IPluginExecutionContext context, object input)
8282
{
8383
try
8484
{
@@ -130,22 +130,22 @@ public async Task<PluginExecutionResult<object>> ExecuteAsync(IPluginExecutionCo
130130
}
131131
}
132132

133-
return PluginExecutionResult<object>.CreateSuccess(output!);
133+
return Interface.PluginExecutionResult<object>.CreateSuccess(output!);
134134
}
135135
else
136136
{
137137
_logger?.LogWarning("Containerized plugin {PluginId} execution failed: {Error}",
138138
_pluginId, result.ErrorMessage);
139139

140-
return PluginExecutionResult<object>.CreateFailure(
140+
return Interface.PluginExecutionResult<object>.CreateFailure(
141141
result.ErrorMessage ?? "Unknown error",
142-
result.Exception);
142+
result.Exception ?? null!);
143143
}
144144
}
145145
catch (Exception ex)
146146
{
147147
_logger?.LogError(ex, "Error executing containerized plugin {PluginId}", _pluginId);
148-
return PluginExecutionResult<object>.CreateFailure($"Plugin execution failed: {ex.Message}", ex);
148+
return Interface.PluginExecutionResult<object>.CreateFailure($"Plugin execution failed: {ex.Message}", ex);
149149
}
150150
}
151151
}

RuntimePluggableClassFactory.Containerized/Security/NuGetSignatureValidator.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public async Task<PackageValidationResult> ValidatePackageAsync(
5050
using var package = new PackageArchiveReader(packageStream);
5151

5252
// Extract package information
53-
var packageInfo = await ExtractPackageInfoInternalAsync(package);
53+
var packageInfo = await ExtractPackageInfoInternalAsync(package, packageStream);
5454

5555
// Check if signature is required
5656
if (options.RequireSignature)
@@ -116,20 +116,25 @@ public async Task<SignedPackageInfo> ExtractPackageInfoAsync(Stream packageStrea
116116
{
117117
packageStream.Seek(0, SeekOrigin.Begin);
118118
using var package = new PackageArchiveReader(packageStream);
119-
return await ExtractPackageInfoInternalAsync(package);
119+
return await ExtractPackageInfoInternalAsync(package, packageStream);
120120
}
121121

122-
private async Task<SignedPackageInfo> ExtractPackageInfoInternalAsync(PackageArchiveReader package)
122+
private async Task<SignedPackageInfo> ExtractPackageInfoInternalAsync(PackageArchiveReader package, Stream packageStream)
123123
{
124124
var identity = package.GetIdentity();
125125

126126
// Calculate package hash using actual package content
127127
byte[] hash;
128128
long packageSize;
129-
using (var stream = package.GetStream())
130-
{
131-
(hash, packageSize) = await ComputeHashAndSizeAsync(stream);
132-
}
129+
130+
// Save current position
131+
var originalPosition = packageStream.Position;
132+
133+
// Compute hash from the package stream
134+
(hash, packageSize) = await ComputeHashAndSizeAsync(packageStream);
135+
136+
// Restore stream position
137+
packageStream.Seek(originalPosition, SeekOrigin.Begin);
133138

134139
return new SignedPackageInfo
135140
{
@@ -160,7 +165,7 @@ private async Task<SignedPackageInfo> ExtractPackageInfoInternalAsync(PackageArc
160165
totalBytes += bytesRead;
161166
}
162167
}
163-
return (sha256.Hash, totalBytes);
168+
return (sha256.Hash ?? Array.Empty<byte>(), totalBytes);
164169
}
165170
}
166171
private async Task<SignatureValidationInternalResult> ValidateSignatureAsync(PackageArchiveReader package)

0 commit comments

Comments
 (0)