diff --git a/Aspire.sln b/Aspire.sln index bd9fcaa7abb..941f9fbf352 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -203,6 +203,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CosmosEndToEnd.ApiService", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground.ServiceDefaults", "playground\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj", "{25208C6F-0A9D-4D60-9EDD-256C9891B1CD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Minio", "src\Components\Aspire.Minio\Aspire.Minio.csproj", "{18508B84-93C1-4F56-8538-355DBD5248EA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -541,6 +543,10 @@ Global {25208C6F-0A9D-4D60-9EDD-256C9891B1CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {25208C6F-0A9D-4D60-9EDD-256C9891B1CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {25208C6F-0A9D-4D60-9EDD-256C9891B1CD}.Release|Any CPU.Build.0 = Release|Any CPU + {18508B84-93C1-4F56-8538-355DBD5248EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18508B84-93C1-4F56-8538-355DBD5248EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18508B84-93C1-4F56-8538-355DBD5248EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18508B84-93C1-4F56-8538-355DBD5248EA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -635,6 +641,7 @@ Global {51DDD6BC-1D6C-466A-B509-FC49E3BD72E4} = {DBEDDF76-1C33-4943-8CCB-337A7D48AFF5} {EABB20A8-CDA2-4AFE-A5B1-FB631200CD64} = {DBEDDF76-1C33-4943-8CCB-337A7D48AFF5} {25208C6F-0A9D-4D60-9EDD-256C9891B1CD} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {18508B84-93C1-4F56-8538-355DBD5248EA} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/Directory.Packages.props b/Directory.Packages.props index aa25f554f07..3a906136885 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -95,6 +95,7 @@ + @@ -117,4 +118,4 @@ - \ No newline at end of file + diff --git a/src/Aspire.Hosting/Minio/MinioBuilderExtensions.cs b/src/Aspire.Hosting/Minio/MinioBuilderExtensions.cs new file mode 100644 index 00000000000..a94463d3311 --- /dev/null +++ b/src/Aspire.Hosting/Minio/MinioBuilderExtensions.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Publishing; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Minio resources to an . +/// +public static class MinioBuilderExtensions +{ + private const string RootUserEnvVarName = "MINIO_ROOT_USER"; + private const string RootPasswordEnvVarName = "MINIO_ROOT_PASSWORD"; + + /// + /// Adds a Minio container to the application model. The default image is "minio/minio" and the tag is "latest". + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port for Minio Admin. + /// The host port for Minio. + /// The root user for the Minio server. + /// The password for the Minio root user. + /// A reference to the . + public static IResourceBuilder AddMinioContainer( + this IDistributedApplicationBuilder builder, + string name, + string rootUser, + string rootPassword, + int minioPort = 9000, + int minioAdminPort = 9001) + { + var minioContainer = new MinioContainerResource(name, rootUser, rootPassword); + + return builder + .AddResource(minioContainer) + .WithManifestPublishingCallback(context => WriteMinioContainerToManifest(context, minioContainer)) + .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: minioPort, containerPort: 9000, name: "minio")) + .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: minioAdminPort, containerPort: 9001, name: "minio", uriScheme: "http")) + .WithAnnotation(new ContainerImageAnnotation { Image = "minio/minio", Tag = "latest" }) + .WithEnvironment("MINIO_ADDRESS", ":9000") + .WithEnvironment("MINIO_CONSOLE_ADDRESS", ":9001") + .WithEnvironment("MINIO_PROMETHEUS_AUTH_TYPE", "public") + .WithEnvironment(context => + { + if (context.PublisherName == "manifest") + { + context.EnvironmentVariables.Add(RootUserEnvVarName, $"{{{minioContainer.Name}.inputs.rootUser}}"); + context.EnvironmentVariables.Add(RootPasswordEnvVarName, $"{{{minioContainer.Name}.inputs.rootPassword}}"); + } + else + { + context.EnvironmentVariables.Add(RootUserEnvVarName, minioContainer.RootUser); + context.EnvironmentVariables.Add(RootPasswordEnvVarName, minioContainer.RootPassword); + } + }) + .WithArgs("server", "/data"); + } + + private static void WriteMinioContainerToManifest(ManifestPublishingContext context, MinioContainerResource resource) + { + // Want to see if there is interest + context.WriteContainer(resource); + } +} diff --git a/src/Aspire.Hosting/Minio/MinioContainerResource.cs b/src/Aspire.Hosting/Minio/MinioContainerResource.cs new file mode 100644 index 00000000000..f047bb0adc9 --- /dev/null +++ b/src/Aspire.Hosting/Minio/MinioContainerResource.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +public class MinioContainerResource(string name, string rootUser, string rootPassword) : ContainerResource(name) +{ + /// + /// The Minio root user. + /// + public string RootUser { get; } = rootUser; + + /// + /// The Minio root password. + /// + public string RootPassword { get; } = rootPassword; + +} diff --git a/src/Aspire.Hosting/Minio/MinioServerResource.cs b/src/Aspire.Hosting/Minio/MinioServerResource.cs new file mode 100644 index 00000000000..9b9592e3534 --- /dev/null +++ b/src/Aspire.Hosting/Minio/MinioServerResource.cs @@ -0,0 +1,41 @@ +// 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.ApplicationModel; + +/// +/// A resource that represents a Minio server. +/// +/// The name of the resource. +public class MinioServerResource(string name) : Resource(name), IResourceWithConnectionString, IResourceWithEnvironment +{ + /// + /// Gets the connection string for the Minio server. + /// + /// A connection string for the Minio server in the form "http://host:port". + public string? GetConnectionString() + { + if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + { + throw new DistributedApplicationException($"Minio resource \"{Name}\" does not have endpoint annotation."); + } + + // Assuming Minio runs on HTTP by default. Adjust if it uses HTTPS. + var endpoint = allocatedEndpoints.SingleOrDefault(); + return endpoint != null ? $"http://{endpoint.EndPointString}" : null; + } + + /// + /// Gets the service port for the Minio server. + /// + /// The service port used by the Minio server. + public int? GetServicePort() + { + if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + { + throw new DistributedApplicationException($"Minio resource \"{Name}\" does not have endpoint annotation."); + } + + return allocatedEndpoints.SingleOrDefault()?.Port; + } +} diff --git a/src/Components/Aspire.Minio/Aspire.Minio.csproj b/src/Components/Aspire.Minio/Aspire.Minio.csproj new file mode 100644 index 00000000000..ba691ab855f --- /dev/null +++ b/src/Components/Aspire.Minio/Aspire.Minio.csproj @@ -0,0 +1,20 @@ + + + + + $(NetCurrent) + true + $(ComponentCommonPackageTags) Minio + Minio based S3 client that integrates with Aspire, including healthchecks and metrics. + $(NoWarn);SYSLIB1100 + + + + + + + + + + + diff --git a/src/Components/Aspire.Minio/MinioClientBuilderExtensionMethods.cs b/src/Components/Aspire.Minio/MinioClientBuilderExtensionMethods.cs new file mode 100644 index 00000000000..604f0b5c71c --- /dev/null +++ b/src/Components/Aspire.Minio/MinioClientBuilderExtensionMethods.cs @@ -0,0 +1,41 @@ +// 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.Hosting; +using Aspire.Minio; +using Minio; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Extensions.Hosting; + +public static class MinioClientBuilderExtensionMethods +{ + public static void AddMinio(this IHostApplicationBuilder builder, string configurationSectionName) + { + + ArgumentNullException.ThrowIfNull(builder); + + // Obtain the configuration settings for the Minio client. + + MinioConfiguration minioSettings = new(); + + builder.Configuration.Bind(configurationSectionName, minioSettings); + + var endpoint = minioSettings.Endpoint; + var accessKey = minioSettings.AccessKey; + var secretKey = minioSettings.SecretKey; + + // Add the Minio client to the service collection. + builder.Services.AddMinio(configureClient => configureClient + .WithEndpoint(endpoint, 9000) + .WithSSL(false) + .WithCredentials(accessKey, secretKey)); + + // Add the Minio health check to the service collection. + + // Add the Minio tracing to the service collection. + + // Add the Minio metrics to the service collection. + + } +} diff --git a/src/Components/Aspire.Minio/MinioConfiguration.cs b/src/Components/Aspire.Minio/MinioConfiguration.cs new file mode 100644 index 00000000000..39de0dfc159 --- /dev/null +++ b/src/Components/Aspire.Minio/MinioConfiguration.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Minio; +internal sealed class MinioConfiguration +{ + public string? Endpoint { get; set; } + + public string? AccessKey { get; set; } + + public string? SecretKey { get; set; } + + public bool HealthChecks { get; set; } = true; + + public bool Tracing { get; set; } = true; + + public bool Metrics { get; set; } = true; +} diff --git a/src/Components/Aspire.Minio/MinioHealthCheck.cs b/src/Components/Aspire.Minio/MinioHealthCheck.cs new file mode 100644 index 00000000000..8ef64e8ce47 --- /dev/null +++ b/src/Components/Aspire.Minio/MinioHealthCheck.cs @@ -0,0 +1,54 @@ +// 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.Diagnostics.HealthChecks; + +namespace Aspire.HealthChecks.Minio; +public class MinioHealthCheck : IHealthCheck + { + private readonly HttpClient _httpClient; + private readonly Uri _minioHealthLiveUri; + private readonly Uri _minioHealthClusterUri; + private readonly Uri _minioHealthClusterReadUri; + + public MinioHealthCheck(HttpClient httpClient, string minioBaseUrl) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _minioHealthLiveUri = new Uri($"{minioBaseUrl}/minio/health/live"); + _minioHealthClusterUri = new Uri($"{minioBaseUrl}/minio/health/cluster"); + _minioHealthClusterReadUri = new Uri($"{minioBaseUrl}/minio/health/cluster/read"); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + // Node Liveness Check + var livenessResponse = await _httpClient.GetAsync(_minioHealthLiveUri, cancellationToken).ConfigureAwait(true); + if (!livenessResponse.IsSuccessStatusCode) + { + return HealthCheckResult.Unhealthy("MinIO is not responding to liveness checks"); + } + + // Cluster Write Quorum Check + var clusterWriteResponse = await _httpClient.GetAsync(_minioHealthClusterUri, cancellationToken).ConfigureAwait(true); + if (!clusterWriteResponse.IsSuccessStatusCode) + { + return HealthCheckResult.Unhealthy("MinIO cluster does not have write quorum"); + } + + // Cluster Read Quorum Check + var clusterReadResponse = await _httpClient.GetAsync(_minioHealthClusterReadUri, cancellationToken).ConfigureAwait(true); + if (!clusterReadResponse.IsSuccessStatusCode) + { + return HealthCheckResult.Unhealthy("MinIO cluster does not have read quorum"); + } + + return HealthCheckResult.Healthy("MinIO is healthy"); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("Error occurred while checking MinIO health", ex); + } + } + } +