Skip to content

Commit f5a5f7a

Browse files
committed
Improve error reporting when loading the Docker configuration file
1 parent 2e15016 commit f5a5f7a

File tree

5 files changed

+105
-34
lines changed

5 files changed

+105
-34
lines changed

src/Testcontainers/Builders/DockerConfig.cs

+20-13
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public JsonDocument Parse()
7777
/// Executes a command equivalent to <c>docker context inspect --format {{.Endpoints.docker.Host}}</c>.
7878
/// </remarks>
7979
/// A <see cref="Uri" /> representing the current Docker endpoint if available; otherwise, <c>null</c>.
80-
[CanBeNull]
80+
[NotNull]
8181
public Uri GetCurrentEndpoint()
8282
{
8383
const string defaultDockerContext = "default";
@@ -99,16 +99,27 @@ public Uri GetCurrentEndpoint()
9999
var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.Default.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant();
100100
var metaFilePath = Path.Combine(_dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash, "meta.json");
101101

102-
if (!File.Exists(metaFilePath))
102+
try
103103
{
104-
return null;
104+
using (var metaFileStream = File.OpenRead(metaFilePath))
105+
{
106+
var meta = JsonSerializer.Deserialize(metaFileStream, SourceGenerationContext.Default.DockerContextMeta);
107+
var host = meta.Endpoints?.Docker?.Host;
108+
if (host == null)
109+
{
110+
throw new DockerConfigurationException($"The Docker host is null in {metaFilePath} (JSONPath: Endpoints.docker.Host)");
111+
}
112+
113+
return new Uri(host.Replace("npipe:////./", "npipe://./"));
114+
}
105115
}
106-
107-
using (var metaFileStream = File.OpenRead(metaFilePath))
116+
catch (Exception notFoundException) when (notFoundException is DirectoryNotFoundException or FileNotFoundException)
108117
{
109-
var meta = JsonSerializer.Deserialize(metaFileStream, SourceGenerationContext.Default.DockerContextMeta);
110-
var host = meta?.Name == dockerContext ? meta.Endpoints?.Docker?.Host : null;
111-
return string.IsNullOrEmpty(host) ? null : new Uri(host.Replace("npipe:////./", "npipe://./"));
118+
throw new DockerConfigurationException($"The Docker context '{dockerContext}' does not exist", notFoundException);
119+
}
120+
catch (Exception exception) when (exception is not DockerConfigurationException)
121+
{
122+
throw new DockerConfigurationException($"The Docker context '{dockerContext}' failed to load from {metaFilePath}", exception);
112123
}
113124
}
114125
}
@@ -162,15 +173,11 @@ private string GetDockerContext()
162173
internal sealed class DockerContextMeta
163174
{
164175
[JsonConstructor]
165-
public DockerContextMeta(string name, DockerContextMetaEndpoints endpoints)
176+
public DockerContextMeta(DockerContextMetaEndpoints endpoints)
166177
{
167-
Name = name;
168178
Endpoints = endpoints;
169179
}
170180

171-
[JsonPropertyName("Name")]
172-
public string Name { get; }
173-
174181
[JsonPropertyName("Endpoints")]
175182
public DockerContextMetaEndpoints Endpoints { get; }
176183
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
4+
namespace DotNet.Testcontainers.Builders
5+
{
6+
/// <summary>
7+
/// The exception that is thrown when the Docker configuration file cannot be read successfully.
8+
/// </summary>
9+
[PublicAPI]
10+
public sealed class DockerConfigurationException : Exception
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="DockerConfigurationException"/> class, using the provided message.
14+
/// </summary>
15+
/// <param name="message">The error message that explains the reason for the exception.</param>
16+
public DockerConfigurationException(string message) : base(message)
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="DockerConfigurationException"/> class, using the provided message and exception that is the cause of this exception.
22+
/// </summary>
23+
/// <param name="message">The error message that explains the reason for the exception.</param>
24+
/// <param name="innerException">The exception that is the cause of the current exception.</param>
25+
public DockerConfigurationException(string message, Exception innerException) : base(message, innerException)
26+
{
27+
}
28+
}
29+
}

src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal sealed class DockerDesktopEndpointAuthenticationProvider : RootlessUnix
1515
/// Initializes a new instance of the <see cref="DockerDesktopEndpointAuthenticationProvider" /> class.
1616
/// </summary>
1717
public DockerDesktopEndpointAuthenticationProvider()
18-
: base(DockerConfig.Instance.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir())
18+
: base(DockerConfig.Instance.GetCurrentEndpoint())
1919
{
2020
}
2121

src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ public RootlessUnixEndpointAuthenticationProvider(params string[] socketPaths)
3030
DockerEngine = socketPath == null ? null : new Uri("unix://" + socketPath);
3131
}
3232

33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="RootlessUnixEndpointAuthenticationProvider" /> class.
35+
/// </summary>
36+
/// <param name="dockerEngine">The Unix socket Docker Engine endpoint.</param>
37+
public RootlessUnixEndpointAuthenticationProvider(Uri dockerEngine)
38+
{
39+
DockerEngine = dockerEngine;
40+
}
41+
3342
/// <summary>
3443
/// Gets the Unix socket Docker Engine endpoint.
3544
/// </summary>

tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs

+46-20
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ public void ReturnsDefaultEndpointWhenDockerContextIsDefault()
4646
public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromPropertiesFile()
4747
{
4848
// Given
49-
using var context = new ConfigMetaFile("custom", "tcp://127.0.0.1:2375/");
49+
using var context = new ConfigMetaFile("custom", new Uri("tcp://127.0.0.1:2375/"));
5050

51-
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom", context.GetDockerConfig() });
51+
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom", $"docker.config={context.DockerConfigDirectoryPath}" });
5252
var dockerConfig = new DockerConfig(customConfiguration);
5353

5454
// When
@@ -62,10 +62,10 @@ public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromPropertiesFile
6262
public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromConfigFile()
6363
{
6464
// Given
65-
using var context = new ConfigMetaFile("custom", "tcp://127.0.0.1:2375/");
65+
using var context = new ConfigMetaFile("custom", new Uri("tcp://127.0.0.1:2375/"));
6666

6767
// This test reads the current context JSON node from the Docker config file.
68-
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { context.GetDockerConfig() });
68+
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { $"docker.config={context.DockerConfigDirectoryPath}" });
6969
var dockerConfig = new DockerConfig(customConfiguration);
7070

7171
// When
@@ -83,17 +83,37 @@ public void ReturnsActiveEndpointWhenDockerContextIsUnset()
8383
}
8484

8585
[Fact]
86-
public void ReturnsNullWhenDockerContextNotFound()
86+
public void ThrowsWhenDockerContextNotFound()
8787
{
8888
// Given
8989
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=missing" });
9090
var dockerConfig = new DockerConfig(customConfiguration);
9191

9292
// When
93-
var currentEndpoint = dockerConfig.GetCurrentEndpoint();
93+
var exception = Assert.Throws<DockerConfigurationException>(() => dockerConfig.GetCurrentEndpoint());
9494

9595
// Then
96-
Assert.Null(currentEndpoint);
96+
Assert.Equal("The Docker context 'missing' does not exist", exception.Message);
97+
Assert.IsType<DirectoryNotFoundException>(exception.InnerException);
98+
}
99+
100+
[Fact]
101+
public void ThrowsWhenDockerConfigEndpointNotFound()
102+
{
103+
// Given
104+
using var context = new ConfigMetaFile("custom");
105+
106+
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom", $"docker.config={context.DockerConfigDirectoryPath}" });
107+
var dockerConfig = new DockerConfig(customConfiguration);
108+
109+
// When
110+
var exception = Assert.Throws<DockerConfigurationException>(() => dockerConfig.GetCurrentEndpoint());
111+
112+
// Then
113+
Assert.StartsWith("The Docker host is null in ", exception.Message);
114+
Assert.Contains(context.DockerConfigDirectoryPath, exception.Message);
115+
Assert.EndsWith(" (JSONPath: Endpoints.docker.Host)", exception.Message);
116+
Assert.Null(exception.InnerException);
97117
}
98118
}
99119

@@ -117,9 +137,9 @@ public void ReturnsActiveEndpointWhenDockerHostIsEmpty()
117137
public void ReturnsConfiguredEndpointWhenDockerHostIsSet()
118138
{
119139
// Given
120-
using var context = new ConfigMetaFile("custom", "");
140+
using var context = new ConfigMetaFile("custom");
121141

122-
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/", context.GetDockerConfig() });
142+
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/", $"docker.config={context.DockerConfigDirectoryPath}" });
123143
var dockerConfig = new DockerConfig(customConfiguration);
124144

125145
// When
@@ -147,26 +167,32 @@ private sealed class ConfigMetaFile : IDisposable
147167

148168
private const string MetaFileJson = "{{\"Name\":\"{0}\",\"Metadata\":{{}},\"Endpoints\":{{\"docker\":{{\"Host\":\"{1}\",\"SkipTLSVerify\":false}}}}}}";
149169

150-
private readonly string _dockerConfigDirectoryPath;
170+
public string DockerConfigDirectoryPath { get; }
151171

152-
public ConfigMetaFile(string context, string endpoint, [CallerMemberName] string caller = "")
172+
public ConfigMetaFile(string context, [CallerMemberName] string caller = "")
153173
{
154-
_dockerConfigDirectoryPath = Path.Combine(TestSession.TempDirectoryPath, caller);
155-
var dockerContextHash = Convert.ToHexString(SHA256.HashData(Encoding.Default.GetBytes(context))).ToLowerInvariant();
156-
var dockerContextMetaDirectoryPath = Path.Combine(_dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash);
157-
_ = Directory.CreateDirectory(dockerContextMetaDirectoryPath);
158-
File.WriteAllText(Path.Combine(_dockerConfigDirectoryPath, "config.json"), string.Format(ConfigFileJson, context));
159-
File.WriteAllText(Path.Combine(dockerContextMetaDirectoryPath, "meta.json"), string.Format(MetaFileJson, context, endpoint));
174+
DockerConfigDirectoryPath = InitializeContext(context, null, caller);
160175
}
161176

162-
public string GetDockerConfig()
177+
public ConfigMetaFile(string context, Uri endpoint, [CallerMemberName] string caller = "")
163178
{
164-
return "docker.config=" + _dockerConfigDirectoryPath;
179+
DockerConfigDirectoryPath = InitializeContext(context, endpoint, caller);
180+
}
181+
182+
private static string InitializeContext(string context, Uri endpoint, [CallerMemberName] string caller = "")
183+
{
184+
var dockerConfigDirectoryPath = Path.Combine(TestSession.TempDirectoryPath, caller);
185+
var dockerContextHash = Convert.ToHexString(SHA256.HashData(Encoding.Default.GetBytes(context))).ToLowerInvariant();
186+
var dockerContextMetaDirectoryPath = Path.Combine(dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash);
187+
_ = Directory.CreateDirectory(dockerContextMetaDirectoryPath);
188+
File.WriteAllText(Path.Combine(dockerConfigDirectoryPath, "config.json"), string.Format(ConfigFileJson, context));
189+
File.WriteAllText(Path.Combine(dockerContextMetaDirectoryPath, "meta.json"), endpoint == null ? "{}" : string.Format(MetaFileJson, context, endpoint.AbsoluteUri));
190+
return dockerConfigDirectoryPath;
165191
}
166192

167193
public void Dispose()
168194
{
169-
Directory.Delete(_dockerConfigDirectoryPath, true);
195+
Directory.Delete(DockerConfigDirectoryPath, true);
170196
}
171197
}
172198
}

0 commit comments

Comments
 (0)