Skip to content

Commit 536a610

Browse files
committed
DataVolume api added
1 parent 0d38552 commit 536a610

File tree

3 files changed

+269
-17
lines changed

3 files changed

+269
-17
lines changed

src/CommunityToolkit.Aspire.Hosting.Minio/MinioBuilderExtensions.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,63 @@ public static IResourceBuilder<MinioContainerResource> AddMinioContainer(
7373

7474
return builderWithResource;
7575
}
76+
77+
/// <summary>
78+
/// Adds a named volume for the data folder to a Minio container resource.
79+
/// </summary>
80+
/// <param name="builder">The resource builder.</param>
81+
/// <param name="name">The name of the volume. Defaults to an auto-generated name based on the application and resource names.</param>
82+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
83+
/// <remarks>
84+
/// <example>
85+
/// Add an Minio container to the application model and reference it in a .NET project. Additionally, in this
86+
/// example a data volume is added to the container to allow data to be persisted across container restarts.
87+
/// <code lang="csharp">
88+
/// var builder = DistributedApplication.CreateBuilder(args);
89+
///
90+
/// var minio = builder.AddMinio("minio")
91+
/// .WithDataVolume();
92+
/// var api = builder.AddProject&lt;Projects.Api&gt;("api")
93+
/// .WithReference(minio);
94+
///
95+
/// builder.Build().Run();
96+
/// </code>
97+
/// </example>
98+
/// </remarks>
99+
public static IResourceBuilder<MinioContainerResource> WithDataVolume(this IResourceBuilder<MinioContainerResource> builder, string? name = null)
100+
{
101+
ArgumentNullException.ThrowIfNull(builder);
102+
103+
return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data");
104+
}
105+
106+
/// <summary>
107+
/// Adds a bind mount for the data folder to a Minio container resource.
108+
/// </summary>
109+
/// <param name="builder">The resource builder.</param>
110+
/// <param name="source">The source directory on the host to mount into the container.</param>
111+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
112+
/// <remarks>
113+
/// <example>
114+
/// Add an Minio container to the application model and reference it in a .NET project. Additionally, in this
115+
/// example a bind mount is added to the container to allow data to be persisted across container restarts.
116+
/// <code lang="csharp">
117+
/// var builder = DistributedApplication.CreateBuilder(args);
118+
///
119+
/// var minio = builder.AddMinio("minio")
120+
/// .WithDataBindMount("./data/minio/data");
121+
/// var api = builder.AddProject&lt;Projects.Api&gt;("api")
122+
/// .WithReference(minio);
123+
///
124+
/// builder.Build().Run();
125+
/// </code>
126+
/// </example>
127+
/// </remarks>
128+
public static IResourceBuilder<MinioContainerResource> WithDataBindMount(this IResourceBuilder<MinioContainerResource> builder, string source)
129+
{
130+
ArgumentNullException.ThrowIfNull(builder);
131+
ArgumentNullException.ThrowIfNull(source);
132+
133+
return builder.WithBindMount(source, "/data");
134+
}
76135
}

tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioFunctionalTests.cs

Lines changed: 171 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,30 +51,184 @@ public async Task StorageGetsCreatedAndUsable()
5151

5252
var minioClient = host.Services.GetRequiredService<IMinioClient>();
5353

54-
var bucketName = "somebucket";
54+
await TestApi(minioClient);
55+
}
56+
57+
[Theory]
58+
[InlineData(true)]
59+
[InlineData(false)]
60+
public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
61+
{
62+
string? volumeName = null;
63+
string? bindMountPath = null;
64+
65+
try
66+
{
67+
using var builder1 = TestDistributedApplicationBuilder.Create(testOutputHelper);
68+
69+
var rootUser = "minioadmin";
70+
var port = 9000;
71+
72+
var passwordParameter = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder1,
73+
$"rootPassword");
74+
builder1.Configuration["Parameters:rootPassword"] = passwordParameter.Value;
75+
var rootPasswordParameter = builder1.AddParameter(passwordParameter.Name);
76+
77+
var minio = builder1.AddMinioContainer("minio",
78+
builder1.AddParameter("username", rootUser),
79+
rootPasswordParameter,
80+
minioPort: port);
81+
82+
if (useVolume)
83+
{
84+
// Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
85+
volumeName = VolumeNameGenerator.Generate(minio, nameof(WithDataShouldPersistStateBetweenUsages));
86+
87+
// if the volume already exists (because of a crashing previous run), delete it
88+
DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true);
89+
minio.WithDataVolume(volumeName);
90+
}
91+
else
92+
{
93+
bindMountPath = Directory.CreateTempSubdirectory().FullName;
94+
minio.WithDataBindMount(bindMountPath);
95+
}
96+
97+
using (var app = builder1.Build())
98+
{
99+
await app.StartAsync();
100+
101+
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
102+
103+
await rns.WaitForResourceHealthyAsync(minio.Resource.Name);
104+
105+
try
106+
{
107+
var webApplicationBuilder = Host.CreateApplicationBuilder();
55108

56-
var mbArgs = new MakeBucketArgs()
57-
.WithBucket(bucketName);
58-
await minioClient.MakeBucketAsync(mbArgs);
109+
webApplicationBuilder.Services.AddMinio(configureClient => configureClient
110+
.WithEndpoint("localhost", port)
111+
.WithCredentials(rootUser, passwordParameter.Value)
112+
.WithSSL(false)
113+
.Build());
114+
115+
using var host = webApplicationBuilder.Build();
116+
117+
await host.StartAsync();
118+
119+
var minioClient = host.Services.GetRequiredService<IMinioClient>();
120+
await TestApi(minioClient);
121+
}
122+
finally
123+
{
124+
// Stops the container, or the Volume would still be in use
125+
await app.StopAsync();
126+
}
127+
}
128+
129+
using var builder2 = TestDistributedApplicationBuilder.Create(testOutputHelper);
130+
builder2.Configuration["Parameters:rootPassword"] = passwordParameter.Value;
131+
var rootPasswordParameter2 = builder2.AddParameter(passwordParameter.Name);
132+
133+
134+
var minio2 = builder2.AddMinioContainer("minio",
135+
builder2.AddParameter("username", rootUser),
136+
rootPasswordParameter2,
137+
minioPort: port);
59138

60-
var res = await minioClient.ListBucketsAsync();
139+
if (useVolume)
140+
{
141+
minio2.WithDataVolume(volumeName);
142+
}
143+
else
144+
{
145+
minio2.WithDataBindMount(bindMountPath!);
146+
}
61147

62-
Assert.NotEmpty(res.Buckets);
148+
using (var app = builder2.Build())
149+
{
150+
await app.StartAsync();
63151

64-
var bytearr = "Hey, I'm using minio client! It's awesome!"u8.ToArray();
65-
var stream = new MemoryStream(bytearr);
152+
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
66153

67-
var objectName = "someobj";
68-
var contentType = "text/plain";
154+
await rns.WaitForResourceHealthyAsync(minio.Resource.Name);
155+
156+
157+
try
158+
{
159+
var webApplicationBuilder = Host.CreateApplicationBuilder();
69160

70-
var putObjectArgs = new PutObjectArgs()
71-
.WithBucket(bucketName)
72-
.WithObject(objectName)
73-
.WithStreamData(stream)
74-
.WithObjectSize(stream.Length)
75-
.WithContentType(contentType);
161+
webApplicationBuilder.Services.AddMinio(configureClient => configureClient
162+
.WithEndpoint("localhost", port)
163+
.WithCredentials(rootUser, passwordParameter.Value)
164+
.WithSSL(false)
165+
.Build());
166+
167+
using var host = webApplicationBuilder.Build();
168+
169+
await host.StartAsync();
170+
171+
var minioClient = host.Services.GetRequiredService<IMinioClient>();
172+
await TestApi(minioClient, isDataPreGenerated: false);
173+
}
174+
finally
175+
{
176+
// Stops the container, or the Volume would still be in use
177+
await app.StopAsync();
178+
}
179+
}
180+
181+
}
182+
finally
183+
{
184+
if (volumeName is not null)
185+
{
186+
DockerUtils.AttemptDeleteDockerVolume(volumeName);
187+
}
188+
189+
if (bindMountPath is not null)
190+
{
191+
try
192+
{
193+
Directory.Delete(bindMountPath, recursive: true);
194+
}
195+
catch
196+
{
197+
// Don't fail test if we can't clean the temporary folder
198+
}
199+
}
200+
}
201+
}
202+
203+
private static async Task TestApi(IMinioClient minioClient, bool isDataPreGenerated = true)
204+
{
205+
const string bucketName = "somebucket";
206+
207+
const string objectName = "someobj";
208+
const string contentType = "text/plain";
209+
210+
if (isDataPreGenerated)
211+
{
212+
var mbArgs = new MakeBucketArgs()
213+
.WithBucket(bucketName);
214+
await minioClient.MakeBucketAsync(mbArgs);
215+
216+
var res = await minioClient.ListBucketsAsync();
217+
218+
Assert.NotEmpty(res.Buckets);
219+
220+
var bytearr = "Hey, I'm using minio client! It's awesome!"u8.ToArray();
221+
var stream = new MemoryStream(bytearr);
222+
223+
var putObjectArgs = new PutObjectArgs()
224+
.WithBucket(bucketName)
225+
.WithObject(objectName)
226+
.WithStreamData(stream)
227+
.WithObjectSize(stream.Length)
228+
.WithContentType(contentType);
76229

77-
await minioClient.PutObjectAsync(putObjectArgs);
230+
await minioClient.PutObjectAsync(putObjectArgs);
231+
}
78232

79233
var statObject = new StatObjectArgs()
80234
.WithBucket(bucketName)

tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/MinioPublicApiTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,43 @@ public void AddMinioContainerShouldThrowWhenNameIsNull()
3030
var exception = Assert.Throws<ArgumentNullException>(action);
3131
Assert.Equal(nameof(name), exception.ParamName);
3232
}
33+
34+
[Theory]
35+
[InlineData(false)]
36+
[InlineData(true)]
37+
public void WithDataShouldThrowWhenBuilderIsNull(bool useVolume)
38+
{
39+
IResourceBuilder<MinioContainerResource> builder = null!;
40+
41+
Func<IResourceBuilder<MinioContainerResource>>? action = null;
42+
43+
if (useVolume)
44+
{
45+
action = () => builder.WithDataVolume();
46+
}
47+
else
48+
{
49+
const string source = "/data";
50+
51+
action = () => builder.WithDataBindMount(source);
52+
}
53+
54+
var exception = Assert.Throws<ArgumentNullException>(action);
55+
Assert.Equal(nameof(builder), exception.ParamName);
56+
}
57+
58+
[Fact]
59+
public void WithDataBindMountShouldThrowWhenSourceIsNull()
60+
{
61+
var builder = new DistributedApplicationBuilder([]);
62+
var resourceBuilder = builder.AddMinioContainer("minio");
63+
64+
string source = null!;
65+
66+
var action = () => resourceBuilder.WithDataBindMount(source);
67+
68+
var exception = Assert.Throws<ArgumentNullException>(action);
69+
Assert.Equal(nameof(source), exception.ParamName);
70+
}
71+
3372
}

0 commit comments

Comments
 (0)