Skip to content

Commit 044afc1

Browse files
committed
Add ability for ContainerResource to reach ProjectResource by addig nginx container as a proxy between containers and ProjectResources available on host.docker.internal
1 parent 3c2a172 commit 044afc1

File tree

7 files changed

+158
-4
lines changed

7 files changed

+158
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,5 @@ FodyWeavers.xsd
431431
**/*/azure.yaml
432432
**/*/next-steps.md
433433

434+
# Nginx config files generated by WithContainerProxy
435+
/samples/Metrics/MetricsApp.AppHost/*-proxy.nginx.conf
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace MetricsApp.AppHost.ContainerProxy;
2+
3+
internal sealed class ContainerProxyAnnotation : IResourceAnnotation
4+
{
5+
public ContainerProxyAnnotation(IResourceWithEndpoints proxiedResource)
6+
{
7+
ProxiedResource = proxiedResource;
8+
}
9+
10+
public string? FilePath { get; set; }
11+
12+
public IResourceWithEndpoints ProxiedResource { get; }
13+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Aspire.Hosting.Lifecycle;
2+
3+
namespace MetricsApp.AppHost.ContainerProxy;
4+
5+
public static class ContainerProxyResourceExtensions
6+
{
7+
/// <summary>
8+
/// 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"/>.
9+
/// </summary>
10+
/// <param name="projectResourceBuilder"></param>
11+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
12+
public static IResourceBuilder<ProjectResource> WithContainerProxy(this IResourceBuilder<ProjectResource> projectResourceBuilder)
13+
{
14+
projectResourceBuilder.ApplicationBuilder.Services.TryAddLifecycleHook<ContainerProxyResourceLifecycleHook>();
15+
16+
projectResourceBuilder.ApplicationBuilder.AddContainer(projectResourceBuilder.Resource.Name + "-proxy", "nginx")
17+
.WithAnnotation(new ContainerProxyAnnotation(projectResourceBuilder.Resource))
18+
.WithContainerRuntimeArgs(context =>
19+
{
20+
context.Args.Add("--network-alias");
21+
context.Args.Add(projectResourceBuilder.Resource.Name);
22+
23+
context.Args.Add("--hostname");
24+
context.Args.Add(projectResourceBuilder.Resource.Name);
25+
})
26+
.WithHttpEndpoint(targetPort: 80, name: "http")
27+
.ExcludeFromManifest();
28+
29+
return projectResourceBuilder;
30+
}
31+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using Aspire.Hosting.Lifecycle;
2+
using CliWrap;
3+
using CliWrap.EventStream;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace MetricsApp.AppHost.ContainerProxy
8+
{
9+
internal sealed class ContainerProxyResourceLifecycleHook : IDistributedApplicationLifecycleHook, IAsyncDisposable
10+
{
11+
private const string NginxConfigTemplate = @"
12+
events {}
13+
14+
http {
15+
server {
16+
listen 80;
17+
18+
location / {
19+
proxy_pass http://{host}:{port};
20+
}
21+
}
22+
}
23+
";
24+
25+
private readonly ILogger<ContainerProxyResourceLifecycleHook> _logger;
26+
private readonly IHostEnvironment _hostEnvironment;
27+
private readonly string _networkNamePrefix;
28+
private readonly string _networkName;
29+
30+
public ContainerProxyResourceLifecycleHook(
31+
ILogger<ContainerProxyResourceLifecycleHook> logger,
32+
IHostEnvironment hostEnvironment)
33+
{
34+
_logger = logger;
35+
_hostEnvironment = hostEnvironment;
36+
37+
_networkNamePrefix = $"{hostEnvironment.ApplicationName}_Proxy_";
38+
_networkName = hostEnvironment.ApplicationName + Guid.NewGuid().ToString().Substring(0, 6);
39+
}
40+
41+
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
42+
{
43+
await CleanupNetworks(CancellationToken.None).ConfigureAwait(false);
44+
45+
_logger.LogInformation("Creating docker network {Network}.", _networkName);
46+
await Cli.Wrap("docker").WithArguments($"network create {_networkName}").ExecuteAsync(cancellationToken).ConfigureAwait(false);
47+
48+
foreach (var containerResource in appModel.Resources.OfType<ContainerResource>())
49+
{
50+
var proxyAnnotation = containerResource.Annotations.OfType<ContainerProxyAnnotation>().SingleOrDefault();
51+
if (proxyAnnotation != null)
52+
{
53+
var confFilePath = Path.Combine(_hostEnvironment.ContentRootPath, $"{containerResource.Name}.nginx.conf");
54+
proxyAnnotation.FilePath = confFilePath;
55+
containerResource.Annotations.Add(new ContainerMountAnnotation(confFilePath, "/etc/nginx/nginx.conf", ContainerMountType.BindMount, true));
56+
}
57+
58+
containerResource.Annotations.Add(new ContainerRuntimeArgsCallbackAnnotation(context =>
59+
{
60+
context.Add("--network");
61+
context.Add(_networkName);
62+
}));
63+
}
64+
}
65+
66+
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
67+
{
68+
foreach (var containerResource in appModel.Resources.OfType<ContainerResource>())
69+
{
70+
var proxyAnnotation = containerResource.Annotations.OfType<ContainerProxyAnnotation>().SingleOrDefault();
71+
if (proxyAnnotation != null && proxyAnnotation.FilePath != null)
72+
{
73+
var endpoint = proxyAnnotation.ProxiedResource.GetEndpoint("http");
74+
var configString = NginxConfigTemplate
75+
.Replace("{host}", "host.docker.internal")
76+
.Replace("{port}", endpoint.Port.ToString());
77+
File.WriteAllText(proxyAnnotation.FilePath, configString);
78+
}
79+
}
80+
81+
return Task.CompletedTask;
82+
}
83+
84+
public async ValueTask DisposeAsync()
85+
{
86+
await CleanupNetworks(CancellationToken.None).ConfigureAwait(false);
87+
}
88+
89+
private async Task CleanupNetworks(CancellationToken cancellationToken)
90+
{
91+
var oldNetworks = Cli.Wrap("docker")
92+
.WithArguments("network ls --format '{{.Name}}'")
93+
.ListenAsync(cancellationToken)
94+
.ToBlockingEnumerable(cancellationToken)
95+
.OfType<StandardOutputCommandEvent>()
96+
.Where(e => e.Text.StartsWith(_networkNamePrefix));
97+
foreach (var oldNetwork in oldNetworks)
98+
{
99+
_logger.LogInformation("Deleting docker network {Network}.", oldNetwork);
100+
await Cli.Wrap("docker").WithArguments($"network rm {oldNetwork}").ExecuteAsync(cancellationToken).ConfigureAwait(false);
101+
}
102+
}
103+
}
104+
}

samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<ItemGroup>
1212
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.0.1" />
13+
<PackageReference Include="CliWrap" Version="3.6.6" />
1314

1415
<ProjectReference Include="..\MetricsApp\MetricsApp.csproj" />
1516
</ItemGroup>

samples/Metrics/MetricsApp.AppHost/Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
var builder = DistributedApplication.CreateBuilder(args);
1+
using MetricsApp.AppHost.ContainerProxy;
2+
3+
var builder = DistributedApplication.CreateBuilder(args);
24

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

810
builder.AddProject<Projects.MetricsApp>("app")
9-
.WithEnvironment("GRAFANA_URL", grafana.GetEndpoint("http"));
11+
.WithEnvironment("GRAFANA_URL", grafana.GetEndpoint("http"))
12+
.WithContainerProxy();
1013

1114
builder.AddContainer("prometheus", "prom/prometheus")
1215
.WithBindMount("../prometheus", "/etc/prometheus", isReadOnly: true)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
global:
2-
scrape_interval: 1s # makes for a good demo
2+
scrape_interval: 5s # makes for a good demo
33

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

0 commit comments

Comments
 (0)