File: ContainerBuilder.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.Extensions.Logging;
using Microsoft.NET.Build.Containers.Resources;
 
namespace Microsoft.NET.Build.Containers;
 
internal enum KnownImageFormats
{
    OCI,
    Docker
}
 
internal static class ContainerBuilder
{
    internal static async Task<int> ContainerizeAsync(
        DirectoryInfo publishDirectory,
        string workingDir,
        string baseRegistry,
        string baseImageName,
        string baseImageTag,
        string? baseImageDigest,
        string[] entrypoint,
        string[] entrypointArgs,
        string[] defaultArgs,
        string[] appCommand,
        string[] appCommandArgs,
        string appCommandInstruction,
        string imageName,
        string[] imageTags,
        string? outputRegistry,
        Dictionary<string, string> labels,
        Port[]? exposedPorts,
        Dictionary<string, string> envVars,
        string containerRuntimeIdentifier,
        string ridGraphPath,
        string localRegistry,
        string? containerUser,
        string? archiveOutputPath,
        bool generateLabels,
        bool generateDigestLabel,
        KnownImageFormats? imageFormat,
        ILoggerFactory loggerFactory,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (!publishDirectory.Exists)
        {
            throw new ArgumentException(string.Format(Resource.GetString(nameof(Strings.PublishDirectoryDoesntExist)), nameof(publishDirectory), publishDirectory.FullName));
        }
        ILogger logger = loggerFactory.CreateLogger("Containerize");
        logger.LogTrace("Trace logging: enabled.");
 
        bool isLocalPull = string.IsNullOrEmpty(baseRegistry);
        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(
            imageName,
            imageTags,
            loggerFactory,
            archiveOutputPath,
            outputRegistry,
            localRegistry);
 
        ImageBuilder? imageBuilder;
        if (sourceRegistry is { } registry)
        {
            try
            {
                var ridGraphPicker = new RidGraphManifestPicker(ridGraphPath);
                imageBuilder = await registry.GetImageManifestAsync(
                    baseImageName,
                    sourceImageReference.Reference,
                    containerRuntimeIdentifier,
                    ridGraphPicker,
                    cancellationToken).ConfigureAwait(false);
            }
            catch (RepositoryNotFoundException)
            {
                logger.LogError(Resource.FormatString(nameof(Strings.RepositoryNotFound), baseImageName, baseImageTag, baseImageDigest, registry.RegistryName));
                return 1;
            }
            catch (UnableToAccessRepositoryException)
            {
                logger.LogError(Resource.FormatString(nameof(Strings.UnableToAccessRepository), baseImageName, registry.RegistryName));
                return 1;
            }
            catch (ContainerHttpException e)
            {
                logger.LogError(e.Message);
                return 1;
            }
        }
        else
        {
            throw new NotSupportedException(Resource.GetString(nameof(Strings.ImagePullNotSupported)));
        }
        if (imageBuilder is null)
        {
            Console.WriteLine(Resource.GetString(nameof(Strings.BaseImageNotFound)), sourceImageReference, containerRuntimeIdentifier);
            return 1;
        }
        logger.LogInformation(Strings.ContainerBuilder_StartBuildingImage, imageName, string.Join(",", imageName), sourceImageReference);
        cancellationToken.ThrowIfCancellationRequested();
 
        // forcibly change the media type if required
        imageBuilder.ManifestMediaType = imageFormat switch
        {
            null => imageBuilder.ManifestMediaType,
            KnownImageFormats.Docker => SchemaTypes.DockerManifestV2,
            KnownImageFormats.OCI => SchemaTypes.OciManifestV1,
            _ => imageBuilder.ManifestMediaType // should be impossible unless we add to the enum
        };
        var userId = imageBuilder.IsWindows ? null : TryParseUserId(containerUser);
        Layer newLayer = Layer.FromDirectory(publishDirectory.FullName, workingDir, imageBuilder.IsWindows, imageBuilder.ManifestMediaType, userId);
        imageBuilder.AddLayer(newLayer);
        imageBuilder.SetWorkingDirectory(workingDir);
 
        bool hasErrors = false;
        (string[] imageEntrypoint, string[] imageCmd) = ImageBuilder.DetermineEntrypointAndCmd(entrypoint, entrypointArgs, defaultArgs, appCommand, appCommandArgs, appCommandInstruction,
            baseImageEntrypoint: imageBuilder.BaseImageConfig.GetEntrypoint(),
            logWarning: s =>
            {
                logger.LogWarning(Resource.GetString(nameof(s)));
            },
            logError: (s, a) =>
            {
                hasErrors = true;
                if (a is null)
                {
                    logger.LogError(Resource.GetString(nameof(s)));
                }
                else
                {
                    logger.LogError(Resource.FormatString(nameof(s), a));
                }
            });
        if (hasErrors)
        {
            return 1;
        }
        imageBuilder.SetEntrypointAndCmd(imageEntrypoint, imageCmd);
 
        if (generateLabels)
        {
            foreach (KeyValuePair<string, string> label in labels)
            {
                // labels are validated by System.CommandLine API
                imageBuilder.AddLabel(label.Key, label.Value);
            }
 
            if (generateDigestLabel)
            {
                imageBuilder.AddBaseImageDigestLabel();
            }
        }
 
        foreach (KeyValuePair<string, string> envVar in envVars)
        {
            imageBuilder.AddEnvironmentVariable(envVar.Key, envVar.Value);
        }
        foreach ((int number, PortType type) in exposedPorts ?? Array.Empty<Port>())
        {
            // ports are validated by System.CommandLine API
            imageBuilder.ExposePort(number, type);
        }
        if (containerUser is { Length: > 0 } user)
        {
            imageBuilder.SetUser(user);
        }
        BuiltImage builtImage = imageBuilder.Build();
        cancellationToken.ThrowIfCancellationRequested();
 
        int exitCode;
        switch (destinationImageReference.Kind)
        {
            case DestinationImageReferenceKind.LocalRegistry:
                exitCode = await PushToLocalRegistryAsync(
                    logger,
                    builtImage,
                    sourceImageReference,
                    destinationImageReference,
                    cancellationToken).ConfigureAwait(false);
                break;
            case DestinationImageReferenceKind.RemoteRegistry:
                exitCode = await PushToRemoteRegistryAsync(
                    logger,
                    builtImage,
                    sourceImageReference,
                    destinationImageReference,
                    cancellationToken).ConfigureAwait(false);
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
 
        return exitCode;
    }
 
    public static int? TryParseUserId(string? containerUser)
    {
        if (containerUser is null)
        {
            return null;
        }
        if (int.TryParse(containerUser, out int userId))
        {
            return userId;
        }
        if (containerUser.Equals("root", StringComparison.OrdinalIgnoreCase))
        {
            return 0; // root user
        }
        // TODO: on Linux we could _potentially_ try to map the user name to a UID
        return null;
    }
 
    private static async Task<int> PushToLocalRegistryAsync(ILogger logger, BuiltImage builtImage, SourceImageReference sourceImageReference,
        DestinationImageReference destinationImageReference,
        CancellationToken cancellationToken)
    {
        ILocalRegistry containerRegistry = destinationImageReference.LocalRegistry!;
        if (!(await containerRegistry.IsAvailableAsync(cancellationToken).ConfigureAwait(false)))
        {
            logger.LogError(Resource.FormatString(nameof(Strings.LocalRegistryNotAvailable)));
            return 7;
        }
 
        try
        {
            await containerRegistry.LoadAsync(builtImage, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false);
            logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToLocalDaemon, destinationImageReference, containerRegistry);
        }
        catch (UnableToDownloadFromRepositoryException)
        {
            logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference);
            return 1;
        }
        catch (Exception ex)
        {
            logger.LogError(Resource.FormatString(nameof(Strings.RegistryOutputPushFailed), ex.Message));
            return 1;
        }
 
        return 0;
    }
 
    private static async Task<int> PushToRemoteRegistryAsync(ILogger logger, BuiltImage builtImage, SourceImageReference sourceImageReference,
        DestinationImageReference destinationImageReference,
        CancellationToken cancellationToken)
    {
        try
        {
            await (destinationImageReference.RemoteRegistry!.PushAsync(
                builtImage,
                sourceImageReference,
                destinationImageReference,
                cancellationToken)).ConfigureAwait(false);
            logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToRegistry, destinationImageReference, destinationImageReference.RemoteRegistry.RegistryName);
        }
        catch (UnableToDownloadFromRepositoryException)
        {
            logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference);
            return 1;
        }
        catch (Exception e)
        {
            logger.LogError(Resource.FormatString(nameof(Strings.RegistryOutputPushFailed), e.Message));
            return 1;
        }
 
        return 0;
    }
}