public async Task InitializeAsync() { ImageName = Environment.GetEnvironmentVariable("QBT_IMAGE") ?? DefaultImageName; var os = Environment.GetEnvironmentVariable("QBT_OS") ?? DefaulOS; var sourceDir = Path.Combine(Utils.StartupFolder, "docker", ImageName.Replace(':', '-'), os); var env = File.ReadAllText(Path.Combine(sourceDir, "env.json")); Console.WriteLine("Test Environment:"); Console.WriteLine(env); Env = JsonConvert.DeserializeObject <Env>(env); await DownloadBinaries(); var config = new DockerClientConfiguration(new Uri("http://localhost:2375")); Client = config.CreateClient(); Console.WriteLine($"\tSearching docker image {ImageName}..."); var images = await Client.Images.ListImagesAsync( new ImagesListParameters { MatchName = ImageName }); if (!images.Any()) { Console.WriteLine("\tImage not found."); Console.WriteLine($"\tCreating image {ImageName}"); var fileName = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.tgz"); Utils.CreateTarGz(fileName, sourceDir); Stream inputStream = null; try { inputStream = File.OpenRead(fileName); var progressStream = await Client.Images.BuildImageFromDockerfileAsync( inputStream, new ImageBuildParameters { Tags = new List <string> { ImageName }, }); using (var reader = new StreamReader(progressStream)) { while (true) { var text = await reader.ReadLineAsync(); if (text == null) { break; } Console.WriteLine($"\t\t{text}"); } } } finally { Console.WriteLine($"\tFinished creating image {ImageName}."); inputStream?.Dispose(); File.Delete(fileName); } } async Task DownloadBinaries() { if (Env.Binaries?.Any() != true) { return; } Console.WriteLine("Downloading binaries..."); using (var httpClient = new HttpClient()) { foreach (var pair in Env.Binaries) { var filename = Path.Combine(sourceDir, pair.Key); if (File.Exists(filename)) { Console.WriteLine($"\t\tFile {pair.Key} already exists. Skipped downloading."); continue; } var uri = string.IsNullOrEmpty(pair.Value) ? new Uri($"https://fedarovich.blob.core.windows.net/qbittorrent-test/{ImageName.Replace(':', '-')}/{os}/{pair.Key}") : new Uri(pair.Value); Console.WriteLine($"\t\tDownloading {pair.Key} from {uri}..."); using (var inStream = await httpClient.GetStreamAsync(uri)) using (var outStream = File.OpenWrite(filename)) { await inStream.CopyToAsync(outStream); } Console.WriteLine($"\t\tDownloaded {pair.Key} to {filename}."); } } } }
/// <summary> /// Runs the docker image build command to build this image /// </summary> /// <inheritdoc /> public override async Task <string> Resolve(CancellationToken ct = default) { if (ct.IsCancellationRequested) { return(null); } if (DeleteOnExit) { ResourceReaper.RegisterImageForCleanup(ImageName, DockerClient); } _logger.LogDebug("Begin building image: {}", ImageName); var tempTarPath = Path.Combine(Path.GetTempPath(), ImageName.Replace('/', '_') + ".tar"); try { using (var tempFile = new FileStream(tempTarPath, FileMode.Create)) using (var tarArchive = TarArchive.CreateOutputTarArchive(tempFile)) { if (!string.IsNullOrWhiteSpace(BasePath)) { // the algorithm here is carefully crafted to minimise the use of // Path.GetFullPath. Path.GetFullPath is used very sparingly and // completely avoided in loops. The reason is because Path.GetFullPath // is a very expensive call and can reduce CPU time by at least 1 order // of magnitude if avoided var fullBasePath = Path.GetFullPath(OS.NormalizePath(BasePath)); var ignoreFullPaths = GetIgnores(fullBasePath); // sending a full path will result in entries with full path var allFullPaths = GetAllFilesInDirectory(fullBasePath); // a thread pool that is starved can decrease the performance of // this method dramatically. Using `AsParallel()` will circumvent such issues. // as a result, methods and classes used by this needs to be thread safe. var validFullPaths = allFullPaths .AsParallel() .Where(f => !IsFileIgnored(ignoreFullPaths, f)); foreach (var fullPath in validFullPaths) { // we can safely perform a substring without expanding the paths // using Path.GetFullPath because we know fullBasePath has already been // expanded and the paths in validFullPaths are derived from fullBasePath var relativePath = fullPath.Substring(fullBasePath.Length); // if fullBasePath does not end with directory separator, // relativePath will start with directory separator and that should not be the case if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString())) { relativePath = relativePath.Substring(1); } await new MountableFile(fullPath) .TransferTo(tarArchive, relativePath, ct) .ConfigureAwait(false); } _logger.LogDebug("Transferred base path [{}] into tar archive", BasePath); } foreach (var entry in Transferables) { var destinationPath = entry.Key; var transferable = entry.Value; await transferable .TransferTo(tarArchive, destinationPath, ct) .ConfigureAwait(false); _logger.LogDebug("Transferred [{}] into tar archive", destinationPath); } tarArchive.Close(); } if (ct.IsCancellationRequested) { return(null); } var buildImageParameters = new ImageBuildParameters { Dockerfile = DockerfilePath, Labels = DeleteOnExit ? ResourceReaper.Labels : null, Tags = new List <string> { ImageName } }; using (var tempFile = new FileStream(tempTarPath, FileMode.Open)) { var output = await DockerClient.Images.BuildImageFromDockerfileAsync(tempFile, buildImageParameters, ct); using (var reader = new StreamReader(output)) { while (!reader.EndOfStream) { _logger.LogTrace(reader.ReadLine()); } } } } finally { File.Delete(tempTarPath); } _logger.LogInformation("Dockerfile image built: {}", ImageName); // we should not catch exceptions thrown by inspect because the image is // expected to be available since we've just built it var image = await DockerClient.Images.InspectImageAsync(ImageName, ct); ImageId = image.ID; return(ImageId); }