From 71994cade8402472cbb5e7ea23a2db6d8aedacf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Fri, 16 Aug 2024 21:06:16 +0200 Subject: [PATCH 01/23] Get the current docker endpoint through the `docker context` command This enables seamless integration with alternative Docker engines such as [Colima][1] or [OrbStack][2]. Without this change it requires a [specific configuration][3] that's far from obvious to figure out. [1]: https://github.com/abiosoft/colima [2]: https://orbstack.dev [3]: https://github.com/testcontainers/testcontainers-java/issues/5034#issuecomment-1036433226 --- ...erDesktopEndpointAuthenticationProvider.cs | 2 +- src/Testcontainers/Builders/DockerProcess.cs | 42 +++++++++++++++++++ ...tlessUnixEndpointAuthenticationProvider.cs | 6 +-- .../Unit/Builders/DockerProcessTest.cs | 14 +++++++ 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 src/Testcontainers/Builders/DockerProcess.cs create mode 100644 tests/Testcontainers.Tests/Unit/Builders/DockerProcessTest.cs diff --git a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs index 5ec60e648..e70afe19c 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(GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) + : base(DockerProcess.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) { } diff --git a/src/Testcontainers/Builders/DockerProcess.cs b/src/Testcontainers/Builders/DockerProcess.cs new file mode 100644 index 000000000..1dbb639c1 --- /dev/null +++ b/src/Testcontainers/Builders/DockerProcess.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; + +namespace DotNet.Testcontainers.Builders +{ + internal static class DockerProcess + { + public static Uri GetCurrentEndpoint() + { + var dockerStartInfo = new ProcessStartInfo(); + dockerStartInfo.FileName = "docker"; + dockerStartInfo.Arguments = "context inspect --format {{.Endpoints.docker.Host}}"; + dockerStartInfo.RedirectStandardOutput = true; + dockerStartInfo.UseShellExecute = false; + + var dockerProcess = new Process(); + dockerProcess.StartInfo = dockerStartInfo; + + try + { + if (dockerProcess.Start()) + { + dockerProcess.WaitForExit(2000); + if (dockerProcess.ExitCode == 0) + { + var endpoint = dockerProcess.StandardOutput.ReadToEnd().Trim(); + return new Uri(endpoint); + } + } + return null; + } + catch + { + return null; + } + finally + { + dockerProcess.Dispose(); + } + } + } +} diff --git a/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs index 0195d6005..ac758ae53 100644 --- a/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/RootlessUnixEndpointAuthenticationProvider.cs @@ -27,10 +27,8 @@ public RootlessUnixEndpointAuthenticationProvider() /// A list of socket paths. 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); } /// diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerProcessTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerProcessTest.cs new file mode 100644 index 000000000..5363adf62 --- /dev/null +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerProcessTest.cs @@ -0,0 +1,14 @@ +namespace DotNet.Testcontainers.Tests.Unit +{ + using Xunit; + + public class DockerProcessTest + { + [Fact] + public void GetCurrentEndpoint() + { + var endpoint = Builders.DockerProcess.GetCurrentEndpoint(); + Assert.NotNull(endpoint); + } + } +} From 027f7047004c58e680a0af33d4656fee6dce1bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 19 Aug 2024 18:34:11 +0200 Subject: [PATCH 02/23] Read the current docker endpoint from the file system --- src/Testcontainers/Builders/DockerConfig.cs | 111 ++++++++++++++++++ ...erDesktopEndpointAuthenticationProvider.cs | 2 +- src/Testcontainers/Builders/DockerProcess.cs | 42 ------- .../DockerRegistryAuthenticationProvider.cs | 49 +++----- ...ckerProcessTest.cs => DockerConfigTest.cs} | 5 +- 5 files changed, 134 insertions(+), 75 deletions(-) create mode 100644 src/Testcontainers/Builders/DockerConfig.cs delete mode 100644 src/Testcontainers/Builders/DockerProcess.cs rename tests/Testcontainers.Tests/Unit/Builders/{DockerProcessTest.cs => DockerConfigTest.cs} (54%) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs new file mode 100644 index 000000000..12d09ec6f --- /dev/null +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -0,0 +1,111 @@ +namespace DotNet.Testcontainers.Builders +{ + using System; + using System.IO; + using System.Text.Json; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + + internal class DockerConfig + { + private static readonly string UserProfileDockerConfigDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker"); + + private static readonly string UserProfileDockerContextMetaPath = Path.Combine(UserProfileDockerConfigDirectoryPath, "contexts", "meta"); + + private static FileInfo GetFile() + { + var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? PropertiesFileConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath; + return new FileInfo(Path.Combine(dockerConfigDirectoryPath, "config.json")); + } + + private readonly FileInfo _file; + + public DockerConfig() : this(GetFile()) + { + } + + public DockerConfig(FileInfo file) + { + _file = file; + } + + public bool Exists => _file.Exists; + + public string FullName => _file.FullName; + + public JsonDocument Parse() + { + using (var dockerConfigFileStream = _file.OpenRead()) + { + return JsonDocument.Parse(dockerConfigFileStream); + } + } + + [CanBeNull] + public Uri GetCurrentEndpoint() + { + try + { + using (var config = Parse()) + { + if (config.RootElement.TryGetProperty("currentContext", out var currentContextNode) && currentContextNode.ValueKind == JsonValueKind.String) + { + var currentContext = currentContextNode.GetString(); + foreach (var metaDirectory in Directory.EnumerateDirectories(UserProfileDockerContextMetaPath, "*", SearchOption.TopDirectoryOnly)) + { + try + { + var endpoint = GetEndpoint(metaDirectory, currentContext); + if (endpoint != null) + { + return endpoint; + } + } + catch + { + // try next meta.json file + } + } + } + } + } + catch + { + return null; + } + + return null; + } + + [CanBeNull] + private static Uri GetEndpoint(string metaDirectory, string currentContext) + { + var metaFilePath = Path.Combine(metaDirectory, "meta.json"); + using (var metaFileStream = new FileStream(metaFilePath, FileMode.Open, FileAccess.Read)) + { + using (var meta = JsonDocument.Parse(metaFileStream)) + { + if (meta.RootElement.TryGetProperty("Name", out var nameNode) && nameNode.ValueKind == JsonValueKind.String && nameNode.GetString() == currentContext) + { + if (meta.RootElement.TryGetProperty("Endpoints", out var endpointsNode) && endpointsNode.ValueKind == JsonValueKind.Object) + { + if (endpointsNode.TryGetProperty("docker", out var dockerNode) && dockerNode.ValueKind == JsonValueKind.Object) + { + if (dockerNode.TryGetProperty("Host", out var hostNode) && hostNode.ValueKind == JsonValueKind.String) + { + var host = hostNode.GetString(); + if (!string.IsNullOrEmpty(host)) + { + return new Uri(host); + } + } + } + } + } + } + } + + return null; + } + } +} diff --git a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs index e70afe19c..71e3d873e 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(DockerProcess.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) + : base(new DockerConfig().GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) { } diff --git a/src/Testcontainers/Builders/DockerProcess.cs b/src/Testcontainers/Builders/DockerProcess.cs deleted file mode 100644 index 1dbb639c1..000000000 --- a/src/Testcontainers/Builders/DockerProcess.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Diagnostics; - -namespace DotNet.Testcontainers.Builders -{ - internal static class DockerProcess - { - public static Uri GetCurrentEndpoint() - { - var dockerStartInfo = new ProcessStartInfo(); - dockerStartInfo.FileName = "docker"; - dockerStartInfo.Arguments = "context inspect --format {{.Endpoints.docker.Host}}"; - dockerStartInfo.RedirectStandardOutput = true; - dockerStartInfo.UseShellExecute = false; - - var dockerProcess = new Process(); - dockerProcess.StartInfo = dockerStartInfo; - - try - { - if (dockerProcess.Start()) - { - dockerProcess.WaitForExit(2000); - if (dockerProcess.ExitCode == 0) - { - var endpoint = dockerProcess.StandardOutput.ReadToEnd().Trim(); - return new Uri(endpoint); - } - } - return null; - } - catch - { - return null; - } - finally - { - dockerProcess.Dispose(); - } - } - } -} diff --git a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs index 600230a6b..5b31d73f9 100644 --- a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs @@ -16,9 +16,7 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth private static readonly ConcurrentDictionary> Credentials = new ConcurrentDictionary>(); - 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; @@ -28,7 +26,7 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth /// The logger. [PublicAPI] public DockerRegistryAuthenticationProvider(ILogger logger) - : this(GetDefaultDockerConfigFilePath(), logger) + : this(new DockerConfig(), logger) { } @@ -39,19 +37,19 @@ public DockerRegistryAuthenticationProvider(ILogger logger) /// The logger. [PublicAPI] public DockerRegistryAuthenticationProvider(string dockerConfigFilePath, ILogger logger) - : this(new FileInfo(dockerConfigFilePath), logger) + : this(new DockerConfig(new FileInfo(dockerConfigFilePath)), logger) { } /// /// Initializes a new instance of the class. /// - /// The Docker config file path. + /// The Docker config. /// The logger. [PublicAPI] - public DockerRegistryAuthenticationProvider(FileInfo dockerConfigFilePath, ILogger logger) + public DockerRegistryAuthenticationProvider(DockerConfig dockerConfig, ILogger logger) { - _dockerConfigFilePath = dockerConfigFilePath; + _dockerConfig = dockerConfig; _logger = logger; } @@ -68,12 +66,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("{}"); @@ -85,28 +77,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); } diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerProcessTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs similarity index 54% rename from tests/Testcontainers.Tests/Unit/Builders/DockerProcessTest.cs rename to tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 5363adf62..4124301a9 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerProcessTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -1,13 +1,14 @@ namespace DotNet.Testcontainers.Tests.Unit { + using DotNet.Testcontainers.Builders; using Xunit; - public class DockerProcessTest + public class DockerConfigTest { [Fact] public void GetCurrentEndpoint() { - var endpoint = Builders.DockerProcess.GetCurrentEndpoint(); + var endpoint = new DockerConfig().GetCurrentEndpoint(); Assert.NotNull(endpoint); } } From 38cd08ae00edc801fe40284e0b5f924405e1acb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 19 Aug 2024 18:55:50 +0200 Subject: [PATCH 03/23] Deserialize the meta.json file with typed objects --- src/Testcontainers/Builders/DockerConfig.cs | 65 +++++++++++-------- .../Builders/SourceGenerationContext.cs | 7 ++ 2 files changed, 45 insertions(+), 27 deletions(-) create mode 100644 src/Testcontainers/Builders/SourceGenerationContext.cs diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index 12d09ec6f..2765f4a4d 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -1,8 +1,9 @@ -namespace DotNet.Testcontainers.Builders +namespace DotNet.Testcontainers.Builders { using System; using System.IO; using System.Text.Json; + using System.Text.Json.Serialization; using DotNet.Testcontainers.Configurations; using JetBrains.Annotations; @@ -53,17 +54,10 @@ public Uri GetCurrentEndpoint() var currentContext = currentContextNode.GetString(); foreach (var metaDirectory in Directory.EnumerateDirectories(UserProfileDockerContextMetaPath, "*", SearchOption.TopDirectoryOnly)) { - try + var endpoint = GetEndpoint(metaDirectory, currentContext); + if (endpoint != null) { - var endpoint = GetEndpoint(metaDirectory, currentContext); - if (endpoint != null) - { - return endpoint; - } - } - catch - { - // try next meta.json file + return endpoint; } } } @@ -80,32 +74,49 @@ public Uri GetCurrentEndpoint() [CanBeNull] private static Uri GetEndpoint(string metaDirectory, string currentContext) { - var metaFilePath = Path.Combine(metaDirectory, "meta.json"); - using (var metaFileStream = new FileStream(metaFilePath, FileMode.Open, FileAccess.Read)) + try { - using (var meta = JsonDocument.Parse(metaFileStream)) + var metaFilePath = Path.Combine(metaDirectory, "meta.json"); + using (var metaFileStream = new FileStream(metaFilePath, FileMode.Open, FileAccess.Read)) { - if (meta.RootElement.TryGetProperty("Name", out var nameNode) && nameNode.ValueKind == JsonValueKind.String && nameNode.GetString() == currentContext) + var meta = JsonSerializer.Deserialize(metaFileStream, SourceGenerationContext.Default.DockerContextMeta); + if (meta?.Name == currentContext) { - if (meta.RootElement.TryGetProperty("Endpoints", out var endpointsNode) && endpointsNode.ValueKind == JsonValueKind.Object) + var host = meta?.Endpoints?.Docker?.Host; + if (!string.IsNullOrEmpty(host)) { - if (endpointsNode.TryGetProperty("docker", out var dockerNode) && dockerNode.ValueKind == JsonValueKind.Object) - { - if (dockerNode.TryGetProperty("Host", out var hostNode) && hostNode.ValueKind == JsonValueKind.String) - { - var host = hostNode.GetString(); - if (!string.IsNullOrEmpty(host)) - { - return new Uri(host); - } - } - } + return new Uri(host); } } } } + catch + { + return null; + } return null; } + + internal class DockerContextMeta + { + [JsonPropertyName("Name"), CanBeNull] + public string Name { get; set; } + + [JsonPropertyName("Endpoints"), CanBeNull] + public DockerContextMetaEndpoints Endpoints { get; set; } + } + + internal class DockerContextMetaEndpoints + { + [JsonPropertyName("docker"), CanBeNull] + public DockerContextMetaEndpointsDocker Docker { get; set; } + } + + internal class DockerContextMetaEndpointsDocker + { + [JsonPropertyName("Host"), CanBeNull] + public string Host { get; set; } + } } } diff --git a/src/Testcontainers/Builders/SourceGenerationContext.cs b/src/Testcontainers/Builders/SourceGenerationContext.cs new file mode 100644 index 000000000..f26ad0b8e --- /dev/null +++ b/src/Testcontainers/Builders/SourceGenerationContext.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace DotNet.Testcontainers.Builders +{ + [JsonSerializable(typeof(DockerConfig.DockerContextMeta))] + internal partial class SourceGenerationContext : JsonSerializerContext; +} From ce075c81526503cc459bde499cf30a248015851e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 19 Aug 2024 19:05:34 +0200 Subject: [PATCH 04/23] Improve DockerConfigTest.GetCurrentEndpoint test --- .../Unit/Builders/DockerConfigTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 4124301a9..e247457db 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -1,5 +1,7 @@ namespace DotNet.Testcontainers.Tests.Unit { + using System; + using System.Diagnostics; using DotNet.Testcontainers.Builders; using Xunit; @@ -10,6 +12,28 @@ public void GetCurrentEndpoint() { var endpoint = new DockerConfig().GetCurrentEndpoint(); Assert.NotNull(endpoint); + + var expectedEndpoint = DockerProcess.GetCurrentEndpoint(); + Assert.Equal(expectedEndpoint, endpoint); + } + } + + internal static class DockerProcess + { + public static Uri GetCurrentEndpoint() + { + using var docker = new Process(); + docker.StartInfo = new ProcessStartInfo + { + FileName = "docker", + Arguments = "context inspect --format {{.Endpoints.docker.Host}}", + RedirectStandardOutput = true, + UseShellExecute = false, + }; + docker.Start(); + docker.WaitForExit(2000); + var endpoint = docker.StandardOutput.ReadToEnd().Trim(); + return new Uri(endpoint); } } } From 8e864ecf4f71291091172896505387f032fa08f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 19 Aug 2024 19:17:20 +0200 Subject: [PATCH 05/23] Avoid instantiating the default DockerConfig twice --- src/Testcontainers/Builders/DockerConfig.cs | 4 +++- .../Builders/DockerDesktopEndpointAuthenticationProvider.cs | 2 +- .../Builders/DockerRegistryAuthenticationProvider.cs | 2 +- tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index 2765f4a4d..49f56f1c4 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -19,9 +19,11 @@ private static FileInfo GetFile() return new FileInfo(Path.Combine(dockerConfigDirectoryPath, "config.json")); } + public static DockerConfig Default { get; } = new DockerConfig(); + private readonly FileInfo _file; - public DockerConfig() : this(GetFile()) + private DockerConfig() : this(GetFile()) { } diff --git a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs index 71e3d873e..7c60044f9 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(new DockerConfig().GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) + : base(DockerConfig.Default.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) { } diff --git a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs index 5b31d73f9..e90b07326 100644 --- a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs @@ -26,7 +26,7 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth /// The logger. [PublicAPI] public DockerRegistryAuthenticationProvider(ILogger logger) - : this(new DockerConfig(), logger) + : this(DockerConfig.Default, logger) { } diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index e247457db..b227685d5 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -10,7 +10,7 @@ public class DockerConfigTest [Fact] public void GetCurrentEndpoint() { - var endpoint = new DockerConfig().GetCurrentEndpoint(); + var endpoint = DockerConfig.Default.GetCurrentEndpoint(); Assert.NotNull(endpoint); var expectedEndpoint = DockerProcess.GetCurrentEndpoint(); From 512784ffcb43f257757039f6c31d5230899666e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 19 Aug 2024 20:51:13 +0200 Subject: [PATCH 06/23] Leverage the existing DockerCli class --- tests/Testcontainers.Commons/DockerCli.cs | 6 +++++ .../Unit/Builders/DockerConfigTest.cs | 24 ++----------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/tests/Testcontainers.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index eb660a6d9..f41f60f0f 100644 --- a/tests/Testcontainers.Commons/DockerCli.cs +++ b/tests/Testcontainers.Commons/DockerCli.cs @@ -53,6 +53,12 @@ public static bool ResourceExists(DockerResource resource, string id) return 0.Equals(commandResult.ExitCode); } + public static Uri GetCurrentEndpoint() + { + var commandResult = new Command("context", "inspect", "--format", "{{.Endpoints.docker.Host}}").Execute(); + return new Uri(commandResult.Stdout); + } + [PublicAPI] private sealed class Command { diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index b227685d5..1eea3328d 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -1,8 +1,7 @@ namespace DotNet.Testcontainers.Tests.Unit { - using System; - using System.Diagnostics; using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Commons; using Xunit; public class DockerConfigTest @@ -13,27 +12,8 @@ public void GetCurrentEndpoint() var endpoint = DockerConfig.Default.GetCurrentEndpoint(); Assert.NotNull(endpoint); - var expectedEndpoint = DockerProcess.GetCurrentEndpoint(); + var expectedEndpoint = DockerCli.GetCurrentEndpoint(); Assert.Equal(expectedEndpoint, endpoint); } } - - internal static class DockerProcess - { - public static Uri GetCurrentEndpoint() - { - using var docker = new Process(); - docker.StartInfo = new ProcessStartInfo - { - FileName = "docker", - Arguments = "context inspect --format {{.Endpoints.docker.Host}}", - RedirectStandardOutput = true, - UseShellExecute = false, - }; - docker.Start(); - docker.WaitForExit(2000); - var endpoint = docker.StandardOutput.ReadToEnd().Trim(); - return new Uri(endpoint); - } - } } From 7df5dc8ebde7785e1b68559f8a9b3e292f6463b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 19 Aug 2024 21:06:22 +0200 Subject: [PATCH 07/23] Log the current endpoint --- .../Unit/Builders/DockerConfigTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 1eea3328d..4b0149219 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -3,16 +3,16 @@ namespace DotNet.Testcontainers.Tests.Unit using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Commons; using Xunit; + using Xunit.Abstractions; - public class DockerConfigTest + public class DockerConfigTest(ITestOutputHelper output) { [Fact] public void GetCurrentEndpoint() { - var endpoint = DockerConfig.Default.GetCurrentEndpoint(); - Assert.NotNull(endpoint); - var expectedEndpoint = DockerCli.GetCurrentEndpoint(); + var endpoint = DockerConfig.Default.GetCurrentEndpoint(); + output.WriteLine($"DockerConfig.Default.GetCurrentEndpoint() => {endpoint}"); Assert.Equal(expectedEndpoint, endpoint); } } From a33f149509cfb375314891ec3077d84c963be415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Tue, 20 Aug 2024 00:49:07 +0200 Subject: [PATCH 08/23] Handle the default context --- src/Testcontainers/Builders/DockerConfig.cs | 35 ++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index 49f56f1c4..ff62c17da 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -49,19 +49,18 @@ public Uri GetCurrentEndpoint() { try { - using (var config = Parse()) + var currentContext = GetCurrentContext(); + if (currentContext == null) { - if (config.RootElement.TryGetProperty("currentContext", out var currentContextNode) && currentContextNode.ValueKind == JsonValueKind.String) + return EnvironmentConfiguration.Instance.GetDockerHost() ?? UnixEndpointAuthenticationProvider.DockerEngine; + } + + foreach (var metaDirectory in Directory.EnumerateDirectories(UserProfileDockerContextMetaPath, "*", SearchOption.TopDirectoryOnly)) + { + var endpoint = GetEndpoint(metaDirectory, currentContext); + if (endpoint != null) { - var currentContext = currentContextNode.GetString(); - foreach (var metaDirectory in Directory.EnumerateDirectories(UserProfileDockerContextMetaPath, "*", SearchOption.TopDirectoryOnly)) - { - var endpoint = GetEndpoint(metaDirectory, currentContext); - if (endpoint != null) - { - return endpoint; - } - } + return endpoint; } } } @@ -73,6 +72,20 @@ public Uri GetCurrentEndpoint() return null; } + [CanBeNull] + private string GetCurrentContext() + { + using (var config = Parse()) + { + if (config.RootElement.TryGetProperty("currentContext", out var currentContextNode) && currentContextNode.ValueKind == JsonValueKind.String) + { + return currentContextNode.GetString(); + } + + return null; + } + } + [CanBeNull] private static Uri GetEndpoint(string metaDirectory, string currentContext) { From 8ba827bd40f22b8a1ba31e58beb99a7f5b66d423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Tue, 20 Aug 2024 11:26:49 +0200 Subject: [PATCH 09/23] Handle DOCKER_CONTEXT and DOCKER_HOST environment variables Like the real `docker context` commands --- src/Testcontainers/Builders/DockerConfig.cs | 47 ++++++++++++----- tests/Testcontainers.Commons/DockerCli.cs | 15 +++++- .../Unit/Builders/DockerConfigTest.cs | 50 ++++++++++++++++++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index ff62c17da..f82e2c7e1 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -2,6 +2,10 @@ { 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; @@ -22,14 +26,20 @@ private static FileInfo GetFile() public static DockerConfig Default { get; } = new DockerConfig(); private readonly FileInfo _file; + private readonly ICustomConfiguration _environment; - private DockerConfig() : this(GetFile()) + private DockerConfig() : this(GetFile(), EnvironmentConfiguration.Instance) { } - public DockerConfig(FileInfo file) + public DockerConfig(ICustomConfiguration environment) : this(GetFile(), environment) + { + } + + public DockerConfig(FileInfo file, ICustomConfiguration environment = null) { _file = file; + _environment = environment ?? EnvironmentConfiguration.Instance; } public bool Exists => _file.Exists; @@ -44,37 +54,48 @@ public JsonDocument Parse() } } + /// + /// Performs the equivalent of running docker context inspect --format {{.Endpoints.docker.Host}} + /// [CanBeNull] public Uri GetCurrentEndpoint() { try { - var currentContext = GetCurrentContext(); - if (currentContext == null) + var envDockerHost = _environment.GetDockerHost(); + if (envDockerHost != null) { - return EnvironmentConfiguration.Instance.GetDockerHost() ?? UnixEndpointAuthenticationProvider.DockerEngine; + return envDockerHost; } - foreach (var metaDirectory in Directory.EnumerateDirectories(UserProfileDockerContextMetaPath, "*", SearchOption.TopDirectoryOnly)) + var currentContext = GetCurrentContext(); + if (currentContext is null or "default") { - var endpoint = GetEndpoint(metaDirectory, currentContext); - if (endpoint != null) - { - return endpoint; - } + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? NpipeEndpointAuthenticationProvider.DockerEngine : UnixEndpointAuthenticationProvider.DockerEngine; } + + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(currentContext)); + var digest = hash.Aggregate(new StringBuilder(hash.Length * 2), (sb, b) => sb.Append(b.ToString("x2"))).ToString(); + var contextDirectory = Path.Combine(UserProfileDockerContextMetaPath, digest); + var endpoint = GetEndpoint(contextDirectory, currentContext); + return endpoint; } catch { return null; } - - return null; } [CanBeNull] private string GetCurrentContext() { + var dockerContext = Environment.GetEnvironmentVariable("DOCKER_CONTEXT"); + if (!string.IsNullOrEmpty(dockerContext)) + { + return dockerContext; + } + using (var config = Parse()) { if (config.RootElement.TryGetProperty("currentContext", out var currentContextNode) && currentContextNode.ValueKind == JsonValueKind.String) diff --git a/tests/Testcontainers.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index f41f60f0f..eb86546bf 100644 --- a/tests/Testcontainers.Commons/DockerCli.cs +++ b/tests/Testcontainers.Commons/DockerCli.cs @@ -55,8 +55,14 @@ public static bool ResourceExists(DockerResource resource, string id) public static Uri GetCurrentEndpoint() { - var commandResult = new Command("context", "inspect", "--format", "{{.Endpoints.docker.Host}}").Execute(); - return new Uri(commandResult.Stdout); + var command = new Command("context", "inspect", "--format", "{{.Endpoints.docker.Host}}"); + var commandResult = command.Execute(); + if (commandResult.ExitCode == 0) + { + return new Uri(commandResult.Stdout); + } + + throw new InvalidOperationException($"Executing `{command}` failed: {commandResult.Stderr}"); } [PublicAPI] @@ -116,6 +122,11 @@ private void AppendStderr(object sender, DataReceivedEventArgs e) { _stderr.Append(e.Data); } + + public override string ToString() + { + return $"{_processStartInfo.FileName} {_processStartInfo.Arguments}"; + } } [PublicAPI] diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 4b0149219..eecd88126 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -1,3 +1,8 @@ +using System; +using System.Reflection; +using DotNet.Testcontainers.Configurations; +using Xunit.Sdk; + namespace DotNet.Testcontainers.Tests.Unit { using DotNet.Testcontainers.Builders; @@ -9,11 +14,54 @@ public class DockerConfigTest(ITestOutputHelper output) { [Fact] public void GetCurrentEndpoint() + { + AssertCurrentEndpoint(); + } + + [Fact] + [UseEnvironmentVariable("DOCKER_CONTEXT", "default")] + public void GetCurrentEndpointWithDefaultDockerContextEnvironmentVariable() + { + AssertCurrentEndpoint(); + } + + [Fact] + [UseEnvironmentVariable("DOCKER_HOST", "tcp://docker:2375")] + public void GetCurrentEndpointWithDockerHostEnvironmentVariable() + { + var endpoint = AssertCurrentEndpoint(); + Assert.Equal(new Uri("tcp://docker:2375"), endpoint); + } + + private Uri AssertCurrentEndpoint() { var expectedEndpoint = DockerCli.GetCurrentEndpoint(); - var endpoint = DockerConfig.Default.GetCurrentEndpoint(); + var endpoint = new DockerConfig(new EnvironmentConfiguration()).GetCurrentEndpoint(); output.WriteLine($"DockerConfig.Default.GetCurrentEndpoint() => {endpoint}"); Assert.Equal(expectedEndpoint, endpoint); + Assert.NotNull(endpoint); + return endpoint; + } + + [Fact] + [UseEnvironmentVariable("DOCKER_CONTEXT", "wrong")] + public void GetCurrentEndpointWithWrongDockerContextEnvironmentVariable() + { + var endpoint = new DockerConfig(new EnvironmentConfiguration()).GetCurrentEndpoint(); + Assert.Null(endpoint); + } + } + + public class UseEnvironmentVariableAttribute(string variable, string value) : BeforeAfterTestAttribute + { + public override void Before(MethodInfo methodUnderTest) + { + Environment.SetEnvironmentVariable(variable, value); + } + + public override void After(MethodInfo methodUnderTest) + { + Environment.SetEnvironmentVariable(variable, null); } } } From 2295592555141d2beea9c2202a2e783373754ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Tue, 20 Aug 2024 14:02:02 +0200 Subject: [PATCH 10/23] Fix tests on Windows --- src/Testcontainers/Builders/DockerConfig.cs | 3 ++- tests/Testcontainers.Commons/DockerCli.cs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index f82e2c7e1..b01e474c8 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -121,7 +121,8 @@ private static Uri GetEndpoint(string metaDirectory, string currentContext) var host = meta?.Endpoints?.Docker?.Host; if (!string.IsNullOrEmpty(host)) { - return new Uri(host); + const string npipePrefix = "npipe:////./"; + return host.StartsWith(npipePrefix, StringComparison.Ordinal) ? new Uri($"npipe://./{host.Substring(npipePrefix.Length)}") : new Uri(host); } } } diff --git a/tests/Testcontainers.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index eb86546bf..44296f398 100644 --- a/tests/Testcontainers.Commons/DockerCli.cs +++ b/tests/Testcontainers.Commons/DockerCli.cs @@ -59,7 +59,10 @@ public static Uri GetCurrentEndpoint() var commandResult = command.Execute(); if (commandResult.ExitCode == 0) { - return new Uri(commandResult.Stdout); + // Because new Uri("npipe:////./pipe/docker_engine") returns "npipe:////pipe/docker_engine" + const string npipePrefix = "npipe:////./"; + var host = commandResult.Stdout; + return host.StartsWith(npipePrefix, StringComparison.Ordinal) ? new Uri($"npipe://./{host.Substring(npipePrefix.Length)}") : new Uri(host); } throw new InvalidOperationException($"Executing `{command}` failed: {commandResult.Stderr}"); From 9ce89e87cd58284bc6fe9167daac55a49a2a4f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Tue, 20 Aug 2024 22:13:44 +0200 Subject: [PATCH 11/23] Add Docker CLI reference --- src/Testcontainers/Builders/DockerConfig.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index b01e474c8..bda88bbc1 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -57,6 +57,9 @@ public JsonDocument Parse() /// /// Performs the equivalent of running docker context inspect --format {{.Endpoints.docker.Host}} /// + /// + /// See the Docker CLI implementation comments. + /// [CanBeNull] public Uri GetCurrentEndpoint() { From 6ccdda9de4b9cb7160d0f0022c4bf7b1eac38099 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:22:02 +0200 Subject: [PATCH 12/23] chore: Add ICustomConfiguration.GetDockerContext --- src/Testcontainers/Builders/DockerConfig.cs | 90 +++++++++++++------ ...erDesktopEndpointAuthenticationProvider.cs | 8 +- .../DockerRegistryAuthenticationProvider.cs | 2 +- .../Builders/SourceGenerationContext.cs | 6 +- ...ontainersEndpointAuthenticationProvider.cs | 6 ++ .../Configurations/CustomConfiguration.cs | 5 ++ .../EnvironmentConfiguration.cs | 9 ++ .../Configurations/ICustomConfiguration.cs | 8 ++ .../PropertiesFileConfiguration.cs | 7 ++ .../Unit/Builders/DockerConfigTest.cs | 33 +++++-- .../Configurations/CustomConfigurationTest.cs | 21 +++++ 11 files changed, 157 insertions(+), 38 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index b01e474c8..7ede93d16 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -11,44 +11,80 @@ using DotNet.Testcontainers.Configurations; using JetBrains.Annotations; - internal class DockerConfig + /// + /// + /// + internal sealed class DockerConfig { private static readonly string UserProfileDockerConfigDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker"); - private static readonly string UserProfileDockerContextMetaPath = Path.Combine(UserProfileDockerConfigDirectoryPath, "contexts", "meta"); + private static readonly string UserProfileDockerContextMetaDirectoryPath = Path.Combine(UserProfileDockerConfigDirectoryPath, "contexts", "meta"); - private static FileInfo GetFile() - { - var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? PropertiesFileConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath; - return new FileInfo(Path.Combine(dockerConfigDirectoryPath, "config.json")); - } + private readonly FileInfo _dockerConfigFile; - public static DockerConfig Default { get; } = new DockerConfig(); + [CanBeNull] + private readonly Uri _dockerHost; - private readonly FileInfo _file; - private readonly ICustomConfiguration _environment; + [CanBeNull] + private readonly string _dockerContext; - private DockerConfig() : this(GetFile(), EnvironmentConfiguration.Instance) + /// + /// + /// + public DockerConfig() + : this(GetDockerConfigFile()) { } - public DockerConfig(ICustomConfiguration environment) : this(GetFile(), environment) + /// + /// + /// + /// + public DockerConfig(FileInfo dockerConfigFile) + : this(dockerConfigFile, EnvironmentConfiguration.Instance, PropertiesFileConfiguration.Instance) { } - public DockerConfig(FileInfo file, ICustomConfiguration environment = null) + /// + /// + /// + /// + public DockerConfig(params ICustomConfiguration[] customConfigurations) + : this(GetDockerConfigFile(), customConfigurations) { - _file = file; - _environment = environment ?? EnvironmentConfiguration.Instance; } - public bool Exists => _file.Exists; + /// + /// + /// + /// + /// + public DockerConfig(FileInfo dockerConfigFile, params ICustomConfiguration[] customConfigurations) + { + _dockerConfigFile = dockerConfigFile; + _dockerHost = customConfigurations.Select(customConfiguration => customConfiguration.GetDockerHost()).FirstOrDefault(dockerHost => dockerHost != null); + _dockerContext = customConfigurations.Select(customConfiguration => customConfiguration.GetDockerContext()).FirstOrDefault(dockerContext => !string.IsNullOrEmpty(dockerContext)); + } - public string FullName => _file.FullName; + /// + /// + /// + public static DockerConfig Instance { get; } + = new DockerConfig(GetDockerConfigFile()); + /// + public bool Exists => _dockerConfigFile.Exists; + + /// + public string FullName => _dockerConfigFile.FullName; + + /// + /// + /// + /// public JsonDocument Parse() { - using (var dockerConfigFileStream = _file.OpenRead()) + using (var dockerConfigFileStream = _dockerConfigFile.OpenRead()) { return JsonDocument.Parse(dockerConfigFileStream); } @@ -62,10 +98,9 @@ public Uri GetCurrentEndpoint() { try { - var envDockerHost = _environment.GetDockerHost(); - if (envDockerHost != null) + if (_dockerHost != null) { - return envDockerHost; + return _dockerHost; } var currentContext = GetCurrentContext(); @@ -77,7 +112,7 @@ public Uri GetCurrentEndpoint() using var sha256 = SHA256.Create(); var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(currentContext)); var digest = hash.Aggregate(new StringBuilder(hash.Length * 2), (sb, b) => sb.Append(b.ToString("x2"))).ToString(); - var contextDirectory = Path.Combine(UserProfileDockerContextMetaPath, digest); + var contextDirectory = Path.Combine(UserProfileDockerContextMetaDirectoryPath, digest); var endpoint = GetEndpoint(contextDirectory, currentContext); return endpoint; } @@ -90,10 +125,9 @@ public Uri GetCurrentEndpoint() [CanBeNull] private string GetCurrentContext() { - var dockerContext = Environment.GetEnvironmentVariable("DOCKER_CONTEXT"); - if (!string.IsNullOrEmpty(dockerContext)) + if (!string.IsNullOrEmpty(_dockerContext)) { - return dockerContext; + return _dockerContext; } using (var config = Parse()) @@ -135,6 +169,12 @@ private static Uri GetEndpoint(string metaDirectory, string currentContext) return null; } + private static FileInfo GetDockerConfigFile() + { + var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? PropertiesFileConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath; + return new FileInfo(Path.Combine(dockerConfigDirectoryPath, "config.json")); + } + internal class DockerContextMeta { [JsonPropertyName("Name"), CanBeNull] diff --git a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs index 7c60044f9..c7eddad4a 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.Default.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) + : base(DockerConfig.Instance.GetCurrentEndpoint()?.AbsolutePath, GetSocketPathFromHomeDesktopDir(), GetSocketPathFromHomeRunDir()) { } @@ -37,6 +37,12 @@ public Uri GetDockerHost() return null; } + /// + public string GetDockerContext() + { + return null; + } + /// public string GetDockerHostOverride() { diff --git a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs index e90b07326..2a2067c4e 100644 --- a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs @@ -26,7 +26,7 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth /// The logger. [PublicAPI] public DockerRegistryAuthenticationProvider(ILogger logger) - : this(DockerConfig.Default, logger) + : this(DockerConfig.Instance, logger) { } diff --git a/src/Testcontainers/Builders/SourceGenerationContext.cs b/src/Testcontainers/Builders/SourceGenerationContext.cs index f26ad0b8e..f7993d8ef 100644 --- a/src/Testcontainers/Builders/SourceGenerationContext.cs +++ b/src/Testcontainers/Builders/SourceGenerationContext.cs @@ -1,7 +1,7 @@ -using System.Text.Json.Serialization; - -namespace DotNet.Testcontainers.Builders +namespace DotNet.Testcontainers.Builders { + using System.Text.Json.Serialization; + [JsonSerializable(typeof(DockerConfig.DockerContextMeta))] internal partial class SourceGenerationContext : JsonSerializerContext; } diff --git a/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs index a043d2e4e..93a42976d 100644 --- a/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs @@ -69,6 +69,12 @@ public Uri GetDockerHost() return _customConfiguration.GetDockerHost(); } + /// + public string GetDockerContext() + { + return _customConfiguration.GetDockerContext(); + } + /// public string GetDockerHostOverride() { diff --git a/src/Testcontainers/Configurations/CustomConfiguration.cs b/src/Testcontainers/Configurations/CustomConfiguration.cs index a44b1ae36..6c627ff54 100644 --- a/src/Testcontainers/Configurations/CustomConfiguration.cs +++ b/src/Testcontainers/Configurations/CustomConfiguration.cs @@ -25,6 +25,11 @@ protected virtual Uri GetDockerHost(string propertyName) return _properties.TryGetValue(propertyName, out var propertyValue) && Uri.TryCreate(propertyValue, UriKind.RelativeOrAbsolute, out var dockerHost) ? dockerHost : null; } + protected virtual string GetDockerContext(string propertyName) + { + return GetPropertyValue(propertyName); + } + protected virtual string GetDockerHostOverride(string propertyName) { return GetPropertyValue(propertyName); diff --git a/src/Testcontainers/Configurations/EnvironmentConfiguration.cs b/src/Testcontainers/Configurations/EnvironmentConfiguration.cs index ebc2dbcad..2d5706944 100644 --- a/src/Testcontainers/Configurations/EnvironmentConfiguration.cs +++ b/src/Testcontainers/Configurations/EnvironmentConfiguration.cs @@ -14,6 +14,8 @@ internal class EnvironmentConfiguration : CustomConfiguration, ICustomConfigurat private const string DockerHost = "DOCKER_HOST"; + private const string DockerContext = "DOCKER_CONTEXT"; + private const string DockerAuthConfig = "DOCKER_AUTH_CONFIG"; private const string DockerCertPath = "DOCKER_CERT_PATH"; @@ -54,6 +56,7 @@ public EnvironmentConfiguration() DockerCertPath, DockerConfig, DockerHost, + DockerContext, DockerTls, DockerTlsVerify, DockerHostOverride, @@ -88,6 +91,12 @@ public Uri GetDockerHost() return GetDockerHost(DockerHost); } + /// + public string GetDockerContext() + { + return GetDockerContext(DockerContext); + } + /// public string GetDockerHostOverride() { diff --git a/src/Testcontainers/Configurations/ICustomConfiguration.cs b/src/Testcontainers/Configurations/ICustomConfiguration.cs index 4cbd51285..7f0b173c2 100644 --- a/src/Testcontainers/Configurations/ICustomConfiguration.cs +++ b/src/Testcontainers/Configurations/ICustomConfiguration.cs @@ -26,6 +26,14 @@ internal interface ICustomConfiguration [CanBeNull] Uri GetDockerHost(); + /// + /// Gets the Docker context custom configuration. + /// + /// The Docker context custom configuration. + /// https://dotnet.testcontainers.org/custom_configuration/. + [CanBeNull] + string GetDockerContext(); + /// /// Gets the Docker host override custom configuration. /// diff --git a/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs b/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs index 86d1dc67c..7e303f33e 100644 --- a/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs +++ b/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs @@ -70,6 +70,13 @@ public Uri GetDockerHost() return GetDockerHost(propertyName); } + /// + public string GetDockerContext() + { + const string propertyName = "docker.context"; + return GetDockerContext(propertyName); + } + /// public string GetDockerHostOverride() { diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index eecd88126..2762a9f44 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -1,17 +1,24 @@ -using System; -using System.Reflection; using DotNet.Testcontainers.Configurations; -using Xunit.Sdk; namespace DotNet.Testcontainers.Tests.Unit { + using System; + using System.Reflection; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Commons; using Xunit; using Xunit.Abstractions; + using Xunit.Sdk; - public class DockerConfigTest(ITestOutputHelper output) + public sealed class DockerConfigTest { + private readonly ITestOutputHelper _testOutputHelper; + + public DockerConfigTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public void GetCurrentEndpoint() { @@ -37,7 +44,7 @@ private Uri AssertCurrentEndpoint() { var expectedEndpoint = DockerCli.GetCurrentEndpoint(); var endpoint = new DockerConfig(new EnvironmentConfiguration()).GetCurrentEndpoint(); - output.WriteLine($"DockerConfig.Default.GetCurrentEndpoint() => {endpoint}"); + _testOutputHelper.WriteLine($"DockerConfig.Default.GetCurrentEndpoint() => {endpoint}"); Assert.Equal(expectedEndpoint, endpoint); Assert.NotNull(endpoint); return endpoint; @@ -52,16 +59,26 @@ public void GetCurrentEndpointWithWrongDockerContextEnvironmentVariable() } } - public class UseEnvironmentVariableAttribute(string variable, string value) : BeforeAfterTestAttribute + public sealed class UseEnvironmentVariableAttribute : BeforeAfterTestAttribute { + private readonly string _variable; + + private readonly string _value; + + public UseEnvironmentVariableAttribute(string variable, string value) + { + _variable = variable; + _value = value; + } + public override void Before(MethodInfo methodUnderTest) { - Environment.SetEnvironmentVariable(variable, value); + Environment.SetEnvironmentVariable(_variable, _value); } public override void After(MethodInfo methodUnderTest) { - Environment.SetEnvironmentVariable(variable, null); + Environment.SetEnvironmentVariable(_variable, null); } } } diff --git a/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs index 72c6ecbd9..1ab8652d2 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs @@ -54,6 +54,17 @@ public void GetDockerHostCustomConfiguration(string propertyName, string propert Assert.Equal(expected, customConfiguration.GetDockerHost()?.ToString()); } + [Theory] + [InlineData("", "", null)] + [InlineData("DOCKER_CONTEXT", "", null)] + [InlineData("DOCKER_CONTEXT", "default", "default")] + public void GetDockerContextCustomConfiguration(string propertyName, string propertyValue, string expected) + { + SetEnvironmentVariable(propertyName, propertyValue); + ICustomConfiguration customConfiguration = new EnvironmentConfiguration(); + Assert.Equal(expected, customConfiguration.GetDockerContext()); + } + [Theory] [InlineData("", "", null)] [InlineData("TESTCONTAINERS_HOST_OVERRIDE", "", null)] @@ -254,6 +265,16 @@ public void GetDockerHostCustomConfiguration(string configuration, string expect Assert.Equal(expected, customConfiguration.GetDockerHost()?.ToString()); } + [Theory] + [InlineData("", null)] + [InlineData("docker.context=", null)] + [InlineData("docker.context=default", "default")] + public void GetDockerContextCustomConfiguration(string configuration, string expected) + { + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { configuration }); + Assert.Equal(expected, customConfiguration.GetDockerContext()); + } + [Theory] [InlineData("", null)] [InlineData("host.override=", null)] From 5555390799a3d417b2f676ed57fb6aff669a948e Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:23:03 +0200 Subject: [PATCH 13/23] chore: Add comments, increase tests --- src/Testcontainers/Builders/DockerConfig.cs | 182 +++++++++--------- tests/Testcontainers.Commons/DockerCli.cs | 17 +- .../Unit/Builders/DockerConfigTest.cs | 131 +++++++------ 3 files changed, 170 insertions(+), 160 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index 18b838aee..5c1d9303d 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -3,7 +3,6 @@ using System; using System.IO; using System.Linq; - using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -12,7 +11,7 @@ using JetBrains.Annotations; /// - /// + /// Represents a Docker config. /// internal sealed class DockerConfig { @@ -22,55 +21,44 @@ internal sealed class DockerConfig private readonly FileInfo _dockerConfigFile; - [CanBeNull] - private readonly Uri _dockerHost; - - [CanBeNull] - private readonly string _dockerContext; + private readonly ICustomConfiguration[] _customConfigurations; /// - /// + /// Initializes a new instance of the class. /// + [PublicAPI] public DockerConfig() - : this(GetDockerConfigFile()) + : this(GetDockerConfigFile(), EnvironmentConfiguration.Instance, PropertiesFileConfiguration.Instance) { } /// - /// + /// Initializes a new instance of the class. /// - /// - public DockerConfig(FileInfo dockerConfigFile) - : this(dockerConfigFile, EnvironmentConfiguration.Instance, PropertiesFileConfiguration.Instance) - { - } - - /// - /// - /// - /// + /// A list of custom configurations. + [PublicAPI] public DockerConfig(params ICustomConfiguration[] customConfigurations) : this(GetDockerConfigFile(), customConfigurations) { } /// - /// + /// Initializes a new instance of the class. /// - /// - /// + /// The Docker config file. + /// A list of custom configurations. + [PublicAPI] public DockerConfig(FileInfo dockerConfigFile, params ICustomConfiguration[] customConfigurations) { _dockerConfigFile = dockerConfigFile; - _dockerHost = customConfigurations.Select(customConfiguration => customConfiguration.GetDockerHost()).FirstOrDefault(dockerHost => dockerHost != null); - _dockerContext = customConfigurations.Select(customConfiguration => customConfiguration.GetDockerContext()).FirstOrDefault(dockerContext => !string.IsNullOrEmpty(dockerContext)); + _customConfigurations = customConfigurations; } /// - /// + /// Gets the instance. /// public static DockerConfig Instance { get; } - = new DockerConfig(GetDockerConfigFile()); + = new DockerConfig(); /// public bool Exists => _dockerConfigFile.Exists; @@ -79,9 +67,9 @@ public DockerConfig(FileInfo dockerConfigFile, params ICustomConfiguration[] cus public string FullName => _dockerConfigFile.FullName; /// - /// + /// Parses the Docker config file. /// - /// + /// A representing the Docker config. public JsonDocument Parse() { using (var dockerConfigFileStream = _dockerConfigFile.OpenRead()) @@ -91,46 +79,61 @@ public JsonDocument Parse() } /// - /// Performs the equivalent of running docker context inspect --format {{.Endpoints.docker.Host}} + /// Gets the current Docker endpoint. /// /// - /// See the Docker CLI implementation comments. + /// See the Docker CLI implementation comments. + /// Executes a command equivalent to docker context inspect --format {{.Endpoints.docker.Host}}. /// + /// A representing the current Docker endpoint if available; otherwise, null. [CanBeNull] public Uri GetCurrentEndpoint() { - try + const string defaultDockerContext = "default"; + + var dockerHost = GetDockerHost(); + if (dockerHost != null) + { + return dockerHost; + } + + var dockerContext = GetCurrentDockerContext(); + if (string.IsNullOrEmpty(dockerContext) || defaultDockerContext.Equals(dockerContext)) { - if (_dockerHost != null) + return TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint; + } + + using (var sha256 = SHA256.Create()) + { + var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant(); + var metaFilePath = Path.Combine(UserProfileDockerContextMetaDirectoryPath, dockerContextHash, "meta.json"); + + if (!File.Exists(metaFilePath)) { - return _dockerHost; + return null; } - var currentContext = GetCurrentContext(); - if (currentContext is null or "default") + using (var metaFileStream = File.OpenRead(metaFilePath)) { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? NpipeEndpointAuthenticationProvider.DockerEngine : UnixEndpointAuthenticationProvider.DockerEngine; + 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://./")); } - - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(currentContext)); - var digest = hash.Aggregate(new StringBuilder(hash.Length * 2), (sb, b) => sb.Append(b.ToString("x2"))).ToString(); - var contextDirectory = Path.Combine(UserProfileDockerContextMetaDirectoryPath, digest); - var endpoint = GetEndpoint(contextDirectory, currentContext); - return endpoint; - } - catch - { - return null; } } [CanBeNull] - private string GetCurrentContext() + private string GetCurrentDockerContext() { - if (!string.IsNullOrEmpty(_dockerContext)) + var dockerContext = GetDockerContext(); + if (!string.IsNullOrEmpty(dockerContext)) + { + return dockerContext; + } + + if (!Exists) { - return _dockerContext; + return null; } using (var config = Parse()) @@ -139,37 +142,23 @@ private string GetCurrentContext() { return currentContextNode.GetString(); } - - return null; + else + { + return null; + } } } [CanBeNull] - private static Uri GetEndpoint(string metaDirectory, string currentContext) + private Uri GetDockerHost() { - try - { - var metaFilePath = Path.Combine(metaDirectory, "meta.json"); - using (var metaFileStream = new FileStream(metaFilePath, FileMode.Open, FileAccess.Read)) - { - var meta = JsonSerializer.Deserialize(metaFileStream, SourceGenerationContext.Default.DockerContextMeta); - if (meta?.Name == currentContext) - { - var host = meta?.Endpoints?.Docker?.Host; - if (!string.IsNullOrEmpty(host)) - { - const string npipePrefix = "npipe:////./"; - return host.StartsWith(npipePrefix, StringComparison.Ordinal) ? new Uri($"npipe://./{host.Substring(npipePrefix.Length)}") : new Uri(host); - } - } - } - } - catch - { - return null; - } + return _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerHost()).FirstOrDefault(dockerHost => dockerHost != null); + } - return null; + [CanBeNull] + private string GetDockerContext() + { + return _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerContext()).FirstOrDefault(dockerContext => !string.IsNullOrEmpty(dockerContext)); } private static FileInfo GetDockerConfigFile() @@ -178,25 +167,44 @@ private static FileInfo GetDockerConfigFile() return new FileInfo(Path.Combine(dockerConfigDirectoryPath, "config.json")); } - internal class DockerContextMeta + internal sealed class DockerContextMeta { - [JsonPropertyName("Name"), CanBeNull] - public string Name { get; set; } + [JsonConstructor] + public DockerContextMeta(string name, DockerContextMetaEndpoints endpoints) + { + Name = name; + Endpoints = endpoints; + } + + [JsonPropertyName("Name")] + public string Name { get; } - [JsonPropertyName("Endpoints"), CanBeNull] - public DockerContextMetaEndpoints Endpoints { get; set; } + [JsonPropertyName("Endpoints")] + public DockerContextMetaEndpoints Endpoints { get; } } - internal class DockerContextMetaEndpoints + internal sealed class DockerContextMetaEndpoints { - [JsonPropertyName("docker"), CanBeNull] - public DockerContextMetaEndpointsDocker Docker { get; set; } + [JsonConstructor] + public DockerContextMetaEndpoints(DockerContextMetaEndpointsDocker docker) + { + Docker = docker; + } + + [JsonPropertyName("docker")] + public DockerContextMetaEndpointsDocker Docker { get; } } - internal class DockerContextMetaEndpointsDocker + internal sealed class DockerContextMetaEndpointsDocker { - [JsonPropertyName("Host"), CanBeNull] - public string Host { get; set; } + [JsonConstructor] + public DockerContextMetaEndpointsDocker(string host) + { + Host = host; + } + + [JsonPropertyName("Host")] + public string Host { get; } } } } diff --git a/tests/Testcontainers.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index 44296f398..f49ab2437 100644 --- a/tests/Testcontainers.Commons/DockerCli.cs +++ b/tests/Testcontainers.Commons/DockerCli.cs @@ -43,7 +43,7 @@ public enum DockerResource public static bool PlatformIsEnabled(DockerPlatform platform) { - var commandResult = new Command("version", "--format '{{.Server.Os}}'").Execute(); + var commandResult = new Command("version", "--format {{.Server.Os}}").Execute(); return 0.Equals(commandResult.ExitCode) && commandResult.Stdout.Contains(platform.ToString().ToLowerInvariant()); } @@ -53,19 +53,10 @@ public static bool ResourceExists(DockerResource resource, string id) return 0.Equals(commandResult.ExitCode); } - public static Uri GetCurrentEndpoint() + public static Uri GetCurrentEndpoint(string context = "") { - var command = new Command("context", "inspect", "--format", "{{.Endpoints.docker.Host}}"); - var commandResult = command.Execute(); - if (commandResult.ExitCode == 0) - { - // Because new Uri("npipe:////./pipe/docker_engine") returns "npipe:////pipe/docker_engine" - const string npipePrefix = "npipe:////./"; - var host = commandResult.Stdout; - return host.StartsWith(npipePrefix, StringComparison.Ordinal) ? new Uri($"npipe://./{host.Substring(npipePrefix.Length)}") : new Uri(host); - } - - throw new InvalidOperationException($"Executing `{command}` failed: {commandResult.Stderr}"); + var commandResult = new Command("context", "inspect", "--format {{.Endpoints.docker.Host}}", context).Execute(); + return 0.Equals(commandResult.ExitCode) ? new Uri(commandResult.Stdout.Replace("npipe:////./", "npipe://./")) : throw new InvalidOperationException($"Unexpected error: {commandResult.Stderr}"); } [PublicAPI] diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 2762a9f44..7e0b0147b 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -1,84 +1,95 @@ -using DotNet.Testcontainers.Configurations; - namespace DotNet.Testcontainers.Tests.Unit { using System; - using System.Reflection; + using System.IO; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Commons; + using DotNet.Testcontainers.Configurations; using Xunit; - using Xunit.Abstractions; - using Xunit.Sdk; - public sealed class DockerConfigTest + public static class DockerConfigTests { - private readonly ITestOutputHelper _testOutputHelper; - - public DockerConfigTest(ITestOutputHelper testOutputHelper) + public sealed class DockerContextConfigurationTests { - _testOutputHelper = testOutputHelper; - } + [Fact] + public void ReturnsDefaultOsEndpointWhenDockerContextIsUnset() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=" }); + var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); - [Fact] - public void GetCurrentEndpoint() - { - AssertCurrentEndpoint(); - } + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); - [Fact] - [UseEnvironmentVariable("DOCKER_CONTEXT", "default")] - public void GetCurrentEndpointWithDefaultDockerContextEnvironmentVariable() - { - AssertCurrentEndpoint(); - } + // Then + Assert.Equal(TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint, currentEndpoint); + } - [Fact] - [UseEnvironmentVariable("DOCKER_HOST", "tcp://docker:2375")] - public void GetCurrentEndpointWithDockerHostEnvironmentVariable() - { - var endpoint = AssertCurrentEndpoint(); - Assert.Equal(new Uri("tcp://docker:2375"), endpoint); - } + [Fact] + public void ReturnsDefaultOsEndpointWhenDockerContextIsDefault() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=default" }); + var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); - private Uri AssertCurrentEndpoint() - { - var expectedEndpoint = DockerCli.GetCurrentEndpoint(); - var endpoint = new DockerConfig(new EnvironmentConfiguration()).GetCurrentEndpoint(); - _testOutputHelper.WriteLine($"DockerConfig.Default.GetCurrentEndpoint() => {endpoint}"); - Assert.Equal(expectedEndpoint, endpoint); - Assert.NotNull(endpoint); - return endpoint; - } + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); - [Fact] - [UseEnvironmentVariable("DOCKER_CONTEXT", "wrong")] - public void GetCurrentEndpointWithWrongDockerContextEnvironmentVariable() - { - var endpoint = new DockerConfig(new EnvironmentConfiguration()).GetCurrentEndpoint(); - Assert.Null(endpoint); - } - } + // Then + Assert.Equal(TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint, currentEndpoint); + } - public sealed class UseEnvironmentVariableAttribute : BeforeAfterTestAttribute - { - private readonly string _variable; + [Fact] + public void ReturnsConfiguredEndpointWhenDockerContextIsNotSet() + { + var currentEndpoint = new DockerConfig().GetCurrentEndpoint(); + Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); + } - private readonly string _value; + [Fact] + public void ReturnsNullWhenDockerContextNotFound() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=missing" }); + var dockerConfig = new DockerConfig(customConfiguration); - public UseEnvironmentVariableAttribute(string variable, string value) - { - _variable = variable; - _value = value; - } + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); - public override void Before(MethodInfo methodUnderTest) - { - Environment.SetEnvironmentVariable(_variable, _value); + // Then + Assert.Null(currentEndpoint); + } } - public override void After(MethodInfo methodUnderTest) + public sealed class DockerHostConfigurationTests { - Environment.SetEnvironmentVariable(_variable, null); + [Fact] + public void ReturnsDefaultOsEndpointWhenDockerHostIsUnset() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=" }); + var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Equal(TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint, currentEndpoint); + } + + [Fact] + public void ReturnsConfiguredEndpointWhenDockerHostIsSet() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/" }); + var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); + } } } } From d98e4bc7eb109dde65cd6911dec22c611fe81c2f Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:48:49 +0200 Subject: [PATCH 14/23] fix: Do not reference TestcontainersSettings.OS --- src/Testcontainers/Builders/DockerConfig.cs | 15 ++++++++------- tests/Testcontainers.Commons/DockerCli.cs | 5 +++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index 5c1d9303d..d170cd7ef 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -3,6 +3,7 @@ using System; using System.IO; using System.Linq; + using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -97,15 +98,15 @@ public Uri GetCurrentEndpoint() return dockerHost; } - var dockerContext = GetCurrentDockerContext(); + var dockerContext = GetCurrentContext(); if (string.IsNullOrEmpty(dockerContext) || defaultDockerContext.Equals(dockerContext)) { - return TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint; + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? NpipeEndpointAuthenticationProvider.DockerEngine : UnixEndpointAuthenticationProvider.DockerEngine; } using (var sha256 = SHA256.Create()) { - var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant(); + var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.Default.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant(); var metaFilePath = Path.Combine(UserProfileDockerContextMetaDirectoryPath, dockerContextHash, "meta.json"); if (!File.Exists(metaFilePath)) @@ -123,7 +124,7 @@ public Uri GetCurrentEndpoint() } [CanBeNull] - private string GetCurrentDockerContext() + private string GetCurrentContext() { var dockerContext = GetDockerContext(); if (!string.IsNullOrEmpty(dockerContext)) @@ -136,11 +137,11 @@ private string GetCurrentDockerContext() return null; } - using (var config = Parse()) + using (var dockerConfigJsonDocument = Parse()) { - if (config.RootElement.TryGetProperty("currentContext", out var currentContextNode) && currentContextNode.ValueKind == JsonValueKind.String) + if (dockerConfigJsonDocument.RootElement.TryGetProperty("currentContext", out var currentContext) && currentContext.ValueKind == JsonValueKind.String) { - return currentContextNode.GetString(); + return currentContext.GetString(); } else { diff --git a/tests/Testcontainers.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index f49ab2437..75aae9368 100644 --- a/tests/Testcontainers.Commons/DockerCli.cs +++ b/tests/Testcontainers.Commons/DockerCli.cs @@ -55,8 +55,9 @@ public static bool ResourceExists(DockerResource resource, string id) public static Uri GetCurrentEndpoint(string context = "") { - var commandResult = new Command("context", "inspect", "--format {{.Endpoints.docker.Host}}", context).Execute(); - return 0.Equals(commandResult.ExitCode) ? new Uri(commandResult.Stdout.Replace("npipe:////./", "npipe://./")) : throw new InvalidOperationException($"Unexpected error: {commandResult.Stderr}"); + var command = new Command("context", "inspect", "--format {{.Endpoints.docker.Host}}", context); + var commandResult = command.Execute(); + return 0.Equals(commandResult.ExitCode) ? new Uri(commandResult.Stdout.Replace("npipe:////./", "npipe://./")) : throw new InvalidOperationException($"Executing '{command}' failed: {commandResult.Stderr}"); } [PublicAPI] From 0fa1b7352e665f9fb7854acdc4dd1815739836d6 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:00:49 +0200 Subject: [PATCH 15/23] chore: Add ThrowIfExecutionFailed --- tests/Testcontainers.Commons/DockerCli.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/Testcontainers.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index 75aae9368..2e9c7e7f8 100644 --- a/tests/Testcontainers.Commons/DockerCli.cs +++ b/tests/Testcontainers.Commons/DockerCli.cs @@ -55,9 +55,9 @@ public static bool ResourceExists(DockerResource resource, string id) public static Uri GetCurrentEndpoint(string context = "") { - var command = new Command("context", "inspect", "--format {{.Endpoints.docker.Host}}", context); - var commandResult = command.Execute(); - return 0.Equals(commandResult.ExitCode) ? new Uri(commandResult.Stdout.Replace("npipe:////./", "npipe://./")) : throw new InvalidOperationException($"Executing '{command}' failed: {commandResult.Stderr}"); + var commandResult = new Command("context", "inspect", "--format {{.Endpoints.docker.Host}}", context).Execute(); + commandResult.ThrowIfExecutionFailed(); + return new Uri(commandResult.Stdout.Replace("npipe:////./", "npipe://./")); } [PublicAPI] @@ -105,7 +105,7 @@ public CommandResult Execute() process.ErrorDataReceived -= AppendStderr; } - return new CommandResult(process.ExitCode, startTime, exitTime, _stdout.ToString(), _stderr.ToString()); + return new CommandResult(this, process.ExitCode, startTime, exitTime, _stdout.ToString(), _stderr.ToString()); } private void AppendStdout(object sender, DataReceivedEventArgs e) @@ -127,15 +127,17 @@ public override string ToString() [PublicAPI] private sealed class CommandResult { - public CommandResult(int exitCode, DateTime startTime, DateTime exitTime, string stdout, string stderr) + public CommandResult(Command command, int exitCode, DateTime startTime, DateTime exitTime, string stdout, string stderr) { - ExitCode = exitCode; + Command = command; StartTime = startTime; ExitTime = exitTime; Stdout = stdout; Stderr = stderr; } + public Command Command { get; } + public int ExitCode { get; } public DateTime StartTime { get; } @@ -145,5 +147,13 @@ public CommandResult(int exitCode, DateTime startTime, DateTime exitTime, string public string Stdout { get; } public string Stderr { get; } + + public void ThrowIfExecutionFailed() + { + if (!0.Equals(ExitCode)) + { + throw new InvalidOperationException($"Executing '{Command}' failed: {Stderr}"); + } + } } } \ No newline at end of file From 74462e6e7dfd6091df578f710b5c7697d4ec44f9 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 27 Aug 2024 08:47:13 +0200 Subject: [PATCH 16/23] fix: Set ExitCode property again --- tests/Testcontainers.Commons/DockerCli.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Testcontainers.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index 2e9c7e7f8..bd391532d 100644 --- a/tests/Testcontainers.Commons/DockerCli.cs +++ b/tests/Testcontainers.Commons/DockerCli.cs @@ -130,6 +130,7 @@ private sealed class CommandResult public CommandResult(Command command, int exitCode, DateTime startTime, DateTime exitTime, string stdout, string stderr) { Command = command; + ExitCode = exitCode; StartTime = startTime; ExitTime = exitTime; Stdout = stdout; From 6c70ea86848fd687bc8759e6544f407552c7f60b Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:48:02 +0200 Subject: [PATCH 17/23] docs: Add docker context docs --- docs/custom_configuration/index.md | 22 ++++++++++++++++--- .../Configurations/TestcontainersSettings.cs | 2 +- .../Unit/Builders/DockerConfigTest.cs | 6 ++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/custom_configuration/index.md b/docs/custom_configuration/index.md index 5c3f60634..975be3aee 100644 --- a/docs/custom_configuration/index.md +++ b/docs/custom_configuration/index.md @@ -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` | @@ -16,11 +17,11 @@ Testcontainers supports various configurations to set up your test environment. | `ryuk.container.privileged` | `TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED` | Runs Ryuk (resource reaper) in privileged mode. | `false` | | `ryuk.container.image` | `TESTCONTAINERS_RYUK_CONTAINER_IMAGE` | The Ryuk (resource reaper) Docker image. | `testcontainers/ryuk:0.5.1` | | `hub.image.name.prefix` | `TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX` | The name to use for substituting the Docker Hub registry part of the image name. | - | - +1) The value represent the string representation of a [TimeSpan](https://learn.microsoft.com/en-us/dotnet/api/system.timespan), for example, `00:00:01` for 1 second. ## Configure remote container runtime @@ -34,9 +35,24 @@ docker.host=tcp://docker:2375 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. + + NAME DESCRIPTION DOCKER ENDPOINT ERROR + tcc tcp://127.0.0.1:64453/0 + +```console title="Properties File" +docker.context=tcc +``` + +```console title="Environment Variable" +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, set the `TestcontainersSettings.Logger` property to an instance of an `ILogger` implementation before creating a Docker resource, such as a container. +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. [testcontainers.org 00:00:00.34] Connected to Docker: Host: tcp://127.0.0.1:60706/ diff --git a/src/Testcontainers/Configurations/TestcontainersSettings.cs b/src/Testcontainers/Configurations/TestcontainersSettings.cs index c2b5146b8..efb611a91 100644 --- a/src/Testcontainers/Configurations/TestcontainersSettings.cs +++ b/src/Testcontainers/Configurations/TestcontainersSettings.cs @@ -141,7 +141,7 @@ static TestcontainersSettings() /// [NotNull] public static IOperatingSystem OS { get; set; } - = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IOperatingSystem)new Windows(DockerEndpointAuthConfig) : new Unix(DockerEndpointAuthConfig); + = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new Windows(DockerEndpointAuthConfig) : new Unix(DockerEndpointAuthConfig); /// public static Task ExposeHostPortsAsync(ushort port, CancellationToken ct = default) diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 7e0b0147b..0098a4a59 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -12,7 +12,7 @@ public static class DockerConfigTests public sealed class DockerContextConfigurationTests { [Fact] - public void ReturnsDefaultOsEndpointWhenDockerContextIsUnset() + public void ReturnsDefaultOsEndpointWhenDockerContextIsEmpty() { // Given ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=" }); @@ -40,7 +40,7 @@ public void ReturnsDefaultOsEndpointWhenDockerContextIsDefault() } [Fact] - public void ReturnsConfiguredEndpointWhenDockerContextIsNotSet() + public void ReturnsActiveEndpointWhenDockerContextIsUnset() { var currentEndpoint = new DockerConfig().GetCurrentEndpoint(); Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); @@ -64,7 +64,7 @@ public void ReturnsNullWhenDockerContextNotFound() public sealed class DockerHostConfigurationTests { [Fact] - public void ReturnsDefaultOsEndpointWhenDockerHostIsUnset() + public void ReturnsDefaultOsEndpointWhenDockerHostIsEmpty() { // Given ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=" }); From a3e18543a52609a99552dabae37bd24b6f217035 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:20:22 +0200 Subject: [PATCH 18/23] docs: Enhance context and logger docs --- docs/api/create_docker_container.md | 21 +++++++++++++++++++++ docs/custom_configuration/index.md | 13 +++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/api/create_docker_container.md b/docs/api/create_docker_container.md index 81ea1b8cc..44a8f5dec 100644 --- a/docs/api/create_docker_container.md +++ b/docs/api/create_docker_container.md @@ -76,6 +76,27 @@ using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); await _container.StartAsync(timeoutCts.Token); ``` +## Getting log messages + +Testcontainers for .NET provides two approaches for retrieving log messages from containers: `GetLogsAsync` and `WithOutputConsumer`. Each method serves different use cases for handling container logs. + +The `GetLogsAsync` method is available through the `IContainer` interface. It allows you to fetch logs from a container for a specific time range or from the beginning until the present. This approach is useful for retrieving logs after a test has run, especially when troubleshooting issues or failures. + +```csharp title="Getting all log messages" +var (stdout, stderr) = await _container.GetLogsAsync(); +``` + +The `WithOutputConsumer` method is part of the `ContainerBuilder` class and is used to continuously forward container log messages to a specified output consumer. This approach provides real-time access to logs as the container runs. + +```csharp title="Forwarding all log messages" +using IOutputConsumer outputConsumer = Consume.RedirectStdoutAndStderrToConsole(); + +_ = new ContainerBuilder() + .WithOutputConsumer(outputConsumer); +``` + +The static class `Consume` offers pre-configured implementations of the `IOutputConsumer` interface for common use cases. If you need additional functionalities beyond those provided by the default implementations, you can create your own implementations of `IOutputConsumer`. + ## Examples An NGINX container that binds the HTTP port to a random host port and hosts static content. The example connects to the web server and checks the HTTP status code. diff --git a/docs/custom_configuration/index.md b/docs/custom_configuration/index.md index 975be3aee..ce97ec8b3 100644 --- a/docs/custom_configuration/index.md +++ b/docs/custom_configuration/index.md @@ -39,8 +39,13 @@ DOCKER_HOST=tcp://docker:2375 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. - NAME DESCRIPTION DOCKER ENDPOINT ERROR - tcc tcp://127.0.0.1:64453/0 +```console 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. ```console title="Properties File" docker.context=tcc @@ -74,6 +79,10 @@ In .NET logging usually goes through the test framework. Testcontainers is not a [testcontainers.org 00:00:06.26] Start Docker container 027af397344d08d5fc174bf5b5d449f6b352a8a506306d3d96390aaa2bb0445d [testcontainers.org 00:00:06.64] Delete Docker container 027af397344d08d5fc174bf5b5d449f6b352a8a506306d3d96390aaa2bb0445d +!!!tip + + These log messages are from the Testcontainers library and contain information about the test resources. They do not include log messages from the containers. To get the container log messages, see: [Getting log messages](https://dotnet.testcontainers.org/api/create_docker_container/#getting-log-messages). + To enable debug log messages in the default implementation, set the property `ConsoleLogger.Instance.DebugLogLevelEnabled` to `true`. This will forward messages related to building or pulling Docker images to the output stream. [properties-file-format]: https://en.wikipedia.org/wiki/.properties From e5b7aafc3f62ec90db86bedb566efafc39c7c066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Luthi?= Date: Wed, 4 Sep 2024 00:10:06 +0200 Subject: [PATCH 19/23] Make the DockerConfig class actually testable --- src/Testcontainers/Builders/DockerConfig.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index d170cd7ef..4417a6850 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -16,11 +16,8 @@ /// internal sealed class DockerConfig { - private static readonly string UserProfileDockerConfigDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker"); - - private static readonly string UserProfileDockerContextMetaDirectoryPath = Path.Combine(UserProfileDockerConfigDirectoryPath, "contexts", "meta"); - private readonly FileInfo _dockerConfigFile; + private readonly string _dockerContextMetaDirectoryPath; private readonly ICustomConfiguration[] _customConfigurations; @@ -52,6 +49,8 @@ public DockerConfig(params ICustomConfiguration[] customConfigurations) public DockerConfig(FileInfo dockerConfigFile, params ICustomConfiguration[] customConfigurations) { _dockerConfigFile = dockerConfigFile; + var dockerConfigDirectory = _dockerConfigFile.DirectoryName ?? throw new ArgumentException($"The {dockerConfigFile.Name} file must be inside a directory.", nameof(dockerConfigFile)); + _dockerContextMetaDirectoryPath = Path.Combine(dockerConfigDirectory, "contexts", "meta"); _customConfigurations = customConfigurations; } @@ -107,7 +106,7 @@ public Uri GetCurrentEndpoint() using (var sha256 = SHA256.Create()) { var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.Default.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant(); - var metaFilePath = Path.Combine(UserProfileDockerContextMetaDirectoryPath, dockerContextHash, "meta.json"); + var metaFilePath = Path.Combine(_dockerContextMetaDirectoryPath, dockerContextHash, "meta.json"); if (!File.Exists(metaFilePath)) { @@ -164,7 +163,9 @@ private string GetDockerContext() private static FileInfo GetDockerConfigFile() { - var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? PropertiesFileConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath; + var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? + PropertiesFileConfiguration.Instance.GetDockerConfig() ?? + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker"); return new FileInfo(Path.Combine(dockerConfigDirectoryPath, "config.json")); } From f5898ec99351bd2a09de3739e4ddb17915c67129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Wed, 4 Sep 2024 10:03:21 +0200 Subject: [PATCH 20/23] Actually test reading the current context from Docker config files --- .../Unit/Builders/DockerConfigTest.cs | 83 ++++++++++++++++--- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 0098a4a59..78de3c794 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -2,6 +2,9 @@ namespace DotNet.Testcontainers.Tests.Unit { using System; using System.IO; + using System.Runtime.CompilerServices; + using System.Security.Cryptography; + using System.Text; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Commons; using DotNet.Testcontainers.Configurations; @@ -12,31 +15,60 @@ public static class DockerConfigTests public sealed class DockerContextConfigurationTests { [Fact] - public void ReturnsDefaultOsEndpointWhenDockerContextIsEmpty() + public void ReturnsActiveEndpointWhenDockerContextIsEmpty() { // Given ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=" }); - var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); + var dockerConfig = new DockerConfig(customConfiguration); // When var currentEndpoint = dockerConfig.GetCurrentEndpoint(); // Then - Assert.Equal(TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint, currentEndpoint); + Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); } [Fact] - public void ReturnsDefaultOsEndpointWhenDockerContextIsDefault() + public void ReturnsDefaultEndpointWhenDockerContextIsDefault() { // Given ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=default" }); - var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); + var dockerConfig = new DockerConfig(customConfiguration); // When var currentEndpoint = dockerConfig.GetCurrentEndpoint(); // Then - Assert.Equal(TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint, currentEndpoint); + Assert.Equal(DockerCli.GetCurrentEndpoint("default"), currentEndpoint); + } + + [Fact] + public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromProperties() + { + // Given + using var context = new TempDockerContext("custom", "tcp://127.0.0.1:2375/"); + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom" }); + var dockerConfig = new DockerConfig(context.ConfigFile, customConfiguration); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); + } + + [Fact] + public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromConfigFile() + { + // Given + using var context = new TempDockerContext("custom", "tcp://127.0.0.1:2375/"); + var dockerConfig = new DockerConfig(context.ConfigFile); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); } [Fact] @@ -64,25 +96,26 @@ public void ReturnsNullWhenDockerContextNotFound() public sealed class DockerHostConfigurationTests { [Fact] - public void ReturnsDefaultOsEndpointWhenDockerHostIsEmpty() + public void ReturnsActiveEndpointWhenDockerHostIsEmpty() { // Given ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=" }); - var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); + var dockerConfig = new DockerConfig(customConfiguration); // When var currentEndpoint = dockerConfig.GetCurrentEndpoint(); // Then - Assert.Equal(TestcontainersSettings.OS.DockerEndpointAuthConfig.Endpoint, currentEndpoint); + Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); } [Fact] public void ReturnsConfiguredEndpointWhenDockerHostIsSet() { // Given + using var context = new TempDockerContext("custom", ""); ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/" }); - var dockerConfig = new DockerConfig(new FileInfo("config.json"), customConfiguration); + var dockerConfig = new DockerConfig(context.ConfigFile, customConfiguration); // When var currentEndpoint = dockerConfig.GetCurrentEndpoint(); @@ -91,5 +124,35 @@ public void ReturnsConfiguredEndpointWhenDockerHostIsSet() Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); } } + + private class TempDockerContext : IDisposable + { + private readonly DirectoryInfo _directory; + + public TempDockerContext(string contextName, string endpoint, [CallerMemberName] string caller = "") + { + _directory = new DirectoryInfo(Path.Combine(TestSession.TempDirectoryPath, caller)); + _directory.Create(); + ConfigFile = new FileInfo(Path.Combine(_directory.FullName, "config.json")); + + // lang=json + var config = $$"""{ "currentContext": "{{contextName}}" }"""; + File.WriteAllText(ConfigFile.FullName, config); + + // lang=json + var meta = $$"""{ "Name": "{{contextName}}", "Metadata": {}, "Endpoints": { "docker": { "Host": "{{endpoint}}", "SkipTLSVerify":false } } }"""; + var dockerContextHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(contextName))).ToLowerInvariant(); + var metaDirectory = Path.Combine(_directory.FullName, "contexts", "meta", dockerContextHash); + Directory.CreateDirectory(metaDirectory); + File.WriteAllText(Path.Combine(metaDirectory, "meta.json"), meta); + } + + public FileInfo ConfigFile { get; } + + public void Dispose() + { + _directory.Delete(recursive: true); + } + } } } From 266ee03d60125ba509e0a92093c9b90d0a54048e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Fri, 6 Sep 2024 13:05:13 +0200 Subject: [PATCH 21/23] Skip ReturnsActiveEndpointWhenDockerContextIsUnset if necessary --- Directory.Packages.props | 1 + tests/Testcontainers.Tests/Testcontainers.Tests.csproj | 1 + .../Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bae22338e..e5601ea20 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,5 +63,6 @@ + \ No newline at end of file diff --git a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj index caa03761c..c99bc7d72 100644 --- a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj +++ b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index 78de3c794..d9db17e8e 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -71,9 +71,14 @@ public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromConfigFile() Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); } - [Fact] + [SkippableFact] public void ReturnsActiveEndpointWhenDockerContextIsUnset() { + var properties = PropertiesFileConfiguration.Instance; + var dockerHost = properties.GetDockerHost(); + var dockerContext = properties.GetDockerContext(); + Skip.If(dockerHost != null || dockerContext != null, "The docker cli doesn't know about ~/.testcontainers.properties"); + var currentEndpoint = new DockerConfig().GetCurrentEndpoint(); Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); } From d7dc0422347a421daedf56d7ea316453c3307519 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:44:25 +0200 Subject: [PATCH 22/23] chore: Remove unnecessary config file arg --- Directory.Packages.props | 1 - src/Testcontainers/Builders/DockerConfig.cs | 50 +++++------- .../DockerRegistryAuthenticationProvider.cs | 11 --- .../Testcontainers.Tests.csproj | 1 - .../Unit/Builders/DockerConfigTest.cs | 78 +++++++++++-------- ...ockerRegistryAuthenticationProviderTest.cs | 2 +- 6 files changed, 65 insertions(+), 78 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e5601ea20..bae22338e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,6 +63,5 @@ - \ No newline at end of file diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs index 4417a6850..1e84c7fbd 100644 --- a/src/Testcontainers/Builders/DockerConfig.cs +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -16,17 +16,20 @@ /// internal sealed class DockerConfig { - private readonly FileInfo _dockerConfigFile; - private readonly string _dockerContextMetaDirectoryPath; + 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; + /// /// Initializes a new instance of the class. /// [PublicAPI] public DockerConfig() - : this(GetDockerConfigFile(), EnvironmentConfiguration.Instance, PropertiesFileConfiguration.Instance) + : this(EnvironmentConfiguration.Instance, PropertiesFileConfiguration.Instance) { } @@ -36,22 +39,10 @@ public DockerConfig() /// A list of custom configurations. [PublicAPI] public DockerConfig(params ICustomConfiguration[] customConfigurations) - : this(GetDockerConfigFile(), customConfigurations) { - } - - /// - /// Initializes a new instance of the class. - /// - /// The Docker config file. - /// A list of custom configurations. - [PublicAPI] - public DockerConfig(FileInfo dockerConfigFile, params ICustomConfiguration[] customConfigurations) - { - _dockerConfigFile = dockerConfigFile; - var dockerConfigDirectory = _dockerConfigFile.DirectoryName ?? throw new ArgumentException($"The {dockerConfigFile.Name} file must be inside a directory.", nameof(dockerConfigFile)); - _dockerContextMetaDirectoryPath = Path.Combine(dockerConfigDirectory, "contexts", "meta"); _customConfigurations = customConfigurations; + _dockerConfigDirectoryPath = GetDockerConfig(); + _dockerConfigFilePath = Path.Combine(_dockerConfigDirectoryPath, "config.json"); } /// @@ -61,10 +52,10 @@ public DockerConfig(FileInfo dockerConfigFile, params ICustomConfiguration[] cus = new DockerConfig(); /// - public bool Exists => _dockerConfigFile.Exists; + public bool Exists => File.Exists(_dockerConfigFilePath); - /// - public string FullName => _dockerConfigFile.FullName; + /// + public string FullName => _dockerConfigFilePath; /// /// Parses the Docker config file. @@ -72,7 +63,7 @@ public DockerConfig(FileInfo dockerConfigFile, params ICustomConfiguration[] cus /// A representing the Docker config. public JsonDocument Parse() { - using (var dockerConfigFileStream = _dockerConfigFile.OpenRead()) + using (var dockerConfigFileStream = File.OpenRead(_dockerConfigFilePath)) { return JsonDocument.Parse(dockerConfigFileStream); } @@ -106,7 +97,7 @@ public Uri GetCurrentEndpoint() using (var sha256 = SHA256.Create()) { var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.Default.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant(); - var metaFilePath = Path.Combine(_dockerContextMetaDirectoryPath, dockerContextHash, "meta.json"); + var metaFilePath = Path.Combine(_dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash, "meta.json"); if (!File.Exists(metaFilePath)) { @@ -149,6 +140,13 @@ private string GetCurrentContext() } } + [NotNull] + private string GetDockerConfig() + { + var dockerConfigDirectoryPath = _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerConfig()).FirstOrDefault(dockerConfig => !string.IsNullOrEmpty(dockerConfig)); + return dockerConfigDirectoryPath ?? UserProfileDockerConfigDirectoryPath; + } + [CanBeNull] private Uri GetDockerHost() { @@ -161,14 +159,6 @@ private string GetDockerContext() return _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerContext()).FirstOrDefault(dockerContext => !string.IsNullOrEmpty(dockerContext)); } - private static FileInfo GetDockerConfigFile() - { - var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? - PropertiesFileConfiguration.Instance.GetDockerConfig() ?? - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker"); - return new FileInfo(Path.Combine(dockerConfigDirectoryPath, "config.json")); - } - internal sealed class DockerContextMeta { [JsonConstructor] diff --git a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs index 2a2067c4e..f0fed9f40 100644 --- a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs @@ -30,17 +30,6 @@ public DockerRegistryAuthenticationProvider(ILogger logger) { } - /// - /// Initializes a new instance of the class. - /// - /// The Docker config file path. - /// The logger. - [PublicAPI] - public DockerRegistryAuthenticationProvider(string dockerConfigFilePath, ILogger logger) - : this(new DockerConfig(new FileInfo(dockerConfigFilePath)), logger) - { - } - /// /// Initializes a new instance of the class. /// diff --git a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj index c99bc7d72..caa03761c 100644 --- a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj +++ b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj @@ -15,7 +15,6 @@ - diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs index d9db17e8e..e7c19496b 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -43,12 +43,13 @@ public void ReturnsDefaultEndpointWhenDockerContextIsDefault() } [Fact] - public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromProperties() + public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromPropertiesFile() { // Given - using var context = new TempDockerContext("custom", "tcp://127.0.0.1:2375/"); - ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom" }); - var dockerConfig = new DockerConfig(context.ConfigFile, customConfiguration); + using var context = new ConfigMetaFile("custom", "tcp://127.0.0.1:2375/"); + + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=custom", context.GetDockerConfig() }); + var dockerConfig = new DockerConfig(customConfiguration); // When var currentEndpoint = dockerConfig.GetCurrentEndpoint(); @@ -61,8 +62,11 @@ public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromProperties() public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromConfigFile() { // Given - using var context = new TempDockerContext("custom", "tcp://127.0.0.1:2375/"); - var dockerConfig = new DockerConfig(context.ConfigFile); + using var context = new ConfigMetaFile("custom", "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() }); + var dockerConfig = new DockerConfig(customConfiguration); // When var currentEndpoint = dockerConfig.GetCurrentEndpoint(); @@ -71,14 +75,9 @@ public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromConfigFile() Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); } - [SkippableFact] + [SkipIfHostOrContextIsSet] public void ReturnsActiveEndpointWhenDockerContextIsUnset() { - var properties = PropertiesFileConfiguration.Instance; - var dockerHost = properties.GetDockerHost(); - var dockerContext = properties.GetDockerContext(); - Skip.If(dockerHost != null || dockerContext != null, "The docker cli doesn't know about ~/.testcontainers.properties"); - var currentEndpoint = new DockerConfig().GetCurrentEndpoint(); Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); } @@ -118,9 +117,10 @@ public void ReturnsActiveEndpointWhenDockerHostIsEmpty() public void ReturnsConfiguredEndpointWhenDockerHostIsSet() { // Given - using var context = new TempDockerContext("custom", ""); - ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/" }); - var dockerConfig = new DockerConfig(context.ConfigFile, customConfiguration); + using var context = new ConfigMetaFile("custom", ""); + + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=tcp://127.0.0.1:2375/", context.GetDockerConfig() }); + var dockerConfig = new DockerConfig(customConfiguration); // When var currentEndpoint = dockerConfig.GetCurrentEndpoint(); @@ -130,33 +130,43 @@ public void ReturnsConfiguredEndpointWhenDockerHostIsSet() } } - private class TempDockerContext : IDisposable + private sealed class SkipIfHostOrContextIsSet : FactAttribute + { + public SkipIfHostOrContextIsSet() + { + const string reason = "The Docker CLI doesn't know about ~/.testcontainers.properties file."; + var dockerHost = PropertiesFileConfiguration.Instance.GetDockerHost(); + var dockerContext = PropertiesFileConfiguration.Instance.GetDockerContext(); + Skip = dockerHost != null || dockerContext != null ? reason : string.Empty; + } + } + + private sealed class ConfigMetaFile : IDisposable { - private readonly DirectoryInfo _directory; + private const string ConfigFileJson = "{{\"currentContext\":\"{0}\"}}"; - public TempDockerContext(string contextName, string endpoint, [CallerMemberName] string caller = "") + private const string MetaFileJson = "{{\"Name\":\"{0}\",\"Metadata\":{{}},\"Endpoints\":{{\"docker\":{{\"Host\":\"{1}\",\"SkipTLSVerify\":false}}}}}}"; + + private readonly string _dockerConfigDirectoryPath; + + public ConfigMetaFile(string context, string endpoint, [CallerMemberName] string caller = "") { - _directory = new DirectoryInfo(Path.Combine(TestSession.TempDirectoryPath, caller)); - _directory.Create(); - ConfigFile = new FileInfo(Path.Combine(_directory.FullName, "config.json")); - - // lang=json - var config = $$"""{ "currentContext": "{{contextName}}" }"""; - File.WriteAllText(ConfigFile.FullName, config); - - // lang=json - var meta = $$"""{ "Name": "{{contextName}}", "Metadata": {}, "Endpoints": { "docker": { "Host": "{{endpoint}}", "SkipTLSVerify":false } } }"""; - var dockerContextHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(contextName))).ToLowerInvariant(); - var metaDirectory = Path.Combine(_directory.FullName, "contexts", "meta", dockerContextHash); - Directory.CreateDirectory(metaDirectory); - File.WriteAllText(Path.Combine(metaDirectory, "meta.json"), meta); + _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)); } - public FileInfo ConfigFile { get; } + public string GetDockerConfig() + { + return "docker.config=" + _dockerConfigDirectoryPath; + } public void Dispose() { - _directory.Delete(recursive: true); + Directory.Delete(_dockerConfigDirectoryPath, true); } } } diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs index 728a083b1..2a954c030 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs @@ -58,7 +58,7 @@ public void GetHostnameFromHubImageNamePrefix(string repository, string name, st [Fact] public void ShouldGetDefaultDockerRegistryAuthenticationConfiguration() { - var authenticationProvider = new DockerRegistryAuthenticationProvider("/tmp/docker.config", NullLogger.Instance); + var authenticationProvider = new DockerRegistryAuthenticationProvider(DockerConfig.Instance, NullLogger.Instance); Assert.Equal(default(DockerRegistryAuthenticationConfiguration), authenticationProvider.GetAuthConfig("index.docker.io")); } From 72938db2215b662648006a2717543aafb4d1173d Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:53:48 +0200 Subject: [PATCH 23/23] docs: Use tabbed group for Docker context config --- docs/custom_configuration/index.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/custom_configuration/index.md b/docs/custom_configuration/index.md index 24c68d546..f44be6c6d 100644 --- a/docs/custom_configuration/index.md +++ b/docs/custom_configuration/index.md @@ -41,7 +41,7 @@ To configure a remote container runtime, Testcontainers provides support for Doc 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. -```console title="List available contexts" +```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 @@ -49,13 +49,15 @@ 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. -```console title="Properties File" -docker.context=tcc -``` +=== "Environment Variable" + ``` + DOCKER_CONTEXT=tcc + ``` -```console title="Environment Variable" -DOCKER_CONTEXT=tcc -``` +=== "Properties File" + ``` + docker.context=tcc + ``` ## Enable logging