Skip to content

Commit

Permalink
feat: Add URL-based resource mapping container builder API (#1118)
Browse files Browse the repository at this point in the history
Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
  • Loading branch information
0xced and HofmeisterAn authored Feb 17, 2024
1 parent 5600106 commit a21b6e1
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 12 deletions.
18 changes: 18 additions & 0 deletions src/Testcontainers/Builders/ContainerBuilder`3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ public TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePat
/// <inheritdoc />
public TBuilderEntity WithResourceMapping(string source, string target, UnixFileModes fileMode = Unix.FileMode644)
{
if (Uri.IsWellFormedUriString(source, UriKind.Absolute) && Uri.TryCreate(source, UriKind.Absolute, out var uri) && new[] { Uri.UriSchemeHttp, Uri.UriSchemeHttps, Uri.UriSchemeFile }.Contains(uri.Scheme))
{
return WithResourceMapping(uri, target, fileMode);
}

var fileAttributes = File.GetAttributes(source);

if ((fileAttributes & FileAttributes.Directory) == FileAttributes.Directory)
Expand Down Expand Up @@ -234,6 +239,19 @@ public TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, Unix
}
}

/// <inheritdoc />
public TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644)
{
if (source.IsFile)
{
return WithResourceMapping(new FileResourceMapping(source.AbsolutePath, target, fileMode));
}
else
{
return WithResourceMapping(new UriResourceMapping(source, target, fileMode));
}
}

/// <inheritdoc />
public TBuilderEntity WithMount(IMount mount)
{
Expand Down
33 changes: 30 additions & 3 deletions src/Testcontainers/Builders/IContainerBuilder`2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,18 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePath, UnixFileModes fileMode = Unix.FileMode644);

/// <summary>
/// Copies a test host directory or file to the container before it starts.
/// Copies the contents of a URL, a test host directory or file to the container before it starts.
/// </summary>
/// <param name="source">The source directory or file to be copied.</param>
/// <param name="target">The target directory path to copy the files to.</param>
/// <remarks>
/// If the source corresponds to a file or the Uri scheme corresponds to a file,
/// the content is copied to the target directory path. If the Uri scheme
/// corresponds to HTTP or HTTPS, the content is copied to the target file path.
///
/// If you prefer to copy a file to a specific target file path instead of a
/// directory, use: <see cref="WithResourceMapping(FileInfo, FileInfo, UnixFileModes)" />.
/// </remarks>
/// <param name="source">The source URL, directory or file to be copied.</param>
/// <param name="target">The target directory or file path to copy the file to.</param>
/// <param name="fileMode">The POSIX file mode permission.</param>
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
[PublicAPI]
Expand Down Expand Up @@ -258,6 +266,25 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
[PublicAPI]
TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, UnixFileModes fileMode = Unix.FileMode644);

/// <summary>
/// Copies a file from a URL to the container before it starts.
/// </summary>
/// <remarks>
/// If the Uri scheme corresponds to a file, the content is copied to the target
/// directory path. If the Uri scheme corresponds to HTTP or HTTPS, the content is
/// copied to the target file path.
///
/// The Uri scheme must be either <c>http</c>, <c>https</c> or <c>file</c>.
///
/// If you prefer to copy a file to a specific target file path instead of a
/// directory, use: <see cref="WithResourceMapping(FileInfo, FileInfo, UnixFileModes)" />.
/// </remarks>
/// <param name="source">The source URL of the file to be copied.</param>
/// <param name="target">The target directory or file path to copy the file to.</param>
/// <param name="fileMode">The POSIX file mode permission.</param>
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644);

/// <summary>
/// Assigns the mount configuration to manage data in the container.
/// </summary>
Expand Down
60 changes: 60 additions & 0 deletions src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

/// <inheritdoc cref="IResourceMapping" />
internal sealed class UriResourceMapping : IResourceMapping
{
private readonly Uri _uri;

/// <summary>
/// Initializes a new instance of the <see cref="UriResourceMapping" /> class.
/// </summary>
/// <param name="uri">The URL of the file to download.</param>
/// <param name="containerPath">The absolute path of the file to map in the container.</param>
/// <param name="fileMode">The POSIX file mode permission.</param>
public UriResourceMapping(Uri uri, string containerPath, UnixFileModes fileMode)
{
_uri = uri;
Type = MountType.Bind;
Source = uri.AbsoluteUri;
Target = containerPath;
FileMode = fileMode;
AccessMode = AccessMode.ReadOnly;
}

/// <inheritdoc />
public MountType Type { get; }

/// <inheritdoc />
public AccessMode AccessMode { get; }

/// <inheritdoc />
public string Source { get; }

/// <inheritdoc />
public string Target { get; }

/// <inheritdoc />
public UnixFileModes FileMode { get; }

/// <inheritdoc />
public Task CreateAsync(CancellationToken ct = default) => Task.CompletedTask;

/// <inheritdoc />
public Task DeleteAsync(CancellationToken ct = default) => Task.CompletedTask;

/// <inheritdoc />
public async Task<byte[]> GetAllBytesAsync(CancellationToken ct = default)
{
using (var httpClient = new HttpClient())
{
return await httpClient.GetByteArrayAsync(_uri)
.ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,18 @@ public void TestFileExistsInTarFile()
}

[UsedImplicitly]
public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IAsyncLifetime, IDisposable
public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IClassFixture<FromResourceMapping.HttpFixture>, IAsyncLifetime, IDisposable
{
private readonly string _testHttpUri;

private readonly string _testFileUri;

public FromResourceMapping(FromResourceMapping.HttpFixture httpFixture)
{
_testHttpUri = httpFixture.BaseAddress;
_testFileUri = new Uri(_testFile.FullName).ToString();
}

public MountType Type
=> MountType.Bind;

Expand Down Expand Up @@ -86,31 +96,32 @@ public async Task TestFileExistsInContainer()
{
// Given
var targetFilePath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);

var targetFilePath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);

var targetFilePath3 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);
var targetDirectoryPath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());

var targetDirectoryPath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());

var targetDirectoryPath3 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());

var targetDirectoryPath4 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
var targetDirectoryPath5 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());

var targetFilePaths = new List<string>();
targetFilePaths.Add(targetFilePath1);
targetFilePaths.Add(targetFilePath2);
targetFilePaths.Add(targetFilePath3);
targetFilePaths.Add(string.Join("/", targetDirectoryPath1, _testFile.Name));
targetFilePaths.Add(string.Join("/", targetDirectoryPath2, _testFile.Name));
targetFilePaths.Add(string.Join("/", targetDirectoryPath3, _testFile.Name));
targetFilePaths.Add(string.Join("/", targetDirectoryPath4, _testFile.Name));
targetFilePaths.Add(string.Join("/", targetDirectoryPath5, _testFile.Name));

await using var container = new ContainerBuilder()
.WithImage(CommonImages.Alpine)
.WithEntrypoint(CommonCommands.SleepInfinity)
.WithResourceMapping(_testFile, new FileInfo(targetFilePath1))
.WithResourceMapping(_testFile.FullName, targetDirectoryPath1)
.WithResourceMapping(_testFile.Directory.FullName, targetDirectoryPath2)
.WithResourceMapping(_testHttpUri, targetFilePath2)
.WithResourceMapping(_testFileUri, targetDirectoryPath3)
.Build();

// When
Expand All @@ -120,13 +131,13 @@ public async Task TestFileExistsInContainer()
await container.StartAsync()
.ConfigureAwait(true);

await container.CopyAsync(fileContent, targetFilePath2)
await container.CopyAsync(fileContent, targetFilePath3)
.ConfigureAwait(true);

await container.CopyAsync(_testFile.FullName, targetDirectoryPath3)
await container.CopyAsync(_testFile.FullName, targetDirectoryPath4)
.ConfigureAwait(true);

await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath4)
await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath5)
.ConfigureAwait(true);

// Then
Expand All @@ -135,6 +146,31 @@ await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath4)

Assert.All(execResults, result => Assert.Equal(0, result.ExitCode));
}

public sealed class HttpFixture : IAsyncLifetime
{
private const ushort HttpPort = 80;

private readonly IContainer _container = new ContainerBuilder()
.WithImage(CommonImages.Alpine)
.WithEntrypoint("/bin/sh", "-c")
.WithCommand($"while true; do echo \"HTTP/1.1 200 OK\r\n\" | nc -l -p {HttpPort}; done")
.WithPortBinding(HttpPort, true)
.Build();

public string BaseAddress
=> new UriBuilder(Uri.UriSchemeHttp, _container.Hostname, _container.GetMappedPublicPort(HttpPort)).ToString();

public Task InitializeAsync()
{
return _container.StartAsync();
}

public Task DisposeAsync()
{
return _container.DisposeAsync().AsTask();
}
}
}

[UsedImplicitly]
Expand Down

0 comments on commit a21b6e1

Please sign in to comment.