From b56712d35d96aa2d8a0e9542be168be2365bcd7a Mon Sep 17 00:00:00 2001 From: Eric Sondergard <4650644+esond@users.noreply.github.com> Date: Mon, 11 May 2026 21:05:34 -0600 Subject: [PATCH 1/2] Fix Ngrok integration on Aspire 13.3 by relocating WithArgs and config file write Aspire 13.3 reworked OnResourceEndpointsAllocated callback timing, which caused WithArgs and YAML config writes deferred inside that callback to be dropped. The ngrok container started without args and without its config file, then exited with "configuration file must define at least one tunnel when using --all". Move WithArgs onto the builder chain via the context callback form, and move the YAML config write into OnBeforeResourceStarted (which fires after endpoint allocation and before the container starts on Aspire 13.3). --- .../NgrokExtensions.cs | 18 +++-- .../NgrokArgsTests.cs | 66 +++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/NgrokArgsTests.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs index da930a5b2..6f5e9e559 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs @@ -57,18 +57,24 @@ public static IResourceBuilder AddNgrok( .WithImage(NgrokContainerValues.Image, NgrokContainerValues.Tag) .WithImageRegistry(NgrokContainerValues.Registry) .WithBindMount(configurationFolder, "/var/tmp/ngrok") - .WithHttpEndpoint(targetPort: 4040, port: endpointPort, name: endpointName); - resourceBuilder.OnResourceEndpointsAllocated(async (resource, e, ct) => + .WithHttpEndpoint(targetPort: 4040, port: endpointPort, name: endpointName) + .WithArgs(context => + { + var hasEndpoints = context.Resource.Annotations + .OfType() + .Any(annotation => annotation.Endpoints.Count > 0); + context.Args.Add("start"); + context.Args.Add(hasEndpoints ? "--all" : "--none"); + context.Args.Add("--config"); + context.Args.Add($"/var/tmp/ngrok/{name}.yml"); + }); + resourceBuilder.OnBeforeResourceStarted(async (resource, e, ct) => { var endpointTuples = resource.Annotations .OfType() .SelectMany(annotation => annotation.Endpoints.Select(ngrokEndpoint => (endpointRefernce: annotation.Resource.GetEndpoint(ngrokEndpoint.EndpointName), ngrokEndpoint))) .ToList(); await CreateNgrokConfigurationFileAsync(configurationFolder, name, endpointTuples, configurationVersion ?? 3); - - resourceBuilder.WithArgs( - "start", endpointTuples.Count > 0 ? "--all" : "--none", - "--config", $"/var/tmp/ngrok/{name}.yml"); }); return resourceBuilder; } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/NgrokArgsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/NgrokArgsTests.cs new file mode 100644 index 000000000..4d230b9ee --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/NgrokArgsTests.cs @@ -0,0 +1,66 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Ngrok.Tests; + +public class NgrokArgsTests +{ + [Fact] + public async Task AddNgrokWithNoTunnelEndpointsPassesStartNoneAndConfigPath() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddNgrok("ngrok"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var argsAnnotation = Assert.Single(resource.Annotations.OfType()); + var context = new CommandLineArgsCallbackContext([], resource, CancellationToken.None); + await argsAnnotation.Callback(context); + + Assert.Equal(["start", "--none", "--config", "/var/tmp/ngrok/ngrok.yml"], context.Args); + } + + [Fact] + public async Task AddNgrokWithTunnelEndpointPassesStartAllAndConfigPath() + { + var builder = DistributedApplication.CreateBuilder(); + + var api = builder.AddProject("api"); + + builder.AddNgrok("ngrok") + .WithTunnelEndpoint(api, "http"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var argsAnnotation = Assert.Single(resource.Annotations.OfType()); + var context = new CommandLineArgsCallbackContext([], resource, CancellationToken.None); + await argsAnnotation.Callback(context); + + Assert.Equal(["start", "--all", "--config", "/var/tmp/ngrok/ngrok.yml"], context.Args); + } + + [Fact] + public async Task AddNgrokArgsUseResourceNameInConfigPath() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddNgrok("my-tunnel"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var argsAnnotation = Assert.Single(resource.Annotations.OfType()); + var context = new CommandLineArgsCallbackContext([], resource, CancellationToken.None); + await argsAnnotation.Callback(context); + + Assert.Contains("/var/tmp/ngrok/my-tunnel.yml", context.Args); + } +} From 69ac87df4cdcb801fc6d7d352ba8f357e1bbfcd7 Mon Sep 17 00:00:00 2001 From: Eric Sondergard <4650644+esond@users.noreply.github.com> Date: Mon, 11 May 2026 21:11:31 -0600 Subject: [PATCH 2/2] Rename tuple element endpointRefernce -> endpointReference Matches the deconstruction name used in CreateNgrokConfigurationFileAsync. --- src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs index 6f5e9e559..bcd922862 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs @@ -72,7 +72,7 @@ public static IResourceBuilder AddNgrok( { var endpointTuples = resource.Annotations .OfType() - .SelectMany(annotation => annotation.Endpoints.Select(ngrokEndpoint => (endpointRefernce: annotation.Resource.GetEndpoint(ngrokEndpoint.EndpointName), ngrokEndpoint))) + .SelectMany(annotation => annotation.Endpoints.Select(ngrokEndpoint => (endpointReference: annotation.Resource.GetEndpoint(ngrokEndpoint.EndpointName), ngrokEndpoint))) .ToList(); await CreateNgrokConfigurationFileAsync(configurationFolder, name, endpointTuples, configurationVersion ?? 3); });