Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Concelier.WebService.Options;
public sealed class ConcelierOptions
{
public StorageOptions Storage { get; set; } = new();
public PluginOptions Plugins { get; set; } = new();
public TelemetryOptions Telemetry { get; set; } = new();
public AuthorityOptions Authority { get; set; } = new();
public MirrorOptions Mirror { get; set; } = new();
public sealed class StorageOptions
{
public string Driver { get; set; } = "mongo";
public string Dsn { get; set; } = string.Empty;
public string? Database { get; set; }
public int CommandTimeoutSeconds { get; set; } = 30;
}
public sealed class PluginOptions
{
public string? BaseDirectory { get; set; }
public string? Directory { get; set; }
public IList<string> SearchPatterns { get; set; } = new List<string>();
}
public sealed class TelemetryOptions
{
public bool Enabled { get; set; } = true;
public bool EnableTracing { get; set; } = true;
public bool EnableMetrics { get; set; } = true;
public bool EnableLogging { get; set; } = true;
public string MinimumLogLevel { get; set; } = "Information";
public string? ServiceName { get; set; }
public string? OtlpEndpoint { get; set; }
public IDictionary<string, string> OtlpHeaders { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public IDictionary<string, string> ResourceAttributes { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public bool ExportConsole { get; set; }
}
public sealed class AuthorityOptions
{
public bool Enabled { get; set; }
public bool AllowAnonymousFallback { get; set; } = true;
public string Issuer { get; set; } = string.Empty;
public string? MetadataAddress { get; set; }
public bool RequireHttpsMetadata { get; set; } = true;
public int BackchannelTimeoutSeconds { get; set; } = 30;
public int TokenClockSkewSeconds { get; set; } = 60;
public IList<string> Audiences { get; set; } = new List<string>();
public IList<string> RequiredScopes { get; set; } = new List<string>();
public IList<string> BypassNetworks { get; set; } = new List<string>();
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
public string? ClientSecretFile { get; set; }
public IList<string> ClientScopes { get; set; } = new List<string>();
public ResilienceOptions Resilience { get; set; } = new();
public sealed class ResilienceOptions
{
public bool? EnableRetries { get; set; }
public IList<TimeSpan> RetryDelays { get; set; } = new List<TimeSpan>();
public bool? AllowOfflineCacheFallback { get; set; }
public TimeSpan? OfflineCacheTolerance { get; set; }
}
}
public sealed class MirrorOptions
{
public bool Enabled { get; set; }
public string ExportRoot { get; set; } = System.IO.Path.Combine("exports", "json");
public string? ActiveExportId { get; set; }
public string LatestDirectoryName { get; set; } = "latest";
public string MirrorDirectoryName { get; set; } = "mirror";
public bool RequireAuthentication { get; set; }
public int MaxIndexRequestsPerHour { get; set; } = 600;
public IList<MirrorDomainOptions> Domains { get; } = new List<MirrorDomainOptions>();
[JsonIgnore]
public string ExportRootAbsolute { get; internal set; } = string.Empty;
}
public sealed class MirrorDomainOptions
{
public string Id { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public bool RequireAuthentication { get; set; }
public int MaxDownloadRequestsPerHour { get; set; } = 1200;
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.IO;
namespace StellaOps.Concelier.WebService.Options;
/// <summary>
/// Post-configuration helpers for <see cref="ConcelierOptions"/>.
/// </summary>
public static class ConcelierOptionsPostConfigure
{
/// <summary>
/// Applies derived settings that require filesystem access, such as loading client secrets from disk.
/// </summary>
/// <param name="options">The options to mutate.</param>
/// <param name="contentRootPath">Application content root used to resolve relative paths.</param>
public static void Apply(ConcelierOptions options, string contentRootPath)
{
ArgumentNullException.ThrowIfNull(options);
options.Authority ??= new ConcelierOptions.AuthorityOptions();
var authority = options.Authority;
if (string.IsNullOrWhiteSpace(authority.ClientSecret)
&& !string.IsNullOrWhiteSpace(authority.ClientSecretFile))
{
var resolvedPath = authority.ClientSecretFile!;
if (!Path.IsPathRooted(resolvedPath))
{
resolvedPath = Path.Combine(contentRootPath, resolvedPath);
}
if (!File.Exists(resolvedPath))
{
throw new InvalidOperationException($"Authority client secret file '{resolvedPath}' was not found.");
}
var secret = File.ReadAllText(resolvedPath).Trim();
if (string.IsNullOrEmpty(secret))
{
throw new InvalidOperationException($"Authority client secret file '{resolvedPath}' is empty.");
}
authority.ClientSecret = secret;
}
options.Mirror ??= new ConcelierOptions.MirrorOptions();
var mirror = options.Mirror;
if (string.IsNullOrWhiteSpace(mirror.ExportRoot))
{
mirror.ExportRoot = Path.Combine("exports", "json");
}
var resolvedRoot = mirror.ExportRoot;
if (!Path.IsPathRooted(resolvedRoot))
{
resolvedRoot = Path.Combine(contentRootPath, resolvedRoot);
}
mirror.ExportRootAbsolute = Path.GetFullPath(resolvedRoot);
if (string.IsNullOrWhiteSpace(mirror.LatestDirectoryName))
{
mirror.LatestDirectoryName = "latest";
}
if (string.IsNullOrWhiteSpace(mirror.MirrorDirectoryName))
{
mirror.MirrorDirectoryName = "mirror";
}
}
}

View File

@@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Concelier.WebService.Options;
public static class ConcelierOptionsValidator
{
public static void Validate(ConcelierOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (!string.Equals(options.Storage.Driver, "mongo", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Only Mongo storage driver is supported (storage.driver == 'mongo').");
}
if (string.IsNullOrWhiteSpace(options.Storage.Dsn))
{
throw new InvalidOperationException("Storage DSN must be configured.");
}
if (options.Storage.CommandTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Command timeout must be greater than zero seconds.");
}
options.Telemetry ??= new ConcelierOptions.TelemetryOptions();
options.Authority ??= new ConcelierOptions.AuthorityOptions();
options.Authority.Resilience ??= new ConcelierOptions.AuthorityOptions.ResilienceOptions();
NormalizeList(options.Authority.Audiences, toLower: false);
NormalizeList(options.Authority.RequiredScopes, toLower: true);
NormalizeList(options.Authority.BypassNetworks, toLower: false);
NormalizeList(options.Authority.ClientScopes, toLower: true);
ValidateResilience(options.Authority.Resilience);
if (options.Authority.RequiredScopes.Count == 0)
{
options.Authority.RequiredScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
}
if (options.Authority.ClientScopes.Count == 0)
{
foreach (var scope in options.Authority.RequiredScopes)
{
options.Authority.ClientScopes.Add(scope);
}
}
if (options.Authority.ClientScopes.Count == 0)
{
options.Authority.ClientScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
}
if (options.Authority.BackchannelTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Authority backchannelTimeoutSeconds must be greater than zero.");
}
if (options.Authority.TokenClockSkewSeconds < 0 || options.Authority.TokenClockSkewSeconds > 300)
{
throw new InvalidOperationException("Authority tokenClockSkewSeconds must be between 0 and 300 seconds.");
}
if (options.Authority.Enabled)
{
if (string.IsNullOrWhiteSpace(options.Authority.Issuer))
{
throw new InvalidOperationException("Authority issuer must be configured when authority is enabled.");
}
if (!Uri.TryCreate(options.Authority.Issuer, UriKind.Absolute, out var issuerUri))
{
throw new InvalidOperationException("Authority issuer must be an absolute URI.");
}
if (options.Authority.RequireHttpsMetadata && !issuerUri.IsLoopback && !string.Equals(issuerUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Authority issuer must use HTTPS when requireHttpsMetadata is enabled.");
}
if (!string.IsNullOrWhiteSpace(options.Authority.MetadataAddress) && !Uri.TryCreate(options.Authority.MetadataAddress, UriKind.Absolute, out _))
{
throw new InvalidOperationException("Authority metadataAddress must be an absolute URI when specified.");
}
if (options.Authority.Audiences.Count == 0)
{
throw new InvalidOperationException("Authority audiences must include at least one entry when authority is enabled.");
}
if (!options.Authority.AllowAnonymousFallback)
{
if (string.IsNullOrWhiteSpace(options.Authority.ClientId))
{
throw new InvalidOperationException("Authority clientId must be configured when anonymous fallback is disabled.");
}
if (string.IsNullOrWhiteSpace(options.Authority.ClientSecret))
{
throw new InvalidOperationException("Authority clientSecret must be configured when anonymous fallback is disabled.");
}
}
}
if (!Enum.TryParse(options.Telemetry.MinimumLogLevel, ignoreCase: true, out LogLevel _))
{
throw new InvalidOperationException($"Telemetry minimum log level '{options.Telemetry.MinimumLogLevel}' is invalid.");
}
if (!string.IsNullOrWhiteSpace(options.Telemetry.OtlpEndpoint) && !Uri.TryCreate(options.Telemetry.OtlpEndpoint, UriKind.Absolute, out _))
{
throw new InvalidOperationException("Telemetry OTLP endpoint must be an absolute URI.");
}
foreach (var attribute in options.Telemetry.ResourceAttributes)
{
if (string.IsNullOrWhiteSpace(attribute.Key))
{
throw new InvalidOperationException("Telemetry resource attribute keys must be non-empty.");
}
}
foreach (var header in options.Telemetry.OtlpHeaders)
{
if (string.IsNullOrWhiteSpace(header.Key))
{
throw new InvalidOperationException("Telemetry OTLP header names must be non-empty.");
}
}
options.Mirror ??= new ConcelierOptions.MirrorOptions();
ValidateMirror(options.Mirror);
}
private static void NormalizeList(IList<string> values, bool toLower)
{
if (values is null || values.Count == 0)
{
return;
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var index = values.Count - 1; index >= 0; index--)
{
var entry = values[index];
if (string.IsNullOrWhiteSpace(entry))
{
values.RemoveAt(index);
continue;
}
var normalized = entry.Trim();
if (toLower)
{
normalized = normalized.ToLowerInvariant();
}
if (!seen.Add(normalized))
{
values.RemoveAt(index);
continue;
}
values[index] = normalized;
}
}
private static void ValidateResilience(ConcelierOptions.AuthorityOptions.ResilienceOptions resilience)
{
if (resilience.RetryDelays is null)
{
return;
}
foreach (var delay in resilience.RetryDelays)
{
if (delay <= TimeSpan.Zero)
{
throw new InvalidOperationException("Authority resilience retryDelays must be greater than zero.");
}
}
if (resilience.OfflineCacheTolerance.HasValue && resilience.OfflineCacheTolerance.Value < TimeSpan.Zero)
{
throw new InvalidOperationException("Authority resilience offlineCacheTolerance must be greater than or equal to zero.");
}
}
private static void ValidateMirror(ConcelierOptions.MirrorOptions mirror)
{
if (mirror.MaxIndexRequestsPerHour < 0)
{
throw new InvalidOperationException("Mirror maxIndexRequestsPerHour must be greater than or equal to zero.");
}
if (string.IsNullOrWhiteSpace(mirror.ExportRoot))
{
throw new InvalidOperationException("Mirror exportRoot must be configured.");
}
if (string.IsNullOrWhiteSpace(mirror.ExportRootAbsolute))
{
throw new InvalidOperationException("Mirror export root could not be resolved.");
}
if (string.IsNullOrWhiteSpace(mirror.LatestDirectoryName))
{
throw new InvalidOperationException("Mirror latestDirectoryName must be provided.");
}
if (string.IsNullOrWhiteSpace(mirror.MirrorDirectoryName))
{
throw new InvalidOperationException("Mirror mirrorDirectoryName must be provided.");
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var domain in mirror.Domains)
{
if (string.IsNullOrWhiteSpace(domain.Id))
{
throw new InvalidOperationException("Mirror domain id must be provided.");
}
var normalized = domain.Id.Trim();
if (!seen.Add(normalized))
{
throw new InvalidOperationException($"Mirror domain id '{normalized}' is duplicated.");
}
if (domain.MaxDownloadRequestsPerHour < 0)
{
throw new InvalidOperationException($"Mirror domain '{normalized}' maxDownloadRequestsPerHour must be greater than or equal to zero.");
}
}
if (mirror.Enabled && mirror.Domains.Count == 0)
{
throw new InvalidOperationException("Mirror distribution requires at least one domain when enabled.");
}
}
}