From 2c90c6dc535b8fddd585682aa24564be273ab96f Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Tue, 22 Sep 2026 16:54:58 +0100 Subject: [PATCH] Route direct Testcontainers usage through the custom Docker Hub proxy Test fixtures that create containers directly via DotNet.Testcontainers (bypassing Aspire's container resource model) were still pulling images straight from their configured registry, so they weren't protected by the existing CUSTOM_CONTAINER_REGISTRY rate-limit mitigation used for Aspire-hosted resources. A generic '.WithContainerRegistryMirror()' builder extension isn't possible here: Testcontainers' fluent builder interface only exposes 'WithImage(string)', with no public getter for the image/registry already configured on a builder, so an extension method can't rewrite just the registry portion of an already-set image. Add a TestContainerRegistry.Resolve(string registry) helper to CommunityToolkit.Aspire.Testing instead, and use it when constructing the image string in every fixture that builds a Testcontainers image directly (Ollama, Meilisearch, SurrealDb, GoFeatureFlag, KurrentDB, SqlServer). The helper only swaps the registry when it's the default docker.io Docker Hub, so it's a no-op for the fixtures already using a different registry (SqlServer, KurrentDB) while still future-proofing them if that ever changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../GoFeatureFlagContainerFixture.cs | 2 +- .../SqlServerContainerFixture.cs | 5 ++-- .../KurrentDBContainerFixture.cs | 2 +- .../MeilisearchContainerFixture.cs | 2 +- .../OllamaContainerFeature.cs | 2 +- .../SurrealDbContainerFixture.cs | 2 +- .../TestContainerRegistry.cs | 29 +++++++++++++++++++ 7 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.Testing/TestContainerRegistry.cs diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagContainerFixture.cs b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagContainerFixture.cs index 7e7dfbab9..521f98f94 100644 --- a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagContainerFixture.cs +++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagContainerFixture.cs @@ -28,7 +28,7 @@ public async ValueTask InitializeAsync() if (RequiresDockerAttribute.IsSupported) { var source = Path.GetFullPath("./goff", Directory.GetCurrentDirectory()); - Container = new ContainerBuilder($"{GoFeatureFlagContainerImageTags.Registry}/{GoFeatureFlagContainerImageTags.Image}:{GoFeatureFlagContainerImageTags.Tag}") + Container = new ContainerBuilder($"{TestContainerRegistry.Resolve(GoFeatureFlagContainerImageTags.Registry)}/{GoFeatureFlagContainerImageTags.Image}:{GoFeatureFlagContainerImageTags.Tag}") .WithPortBinding(1031, true) .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPath("/health").ForPort(1031))) .WithBindMount(source, "/goff") diff --git a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/SqlServerContainerFixture.cs b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/SqlServerContainerFixture.cs index 774c1f27f..78b88b67a 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/SqlServerContainerFixture.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/SqlServerContainerFixture.cs @@ -1,4 +1,5 @@ -using Testcontainers.MsSql; +using CommunityToolkit.Aspire.Testing; +using Testcontainers.MsSql; namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests; @@ -25,7 +26,7 @@ public async ValueTask DisposeAsync() public static async Task CreateContainerAsync() { - var container = new MsSqlBuilder($"{Registry}/{Image}:{Tag}") + var container = new MsSqlBuilder($"{TestContainerRegistry.Resolve(Registry)}/{Image}:{Tag}") .Build(); await container.StartAsync(); diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs index bf7214c3a..99bd45b52 100644 --- a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs +++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs @@ -27,7 +27,7 @@ public async ValueTask InitializeAsync() { if (RequiresDockerAttribute.IsSupported) { - Container = new ContainerBuilder($"{KurrentDBContainerImageTags.Registry}/{KurrentDBContainerImageTags.Image}:{KurrentDBContainerImageTags.Tag}") + Container = new ContainerBuilder($"{TestContainerRegistry.Resolve(KurrentDBContainerImageTags.Registry)}/{KurrentDBContainerImageTags.Image}:{KurrentDBContainerImageTags.Tag}") .WithPortBinding(2113, true) .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(2113))) .WithEnvironment("KURRENTDB_CLUSTER_SIZE", "1") diff --git a/tests/CommunityToolkit.Aspire.Meilisearch.Tests/MeilisearchContainerFixture.cs b/tests/CommunityToolkit.Aspire.Meilisearch.Tests/MeilisearchContainerFixture.cs index ff8149656..907f8f2e2 100644 --- a/tests/CommunityToolkit.Aspire.Meilisearch.Tests/MeilisearchContainerFixture.cs +++ b/tests/CommunityToolkit.Aspire.Meilisearch.Tests/MeilisearchContainerFixture.cs @@ -45,7 +45,7 @@ public async ValueTask InitializeAsync() //The master key must be at least 16-bytes-long and composed of valid UTF-8 characters. _masterKey = param.GetDefaultValue(); - Container = new ContainerBuilder($"{MeilisearchContainerImageTags.Registry}/{MeilisearchContainerImageTags.Image}:{MeilisearchContainerImageTags.Tag}") + Container = new ContainerBuilder($"{TestContainerRegistry.Resolve(MeilisearchContainerImageTags.Registry)}/{MeilisearchContainerImageTags.Image}:{MeilisearchContainerImageTags.Tag}") .WithPortBinding(7700, true) .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(7700))) .WithEnvironment("MEILI_MASTER_KEY", _masterKey) diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaContainerFeature.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaContainerFeature.cs index 09c18e7aa..ab0e6884a 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaContainerFeature.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaContainerFeature.cs @@ -23,7 +23,7 @@ public async ValueTask InitializeAsync() { if (RequiresDockerAttribute.IsSupported) { - Container = new ContainerBuilder($"{OllamaContainerImageTags.Registry}/{OllamaContainerImageTags.Image}:{OllamaContainerImageTags.Tag}") + Container = new ContainerBuilder($"{TestContainerRegistry.Resolve(OllamaContainerImageTags.Registry)}/{OllamaContainerImageTags.Image}:{OllamaContainerImageTags.Tag}") .WithPortBinding(11434, true) .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(11434))) .Build(); diff --git a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbContainerFixture.cs b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbContainerFixture.cs index a12c4d5c4..a332b0618 100644 --- a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbContainerFixture.cs +++ b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbContainerFixture.cs @@ -48,7 +48,7 @@ public async ValueTask InitializeAsync() _password = paramGenerator.GetDefaultValue(); - Container = new ContainerBuilder($"{SurrealDbContainerImageTags.Registry}/{SurrealDbContainerImageTags.Image}:{SurrealDbContainerImageTags.Tag}") + Container = new ContainerBuilder($"{TestContainerRegistry.Resolve(SurrealDbContainerImageTags.Registry)}/{SurrealDbContainerImageTags.Image}:{SurrealDbContainerImageTags.Tag}") .WithPortBinding(_port, true) .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(_port).ForPath("/health"))) .WithEnvironment("SURREAL_USER", _username) diff --git a/tests/CommunityToolkit.Aspire.Testing/TestContainerRegistry.cs b/tests/CommunityToolkit.Aspire.Testing/TestContainerRegistry.cs new file mode 100644 index 000000000..f4394fd52 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Testing/TestContainerRegistry.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.Testing; + +/// +/// Helper for tests that create containers directly via the Testcontainers library (i.e. not through +/// Aspire's container resource model) so that they can also be routed through a custom container registry. +/// +public static class TestContainerRegistry +{ + private const string DockerHubRegistry = "docker.io"; + + /// + /// Resolves the registry to use for a Testcontainers image reference. If is the + /// default Docker Hub registry and the CUSTOM_CONTAINER_REGISTRY environment variable is set, the + /// custom registry is returned instead to avoid Docker Hub rate limiting. Otherwise, + /// is returned unchanged. + /// + /// The registry the image would otherwise be pulled from. + public static string Resolve(string registry) + { + string? customRegistry = Environment.GetEnvironmentVariable("CUSTOM_CONTAINER_REGISTRY"); + + return registry == DockerHubRegistry && !string.IsNullOrEmpty(customRegistry) + ? customRegistry + : registry; + } +}