Files
git.stella-ops.org/src/Cli/StellaOps.Cli/Configuration/CliBootstrapper.cs
master a1ce3f74fa
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
- Added MongoPackRunApprovalStore for managing approval states with MongoDB.
- Introduced MongoPackRunArtifactUploader for uploading and storing artifacts.
- Created MongoPackRunLogStore to handle logging of pack run events.
- Developed MongoPackRunStateStore for persisting and retrieving pack run states.
- Implemented unit tests for MongoDB stores to ensure correct functionality.
- Added MongoTaskRunnerTestContext for setting up MongoDB test environment.
- Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
2025-11-07 10:01:47 +02:00

437 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
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<StellaOpsCliOptions>(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.ConcelierUrl = ResolveWithFallback(cliOptions.ConcelierUrl, configuration, "STELLAOPS_CONCELIER_URL", "StellaOps:ConcelierUrl", "ConcelierUrl");
cliOptions.AdvisoryAiUrl = ResolveWithFallback(cliOptions.AdvisoryAiUrl, configuration, "STELLAOPS_ADVISORYAI_URL", "StellaOps:AdvisoryAiUrl", "AdvisoryAiUrl");
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.ConcelierUrl = cliOptions.ConcelierUrl?.Trim() ?? string.Empty;
cliOptions.AdvisoryAiUrl = cliOptions.AdvisoryAiUrl?.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.OperatorReason = ResolveWithFallback(
authority.OperatorReason,
configuration,
"STELLAOPS_ORCH_REASON",
"StellaOps:Authority:OperatorReason",
"Authority:OperatorReason");
authority.OperatorTicket = ResolveWithFallback(
authority.OperatorTicket,
configuration,
"STELLAOPS_ORCH_TICKET",
"StellaOps:Authority:OperatorTicket",
"Authority:OperatorTicket");
authority.BackfillReason = ResolveWithFallback(
authority.BackfillReason,
configuration,
"STELLAOPS_ORCH_BACKFILL_REASON",
"StellaOps:Authority:BackfillReason",
"Authority:BackfillReason");
authority.BackfillTicket = ResolveWithFallback(
authority.BackfillTicket,
configuration,
"STELLAOPS_ORCH_BACKFILL_TICKET",
"StellaOps:Authority:BackfillTicket",
"Authority:BackfillTicket");
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.OperatorReason = authority.OperatorReason?.Trim() ?? string.Empty;
authority.OperatorTicket = authority.OperatorTicket?.Trim() ?? string.Empty;
authority.BackfillReason = authority.BackfillReason?.Trim() ?? string.Empty;
authority.BackfillTicket = authority.BackfillTicket?.Trim() ?? string.Empty;
authority.Resilience ??= new StellaOpsCliAuthorityResilienceOptions();
authority.Resilience.RetryDelays ??= new List<TimeSpan>();
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);
}
cliOptions.Offline ??= new StellaOpsCliOfflineOptions();
var offline = cliOptions.Offline;
var kitsDirectory = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_OFFLINE_KITS_DIRECTORY",
"STELLAOPS_OFFLINE_KITS_DIR",
"StellaOps:Offline:KitsDirectory",
"StellaOps:Offline:KitDirectory",
"Offline:KitsDirectory",
"Offline:KitDirectory");
if (string.IsNullOrWhiteSpace(kitsDirectory))
{
kitsDirectory = offline.KitsDirectory ?? "offline-kits";
}
offline.KitsDirectory = Path.GetFullPath(kitsDirectory);
if (!Directory.Exists(offline.KitsDirectory))
{
Directory.CreateDirectory(offline.KitsDirectory);
}
var mirror = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_OFFLINE_MIRROR_URL",
"StellaOps:Offline:KitMirror",
"Offline:KitMirror",
"Offline:MirrorUrl");
offline.MirrorUrl = string.IsNullOrWhiteSpace(mirror) ? null : mirror.Trim();
cliOptions.Plugins ??= new StellaOpsCliPluginOptions();
var pluginOptions = cliOptions.Plugins;
pluginOptions.BaseDirectory = ResolveWithFallback(
pluginOptions.BaseDirectory,
configuration,
"STELLAOPS_CLI_PLUGIN_BASE_DIRECTORY",
"StellaOps:Plugins:BaseDirectory",
"Plugins:BaseDirectory");
pluginOptions.BaseDirectory = (pluginOptions.BaseDirectory ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.BaseDirectory))
{
pluginOptions.BaseDirectory = AppContext.BaseDirectory;
}
pluginOptions.BaseDirectory = Path.GetFullPath(pluginOptions.BaseDirectory);
pluginOptions.Directory = ResolveWithFallback(
pluginOptions.Directory,
configuration,
"STELLAOPS_CLI_PLUGIN_DIRECTORY",
"StellaOps:Plugins:Directory",
"Plugins:Directory");
pluginOptions.Directory = (pluginOptions.Directory ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.Directory))
{
pluginOptions.Directory = Path.Combine("plugins", "cli");
}
if (!Path.IsPathRooted(pluginOptions.Directory))
{
pluginOptions.Directory = Path.GetFullPath(Path.Combine(pluginOptions.BaseDirectory, pluginOptions.Directory));
}
else
{
pluginOptions.Directory = Path.GetFullPath(pluginOptions.Directory);
}
pluginOptions.ManifestSearchPattern = ResolveWithFallback(
pluginOptions.ManifestSearchPattern,
configuration,
"STELLAOPS_CLI_PLUGIN_MANIFEST_PATTERN",
"StellaOps:Plugins:ManifestSearchPattern",
"Plugins:ManifestSearchPattern");
pluginOptions.ManifestSearchPattern = (pluginOptions.ManifestSearchPattern ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.ManifestSearchPattern))
{
pluginOptions.ManifestSearchPattern = "*.manifest.json";
}
if (pluginOptions.SearchPatterns is null || pluginOptions.SearchPatterns.Count == 0)
{
pluginOptions.SearchPatterns = new List<string> { "StellaOps.Cli.Plugin.*.dll" };
}
else
{
pluginOptions.SearchPatterns = pluginOptions.SearchPatterns
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
.Select(pattern => pattern.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (pluginOptions.SearchPatterns.Count == 0)
{
pluginOptions.SearchPatterns.Add("StellaOps.Cli.Plugin.*.dll");
}
}
if (pluginOptions.PluginOrder is null)
{
pluginOptions.PluginOrder = new List<string>();
}
else
{
pluginOptions.PluginOrder = pluginOptions.PluginOrder
.Where(name => !string.IsNullOrWhiteSpace(name))
.Select(name => name.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
};
});
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<TimeSpan> 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"));
}
}