using System; using System.Collections.Generic; using System.Globalization; using System.IO; using Microsoft.Extensions.Configuration; using StellaOps.Configuration; using StellaOps.Auth.Abstractions; namespace StellaOps.Cli.Configuration; public static class CliBootstrapper { public static (StellaOpsCliOptions Options, IConfigurationRoot Configuration) Build(string[] args) { var bootstrap = StellaOpsConfigurationBootstrapper.Build(options => { options.BindingSection = "StellaOps"; options.ConfigureBuilder = builder => { if (args.Length > 0) { builder.AddCommandLine(args); } }; options.PostBind = (cliOptions, configuration) => { cliOptions.ApiKey = ResolveWithFallback(cliOptions.ApiKey, configuration, "API_KEY", "StellaOps:ApiKey", "ApiKey"); cliOptions.BackendUrl = ResolveWithFallback(cliOptions.BackendUrl, configuration, "STELLAOPS_BACKEND_URL", "StellaOps:BackendUrl", "BackendUrl"); cliOptions.ScannerSignaturePublicKeyPath = ResolveWithFallback(cliOptions.ScannerSignaturePublicKeyPath, configuration, "SCANNER_PUBLIC_KEY", "STELLAOPS_SCANNER_PUBLIC_KEY", "StellaOps:ScannerSignaturePublicKeyPath", "ScannerSignaturePublicKeyPath"); cliOptions.ApiKey = cliOptions.ApiKey?.Trim() ?? string.Empty; cliOptions.BackendUrl = cliOptions.BackendUrl?.Trim() ?? string.Empty; cliOptions.ScannerSignaturePublicKeyPath = cliOptions.ScannerSignaturePublicKeyPath?.Trim() ?? string.Empty; var attemptsRaw = ResolveWithFallback( string.Empty, configuration, "SCANNER_DOWNLOAD_ATTEMPTS", "STELLAOPS_SCANNER_DOWNLOAD_ATTEMPTS", "StellaOps:ScannerDownloadAttempts", "ScannerDownloadAttempts"); if (string.IsNullOrWhiteSpace(attemptsRaw)) { attemptsRaw = cliOptions.ScannerDownloadAttempts.ToString(CultureInfo.InvariantCulture); } if (int.TryParse(attemptsRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedAttempts) && parsedAttempts > 0) { cliOptions.ScannerDownloadAttempts = parsedAttempts; } if (cliOptions.ScannerDownloadAttempts <= 0) { cliOptions.ScannerDownloadAttempts = 3; } cliOptions.Authority ??= new StellaOpsCliAuthorityOptions(); var authority = cliOptions.Authority; authority.Url = ResolveWithFallback( authority.Url, configuration, "STELLAOPS_AUTHORITY_URL", "StellaOps:Authority:Url", "Authority:Url", "Authority:Issuer"); authority.ClientId = ResolveWithFallback( authority.ClientId, configuration, "STELLAOPS_AUTHORITY_CLIENT_ID", "StellaOps:Authority:ClientId", "Authority:ClientId"); authority.ClientSecret = ResolveWithFallback( authority.ClientSecret ?? string.Empty, configuration, "STELLAOPS_AUTHORITY_CLIENT_SECRET", "StellaOps:Authority:ClientSecret", "Authority:ClientSecret"); authority.Username = ResolveWithFallback( authority.Username, configuration, "STELLAOPS_AUTHORITY_USERNAME", "StellaOps:Authority:Username", "Authority:Username"); authority.Password = ResolveWithFallback( authority.Password ?? string.Empty, configuration, "STELLAOPS_AUTHORITY_PASSWORD", "StellaOps:Authority:Password", "Authority:Password"); authority.Scope = ResolveWithFallback( authority.Scope, configuration, "STELLAOPS_AUTHORITY_SCOPE", "StellaOps:Authority:Scope", "Authority:Scope"); authority.TokenCacheDirectory = ResolveWithFallback( authority.TokenCacheDirectory, configuration, "STELLAOPS_AUTHORITY_TOKEN_CACHE_DIR", "StellaOps:Authority:TokenCacheDirectory", "Authority:TokenCacheDirectory"); authority.Url = authority.Url?.Trim() ?? string.Empty; authority.ClientId = authority.ClientId?.Trim() ?? string.Empty; authority.ClientSecret = string.IsNullOrWhiteSpace(authority.ClientSecret) ? null : authority.ClientSecret.Trim(); authority.Username = authority.Username?.Trim() ?? string.Empty; authority.Password = string.IsNullOrWhiteSpace(authority.Password) ? null : authority.Password.Trim(); authority.Scope = string.IsNullOrWhiteSpace(authority.Scope) ? StellaOpsScopes.ConcelierJobsTrigger : authority.Scope.Trim(); authority.Resilience ??= new StellaOpsCliAuthorityResilienceOptions(); authority.Resilience.RetryDelays ??= new List(); var resilience = authority.Resilience; if (!resilience.EnableRetries.HasValue) { var raw = ResolveWithFallback( string.Empty, configuration, "STELLAOPS_AUTHORITY_ENABLE_RETRIES", "StellaOps:Authority:Resilience:EnableRetries", "StellaOps:Authority:EnableRetries", "Authority:Resilience:EnableRetries", "Authority:EnableRetries"); if (TryParseBoolean(raw, out var parsed)) { resilience.EnableRetries = parsed; } } var retryDelaysRaw = ResolveWithFallback( string.Empty, configuration, "STELLAOPS_AUTHORITY_RETRY_DELAYS", "StellaOps:Authority:Resilience:RetryDelays", "StellaOps:Authority:RetryDelays", "Authority:Resilience:RetryDelays", "Authority:RetryDelays"); if (!string.IsNullOrWhiteSpace(retryDelaysRaw)) { resilience.RetryDelays.Clear(); foreach (var delay in ParseRetryDelays(retryDelaysRaw)) { if (delay > TimeSpan.Zero) { resilience.RetryDelays.Add(delay); } } } if (!resilience.AllowOfflineCacheFallback.HasValue) { var raw = ResolveWithFallback( string.Empty, configuration, "STELLAOPS_AUTHORITY_ALLOW_OFFLINE_CACHE_FALLBACK", "StellaOps:Authority:Resilience:AllowOfflineCacheFallback", "StellaOps:Authority:AllowOfflineCacheFallback", "Authority:Resilience:AllowOfflineCacheFallback", "Authority:AllowOfflineCacheFallback"); if (TryParseBoolean(raw, out var parsed)) { resilience.AllowOfflineCacheFallback = parsed; } } if (!resilience.OfflineCacheTolerance.HasValue) { var raw = ResolveWithFallback( string.Empty, configuration, "STELLAOPS_AUTHORITY_OFFLINE_CACHE_TOLERANCE", "StellaOps:Authority:Resilience:OfflineCacheTolerance", "StellaOps:Authority:OfflineCacheTolerance", "Authority:Resilience:OfflineCacheTolerance", "Authority:OfflineCacheTolerance"); if (TimeSpan.TryParse(raw, CultureInfo.InvariantCulture, out var tolerance) && tolerance >= TimeSpan.Zero) { resilience.OfflineCacheTolerance = tolerance; } } var defaultTokenCache = GetDefaultTokenCacheDirectory(); if (string.IsNullOrWhiteSpace(authority.TokenCacheDirectory)) { authority.TokenCacheDirectory = defaultTokenCache; } else { authority.TokenCacheDirectory = Path.GetFullPath(authority.TokenCacheDirectory); } }; }); return (bootstrap.Options, bootstrap.Configuration); } private static string ResolveWithFallback(string currentValue, IConfiguration configuration, params string[] keys) { if (!string.IsNullOrWhiteSpace(currentValue)) { return currentValue; } foreach (var key in keys) { var value = configuration[key]; if (!string.IsNullOrWhiteSpace(value)) { return value; } } return string.Empty; } private static bool TryParseBoolean(string value, out bool parsed) { if (string.IsNullOrWhiteSpace(value)) { parsed = default; return false; } if (bool.TryParse(value, out parsed)) { return true; } if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)) { parsed = numeric != 0; return true; } parsed = default; return false; } private static IEnumerable ParseRetryDelays(string raw) { if (string.IsNullOrWhiteSpace(raw)) { yield break; } var separators = new[] { ',', ';', ' ' }; foreach (var token in raw.Split(separators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { if (TimeSpan.TryParse(token, CultureInfo.InvariantCulture, out var delay) && delay > TimeSpan.Zero) { yield return delay; } } } private static string GetDefaultTokenCacheDirectory() { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (string.IsNullOrWhiteSpace(home)) { home = AppContext.BaseDirectory; } return Path.GetFullPath(Path.Combine(home, ".stellaops", "tokens")); } }