Skip to content

Commit

Permalink
Merge branch 'release/3.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
marcwittke committed May 23, 2018
2 parents af12cc3 + 0da5c04 commit 2ab418e
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/Backend.Fx.Testing/Backend.Fx.Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.2" />
<PackageReference Include="FakeItEasy" Version="4.6.0" />
<PackageReference Include="Polly" Version="6.0.1" />
<PackageReference Include="SharpZipLib.NETStandard" Version="1.0.7" />
<PackageReference Include="xunit" Version="2.3.1" />
</ItemGroup>

Expand Down
62 changes: 62 additions & 0 deletions src/Backend.Fx.Testing/Containers/DatabaseDockerContainer.cs
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);
}
}
}
}
116 changes: 116 additions & 0 deletions src/Backend.Fx.Testing/Containers/DockerContainer.cs
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);
}
}
}
75 changes: 75 additions & 0 deletions src/Backend.Fx.Testing/Containers/DockerUtilities.cs
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 src/Backend.Fx.Testing/Containers/MssqlDockerContainer.cs
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;
}
}
}

0 comments on commit 2ab418e

Please sign in to comment.