Skip to content

Commit

Permalink
Improve error reporting when loading the Docker configuration file
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed Nov 26, 2024
1 parent 2e15016 commit 1e95bea
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 34 deletions.
33 changes: 20 additions & 13 deletions src/Testcontainers/Builders/DockerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public JsonDocument Parse()
/// 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]
[NotNull]
public Uri GetCurrentEndpoint()
{
const string defaultDockerContext = "default";
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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; }
}
Expand Down
29 changes: 29 additions & 0 deletions src/Testcontainers/Builders/DockerConfigurationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace DotNet.Testcontainers.Builders
{
using System;
using JetBrains.Annotations;

/// <summary>
/// The exception that is thrown when the Docker configuration file cannot be read successfully.
/// </summary>
[PublicAPI]
public sealed class DockerConfigurationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="DockerConfigurationException"/> class, using the provided message.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
public DockerConfigurationException(string message) : base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerConfigurationException"/> class, using the provided message and exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public DockerConfigurationException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
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(DockerConfig.Instance.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir())
: base(DockerConfig.Instance.GetCurrentEndpoint())
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ public RootlessUnixEndpointAuthenticationProvider(params string[] socketPaths)
DockerEngine = socketPath == null ? null : new Uri("unix://" + socketPath);
}

/// <summary>
/// Initializes a new instance of the <see cref="RootlessUnixEndpointAuthenticationProvider" /> class.
/// </summary>
/// <param name="dockerEngine">The Unix socket Docker Engine endpoint.</param>
public RootlessUnixEndpointAuthenticationProvider(Uri dockerEngine)
{
DockerEngine = dockerEngine;
}

/// <summary>
/// Gets the Unix socket Docker Engine endpoint.
/// </summary>
Expand Down
66 changes: 46 additions & 20 deletions tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<DockerConfigurationException>(() => dockerConfig.GetCurrentEndpoint());

// Then
Assert.Null(currentEndpoint);
Assert.Equal("The Docker context 'missing' does not exist", exception.Message);
Assert.IsType<DirectoryNotFoundException>(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<DockerConfigurationException>(() => 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);
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}
}
Expand Down

0 comments on commit 1e95bea

Please sign in to comment.