diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokEndpoint.cs b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokEndpoint.cs index 563b89dd..eabcb6c3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokEndpoint.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokEndpoint.cs @@ -5,4 +5,5 @@ namespace Aspire.Hosting.ApplicationModel; /// /// A unique name for this endpoint's configuration. /// The address you would like to use to forward traffic to your upstream service. Leave empty to get a randomly assigned address. -public sealed record NgrokEndpoint(string EndpointName, string? Url); \ No newline at end of file +/// An optional dictionary of labels to apply to the endpoint. +public sealed record NgrokEndpoint(string EndpointName, string? Url, IDictionary? Labels = null); \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs index 12ba5ecb..dc05ade6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs @@ -21,13 +21,15 @@ public static class NgrokExtensions /// The folder where temporary ngrok configuration files will be stored; defaults to .ngrok /// The port of the endpoint for this resource, defaults to a randomly assigned port. /// The name of the endpoint for this resource, defaults to http. + /// The output version of the ngrok configuration file. /// A reference to the . public static IResourceBuilder AddNgrok( this IDistributedApplicationBuilder builder, [ResourceName] string name, string? configurationFolder = null, int? endpointPort = null, - [EndpointName] string? endpointName = null) + [EndpointName] string? endpointName = null, + int? configurationVersion = null) { ArgumentNullException.ThrowIfNull(builder); if (configurationFolder is not null) @@ -38,6 +40,11 @@ public static IResourceBuilder AddNgrok( ArgumentOutOfRangeException.ThrowIfLessThan(endpointPort.Value, 1, nameof(endpointPort)); ArgumentOutOfRangeException.ThrowIfGreaterThan(endpointPort.Value, 65535, nameof(endpointPort)); } + if (configurationVersion is not null) + { + ArgumentOutOfRangeException.ThrowIfLessThan(configurationVersion.Value, 2, nameof(configurationVersion)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(configurationVersion.Value, 3, nameof(configurationVersion)); + } configurationFolder ??= Path.Combine(builder.AppHostDirectory, ".ngrok"); if (!Directory.Exists(configurationFolder)) @@ -55,7 +62,7 @@ public static IResourceBuilder AddNgrok( .OfType() .SelectMany(annotation => annotation.Endpoints.Select(ngrokEndpoint => (endpointRefernce: annotation.Resource.GetEndpoint(ngrokEndpoint.EndpointName), ngrokEndpoint))) .ToList(); - await CreateNgrokConfigurationFileAsync(configurationFolder, name, endpointTuples); + await CreateNgrokConfigurationFileAsync(configurationFolder, name, endpointTuples, configurationVersion ?? 3); resourceBuilder.WithArgs( "start", endpointTuples.Count > 0 ? "--all" : "--none", @@ -104,25 +111,26 @@ public static IResourceBuilder WithTunnelEndpoint( this IResourceBuilder builder, IResourceBuilder resource, string endpointName, - string? ngrokUrl = null) where TResource : IResourceWithEndpoints + string? ngrokUrl = null, + IDictionary? labels = null) where TResource : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(resource); ArgumentException.ThrowIfNullOrWhiteSpace(endpointName); if (ngrokUrl is not null) ArgumentException.ThrowIfNullOrWhiteSpace(ngrokUrl); - + var existingAnnotation = builder.Resource.Annotations .OfType() .SingleOrDefault(a => a.Resource.Name == resource.Resource.Name); if (existingAnnotation is not null) { - existingAnnotation.Endpoints.Add(new NgrokEndpoint(endpointName, ngrokUrl)); + existingAnnotation.Endpoints.Add(new NgrokEndpoint(endpointName, ngrokUrl, labels)); } else { var newAnnotation = new NgrokEndpointAnnotation(resource.Resource); - newAnnotation.Endpoints.Add(new NgrokEndpoint(endpointName, ngrokUrl)); + newAnnotation.Endpoints.Add(new NgrokEndpoint(endpointName, ngrokUrl, labels)); builder.Resource.Annotations.Add(newAnnotation); } @@ -132,25 +140,48 @@ public static IResourceBuilder WithTunnelEndpoint( private static async Task CreateNgrokConfigurationFileAsync( string configurationFolder, string name, - IList<(EndpointReference, NgrokEndpoint)> endpointTuples) + IList<(EndpointReference, NgrokEndpoint)> endpointTuples, + int configurationVersion) { var ngrokConfig = new StringBuilder(); - ngrokConfig.AppendLine("version: 3"); + ngrokConfig.AppendLine($"version: {configurationVersion}"); ngrokConfig.AppendLine(); - ngrokConfig.AppendLine("agent:"); - ngrokConfig.AppendLine( " log: stdout"); - if (endpointTuples.Count > 0) + switch (configurationVersion) { - ngrokConfig.AppendLine(); - ngrokConfig.AppendLine("endpoints:"); - foreach (var (endpointReference, ngrokEndpoint) in endpointTuples) - { - ngrokConfig.AppendLine($" - name: {endpointReference.Resource.Name}-{endpointReference.EndpointName}"); - if (!string.IsNullOrWhiteSpace(ngrokEndpoint.Url)) - ngrokConfig.AppendLine($" url: {ngrokEndpoint.Url}"); - ngrokConfig.AppendLine(" upstream:"); - ngrokConfig.AppendLine($" url: {GetUpstreamUrl(endpointReference)}"); - } + case 2: + ngrokConfig.AppendLine("tunnels:"); + foreach (var (endpointReference, ngrokEndpoint) in endpointTuples) + { + ngrokConfig.AppendLine($" {endpointReference.Resource.Name}-{endpointReference.EndpointName}:"); + if (ngrokEndpoint.Labels is null) + continue; + ngrokConfig.AppendLine(" labels:"); + foreach (var label in ngrokEndpoint.Labels) + { + ngrokConfig.AppendLine($" - {label.Key}={label.Value}"); + } + ngrokConfig.AppendLine($" addr: {GetUpstreamUrl(endpointReference)}"); + } + break; + case 3: + ngrokConfig.AppendLine("agent:"); + ngrokConfig.AppendLine( " log: stdout"); + if (endpointTuples.Count > 0) + { + ngrokConfig.AppendLine(); + ngrokConfig.AppendLine("endpoints:"); + foreach (var (endpointReference, ngrokEndpoint) in endpointTuples) + { + ngrokConfig.AppendLine($" - name: {endpointReference.Resource.Name}-{endpointReference.EndpointName}"); + if (!string.IsNullOrWhiteSpace(ngrokEndpoint.Url)) + ngrokConfig.AppendLine($" url: {ngrokEndpoint.Url}"); + ngrokConfig.AppendLine(" upstream:"); + ngrokConfig.AppendLine($" url: {GetUpstreamUrl(endpointReference)}"); + } + } + break; + default: + break; } await File.WriteAllTextAsync(Path.Combine(configurationFolder, $"{name}.yml"), ngrokConfig.ToString()); diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.Ngrok/PublicAPI.Shipped.txt index 3b5032d8..206a9eb5 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/PublicAPI.Shipped.txt +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/PublicAPI.Shipped.txt @@ -2,18 +2,20 @@ Aspire.Hosting.ApplicationModel.NgrokResource Aspire.Hosting.ApplicationModel.NgrokResource.NgrokResource(string! name) -> void Aspire.Hosting.ApplicationModel.NgrokEndpoint -Aspire.Hosting.ApplicationModel.NgrokEndpoint.NgrokEndpoint(string! EndpointName, string? Url) -> void +Aspire.Hosting.ApplicationModel.NgrokEndpoint.NgrokEndpoint(string! EndpointName, string? Url, System.Collections.Generic.IDictionary? Labels = null) -> void Aspire.Hosting.ApplicationModel.NgrokEndpoint.EndpointName.get -> string! Aspire.Hosting.ApplicationModel.NgrokEndpoint.EndpointName.init -> void Aspire.Hosting.ApplicationModel.NgrokEndpoint.Url.get -> string? Aspire.Hosting.ApplicationModel.NgrokEndpoint.Url.init -> void +Aspire.Hosting.ApplicationModel.NgrokEndpoint.Labels.get -> System.Collections.Generic.IDictionary? +Aspire.Hosting.ApplicationModel.NgrokEndpoint.Labels.init -> void Aspire.Hosting.ApplicationModel.NgrokEndpointAnnotation Aspire.Hosting.ApplicationModel.NgrokEndpointAnnotation.NgrokEndpointAnnotation(Aspire.Hosting.ApplicationModel.IResourceWithEndpoints! Resource) -> void Aspire.Hosting.ApplicationModel.NgrokEndpointAnnotation.Resource.get -> Aspire.Hosting.ApplicationModel.IResourceWithEndpoints! Aspire.Hosting.ApplicationModel.NgrokEndpointAnnotation.Resource.init -> void Aspire.Hosting.ApplicationModel.NgrokEndpointAnnotation.Endpoints.get -> System.Collections.Generic.ICollection! Aspire.Hosting.NgrokExtensions -static Aspire.Hosting.NgrokExtensions.AddNgrok(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string? configurationFolder = null, int? endpointPort = null, string? endpointName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.NgrokExtensions.AddNgrok(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string? configurationFolder = null, int? endpointPort = null, string? endpointName = null, int? configurationVersion = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.NgrokExtensions.WithAuthToken(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! ngrokAuthToken) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.NgrokExtensions.WithAuthToken(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! ngrokAuthToken) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! -static Aspire.Hosting.NgrokExtensions.WithTunnelEndpoint(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! resource, string! endpointName, string? ngrokUrl = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.NgrokExtensions.WithTunnelEndpoint(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! resource, string! endpointName, string? ngrokUrl = null, System.Collections.Generic.IDictionary? labels = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/WithTunnelEndpointTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/WithTunnelEndpointTests.cs index be5d26fa..cbef007d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/WithTunnelEndpointTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/WithTunnelEndpointTests.cs @@ -66,6 +66,49 @@ public void WithTunnelEndpointSetsAnnotationUrl() Assert.Equal("custom-url", endpoint.Url); } + + [Fact] + public void WithTunnelEndpointSetsLabelsToNullByDefault() + { + 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 annotation = Assert.Single(resource.Annotations.OfType()); + var endpoint = Assert.Single(annotation.Endpoints); + + Assert.Null(endpoint.Labels); + } + + [Fact] + public void WithTunnelEndpointSetsLabels() + { + var builder = DistributedApplication.CreateBuilder(); + + var api = builder + .AddProject("api"); + + builder.AddNgrok("ngrok") + .WithTunnelEndpoint(api,"http", "custom-url", new Dictionary() { ["key"] = "value" }); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + var annotation = Assert.Single(resource.Annotations.OfType()); + var endpoint = Assert.Single(annotation.Endpoints); + + Assert.Equal("key", endpoint.Labels!.Keys.First()); + Assert.Equal("value", endpoint.Labels!["key"]); + } [Fact] public void WithTunnelNullResourceBuilderThrows()