Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add Keycloak health port 9000 starting from major version 25 #1213

Merged
merged 10 commits into from
Aug 7, 2024
22 changes: 18 additions & 4 deletions src/Testcontainers.Keycloak/KeycloakBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public sealed class KeycloakBuilder : ContainerBuilder<KeycloakBuilder, Keycloak

public const ushort KeycloakPort = 8080;

public const ushort KeycloakHealthPort = 9000;

public const string DefaultUsername = "admin";

public const string DefaultPassword = "admin";
Expand Down Expand Up @@ -60,7 +62,20 @@ public KeycloakBuilder WithPassword(string password)
public override KeycloakContainer Build()
{
Validate();
return new KeycloakContainer(DockerResourceConfiguration);

Predicate<System.Version> predicate = v => v.Major >= 25;

var image = DockerResourceConfiguration.Image;

// 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(isMajorVersionGreaterOrEqual25 ? KeycloakHealthPort : KeycloakPort));

var keycloakBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy);
return new KeycloakContainer(keycloakBuilder.DockerResourceConfiguration);
}

/// <inheritdoc />
Expand All @@ -70,11 +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");
}

/// <inheritdoc />
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.Keycloak/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/Testcontainers/Images/DockerImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Images
{
using System;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;

/// <inheritdoc cref="IImage" />
Expand All @@ -10,6 +11,8 @@ public sealed class DockerImage : IImage
{
private const string LatestTag = "latest";

private const string NightlyTag = "nightly";

private static readonly Func<string, IImage> GetDockerImage = MatchImage.Match;

private static readonly char[] TrimChars = { ' ', ':', '/' };
Expand Down Expand Up @@ -107,6 +110,25 @@ public DockerImage(
/// <inheritdoc />
public string GetHostname() => _lazyHostname.Value;

/// <inheritdoc />
public bool MatchLatestOrNightly()
{
return MatchVersion((string tag) => LatestTag.Equals(tag) || NightlyTag.Equals(tag));
}

/// <inheritdoc />
public bool MatchVersion(Predicate<string> predicate)
{
return predicate(Tag);
}

/// <inheritdoc />
public bool MatchVersion(Predicate<Version> 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);
Expand Down
19 changes: 19 additions & 0 deletions src/Testcontainers/Images/FutureDockerImage.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace DotNet.Testcontainers.Images
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet.Models;
Expand Down Expand Up @@ -74,6 +75,24 @@ public string GetHostname()
return _configuration.Image.GetHostname();
}

/// <inheritdoc />
public bool MatchLatestOrNightly()
{
return _configuration.Image.MatchLatestOrNightly();
}

/// <inheritdoc />
public bool MatchVersion(Predicate<string> predicate)
{
return _configuration.Image.MatchVersion(predicate);
}

/// <inheritdoc />
public bool MatchVersion(Predicate<System.Version> predicate)
{
return _configuration.Image.MatchVersion(predicate);
}

/// <inheritdoc />
public async Task CreateAsync(CancellationToken ct = default)
{
Expand Down
21 changes: 21 additions & 0 deletions src/Testcontainers/Images/IImage.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace DotNet.Testcontainers.Images
{
using System;
using JetBrains.Annotations;

/// <summary>
Expand Down Expand Up @@ -41,5 +42,25 @@ public interface IImage
/// <returns>The registry hostname.</returns>
[CanBeNull]
string GetHostname();

/// <summary>
/// Checks if the tag matches either the latest or nightly tag.
/// </summary>
/// <returns>True if the tag matches the latest or nightly tag, otherwise false.</returns>
bool MatchLatestOrNightly();

/// <summary>
/// Checks if the tag matches the specified predicate.
/// </summary>
/// <param name="predicate">The predicate to match the tag against.</param>
/// <returns>True if the tag matches the predicate, otherwise false.</returns>
bool MatchVersion(Predicate<string> predicate);

/// <summary>
/// Checks if the tag matches the specified predicate.
/// </summary>
/// <param name="predicate">The predicate to match the tag against.</param>
/// <returns>True if the tag matches the predicate, otherwise false.</returns>
bool MatchVersion(Predicate<Version> predicate);
}
}
29 changes: 26 additions & 3 deletions tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs
Original file line number Diff line number Diff line change
@@ -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()
{
Expand Down Expand Up @@ -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())
{
}
}
}
1 change: 1 addition & 0 deletions tests/Testcontainers.Keycloak.Tests/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
16 changes: 16 additions & 0 deletions tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace DotNet.Testcontainers.Tests.Fixtures
{
using System;
using System.IO;
using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
Expand Down Expand Up @@ -27,6 +28,21 @@ public string GetHostname()
return _image.GetHostname();
}

public bool MatchLatestOrNightly()
{
return _image.MatchLatestOrNightly();
}

public bool MatchVersion(Predicate<string> predicate)
{
return _image.MatchVersion(predicate);
}

public bool MatchVersion(Predicate<Version> predicate)
{
return _image.MatchVersion(predicate);
}

public Task InitializeAsync()
{
return _image.CreateAsync();
Expand Down
67 changes: 67 additions & 0 deletions tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Version> 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<Version> 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);
}
}
}
Loading