-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
436 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
src/Backend.Fx.Testing/Containers/DatabaseDockerContainer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
namespace Backend.Fx.Testing.Containers | ||
{ | ||
using System.Collections.Generic; | ||
using System.Data; | ||
using System.Globalization; | ||
using Docker.DotNet.Models; | ||
|
||
public abstract class DatabaseDockerContainer : DockerContainer | ||
{ | ||
private readonly string[] env; | ||
|
||
protected DatabaseDockerContainer(string baseImage, string name, string[] env, string dockerApiUrl) | ||
: base(baseImage, name, dockerApiUrl) | ||
{ | ||
this.env = env; | ||
} | ||
|
||
protected abstract int DatabasePort { get; } | ||
|
||
protected int LocalTcpPort { get; } = TcpPorts.GetUnused(); | ||
|
||
public abstract string ConnectionString { get; } | ||
|
||
protected override CreateContainerParameters CreateParameters | ||
{ | ||
get | ||
{ | ||
return new CreateContainerParameters | ||
{ | ||
Image = BaseImage, | ||
AttachStderr = true, | ||
AttachStdin = true, | ||
AttachStdout = true, | ||
Env = env, | ||
ExposedPorts = new Dictionary<string, EmptyStruct> { { DatabasePort.ToString(CultureInfo.InvariantCulture), new EmptyStruct() } }, | ||
HostConfig = new HostConfig | ||
{ | ||
PortBindings = new Dictionary<string, IList<PortBinding>> | ||
{ | ||
{ | ||
DatabasePort.ToString(CultureInfo.InvariantCulture), | ||
new List<PortBinding> {new PortBinding {HostPort = LocalTcpPort.ToString(CultureInfo.InvariantCulture)}} | ||
} | ||
} | ||
}, | ||
Name = Name, | ||
}; | ||
} | ||
} | ||
|
||
public abstract IDbConnection CreateConnection(); | ||
|
||
protected override void Dispose(bool disposing) | ||
{ | ||
base.Dispose(disposing); | ||
if (disposing) | ||
{ | ||
TcpPorts.Free(LocalTcpPort); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
namespace Backend.Fx.Testing.Containers | ||
{ | ||
using System; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Docker.DotNet; | ||
using Docker.DotNet.Models; | ||
using Extensions; | ||
using Fx.Logging; | ||
using JetBrains.Annotations; | ||
using Polly; | ||
|
||
/// <summary> | ||
/// An abstraction over a container running on local docker. Communication is done using the Docker API | ||
/// </summary> | ||
public abstract class DockerContainer : IDisposable | ||
{ | ||
private static readonly ILogger Logger = LogManager.Create<DockerContainer>(); | ||
|
||
protected DockerContainer([NotNull] string baseImage, string name, string dockerApiUrl, string containerId = null) | ||
{ | ||
BaseImage = baseImage ?? throw new ArgumentNullException(nameof(baseImage)); | ||
Name = name; | ||
ContainerId = containerId; | ||
Client = new DockerClientConfiguration(new Uri(dockerApiUrl)).CreateClient(); | ||
} | ||
|
||
public string BaseImage { get; } | ||
public string Name { get; private set; } | ||
|
||
public string ContainerId { get; private set; } | ||
|
||
protected abstract CreateContainerParameters CreateParameters { get; } | ||
|
||
/// <summary> | ||
/// Return true from your implementation, when the container is running and can be used by clients | ||
/// </summary> | ||
/// <returns></returns> | ||
public abstract bool HealthCheck(); | ||
|
||
public bool WaitUntilIsHealthy(int retries = 10) | ||
{ | ||
return Policy | ||
.HandleResult<bool>(r => !r) | ||
.WaitAndRetry(retries, | ||
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), | ||
(result, span) => { | ||
Logger.Info(result.Result | ||
? $"Container {ContainerId} is healthy" | ||
: $"Container {ContainerId} not yet healthy"); | ||
}) | ||
.Execute(HealthCheck); | ||
} | ||
|
||
protected DockerClient Client { get; } | ||
|
||
/// <summary> | ||
/// Creates a container from the base image and starts it | ||
/// </summary> | ||
/// <returns></returns> | ||
public async Task CreateAndStart() | ||
{ | ||
if (ContainerId != null) | ||
{ | ||
throw new InvalidOperationException($"Container {ContainerId} has been created before."); | ||
} | ||
|
||
Logger.Info($"Creating container from base image {BaseImage}"); | ||
CreateContainerResponse response = await Client.Containers.CreateContainerAsync(CreateParameters); | ||
if (Name == null) | ||
{ | ||
var inspect = await Client.Containers.InspectContainerAsync(ContainerId); | ||
Name = inspect.Name; | ||
} | ||
ContainerId = response.ID; | ||
Logger.Info($"Container {ContainerId} successfully created"); | ||
|
||
Logger.Info($"Starting container {ContainerId}"); | ||
bool isStarted = await Client.Containers.StartContainerAsync(ContainerId, new ContainerStartParameters()); | ||
if(!isStarted) | ||
{ | ||
throw new Exception($"Starting container {ContainerId} failed"); | ||
} | ||
Logger.Info($"Container {ContainerId} was started successfully"); | ||
} | ||
|
||
/// <summary> | ||
/// Kills and removes the container | ||
/// </summary> | ||
/// <param name="disposing"></param> | ||
protected virtual void Dispose(bool disposing) | ||
{ | ||
if (disposing) | ||
{ | ||
if (ContainerId != null) | ||
{ | ||
Logger.Info($"Stopping container {ContainerId}"); | ||
AsyncHelper.RunSync(() => Client.Containers.KillContainerAsync(ContainerId, new ContainerKillParameters())); | ||
Logger.Info($"Container {ContainerId} was stopped successfully"); | ||
|
||
Logger.Info($"Removing container {ContainerId}"); | ||
AsyncHelper.RunSync(() => Client.Containers.RemoveContainerAsync(ContainerId, new ContainerRemoveParameters())); | ||
Logger.Info($"Container {ContainerId} was removed successfully"); | ||
} | ||
|
||
Client?.Dispose(); | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
Dispose(true); | ||
GC.SuppressFinalize(this); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
namespace Backend.Fx.Testing.Containers | ||
{ | ||
using System; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Docker.DotNet; | ||
using Docker.DotNet.Models; | ||
using Fx.Logging; | ||
|
||
public class DockerUtilities | ||
{ | ||
private static readonly ILogger Logger = LogManager.Create<DockerUtilities>(); | ||
|
||
public static async Task KillAllOlderThan(string dockerApiUri, TimeSpan maxAge) | ||
{ | ||
using (var client = new DockerClientConfiguration(new Uri(dockerApiUri)).CreateClient()) | ||
{ | ||
var list = await client.Containers.ListContainersAsync(new ContainersListParameters{ }); | ||
var tooOldContainers = list.Where(cnt => cnt.Created + maxAge < DateTime.UtcNow); | ||
foreach (var tooOldContainer in tooOldContainers) | ||
{ | ||
Logger.Warn($"Killing container {tooOldContainer.ID}"); | ||
await client.Containers.KillContainerAsync(tooOldContainer.ID, new ContainerKillParameters()); | ||
} | ||
} | ||
} | ||
|
||
public static async Task<string> DetectDockerClientApi(params string[] urisToDetect) | ||
{ | ||
Logger.Info("Detecting local machine's docker API url"); | ||
urisToDetect = new[] { | ||
"npipe://./pipe/docker_engine", | ||
"http://localhost:2376", | ||
"http://localhost:2375" | ||
} | ||
.Concat(urisToDetect) | ||
.Distinct() | ||
.ToArray(); | ||
|
||
foreach (var uriToDetect in urisToDetect) | ||
{ | ||
string uri = await DetectDockerClientApi(uriToDetect); | ||
if (uri != null) | ||
{ | ||
return uri; | ||
} | ||
} | ||
|
||
Logger.Warn("No Docker API detected"); | ||
return null; | ||
} | ||
|
||
private static async Task<string> DetectDockerClientApi(string uriToDetect) | ||
{ | ||
try | ||
{ | ||
Logger.Info($"Trying {uriToDetect}"); | ||
VersionResponse version; | ||
using (var client = new DockerClientConfiguration(new Uri(uriToDetect)).CreateClient()) | ||
{ | ||
version = await client.System.GetVersionAsync(); | ||
} | ||
|
||
Logger.Info($"Docker API version {version.APIVersion} detected at {uriToDetect}"); | ||
return uriToDetect; | ||
} | ||
catch (Exception ex) | ||
{ | ||
Logger.Debug(ex, $"Check for Docker API at {uriToDetect} failed"); | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
src/Backend.Fx.Testing/Containers/MssqlDockerContainer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
namespace Backend.Fx.Testing.Containers | ||
{ | ||
using System; | ||
using System.Data; | ||
using System.IO; | ||
using System.Threading.Tasks; | ||
using Docker.DotNet.Models; | ||
using ICSharpCode.SharpZipLib.GZip; | ||
using ICSharpCode.SharpZipLib.Tar; | ||
using JetBrains.Annotations; | ||
|
||
public abstract class MssqlDockerContainer : DatabaseDockerContainer | ||
{ | ||
private readonly string saPassword; | ||
|
||
protected MssqlDockerContainer(string dockerApiUrl, [NotNull] string saPassword, string name = null, string baseImage = null) | ||
: base(baseImage ?? "microsoft/mssql-server-linux:latest", name, new[] { $"SA_PASSWORD={saPassword}","ACCEPT_EULA=Y", "MSSQL_PID=Developer" }, dockerApiUrl) | ||
{ | ||
this.saPassword = saPassword ?? throw new ArgumentNullException(nameof(saPassword)); | ||
} | ||
|
||
protected override int DatabasePort { get; } = 1433; | ||
|
||
public override string ConnectionString | ||
{ | ||
get | ||
{ | ||
return $"Server=localhost,{LocalTcpPort};User=sa;Password={saPassword};"; | ||
} | ||
} | ||
|
||
public override bool HealthCheck() | ||
{ | ||
try | ||
{ | ||
using (IDbConnection conn = CreateConnection()) | ||
{ | ||
conn.Open(); | ||
var cmd = conn.CreateCommand(); | ||
cmd.CommandText = "SELECT 1"; | ||
cmd.ExecuteScalar(); | ||
return true; | ||
} | ||
} | ||
catch (Exception) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
public async Task Restore(string bakFilePath, string dbName) | ||
{ | ||
string targetPath = "/var/tmp"; | ||
|
||
// the only possibility to copy something into the container is the method ExtractArchiveToContainer | ||
// so we have to provide the backup as tar/gzipped stream | ||
using (var stream = CreateTarGz(bakFilePath)) | ||
{ | ||
var parameters = new ContainerPathStatParameters { Path = targetPath, AllowOverwriteDirWithFile = true }; | ||
await Client.Containers.ExtractArchiveToContainerAsync(ContainerId, parameters, stream); | ||
} | ||
|
||
using (IDbConnection connection = CreateConnection()) | ||
{ | ||
connection.Open(); | ||
|
||
using (var restoreCommand = connection.CreateCommand()) | ||
{ | ||
var restoreCommandCommandText | ||
= $"RESTORE DATABASE [{dbName}] FROM DISK = N'{targetPath}/{Path.GetFileName(bakFilePath)}' " + | ||
"WITH FILE = 1, " + | ||
$"MOVE N'mep-prod-sql_Data' TO N'/var/opt/mssql/data/{dbName}_data.mdf', " + | ||
$"MOVE N'mep-prod-sql_Log' TO N'/var/opt/mssql/data/{dbName}_log.ldf', " + | ||
"NOUNLOAD, REPLACE "; | ||
|
||
restoreCommand.CommandText = restoreCommandCommandText; | ||
restoreCommand.ExecuteNonQuery(); | ||
} | ||
} | ||
} | ||
|
||
private Stream CreateTarGz(string sourceFile) | ||
{ | ||
|
||
Stream outStream = new MemoryStream(); | ||
using (Stream gzoStream = new GZipOutputStream(outStream)) | ||
{ | ||
using (TarArchive tarArchive = TarArchive.CreateOutputTarArchive(gzoStream)) | ||
{ | ||
TarEntry tarEntry = TarEntry.CreateEntryFromFile(sourceFile); | ||
tarArchive.WriteEntry(tarEntry, true); | ||
tarArchive.Close(); | ||
} | ||
} | ||
|
||
outStream.Seek(0, SeekOrigin.Begin); | ||
return outStream; | ||
} | ||
} | ||
} |
Oops, something went wrong.