File: Devcontainers\DevcontainerPortForwardingLifecycleHook.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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.Devcontainers.Codespaces;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Devcontainers;
 
internal sealed class DevcontainerPortForwardingLifecycleHook : IDistributedApplicationLifecycleHook
{
    private readonly ILogger _hostingLogger;
    private readonly IOptions<CodespacesOptions> _codespacesOptions;
    private readonly IOptions<DevcontainersOptions> _devcontainersOptions;
    private readonly DevcontainerSettingsWriter _settingsWriter;
 
    public DevcontainerPortForwardingLifecycleHook(ILoggerFactory loggerFactory, IOptions<CodespacesOptions> codespacesOptions, IOptions<DevcontainersOptions> devcontainersOptions, DevcontainerSettingsWriter settingsWriter)
    {
        _hostingLogger = loggerFactory.CreateLogger("Aspire.Hosting");
        _codespacesOptions = codespacesOptions;
        _devcontainersOptions = devcontainersOptions;
        _settingsWriter = settingsWriter;
    }
 
    public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
    {
        if (!_devcontainersOptions.Value.IsDevcontainer && !_codespacesOptions.Value.IsCodespace)
        {
            // We aren't a codespace so there is nothing to do here.
            return;
        }
 
        foreach (var resource in appModel.Resources)
        {
            if (resource.Name == KnownResourceNames.AspireDashboard)
            {
                // We don't configure the dashboard here because if we print out the URL
                // the dashboard will launch immediately but it hasn't actually started
                // which would lead to a poor experience. So we'll let the dashboard
                // URL writing logic call the helper directly.
                continue;
            }
 
            if (resource is not IResourceWithEndpoints resourceWithEndpoints)
            {
                continue;
            }
 
            foreach (var endpoint in resourceWithEndpoints.Annotations.OfType<EndpointAnnotation>())
            {
                if (_codespacesOptions.Value.IsCodespace && !(endpoint.UriScheme is "https" or "http"))
                {
                    // Codespaces only does port forwarding over HTTPS. If the protocol is not HTTP or HTTPS
                    // it cannot be forwarded because it can't intercept access to the endpoint without breaking
                    // the non-HTTP protocol to do GitHub auth.
                    continue;
                }
 
                // TODO: This is inefficient because we are opening the file, parsing it, updating it
                //       and writing it each time. Its like this for now beause I need to use the logic
                //       in a few places (here and when we print out the Dashboard URL) - but will need
                //       to come back and optimize this to support some kind of batching.
                await _settingsWriter.SetPortAttributesAsync(
                    endpoint.AllocatedEndpoint!.Port,
                    endpoint.UriScheme,
                    $"{resource.Name}-{endpoint.Name}",
                    cancellationToken).ConfigureAwait(false);
 
                _hostingLogger.LogInformation("Port forwarding: {Url}", endpoint.AllocatedEndpoint!.UriString);
            }
        }
    }
}