Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.1.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
<PackageVersion Include="Minio" Version="6.0.1"/>
<!-- Open Telemetry -->
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.7.0-alpha.1" />
Expand All @@ -117,4 +118,4 @@
<!-- unit test dependencies -->
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.0.0" />
</ItemGroup>
</Project>
</Project>
68 changes: 68 additions & 0 deletions src/Aspire.Hosting/Minio/MinioBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides extension methods for adding Minio resources to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class MinioBuilderExtensions
{
private const string RootUserEnvVarName = "MINIO_ROOT_USER";
private const string RootPasswordEnvVarName = "MINIO_ROOT_PASSWORD";

/// <summary>
/// Adds a Minio container to the application model. The default image is "minio/minio" and the tag is "latest".
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="minioAdminPort">The host port for Minio Admin.</param>
/// <param name="minioPort">The host port for Minio.</param>
/// <param name="rootUser">The root user for the Minio server.</param>
/// <param name="rootPassword">The password for the Minio root user.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{MinioContainerResource}"/>.</returns>
public static IResourceBuilder<MinioContainerResource> 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);
}
}
18 changes: 18 additions & 0 deletions src/Aspire.Hosting/Minio/MinioContainerResource.cs
Original file line number Diff line number Diff line change
@@ -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)
{
/// <summary>
/// The Minio root user.
/// </summary>
public string RootUser { get; } = rootUser;

/// <summary>
/// The Minio root password.
/// </summary>
public string RootPassword { get; } = rootPassword;

}
41 changes: 41 additions & 0 deletions src/Aspire.Hosting/Minio/MinioServerResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A resource that represents a Minio server.
/// </summary>
/// <param name="name">The name of the resource.</param>
public class MinioServerResource(string name) : Resource(name), IResourceWithConnectionString, IResourceWithEnvironment
{
/// <summary>
/// Gets the connection string for the Minio server.
/// </summary>
/// <returns>A connection string for the Minio server in the form "http://host:port".</returns>
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;
}

/// <summary>
/// Gets the service port for the Minio server.
/// </summary>
/// <returns>The service port used by the Minio server.</returns>
public int? GetServicePort()
{
if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints))
{
throw new DistributedApplicationException($"Minio resource \"{Name}\" does not have endpoint annotation.");
}

return allocatedEndpoints.SingleOrDefault()?.Port;
}
}
20 changes: 20 additions & 0 deletions src/Components/Aspire.Minio/Aspire.Minio.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(NetCurrent)</TargetFramework>
<IsPackable>true</IsPackable>
<PackageTags>$(ComponentCommonPackageTags) Minio</PackageTags>
<Description>Minio based S3 client that integrates with Aspire, including healthchecks and metrics.</Description>
<NoWarn>$(NoWarn);SYSLIB1100</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Minio" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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.

}
}
18 changes: 18 additions & 0 deletions src/Components/Aspire.Minio/MinioConfiguration.cs
Original file line number Diff line number Diff line change
@@ -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;
}
54 changes: 54 additions & 0 deletions src/Components/Aspire.Minio/MinioHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -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<HealthCheckResult> 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);
}
}
}