diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs
index 9e48bc29b..38bd4dcb8 100644
--- a/src/Testcontainers/Builders/DockerConfig.cs
+++ b/src/Testcontainers/Builders/DockerConfig.cs
@@ -77,7 +77,7 @@ public JsonDocument Parse()
/// Executes a command equivalent to docker context inspect --format {{.Endpoints.docker.Host}}.
///
/// A representing the current Docker endpoint if available; otherwise, null.
- [CanBeNull]
+ [NotNull]
public Uri GetCurrentEndpoint()
{
const string defaultDockerContext = "default";
@@ -99,16 +99,27 @@ public Uri GetCurrentEndpoint()
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))
+ try
{
- return null;
+ using (var metaFileStream = File.OpenRead(metaFilePath))
+ {
+ var meta = JsonSerializer.Deserialize(metaFileStream, SourceGenerationContext.Default.DockerContextMeta);
+ var host = meta.Endpoints?.Docker?.Host;
+ if (host == null)
+ {
+ throw new DockerConfigurationException($"The Docker host is null in {metaFilePath} (JSONPath: Endpoints.docker.Host)");
+ }
+
+ return new Uri(host.Replace("npipe:////./", "npipe://./"));
+ }
}
-
- using (var metaFileStream = File.OpenRead(metaFilePath))
+ catch (Exception notFoundException) when (notFoundException is DirectoryNotFoundException or FileNotFoundException)
{
- 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://./"));
+ throw new DockerConfigurationException($"The Docker context '{dockerContext}' does not exist", notFoundException);
+ }
+ catch (Exception exception) when (exception is not DockerConfigurationException)
+ {
+ throw new DockerConfigurationException($"The Docker context '{dockerContext}' failed to load from {metaFilePath}", exception);
}
}
}
@@ -162,15 +173,11 @@ private string GetDockerContext()
internal sealed class DockerContextMeta
{
[JsonConstructor]
- public DockerContextMeta(string name, DockerContextMetaEndpoints endpoints)
+ public DockerContextMeta(DockerContextMetaEndpoints endpoints)
{
- Name = name;
Endpoints = endpoints;
}
- [JsonPropertyName("Name")]
- public string Name { get; }
-
[JsonPropertyName("Endpoints")]
public DockerContextMetaEndpoints Endpoints { get; }
}
diff --git a/src/Testcontainers/Builders/DockerConfigurationException.cs b/src/Testcontainers/Builders/DockerConfigurationException.cs
new file mode 100644
index 000000000..adc4e772c
--- /dev/null
+++ b/src/Testcontainers/Builders/DockerConfigurationException.cs
@@ -0,0 +1,29 @@
+namespace DotNet.Testcontainers.Builders
+{
+ using System;
+ using JetBrains.Annotations;
+
+ ///
+ /// The exception that is thrown when the Docker configuration file cannot be read successfully.
+ ///
+ [PublicAPI]
+ public sealed class DockerConfigurationException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class, using the provided message.
+ ///
+ /// The error message that explains the reason for the exception.
+ public DockerConfigurationException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class, using the provided message and exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public DockerConfigurationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs
index c7eddad4a..67c9f7b89 100644
--- a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs
+++ b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs
@@ -15,7 +15,7 @@ internal sealed class DockerDesktopEndpointAuthenticationProvider : RootlessUnix
/// Initializes a new instance of the class.
///
public DockerDesktopEndpointAuthenticationProvider()
- : base(DockerConfig.Instance.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir())
+ : base(DockerConfig.Instance.GetCurrentEndpoint())
{
}
diff --git a/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs
index e8e1d6f6d..7fbde86e1 100644
--- a/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs
+++ b/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs
@@ -30,6 +30,15 @@ public RootlessUnixEndpointAuthenticationProvider(params string[] socketPaths)
DockerEngine = socketPath == null ? null : new Uri("unix://" + socketPath);
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Unix socket Docker Engine endpoint.
+ public RootlessUnixEndpointAuthenticationProvider(Uri dockerEngine)
+ {
+ DockerEngine = dockerEngine;
+ }
+
///
/// Gets the Unix socket Docker Engine endpoint.
///
diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs
index e7c19496b..e86c41c8f 100644
--- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs
@@ -46,9 +46,9 @@ public void ReturnsDefaultEndpointWhenDockerContextIsDefault()
public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromPropertiesFile()
{
// Given
- using var context = new ConfigMetaFile("custom", "tcp://127.0.0.1:2375/");
+ using var context = new ConfigMetaFile("custom", new Uri("tcp://127.0.0.1:2375/"));
- ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom", context.GetDockerConfig() });
+ ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom", $"docker.config={context.DockerConfigDirectoryPath}" });
var dockerConfig = new DockerConfig(customConfiguration);
// When
@@ -62,10 +62,10 @@ public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromPropertiesFile
public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromConfigFile()
{
// Given
- using var context = new ConfigMetaFile("custom", "tcp://127.0.0.1:2375/");
+ using var context = new ConfigMetaFile("custom", new Uri("tcp://127.0.0.1:2375/"));
// This test reads the current context JSON node from the Docker config file.
- ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { context.GetDockerConfig() });
+ ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { $"docker.config={context.DockerConfigDirectoryPath}" });
var dockerConfig = new DockerConfig(customConfiguration);
// When
@@ -83,17 +83,37 @@ public void ReturnsActiveEndpointWhenDockerContextIsUnset()
}
[Fact]
- public void ReturnsNullWhenDockerContextNotFound()
+ public void ThrowsWhenDockerContextNotFound()
{
// Given
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=missing" });
var dockerConfig = new DockerConfig(customConfiguration);
// When
- var currentEndpoint = dockerConfig.GetCurrentEndpoint();
+ var exception = Assert.Throws(() => dockerConfig.GetCurrentEndpoint());
// Then
- Assert.Null(currentEndpoint);
+ Assert.Equal("The Docker context 'missing' does not exist", exception.Message);
+ Assert.IsType(exception.InnerException);
+ }
+
+ [Fact]
+ public void ThrowsWhenDockerConfigEndpointNotFound()
+ {
+ // Given
+ using var context = new ConfigMetaFile("custom");
+
+ ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom", $"docker.config={context.DockerConfigDirectoryPath}" });
+ var dockerConfig = new DockerConfig(customConfiguration);
+
+ // When
+ var exception = Assert.Throws(() => dockerConfig.GetCurrentEndpoint());
+
+ // Then
+ Assert.StartsWith("The Docker host is null in ", exception.Message);
+ Assert.Contains(context.DockerConfigDirectoryPath, exception.Message);
+ Assert.EndsWith(" (JSONPath: Endpoints.docker.Host)", exception.Message);
+ Assert.Null(exception.InnerException);
}
}
@@ -117,9 +137,9 @@ public void ReturnsActiveEndpointWhenDockerHostIsEmpty()
public void ReturnsConfiguredEndpointWhenDockerHostIsSet()
{
// Given
- using var context = new ConfigMetaFile("custom", "");
+ using var context = new ConfigMetaFile("custom");
- ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/", context.GetDockerConfig() });
+ ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/", $"docker.config={context.DockerConfigDirectoryPath}" });
var dockerConfig = new DockerConfig(customConfiguration);
// When
@@ -147,26 +167,32 @@ private sealed class ConfigMetaFile : IDisposable
private const string MetaFileJson = "{{\"Name\":\"{0}\",\"Metadata\":{{}},\"Endpoints\":{{\"docker\":{{\"Host\":\"{1}\",\"SkipTLSVerify\":false}}}}}}";
- private readonly string _dockerConfigDirectoryPath;
+ public string DockerConfigDirectoryPath { get; }
- public ConfigMetaFile(string context, string endpoint, [CallerMemberName] string caller = "")
+ public ConfigMetaFile(string context, [CallerMemberName] string caller = "")
{
- _dockerConfigDirectoryPath = Path.Combine(TestSession.TempDirectoryPath, caller);
- var dockerContextHash = Convert.ToHexString(SHA256.HashData(Encoding.Default.GetBytes(context))).ToLowerInvariant();
- var dockerContextMetaDirectoryPath = Path.Combine(_dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash);
- _ = Directory.CreateDirectory(dockerContextMetaDirectoryPath);
- File.WriteAllText(Path.Combine(_dockerConfigDirectoryPath, "config.json"), string.Format(ConfigFileJson, context));
- File.WriteAllText(Path.Combine(dockerContextMetaDirectoryPath, "meta.json"), string.Format(MetaFileJson, context, endpoint));
+ DockerConfigDirectoryPath = InitializeContext(context, null, caller);
}
- public string GetDockerConfig()
+ public ConfigMetaFile(string context, Uri endpoint, [CallerMemberName] string caller = "")
{
- return "docker.config=" + _dockerConfigDirectoryPath;
+ DockerConfigDirectoryPath = InitializeContext(context, endpoint, caller);
+ }
+
+ private static string InitializeContext(string context, Uri endpoint, [CallerMemberName] string caller = "")
+ {
+ var dockerConfigDirectoryPath = Path.Combine(TestSession.TempDirectoryPath, caller);
+ var dockerContextHash = Convert.ToHexString(SHA256.HashData(Encoding.Default.GetBytes(context))).ToLowerInvariant();
+ var dockerContextMetaDirectoryPath = Path.Combine(dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash);
+ _ = Directory.CreateDirectory(dockerContextMetaDirectoryPath);
+ File.WriteAllText(Path.Combine(dockerConfigDirectoryPath, "config.json"), string.Format(ConfigFileJson, context));
+ File.WriteAllText(Path.Combine(dockerContextMetaDirectoryPath, "meta.json"), endpoint == null ? "{}" : string.Format(MetaFileJson, context, endpoint.AbsoluteUri));
+ return dockerConfigDirectoryPath;
}
public void Dispose()
{
- Directory.Delete(_dockerConfigDirectoryPath, true);
+ Directory.Delete(DockerConfigDirectoryPath, true);
}
}
}