Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,21 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
}
}

// `devtunnel show <id>` resolves a bare ID and returns its cluster-qualified ID, but
// `devtunnel access list <id> --port-number <port>` requires the qualified form.
// Remove this workaround when the CLI resolves bare IDs for per-port access queries.
// See https://github.com/microsoft/aspire/issues/18790.
var resolvedTunnelId = tunnelStatus.TunnelId;

// Get tunnel and port access status
var tunnelAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.ResolvedTunnelId, portNumber: null, logger, cancellationToken).ConfigureAwait(false);
var tunnelAccessStatus = await _devTunnelClient.GetAccessAsync(resolvedTunnelId, portNumber: null, logger, cancellationToken).ConfigureAwait(false);
_tunnelResource.LastKnownAccessStatus = tunnelAccessStatus;

// Get access status for each port
foreach (var portResource in _tunnelResource.Ports)
{
var tunnelPort = await portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false);
var portAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.ResolvedTunnelId, tunnelPort, logger, cancellationToken).ConfigureAwait(false);
var portAccessStatus = await _devTunnelClient.GetAccessAsync(resolvedTunnelId, tunnelPort, logger, cancellationToken).ConfigureAwait(false);
portResource.LastKnownAccessStatus = portAccessStatus;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,15 @@ public static IResourceBuilder<DevTunnelResource> AddDevTunnel(
await devTunnelEnvironmentManager.EnsureUserLoggedInAsync(ct).ConfigureAwait(false);

// Create the dev tunnel
string resolvedTunnelId;
try
{
logger.LogInformation("Creating dev tunnel '{TunnelId}'", tunnelResource.TunnelId);
var tunnelStatus = await devTunnelClient.CreateTunnelAsync(tunnelResource.TunnelId, tunnelResource.Options, logger, ct).ConfigureAwait(false);
// The CLI resolves a bare ID and returns its cluster-qualified ID. Use that ID for
// port operations because bare IDs may not resolve tunnels across clusters.
// See https://github.com/microsoft/aspire/issues/18790.
resolvedTunnelId = tunnelStatus.TunnelId;
logger.LogDebug("Dev tunnel '{TunnelId}' created", tunnelResource.TunnelId);
}
catch (Exception ex)
Expand Down Expand Up @@ -174,13 +179,13 @@ public static IResourceBuilder<DevTunnelResource> AddDevTunnel(

async Task DeleteUnmodeledPortsAsync()
{
var existingPorts = await devTunnelClient.GetPortListAsync(tunnelResource.ResolvedTunnelId, logger, ct).ConfigureAwait(false);
var existingPorts = await devTunnelClient.GetPortListAsync(resolvedTunnelId, logger, ct).ConfigureAwait(false);
var modeledPortNumbers = (await Task.WhenAll(tunnelResource.Ports.Select(p => p.GetTunnelPortAsync(ct).AsTask())).ConfigureAwait(false)).ToHashSet();
var unmodeledPorts = existingPorts.Ports.Where(p => !modeledPortNumbers.Contains(p.PortNumber)).ToList();
if (unmodeledPorts.Count > 0)
{
logger.LogInformation("Deleting {Count} unmodeled ports from dev tunnel '{TunnelId}': {Ports}", unmodeledPorts.Count, tunnelResource.TunnelId, string.Join(", ", unmodeledPorts.Select(p => p.PortNumber)));
await Task.WhenAll(unmodeledPorts.Select(p => devTunnelClient.DeletePortAsync(tunnelResource.ResolvedTunnelId, p.PortNumber, logger, ct))).ConfigureAwait(false);
await Task.WhenAll(unmodeledPorts.Select(p => devTunnelClient.DeletePortAsync(resolvedTunnelId, p.PortNumber, logger, ct))).ConfigureAwait(false);
}
}

Expand All @@ -200,7 +205,7 @@ await notifications.PublishUpdateAsync(portResource, snapshot => snapshot with
try
{
_ = await devTunnelClient.CreatePortAsync(
portResource.DevTunnel.ResolvedTunnelId,
resolvedTunnelId,
tunnelPort,
portResource.Options,
portLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ public async Task OnBeforeResourceStarted_WithRegion_UsesResolvedTunnelIdForPort
{
var client = new TestDevTunnelClient
{
CreatedTunnelId = "mytunnel.eun1",
PortList = new()
{
Ports = [
Expand Down Expand Up @@ -228,6 +229,102 @@ public async Task OnBeforeResourceStarted_WithRegion_UsesResolvedTunnelIdForPort
Assert.Contains(calls, call => call.Method == nameof(IDevTunnelClient.DeletePortAsync) && call.TunnelId == "mytunnel.eun1" && call.PortNumber == 6000);
}

[Fact]
public async Task OnBeforeResourceStarted_WithAutoSelectedRegion_UsesCreatedTunnelIdForPortOperations()
{
var client = new TestDevTunnelClient
{
CreatedTunnelId = "mytunnel.eun1",
PortList = new()
{
Ports = [
new(5001, "https"),
new(6000, "https")
]
}
};

using var builder = TestDistributedApplicationBuilder.Create();
builder.Services.AddSingleton<IDevTunnelClient>(client);
builder.Services.AddSingleton<IRequiredCommandValidator, TestRequiredCommandValidator>();

var target = builder.AddProject<ProjectA>("target")
.WithHttpEndpoint(port: 5000, targetPort: 5001, name: "http");
var tunnel = builder.AddDevTunnel("tunnel", "mytunnel")
.WithReference(target);
var tunnelPort = Assert.Single(tunnel.Resource.Ports);
tunnelPort.TargetEndpoint.EndpointAnnotation.AllocatedEndpoint = new(
tunnelPort.TargetEndpoint.EndpointAnnotation,
"localhost",
5000);

using var app = builder.Build();

await builder.Eventing.PublishAsync(new BeforeResourceStartedEvent(tunnel.Resource, app.Services)).DefaultTimeout();

var calls = client.Calls.ToArray();
Assert.Contains(calls, call => call.Method == nameof(IDevTunnelClient.CreateTunnelAsync) && call.TunnelId == "mytunnel");
Assert.Contains(calls, call => call.Method == nameof(IDevTunnelClient.GetPortListAsync) && call.TunnelId == "mytunnel.eun1");
Assert.Contains(calls, call => call.Method == nameof(IDevTunnelClient.CreatePortAsync) && call.TunnelId == "mytunnel.eun1" && call.PortNumber == 5001);
Assert.Contains(calls, call => call.Method == nameof(IDevTunnelClient.DeletePortAsync) && call.TunnelId == "mytunnel.eun1" && call.PortNumber == 6000);
}

[Fact]
public async Task DevTunnelHealthCheck_WithAutoSelectedRegion_UsesReturnedTunnelIdForAccessOperations()
{
var client = new TestDevTunnelClient
{
TunnelStatus = new("mytunnel.eun1", HostConnections: 1, ClientConnections: 0, Description: "", Labels: [])
{
Ports = [
new(5001, "https")
{
PortUri = new("https://mytunnel-5001.eun1.devtunnels.ms")
}
]
}
};

using var builder = TestDistributedApplicationBuilder.Create();
builder.Services.AddSingleton<IDevTunnelClient>(client);

var target = builder.AddProject<ProjectA>("target")
.WithHttpEndpoint(port: 5000, targetPort: 5001, name: "http");
var tunnel = builder.AddDevTunnel("tunnel", "mytunnel")
.WithReference(target);

using var app = builder.Build();
var healthCheck = new DevTunnelHealthCheck(
client,
app.Services.GetRequiredService<LoggedOutNotificationManager>(),
tunnel.Resource,
app.Services.GetRequiredService<ILogger<DevTunnelHealthCheck>>());

var result = await healthCheck.CheckHealthAsync(new HealthCheckContext()).DefaultTimeout();

Assert.Equal(HealthStatus.Healthy, result.Status);
Assert.Collection(
client.Calls,
call =>
{
Assert.Equal(nameof(IDevTunnelClient.GetTunnelAsync), call.Method);
Assert.Equal("mytunnel", call.TunnelId);
Assert.Null(call.PortNumber);
},
call =>
{
Assert.Equal(nameof(IDevTunnelClient.GetAccessAsync), call.Method);
Assert.Equal("mytunnel.eun1", call.TunnelId);
Assert.Null(call.PortNumber);
},
call =>
{
Assert.Equal(nameof(IDevTunnelClient.GetAccessAsync), call.Method);
Assert.Equal("mytunnel.eun1", call.TunnelId);
Assert.Equal(5001, call.PortNumber);
});
}

[Fact]
public async Task DevTunnelHealthCheck_WithRegion_UsesResolvedTunnelIdForTunnelAndAccessOperations()
{
Expand Down
4 changes: 3 additions & 1 deletion tests/Aspire.Hosting.DevTunnels.Tests/TestDevTunnelClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ internal sealed class TestDevTunnelClient(Version? cliVersion = null) : IDevTunn

public DevTunnelStatus TunnelStatus { get; set; } = new("test-tunnel", HostConnections: 1, ClientConnections: 0, Description: "", Labels: []);

public string? CreatedTunnelId { get; set; }

public DevTunnelAccessStatus AccessStatus { get; set; } = new();

public Task<Version> GetVersionAsync(ILogger? logger = null, CancellationToken cancellationToken = default)
Expand All @@ -42,7 +44,7 @@ public Task<UserLoginStatus> UserLoginAsync(LoginProvider provider, ILogger? log
public Task<DevTunnelStatus> CreateTunnelAsync(string tunnelId, DevTunnelOptions options, ILogger? logger = null, CancellationToken cancellationToken = default)
{
Calls.Enqueue(new(nameof(CreateTunnelAsync), tunnelId));
return Task.FromResult(new DevTunnelStatus(tunnelId, HostConnections: 1, ClientConnections: 0, Description: "", Labels: []));
return Task.FromResult(new DevTunnelStatus(CreatedTunnelId ?? tunnelId, HostConnections: 1, ClientConnections: 0, Description: "", Labels: []));
}

public Task<DevTunnelPortList> GetPortListAsync(string tunnelId, ILogger? logger = null, CancellationToken cancellationToken = default)
Expand Down
Loading