Skip to content

Commit

Permalink
feat: Add support for copying directories and files to a container (t…
Browse files Browse the repository at this point in the history
  • Loading branch information
HofmeisterAn committed Jun 19, 2023
1 parent de410de commit f7f3c49
Show file tree
Hide file tree
Showing 27 changed files with 611 additions and 48 deletions.
2 changes: 1 addition & 1 deletion src/Testcontainers.Kafka/KafkaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ protected override KafkaBuilder Init()
startupScript.Append("echo '' > /etc/confluent/docker/ensure");
startupScript.Append(lf);
startupScript.Append("/etc/confluent/docker/run");
return container.CopyFileAsync(StartupScriptFilePath, Encoding.Default.GetBytes(startupScript.ToString()), 493, ct: ct);
return container.CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), StartupScriptFilePath, Unix.FileMode755, ct);
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.MariaDb/MariaDbContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await CopyFileAsync(scriptFilePath, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct)
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new[] { "mysql", "--protocol=TCP", $"--port={MariaDbBuilder.MariaDbPort}", $"--user={_configuration.Username}", $"--password={_configuration.Password}", _configuration.Database, $"--execute=source {scriptFilePath};" }, ct)
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.MongoDb/MongoDbContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await CopyFileAsync(scriptFilePath, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct)
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new MongoDbShellCommand($"load('{scriptFilePath}')", _configuration.Username, _configuration.Password), ct)
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.MsSql/MsSqlContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await CopyFileAsync(scriptFilePath, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct)
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new[] { "/opt/mssql-tools/bin/sqlcmd", "-b", "-r", "1", "-U", _configuration.Username, "-P", _configuration.Password, "-i", scriptFilePath }, ct)
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.MySql/MySqlContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await CopyFileAsync(scriptFilePath, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct)
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new[] { "mysql", "--protocol=TCP", $"--port={MySqlBuilder.MySqlPort}", $"--user={_configuration.Username}", $"--password={_configuration.Password}", _configuration.Database, $"--execute=source {scriptFilePath};" }, ct)
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.Oracle/OracleContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await CopyFileAsync(scriptFilePath, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct)
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new[] { "/bin/sh", "-c", $"exit | sqlplus -LOGON -SILENT {_configuration.Username}/{_configuration.Password}@localhost:1521/{_configuration.Database} @{scriptFilePath}" }, ct)
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.PostgreSql/PostgreSqlContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await CopyFileAsync(scriptFilePath, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct)
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new[] { "psql", "--username", _configuration.Username, "--dbname", _configuration.Database, "--file", scriptFilePath }, ct)
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.Redis/RedisContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await CopyFileAsync(scriptFilePath, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct)
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new[] { "redis-cli", "--eval", scriptFilePath, "0" }, ct)
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.Redpanda/RedpandaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected override RedpandaBuilder Init()
startupScript.Append("--mode dev-container ");
startupScript.Append("--kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 ");
startupScript.Append("--advertise-kafka-addr PLAINTEXT://127.0.0.1:29092,OUTSIDE://" + container.Hostname + ":" + container.GetMappedPublicPort(RedpandaPort));
return container.CopyFileAsync(StartupScriptFilePath, Encoding.Default.GetBytes(startupScript.ToString()), 493, ct: ct);
return container.CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), StartupScriptFilePath, Unix.FileMode755, ct);
});
}

Expand Down
32 changes: 32 additions & 0 deletions src/Testcontainers/Clients/ITestcontainersClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Clients
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet.Models;
Expand Down Expand Up @@ -103,6 +104,37 @@ internal interface ITestcontainersClient
/// <returns>Task that completes when the shell command has been executed.</returns>
Task<ExecResult> ExecAsync(string id, IList<string> command, CancellationToken ct = default);

/// <summary>
/// Copies the content of an implementation of <see cref="IResourceMapping" /> to the container.
/// </summary>
/// <param name="id">The container id.</param>
/// <param name="resourceMapping">The resource mapping to add to the archive.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that completes when the content has been copied.</returns>
Task CopyAsync(string id, IResourceMapping resourceMapping, CancellationToken ct = default);

/// <summary>
/// Copies a test host directory to the container.
/// </summary>
/// <param name="id">The container id.</param>
/// <param name="source">The source directory 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 has been copied.</returns>
Task CopyAsync(string id, DirectoryInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default);

/// <summary>
/// Copies a test host file to the container.
/// </summary>
/// <param name="id">The container id.</param>
/// <param name="source">The source file to be copied.</param>
/// <param name="target">The target directory path to copy the file to.</param>
/// <param name="fileMode">The POSIX file mode permission.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that completes when the file has been copied.</returns>
Task CopyAsync(string id, FileInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default);

/// <summary>
/// Copies a file to the container.
/// </summary>
Expand Down
73 changes: 55 additions & 18 deletions src/Testcontainers/Clients/TestcontainersClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal sealed class TestcontainersClient : ITestcontainersClient

public const string TestcontainersSessionIdLabel = TestcontainersLabel + ".session-id";

private readonly string _osRootDirectory = Path.GetPathRoot(Directory.GetCurrentDirectory());
private static readonly string OSRootDirectory = Path.GetPathRoot(Directory.GetCurrentDirectory());

private readonly DockerRegistryAuthenticationProvider _registryAuthenticationProvider;

Expand Down Expand Up @@ -79,7 +79,7 @@ private TestcontainersClient(
public IDockerSystemOperations System { get; }

/// <inheritdoc />
public bool IsRunningInsideDocker => File.Exists(Path.Combine(_osRootDirectory, ".dockerenv"));
public bool IsRunningInsideDocker => File.Exists(Path.Combine(OSRootDirectory, ".dockerenv"));

/// <inheritdoc />
public Task<long> GetContainerExitCodeAsync(string id, CancellationToken ct = default)
Expand Down Expand Up @@ -147,7 +147,7 @@ await Container.RemoveAsync(id, ct)
catch (DockerApiException e)
{
// The Docker daemon may already start the progress to removes the container (AutoRemove):
// https://docs.docker.com/engine/api/v1.41/#operation/ContainerCreate.
// https://docs.docker.com/engine/api/v1.43/#operation/ContainerCreate.
if (!e.Message.Contains($"removal of container {id} is already in progress"))
{
throw;
Expand All @@ -162,11 +162,58 @@ public Task<ExecResult> ExecAsync(string id, IList<string> command, Cancellation
return Container.ExecAsync(id, command, ct);
}

/// <inheritdoc />
public async Task CopyAsync(string id, IResourceMapping resourceMapping, CancellationToken ct = default)
{
using (var tarOutputMemStream = new TarOutputMemoryStream())
{
await tarOutputMemStream.AddAsync(resourceMapping, ct)
.ConfigureAwait(false);

tarOutputMemStream.Close();
tarOutputMemStream.Seek(0, SeekOrigin.Begin);

await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct)
.ConfigureAwait(false);
}
}

/// <inheritdoc />
public async Task CopyAsync(string id, DirectoryInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default)
{
using (var tarOutputMemStream = new TarOutputMemoryStream(target))
{
await tarOutputMemStream.AddAsync(source, true, fileMode, ct)
.ConfigureAwait(false);

tarOutputMemStream.Close();
tarOutputMemStream.Seek(0, SeekOrigin.Begin);

await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct)
.ConfigureAwait(false);
}
}

/// <inheritdoc />
public async Task CopyAsync(string id, FileInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default)
{
using (var tarOutputMemStream = new TarOutputMemoryStream(target))
{
await tarOutputMemStream.AddAsync(source, fileMode, ct)
.ConfigureAwait(false);

tarOutputMemStream.Close();
tarOutputMemStream.Seek(0, SeekOrigin.Begin);

await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct)
.ConfigureAwait(false);
}
}

/// <inheritdoc />
public async Task CopyFileAsync(string id, string filePath, byte[] fileContent, int accessMode, int userId, int groupId, CancellationToken ct = default)
{
IOperatingSystem os = new Unix(dockerEndpointAuthConfig: null);
var containerPath = os.NormalizePath(filePath);
var containerPath = Unix.Instance.NormalizePath(filePath);

using (var tarOutputMemStream = new MemoryStream())
{
Expand Down Expand Up @@ -200,7 +247,7 @@ await tarOutputStream.CloseEntryAsync(ct)

tarOutputMemStream.Seek(0, SeekOrigin.Begin);

await Container.ExtractArchiveToContainerAsync(id, Path.AltDirectorySeparatorChar.ToString(), tarOutputMemStream, ct)
await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct)
.ConfigureAwait(false);
}
}
Expand All @@ -210,8 +257,7 @@ public async Task<byte[]> ReadFileAsync(string id, string filePath, Cancellation
{
Stream tarStream;

IOperatingSystem os = new Unix(dockerEndpointAuthConfig: null);
var containerPath = os.NormalizePath(filePath);
var containerPath = Unix.Instance.NormalizePath(filePath);

try
{
Expand Down Expand Up @@ -252,15 +298,6 @@ public async Task<byte[]> ReadFileAsync(string id, string filePath, Cancellation
/// <inheritdoc />
public async Task<string> RunAsync(IContainerConfiguration configuration, CancellationToken ct = default)
{
async Task CopyResourceMappingAsync(string containerId, IResourceMapping resourceMapping)
{
var resourceMappingContent = await resourceMapping.GetAllBytesAsync(ct)
.ConfigureAwait(false);

await CopyFileAsync(containerId, resourceMapping.Target, resourceMappingContent, 420, 0, 0, ct)
.ConfigureAwait(false);
}

if (TestcontainersSettings.ResourceReaperEnabled && ResourceReaper.DefaultSessionId.Equals(configuration.SessionId))
{
var isWindowsEngineEnabled = await System.GetIsWindowsEngineEnabled(ct)
Expand Down Expand Up @@ -302,7 +339,7 @@ await Network.ConnectAsync("bridge", id, ct)

if (configuration.ResourceMappings.Any())
{
await Task.WhenAll(configuration.ResourceMappings.Values.Select(resourceMapping => CopyResourceMappingAsync(id, resourceMapping)))
await Task.WhenAll(configuration.ResourceMappings.Values.Select(resourceMapping => CopyAsync(id, resourceMapping, ct)))
.ConfigureAwait(false);
}

Expand Down
60 changes: 60 additions & 0 deletions src/Testcontainers/Configurations/Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,60 @@ namespace DotNet.Testcontainers.Configurations
[PublicAPI]
public sealed class Unix : IOperatingSystem
{
/// <summary>
/// Represents the Unix file mode 644, which grants read and write permissions to the user and read permissions to the group and others.
/// </summary>
public const UnixFileMode FileMode644 =
UnixFileMode.UserRead |
UnixFileMode.UserWrite |
UnixFileMode.GroupRead |
UnixFileMode.OtherRead;

/// <summary>
/// Represents the Unix file mode 666, which grants read and write permissions to the user, group, and others.
/// </summary>
public const UnixFileMode FileMode666 =
UnixFileMode.UserRead |
UnixFileMode.UserWrite |
UnixFileMode.GroupRead |
UnixFileMode.GroupWrite |
UnixFileMode.OtherRead |
UnixFileMode.OtherWrite;

/// <summary>
/// Represents the Unix file mode 700, which grants read, write, and execute permissions to the user, and no permissions to the group and others.
/// </summary>
public const UnixFileMode FileMode700 =
UnixFileMode.UserRead |
UnixFileMode.UserWrite |
UnixFileMode.UserExecute;

/// <summary>
/// Represents the Unix file mode 755, which grants read, write, and execute permissions to the user, and read and execute permissions to the group and others.
/// </summary>
public const UnixFileMode FileMode755 =
UnixFileMode.UserRead |
UnixFileMode.UserWrite |
UnixFileMode.UserExecute |
UnixFileMode.GroupRead |
UnixFileMode.GroupExecute |
UnixFileMode.OtherRead |
UnixFileMode.OtherExecute;

/// <summary>
/// Represents the Unix file mode 777, which grants read, write, and execute permissions to the user, group, and others.
/// </summary>
public const UnixFileMode FileMode777 =
UnixFileMode.UserRead |
UnixFileMode.UserWrite |
UnixFileMode.UserExecute |
UnixFileMode.GroupRead |
UnixFileMode.GroupWrite |
UnixFileMode.GroupExecute |
UnixFileMode.OtherRead |
UnixFileMode.OtherWrite |
UnixFileMode.OtherExecute;

/// <summary>
/// Initializes a new instance of the <see cref="Unix" /> class.
/// </summary>
Expand Down Expand Up @@ -49,6 +103,12 @@ public Unix(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig)
DockerEndpointAuthConfig = dockerEndpointAuthConfig;
}

/// <summary>
/// Gets the <see cref="IOperatingSystem" /> instance.
/// </summary>
public static IOperatingSystem Instance { get; }
= new Unix(dockerEndpointAuthConfig: null);

/// <inheritdoc />
public IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig { get; }

Expand Down
Loading

0 comments on commit f7f3c49

Please sign in to comment.