File: Tasks\CreateNewImageToolTask.cs
Web Access
Project: ..\..\..\src\Containers\Microsoft.NET.Build.Containers\Microsoft.NET.Build.Containers.csproj (Microsoft.NET.Build.Containers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.NET.Build.Containers.Resources;
 
namespace Microsoft.NET.Build.Containers.Tasks;
 
/// <summary>
/// This task will shell out to the net7.0-targeted application for VS scenarios.
/// </summary>
public partial class CreateNewImage : ToolTask, ICancelableTask
{
    // Unused, ToolExe is set via targets and overrides this.
    protected override string ToolName => "dotnet";
 
    private (bool success, string user, string pass) extractionInfo;
 
    private string DotNetPath
    {
        get
        {
            // DOTNET_HOST_PATH, if set, is the full path to the dotnet host executable.
            // However, not all environments/scenarios set it correctly - some set it to just the directory.
 
            // this is the expected correct case - DOTNET_HOST_PATH is set to the full path of the dotnet host executable
            string path = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? "";
            if (Path.IsPathRooted(path) && File.Exists(path))
            {
                return path;
            }
            // some environments set it to just the directory, so we need to check that too
            if (Path.IsPathRooted(path) && Directory.Exists(path))
            {
                path = Path.Combine(path, ToolExe);
                if (File.Exists(path))
                {
                    return path;
                }
            }
            // last-chance fallback - use the ToolPath and ToolExe properties to try to synthesize the path
            if (string.IsNullOrEmpty(path))
            {
                // no
                path = string.IsNullOrEmpty(ToolPath) ? "" : ToolPath;
                path = Path.Combine(path, ToolExe);
            }
 
            return path;
        }
    }
 
    protected override string GenerateFullPathToTool() => DotNetPath;
 
    /// <summary>
    /// Workaround to avoid storing user/pass into the EnvironmentVariables property, which gets logged by the task.
    /// </summary>
    /// <param name="pathToTool"></param>
    /// <param name="commandLineCommands"></param>
    /// <param name="responseFileSwitch"></param>
    /// <returns></returns>
    protected override ProcessStartInfo GetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch)
    {
        VSHostObject hostObj = new(HostObject as System.Collections.Generic.IEnumerable<ITaskItem>);
        if (hostObj.ExtractCredentials(out string user, out string pass, (string s) => Log.LogWarning(s)))
        {
            extractionInfo = (true, user, pass);
        }
        else
        {
            Log.LogMessage(MessageImportance.Low, Resource.GetString(nameof(Strings.HostObjectNotDetected)));
        }
 
        ProcessStartInfo startInfo = base.GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch)!;
 
        if (extractionInfo.success)
        {
            startInfo.Environment[ContainerHelpers.HostObjectUser] = extractionInfo.user;
            startInfo.Environment[ContainerHelpers.HostObjectPass] = extractionInfo.pass;
        }
 
        return startInfo;
    }
 
    protected override string GenerateCommandLineCommands() => GenerateCommandLineCommandsInt();
 
    /// <remarks>
    /// For unit test purposes
    /// </remarks>
    internal string GenerateCommandLineCommandsInt()
    {
        if (string.IsNullOrWhiteSpace(PublishDirectory))
        {
            throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredPropertyNotSetOrEmpty), nameof(PublishDirectory)));
        }
        if (string.IsNullOrWhiteSpace(BaseRegistry))
        {
            throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredPropertyNotSetOrEmpty), nameof(BaseRegistry)));
        }
        if (string.IsNullOrWhiteSpace(BaseImageName))
        {
            throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredPropertyNotSetOrEmpty), nameof(BaseImageName)));
        }
        if (string.IsNullOrWhiteSpace(Repository))
        {
            throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredPropertyNotSetOrEmpty), nameof(Repository)));
        }
        if (string.IsNullOrWhiteSpace(WorkingDirectory))
        {
            throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredPropertyNotSetOrEmpty), nameof(WorkingDirectory)));
        }
 
        CommandLineBuilder builder = new();
 
        //mandatory options
        builder.AppendFileNameIfNotNull(Path.Combine(ContainerizeDirectory, "containerize.dll"));
        builder.AppendFileNameIfNotNull(PublishDirectory.TrimEnd(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }));
        builder.AppendSwitchIfNotNull("--baseregistry ", BaseRegistry);
        builder.AppendSwitchIfNotNull("--baseimagename ", BaseImageName);
        builder.AppendSwitchIfNotNull("--repository ", Repository);
        builder.AppendSwitchIfNotNull("--workingdirectory ", WorkingDirectory);
 
        //optional options
        if (!string.IsNullOrWhiteSpace(BaseImageTag))
        {
            builder.AppendSwitchIfNotNull("--baseimagetag ", BaseImageTag);
        }
        if (!string.IsNullOrWhiteSpace(BaseImageDigest))
        {
            builder.AppendSwitchIfNotNull("--baseimagedigest ", BaseImageDigest);
        }
        if (!string.IsNullOrWhiteSpace(OutputRegistry))
        {
            builder.AppendSwitchIfNotNull("--outputregistry ", OutputRegistry);
        }
        if (!string.IsNullOrWhiteSpace(LocalRegistry))
        {
            builder.AppendSwitchIfNotNull("--localregistry ", LocalRegistry);
        }
        if (!string.IsNullOrWhiteSpace(AppCommandInstruction))
        {
            builder.AppendSwitchIfNotNull("--appcommandinstruction ", AppCommandInstruction);
        }
        if (!string.IsNullOrWhiteSpace(ImageFormat))
        {
            builder.AppendSwitchIfNotNull("--image-format ", ImageFormat);
        }
 
        AppendSwitchIfNotNullSanitized(builder, "--entrypoint ", nameof(Entrypoint), Entrypoint);
        AppendSwitchIfNotNullSanitized(builder, "--entrypointargs ", nameof(EntrypointArgs), EntrypointArgs);
        AppendSwitchIfNotNullSanitized(builder, "--defaultargs ", nameof(DefaultArgs), DefaultArgs);
        AppendSwitchIfNotNullSanitized(builder, "--appcommand ", nameof(AppCommand), AppCommand);
        AppendSwitchIfNotNullSanitized(builder, "--appcommandargs ", nameof(AppCommandArgs), AppCommandArgs);
 
        if (Labels.Any(e => string.IsNullOrWhiteSpace(e.ItemSpec)))
        {
            Log.LogWarningWithCodeFromResources(nameof(Strings.EmptyValuesIgnored), nameof(Labels));
        }
        var sanitizedLabels = Labels.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec));
        if (sanitizedLabels.Any(i => i.GetMetadata("Value") is null))
        {
            Log.LogWarningWithCodeFromResources(nameof(Strings.ItemsWithoutMetadata), nameof(Labels));
            sanitizedLabels = sanitizedLabels.Where(i => i.GetMetadata("Value") is not null);
        }
 
        string[] readyLabels = sanitizedLabels.Select(i => i.ItemSpec + "=" + i.GetMetadata("Value")).ToArray();
        builder.AppendSwitchIfNotNull("--labels ", readyLabels, delimiter: " ");
 
        if (ImageTags.Any(string.IsNullOrWhiteSpace))
        {
            Log.LogWarningWithCodeFromResources(nameof(Strings.EmptyOrWhitespacePropertyIgnored), nameof(ImageTags));
        }
        string[] sanitizedImageTags = ImageTags.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
        builder.AppendSwitchIfNotNull("--imagetags ", sanitizedImageTags, delimiter: " ");
 
        if (ExposedPorts.Any(e => string.IsNullOrWhiteSpace(e.ItemSpec)))
        {
            Log.LogWarningWithCodeFromResources(nameof(Strings.EmptyValuesIgnored), nameof(ExposedPorts));
        }
        var sanitizedPorts = ExposedPorts.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec));
        string[] readyPorts =
            sanitizedPorts
                .Select(i => (i.ItemSpec, i.GetMetadata("Type")))
                .Select(pair => string.IsNullOrWhiteSpace(pair.Item2) ? pair.Item1 : (pair.Item1 + "/" + pair.Item2))
                .ToArray();
        builder.AppendSwitchIfNotNull("--ports ", readyPorts, delimiter: " ");
 
        if (ContainerEnvironmentVariables.Any(e => string.IsNullOrWhiteSpace(e.ItemSpec)))
        {
            Log.LogWarningWithCodeFromResources(nameof(Strings.EmptyValuesIgnored), nameof(ContainerEnvironmentVariables));
        }
        var sanitizedEnvVariables = ContainerEnvironmentVariables.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec));
        if (sanitizedEnvVariables.Any(i => i.GetMetadata("Value") is null))
        {
            Log.LogWarningWithCodeFromResources(nameof(Strings.ItemsWithoutMetadata), nameof(ContainerEnvironmentVariables));
            sanitizedEnvVariables = sanitizedEnvVariables.Where(i => i.GetMetadata("Value") is not null);
        }
        string[] readyEnvVariables = sanitizedEnvVariables.Select(i => i.ItemSpec + "=" + i.GetMetadata("Value")).ToArray();
        builder.AppendSwitchIfNotNull("--environmentvariables ", readyEnvVariables, delimiter: " ");
 
        if (!string.IsNullOrWhiteSpace(ContainerRuntimeIdentifier))
        {
            builder.AppendSwitchIfNotNull("--rid ", ContainerRuntimeIdentifier);
        }
 
        if (!string.IsNullOrWhiteSpace(RuntimeIdentifierGraphPath))
        {
            builder.AppendSwitchIfNotNull("--ridgraphpath ", RuntimeIdentifierGraphPath);
        }
 
        if (!string.IsNullOrWhiteSpace(ContainerUser))
        {
            builder.AppendSwitchIfNotNull("--container-user ", ContainerUser);
        }
 
        if (!string.IsNullOrWhiteSpace(ArchiveOutputPath))
        {
            builder.AppendSwitchIfNotNull("--archiveoutputpath ", ArchiveOutputPath);
        }
 
        if (GenerateLabels)
        {
            builder.AppendSwitch("--generate-labels");
        }
 
        if (GenerateDigestLabel)
        {
            builder.AppendSwitch("--generate-digest-label");
        }
 
        return builder.ToString();
 
        void AppendSwitchIfNotNullSanitized(CommandLineBuilder builder, string commandArgName, string propertyName, ITaskItem[] value)
        {
            ITaskItem[] santized = value.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec)).ToArray();
            if (santized.Length != value.Length)
            {
                Log.LogWarningWithCodeFromResources(nameof(Strings.EmptyValuesIgnored), propertyName);
            }
            builder.AppendSwitchIfNotNull(commandArgName, santized, delimiter: " ");
        }
    }
}