File: Publishing\ContainerImageInspectionResult.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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.CodeAnalysis;
 
namespace Aspire.Hosting.Publishing;
 
/// <summary>
/// Describes the outcome of a container image inspection operation.
/// </summary>
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public enum ContainerImageInspectionStatus
{
    /// <summary>
    /// The inspection completed successfully.
    /// </summary>
    Succeeded,
 
    /// <summary>
    /// The container runtime does not support the requested inspection.
    /// </summary>
    Unsupported,
 
    /// <summary>
    /// The inspection failed.
    /// </summary>
    Failed
}
 
/// <summary>
/// Represents typed container image configuration metadata.
/// </summary>
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class ContainerImageConfig
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ContainerImageConfig"/> class.
    /// </summary>
    /// <param name="entrypoint">The image entrypoint.</param>
    /// <param name="command">The default image command.</param>
    /// <param name="workingDirectory">The image working directory.</param>
    public ContainerImageConfig(
        IReadOnlyList<string> entrypoint,
        IReadOnlyList<string> command,
        string? workingDirectory)
    {
        ArgumentNullException.ThrowIfNull(entrypoint);
        ArgumentNullException.ThrowIfNull(command);
 
        Entrypoint = entrypoint;
        Command = command;
        WorkingDirectory = workingDirectory;
    }
 
    /// <summary>
    /// Gets the image entrypoint.
    /// </summary>
    public IReadOnlyList<string> Entrypoint { get; }
 
    /// <summary>
    /// Gets the default image command.
    /// </summary>
    public IReadOnlyList<string> Command { get; }
 
    /// <summary>
    /// Gets the image working directory.
    /// </summary>
    public string? WorkingDirectory { get; }
}
 
/// <summary>
/// Represents the result of inspecting a container image configuration.
/// </summary>
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class ContainerImageConfigInspectionResult
{
    private readonly Func<ContainerImageConfig?>? _configAccessor;
 
    internal ContainerImageConfigInspectionResult(
        ContainerImageInspectionStatus status,
        string? rawJson,
        string? errorMessage,
        Func<ContainerImageConfig?>? configAccessor)
    {
        Status = status;
        RawJson = rawJson;
        ErrorMessage = errorMessage;
        _configAccessor = configAccessor;
    }
 
    /// <summary>
    /// Gets the inspection status.
    /// </summary>
    public ContainerImageInspectionStatus Status { get; }
 
    /// <summary>
    /// Gets the runtime-native JSON returned by the inspection command, when available.
    /// </summary>
    public string? RawJson { get; }
 
    /// <summary>
    /// Gets the failure description when <see cref="Status"/> is <see cref="ContainerImageInspectionStatus.Failed"/>.
    /// </summary>
    public string? ErrorMessage { get; }
 
    /// <summary>
    /// Attempts to retrieve the typed image configuration.
    /// </summary>
    /// <param name="config">The image configuration when inspection succeeded.</param>
    /// <returns><see langword="true"/> when typed configuration metadata is available; otherwise, <see langword="false"/>.</returns>
    public bool TryGetConfig([NotNullWhen(true)] out ContainerImageConfig? config)
    {
        config = Status == ContainerImageInspectionStatus.Succeeded
            ? _configAccessor?.Invoke()
            : null;
 
        return config is not null;
    }
 
    /// <summary>
    /// Creates a successful image configuration inspection result.
    /// </summary>
    /// <param name="config">The inspected image configuration.</param>
    /// <param name="rawJson">The runtime-native JSON returned by the inspection command, when available.</param>
    /// <returns>A successful image configuration inspection result.</returns>
    public static ContainerImageConfigInspectionResult Success(ContainerImageConfig config, string? rawJson = null)
    {
        ArgumentNullException.ThrowIfNull(config);
 
        return new(
            ContainerImageInspectionStatus.Succeeded,
            rawJson,
            errorMessage: null,
            () => config);
    }
 
    /// <summary>
    /// Creates a failed image configuration inspection result.
    /// </summary>
    /// <param name="errorMessage">The failure description.</param>
    /// <param name="rawJson">The runtime-native JSON returned by the inspection command, when available.</param>
    /// <returns>A failed image configuration inspection result.</returns>
    public static ContainerImageConfigInspectionResult Failure(string errorMessage, string? rawJson = null)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(errorMessage);
 
        return new(
            ContainerImageInspectionStatus.Failed,
            rawJson,
            errorMessage,
            configAccessor: null);
    }
 
    /// <summary>
    /// Gets an image configuration inspection result for a runtime that does not support inspection.
    /// </summary>
    public static ContainerImageConfigInspectionResult Unsupported { get; } = new(
        ContainerImageInspectionStatus.Unsupported,
        rawJson: null,
        errorMessage: null,
        configAccessor: null);
}
 
/// <summary>
/// Represents a platform-specific container image manifest.
/// </summary>
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class ContainerImageManifest
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ContainerImageManifest"/> class.
    /// </summary>
    /// <param name="digest">The immutable manifest digest.</param>
    /// <param name="operatingSystem">The target operating system.</param>
    /// <param name="architecture">The target architecture.</param>
    /// <exception cref="ArgumentException">Thrown when <paramref name="digest"/> is not a lowercase SHA-256 digest or another argument is empty.</exception>
    public ContainerImageManifest(string digest, string operatingSystem, string architecture)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(digest);
        ArgumentException.ThrowIfNullOrWhiteSpace(operatingSystem);
        ArgumentException.ThrowIfNullOrWhiteSpace(architecture);
        if (!IsValidDigest(digest))
        {
            throw new ArgumentException(
                "The container image manifest digest must use the format 'sha256:' followed by 64 lowercase hexadecimal characters.",
                nameof(digest));
        }
 
        Digest = digest;
        OperatingSystem = operatingSystem;
        Architecture = architecture;
    }
 
    /// <summary>
    /// Gets the immutable manifest digest.
    /// </summary>
    public string Digest { get; }
 
    /// <summary>
    /// Gets the target operating system.
    /// </summary>
    public string OperatingSystem { get; }
 
    /// <summary>
    /// Gets the target architecture.
    /// </summary>
    public string Architecture { get; }
 
    internal static bool IsValidDigest(string digest)
    {
        const string Prefix = "sha256:";
        if (digest.Length != Prefix.Length + 64 ||
            !digest.StartsWith(Prefix, StringComparison.Ordinal))
        {
            return false;
        }
 
        foreach (var character in digest.AsSpan(Prefix.Length))
        {
            if (character is not (>= '0' and <= '9' or >= 'a' and <= 'f'))
            {
                return false;
            }
        }
 
        return true;
    }
}
 
/// <summary>
/// Represents the result of inspecting a container image manifest.
/// </summary>
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class ContainerImageManifestInspectionResult
{
    private readonly Func<string, string, ContainerImageManifest?>? _manifestAccessor;
 
    internal ContainerImageManifestInspectionResult(
        ContainerImageInspectionStatus status,
        string? rawJson,
        string? errorMessage,
        Func<string, string, ContainerImageManifest?>? manifestAccessor)
    {
        Status = status;
        RawJson = rawJson;
        ErrorMessage = errorMessage;
        _manifestAccessor = manifestAccessor;
    }
 
    /// <summary>
    /// Gets the inspection status.
    /// </summary>
    public ContainerImageInspectionStatus Status { get; }
 
    /// <summary>
    /// Gets the runtime-native JSON returned by the inspection command, when available.
    /// </summary>
    public string? RawJson { get; }
 
    /// <summary>
    /// Gets the failure description when <see cref="Status"/> is <see cref="ContainerImageInspectionStatus.Failed"/>.
    /// </summary>
    public string? ErrorMessage { get; }
 
    /// <summary>
    /// Attempts to retrieve a manifest for the requested platform.
    /// </summary>
    /// <param name="operatingSystem">The target operating system.</param>
    /// <param name="architecture">The target architecture.</param>
    /// <param name="manifest">The matching manifest when one is available.</param>
    /// <returns><see langword="true"/> when a matching manifest is available; otherwise, <see langword="false"/>.</returns>
    public bool TryGetManifest(
        string operatingSystem,
        string architecture,
        [NotNullWhen(true)] out ContainerImageManifest? manifest)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(operatingSystem);
        ArgumentException.ThrowIfNullOrWhiteSpace(architecture);
 
        manifest = Status == ContainerImageInspectionStatus.Succeeded
            ? _manifestAccessor?.Invoke(operatingSystem, architecture)
            : null;
 
        return manifest is not null;
    }
 
    /// <summary>
    /// Creates a successful image manifest inspection result.
    /// </summary>
    /// <param name="manifests">The inspected platform-specific image manifests.</param>
    /// <param name="rawJson">The runtime-native JSON returned by the inspection command, when available.</param>
    /// <returns>A successful image manifest inspection result.</returns>
    public static ContainerImageManifestInspectionResult Success(
        IReadOnlyList<ContainerImageManifest> manifests,
        string? rawJson = null)
    {
        ArgumentNullException.ThrowIfNull(manifests);
 
        var manifestSnapshot = manifests.ToArray();
        return new(
            ContainerImageInspectionStatus.Succeeded,
            rawJson,
            errorMessage: null,
            (operatingSystem, architecture) => manifestSnapshot.FirstOrDefault(manifest =>
                string.Equals(manifest.OperatingSystem, operatingSystem, StringComparison.OrdinalIgnoreCase) &&
                string.Equals(manifest.Architecture, architecture, StringComparison.OrdinalIgnoreCase)));
    }
 
    /// <summary>
    /// Creates a failed image manifest inspection result.
    /// </summary>
    /// <param name="errorMessage">The failure description.</param>
    /// <param name="rawJson">The runtime-native JSON returned by the inspection command, when available.</param>
    /// <returns>A failed image manifest inspection result.</returns>
    public static ContainerImageManifestInspectionResult Failure(string errorMessage, string? rawJson = null)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(errorMessage);
 
        return new(
            ContainerImageInspectionStatus.Failed,
            rawJson,
            errorMessage,
            manifestAccessor: null);
    }
 
    /// <summary>
    /// Gets an image manifest inspection result for a runtime that does not support inspection.
    /// </summary>
    public static ContainerImageManifestInspectionResult Unsupported { get; } = new(
        ContainerImageInspectionStatus.Unsupported,
        rawJson: null,
        errorMessage: null,
        manifestAccessor: null);
}