Skip to content

Commit 7948fb0

Browse files
committed
Throw the new DockerUnavailableException when Docker is not available
(because it is either not running or misconfigured) The `ArgumentException` was not the appropriate exception to throw since the root of the problem is not a bad argument but the state of the system which is wrong. Also improve the error message to mention all the endpoints are applicable but not available.
1 parent 8470afd commit 7948fb0

File tree

4 files changed

+73
-25
lines changed

4 files changed

+73
-25
lines changed

src/Testcontainers/Builders/AbstractBuilder`4.cs

+16-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Builders
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.Linq;
56
using DotNet.Testcontainers.Clients;
67
using DotNet.Testcontainers.Configurations;
78
using DotNet.Testcontainers.Containers;
@@ -141,9 +142,21 @@ protected virtual void Validate()
141142
_ = Guard.Argument(DockerResourceConfiguration.Logger, nameof(IResourceConfiguration<TCreateResourceEntity>.Logger))
142143
.NotNull();
143144

144-
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/";
145-
_ = Guard.Argument(DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IResourceConfiguration<TCreateResourceEntity>.DockerEndpointAuthConfig))
146-
.ThrowIf(argument => argument.Value == null, argument => new ArgumentException(containerRuntimeNotFound, argument.Name));
145+
if (DockerResourceConfiguration.DockerEndpointAuthConfig == null)
146+
{
147+
var message = TestcontainersSettings.UnavailableEndpoints.Count == 0
148+
? "Docker is either not running or misconfigured. Please ensure that Docker is running and that the endpoint is properly configured."
149+
: $"Docker is either not running or misconfigured. Please ensure that Docker is available at {string.Join(" or ", TestcontainersSettings.UnavailableEndpoints.Select(e => e.Uri))}";
150+
151+
var innerException = TestcontainersSettings.UnavailableEndpoints.Count switch
152+
{
153+
0 => null,
154+
1 => TestcontainersSettings.UnavailableEndpoints[0].Exception,
155+
_ => new AggregateException(TestcontainersSettings.UnavailableEndpoints.Select(e => e.Exception)),
156+
};
157+
throw new DockerUnavailableException(message + "\nYou can customize your configuration using either the environment variables or the ~/.testcontainers.properties file. " +
158+
"For more information, visit:\nhttps://dotnet.testcontainers.org/custom_configuration/", innerException);
159+
}
147160

148161
const string reuseNotSupported = "Reuse cannot be used in conjunction with WithCleanUp(true).";
149162
_ = Guard.Argument(DockerResourceConfiguration, nameof(IResourceConfiguration<TCreateResourceEntity>.Reuse))

src/Testcontainers/Builders/DockerEndpointAuthenticationProvider.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace DotNet.Testcontainers.Builders
33
using System;
44
using System.Threading;
55
using System.Threading.Tasks;
6+
using JetBrains.Annotations;
67
using DotNet.Testcontainers.Configurations;
78
using DotNet.Testcontainers.Containers;
89

@@ -11,6 +12,9 @@ internal class DockerEndpointAuthenticationProvider : IDockerEndpointAuthenticat
1112
{
1213
private static readonly TaskFactory TaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
1314

15+
[CanBeNull]
16+
public (Uri, Exception)? UnavailableEndpoint;
17+
1418
/// <inheritdoc />
1519
public virtual bool IsApplicable()
1620
{
@@ -40,8 +44,9 @@ await dockerClient.System.PingAsync()
4044

4145
return true;
4246
}
43-
catch (Exception)
47+
catch (Exception e)
4448
{
49+
UnavailableEndpoint = (dockerClientConfiguration.EndpointBaseUri, e);
4550
return false;
4651
}
4752
}

src/Testcontainers/Configurations/TestcontainersSettings.cs

+30-21
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,54 @@ namespace DotNet.Testcontainers.Configurations
1818
public static class TestcontainersSettings
1919
{
2020
[CanBeNull]
21-
private static readonly IDockerEndpointAuthenticationProvider DockerEndpointAuthProvider
22-
= new IDockerEndpointAuthenticationProvider[]
23-
{
24-
new TestcontainersEndpointAuthenticationProvider(),
25-
new MTlsEndpointAuthenticationProvider(),
26-
new TlsEndpointAuthenticationProvider(),
27-
new EnvironmentEndpointAuthenticationProvider(),
28-
new NpipeEndpointAuthenticationProvider(),
29-
new UnixEndpointAuthenticationProvider(),
30-
new DockerDesktopEndpointAuthenticationProvider(),
31-
new RootlessUnixEndpointAuthenticationProvider(),
32-
}
33-
.Where(authProvider => authProvider.IsApplicable())
34-
.FirstOrDefault(authProvider => authProvider.IsAvailable());
21+
private static readonly IDockerEndpointAuthenticationProvider DockerEndpointAuthProvider;
3522

3623
[CanBeNull]
37-
private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig
38-
= DockerEndpointAuthProvider?.GetAuthConfig();
24+
private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig;
25+
26+
internal static readonly IReadOnlyList<(Uri Uri, Exception Exception)> UnavailableEndpoints;
3927

4028
static TestcontainersSettings()
4129
{
30+
var providers = new IDockerEndpointAuthenticationProvider[]
31+
{
32+
new TestcontainersEndpointAuthenticationProvider(),
33+
new MTlsEndpointAuthenticationProvider(),
34+
new TlsEndpointAuthenticationProvider(),
35+
new EnvironmentEndpointAuthenticationProvider(),
36+
new NpipeEndpointAuthenticationProvider(),
37+
new UnixEndpointAuthenticationProvider(),
38+
new DockerDesktopEndpointAuthenticationProvider(),
39+
new RootlessUnixEndpointAuthenticationProvider(),
40+
};
41+
42+
DockerEndpointAuthProvider = providers.Where(authProvider => authProvider.IsApplicable()).FirstOrDefault(authProvider => authProvider.IsAvailable());
43+
DockerEndpointAuthConfig = DockerEndpointAuthProvider?.GetAuthConfig();
44+
UnavailableEndpoints = providers.OfType<DockerEndpointAuthenticationProvider>().Select(e => e.UnavailableEndpoint).Where(e => e.HasValue).Select(e => e.Value).ToList();
45+
if (DockerEndpointAuthProvider is ICustomConfiguration config)
46+
{
47+
DockerHostOverride = config.GetDockerHostOverride();
48+
DockerSocketOverride = config.GetDockerSocketOverride();
49+
}
50+
else
51+
{
52+
DockerHostOverride = EnvironmentConfiguration.Instance.GetDockerHostOverride() ?? PropertiesFileConfiguration.Instance.GetDockerHostOverride();
53+
DockerSocketOverride = EnvironmentConfiguration.Instance.GetDockerSocketOverride() ?? PropertiesFileConfiguration.Instance.GetDockerSocketOverride();
54+
}
55+
OS = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new Windows(DockerEndpointAuthConfig) : new Unix(DockerEndpointAuthConfig);
4256
}
4357

4458
/// <summary>
4559
/// Gets or sets the Docker host override value.
4660
/// </summary>
4761
[CanBeNull]
4862
public static string DockerHostOverride { get; set; }
49-
= DockerEndpointAuthProvider is ICustomConfiguration config
50-
? config.GetDockerHostOverride() : EnvironmentConfiguration.Instance.GetDockerHostOverride() ?? PropertiesFileConfiguration.Instance.GetDockerHostOverride();
5163

5264
/// <summary>
5365
/// Gets or sets the Docker socket override value.
5466
/// </summary>
5567
[CanBeNull]
5668
public static string DockerSocketOverride { get; set; }
57-
= DockerEndpointAuthProvider is ICustomConfiguration config
58-
? config.GetDockerSocketOverride() : EnvironmentConfiguration.Instance.GetDockerSocketOverride() ?? PropertiesFileConfiguration.Instance.GetDockerSocketOverride();
5969

6070
/// <summary>
6171
/// Gets or sets a value indicating whether the <see cref="ResourceReaper" /> is enabled or not.
@@ -141,7 +151,6 @@ static TestcontainersSettings()
141151
/// </summary>
142152
[NotNull]
143153
public static IOperatingSystem OS { get; set; }
144-
= RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new Windows(DockerEndpointAuthConfig) : new Unix(DockerEndpointAuthConfig);
145154

146155
/// <inheritdoc cref="PortForwardingContainer.ExposeHostPortsAsync" />
147156
public static Task ExposeHostPortsAsync(ushort port, CancellationToken ct = default)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace DotNet.Testcontainers
2+
{
3+
using System;
4+
using JetBrains.Annotations;
5+
6+
/// <summary>
7+
/// The exception that is thrown when Docker is not available (because it is either not running or misconfigured).
8+
/// </summary>
9+
[PublicAPI]
10+
public sealed class DockerUnavailableException : Exception
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="DockerUnavailableException"/> class, using the provided message.
14+
/// </summary>
15+
/// <param name="message">The error message that explains the reason for the exception.</param>
16+
/// <param name="innerException">The exception that is the cause of the current exception.</param>
17+
internal DockerUnavailableException(string message, Exception innerException) : base(message, innerException)
18+
{
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)