Skip to content

Commit

Permalink
feat(testcontainers#540): Support for registry auth credentials from …
Browse files Browse the repository at this point in the history
…env var DOCKER_AUTH_CONFIG
  • Loading branch information
vova-lantsov-dev committed Aug 6, 2022
1 parent 69b69aa commit 9253efa
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 14 deletions.
2 changes: 2 additions & 0 deletions Testcontainers.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ryuk/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
22 changes: 10 additions & 12 deletions src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,18 @@ private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string
{
IDockerRegistryAuthenticationConfiguration authConfig;

if (this.dockerConfigFile.Exists)
var dockerConfigDocument = EnvironmentConfiguration.Instance.GetDockerAuthConfig(this.dockerConfigFile);
if (dockerConfigDocument != null)
{
using (var dockerConfigFileStream = new FileStream(this.dockerConfigFile.FullName, FileMode.Open, FileAccess.Read))
using (dockerConfigDocument)
{
using (var dockerConfigDocument = JsonDocument.Parse(dockerConfigFileStream))
{
authConfig = new IDockerRegistryAuthenticationProvider[]
{
new CredsHelperProvider(dockerConfigDocument, this.logger), new CredsStoreProvider(dockerConfigDocument, this.logger), new Base64Provider(dockerConfigDocument, this.logger),
}
.AsParallel()
.Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname))
.FirstOrDefault(authenticationProvider => authenticationProvider != null);
}
authConfig = new IDockerRegistryAuthenticationProvider[]
{
new CredsHelperProvider(dockerConfigDocument, this.logger), new CredsStoreProvider(dockerConfigDocument, this.logger), new Base64Provider(dockerConfigDocument, this.logger),
}
.AsParallel()
.Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname))
.FirstOrDefault(authenticationProvider => authenticationProvider != null);
}
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ internal sealed class EnvironmentEndpointAuthenticationProvider : DockerEndpoint

public EnvironmentEndpointAuthenticationProvider()
{
ICustomConfiguration propertiesFileConfiguration = new PropertiesFileConfiguration();
ICustomConfiguration environmentConfiguration = new EnvironmentConfiguration();
var propertiesFileConfiguration = PropertiesFileConfiguration.Instance;
var environmentConfiguration = EnvironmentConfiguration.Instance;
this.dockerEngine = propertiesFileConfiguration.GetDockerHost() ?? environmentConfiguration.GetDockerHost();
}

Expand Down
90 changes: 90 additions & 0 deletions src/Testcontainers/Builders/EnvironmentVariableBase64Provider.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Initializes a new instance of the <see cref="EnvironmentVariableBase64Provider"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
[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]);
}
}
}
30 changes: 30 additions & 0 deletions src/Testcontainers/Configurations/CustomConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using DotNet.Testcontainers.Images;

internal abstract class CustomConfiguration
Expand All @@ -13,6 +15,34 @@ protected CustomConfiguration(IReadOnlyDictionary<string, string> properties)
this.properties = properties;
}

#pragma warning disable CA1822 // Method can be static
public JsonDocument GetDockerAuthConfig(FileInfo dockerConfigFile)
#pragma warning restore CA1822
{
var environmentVariableValue = Environment.GetEnvironmentVariable("DOCKER_AUTH_CONFIG");
if (!string.IsNullOrEmpty(environmentVariableValue))
{
try
{
return JsonDocument.Parse(environmentVariableValue);
}
catch (JsonException)
{
return null;
}
}

if (dockerConfigFile.Exists)
{
using (var dockerConfigFileStream = new FileStream(dockerConfigFile.FullName, FileMode.Open, FileAccess.Read))
{
return JsonDocument.Parse(dockerConfigFileStream);
}
}

return null;
}

protected Uri GetDockerHost(string propertyName)
{
return this.properties.TryGetValue(propertyName, out var propertyValue) && Uri.TryCreate(propertyValue, UriKind.RelativeOrAbsolute, out var dockerHost) ? dockerHost : null;
Expand Down
4 changes: 4 additions & 0 deletions src/Testcontainers/Configurations/EnvironmentConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/// </summary>
internal sealed class EnvironmentConfiguration : CustomConfiguration, ICustomConfiguration
{
private static readonly Lazy<ICustomConfiguration> InstanceLazy = new Lazy<ICustomConfiguration>(() => new EnvironmentConfiguration());

private const string DockerConfig = "DOCKER_CONFIG";

private const string DockerHost = "DOCKER_HOST";
Expand All @@ -28,6 +30,8 @@ public EnvironmentConfiguration()
{
}

public static ICustomConfiguration Instance => InstanceLazy.Value;

/// <inheritdoc />
public Uri GetDockerHost()
{
Expand Down
10 changes: 10 additions & 0 deletions src/Testcontainers/Configurations/ICustomConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace DotNet.Testcontainers.Configurations
{
using System;
using System.IO;
using System.Text.Json;
using DotNet.Testcontainers.Images;
using JetBrains.Annotations;

Expand Down Expand Up @@ -39,5 +41,13 @@ internal interface ICustomConfiguration
/// <remarks>https://www.testcontainers.org/features/image_name_substitution/.</remarks>
[CanBeNull]
string GetHubImageNamePrefix();

/// <summary>
/// Gets the Docker authentication info from either environment variable or the specified file.
/// </summary>
/// <param name="dockerConfigFile">The Docker config file to read from.</param>
/// <returns>The JSON document with parsed Docker configuration.</returns>
[CanBeNull]
JsonDocument GetDockerAuthConfig([NotNull] FileInfo dockerConfigFile);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
/// </summary>
internal sealed class PropertiesFileConfiguration : CustomConfiguration, ICustomConfiguration
{
private static readonly Lazy<ICustomConfiguration> InstanceLazy = new Lazy<ICustomConfiguration>(() => new PropertiesFileConfiguration());

/// <summary>
/// Initializes a new instance of the <see cref="PropertiesFileConfiguration" /> class.
/// </summary>
Expand Down Expand Up @@ -41,6 +43,8 @@ public PropertiesFileConfiguration(params string[] lines)
{
}

public static ICustomConfiguration Instance => InstanceLazy.Value;

/// <inheritdoc />
public Uri GetDockerHost()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,64 @@ public void ShouldGetAuthConfig()
}
}

public sealed class EnvironmentVariableBase64ProviderTest
{
private sealed class EnvironmentVariableScope : IDisposable
{
public EnvironmentVariableScope(string authConfigValue)
{
Environment.SetEnvironmentVariable("DOCKER_AUTH_CONFIG", authConfigValue);
}

public void Dispose()
{
Environment.SetEnvironmentVariable("DOCKER_AUTH_CONFIG", null);
}
}

[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
using var scope = new EnvironmentVariableScope(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=\"}}}";
using var scope = new EnvironmentVariableScope(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]
Expand Down

0 comments on commit 9253efa

Please sign in to comment.