File: Dcp\Model\ModelCommon.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.
 
namespace Aspire.Hosting.Dcp.Model;
 
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using k8s;
using k8s.Models;
 
internal interface IAnnotationHolder
{
    void Annotate(string annotationName, string value);
    void AnnotateAsObjectList<TValue>(string annotationName, TValue value);
}
 
internal abstract class CustomResource : KubernetesObject, IMetadata<V1ObjectMeta>, IAnnotationHolder
{
    public const string ServiceProducerAnnotation = "service-producer";
    public const string ServiceConsumerAnnotation = "service-consumer";
    public const string EndpointNameAnnotation = "endpoint-name";
    public const string ResourceNameAnnotation = "resource-name";
    public const string OtelServiceNameAnnotation = "otel-service-name";
    public const string OtelServiceInstanceIdAnnotation = "otel-service-instance-id";
    public const string ResourceStateAnnotation = "resource-state";
    public const string ResourceReplicaCount = "resource-replica-count";
    public const string ResourceReplicaIndex = "resource-replica-index";
 
    public string? AppModelResourceName => Metadata.Annotations?.TryGetValue(ResourceNameAnnotation, out var value) is true ? value : null;
 
    public string? AppModelInitialState => Metadata.Annotations?.TryGetValue(ResourceStateAnnotation, out var value) is true ? value : null;
 
    [JsonPropertyName("metadata")]
    public V1ObjectMeta Metadata { get; set; } = new V1ObjectMeta();
 
    public void Annotate(string annotationName, string value)
    {
        if (Metadata.Annotations is null)
        {
            Metadata.Annotations = new Dictionary<string, string>();
        }
 
        Metadata.Annotations[annotationName] = value;
    }
 
    public void AnnotateAsObjectList<TValue>(string annotationName, TValue value)
    {
        if (Metadata.Annotations is null)
        {
            Metadata.Annotations = new Dictionary<string, string>();
        }
 
        AnnotateAsObjectList<TValue>(Metadata.Annotations, annotationName, value);
    }
 
    public bool TryGetAnnotationAsObjectList<TValue>(string annotationName, [NotNullWhen(true)] out List<TValue>? list)
    {
        return TryGetAnnotationAsObjectList<TValue>(Metadata.Annotations, annotationName, out list);
    }
 
    internal static bool TryGetAnnotationAsObjectList<TValue>(IDictionary<string, string>? annotations, string annotationName, [NotNullWhen(true)] out List<TValue>? list)
    {
        list = null;
 
        if (annotations is null)
        {
            return false;
        }
 
        string? annotationValue;
        bool found = annotations.TryGetValue(annotationName, out annotationValue);
        if (!found || string.IsNullOrWhiteSpace(annotationValue))
        {
            return false;
        }
 
        try
        {
            list = JsonSerializer.Deserialize<List<TValue>>(annotationValue);
        }
        catch
        {
            return false;
        }
 
        return list is not null;
    }
 
    internal static void AnnotateAsObjectList<TValue>(IDictionary<string, string> annotations, string annotationName, TValue value)
    {
        List<TValue> values;
        if (annotations.TryGetValue(annotationName, out var annotationVal) && !string.IsNullOrWhiteSpace(annotationVal))
        {
            try
            {
                values = JsonSerializer.Deserialize<List<TValue>>(annotationVal) ?? new();
            }
            catch
            {
                values = new();
            }
 
            if (!values.Contains(value))
            {
                values.Add(value);
            }
        }
        else
        {
            values = [value];
        }
 
        var newAnnotationVal = JsonSerializer.Serialize(values);
        annotations[annotationName] = newAnnotationVal;
    }
}
 
internal abstract class CustomResource<TSpec, TStatus> : CustomResource
{
    [JsonPropertyName("spec")]
    public TSpec Spec { get; set; }
 
    [JsonPropertyName("status")]
    public TStatus? Status { get; set; }
 
    public CustomResource(TSpec spec)
    {
        Spec = spec;
    }
}
 
internal sealed class CustomResourceList<T> : KubernetesObject
where T : CustomResource
{
    [JsonPropertyName("metadata")]
    public V1ListMeta Metadata { get; set; } = new V1ListMeta();
 
    [JsonPropertyName("items")]
    public required List<T> Items { get; set; }
}
 
internal sealed class EnvVar
{
    // Name of the environment variable
    [JsonPropertyName("name")]
    public string? Name { get; set; }
 
    // Value of the environment variable
    [JsonPropertyName("value")]
    public string? Value { get; set; }
}
 
internal static class Conventions
{
    // Indicates that process ID of some process is not known
    public const int UnknownPID = -1;
 
    // Indicates that the exit code of some process is not known
    public const int UnknownExitCode = -1;
}
 
internal sealed class ServiceProducerAnnotation
{
    // Name of the service produced
    [JsonPropertyName("serviceName")]
    public string ServiceName { get; set; }
 
    // Desired address for the service.
    // If not set, the service must specify the placeholder for address injection via addressFor template function.
    [JsonPropertyName("address")]
    public string? Address { get; set; }
 
    // The port that the program will use to implement the service endpoint.
    // If not set, the service must specify the placeholder for address injection via portFor template function.
    [JsonPropertyName("port")]
    public int? Port { get; set; }
 
    [JsonConstructor]
    public ServiceProducerAnnotation(string serviceName)
    {
        ServiceName = serviceName;
    }
 
    public override bool Equals(object? obj)
    {
        if (obj is null || obj is not ServiceProducerAnnotation) { return false; }
 
        var other = (ServiceProducerAnnotation)obj;
 
        if (!string.Equals(ServiceName, other.ServiceName, StringComparison.Ordinal)) { return false; }
 
        // Note: string.Equals(null, null) is true, which is what we want here.
        if (!string.Equals(Address, other.Address, StringComparison.Ordinal)) { return false; }
 
        if (Port != other.Port) { return false; }
 
        return true;
    }
 
    public override int GetHashCode()
    {
        return HashCode.Combine(ServiceName, Address, Port);
    }
}
 
internal sealed record NamespacedName(string Name, string? Namespace);
 
internal static class Rules
{
    public static bool IsValidObjectName(string candidate)
    {
        if (string.IsNullOrWhiteSpace(candidate))
        {
            return false;
        }
 
        if (candidate.Length > 253)
        {
            return false;
        }
 
        // Can only contain alphanumeric characters, hyphen, period, underscore, tilde.
        // (essentially the same as URL path characters).
        // Needs to start with a letter, underscore, or tilde.
        bool isValid = Regex.IsMatch(candidate, @"^[[a-zA-Z_~][a-zA-Z0-9\-._~]*$");
        return isValid;
    }
}
 
internal static class HealthStatus
{
    /// <summary>
    /// DCP considers the resource to be healthy.
    /// </summary>
    public const string Healthy = "Healthy";
 
    /// <summary>
    /// DCP considers the resource to be unhealthy.
    /// </summary>
    public const string Unhealthy = "Unhealthy";
 
    /// <summary>
    /// DCP considers the resource to be in a cautionary state (partially healthy or unknown state).
    /// </summary>
    public const string Caution = "Caution";
}
 
internal sealed class HealthProbe
{
    /// <summary>
    /// Name of the health probe, used to identify the results of a specific probe in the status output
    /// of the parent resource. Name must be unique for a given parent resource.
    /// </summary>
    [JsonPropertyName("name")]
    public string? Name;
 
    /// <summary>
    /// The type of health probe. For valid values see <see cref="HealthProbeType"/>.
    /// </summary>
    [JsonPropertyName("type")]
    public string? Type;
 
    /// <summary>
    /// If <see cref="HealthProbe.Type"/> is HTTP, this configures the health probe.
    /// </summary>
    [JsonPropertyName("httpProbe")]
    public HttpHealthProbeConfig? HttpProbe { get; set; }
 
    /// <summary>
    /// If <see cref="HealthProbe.Type"/> is Executable, this configures the health probe.
    /// </summary>
    [JsonPropertyName("executableProbe")]
    public ExecutableHealthProbeConfig? ExecutableProbe { get; set; }
 
    /// <summary>
    /// Determines how often the probe executes.
    /// </summary>
    [JsonPropertyName("schedule")]
    public HealthProbeSchedule? Schedule { get; set; }
 
    /// <summary>
    /// Optional annotations (metadata) for the health probe.
    /// </summary>
    [JsonPropertyName("annotations")]
    public IDictionary<string, string>? Annotations { get; set; }
}
 
internal static class HealthProbeType
{
    /// <summary>
    /// The health probe makes an HTTP request that expects a success response (i.e. 200).
    /// </summary>
    public const string Http = "HTTP";
 
    /// <summary>
    /// The health probe runs an executable and expects a successful exit code (0).
    /// </summary>
    public const string Executable = "Executable";
 
    /// <summary>
    /// The health probe executes a command in a container and expects a successful exit code (0).
    /// </summary>
    public const string ContainerExec = "ContainerExec";
}
 
internal sealed class HttpHealthProbeConfig
{
    /// <summary>
    /// The URL to probe.
    /// </summary>
    [JsonPropertyName("url")]
    public string? Url { get; set; }
 
    /// <summary>
    /// Optional collection of HTTP headers to apply to the health probe request.
    /// </summary>
    public List<HttpProbeHeader>? Headers { get; set; }
}
 
internal sealed class HttpProbeHeader
{
    /// <summary>
    /// The name of the header.
    /// </summary>
    [JsonPropertyName("name")]
    public string? Name { get; set; }
 
    /// <summary>
    /// The (optional) value of the header.
    /// </summary>
    [JsonPropertyName("value")]
    public string? Value { get; set; }
}
 
internal sealed class ExecutableHealthProbeConfig
{
    // Labels to apply to probe Executable objects
    [JsonPropertyName("labels")]
    public IDictionary<string, string>? Labels { get; set; }
 
    // Annotations to apply to probe Executable objects
    [JsonPropertyName("annotations")]
    public IDictionary<string, string>? Annotations { get; set; }
 
    // Spec for the probe Executable
    [JsonPropertyName("spec")]
    public ExecutableSpec Spec { get; set; } = new ExecutableSpec();
}
 
internal sealed class HealthProbeSchedule
{
    /// <summary>
    /// The schedule kind for the health probe. For valid values see <see cref="HealthProbeScheduleKind"/>.
    /// </summary>
    [JsonPropertyName("kind")]
    public string? Kind { get; set; }
 
    /// <summary>
    /// The interval at which the health probe should be run.
    /// </summary>
    [JsonPropertyName("interval")]
    public TimeSpan? Interval { get; set; }
 
    /// <summary>
    /// Optional timeout for the health probe, which will mark the probe as failed if it takes longer than this duration.
    /// </summary>
    [JsonPropertyName("timeout")]
    public TimeSpan? Timeout { get; set; }
 
    /// <summary>
    /// Optional initial delay before the health probe starts running.
    /// </summary>
    [JsonPropertyName("initialDelay")]
    public TimeSpan? InitialDelay { get; set; }
}
 
internal static class HealthProbeScheduleKind
{
    /// <summary>
    /// The health probe is run continuously (based on the configured schedule).
    /// </summary>
    public const string Continuous = "Continuous";
 
    /// <summary>
    /// The health probe is run based on the configured schedule until it succeeds once, at
    /// which point it is considered successful for the remainder of the parent object's lifetime.
    /// This is useful for expensive startup probes such as checking for a database to be ready to
    /// accept requests.
    /// </summary>
    public const string UntilSuccess = "UntilSuccess";
}
 
internal sealed class HealthProbeResult
{
    /// <summary>
    /// The outcome of the health probe (success, failure, etc.)
    /// </summary>
    [JsonPropertyName("outcome")]
    public string? Outcome { get; set;}
 
    /// <summary>
    /// The timestamp when the health probe result was determined.
    /// </summary>
    [JsonPropertyName("timestamp")]
    public DateTime? Timestamp { get; set; }
 
    /// <summary>
    /// The name of the probe that generated this result. Corresponds to a specific <see cref="HealthProbe"/>
    /// configured in the spec of the parent resource.
    /// </summary>
    [JsonPropertyName("probeName")]
    public string? ProbeName { get; set; }
 
    /// <summary>
    /// Optional human-readable message describing the probe outcome.
    /// </summary>
    [JsonPropertyName("reason")]
    public string? Reason { get; set; }
}
 
internal static class HealthProbeOutcome
{
    /// <summary>
    /// The health probe was successful.
    /// </summary>
    public const string Success = "Success";
 
    /// <summary>
    /// The health probe failed.
    /// </summary>
    public const string Failure = "Failure";
 
    /// <summary>
    /// The health probe hasn't succeeded or failed yet.
    /// </summary>
    public const string Unknown = "Unknown";
}
 
internal static class Logs
{
    public const string StreamTypeStartupStdOut = "startup_stdout";
    public const string StreamTypeStartupStdErr = "startup_stderr";
    public const string StreamTypeStdOut = "stdout";
    public const string StreamTypeStdErr = "stderr";
    public const string StreamTypeAll = "all";
    public const string SubResourceName = "log";
}