Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,5 @@ FodyWeavers.xsd
**/*/azure.yaml
**/*/next-steps.md

# Nginx config files generated by WithContainerProxy
/samples/Metrics/MetricsApp.AppHost/*-proxy.nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace MetricsApp.AppHost.ContainerProxy;

internal sealed class ContainerProxyAnnotation : IResourceAnnotation
{
public ContainerProxyAnnotation(IResourceWithEndpoints proxiedResource)
{
ProxiedResource = proxiedResource;
}

public string? FilePath { get; set; }

public IResourceWithEndpoints ProxiedResource { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Aspire.Hosting.Lifecycle;

namespace MetricsApp.AppHost.ContainerProxy;

public static class ContainerProxyResourceExtensions
{
/// <summary>
/// Adds nginx container with name of <see cref="projectResourceBuilder"/> resource, which will proxy requests from other <see cref="ContainerResource"/>s to <see cref="ProjectResource"/> identified by <see cref="projectResourceBuilder"/>.
/// </summary>
/// <param name="projectResourceBuilder"></param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<ProjectResource> WithContainerProxy(this IResourceBuilder<ProjectResource> projectResourceBuilder)
{
projectResourceBuilder.ApplicationBuilder.Services.TryAddLifecycleHook<ContainerProxyResourceLifecycleHook>();

projectResourceBuilder.ApplicationBuilder.AddContainer(projectResourceBuilder.Resource.Name + "-proxy", "nginx")
.WithAnnotation(new ContainerProxyAnnotation(projectResourceBuilder.Resource))
.WithContainerRuntimeArgs(context =>
{
context.Args.Add("--network-alias");
context.Args.Add(projectResourceBuilder.Resource.Name);

context.Args.Add("--hostname");
context.Args.Add(projectResourceBuilder.Resource.Name);
})
.WithHttpEndpoint(targetPort: 80, name: "http")
.ExcludeFromManifest();

return projectResourceBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Aspire.Hosting.Lifecycle;
using CliWrap;
using CliWrap.EventStream;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace MetricsApp.AppHost.ContainerProxy
{
internal sealed class ContainerProxyResourceLifecycleHook : IDistributedApplicationLifecycleHook, IAsyncDisposable
{
private const string NginxConfigTemplate = @"
events {}

http {
server {
listen 80;

location / {
proxy_pass http://{host}:{port};
}
}
}
";

private readonly ILogger<ContainerProxyResourceLifecycleHook> _logger;
private readonly IHostEnvironment _hostEnvironment;
private readonly string _networkNamePrefix;
private readonly string _networkName;

public ContainerProxyResourceLifecycleHook(
ILogger<ContainerProxyResourceLifecycleHook> logger,
IHostEnvironment hostEnvironment)
{
_logger = logger;
_hostEnvironment = hostEnvironment;

_networkNamePrefix = $"{hostEnvironment.ApplicationName}_Proxy_";
_networkName = hostEnvironment.ApplicationName + Guid.NewGuid().ToString().Substring(0, 6);
}

public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
await CleanupNetworks(CancellationToken.None).ConfigureAwait(false);

_logger.LogInformation("Creating docker network {Network}.", _networkName);
await Cli.Wrap("docker").WithArguments($"network create {_networkName}").ExecuteAsync(cancellationToken).ConfigureAwait(false);

foreach (var containerResource in appModel.Resources.OfType<ContainerResource>())
{
var proxyAnnotation = containerResource.Annotations.OfType<ContainerProxyAnnotation>().SingleOrDefault();
if (proxyAnnotation != null)
{
var confFilePath = Path.Combine(_hostEnvironment.ContentRootPath, $"{containerResource.Name}.nginx.conf");
proxyAnnotation.FilePath = confFilePath;
containerResource.Annotations.Add(new ContainerMountAnnotation(confFilePath, "/etc/nginx/nginx.conf", ContainerMountType.BindMount, true));
}

containerResource.Annotations.Add(new ContainerRuntimeArgsCallbackAnnotation(context =>
{
context.Add("--network");
context.Add(_networkName);
}));
}
}

public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
foreach (var containerResource in appModel.Resources.OfType<ContainerResource>())
{
var proxyAnnotation = containerResource.Annotations.OfType<ContainerProxyAnnotation>().SingleOrDefault();
if (proxyAnnotation != null && proxyAnnotation.FilePath != null)
{
var endpoint = proxyAnnotation.ProxiedResource.GetEndpoint("http");
var configString = NginxConfigTemplate
.Replace("{host}", "host.docker.internal")
.Replace("{port}", endpoint.Port.ToString());
File.WriteAllText(proxyAnnotation.FilePath, configString);
}
}

return Task.CompletedTask;
}

public async ValueTask DisposeAsync()
{
await CleanupNetworks(CancellationToken.None).ConfigureAwait(false);
}

private async Task CleanupNetworks(CancellationToken cancellationToken)
{
var oldNetworks = Cli.Wrap("docker")
.WithArguments("network ls --format '{{.Name}}'")
.ListenAsync(cancellationToken)
.ToBlockingEnumerable(cancellationToken)
.OfType<StandardOutputCommandEvent>()
.Where(e => e.Text.StartsWith(_networkNamePrefix));
foreach (var oldNetwork in oldNetworks)
{
_logger.LogInformation("Deleting docker network {Network}.", oldNetwork);
await Cli.Wrap("docker").WithArguments($"network rm {oldNetwork}").ExecuteAsync(cancellationToken).ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.0.1" />
<PackageReference Include="CliWrap" Version="3.6.6" />

<ProjectReference Include="..\MetricsApp\MetricsApp.csproj" />
</ItemGroup>
Expand Down
7 changes: 5 additions & 2 deletions samples/Metrics/MetricsApp.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
var builder = DistributedApplication.CreateBuilder(args);
using MetricsApp.AppHost.ContainerProxy;

var builder = DistributedApplication.CreateBuilder(args);

var grafana = builder.AddContainer("grafana", "grafana/grafana")
.WithBindMount("../grafana/config", "/etc/grafana", isReadOnly: true)
.WithBindMount("../grafana/dashboards", "/var/lib/grafana/dashboards", isReadOnly: true)
.WithHttpEndpoint(targetPort: 3000, name: "http");

builder.AddProject<Projects.MetricsApp>("app")
.WithEnvironment("GRAFANA_URL", grafana.GetEndpoint("http"));
.WithEnvironment("GRAFANA_URL", grafana.GetEndpoint("http"))
.WithContainerProxy();

builder.AddContainer("prometheus", "prom/prometheus")
.WithBindMount("../prometheus", "/etc/prometheus", isReadOnly: true)
Expand Down
4 changes: 2 additions & 2 deletions samples/Metrics/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
global:
scrape_interval: 1s # makes for a good demo
scrape_interval: 5s # makes for a good demo

scrape_configs:
- job_name: 'metricsapp'
static_configs:
- targets: ['host.docker.internal:5048'] # hard-coded port matches launchSettings.json
- targets: ['app']