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,16 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Runtime.InteropServices;
using WireMock.Net.Testcontainers.Models;
namespace WireMock.Net.Testcontainers.Utils;
internal static class ContainerInfoProvider
{
public static readonly Dictionary<OSPlatform, ContainerInfo> Info = new()
{
{ OSPlatform.Linux, new ContainerInfo("sheyenrath/wiremock.net-alpine", "/app/__admin/mappings") },
{ OSPlatform.Windows, new ContainerInfo("sheyenrath/wiremock.net-windows", @"c:\app\__admin\mappings") }
};
}

View File

@@ -0,0 +1,25 @@
// Copyright © WireMock.Net
using System;
using System.Runtime.InteropServices;
using DotNet.Testcontainers.Configurations;
using System.Threading.Tasks;
namespace WireMock.Net.Testcontainers.Utils;
internal static class ContainerUtils
{
public static Lazy<Task<OSPlatform>> GetImageOSAsync = new(async () =>
{
if (TestcontainersSettings.OS.DockerEndpointAuthConfig == null)
{
throw new InvalidOperationException($"The {nameof(TestcontainersSettings.OS.DockerEndpointAuthConfig)} is null. Check if Docker is started.");
}
using var dockerClientConfig = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration();
using var dockerClient = dockerClientConfig.CreateClient();
var version = await dockerClient.System.GetVersionAsync();
return version.Os.IndexOf("Windows", StringComparison.OrdinalIgnoreCase) >= 0 ? OSPlatform.Windows : OSPlatform.Linux;
});
}

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<Compile Include="..\WireMock.Net\Http\HttpClientFactory2.cs" Link="Http\HttpClientFactory2.cs" />
<Compile Include="..\WireMock.Net\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net
using System;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
@@ -19,6 +18,10 @@ public sealed class WireMockConfiguration : ContainerConfiguration
public string? StaticMappingsPath { get; private set; }
public bool WatchStaticMappings { get; private set; }
public bool WatchStaticMappingsInSubdirectories { get; private set; }
public bool HasBasicAuthentication => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password);
public WireMockConfiguration(string? username = null, string? password = null)
@@ -65,16 +68,30 @@ public sealed class WireMockConfiguration : ContainerConfiguration
Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
StaticMappingsPath = BuildConfiguration.Combine(oldValue.StaticMappingsPath, newValue.StaticMappingsPath);
WatchStaticMappings = BuildConfiguration.Combine(oldValue.WatchStaticMappings, newValue.WatchStaticMappings);
WatchStaticMappingsInSubdirectories = BuildConfiguration.Combine(oldValue.WatchStaticMappingsInSubdirectories, newValue.WatchStaticMappingsInSubdirectories);
}
/// <summary>
/// Set the StaticMappingsPath.
/// </summary>
/// <param name="path">The path which contains the StaticMappings.</param>
/// <returns><see cref="WireMockConfiguration"/> </returns>
/// <returns><see cref="WireMockConfiguration"/></returns>
public WireMockConfiguration WithStaticMappingsPath(string path)
{
StaticMappingsPath = path;
return this;
}
/// <summary>
/// Watch the static mappings.
/// </summary>
/// <param name="includeSubDirectories">Also look in SubDirectories.</param>
/// <returns><see cref="WireMockConfiguration"/></returns>
public WireMockConfiguration WithWatchStaticMappings(bool includeSubDirectories)
{
WatchStaticMappings = true;
WatchStaticMappingsInSubdirectories = includeSubDirectories;
return this;
}
}

View File

@@ -1,14 +1,20 @@
// Copyright © WireMock.Net
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using RestEase;
using Stef.Validation;
using WireMock.Client;
using WireMock.Client.Extensions;
using WireMock.Http;
using WireMock.Net.Testcontainers.Utils;
using WireMock.Util;
namespace WireMock.Net.Testcontainers;
@@ -17,17 +23,23 @@ namespace WireMock.Net.Testcontainers;
/// </summary>
public sealed class WireMockContainer : DockerContainer
{
private const int EnhancedFileSystemWatcherTimeoutMs = 2000;
internal const int ContainerPort = 80;
private readonly WireMockConfiguration _configuration;
private IWireMockAdminApi? _adminApi;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
/// <summary>
/// Initializes a new instance of the <see cref="WireMockContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public WireMockContainer(WireMockConfiguration configuration) : base(configuration)
{
_configuration = Guard.NotNull(configuration);
_configuration = Stef.Validation.Guard.NotNull(configuration);
Started += WireMockContainer_Started;
}
/// <summary>
@@ -92,6 +104,71 @@ public sealed class WireMockContainer : DockerContainer
return client;
}
/// <summary>
/// Copies a test host directory or file to the container and triggers a reload of the static mappings if required.
/// </summary>
/// <param name="source">The source directory or file to be copied.</param>
/// <param name="target">The target directory path to copy the files to.</param>
/// <param name="fileMode">The POSIX file mode permission.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that completes when the directory or file has been copied.</returns>
public new async Task CopyAsync(string source, string target, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default)
{
await base.CopyAsync(source, target, fileMode, ct);
if (_configuration.WatchStaticMappings && await PathStartsWithContainerMappingsPath(target))
{
await ReloadStaticMappingsAsync(target, ct);
}
}
/// <summary>
/// Reload the static mappings.
/// </summary>
/// <param name="cancellationToken">The optional cancellationToken.</param>
public async Task ReloadStaticMappingsAsync(CancellationToken cancellationToken = default)
{
if (_adminApi == null)
{
return;
}
try
{
await _adminApi.ReloadStaticMappingsAsync(cancellationToken);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error calling /__admin/mappings/reloadStaticMappings");
}
}
/// <inheritdoc />
protected override ValueTask DisposeAsyncCore()
{
if (_enhancedFileSystemWatcher != null)
{
_enhancedFileSystemWatcher.EnableRaisingEvents = false;
_enhancedFileSystemWatcher.Created -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Changed -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Deleted -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Dispose();
_enhancedFileSystemWatcher = null;
}
Started -= WireMockContainer_Started;
return base.DisposeAsyncCore();
}
private static async Task<bool> PathStartsWithContainerMappingsPath(string value)
{
var imageOs = await ContainerUtils.GetImageOSAsync.Value;
return value.StartsWith(ContainerInfoProvider.Info[imageOs].MappingsPath);
}
private void ValidateIfRunning()
{
if (State != TestcontainersStates.Running)
@@ -100,5 +177,35 @@ public sealed class WireMockContainer : DockerContainer
}
}
private void WireMockContainer_Started(object sender, EventArgs e)
{
_adminApi = CreateWireMockAdminClient();
if (!_configuration.WatchStaticMappings || string.IsNullOrEmpty(_configuration.StaticMappingsPath))
{
return;
}
_enhancedFileSystemWatcher = new EnhancedFileSystemWatcher(_configuration.StaticMappingsPath!, "*.json", EnhancedFileSystemWatcherTimeoutMs)
{
IncludeSubdirectories = _configuration.WatchStaticMappingsInSubdirectories
};
_enhancedFileSystemWatcher.Created += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Changed += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Deleted += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.EnableRaisingEvents = true;
}
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
{
await ReloadStaticMappingsAsync(args.FullPath);
}
private async Task ReloadStaticMappingsAsync(string path, CancellationToken cancellationToken = default)
{
Logger.LogInformation("MappingFile created, changed or deleted: '{Path}'. Triggering ReloadStaticMappings.", path);
await ReloadStaticMappingsAsync(cancellationToken);
}
private Uri GetPublicUri() => new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(ContainerPort)).Uri;
}

View File

@@ -1,15 +1,13 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using JetBrains.Annotations;
using Stef.Validation;
using WireMock.Net.Testcontainers.Models;
using WireMock.Net.Testcontainers.Utils;
namespace WireMock.Net.Testcontainers;
@@ -19,26 +17,6 @@ namespace WireMock.Net.Testcontainers;
public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContainerBuilder, WireMockContainer, WireMockConfiguration>
{
private const string DefaultLogger = "WireMockConsoleLogger";
private readonly Dictionary<OSPlatform, ContainerInfo> _info = new()
{
{ OSPlatform.Linux, new ContainerInfo("sheyenrath/wiremock.net-alpine", "/app/__admin/mappings") },
{ OSPlatform.Windows, new ContainerInfo("sheyenrath/wiremock.net-windows", @"c:\app\__admin\mappings") }
};
private readonly Lazy<Task<OSPlatform>> _getOSAsLazy = new(async () =>
{
if (TestcontainersSettings.OS.DockerEndpointAuthConfig == null)
{
throw new InvalidOperationException($"The {nameof(TestcontainersSettings.OS.DockerEndpointAuthConfig)} is null. Check if Docker is started.");
}
using var dockerClientConfig = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration();
using var dockerClient = dockerClientConfig.CreateClient();
var version = await dockerClient.System.GetVersionAsync();
return version.Os.IndexOf("Windows", StringComparison.OrdinalIgnoreCase) >= 0 ? OSPlatform.Windows : OSPlatform.Linux;
});
private OSPlatform? _imageOS;
/// <summary>
@@ -58,7 +36,7 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
[PublicAPI]
public WireMockContainerBuilder WithImage()
{
_imageOS ??= _getOSAsLazy.Value.GetAwaiter().GetResult();
_imageOS ??= ContainerUtils.GetImageOSAsync.Value.GetAwaiter().GetResult();
return WithImage(_imageOS.Value);
}
@@ -130,7 +108,9 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
[PublicAPI]
public WireMockContainerBuilder WithWatchStaticMappings(bool includeSubDirectories)
{
return WithCommand("--WatchStaticMappings true").WithCommand($"--WatchStaticMappingsInSubdirectories {includeSubDirectories}");
return Merge(DockerResourceConfiguration, DockerResourceConfiguration.WithWatchStaticMappings(includeSubDirectories))
.WithCommand("--WatchStaticMappings true")
.WithCommand($"--WatchStaticMappingsInSubdirectories {includeSubDirectories}");
}
/// <summary>
@@ -181,7 +161,7 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
if (!string.IsNullOrEmpty(builder.DockerResourceConfiguration.StaticMappingsPath))
{
builder = builder.WithBindMount(builder.DockerResourceConfiguration.StaticMappingsPath, _info[_imageOS.Value].MappingsPath);
builder = builder.WithBindMount(builder.DockerResourceConfiguration.StaticMappingsPath, ContainerInfoProvider.Info[_imageOS.Value].MappingsPath);
}
builder.Validate();
@@ -198,7 +178,7 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
return builder
.WithPortBinding(WireMockContainer.ContainerPort, true)
.WithCommand($"--WireMockLogger {DefaultLogger}")
.WithWaitStrategy(waitForContainerOS.UntilMessageIsLogged("By Stef Heyenrath"));
.WithWaitStrategy(waitForContainerOS.UntilMessageIsLogged("WireMock.Net server running"));
}
/// <inheritdoc />
@@ -222,6 +202,6 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
private WireMockContainerBuilder WithImage(OSPlatform os)
{
_imageOS = os;
return WithImage(_info[os].Image);
return WithImage(ContainerInfoProvider.Info[os].Image);
}
}