From 6ed2fa204532d933e5376c0f40c2281b84ff610a Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Fri, 9 May 2025 02:13:20 +0300 Subject: [PATCH 01/18] Minio integration raw draft --- ...kit.Aspire.Hosting.Minio.ApiService.csproj | 13 ++ .../Program.cs | 24 ++++ .../Properties/launchSettings.json | 23 ++++ .../appsettings.json | 9 ++ ...oolkit.Aspire.Hosting.Minio.AppHost.csproj | 21 ++++ .../Program.cs | 15 +++ .../Properties/launchSettings.json | 29 +++++ .../appsettings.json | 9 ++ ...spire.Hosting.Minio.ServiceDefaults.csproj | 21 ++++ .../Extensions.cs | 117 ++++++++++++++++++ Directory.Packages.props | 1 + ...mmunityToolkit.Aspire.Hosting.Minio.csproj | 19 +++ .../MinioBuilderExtensions.cs | 78 ++++++++++++ .../MinioContainerResource.cs | 45 +++++++ .../MinioHealthCheck.cs | 54 ++++++++ .../README.md | 0 ...ommunityToolkit.Aspire.Minio.Client.csproj | 17 +++ .../MinioClientBuilderExtensionMethods.cs | 87 +++++++++++++ .../MinioClientSettings.cs | 93 ++++++++++++++ .../AppHostTests.cs | 26 ++++ ...yToolkit.Aspire.Hosting.Minio.Tests.csproj | 10 ++ .../MinioFunctionalTests.cs | 88 +++++++++++++ .../ResourceCreationTests.cs | 47 +++++++ .../TestDistributedApplicationBuilder.cs | 2 +- 24 files changed, 847 insertions(+), 1 deletion(-) create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj create mode 100644 CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Minio/README.md create mode 100644 src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj create mode 100644 src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs create mode 100644 src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj new file mode 100644 index 00000000..5a79e678 --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj @@ -0,0 +1,13 @@ + + + + enable + enable + + + + + + + + diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs new file mode 100644 index 00000000..8515e888 --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs @@ -0,0 +1,24 @@ +using CommunityToolkit.Aspire.Minio.Client; +using Microsoft.AspNetCore.Mvc; +using Minio; +using Minio.DataModel.Args; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.AddMinioClient("minio"); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +app.MapGet("/", () => "Hello World!"); + +app.MapGet("/search/{bucketId}", async (string bucketId, [FromServices] MinioClient minioClient) => +{ + return await minioClient.PresignedGetObjectAsync(new PresignedGetObjectArgs() + .WithBucket(bucketId)); +}); + +app.Run(); \ No newline at end of file diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json new file mode 100644 index 00000000..34090099 --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:52323", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7042;http://localhost:52323", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj new file mode 100644 index 00000000..2817c2ad --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + Exe + enable + enable + true + 6adfc361-47fd-4c05-88de-c95763714bb0 + + + + + + + + + + + + diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs new file mode 100644 index 00000000..d429da05 --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs @@ -0,0 +1,15 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var username = builder.AddParameter("user", "admin"); +var password = builder.AddParameter("password", "adminpasswordreallysecret", secret: true); + +var minio = builder.AddMinioContainer("minio", username, password); + +builder.AddProject("apiservice") + .WithReference(minio) + .WaitFor(minio) + .WithHttpHealthCheck("/health"); + +builder.Build().Run(); \ No newline at end of file diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..b2cfe754 --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:18120;http://localhost:12038", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:32476", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:12800" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15173", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19187", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:21252" + } + } + } +} diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj b/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj new file mode 100644 index 00000000..c9a4399a --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs b/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..b34d7625 --- /dev/null +++ b/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 9fba0aaf..01441641 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -64,6 +64,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj b/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj new file mode 100644 index 00000000..181409e4 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj @@ -0,0 +1,19 @@ + + + + MinioS3 Hosting integration + A .NET Aspire hosting integration for MinioS# + enable + enable + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs new file mode 100644 index 00000000..2ba97eeb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs @@ -0,0 +1,78 @@ +using System.Net.Sockets; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Publishing; +using CommunityToolkit.Aspire.Hosting.Minio; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +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 MinioO 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, + IResourceBuilder? rootUser = null, + IResourceBuilder? rootPassword = null, + int minioPort = 9000, + int minioConsolePort = 9001) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + var rootPasswordParameter = rootPassword?.Resource ?? + ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-rootPassword"); + + var minioContainer = new MinioContainerResource(name, rootUser?.Resource, rootPasswordParameter); + + var builderWithResource = builder + .AddResource(minioContainer) + .WithManifestPublishingCallback(context => WriteMinioContainerToManifest(context, minioContainer)) + .WithHttpEndpoint(targetPort: 9000, port: minioPort, name: MinioContainerResource.PrimaryEndpointName) + .WithHttpEndpoint(targetPort: 9001, port: minioConsolePort, name: "console") + .WithAnnotation(new ContainerImageAnnotation { Image = "minio/minio", Tag = "latest" }) + .WithEnvironment("MINIO_ADDRESS", $":{minioPort.ToString()}") + .WithEnvironment("MINIO_CONSOLE_ADDRESS", $":{minioConsolePort.ToString()}") + .WithEnvironment("MINIO_PROMETHEUS_AUTH_TYPE", "public") + .WithEnvironment(RootUserEnvVarName, minioContainer.RootUser?.Value ?? MinioContainerResource.DefaultUserName) + .WithEnvironment(RootPasswordEnvVarName, minioContainer.RootPassword.Value) + .WithArgs("server", "/data"); + + var endpoint = builderWithResource.Resource.GetEndpoint(MinioContainerResource.PrimaryEndpointName); + var healthCheckKey = $"{name}_check"; + + builder.Services.AddHealthChecks() + .Add(new HealthCheckRegistration( + healthCheckKey, + sp => new MinioHealthCheck(endpoint.Url), + failureStatus: default, + tags: default, + timeout: default)); + + builderWithResource.WithHealthCheck(healthCheckKey); + + return builderWithResource; + } + + private static async Task WriteMinioContainerToManifest(ManifestPublishingContext context, MinioContainerResource resource) + { + // Want to see if there is interest + await context.WriteContainerAsync(resource); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs new file mode 100644 index 00000000..f6572e0e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs @@ -0,0 +1,45 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a MiniO storage +/// +/// The name of the resource +/// A parameter that contains the MiniO server admin user name, or null to +/// A parameter that contains the Minio server admin password +public sealed class MinioContainerResource( + string name, + ParameterResource? rootUser, + ParameterResource rootPassword) : ContainerResource(name), + IResourceWithConnectionString +{ + internal const string PrimaryEndpointName = "http"; + internal const string DefaultUserName = "admin"; + + /// + /// The MiniO root user. + /// + public ParameterResource? RootUser { get; set; } = rootUser; + + /// + /// The MiniO root password. + /// + public ParameterResource RootPassword { get; } = rootPassword; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Minio. This endpoint is used for all API calls over HTTP. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + + internal ReferenceExpression RootUserNameReference => + RootUser is not null ? + ReferenceExpression.Create($"{RootUser}") : + ReferenceExpression.Create($"{DefaultUserName}"); + + /// + /// Gets the connection string expression for the Minio + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create($"Endpoint=http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs new file mode 100644 index 00000000..9cbccbd7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace CommunityToolkit.Aspire.Hosting.Minio; + +internal sealed class MinioHealthCheck : IHealthCheck +{ + private readonly HttpClient _httpClient; + private readonly Uri _minioHealthLiveUri; + private readonly Uri _minioHealthClusterUri; + private readonly Uri _minioHealthClusterReadUri; + + public MinioHealthCheck(string minioBaseUrl) + { + _httpClient = new HttpClient(); + _httpClient.BaseAddress = new Uri(minioBaseUrl); + _minioHealthLiveUri = new Uri("/minio/health/live", UriKind.Relative); + _minioHealthClusterUri = new Uri("/minio/health/cluster", UriKind.Relative); + _minioHealthClusterReadUri = new Uri("/minio/health/cluster/read", UriKind.Relative); + } + + /// + 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); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/README.md b/src/CommunityToolkit.Aspire.Hosting.Minio/README.md new file mode 100644 index 00000000..e69de29b diff --git a/src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj b/src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj new file mode 100644 index 00000000..213619e7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj @@ -0,0 +1,17 @@ + + + + MinioS3 Client integration + A .NET Aspire client integration for MinioS# + enable + enable + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs new file mode 100644 index 00000000..203e172c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs @@ -0,0 +1,87 @@ +using Microsoft.Extensions.Hosting; +using Minio; +using Microsoft.Extensions.Configuration; + +namespace CommunityToolkit.Aspire.Minio.Client; + +/// +/// Provides extension methods for registering MiniO-related services in an . +/// +public static class MinioClientBuilderExtensionMethods +{ + private const string DefaultConfigSectionName = "Aspire:Minio:Client"; + + /// + /// Adds Minio Client to ASPNet host + /// + /// + /// Name of the configuration settings section + /// The connection name to use to find a connection string. + /// An optional delegate that can be used for customizing options. It is invoked after the settings are read from the configuration. + public static void AddMinioClient( + this IHostApplicationBuilder builder, + string connectionName, + string? configurationSectionName = DefaultConfigSectionName, + Action? configureSettings = null) + { + var settings = GetMinioClientSettings(builder, configurationSectionName, configureSettings); + + builder.AddMinioInternal(connectionName, settings); + } + + private static void AddMinioInternal(this IHostApplicationBuilder builder, string connectionName, MinioClientSettings settings) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(settings); + + if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + settings.ParseConnectionString(connectionString); + } + + if (settings.Credentials is null) + { + var credentials = new MinioCredentials(); + credentials.SecretKey = builder.Configuration.GetValue("Parameters:user") ?? "admin"; + credentials.AccessKey = builder.Configuration.GetValue("Parameters:password") ?? "admin"; + + settings.Credentials = credentials; + } + + // Add the Minio client to the service collection. + builder.Services.AddMinio( + configureClient => + { + var client = configureClient + .WithEndpoint(settings.Endpoint) + .WithSSL(settings.UseSsl); + + if (settings.Credentials is not null) + client.WithCredentials(settings.Credentials.AccessKey, settings.Credentials.SecretKey); + + if (settings.UserAgentHeaderInfo is not null) + client.SetAppInfo(settings.UserAgentHeaderInfo.AppName, settings.UserAgentHeaderInfo.AppVersion); + + if (settings.SetTraceOn) + client.SetTraceOn(); + else + client.SetTraceOff(); + }, + settings.ServiceLifetime + ); + } + + private static MinioClientSettings GetMinioClientSettings(IHostApplicationBuilder builder, + string? configurationSectionName, + Action? configureSettings) + { + var settings = new MinioClientSettings(); + + builder.Configuration.Bind(configurationSectionName ?? DefaultConfigSectionName, settings); + + if (configureSettings is not null) + configureSettings.Invoke(settings); + + return settings; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs new file mode 100644 index 00000000..12c23b3e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Data.Common; + +namespace CommunityToolkit.Aspire.Minio.Client; + +/// +/// Minio client configuration +/// +public sealed class MinioClientSettings +{ + private const string ConnectionStringEndpoint = "Endpoint"; + + /// + /// Endpoint URL + /// + public Uri? Endpoint { get; set; } + + /// + public MinioCredentials? Credentials { get; set; } + + /// + /// Use ssl connection + /// + public bool UseSsl { get; set; } = false; + + /// + public HeaderAppInformation? UserAgentHeaderInfo { get; set; } + + /// + /// Minio client service lifetime + /// + public ServiceLifetime ServiceLifetime = ServiceLifetime.Singleton; + + /// + /// Turn on tracing + /// + public bool SetTraceOn { get; set; } = true; + + internal void ParseConnectionString(string? connectionString) + { + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + Endpoint = uri; + } + else + { + var connectionBuilder = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + if (connectionBuilder.TryGetValue(ConnectionStringEndpoint, out var endpoint) + && + Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var serviceUri)) + { + Endpoint = serviceUri; + } + + } + } +} + +/// +/// Sets app version and name. Used for constructing User-Agent header in all HTTP requests +/// +public class HeaderAppInformation +{ + /// + /// Set app name for MinioClient + /// + public string AppName { get; set; } = "CommunityToolkit.Aspire.Minio.Client"; + + /// + /// SetAppVersion for MinioClient + /// + public string AppVersion { get; set; } = "1.0"; +} + +/// +/// Minio credentials (access and secret keys) +/// +public class MinioCredentials +{ + /// + /// Minio Access Key + /// + public string AccessKey { get; set; } = string.Empty; + + /// + /// Minio Secret Key + /// + public string SecretKey { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs new file mode 100644 index 00000000..c4d6b1e9 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Aspire.Hosting; +using CommunityToolkit.Aspire.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Minio.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + [Fact] + public async Task ResourceStartsAndRespondsOk() + { + var resourceName = "apiservice"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + var response = await httpClient.GetAsync("/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj new file mode 100644 index 00000000..6bf6026b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs new file mode 100644 index 00000000..54c90974 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs @@ -0,0 +1,88 @@ +using Aspire.Components.Common.Tests; +using Aspire.Hosting; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.Hosting; +using Minio; +using Minio.DataModel.Args; +using Xunit.Abstractions; + +namespace CommunityToolkit.Aspire.Hosting.Minio.Tests; + +[RequiresDocker] +public class MinioFunctionalTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task StorageGetsCreatedAndUsable() + { + using var distributedApplicationBuilder = TestDistributedApplicationBuilder.Create(testOutputHelper); + var rootUser = "minioadmin"; + var port = 9000; + + var passwordParameter = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, + $"rootPassword"); + distributedApplicationBuilder.Configuration["Parameters:rootPassword"] = passwordParameter.Value; + var rootPasswordParameter = distributedApplicationBuilder.AddParameter(passwordParameter.Name); + + var minio = distributedApplicationBuilder + .AddMinioContainer("minio", + distributedApplicationBuilder.AddParameter("username", rootUser), + rootPasswordParameter, + minioPort: port); + + await using var app = await distributedApplicationBuilder.BuildAsync(); + + await app.StartAsync(); + + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceHealthyAsync(minio.Resource.Name); + + var webApplicationBuilder = Host.CreateApplicationBuilder(); + + webApplicationBuilder.Services.AddMinio(configureClient => configureClient + .WithEndpoint("localhost", port) + .WithCredentials(rootUser, passwordParameter.Value) + .WithSSL(false) + .Build()); + + using var host = webApplicationBuilder.Build(); + + await host.StartAsync(); + + var minioClient = host.Services.GetRequiredService(); + + var bucketName = "somebucket"; + + var mbArgs = new MakeBucketArgs() + .WithBucket(bucketName); + await minioClient.MakeBucketAsync(mbArgs); + + var res = await minioClient.ListBucketsAsync(); + + Assert.NotEmpty(res.Buckets); + + var bytearr = "Hey, I'm using minio client! It's awesome!"u8.ToArray(); + var stream = new MemoryStream(bytearr); + + var objectName = "someobj"; + var contentType = "text/plain"; + + var putObjectArgs = new PutObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithStreamData(stream) + .WithObjectSize(stream.Length) + .WithContentType(contentType); + + await minioClient.PutObjectAsync(putObjectArgs); + + var statObject = new StatObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName); + + var meta = await minioClient.StatObjectAsync(statObject); + + Assert.NotNull(meta); + Assert.Equal(contentType, meta.ContentType); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs new file mode 100644 index 00000000..159ed9cb --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs @@ -0,0 +1,47 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Minio.Tests; + +public class ResourceCreationTests +{ + [Fact] + public void MinioResourseGetsAdded() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddMinioContainer( "minio"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("minio", resource.Name); + } + + [Fact] + public void MinioResourceHasHealthCheck() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddMinioContainer( "minio"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + Assert.Equal("minio", resource.Name); + + var result = resource.TryGetAnnotationsOfType(out var annotations); + + Assert.True(result); + Assert.NotNull(annotations); + + Assert.Single(annotations); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs b/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs index bfdb69cf..be6d8909 100644 --- a/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs +++ b/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs @@ -79,7 +79,7 @@ void Configure(DistributedApplicationOptions applicationOptions, HostApplication var cfg = hostBuilderOptions.Configuration ??= new(); cfg.AddInMemoryCollection(new Dictionary { - ["DcpPublisher:RandomizePorts"] = "true", + ["DcpPublisher:RandomizePorts"] = "false", ["DcpPublisher:DeleteResourcesOnShutdown"] = "true", ["DcpPublisher:ResourceNameSuffix"] = $"{Random.Shared.Next():x}", }); From 7f9cead658d50c643c9990cabf5e4821a743b763 Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Thu, 15 May 2025 19:59:13 +0300 Subject: [PATCH 02/18] Tests draft --- .../Program.cs | 18 +++++++++++++++--- .../AppHostTests.cs | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs index 8515e888..293abffb 100644 --- a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs +++ b/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs @@ -15,10 +15,22 @@ app.MapGet("/", () => "Hello World!"); -app.MapGet("/search/{bucketId}", async (string bucketId, [FromServices] MinioClient minioClient) => +app.MapPut("/buckets/{bucketName}", async (string bucketName, [FromServices] IMinioClient minioClient) => { - return await minioClient.PresignedGetObjectAsync(new PresignedGetObjectArgs() - .WithBucket(bucketId)); + var mbArgs = new MakeBucketArgs() + .WithBucket(bucketName); + await minioClient.MakeBucketAsync(mbArgs); + + return Results.Ok(); +}); + +app.MapGet("/buckets/{bucketName}", async (string bucketName, [FromServices] IMinioClient minioClient) => +{ + var exists = await minioClient.BucketExistsAsync(new BucketExistsArgs().WithBucket(bucketName)); + + if(exists) + return Results.Ok(); + return Results.NotFound(); }); app.Run(); \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs index c4d6b1e9..25776b8f 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs @@ -23,4 +23,21 @@ public async Task ResourceStartsAndRespondsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + [Fact] + public async Task ApiServiceCreateData() + { + var resourceName = "apiservice"; + + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync("minio").WaitAsync(TimeSpan.FromMinutes(5)); + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + var bucketName = "somebucket"; + var createResponse = await httpClient.PutAsync($"/buckets/{bucketName}", null).WaitAsync(TimeSpan.FromMinutes(5)); + Assert.Equal(HttpStatusCode.OK, createResponse.StatusCode); + + var getResponse = await httpClient.GetAsync($"/buckets/{bucketName}"); + Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); + } } From f556a63084a1c0e32f7124c11ad062cf68789f83 Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Thu, 15 May 2025 20:40:16 +0300 Subject: [PATCH 03/18] Refinement + project structure + tests --- ...kit.Aspire.Hosting.Minio.ApiService.csproj | 2 +- .../Program.cs | 3 +- .../Properties/launchSettings.json | 0 .../appsettings.json | 0 ...oolkit.Aspire.Hosting.Minio.AppHost.csproj | 2 +- .../Program.cs | 4 +- .../Properties/launchSettings.json | 0 .../appsettings.json | 0 ...spire.Hosting.Minio.ServiceDefaults.csproj | 0 .../Extensions.cs | 0 .../MinioBuilderExtensions.cs | 28 ++--- .../MinioContainerImageTags.cs | 11 ++ .../MinioContainerResource.cs | 27 +++-- .../MinioHealthCheck.cs | 4 +- .../MinioClientBuilderExtensionMethods.cs | 95 ++++++++------- .../MinioClientSettings.cs | 19 ++- .../README.md | 114 ++++++++++++++++++ ...yToolkit.Aspire.Hosting.Minio.Tests.csproj | 2 +- .../MinioPublicApiTests.cs | 33 +++++ .../ResourceCreationTests.cs | 6 +- ...tyToolkit.Aspire.Minio.Client.Tests.csproj | 14 +++ .../ConfigurationTests.cs | 15 +++ .../ConformanceTests.cs | 90 ++++++++++++++ .../MinioClientPublicApiTests.cs | 35 ++++++ .../MinioContainerFeature.cs | 49 ++++++++ .../ConformanceTests.cs | 1 + 26 files changed, 474 insertions(+), 80 deletions(-) rename {CommunityToolkit.Aspire.Hosting.Minio.ApiService => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService}/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj (73%) rename {CommunityToolkit.Aspire.Hosting.Minio.ApiService => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService}/Program.cs (91%) rename {CommunityToolkit.Aspire.Hosting.Minio.ApiService => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService}/Properties/launchSettings.json (100%) rename {CommunityToolkit.Aspire.Hosting.Minio.ApiService => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService}/appsettings.json (100%) rename {CommunityToolkit.Aspire.Hosting.Minio.AppHost => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost}/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj (79%) rename {CommunityToolkit.Aspire.Hosting.Minio.AppHost => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost}/Program.cs (69%) rename {CommunityToolkit.Aspire.Hosting.Minio.AppHost => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost}/Properties/launchSettings.json (100%) rename {CommunityToolkit.Aspire.Hosting.Minio.AppHost => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost}/appsettings.json (100%) rename {CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults}/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj (100%) rename {CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults => examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults}/Extensions.cs (100%) create mode 100644 src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Minio.Client/README.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConfigurationTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConformanceTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioClientPublicApiTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioContainerFeature.cs diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj similarity index 73% rename from CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj index 5a79e678..bbc04055 100644 --- a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj +++ b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj @@ -7,7 +7,7 @@ - + diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs similarity index 91% rename from CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs index 293abffb..7aa497bf 100644 --- a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs +++ b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs @@ -1,5 +1,4 @@ -using CommunityToolkit.Aspire.Minio.Client; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Minio; using Minio.DataModel.Args; diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json similarity index 100% rename from CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Properties/launchSettings.json diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json similarity index 100% rename from CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/appsettings.json diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj similarity index 79% rename from CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj index 2817c2ad..a29ef1f7 100644 --- a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj +++ b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj @@ -15,7 +15,7 @@ - + diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs similarity index 69% rename from CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs index d429da05..6e7a526f 100644 --- a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs +++ b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Program.cs @@ -2,8 +2,8 @@ var builder = DistributedApplication.CreateBuilder(args); -var username = builder.AddParameter("user", "admin"); -var password = builder.AddParameter("password", "adminpasswordreallysecret", secret: true); +var username = builder.AddParameter("user", "minioadmin"); +var password = builder.AddParameter("password", "minioadmin", secret: true); var minio = builder.AddMinioContainer("minio", username, password); diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json similarity index 100% rename from CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/Properties/launchSettings.json diff --git a/CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json similarity index 100% rename from CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/appsettings.json diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj similarity index 100% rename from CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults.csproj diff --git a/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs similarity index 100% rename from CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs rename to examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ServiceDefaults/Extensions.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs index 2ba97eeb..08eb8f61 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs @@ -1,6 +1,4 @@ -using System.Net.Sockets; -using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Publishing; +using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.Minio; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -38,19 +36,20 @@ public static IResourceBuilder AddMinioContainer( var rootPasswordParameter = rootPassword?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-rootPassword"); + + var rootUserParameter = rootUser?.Resource ?? new ParameterResource("user", _ => MinioContainerResource.DefaultUserName); - var minioContainer = new MinioContainerResource(name, rootUser?.Resource, rootPasswordParameter); + var minioContainer = new MinioContainerResource(name, rootUserParameter, rootPasswordParameter); var builderWithResource = builder .AddResource(minioContainer) - .WithManifestPublishingCallback(context => WriteMinioContainerToManifest(context, minioContainer)) + .WithImage(MinioContainerImageTags.Image, MinioContainerImageTags.Tag) + .WithImageRegistry(MinioContainerImageTags.Registry) .WithHttpEndpoint(targetPort: 9000, port: minioPort, name: MinioContainerResource.PrimaryEndpointName) .WithHttpEndpoint(targetPort: 9001, port: minioConsolePort, name: "console") - .WithAnnotation(new ContainerImageAnnotation { Image = "minio/minio", Tag = "latest" }) .WithEnvironment("MINIO_ADDRESS", $":{minioPort.ToString()}") .WithEnvironment("MINIO_CONSOLE_ADDRESS", $":{minioConsolePort.ToString()}") - .WithEnvironment("MINIO_PROMETHEUS_AUTH_TYPE", "public") - .WithEnvironment(RootUserEnvVarName, minioContainer.RootUser?.Value ?? MinioContainerResource.DefaultUserName) + .WithEnvironment(RootUserEnvVarName, minioContainer.RootUser.Value) .WithEnvironment(RootPasswordEnvVarName, minioContainer.RootPassword.Value) .WithArgs("server", "/data"); @@ -60,7 +59,12 @@ public static IResourceBuilder AddMinioContainer( builder.Services.AddHealthChecks() .Add(new HealthCheckRegistration( healthCheckKey, - sp => new MinioHealthCheck(endpoint.Url), + sp => + { + var httpClient = sp.GetRequiredService().CreateClient("miniohealth"); + + return new MinioHealthCheck(endpoint.Url, httpClient); + }, failureStatus: default, tags: default, timeout: default)); @@ -69,10 +73,4 @@ public static IResourceBuilder AddMinioContainer( return builderWithResource; } - - private static async Task WriteMinioContainerToManifest(ManifestPublishingContext context, MinioContainerResource resource) - { - // Want to see if there is interest - await context.WriteContainerAsync(resource); - } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerImageTags.cs new file mode 100644 index 00000000..49801018 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerImageTags.cs @@ -0,0 +1,11 @@ +namespace CommunityToolkit.Aspire.Hosting.Minio; + +internal static class MinioContainerImageTags +{ + /// docker.io + public const string Registry = "docker.io"; + /// minio/minio + public const string Image = "minio/minio"; + /// RELEASE.2025-04-22T22-12-26Z + public const string Tag = "RELEASE.2025-04-22T22-12-26Z"; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs index f6572e0e..0dbf07a7 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs @@ -8,17 +8,17 @@ /// A parameter that contains the Minio server admin password public sealed class MinioContainerResource( string name, - ParameterResource? rootUser, + ParameterResource rootUser, ParameterResource rootPassword) : ContainerResource(name), IResourceWithConnectionString { internal const string PrimaryEndpointName = "http"; - internal const string DefaultUserName = "admin"; + internal const string DefaultUserName = "minioadmin"; /// /// The MiniO root user. /// - public ParameterResource? RootUser { get; set; } = rootUser; + public ParameterResource RootUser { get; set; } = rootUser; /// /// The MiniO root password. @@ -31,15 +31,22 @@ public sealed class MinioContainerResource( /// Gets the primary endpoint for the Minio. This endpoint is used for all API calls over HTTP. /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); - - internal ReferenceExpression RootUserNameReference => - RootUser is not null ? - ReferenceExpression.Create($"{RootUser}") : - ReferenceExpression.Create($"{DefaultUserName}"); /// /// Gets the connection string expression for the Minio /// - public ReferenceExpression ConnectionStringExpression => - ReferenceExpression.Create($"Endpoint=http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + public ReferenceExpression ConnectionStringExpression => GetConnectionString(); + + private ReferenceExpression GetConnectionString() + { + var builder = new ReferenceExpressionBuilder(); + + builder.Append( + $"Endpoint=http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + + builder.Append($";AccessKey={RootUser.Value}"); + builder.Append($";SecretKey={RootPassword.Value}"); + + return builder.Build(); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs index 9cbccbd7..110d231e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs @@ -9,9 +9,9 @@ internal sealed class MinioHealthCheck : IHealthCheck private readonly Uri _minioHealthClusterUri; private readonly Uri _minioHealthClusterReadUri; - public MinioHealthCheck(string minioBaseUrl) + public MinioHealthCheck(string minioBaseUrl, HttpClient? httpClient = null) { - _httpClient = new HttpClient(); + _httpClient = httpClient ?? new HttpClient(); _httpClient.BaseAddress = new Uri(minioBaseUrl); _minioHealthLiveUri = new Uri("/minio/health/live", UriKind.Relative); _minioHealthClusterUri = new Uri("/minio/health/cluster", UriKind.Relative); diff --git a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs index 203e172c..70e0ec2c 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs +++ b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs @@ -1,8 +1,10 @@ -using Microsoft.Extensions.Hosting; +using CommunityToolkit.Aspire.Minio.Client; using Minio; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; -namespace CommunityToolkit.Aspire.Minio.Client; +namespace Microsoft.Extensions.Hosting; /// /// Provides extension methods for registering MiniO-related services in an . @@ -14,64 +16,70 @@ public static class MinioClientBuilderExtensionMethods /// /// Adds Minio Client to ASPNet host /// - /// + /// The used to add services. /// Name of the configuration settings section /// The connection name to use to find a connection string. /// An optional delegate that can be used for customizing options. It is invoked after the settings are read from the configuration. public static void AddMinioClient( this IHostApplicationBuilder builder, - string connectionName, + string? connectionName = null, string? configurationSectionName = DefaultConfigSectionName, Action? configureSettings = null) { - var settings = GetMinioClientSettings(builder, configurationSectionName, configureSettings); + ArgumentNullException.ThrowIfNull(builder); + + var settings = GetMinioClientSettings(builder, connectionName, configurationSectionName, configureSettings); - builder.AddMinioInternal(connectionName, settings); + builder.AddMinioInternal(settings); } - private static void AddMinioInternal(this IHostApplicationBuilder builder, string connectionName, MinioClientSettings settings) + private static void AddMinioInternal(this IHostApplicationBuilder builder, MinioClientSettings settings) { - ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(settings); - - if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + + // Add the Minio client to the service collection. + void ConfigureClient(IMinioClient configureClient) { - settings.ParseConnectionString(connectionString); + var client = configureClient.WithEndpoint(settings.Endpoint) + .WithSSL(settings.UseSsl); + + if (settings.Credentials is not null) client.WithCredentials(settings.Credentials.AccessKey, settings.Credentials.SecretKey); + + if (settings.UserAgentHeaderInfo is not null) client.SetAppInfo(settings.UserAgentHeaderInfo.AppName, settings.UserAgentHeaderInfo.AppVersion); + + if (settings.SetTraceOn) + client.SetTraceOn(); + else + client.SetTraceOff(); } - if (settings.Credentials is null) + var minioClientFactory = new MinioClientFactory(ConfigureClient); + builder.Services.TryAddSingleton(minioClientFactory); + + IMinioClient GetClient() { - var credentials = new MinioCredentials(); - credentials.SecretKey = builder.Configuration.GetValue("Parameters:user") ?? "admin"; - credentials.AccessKey = builder.Configuration.GetValue("Parameters:password") ?? "admin"; + if (settings.Endpoint is null) + throw new InvalidOperationException("The MiniO endpoint must be provided either in configuration section, or as a part of connection string or settings delegate"); - settings.Credentials = credentials; + return minioClientFactory.CreateClient(); + } + + switch (settings.ServiceLifetime) + { + case ServiceLifetime.Singleton: + builder.Services.TryAddSingleton(_ => GetClient()); + break; + case ServiceLifetime.Scoped: + builder.Services.TryAddScoped(_ => GetClient()); + break; + case ServiceLifetime.Transient: + builder.Services.TryAddTransient(_ => GetClient()); + break; } - - // Add the Minio client to the service collection. - builder.Services.AddMinio( - configureClient => - { - var client = configureClient - .WithEndpoint(settings.Endpoint) - .WithSSL(settings.UseSsl); - - if (settings.Credentials is not null) - client.WithCredentials(settings.Credentials.AccessKey, settings.Credentials.SecretKey); - - if (settings.UserAgentHeaderInfo is not null) - client.SetAppInfo(settings.UserAgentHeaderInfo.AppName, settings.UserAgentHeaderInfo.AppVersion); - - if (settings.SetTraceOn) - client.SetTraceOn(); - else - client.SetTraceOff(); - }, - settings.ServiceLifetime - ); } private static MinioClientSettings GetMinioClientSettings(IHostApplicationBuilder builder, + string? connectionName, string? configurationSectionName, Action? configureSettings) { @@ -79,9 +87,14 @@ private static MinioClientSettings GetMinioClientSettings(IHostApplicationBuilde builder.Configuration.Bind(configurationSectionName ?? DefaultConfigSectionName, settings); - if (configureSettings is not null) - configureSettings.Invoke(settings); - + if (!string.IsNullOrEmpty(connectionName) && + builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + settings.ParseConnectionString(connectionString); + } + + configureSettings?.Invoke(settings); + return settings; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs index 12c23b3e..55788333 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs +++ b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Minio; using System.Data.Common; namespace CommunityToolkit.Aspire.Minio.Client; @@ -9,6 +10,8 @@ namespace CommunityToolkit.Aspire.Minio.Client; public sealed class MinioClientSettings { private const string ConnectionStringEndpoint = "Endpoint"; + private const string AccessKey = "AccessKey"; + private const string SecretKey = "SecretKey"; /// /// Endpoint URL @@ -32,9 +35,10 @@ public sealed class MinioClientSettings public ServiceLifetime ServiceLifetime = ServiceLifetime.Singleton; /// - /// Turn on tracing + /// Turn on tracing. + /// Isn't aspire tracing compatible yet. /// - public bool SetTraceOn { get; set; } = true; + public bool SetTraceOn { get; set; } = false; internal void ParseConnectionString(string? connectionString) { @@ -56,6 +60,17 @@ internal void ParseConnectionString(string? connectionString) Endpoint = serviceUri; } + if (connectionBuilder.TryGetValue(AccessKey, out var accessKey) + && + connectionBuilder.TryGetValue(SecretKey, out var secretKey) + && + !string.IsNullOrEmpty(accessKey.ToString()) && !string.IsNullOrEmpty(secretKey.ToString())) + { + Credentials = new MinioCredentials + { + AccessKey = accessKey.ToString()!, SecretKey = secretKey.ToString()! + }; + } } } } diff --git a/src/CommunityToolkit.Aspire.Minio.Client/README.md b/src/CommunityToolkit.Aspire.Minio.Client/README.md new file mode 100644 index 00000000..1c008125 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Minio.Client/README.md @@ -0,0 +1,114 @@ +# CommunityToolkit.Aspire.Minio.Client + +Registers a [MiniOClient](https://github.com/minio/minio-dotnet) in the DI container for connecting to MiniO. + +## Getting started + +### Prerequisites + +- Minio or other S3-compatible storage. + +### Install the package + +Install the .NET Aspire Minio Client library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Minio.Client +``` + +## Usage example + +In the _Program.cs_ file of your project, call the `AddMinioClient` extension method to register a `MinioClient` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddMinioClient(); +``` + +## Configuration + +The .NET Aspire Minio Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddMinioClient()`: + +```csharp +builder.AddMeilisearchClient("minio"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section: + +```json +{ + "ConnectionStrings": { + "minio": "Endpoint=http://localhost:19530/;MasterKey=123456!@#$%" + } +} +``` + +### Use configuration providers + +The .NET Aspire Meilisearch Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `MeilisearchClientSettings` from configuration by using the `Aspire:Meilisearch:Client` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "Meilisearch": { + "Client": { + "Endpoint": "http://localhost:19530/", + "MasterKey": "123456!@#$%" + } + } + } +} +``` + +### Use inline delegates + +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code: + +```csharp +builder.AddMeilisearchClient("meilisearch", settings => settings.MasterKey = "123456!@#$%"); +``` + +## AppHost extensions + +In your AppHost project, install the `CommunityToolkit.Aspire.Hosting.Minio` library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.Minio +``` + +Then, in the _Program.cs_ file of `AppHost`, register a MiniO host and consume the connection using the following methods: + +```csharp +var minio = builder.AddMinioContainer("minio"); + +var myService = builder.AddProject() + .WithReference(minio); +``` + +The `WithReference` method configures a connection in the `MyService` project named `minio`. In the _Program.cs_ file of `MyService`, the Minio connection can be consumed using: + +```csharp +builder.AddMinioClient("minio"); +``` + +Then, in your service, inject `MeilisearchClient` and use it to interact with the Meilisearch API: + +```csharp +public class MyService(MeilisearchClient meilisearchClient) +{ + // ... +} +``` + +## Additional documentation + +- https://github.com/minio/minio-dotnet +- https://min.io/docs/minio/linux/developers/dotnet/minio-dotnet.html + +## Feedback & contributing + +https://github.com/CommunityToolkit/Aspire + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj index 6bf6026b..7568cadb 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs new file mode 100644 index 00000000..4473527b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs @@ -0,0 +1,33 @@ +// 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; + +namespace CommunityToolkit.Aspire.Hosting.Minio.Tests; + +public class MinioPublicApiTests +{ + [Fact] + public void AddMinioContainerShouldThrowWhenBuilderIsNull() + { + IDistributedApplicationBuilder builder = null!; + const string name = "Minio"; + + var action = () => builder.AddMinioContainer(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddMinioContainerShouldThrowWhenNameIsNull() + { + IDistributedApplicationBuilder builder = new DistributedApplicationBuilder([]); + string name = null!; + + var action = () => builder.AddMinioContainer(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs index 159ed9cb..87201daf 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ResourceCreationTests.cs @@ -5,11 +5,11 @@ namespace CommunityToolkit.Aspire.Hosting.Minio.Tests; public class ResourceCreationTests { [Fact] - public void MinioResourseGetsAdded() + public void MinioResourceGetsAdded() { var builder = DistributedApplication.CreateBuilder(); - builder.AddMinioContainer( "minio"); + builder.AddMinioContainer("minio"); using var app = builder.Build(); @@ -25,7 +25,7 @@ public void MinioResourceHasHealthCheck() { var builder = DistributedApplication.CreateBuilder(); - builder.AddMinioContainer( "minio"); + builder.AddMinioContainer("minio"); using var app = builder.Build(); diff --git a/tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj new file mode 100644 index 00000000..14130e6d --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConfigurationTests.cs b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConfigurationTests.cs new file mode 100644 index 00000000..de3e563f --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConfigurationTests.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.Minio.Client.Tests; + +public class ConfigurationTests +{ + [Fact] + public void EndpointIsNullByDefault() => + Assert.Null(new MinioClientSettings().Endpoint); + + [Fact] + public void CredentialsIsNullByDefault() => + Assert.Null(new MinioClientSettings().Credentials); +} diff --git a/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConformanceTests.cs new file mode 100644 index 00000000..5012126f --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ConformanceTests.cs @@ -0,0 +1,90 @@ +using Aspire.Components.Common.Tests; +using Aspire.Components.ConformanceTests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Minio; +using Minio.DataModel.Args; + +namespace CommunityToolkit.Aspire.Minio.Client.Tests; + +public class ConformanceTests(MinioContainerFeature minioContainerFeature) : ConformanceTests, IClassFixture +{ + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + protected override string ActivitySourceName => string.Empty; + + protected override string[] RequiredLogCategories => []; + + protected override bool CanConnectToServer => RequiresDockerAttribute.IsSupported; + + protected override bool SupportsKeyedRegistrations => false; + + protected override string ConfigurationSectionName => "Aspire:Minio:Client"; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + { + var endpoint = RequiresDockerAttribute.IsSupported ? + $"{minioContainerFeature.GetContainerEndpoint()}" : + "Endpoint=http://localhost:9000"; + + const string accessKey = "minioadmin"; + const string secretKey = "minioadmin"; + + var connString = $"{endpoint};AccessKey={accessKey}; SecretKey={secretKey}"; + + configuration.AddInMemoryCollection( + [ + new KeyValuePair(CreateConfigKey(ConfigurationSectionName, null, suffix: "Endpoint"), endpoint), + new KeyValuePair(CreateConfigKey(ConfigurationSectionName+":Credentials", null, suffix: "AccessKey"), accessKey), + new KeyValuePair(CreateConfigKey(ConfigurationSectionName+":Credentials", null, suffix: "SecretKey"), secretKey), + new KeyValuePair($"ConnectionStrings:{key ?? "minio"}", connString) + ]); + } + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + builder.AddMinioClient("minio", configureSettings: configure); + } + + protected override void SetHealthCheck(MinioClientSettings options, bool enabled) + { + throw new NotImplementedException(); + } + + protected override void SetMetrics(MinioClientSettings options, bool enabled) + { + throw new NotImplementedException(); + } + + protected override void SetTracing(MinioClientSettings options, bool enabled) + { + throw new NotImplementedException(); + } + + protected override void TriggerActivity(IMinioClient service) + { + using var source = new CancellationTokenSource(100); + + if (service is MinioClient minioClient) + { + minioClient.ListBucketsAsync(source.Token).Wait(); + } + } + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Minio": { + "Client": { + "Endpoint": "http://localhost:9001", + "Credentials": { + "AccessKey": "minioadmin", + "SecretKey": "minioadmin", + } + } + } + } + } + """; + +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioClientPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioClientPublicApiTests.cs new file mode 100644 index 00000000..07a2b9f1 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioClientPublicApiTests.cs @@ -0,0 +1,35 @@ +// 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; + +namespace CommunityToolkit.Aspire.Minio.Client.Tests; + +public class MinioClientPublicApiTests +{ + [Fact] + public void AddMinioClientShouldThrowWhenBuilderIsNull() + { + IHostApplicationBuilder builder = null!; + + var configurationSectionName = "minio"; + + var action = () => builder.AddMinioClient(configurationSectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddMinioClientShouldThrowWhenConfigurationSectionNameIsNull() + { + IHostApplicationBuilder builder = null!; + + string? configurationSectionName = null; + + var action = () => builder.AddMinioClient(configurationSectionName: configurationSectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } +} diff --git a/tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioContainerFeature.cs b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioContainerFeature.cs new file mode 100644 index 00000000..3feee2c6 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/MinioContainerFeature.cs @@ -0,0 +1,49 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Hosting.Minio; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace CommunityToolkit.Aspire.Minio.Client.Tests; + +public class MinioContainerFeature : IAsyncLifetime +{ + private const int MinioPort = 9000; + public IContainer? Container { get; private set; } + public string GetContainerEndpoint() + { + if (Container is null) + { + throw new InvalidOperationException("The test container was not initialized."); + } + var endpoint = new UriBuilder("http", Container.Hostname, Container.GetMappedPublicPort(MinioPort)).ToString(); + return endpoint; + } + + public async Task InitializeAsync() + { + if (RequiresDockerAttribute.IsSupported) + { + Container = new ContainerBuilder() + .WithImage($"{MinioContainerImageTags.Registry}/{MinioContainerImageTags.Image}:{MinioContainerImageTags.Tag}") + .WithPortBinding(MinioPort, true) + .WithCommand("server", "/data") + .WithEnvironment("MINIO_ADDRESS", $":{9000.ToString()}") + .WithEnvironment("MINIO_ROOT_USER", "minioadmin") + .WithEnvironment("MINIO_ROOT_PASSWORD", "minioadmin") + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => request.ForPath("/minio/health/ready") + .ForPort(MinioPort))) + .Build(); + + await Container.StartAsync(); + } + } + + public async Task DisposeAsync() + { + if (Container is not null) + { + await Container.DisposeAsync(); + } + } +} diff --git a/tests/CommunityToolkit.Aspire.Testing/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.Testing/ConformanceTests.cs index b7abaa64..ec0c5ca6 100644 --- a/tests/CommunityToolkit.Aspire.Testing/ConformanceTests.cs +++ b/tests/CommunityToolkit.Aspire.Testing/ConformanceTests.cs @@ -356,6 +356,7 @@ public void ConfigurationSchemaInvalidJsonConfigTest() public void ConnectionInformationIsDelayValidated(bool useKey) { SetupConnectionInformationIsDelayValidated(); + SkipIfKeyedRegistrationIsNotSupported(useKey); var builder = Host.CreateEmptyApplicationBuilder(null); From 8038e308831c32136d6d455cdf04949a281d0460 Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Sun, 18 May 2025 16:58:35 +0300 Subject: [PATCH 04/18] Documentation draft --- CODEOWNERS | 12 +++++- .../README.md | 37 +++++++++++++++++++ .../README.md | 31 +++++++++------- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c6f3c761..b67fb7f0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -102,4 +102,14 @@ # CommunityToolkit.Aspire.Hosting.SqlServer.Extensions /src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/ @Alirexaa /tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/ @Alirexaa -/examples/sqlserver-ext/ @Alirexaa \ No newline at end of file +/examples/sqlserver-ext/ @Alirexaa + +# CommunityToolkit.Aspire.Minio.Client +# CommunityToolkit.Aspire.Hosting.Minio + +/examples/minio/ @Harold-Morgan +/src/CommunityToolkit.Aspire.Hosting.Minio/ @Harold-Morgan +/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ @Harold-Morgan + +/src/CommunityToolkit.Aspire.Minio.Client/ @Harold-Morgan +/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ @Harold-Morgan \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/README.md b/src/CommunityToolkit.Aspire.Hosting.Minio/README.md index e69de29b..f9a6d1a3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/README.md @@ -0,0 +1,37 @@ +# CommunityToolkit.Aspire.Hosting.MinoO library + +Provides extension methods and resource definitions for the .NET Aspire AppHost to support running [MiniO](https://min.io/) containers. + +## Getting Started + +### Install the package + +In your AppHost project, install the package using the following command: + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.Minio +``` + +### Example usage + +Then, in the _Program.cs_ file of `AppHost`, add a Minio resource and consume the connection using the following methods: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var minio = builder.AddMinio("minio"); + +var myService = builder.AddProject() + .WithReference(minio); + +builder.Build().Run(); +``` + +## Additional Information + +https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-minio + +## Feedback & contributing + +https://github.com/CommunityToolkit/Aspire + diff --git a/src/CommunityToolkit.Aspire.Minio.Client/README.md b/src/CommunityToolkit.Aspire.Minio.Client/README.md index 1c008125..c3d446f7 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/README.md +++ b/src/CommunityToolkit.Aspire.Minio.Client/README.md @@ -10,7 +10,7 @@ Registers a [MiniOClient](https://github.com/minio/minio-dotnet) in the DI conta ### Install the package -Install the .NET Aspire Minio Client library with [NuGet](https://www.nuget.org): +Install the .NET Aspire MiniO Client library with [NuGet](https://www.nuget.org): ```dotnetcli dotnet add package CommunityToolkit.Aspire.Minio.Client @@ -26,14 +26,14 @@ builder.AddMinioClient(); ## Configuration -The .NET Aspire Minio Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project. +The .NET Aspire MiniO Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project. ### Use a connection string When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddMinioClient()`: ```csharp -builder.AddMeilisearchClient("minio"); +builder.AddMinioClient("minio"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section: @@ -41,22 +41,26 @@ And then the connection string will be retrieved from the `ConnectionStrings` co ```json { "ConnectionStrings": { - "minio": "Endpoint=http://localhost:19530/;MasterKey=123456!@#$%" + "minio": "Endpoint=http://localhost:9001/;AccessKey=minioAdmin;SecretKey=minioAdmin" } } ``` ### Use configuration providers -The .NET Aspire Meilisearch Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `MeilisearchClientSettings` from configuration by using the `Aspire:Meilisearch:Client` key. Example `appsettings.json` that configures some of the options: +The .NET Aspire MiniO Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). +It loads the `MinioClientSettings` from configuration by using the `Aspire:Minio:Client` key. +This key can be overriden by using the `configurationSectionName` method parameter. +Example `appsettings.json` that configures some of the options: ```json { "Aspire": { - "Meilisearch": { + "Minio": { "Client": { - "Endpoint": "http://localhost:19530/", - "MasterKey": "123456!@#$%" + "Endpoint": "http://localhost:9001/", + "AccessKey": "minioAdmin", + "SecretKey": "minioAdmin" } } } @@ -65,10 +69,10 @@ The .NET Aspire Meilisearch Client integration supports [Microsoft.Extensions.Co ### Use inline delegates -Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code: +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code: ```csharp -builder.AddMeilisearchClient("meilisearch", settings => settings.MasterKey = "123456!@#$%"); +builder.AddMinioClient("minio", configureSettings: settings => settings.SecretKey = "minioAdmin"); ``` ## AppHost extensions @@ -88,16 +92,17 @@ var myService = builder.AddProject() .WithReference(minio); ``` -The `WithReference` method configures a connection in the `MyService` project named `minio`. In the _Program.cs_ file of `MyService`, the Minio connection can be consumed using: +The `WithReference` method configures a connection in the `MyService` project named `minio`. +In the _Program.cs_ file of `MyService`, the MiniO connection can be consumed using: ```csharp builder.AddMinioClient("minio"); ``` -Then, in your service, inject `MeilisearchClient` and use it to interact with the Meilisearch API: +Then, in your service, inject `IMinioClient` and use it to interact with the MiniO or other S3 compatible API: ```csharp -public class MyService(MeilisearchClient meilisearchClient) +public class MyService(IMinioClient minioClient) { // ... } From a3cba124c4bd6ad88cb7991b777ec84c0d2b3a7e Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Sun, 18 May 2025 17:14:51 +0300 Subject: [PATCH 05/18] Tests added to tests.yaml --- .github/workflows/tests.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 77fd59e1..54fc6a43 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -51,6 +51,7 @@ jobs: Hosting.SqlServer.Extensions.Tests, Hosting.Sqlite.Tests, Hosting.k6.Tests, + Hosting.Minio.Tests, # Client integration tests EventStore.Tests, @@ -61,6 +62,7 @@ jobs: Microsoft.EntityFrameworkCore.Sqlite.Tests, OllamaSharp.Tests, RavenDB.Client.Tests, + Minio.Client.Tests, ] steps: From 28f7542e6bd89197ec013cf908aefa288df00fd0 Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Sat, 24 May 2025 10:54:52 +0300 Subject: [PATCH 06/18] Fix ubuntu build --- .../CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj | 2 +- .../CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj | 6 +++--- .../CommunityToolkit.Aspire.Hosting.Minio.csproj | 6 +----- .../CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj | 2 +- .../CommunityToolkit.Aspire.Minio.Client.Tests.csproj | 3 +++ 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj index bbc04055..50718ea7 100644 --- a/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj +++ b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/CommunityToolkit.Aspire.Hosting.Minio.ApiService.csproj @@ -6,8 +6,8 @@ - + diff --git a/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj index a29ef1f7..18cd3dcd 100644 --- a/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj +++ b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.AppHost/CommunityToolkit.Aspire.Hosting.Minio.AppHost.csproj @@ -1,12 +1,12 @@  - + Exe enable enable true - 6adfc361-47fd-4c05-88de-c95763714bb0 + bfe6b134-1a06-4449-a146-ba3cdb0d02a5 @@ -14,8 +14,8 @@ - + diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj b/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj index 181409e4..ee80e7c4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj @@ -2,7 +2,7 @@ MinioS3 Hosting integration - A .NET Aspire hosting integration for MinioS# + A .NET Aspire hosting integration for MiniO enable enable @@ -12,8 +12,4 @@ - - - - diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj index 7568cadb..4da9a012 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/CommunityToolkit.Aspire.Hosting.Minio.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj index 14130e6d..cc38672d 100644 --- a/tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj @@ -1,7 +1,9 @@  + + @@ -11,4 +13,5 @@ + From 20e93db61254a2cbd724b8c217911c25e40f8f1e Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Sat, 24 May 2025 11:30:14 +0300 Subject: [PATCH 07/18] README.md final draft --- README.md | 84 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9cc9ac47..cfca5e2f 100644 --- a/README.md +++ b/README.md @@ -12,43 +12,44 @@ All features are contributed by you, our amazing .NET community, and maintained This repository contains the source code for the .NET Aspire Community Toolkit, a collection of community created Integrations and extensions for [.NET Aspire](https://aka.ms/dotnet/aspire). -| Package | Description | -|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| - **Learn More**: [`Hosting.Azure.StaticWebApps`][swa-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields]][swa-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields-preview]][swa-nuget-preview] | A hosting integration for the [Azure Static Web Apps emulator](https://learn.microsoft.com/azure/static-web-apps/static-web-apps-cli-overview) (Note: this does not support deployment of a project to Azure Static Web Apps). | -| - **Learn More**: [`Hosting.Golang`][golang-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields]][golang-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields-preview]][golang-nuget-preview] | A hosting integration Golang apps. | -| - **Learn More**: [`Hosting.Java`][java-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields]][java-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields-preview]][java-nuget-preview] | An integration for running Java code in .NET Aspire either using the local JDK or using a container. | -| - **Learn More**: [`Hosting.NodeJS.Extensions`][nodejs-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.NodeJS.Extensions][nodejs-ext-shields]][nodejs-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.NodeJS.Extensions][nodejs-ext-shields-preview]][nodejs-ext-nuget-preview] | An integration that contains some additional extensions for running Node.js applications | -| - **Learn More**: [`Hosting.Ollama`][ollama-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields]][ollama-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields-preview]][ollama-nuget-preview] | An Aspire hosting integration leveraging the [Ollama](https://ollama.com) container with support for downloading a model on startup. | -| - **Learn More**: [`OllamaSharp`][ollama-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields]][ollamasharp-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields-preview]][ollamasharp-nuget-preview] | An Aspire client integration for the [OllamaSharp](https://github.com/awaescher/OllamaSharp) package. | -| - **Learn More**: [`Hosting.Meilisearch`][meilisearch-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields]][meilisearch-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields-preview]][meilisearch-nuget-preview] | An Aspire hosting integration leveraging the [Meilisearch](https://meilisearch.com) container. | -| - **Learn More**: [`Meilisearch`][meilisearch-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields]][meilisearch-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields-preview]][meilisearch-client-nuget-preview] | An Aspire client integration for the [Meilisearch](https://github.com/meilisearch/meilisearch-dotnet) package. | -| - **Learn More**: [`Hosting.Azure.DataApiBuilder`][dab-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields]][dab-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields-preview]][dab-nuget-preview] | A hosting integration for the [Azure Data API builder](https://learn.microsoft.com/en-us/azure/data-api-builder/overview). | -| - **Learn More**: [`Hosting.Deno`][deno-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields]][deno-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields-preview]][deno-nuget-preview] | A hosting integration for the Deno apps. | -| - **Learn More**: [`Hosting.SqlDatabaseProjects`][sql-database-projects-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields]][sql-database-projects-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields-preview]][sql-database-projects-nuget-preview] | A hosting integration for the SQL Databases Projects. | -| - **Learn More**: [`Hosting.Rust`][rust-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields]][rust-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields-preview]][rust-nuget-preview] | A hosting integration for the Rust apps. | -| - **Learn More**: [`Hosting.Bun`][bun-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields]][bun-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields-preview]][bun-nuget-preview] | A hosting integration for the Bun apps. | -| - **Learn More**: [`Hosting.Python.Extensions`][python-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Python.Extensions][python-ext-shields]][python-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Python.Extensions][python-ext-shields-preview]][python-ext-nuget-preview] | An integration that contains some additional extensions for running python applications | -| - **Learn More**: [`Hosting.EventStore`][eventstore-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields]][eventstore-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields-preview]][eventstore-nuget-preview] | An Aspire hosting integration leveraging the [EventStore](https://eventstore.com) container. | -| - **Learn More**: [`EventStore`][eventstore-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields]][eventstore-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields-preview]][eventstore-client-nuget-preview] | An Aspire client integration for the [EventStore](https://github.com/EventStore/EventStore-Client-Dotnet) package. | -| - **Learn More**: [`Hosting.ActiveMQ`][activemq-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields]][activemq-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields-preview]][activemq-nuget-preview] | An Aspire hosting integration leveraging the [ActiveMq](https://activemq.apache.org) container. | -| - **Learn More**: [`Hosting.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields]][sqlite-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields-preview]][sqlite-hosting-nuget-preview] | An Aspire hosting integration to setup a SQLite database with optional SQLite Web as a dev UI. | -| - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. | -| - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. | -| - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. | -| - **Learn More**: [`Hosting.Azure.Dapr.Redis`][dapr-azureredis-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. | -| - **Learn More**: [`Hosting.RavenDB`][ravendb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields]][ravendb-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields-preview]][ravendb-nuget-preview] | An Aspire integration leveraging the [RavenDB](https://ravendb.net/) container. | -| - **Learn More**: [`RavenDB.Client`][ravendb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields]][ravendb-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields-preview]][ravendb-client-nuget-preview] | An Aspire client integration for the [RavenDB.Client](https://www.nuget.org/packages/RavenDB.client) package. | -| - **Learn More**: [`Hosting.GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields]][go-feature-flag-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields-preview]][go-feature-flag-nuget-preview] | An Aspire hosting integration leveraging the [GoFeatureFlag](https://gofeatureflag.org/) container. | -| - **Learn More**: [`GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields]][go-feature-flag-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields-preview]][go-feature-flag-client-nuget-preview] | An Aspire client integration for the [GoFeatureFlag](https://github.com/open-feature/dotnet-sdk-contrib/tree/main/src/OpenFeature.Contrib.Providers.GOFeatureFlag) package. | -| - **Learn More**: [`Hosting.MongoDB.Extensions`][mongodb-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.MongoDB.Extensions][mongodb-ext-shields]][mongodb-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MongoDB.Extensions][mongodb-ext-shields-preview]][mongodb-ext-nuget-preview] | An integration that contains some additional extensions for hosting MongoDB container. | -| - **Learn More**: [`Hosting.PostgreSQL.Extensions`][postgres-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.PostgreSQL.Extensions][postgres-ext-shields]][postgres-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions][postgres-ext-shields-preview]][postgres-ext-nuget-preview] | An integration that contains some additional extensions for hosting PostgreSQL container. | -| - **Learn More**: [`Hosting.Redis.Extensions`][redis-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Redis.Extensions][redis-ext-shields]][redis-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Redis.Extensions][redis-ext-shields-preview]][redis-ext-nuget-preview] | An integration that contains some additional extensions for hosting Redis container. | -| - **Learn More**: [`Hosting.SqlServer.Extensions`][sqlserver-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.SqlServer.Extensions][sqlserver-ext-shields]][sqlserver-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.SqlServer.Extensions][sqlserver-ext-shields-preview]][sqlserver-ext-nuget-preview] | An integration that contains some additional extensions for hosting SqlServer container. | -| - **Learn More**: [`Hosting.LavinMQ`][lavinmq-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields]][lavinmq-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields-preview]][lavinmq-nuget-preview] | An Aspire hosting integration for [LavinMQ](https://www.lavinmq.com). | -| - **Learn More**: [`Hosting.MailPit`][mailpit-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields]][mailpit-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields-preview]][mailpit-ext-nuget-preview] | An Aspire integration leveraging the [MailPit](https://mailpit.axllent.org/) container. | -| - **Learn More**: [`Hosting.k6`][k6-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields]][k6-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields-preview]][k6-nuget-preview] | An Aspire integration leveraging the [Grafana k6](https://k6.io/) container. | -| - **Learn More**: [`Hosting.MySql.Extensions`][mysql-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.MySql.Extensions][mysql-ext-shields]][mysql-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MySql.Extensions][mysql-ext-shields-preview]][mysql-ext-nuget-preview] | An integration that contains some additional extensions for hosting MySql container. | - +| Package | Description | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| - **Learn More**: [`Hosting.Azure.StaticWebApps`][swa-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields]][swa-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields-preview]][swa-nuget-preview] | A hosting integration for the [Azure Static Web Apps emulator](https://learn.microsoft.com/azure/static-web-apps/static-web-apps-cli-overview) (Note: this does not support deployment of a project to Azure Static Web Apps). | +| - **Learn More**: [`Hosting.Golang`][golang-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields]][golang-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields-preview]][golang-nuget-preview] | A hosting integration Golang apps. | +| - **Learn More**: [`Hosting.Java`][java-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields]][java-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields-preview]][java-nuget-preview] | An integration for running Java code in .NET Aspire either using the local JDK or using a container. | +| - **Learn More**: [`Hosting.NodeJS.Extensions`][nodejs-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.NodeJS.Extensions][nodejs-ext-shields]][nodejs-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.NodeJS.Extensions][nodejs-ext-shields-preview]][nodejs-ext-nuget-preview] | An integration that contains some additional extensions for running Node.js applications | +| - **Learn More**: [`Hosting.Ollama`][ollama-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields]][ollama-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields-preview]][ollama-nuget-preview] | An Aspire hosting integration leveraging the [Ollama](https://ollama.com) container with support for downloading a model on startup. | +| - **Learn More**: [`OllamaSharp`][ollama-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields]][ollamasharp-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields-preview]][ollamasharp-nuget-preview] | An Aspire client integration for the [OllamaSharp](https://github.com/awaescher/OllamaSharp) package. | +| - **Learn More**: [`Hosting.Meilisearch`][meilisearch-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields]][meilisearch-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields-preview]][meilisearch-nuget-preview] | An Aspire hosting integration leveraging the [Meilisearch](https://meilisearch.com) container. | +| - **Learn More**: [`Meilisearch`][meilisearch-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields]][meilisearch-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields-preview]][meilisearch-client-nuget-preview] | An Aspire client integration for the [Meilisearch](https://github.com/meilisearch/meilisearch-dotnet) package. | +| - **Learn More**: [`Hosting.Azure.DataApiBuilder`][dab-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields]][dab-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields-preview]][dab-nuget-preview] | A hosting integration for the [Azure Data API builder](https://learn.microsoft.com/en-us/azure/data-api-builder/overview). | +| - **Learn More**: [`Hosting.Deno`][deno-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields]][deno-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields-preview]][deno-nuget-preview] | A hosting integration for the Deno apps. | +| - **Learn More**: [`Hosting.SqlDatabaseProjects`][sql-database-projects-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields]][sql-database-projects-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields-preview]][sql-database-projects-nuget-preview] | A hosting integration for the SQL Databases Projects. | +| - **Learn More**: [`Hosting.Rust`][rust-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields]][rust-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields-preview]][rust-nuget-preview] | A hosting integration for the Rust apps. | +| - **Learn More**: [`Hosting.Bun`][bun-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields]][bun-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields-preview]][bun-nuget-preview] | A hosting integration for the Bun apps. | +| - **Learn More**: [`Hosting.Python.Extensions`][python-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Python.Extensions][python-ext-shields]][python-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Python.Extensions][python-ext-shields-preview]][python-ext-nuget-preview] | An integration that contains some additional extensions for running python applications | +| - **Learn More**: [`Hosting.EventStore`][eventstore-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields]][eventstore-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields-preview]][eventstore-nuget-preview] | An Aspire hosting integration leveraging the [EventStore](https://eventstore.com) container. | +| - **Learn More**: [`EventStore`][eventstore-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields]][eventstore-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields-preview]][eventstore-client-nuget-preview] | An Aspire client integration for the [EventStore](https://github.com/EventStore/EventStore-Client-Dotnet) package. | +| - **Learn More**: [`Hosting.ActiveMQ`][activemq-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields]][activemq-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields-preview]][activemq-nuget-preview] | An Aspire hosting integration leveraging the [ActiveMq](https://activemq.apache.org) container. | +| - **Learn More**: [`Hosting.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields]][sqlite-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields-preview]][sqlite-hosting-nuget-preview] | An Aspire hosting integration to setup a SQLite database with optional SQLite Web as a dev UI. | +| - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. | +| - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. | +| - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. | +| - **Learn More**: [`Hosting.Azure.Dapr.Redis`][dapr-azureredis-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. | +| - **Learn More**: [`Hosting.RavenDB`][ravendb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields]][ravendb-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields-preview]][ravendb-nuget-preview] | An Aspire integration leveraging the [RavenDB](https://ravendb.net/) container. | +| - **Learn More**: [`RavenDB.Client`][ravendb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields]][ravendb-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields-preview]][ravendb-client-nuget-preview] | An Aspire client integration for the [RavenDB.Client](https://www.nuget.org/packages/RavenDB.client) package. | +| - **Learn More**: [`Hosting.GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields]][go-feature-flag-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields-preview]][go-feature-flag-nuget-preview] | An Aspire hosting integration leveraging the [GoFeatureFlag](https://gofeatureflag.org/) container. | +| - **Learn More**: [`GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields]][go-feature-flag-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields-preview]][go-feature-flag-client-nuget-preview] | An Aspire client integration for the [GoFeatureFlag](https://github.com/open-feature/dotnet-sdk-contrib/tree/main/src/OpenFeature.Contrib.Providers.GOFeatureFlag) package. | +| - **Learn More**: [`Hosting.MongoDB.Extensions`][mongodb-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.MongoDB.Extensions][mongodb-ext-shields]][mongodb-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MongoDB.Extensions][mongodb-ext-shields-preview]][mongodb-ext-nuget-preview] | An integration that contains some additional extensions for hosting MongoDB container. | +| - **Learn More**: [`Hosting.PostgreSQL.Extensions`][postgres-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.PostgreSQL.Extensions][postgres-ext-shields]][postgres-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions][postgres-ext-shields-preview]][postgres-ext-nuget-preview] | An integration that contains some additional extensions for hosting PostgreSQL container. | +| - **Learn More**: [`Hosting.Redis.Extensions`][redis-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Redis.Extensions][redis-ext-shields]][redis-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Redis.Extensions][redis-ext-shields-preview]][redis-ext-nuget-preview] | An integration that contains some additional extensions for hosting Redis container. | +| - **Learn More**: [`Hosting.SqlServer.Extensions`][sqlserver-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.SqlServer.Extensions][sqlserver-ext-shields]][sqlserver-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.SqlServer.Extensions][sqlserver-ext-shields-preview]][sqlserver-ext-nuget-preview] | An integration that contains some additional extensions for hosting SqlServer container. | +| - **Learn More**: [`Hosting.LavinMQ`][lavinmq-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields]][lavinmq-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields-preview]][lavinmq-nuget-preview] | An Aspire hosting integration for [LavinMQ](https://www.lavinmq.com). | +| - **Learn More**: [`Hosting.MailPit`][mailpit-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields]][mailpit-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields-preview]][mailpit-ext-nuget-preview] | An Aspire integration leveraging the [MailPit](https://mailpit.axllent.org/) container. | +| - **Learn More**: [`Hosting.k6`][k6-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields]][k6-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields-preview]][k6-nuget-preview] | An Aspire integration leveraging the [Grafana k6](https://k6.io/) container. | +| - **Learn More**: [`Hosting.MySql.Extensions`][mysql-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.MySql.Extensions][mysql-ext-shields]][mysql-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MySql.Extensions][mysql-ext-shields-preview]][mysql-ext-nuget-preview] | An integration that contains some additional extensions for hosting MySql container. | +| - **Learn More**: [`Hosting.MiniO`][minio-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields]][minio-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields-preview]][minio-hosting-nuget-preview] | An Aspire hosting integration to setup a [MiniO S3](https://min.io/) storage. | +| - **Learn More**: [`MiniO.Client`][minio-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Minio.Client][minio-client-shields]][minio-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Client.Minio][minio-client-shields-preview]][minio-client-nuget-preview] | An Aspire client integration for the [Minio](https://github.com/minio/minio-dotnet) package. | ## 🙌 Getting Started @@ -253,3 +254,12 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) [mysql-ext-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.MySql.Extensions/ [mysql-ext-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.MySql.Extensions?label=nuget%20(preview) [mysql-ext-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.MySql.Extensions/absoluteLatest +[minio-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-minio +[minio-hosting-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Minio +[minio-hosting-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Minio/ +[minio-hosting-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Minio?label=nuget%20(preview) +[minio-hosting-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Minio/absoluteLatest +[minio-client-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Minio.Client +[minio-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Minio.Client/ +[minio-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Minio.Client?label=nuget%20(preview) +[minio-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Minio.Client/absoluteLatest From 53e1a5c1d5ae6c0dd512d9d30e69eea4e36665a7 Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Sat, 24 May 2025 13:56:38 +0300 Subject: [PATCH 08/18] DataVolume api added --- .../MinioBuilderExtensions.cs | 59 ++++++ .../MinioFunctionalTests.cs | 188 ++++++++++++++++-- .../MinioPublicApiTests.cs | 39 ++++ 3 files changed, 269 insertions(+), 17 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs index 08eb8f61..53bb5ae8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs @@ -73,4 +73,63 @@ public static IResourceBuilder AddMinioContainer( return builderWithResource; } + + /// + /// Adds a named volume for the data folder to a Minio container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// The . + /// + /// + /// Add an Minio container to the application model and reference it in a .NET project. Additionally, in this + /// example a data volume is added to the container to allow data to be persisted across container restarts. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var minio = builder.AddMinio("minio") + /// .WithDataVolume(); + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(minio); + /// + /// builder.Build().Run(); + /// + /// + /// + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data"); + } + + /// + /// Adds a bind mount for the data folder to a Minio container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// The . + /// + /// + /// Add an Minio container to the application model and reference it in a .NET project. Additionally, in this + /// example a bind mount is added to the container to allow data to be persisted across container restarts. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var minio = builder.AddMinio("minio") + /// .WithDataBindMount("./data/minio/data"); + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(minio); + /// + /// builder.Build().Run(); + /// + /// + /// + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, "/data"); + } } \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs index 54c90974..9e867dda 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs @@ -51,30 +51,184 @@ public async Task StorageGetsCreatedAndUsable() var minioClient = host.Services.GetRequiredService(); - var bucketName = "somebucket"; + await TestApi(minioClient); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) + { + string? volumeName = null; + string? bindMountPath = null; + + try + { + using var builder1 = TestDistributedApplicationBuilder.Create(testOutputHelper); + + var rootUser = "minioadmin"; + var port = 9000; + + var passwordParameter = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder1, + $"rootPassword"); + builder1.Configuration["Parameters:rootPassword"] = passwordParameter.Value; + var rootPasswordParameter = builder1.AddParameter(passwordParameter.Name); + + var minio = builder1.AddMinioContainer("minio", + builder1.AddParameter("username", rootUser), + rootPasswordParameter, + minioPort: port); + + if (useVolume) + { + // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails + volumeName = VolumeNameGenerator.Generate(minio, nameof(WithDataShouldPersistStateBetweenUsages)); + + // if the volume already exists (because of a crashing previous run), delete it + DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); + minio.WithDataVolume(volumeName); + } + else + { + bindMountPath = Directory.CreateTempSubdirectory().FullName; + minio.WithDataBindMount(bindMountPath); + } + + using (var app = builder1.Build()) + { + await app.StartAsync(); + + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceHealthyAsync(minio.Resource.Name); + + try + { + var webApplicationBuilder = Host.CreateApplicationBuilder(); - var mbArgs = new MakeBucketArgs() - .WithBucket(bucketName); - await minioClient.MakeBucketAsync(mbArgs); + webApplicationBuilder.Services.AddMinio(configureClient => configureClient + .WithEndpoint("localhost", port) + .WithCredentials(rootUser, passwordParameter.Value) + .WithSSL(false) + .Build()); + + using var host = webApplicationBuilder.Build(); + + await host.StartAsync(); + + var minioClient = host.Services.GetRequiredService(); + await TestApi(minioClient); + } + finally + { + // Stops the container, or the Volume would still be in use + await app.StopAsync(); + } + } + + using var builder2 = TestDistributedApplicationBuilder.Create(testOutputHelper); + builder2.Configuration["Parameters:rootPassword"] = passwordParameter.Value; + var rootPasswordParameter2 = builder2.AddParameter(passwordParameter.Name); + + + var minio2 = builder2.AddMinioContainer("minio", + builder2.AddParameter("username", rootUser), + rootPasswordParameter2, + minioPort: port); - var res = await minioClient.ListBucketsAsync(); + if (useVolume) + { + minio2.WithDataVolume(volumeName); + } + else + { + minio2.WithDataBindMount(bindMountPath!); + } - Assert.NotEmpty(res.Buckets); + using (var app = builder2.Build()) + { + await app.StartAsync(); - var bytearr = "Hey, I'm using minio client! It's awesome!"u8.ToArray(); - var stream = new MemoryStream(bytearr); + var rns = app.Services.GetRequiredService(); - var objectName = "someobj"; - var contentType = "text/plain"; + await rns.WaitForResourceHealthyAsync(minio.Resource.Name); + + + try + { + var webApplicationBuilder = Host.CreateApplicationBuilder(); - var putObjectArgs = new PutObjectArgs() - .WithBucket(bucketName) - .WithObject(objectName) - .WithStreamData(stream) - .WithObjectSize(stream.Length) - .WithContentType(contentType); + webApplicationBuilder.Services.AddMinio(configureClient => configureClient + .WithEndpoint("localhost", port) + .WithCredentials(rootUser, passwordParameter.Value) + .WithSSL(false) + .Build()); + + using var host = webApplicationBuilder.Build(); + + await host.StartAsync(); + + var minioClient = host.Services.GetRequiredService(); + await TestApi(minioClient, isDataPreGenerated: false); + } + finally + { + // Stops the container, or the Volume would still be in use + await app.StopAsync(); + } + } + + } + finally + { + if (volumeName is not null) + { + DockerUtils.AttemptDeleteDockerVolume(volumeName); + } + + if (bindMountPath is not null) + { + try + { + Directory.Delete(bindMountPath, recursive: true); + } + catch + { + // Don't fail test if we can't clean the temporary folder + } + } + } + } + + private static async Task TestApi(IMinioClient minioClient, bool isDataPreGenerated = true) + { + const string bucketName = "somebucket"; + + const string objectName = "someobj"; + const string contentType = "text/plain"; + + if (isDataPreGenerated) + { + var mbArgs = new MakeBucketArgs() + .WithBucket(bucketName); + await minioClient.MakeBucketAsync(mbArgs); + + var res = await minioClient.ListBucketsAsync(); + + Assert.NotEmpty(res.Buckets); + + var bytearr = "Hey, I'm using minio client! It's awesome!"u8.ToArray(); + var stream = new MemoryStream(bytearr); + + var putObjectArgs = new PutObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithStreamData(stream) + .WithObjectSize(stream.Length) + .WithContentType(contentType); - await minioClient.PutObjectAsync(putObjectArgs); + await minioClient.PutObjectAsync(putObjectArgs); + } var statObject = new StatObjectArgs() .WithBucket(bucketName) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs index 4473527b..1be195c7 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs @@ -30,4 +30,43 @@ public void AddMinioContainerShouldThrowWhenNameIsNull() var exception = Assert.Throws(action); Assert.Equal(nameof(name), exception.ParamName); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WithDataShouldThrowWhenBuilderIsNull(bool useVolume) + { + IResourceBuilder builder = null!; + + Func>? action = null; + + if (useVolume) + { + action = () => builder.WithDataVolume(); + } + else + { + const string source = "/data"; + + action = () => builder.WithDataBindMount(source); + } + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void WithDataBindMountShouldThrowWhenSourceIsNull() + { + var builder = new DistributedApplicationBuilder([]); + var resourceBuilder = builder.AddMinioContainer("minio"); + + string source = null!; + + var action = () => resourceBuilder.WithDataBindMount(source); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(source), exception.ParamName); + } + } From 7178b44cd867020312c9c4c54cc286e30ab66098 Mon Sep 17 00:00:00 2001 From: Harold-Morgan Date: Mon, 26 May 2025 23:03:04 +0300 Subject: [PATCH 09/18] Password/user/port parameters api changed --- .../MinioBuilderExtensions.cs | 72 +++++++++++++++---- .../MinioContainerResource.cs | 55 ++++++++++---- .../MinioFunctionalTests.cs | 6 +- .../MinioPublicApiTests.cs | 39 ++++++++++ 4 files changed, 141 insertions(+), 31 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs index 53bb5ae8..074c2e95 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs @@ -18,18 +18,16 @@ public static class MinioBuilderExtensions ///
/// 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 MinioO Admin. - /// The host port for MiniO. - /// The root user for the MiniO server. - /// The password for the MiniO root user. + /// The host port for MiniO. + /// The parameter used to provide the root user name for the MiniO resource. If a default value will be used. + /// The parameter used to provide the administrator password for the MiniO resource. If a random password will be generated. /// A reference to the . public static IResourceBuilder AddMinioContainer( this IDistributedApplicationBuilder builder, - string name, + [ResourceName] string name, IResourceBuilder? rootUser = null, IResourceBuilder? rootPassword = null, - int minioPort = 9000, - int minioConsolePort = 9001) + int? port = null) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); @@ -39,18 +37,16 @@ public static IResourceBuilder AddMinioContainer( var rootUserParameter = rootUser?.Resource ?? new ParameterResource("user", _ => MinioContainerResource.DefaultUserName); - var minioContainer = new MinioContainerResource(name, rootUserParameter, rootPasswordParameter); + var resource = new MinioContainerResource(name, rootUserParameter, rootPasswordParameter); var builderWithResource = builder - .AddResource(minioContainer) + .AddResource(resource) .WithImage(MinioContainerImageTags.Image, MinioContainerImageTags.Tag) .WithImageRegistry(MinioContainerImageTags.Registry) - .WithHttpEndpoint(targetPort: 9000, port: minioPort, name: MinioContainerResource.PrimaryEndpointName) - .WithHttpEndpoint(targetPort: 9001, port: minioConsolePort, name: "console") - .WithEnvironment("MINIO_ADDRESS", $":{minioPort.ToString()}") - .WithEnvironment("MINIO_CONSOLE_ADDRESS", $":{minioConsolePort.ToString()}") - .WithEnvironment(RootUserEnvVarName, minioContainer.RootUser.Value) - .WithEnvironment(RootPasswordEnvVarName, minioContainer.RootPassword.Value) + .WithHttpEndpoint(targetPort: 9000, port: port, name: MinioContainerResource.PrimaryEndpointName) + .WithHttpEndpoint(targetPort: 9001, name: MinioContainerResource.ConsoleEndpointName) + .WithEnvironment(RootUserEnvVarName, resource.RootUser.Value) + .WithEnvironment(RootPasswordEnvVarName, resource.PasswordParameter.Value) .WithArgs("server", "/data"); var endpoint = builderWithResource.Resource.GetEndpoint(MinioContainerResource.PrimaryEndpointName); @@ -74,6 +70,52 @@ public static IResourceBuilder AddMinioContainer( return builderWithResource; } + + /// + /// Configures the user name that the Minio resource uses. + /// + /// The resource builder. + /// The parameter used to provide the user name for the PostgreSQL resource. + /// The . + public static IResourceBuilder WithUserName(this IResourceBuilder builder, IResourceBuilder userName) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(userName); + + builder.Resource.RootUser = userName.Resource; + return builder; + } + + /// + /// Configures the password that the MiniO resource is used. + /// + /// The resource builder. + /// The parameter used to provide the password for the MiniO resource. If , no password will be configured. + /// The . + public static IResourceBuilder WithPassword(this IResourceBuilder builder, IResourceBuilder password) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.Resource.SetPassword(password.Resource); + return builder; + } + + /// + /// Configures the host port that the PGAdmin resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for PGAdmin. + /// The port to bind on the host. If is used, a random port will be assigned. + /// The resource builder for PGAdmin. + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint("http", endpoint => + { + endpoint.Port = port; + }); + } + /// /// Adds a named volume for the data folder to a Minio container resource. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs index 0dbf07a7..7dae1f8c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs @@ -3,27 +3,33 @@ /// /// A resource that represents a MiniO storage /// -/// The name of the resource -/// A parameter that contains the MiniO server admin user name, or null to -/// A parameter that contains the Minio server admin password -public sealed class MinioContainerResource( - string name, - ParameterResource rootUser, - ParameterResource rootPassword) : ContainerResource(name), - IResourceWithConnectionString +public sealed class MinioContainerResource : ContainerResource, IResourceWithConnectionString { internal const string PrimaryEndpointName = "http"; + internal const string ConsoleEndpointName = "console"; internal const string DefaultUserName = "minioadmin"; + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// A parameter that contains the Minio server root user name. + /// A parameter that contains the Minio server root password. + public MinioContainerResource(string name, ParameterResource user, ParameterResource password) : base(name) + { + RootUser = user; + PasswordParameter = password; + } + /// /// The MiniO root user. /// - public ParameterResource RootUser { get; set; } = rootUser; + public ParameterResource RootUser { get; set; } /// /// The MiniO root password. /// - public ParameterResource RootPassword { get; } = rootPassword; + public ParameterResource PasswordParameter { get; set; } private EndpointReference? _primaryEndpoint; @@ -36,17 +42,40 @@ public sealed class MinioContainerResource( /// Gets the connection string expression for the Minio ///
public ReferenceExpression ConnectionStringExpression => GetConnectionString(); - + + /// + /// Gets the connection string for the MiniO server. + /// + /// A to observe while waiting for the task to complete. + /// A connection string for the PostgreSQL server in the form "Host=host;Port=port;Username=postgres;Password=password". + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + { + if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) + { + return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken); + } + + return ConnectionStringExpression.GetValueAsync(cancellationToken); + } + + /// + /// Gets the connection string for the MiniO server. + /// private ReferenceExpression GetConnectionString() { var builder = new ReferenceExpressionBuilder(); builder.Append( $"Endpoint=http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); - + builder.Append($";AccessKey={RootUser.Value}"); - builder.Append($";SecretKey={RootPassword.Value}"); + builder.Append($";SecretKey={PasswordParameter.Value}"); return builder.Build(); } + + internal void SetPassword(ParameterResource password) + { + PasswordParameter = password; + } } \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs index 9e867dda..e703f269 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs @@ -27,7 +27,7 @@ public async Task StorageGetsCreatedAndUsable() .AddMinioContainer("minio", distributedApplicationBuilder.AddParameter("username", rootUser), rootPasswordParameter, - minioPort: port); + port: port); await using var app = await distributedApplicationBuilder.BuildAsync(); @@ -77,7 +77,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var minio = builder1.AddMinioContainer("minio", builder1.AddParameter("username", rootUser), rootPasswordParameter, - minioPort: port); + port: port); if (useVolume) { @@ -134,7 +134,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var minio2 = builder2.AddMinioContainer("minio", builder2.AddParameter("username", rootUser), rootPasswordParameter2, - minioPort: port); + port: port); if (useVolume) { diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs index 1be195c7..fa0ccda4 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs @@ -68,5 +68,44 @@ public void WithDataBindMountShouldThrowWhenSourceIsNull() var exception = Assert.Throws(action); Assert.Equal(nameof(source), exception.ParamName); } + + [Fact] + public void VerifyMinioContainerResourceWithHostPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddMinioContainer("minio") + .WithHostPort(1000); + + var resource = Assert.Single(builder.Resources.OfType()); + var endpoint = Assert.Single(resource.Annotations.OfType(), x => x.Name == "http"); + Assert.Equal(1000, endpoint.Port); + } + + [Fact] + public async Task VerifyMinioContainerResourceWithPassword() + { + var builder = DistributedApplication.CreateBuilder(); + var password = "p@ssw0rd1"; + var pass = builder.AddParameter("pass", password); + var minio = builder.AddMinioContainer("minio") + .WithPassword(pass) + .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + var connectionString = await minio.Resource.GetConnectionStringAsync(); + Assert.Equal("Endpoint=http://localhost:2000;AccessKey=minioadmin;SecretKey=p@ssw0rd1", connectionString); + } + [Fact] + public async Task VerifyMinioContainerResourceWithUserName() + { + var builder = DistributedApplication.CreateBuilder(); + var user = "user1"; + var pass = builder.AddParameter("user", user); + var postgres = builder.AddMinioContainer("minio") + .WithUserName(pass) + .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + var connectionString = await postgres.Resource.GetConnectionStringAsync(); + Assert.Equal($"Endpoint=http://localhost:2000;AccessKey=user1;SecretKey={postgres.Resource.PasswordParameter.Value}", connectionString); + } } From d625e3b7a3c1c61401cf08739601c2ad73b76fbd Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 29 May 2025 01:25:28 -0700 Subject: [PATCH 10/18] Update src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs --- .../MinioContainerResource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs index 7dae1f8c..74e6edf6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs @@ -68,8 +68,8 @@ private ReferenceExpression GetConnectionString() builder.Append( $"Endpoint=http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); - builder.Append($";AccessKey={RootUser.Value}"); - builder.Append($";SecretKey={PasswordParameter.Value}"); + builder.Append($";AccessKey={RootUser}"); + builder.Append($";SecretKey={PasswordParameter}"); return builder.Build(); } From 19f7e2703927f694f7b148a2121a25d2d5b60845 Mon Sep 17 00:00:00 2001 From: hmorgan Date: Thu, 29 May 2025 17:30:20 +0300 Subject: [PATCH 11/18] Randomize ports fxd + new healthcheck --- .../MinioBuilderExtensions.cs | 18 +++---- .../MinioHealthCheck.cs | 54 ------------------- .../TestDistributedApplicationBuilder.cs | 2 +- 3 files changed, 8 insertions(+), 66 deletions(-) delete mode 100644 src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs index 074c2e95..bcfba45e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs @@ -53,17 +53,13 @@ public static IResourceBuilder AddMinioContainer( var healthCheckKey = $"{name}_check"; builder.Services.AddHealthChecks() - .Add(new HealthCheckRegistration( - healthCheckKey, - sp => - { - var httpClient = sp.GetRequiredService().CreateClient("miniohealth"); - - return new MinioHealthCheck(endpoint.Url, httpClient); - }, - failureStatus: default, - tags: default, - timeout: default)); + .AddUrlGroup(options => + { + var uri = new Uri(endpoint.Url); + options.AddUri(new Uri(uri,"/minio/health/live"), setup => setup.ExpectHttpCode(200)); + options.AddUri(new Uri(uri, "/minio/health/cluster"), setup => setup.ExpectHttpCode(200)); + options.AddUri(new Uri(uri, "/minio/health/cluster/read"), setup => setup.ExpectHttpCode(200)); + }, healthCheckKey); builderWithResource.WithHealthCheck(healthCheckKey); diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs deleted file mode 100644 index 110d231e..00000000 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioHealthCheck.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace CommunityToolkit.Aspire.Hosting.Minio; - -internal sealed class MinioHealthCheck : IHealthCheck -{ - private readonly HttpClient _httpClient; - private readonly Uri _minioHealthLiveUri; - private readonly Uri _minioHealthClusterUri; - private readonly Uri _minioHealthClusterReadUri; - - public MinioHealthCheck(string minioBaseUrl, HttpClient? httpClient = null) - { - _httpClient = httpClient ?? new HttpClient(); - _httpClient.BaseAddress = new Uri(minioBaseUrl); - _minioHealthLiveUri = new Uri("/minio/health/live", UriKind.Relative); - _minioHealthClusterUri = new Uri("/minio/health/cluster", UriKind.Relative); - _minioHealthClusterReadUri = new Uri("/minio/health/cluster/read", UriKind.Relative); - } - - /// - 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); - } - } -} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs b/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs index be6d8909..bfdb69cf 100644 --- a/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs +++ b/tests/CommunityToolkit.Aspire.Testing/TestDistributedApplicationBuilder.cs @@ -79,7 +79,7 @@ void Configure(DistributedApplicationOptions applicationOptions, HostApplication var cfg = hostBuilderOptions.Configuration ??= new(); cfg.AddInMemoryCollection(new Dictionary { - ["DcpPublisher:RandomizePorts"] = "false", + ["DcpPublisher:RandomizePorts"] = "true", ["DcpPublisher:DeleteResourcesOnShutdown"] = "true", ["DcpPublisher:ResourceNameSuffix"] = $"{Random.Shared.Next():x}", }); From c1b1a7c1c3b84279df8cecc5541fe5aeb43d432d Mon Sep 17 00:00:00 2001 From: hmorgan Date: Thu, 29 May 2025 22:21:31 +0300 Subject: [PATCH 12/18] Fix tests --- .../MinioFunctionalTests.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs index e703f269..26d0ddcf 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs @@ -16,7 +16,6 @@ public async Task StorageGetsCreatedAndUsable() { using var distributedApplicationBuilder = TestDistributedApplicationBuilder.Create(testOutputHelper); var rootUser = "minioadmin"; - var port = 9000; var passwordParameter = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, $"rootPassword"); @@ -26,8 +25,9 @@ public async Task StorageGetsCreatedAndUsable() var minio = distributedApplicationBuilder .AddMinioContainer("minio", distributedApplicationBuilder.AddParameter("username", rootUser), - rootPasswordParameter, - port: port); + rootPasswordParameter); + + var minioEndpoint = minio.GetEndpoint("http"); await using var app = await distributedApplicationBuilder.BuildAsync(); @@ -40,7 +40,7 @@ public async Task StorageGetsCreatedAndUsable() var webApplicationBuilder = Host.CreateApplicationBuilder(); webApplicationBuilder.Services.AddMinio(configureClient => configureClient - .WithEndpoint("localhost", port) + .WithEndpoint("localhost", minioEndpoint.Port) .WithCredentials(rootUser, passwordParameter.Value) .WithSSL(false) .Build()); @@ -67,31 +67,31 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using var builder1 = TestDistributedApplicationBuilder.Create(testOutputHelper); var rootUser = "minioadmin"; - var port = 9000; var passwordParameter = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder1, $"rootPassword"); builder1.Configuration["Parameters:rootPassword"] = passwordParameter.Value; var rootPasswordParameter = builder1.AddParameter(passwordParameter.Name); - var minio = builder1.AddMinioContainer("minio", + var minio1 = builder1.AddMinioContainer("minio", builder1.AddParameter("username", rootUser), - rootPasswordParameter, - port: port); + rootPasswordParameter); + + var minio1Endpoint = minio1.GetEndpoint("http"); if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.Generate(minio, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(minio1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); - minio.WithDataVolume(volumeName); + minio1.WithDataVolume(volumeName); } else { bindMountPath = Directory.CreateTempSubdirectory().FullName; - minio.WithDataBindMount(bindMountPath); + minio1.WithDataBindMount(bindMountPath); } using (var app = builder1.Build()) @@ -100,14 +100,14 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(minio.Resource.Name); + await rns.WaitForResourceHealthyAsync(minio1.Resource.Name); try { var webApplicationBuilder = Host.CreateApplicationBuilder(); webApplicationBuilder.Services.AddMinio(configureClient => configureClient - .WithEndpoint("localhost", port) + .WithEndpoint("localhost", minio1Endpoint.Port) .WithCredentials(rootUser, passwordParameter.Value) .WithSSL(false) .Build()); @@ -133,8 +133,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var minio2 = builder2.AddMinioContainer("minio", builder2.AddParameter("username", rootUser), - rootPasswordParameter2, - port: port); + rootPasswordParameter2); + + var minio2Endpoint = minio2.GetEndpoint("http"); if (useVolume) { @@ -151,7 +152,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(minio.Resource.Name); + await rns.WaitForResourceHealthyAsync(minio1.Resource.Name); try @@ -159,7 +160,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var webApplicationBuilder = Host.CreateApplicationBuilder(); webApplicationBuilder.Services.AddMinio(configureClient => configureClient - .WithEndpoint("localhost", port) + .WithEndpoint("localhost", minio2Endpoint.Port) .WithCredentials(rootUser, passwordParameter.Value) .WithSSL(false) .Build()); From 8ca5d91b28d22f31f3c37fb7ff5ae45fbcd906d3 Mon Sep 17 00:00:00 2001 From: hmorgan Date: Mon, 2 Jun 2025 17:59:12 +0300 Subject: [PATCH 13/18] Fix slnx support --- CommunityToolkit.Aspire.slnx | 469 ++++++++++++++++++----------------- 1 file changed, 239 insertions(+), 230 deletions(-) diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index ffb93507..0c14a6f9 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -1,231 +1,240 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 67b09bfa0ba2ac48629eac36dabf0b3ce4dc8fe6 Mon Sep 17 00:00:00 2001 From: hmorgan Date: Tue, 3 Jun 2025 22:06:25 +0300 Subject: [PATCH 14/18] Naming inconsistency fxd --- README.md | 4 +-- ...mmunityToolkit.Aspire.Hosting.Minio.csproj | 4 +-- .../MinioBuilderExtensions.cs | 32 +++++++++---------- .../MinioContainerResource.cs | 18 +++++------ .../README.md | 6 ++-- ...ommunityToolkit.Aspire.Minio.Client.csproj | 4 +-- .../MinioClientBuilderExtensionMethods.cs | 8 ++--- .../MinioClientSettings.cs | 10 +++--- .../README.md | 16 +++++----- 9 files changed, 51 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index cfca5e2f..877d88d3 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ This repository contains the source code for the .NET Aspire Community Toolkit, | - **Learn More**: [`Hosting.MailPit`][mailpit-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields]][mailpit-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields-preview]][mailpit-ext-nuget-preview] | An Aspire integration leveraging the [MailPit](https://mailpit.axllent.org/) container. | | - **Learn More**: [`Hosting.k6`][k6-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields]][k6-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields-preview]][k6-nuget-preview] | An Aspire integration leveraging the [Grafana k6](https://k6.io/) container. | | - **Learn More**: [`Hosting.MySql.Extensions`][mysql-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.MySql.Extensions][mysql-ext-shields]][mysql-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MySql.Extensions][mysql-ext-shields-preview]][mysql-ext-nuget-preview] | An integration that contains some additional extensions for hosting MySql container. | -| - **Learn More**: [`Hosting.MiniO`][minio-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields]][minio-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields-preview]][minio-hosting-nuget-preview] | An Aspire hosting integration to setup a [MiniO S3](https://min.io/) storage. | -| - **Learn More**: [`MiniO.Client`][minio-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Minio.Client][minio-client-shields]][minio-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Client.Minio][minio-client-shields-preview]][minio-client-nuget-preview] | An Aspire client integration for the [Minio](https://github.com/minio/minio-dotnet) package. | +| - **Learn More**: [`Hosting.MinIO`][minio-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields]][minio-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields-preview]][minio-hosting-nuget-preview] | An Aspire hosting integration to setup a [MinIO S3](https://min.io/) storage. | +| - **Learn More**: [`MinIO.Client`][minio-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Minio.Client][minio-client-shields]][minio-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Client.Minio][minio-client-shields-preview]][minio-client-nuget-preview] | An Aspire client integration for the [MinIO](https://github.com/minio/minio-dotnet) package. | ## 🙌 Getting Started diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj b/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj index ee80e7c4..a6a3109e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/CommunityToolkit.Aspire.Hosting.Minio.csproj @@ -1,8 +1,8 @@  - MinioS3 Hosting integration - A .NET Aspire hosting integration for MiniO + minio hosting cloud storage + A .NET Aspire hosting integration for MinIO enable enable diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs index bcfba45e..2d319f6a 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs @@ -6,7 +6,7 @@ namespace Aspire.Hosting; /// -/// Provides extension methods for adding MiniO resources to an . +/// Provides extension methods for adding MinIO resources to an . /// public static class MinioBuilderExtensions { @@ -14,13 +14,13 @@ public static class MinioBuilderExtensions 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". + /// 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. - /// The parameter used to provide the root user name for the MiniO resource. If a default value will be used. - /// The parameter used to provide the administrator password for the MiniO resource. If a random password will be generated. + /// The host port for MinIO. + /// The parameter used to provide the root user name for the MinIO resource. If a default value will be used. + /// The parameter used to provide the administrator password for the MinIO resource. If a random password will be generated. /// A reference to the . public static IResourceBuilder AddMinioContainer( this IDistributedApplicationBuilder builder, @@ -68,10 +68,10 @@ public static IResourceBuilder AddMinioContainer( /// - /// Configures the user name that the Minio resource uses. + /// Configures the user name that the MinIO resource uses. /// /// The resource builder. - /// The parameter used to provide the user name for the PostgreSQL resource. + /// The parameter used to provide the user name for the MinIO resource. /// The . public static IResourceBuilder WithUserName(this IResourceBuilder builder, IResourceBuilder userName) { @@ -83,10 +83,10 @@ public static IResourceBuilder WithUserName(this IResour } /// - /// Configures the password that the MiniO resource is used. + /// Configures the password that the MinIO resource is used. /// /// The resource builder. - /// The parameter used to provide the password for the MiniO resource. If , no password will be configured. + /// The parameter used to provide the password for the MinIO resource. If , no password will be configured. /// The . public static IResourceBuilder WithPassword(this IResourceBuilder builder, IResourceBuilder password) { @@ -97,11 +97,11 @@ public static IResourceBuilder WithPassword(this IResour } /// - /// Configures the host port that the PGAdmin resource is exposed on instead of using randomly assigned port. + /// Configures the host port that the MinIO resource is exposed on instead of using randomly assigned port. /// - /// The resource builder for PGAdmin. + /// The resource builder for MinIO. /// The port to bind on the host. If is used, a random port will be assigned. - /// The resource builder for PGAdmin. + /// The resource builder for MinIO. public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) { ArgumentNullException.ThrowIfNull(builder); @@ -113,14 +113,14 @@ public static IResourceBuilder WithHostPort(this IResour } /// - /// Adds a named volume for the data folder to a Minio container resource. + /// Adds a named volume for the data folder to a MinIO container resource. /// /// The resource builder. /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. /// The . /// /// - /// Add an Minio container to the application model and reference it in a .NET project. Additionally, in this + /// Add an MinIO container to the application model and reference it in a .NET project. Additionally, in this /// example a data volume is added to the container to allow data to be persisted across container restarts. /// /// var builder = DistributedApplication.CreateBuilder(args); @@ -142,14 +142,14 @@ public static IResourceBuilder WithDataVolume(this IReso } /// - /// Adds a bind mount for the data folder to a Minio container resource. + /// Adds a bind mount for the data folder to a MinIO container resource. /// /// The resource builder. /// The source directory on the host to mount into the container. /// The . /// /// - /// Add an Minio container to the application model and reference it in a .NET project. Additionally, in this + /// Add an MinIO container to the application model and reference it in a .NET project. Additionally, in this /// example a bind mount is added to the container to allow data to be persisted across container restarts. /// /// var builder = DistributedApplication.CreateBuilder(args); diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs index 74e6edf6..3bfb68d3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs @@ -1,7 +1,7 @@ namespace Aspire.Hosting.ApplicationModel; /// -/// A resource that represents a MiniO storage +/// A resource that represents a MinIO storage /// public sealed class MinioContainerResource : ContainerResource, IResourceWithConnectionString { @@ -13,8 +13,8 @@ public sealed class MinioContainerResource : ContainerResource, IResourceWithCon /// Initializes a new instance of the class. /// /// The name of the resource. - /// A parameter that contains the Minio server root user name. - /// A parameter that contains the Minio server root password. + /// A parameter that contains the MinIO server root user name. + /// A parameter that contains the MinIO server root password. public MinioContainerResource(string name, ParameterResource user, ParameterResource password) : base(name) { RootUser = user; @@ -22,19 +22,19 @@ public MinioContainerResource(string name, ParameterResource user, ParameterReso } /// - /// The MiniO root user. + /// The MinIO root user. /// public ParameterResource RootUser { get; set; } /// - /// The MiniO root password. + /// The MinIO root password. /// public ParameterResource PasswordParameter { get; set; } private EndpointReference? _primaryEndpoint; /// - /// Gets the primary endpoint for the Minio. This endpoint is used for all API calls over HTTP. + /// Gets the primary endpoint for the MinIO. This endpoint is used for all API calls over HTTP. /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); @@ -44,10 +44,10 @@ public MinioContainerResource(string name, ParameterResource user, ParameterReso public ReferenceExpression ConnectionStringExpression => GetConnectionString(); /// - /// Gets the connection string for the MiniO server. + /// Gets the connection string for the MinIO server. /// /// A to observe while waiting for the task to complete. - /// A connection string for the PostgreSQL server in the form "Host=host;Port=port;Username=postgres;Password=password". + /// A connection string for the MinIO server in the form "Host=host;Port=port;Username=postgres;Password=password". public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) @@ -59,7 +59,7 @@ public MinioContainerResource(string name, ParameterResource user, ParameterReso } /// - /// Gets the connection string for the MiniO server. + /// Gets the connection string for the MinIO server. /// private ReferenceExpression GetConnectionString() { diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/README.md b/src/CommunityToolkit.Aspire.Hosting.Minio/README.md index f9a6d1a3..4b8f07e6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/README.md @@ -1,6 +1,6 @@ -# CommunityToolkit.Aspire.Hosting.MinoO library +# CommunityToolkit.Aspire.Hosting.MinIO library -Provides extension methods and resource definitions for the .NET Aspire AppHost to support running [MiniO](https://min.io/) containers. +Provides extension methods and resource definitions for the .NET Aspire AppHost to support running [MinIO](https://min.io/) containers. ## Getting Started @@ -14,7 +14,7 @@ dotnet add package CommunityToolkit.Aspire.Hosting.Minio ### Example usage -Then, in the _Program.cs_ file of `AppHost`, add a Minio resource and consume the connection using the following methods: +Then, in the _Program.cs_ file of `AppHost`, add a MinIO resource and consume the connection using the following methods: ```csharp var builder = DistributedApplication.CreateBuilder(args); diff --git a/src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj b/src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj index 213619e7..fead166e 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj +++ b/src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj @@ -1,8 +1,8 @@  - MinioS3 Client integration - A .NET Aspire client integration for MinioS# + minio hosting cloud storage + A .NET Aspire client integration for MinIO enable enable diff --git a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs index 70e0ec2c..464e210b 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs +++ b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientBuilderExtensionMethods.cs @@ -7,14 +7,14 @@ namespace Microsoft.Extensions.Hosting; /// -/// Provides extension methods for registering MiniO-related services in an . +/// Provides extension methods for registering MinIO-related services in an . /// public static class MinioClientBuilderExtensionMethods { private const string DefaultConfigSectionName = "Aspire:Minio:Client"; /// - /// Adds Minio Client to ASPNet host + /// Adds MinIO Client to ASPNet host /// /// The used to add services. /// Name of the configuration settings section @@ -37,7 +37,7 @@ private static void AddMinioInternal(this IHostApplicationBuilder builder, Minio { ArgumentNullException.ThrowIfNull(settings); - // Add the Minio client to the service collection. + // Add the MinIO client to the service collection. void ConfigureClient(IMinioClient configureClient) { var client = configureClient.WithEndpoint(settings.Endpoint) @@ -59,7 +59,7 @@ void ConfigureClient(IMinioClient configureClient) IMinioClient GetClient() { if (settings.Endpoint is null) - throw new InvalidOperationException("The MiniO endpoint must be provided either in configuration section, or as a part of connection string or settings delegate"); + throw new InvalidOperationException("The MinIO endpoint must be provided either in configuration section, or as a part of connection string or settings delegate"); return minioClientFactory.CreateClient(); } diff --git a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs index 55788333..442a4726 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs +++ b/src/CommunityToolkit.Aspire.Minio.Client/MinioClientSettings.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Aspire.Minio.Client; /// -/// Minio client configuration +/// MinIO client configuration /// public sealed class MinioClientSettings { @@ -30,7 +30,7 @@ public sealed class MinioClientSettings public HeaderAppInformation? UserAgentHeaderInfo { get; set; } /// - /// Minio client service lifetime + /// MinIO client service lifetime /// public ServiceLifetime ServiceLifetime = ServiceLifetime.Singleton; @@ -92,17 +92,17 @@ public class HeaderAppInformation } /// -/// Minio credentials (access and secret keys) +/// MinIO credentials (access and secret keys) /// public class MinioCredentials { /// - /// Minio Access Key + /// MinIO Access Key /// public string AccessKey { get; set; } = string.Empty; /// - /// Minio Secret Key + /// MinIO Secret Key /// public string SecretKey { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Minio.Client/README.md b/src/CommunityToolkit.Aspire.Minio.Client/README.md index c3d446f7..de3ffb9a 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/README.md +++ b/src/CommunityToolkit.Aspire.Minio.Client/README.md @@ -1,16 +1,16 @@ # CommunityToolkit.Aspire.Minio.Client -Registers a [MiniOClient](https://github.com/minio/minio-dotnet) in the DI container for connecting to MiniO. +Registers a [MinIOClient](https://github.com/minio/minio-dotnet) in the DI container for connecting to MinIO. ## Getting started ### Prerequisites -- Minio or other S3-compatible storage. +- MinIO or other S3-compatible storage. ### Install the package -Install the .NET Aspire MiniO Client library with [NuGet](https://www.nuget.org): +Install the .NET Aspire MinIO Client library with [NuGet](https://www.nuget.org): ```dotnetcli dotnet add package CommunityToolkit.Aspire.Minio.Client @@ -26,7 +26,7 @@ builder.AddMinioClient(); ## Configuration -The .NET Aspire MiniO Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project. +The .NET Aspire MinIO Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project. ### Use a connection string @@ -48,7 +48,7 @@ And then the connection string will be retrieved from the `ConnectionStrings` co ### Use configuration providers -The .NET Aspire MiniO Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). +The .NET Aspire MinIO Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `MinioClientSettings` from configuration by using the `Aspire:Minio:Client` key. This key can be overriden by using the `configurationSectionName` method parameter. Example `appsettings.json` that configures some of the options: @@ -83,7 +83,7 @@ In your AppHost project, install the `CommunityToolkit.Aspire.Hosting.Minio` lib dotnet add package CommunityToolkit.Aspire.Hosting.Minio ``` -Then, in the _Program.cs_ file of `AppHost`, register a MiniO host and consume the connection using the following methods: +Then, in the _Program.cs_ file of `AppHost`, register a MinIO host and consume the connection using the following methods: ```csharp var minio = builder.AddMinioContainer("minio"); @@ -93,13 +93,13 @@ var myService = builder.AddProject() ``` The `WithReference` method configures a connection in the `MyService` project named `minio`. -In the _Program.cs_ file of `MyService`, the MiniO connection can be consumed using: +In the _Program.cs_ file of `MyService`, the MinIO connection can be consumed using: ```csharp builder.AddMinioClient("minio"); ``` -Then, in your service, inject `IMinioClient` and use it to interact with the MiniO or other S3 compatible API: +Then, in your service, inject `IMinioClient` and use it to interact with the MinIO or other S3 compatible API: ```csharp public class MyService(IMinioClient minioClient) From d8606340563c45f4846ba9398fd37447ff139163 Mon Sep 17 00:00:00 2001 From: Egor Gusarenko <6255074+Harold-Morgan@users.noreply.github.com> Date: Wed, 4 Jun 2025 07:02:05 +0300 Subject: [PATCH 15/18] Update src/CommunityToolkit.Aspire.Minio.Client/README.md Co-authored-by: Aaron Powell --- src/CommunityToolkit.Aspire.Minio.Client/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Minio.Client/README.md b/src/CommunityToolkit.Aspire.Minio.Client/README.md index de3ffb9a..0e5bd765 100644 --- a/src/CommunityToolkit.Aspire.Minio.Client/README.md +++ b/src/CommunityToolkit.Aspire.Minio.Client/README.md @@ -21,7 +21,7 @@ dotnet add package CommunityToolkit.Aspire.Minio.Client In the _Program.cs_ file of your project, call the `AddMinioClient` extension method to register a `MinioClient` for use via the dependency injection container. The method takes a connection name parameter. ```csharp -builder.AddMinioClient(); +builder.AddMinioClient("minio"); ``` ## Configuration From 11557a993914dc9ab79f3fcb8f9bb457f16ee855 Mon Sep 17 00:00:00 2001 From: hmorgan Date: Wed, 4 Jun 2025 07:25:11 +0300 Subject: [PATCH 16/18] Apiservice test timeout --- .../AppHostTests.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs index 25776b8f..26029f09 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs @@ -27,17 +27,19 @@ public async Task ResourceStartsAndRespondsOk() [Fact] public async Task ApiServiceCreateData() { - var resourceName = "apiservice"; - - await fixture.ResourceNotificationService.WaitForResourceHealthyAsync("minio").WaitAsync(TimeSpan.FromMinutes(5)); - await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + const string resourceName = "apiservice"; + + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync("minio", cts.Token).WaitAsync(cts.Token); + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName, cts.Token).WaitAsync(cts.Token); var httpClient = fixture.CreateHttpClient(resourceName); var bucketName = "somebucket"; - var createResponse = await httpClient.PutAsync($"/buckets/{bucketName}", null).WaitAsync(TimeSpan.FromMinutes(5)); - Assert.Equal(HttpStatusCode.OK, createResponse.StatusCode); + var createBucketResponse = await httpClient.PutAsync($"/buckets/{bucketName}", null).WaitAsync(cts.Token); + Assert.Equal(HttpStatusCode.OK, createBucketResponse.StatusCode); - var getResponse = await httpClient.GetAsync($"/buckets/{bucketName}"); - Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); + var getBucketResponse = await httpClient.GetAsync($"/buckets/{bucketName}"); + Assert.Equal(HttpStatusCode.OK, getBucketResponse.StatusCode); } } From 90a326e7055c9133867e6cef63edce6f9a237194 Mon Sep 17 00:00:00 2001 From: hmorgan Date: Thu, 5 Jun 2025 18:04:25 +0300 Subject: [PATCH 17/18] ApiServiceCreateData test now checks file upload/download result --- .../Program.cs | 42 +++++++++++++++++++ .../AppHostTests.cs | 18 ++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs index 7aa497bf..2b9c0373 100644 --- a/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs +++ b/examples/minio/CommunityToolkit.Aspire.Hosting.Minio.ApiService/Program.cs @@ -32,4 +32,46 @@ return Results.NotFound(); }); +app.MapPost("/buckets/{bucketName}/{fileName}/upload", + async ([FromRoute] string bucketName, + [FromRoute] string fileName, + HttpRequest request, + [FromServices] IMinioClient minioClient) => + { + var memstream = new MemoryStream(); + + await request.Body.CopyToAsync(memstream); + + var length = memstream.Length; + memstream.Seek(0, SeekOrigin.Begin); + + var putObjectArgs = new PutObjectArgs() + .WithObject(fileName) + .WithBucket(bucketName) + .WithStreamData(memstream) + .WithObjectSize(length); + + await minioClient.PutObjectAsync(putObjectArgs); + + return Results.Ok(); + }).DisableAntiforgery(); + +app.MapGet("/buckets/{bucketName}/{fileName}/download", + async (string bucketName, string fileName, [FromServices] IMinioClient minioClient) => + { + var memStream = new MemoryStream(); + + var getObjectArgs = new GetObjectArgs() + .WithBucket(bucketName) + .WithObject(fileName) + .WithCallbackStream(stream => + { + stream.CopyToAsync(memStream); + }); + + await minioClient.GetObjectAsync(getObjectArgs); + + return Results.File(memStream.ToArray(), "application/octet-stream", fileName); + }); + app.Run(); \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs index 26029f09..e07bc37b 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/AppHostTests.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Components.Common.Tests; -using Aspire.Hosting; using CommunityToolkit.Aspire.Testing; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; +using System.Text; namespace CommunityToolkit.Aspire.Hosting.Minio.Tests; @@ -41,5 +39,19 @@ public async Task ApiServiceCreateData() var getBucketResponse = await httpClient.GetAsync($"/buckets/{bucketName}"); Assert.Equal(HttpStatusCode.OK, getBucketResponse.StatusCode); + + var uploadedString = "Hello World"; + var bytes = Encoding.UTF8.GetBytes(uploadedString); + using var content = new ByteArrayContent(bytes); + content.Headers.ContentType = new("plain/text"); + + var uploadFileResponse = await httpClient.PostAsync($"/buckets/{bucketName}/fileName/upload", content); + Assert.Equal(HttpStatusCode.OK, uploadFileResponse.StatusCode); + + var downloadFileResponse = await httpClient.GetAsync($"/buckets/{bucketName}/fileName/download"); + Assert.Equal(HttpStatusCode.OK, downloadFileResponse.StatusCode); + + var downloadedString = await downloadFileResponse.Content.ReadAsStringAsync(); + Assert.Equal(uploadedString, downloadedString); } } From e7b0f708e616bafae27d8f7f82f6e5ba54fa1098 Mon Sep 17 00:00:00 2001 From: hmorgan Date: Thu, 5 Jun 2025 18:35:13 +0300 Subject: [PATCH 18/18] MinioContainerResource.cs class structure revamped --- .../MinioContainerResource.cs | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs index 3bfb68d3..4a964d91 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Minio/MinioContainerResource.cs @@ -3,33 +3,25 @@ /// /// A resource that represents a MinIO storage /// -public sealed class MinioContainerResource : ContainerResource, IResourceWithConnectionString +/// The name of the resource. +/// A parameter that contains the MinIO server root username. +/// A parameter that contains the MinIO server root password. +public sealed class MinioContainerResource(string name, ParameterResource rootUser, ParameterResource passwordParameter) : ContainerResource(name), + IResourceWithConnectionString { internal const string PrimaryEndpointName = "http"; internal const string ConsoleEndpointName = "console"; internal const string DefaultUserName = "minioadmin"; - /// - /// Initializes a new instance of the class. - /// - /// The name of the resource. - /// A parameter that contains the MinIO server root user name. - /// A parameter that contains the MinIO server root password. - public MinioContainerResource(string name, ParameterResource user, ParameterResource password) : base(name) - { - RootUser = user; - PasswordParameter = password; - } - /// /// The MinIO root user. /// - public ParameterResource RootUser { get; set; } + public ParameterResource RootUser { get; set; } = rootUser; /// /// The MinIO root password. /// - public ParameterResource PasswordParameter { get; set; } + public ParameterResource PasswordParameter { get; private set; } = passwordParameter; private EndpointReference? _primaryEndpoint;