release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View 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;
}
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}