From e4f5145adcb8b69fabb9f45728a7855a79af478a Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 5 Feb 2026 18:39:09 +0100 Subject: [PATCH] add out of proc com task support --- .../Microsoft.NET.Build.Containers.csproj | 1 - .../Tasks/CreateNewImage.cs | 32 +++++ .../Tasks/CreateNewImageToolTask.cs | 4 +- .../VSHostObject.cs | 111 +++++++++++++++--- 4 files changed, 128 insertions(+), 20 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj index 4708b24ea62d..4ffaf57f4a43 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj +++ b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj @@ -85,7 +85,6 @@ - diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index 777ed43ee10f..124ee9749f45 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -58,6 +58,38 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) return !Log.HasLoggedErrors; } + bool credentialsSet = false; + VSHostObject hostObj = new(HostObject, Log); + if (hostObj.TryGetCredentials() is (string userName, string pass)) + { + // Set credentials for the duration of this operation. + // These will be cleared in the finally block to minimize exposure. + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, userName); + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, pass); + credentialsSet = true; + } + else + { + Log.LogMessage(MessageImportance.Low, Resource.GetString(nameof(Strings.HostObjectNotDetected))); + } + + try + { + return await ExecuteAsyncCore(logger, msbuildLoggerFactory, cancellationToken).ConfigureAwait(false); + } + finally + { + // Clear credentials from environment to minimize exposure window. + if (credentialsSet) + { + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, null); + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, null); + } + } + } + + private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuildLoggerFactory, CancellationToken cancellationToken) + { RegistryMode sourceRegistryMode = BaseRegistry.Equals(OutputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull; Registry? sourceRegistry = IsLocalPull ? null : new Registry(BaseRegistry, logger, sourceRegistryMode); SourceImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag, BaseImageDigest); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs index 5381d2afa590..8bcec281ff38 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs @@ -63,8 +63,8 @@ private string DotNetPath /// protected override ProcessStartInfo GetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch) { - VSHostObject hostObj = new(HostObject as System.Collections.Generic.IEnumerable); - if (hostObj.ExtractCredentials(out string user, out string pass, (string s) => Log.LogWarning(s))) + VSHostObject hostObj = new(HostObject, Log); + if (hostObj.TryGetCredentials() is (string user, string pass)) { extractionInfo = (true, user, pass); } diff --git a/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs b/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs index f65843f5ae39..89ea5b16ceae 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs @@ -1,44 +1,121 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using System.Text.Json; using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; namespace Microsoft.NET.Build.Containers.Tasks; -internal sealed class VSHostObject +internal sealed class VSHostObject(ITaskHost? hostObject, TaskLoggingHelper log) { private const string CredentialItemSpecName = "MsDeployCredential"; private const string UserMetaDataName = "UserName"; private const string PasswordMetaDataName = "Password"; - IEnumerable? _hostObject; - public VSHostObject(IEnumerable? hostObject) + private readonly ITaskHost? _hostObject = hostObject; + private readonly TaskLoggingHelper _log = log; + + /// + /// Tries to extract credentials from the host object. + /// + /// A tuple of (username, password) if credentials were found with non-empty username, null otherwise. + public (string username, string password)? TryGetCredentials() { - _hostObject = hostObject; + if (_hostObject is null) + { + return null; + } + + IEnumerable? taskItems = GetTaskItems(); + if (taskItems is null) + { + _log.LogMessage(MessageImportance.Low, "No task items found in host object."); + return null; + } + + ITaskItem? credentialItem = taskItems.FirstOrDefault(p => p.ItemSpec == CredentialItemSpecName); + if (credentialItem is null) + { + return null; + } + + string username = credentialItem.GetMetadata(UserMetaDataName); + if (string.IsNullOrEmpty(username)) + { + return null; + } + + string password = credentialItem.GetMetadata(PasswordMetaDataName); + return (username, password); } - public bool ExtractCredentials(out string username, out string password, Action logMethod) + private IEnumerable? GetTaskItems() { - bool retVal = false; - username = password = string.Empty; - if (_hostObject != null) + try { - ITaskItem credentialItem = _hostObject.FirstOrDefault(p => p.ItemSpec == CredentialItemSpecName); - if (credentialItem != null) + // This call mirrors the behavior of Microsoft.WebTools.Publish.MSDeploy.VSMsDeployTaskHostObject.QueryAllTaskItems. + // Expected contract: + // - Instance method on the host object named "QueryAllTaskItems". + // - Signature: string QueryAllTaskItems(). + // - Returns a JSON array of objects with the shape: + // [{ "ItemSpec": "", "Metadata": { "": "", ... } }, ...] + // The JSON is deserialized into TaskItemDto records and converted to ITaskItem instances. + // Only UserName and Password metadata are extracted to avoid conflicts with reserved MSBuild metadata. + string? rawTaskItems = (string?)_hostObject!.GetType().InvokeMember( + "QueryAllTaskItems", + BindingFlags.InvokeMethod, + null, + _hostObject, + null); + + if (!string.IsNullOrEmpty(rawTaskItems)) { - retVal = true; - username = credentialItem.GetMetadata(UserMetaDataName); - if (!string.IsNullOrEmpty(username)) + List? dtos = JsonSerializer.Deserialize>(rawTaskItems); + if (dtos is not null && dtos.Count > 0) { - password = credentialItem.GetMetadata(PasswordMetaDataName); + _log.LogMessage(MessageImportance.Low, "Successfully retrieved task items via QueryAllTaskItems."); + return dtos.Select(ConvertToTaskItem).ToList(); } - else + } + + _log.LogMessage(MessageImportance.Low, "QueryAllTaskItems returned null or empty result."); + } + catch (Exception ex) + { + _log.LogMessage(MessageImportance.Low, "Exception trying to call QueryAllTaskItems: {0}", ex.Message); + } + + // Fallback: try to use the host object directly as IEnumerable (legacy behavior). + if (_hostObject is IEnumerable enumerableHost) + { + _log.LogMessage(MessageImportance.Low, "Falling back to IEnumerable host object."); + return enumerableHost; + } + + return null; + + static TaskItem ConvertToTaskItem(TaskItemDto dto) + { + TaskItem taskItem = new(dto.ItemSpec ?? string.Empty); + if (dto.Metadata is not null) + { + if (dto.Metadata.TryGetValue(UserMetaDataName, out string? userName)) { - logMethod("HostObject credentials not detected. Falling back to Docker credential retrieval."); + taskItem.SetMetadata(UserMetaDataName, userName); + } + + if (dto.Metadata.TryGetValue(PasswordMetaDataName, out string? password)) + { + taskItem.SetMetadata(PasswordMetaDataName, password); } } + + return taskItem; } - return retVal; } + + private readonly record struct TaskItemDto(string? ItemSpec, Dictionary? Metadata); }