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,157 @@
namespace StellaOps.Plugin.Sdk;
using Microsoft.Extensions.Logging;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
/// <summary>
/// Base class for simplified plugin development.
/// Provides common patterns and reduces boilerplate.
/// </summary>
public abstract class PluginBase : IPlugin
{
private IPluginContext? _context;
/// <summary>
/// Gets the plugin context.
/// </summary>
protected IPluginContext Context => _context ?? throw new InvalidOperationException("Plugin not initialized");
/// <summary>
/// Gets the plugin logger.
/// </summary>
protected IPluginLogger Logger => _context?.Logger ?? NullPluginLogger.Instance;
/// <summary>
/// Gets the plugin configuration.
/// </summary>
protected IPluginConfiguration Configuration => _context?.Configuration ?? EmptyConfiguration.Instance;
/// <summary>
/// Gets the time provider.
/// </summary>
protected TimeProvider TimeProvider => _context?.TimeProvider ?? TimeProvider.System;
/// <summary>
/// Gets the plugin services.
/// </summary>
protected IPluginServices Services => _context?.Services ?? throw new InvalidOperationException("Plugin not initialized");
/// <inheritdoc />
public abstract PluginInfo Info { get; }
/// <inheritdoc />
public virtual PluginTrustLevel TrustLevel => PluginTrustLevel.Untrusted;
/// <inheritdoc />
public abstract PluginCapabilities Capabilities { get; }
/// <inheritdoc />
public PluginLifecycleState State { get; protected set; } = PluginLifecycleState.Discovered;
/// <inheritdoc />
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
{
_context = context;
State = PluginLifecycleState.Initializing;
try
{
await OnInitializeAsync(context, ct);
State = PluginLifecycleState.Active;
Logger.Info("Plugin {PluginId} initialized successfully", Info.Id);
}
catch (Exception ex)
{
State = PluginLifecycleState.Failed;
Logger.Error(ex, "Plugin {PluginId} failed to initialize", Info.Id);
throw;
}
}
/// <summary>
/// Override to add initialization logic.
/// </summary>
/// <param name="context">The plugin context.</param>
/// <param name="ct">Cancellation token.</param>
protected virtual Task OnInitializeAsync(IPluginContext context, CancellationToken ct)
=> Task.CompletedTask;
/// <inheritdoc />
public virtual Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
{
return Task.FromResult(State == PluginLifecycleState.Active
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy($"Plugin is in state {State}"));
}
/// <inheritdoc />
public virtual async ValueTask DisposeAsync()
{
try
{
await OnDisposeAsync();
}
finally
{
State = PluginLifecycleState.Stopped;
}
}
/// <summary>
/// Override to add cleanup logic.
/// </summary>
protected virtual ValueTask OnDisposeAsync() => ValueTask.CompletedTask;
/// <summary>
/// Ensures the plugin is in active state.
/// </summary>
/// <exception cref="InvalidOperationException">If plugin is not active.</exception>
protected void EnsureActive()
{
if (State != PluginLifecycleState.Active)
throw new InvalidOperationException($"{Info.Name} is not active (state: {State})");
}
}
/// <summary>
/// Null implementation of plugin logger that discards all log messages.
/// </summary>
internal sealed class NullPluginLogger : IPluginLogger
{
public static readonly NullPluginLogger Instance = new();
private NullPluginLogger() { }
public void Log(LogLevel level, string message, params object[] args) { }
public void Log(LogLevel level, Exception exception, string message, params object[] args) { }
public void Debug(string message, params object[] args) { }
public void Info(string message, params object[] args) { }
public void Warning(string message, params object[] args) { }
public void Error(string message, params object[] args) { }
public void Error(Exception ex, string message, params object[] args) { }
public IPluginLogger WithProperty(string name, object value) => this;
public IPluginLogger ForOperation(string operationName) => this;
public bool IsEnabled(LogLevel level) => false;
}
/// <summary>
/// Empty configuration that returns defaults for all values.
/// </summary>
internal sealed class EmptyConfiguration : IPluginConfiguration
{
public static readonly EmptyConfiguration Instance = new();
private EmptyConfiguration() { }
public T? GetValue<T>(string key, T? defaultValue = default) => defaultValue;
public T Bind<T>(string? sectionKey = null) where T : class, new() => new();
public Task<string?> GetSecretAsync(string secretName, CancellationToken ct)
=> Task.FromResult<string?>(null);
public bool HasKey(string key) => false;
}

View File

@@ -0,0 +1,33 @@
namespace StellaOps.Plugin.Sdk;
/// <summary>
/// Attribute for marking plugin configuration properties.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class PluginConfigAttribute : Attribute
{
/// <summary>
/// Configuration key. If not specified, property name is used.
/// </summary>
public string? Key { get; set; }
/// <summary>
/// Description of the configuration option.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Whether the configuration is required.
/// </summary>
public bool Required { get; set; }
/// <summary>
/// Default value if not specified.
/// </summary>
public object? DefaultValue { get; set; }
/// <summary>
/// Whether this is a secret value (should not be logged).
/// </summary>
public bool Secret { get; set; }
}

View File

@@ -0,0 +1,83 @@
namespace StellaOps.Plugin.Sdk;
using System.Diagnostics;
using System.Globalization;
using StellaOps.Plugin.Abstractions.Context;
/// <summary>
/// Extension methods for common plugin operations.
/// </summary>
public static class PluginExtensions
{
/// <summary>
/// Get required configuration value.
/// </summary>
/// <typeparam name="T">Target type.</typeparam>
/// <param name="config">Configuration instance.</param>
/// <param name="key">Configuration key.</param>
/// <returns>The configuration value.</returns>
/// <exception cref="InvalidOperationException">If key is not found.</exception>
public static T GetRequiredValue<T>(this IPluginConfiguration config, string key)
{
var value = config.GetValue<T>(key);
if (value == null)
throw new InvalidOperationException($"Required configuration key '{key}' not found");
return value;
}
/// <summary>
/// Get secret with caching.
/// </summary>
/// <param name="config">Configuration instance.</param>
/// <param name="key">Secret key.</param>
/// <param name="cacheDuration">How long to cache the secret.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The secret value or null.</returns>
public static async Task<string?> GetCachedSecretAsync(
this IPluginConfiguration config,
string key,
TimeSpan cacheDuration,
CancellationToken ct)
{
// Implementation would cache secrets to reduce vault calls
// For now, just pass through to GetSecretAsync
return await config.GetSecretAsync(key, ct);
}
/// <summary>
/// Create a scoped logger for a specific operation.
/// </summary>
/// <param name="logger">Logger instance.</param>
/// <param name="operationName">Name of the operation.</param>
/// <returns>Disposable scope that logs completion time.</returns>
public static IDisposable BeginScope(this IPluginLogger logger, string operationName)
{
logger.Debug("Starting operation: {Operation}", operationName);
var sw = Stopwatch.StartNew();
return new ScopeDisposable(() =>
{
sw.Stop();
logger.Debug("Completed operation: {Operation} in {Elapsed}ms",
operationName, sw.ElapsedMilliseconds);
});
}
/// <summary>
/// Get HTTP client from services.
/// </summary>
/// <param name="services">Plugin services.</param>
/// <param name="name">Named client name.</param>
/// <returns>HttpClient instance.</returns>
public static HttpClient GetHttpClient(this IPluginServices services, string name = "")
{
var factory = services.GetRequiredService<IHttpClientFactory>();
return factory.CreateClient(name);
}
private sealed class ScopeDisposable(Action onDispose) : IDisposable
{
public void Dispose() => onDispose();
}
}

View File

@@ -0,0 +1,120 @@
namespace StellaOps.Plugin.Sdk;
using StellaOps.Plugin.Abstractions;
/// <summary>
/// Fluent builder for creating PluginInfo.
/// </summary>
public sealed class PluginInfoBuilder
{
private string _id = "";
private string _name = "";
private string _version = "1.0.0";
private string _vendor = "";
private string? _description;
private string? _licenseId;
private string? _projectUrl;
private string? _iconUrl;
/// <summary>
/// Sets the plugin ID.
/// </summary>
/// <param name="id">Plugin ID in reverse domain notation (e.g., com.example.plugin).</param>
public PluginInfoBuilder WithId(string id)
{
_id = id;
return this;
}
/// <summary>
/// Sets the plugin name.
/// </summary>
/// <param name="name">Human-readable plugin name.</param>
public PluginInfoBuilder WithName(string name)
{
_name = name;
return this;
}
/// <summary>
/// Sets the plugin version.
/// </summary>
/// <param name="version">SemVer version string.</param>
public PluginInfoBuilder WithVersion(string version)
{
_version = version;
return this;
}
/// <summary>
/// Sets the plugin vendor.
/// </summary>
/// <param name="vendor">Vendor/company name.</param>
public PluginInfoBuilder WithVendor(string vendor)
{
_vendor = vendor;
return this;
}
/// <summary>
/// Sets the plugin description.
/// </summary>
/// <param name="description">Description of what the plugin does.</param>
public PluginInfoBuilder WithDescription(string description)
{
_description = description;
return this;
}
/// <summary>
/// Sets the plugin license.
/// </summary>
/// <param name="licenseId">SPDX license identifier.</param>
public PluginInfoBuilder WithLicense(string licenseId)
{
_licenseId = licenseId;
return this;
}
/// <summary>
/// Sets the plugin project URL.
/// </summary>
/// <param name="projectUrl">Project URL.</param>
public PluginInfoBuilder WithProjectUrl(string projectUrl)
{
_projectUrl = projectUrl;
return this;
}
/// <summary>
/// Sets the plugin icon URL.
/// </summary>
/// <param name="iconUrl">Icon URL.</param>
public PluginInfoBuilder WithIconUrl(string iconUrl)
{
_iconUrl = iconUrl;
return this;
}
/// <summary>
/// Builds the PluginInfo.
/// </summary>
/// <exception cref="InvalidOperationException">If required fields are not set.</exception>
public PluginInfo Build()
{
if (string.IsNullOrEmpty(_id))
throw new InvalidOperationException("Plugin ID is required");
if (string.IsNullOrEmpty(_name))
throw new InvalidOperationException("Plugin name is required");
return new PluginInfo(
Id: _id,
Name: _name,
Version: _version,
Vendor: _vendor,
Description: _description,
LicenseId: _licenseId,
ProjectUrl: _projectUrl,
IconUrl: _iconUrl);
}
}

View File

@@ -0,0 +1,35 @@
namespace StellaOps.Plugin.Sdk;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Options base class with validation support.
/// </summary>
public abstract class PluginOptionsBase : IValidatableObject
{
/// <summary>
/// Override to add custom validation logic.
/// </summary>
/// <param name="validationContext">Validation context.</param>
/// <returns>Validation results.</returns>
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
yield break;
}
/// <summary>
/// Validates the options and throws if invalid.
/// </summary>
/// <exception cref="ValidationException">If validation fails.</exception>
public void ValidateAndThrow()
{
var context = new ValidationContext(this);
var results = new List<ValidationResult>();
if (!Validator.TryValidateObject(this, context, results, validateAllProperties: true))
{
var errors = string.Join(", ", results.Select(r => r.ErrorMessage));
throw new ValidationException($"Configuration validation failed: {errors}");
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Description>Plugin SDK for building Stella Ops plugins</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Plugin.Abstractions\StellaOps.Plugin.Abstractions.csproj" />
</ItemGroup>
</Project>