Add integration tests for migration categories and execution
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
- Implemented MigrationCategoryTests to validate migration categorization for startup, release, seed, and data migrations. - Added tests for edge cases, including null, empty, and whitespace migration names. - Created StartupMigrationHostTests to verify the behavior of the migration host with real PostgreSQL instances using Testcontainers. - Included tests for migration execution, schema creation, and handling of pending release migrations. - Added SQL migration files for testing: creating a test table, adding a column, a release migration, and seeding data.
This commit is contained in:
754
docs/router/25-Step.md
Normal file
754
docs/router/25-Step.md
Normal file
@@ -0,0 +1,754 @@
|
||||
# Step 25: Configuration Hot-Reload
|
||||
|
||||
**Phase 7: Testing & Documentation**
|
||||
**Estimated Complexity:** Medium
|
||||
**Dependencies:** All previous configuration steps
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Configuration hot-reload enables dynamic updates to router and microservice configuration without restarts. This includes route definitions, rate limits, circuit breaker settings, and JWKS rotation.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. Support YAML configuration hot-reload
|
||||
2. Implement file watcher for configuration changes
|
||||
3. Provide atomic configuration updates
|
||||
4. Support validation before applying changes
|
||||
5. Enable rollback on invalid configuration
|
||||
|
||||
---
|
||||
|
||||
## Configuration Watcher
|
||||
|
||||
```csharp
|
||||
namespace StellaOps.Router.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Watches configuration files for changes and triggers reloads.
|
||||
/// </summary>
|
||||
public sealed class ConfigurationWatcher : IHostedService, IDisposable
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IOptionsMonitor<RouterConfig> _routerConfig;
|
||||
private readonly ILogger<ConfigurationWatcher> _logger;
|
||||
private readonly List<FileSystemWatcher> _watchers = new();
|
||||
private readonly Subject<ConfigurationChange> _changes = new();
|
||||
private readonly TimeSpan _debounceInterval = TimeSpan.FromMilliseconds(500);
|
||||
private readonly ConcurrentDictionary<string, DateTimeOffset> _lastChange = new();
|
||||
|
||||
public IObservable<ConfigurationChange> Changes => _changes;
|
||||
|
||||
public ConfigurationWatcher(
|
||||
IConfiguration configuration,
|
||||
IOptionsMonitor<RouterConfig> routerConfig,
|
||||
ILogger<ConfigurationWatcher> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_routerConfig = routerConfig;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Watch all YAML configuration files
|
||||
var configPaths = GetConfigurationFilePaths();
|
||||
|
||||
foreach (var path in configPaths)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
continue;
|
||||
|
||||
var directory = Path.GetDirectoryName(path)!;
|
||||
var fileName = Path.GetFileName(path);
|
||||
|
||||
var watcher = new FileSystemWatcher(directory)
|
||||
{
|
||||
Filter = fileName,
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
watcher.Changed += OnConfigurationFileChanged;
|
||||
_watchers.Add(watcher);
|
||||
|
||||
_logger.LogInformation("Watching configuration file: {Path}", path);
|
||||
}
|
||||
|
||||
// Also subscribe to IOptionsMonitor for programmatic changes
|
||||
_routerConfig.OnChange(config =>
|
||||
{
|
||||
_changes.OnNext(new ConfigurationChange
|
||||
{
|
||||
Section = "Router",
|
||||
ChangeType = ChangeType.Modified,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
});
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnConfigurationFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
// Debounce rapid changes
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
if (_lastChange.TryGetValue(e.FullPath, out var lastChange) &&
|
||||
now - lastChange < _debounceInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastChange[e.FullPath] = now;
|
||||
|
||||
_logger.LogInformation("Configuration file changed: {Path}", e.FullPath);
|
||||
|
||||
// Delay to allow file writes to complete
|
||||
Task.Delay(100).ContinueWith(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate configuration before notifying
|
||||
if (ValidateConfiguration(e.FullPath))
|
||||
{
|
||||
_changes.OnNext(new ConfigurationChange
|
||||
{
|
||||
Section = DetermineSectionFromPath(e.FullPath),
|
||||
ChangeType = ChangeType.Modified,
|
||||
FilePath = e.FullPath,
|
||||
Timestamp = now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to process configuration change for {Path}", e.FullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool ValidateConfiguration(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var yaml = File.ReadAllText(path);
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
// Try to deserialize to validate YAML syntax
|
||||
var doc = deserializer.Deserialize<Dictionary<string, object>>(yaml);
|
||||
return doc != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid configuration file: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string DetermineSectionFromPath(string path)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(path).ToLower();
|
||||
|
||||
return fileName switch
|
||||
{
|
||||
"router" => "Router",
|
||||
"routes" => "Routes",
|
||||
"ratelimits" => "RateLimits",
|
||||
"endpoints" => "Endpoints",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetConfigurationFilePaths()
|
||||
{
|
||||
// Get paths from configuration providers
|
||||
var paths = new List<string>();
|
||||
|
||||
if (_configuration is IConfigurationRoot root)
|
||||
{
|
||||
foreach (var provider in root.Providers)
|
||||
{
|
||||
if (provider is FileConfigurationProvider fileProvider)
|
||||
{
|
||||
var source = fileProvider.Source;
|
||||
if (source.FileProvider?.GetFileInfo(source.Path ?? "") is { Exists: true } fileInfo)
|
||||
{
|
||||
paths.Add(fileInfo.PhysicalPath ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths.Where(p => !string.IsNullOrEmpty(p));
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var watcher in _watchers)
|
||||
{
|
||||
watcher.EnableRaisingEvents = false;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var watcher in _watchers)
|
||||
{
|
||||
watcher.Dispose();
|
||||
}
|
||||
_changes.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ConfigurationChange
|
||||
{
|
||||
public string Section { get; init; } = "";
|
||||
public ChangeType ChangeType { get; init; }
|
||||
public string? FilePath { get; init; }
|
||||
public DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
public enum ChangeType
|
||||
{
|
||||
Added,
|
||||
Modified,
|
||||
Removed
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route Configuration Reloader
|
||||
|
||||
```csharp
|
||||
namespace StellaOps.Router.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Handles hot-reload of route configurations.
|
||||
/// </summary>
|
||||
public sealed class RouteConfigurationReloader : IHostedService
|
||||
{
|
||||
private readonly ConfigurationWatcher _watcher;
|
||||
private readonly IRouteRegistry _routeRegistry;
|
||||
private readonly ILogger<RouteConfigurationReloader> _logger;
|
||||
private IDisposable? _subscription;
|
||||
|
||||
public RouteConfigurationReloader(
|
||||
ConfigurationWatcher watcher,
|
||||
IRouteRegistry routeRegistry,
|
||||
ILogger<RouteConfigurationReloader> logger)
|
||||
{
|
||||
_watcher = watcher;
|
||||
_routeRegistry = routeRegistry;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_subscription = _watcher.Changes
|
||||
.Where(c => c.Section == "Routes")
|
||||
.Subscribe(OnRoutesChanged);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRoutesChanged(ConfigurationChange change)
|
||||
{
|
||||
_logger.LogInformation("Reloading routes from {Path}", change.FilePath);
|
||||
|
||||
try
|
||||
{
|
||||
_routeRegistry.Reload();
|
||||
_logger.LogInformation("Routes reloaded successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to reload routes, keeping previous configuration");
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limit Configuration Reloader
|
||||
|
||||
```csharp
|
||||
namespace StellaOps.Router.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Handles hot-reload of rate limit configurations.
|
||||
/// </summary>
|
||||
public sealed class RateLimitConfigurationReloader : IHostedService
|
||||
{
|
||||
private readonly ConfigurationWatcher _watcher;
|
||||
private readonly IRateLimiter _rateLimiter;
|
||||
private readonly IOptionsMonitor<RateLimitConfig> _config;
|
||||
private readonly ILogger<RateLimitConfigurationReloader> _logger;
|
||||
private IDisposable? _subscription;
|
||||
|
||||
public RateLimitConfigurationReloader(
|
||||
ConfigurationWatcher watcher,
|
||||
IRateLimiter rateLimiter,
|
||||
IOptionsMonitor<RateLimitConfig> config,
|
||||
ILogger<RateLimitConfigurationReloader> logger)
|
||||
{
|
||||
_watcher = watcher;
|
||||
_rateLimiter = rateLimiter;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_subscription = _watcher.Changes
|
||||
.Where(c => c.Section == "RateLimits")
|
||||
.Subscribe(OnRateLimitsChanged);
|
||||
|
||||
_config.OnChange(OnRateLimitConfigChanged);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRateLimitsChanged(ConfigurationChange change)
|
||||
{
|
||||
_logger.LogInformation("Rate limit configuration changed, applying updates");
|
||||
ApplyRateLimitChanges();
|
||||
}
|
||||
|
||||
private void OnRateLimitConfigChanged(RateLimitConfig config)
|
||||
{
|
||||
_logger.LogInformation("Rate limit options changed, applying updates");
|
||||
ApplyRateLimitChanges();
|
||||
}
|
||||
|
||||
private void ApplyRateLimitChanges()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Rate limiter will pick up new config from IOptionsMonitor
|
||||
// Clear any cached tier information
|
||||
if (_rateLimiter is ICacheableRateLimiter cacheable)
|
||||
{
|
||||
cacheable.ClearCache();
|
||||
}
|
||||
|
||||
_logger.LogInformation("Rate limit configuration applied successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to apply rate limit changes");
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICacheableRateLimiter
|
||||
{
|
||||
void ClearCache();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JWKS Hot-Reload
|
||||
|
||||
```csharp
|
||||
namespace StellaOps.Router.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Handles JWKS rotation and cache refresh.
|
||||
/// </summary>
|
||||
public sealed class JwksReloader : IHostedService
|
||||
{
|
||||
private readonly IJwksCache _jwksCache;
|
||||
private readonly JwtAuthenticationConfig _config;
|
||||
private readonly ILogger<JwksReloader> _logger;
|
||||
private Timer? _refreshTimer;
|
||||
|
||||
public JwksReloader(
|
||||
IJwksCache jwksCache,
|
||||
IOptions<JwtAuthenticationConfig> config,
|
||||
ILogger<JwksReloader> logger)
|
||||
{
|
||||
_jwksCache = jwksCache;
|
||||
_config = config.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Periodic refresh of JWKS
|
||||
var interval = _config.JwksRefreshInterval;
|
||||
|
||||
_refreshTimer = new Timer(
|
||||
RefreshJwks,
|
||||
null,
|
||||
interval,
|
||||
interval);
|
||||
|
||||
_logger.LogInformation(
|
||||
"JWKS refresh scheduled every {Interval}",
|
||||
interval);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void RefreshJwks(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Refreshing JWKS cache");
|
||||
|
||||
await _jwksCache.RefreshAsync(CancellationToken.None);
|
||||
|
||||
_logger.LogDebug("JWKS cache refreshed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to refresh JWKS cache, will retry");
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
```csharp
|
||||
namespace StellaOps.Router.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Validates configuration before applying changes.
|
||||
/// </summary>
|
||||
public interface IConfigurationValidator
|
||||
{
|
||||
ValidationResult Validate<T>(T config) where T : class;
|
||||
}
|
||||
|
||||
public sealed class ConfigurationValidator : IConfigurationValidator
|
||||
{
|
||||
private readonly ILogger<ConfigurationValidator> _logger;
|
||||
|
||||
public ConfigurationValidator(ILogger<ConfigurationValidator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ValidationResult Validate<T>(T config) where T : class
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Use data annotations validation
|
||||
var context = new ValidationContext(config);
|
||||
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
|
||||
|
||||
if (!Validator.TryValidateObject(config, context, results, validateAllProperties: true))
|
||||
{
|
||||
errors.AddRange(results.Select(r => r.ErrorMessage ?? "Unknown validation error"));
|
||||
}
|
||||
|
||||
// Type-specific validation
|
||||
errors.AddRange(config switch
|
||||
{
|
||||
RouterConfig router => ValidateRouterConfig(router),
|
||||
RateLimitConfig rateLimit => ValidateRateLimitConfig(rateLimit),
|
||||
_ => Enumerable.Empty<string>()
|
||||
});
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Configuration validation failed: {Errors}",
|
||||
string.Join(", ", errors));
|
||||
}
|
||||
|
||||
return new ValidationResult
|
||||
{
|
||||
IsValid = !errors.Any(),
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<string> ValidateRouterConfig(RouterConfig config)
|
||||
{
|
||||
if (config.MaxPayloadSize <= 0)
|
||||
yield return "MaxPayloadSize must be positive";
|
||||
|
||||
if (config.RequestTimeout <= TimeSpan.Zero)
|
||||
yield return "RequestTimeout must be positive";
|
||||
}
|
||||
|
||||
private IEnumerable<string> ValidateRateLimitConfig(RateLimitConfig config)
|
||||
{
|
||||
foreach (var (tier, limits) in config.Tiers)
|
||||
{
|
||||
if (limits.RequestsPerMinute <= 0)
|
||||
yield return $"Tier {tier}: RequestsPerMinute must be positive";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ValidationResult
|
||||
{
|
||||
public bool IsValid { get; init; }
|
||||
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Atomic Configuration Update
|
||||
|
||||
```csharp
|
||||
namespace StellaOps.Router.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides atomic configuration updates with rollback support.
|
||||
/// </summary>
|
||||
public sealed class AtomicConfigurationUpdater
|
||||
{
|
||||
private readonly IConfigurationValidator _validator;
|
||||
private readonly ILogger<AtomicConfigurationUpdater> _logger;
|
||||
private readonly ReaderWriterLockSlim _lock = new();
|
||||
|
||||
public AtomicConfigurationUpdater(
|
||||
IConfigurationValidator validator,
|
||||
ILogger<AtomicConfigurationUpdater> logger)
|
||||
{
|
||||
_validator = validator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Atomically updates configuration with validation and rollback.
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateAsync<T>(
|
||||
T currentConfig,
|
||||
T newConfig,
|
||||
Func<T, Task> applyAction,
|
||||
Func<T, Task>? rollbackAction = null)
|
||||
where T : class
|
||||
{
|
||||
// Validate new configuration
|
||||
var validation = _validator.Validate(newConfig);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Configuration update rejected: {Errors}",
|
||||
string.Join(", ", validation.Errors));
|
||||
return false;
|
||||
}
|
||||
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
// Store current config for rollback
|
||||
var backup = currentConfig;
|
||||
|
||||
try
|
||||
{
|
||||
await applyAction(newConfig);
|
||||
_logger.LogInformation("Configuration updated successfully");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Configuration update failed, rolling back");
|
||||
|
||||
if (rollbackAction != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await rollbackAction(backup);
|
||||
_logger.LogInformation("Configuration rolled back successfully");
|
||||
}
|
||||
catch (Exception rollbackEx)
|
||||
{
|
||||
_logger.LogError(rollbackEx, "Rollback failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration API Endpoints
|
||||
|
||||
```csharp
|
||||
namespace StellaOps.Router.Gateway;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for configuration management.
|
||||
/// </summary>
|
||||
public static class ConfigurationEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapConfigurationEndpoints(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
string basePath = "/api/config")
|
||||
{
|
||||
var group = endpoints.MapGroup(basePath)
|
||||
.RequireAuthorization("admin");
|
||||
|
||||
group.MapGet("/", GetConfiguration);
|
||||
group.MapGet("/{section}", GetConfigurationSection);
|
||||
group.MapPost("/reload", ReloadConfiguration);
|
||||
group.MapPost("/validate", ValidateConfiguration);
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetConfiguration(
|
||||
IConfiguration configuration)
|
||||
{
|
||||
var sections = new Dictionary<string, object>();
|
||||
|
||||
foreach (var child in configuration.GetChildren())
|
||||
{
|
||||
sections[child.Key] = GetSectionValue(child);
|
||||
}
|
||||
|
||||
return Results.Ok(sections);
|
||||
}
|
||||
|
||||
private static object GetSectionValue(IConfigurationSection section)
|
||||
{
|
||||
var children = section.GetChildren().ToList();
|
||||
|
||||
if (!children.Any())
|
||||
{
|
||||
return section.Value ?? "";
|
||||
}
|
||||
|
||||
if (children.All(c => int.TryParse(c.Key, out _)))
|
||||
{
|
||||
// Array
|
||||
return children.Select(c => GetSectionValue(c)).ToList();
|
||||
}
|
||||
|
||||
// Object
|
||||
return children.ToDictionary(c => c.Key, c => GetSectionValue(c));
|
||||
}
|
||||
|
||||
private static IResult GetConfigurationSection(
|
||||
string section,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
var configSection = configuration.GetSection(section);
|
||||
|
||||
if (!configSection.Exists())
|
||||
{
|
||||
return Results.NotFound(new { error = $"Section '{section}' not found" });
|
||||
}
|
||||
|
||||
return Results.Ok(GetSectionValue(configSection));
|
||||
}
|
||||
|
||||
private static async Task<IResult> ReloadConfiguration(
|
||||
ConfigurationWatcher watcher,
|
||||
ILogger<ConfigurationWatcher> logger)
|
||||
{
|
||||
logger.LogInformation("Manual configuration reload triggered");
|
||||
|
||||
// Trigger reload notification
|
||||
// In practice, would re-read configuration files
|
||||
|
||||
return Results.Ok(new { message = "Configuration reload triggered" });
|
||||
}
|
||||
|
||||
private static async Task<IResult> ValidateConfiguration(
|
||||
HttpRequest request,
|
||||
IConfigurationValidator validator)
|
||||
{
|
||||
var body = await request.ReadFromJsonAsync<Dictionary<string, object>>();
|
||||
|
||||
if (body == null)
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid request body" });
|
||||
}
|
||||
|
||||
// Basic syntax validation
|
||||
return Results.Ok(new { valid = true });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## YAML Configuration
|
||||
|
||||
```yaml
|
||||
Configuration:
|
||||
# Enable hot-reload
|
||||
HotReload:
|
||||
Enabled: true
|
||||
DebounceInterval: "00:00:00.500"
|
||||
ValidateBeforeApply: true
|
||||
|
||||
# Files to watch
|
||||
WatchPaths:
|
||||
- "/etc/stellaops/router.yaml"
|
||||
- "/etc/stellaops/routes.yaml"
|
||||
- "/etc/stellaops/ratelimits.yaml"
|
||||
|
||||
# JWKS refresh settings
|
||||
Jwks:
|
||||
RefreshInterval: "00:05:00"
|
||||
RefreshOnError: true
|
||||
MaxRetries: 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. `StellaOps.Router.Configuration/ConfigurationWatcher.cs`
|
||||
2. `StellaOps.Router.Configuration/RouteConfigurationReloader.cs`
|
||||
3. `StellaOps.Router.Configuration/RateLimitConfigurationReloader.cs`
|
||||
4. `StellaOps.Router.Configuration/JwksReloader.cs`
|
||||
5. `StellaOps.Router.Configuration/IConfigurationValidator.cs`
|
||||
6. `StellaOps.Router.Configuration/ConfigurationValidator.cs`
|
||||
7. `StellaOps.Router.Configuration/AtomicConfigurationUpdater.cs`
|
||||
8. `StellaOps.Router.Gateway/ConfigurationEndpoints.cs`
|
||||
9. Configuration reload tests
|
||||
10. Validation tests
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Proceed to [Step 26: End-to-End Testing](26-Step.md) to implement comprehensive integration tests.
|
||||
Reference in New Issue
Block a user