115 lines
5.2 KiB
C#
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;
|
|
}
|
|
}
|