Skip to content

Commit f7f3c49

Browse files
authored
feat: Add support for copying directories and files to a container (#913)
1 parent de410de commit f7f3c49

File tree

27 files changed

+611
-48
lines changed

27 files changed

+611
-48
lines changed

src/Testcontainers.Kafka/KafkaBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ protected override KafkaBuilder Init()
8484
startupScript.Append("echo '' > /etc/confluent/docker/ensure");
8585
startupScript.Append(lf);
8686
startupScript.Append("/etc/confluent/docker/run");
87-
return container.CopyFileAsync(StartupScriptFilePath, Encoding.Default.GetBytes(startupScript.ToString()), 493, ct: ct);
87+
return container.CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), StartupScriptFilePath, Unix.FileMode755, ct);
8888
});
8989
}
9090

src/Testcontainers.MariaDb/MariaDbContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
4242
{
4343
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());
4444

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

4848
return await ExecAsync(new[] { "mysql", "--protocol=TCP", $"--port={MariaDbBuilder.MariaDbPort}", $"--user={_configuration.Username}", $"--password={_configuration.Password}", _configuration.Database, $"--execute=source {scriptFilePath};" }, ct)

src/Testcontainers.MongoDb/MongoDbContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
4040
{
4141
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());
4242

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

4646
return await ExecAsync(new MongoDbShellCommand($"load('{scriptFilePath}')", _configuration.Username, _configuration.Password), ct)

src/Testcontainers.MsSql/MsSqlContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
4242
{
4343
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());
4444

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

4848
return await ExecAsync(new[] { "/opt/mssql-tools/bin/sqlcmd", "-b", "-r", "1", "-U", _configuration.Username, "-P", _configuration.Password, "-i", scriptFilePath }, ct)

src/Testcontainers.MySql/MySqlContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
4242
{
4343
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());
4444

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

4848
return await ExecAsync(new[] { "mysql", "--protocol=TCP", $"--port={MySqlBuilder.MySqlPort}", $"--user={_configuration.Username}", $"--password={_configuration.Password}", _configuration.Database, $"--execute=source {scriptFilePath};" }, ct)

src/Testcontainers.Oracle/OracleContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
3737
{
3838
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());
3939

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

4343
return await ExecAsync(new[] { "/bin/sh", "-c", $"exit | sqlplus -LOGON -SILENT {_configuration.Username}/{_configuration.Password}@localhost:1521/{_configuration.Database} @{scriptFilePath}" }, ct)

src/Testcontainers.PostgreSql/PostgreSqlContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
4242
{
4343
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());
4444

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

4848
return await ExecAsync(new[] { "psql", "--username", _configuration.Username, "--dbname", _configuration.Database, "--file", scriptFilePath }, ct)

src/Testcontainers.Redis/RedisContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation
3333
{
3434
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());
3535

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

3939
return await ExecAsync(new[] { "redis-cli", "--eval", scriptFilePath, "0" }, ct)

src/Testcontainers.Redpanda/RedpandaBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected override RedpandaBuilder Init()
6161
startupScript.Append("--mode dev-container ");
6262
startupScript.Append("--kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 ");
6363
startupScript.Append("--advertise-kafka-addr PLAINTEXT://127.0.0.1:29092,OUTSIDE://" + container.Hostname + ":" + container.GetMappedPublicPort(RedpandaPort));
64-
return container.CopyFileAsync(StartupScriptFilePath, Encoding.Default.GetBytes(startupScript.ToString()), 493, ct: ct);
64+
return container.CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), StartupScriptFilePath, Unix.FileMode755, ct);
6565
});
6666
}
6767

src/Testcontainers/Clients/ITestcontainersClient.cs

+32
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Clients
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.IO;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Docker.DotNet.Models;
@@ -103,6 +104,37 @@ internal interface ITestcontainersClient
103104
/// <returns>Task that completes when the shell command has been executed.</returns>
104105
Task<ExecResult> ExecAsync(string id, IList<string> command, CancellationToken ct = default);
105106

107+
/// <summary>
108+
/// Copies the content of an implementation of <see cref="IResourceMapping" /> to the container.
109+
/// </summary>
110+
/// <param name="id">The container id.</param>
111+
/// <param name="resourceMapping">The resource mapping to add to the archive.</param>
112+
/// <param name="ct">Cancellation token.</param>
113+
/// <returns>A task that completes when the content has been copied.</returns>
114+
Task CopyAsync(string id, IResourceMapping resourceMapping, CancellationToken ct = default);
115+
116+
/// <summary>
117+
/// Copies a test host directory to the container.
118+
/// </summary>
119+
/// <param name="id">The container id.</param>
120+
/// <param name="source">The source directory to be copied.</param>
121+
/// <param name="target">The target directory path to copy the files to.</param>
122+
/// <param name="fileMode">The POSIX file mode permission.</param>
123+
/// <param name="ct">Cancellation token.</param>
124+
/// <returns>A task that completes when the directory has been copied.</returns>
125+
Task CopyAsync(string id, DirectoryInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default);
126+
127+
/// <summary>
128+
/// Copies a test host file to the container.
129+
/// </summary>
130+
/// <param name="id">The container id.</param>
131+
/// <param name="source">The source file to be copied.</param>
132+
/// <param name="target">The target directory path to copy the file to.</param>
133+
/// <param name="fileMode">The POSIX file mode permission.</param>
134+
/// <param name="ct">Cancellation token.</param>
135+
/// <returns>A task that completes when the file has been copied.</returns>
136+
Task CopyAsync(string id, FileInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default);
137+
106138
/// <summary>
107139
/// Copies a file to the container.
108140
/// </summary>

src/Testcontainers/Clients/TestcontainersClient.cs

+55-18
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal sealed class TestcontainersClient : ITestcontainersClient
2626

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

29-
private readonly string _osRootDirectory = Path.GetPathRoot(Directory.GetCurrentDirectory());
29+
private static readonly string OSRootDirectory = Path.GetPathRoot(Directory.GetCurrentDirectory());
3030

3131
private readonly DockerRegistryAuthenticationProvider _registryAuthenticationProvider;
3232

@@ -79,7 +79,7 @@ private TestcontainersClient(
7979
public IDockerSystemOperations System { get; }
8080

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

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

165+
/// <inheritdoc />
166+
public async Task CopyAsync(string id, IResourceMapping resourceMapping, CancellationToken ct = default)
167+
{
168+
using (var tarOutputMemStream = new TarOutputMemoryStream())
169+
{
170+
await tarOutputMemStream.AddAsync(resourceMapping, ct)
171+
.ConfigureAwait(false);
172+
173+
tarOutputMemStream.Close();
174+
tarOutputMemStream.Seek(0, SeekOrigin.Begin);
175+
176+
await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct)
177+
.ConfigureAwait(false);
178+
}
179+
}
180+
181+
/// <inheritdoc />
182+
public async Task CopyAsync(string id, DirectoryInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default)
183+
{
184+
using (var tarOutputMemStream = new TarOutputMemoryStream(target))
185+
{
186+
await tarOutputMemStream.AddAsync(source, true, fileMode, ct)
187+
.ConfigureAwait(false);
188+
189+
tarOutputMemStream.Close();
190+
tarOutputMemStream.Seek(0, SeekOrigin.Begin);
191+
192+
await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct)
193+
.ConfigureAwait(false);
194+
}
195+
}
196+
197+
/// <inheritdoc />
198+
public async Task CopyAsync(string id, FileInfo source, string target, UnixFileMode fileMode, CancellationToken ct = default)
199+
{
200+
using (var tarOutputMemStream = new TarOutputMemoryStream(target))
201+
{
202+
await tarOutputMemStream.AddAsync(source, fileMode, ct)
203+
.ConfigureAwait(false);
204+
205+
tarOutputMemStream.Close();
206+
tarOutputMemStream.Seek(0, SeekOrigin.Begin);
207+
208+
await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct)
209+
.ConfigureAwait(false);
210+
}
211+
}
212+
165213
/// <inheritdoc />
166214
public async Task CopyFileAsync(string id, string filePath, byte[] fileContent, int accessMode, int userId, int groupId, CancellationToken ct = default)
167215
{
168-
IOperatingSystem os = new Unix(dockerEndpointAuthConfig: null);
169-
var containerPath = os.NormalizePath(filePath);
216+
var containerPath = Unix.Instance.NormalizePath(filePath);
170217

171218
using (var tarOutputMemStream = new MemoryStream())
172219
{
@@ -200,7 +247,7 @@ await tarOutputStream.CloseEntryAsync(ct)
200247

201248
tarOutputMemStream.Seek(0, SeekOrigin.Begin);
202249

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

213-
IOperatingSystem os = new Unix(dockerEndpointAuthConfig: null);
214-
var containerPath = os.NormalizePath(filePath);
260+
var containerPath = Unix.Instance.NormalizePath(filePath);
215261

216262
try
217263
{
@@ -252,15 +298,6 @@ public async Task<byte[]> ReadFileAsync(string id, string filePath, Cancellation
252298
/// <inheritdoc />
253299
public async Task<string> RunAsync(IContainerConfiguration configuration, CancellationToken ct = default)
254300
{
255-
async Task CopyResourceMappingAsync(string containerId, IResourceMapping resourceMapping)
256-
{
257-
var resourceMappingContent = await resourceMapping.GetAllBytesAsync(ct)
258-
.ConfigureAwait(false);
259-
260-
await CopyFileAsync(containerId, resourceMapping.Target, resourceMappingContent, 420, 0, 0, ct)
261-
.ConfigureAwait(false);
262-
}
263-
264301
if (TestcontainersSettings.ResourceReaperEnabled && ResourceReaper.DefaultSessionId.Equals(configuration.SessionId))
265302
{
266303
var isWindowsEngineEnabled = await System.GetIsWindowsEngineEnabled(ct)
@@ -302,7 +339,7 @@ await Network.ConnectAsync("bridge", id, ct)
302339

303340
if (configuration.ResourceMappings.Any())
304341
{
305-
await Task.WhenAll(configuration.ResourceMappings.Values.Select(resourceMapping => CopyResourceMappingAsync(id, resourceMapping)))
342+
await Task.WhenAll(configuration.ResourceMappings.Values.Select(resourceMapping => CopyAsync(id, resourceMapping, ct)))
306343
.ConfigureAwait(false);
307344
}
308345

src/Testcontainers/Configurations/Unix.cs

+60
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,60 @@ namespace DotNet.Testcontainers.Configurations
1010
[PublicAPI]
1111
public sealed class Unix : IOperatingSystem
1212
{
13+
/// <summary>
14+
/// Represents the Unix file mode 644, which grants read and write permissions to the user and read permissions to the group and others.
15+
/// </summary>
16+
public const UnixFileMode FileMode644 =
17+
UnixFileMode.UserRead |
18+
UnixFileMode.UserWrite |
19+
UnixFileMode.GroupRead |
20+
UnixFileMode.OtherRead;
21+
22+
/// <summary>
23+
/// Represents the Unix file mode 666, which grants read and write permissions to the user, group, and others.
24+
/// </summary>
25+
public const UnixFileMode FileMode666 =
26+
UnixFileMode.UserRead |
27+
UnixFileMode.UserWrite |
28+
UnixFileMode.GroupRead |
29+
UnixFileMode.GroupWrite |
30+
UnixFileMode.OtherRead |
31+
UnixFileMode.OtherWrite;
32+
33+
/// <summary>
34+
/// Represents the Unix file mode 700, which grants read, write, and execute permissions to the user, and no permissions to the group and others.
35+
/// </summary>
36+
public const UnixFileMode FileMode700 =
37+
UnixFileMode.UserRead |
38+
UnixFileMode.UserWrite |
39+
UnixFileMode.UserExecute;
40+
41+
/// <summary>
42+
/// 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.
43+
/// </summary>
44+
public const UnixFileMode FileMode755 =
45+
UnixFileMode.UserRead |
46+
UnixFileMode.UserWrite |
47+
UnixFileMode.UserExecute |
48+
UnixFileMode.GroupRead |
49+
UnixFileMode.GroupExecute |
50+
UnixFileMode.OtherRead |
51+
UnixFileMode.OtherExecute;
52+
53+
/// <summary>
54+
/// Represents the Unix file mode 777, which grants read, write, and execute permissions to the user, group, and others.
55+
/// </summary>
56+
public const UnixFileMode FileMode777 =
57+
UnixFileMode.UserRead |
58+
UnixFileMode.UserWrite |
59+
UnixFileMode.UserExecute |
60+
UnixFileMode.GroupRead |
61+
UnixFileMode.GroupWrite |
62+
UnixFileMode.GroupExecute |
63+
UnixFileMode.OtherRead |
64+
UnixFileMode.OtherWrite |
65+
UnixFileMode.OtherExecute;
66+
1367
/// <summary>
1468
/// Initializes a new instance of the <see cref="Unix" /> class.
1569
/// </summary>
@@ -49,6 +103,12 @@ public Unix(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig)
49103
DockerEndpointAuthConfig = dockerEndpointAuthConfig;
50104
}
51105

106+
/// <summary>
107+
/// Gets the <see cref="IOperatingSystem" /> instance.
108+
/// </summary>
109+
public static IOperatingSystem Instance { get; }
110+
= new Unix(dockerEndpointAuthConfig: null);
111+
52112
/// <inheritdoc />
53113
public IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig { get; }
54114

0 commit comments

Comments
 (0)