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.Eventing;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Devcontainers;
 
internal sealed class DevcontainerPortForwardingLifecycleHook : IDistributedApplicationEventingSubscriber
{
    private readonly IOptions<CodespacesOptions> _codespacesOptions;
    private readonly IOptions<DevcontainersOptions> _devcontainersOptions;
    private readonly IOptions<SshRemoteOptions> _sshRemoteOptions;
    private readonly DevcontainerSettingsWriter _settingsWriter;
 
    public DevcontainerPortForwardingLifecycleHook(
        IOptions<CodespacesOptions> codespacesOptions,
        IOptions<DevcontainersOptions> devcontainersOptions,
        IOptions<SshRemoteOptions> sshRemoteOptions,
        DevcontainerSettingsWriter settingsWriter)
    {
        _codespacesOptions = codespacesOptions;
        _devcontainersOptions = devcontainersOptions;
        _sshRemoteOptions = sshRemoteOptions;
        _settingsWriter = settingsWriter;
    }
 
    public Task OnResourceEndpointsAllocatedAsync(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken)
    {
        foreach (var endpoint in @event.Resource.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;
            }
 
            _settingsWriter.AddPortForward(
                endpoint.AllocatedEndpoint!.UriString,
                endpoint.AllocatedEndpoint!.Port,
                endpoint.UriScheme,
                $"{@event.Resource.Name}-{endpoint.Name}");
        }
 
        return Task.CompletedTask;
    }
 
    public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext execContext, CancellationToken cancellationToken)
    {
        if (!_devcontainersOptions.Value.IsDevcontainer && !_codespacesOptions.Value.IsCodespace && !_sshRemoteOptions.Value.IsSshRemote)
        {
            // We aren't a codespace, devcontainer, or SSH remote so there is nothing to do here.
            return Task.CompletedTask;
        }
 
        eventing.Subscribe<ResourceEndpointsAllocatedEvent>(OnResourceEndpointsAllocatedAsync);
        return Task.CompletedTask;
    }
}