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);
}