| File: Packaging\IntegrationSourceConfiguration.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; using System.Globalization; using System.Text.Json.Serialization; using Aspire.Cli.Configuration; using Aspire.Cli.Resources; using Aspire.Cli.Utils; using Microsoft.Extensions.Configuration; namespace Aspire.Cli.Packaging; internal sealed class IntegrationSourceConfiguration { internal const string SectionName = "integrationSources"; internal const string DefaultName = "default"; [JsonPropertyName("enabled")] [Description("Whether to search this source. Defaults to true. Disable default to omit built-in channel and NuGet discovery.")] public bool? Enabled { get; init; } [JsonPropertyName("source")] [Description("NuGet service index URL or local package directory. The default entry uses built-in discovery when omitted.")] public string? Source { get; init; } [JsonPropertyName("patterns")] [Description("Optional allowed package IDs: exact IDs, prefixes ending in *, or * for all packages. Defaults to * when omitted.")] public string[]? Patterns { get; init; } [JsonIgnore] internal string Name { get; init; } = DefaultName; internal bool AllowsPackage(string packageId) => Patterns!.Any(pattern => PackageSourceOverrideMappings.MatchesPackage(pattern, packageId)); internal IEnumerable<string> SearchTerms => Patterns!.Contains("*") ? [""] : Patterns!.Select(pattern => pattern.TrimEnd('*')).Distinct(StringComparer.OrdinalIgnoreCase); internal static IReadOnlyList<IntegrationSourceConfiguration> Resolve(DirectoryInfo directory, FileInfo globalSettingsFile) { using var configuration = new ConfigurationManager(); configuration.AddEnvironmentVariables(); ConfigurationHelper.RegisterSettingsFiles(configuration, directory, globalSettingsFile, persistNormalization: false); return Resolve(configuration, directory); } internal static IReadOnlyList<string> GetConfiguredSources(DirectoryInfo directory, FileInfo globalSettingsFile) { using var configuration = new ConfigurationManager(); configuration.AddEnvironmentVariables(); ConfigurationHelper.RegisterSettingsFiles(configuration, directory, globalSettingsFile, persistNormalization: false); // Disabling discovery does not release feeds that installed packages may still need. var section = configuration.GetSection(SectionName); if (section.Value is not null) { throw InvalidConfiguration(SectionName); } var sources = new List<string>(); foreach (var entry in section.GetChildren()) { SourceNameValidator.Validate(entry.Key, $"{SectionName}.{entry.Key}"); if (entry.Value is not null) { throw InvalidConfiguration($"{SectionName}.{entry.Key}"); } if (entry["source"] is { } source) { sources.Add(ResolveSource(source, $"{SectionName}.{entry.Key}.source", directory)); } } return sources.Distinct(PackageSourceIdentity.Comparer).ToArray(); } internal static IReadOnlyList<IntegrationSourceConfiguration> Resolve(IConfiguration configuration, DirectoryInfo directory) { var section = configuration.GetSection(SectionName); if (section.Value is not null) { throw InvalidConfiguration(SectionName); } var sources = new List<IntegrationSourceConfiguration>(); foreach (var name in new[] { DefaultName }.Concat(section.GetChildren().Select(child => child.Key)).Distinct(StringComparer.OrdinalIgnoreCase)) { var settings = section.GetSection(name); var key = $"{SectionName}.{name}"; SourceNameValidator.Validate(name, key); if (settings.Value is not null) { throw InvalidConfiguration(key); } if (settings.GetBool("enabled") is false) { continue; } var source = settings["source"]; var patternsSection = settings.GetSection("patterns"); var patterns = patternsSection.GetChildren().Select(child => child.Value).ToArray(); // Explicit "patterns": null or {} must not be treated as an omitted property. var hasPatterns = settings.GetChildren().Any(child => child.Key.Equals("patterns", StringComparison.OrdinalIgnoreCase)); if (source is null && name.Equals(DefaultName, StringComparison.OrdinalIgnoreCase) && !hasPatterns) { sources.Add(new IntegrationSourceConfiguration()); continue; } if (!hasPatterns) { patterns = ["*"]; } if (patternsSection.Value is not null || patterns.Length == 0 || patternsSection.GetChildren().Where((child, index) => child.Key != index.ToString(CultureInfo.InvariantCulture)).Any() || patterns.Any(pattern => pattern is null || !IsPattern(pattern))) { throw InvalidConfiguration($"{key}.patterns"); } var resolvedSource = ResolveSource(source, $"{key}.source", directory); sources.Add(new IntegrationSourceConfiguration { Name = name, Source = resolvedSource, Patterns = patterns.Select(pattern => pattern!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray() }); } return [ .. sources.Where(source => source.Source is null), .. sources.Where(source => source.Source is not null) .GroupBy(source => source.Source!, PackageSourceIdentity.Comparer) .Select(group => new IntegrationSourceConfiguration { Name = group.First().Name, Source = group.First().Source, Patterns = group.SelectMany(source => source.Patterns!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray() }) ]; } private static bool IsIdentifier(string value) => value.Length > 0 && value.All(c => char.IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_'); private static string ResolveSource(string? source, string key, DirectoryInfo directory) { if (string.IsNullOrWhiteSpace(source) || PackageSourceOverrideMappings.HasCredentialMaterial(source) || Uri.TryCreate(source, UriKind.Absolute, out var uri) && !uri.IsFile && uri.Scheme is not ("https" or "http")) { throw InvalidConfiguration(key); } return PackageSourceOverrideMappings.ResolveForWorkingDirectory(source, directory); } private static bool IsPattern(string pattern) => pattern == "*" || IsIdentifier(pattern.EndsWith('*') ? pattern[..^1] : pattern); private static InvalidOperationException InvalidConfiguration(string key) => new(string.Format(CultureInfo.CurrentCulture, ErrorStrings.InvalidIntegrationSource, key)); }