audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -0,0 +1,239 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
namespace StellaOps.FeatureFlags.Providers;
/// <summary>
/// Feature flag provider that reads flags from IConfiguration.
/// Supports simple boolean flags and structured flag definitions.
/// </summary>
/// <remarks>
/// Configuration format:
/// <code>
/// {
/// "FeatureFlags": {
/// "MyFeature": true,
/// "MyComplexFeature": {
/// "Enabled": true,
/// "Variant": "blue"
/// }
/// }
/// }
/// </code>
/// </remarks>
public sealed class ConfigurationFeatureFlagProvider : FeatureFlagProviderBase, IDisposable
{
private readonly IConfiguration _configuration;
private readonly string _sectionName;
private readonly Dictionary<string, bool> _lastValues = new();
private readonly IDisposable? _changeToken;
private Action<FeatureFlagChangedEvent>? _changeCallback;
/// <summary>
/// Creates a new configuration-based feature flag provider.
/// </summary>
/// <param name="configuration">The configuration root.</param>
/// <param name="sectionName">Configuration section name (default: "FeatureFlags").</param>
/// <param name="priority">Provider priority (default: 50).</param>
public ConfigurationFeatureFlagProvider(
IConfiguration configuration,
string sectionName = "FeatureFlags",
int priority = 50)
{
_configuration = configuration;
_sectionName = sectionName;
Priority = priority;
// Initialize last values
InitializeLastValues();
// Watch for configuration changes
_changeToken = ChangeToken.OnChange(
() => _configuration.GetReloadToken(),
OnConfigurationChanged);
}
/// <inheritdoc />
public override string Name => "Configuration";
/// <inheritdoc />
public override int Priority { get; }
/// <inheritdoc />
public override bool SupportsWatch => true;
/// <inheritdoc />
public override Task<FeatureFlagResult?> TryGetFlagAsync(
string flagKey,
FeatureFlagEvaluationContext context,
CancellationToken ct = default)
{
var section = _configuration.GetSection($"{_sectionName}:{flagKey}");
if (!section.Exists())
{
return Task.FromResult<FeatureFlagResult?>(null);
}
// Check if it's a simple boolean value
if (bool.TryParse(section.Value, out var boolValue))
{
return Task.FromResult<FeatureFlagResult?>(
CreateResult(flagKey, boolValue, null, "From configuration (boolean)"));
}
// Check if it's a structured definition
var enabled = section.GetValue<bool?>("Enabled") ?? section.GetValue<bool?>("enabled");
if (enabled.HasValue)
{
var variant = section.GetValue<string?>("Variant") ?? section.GetValue<string?>("variant");
return Task.FromResult<FeatureFlagResult?>(
CreateResult(flagKey, enabled.Value, variant, "From configuration (structured)"));
}
// Treat any non-empty value as enabled
if (!string.IsNullOrEmpty(section.Value))
{
return Task.FromResult<FeatureFlagResult?>(
CreateResult(flagKey, true, section.Value, "From configuration (value)"));
}
return Task.FromResult<FeatureFlagResult?>(null);
}
/// <inheritdoc />
public override Task<IReadOnlyList<FeatureFlagDefinition>> ListFlagsAsync(
CancellationToken ct = default)
{
var section = _configuration.GetSection(_sectionName);
var flags = new List<FeatureFlagDefinition>();
foreach (var child in section.GetChildren())
{
var key = child.Key;
bool defaultValue = false;
string? description = null;
if (bool.TryParse(child.Value, out var boolValue))
{
defaultValue = boolValue;
}
else if (child.GetChildren().Any())
{
defaultValue = child.GetValue<bool?>("Enabled") ?? child.GetValue<bool?>("enabled") ?? false;
description = child.GetValue<string?>("Description") ?? child.GetValue<string?>("description");
}
flags.Add(new FeatureFlagDefinition(
key,
description,
defaultValue,
defaultValue));
}
return Task.FromResult<IReadOnlyList<FeatureFlagDefinition>>(flags);
}
/// <inheritdoc />
public override async IAsyncEnumerable<FeatureFlagChangedEvent> WatchAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
{
var channel = System.Threading.Channels.Channel.CreateUnbounded<FeatureFlagChangedEvent>();
_changeCallback = evt => channel.Writer.TryWrite(evt);
try
{
await foreach (var evt in channel.Reader.ReadAllAsync(ct))
{
yield return evt;
}
}
finally
{
_changeCallback = null;
channel.Writer.Complete();
}
}
private void InitializeLastValues()
{
var section = _configuration.GetSection(_sectionName);
foreach (var child in section.GetChildren())
{
var value = GetFlagValue(child);
if (value.HasValue)
{
_lastValues[child.Key] = value.Value;
}
}
}
private void OnConfigurationChanged()
{
var section = _configuration.GetSection(_sectionName);
var currentKeys = new HashSet<string>();
foreach (var child in section.GetChildren())
{
currentKeys.Add(child.Key);
var newValue = GetFlagValue(child);
if (newValue.HasValue)
{
if (_lastValues.TryGetValue(child.Key, out var oldValue))
{
if (oldValue != newValue.Value)
{
// Value changed
_lastValues[child.Key] = newValue.Value;
NotifyChange(child.Key, oldValue, newValue.Value);
}
}
else
{
// New flag
_lastValues[child.Key] = newValue.Value;
NotifyChange(child.Key, false, newValue.Value);
}
}
}
// Check for deleted flags
foreach (var key in _lastValues.Keys.Except(currentKeys).ToList())
{
var oldValue = _lastValues[key];
_lastValues.Remove(key);
NotifyChange(key, oldValue, false);
}
}
private static bool? GetFlagValue(IConfigurationSection section)
{
if (bool.TryParse(section.Value, out var boolValue))
{
return boolValue;
}
var enabled = section.GetValue<bool?>("Enabled") ?? section.GetValue<bool?>("enabled");
return enabled;
}
private void NotifyChange(string key, bool oldValue, bool newValue)
{
var evt = new FeatureFlagChangedEvent(
key,
oldValue,
newValue,
Name,
DateTimeOffset.UtcNow);
_changeCallback?.Invoke(evt);
}
/// <inheritdoc />
public void Dispose()
{
_changeToken?.Dispose();
}
}

View File

@@ -0,0 +1,241 @@
using System.Runtime.CompilerServices;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
namespace StellaOps.FeatureFlags.Providers;
/// <summary>
/// Feature flag provider that reads flags from a settings store connector.
/// Works with connectors that support native feature flags (Azure App Config, AWS AppConfig).
/// </summary>
public sealed class SettingsStoreFeatureFlagProvider : FeatureFlagProviderBase, IDisposable
{
private readonly ISettingsStoreConnectorCapability _connector;
private readonly ConnectorContext _context;
private readonly string _providerName;
private readonly Dictionary<string, bool> _lastValues = new();
private Action<FeatureFlagChangedEvent>? _changeCallback;
private CancellationTokenSource? _watchCts;
private Task? _watchTask;
private bool _disposed;
/// <summary>
/// Creates a new settings store feature flag provider.
/// </summary>
/// <param name="connector">The settings store connector.</param>
/// <param name="context">The connector context.</param>
/// <param name="providerName">Display name for this provider.</param>
/// <param name="priority">Provider priority (default: 100).</param>
public SettingsStoreFeatureFlagProvider(
ISettingsStoreConnectorCapability connector,
ConnectorContext context,
string? providerName = null,
int priority = 100)
{
_connector = connector;
_context = context;
_providerName = providerName ?? connector.DisplayName;
Priority = priority;
if (!connector.SupportsFeatureFlags)
{
throw new ArgumentException(
$"Connector '{connector.ConnectorType}' does not support native feature flags. " +
"Use ConfigurationFeatureFlagProvider with a convention-based approach instead.",
nameof(connector));
}
}
/// <inheritdoc />
public override string Name => _providerName;
/// <inheritdoc />
public override int Priority { get; }
/// <inheritdoc />
public override bool SupportsWatch => _connector.SupportsWatch;
/// <inheritdoc />
public override async Task<FeatureFlagResult?> TryGetFlagAsync(
string flagKey,
FeatureFlagEvaluationContext context,
CancellationToken ct = default)
{
// Convert our context to the connector's context format
var connectorContext = new FeatureFlagContext(
context.UserId,
context.TenantId,
context.Attributes?.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value?.ToString() ?? string.Empty));
var result = await _connector.GetFeatureFlagAsync(
_context,
flagKey,
connectorContext,
ct);
if (result is null)
{
return null;
}
return new FeatureFlagResult(
result.Key,
result.Enabled,
result.Variant,
result.EvaluationReason,
Name);
}
/// <inheritdoc />
public override async Task<IReadOnlyList<FeatureFlagDefinition>> ListFlagsAsync(
CancellationToken ct = default)
{
var flags = await _connector.ListFeatureFlagsAsync(_context, ct);
return flags.Select(f => new FeatureFlagDefinition(
f.Key,
f.Description,
f.DefaultValue,
f.DefaultValue,
null)).ToList();
}
/// <inheritdoc />
public override async IAsyncEnumerable<FeatureFlagChangedEvent> WatchAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
if (!_connector.SupportsWatch)
{
yield break;
}
var channel = System.Threading.Channels.Channel.CreateUnbounded<FeatureFlagChangedEvent>();
_changeCallback = evt => channel.Writer.TryWrite(evt);
// Initialize last values
try
{
var flags = await _connector.ListFeatureFlagsAsync(_context, ct);
foreach (var flag in flags)
{
_lastValues[flag.Key] = flag.DefaultValue;
}
}
catch
{
// Ignore initialization errors
}
// Start watching for changes in the background
_watchCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_watchTask = WatchSettingsAsync(_watchCts.Token);
try
{
await foreach (var evt in channel.Reader.ReadAllAsync(ct))
{
yield return evt;
}
}
finally
{
_changeCallback = null;
_watchCts?.Cancel();
channel.Writer.Complete();
}
}
private async Task WatchSettingsAsync(CancellationToken ct)
{
// Poll for flag changes since settings store watch is for KV, not flags
var pollInterval = TimeSpan.FromSeconds(30);
while (!ct.IsCancellationRequested)
{
try
{
await Task.Delay(pollInterval, ct);
var flags = await _connector.ListFeatureFlagsAsync(_context, ct);
var currentKeys = new HashSet<string>();
foreach (var flag in flags)
{
currentKeys.Add(flag.Key);
if (_lastValues.TryGetValue(flag.Key, out var oldValue))
{
if (oldValue != flag.DefaultValue)
{
_lastValues[flag.Key] = flag.DefaultValue;
NotifyChange(flag.Key, oldValue, flag.DefaultValue);
}
}
else
{
_lastValues[flag.Key] = flag.DefaultValue;
NotifyChange(flag.Key, false, flag.DefaultValue);
}
}
// Check for deleted flags
foreach (var key in _lastValues.Keys.Except(currentKeys).ToList())
{
var oldValue = _lastValues[key];
_lastValues.Remove(key);
NotifyChange(key, oldValue, false);
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
break;
}
catch
{
// Ignore errors and retry
await Task.Delay(TimeSpan.FromSeconds(5), ct);
}
}
}
private void NotifyChange(string key, bool oldValue, bool newValue)
{
var evt = new FeatureFlagChangedEvent(
key,
oldValue,
newValue,
Name,
DateTimeOffset.UtcNow);
_changeCallback?.Invoke(evt);
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
return;
_watchCts?.Cancel();
_watchCts?.Dispose();
try
{
_watchTask?.Wait(TimeSpan.FromSeconds(5));
}
catch
{
// Ignore
}
if (_connector is IDisposable disposable)
{
disposable.Dispose();
}
_disposed = true;
}
}