Files
vegasco/tests/WebApi.Tests.System/DockerComposeFixture.cs

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);
}
}