File: Runtime\WorkloadDefinition.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Core\Aspire.Hosting.Native.Core.csproj (Aspire.Hosting.Native.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Aspire.Hosting.Native.Runtime;
 
/// <summary>Declares a named endpoint independently of container or executable implementation.</summary>
internal sealed record WorkloadPort(string Name, WorkloadProtocol Protocol, string UriScheme, int TargetPort, int HostPort,
    string EnvironmentVariable)
{
    public bool IsExternal { get; init; }
}
 
/// <summary>Contains a named host-facing allocation, not a container-network address.</summary>
internal sealed record AllocatedWorkloadPort(string Name, WorkloadProtocol Protocol, string UriScheme, string Host, int Port);
 
/// <summary>Describes a bind mount or an explicitly named externally retained volume.</summary>
internal sealed record WorkloadMount(WorkloadMountKind Type, string Source, string Target, bool ReadOnly);
 
/// <summary>Validates launch declarations before granting an executor a cleanup obligation.</summary>
internal static class WorkloadDefinition
{
    public static void Validate(WorkloadPlan plan) => ValidateCore(plan, requireLocalPaths: true);
 
    /// <summary>Validates desired intent without requiring a deployment host's paths to exist on the authoring machine.</summary>
    public static void ValidateDeclaration(WorkloadPlan plan) => ValidateCore(plan, requireLocalPaths: false);
 
    private static void ValidateCore(WorkloadPlan plan, bool requireLocalPaths)
    {
        ArgumentNullException.ThrowIfNull(plan);
        ValidateEnvironmentAndArguments(plan);
        ValidateEndpoints(plan);
        switch (plan)
        {
            case ContainerWorkload container:
                ValidateContainer(container, requireLocalPaths);
                break;
            case ExecutableWorkload executable:
                ValidateExecutable(executable, requireLocalPaths);
                break;
            default:
                throw new NotSupportedException("The workload primitive is not implemented.");
        }
    }
 
    private static void ValidateEnvironmentAndArguments(WorkloadPlan plan)
    {
        if (plan.Environment.IsDefault || plan.Arguments.IsDefault || plan.Endpoints.IsDefault ||
            plan.Environment.Length > 64 || plan.Arguments.Length > 128 || plan.Endpoints.Length > 32 ||
            plan.Arguments.Any(argument => argument is null || argument.Length > 8192 || argument.Contains('\0')) ||
            plan.Environment.Any(variable => variable is null || !ValidEnvironmentName(variable.Name) ||
                variable.Value is null || variable.Value.Length > 8192 || variable.Value.Contains('\0')) ||
            plan.Environment.Select(variable => variable.Name).Distinct(StringComparer.Ordinal).Count() != plan.Environment.Length)
        {
            throw new ArgumentException("Invalid workload environment, arguments, or endpoint capacity.", nameof(plan));
        }
    }
 
    private static void ValidateEndpoints(WorkloadPlan plan)
    {
        var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var environmentNames = plan.Environment.Select(variable => variable.Name).ToHashSet(StringComparer.Ordinal);
        foreach (var endpoint in plan.Endpoints)
        {
            if (endpoint is null || string.IsNullOrEmpty(endpoint.Name) || endpoint.Name.Length > 64 ||
                !char.IsAsciiLetter(endpoint.Name[0]) ||
                endpoint.Name.Any(character => !(char.IsAsciiLetterOrDigit(character) || character == '-')) ||
                !names.Add(endpoint.Name) || endpoint.Protocol is not (WorkloadProtocol.Tcp or WorkloadProtocol.Udp) ||
                endpoint.UriScheme is null || endpoint.UriScheme.Length > 64 || !Uri.CheckSchemeName(endpoint.UriScheme) ||
                endpoint.TargetPort is < 0 or > 65535 || endpoint.HostPort is < 0 or > 65535 ||
                endpoint.EnvironmentVariable is null ||
                endpoint.EnvironmentVariable.Length > 0 &&
                    (!ValidEnvironmentName(endpoint.EnvironmentVariable) || !environmentNames.Add(endpoint.EnvironmentVariable)))
            {
                throw new ArgumentException("Invalid or duplicate named workload endpoint.", nameof(plan));
            }
            if (plan is ContainerWorkload && (endpoint.TargetPort == 0 || endpoint.EnvironmentVariable.Length > 0) ||
                plan is ExecutableWorkload && endpoint.TargetPort == 0 && endpoint.EnvironmentVariable.Length == 0 ||
                endpoint.TargetPort > 0 && endpoint.EnvironmentVariable.Length > 0)
            {
                throw new ArgumentException("Use a container target port or an executable target port/port environment variable.", nameof(plan));
            }
        }
    }
 
    private static void ValidateContainer(ContainerWorkload plan, bool requireLocalPaths)
    {
        if (string.IsNullOrWhiteSpace(plan.Image) || plan.Image.Length > 2048 ||
            plan.Image.Any(char.IsWhiteSpace) || plan.Image.Contains('\0') ||
            plan.Command is { } command && (string.IsNullOrWhiteSpace(command) || command.Length > 8192 || command.Contains('\0')) ||
            plan.Mounts.IsDefault || plan.Mounts.Length > 64)
        {
            throw new ArgumentException("Invalid container image, command, or mounts.", nameof(plan));
        }
        ValidateMounts(plan, requireLocalPaths);
        if (plan.Endpoints.Select(endpoint => (endpoint.TargetPort, endpoint.Protocol)).Distinct().Count() != plan.Endpoints.Length)
        {
            throw new ArgumentException("Container target ports must be unique for each protocol.", nameof(plan));
        }
    }
 
    private static void ValidateMounts(ContainerWorkload plan, bool requireLocalPaths)
    {
        var targets = new HashSet<string>(StringComparer.Ordinal);
        foreach (var mount in plan.Mounts)
        {
            if (mount is null || mount.Source is null || string.IsNullOrWhiteSpace(mount.Target) ||
                !mount.Target.StartsWith('/') || mount.Target.Length > 4096 || mount.Target.Contains('\0') ||
                !targets.Add(mount.Target) ||
                mount.Type is not (WorkloadMountKind.Bind or WorkloadMountKind.Volume) ||
                mount.Type == WorkloadMountKind.Bind && (!Path.IsPathFullyQualified(mount.Source) ||
                    requireLocalPaths && !(File.Exists(mount.Source) || Directory.Exists(mount.Source))) ||
                mount.Type == WorkloadMountKind.Volume && (mount.Source.Length is < 1 or > 255 ||
                    !char.IsAsciiLetterOrDigit(mount.Source[0]) ||
                    mount.Source.Any(character => !(char.IsAsciiLetterOrDigit(character) || character is '_' or '.' or '-'))))
            {
                throw new ArgumentException("Invalid or duplicate container mount.", nameof(plan));
            }
        }
    }
 
    private static void ValidateExecutable(ExecutableWorkload plan, bool requireLocalPaths)
    {
        if (!Path.IsPathFullyQualified(plan.ExecutablePath) || requireLocalPaths && !File.Exists(plan.ExecutablePath) ||
            !Path.IsPathFullyQualified(plan.WorkingDirectory) || requireLocalPaths && !Directory.Exists(plan.WorkingDirectory))
        {
            throw new ArgumentException("Executables require existing absolute paths.", nameof(plan));
        }
    }
 
    private static bool ValidEnvironmentName(string name) =>
        !string.IsNullOrEmpty(name) && name.Length <= 128 &&
        !name.Contains('=') && !name.Contains('\0');
}