File: KubernetesEnvironmentContext.cs
Web Access
Project: src\src\Aspire.Hosting.Kubernetes\Aspire.Hosting.Kubernetes.csproj (Aspire.Hosting.Kubernetes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Kubernetes;
 
internal sealed class KubernetesEnvironmentContext(KubernetesEnvironmentResource environment, ILogger logger)
{
    private readonly Dictionary<IResource, KubernetesResource> _kubernetesComponents = new(new ResourceNameComparer());
    private readonly EndpointPropertyInputCache _endpointInputs = new();
 
    public ParameterResource GetEndpointInput(EndpointReferenceExpression reference, DistributedApplicationExecutionContext executionContext)
    {
        if (!reference.Endpoint.EndpointAnnotation.IsExternal ||
            reference.Endpoint.EndpointAnnotation.UriScheme is not ("http" or "https"))
        {
            throw new NotSupportedException(
                $"Kubernetes cannot provide browser origins for endpoint '{reference.Endpoint.EndpointName}' on resource '{reference.Endpoint.Resource.Name}' because it is not an external HTTP(S) endpoint. " +
                "Reference an external HTTP(S) endpoint or supply an explicit origin parameter.");
        }
 
        var resources = executionContext.Services.GetRequiredService<DistributedApplicationModel>().Resources;
        _endpointInputs.ReserveModelNames(resources);
        return _endpointInputs.GetOrAdd(reference, executionContext.Services.GetRequiredService<IConfiguration>(),
            () => GetOriginDefault(reference, resources));
    }
 
    private string? GetOriginDefault(EndpointReferenceExpression reference, IEnumerable<IResource> resources)
    {
        var origins = new List<string>();
        foreach (var ingress in resources.OfType<KubernetesIngressResource>().Where(ingress => ingress.Parent == environment && ingress.ShouldMaterialize))
        {
            foreach (var path in ingress.Paths.Where(path => Matches(path.Endpoint)))
            {
                if (!AddOrigins(path.Host, ingress.Hostnames, ingress.TlsConfigs.Count > 0, gateway: false))
                {
                    return null;
                }
            }
        }
 
        foreach (var gateway in resources.OfType<KubernetesGatewayResource>().Where(gateway => gateway.Parent == environment && gateway.ShouldMaterialize))
        {
            foreach (var route in gateway.Routes.Where(route => Matches(route.Endpoint)))
            {
                if (!AddOrigins(route.Host, gateway.Hostnames, gateway.TlsConfigs.Count > 0, gateway: true))
                {
                    return null;
                }
            }
        }
 
        var distinct = origins.Distinct(StringComparer.Ordinal).ToArray();
        return reference.Property == EndpointProperty.PrimaryBrowserOrigin
            ? distinct.FirstOrDefault()
            : distinct.Length == 0 ? null : string.Join(",", distinct);
 
        bool Matches(EndpointReference endpoint) =>
            string.Equals(endpoint.Resource.Name, reference.Endpoint.Resource.Name, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(endpoint.EndpointName, reference.Endpoint.EndpointName, StringComparison.OrdinalIgnoreCase);
 
        bool AddOrigins(string? routeHost, IReadOnlyList<ReferenceExpression> hostnames, bool tls, bool gateway)
        {
            // Host-scoped routes override inherited hostnames. Wildcard/hostless routes and
            // deferred host expressions cannot establish a concrete published default.
            IReadOnlyList<ReferenceExpression> hosts = routeHost is not null ? [ReferenceExpression.Create($"{routeHost}")] : hostnames;
            if (hosts.Count == 0)
            {
                return false;
            }
 
            foreach (var host in hosts)
            {
                // A host-scoped route can differ from the hostnames on the TLS listener.
                // Use HTTPS only when that listener also serves the selected hostname.
                var usesTls = tls && (routeHost is null || hostnames.Count == 0 ||
                    hostnames.Any(hostname => hostname.ValueProviders.Count == 0 &&
                        MatchesHostname(hostname.ValueExpression, routeHost, gateway)));
                if (tls && !usesTls && hostnames.Any(hostname => hostname.ValueProviders.Count != 0))
                {
                    return false;
                }
                if (host.ValueProviders.Count != 0 ||
                    Uri.CheckHostName(host.ValueExpression) is not (UriHostNameType.Dns or UriHostNameType.IPv4 or UriHostNameType.IPv6) ||
                    !Uri.TryCreate($"{(usesTls ? "https" : "http")}://{host.ValueExpression}", UriKind.Absolute, out var uri))
                {
                    return false;
                }
 
                origins.Add(BrowserOriginHelpers.GetOrigin(uri));
            }
 
            return true;
        }
    }
 
    private static bool MatchesHostname(string pattern, string hostname, bool gateway)
    {
        if (!HostString.MatchesAny(hostname, [pattern]))
        {
            return false;
        }
 
        // HostString supports Gateway-style multi-label wildcard matching. Ingress
        // additionally restricts "*.example.com" to a single label before the suffix.
        // https://kubernetes.io/docs/concepts/services-networking/ingress/#hostname-wildcards
        if (gateway || !pattern.StartsWith("*.", StringComparison.Ordinal))
        {
            return true;
        }
 
        var prefixLength = hostname.Length - pattern.Length + 1;
        return !hostname.AsSpan(0, prefixLength).Contains('.');
    }
 
    public ILogger Logger => logger;
 
    public async Task<KubernetesResource> CreateKubernetesResourceAsync(IResource resource, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
    {
        if (_kubernetesComponents.TryGetValue(resource, out var existingResource))
        {
            return existingResource;
        }
 
        logger.LogInformation("Creating Kubernetes resource for {ResourceName}", resource.Name);
 
        var serviceResource = new KubernetesResource(resource.Name, resource, environment);
        _kubernetesComponents[resource] = serviceResource;
 
        await serviceResource.ProcessResourceAsync(this, executionContext, cancellationToken).ConfigureAwait(false);
 
        return serviceResource;
    }
}