diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 8d329f24a6d..d67d2199e99 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -45,6 +45,8 @@ internal record struct HostResourceWithEndpoints( /// internal sealed class ContainerCreator : IObjectCreator, IObjectCreator { + private const string ContainerTunnelContainerName = "aspire"; + private readonly IConfiguration _configuration; private readonly IOptions _options; private readonly DcpNameGenerator _nameGenerator; @@ -123,7 +125,9 @@ internal void PrepareContainerNetworks() public IEnumerable> PrepareObjects() { - var modelContainerResources = _model.GetContainerResources(); + var modelContainerResources = _model.GetContainerResources().ToArray(); + ValidateContainerTunnelContainerNameConflicts(modelContainerResources); + var result = new List>(); foreach (var container in modelContainerResources) @@ -194,6 +198,39 @@ public IEnumerable> PrepareObjects() return result; } + private void ValidateContainerTunnelContainerNameConflicts(IEnumerable modelContainerResources) + { + if (!_options.Value.EnableAspireContainerTunnel) + { + return; + } + + foreach (var container in modelContainerResources) + { + if (IsContainerTunnelContainerName(container.Name)) + { + throw new DistributedApplicationException($"Container resource name '{container.Name}' conflicts with the Aspire container tunnel container name '{ContainerTunnelContainerName}'. Rename the resource or disable the Aspire container tunnel."); + } + + if (container.TryGetLastAnnotation(out var containerNameAnnotation) && + IsContainerTunnelContainerName(containerNameAnnotation.Name)) + { + throw new DistributedApplicationException($"Container resource '{container.Name}' uses container name '{containerNameAnnotation.Name}', which conflicts with the Aspire container tunnel container name '{ContainerTunnelContainerName}'. Rename the container or disable the Aspire container tunnel."); + } + + foreach (var aliasAnnotation in container.Annotations.OfType()) + { + if (IsContainerTunnelContainerName(aliasAnnotation.Alias)) + { + throw new DistributedApplicationException($"Container resource '{container.Name}' uses network alias '{aliasAnnotation.Alias}', which conflicts with the Aspire container tunnel container name '{ContainerTunnelContainerName}'. Rename the alias or disable the Aspire container tunnel."); + } + } + } + + static bool IsContainerTunnelContainerName(string name) + => string.Equals(name, ContainerTunnelContainerName, StringComparison.OrdinalIgnoreCase); + } + public bool IsReadyToCreate(RenderedModelResource resource, ContainerCreationContext cctx) { // Containers are always "created" (submitted to DCP), they just get Spec.Start = false initially diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 0dee41d64fe..e39ecadbbe2 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -293,6 +293,96 @@ public async Task CreateContainer_ArgsResolvedInSnapshot_UsesEffectiveArgsFromCr Assert.Equal(effectiveArgs, GetEnumerablePropertyValue(snapshot, KnownProperties.Container.Args).ToArray()); } + [Theory] + [InlineData("aspire")] + [InlineData("ASPIRE")] + public async Task RunApplicationAsync_ThrowsWhenContainerResourceNameConflictsWithContainerTunnelName(string containerName) + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer(containerName, "image"); + + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: new TestKubernetesService(), + dcpOptions: new DcpOptions { EnableAspireContainerTunnel = true }); + + var ex = await Assert.ThrowsAsync(() => appExecutor.RunApplicationAsync()); + Assert.Contains("container tunnel container name", ex.Message); + } + + [Theory] + [InlineData("aspire")] + [InlineData("ASPIRE")] + public async Task RunApplicationAsync_ThrowsWhenExplicitContainerNameConflictsWithContainerTunnelName(string containerName) + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("aContainer", "image") + .WithContainerName(containerName); + + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: new TestKubernetesService(), + dcpOptions: new DcpOptions { EnableAspireContainerTunnel = true }); + + var ex = await Assert.ThrowsAsync(() => appExecutor.RunApplicationAsync()); + Assert.Contains("container tunnel container name", ex.Message); + } + + [Theory] + [InlineData("aspire")] + [InlineData("ASPIRE")] + public async Task RunApplicationAsync_ThrowsWhenNetworkAliasConflictsWithContainerTunnelName(string alias) + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("aContainer", "image") + .WithContainerNetworkAlias(alias); + + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: new TestKubernetesService(), + dcpOptions: new DcpOptions { EnableAspireContainerTunnel = true }); + + var ex = await Assert.ThrowsAsync(() => appExecutor.RunApplicationAsync()); + Assert.Contains("container tunnel container name", ex.Message); + } + + [Fact] + public async Task RunApplicationAsync_AllowsContainerNameMatchingContainerTunnelNameWhenContainerTunnelDisabled() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("aspire", "image"); + builder.AddContainer("aContainer", "image") + .WithContainerName("ASPIRE"); + builder.AddContainer("bContainer", "image") + .WithContainerNetworkAlias("ASPIRE"); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: kubernetesService, + dcpOptions: new DcpOptions { EnableAspireContainerTunnel = false }); + + await appExecutor.RunApplicationAsync(); + + Assert.Equal(3, kubernetesService.CreatedResources.OfType().Count()); + } + [Fact] public async Task ResourceRestarted_EnvironmentCallbacksApplied() {