From 6df4a7e593b8d58a83cf017f0c7828eb469979be Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 9 Mar 2026 10:05:40 +0100 Subject: [PATCH 1/3] Containers: support building multi-arch with podman. --- .../LocalDaemons/DockerCli.cs | 143 +++++++++++++++--- .../Resources/Strings.Designer.cs | 9 -- .../Resources/Strings.resx | 4 - .../Resources/xlf/Strings.cs.xlf | 5 - .../Resources/xlf/Strings.de.xlf | 5 - .../Resources/xlf/Strings.es.xlf | 5 - .../Resources/xlf/Strings.fr.xlf | 5 - .../Resources/xlf/Strings.it.xlf | 5 - .../Resources/xlf/Strings.ja.xlf | 5 - .../Resources/xlf/Strings.ko.xlf | 5 - .../Resources/xlf/Strings.pl.xlf | 5 - .../Resources/xlf/Strings.pt-BR.xlf | 5 - .../Resources/xlf/Strings.ru.xlf | 5 - .../Resources/xlf/Strings.tr.xlf | 5 - .../Resources/xlf/Strings.zh-Hans.xlf | 5 - .../Resources/xlf/Strings.zh-Hant.xlf | 5 - .../Tasks/CreateImageIndex.cs | 6 - .../DockerIsAvailableAndSupportsArchFact.cs | 2 +- .../DockerIsAvailableAndSupportsArchTheory.cs | 2 +- .../EndToEndTests.cs | 6 +- .../DockerAvailableUtils.cs | 2 +- 21 files changed, 130 insertions(+), 109 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs index ebc8064f8e41..8148807a49f8 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs @@ -89,16 +89,10 @@ private async Task LoadAsync( SourceImageReference sourceReference, DestinationImageReference destinationReference, Func 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 @@ -134,9 +128,103 @@ 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 firstTag = destinationReference.Tags.First(); + string manifestName = $"{destinationReference.Repository}:{firstTag}"; + + try + { + // Create the manifest list. + await RunAndIgnoreAsync($"manifest rm {manifestName}"); + await RunAsync($"manifest create {manifestName}"); + + // Load per-arch images and add them to the manifest list. + // We intentionally don't remove these because that makes the manifest unusable. + foreach (var image in multiArchImage.Images!) + { + string repo = $"{destinationReference.Repository}-{image.Architecture}"; + string tag = $"{repo}:{firstTag}"; + var destRef = new DestinationImageReference(this, repo, new[] { 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 + { + await RunAndIgnoreAsync($"manifest rm {manifestName}"); + + 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 IsAvailableAsync(CancellationToken cancellationToken) { bool commandPathWasUnknown = _command is null; // avoid running the version command twice. @@ -655,18 +743,12 @@ internal static bool IsContainerdStoreEnabledForDocker() } } - private async Task TryRunVersionCommandAsync(string command, CancellationToken cancellationToken) + private static async Task 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) { @@ -677,6 +759,29 @@ private async Task 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() diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs index bcf850cdb63b..b2ed7c8ef74c 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs @@ -456,15 +456,6 @@ internal static string ImageConfigMissingOs { } } - /// - /// Looks up a localized string similar to Image index creation for Podman is not supported.. - /// - internal static string ImageIndex_PodmanNotSupported { - get { - return ResourceManager.GetString("ImageIndex_PodmanNotSupported", resourceCulture); - } - } - /// /// Looks up a localized string similar to CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings.. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx index 05e8baba0154..314fbf55abe9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx @@ -401,10 +401,6 @@ Cannot create image index because at least one of the provided images' config is missing 'os'. - - Image index creation for Podman is not supported. - - Error while reading daemon config: {0} {0} is the exception message that ends with period diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf index d5ed687ee973..db54409eb750 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf @@ -179,11 +179,6 @@ Image s indexem {0} byla vložena do registru {1}. - - Image index creation for Podman is not supported. - Vytvoření indexu image pro Podman se nepodporuje. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Nepodařilo se načíst bitovou kopii z místního registru. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf index 9032e7b789d2..416dc6ae970d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf @@ -179,11 +179,6 @@ Bildindex „{0}“ wurde in die Registrierung „{1}“ gepusht. - - Image index creation for Podman is not supported. - Die Imageindexerstellung für Podman wird nicht unterstützt. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Fehler beim Laden des Images aus der lokalen Registrierung. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf index 164c9d447a5d..b6a1e4ebb621 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf @@ -179,11 +179,6 @@ Imagen “{0}” insertada en el registro “{1}”. - - Image index creation for Podman is not supported. - No se admite la creación de índices de imagen para Podman. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: no se pudo cargar la imagen desde el registro local. Stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf index f0d3a647de7b..2a01de82ff8d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf @@ -179,11 +179,6 @@ L’index de l’image « {0} » a été envoyé vers le registre « {1} ». - - Image index creation for Podman is not supported. - La création d’index d’image pour Podman n’est pas prise en charge. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Échec du chargement de l'image à partir du registre local. sortie standard : {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf index eaf97ac4648f..8b4a9a99f860 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf @@ -179,11 +179,6 @@ È stato eseguito il push dell'indice immagine "{0}" nel registro "{1}". - - Image index creation for Podman is not supported. - La creazione dell'indice delle immagini per Podman non è supportata. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: non è stato possibile caricare l'immagine dal registro locale. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf index 8c99589574ad..06dd904ec209 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf @@ -179,11 +179,6 @@ イメージ インデックス '{0}' をレジストリ '{1}' にプッシュしました。 - - Image index creation for Podman is not supported. - Podman のイメージ インデックスの作成はサポートされていません。 - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: ローカル レジストリからイメージを読み込めませんでした。stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf index 9a04cc60f3a9..0d232b90796d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf @@ -179,11 +179,6 @@ '{0}' 이미지 인덱스를 '{1}' 레지스트리에 푸시했습니다. - - Image index creation for Podman is not supported. - Podman에 대한 이미지 인덱스 만들기는 지원되지 않습니다. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: 로컬 레지스트리에서 이미지를 로드하지 못했습니다. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf index 801b6190de72..bb5b8b8d6260 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf @@ -179,11 +179,6 @@ Wypchnięto indeks obrazu „{0}” do rejestru „{1}”. - - Image index creation for Podman is not supported. - Tworzenie indeksu obrazu dla podmanu nie jest obsługiwane. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Nie można załadować obrazu z rejestru lokalnego. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf index 971dda801778..a6333a8837ca 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf @@ -179,11 +179,6 @@ Índice de imagem enviada por push ''{0}'' para o registro ''{1}''. - - Image index creation for Podman is not supported. - Não há suporte para a criação de índice de imagem para Podman. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: falha ao carregar a imagem do registro local. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf index 1f4ef89cfdfa..e9fed94121bf 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf @@ -179,11 +179,6 @@ Индекс изображения "{0}" отправлен в реестр "{1}". - - Image index creation for Podman is not supported. - Создание индекса образа для Podman не поддерживается. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: не удалось загрузить образ из локального реестра. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf index faad0ded04f1..b1de086df845 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf @@ -179,11 +179,6 @@ '{0}' görüntü dizini '{1}' kayıt defterine gönderildi. - - Image index creation for Podman is not supported. - Podman için görüntü dizini oluşturma desteklenmiyor. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Görüntü yerel kayıt defterinden yüklenemedi. stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf index f75697ff279f..e4ff81fbbbbb 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf @@ -179,11 +179,6 @@ 已将映像索引“{0}”推送到注册表“{1}”。 - - Image index creation for Podman is not supported. - 不支持为 Podman 创建映像索引。 - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: 未能从本地注册表加载映像。stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf index f9bdce96c863..2e8a3023c95b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf @@ -179,11 +179,6 @@ 已將影像 '{0}' 推送至登錄 '{1}'。 - - Image index creation for Podman is not supported. - 不支援建立 Podman 的映射索引。 - - CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: 無法從本機登錄載入映像。stdout: {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs index f103fe8c1246..ec8214fbf5b3 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs @@ -46,12 +46,6 @@ internal async Task 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(); diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs index ae0e76f3c153..c161245e8f00 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs @@ -11,7 +11,7 @@ public DockerIsAvailableAndSupportsArchFactAttribute(string arch, bool checkCont { base.Skip = "Skipping test because Docker is not available on this host."; } - else if (checkContainerdStoreAvailability && !DockerSupportsArchHelper.IsContainerdStoreEnabledForDocker) + else if (checkContainerdStoreAvailability && !ContainerCli.IsPodman && !DockerSupportsArchHelper.IsContainerdStoreEnabledForDocker) { base.Skip = "Skipping test because Docker daemon is not using containerd as the storage driver."; } diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs index 382e57604fab..5aaa244b6133 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs @@ -11,7 +11,7 @@ public DockerIsAvailableAndSupportsArchTheoryAttribute(string arch, bool checkCo { base.Skip = "Skipping test because Docker is not available on this host."; } - else if (checkContainerdStoreAvailability && !DockerSupportsArchHelper.IsContainerdStoreEnabledForDocker) + else if (checkContainerdStoreAvailability && !ContainerCli.IsPodman && !DockerSupportsArchHelper.IsContainerdStoreEnabledForDocker) { base.Skip = "Skipping test because Docker daemon is not using containerd as the storage driver."; } diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs index 1293348dec60..8a8678cae54e 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -1522,7 +1522,7 @@ public void EnforcesOciSchemaForMultiRIDTarballOutput() "/p:EnableSdkContainerSupport=true", "/p:ContainerArchiveOutputPath=archive.tar.gz", "-getProperty:GeneratedImageIndex", - "-getItem:GeneratedContainers", + "-getItem:GeneratedContainer", "/bl") .WithWorkingDirectory(newProjectDir.FullName) .Execute(); @@ -1531,10 +1531,10 @@ public void EnforcesOciSchemaForMultiRIDTarballOutput() publishResult.StdOut.Should().NotBeNull(); var jsonDump = JsonDocument.Parse(publishResult.StdOut); var index = JsonDocument.Parse(jsonDump.RootElement.GetProperty("Properties").GetProperty("GeneratedImageIndex").ToString()); - var containers = jsonDump.RootElement.GetProperty("Items").GetProperty("GeneratedContainers").EnumerateArray().ToArray(); + var containers = jsonDump.RootElement.GetProperty("Items").GetProperty("GeneratedContainer").EnumerateArray().ToArray(); index.RootElement.GetProperty("mediaType").GetString().Should().Be("application/vnd.oci.image.index.v1+json"); - containers.Should().HaveCount(2); + containers.Should().HaveCountGreaterThanOrEqualTo(2); foreach (var container in containers) { container.GetProperty("ManifestMediaType").GetString().Should().Be("application/vnd.oci.image.manifest.v1+json"); diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs b/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs index b4f3c899b13c..4d1cdfee4ac0 100644 --- a/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs +++ b/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs @@ -31,7 +31,7 @@ public DockerAvailableFactAttribute(bool skipPodman = false, bool checkContainer { base.Skip = "Skipping test because Docker is not available on this host."; } - else if (checkContainerdStoreAvailability && !DockerCli.IsContainerdStoreEnabledForDocker()) + else if (checkContainerdStoreAvailability && DockerCliStatus.Command != DockerCli.PodmanCommand && !DockerCli.IsContainerdStoreEnabledForDocker()) { base.Skip = "Skipping test because Docker daemon is not using containerd as the storage driver."; } From 0a1769c15a629bfd869bbc27e382ebca60dda9f2 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Tue, 17 Mar 2026 11:13:03 +0100 Subject: [PATCH 2/3] Refactorings. --- .../LocalDaemons/DockerCli.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs index 8148807a49f8..20023d1e06be 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs @@ -160,14 +160,18 @@ private async Task LoadMultiArchImageWithPodmanAsync(MultiArchImage multiArchIma string commandPath = await FindFullCommandPath(cancellationToken); var createdImages = new List(); - string firstTag = destinationReference.Tags.First(); - string manifestName = $"{destinationReference.Repository}:{firstTag}"; try { + string firstTag = destinationReference.Tags.First(); + string manifestName = $"{destinationReference.Repository}:{firstTag}"; + // Create the manifest list. - await RunAndIgnoreAsync($"manifest rm {manifestName}"); + // '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. @@ -194,8 +198,6 @@ private async Task LoadMultiArchImageWithPodmanAsync(MultiArchImage multiArchIma } catch { - await RunAndIgnoreAsync($"manifest rm {manifestName}"); - foreach (string image in createdImages) { await RunAndIgnoreAsync($"rmi {image}"); From 5c31d9cb32320c6b0b10da1e34e4ca30c2ad4179 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Thu, 2 Apr 2026 11:18:32 +0200 Subject: [PATCH 3/3] PR feedback. --- .../LocalDaemons/DockerCli.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs index 20023d1e06be..ab678bc2c068 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs @@ -175,11 +175,12 @@ private async Task LoadMultiArchImageWithPodmanAsync(MultiArchImage multiArchIma // Load per-arch images and add them to the manifest list. // We intentionally don't remove these because that makes the manifest unusable. - foreach (var image in multiArchImage.Images!) + 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, new[] { firstTag }); + var destRef = new DestinationImageReference(this, repo, [firstTag]); await LoadAsync(image, sourceReference, destRef, WriteDockerImageToStreamAsync, cancellationToken); createdImages.Add(tag); @@ -604,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);