Skip to content

Commit 8e69f36

Browse files
authored
Add WireMockHealthCheck in WireMock.Net.Aspire (#1375)
* 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. * Move WireMockMappingState to its own file * Dispose the cancellation tokens in WireMockServerLifecycleHook
1 parent 2160188 commit 8e69f36

File tree

8 files changed

+104
-16
lines changed

8 files changed

+104
-16
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 == WireMockMappingState.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+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright © WireMock.Net
2+
3+
namespace WireMock.Net.Aspire;
4+
5+
internal enum WireMockMappingState
6+
{
7+
NoMappings,
8+
NotSubmitted,
9+
Submitted,
10+
}

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 = WireMockMappingState.NotSubmitted;
175187

176188
return wiremock;
177189
}

src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs

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

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

17-
var wireMockServerResources = appModel.Resources
18-
.OfType<WireMockServerResource>()
19-
.ToArray();
16+
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
17+
{
18+
_linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
2019

21-
foreach (var wireMockServerResource in wireMockServerResources)
20+
_mappingTask = Task.Run(async () =>
2221
{
23-
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
22+
var wireMockServerResources = appModel.Resources
23+
.OfType<WireMockServerResource>()
24+
.ToArray();
2425

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

30-
await wireMockServerResource.CallApiMappingBuilderActionAsync(cts.Token);
33+
await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token);
3134

32-
wireMockServerResource.StartWatchingStaticMappings(cts.Token);
35+
await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token);
36+
37+
wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token);
3338
}
34-
}
39+
}, _linkedCts.Token);
40+
41+
return Task.CompletedTask;
3542
}
3643

3744
public async ValueTask DisposeAsync()
3845
{
3946
await _shutdownCts.CancelAsync();
47+
48+
_linkedCts?.Dispose();
49+
_shutdownCts.Dispose();
50+
51+
if (_mappingTask is not null)
52+
{
53+
await _mappingTask;
54+
}
4055
}
4156
}

src/WireMock.Net.Aspire/WireMockServerResource.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Stef.Validation;
66
using WireMock.Client;
77
using WireMock.Client.Extensions;
8+
using WireMock.Net.Aspire;
89
using WireMock.Util;
910

1011
// ReSharper disable once CheckNamespace
@@ -19,6 +20,7 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
1920

2021
internal WireMockServerArguments Arguments { get; }
2122
internal Lazy<IWireMockAdminApi> AdminApi => new(CreateWireMockAdminApi);
23+
internal WireMockMappingState ApiMappingState { get; set; } = WireMockMappingState.NoMappings;
2224

2325
private ILogger? _logger;
2426
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
@@ -64,6 +66,8 @@ internal async Task CallApiMappingBuilderActionAsync(CancellationToken cancellat
6466

6567
var mappingBuilder = AdminApi.Value.GetMappingBuilder();
6668
await Arguments.ApiMappingBuilder.Invoke(mappingBuilder, cancellationToken);
69+
70+
ApiMappingState = WireMockMappingState.Submitted;
6771
}
6872

6973
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)