File: Tasks\CreateNewImage.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 Microsoft.Build.Framework;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.MSBuild;
using Microsoft.NET.Build.Containers.Resources;
using ILogger = Microsoft.Extensions.Logging.ILogger;
 
namespace Microsoft.NET.Build.Containers.Tasks;
 
public sealed partial class CreateNewImage : Microsoft.Build.Utilities.Task, ICancelableTask, IDisposable
{
    private readonly CancellationTokenSource _cancellationTokenSource = new();
 
    /// <summary>
    /// Unused. For interface parity with the ToolTask implementation of the task.
    /// </summary>
    public string ToolExe { get; set; }
 
    /// <summary>
    /// Unused. For interface parity with the ToolTask implementation of the task.
    /// </summary>
    public string ToolPath { get; set; }
 
    private bool IsLocalPull => string.IsNullOrWhiteSpace(BaseRegistry);
 
    public void Cancel() => _cancellationTokenSource.Cancel();
 
    public override bool Execute()
    {
        try
        {
            Task.Run(() => ExecuteAsync(_cancellationTokenSource.Token)).GetAwaiter().GetResult();
        }
        catch (TaskCanceledException ex)
        {
            Log.LogWarningFromException(ex);
        }
        catch (OperationCanceledException ex)
        {
            Log.LogWarningFromException(ex);
        }
        return !Log.HasLoggedErrors;
    }
 
    internal async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        using MSBuildLoggerProvider loggerProvider = new(Log);
        ILoggerFactory msbuildLoggerFactory = new LoggerFactory(new[] { loggerProvider });
        ILogger logger = msbuildLoggerFactory.CreateLogger<CreateNewImage>();
 
        if (!Directory.Exists(PublishDirectory))
        {
            Log.LogErrorWithCodeFromResources(nameof(Strings.PublishDirectoryDoesntExist), nameof(PublishDirectory), PublishDirectory);
            return !Log.HasLoggedErrors;
        }
 
        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);
 
        DestinationImageReference destinationImageReference = DestinationImageReference.CreateFromSettings(
            Repository,
            ImageTags,
            msbuildLoggerFactory,
            ArchiveOutputPath,
            OutputRegistry,
            LocalRegistry);
 
        var telemetry = new Telemetry(sourceImageReference, destinationImageReference, Log);
 
        ImageBuilder? imageBuilder;
        if (sourceRegistry is { } registry)
        {
            try
            {
                var picker = new RidGraphManifestPicker(RuntimeIdentifierGraphPath);
                imageBuilder = await registry.GetImageManifestAsync(
                    BaseImageName,
                    sourceImageReference.Reference,
                    ContainerRuntimeIdentifier,
                    picker,
                    cancellationToken).ConfigureAwait(false);
            }
            catch (RepositoryNotFoundException)
            {
                telemetry.LogUnknownRepository();
                Log.LogErrorWithCodeFromResources(nameof(Strings.RepositoryNotFound), BaseImageName, BaseImageTag, BaseImageDigest, registry.RegistryName);
                return !Log.HasLoggedErrors;
            }
            catch (UnableToAccessRepositoryException)
            {
                telemetry.LogCredentialFailure(sourceImageReference);
                Log.LogErrorWithCodeFromResources(nameof(Strings.UnableToAccessRepository), BaseImageName, registry.RegistryName);
                return !Log.HasLoggedErrors;
            }
            catch (ContainerHttpException e)
            {
                Log.LogErrorFromException(e, showStackTrace: false, showDetail: true, file: null);
                return !Log.HasLoggedErrors;
            }
            catch (BaseImageNotFoundException e)
            {
                telemetry.LogRidMismatch(e.RequestedRuntimeIdentifier, e.AvailableRuntimeIdentifiers.ToArray());
                Log.LogErrorFromException(e, showStackTrace: false, showDetail: true, file: null);
                return !Log.HasLoggedErrors;
            }
        }
        else
        {
            throw new NotSupportedException(Resource.GetString(nameof(Strings.ImagePullNotSupported)));
        }
 
        if (imageBuilder is null)
        {
            Log.LogErrorWithCodeFromResources(nameof(Strings.BaseImageNotFound), sourceImageReference, ContainerRuntimeIdentifier);
            return !Log.HasLoggedErrors;
        }
 
        (string message, object[] parameters) = SkipPublishing ?
            (Strings.ContainerBuilder_StartBuildingImageForRid, new object[] { Repository, ContainerRuntimeIdentifier, sourceImageReference }) :
            (Strings.ContainerBuilder_StartBuildingImage, new object[] { Repository, String.Join(",", ImageTags), sourceImageReference });
        Log.LogMessage(MessageImportance.High, message, parameters);
 
        // forcibly change the media type if required
        if (ImageFormat is not null)
        {
            if (Enum.TryParse<KnownImageFormats>(ImageFormat, out var imageFormat))
            {
                imageBuilder.ManifestMediaType = imageFormat switch
                {
                    KnownImageFormats.Docker => SchemaTypes.DockerManifestV2,
                    KnownImageFormats.OCI => SchemaTypes.OciManifestV1,
                    _ => imageBuilder.ManifestMediaType // should be impossible unless we add to the enum
                };
            }
            else
            {
                Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidContainerImageFormat), ImageFormat, string.Join(",", Enum.GetValues<KnownImageFormats>()));
            }
        }
 
        // forcibly change the media type if required
        if (ImageFormat is not null)
        {
            if (Enum.TryParse<KnownImageFormats>(ImageFormat, out var imageFormat))
            {
                imageBuilder.ManifestMediaType = imageFormat switch
                {
                    KnownImageFormats.Docker => SchemaTypes.DockerManifestV2,
                    KnownImageFormats.OCI => SchemaTypes.OciManifestV1,
                    _ => imageBuilder.ManifestMediaType // should be impossible unless we add to the enum
                };
            }
            else
            {
                Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidContainerImageFormat), ImageFormat, string.Join(",", Enum.GetValues<KnownImageFormats>()));
            }
        }
        var userId = imageBuilder.IsWindows ? null : ContainerBuilder.TryParseUserId(ContainerUser);
        Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory, imageBuilder.IsWindows, imageBuilder.ManifestMediaType, userId);
        imageBuilder.AddLayer(newLayer);
        imageBuilder.SetWorkingDirectory(WorkingDirectory);
 
        (string[] entrypoint, string[] cmd) = DetermineEntrypointAndCmd(baseImageEntrypoint: imageBuilder.BaseImageConfig.GetEntrypoint());
        imageBuilder.SetEntrypointAndCmd(entrypoint, cmd);
 
        string? baseImageLabel = null;
        string? baseImageDigest = null;
        if (GenerateLabels)
        {
            foreach (ITaskItem label in Labels)
            {
                imageBuilder.AddLabel(label.ItemSpec, label.GetMetadata("Value"));
            }
 
            if (GenerateDigestLabel)
            {
                (baseImageLabel, baseImageDigest) = imageBuilder.AddBaseImageDigestLabel();
            }
        }
        else
        {
            if (GenerateDigestLabel)
            {
                Log.LogMessageFromResources(nameof(Strings.GenerateDigestLabelWithoutGenerateLabels));
            }
        }
 
        SetEnvironmentVariables(imageBuilder, ContainerEnvironmentVariables);
 
        SetPorts(imageBuilder, ExposedPorts);
 
        if (ContainerUser is { Length: > 0 } user)
        {
            imageBuilder.SetUser(user);
        }
 
        // at the end of this step, if any failed then bail out.
        if (Log.HasLoggedErrors)
        {
            return false;
        }
 
        BuiltImage builtImage = imageBuilder.Build();
        cancellationToken.ThrowIfCancellationRequested();
 
        // at this point we're done with modifications and are just pushing the data other places
        GeneratedContainerManifest = builtImage.Manifest;
        GeneratedContainerConfiguration = builtImage.Config;
        GeneratedContainerDigest = builtImage.ManifestDigest;
        GeneratedArchiveOutputPath = ArchiveOutputPath;
        GeneratedContainerMediaType = builtImage.ManifestMediaType;
        GeneratedContainerNames = destinationImageReference.FullyQualifiedImageNames().Select(name => new Microsoft.Build.Utilities.TaskItem(name)).ToArray();
        if (baseImageLabel is not null && baseImageDigest is not null)
        {
            var labelItem = new Microsoft.Build.Utilities.TaskItem(baseImageLabel);
            labelItem.SetMetadata("Value", baseImageDigest);
            GeneratedDigestLabel = labelItem;
        }
 
        if (!SkipPublishing)
        {
            await ImagePublisher.PublishImageAsync(builtImage, sourceImageReference, destinationImageReference, Log, telemetry, cancellationToken)
                .ConfigureAwait(false);
        }
 
        return !Log.HasLoggedErrors;
    }
 
    private void SetPorts(ImageBuilder image, ITaskItem[] exposedPorts)
    {
        foreach (var port in exposedPorts)
        {
            var portNo = port.ItemSpec;
            var portType = port.GetMetadata("Type");
            if (ContainerHelpers.TryParsePort(portNo, portType, out Port? parsedPort, out ContainerHelpers.ParsePortError? errors))
            {
                image.ExposePort(parsedPort.Value.Number, parsedPort.Value.Type);
            }
            else
            {
                ContainerHelpers.ParsePortError parsedErrors = (ContainerHelpers.ParsePortError)errors!;
 
                if (parsedErrors.HasFlag(ContainerHelpers.ParsePortError.MissingPortNumber))
                {
                    Log.LogErrorWithCodeFromResources(nameof(Strings.MissingPortNumber), port.ItemSpec);
                }
                else
                {
                    if (parsedErrors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortNumber) && parsedErrors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortType))
                    {
                        Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidPort_NumberAndType), portNo, portType);
                    }
                    else if (parsedErrors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortNumber))
                    {
                        Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidPort_Number), portNo);
                    }
                    else if (parsedErrors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortType))
                    {
                        Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidPort_Type), portType);
                    }
                }
            }
        }
    }
 
    private void SetEnvironmentVariables(ImageBuilder img, ITaskItem[] envVars)
    {
        foreach (ITaskItem envVar in envVars)
        {
            var value = envVar.GetMetadata("Value");
            img.AddEnvironmentVariable(envVar.ItemSpec, value);
        }
    }
 
    public void Dispose()
    {
        _cancellationTokenSource.Dispose();
    }
 
    internal (string[] entrypoint, string[] cmd) DetermineEntrypointAndCmd(string[]? baseImageEntrypoint)
    {
        string[] entrypoint = Entrypoint.Select(i => i.ItemSpec).ToArray();
        string[] entrypointArgs = EntrypointArgs.Select(i => i.ItemSpec).ToArray();
        string[] cmd = DefaultArgs.Select(i => i.ItemSpec).ToArray();
        string[] appCommand = AppCommand.Select(i => i.ItemSpec).ToArray();
        string[] appCommandArgs = AppCommandArgs.Select(i => i.ItemSpec).ToArray();
        string appCommandInstruction = AppCommandInstruction;
 
        return ImageBuilder.DetermineEntrypointAndCmd(entrypoint, entrypointArgs, cmd, appCommand, appCommandArgs, appCommandInstruction, baseImageEntrypoint,
            logWarning: s => Log.LogWarningWithCodeFromResources(s),
            logError: (s, a) => { if (a is null) Log.LogErrorWithCodeFromResources(s); else Log.LogErrorWithCodeFromResources(s, a); });
    }
}