diff --git a/docs/custom_configuration/index.md b/docs/custom_configuration/index.md index 6598d0ae1..f44be6c6d 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` | @@ -36,6 +37,28 @@ To configure a remote container runtime, Testcontainers provides support for Doc docker.host=tcp://docker:2375 ``` +## Use a different context + +You can switch between contexts using the properties file or an environment variable. Once the context is set, Testcontainers will connect to the specified endpoint based on the given value. + +```title="List available contexts" +PS C:\Sources\dotnet\testcontainers-dotnet> docker context ls +NAME DESCRIPTION DOCKER ENDPOINT ERROR +tcc tcp://127.0.0.1:60706/0 +``` + +Setting the context to `tcc` in this example will use the Docker host running at `127.0.0.1:60706` to create and run the test resources. + +=== "Environment Variable" + ``` + DOCKER_CONTEXT=tcc + ``` + +=== "Properties File" + ``` + docker.context=tcc + ``` + ## Enable logging In .NET logging usually goes through the test framework. Testcontainers is not aware of the project's test framework and may not forward log messages to the appropriate output stream. The default implementation forwards log messages to the `Console` (respectively `stdout` and `stderr`). The output should at least pop up in the IDE running tests in the `Debug` configuration. To override the default implementation, use the builder's `WithLogger(ILogger)` method and provide an `ILogger` instance to replace the default console logger. diff --git a/src/Testcontainers/Builders/DockerConfig.cs b/src/Testcontainers/Builders/DockerConfig.cs new file mode 100644 index 000000000..1e84c7fbd --- /dev/null +++ b/src/Testcontainers/Builders/DockerConfig.cs @@ -0,0 +1,202 @@ +namespace DotNet.Testcontainers.Builders +{ + using System; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Security.Cryptography; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + + /// + /// Represents a Docker config. + /// + internal sealed class DockerConfig + { + private static readonly string UserProfileDockerConfigDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker"); + + private readonly ICustomConfiguration[] _customConfigurations; + + private readonly string _dockerConfigDirectoryPath; + + private readonly string _dockerConfigFilePath; + + /// + /// Initializes a new instance of the class. + /// + [PublicAPI] + public DockerConfig() + : this(EnvironmentConfiguration.Instance, PropertiesFileConfiguration.Instance) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A list of custom configurations. + [PublicAPI] + public DockerConfig(params ICustomConfiguration[] customConfigurations) + { + _customConfigurations = customConfigurations; + _dockerConfigDirectoryPath = GetDockerConfig(); + _dockerConfigFilePath = Path.Combine(_dockerConfigDirectoryPath, "config.json"); + } + + /// + /// Gets the instance. + /// + public static DockerConfig Instance { get; } + = new DockerConfig(); + + /// + public bool Exists => File.Exists(_dockerConfigFilePath); + + /// + public string FullName => _dockerConfigFilePath; + + /// + /// Parses the Docker config file. + /// + /// A representing the Docker config. + public JsonDocument Parse() + { + using (var dockerConfigFileStream = File.OpenRead(_dockerConfigFilePath)) + { + return JsonDocument.Parse(dockerConfigFileStream); + } + } + + /// + /// Gets the current Docker endpoint. + /// + /// + /// 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() + { + const string defaultDockerContext = "default"; + + var dockerHost = GetDockerHost(); + if (dockerHost != null) + { + return dockerHost; + } + + var dockerContext = GetCurrentContext(); + if (string.IsNullOrEmpty(dockerContext) || defaultDockerContext.Equals(dockerContext)) + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? NpipeEndpointAuthenticationProvider.DockerEngine : UnixEndpointAuthenticationProvider.DockerEngine; + } + + using (var sha256 = SHA256.Create()) + { + var dockerContextHash = BitConverter.ToString(sha256.ComputeHash(Encoding.Default.GetBytes(dockerContext))).Replace("-", string.Empty).ToLowerInvariant(); + var metaFilePath = Path.Combine(_dockerConfigDirectoryPath, "contexts", "meta", dockerContextHash, "meta.json"); + + if (!File.Exists(metaFilePath)) + { + return null; + } + + using (var metaFileStream = File.OpenRead(metaFilePath)) + { + var meta = JsonSerializer.Deserialize(metaFileStream, SourceGenerationContext.Default.DockerContextMeta); + var host = meta?.Name == dockerContext ? meta.Endpoints?.Docker?.Host : null; + return string.IsNullOrEmpty(host) ? null : new Uri(host.Replace("npipe:////./", "npipe://./")); + } + } + } + + [CanBeNull] + private string GetCurrentContext() + { + var dockerContext = GetDockerContext(); + if (!string.IsNullOrEmpty(dockerContext)) + { + return dockerContext; + } + + if (!Exists) + { + return null; + } + + using (var dockerConfigJsonDocument = Parse()) + { + if (dockerConfigJsonDocument.RootElement.TryGetProperty("currentContext", out var currentContext) && currentContext.ValueKind == JsonValueKind.String) + { + return currentContext.GetString(); + } + else + { + return null; + } + } + } + + [NotNull] + private string GetDockerConfig() + { + var dockerConfigDirectoryPath = _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerConfig()).FirstOrDefault(dockerConfig => !string.IsNullOrEmpty(dockerConfig)); + return dockerConfigDirectoryPath ?? UserProfileDockerConfigDirectoryPath; + } + + [CanBeNull] + private Uri GetDockerHost() + { + return _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerHost()).FirstOrDefault(dockerHost => dockerHost != null); + } + + [CanBeNull] + private string GetDockerContext() + { + return _customConfigurations.Select(customConfiguration => customConfiguration.GetDockerContext()).FirstOrDefault(dockerContext => !string.IsNullOrEmpty(dockerContext)); + } + + internal sealed class DockerContextMeta + { + [JsonConstructor] + public DockerContextMeta(string name, DockerContextMetaEndpoints endpoints) + { + Name = name; + Endpoints = endpoints; + } + + [JsonPropertyName("Name")] + public string Name { get; } + + [JsonPropertyName("Endpoints")] + public DockerContextMetaEndpoints Endpoints { get; } + } + + internal sealed class DockerContextMetaEndpoints + { + [JsonConstructor] + public DockerContextMetaEndpoints(DockerContextMetaEndpointsDocker docker) + { + Docker = docker; + } + + [JsonPropertyName("docker")] + public DockerContextMetaEndpointsDocker Docker { get; } + } + + internal sealed class DockerContextMetaEndpointsDocker + { + [JsonConstructor] + public DockerContextMetaEndpointsDocker(string host) + { + Host = host; + } + + [JsonPropertyName("Host")] + public string Host { get; } + } + } +} diff --git a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs index 5ec60e648..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(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 600230a6b..f0fed9f40 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,30 +26,19 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth /// The logger. [PublicAPI] public DockerRegistryAuthenticationProvider(ILogger logger) - : this(GetDefaultDockerConfigFilePath(), logger) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The Docker config file path. - /// The logger. - [PublicAPI] - public DockerRegistryAuthenticationProvider(string dockerConfigFilePath, ILogger logger) - : this(new FileInfo(dockerConfigFilePath), logger) + : this(DockerConfig.Instance, 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 +55,6 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) return lazyAuthConfig.Value; } - private static string GetDefaultDockerConfigFilePath() - { - var dockerConfigDirectoryPath = EnvironmentConfiguration.Instance.GetDockerConfig() ?? PropertiesFileConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath; - return Path.Combine(dockerConfigDirectoryPath, "config.json"); - } - private static JsonDocument GetDefaultDockerAuthConfig() { return EnvironmentConfiguration.Instance.GetDockerAuthConfig() ?? PropertiesFileConfiguration.Instance.GetDockerAuthConfig() ?? JsonDocument.Parse("{}"); @@ -85,28 +66,25 @@ private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string { IDockerRegistryAuthenticationConfiguration authConfig; - if (_dockerConfigFilePath.Exists) + if (_dockerConfig.Exists) { - using (var dockerConfigFileStream = new FileStream(_dockerConfigFilePath.FullName, FileMode.Open, FileAccess.Read)) + using (var dockerConfigJsonDocument = _dockerConfig.Parse()) { - using (var dockerConfigJsonDocument = JsonDocument.Parse(dockerConfigFileStream)) - { - authConfig = new IDockerRegistryAuthenticationProvider[] - { - new CredsHelperProvider(dockerConfigJsonDocument, _logger), - new CredsStoreProvider(dockerConfigJsonDocument, _logger), - new Base64Provider(dockerConfigJsonDocument, _logger), - new Base64Provider(dockerAuthConfigJsonDocument, _logger), - } - .AsParallel() - .Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname)) - .FirstOrDefault(authenticationProvider => authenticationProvider != null); - } + authConfig = new IDockerRegistryAuthenticationProvider[] + { + new CredsHelperProvider(dockerConfigJsonDocument, _logger), + new CredsStoreProvider(dockerConfigJsonDocument, _logger), + new Base64Provider(dockerConfigJsonDocument, _logger), + new Base64Provider(dockerAuthConfigJsonDocument, _logger), + } + .AsParallel() + .Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname)) + .FirstOrDefault(authenticationProvider => authenticationProvider != null); } } else { - _logger.DockerConfigFileNotFound(_dockerConfigFilePath.FullName); + _logger.DockerConfigFileNotFound(_dockerConfig.FullName); IDockerRegistryAuthenticationProvider authConfigProvider = new Base64Provider(dockerAuthConfigJsonDocument, _logger); authConfig = authConfigProvider.GetAuthConfig(hostname); } 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/src/Testcontainers/Builders/SourceGenerationContext.cs b/src/Testcontainers/Builders/SourceGenerationContext.cs new file mode 100644 index 000000000..f7993d8ef --- /dev/null +++ b/src/Testcontainers/Builders/SourceGenerationContext.cs @@ -0,0 +1,7 @@ +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.Commons/DockerCli.cs b/tests/Testcontainers.Commons/DockerCli.cs index eb660a6d9..bd391532d 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,6 +53,13 @@ public static bool ResourceExists(DockerResource resource, string id) return 0.Equals(commandResult.ExitCode); } + public static Uri GetCurrentEndpoint(string context = "") + { + var commandResult = new Command("context", "inspect", "--format {{.Endpoints.docker.Host}}", context).Execute(); + commandResult.ThrowIfExecutionFailed(); + return new Uri(commandResult.Stdout.Replace("npipe:////./", "npipe://./")); + } + [PublicAPI] private sealed class Command { @@ -98,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) @@ -110,13 +117,19 @@ private void AppendStderr(object sender, DataReceivedEventArgs e) { _stderr.Append(e.Data); } + + public override string ToString() + { + return $"{_processStartInfo.FileName} {_processStartInfo.Arguments}"; + } } [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) { + Command = command; ExitCode = exitCode; StartTime = startTime; ExitTime = exitTime; @@ -124,6 +137,8 @@ public CommandResult(int exitCode, DateTime startTime, DateTime exitTime, string Stderr = stderr; } + public Command Command { get; } + public int ExitCode { get; } public DateTime StartTime { get; } @@ -133,5 +148,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 diff --git a/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs new file mode 100644 index 000000000..e7c19496b --- /dev/null +++ b/tests/Testcontainers.Tests/Unit/Builders/DockerConfigTest.cs @@ -0,0 +1,173 @@ +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; + using Xunit; + + public static class DockerConfigTests + { + public sealed class DockerContextConfigurationTests + { + [Fact] + public void ReturnsActiveEndpointWhenDockerContextIsEmpty() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=" }); + var dockerConfig = new DockerConfig(customConfiguration); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); + } + + [Fact] + public void ReturnsDefaultEndpointWhenDockerContextIsDefault() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=default" }); + var dockerConfig = new DockerConfig(customConfiguration); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Equal(DockerCli.GetCurrentEndpoint("default"), currentEndpoint); + } + + [Fact] + public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromPropertiesFile() + { + // Given + 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(); + + // Then + Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); + } + + [Fact] + public void ReturnsConfiguredEndpointWhenDockerContextIsCustomFromConfigFile() + { + // Given + 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(); + + // Then + Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); + } + + [SkipIfHostOrContextIsSet] + public void ReturnsActiveEndpointWhenDockerContextIsUnset() + { + var currentEndpoint = new DockerConfig().GetCurrentEndpoint(); + Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); + } + + [Fact] + public void ReturnsNullWhenDockerContextNotFound() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.context=missing" }); + var dockerConfig = new DockerConfig(customConfiguration); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Null(currentEndpoint); + } + } + + public sealed class DockerHostConfigurationTests + { + [Fact] + public void ReturnsActiveEndpointWhenDockerHostIsEmpty() + { + // Given + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=" }); + var dockerConfig = new DockerConfig(customConfiguration); + + // When + var currentEndpoint = dockerConfig.GetCurrentEndpoint(); + + // Then + Assert.Equal(DockerCli.GetCurrentEndpoint(), currentEndpoint); + } + + [Fact] + public void ReturnsConfiguredEndpointWhenDockerHostIsSet() + { + // Given + 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(); + + // Then + Assert.Equal(new Uri("tcp://127.0.0.1:2375/"), currentEndpoint); + } + } + + 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 const string ConfigFileJson = "{{\"currentContext\":\"{0}\"}}"; + + 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 = "") + { + _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 string GetDockerConfig() + { + return "docker.config=" + _dockerConfigDirectoryPath; + } + + public void Dispose() + { + Directory.Delete(_dockerConfigDirectoryPath, true); + } + } + } +} 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)] 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")); }