301 lines
8.1 KiB
C#
301 lines
8.1 KiB
C#
|
|
using Ductus.FluentDocker.Builders;
|
|||
|
|
using Ductus.FluentDocker.Extensions;
|
|||
|
|
using Ductus.FluentDocker.Model.Common;
|
|||
|
|
using Ductus.FluentDocker.Model.Compose;
|
|||
|
|
using Ductus.FluentDocker.Services;
|
|||
|
|
using Ductus.FluentDocker.Services.Extensions;
|
|||
|
|
|
|||
|
|
namespace WebApi.Tests.System;
|
|||
|
|
|
|||
|
|
public sealed class DockerComposeFixture : IDisposable
|
|||
|
|
{
|
|||
|
|
private const string ComposeFileName = "compose.system.yaml";
|
|||
|
|
|
|||
|
|
private const string AppServiceName = "app";
|
|||
|
|
private const string AppServiceInternalPort = "8080";
|
|||
|
|
private const string AppServiceInternalPortAndProtocol = $"{AppServiceInternalPort}/tcp";
|
|||
|
|
|
|||
|
|
private const string LoginServiceName = "login";
|
|||
|
|
private const string LoginServiceInternalPort = "8080";
|
|||
|
|
private const string LoginServiceInternalPortAndProtocol = $"{LoginServiceInternalPort}/tcp";
|
|||
|
|
|
|||
|
|
private static readonly string ComposeFilePath = Path.GetFullPath(Path.Combine("../../..", ComposeFileName));
|
|||
|
|
|
|||
|
|
private readonly ICompositeService _dockerService;
|
|||
|
|
|
|||
|
|
|
|||
|
|
private bool _hasCheckedForThisContainer;
|
|||
|
|
private bool _hasCheckedIfTestRunInContainer;
|
|||
|
|
|
|||
|
|
private IHostService? _host;
|
|||
|
|
|
|||
|
|
private bool _isTestRunningInContainer;
|
|||
|
|
private INetworkService? _networkService;
|
|||
|
|
private IContainerService? _testApplicationContainer;
|
|||
|
|
|
|||
|
|
public DockerComposeFixture()
|
|||
|
|
{
|
|||
|
|
_dockerService = GetDockerComposeServices();
|
|||
|
|
_dockerService.Start();
|
|||
|
|
AttachDockerNetworksIfRunningInContainer();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private IContainerService? _appContainer;
|
|||
|
|
public IContainerService AppContainer
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
_appContainer ??= _dockerService.Containers.First(x => x.Name == AppServiceName);
|
|||
|
|
return _appContainer;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private IContainerService? _loginContainer;
|
|||
|
|
public IContainerService LoginContainer
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
_loginContainer ??= _dockerService.Containers.First(x => x.Name == LoginServiceName);
|
|||
|
|
return _loginContainer;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IContainerService? TestApplicationContainer
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
if (!_hasCheckedForThisContainer)
|
|||
|
|
{
|
|||
|
|
_testApplicationContainer = DockerHost.GetRunningContainers()
|
|||
|
|
.FirstOrDefault(x => x.Id.StartsWith(Environment.MachineName));
|
|||
|
|
|
|||
|
|
if (_testApplicationContainer is not null)
|
|||
|
|
{
|
|||
|
|
// If the test is running inside a container (i.e. usually in a pipeline), we do not want to mess with the container, just release the resources held by this program
|
|||
|
|
_testApplicationContainer.RemoveOnDispose = false;
|
|||
|
|
_testApplicationContainer.StopOnDispose = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_hasCheckedForThisContainer = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return _testApplicationContainer;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IHostService DockerHost
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
var hosts = new Hosts().Discover();
|
|||
|
|
_host = hosts.FirstOrDefault(x => x.IsNative) ??
|
|||
|
|
hosts.FirstOrDefault(x => x.Name == "default") ??
|
|||
|
|
hosts.FirstOrDefault();
|
|||
|
|
|
|||
|
|
if (_host is null) throw new InvalidOperationException("No docker host found");
|
|||
|
|
|
|||
|
|
return _host;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool IsTestRunningInContainer
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
if (!_hasCheckedIfTestRunInContainer)
|
|||
|
|
{
|
|||
|
|
_isTestRunningInContainer = TestApplicationContainer is not null;
|
|||
|
|
_hasCheckedIfTestRunInContainer = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return _isTestRunningInContainer;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
_networkService?.Dispose();
|
|||
|
|
|
|||
|
|
_testApplicationContainer?.Dispose();
|
|||
|
|
_appContainer?.Dispose();
|
|||
|
|
_loginContainer?.Dispose();
|
|||
|
|
|
|||
|
|
// Kill container because otherwise the _dockerService.Dispose() takes much longer
|
|||
|
|
KillDockerComposeServices();
|
|||
|
|
|
|||
|
|
_dockerService.Dispose();
|
|||
|
|
|
|||
|
|
_host?.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private ICompositeService GetDockerComposeServices()
|
|||
|
|
{
|
|||
|
|
var services = new Builder()
|
|||
|
|
.UseContainer()
|
|||
|
|
.UseCompose()
|
|||
|
|
.AssumeComposeVersion(ComposeVersion.V2)
|
|||
|
|
.FromFile((TemplateString)ComposeFilePath)
|
|||
|
|
.ForceBuild()
|
|||
|
|
.RemoveOrphans()
|
|||
|
|
.Wait("app", WaitForApplicationToListenToRequests)
|
|||
|
|
.Build();
|
|||
|
|
|
|||
|
|
return services;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private int WaitForApplicationToListenToRequests(IContainerService container, int iteration)
|
|||
|
|
{
|
|||
|
|
const int maxTryCount = 15;
|
|||
|
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(iteration, maxTryCount);
|
|||
|
|
|
|||
|
|
var isStarted = container.Logs().ReadToEnd().Reverse().Any(x => x.Contains("Now listening on:"));
|
|||
|
|
return isStarted ? 0 : 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void AttachDockerNetworksIfRunningInContainer()
|
|||
|
|
{
|
|||
|
|
if (!IsTestRunningInContainer) return;
|
|||
|
|
|
|||
|
|
var randomNetworkName = Guid.NewGuid().ToString("N");
|
|||
|
|
_networkService = DockerHost.CreateNetwork(randomNetworkName, removeOnDispose: true);
|
|||
|
|
|
|||
|
|
_networkService.Attach(AppContainer, true, AppServiceName);
|
|||
|
|
_networkService.Attach(TestApplicationContainer, true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public string GetAppUrl()
|
|||
|
|
{
|
|||
|
|
return IsTestRunningInContainer
|
|||
|
|
? GetAppUrlWhenRunningInsideContainer()
|
|||
|
|
: GetUrlFromOutsideContainer(AppContainer, AppServiceInternalPortAndProtocol);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string GetAppUrlWhenRunningInsideContainer()
|
|||
|
|
{
|
|||
|
|
return $"http://{AppServiceName}:{AppServiceInternalPort}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public string GetLoginUrl()
|
|||
|
|
{
|
|||
|
|
return IsTestRunningInContainer
|
|||
|
|
? GetLoginUrlWhenRunningInsideContainer()
|
|||
|
|
: GetUrlFromOutsideContainer(LoginContainer, LoginServiceInternalPortAndProtocol);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string GetLoginUrlWhenRunningInsideContainer()
|
|||
|
|
{
|
|||
|
|
return $"http://{LoginServiceName}:{LoginServiceInternalPort}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string GetUrlFromOutsideContainer(IContainerService container, string portAndProto)
|
|||
|
|
{
|
|||
|
|
var ipEndpoint = container.ToHostExposedEndpoint(portAndProto);
|
|||
|
|
return $"http://{ipEndpoint.Address}:{ipEndpoint.Port}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void KillDockerComposeServices()
|
|||
|
|
{
|
|||
|
|
foreach (var container in _dockerService.Containers) container.Remove(true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
public sealed class DockerComposeFixture2 : IDisposable
|
|||
|
|
{
|
|||
|
|
private const string ComposeFileName = "compose.system.yaml";
|
|||
|
|
|
|||
|
|
private static readonly string ComposeFilePath = Path.GetFullPath(Path.Combine("../../..", ComposeFileName));
|
|||
|
|
|
|||
|
|
private readonly ICompositeService _dockerService;
|
|||
|
|
|
|||
|
|
private IHostService? _host;
|
|||
|
|
|
|||
|
|
private INetworkService? _networkService;
|
|||
|
|
|
|||
|
|
public AppContainer AppContainer { get; init; }
|
|||
|
|
public LoginContainer LoginContainer { get; init; }
|
|||
|
|
public TestAppContainer TestApplicationContainer { get; init; }
|
|||
|
|
|
|||
|
|
public DockerComposeFixture2()
|
|||
|
|
{
|
|||
|
|
_dockerService = GetDockerComposeServices();
|
|||
|
|
_dockerService.Start();
|
|||
|
|
|
|||
|
|
TestApplicationContainer = new TestAppContainer(DockerHost);
|
|||
|
|
AppContainer = new AppContainer(_dockerService, TestApplicationContainer.IsTestRunningInContainer);
|
|||
|
|
LoginContainer = new LoginContainer(_dockerService, TestApplicationContainer.IsTestRunningInContainer);
|
|||
|
|
|
|||
|
|
AttachDockerNetworksIfRunningInContainer();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IHostService DockerHost
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
var hosts = new Hosts().Discover();
|
|||
|
|
_host = hosts.FirstOrDefault(x => x.IsNative) ??
|
|||
|
|
hosts.FirstOrDefault(x => x.Name == "default") ??
|
|||
|
|
hosts.FirstOrDefault();
|
|||
|
|
|
|||
|
|
if (_host is null) throw new InvalidOperationException("No docker host found");
|
|||
|
|
|
|||
|
|
return _host;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
_networkService?.Dispose();
|
|||
|
|
|
|||
|
|
TestApplicationContainer.Dispose();
|
|||
|
|
AppContainer.Dispose();
|
|||
|
|
LoginContainer.Dispose();
|
|||
|
|
|
|||
|
|
// Kill container because otherwise the _dockerService.Dispose() takes much longer
|
|||
|
|
KillDockerComposeServices();
|
|||
|
|
|
|||
|
|
_dockerService.Dispose();
|
|||
|
|
|
|||
|
|
_host?.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private ICompositeService GetDockerComposeServices()
|
|||
|
|
{
|
|||
|
|
var services = new Builder()
|
|||
|
|
.UseContainer()
|
|||
|
|
.UseCompose()
|
|||
|
|
.AssumeComposeVersion(ComposeVersion.V2)
|
|||
|
|
.FromFile((TemplateString)ComposeFilePath)
|
|||
|
|
.ForceBuild()
|
|||
|
|
.RemoveOrphans()
|
|||
|
|
.Wait("app", WaitForApplicationToListenToRequests)
|
|||
|
|
.Build();
|
|||
|
|
|
|||
|
|
return services;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private int WaitForApplicationToListenToRequests(IContainerService container, int iteration)
|
|||
|
|
{
|
|||
|
|
const int maxTryCount = 15;
|
|||
|
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(iteration, maxTryCount);
|
|||
|
|
|
|||
|
|
var isStarted = container.Logs().ReadToEnd().Reverse().Any(x => x.Contains("Now listening on:"));
|
|||
|
|
return isStarted ? 0 : 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void AttachDockerNetworksIfRunningInContainer()
|
|||
|
|
{
|
|||
|
|
if (!TestApplicationContainer.IsTestRunningInContainer) return;
|
|||
|
|
|
|||
|
|
var randomNetworkName = Guid.NewGuid().ToString("N");
|
|||
|
|
_networkService = DockerHost.CreateNetwork(randomNetworkName, removeOnDispose: true);
|
|||
|
|
|
|||
|
|
_networkService.Attach(AppContainer.Container!, true, AppContainer.ServiceName);
|
|||
|
|
_networkService.Attach(TestApplicationContainer.Container, true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void KillDockerComposeServices()
|
|||
|
|
{
|
|||
|
|
foreach (var container in _dockerService.Containers) container.Remove(true);
|
|||
|
|
}
|
|||
|
|
}
|