Skip to content

Commit

Permalink
Hosting.Ngrok - support v2 configuration file with labels (#447)
Browse files Browse the repository at this point in the history
* Add labels to endpoint & version to output.

* Set tunnel endpoint labels.

* Add missing colon.

* Add labels to endpoint & version to output.

* Set tunnel endpoint labels.

* Add missing colon.

---------

Co-authored-by: Sascha Kiefer <[email protected]>
Co-authored-by: Aaron Powell <[email protected]>
  • Loading branch information
3 people authored Feb 11, 2025
1 parent 592658e commit b1e91d5
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 25 deletions.
3 changes: 2 additions & 1 deletion src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ namespace Aspire.Hosting.ApplicationModel;
/// </summary>
/// <param name="EndpointName">A unique name for this endpoint's configuration.</param>
/// <param name="Url">The address you would like to use to forward traffic to your upstream service. Leave empty to get a randomly assigned address.</param>
public sealed record NgrokEndpoint(string EndpointName, string? Url);
/// <param name="Labels">An optional dictionary of labels to apply to the endpoint.</param>
public sealed record NgrokEndpoint(string EndpointName, string? Url, IDictionary<string, string>? Labels = null);
73 changes: 52 additions & 21 deletions src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ public static class NgrokExtensions
/// <param name="configurationFolder">The folder where temporary ngrok configuration files will be stored; defaults to .ngrok</param>
/// <param name="endpointPort">The port of the endpoint for this resource, defaults to a randomly assigned port.</param>
/// <param name="endpointName">The name of the endpoint for this resource, defaults to http.</param>
/// <param name="configurationVersion">The output version of the ngrok configuration file.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{TResource}"/>.</returns>
public static IResourceBuilder<NgrokResource> 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)
Expand All @@ -38,6 +40,11 @@ public static IResourceBuilder<NgrokResource> 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))
Expand All @@ -55,7 +62,7 @@ public static IResourceBuilder<NgrokResource> AddNgrok(
.OfType<NgrokEndpointAnnotation>()
.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",
Expand Down Expand Up @@ -104,25 +111,26 @@ public static IResourceBuilder<NgrokResource> WithTunnelEndpoint<TResource>(
this IResourceBuilder<NgrokResource> builder,
IResourceBuilder<TResource> resource,
string endpointName,
string? ngrokUrl = null) where TResource : IResourceWithEndpoints
string? ngrokUrl = null,
IDictionary<string, string>? 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<NgrokEndpointAnnotation>()
.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);
}

Expand All @@ -132,25 +140,48 @@ public static IResourceBuilder<NgrokResource> WithTunnelEndpoint<TResource>(
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string!, string!>? 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<string!, string!>?
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.ApplicationModel.NgrokEndpoint!>!
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<Aspire.Hosting.ApplicationModel.NgrokResource!>!
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<Aspire.Hosting.ApplicationModel.NgrokResource!>!
static Aspire.Hosting.NgrokExtensions.WithAuthToken(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>! builder, string! ngrokAuthToken) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>!
static Aspire.Hosting.NgrokExtensions.WithAuthToken(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! ngrokAuthToken) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>!
static Aspire.Hosting.NgrokExtensions.WithTunnelEndpoint<TResource>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder<TResource>! resource, string! endpointName, string? ngrokUrl = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>!
static Aspire.Hosting.NgrokExtensions.WithTunnelEndpoint<TResource>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder<TResource>! resource, string! endpointName, string? ngrokUrl = null, System.Collections.Generic.IDictionary<string!, string!>? labels = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.NgrokResource!>!
Original file line number Diff line number Diff line change
Expand Up @@ -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<Projects.CommunityToolkit_Aspire_Hosting_Ngrok_ApiService>("api");

builder.AddNgrok("ngrok")
.WithTunnelEndpoint(api,"http");

using var app = builder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var resource = Assert.Single(appModel.Resources.OfType<NgrokResource>());
var annotation = Assert.Single(resource.Annotations.OfType<NgrokEndpointAnnotation>());
var endpoint = Assert.Single(annotation.Endpoints);

Assert.Null(endpoint.Labels);
}

[Fact]
public void WithTunnelEndpointSetsLabels()
{
var builder = DistributedApplication.CreateBuilder();

var api = builder
.AddProject<Projects.CommunityToolkit_Aspire_Hosting_Ngrok_ApiService>("api");

builder.AddNgrok("ngrok")
.WithTunnelEndpoint(api,"http", "custom-url", new Dictionary<string, string>() { ["key"] = "value" });

using var app = builder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var resource = Assert.Single(appModel.Resources.OfType<NgrokResource>());
var annotation = Assert.Single(resource.Annotations.OfType<NgrokEndpointAnnotation>());
var endpoint = Assert.Single(annotation.Endpoints);

Assert.Equal("key", endpoint.Labels!.Keys.First());
Assert.Equal("value", endpoint.Labels!["key"]);
}

[Fact]
public void WithTunnelNullResourceBuilderThrows()
Expand Down

0 comments on commit b1e91d5

Please sign in to comment.