release orchestrator v1 draft and build fixes
This commit is contained in:
157
src/Plugin/StellaOps.Plugin.Sdk/PluginBase.cs
Normal file
157
src/Plugin/StellaOps.Plugin.Sdk/PluginBase.cs
Normal 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;
|
||||
}
|
||||
33
src/Plugin/StellaOps.Plugin.Sdk/PluginConfigAttribute.cs
Normal file
33
src/Plugin/StellaOps.Plugin.Sdk/PluginConfigAttribute.cs
Normal 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; }
|
||||
}
|
||||
83
src/Plugin/StellaOps.Plugin.Sdk/PluginExtensions.cs
Normal file
83
src/Plugin/StellaOps.Plugin.Sdk/PluginExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
120
src/Plugin/StellaOps.Plugin.Sdk/PluginInfoBuilder.cs
Normal file
120
src/Plugin/StellaOps.Plugin.Sdk/PluginInfoBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
35
src/Plugin/StellaOps.Plugin.Sdk/PluginOptionsBase.cs
Normal file
35
src/Plugin/StellaOps.Plugin.Sdk/PluginOptionsBase.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Plugin/StellaOps.Plugin.Sdk/StellaOps.Plugin.Sdk.csproj
Normal file
20
src/Plugin/StellaOps.Plugin.Sdk/StellaOps.Plugin.Sdk.csproj
Normal 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>
|
||||
Reference in New Issue
Block a user