Skip to content

Commit

Permalink
feat(#540): Add Docker registry authentication provider for DOCKER_AU…
Browse files Browse the repository at this point in the history
…TH_CONFIG environment variable
  • Loading branch information
vova-lantsov-dev authored and HofmeisterAn committed Aug 10, 2022
1 parent 477776a commit ced98a9
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 49 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_VERSION: 6.0.x
TZ: CET # https://stackoverflow.com/q/53510011

jobs:
Expand All @@ -37,8 +36,6 @@ jobs:

- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore .NET Tools
run: dotnet tool restore
Expand Down Expand Up @@ -110,6 +107,9 @@ jobs:
Get-ChildItem -Path 'test-coverage' -Filter *.xml | % { (Get-Content $_) -Replace '[A-Za-z0-9:\-\/\\]+tests', '${{ github.workspace }}/tests' | Set-Content $_ }
shell: pwsh

- name: Setup .NET
uses: actions/setup-dotnet@v2

- name: Restore .NET Tools
run: dotnet tool restore

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- 520 Add MariaDB module (@renemadsen)
- 528 Do not require the Docker host configuration (`DockerEndpointAuthConfig`) on `TestcontainersSettings` initialization
- 538 Support optional username and password in MongoDB connection string (@the-avid-engineer)
- 540 Add Docker registry authentication provider for `DOCKER_AUTH_CONFIG` environment variable (@vova-lantsov-dev)
- 541 Allow MsSqlTestcontainerConfiguration custom database names (@enginexon)

### Fixed
Expand Down
84 changes: 49 additions & 35 deletions src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth

private static readonly ConcurrentDictionary<string, Lazy<IDockerRegistryAuthenticationConfiguration>> Credentials = new ConcurrentDictionary<string, Lazy<IDockerRegistryAuthenticationConfiguration>>();

private readonly FileInfo dockerConfigFile;
private static readonly string UserProfileDockerConfigDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker");

private readonly FileInfo dockerConfigFilePath;

private readonly ILogger logger;

Expand All @@ -26,30 +28,30 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth
/// <param name="logger">The logger.</param>
[PublicAPI]
public DockerRegistryAuthenticationProvider(ILogger logger)
: this(GetDefaultDockerConfigFile(), logger)
: this(GetDefaultDockerConfigFilePath(), logger)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerRegistryAuthenticationProvider" /> class.
/// </summary>
/// <param name="dockerConfigFile">The Docker config file path.</param>
/// <param name="dockerConfigFilePath">The Docker config file path.</param>
/// <param name="logger">The logger.</param>
[PublicAPI]
public DockerRegistryAuthenticationProvider(string dockerConfigFile, ILogger logger)
: this(new FileInfo(dockerConfigFile), logger)
public DockerRegistryAuthenticationProvider(string dockerConfigFilePath, ILogger logger)
: this(new FileInfo(dockerConfigFilePath), logger)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerRegistryAuthenticationProvider" /> class.
/// </summary>
/// <param name="dockerConfigFile">The Docker config file path.</param>
/// <param name="dockerConfigFilePath">The Docker config file path.</param>
/// <param name="logger">The logger.</param>
[PublicAPI]
public DockerRegistryAuthenticationProvider(FileInfo dockerConfigFile, ILogger logger)
public DockerRegistryAuthenticationProvider(FileInfo dockerConfigFilePath, ILogger logger)
{
this.dockerConfigFile = dockerConfigFile;
this.dockerConfigFilePath = dockerConfigFilePath;
this.logger = logger;
}

Expand All @@ -66,45 +68,57 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname)
return lazyAuthConfig.Value;
}

private static string GetDefaultDockerConfigFile()
private static string GetDefaultDockerConfigFilePath()
{
var dockerConfigDirectory = Environment.GetEnvironmentVariable("DOCKER_CONFIG");
return dockerConfigDirectory == null ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker", "config.json") : Path.Combine(dockerConfigDirectory, "config.json");
var dockerConfigDirectoryPath = PropertiesFileConfiguration.Instance.GetDockerConfig() ?? EnvironmentConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath;
return Path.Combine(dockerConfigDirectoryPath, "config.json");
}

private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string hostname)
private static JsonDocument GetDefaultDockerAuthConfig()
{
IDockerRegistryAuthenticationConfiguration authConfig;
return PropertiesFileConfiguration.Instance.GetDockerAuthConfig() ?? EnvironmentConfiguration.Instance.GetDockerAuthConfig() ?? JsonDocument.Parse("{}");
}

if (this.dockerConfigFile.Exists)
private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string hostname)
{
using (var dockerAuthConfigJsonDocument = GetDefaultDockerAuthConfig())
{
using (var dockerConfigFileStream = new FileStream(this.dockerConfigFile.FullName, FileMode.Open, FileAccess.Read))
IDockerRegistryAuthenticationConfiguration authConfig;

if (this.dockerConfigFilePath.Exists)
{
using (var dockerConfigDocument = JsonDocument.Parse(dockerConfigFileStream))
using (var dockerConfigFileStream = new FileStream(this.dockerConfigFilePath.FullName, FileMode.Open, FileAccess.Read))
{
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);
using (var dockerConfigJsonDocument = JsonDocument.Parse(dockerConfigFileStream))
{
authConfig = new IDockerRegistryAuthenticationProvider[]
{
new CredsHelperProvider(dockerConfigJsonDocument, this.logger),
new CredsStoreProvider(dockerConfigJsonDocument, this.logger),
new Base64Provider(dockerConfigJsonDocument, this.logger),
new Base64Provider(dockerAuthConfigJsonDocument, this.logger),
}
.AsParallel()
.Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname))
.FirstOrDefault(authenticationProvider => authenticationProvider != null);
}
}
}
}
else
{
this.logger.DockerConfigFileNotFound(this.dockerConfigFile.FullName);
return default(DockerRegistryAuthenticationConfiguration);
}
else
{
this.logger.DockerConfigFileNotFound(this.dockerConfigFilePath.FullName);
IDockerRegistryAuthenticationProvider authConfigProvider = new Base64Provider(dockerAuthConfigJsonDocument, this.logger);
authConfig = authConfigProvider.GetAuthConfig(hostname);
}

if (authConfig == null)
{
this.logger.DockerRegistryCredentialNotFound(hostname);
return default(DockerRegistryAuthenticationConfiguration);
}
if (authConfig == null)
{
this.logger.DockerRegistryCredentialNotFound(hostname);
return default(DockerRegistryAuthenticationConfiguration);
}

return authConfig;
return authConfig;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ internal sealed class EnvironmentEndpointAuthenticationProvider : DockerEndpoint
{
private readonly Uri dockerEngine;

/// <summary>
/// Initializes a new instance of the <see cref="EnvironmentEndpointAuthenticationProvider" /> class.
/// </summary>
public EnvironmentEndpointAuthenticationProvider()
{
ICustomConfiguration propertiesFileConfiguration = new PropertiesFileConfiguration();
ICustomConfiguration environmentConfiguration = new EnvironmentConfiguration();
this.dockerEngine = propertiesFileConfiguration.GetDockerHost() ?? environmentConfiguration.GetDockerHost();
this.dockerEngine = PropertiesFileConfiguration.Instance.GetDockerHost() ?? EnvironmentConfiguration.Instance.GetDockerHost();
}

/// <inheritdoc />
Expand Down
4 changes: 4 additions & 0 deletions src/Testcontainers/Clients/DefaultLabels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ namespace DotNet.Testcontainers.Clients

internal sealed class DefaultLabels : ReadOnlyDictionary<string, string>
{
static DefaultLabels()
{
}

private DefaultLabels(Guid resourceReaperSessionId)
: base(new Dictionary<string, string>
{
Expand Down
26 changes: 26 additions & 0 deletions src/Testcontainers/Configurations/CustomConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using DotNet.Testcontainers.Images;

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

protected string GetDockerConfig(string propertyName)
{
_ = this.properties.TryGetValue(propertyName, out var propertyValue);
return propertyValue;
}

protected Uri GetDockerHost(string propertyName)
{
return this.properties.TryGetValue(propertyName, out var propertyValue) && Uri.TryCreate(propertyValue, UriKind.RelativeOrAbsolute, out var dockerHost) ? dockerHost : null;
}

protected JsonDocument GetDockerAuthConfig(string propertyName)
{
_ = this.properties.TryGetValue(propertyName, out var propertyValue);

if (string.IsNullOrEmpty(propertyValue))
{
return null;
}

try
{
return JsonDocument.Parse(propertyValue);
}
catch (Exception)
{
return null;
}
}

protected bool GetRyukDisabled(string propertyName)
{
return this.properties.TryGetValue(propertyName, out var propertyValue) && bool.TryParse(propertyValue, out var ryukDisabled) && ryukDisabled;
Expand Down
27 changes: 26 additions & 1 deletion src/Testcontainers/Configurations/EnvironmentConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
using System;
using System.Linq;
using System.Text.Json;
using DotNet.Testcontainers.Images;

/// <summary>
Expand All @@ -13,27 +14,51 @@ internal sealed class EnvironmentConfiguration : CustomConfiguration, ICustomCon

private const string DockerHost = "DOCKER_HOST";

private const string DockerAuthConfig = "DOCKER_AUTH_CONFIG";

private const string RyukDisabled = "TESTCONTAINERS_RYUK_DISABLED";

private const string RyukContainerImage = "TESTCONTAINERS_RYUK_CONTAINER_IMAGE";

private const string HubImageNamePrefix = "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX";

static EnvironmentConfiguration()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="EnvironmentConfiguration" /> class.
/// </summary>
public EnvironmentConfiguration()
: base(new[] { DockerConfig, DockerHost, RyukDisabled, RyukContainerImage, HubImageNamePrefix }
: base(new[] { DockerConfig, DockerHost, DockerAuthConfig, RyukDisabled, RyukContainerImage, HubImageNamePrefix }
.ToDictionary(key => key, Environment.GetEnvironmentVariable))
{
}

/// <summary>
/// Gets the <see cref="ICustomConfiguration" /> instance.
/// </summary>
public static ICustomConfiguration Instance { get; }
= new EnvironmentConfiguration();

/// <inheritdoc />
public string GetDockerConfig()
{
return this.GetDockerConfig(DockerConfig);
}

/// <inheritdoc />
public Uri GetDockerHost()
{
return this.GetDockerHost(DockerHost);
}

/// <inheritdoc />
public JsonDocument GetDockerAuthConfig()
{
return this.GetDockerAuthConfig(DockerAuthConfig);
}

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

Expand All @@ -9,6 +10,14 @@
/// </summary>
internal interface ICustomConfiguration
{
/// <summary>
/// Gets the Docker config custom configuration.
/// </summary>
/// <returns>The Docker config custom configuration.</returns>
/// <remarks>https://www.testcontainers.org/features/configuration/#customizing-docker-host-detection.</remarks>
[CanBeNull]
string GetDockerConfig();

/// <summary>
/// Gets the Docker host custom configuration.
/// </summary>
Expand All @@ -17,6 +26,14 @@ internal interface ICustomConfiguration
[CanBeNull]
Uri GetDockerHost();

/// <summary>
/// Gets the Docker registry authentication custom configuration.
/// </summary>
/// <returns>The Docker authentication custom configuration.</returns>
/// <remarks>https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#access-an-image-from-a-private-container-registry.</remarks>
[CanBeNull]
JsonDocument GetDockerAuthConfig();

/// <summary>
/// Gets the Ryuk disabled custom configuration.
/// </summary>
Expand Down
Loading

0 comments on commit ced98a9

Please sign in to comment.