audit, advisories and doctors/setup work
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Models;
|
||||
|
||||
namespace StellaOps.Configuration.SettingsStore;
|
||||
|
||||
/// <summary>
|
||||
/// A no-op plugin logger for standalone configuration providers.
|
||||
/// </summary>
|
||||
internal sealed class NullPluginLogger : IPluginLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton instance of the null logger.
|
||||
/// </summary>
|
||||
public static readonly NullPluginLogger Instance = new();
|
||||
|
||||
private NullPluginLogger() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log(LogLevel level, string message, params object[] args) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log(LogLevel level, Exception exception, string message, params object[] args) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPluginLogger WithProperty(string name, object value) => this;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPluginLogger ForOperation(string operationName) => this;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel level) => false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A no-op secret resolver that returns null for all paths.
|
||||
/// Used when secrets are provided directly in configuration rather than via references.
|
||||
/// </summary>
|
||||
internal sealed class NullSecretResolver : ISecretResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton instance of the null secret resolver.
|
||||
/// </summary>
|
||||
public static readonly NullSecretResolver Instance = new();
|
||||
|
||||
private NullSecretResolver() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string?> ResolveAsync(string path, CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult<string?>(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyDictionary<string, string>> ResolveManyAsync(
|
||||
IEnumerable<string> paths,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyDictionary<string, string>>(
|
||||
new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A secret resolver that uses environment variables.
|
||||
/// Secret paths are treated as environment variable names.
|
||||
/// </summary>
|
||||
internal sealed class EnvironmentSecretResolver : ISecretResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton instance of the environment secret resolver.
|
||||
/// </summary>
|
||||
public static readonly EnvironmentSecretResolver Instance = new();
|
||||
|
||||
private EnvironmentSecretResolver() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string?> ResolveAsync(string path, CancellationToken ct = default)
|
||||
{
|
||||
// Remove common prefixes like "env://" or "$"
|
||||
var varName = path;
|
||||
if (varName.StartsWith("env://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
varName = varName[6..];
|
||||
}
|
||||
else if (varName.StartsWith("$", StringComparison.Ordinal))
|
||||
{
|
||||
varName = varName[1..];
|
||||
}
|
||||
|
||||
var value = Environment.GetEnvironmentVariable(varName);
|
||||
return Task.FromResult(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyDictionary<string, string>> ResolveManyAsync(
|
||||
IEnumerable<string> paths,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var results = new Dictionary<string, string>();
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var value = await ResolveAsync(path, ct);
|
||||
if (value is not null)
|
||||
{
|
||||
results[path] = value;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors.SettingsStore;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Models;
|
||||
|
||||
namespace StellaOps.Configuration.SettingsStore.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the AWS Parameter Store configuration provider.
|
||||
/// </summary>
|
||||
public sealed class AwsParameterStoreConfigurationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// AWS region (e.g., "us-east-1").
|
||||
/// </summary>
|
||||
public string Region { get; set; } = "us-east-1";
|
||||
|
||||
/// <summary>
|
||||
/// AWS access key ID (optional - can use instance profile).
|
||||
/// </summary>
|
||||
public string? AccessKeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AWS secret access key (optional - can use instance profile).
|
||||
/// </summary>
|
||||
public string? SecretAccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable name containing the secret access key.
|
||||
/// </summary>
|
||||
public string? SecretAccessKeyEnvironmentVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AWS session token for temporary credentials (optional).
|
||||
/// </summary>
|
||||
public string? SessionToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameter path prefix (e.g., "/myapp/config/").
|
||||
/// </summary>
|
||||
public string Path { get; set; } = "/";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to reload configuration when changes are detected.
|
||||
/// Note: AWS Parameter Store uses polling, not push notifications.
|
||||
/// </summary>
|
||||
public bool ReloadOnChange { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Polling interval for change detection.
|
||||
/// </summary>
|
||||
public TimeSpan ReloadInterval { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the configuration source is optional.
|
||||
/// </summary>
|
||||
public bool Optional { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional logger factory.
|
||||
/// </summary>
|
||||
public ILoggerFactory? LoggerFactory { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration source for AWS Parameter Store.
|
||||
/// </summary>
|
||||
public sealed class AwsParameterStoreConfigurationSource : SettingsStoreConfigurationSource
|
||||
{
|
||||
private readonly AwsParameterStoreConfigurationOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AWS Parameter Store configuration source.
|
||||
/// </summary>
|
||||
public AwsParameterStoreConfigurationSource(AwsParameterStoreConfigurationOptions options)
|
||||
{
|
||||
_options = options;
|
||||
Prefix = options.Path;
|
||||
ReloadOnChange = options.ReloadOnChange;
|
||||
ReloadInterval = options.ReloadInterval;
|
||||
Optional = options.Optional;
|
||||
LoggerFactory = options.LoggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ISettingsStoreConnectorCapability CreateConnector()
|
||||
{
|
||||
return new AwsParameterStoreConnector();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ConnectorContext CreateContext()
|
||||
{
|
||||
var configObj = new Dictionary<string, object?>
|
||||
{
|
||||
["region"] = _options.Region
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.AccessKeyId))
|
||||
{
|
||||
configObj["accessKeyId"] = _options.AccessKeyId;
|
||||
}
|
||||
|
||||
// Resolve secret access key
|
||||
var secretKey = _options.SecretAccessKey;
|
||||
if (string.IsNullOrEmpty(secretKey) && !string.IsNullOrEmpty(_options.SecretAccessKeyEnvironmentVariable))
|
||||
{
|
||||
secretKey = Environment.GetEnvironmentVariable(_options.SecretAccessKeyEnvironmentVariable);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
configObj["secretAccessKey"] = secretKey;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.SessionToken))
|
||||
{
|
||||
configObj["sessionToken"] = _options.SessionToken;
|
||||
}
|
||||
|
||||
var configJson = JsonSerializer.Serialize(configObj);
|
||||
var config = JsonDocument.Parse(configJson).RootElement;
|
||||
|
||||
return new ConnectorContext(
|
||||
Guid.Empty,
|
||||
Guid.Empty,
|
||||
config,
|
||||
NullSecretResolver.Instance,
|
||||
NullPluginLogger.Instance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
{
|
||||
return new AwsParameterStoreConfigurationProvider(this, CreateConnector(), CreateContext());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration provider that loads settings from AWS Parameter Store.
|
||||
/// </summary>
|
||||
public sealed class AwsParameterStoreConfigurationProvider : SettingsStoreConfigurationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new AWS Parameter Store configuration provider.
|
||||
/// </summary>
|
||||
public AwsParameterStoreConfigurationProvider(
|
||||
SettingsStoreConfigurationSource source,
|
||||
ISettingsStoreConnectorCapability connector,
|
||||
ConnectorContext context)
|
||||
: base(source, connector, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding AWS Parameter Store configuration.
|
||||
/// </summary>
|
||||
public static class AwsParameterStoreConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds AWS Parameter Store as a configuration source.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddAwsParameterStore(
|
||||
this IConfigurationBuilder builder,
|
||||
string region,
|
||||
string path = "/",
|
||||
bool reloadOnChange = false,
|
||||
Action<AwsParameterStoreConfigurationOptions>? configure = null)
|
||||
{
|
||||
var options = new AwsParameterStoreConfigurationOptions
|
||||
{
|
||||
Region = region,
|
||||
Path = path,
|
||||
ReloadOnChange = reloadOnChange
|
||||
};
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
return builder.Add(new AwsParameterStoreConfigurationSource(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds AWS Parameter Store as a configuration source using options.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddAwsParameterStore(
|
||||
this IConfigurationBuilder builder,
|
||||
Action<AwsParameterStoreConfigurationOptions> configure)
|
||||
{
|
||||
var options = new AwsParameterStoreConfigurationOptions();
|
||||
configure(options);
|
||||
|
||||
return builder.Add(new AwsParameterStoreConfigurationSource(options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors.SettingsStore;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Models;
|
||||
|
||||
namespace StellaOps.Configuration.SettingsStore.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the Azure App Configuration provider.
|
||||
/// </summary>
|
||||
public sealed class AzureAppConfigurationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Connection string for Azure App Configuration.
|
||||
/// Format: Endpoint=https://xxx.azconfig.io;Id=xxx;Secret=xxx
|
||||
/// </summary>
|
||||
public string? ConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable name containing the connection string.
|
||||
/// </summary>
|
||||
public string? ConnectionStringEnvironmentVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Azure App Configuration endpoint (alternative to connection string).
|
||||
/// </summary>
|
||||
public string? Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Credential ID for HMAC authentication (used with Endpoint).
|
||||
/// </summary>
|
||||
public string? Credential { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Secret for HMAC authentication (used with Endpoint).
|
||||
/// </summary>
|
||||
public string? Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable name containing the secret.
|
||||
/// </summary>
|
||||
public string? SecretEnvironmentVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Label to filter settings by (e.g., "production", "staging").
|
||||
/// </summary>
|
||||
public string? Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Key prefix to filter settings by.
|
||||
/// </summary>
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to reload configuration when changes are detected.
|
||||
/// </summary>
|
||||
public bool ReloadOnChange { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the configuration source is optional.
|
||||
/// </summary>
|
||||
public bool Optional { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional logger factory.
|
||||
/// </summary>
|
||||
public ILoggerFactory? LoggerFactory { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration source for Azure App Configuration.
|
||||
/// </summary>
|
||||
public sealed class AzureAppConfigurationSource : SettingsStoreConfigurationSource
|
||||
{
|
||||
private readonly AzureAppConfigurationOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Azure App Configuration source.
|
||||
/// </summary>
|
||||
public AzureAppConfigurationSource(AzureAppConfigurationOptions options)
|
||||
{
|
||||
_options = options;
|
||||
Prefix = options.Prefix;
|
||||
ReloadOnChange = options.ReloadOnChange;
|
||||
Optional = options.Optional;
|
||||
LoggerFactory = options.LoggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ISettingsStoreConnectorCapability CreateConnector()
|
||||
{
|
||||
return new AzureAppConfigConnector();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ConnectorContext CreateContext()
|
||||
{
|
||||
var configObj = new Dictionary<string, object?>();
|
||||
|
||||
// Resolve connection string
|
||||
var connectionString = _options.ConnectionString;
|
||||
if (string.IsNullOrEmpty(connectionString) && !string.IsNullOrEmpty(_options.ConnectionStringEnvironmentVariable))
|
||||
{
|
||||
connectionString = Environment.GetEnvironmentVariable(_options.ConnectionStringEnvironmentVariable);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
configObj["connectionString"] = connectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use endpoint + credential + secret
|
||||
if (!string.IsNullOrEmpty(_options.Endpoint))
|
||||
{
|
||||
configObj["endpoint"] = _options.Endpoint;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.Credential))
|
||||
{
|
||||
configObj["credential"] = _options.Credential;
|
||||
}
|
||||
|
||||
// Resolve secret
|
||||
var secret = _options.Secret;
|
||||
if (string.IsNullOrEmpty(secret) && !string.IsNullOrEmpty(_options.SecretEnvironmentVariable))
|
||||
{
|
||||
secret = Environment.GetEnvironmentVariable(_options.SecretEnvironmentVariable);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(secret))
|
||||
{
|
||||
configObj["secret"] = secret;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.Label))
|
||||
{
|
||||
configObj["label"] = _options.Label;
|
||||
}
|
||||
|
||||
var configJson = JsonSerializer.Serialize(configObj);
|
||||
var config = JsonDocument.Parse(configJson).RootElement;
|
||||
|
||||
return new ConnectorContext(
|
||||
Guid.Empty,
|
||||
Guid.Empty,
|
||||
config,
|
||||
NullSecretResolver.Instance,
|
||||
NullPluginLogger.Instance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
{
|
||||
return new AzureAppConfigurationProvider(this, CreateConnector(), CreateContext());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration provider that loads settings from Azure App Configuration.
|
||||
/// </summary>
|
||||
public sealed class AzureAppConfigurationProvider : SettingsStoreConfigurationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new Azure App Configuration provider.
|
||||
/// </summary>
|
||||
public AzureAppConfigurationProvider(
|
||||
SettingsStoreConfigurationSource source,
|
||||
ISettingsStoreConnectorCapability connector,
|
||||
ConnectorContext context)
|
||||
: base(source, connector, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding Azure App Configuration.
|
||||
/// </summary>
|
||||
public static class AzureAppConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Azure App Configuration as a configuration source using a connection string.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddAzureAppConfiguration(
|
||||
this IConfigurationBuilder builder,
|
||||
string connectionString,
|
||||
string? label = null,
|
||||
bool reloadOnChange = true,
|
||||
Action<AzureAppConfigurationOptions>? configure = null)
|
||||
{
|
||||
var options = new AzureAppConfigurationOptions
|
||||
{
|
||||
ConnectionString = connectionString,
|
||||
Label = label,
|
||||
ReloadOnChange = reloadOnChange
|
||||
};
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
return builder.Add(new AzureAppConfigurationSource(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Azure App Configuration as a configuration source using options.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddAzureAppConfiguration(
|
||||
this IConfigurationBuilder builder,
|
||||
Action<AzureAppConfigurationOptions> configure)
|
||||
{
|
||||
var options = new AzureAppConfigurationOptions();
|
||||
configure(options);
|
||||
|
||||
return builder.Add(new AzureAppConfigurationSource(options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors.SettingsStore;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Models;
|
||||
|
||||
namespace StellaOps.Configuration.SettingsStore.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the Consul KV configuration provider.
|
||||
/// </summary>
|
||||
public sealed class ConsulConfigurationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Consul server address (e.g., "http://localhost:8500").
|
||||
/// </summary>
|
||||
public string Address { get; set; } = "http://localhost:8500";
|
||||
|
||||
/// <summary>
|
||||
/// ACL token for authentication (optional).
|
||||
/// </summary>
|
||||
public string? Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable name containing the ACL token.
|
||||
/// </summary>
|
||||
public string? TokenEnvironmentVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Key prefix to filter settings by (e.g., "myapp/config/").
|
||||
/// </summary>
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to reload configuration when changes are detected.
|
||||
/// </summary>
|
||||
public bool ReloadOnChange { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the configuration source is optional.
|
||||
/// </summary>
|
||||
public bool Optional { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional logger factory.
|
||||
/// </summary>
|
||||
public ILoggerFactory? LoggerFactory { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration source for Consul KV.
|
||||
/// </summary>
|
||||
public sealed class ConsulConfigurationSource : SettingsStoreConfigurationSource
|
||||
{
|
||||
private readonly ConsulConfigurationOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Consul configuration source.
|
||||
/// </summary>
|
||||
public ConsulConfigurationSource(ConsulConfigurationOptions options)
|
||||
{
|
||||
_options = options;
|
||||
Prefix = options.Prefix;
|
||||
ReloadOnChange = options.ReloadOnChange;
|
||||
Optional = options.Optional;
|
||||
LoggerFactory = options.LoggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ISettingsStoreConnectorCapability CreateConnector()
|
||||
{
|
||||
return new ConsulKvConnector();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ConnectorContext CreateContext()
|
||||
{
|
||||
var configObj = new Dictionary<string, object?>
|
||||
{
|
||||
["address"] = _options.Address
|
||||
};
|
||||
|
||||
// Resolve token
|
||||
var token = _options.Token;
|
||||
if (string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(_options.TokenEnvironmentVariable))
|
||||
{
|
||||
token = Environment.GetEnvironmentVariable(_options.TokenEnvironmentVariable);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
configObj["token"] = token;
|
||||
}
|
||||
|
||||
var configJson = JsonSerializer.Serialize(configObj);
|
||||
var config = JsonDocument.Parse(configJson).RootElement;
|
||||
|
||||
return new ConnectorContext(
|
||||
Guid.Empty,
|
||||
Guid.Empty,
|
||||
config,
|
||||
NullSecretResolver.Instance,
|
||||
NullPluginLogger.Instance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
{
|
||||
return new ConsulConfigurationProvider(this, CreateConnector(), CreateContext());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration provider that loads settings from Consul KV.
|
||||
/// </summary>
|
||||
public sealed class ConsulConfigurationProvider : SettingsStoreConfigurationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new Consul configuration provider.
|
||||
/// </summary>
|
||||
public ConsulConfigurationProvider(
|
||||
SettingsStoreConfigurationSource source,
|
||||
ISettingsStoreConnectorCapability connector,
|
||||
ConnectorContext context)
|
||||
: base(source, connector, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding Consul configuration.
|
||||
/// </summary>
|
||||
public static class ConsulConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Consul KV as a configuration source.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddConsulKv(
|
||||
this IConfigurationBuilder builder,
|
||||
string address,
|
||||
string prefix = "",
|
||||
bool reloadOnChange = true,
|
||||
Action<ConsulConfigurationOptions>? configure = null)
|
||||
{
|
||||
var options = new ConsulConfigurationOptions
|
||||
{
|
||||
Address = address,
|
||||
Prefix = prefix,
|
||||
ReloadOnChange = reloadOnChange
|
||||
};
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
return builder.Add(new ConsulConfigurationSource(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Consul KV as a configuration source using options.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddConsulKv(
|
||||
this IConfigurationBuilder builder,
|
||||
Action<ConsulConfigurationOptions> configure)
|
||||
{
|
||||
var options = new ConsulConfigurationOptions();
|
||||
configure(options);
|
||||
|
||||
return builder.Add(new ConsulConfigurationSource(options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors.SettingsStore;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Models;
|
||||
|
||||
namespace StellaOps.Configuration.SettingsStore.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the etcd configuration provider.
|
||||
/// </summary>
|
||||
public sealed class EtcdConfigurationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Single etcd server address (e.g., "http://localhost:2379").
|
||||
/// Use either Address or Endpoints, not both.
|
||||
/// </summary>
|
||||
public string? Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiple etcd server endpoints for cluster configuration.
|
||||
/// </summary>
|
||||
public string[]? Endpoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Username for basic authentication (optional).
|
||||
/// </summary>
|
||||
public string? Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Password for basic authentication (optional).
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable name containing the password.
|
||||
/// </summary>
|
||||
public string? PasswordEnvironmentVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Key prefix to filter settings by (e.g., "/myapp/config/").
|
||||
/// </summary>
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to reload configuration when changes are detected.
|
||||
/// </summary>
|
||||
public bool ReloadOnChange { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the configuration source is optional.
|
||||
/// </summary>
|
||||
public bool Optional { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional logger factory.
|
||||
/// </summary>
|
||||
public ILoggerFactory? LoggerFactory { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration source for etcd.
|
||||
/// </summary>
|
||||
public sealed class EtcdConfigurationSource : SettingsStoreConfigurationSource
|
||||
{
|
||||
private readonly EtcdConfigurationOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new etcd configuration source.
|
||||
/// </summary>
|
||||
public EtcdConfigurationSource(EtcdConfigurationOptions options)
|
||||
{
|
||||
_options = options;
|
||||
Prefix = options.Prefix;
|
||||
ReloadOnChange = options.ReloadOnChange;
|
||||
Optional = options.Optional;
|
||||
LoggerFactory = options.LoggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ISettingsStoreConnectorCapability CreateConnector()
|
||||
{
|
||||
return new EtcdConnector();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ConnectorContext CreateContext()
|
||||
{
|
||||
var configObj = new Dictionary<string, object?>();
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.Address))
|
||||
{
|
||||
configObj["address"] = _options.Address;
|
||||
}
|
||||
else if (_options.Endpoints is { Length: > 0 })
|
||||
{
|
||||
configObj["endpoints"] = _options.Endpoints;
|
||||
}
|
||||
else
|
||||
{
|
||||
configObj["address"] = "http://localhost:2379";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.Username))
|
||||
{
|
||||
configObj["username"] = _options.Username;
|
||||
}
|
||||
|
||||
// Resolve password
|
||||
var password = _options.Password;
|
||||
if (string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(_options.PasswordEnvironmentVariable))
|
||||
{
|
||||
password = Environment.GetEnvironmentVariable(_options.PasswordEnvironmentVariable);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
configObj["password"] = password;
|
||||
}
|
||||
|
||||
var configJson = JsonSerializer.Serialize(configObj);
|
||||
var config = JsonDocument.Parse(configJson).RootElement;
|
||||
|
||||
return new ConnectorContext(
|
||||
Guid.Empty,
|
||||
Guid.Empty,
|
||||
config,
|
||||
NullSecretResolver.Instance,
|
||||
NullPluginLogger.Instance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
{
|
||||
return new EtcdConfigurationProvider(this, CreateConnector(), CreateContext());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration provider that loads settings from etcd.
|
||||
/// </summary>
|
||||
public sealed class EtcdConfigurationProvider : SettingsStoreConfigurationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new etcd configuration provider.
|
||||
/// </summary>
|
||||
public EtcdConfigurationProvider(
|
||||
SettingsStoreConfigurationSource source,
|
||||
ISettingsStoreConnectorCapability connector,
|
||||
ConnectorContext context)
|
||||
: base(source, connector, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding etcd configuration.
|
||||
/// </summary>
|
||||
public static class EtcdConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds etcd as a configuration source.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddEtcd(
|
||||
this IConfigurationBuilder builder,
|
||||
string address,
|
||||
string prefix = "",
|
||||
bool reloadOnChange = true,
|
||||
Action<EtcdConfigurationOptions>? configure = null)
|
||||
{
|
||||
var options = new EtcdConfigurationOptions
|
||||
{
|
||||
Address = address,
|
||||
Prefix = prefix,
|
||||
ReloadOnChange = reloadOnChange
|
||||
};
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
return builder.Add(new EtcdConfigurationSource(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds etcd as a configuration source with multiple endpoints.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddEtcd(
|
||||
this IConfigurationBuilder builder,
|
||||
string[] endpoints,
|
||||
string prefix = "",
|
||||
bool reloadOnChange = true,
|
||||
Action<EtcdConfigurationOptions>? configure = null)
|
||||
{
|
||||
var options = new EtcdConfigurationOptions
|
||||
{
|
||||
Endpoints = endpoints,
|
||||
Prefix = prefix,
|
||||
ReloadOnChange = reloadOnChange
|
||||
};
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
return builder.Add(new EtcdConfigurationSource(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds etcd as a configuration source using options.
|
||||
/// </summary>
|
||||
public static IConfigurationBuilder AddEtcd(
|
||||
this IConfigurationBuilder builder,
|
||||
Action<EtcdConfigurationOptions> configure)
|
||||
{
|
||||
var options = new EtcdConfigurationOptions();
|
||||
configure(options);
|
||||
|
||||
return builder.Add(new EtcdConfigurationSource(options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
|
||||
using StellaOps.ReleaseOrchestrator.Plugin.Models;
|
||||
|
||||
namespace StellaOps.Configuration.SettingsStore;
|
||||
|
||||
/// <summary>
|
||||
/// Base configuration source for settings store providers.
|
||||
/// </summary>
|
||||
public abstract class SettingsStoreConfigurationSource : IConfigurationSource
|
||||
{
|
||||
/// <summary>
|
||||
/// The prefix to filter settings by (e.g., "myapp/").
|
||||
/// </summary>
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to reload configuration when changes are detected.
|
||||
/// </summary>
|
||||
public bool ReloadOnChange { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Interval between polling for changes (when connector doesn't support native watch).
|
||||
/// </summary>
|
||||
public TimeSpan ReloadInterval { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Optional logger factory for diagnostics.
|
||||
/// </summary>
|
||||
public ILoggerFactory? LoggerFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the configuration source is optional (won't throw if unavailable).
|
||||
/// </summary>
|
||||
public bool Optional { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Key separator to use when normalizing keys for .NET configuration.
|
||||
/// Settings store keys like "app/db/connection" become "app:db:connection".
|
||||
/// </summary>
|
||||
public char KeySeparator { get; set; } = '/';
|
||||
|
||||
/// <summary>
|
||||
/// Creates the settings store connector for this source.
|
||||
/// </summary>
|
||||
protected abstract ISettingsStoreConnectorCapability CreateConnector();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the connector context with configuration and secret resolver.
|
||||
/// </summary>
|
||||
protected abstract ConnectorContext CreateContext();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IConfigurationProvider Build(IConfigurationBuilder builder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base configuration provider for settings stores with hot-reload support.
|
||||
/// </summary>
|
||||
public abstract class SettingsStoreConfigurationProvider : ConfigurationProvider, IDisposable
|
||||
{
|
||||
private readonly SettingsStoreConfigurationSource _source;
|
||||
private readonly ISettingsStoreConnectorCapability _connector;
|
||||
private readonly ConnectorContext _context;
|
||||
private readonly ILogger? _logger;
|
||||
private CancellationTokenSource? _watchCts;
|
||||
private Task? _watchTask;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new settings store configuration provider.
|
||||
/// </summary>
|
||||
protected SettingsStoreConfigurationProvider(
|
||||
SettingsStoreConfigurationSource source,
|
||||
ISettingsStoreConnectorCapability connector,
|
||||
ConnectorContext context)
|
||||
{
|
||||
_source = source;
|
||||
_connector = connector;
|
||||
_context = context;
|
||||
_logger = source.LoggerFactory?.CreateLogger(GetType());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
if (_source.ReloadOnChange && _connector.SupportsWatch)
|
||||
{
|
||||
StartWatching();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_source.Optional)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to load configuration from {_connector.DisplayName}: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
_logger?.LogWarning(ex, "Optional configuration source {ConnectorType} failed to load",
|
||||
_connector.ConnectorType);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync(CancellationToken ct)
|
||||
{
|
||||
var settings = await _connector.GetSettingsAsync(_context, _source.Prefix, ct);
|
||||
|
||||
var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var setting in settings)
|
||||
{
|
||||
var key = NormalizeKey(setting.Key);
|
||||
data[key] = setting.Value;
|
||||
}
|
||||
|
||||
Data = data;
|
||||
|
||||
_logger?.LogDebug("Loaded {Count} settings from {ConnectorType} with prefix '{Prefix}'",
|
||||
settings.Count, _connector.ConnectorType, _source.Prefix);
|
||||
}
|
||||
|
||||
private void StartWatching()
|
||||
{
|
||||
_watchCts = new CancellationTokenSource();
|
||||
_watchTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var change in _connector.WatchAsync(_context, _source.Prefix, _watchCts.Token))
|
||||
{
|
||||
HandleChange(change);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_watchCts.Token.IsCancellationRequested)
|
||||
{
|
||||
// Expected during shutdown
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error watching {ConnectorType} for changes", _connector.ConnectorType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleChange(SettingChange change)
|
||||
{
|
||||
var key = NormalizeKey(change.Key);
|
||||
|
||||
switch (change.ChangeType)
|
||||
{
|
||||
case SettingChangeType.Created:
|
||||
case SettingChangeType.Updated:
|
||||
if (change.NewValue is not null)
|
||||
{
|
||||
Data[key] = change.NewValue.Value;
|
||||
_logger?.LogDebug("Configuration key '{Key}' {ChangeType}", key, change.ChangeType);
|
||||
}
|
||||
break;
|
||||
|
||||
case SettingChangeType.Deleted:
|
||||
Data.Remove(key);
|
||||
_logger?.LogDebug("Configuration key '{Key}' deleted", key);
|
||||
break;
|
||||
}
|
||||
|
||||
OnReload();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a settings store key to .NET configuration format.
|
||||
/// Replaces the key separator with colons and removes the prefix.
|
||||
/// </summary>
|
||||
protected string NormalizeKey(string key)
|
||||
{
|
||||
// Remove prefix if present
|
||||
if (!string.IsNullOrEmpty(_source.Prefix) && key.StartsWith(_source.Prefix, StringComparison.Ordinal))
|
||||
{
|
||||
key = key[_source.Prefix.Length..];
|
||||
}
|
||||
|
||||
// Trim leading separator
|
||||
key = key.TrimStart(_source.KeySeparator);
|
||||
|
||||
// Replace separator with colon (standard .NET configuration separator)
|
||||
return key.Replace(_source.KeySeparator, ':');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources used by the provider.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_watchCts?.Cancel();
|
||||
_watchCts?.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
_watchTask?.Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
// Ignore cancellation exceptions
|
||||
}
|
||||
|
||||
if (_connector is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Configuration.SettingsStore</RootNamespace>
|
||||
<Description>Configuration providers for settings stores (Consul, etcd, Azure App Config, AWS Parameter Store)</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ReleaseOrchestrator\__Libraries\StellaOps.ReleaseOrchestrator.Plugin\StellaOps.ReleaseOrchestrator.Plugin.csproj" />
|
||||
<ProjectReference Include="..\..\ReleaseOrchestrator\__Libraries\StellaOps.ReleaseOrchestrator.IntegrationHub\StellaOps.ReleaseOrchestrator.IntegrationHub.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user