Files
git.stella-ops.org/src/Platform/StellaOps.Platform.WebService/Services/EnvironmentSettingsComposer.cs

115 lines
5.2 KiB
C#

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
using Microsoft.Extensions.Options;
using StellaOps.Platform.WebService.Options;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Composes a final <see cref="PlatformEnvironmentSettingsOptions"/> from three layers:
/// <list type="number">
/// <item>Environment variables (via <see cref="Configuration.StellaOpsEnvVarPostConfigure"/>)</item>
/// <item>YAML/JSON configuration (standard IOptions binding)</item>
/// <item>Database (via <see cref="IEnvironmentSettingsStore"/>)</item>
/// </list>
/// Layer 3 (DB) has highest priority and is overlaid last.
/// </summary>
public sealed class EnvironmentSettingsComposer
{
private readonly IOptionsMonitor<PlatformServiceOptions> _optionsMonitor;
private readonly IEnvironmentSettingsStore _store;
public EnvironmentSettingsComposer(
IOptionsMonitor<PlatformServiceOptions> optionsMonitor,
IEnvironmentSettingsStore store)
{
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_store = store ?? throw new ArgumentNullException(nameof(store));
}
/// <summary>
/// Returns the merged environment settings with all three layers applied.
/// </summary>
public async Task<PlatformEnvironmentSettingsOptions> ComposeAsync(CancellationToken ct = default)
{
var source = _optionsMonitor.CurrentValue.EnvironmentSettings;
// Clone the options so we don't mutate the originals
var merged = new PlatformEnvironmentSettingsOptions
{
ClientId = source.ClientId,
AuthorizeEndpoint = source.AuthorizeEndpoint,
TokenEndpoint = source.TokenEndpoint,
LogoutEndpoint = source.LogoutEndpoint,
RedirectUri = source.RedirectUri,
PostLogoutRedirectUri = source.PostLogoutRedirectUri,
Scope = source.Scope,
Audience = source.Audience,
DpopAlgorithms = new List<string>(source.DpopAlgorithms),
RefreshLeewaySeconds = source.RefreshLeewaySeconds,
ApiBaseUrls = new Dictionary<string, string>(source.ApiBaseUrls, StringComparer.OrdinalIgnoreCase),
OtlpEndpoint = source.OtlpEndpoint,
TelemetrySampleRate = source.TelemetrySampleRate,
WelcomeTitle = source.WelcomeTitle,
WelcomeMessage = source.WelcomeMessage,
WelcomeDocsUrl = source.WelcomeDocsUrl,
DoctorFixEnabled = source.DoctorFixEnabled,
};
// Overlay DB values (Layer 3 — highest priority)
var dbSettings = await _store.GetAllAsync(ct).ConfigureAwait(false);
foreach (var (key, value) in dbSettings)
{
if (key.StartsWith("ApiBaseUrls:", StringComparison.OrdinalIgnoreCase))
{
var serviceName = key["ApiBaseUrls:".Length..];
merged.ApiBaseUrls[serviceName] = value;
}
else
{
// Map scalar settings by key name
ApplyScalarSetting(merged, key, value);
}
}
return merged;
}
private static void ApplyScalarSetting(PlatformEnvironmentSettingsOptions options, string key, string value)
{
// Case-insensitive key matching for known scalar settings
if (string.Equals(key, "ClientId", StringComparison.OrdinalIgnoreCase))
options.ClientId = value;
else if (string.Equals(key, "AuthorizeEndpoint", StringComparison.OrdinalIgnoreCase))
options.AuthorizeEndpoint = value;
else if (string.Equals(key, "TokenEndpoint", StringComparison.OrdinalIgnoreCase))
options.TokenEndpoint = value;
else if (string.Equals(key, "LogoutEndpoint", StringComparison.OrdinalIgnoreCase))
options.LogoutEndpoint = value;
else if (string.Equals(key, "RedirectUri", StringComparison.OrdinalIgnoreCase))
options.RedirectUri = value;
else if (string.Equals(key, "PostLogoutRedirectUri", StringComparison.OrdinalIgnoreCase))
options.PostLogoutRedirectUri = value;
else if (string.Equals(key, "Scope", StringComparison.OrdinalIgnoreCase))
options.Scope = value;
else if (string.Equals(key, "Audience", StringComparison.OrdinalIgnoreCase))
options.Audience = value;
else if (string.Equals(key, "OtlpEndpoint", StringComparison.OrdinalIgnoreCase))
options.OtlpEndpoint = value;
else if (string.Equals(key, "TelemetrySampleRate", StringComparison.OrdinalIgnoreCase)
&& double.TryParse(value, out var rate))
options.TelemetrySampleRate = rate;
else if (string.Equals(key, "WelcomeTitle", StringComparison.OrdinalIgnoreCase))
options.WelcomeTitle = value;
else if (string.Equals(key, "WelcomeMessage", StringComparison.OrdinalIgnoreCase))
options.WelcomeMessage = value;
else if (string.Equals(key, "WelcomeDocsUrl", StringComparison.OrdinalIgnoreCase))
options.WelcomeDocsUrl = value;
else if (string.Equals(key, "DoctorFixEnabled", StringComparison.OrdinalIgnoreCase)
&& bool.TryParse(value, out var fix))
options.DoctorFixEnabled = fix;
}
}