WireMock.Net.Testcontainers: implement watching the static mapping folder for changes (#1189)

* WireMock.Net.Testcontainers: implement watching the static mapping files + folder for changes

* ReloadStaticMappings

* fix

* .

* .

* .

* .

* .

* .

* .

* CopyAsync

* <VersionPrefix>1.6.7-preview-02</VersionPrefix>

* <VersionPrefix>1.6.7-preview-03</VersionPrefix>
This commit is contained in:
Stef Heyenrath
2024-12-15 11:31:25 +01:00
committed by GitHub
parent c548600dea
commit 2a19b4491f
26 changed files with 511 additions and 121 deletions

View File

@@ -0,0 +1,14 @@
// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
namespace WireMock.Net.Aspire.Extensions;
internal static class ResourceLoggerServiceExtensions
{
public static void SetLogger(this ResourceLoggerService resourceLoggerService, WireMockServerResource wireMockServerResource)
{
var logger = resourceLoggerService.GetLogger(wireMockServerResource);
wireMockServerResource.SetLogger(logger);
}
}

View File

@@ -30,6 +30,10 @@
<None Include="../../resources/WireMock.Net-LogoAspire.png" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\WireMock.Net\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

View File

@@ -50,7 +50,7 @@ public class WireMockServerArguments
///
/// Default value is <c>false</c>.
/// </summary>
public bool WithWatchStaticMappings { get; set; }
public bool WatchStaticMappings { get; set; }
/// <summary>
/// Specifies the path for the (static) mapping json files.
@@ -65,7 +65,7 @@ public class WireMockServerArguments
/// <summary>
/// Optional delegate that will be invoked to configure the WireMock.Net resource using the <see cref="AdminApiMappingBuilder"/>.
/// </summary>
public Func<AdminApiMappingBuilder, Task>? ApiMappingBuilder { get; set; }
public Func<AdminApiMappingBuilder, CancellationToken, Task>? ApiMappingBuilder { get; set; }
/// <summary>
/// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server.
@@ -88,7 +88,7 @@ public class WireMockServerArguments
Add(args, "--ReadStaticMappings", "true");
}
if (WithWatchStaticMappings)
if (WatchStaticMappings)
{
Add(args, "--ReadStaticMappings", "true");
Add(args, "--WatchStaticMappings", "true");

View File

@@ -112,7 +112,7 @@ public static class WireMockServerBuilderExtensions
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithWatchStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.WithWatchStaticMappings = true;
Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true;
return wiremock;
}
@@ -124,8 +124,10 @@ public static class WireMockServerBuilderExtensions
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithMappingsPath(this IResourceBuilder<WireMockServerResource> wiremock, string mappingsPath)
{
return Guard.NotNull(wiremock)
.WithBindMount(Guard.NotNullOrWhiteSpace(mappingsPath), DefaultLinuxMappingsPath);
Guard.NotNullOrWhiteSpace(mappingsPath);
Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath;
return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath);
}
/// <summary>
@@ -151,6 +153,17 @@ public static class WireMockServerBuilderExtensions
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
/// <returns></returns>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
{
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
}
/// <summary>
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
/// <returns></returns>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, CancellationToken, Task> configure)
{
Guard.NotNull(wiremock);

View File

@@ -3,50 +3,39 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
using RestEase;
using WireMock.Client;
using WireMock.Client.Extensions;
namespace WireMock.Net.Aspire;
internal class WireMockServerLifecycleHook(ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
internal class WireMockServerLifecycleHook(ILoggerFactory loggerFactory) : IDistributedApplicationLifecycleHook, IAsyncDisposable
{
private readonly CancellationTokenSource _shutdownCts = new();
public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.Where(resource => resource.Arguments.ApiMappingBuilder is not null)
.ToArray();
if (wireMockServerResources.Length == 0)
{
return;
}
foreach (var wireMockServerResource in wireMockServerResources)
{
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
var endpoint = wireMockServerResource.GetEndpoint();
if (endpoint.IsAllocated)
{
var adminApi = CreateWireMockAdminApi(wireMockServerResource);
await wireMockServerResource.WaitForHealthAsync(cts.Token);
var logger = loggerService.GetLogger(wireMockServerResource);
logger.LogInformation("Checking Health status from WireMock.Net");
await wireMockServerResource.CallApiMappingBuilderActionAsync(cts.Token);
await adminApi.WaitForHealthAsync(cancellationToken: cancellationToken);
logger.LogInformation("Calling ApiMappingBuilder to add mappings to WireMock.Net");
var mappingBuilder = adminApi.GetMappingBuilder();
await wireMockServerResource.Arguments.ApiMappingBuilder!.Invoke(mappingBuilder);
wireMockServerResource.StartWatchingStaticMappings(cts.Token);
}
}
}
private static IWireMockAdminApi CreateWireMockAdminApi(WireMockServerResource resource)
public async ValueTask DisposeAsync()
{
var adminApi = RestClient.For<IWireMockAdminApi>(resource.GetEndpoint().Url);
return resource.Arguments.HasBasicAuthentication ?
adminApi.WithAuthorization(resource.Arguments.AdminUsername!, resource.Arguments.AdminPassword!) :
adminApi;
await _shutdownCts.CancelAsync();
}
}

View File

@@ -1,6 +1,11 @@
// Copyright © WireMock.Net
using Microsoft.Extensions.Logging;
using RestEase;
using Stef.Validation;
using WireMock.Client;
using WireMock.Client.Extensions;
using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting.ApplicationModel;
@@ -10,7 +15,13 @@ namespace Aspire.Hosting.ApplicationModel;
/// </summary>
public class WireMockServerResource : ContainerResource, IResourceWithServiceDiscovery
{
private const int EnhancedFileSystemWatcherTimeoutMs = 2000;
internal WireMockServerArguments Arguments { get; }
internal Lazy<IWireMockAdminApi> AdminApi => new(CreateWireMockAdminApi);
private ILogger? _logger;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
/// <summary>
/// Initializes a new instance of the <see cref="WireMockServerResource"/> class.
@@ -30,4 +41,82 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
{
return new EndpointReference(this, "http");
}
internal void SetLogger(ILogger logger)
{
_logger = logger;
}
internal async Task WaitForHealthAsync(CancellationToken cancellationToken)
{
_logger?.LogInformation("Checking Health status from WireMock.Net");
await AdminApi.Value.WaitForHealthAsync(cancellationToken: cancellationToken);
}
internal async Task CallApiMappingBuilderActionAsync(CancellationToken cancellationToken)
{
if (Arguments.ApiMappingBuilder == null)
{
return;
}
_logger?.LogInformation("Calling ApiMappingBuilder to add mappings to WireMock.Net");
var mappingBuilder = AdminApi.Value.GetMappingBuilder();
await Arguments.ApiMappingBuilder.Invoke(mappingBuilder, cancellationToken);
}
internal void StartWatchingStaticMappings(CancellationToken cancellationToken)
{
if (!Arguments.WatchStaticMappings || string.IsNullOrEmpty(Arguments.MappingsPath))
{
return;
}
cancellationToken.Register(() =>
{
if (_enhancedFileSystemWatcher != null)
{
_enhancedFileSystemWatcher.EnableRaisingEvents = false;
_enhancedFileSystemWatcher.Created -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Changed -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Deleted -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Dispose();
_enhancedFileSystemWatcher = null;
}
});
_logger?.LogInformation("Starting to watch static mappings on path: '{Path}'. ", Arguments.MappingsPath);
_enhancedFileSystemWatcher = new EnhancedFileSystemWatcher(Arguments.MappingsPath, "*.json", EnhancedFileSystemWatcherTimeoutMs)
{
IncludeSubdirectories = true
};
_enhancedFileSystemWatcher.Created += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Changed += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Deleted += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.EnableRaisingEvents = true;
}
private IWireMockAdminApi CreateWireMockAdminApi()
{
var adminApi = RestClient.For<IWireMockAdminApi>(GetEndpoint().Url);
return Arguments.HasBasicAuthentication ?
adminApi.WithAuthorization(Arguments.AdminUsername!, Arguments.AdminPassword!) :
adminApi;
}
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
{
_logger?.LogInformation("MappingFile created, changed or deleted: '{0}'. Triggering ReloadStaticMappings.", args.FullPath);
try
{
await AdminApi.Value.ReloadStaticMappingsAsync();
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Error calling /__admin/mappings/reloadStaticMappings");
}
}
}