diff --git a/src/Testcontainers/Builders/AbstractBuilder`4.cs b/src/Testcontainers/Builders/AbstractBuilder`4.cs index 6af5ceb79..14b1f4c8c 100644 --- a/src/Testcontainers/Builders/AbstractBuilder`4.cs +++ b/src/Testcontainers/Builders/AbstractBuilder`4.cs @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Builders { using System; using System.Collections.Generic; + using System.Linq; using DotNet.Testcontainers.Clients; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; @@ -141,9 +142,21 @@ protected virtual void Validate() _ = Guard.Argument(DockerResourceConfiguration.Logger, nameof(IResourceConfiguration.Logger)) .NotNull(); - const string containerRuntimeNotFound = "Docker is either not running or misconfigured. Please ensure that Docker is running and that the endpoint is properly configured. You can customize your configuration using either the environment variables or the ~/.testcontainers.properties file. For more information, visit:\nhttps://dotnet.testcontainers.org/custom_configuration/"; - _ = Guard.Argument(DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IResourceConfiguration.DockerEndpointAuthConfig)) - .ThrowIf(argument => argument.Value == null, argument => new ArgumentException(containerRuntimeNotFound, argument.Name)); + if (DockerResourceConfiguration.DockerEndpointAuthConfig == null) + { + var message = TestcontainersSettings.UnavailableEndpoints.Count == 0 + ? "Docker is either not running or misconfigured. Please ensure that Docker is running and that the endpoint is properly configured." + : $"Docker is either not running or misconfigured. Please ensure that Docker is available at {string.Join(" or ", TestcontainersSettings.UnavailableEndpoints.Select(e => e.Uri))}"; + + var innerException = TestcontainersSettings.UnavailableEndpoints.Count switch + { + 0 => null, + 1 => TestcontainersSettings.UnavailableEndpoints[0].Exception, + _ => new AggregateException(TestcontainersSettings.UnavailableEndpoints.Select(e => e.Exception)), + }; + throw new DockerUnavailableException(message + "\nYou can customize your configuration using either the environment variables or the ~/.testcontainers.properties file. " + + "For more information, visit:\nhttps://dotnet.testcontainers.org/custom_configuration/", innerException); + } const string reuseNotSupported = "Reuse cannot be used in conjunction with WithCleanUp(true)."; _ = Guard.Argument(DockerResourceConfiguration, nameof(IResourceConfiguration.Reuse)) diff --git a/src/Testcontainers/Builders/DockerEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerEndpointAuthenticationProvider.cs index ad6b14af8..b9429edd6 100644 --- a/src/Testcontainers/Builders/DockerEndpointAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerEndpointAuthenticationProvider.cs @@ -3,6 +3,7 @@ namespace DotNet.Testcontainers.Builders using System; using System.Threading; using System.Threading.Tasks; + using JetBrains.Annotations; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; @@ -11,6 +12,9 @@ internal class DockerEndpointAuthenticationProvider : IDockerEndpointAuthenticat { private static readonly TaskFactory TaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); + [CanBeNull] + public (Uri, Exception)? UnavailableEndpoint; + /// public virtual bool IsApplicable() { @@ -40,8 +44,9 @@ await dockerClient.System.PingAsync() return true; } - catch (Exception) + catch (Exception e) { + UnavailableEndpoint = (dockerClientConfiguration.EndpointBaseUri, e); return false; } } diff --git a/src/Testcontainers/Configurations/TestcontainersSettings.cs b/src/Testcontainers/Configurations/TestcontainersSettings.cs index 3f7e3d57f..9a7c9179e 100644 --- a/src/Testcontainers/Configurations/TestcontainersSettings.cs +++ b/src/Testcontainers/Configurations/TestcontainersSettings.cs @@ -18,27 +18,41 @@ namespace DotNet.Testcontainers.Configurations public static class TestcontainersSettings { [CanBeNull] - private static readonly IDockerEndpointAuthenticationProvider DockerEndpointAuthProvider - = new IDockerEndpointAuthenticationProvider[] - { - new TestcontainersEndpointAuthenticationProvider(), - new MTlsEndpointAuthenticationProvider(), - new TlsEndpointAuthenticationProvider(), - new EnvironmentEndpointAuthenticationProvider(), - new NpipeEndpointAuthenticationProvider(), - new UnixEndpointAuthenticationProvider(), - new DockerDesktopEndpointAuthenticationProvider(), - new RootlessUnixEndpointAuthenticationProvider(), - } - .Where(authProvider => authProvider.IsApplicable()) - .FirstOrDefault(authProvider => authProvider.IsAvailable()); + private static readonly IDockerEndpointAuthenticationProvider DockerEndpointAuthProvider; [CanBeNull] - private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig - = DockerEndpointAuthProvider?.GetAuthConfig(); + private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig; + + internal static readonly IReadOnlyList<(Uri Uri, Exception Exception)> UnavailableEndpoints; static TestcontainersSettings() { + var providers = new IDockerEndpointAuthenticationProvider[] + { + new TestcontainersEndpointAuthenticationProvider(), + new MTlsEndpointAuthenticationProvider(), + new TlsEndpointAuthenticationProvider(), + new EnvironmentEndpointAuthenticationProvider(), + new NpipeEndpointAuthenticationProvider(), + new UnixEndpointAuthenticationProvider(), + new DockerDesktopEndpointAuthenticationProvider(), + new RootlessUnixEndpointAuthenticationProvider(), + }; + + DockerEndpointAuthProvider = providers.Where(authProvider => authProvider.IsApplicable()).FirstOrDefault(authProvider => authProvider.IsAvailable()); + DockerEndpointAuthConfig = DockerEndpointAuthProvider?.GetAuthConfig(); + UnavailableEndpoints = providers.OfType().Select(e => e.UnavailableEndpoint).Where(e => e.HasValue).Select(e => e.Value).ToList(); + if (DockerEndpointAuthProvider is ICustomConfiguration config) + { + DockerHostOverride = config.GetDockerHostOverride(); + DockerSocketOverride = config.GetDockerSocketOverride(); + } + else + { + DockerHostOverride = EnvironmentConfiguration.Instance.GetDockerHostOverride() ?? PropertiesFileConfiguration.Instance.GetDockerHostOverride(); + DockerSocketOverride = EnvironmentConfiguration.Instance.GetDockerSocketOverride() ?? PropertiesFileConfiguration.Instance.GetDockerSocketOverride(); + } + OS = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new Windows(DockerEndpointAuthConfig) : new Unix(DockerEndpointAuthConfig); } /// @@ -46,16 +60,12 @@ static TestcontainersSettings() /// [CanBeNull] public static string DockerHostOverride { get; set; } - = DockerEndpointAuthProvider is ICustomConfiguration config - ? config.GetDockerHostOverride() : EnvironmentConfiguration.Instance.GetDockerHostOverride() ?? PropertiesFileConfiguration.Instance.GetDockerHostOverride(); /// /// Gets or sets the Docker socket override value. /// [CanBeNull] public static string DockerSocketOverride { get; set; } - = DockerEndpointAuthProvider is ICustomConfiguration config - ? config.GetDockerSocketOverride() : EnvironmentConfiguration.Instance.GetDockerSocketOverride() ?? PropertiesFileConfiguration.Instance.GetDockerSocketOverride(); /// /// Gets or sets a value indicating whether the is enabled or not. @@ -141,7 +151,6 @@ static TestcontainersSettings() /// [NotNull] public static IOperatingSystem OS { get; set; } - = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new Windows(DockerEndpointAuthConfig) : new Unix(DockerEndpointAuthConfig); /// public static Task ExposeHostPortsAsync(ushort port, CancellationToken ct = default) diff --git a/src/Testcontainers/DockerUnavailableException.cs b/src/Testcontainers/DockerUnavailableException.cs new file mode 100644 index 000000000..1353c10c7 --- /dev/null +++ b/src/Testcontainers/DockerUnavailableException.cs @@ -0,0 +1,21 @@ +namespace DotNet.Testcontainers +{ + using System; + using JetBrains.Annotations; + + /// + /// The exception that is thrown when Docker is not available (because it is either not running or misconfigured). + /// + [PublicAPI] + public sealed class DockerUnavailableException : Exception + { + /// + /// Initializes a new instance of the class, using the provided message. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + internal DockerUnavailableException(string message, Exception innerException) : base(message, innerException) + { + } + } +}