release orchestrator v1 draft and build fixes
This commit is contained in:
221
src/Plugin/StellaOps.Plugin.Host/Context/PluginConfiguration.cs
Normal file
221
src/Plugin/StellaOps.Plugin.Host/Context/PluginConfiguration.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
using StellaOps.Plugin.Abstractions.Manifest;
|
||||
|
||||
namespace StellaOps.Plugin.Host.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of IPluginConfiguration.
|
||||
/// </summary>
|
||||
public sealed class PluginConfiguration : IPluginConfiguration
|
||||
{
|
||||
private readonly Dictionary<string, object> _values;
|
||||
private readonly PluginManifest _manifest;
|
||||
private readonly Func<string, CancellationToken, Task<string?>>? _secretProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new plugin configuration.
|
||||
/// </summary>
|
||||
/// <param name="manifest">The plugin manifest (contains default config).</param>
|
||||
/// <param name="overrides">Optional configuration overrides.</param>
|
||||
/// <param name="secretProvider">Optional secret provider function.</param>
|
||||
public PluginConfiguration(
|
||||
PluginManifest manifest,
|
||||
IReadOnlyDictionary<string, object>? overrides = null,
|
||||
Func<string, CancellationToken, Task<string?>>? secretProvider = null)
|
||||
{
|
||||
_manifest = manifest;
|
||||
_secretProvider = secretProvider;
|
||||
_values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Load defaults from manifest
|
||||
if (manifest.DefaultConfig != null)
|
||||
{
|
||||
LoadFromJsonDocument(manifest.DefaultConfig);
|
||||
}
|
||||
|
||||
// Apply overrides
|
||||
if (overrides != null)
|
||||
{
|
||||
foreach (var kvp in overrides)
|
||||
{
|
||||
_values[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? GetValue<T>(string key, T? defaultValue = default)
|
||||
{
|
||||
if (!_values.TryGetValue(key, out var value))
|
||||
return defaultValue;
|
||||
|
||||
var converted = ConvertValue<T>(value);
|
||||
return converted ?? defaultValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Bind<T>(string? sectionKey = null) where T : class, new()
|
||||
{
|
||||
var result = new T();
|
||||
var prefix = string.IsNullOrEmpty(sectionKey) ? "" : sectionKey + ":";
|
||||
var type = typeof(T);
|
||||
|
||||
foreach (var property in type.GetProperties())
|
||||
{
|
||||
if (!property.CanWrite)
|
||||
continue;
|
||||
|
||||
var key = string.IsNullOrEmpty(prefix) ? property.Name : $"{prefix}{property.Name}";
|
||||
|
||||
if (_values.TryGetValue(key, out var value))
|
||||
{
|
||||
try
|
||||
{
|
||||
var converted = ConvertValueToType(value, property.PropertyType);
|
||||
if (converted != null)
|
||||
{
|
||||
property.SetValue(result, converted);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip properties that can't be converted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetSecretAsync(string secretName, CancellationToken ct)
|
||||
{
|
||||
if (_secretProvider != null)
|
||||
{
|
||||
return await _secretProvider(secretName, ct);
|
||||
}
|
||||
|
||||
// Fall back to environment variable
|
||||
return Environment.GetEnvironmentVariable(secretName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasKey(string key)
|
||||
{
|
||||
return _values.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all configuration values.
|
||||
/// </summary>
|
||||
/// <returns>All configuration values.</returns>
|
||||
public IReadOnlyDictionary<string, object> GetAll()
|
||||
{
|
||||
return _values;
|
||||
}
|
||||
|
||||
private void LoadFromJsonDocument(JsonDocument document)
|
||||
{
|
||||
LoadFromJsonElement("", document.RootElement);
|
||||
}
|
||||
|
||||
private void LoadFromJsonElement(string prefix, JsonElement element)
|
||||
{
|
||||
switch (element.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
var key = string.IsNullOrEmpty(prefix)
|
||||
? property.Name
|
||||
: $"{prefix}:{property.Name}";
|
||||
LoadFromJsonElement(key, property.Value);
|
||||
}
|
||||
break;
|
||||
|
||||
case JsonValueKind.Array:
|
||||
var index = 0;
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
var key = $"{prefix}:{index}";
|
||||
LoadFromJsonElement(key, item);
|
||||
index++;
|
||||
}
|
||||
break;
|
||||
|
||||
case JsonValueKind.String:
|
||||
_values[prefix] = element.GetString()!;
|
||||
break;
|
||||
|
||||
case JsonValueKind.Number:
|
||||
if (element.TryGetInt64(out var longValue))
|
||||
_values[prefix] = longValue;
|
||||
else if (element.TryGetDouble(out var doubleValue))
|
||||
_values[prefix] = doubleValue;
|
||||
break;
|
||||
|
||||
case JsonValueKind.True:
|
||||
_values[prefix] = true;
|
||||
break;
|
||||
|
||||
case JsonValueKind.False:
|
||||
_values[prefix] = false;
|
||||
break;
|
||||
|
||||
case JsonValueKind.Null:
|
||||
case JsonValueKind.Undefined:
|
||||
// Skip null values
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static T? ConvertValue<T>(object value)
|
||||
{
|
||||
return (T?)ConvertValueToType(value, typeof(T));
|
||||
}
|
||||
|
||||
private static object? ConvertValueToType(object value, Type targetType)
|
||||
{
|
||||
if (value.GetType() == targetType)
|
||||
return value;
|
||||
|
||||
try
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||
|
||||
if (value is JsonElement jsonElement)
|
||||
{
|
||||
var json = jsonElement.GetRawText();
|
||||
return JsonSerializer.Deserialize(json, targetType);
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(string))
|
||||
return value.ToString();
|
||||
|
||||
if (underlyingType == typeof(int))
|
||||
return Convert.ToInt32(value);
|
||||
|
||||
if (underlyingType == typeof(long))
|
||||
return Convert.ToInt64(value);
|
||||
|
||||
if (underlyingType == typeof(double))
|
||||
return Convert.ToDouble(value);
|
||||
|
||||
if (underlyingType == typeof(bool))
|
||||
return Convert.ToBoolean(value);
|
||||
|
||||
if (underlyingType == typeof(TimeSpan) && value is string strValue)
|
||||
return TimeSpan.Parse(strValue);
|
||||
|
||||
if (underlyingType == typeof(Guid) && value is string guidStr)
|
||||
return Guid.Parse(guidStr);
|
||||
|
||||
return Convert.ChangeType(value, underlyingType);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/Plugin/StellaOps.Plugin.Host/Context/PluginContext.cs
Normal file
130
src/Plugin/StellaOps.Plugin.Host/Context/PluginContext.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
using StellaOps.Plugin.Abstractions.Manifest;
|
||||
|
||||
namespace StellaOps.Plugin.Host.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of IPluginContext provided to plugins during initialization.
|
||||
/// </summary>
|
||||
public sealed class PluginContext : IPluginContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IPluginConfiguration Configuration { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPluginLogger Logger { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPluginServices Services { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid? TenantId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid InstanceId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public CancellationToken ShutdownToken { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeProvider TimeProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The plugin manifest.
|
||||
/// </summary>
|
||||
public PluginManifest Manifest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust level of the plugin.
|
||||
/// </summary>
|
||||
public PluginTrustLevel TrustLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new plugin context.
|
||||
/// </summary>
|
||||
public PluginContext(
|
||||
PluginManifest manifest,
|
||||
PluginTrustLevel trustLevel,
|
||||
IPluginConfiguration configuration,
|
||||
IPluginLogger logger,
|
||||
IPluginServices services,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken shutdownToken,
|
||||
Guid? tenantId = null)
|
||||
{
|
||||
Manifest = manifest;
|
||||
TrustLevel = trustLevel;
|
||||
Configuration = configuration;
|
||||
Logger = logger;
|
||||
Services = services;
|
||||
TimeProvider = timeProvider;
|
||||
ShutdownToken = shutdownToken;
|
||||
TenantId = tenantId;
|
||||
InstanceId = Guid.NewGuid();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating plugin contexts.
|
||||
/// </summary>
|
||||
public sealed class PluginContextFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly PluginHostOptions _options;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new plugin context factory.
|
||||
/// </summary>
|
||||
public PluginContextFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
PluginHostOptions options,
|
||||
IServiceProvider serviceProvider,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_options = options;
|
||||
_serviceProvider = serviceProvider;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a plugin context for a manifest.
|
||||
/// </summary>
|
||||
/// <param name="manifest">The plugin manifest.</param>
|
||||
/// <param name="trustLevel">Trust level.</param>
|
||||
/// <param name="shutdownToken">Shutdown token.</param>
|
||||
/// <param name="tenantId">Optional tenant ID.</param>
|
||||
/// <returns>The created context.</returns>
|
||||
public PluginContext Create(
|
||||
PluginManifest manifest,
|
||||
PluginTrustLevel trustLevel,
|
||||
CancellationToken shutdownToken,
|
||||
Guid? tenantId = null)
|
||||
{
|
||||
var pluginId = manifest.Info.Id;
|
||||
|
||||
// Create configuration
|
||||
var configuration = new PluginConfiguration(manifest);
|
||||
|
||||
// Create logger
|
||||
var innerLogger = _loggerFactory.CreateLogger($"Plugin.{pluginId}");
|
||||
var logger = new PluginLogger(innerLogger, pluginId);
|
||||
|
||||
// Create services wrapper
|
||||
var services = new PluginServices(_serviceProvider, trustLevel);
|
||||
|
||||
return new PluginContext(
|
||||
manifest,
|
||||
trustLevel,
|
||||
configuration,
|
||||
logger,
|
||||
services,
|
||||
_timeProvider,
|
||||
shutdownToken,
|
||||
tenantId);
|
||||
}
|
||||
}
|
||||
112
src/Plugin/StellaOps.Plugin.Host/Context/PluginLogger.cs
Normal file
112
src/Plugin/StellaOps.Plugin.Host/Context/PluginLogger.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
|
||||
namespace StellaOps.Plugin.Host.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of IPluginLogger that wraps a Microsoft.Extensions.Logging logger.
|
||||
/// </summary>
|
||||
public sealed class PluginLogger : IPluginLogger
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _pluginId;
|
||||
private readonly Dictionary<string, object> _properties;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new plugin logger.
|
||||
/// </summary>
|
||||
/// <param name="logger">The underlying logger.</param>
|
||||
/// <param name="pluginId">The plugin ID for context.</param>
|
||||
public PluginLogger(ILogger logger, string pluginId)
|
||||
: this(logger, pluginId, new Dictionary<string, object>())
|
||||
{
|
||||
}
|
||||
|
||||
private PluginLogger(ILogger logger, string pluginId, Dictionary<string, object> properties)
|
||||
{
|
||||
_logger = logger;
|
||||
_pluginId = pluginId;
|
||||
_properties = properties;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log(LogLevel level, string message, params object[] args)
|
||||
{
|
||||
_logger.Log(level, message, args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log(LogLevel level, Exception exception, string message, params object[] args)
|
||||
{
|
||||
_logger.Log(level, exception, message, args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPluginLogger WithProperty(string name, object value)
|
||||
{
|
||||
var newProperties = new Dictionary<string, object>(_properties)
|
||||
{
|
||||
[name] = value
|
||||
};
|
||||
|
||||
// Create a logger that includes the property in scope
|
||||
return new PropertyScopedPluginLogger(_logger, _pluginId, newProperties);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPluginLogger ForOperation(string operationName)
|
||||
{
|
||||
return WithProperty("Operation", operationName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel level) => _logger.IsEnabled(level);
|
||||
|
||||
/// <summary>
|
||||
/// Plugin logger with property scope support.
|
||||
/// </summary>
|
||||
private sealed class PropertyScopedPluginLogger : IPluginLogger
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _pluginId;
|
||||
private readonly Dictionary<string, object> _properties;
|
||||
|
||||
public PropertyScopedPluginLogger(
|
||||
ILogger logger,
|
||||
string pluginId,
|
||||
Dictionary<string, object> properties)
|
||||
{
|
||||
_logger = logger;
|
||||
_pluginId = pluginId;
|
||||
_properties = properties;
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, string message, params object[] args)
|
||||
{
|
||||
using var scope = _logger.BeginScope(_properties);
|
||||
_logger.Log(level, message, args);
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, Exception exception, string message, params object[] args)
|
||||
{
|
||||
using var scope = _logger.BeginScope(_properties);
|
||||
_logger.Log(level, exception, message, args);
|
||||
}
|
||||
|
||||
public IPluginLogger WithProperty(string name, object value)
|
||||
{
|
||||
var newProperties = new Dictionary<string, object>(_properties)
|
||||
{
|
||||
[name] = value
|
||||
};
|
||||
return new PropertyScopedPluginLogger(_logger, _pluginId, newProperties);
|
||||
}
|
||||
|
||||
public IPluginLogger ForOperation(string operationName)
|
||||
{
|
||||
return WithProperty("Operation", operationName);
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel level) => _logger.IsEnabled(level);
|
||||
}
|
||||
}
|
||||
120
src/Plugin/StellaOps.Plugin.Host/Context/PluginServices.cs
Normal file
120
src/Plugin/StellaOps.Plugin.Host/Context/PluginServices.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
|
||||
namespace StellaOps.Plugin.Host.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of IPluginServices that provides access to platform services.
|
||||
/// </summary>
|
||||
public sealed class PluginServices : IPluginServices, IAsyncDisposable
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly PluginTrustLevel _trustLevel;
|
||||
private readonly HashSet<Type> _restrictedTypes;
|
||||
private readonly IServiceScope? _scope;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new plugin services wrapper.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The underlying service provider.</param>
|
||||
/// <param name="trustLevel">Trust level of the plugin.</param>
|
||||
public PluginServices(IServiceProvider serviceProvider, PluginTrustLevel trustLevel)
|
||||
: this(serviceProvider, trustLevel, null)
|
||||
{
|
||||
}
|
||||
|
||||
private PluginServices(IServiceProvider serviceProvider, PluginTrustLevel trustLevel, IServiceScope? scope)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_trustLevel = trustLevel;
|
||||
_scope = scope;
|
||||
_restrictedTypes = GetRestrictedTypes(trustLevel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? GetService<T>() where T : class
|
||||
{
|
||||
ValidateAccess(typeof(T));
|
||||
return _serviceProvider.GetService<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetRequiredService<T>() where T : class
|
||||
{
|
||||
ValidateAccess(typeof(T));
|
||||
return _serviceProvider.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<T> GetServices<T>() where T : class
|
||||
{
|
||||
ValidateAccess(typeof(T));
|
||||
return _serviceProvider.GetServices<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncDisposable CreateScope(out IPluginServices scopedServices)
|
||||
{
|
||||
var scope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||
var scoped = new PluginServices(scope.ServiceProvider, _trustLevel, scope);
|
||||
scopedServices = scoped;
|
||||
return scoped;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_scope is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_scope?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateAccess(Type serviceType)
|
||||
{
|
||||
if (IsRestricted(serviceType))
|
||||
{
|
||||
throw new UnauthorizedAccessException(
|
||||
$"Plugin does not have permission to access service '{serviceType.Name}'. " +
|
||||
$"Trust level: {_trustLevel}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsRestricted(Type serviceType)
|
||||
{
|
||||
// Built-in plugins have full access
|
||||
if (_trustLevel == PluginTrustLevel.BuiltIn)
|
||||
return false;
|
||||
|
||||
// Check if the type or any of its interfaces are restricted
|
||||
if (_restrictedTypes.Contains(serviceType))
|
||||
return true;
|
||||
|
||||
foreach (var iface in serviceType.GetInterfaces())
|
||||
{
|
||||
if (_restrictedTypes.Contains(iface))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static HashSet<Type> GetRestrictedTypes(PluginTrustLevel trustLevel)
|
||||
{
|
||||
var restricted = new HashSet<Type>();
|
||||
|
||||
// Untrusted plugins have more restrictions
|
||||
if (trustLevel == PluginTrustLevel.Untrusted)
|
||||
{
|
||||
// Add types that untrusted plugins cannot access
|
||||
// This will be populated based on security requirements
|
||||
}
|
||||
|
||||
return restricted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user