Skip to content
Open
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
149 changes: 129 additions & 20 deletions src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,10 @@ private async Task LoadAsync<T>(
SourceImageReference sourceReference,
DestinationImageReference destinationReference,
Func<T, SourceImageReference, DestinationImageReference, Stream, CancellationToken, Task> writeStreamFunc,
CancellationToken cancellationToken,
bool checkContainerdStore = false)
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (checkContainerdStore && !IsContainerdStoreEnabledForDocker())
{
throw new DockerLoadException(Strings.ImageLoadFailed_ContainerdStoreDisabled);
}

string commandPath = await FindFullCommandPath(cancellationToken);

// call `docker load` and get it ready to receive input
Expand Down Expand Up @@ -134,9 +128,106 @@ public async Task LoadAsync(BuiltImage image, SourceImageReference sourceReferen
// For loading to the local registry, we use the Docker format. Two reasons: one - compatibility with previous behavior before oci formatted publishing was available, two - Podman cannot load multi tag oci image tarball.
=> await LoadAsync(image, sourceReference, destinationReference, WriteDockerImageToStreamAsync, cancellationToken);

public async Task LoadAsync(MultiArchImage multiArchImage, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken)
=> await LoadAsync(multiArchImage, sourceReference, destinationReference, WriteMultiArchOciImageToStreamAsync, cancellationToken, checkContainerdStore: true);

public async Task LoadAsync(MultiArchImage multiArchImage, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken)
{
string? command = await GetCommandAsync(cancellationToken);
if (command == PodmanCommand)
{
// Podman's 'load' only keeps the image matching the host architecture from a multi-arch OCI tarball.
// Instead, we load each arch separately and assemble them using 'podman manifest'.
await LoadMultiArchImageWithPodmanAsync(multiArchImage, sourceReference, destinationReference, cancellationToken);
}
else
{
// Docker requires the containerd image store to load multi-arch OCI tarballs.
await LoadMultiArchImageWithDockerAsync(multiArchImage, sourceReference, destinationReference, cancellationToken);
}
}

private async Task LoadMultiArchImageWithDockerAsync(MultiArchImage multiArchImage, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken)
{
if (!IsContainerdStoreEnabledForDocker())
{
throw new DockerLoadException(Strings.ImageLoadFailed_ContainerdStoreDisabled);
}

await LoadAsync(multiArchImage, sourceReference, destinationReference, WriteMultiArchOciImageToStreamAsync, cancellationToken);
}

private async Task LoadMultiArchImageWithPodmanAsync(MultiArchImage multiArchImage, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

string commandPath = await FindFullCommandPath(cancellationToken);
var createdImages = new List<string>();

try
{
string firstTag = destinationReference.Tags.First();
string manifestName = $"{destinationReference.Repository}:{firstTag}";

// Create the manifest list.
// 'create' will fail if the manifest already exists.
// Note that manifests are considered a type of image, so we can use the regular 'rmi' and 'tag' commands for them.
await RunAndIgnoreAsync($"rmi {manifestName}");
await RunAsync($"manifest create {manifestName}");
createdImages.Add(manifestName);

// Load per-arch images and add them to the manifest list.
// We intentionally don't remove these because that makes the manifest unusable.
Debug.Assert(multiArchImage.Images is not null);
foreach (var image in multiArchImage.Images)
{
string repo = $"{destinationReference.Repository}-{image.Architecture}";
string tag = $"{repo}:{firstTag}";
var destRef = new DestinationImageReference(this, repo, [firstTag]);

await LoadAsync(image, sourceReference, destRef, WriteDockerImageToStreamAsync, cancellationToken);
createdImages.Add(tag);

await RunAsync($"manifest add {manifestName} {tag}");
}

// Tag the manifest list for any additional tags.
foreach (string tag in destinationReference.Tags.Skip(1))
{
string additionalTag = $"{destinationReference.Repository}:{tag}";

await RunAsync($"tag {manifestName} {additionalTag}");
createdImages.Add(additionalTag);
}
}
catch
{
foreach (string image in createdImages)
{
await RunAndIgnoreAsync($"rmi {image}");
}

throw;
}

async Task RunAsync(string arguments)
{
(int exitCode, string stderr) = await RunProcessAsync(commandPath, arguments, cancellationToken);

if (exitCode != 0)
{
throw new DockerLoadException(Resource.FormatString(nameof(Strings.ImageLoadFailed), stderr));
}
}

async Task RunAndIgnoreAsync(string arguments)
{
try
{
_ = await RunProcessAsync(commandPath, arguments, CancellationToken.None);
}
catch
{ }
}
}

public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
{
bool commandPathWasUnknown = _command is null; // avoid running the version command twice.
Expand Down Expand Up @@ -514,7 +605,8 @@ public static async Task WriteMultiArchOciImageToStreamAsync(

using TarWriter writer = new(imageStream, TarEntryFormat.Pax, leaveOpen: true);

foreach (var image in multiArchImage.Images!)
Debug.Assert(multiArchImage.Images is not null);
foreach (var image in multiArchImage.Images)
{
await WriteOciImageToBlobs(writer, image, sourceReference, cancellationToken)
.ConfigureAwait(false);
Expand Down Expand Up @@ -655,18 +747,12 @@ internal static bool IsContainerdStoreEnabledForDocker()
}
}

private async Task<bool> TryRunVersionCommandAsync(string command, CancellationToken cancellationToken)
private static async Task<bool> TryRunVersionCommandAsync(string command, CancellationToken cancellationToken)
{
try
{
ProcessStartInfo psi = new(command, "version")
{
RedirectStandardOutput = true,
RedirectStandardError = true
};
using var process = Process.Start(psi)!;
await process.WaitForExitAsync(cancellationToken);
return process.ExitCode == 0;
(int ExitCode, string StandardError) = await RunProcessAsync(command, "version", cancellationToken);
return ExitCode == 0;
}
catch (OperationCanceledException)
{
Expand All @@ -677,6 +763,29 @@ private async Task<bool> TryRunVersionCommandAsync(string command, CancellationT
return false;
}
}

private static async Task<(int ExitCode, string StandardError)> RunProcessAsync(string commandPath, string arguments, CancellationToken cancellationToken)
{
ProcessStartInfo psi = new(commandPath, arguments)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true
};

using Process process = Process.Start(psi) ??
throw new NotImplementedException(Resource.FormatString(Strings.ContainerRuntimeProcessCreationFailed, commandPath));

// Close standard input and ignore standard output.
process.StandardInput.Close();
_ = process.StandardOutput.ReadToEndAsync(cancellationToken)
.ContinueWith(t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

string stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);

return (process.ExitCode, stderr);
}
#endif

public override string ToString()
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,6 @@
<value>Cannot create image index because at least one of the provided images' config is missing 'os'.</value>
<comment/>
</data>
<data name="ImageIndex_PodmanNotSupported" xml:space="preserve">
<value>Image index creation for Podman is not supported.</value>
<comment/>
</data>
<data name="LocalDocker_FailedToGetConfig" xml:space="preserve">
<value>Error while reading daemon config: {0}</value>
<comment>{0} is the exception message that ends with period</comment>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ internal async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (LocalRegistry == "Podman")
{
Log.LogError(Strings.ImageIndex_PodmanNotSupported);
return false;
}

using MSBuildLoggerProvider loggerProvider = new(Log);
ILoggerFactory msbuildLoggerFactory = new LoggerFactory(new[] { loggerProvider });
ILogger logger = msbuildLoggerFactory.CreateLogger<CreateImageIndex>();
Expand Down
Loading
Loading