From f40eebf928c2ad101ef797eb3c42dc1659ee38b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Wed, 4 Sep 2024 21:21:03 +0200 Subject: [PATCH 1/3] Fix potential NullReferenceException While experimenting with Podman I tried to run tests with the following settings: ```sh cd testcontainers-dotnet/tests/Testcontainers.PostgreSql.Tests export DOCKER_HOST=unix://${HOME}/.local/share/containers/podman/machine/podman.sock export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/run/user/501/podman/podman.sock dotnet test --filter ConnectionStateReturnsOpen ``` This lead to a `NullReferenceException` ``` [xUnit.net 00:00:01.52] Testcontainers.PostgreSql.PostgreSqlContainerTest.ConnectionStateReturnsOpen [FAIL] Failed Testcontainers.PostgreSql.PostgreSqlContainerTest.ConnectionStateReturnsOpen [1 ms] Error Message: System.NullReferenceException : Object reference not set to an instance of an object. Stack Trace: at DotNet.Testcontainers.Containers.DockerContainer.get_State() in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 177 at DotNet.Testcontainers.Configurations.UntilContainerIsRunning.UntilAsync(IContainer container) in ~/testcontainers-dotnet/src/Testcontainers/Configurations/WaitStrategies/UntilContainerIsRunning.cs:line 12 at DotNet.Testcontainers.Configurations.WaitStrategy.UntilAsync(IContainer container, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs:line 102 at DotNet.Testcontainers.Containers.DockerContainer.CheckReadinessAsync(WaitStrategy waitStrategy, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 534 at DotNet.Testcontainers.Configurations.WaitStrategy.<>c__DisplayClass24_0.<g__UntilAsync|0>d.MoveNext() in ~/testcontainers-dotnet/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs:line 184 --- End of stack trace from previous location --- at DotNet.Testcontainers.Configurations.WaitStrategy.WaitUntilAsync(Func`1 wait, TimeSpan interval, TimeSpan timeout, Int32 retries, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs:line 213 at DotNet.Testcontainers.Containers.DockerContainer.CheckReadinessAsync(IEnumerable`1 waitStrategies, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 552 at DotNet.Testcontainers.Containers.DockerContainer.UnsafeStartAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 479 at DotNet.Testcontainers.Containers.DockerContainer.StartAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 282 at DotNet.Testcontainers.Containers.ResourceReaper.GetAndStartNewAsync(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, IImage resourceReaperImage, IMount dockerSocket, ILogger logger, Boolean requiresPrivilegedMode, TimeSpan initTimeout, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/ResourceReaper.cs:line 219 at DotNet.Testcontainers.Containers.ResourceReaper.GetAndStartNewAsync(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, IImage resourceReaperImage, IMount dockerSocket, ILogger logger, Boolean requiresPrivilegedMode, TimeSpan initTimeout, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/ResourceReaper.cs:line 243 at DotNet.Testcontainers.Containers.ResourceReaper.GetAndStartDefaultAsync(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger, Boolean isWindowsEngineEnabled, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/ResourceReaper.cs:line 135 at DotNet.Testcontainers.Clients.TestcontainersClient.RunAsync(IContainerConfiguration configuration, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Clients/TestcontainersClient.cs:line 294 at DotNet.Testcontainers.Containers.DockerContainer.UnsafeCreateAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 415 at DotNet.Testcontainers.Containers.DockerContainer.StartAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 279 ``` The problem with this configuration is that the reaper container doesn't start but this is undiagnosable because of the swallowed exception. So instead of swallowing the exception in `ByIdAsync` the `DockerApiException` is now caught inside `ExistsWithIdAsync` instead. Although not obvious, the exception after this commit is better and can put you on the right track to understand that the issue is about the reaper container not starting properly. ``` [xUnit.net 00:00:01.39] Testcontainers.PostgreSql.PostgreSqlContainerTest.ConnectionStateReturnsOpen [FAIL] Failed Testcontainers.PostgreSql.PostgreSqlContainerTest.ConnectionStateReturnsOpen [1 ms] Error Message: Docker.DotNet.DockerContainerNotFoundException : Docker API responded with status code=NotFound, response={"cause":"no such container","message":"no container with name or ID \"91ce0224526fddb896f2163e764e03891fce19120059fdbd04c2112cbab076a6\" found: no such container","response":404} Stack Trace: at Docker.DotNet.ContainerOperations.<>c.<.cctor>b__30_0(HttpStatusCode statusCode, String responseBody) at Docker.DotNet.DockerClient.HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpResponseMessage response, IEnumerable`1 handlers) at Docker.DotNet.DockerClient.MakeRequestAsync(IEnumerable`1 errorHandlers, HttpMethod method, String path, IQueryString queryString, IRequestContent body, IDictionary`2 headers, TimeSpan timeout, CancellationToken token) at Docker.DotNet.ContainerOperations.InspectContainerAsync(String id, CancellationToken cancellationToken) at DotNet.Testcontainers.Clients.DockerContainerOperations.ByIdAsync(String id, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Clients/DockerContainerOperations.cs:line 36 at DotNet.Testcontainers.Containers.DockerContainer.CheckReadinessAsync(WaitStrategy waitStrategy, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 531 at DotNet.Testcontainers.Configurations.WaitStrategy.<>c__DisplayClass24_0.<g__UntilAsync|0>d.MoveNext() in ~/testcontainers-dotnet/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs:line 184 --- End of stack trace from previous location --- at DotNet.Testcontainers.Configurations.WaitStrategy.WaitUntilAsync(Func`1 wait, TimeSpan interval, TimeSpan timeout, Int32 retries, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs:line 213 at DotNet.Testcontainers.Containers.DockerContainer.CheckReadinessAsync(IEnumerable`1 waitStrategies, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 552 at DotNet.Testcontainers.Containers.DockerContainer.UnsafeStartAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 479 at DotNet.Testcontainers.Containers.DockerContainer.StartAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 282 at DotNet.Testcontainers.Containers.ResourceReaper.GetAndStartNewAsync(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, IImage resourceReaperImage, IMount dockerSocket, ILogger logger, Boolean requiresPrivilegedMode, TimeSpan initTimeout, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/ResourceReaper.cs:line 219 at DotNet.Testcontainers.Containers.ResourceReaper.GetAndStartNewAsync(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, IImage resourceReaperImage, IMount dockerSocket, ILogger logger, Boolean requiresPrivilegedMode, TimeSpan initTimeout, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/ResourceReaper.cs:line 243 at DotNet.Testcontainers.Containers.ResourceReaper.GetAndStartDefaultAsync(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger, Boolean isWindowsEngineEnabled, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/ResourceReaper.cs:line 135 at DotNet.Testcontainers.Clients.TestcontainersClient.RunAsync(IContainerConfiguration configuration, CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Clients/TestcontainersClient.cs:line 294 at DotNet.Testcontainers.Containers.DockerContainer.UnsafeCreateAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 415 at DotNet.Testcontainers.Containers.DockerContainer.StartAsync(CancellationToken ct) in ~/testcontainers-dotnet/src/Testcontainers/Containers/DockerContainer.cs:line 27 ``` By the way, the solution is `export TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true`, thank you [Stack Overflow](https://stackoverflow.com/questions/71549856/testcontainers-with-podman-in-java-tests/75110548#75110548)! --- .../Clients/DockerContainerOperations.cs | 21 +++++++++---------- .../Containers/DockerContainer.cs | 12 +++++++++-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Testcontainers/Clients/DockerContainerOperations.cs b/src/Testcontainers/Clients/DockerContainerOperations.cs index 73cf62a31..a08a08593 100644 --- a/src/Testcontainers/Clients/DockerContainerOperations.cs +++ b/src/Testcontainers/Clients/DockerContainerOperations.cs @@ -32,26 +32,25 @@ public async Task> GetAllAsync(FilterByProper } public async Task ByIdAsync(string id, CancellationToken ct = default) + { + return await DockerClient.Containers.InspectContainerAsync(id, ct) + .ConfigureAwait(false); + } + + public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) { try { - return await DockerClient.Containers.InspectContainerAsync(id, ct) + await ByIdAsync(id, ct) .ConfigureAwait(false); + return true; } - catch (DockerApiException) + catch (DockerContainerNotFoundException) { - return null; + return false; } } - public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) - { - var response = await ByIdAsync(id, ct) - .ConfigureAwait(false); - - return response != null; - } - public async Task GetExitCodeAsync(string id, CancellationToken ct = default) { var response = await DockerClient.Containers.WaitContainerAsync(id, ct) diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs index 8cd148758..b0e1143f0 100644 --- a/src/Testcontainers/Containers/DockerContainer.cs +++ b/src/Testcontainers/Containers/DockerContainer.cs @@ -7,6 +7,7 @@ namespace DotNet.Testcontainers.Containers using System.Linq; using System.Threading; using System.Threading.Tasks; + using Docker.DotNet; using Docker.DotNet.Models; using DotNet.Testcontainers.Clients; using DotNet.Testcontainers.Configurations; @@ -507,8 +508,15 @@ protected virtual async Task UnsafeStopAsync(CancellationToken ct = default) await _client.StopAsync(_container.ID, ct) .ConfigureAwait(false); - _container = await _client.Container.ByIdAsync(_container.ID, ct) - .ConfigureAwait(false); + try + { + _container = await _client.Container.ByIdAsync(_container.ID, ct) + .ConfigureAwait(false); + } + catch (DockerApiException) + { + _container = null; + } StoppedTime = DateTime.UtcNow; Stopped?.Invoke(this, EventArgs.Empty); From 13d92027beac573adee94d60717998ac8ce61019 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:30:51 +0200 Subject: [PATCH 2/3] chore: Apply PR suggestion to other resource operations --- .../Clients/DockerContainerOperations.cs | 3 ++- .../Clients/DockerImageOperations.cs | 22 +++++++-------- .../Clients/DockerNetworkOperations.cs | 22 +++++++-------- .../Clients/DockerVolumeOperations.cs | 20 +++++++------- .../Clients/TestcontainersClient.cs | 27 ++++++++++++++++--- .../Containers/DockerContainer.cs | 4 +-- 6 files changed, 59 insertions(+), 39 deletions(-) diff --git a/src/Testcontainers/Clients/DockerContainerOperations.cs b/src/Testcontainers/Clients/DockerContainerOperations.cs index a08a08593..f695d3b23 100644 --- a/src/Testcontainers/Clients/DockerContainerOperations.cs +++ b/src/Testcontainers/Clients/DockerContainerOperations.cs @@ -41,8 +41,9 @@ public async Task ExistsWithIdAsync(string id, CancellationToken ct = defa { try { - await ByIdAsync(id, ct) + _ = await ByIdAsync(id, ct) .ConfigureAwait(false); + return true; } catch (DockerContainerNotFoundException) diff --git a/src/Testcontainers/Clients/DockerImageOperations.cs b/src/Testcontainers/Clients/DockerImageOperations.cs index 5c6c2196b..041cd63e2 100644 --- a/src/Testcontainers/Clients/DockerImageOperations.cs +++ b/src/Testcontainers/Clients/DockerImageOperations.cs @@ -35,26 +35,26 @@ public async Task> GetAllAsync(FilterByProperty } public async Task ByIdAsync(string id, CancellationToken ct = default) + { + return await DockerClient.Images.InspectImageAsync(id, ct) + .ConfigureAwait(false); + } + + public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) { try { - return await DockerClient.Images.InspectImageAsync(id, ct) + _ = await ByIdAsync(id, ct) .ConfigureAwait(false); + + return true; } - catch (DockerApiException) + catch (DockerImageNotFoundException) { - return null; + return false; } } - public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) - { - var response = await ByIdAsync(id, ct) - .ConfigureAwait(false); - - return response != null; - } - public async Task CreateAsync(IImage image, IDockerRegistryAuthenticationConfiguration dockerRegistryAuthConfig, CancellationToken ct = default) { var createParameters = new ImagesCreateParameters diff --git a/src/Testcontainers/Clients/DockerNetworkOperations.cs b/src/Testcontainers/Clients/DockerNetworkOperations.cs index d4a15af04..982ad2822 100644 --- a/src/Testcontainers/Clients/DockerNetworkOperations.cs +++ b/src/Testcontainers/Clients/DockerNetworkOperations.cs @@ -30,26 +30,26 @@ public async Task> GetAllAsync(FilterByProperty fil } public async Task ByIdAsync(string id, CancellationToken ct = default) + { + return await DockerClient.Networks.InspectNetworkAsync(id, ct) + .ConfigureAwait(false); + } + + public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) { try { - return await DockerClient.Networks.InspectNetworkAsync(id, ct) + _ = await ByIdAsync(id, ct) .ConfigureAwait(false); + + return true; } - catch (DockerApiException) + catch (DockerNetworkNotFoundException) { - return null; + return false; } } - public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) - { - var response = await ByIdAsync(id, ct) - .ConfigureAwait(false); - - return response != null; - } - public async Task CreateAsync(INetworkConfiguration configuration, CancellationToken ct = default) { var createParameters = new NetworksCreateParameters diff --git a/src/Testcontainers/Clients/DockerVolumeOperations.cs b/src/Testcontainers/Clients/DockerVolumeOperations.cs index aebd74d71..c04fd42a4 100644 --- a/src/Testcontainers/Clients/DockerVolumeOperations.cs +++ b/src/Testcontainers/Clients/DockerVolumeOperations.cs @@ -34,26 +34,26 @@ public async Task> GetAllAsync(FilterByProperty filt } public async Task ByIdAsync(string id, CancellationToken ct = default) + { + return await DockerClient.Volumes.InspectAsync(id, ct) + .ConfigureAwait(false); + } + + public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) { try { - return await DockerClient.Volumes.InspectAsync(id, ct) + _ = await ByIdAsync(id, ct) .ConfigureAwait(false); + + return true; } catch (DockerApiException) { - return null; + return false; } } - public async Task ExistsWithIdAsync(string id, CancellationToken ct = default) - { - var response = await ByIdAsync(id, ct) - .ConfigureAwait(false); - - return response != null; - } - public async Task CreateAsync(IVolumeConfiguration configuration, CancellationToken ct = default) { var createParameters = new VolumesCreateParameters diff --git a/src/Testcontainers/Clients/TestcontainersClient.cs b/src/Testcontainers/Clients/TestcontainersClient.cs index 1d1090f66..576c51372 100644 --- a/src/Testcontainers/Clients/TestcontainersClient.cs +++ b/src/Testcontainers/Clients/TestcontainersClient.cs @@ -9,6 +9,7 @@ namespace DotNet.Testcontainers.Clients using System.Threading; using System.Threading.Tasks; using Docker.DotNet; + using Docker.DotNet.Models; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; @@ -286,6 +287,8 @@ public async Task ReadFileAsync(string id, string filePath, Cancellation /// public async Task RunAsync(IContainerConfiguration configuration, CancellationToken ct = default) { + ImageInspectResponse cachedImage; + if (TestcontainersSettings.ResourceReaperEnabled && ResourceReaper.DefaultSessionId.Equals(configuration.SessionId)) { var isWindowsEngineEnabled = await System.GetIsWindowsEngineEnabled(ct) @@ -295,8 +298,15 @@ public async Task RunAsync(IContainerConfiguration configuration, Cancel .ConfigureAwait(false); } - var cachedImage = await Image.ByIdAsync(configuration.Image.FullName, ct) - .ConfigureAwait(false); + try + { + cachedImage = await Image.ByIdAsync(configuration.Image.FullName, ct) + .ConfigureAwait(false); + } + catch (DockerImageNotFoundException) + { + cachedImage = null; + } if (configuration.ImagePullPolicy(cachedImage)) { @@ -325,8 +335,17 @@ await Task.WhenAll(configuration.ResourceMappings.Select(resourceMapping => Copy /// public async Task BuildAsync(IImageFromDockerfileConfiguration configuration, CancellationToken ct = default) { - var cachedImage = await Image.ByIdAsync(configuration.Image.FullName, ct) - .ConfigureAwait(false); + ImageInspectResponse cachedImage; + + try + { + cachedImage = await Image.ByIdAsync(configuration.Image.FullName, ct) + .ConfigureAwait(false); + } + catch (DockerImageNotFoundException) + { + cachedImage = null; + } if (configuration.ImageBuildPolicy(cachedImage)) { diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs index b0e1143f0..138beaa5b 100644 --- a/src/Testcontainers/Containers/DockerContainer.cs +++ b/src/Testcontainers/Containers/DockerContainer.cs @@ -515,7 +515,7 @@ await _client.StopAsync(_container.ID, ct) } catch (DockerApiException) { - _container = null; + _container = new ContainerInspectResponse(); } StoppedTime = DateTime.UtcNow; @@ -525,7 +525,7 @@ await _client.StopAsync(_container.ID, ct) /// protected override bool Exists() { - return _container != null && ContainerHasBeenCreatedStates.HasFlag(State); + return ContainerHasBeenCreatedStates.HasFlag(State); } /// From bbb8736a14e40b04b4eb66e84afe240a65bc632c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:34:37 +0200 Subject: [PATCH 3/3] chore: Catch specific DockerContainerNotFoundException exception --- src/Testcontainers/Containers/DockerContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs index 138beaa5b..c51879071 100644 --- a/src/Testcontainers/Containers/DockerContainer.cs +++ b/src/Testcontainers/Containers/DockerContainer.cs @@ -513,7 +513,7 @@ await _client.StopAsync(_container.ID, ct) _container = await _client.Container.ByIdAsync(_container.ID, ct) .ConfigureAwait(false); } - catch (DockerApiException) + catch (DockerContainerNotFoundException) { _container = new ContainerInspectResponse(); }