Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Get Docker endpoint from Docker context #1235

Merged
merged 26 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
71994ca
Get the current docker endpoint through the `docker context` command
0xced Aug 16, 2024
027f704
Read the current docker endpoint from the file system
0xced Aug 19, 2024
38cd08a
Deserialize the meta.json file with typed objects
0xced Aug 19, 2024
ce075c8
Improve DockerConfigTest.GetCurrentEndpoint test
0xced Aug 19, 2024
8e864ec
Avoid instantiating the default DockerConfig twice
0xced Aug 19, 2024
512784f
Leverage the existing DockerCli class
0xced Aug 19, 2024
7df5dc8
Log the current endpoint
0xced Aug 19, 2024
a33f149
Handle the default context
0xced Aug 19, 2024
8ba827b
Handle DOCKER_CONTEXT and DOCKER_HOST environment variables
0xced Aug 20, 2024
2295592
Fix tests on Windows
0xced Aug 20, 2024
9ce89e8
Add Docker CLI reference
0xced Aug 20, 2024
6ccdda9
chore: Add ICustomConfiguration.GetDockerContext
HofmeisterAn Aug 23, 2024
58d6f65
Merge branch 'docker-context' of github.com:0xced/testcontainers-dotn…
HofmeisterAn Aug 23, 2024
5555390
chore: Add comments, increase tests
HofmeisterAn Aug 26, 2024
d98e4bc
fix: Do not reference TestcontainersSettings.OS
HofmeisterAn Aug 26, 2024
0fa1b73
chore: Add ThrowIfExecutionFailed
HofmeisterAn Aug 26, 2024
74462e6
fix: Set ExitCode property again
HofmeisterAn Aug 27, 2024
6c70ea8
docs: Add docker context docs
HofmeisterAn Aug 27, 2024
a3e1854
docs: Enhance context and logger docs
HofmeisterAn Aug 28, 2024
a8862ca
Merge branch 'develop' into 0xced-docker-context
HofmeisterAn Sep 3, 2024
e5b7aaf
Make the DockerConfig class actually testable
0xced Sep 3, 2024
f5898ec
Actually test reading the current context from Docker config files
0xced Sep 4, 2024
266ee03
Skip ReturnsActiveEndpointWhenDockerContextIsUnset if necessary
0xced Sep 6, 2024
afd4fd3
Merge branch 'develop' into docker-context
HofmeisterAn Sep 6, 2024
d7dc042
chore: Remove unnecessary config file arg
HofmeisterAn Sep 6, 2024
72938db
docs: Use tabbed group for Docker context config
HofmeisterAn Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/custom_configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Testcontainers supports various configurations to set up your test environment.
|-----------------------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| `docker.config` | `DOCKER_CONFIG` | The directory path that contains the Docker configuration (`config.json`) file. | `~/.docker/` |
| `docker.host` | `DOCKER_HOST` | The Docker daemon socket to connect to. | - |
| `docker.context` | `DOCKER_CONTEXT` | The Docker context to connect to. | - |
| `docker.auth.config` | `DOCKER_AUTH_CONFIG` | The Docker configuration file content (GitLab: [Use statically-defined credentials][use-statically-defined-credentials]). | - |
| `docker.cert.path` | `DOCKER_CERT_PATH` | The directory path that contains the client certificate (`{ca,cert,key}.pem`) files. | `~/.docker/` |
| `docker.tls` | `DOCKER_TLS` | Enables TLS. | `false` |
Expand Down Expand Up @@ -36,6 +37,28 @@ To configure a remote container runtime, Testcontainers provides support for Doc
docker.host=tcp://docker:2375
```

## Use a different context

You can switch between contexts using the properties file or an environment variable. Once the context is set, Testcontainers will connect to the specified endpoint based on the given value.

```title="List available contexts"
PS C:\Sources\dotnet\testcontainers-dotnet> docker context ls
NAME DESCRIPTION DOCKER ENDPOINT ERROR
tcc tcp://127.0.0.1:60706/0
```

Setting the context to `tcc` in this example will use the Docker host running at `127.0.0.1:60706` to create and run the test resources.

=== "Environment Variable"
```
DOCKER_CONTEXT=tcc
```

=== "Properties File"
```
docker.context=tcc
```

## Enable logging

In .NET logging usually goes through the test framework. Testcontainers is not aware of the project's test framework and may not forward log messages to the appropriate output stream. The default implementation forwards log messages to the `Console` (respectively `stdout` and `stderr`). The output should at least pop up in the IDE running tests in the `Debug` configuration. To override the default implementation, use the builder's `WithLogger(ILogger)` method and provide an `ILogger` instance to replace the default console logger.
Expand Down
202 changes: 202 additions & 0 deletions src/Testcontainers/Builders/DockerConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
namespace DotNet.Testcontainers.Builders
{
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using DotNet.Testcontainers.Configurations;
using JetBrains.Annotations;

/// <summary>
/// Represents a Docker config.
/// </summary>
internal sealed class DockerConfig
{
private static readonly string UserProfileDockerConfigDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker");

private readonly ICustomConfiguration[] _customConfigurations;

private readonly string _dockerConfigDirectoryPath;

private readonly string _dockerConfigFilePath;

/// <summary>
/// Initializes a new instance of the <see cref="DockerConfig" /> class.
/// </summary>
[PublicAPI]
public DockerConfig()
: this(EnvironmentConfiguration.Instance, PropertiesFileConfiguration.Instance)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerConfig" /> class.
/// </summary>
/// <param name="customConfigurations">A list of custom configurations.</param>
[PublicAPI]
public DockerConfig(params ICustomConfiguration[] customConfigurations)
{
_customConfigurations = customConfigurations;
_dockerConfigDirectoryPath = GetDockerConfig();
_dockerConfigFilePath = Path.Combine(_dockerConfigDirectoryPath, "config.json");
}

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

/// <inheritdoc cref="FileInfo.Exists" />
public bool Exists => File.Exists(_dockerConfigFilePath);

/// <inheritdoc cref="FileInfo.FullName" />
public string FullName => _dockerConfigFilePath;

/// <summary>
/// Parses the Docker config file.
/// </summary>
/// <returns>A <see cref="JsonDocument" /> representing the Docker config.</returns>
public JsonDocument Parse()
{
using (var dockerConfigFileStream = File.OpenRead(_dockerConfigFilePath))
{
return JsonDocument.Parse(dockerConfigFileStream);
}
}

/// <summary>
/// Gets the current Docker endpoint.
/// </summary>
/// <remarks>
/// See the Docker CLI implementation <a href="https://github.com/docker/cli/blob/v25.0.0/cli/command/cli.go#L364-L390">comments</a>.
/// Executes a command equivalent to <c>docker context inspect --format {{.Endpoints.docker.Host}}</c>.
/// </remarks>
/// A <see cref="Uri" /> representing the current Docker endpoint if available; otherwise, <c>null</c>.
[CanBeNull]
public Uri GetCurrentEndpoint()
{
const string defaultDockerContext = "default";

var dockerHost = GetDockerHost();
if (dockerHost != null)
{
return dockerHost;
}

var dockerContext = GetCurrentContext();
if (string.IsNullOrEmpty(dockerContext) || defaultDockerContext.Equals(dockerContext))
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? NpipeEndpointAuthenticationProvider.DockerEngine : UnixEndpointAuthenticationProvider.DockerEngine;
}

using (var sha256 = SHA256.Create())
{
var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.Default.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant();
var metaFilePath = Path.Combine(_dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash, "meta.json");

if (!File.Exists(metaFilePath))
{
return null;
}

using (var metaFileStream = File.OpenRead(metaFilePath))
{
var meta = JsonSerializer.Deserialize(metaFileStream, SourceGenerationContext.Default.DockerContextMeta);
var host = meta?.Name == dockerContext ? meta.Endpoints?.Docker?.Host : null;
return string.IsNullOrEmpty(host) ? null : new Uri(host.Replace("npipe:////./", "npipe://./"));
}
}
}

[CanBeNull]
private string GetCurrentContext()
{
var dockerContext = GetDockerContext();
if (!string.IsNullOrEmpty(dockerContext))
{
return dockerContext;
}

if (!Exists)
{
return null;
}

using (var dockerConfigJsonDocument = Parse())
{
if (dockerConfigJsonDocument.RootElement.TryGetProperty("currentContext", out var currentContext) && currentContext.ValueKind == JsonValueKind.String)
{
return currentContext.GetString();
}
else
{
return null;
}
}
}

[NotNull]
private string GetDockerConfig()
{
var dockerConfigDirectoryPath = _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerConfig()).FirstOrDefault(dockerConfig => !string.IsNullOrEmpty(dockerConfig));
return dockerConfigDirectoryPath ?? UserProfileDockerConfigDirectoryPath;
}

[CanBeNull]
private Uri GetDockerHost()
{
return _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerHost()).FirstOrDefault(dockerHost => dockerHost != null);
}

[CanBeNull]
private string GetDockerContext()
{
return _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerContext()).FirstOrDefault(dockerContext => !string.IsNullOrEmpty(dockerContext));
}

internal sealed class DockerContextMeta
{
[JsonConstructor]
public DockerContextMeta(string name, DockerContextMetaEndpoints endpoints)
{
Name = name;
Endpoints = endpoints;
}

[JsonPropertyName("Name")]
public string Name { get; }

[JsonPropertyName("Endpoints")]
public DockerContextMetaEndpoints Endpoints { get; }
}

internal sealed class DockerContextMetaEndpoints
{
[JsonConstructor]
public DockerContextMetaEndpoints(DockerContextMetaEndpointsDocker docker)
{
Docker = docker;
}

[JsonPropertyName("docker")]
public DockerContextMetaEndpointsDocker Docker { get; }
}

internal sealed class DockerContextMetaEndpointsDocker
{
[JsonConstructor]
public DockerContextMetaEndpointsDocker(string host)
{
Host = host;
}

[JsonPropertyName("Host")]
public string Host { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal sealed class DockerDesktopEndpointAuthenticationProvider : RootlessUnix
/// Initializes a new instance of the <see cref="DockerDesktopEndpointAuthenticationProvider" /> class.
/// </summary>
public DockerDesktopEndpointAuthenticationProvider()
: base(GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir())
: base(DockerConfig.Instance.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir())
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
{
}

Expand All @@ -37,6 +37,12 @@ public Uri GetDockerHost()
return null;
}

/// <inheritdoc />
public string GetDockerContext()
{
return null;
}

/// <inheritdoc />
public string GetDockerHostOverride()
{
Expand Down
58 changes: 18 additions & 40 deletions src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth

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

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

private readonly FileInfo _dockerConfigFilePath;
private readonly DockerConfig _dockerConfig;

private readonly ILogger _logger;

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

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

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

Expand All @@ -68,12 +55,6 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname)
return lazyAuthConfig.Value;
}

private static string GetDefaultDockerConfigFilePath()
{
var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? PropertiesFileConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath;
return Path.Combine(dockerConfigDirectoryPath, "config.json");
}

private static JsonDocument GetDefaultDockerAuthConfig()
{
return EnvironmentConfiguration.Instance.GetDockerAuthConfig() ?? PropertiesFileConfiguration.Instance.GetDockerAuthConfig() ?? JsonDocument.Parse("{}");
Expand All @@ -85,28 +66,25 @@ private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string
{
IDockerRegistryAuthenticationConfiguration authConfig;

if (_dockerConfigFilePath.Exists)
if (_dockerConfig.Exists)
{
using (var dockerConfigFileStream = new FileStream(_dockerConfigFilePath.FullName, FileMode.Open, FileAccess.Read))
using (var dockerConfigJsonDocument = _dockerConfig.Parse())
{
using (var dockerConfigJsonDocument = JsonDocument.Parse(dockerConfigFileStream))
{
authConfig = new IDockerRegistryAuthenticationProvider[]
{
new CredsHelperProvider(dockerConfigJsonDocument, _logger),
new CredsStoreProvider(dockerConfigJsonDocument, _logger),
new Base64Provider(dockerConfigJsonDocument, _logger),
new Base64Provider(dockerAuthConfigJsonDocument, _logger),
}
.AsParallel()
.Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname))
.FirstOrDefault(authenticationProvider => authenticationProvider != null);
}
authConfig = new IDockerRegistryAuthenticationProvider[]
{
new CredsHelperProvider(dockerConfigJsonDocument, _logger),
new CredsStoreProvider(dockerConfigJsonDocument, _logger),
new Base64Provider(dockerConfigJsonDocument, _logger),
new Base64Provider(dockerAuthConfigJsonDocument, _logger),
}
.AsParallel()
.Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname))
.FirstOrDefault(authenticationProvider => authenticationProvider != null);
}
}
else
{
_logger.DockerConfigFileNotFound(_dockerConfigFilePath.FullName);
_logger.DockerConfigFileNotFound(_dockerConfig.FullName);
IDockerRegistryAuthenticationProvider authConfigProvider = new Base64Provider(dockerAuthConfigJsonDocument, _logger);
authConfig = authConfigProvider.GetAuthConfig(hostname);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ public RootlessUnixEndpointAuthenticationProvider()
/// <param name="socketPaths">A list of socket paths.</param>
public RootlessUnixEndpointAuthenticationProvider(params string[] socketPaths)
{
DockerEngine = socketPaths
.Where(File.Exists)
.Select(socketPath => new Uri("unix://" + socketPath))
.FirstOrDefault();
var socketPath = socketPaths.FirstOrDefault(File.Exists);
DockerEngine = socketPath == null ? null : new Uri("unix://" + socketPath);
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Testcontainers/Builders/SourceGenerationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DotNet.Testcontainers.Builders
{
using System.Text.Json.Serialization;

[JsonSerializable(typeof(DockerConfig.DockerContextMeta))]
internal partial class SourceGenerationContext : JsonSerializerContext;
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ public Uri GetDockerHost()
return _customConfiguration.GetDockerHost();
}

/// <inheritdoc />
public string GetDockerContext()
{
return _customConfiguration.GetDockerContext();
}

/// <inheritdoc />
public string GetDockerHostOverride()
{
Expand Down
Loading
Loading