From efc84d14b823dd64e01ede431386ea16135898e4 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:29:19 +0100 Subject: [PATCH 1/8] Enhance Keycloak container setup - Added `KeycloakHealthPort` constant set to 9000 in `KeycloakBuilder` for health check configurations. - Introduced `DefaultUsername` and `DefaultPassword` constants as "admin" in `KeycloakBuilder` for default access credentials. - Implemented logic for special handling of Keycloak version 25 and above, including: * Parsing the major version from the image tag. * Configuring `KeycloakHealthPort` port binding and a wait strategy based on the `/health/ready` endpoint for versions 25+, adapting to new health check mechanisms. --- src/Testcontainers.Keycloak/KeycloakBuilder.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index e61810f01..585010485 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -8,6 +8,8 @@ public sealed class KeycloakBuilder : ContainerBuilder 0) + { + if (int.TryParse(DockerResourceConfiguration.Image.Tag.Substring(0, tagMajorIndex), out var tagMajorVersion) + && tagMajorVersion >= 25) + { + WithPortBinding(KeycloakHealthPort, true); + WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPath("/health/ready").ForPort(KeycloakHealthPort))); + } + } } /// From ac9664ef34e8c61e60712ea3e5d34ff3bcacfeaa Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:51:02 +0100 Subject: [PATCH 2/8] Refactor KeycloakBuilder and add health tests This commit makes significant updates to the KeycloakBuilder and introduces a new test class for Keycloak container health. In the KeycloakBuilder, the Build method has been revised to remove the immediate call to `Validate()` and the direct return of a new `KeycloakContainer`. Instead, it now includes logic to extract the major version number from the `DockerResourceConfiguration.Image.Tag`. If the version is 25 or higher, the builder is modified to add port binding for the Keycloak health port and a wait strategy for the `/health/ready` endpoint. After these potential modifications, `Validate()` is called to ensure the configuration is correct before returning the container. Additionally, the validation logic that previously checked the Docker image tag's major version has been removed from the direct validation method and incorporated into the Build method. This change streamlines the validation process, focusing it solely on configuration validation. A new test class, `KeycloakContainerHealthTest`, has been added to verify the health of the Keycloak container. It includes a method `HealthIsReady` that tests whether the container starts successfully and if its health endpoint returns an HTTP OK status. This method dynamically adjusts the `HttpClient.BaseAddress` based on the container's accessible hostname and port, ensuring the test is robust across different environments. The test covers both below and at the threshold versions with inline data for "quay.io/keycloak/keycloak:24.0" and "quay.io/keycloak/keycloak:25.0", ensuring the new health check configuration is properly applied. Containers are disposed of post-test to prevent resource leakage. --- .../KeycloakBuilder.cs | 29 ++++++------- .../KeycloakContainerHealthTest.cs | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 585010485..8439477a8 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -61,8 +61,21 @@ public KeycloakBuilder WithPassword(string password) /// public override KeycloakContainer Build() { - Validate(); - return new KeycloakContainer(DockerResourceConfiguration); + var builder = this; + + var tagMajorIndex = DockerResourceConfiguration.Image.Tag.IndexOf(".", StringComparison.Ordinal); + if (tagMajorIndex > 0 + && int.TryParse(DockerResourceConfiguration.Image.Tag.Substring(0, tagMajorIndex), out var tagMajorVersion) + && tagMajorVersion >= 25) + { + builder = builder + .WithPortBinding(KeycloakHealthPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPath("/health/ready").ForPort(KeycloakHealthPort))); + } + + builder.Validate(); + return new KeycloakContainer(builder.DockerResourceConfiguration); } /// @@ -91,18 +104,6 @@ protected override void Validate() _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) .NotNull() .NotEmpty(); - - var tagMajorIndex = DockerResourceConfiguration.Image.Tag.IndexOf(".", StringComparison.Ordinal); - if (tagMajorIndex > 0) - { - if (int.TryParse(DockerResourceConfiguration.Image.Tag.Substring(0, tagMajorIndex), out var tagMajorVersion) - && tagMajorVersion >= 25) - { - WithPortBinding(KeycloakHealthPort, true); - WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(KeycloakHealthPort))); - } - } } /// diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs new file mode 100644 index 000000000..b34b31c45 --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs @@ -0,0 +1,41 @@ +namespace Testcontainers.Keycloak.Tests; + +public sealed class KeycloakContainerHealthTest +{ + + [Theory] + [InlineData("quay.io/keycloak/keycloak:24.0")] + [InlineData("quay.io/keycloak/keycloak:25.0")] + public async Task HealthIsReady(string image) + { + // Given + var keycloakContainer = new KeycloakBuilder().WithImage(image).Build(); + await keycloakContainer.StartAsync() + .ConfigureAwait(true); + + try + { + using var httpClient = new HttpClient(); + + try { + httpClient.BaseAddress = new UriBuilder(Uri.UriSchemeHttp, keycloakContainer.Hostname, keycloakContainer.GetMappedPublicPort(KeycloakBuilder.KeycloakHealthPort)).Uri; + } + catch (Exception e) + { + httpClient.BaseAddress = new Uri(keycloakContainer.GetBaseAddress()); + } + + // When + using var response = await httpClient.GetAsync("/health/ready") + .ConfigureAwait(true); + + // Then + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + finally + { + await keycloakContainer.DisposeAsync().AsTask() + .ConfigureAwait(true); + } + } +} \ No newline at end of file From d1006450a0609a7755cb40c588b3044239ab2ccc Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:56:48 +0100 Subject: [PATCH 3/8] Update src/Testcontainers.Keycloak/KeycloakBuilder.cs PR suggestion Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- .../KeycloakBuilder.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 8439477a8..bdcc1c0f7 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -61,21 +61,21 @@ public KeycloakBuilder WithPassword(string password) /// public override KeycloakContainer Build() { - var builder = this; - - var tagMajorIndex = DockerResourceConfiguration.Image.Tag.IndexOf(".", StringComparison.Ordinal); - if (tagMajorIndex > 0 - && int.TryParse(DockerResourceConfiguration.Image.Tag.Substring(0, tagMajorIndex), out var tagMajorVersion) - && tagMajorVersion >= 25) - { - builder = builder - .WithPortBinding(KeycloakHealthPort, true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(KeycloakHealthPort))); - } - - builder.Validate(); - return new KeycloakContainer(builder.DockerResourceConfiguration); + Validate(); + + var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); + + var isLatestVersion = "latest".Equals(DockerResourceConfiguration.Image.Tag, StringComparison.OrdinalIgnoreCase); + + // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. + var isMajorVersionAtLeast25 = int.TryParse(majorVersionString, out var majorVersion) && majorVersion >= 25; + + var waitStrategy = Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => + request.ForPath("/health/ready").ForPort(isLatestVersion || isMajorVersionAtLeast25 ? KeycloakHealthPort : KeycloakPort)); + + var couchbaseBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy); + return new KeycloakContainer(couchbaseBuilder.DockerResourceConfiguration); } /// From 1405e7065911ddf74ccd014aafc7e470b05a0296 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:12:15 +0100 Subject: [PATCH 4/8] Improve KeycloakBuilder version handling and consistency Enhanced `KeycloakBuilder` to better handle the `isLatestVersion` flag by including "nightly" tags and refining major version parsing. Updated the wait strategy to use `KeycloakHealthPort` for the latest versions. Renamed `couchbaseBuilder` to `keycloakBuilder` for consistency. Adjusted wait strategy condition to check for any strategies. Added `System.Linq` to global usings in `Usings.cs`. --- src/Testcontainers.Keycloak/KeycloakBuilder.cs | 18 +++++++++++------- src/Testcontainers.Keycloak/Usings.cs | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index bdcc1c0f7..32e106e28 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -63,19 +63,23 @@ public override KeycloakContainer Build() { Validate(); - var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); + var isLatestVersion = "latest".Equals(DockerResourceConfiguration.Image.Tag, StringComparison.OrdinalIgnoreCase) + || "nightly".Equals(DockerResourceConfiguration.Image.Tag, StringComparison.OrdinalIgnoreCase); - var isLatestVersion = "latest".Equals(DockerResourceConfiguration.Image.Tag, StringComparison.OrdinalIgnoreCase); + if (!isLatestVersion) + { + var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); - // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. - var isMajorVersionAtLeast25 = int.TryParse(majorVersionString, out var majorVersion) && majorVersion >= 25; + // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. + isLatestVersion |= (int.TryParse(majorVersionString, out var majorVersion) && majorVersion >= 25); + } var waitStrategy = Wait.ForUnixContainer() .UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(isLatestVersion || isMajorVersionAtLeast25 ? KeycloakHealthPort : KeycloakPort)); + request.ForPath("/health/ready").ForPort(isLatestVersion ? KeycloakHealthPort : KeycloakPort)); - var couchbaseBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy); - return new KeycloakContainer(couchbaseBuilder.DockerResourceConfiguration); + var keycloakBuilder = DockerResourceConfiguration.WaitStrategies.Any() ? this : WithWaitStrategy(waitStrategy); + return new KeycloakContainer(keycloakBuilder.DockerResourceConfiguration); } /// diff --git a/src/Testcontainers.Keycloak/Usings.cs b/src/Testcontainers.Keycloak/Usings.cs index 8e5c20fd5..f82c6873d 100644 --- a/src/Testcontainers.Keycloak/Usings.cs +++ b/src/Testcontainers.Keycloak/Usings.cs @@ -1,4 +1,5 @@ global using System; +global using System.Linq; global using Docker.DotNet.Models; global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; From c469c994115bf894d4306e2f47b0fe90ff804d9c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:47:12 +0200 Subject: [PATCH 5/8] feat: Add IImage.MatchVersion --- .../KeycloakBuilder.cs | 36 +++++----- src/Testcontainers.Keycloak/Usings.cs | 1 + src/Testcontainers/Images/DockerImage.cs | 22 ++++++ .../Images/FutureDockerImage.cs | 19 ++++++ src/Testcontainers/Images/IImage.cs | 21 ++++++ .../Fixtures/Images/HealthCheckFixture.cs | 16 +++++ .../Unit/Images/TestcontainersImageTest.cs | 67 +++++++++++++++++++ 7 files changed, 165 insertions(+), 17 deletions(-) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 8439477a8..3227360b0 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -61,21 +61,24 @@ public KeycloakBuilder WithPassword(string password) /// public override KeycloakContainer Build() { - var builder = this; - - var tagMajorIndex = DockerResourceConfiguration.Image.Tag.IndexOf(".", StringComparison.Ordinal); - if (tagMajorIndex > 0 - && int.TryParse(DockerResourceConfiguration.Image.Tag.Substring(0, tagMajorIndex), out var tagMajorVersion) - && tagMajorVersion >= 25) - { - builder = builder - .WithPortBinding(KeycloakHealthPort, true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(KeycloakHealthPort))); - } - - builder.Validate(); - return new KeycloakContainer(builder.DockerResourceConfiguration); + Validate(); + + var image = DockerResourceConfiguration.Image; + _ = image.MatchVersion((string tag) => "Foo".Equals(tag)); + + var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); + + var isLatestVersion = "latest".Equals(DockerResourceConfiguration.Image.Tag, StringComparison.OrdinalIgnoreCase); + + // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. + var isMajorVersionAtLeast25 = int.TryParse(majorVersionString, out var majorVersion) && majorVersion >= 25; + + var waitStrategy = Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => + request.ForPath("/health/ready").ForPort(isLatestVersion || isMajorVersionAtLeast25 ? KeycloakHealthPort : KeycloakPort)); + + var keycloakBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy); + return new KeycloakContainer(keycloakBuilder.DockerResourceConfiguration); } /// @@ -88,8 +91,7 @@ protected override KeycloakBuilder Init() .WithUsername(DefaultUsername) .WithPassword(DefaultPassword) .WithEnvironment("KC_HEALTH_ENABLED", "true") - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(KeycloakPort))); + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPath("/health/ready").ForPort(KeycloakPort))); } /// diff --git a/src/Testcontainers.Keycloak/Usings.cs b/src/Testcontainers.Keycloak/Usings.cs index 8e5c20fd5..f82c6873d 100644 --- a/src/Testcontainers.Keycloak/Usings.cs +++ b/src/Testcontainers.Keycloak/Usings.cs @@ -1,4 +1,5 @@ global using System; +global using System.Linq; global using Docker.DotNet.Models; global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs index b37ae0c4a..308f22f7d 100644 --- a/src/Testcontainers/Images/DockerImage.cs +++ b/src/Testcontainers/Images/DockerImage.cs @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Images { using System; using System.Linq; + using System.Text.RegularExpressions; using JetBrains.Annotations; /// @@ -10,6 +11,8 @@ public sealed class DockerImage : IImage { private const string LatestTag = "latest"; + private const string NightlyTag = "nightly"; + private static readonly Func GetDockerImage = MatchImage.Match; private static readonly char[] TrimChars = { ' ', ':', '/' }; @@ -107,6 +110,25 @@ public DockerImage( /// public string GetHostname() => _lazyHostname.Value; + /// + public bool MatchLatestOrNightly() + { + return MatchVersion((string tag) => LatestTag.Equals(tag) || NightlyTag.Equals(tag)); + } + + /// + public bool MatchVersion(Predicate predicate) + { + return predicate(Tag); + } + + /// + public bool MatchVersion(Predicate predicate) + { + var versionMatch = Regex.Match(Tag, "^\\d+\\.\\d+\\.\\d+", RegexOptions.None, TimeSpan.FromSeconds(1)); + return versionMatch.Success && Version.TryParse(versionMatch.Value, out var version) && predicate(version); + } + private static string TrimOrDefault(string value, string defaultValue = default) { return string.IsNullOrEmpty(value) ? defaultValue : value.Trim(TrimChars); diff --git a/src/Testcontainers/Images/FutureDockerImage.cs b/src/Testcontainers/Images/FutureDockerImage.cs index 02271788b..23817169d 100644 --- a/src/Testcontainers/Images/FutureDockerImage.cs +++ b/src/Testcontainers/Images/FutureDockerImage.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Images { + using System; using System.Threading; using System.Threading.Tasks; using Docker.DotNet.Models; @@ -74,6 +75,24 @@ public string GetHostname() return _configuration.Image.GetHostname(); } + /// + public bool MatchLatestOrNightly() + { + return _configuration.Image.MatchLatestOrNightly(); + } + + /// + public bool MatchVersion(Predicate predicate) + { + return _configuration.Image.MatchVersion(predicate); + } + + /// + public bool MatchVersion(Predicate predicate) + { + return _configuration.Image.MatchVersion(predicate); + } + /// public async Task CreateAsync(CancellationToken ct = default) { diff --git a/src/Testcontainers/Images/IImage.cs b/src/Testcontainers/Images/IImage.cs index b9badc5d3..55bc45335 100644 --- a/src/Testcontainers/Images/IImage.cs +++ b/src/Testcontainers/Images/IImage.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Images { + using System; using JetBrains.Annotations; /// @@ -41,5 +42,25 @@ public interface IImage /// The registry hostname. [CanBeNull] string GetHostname(); + + /// + /// + /// + /// + bool MatchLatestOrNightly(); + + /// + /// + /// + /// + /// + bool MatchVersion(Predicate predicate); + + /// + /// + /// + /// + /// + bool MatchVersion(Predicate predicate); } } diff --git a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs index db4034d3e..932c95526 100644 --- a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Tests.Fixtures { + using System; using System.IO; using System.Threading.Tasks; using DotNet.Testcontainers.Builders; @@ -27,6 +28,21 @@ public string GetHostname() return _image.GetHostname(); } + public bool MatchLatestOrNightly() + { + return _image.MatchLatestOrNightly(); + } + + public bool MatchVersion(Predicate predicate) + { + return _image.MatchVersion(predicate); + } + + public bool MatchVersion(Predicate predicate) + { + return _image.MatchVersion(predicate); + } + public Task InitializeAsync() { return _image.CreateAsync(); diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs index 4eacc1c80..bab17526a 100644 --- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs @@ -59,5 +59,72 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl Assert.Equal(expected.Tag, dockerImage.Tag); Assert.Equal(expected.FullName, dockerImage.FullName); } + + [Fact] + public void MatchLatestOrNightly_TagIsLatest_ReturnsTrue() + { + // Given + IImage dockerImage = new DockerImage("foo:latest"); + + // When + var result = dockerImage.MatchLatestOrNightly(); + + // Then + Assert.True(result); + } + + [Fact] + public void MatchLatestOrNightly_TagIsNightly_ReturnsTrue() + { + // Given + IImage dockerImage = new DockerImage("foo:nightly"); + + // When + var result = dockerImage.MatchLatestOrNightly(); + + // Then + Assert.True(result); + } + + [Fact] + public void MatchLatestOrNightly_TagIsNeither_ReturnsFalse() + { + // Given + IImage dockerImage = new DockerImage("foo:1.0.0"); + + // When + var result = dockerImage.MatchLatestOrNightly(); + + // Then + Assert.False(result); + } + + [Fact] + public void MatchVersion_ReturnsTrue_WhenVersionMatchesPredicate() + { + // Given + Predicate predicate = v => v.Major == 1 && v.Minor == 0 && v.Build == 0; + IImage dockerImage = new DockerImage("foo:1.0.0"); + + // When + var result = dockerImage.MatchVersion(predicate); + + // Then + Assert.True(result); + } + + [Fact] + public void MatchVersion_ReturnsFalse_WhenVersionDoesNotMatchPredicate() + { + // Given + Predicate predicate = v => v.Major == 0 && v.Minor == 0 && v.Build == 1; + IImage dockerImage = new DockerImage("foo:1.0.0"); + + // When + var result = dockerImage.MatchVersion(predicate); + + // Then + Assert.False(result); + } } } From 25ddadbd34bb35f169d8899a6a8e3058582904dc Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:12:15 +0100 Subject: [PATCH 6/8] Improve KeycloakBuilder version handling and consistency Enhanced `KeycloakBuilder` to better handle the `isLatestVersion` flag by including "nightly" tags and refining major version parsing. Updated the wait strategy to use `KeycloakHealthPort` for the latest versions. Renamed `couchbaseBuilder` to `keycloakBuilder` for consistency. Adjusted wait strategy condition to check for any strategies. Added `System.Linq` to global usings in `Usings.cs`. --- src/Testcontainers.Keycloak/KeycloakBuilder.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 3227360b0..656b5a020 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -68,14 +68,17 @@ public override KeycloakContainer Build() var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); - var isLatestVersion = "latest".Equals(DockerResourceConfiguration.Image.Tag, StringComparison.OrdinalIgnoreCase); + if (!isLatestVersion) + { + var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); - // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. - var isMajorVersionAtLeast25 = int.TryParse(majorVersionString, out var majorVersion) && majorVersion >= 25; + // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. + isLatestVersion |= (int.TryParse(majorVersionString, out var majorVersion) && majorVersion >= 25); + } var waitStrategy = Wait.ForUnixContainer() .UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(isLatestVersion || isMajorVersionAtLeast25 ? KeycloakHealthPort : KeycloakPort)); + request.ForPath("/health/ready").ForPort(isLatestVersion ? KeycloakHealthPort : KeycloakPort)); var keycloakBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy); return new KeycloakContainer(keycloakBuilder.DockerResourceConfiguration); From 16aa6b5c2d4cd4e180a0f751371e58f6e20b8d53 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:43:56 +0200 Subject: [PATCH 7/8] chore: Use new IImage.MatchVersion member --- .../KeycloakBuilder.cs | 20 ++++----- src/Testcontainers/Images/DockerImage.cs | 2 +- .../KeycloakContainerHealthTest.cs | 41 ------------------- .../KeycloakContainerTest.cs | 29 +++++++++++-- tests/Testcontainers.Keycloak.Tests/Usings.cs | 1 + 5 files changed, 35 insertions(+), 58 deletions(-) delete mode 100644 tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 656b5a020..1f255cd3a 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -63,22 +63,16 @@ public override KeycloakContainer Build() { Validate(); - var image = DockerResourceConfiguration.Image; - _ = image.MatchVersion((string tag) => "Foo".Equals(tag)); - - var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); + Predicate predicate = v => v.Major >= 25; - if (!isLatestVersion) - { - var majorVersionString = new string(DockerResourceConfiguration.Image.Tag.TakeWhile(char.IsDigit).ToArray()); + var image = DockerResourceConfiguration.Image; - // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. - isLatestVersion |= (int.TryParse(majorVersionString, out var majorVersion) && majorVersion >= 25); - } + // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. + var isMajorVersionGreaterOrEqual25 = image.MatchLatestOrNightly() || image.MatchVersion(predicate); var waitStrategy = Wait.ForUnixContainer() .UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(isLatestVersion ? KeycloakHealthPort : KeycloakPort)); + request.ForPath("/health/ready").ForPort(isMajorVersionGreaterOrEqual25 ? KeycloakHealthPort : KeycloakPort)); var keycloakBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy); return new KeycloakContainer(keycloakBuilder.DockerResourceConfiguration); @@ -91,10 +85,10 @@ protected override KeycloakBuilder Init() .WithImage(KeycloakImage) .WithCommand("start-dev") .WithPortBinding(KeycloakPort, true) + .WithPortBinding(KeycloakHealthPort, true) .WithUsername(DefaultUsername) .WithPassword(DefaultPassword) - .WithEnvironment("KC_HEALTH_ENABLED", "true") - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPath("/health/ready").ForPort(KeycloakPort))); + .WithEnvironment("KC_HEALTH_ENABLED", "true"); } /// diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs index 308f22f7d..468d292ee 100644 --- a/src/Testcontainers/Images/DockerImage.cs +++ b/src/Testcontainers/Images/DockerImage.cs @@ -125,7 +125,7 @@ public bool MatchVersion(Predicate predicate) /// public bool MatchVersion(Predicate predicate) { - var versionMatch = Regex.Match(Tag, "^\\d+\\.\\d+\\.\\d+", RegexOptions.None, TimeSpan.FromSeconds(1)); + var versionMatch = Regex.Match(Tag, "^\\d+(\\.\\d+)?(\\.\\d+)?", RegexOptions.None, TimeSpan.FromSeconds(1)); return versionMatch.Success && Version.TryParse(versionMatch.Value, out var version) && predicate(version); } diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs deleted file mode 100644 index b34b31c45..000000000 --- a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerHealthTest.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Testcontainers.Keycloak.Tests; - -public sealed class KeycloakContainerHealthTest -{ - - [Theory] - [InlineData("quay.io/keycloak/keycloak:24.0")] - [InlineData("quay.io/keycloak/keycloak:25.0")] - public async Task HealthIsReady(string image) - { - // Given - var keycloakContainer = new KeycloakBuilder().WithImage(image).Build(); - await keycloakContainer.StartAsync() - .ConfigureAwait(true); - - try - { - using var httpClient = new HttpClient(); - - try { - httpClient.BaseAddress = new UriBuilder(Uri.UriSchemeHttp, keycloakContainer.Hostname, keycloakContainer.GetMappedPublicPort(KeycloakBuilder.KeycloakHealthPort)).Uri; - } - catch (Exception e) - { - httpClient.BaseAddress = new Uri(keycloakContainer.GetBaseAddress()); - } - - // When - using var response = await httpClient.GetAsync("/health/ready") - .ConfigureAwait(true); - - // Then - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - finally - { - await keycloakContainer.DisposeAsync().AsTask() - .ConfigureAwait(true); - } - } -} \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs index 85f6f69f5..23884b4d8 100644 --- a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs +++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs @@ -1,8 +1,13 @@ -namespace Testcontainers.Keycloak.Tests; +namespace Testcontainers.Keycloak; -public sealed class KeycloakContainerTest : IAsyncLifetime +public abstract class KeycloakContainerTest : IAsyncLifetime { - private readonly KeycloakContainer _keycloakContainer = new KeycloakBuilder().Build(); + private readonly KeycloakContainer _keycloakContainer; + + private KeycloakContainerTest(KeycloakContainer keycloakContainer) + { + _keycloakContainer = keycloakContainer; + } public Task InitializeAsync() { @@ -42,4 +47,22 @@ public async Task MasterRealmIsEnabled() // Then Assert.True(masterRealm.Enabled); } + + [UsedImplicitly] + public sealed class KeycloakDefaultConfiguration : KeycloakContainerTest + { + public KeycloakDefaultConfiguration() + : base(new KeycloakBuilder().Build()) + { + } + } + + [UsedImplicitly] + public sealed class KeycloakV25Configuration : KeycloakContainerTest + { + public KeycloakV25Configuration() + : base(new KeycloakBuilder().WithImage("quay.io/keycloak/keycloak:25.0").Build()) + { + } + } } \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/Usings.cs b/tests/Testcontainers.Keycloak.Tests/Usings.cs index 66a86e46d..b6c54f314 100644 --- a/tests/Testcontainers.Keycloak.Tests/Usings.cs +++ b/tests/Testcontainers.Keycloak.Tests/Usings.cs @@ -2,5 +2,6 @@ global using System.Net; global using System.Net.Http; global using System.Threading.Tasks; +global using JetBrains.Annotations; global using Keycloak.Net; global using Xunit; \ No newline at end of file From 1d5885225738d29208bd194037900956c1f97b6f Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:58:24 +0200 Subject: [PATCH 8/8] docs: Add XML doc --- src/Testcontainers/Images/IImage.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Testcontainers/Images/IImage.cs b/src/Testcontainers/Images/IImage.cs index 55bc45335..307879d23 100644 --- a/src/Testcontainers/Images/IImage.cs +++ b/src/Testcontainers/Images/IImage.cs @@ -44,23 +44,23 @@ public interface IImage string GetHostname(); /// - /// + /// Checks if the tag matches either the latest or nightly tag. /// - /// + /// True if the tag matches the latest or nightly tag, otherwise false. bool MatchLatestOrNightly(); /// - /// + /// Checks if the tag matches the specified predicate. /// - /// - /// + /// The predicate to match the tag against. + /// True if the tag matches the predicate, otherwise false. bool MatchVersion(Predicate predicate); /// - /// + /// Checks if the tag matches the specified predicate. /// - /// - /// + /// The predicate to match the tag against. + /// True if the tag matches the predicate, otherwise false. bool MatchVersion(Predicate predicate); } }