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 2, 2022
1 parent 0f4d0ff commit a866f91
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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))
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]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit a866f91

Please sign in to comment.