Skip to content

Commit 5d6188d

Browse files
authored
feat: Relax Base64 auth provider and ignore path segments in Docker registry URLs (#1516)
1 parent dd1d1f1 commit 5d6188d

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

src/Testcontainers/Builders/Base64Provider.cs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,40 @@ public Base64Provider(JsonElement jsonElement, ILogger logger)
3939
}
4040

4141
/// <summary>
42-
/// Gets a predicate that determines whether a <see cref="JsonProperty" /> contains a Docker registry key.
42+
/// Determines whether the specified JSON property contains a Docker registry
43+
/// that matches the given registry host.
4344
/// </summary>
44-
public static Func<JsonProperty, string, bool> HasDockerRegistryKey { get; }
45-
= (property, hostname) => property.Name.Equals(hostname, StringComparison.OrdinalIgnoreCase) || property.Name.EndsWith("://" + hostname, StringComparison.OrdinalIgnoreCase);
45+
/// <param name="property">The JSON property to check.</param>
46+
/// <param name="registryHost">The registry host to match against.</param>
47+
/// <returns><c>true</c> if the property contains a matching Docker registry; otherwise, <c>false</c>.</returns>
48+
public static bool HasDockerRegistryName(JsonProperty property, string registryHost)
49+
{
50+
var propertyName = property.Name;
51+
52+
if (propertyName.Equals(registryHost, StringComparison.OrdinalIgnoreCase))
53+
{
54+
return true;
55+
}
56+
57+
if (propertyName.EndsWith("://" + registryHost, StringComparison.OrdinalIgnoreCase))
58+
{
59+
return true;
60+
}
61+
62+
if (TryGetHost(propertyName, out var propertyNameNormalized) && TryGetHost(registryHost, out var registryHostNormalized))
63+
{
64+
return string.Equals(propertyNameNormalized, registryHostNormalized, StringComparison.OrdinalIgnoreCase);
65+
}
66+
else
67+
{
68+
return false;
69+
}
70+
}
4671

4772
/// <inheritdoc />
4873
public bool IsApplicable(string hostname)
4974
{
50-
return !JsonValueKind.Undefined.Equals(_rootElement.ValueKind) && !JsonValueKind.Null.Equals(_rootElement.ValueKind) && _rootElement.EnumerateObject().Any(property => HasDockerRegistryKey(property, hostname));
75+
return !JsonValueKind.Undefined.Equals(_rootElement.ValueKind) && !JsonValueKind.Null.Equals(_rootElement.ValueKind) && _rootElement.EnumerateObject().Any(property => HasDockerRegistryName(property, hostname));
5176
}
5277

5378
/// <inheritdoc />
@@ -60,7 +85,7 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname)
6085
return null;
6186
}
6287

63-
var authProperty = _rootElement.EnumerateObject().LastOrDefault(property => HasDockerRegistryKey(property, hostname));
88+
var authProperty = _rootElement.EnumerateObject().LastOrDefault(property => HasDockerRegistryName(property, hostname));
6489

6590
if (JsonValueKind.Undefined.Equals(authProperty.Value.ValueKind))
6691
{
@@ -120,5 +145,27 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname)
120145
_logger.DockerRegistryCredentialFound(hostname);
121146
return new DockerRegistryAuthenticationConfiguration(authProperty.Name, credential[0], credential[1]);
122147
}
148+
149+
/// <summary>
150+
/// Tries to extract the host from the specified value.
151+
/// </summary>
152+
/// <param name="value">The string to extract the host from.</param>
153+
/// <param name="host">The extracted host if successful; otherwise, the original string.</param>
154+
/// <returns><c>true</c> if the host was successfully extracted; otherwise, <c>false</c>.</returns>
155+
private static bool TryGetHost(string value, out string host)
156+
{
157+
var uriToParse = value.Contains("://") ? value : "dummy://" + value;
158+
159+
if (Uri.TryCreate(uriToParse, UriKind.Absolute, out var uri))
160+
{
161+
host = uri.Port == -1 || uri.IsDefaultPort ? uri.Host : uri.Host + ":" + uri.Port;
162+
return true;
163+
}
164+
else
165+
{
166+
host = value;
167+
return false;
168+
}
169+
}
123170
}
124171
}

src/Testcontainers/Builders/CredsHelperProvider.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public CredsHelperProvider(JsonElement jsonElement, ILogger logger)
3939
/// <inheritdoc />
4040
public bool IsApplicable(string hostname)
4141
{
42-
return !JsonValueKind.Undefined.Equals(_rootElement.ValueKind) && !JsonValueKind.Null.Equals(_rootElement.ValueKind) && _rootElement.EnumerateObject().Any(property => Base64Provider.HasDockerRegistryKey(property, hostname));
42+
return !JsonValueKind.Undefined.Equals(_rootElement.ValueKind) && !JsonValueKind.Null.Equals(_rootElement.ValueKind) && _rootElement.EnumerateObject().Any(property => Base64Provider.HasDockerRegistryName(property, hostname));
4343
}
4444

4545
/// <inheritdoc />
@@ -52,7 +52,7 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname)
5252
return null;
5353
}
5454

55-
var registryEndpointProperty = _rootElement.EnumerateObject().LastOrDefault(property => Base64Provider.HasDockerRegistryKey(property, hostname));
55+
var registryEndpointProperty = _rootElement.EnumerateObject().LastOrDefault(property => Base64Provider.HasDockerRegistryName(property, hostname));
5656

5757
if (!JsonValueKind.String.Equals(registryEndpointProperty.Value.ValueKind))
5858
{

tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,29 @@ public sealed class Base64ProviderTest
6666
private readonly WarnLogger _warnLogger = new WarnLogger();
6767

6868
[Theory]
69-
[InlineData("{\"auths\":{\"ghcr.io\":{}}}")]
70-
[InlineData("{\"auths\":{\"://ghcr.io\":{}}}")]
71-
public void ResolvePartialDockerRegistry(string jsonDocument)
69+
[InlineData("{\"auths\":{\"ghcr.io\":{}}}", "ghcr.io", true)]
70+
[InlineData("{\"auths\":{\"ghcr.io\":{}}}", "ghcr", false)]
71+
[InlineData("{\"auths\":{\"http://ghcr.io\":{}}}", "ghcr.io", true)]
72+
[InlineData("{\"auths\":{\"https://ghcr.io\":{}}}", "ghcr.io", true)]
73+
[InlineData("{\"auths\":{\"registry.example.com:5000\":{}}}", "registry.example.com:5000", true)]
74+
[InlineData("{\"auths\":{\"localhost:5000\":{}}}", "localhost:5000", true)]
75+
[InlineData("{\"auths\":{\"registry.example.com:5000\":{}}}", "registry.example.com", false)]
76+
[InlineData("{\"auths\":{\"localhost:5000\":{}}}", "localhost", false)]
77+
[InlineData("{\"auths\":{\"https://registry.example.com:5000\":{}}}", "registry.example.com:5000", true)]
78+
[InlineData("{\"auths\":{\"http://localhost:8080\":{}}}", "localhost:8080", true)]
79+
[InlineData("{\"auths\":{\"docker.io\":{}}}", "docker.io", true)]
80+
[InlineData("{\"auths\":{\"docker.io\":{}}}", "index.docker.io", false)]
81+
[InlineData("{\"auths\":{\"index.docker.io\":{}}}", "docker.io", false)]
82+
[InlineData("{\"auths\":{\"https://index.docker.io/v1/\":{}}}", "index.docker.io", true)]
83+
[InlineData("{\"auths\":{\"registry.k8s.io\":{}}}", "registry.k8s.io", true)]
84+
[InlineData("{\"auths\":{\"gcr.io\":{}}}", "gcr.io", true)]
85+
[InlineData("{\"auths\":{\"us-docker.pkg.dev\":{}}}", "us-docker.pkg.dev", true)]
86+
[InlineData("{\"auths\":{\"quay.io\":{}}}", "quay.io", true)]
87+
[InlineData("{\"auths\":{\"localhost\":{}}}", "localhost", true)]
88+
[InlineData("{\"auths\":{\"127.0.0.1:5000\":{}}}", "127.0.0.1:5000", true)]
89+
[InlineData("{\"auths\":{\"[::1]:5000\":{}}}", "[::1]:5000", true)]
90+
[InlineData("{\"auths\":{\"https://registry.example.com/v2\":{}}}", "registry.example.com", true)]
91+
public void ResolvePartialDockerRegistry(string jsonDocument, string hostname, bool expectedResult)
7292
{
7393
// Given
7494
var jsonElement = JsonDocument.Parse(jsonDocument).RootElement;
@@ -77,8 +97,7 @@ public void ResolvePartialDockerRegistry(string jsonDocument)
7797
var authenticationProvider = new Base64Provider(jsonElement, NullLogger.Instance);
7898

7999
// Then
80-
Assert.False(authenticationProvider.IsApplicable("ghcr"));
81-
Assert.True(authenticationProvider.IsApplicable("ghcr.io"));
100+
Assert.Equal(expectedResult, authenticationProvider.IsApplicable(hostname));
82101
}
83102

84103
[Theory]

0 commit comments

Comments
 (0)