From a866f91475c379d644bb7b8f6f21d666666c9b50 Mon Sep 17 00:00:00 2001 From: vova-lantsov-dev Date: Tue, 2 Aug 2022 23:32:20 +0300 Subject: [PATCH] feat(#540): Support for registry auth credentials from env var DOCKER_AUTH_CONFIG --- .../DockerRegistryAuthenticationProvider.cs | 6 +- .../EnvironmentVariableBase64Provider.cs | 90 +++++++++++++++++++ ...ockerRegistryAuthenticationProviderTest.cs | 50 +++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/Testcontainers/Builders/EnvironmentVariableBase64Provider.cs diff --git a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs index a510858c9..5db612a21 100644 --- a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs @@ -84,7 +84,11 @@ private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string { authConfig = new IDockerRegistryAuthenticationProvider[] { - new CredsHelperProvider(dockerConfigDocument, this.logger), new CredsStoreProvider(dockerConfigDocument, this.logger), new Base64Provider(dockerConfigDocument, this.logger), + // environment variable provider goes first as it should overwrite the Docker configuration file when set + new EnvironmentVariableBase64Provider(this.logger), + new CredsHelperProvider(dockerConfigDocument, this.logger), + new CredsStoreProvider(dockerConfigDocument, this.logger), + new Base64Provider(dockerConfigDocument, this.logger), } .AsParallel() .Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname)) diff --git a/src/Testcontainers/Builders/EnvironmentVariableBase64Provider.cs b/src/Testcontainers/Builders/EnvironmentVariableBase64Provider.cs new file mode 100644 index 000000000..9ffbc8cad --- /dev/null +++ b/src/Testcontainers/Builders/EnvironmentVariableBase64Provider.cs @@ -0,0 +1,90 @@ +namespace DotNet.Testcontainers.Builders +{ + using System; + using System.Linq; + using System.Text; + using System.Text.Json; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + using Microsoft.Extensions.Logging; + + internal sealed class EnvironmentVariableBase64Provider : IDockerRegistryAuthenticationProvider + { + private readonly JsonElement rootElement; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + [PublicAPI] + public EnvironmentVariableBase64Provider(ILogger logger) + { + var environmentVariableValue = Environment.GetEnvironmentVariable("DOCKER_AUTH_CONFIG"); + if (!string.IsNullOrEmpty(environmentVariableValue)) + { + try + { + this.rootElement = JsonDocument.Parse(environmentVariableValue).RootElement.TryGetProperty("auths", out var auths) ? auths : default; + } + catch (JsonException) + { + // silent + } + } + + this.logger = logger; + } + + public bool IsApplicable(string hostname) + { +#if NETSTANDARD2_1_OR_GREATER + return !default(JsonElement).Equals(this.rootElement) && !JsonValueKind.Null.Equals(this.rootElement.ValueKind) && this.rootElement.EnumerateObject().Any(property => property.Name.Contains(hostname, StringComparison.OrdinalIgnoreCase)); +#else + return !default(JsonElement).Equals(this.rootElement) && !JsonValueKind.Null.Equals(this.rootElement.ValueKind) && this.rootElement.EnumerateObject().Any(property => property.Name.IndexOf(hostname, StringComparison.OrdinalIgnoreCase) >= 0); +#endif + } + + public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) + { + this.logger.SearchingDockerRegistryCredential("EnvironmentVariableAuths"); + + if (!this.IsApplicable(hostname)) + { + return null; + } + +#if NETSTANDARD2_1_OR_GREATER + var authProperty = this.rootElement.EnumerateObject().LastOrDefault(property => property.Name.Contains(hostname, StringComparison.OrdinalIgnoreCase)); +#else + var authProperty = this.rootElement.EnumerateObject().LastOrDefault(property => property.Name.IndexOf(hostname, StringComparison.OrdinalIgnoreCase) >= 0); +#endif + + if (JsonValueKind.Undefined.Equals(authProperty.Value.ValueKind)) + { + return null; + } + + if (!authProperty.Value.TryGetProperty("auth", out var auth)) + { + return null; + } + + if (string.IsNullOrEmpty(auth.GetString())) + { + return null; + } + + var credentialInBytes = Convert.FromBase64String(auth.GetString()); + var credential = Encoding.UTF8.GetString(credentialInBytes).Split(new[] { ':' }, 2); + + if (credential.Length != 2) + { + return null; + } + + this.logger.DockerRegistryCredentialFound(hostname); + return new DockerRegistryAuthenticationConfiguration(authProperty.Name, credential[0], credential[1]); + } + } +} diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs index ca7fd799b..84b91f6a8 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs @@ -104,6 +104,56 @@ public void ShouldGetAuthConfig() } } + public sealed class EnvironmentVariableBase64ProviderTest + { + private static void SetEnvironmentVariable(string authConfigValue) + { + Environment.SetEnvironmentVariable("DOCKER_AUTH_CONFIG", authConfigValue); + } + + [Theory] + [InlineData("{}", false)] + [InlineData("{\"auths\":null}", false)] + [InlineData("{\"auths\":{}}", false)] + [InlineData("{\"auths\":{\"ghcr.io\":{}}}", false)] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{}}}", true)] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":null}}}", true)] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"\"}}}", true)] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU=\"}}}", true)] + public void ShouldGetNull(string jsonAuthConfig, bool isApplicable) + { + // Given + SetEnvironmentVariable(jsonAuthConfig); + + // When + var authenticationProvider = new EnvironmentVariableBase64Provider(TestcontainersSettings.Logger); + var authConfig = authenticationProvider.GetAuthConfig(DockerRegistry); + + // Then + Assert.Equal(isApplicable, authenticationProvider.IsApplicable(DockerRegistry)); + Assert.Null(authConfig); + } + + [Fact] + public void ShouldGetAuthConfig() + { + // Given + const string jsonAuthConfig = "{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU6cGFzc3dvcmQ=\"}}}"; + SetEnvironmentVariable(jsonAuthConfig); + + // When + var authenticationProvider = new EnvironmentVariableBase64Provider(TestcontainersSettings.Logger); + var authConfig = authenticationProvider.GetAuthConfig(DockerRegistry); + + // Then + Assert.True(authenticationProvider.IsApplicable(DockerRegistry)); + Assert.NotNull(authConfig); + Assert.Equal(DockerRegistry, authConfig.RegistryEndpoint); + Assert.Equal("username", authConfig.Username); + Assert.Equal("password", authConfig.Password); + } + } + public sealed class CredsStoreProviderTest : SetEnvVarPath { [Theory]