Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Replace SharpZipLib TarArchive helper class with TarOutputStream #724

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
type: boolean

concurrency:
group: ${{ github.head_ref || github.sha }}
group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
cancel-in-progress: true

env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [ develop, master ]

concurrency:
group: ${{ github.head_ref || github.sha }}
group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
cancel-in-progress: true

jobs:
Expand Down
7 changes: 4 additions & 3 deletions src/Testcontainers/Clients/DockerImageOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,14 @@ await this.DeleteAsync(image, ct)
Labels = configuration.Labels.ToDictionary(item => item.Key, item => item.Value),
};

var dockerfileArchiveFilePath = dockerfileArchive.Tar();
var dockerfileArchiveFilePath = await dockerfileArchive.Tar(ct)
.ConfigureAwait(false);

try
{
using (var dockerfileStream = new FileStream(dockerfileArchiveFilePath, FileMode.Open, FileAccess.Read))
using (var dockerfileArchiveStream = new FileStream(dockerfileArchiveFilePath, FileMode.Open, FileAccess.Read))
{
await this.Docker.Images.BuildImageFromDockerfileAsync(buildParameters, dockerfileStream, Array.Empty<AuthConfig>(), new Dictionary<string, string>(), this.traceProgress, ct)
await this.Docker.Images.BuildImageFromDockerfileAsync(buildParameters, dockerfileArchiveStream, Array.Empty<AuthConfig>(), new Dictionary<string, string>(), this.traceProgress, ct)
.ConfigureAwait(false);

var imageHasBeenCreated = await this.ExistsWithNameAsync(image.FullName, ct)
Expand Down
16 changes: 9 additions & 7 deletions src/Testcontainers/Clients/TestcontainersClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ public async Task CopyFileAsync(string id, string filePath, byte[] fileContent,
IOperatingSystem os = new Unix(dockerEndpointAuthConfig: null);
var containerPath = os.NormalizePath(filePath);

using (var memStream = new MemoryStream())
using (var tarOutputMemStream = new MemoryStream())
{
using (var tarOutputStream = new TarOutputStream(memStream, Encoding.Default))
using (var tarOutputStream = new TarOutputStream(tarOutputMemStream, Encoding.Default))
{
tarOutputStream.IsStreamOwner = false;

Expand All @@ -201,22 +201,24 @@ public async Task CopyFileAsync(string id, string filePath, byte[] fileContent,

var entry = new TarEntry(header);

tarOutputStream.PutNextEntry(entry);
await tarOutputStream.PutNextEntryAsync(entry, ct)
.ConfigureAwait(false);

#if NETSTANDARD2_1_OR_GREATER
await tarOutputStream.WriteAsync(fileContent.AsMemory(0, fileContent.Length), ct)
await tarOutputStream.WriteAsync(fileContent, ct)
.ConfigureAwait(false);
#else
await tarOutputStream.WriteAsync(fileContent, 0, fileContent.Length, ct)
.ConfigureAwait(false);
#endif

tarOutputStream.CloseEntry();
await tarOutputStream.CloseEntryAsync(ct)
.ConfigureAwait(false);
}

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

await this.containers.ExtractArchiveToContainerAsync(id, Path.AltDirectorySeparatorChar.ToString(), memStream, ct)
await this.containers.ExtractArchiveToContainerAsync(id, Path.AltDirectorySeparatorChar.ToString(), tarOutputMemStream, ct)
.ConfigureAwait(false);
}
}
Expand Down
35 changes: 22 additions & 13 deletions src/Testcontainers/Images/DockerIgnoreFile.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace DotNet.Testcontainers.Images
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -13,35 +14,43 @@ internal sealed class DockerIgnoreFile : IgnoreFile
/// <summary>
/// Initializes a new instance of the <see cref="DockerIgnoreFile" /> class.
/// </summary>
/// <param name="dockerIgnoreFileDirectory">Directory that contains all docker configuration files.</param>
/// <param name="dockerIgnoreFile">.dockerignore file name.</param>
/// <param name="dockerignoreFileDirectory">Directory that contains all docker configuration files.</param>
/// <param name="dockerignoreFile">.dockerignore file name.</param>
/// <param name="dockerfileFile">Dockerfile file name.</param>
/// <param name="logger">The logger.</param>
public DockerIgnoreFile(string dockerIgnoreFileDirectory, string dockerIgnoreFile, string dockerfileFile, ILogger logger)
: this(new DirectoryInfo(dockerIgnoreFileDirectory), dockerIgnoreFile, dockerfileFile, logger)
public DockerIgnoreFile(string dockerignoreFileDirectory, string dockerignoreFile, string dockerfileFile, ILogger logger)
: this(new DirectoryInfo(dockerignoreFileDirectory), dockerignoreFile, dockerfileFile, logger)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerIgnoreFile" /> class.
/// </summary>
/// <param name="dockerIgnoreFileDirectory">Directory that contains all docker configuration files.</param>
/// <param name="dockerIgnoreFile">.dockerignore file name.</param>
/// <param name="dockerignoreFileDirectory">Directory that contains all docker configuration files.</param>
/// <param name="dockerignoreFile">.dockerignore file name.</param>
/// <param name="dockerfileFile">Dockerfile file name.</param>
/// <param name="logger">The logger.</param>
public DockerIgnoreFile(DirectoryInfo dockerIgnoreFileDirectory, string dockerIgnoreFile, string dockerfileFile, ILogger logger)
: base(GetPatterns(dockerIgnoreFileDirectory, dockerIgnoreFile, dockerfileFile), logger)
public DockerIgnoreFile(DirectoryInfo dockerignoreFileDirectory, string dockerignoreFile, string dockerfileFile, ILogger logger)
: base(GetPatterns(dockerignoreFileDirectory, dockerignoreFile, dockerfileFile), logger)
{
}

private static IEnumerable<string> GetPatterns(DirectoryInfo dockerIgnoreFileDirectory, string dockerIgnoreFile, string dockerfileFile)
private static IEnumerable<string> GetPatterns(DirectoryInfo dockerignoreFileDirectory, string dockerignoreFile, string dockerfileFile)
{
var dockerignoreFilePath = Path.Combine(dockerignoreFileDirectory.FullName, dockerignoreFile);

// These files are necessary and sent to the Docker daemon. The ADD and COPY instructions do not copy them to the image:
// https://docs.docker.com/engine/reference/builder/#dockerignore-file.
var negateNecessaryFiles = new[] { dockerIgnoreFile, dockerfileFile }.Select(file => "!" + file);
return dockerIgnoreFileDirectory.GetFiles(dockerIgnoreFile, SearchOption.TopDirectoryOnly).Any()
? File.ReadLines(Path.Combine(dockerIgnoreFileDirectory.FullName, dockerIgnoreFile)).Concat(negateNecessaryFiles)
: negateNecessaryFiles;
var negateNecessaryFiles = new[] { dockerignoreFile, dockerfileFile }
.Select(file => "!" + file);

var dockerignorePatterns = File.Exists(dockerignoreFilePath)
? File.ReadLines(dockerignoreFilePath)
: Array.Empty<string>();

return new[] { "**/.idea", "**/.vs" }
.Concat(dockerignorePatterns)
.Concat(negateNecessaryFiles);
}
}
}
33 changes: 21 additions & 12 deletions src/Testcontainers/Images/DockerfileArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DotNet.Testcontainers.Configurations;
using ICSharpCode.SharpZipLib.Tar;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -64,7 +67,7 @@ public DockerfileArchive(DirectoryInfo dockerfileDirectory, FileInfo dockerfile,
}

/// <inheritdoc />
public string Tar()
public async Task<string> Tar(CancellationToken ct = default)
{
var dockerfileDirectoryPath = OS.NormalizePath(this.dockerfileDirectory.FullName);

Expand All @@ -76,11 +79,11 @@ public string Tar()

var dockerIgnoreFile = new DockerIgnoreFile(dockerfileDirectoryPath, ".dockerignore", dockerfileFilePath, this.logger);

using (var stream = new FileStream(dockerfileArchiveFilePath, FileMode.Create))
using (var tarOutputFileStream = new FileStream(dockerfileArchiveFilePath, FileMode.Create, FileAccess.Write))
{
using (var tarArchive = TarArchive.CreateOutputTarArchive(stream))
using (var tarOutputStream = new TarOutputStream(tarOutputFileStream, Encoding.Default))
{
tarArchive.RootPath = dockerfileDirectoryPath;
tarOutputStream.IsStreamOwner = false;

foreach (var absoluteFilePath in GetFiles(dockerfileDirectoryPath))
{
Expand All @@ -92,21 +95,27 @@ public string Tar()
continue;
}

// SharpZipLib's WriteEntry(TarEntry, bool) writes the entry, but without bytes if the file is used by another process.
// This results in a TarException on TarArchive.Dispose(): Entry closed at '0' before the 'x' bytes specified in the header were written: https://github.com/icsharpcode/SharpZipLib/issues/819.
try
{
var fileStream = File.Open(absoluteFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
fileStream.Dispose();
using (var inputStream = new FileStream(absoluteFilePath, FileMode.Open, FileAccess.Read))
{
var entry = TarEntry.CreateTarEntry(relativeFilePath);
entry.Size = inputStream.Length;

await tarOutputStream.PutNextEntryAsync(entry, ct)
.ConfigureAwait(false);

await inputStream.CopyToAsync(tarOutputStream, 4096, ct)
.ConfigureAwait(false);

await tarOutputStream.CloseEntryAsync(ct)
.ConfigureAwait(false);
}
}
catch (IOException e)
{
throw new IOException("Cannot create Docker image tar archive.", e);
}

var tarEntry = TarEntry.CreateEntryFromFile(absoluteFilePath);
tarEntry.Name = relativeFilePath;
tarArchive.WriteEntry(tarEntry, false);
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/Testcontainers/Images/ITarArchive.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace DotNet.Testcontainers.Images
{
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// Collects files into one tar archive file.
/// </summary>
Expand All @@ -8,7 +11,8 @@ internal interface ITarArchive
/// <summary>
/// Creates a tar archive file.
/// </summary>
/// <returns>Path to the created archive file.</returns>
string Tar();
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the tar archive file has been created.</returns>
Task<string> Tar(CancellationToken ct = default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace DotNet.Testcontainers.Tests.Unit
public sealed class ImageFromDockerfileTest
{
[Fact]
public void DockerfileArchiveTar()
public async Task DockerfileArchiveTar()
{
// Given
var image = new DockerImage("testcontainers", "test", "0.1.0");
Expand All @@ -23,12 +23,15 @@ public void DockerfileArchiveTar()

var actual = new SortedSet<string>();

var dockerFileArchive = new DockerfileArchive("Assets", "Dockerfile", image, NullLogger.Instance);
var dockerfileArchive = new DockerfileArchive("Assets", "Dockerfile", image, NullLogger.Instance);

var dockerfileArchiveFilePath = await dockerfileArchive.Tar()
.ConfigureAwait(false);

// When
using (var tarOut = new FileInfo(dockerFileArchive.Tar()).OpenRead())
using (var tarOut = new FileStream(dockerfileArchiveFilePath, FileMode.Open, FileAccess.Read))
{
using (var tarIn = TarArchive.CreateInputTarArchive(tarOut, Encoding.UTF8))
using (var tarIn = TarArchive.CreateInputTarArchive(tarOut, Encoding.Default))
{
tarIn.ProgressMessageEvent += (_, entry, _) => actual.Add(entry.Name);
tarIn.ListContents();
Expand Down