Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- 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.
437 lines
18 KiB
C#
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"));
|
|
}
|
|
}
|