| File: DevTunnelResource.cs | Web Access |
| Project: src\src\Aspire.Hosting.DevTunnels\Aspire.Hosting.DevTunnels.csproj (Aspire.Hosting.DevTunnels) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Globalization; using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.DevTunnels; /// <summary> /// A resource representing a persistent dev tunnel that runs for the life of the AppHost. /// </summary> /// <param name="name"></param> /// <param name="tunnelId"></param> /// <param name="command"></param> /// <param name="workingDirectory"></param> /// <param name="options"></param> [DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}, TunnelId = {TunnelId}")] public sealed class DevTunnelResource(string name, string tunnelId, string command, string workingDirectory, DevTunnelOptions? options = null) : ExecutableResource(name, command, workingDirectory) { /// <summary> /// Options controlling how this tunnel is created and managed. /// </summary> public DevTunnelOptions Options { get; } = options ?? new DevTunnelOptions(); /// <summary> /// The unique ID for the dev tunnel. /// </summary> public string TunnelId { get; init; } = tunnelId; /// <summary> /// Gets the fully qualified tunnel ID including the region suffix if a region is specified. /// </summary> internal string ResolvedTunnelId => Options.Region is not null ? $"{TunnelId}.{Options.RegionCode}" : TunnelId; internal List<DevTunnelPortResource> Ports { get; } = []; internal DevTunnelStatus? LastKnownStatus { get; set; } internal DevTunnelAccessStatus? LastKnownAccessStatus { get; set; } } /// <summary> /// A resource representing a single forwarded endpoint/port on a dev tunnel. /// Contains an endpoint that resolves to the public tunnel URL of this port. /// </summary> public sealed class DevTunnelPortResource : Resource, IResourceWithServiceDiscovery, IResourceWithWaitSupport { /// <summary> /// The name of the endpoint within this resource that represents the public URL of the tunnel for this port. /// </summary> internal const string TunnelEndpointName = "tunnel"; internal const string ShowTunnelUrlsCommandName = "show-tunnel-urls"; internal const string TunnelUrlPropertyName = "TunnelUrl"; internal const string InspectUrlPropertyName = "InspectUrl"; internal const string LocalEndpointUrlPropertyName = "LocalEndpointUrl"; /// <summary> /// Initializes a new instance of the <see cref="DevTunnelPortResource"/> class, representing a single forwarded endpoint/port on a dev tunnel. /// </summary> /// <param name="name">The name of the port resource.</param> /// <param name="tunnel">The parent <see cref="DevTunnelResource"/> this port belongs to.</param> /// <param name="targetEndpoint">The endpoint to be forwarded through the tunnel.</param> /// <param name="options">Options controlling how this port is exposed.</param> public DevTunnelPortResource( string name, DevTunnelResource tunnel, EndpointReference targetEndpoint, DevTunnelPortOptions? options = null) : base(name) { DevTunnel = tunnel; Options = options ?? new DevTunnelPortOptions(); TargetEndpoint = targetEndpoint; TunnelEndpointAnnotation = new EndpointAnnotation( System.Net.Sockets.ProtocolType.Tcp, "https", transport: null, name: TunnelEndpointName, isProxied: false); TunnelEndpoint = new(targetEndpoint.Resource, TunnelEndpointAnnotation); } /// <summary> /// The dev tunnel this port belongs to. /// </summary> public DevTunnelResource DevTunnel { get; } /// <summary> /// Options controlling how this port is exposed. /// </summary> public DevTunnelPortOptions Options { get; } internal EndpointReference TunnelEndpoint { get; } internal EndpointAnnotation TunnelEndpointAnnotation { get; } internal EndpointReference TargetEndpoint { get; init; } internal DevTunnelPort? LastKnownStatus { get; set; } internal DevTunnelAccessStatus? LastKnownAccessStatus { get; set; } internal async ValueTask<int> GetTunnelPortAsync(CancellationToken cancellationToken = default) { if (TargetEndpoint.Resource.IsContainer()) { // Dev tunnel hosting runs on the host, so container endpoints must forward the host-reachable // allocated port rather than the container-internal target port. return await GetResolvedEndpointPortAsync(EndpointProperty.Port, cancellationToken).ConfigureAwait(false) ?? TargetEndpoint.Port; } if (TargetEndpoint.TargetPort is int targetPort) { return targetPort; } return await GetResolvedEndpointPortAsync(EndpointProperty.TargetPort, cancellationToken).ConfigureAwait(false) ?? TargetEndpoint.Port; } private async ValueTask<int?> GetResolvedEndpointPortAsync(EndpointProperty property, CancellationToken cancellationToken) { string? resolvedTargetPort = null; try { resolvedTargetPort = await TargetEndpoint.Property(property).GetValueAsync(cancellationToken).ConfigureAwait(false); } catch (InvalidOperationException) when (property == EndpointProperty.TargetPort && TargetEndpoint.IsAllocated) { // Endpoint references can only resolve targetPort dynamically when DCP reports a target-port expression. } if (int.TryParse(resolvedTargetPort, NumberStyles.None, CultureInfo.InvariantCulture, out var port)) { return port; } return null; } }