Skip to content

Commit 2c4d484

Browse files
committed
Add WireMockHealthCheck
For use with Aspire, to make WaitFor(wiremock) more useful. Calls /__admin/health and checks the result, as well as checks if mappings using AdminApiMappingBuilder has been submitted to the server. This created a catch-22 problem where the mappings were not submitted until the health check was healthy, but the health check was not healthy until the mappings were submitted. To avoid this, the WireMockServerLifecycleHook class has been slightly re-arranged, and is now using the AfterEndpointsAllocatedAsync callback rather than the AfterResourcesCreatedAsync callback. Within which a separate Task is created that waits until the server is ready and submits the mappings.
1 parent 1feb0ad commit 2c4d484

File tree

7 files changed

+87
-12
lines changed

7 files changed

+87
-12
lines changed

examples-Aspire/AspireApp1.AppHost/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
builder.AddProject<Projects.AspireApp1_Web>("webfrontend")
4747
.WithExternalHttpEndpoints()
48-
.WithReference(apiService);
48+
.WithReference(apiService)
49+
.WaitFor(apiService);
4950

5051
builder.Build().Run();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright © WireMock.Net
2+
3+
using Aspire.Hosting.ApplicationModel;
4+
using Microsoft.Extensions.Diagnostics.HealthChecks;
5+
using WireMock.Client;
6+
7+
namespace WireMock.Net.Aspire;
8+
9+
/// <summary>
10+
/// WireMockHealthCheck
11+
/// </summary>
12+
public class WireMockHealthCheck(WireMockServerResource resource) : IHealthCheck
13+
{
14+
private const string HealthStatusHealthy = "Healthy";
15+
16+
/// <inheritdoc />
17+
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
18+
{
19+
if (!await IsHealthyAsync(resource.AdminApi.Value, cancellationToken))
20+
{
21+
return HealthCheckResult.Unhealthy("WireMock.Net is not healthy");
22+
}
23+
24+
if (resource.ApiMappingState == WireMockServerResource.MappingState.NotSubmitted)
25+
{
26+
return HealthCheckResult.Unhealthy("WireMock.Net has not received mappings");
27+
}
28+
29+
return HealthCheckResult.Healthy();
30+
}
31+
32+
private static async Task<bool> IsHealthyAsync(IWireMockAdminApi adminApi, CancellationToken cancellationToken)
33+
{
34+
try
35+
{
36+
var status = await adminApi.GetHealthAsync(cancellationToken);
37+
return string.Equals(status, HealthStatusHealthy, StringComparison.OrdinalIgnoreCase);
38+
}
39+
catch
40+
{
41+
return false;
42+
}
43+
}
44+
}

src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Aspire.Hosting.ApplicationModel;
44
using Aspire.Hosting.Lifecycle;
55
using Aspire.Hosting.WireMock;
6+
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Extensions.Diagnostics.HealthChecks;
78
using Stef.Validation;
89
using WireMock.Client.Builders;
@@ -53,11 +54,21 @@ public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistrib
5354
Guard.NotNull(arguments);
5455

5556
var wireMockContainerResource = new WireMockServerResource(name, arguments);
57+
58+
var healthCheckKey = $"{name}_check";
59+
var healthCheckRegistration = new HealthCheckRegistration(
60+
healthCheckKey,
61+
_ => new WireMockHealthCheck(wireMockContainerResource),
62+
failureStatus: null,
63+
tags: null);
64+
builder.Services.AddHealthChecks().Add(healthCheckRegistration);
65+
5666
var resourceBuilder = builder
5767
.AddResource(wireMockContainerResource)
5868
.WithImage(DefaultLinuxImage)
5969
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
6070
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort)
71+
.WithHealthCheck(healthCheckKey)
6172
.WithWireMockInspectorCommand();
6273

6374
if (!string.IsNullOrEmpty(arguments.MappingsPath))
@@ -172,6 +183,7 @@ public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(thi
172183

173184
wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
174185
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
186+
wiremock.Resource.ApiMappingState = WireMockServerResource.MappingState.NotSubmitted;
175187

176188
return wiremock;
177189
}

src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,38 @@ internal class WireMockServerLifecycleHook(ILoggerFactory loggerFactory) : IDist
1010
{
1111
private readonly CancellationTokenSource _shutdownCts = new();
1212

13-
public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
13+
private Task? _mappingTask;
14+
15+
public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
1416
{
1517
var cts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
1618

17-
var wireMockServerResources = appModel.Resources
18-
.OfType<WireMockServerResource>()
19-
.ToArray();
20-
21-
foreach (var wireMockServerResource in wireMockServerResources)
19+
_mappingTask = Task.Run(async () =>
2220
{
23-
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
21+
var wireMockServerResources = appModel.Resources
22+
.OfType<WireMockServerResource>()
23+
.ToArray();
2424

25-
var endpoint = wireMockServerResource.GetEndpoint();
26-
if (endpoint.IsAllocated)
25+
foreach (var wireMockServerResource in wireMockServerResources)
2726
{
27+
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
28+
29+
var endpoint = wireMockServerResource.GetEndpoint();
30+
System.Diagnostics.Debug.Assert(endpoint.IsAllocated);
31+
2832
await wireMockServerResource.WaitForHealthAsync(cts.Token);
2933

3034
await wireMockServerResource.CallApiMappingBuilderActionAsync(cts.Token);
3135

3236
wireMockServerResource.StartWatchingStaticMappings(cts.Token);
3337
}
34-
}
38+
}, cts.Token);
3539
}
3640

3741
public async ValueTask DisposeAsync()
3842
{
3943
await _shutdownCts.CancelAsync();
44+
if (_mappingTask is not null)
45+
await _mappingTask;
4046
}
4147
}

src/WireMock.Net.Aspire/WireMockServerResource.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
2020
internal WireMockServerArguments Arguments { get; }
2121
internal Lazy<IWireMockAdminApi> AdminApi => new(CreateWireMockAdminApi);
2222

23+
internal enum MappingState
24+
{
25+
NoMappings,
26+
NotSubmitted,
27+
Submitted,
28+
}
29+
internal MappingState ApiMappingState { get; set; } = MappingState.NoMappings;
30+
2331
private ILogger? _logger;
2432
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
2533

@@ -64,6 +72,8 @@ internal async Task CallApiMappingBuilderActionAsync(CancellationToken cancellat
6472

6573
var mappingBuilder = AdminApi.Value.GetMappingBuilder();
6674
await Arguments.ApiMappingBuilder.Invoke(mappingBuilder, cancellationToken);
75+
76+
ApiMappingState = MappingState.Submitted;
6777
}
6878

6979
internal void StartWatchingStaticMappings(CancellationToken cancellationToken)

test/WireMock.Net.Aspire.Tests/IntegrationTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public async Task StartAppHostWithWireMockAndCreateHttpClientToCallTheMockedWeat
1919
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
2020
await using var app = await appHostBuilder.BuildAsync();
2121
await app.StartAsync();
22+
await app.ResourceNotifications.WaitForResourceHealthyAsync("wiremock-service");
2223

2324
using var httpClient = app.CreateHttpClient("wiremock-service");
2425

@@ -46,6 +47,7 @@ public async Task StartAppHostWithWireMockAndCreateWireMockAdminClientToCallTheA
4647
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
4748
await using var app = await appHostBuilder.BuildAsync();
4849
await app.StartAsync();
50+
await app.ResourceNotifications.WaitForResourceHealthyAsync("wiremock-service");
4951

5052
var adminClient = app.CreateWireMockAdminClient("wiremock-service");
5153

test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public void AddWireMock()
6767
MappingsPath = null,
6868
HttpPort = port
6969
});
70-
wiremock.Resource.Annotations.Should().HaveCount(5);
70+
wiremock.Resource.Annotations.Should().HaveCount(6);
7171

7272
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
7373
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation

0 commit comments

Comments
 (0)