Skip to content

Commit 1d4cd60

Browse files
committed
Add health check for MinIO
1 parent 9568db4 commit 1d4cd60

12 files changed

+266
-31
lines changed

src/Plugins/MinIO/ConfigurationKeys.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace Monai.Deploy.Storage.MinIO
1818
{
1919
internal static class ConfigurationKeys
2020
{
21+
public static readonly string StorageServiceName = "minio";
22+
2123
public static readonly string EndPoint = "endpoint";
2224
public static readonly string AccessKey = "accessKey";
2325
public static readonly string AccessToken = "accessToken";
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Microsoft.Extensions.DependencyInjection;
18+
using Microsoft.Extensions.Diagnostics.HealthChecks;
19+
using Microsoft.Extensions.Logging;
20+
21+
namespace Monai.Deploy.Storage.MinIO
22+
{
23+
public class HealthCheckBuilder : HealthCheckRegistrationBase
24+
{
25+
public HealthCheckBuilder(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName)
26+
{
27+
}
28+
29+
public override IHealthChecksBuilder Configure(
30+
IHealthChecksBuilder builder,
31+
IServiceProvider serviceProvider,
32+
HealthStatus? failureStatus = null,
33+
IEnumerable<string>? tags = null,
34+
TimeSpan? timeout = null)
35+
{
36+
var minioClientFactory = serviceProvider.GetRequiredService<IMinIoClientFactory>();
37+
var logger = serviceProvider.GetRequiredService<ILogger<MinIoHealthCheck>>();
38+
39+
builder.Add(new HealthCheckRegistration(
40+
ConfigurationKeys.StorageServiceName,
41+
new MinIoHealthCheck(minioClientFactory, logger),
42+
failureStatus,
43+
tags,
44+
timeout));
45+
return builder;
46+
}
47+
}
48+
}

src/Plugins/MinIO/LoggerMethods.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@ public static partial class LoggerMethods
2828

2929
[LoggerMessage(EventId = 20002, Level = LogLevel.Error, Message = "Error verifying objects in bucket '{bucketName}'.")]
3030
public static partial void VerifyObjectError(this ILogger logger, string bucketName, Exception ex);
31+
32+
[LoggerMessage(EventId = 20003, Level = LogLevel.Error, Message = "Health check failure.")]
33+
public static partial void HealthCheckError(this ILogger logger, Exception ex);
3134
}
3235
}

src/Plugins/MinIO/Mc/mc.exe

-22.5 MB
Binary file not shown.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Microsoft.Extensions.Diagnostics.HealthChecks;
18+
using Microsoft.Extensions.Logging;
19+
20+
namespace Monai.Deploy.Storage.MinIO
21+
{
22+
internal class MinIoHealthCheck : IHealthCheck
23+
{
24+
private readonly IMinIoClientFactory _minIoClientFactory;
25+
private readonly ILogger<MinIoHealthCheck> _logger;
26+
27+
public MinIoHealthCheck(IMinIoClientFactory minIoClientFactory, ILogger<MinIoHealthCheck> logger)
28+
{
29+
_minIoClientFactory = minIoClientFactory ?? throw new ArgumentNullException(nameof(minIoClientFactory));
30+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
31+
}
32+
33+
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new())
34+
{
35+
try
36+
{
37+
var minioClient = _minIoClientFactory.GetClient();
38+
await minioClient.ListBucketsAsync(cancellationToken).ConfigureAwait(false);
39+
40+
return HealthCheckResult.Healthy();
41+
}
42+
catch (Exception exception)
43+
{
44+
_logger.HealthCheckError(exception);
45+
return HealthCheckResult.Unhealthy(exception: exception);
46+
}
47+
}
48+
}
49+
}

src/Plugins/MinIO/StorageAdminService.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ private async Task<List<string>> ExecuteAsync(string cmd)
110110

111111
using (var process = CreateProcess(cmd))
112112
{
113-
var (lines, errors) = await RunProcessAsync(process);
113+
var (lines, errors) = await RunProcessAsync(process).ConfigureAwait(false);
114114
if (errors.Any())
115115
{
116116
throw new InvalidOperationException($"Unknown Error {string.Join("\n", errors)}");
@@ -174,7 +174,7 @@ public async Task<bool> HasConnectionAsync()
174174

175175
public async Task<bool> SetConnectionAsync()
176176
{
177-
if (await HasConnectionAsync())
177+
if (await HasConnectionAsync().ConfigureAwait(false))
178178
{
179179
return true;
180180
}
@@ -227,11 +227,11 @@ public async Task<Credentials> CreateUserAsync(string username, PolicyRequest[]
227227
Guard.Against.NullOrWhiteSpace(username, nameof(username));
228228
Guard.Against.Null(policyRequests, nameof(policyRequests));
229229

230-
if (!await SetConnectionAsync())
230+
if (!await SetConnectionAsync().ConfigureAwait(false))
231231
{
232-
throw new InvalidOperationException("Unable to set connection for more information, attempt mc alias set {_serviceName} http://{_endpoint} {_accessKey} {_secretKey}");
232+
throw new InvalidOperationException($"Unable to set connection for more information, attempt mc alias set {_serviceName} http://{_endpoint} {_accessKey} {_secretKey}");
233233
}
234-
if (await UserAlreadyExistsAsync(username))
234+
if (await UserAlreadyExistsAsync(username).ConfigureAwait(false))
235235
{
236236
throw new InvalidOperationException("User already exists");
237237
}
@@ -245,7 +245,7 @@ public async Task<Credentials> CreateUserAsync(string username, PolicyRequest[]
245245

246246
if (result.Any(r => r.Contains($"Added user `{username}` successfully.")) is false)
247247
{
248-
await RemoveUserAsync(username);
248+
await RemoveUserAsync(username).ConfigureAwait(false);
249249
throw new InvalidOperationException($"Unknown Output {string.Join("\n", result)}");
250250
}
251251

@@ -280,7 +280,7 @@ private async Task<string> CreatePolicyAsync(PolicyRequest[] policyRequests, str
280280
var result = await ExecuteAsync($"admin policy add {_serviceName} pol_{username} {policyFileName}").ConfigureAwait(false);
281281
if (result.Any(r => r.Contains($"Added policy `pol_{username}` successfully.")) is false)
282282
{
283-
await RemoveUserAsync(username);
283+
await RemoveUserAsync(username).ConfigureAwait(false);
284284
File.Delete($"{username}.json");
285285
throw new InvalidOperationException("Failed to create policy, user has been removed");
286286
}

src/Plugins/MinIO/Tests/MinioPolicyExtensionsTest.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,11 @@ public async Task Should_Set_Correct_Policy()
6464

6565
try
6666
{
67-
var result = await systemUnderTest.CreateUserAsync(userName, policys);
68-
}
69-
catch (Exception ex)
70-
{
71-
var message = ex.Message;
67+
var result = await systemUnderTest.CreateUserAsync(userName, policys).ConfigureAwait(false);
7268
}
7369
finally
7470
{
75-
await systemUnderTest.RemoveUserAsync(userName);
71+
await systemUnderTest.RemoveUserAsync(userName).ConfigureAwait(false);
7672
}
7773
}
7874
}

src/S3Policy/Tests/Extensions/PolicyExtensionsTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public void ToPolicy_NullFolder_ThrowsException()
102102
}
103103

104104
[Fact]
105-
public async Task ToPolicy_Should_Set_Correct_Allow_All_Path()
105+
public void ToPolicy_Should_Set_Correct_Allow_All_Path()
106106
{
107107
const string bucketName = "test-bucket";
108108
const string payloadId = "00000000-1000-0000-0000-000000000000";
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Ardalis.GuardClauses;
18+
using Microsoft.Extensions.DependencyInjection;
19+
using Microsoft.Extensions.Diagnostics.HealthChecks;
20+
using Monai.Deploy.Storage.Configuration;
21+
22+
namespace Monai.Deploy.Storage
23+
{
24+
public abstract class HealthCheckRegistrationBase
25+
{
26+
protected string FullyQualifiedAssemblyName { get; }
27+
protected string AssemblyFilename { get; }
28+
29+
protected HealthCheckRegistrationBase(string fullyQualifiedAssemblyName)
30+
{
31+
Guard.Against.NullOrWhiteSpace(fullyQualifiedAssemblyName, nameof(fullyQualifiedAssemblyName));
32+
FullyQualifiedAssemblyName = fullyQualifiedAssemblyName;
33+
AssemblyFilename = ParseAssemblyName();
34+
}
35+
36+
private string ParseAssemblyName()
37+
{
38+
var assemblyNameParts = FullyQualifiedAssemblyName.Split(',', StringSplitOptions.None);
39+
if (assemblyNameParts.Length < 2 || string.IsNullOrWhiteSpace(assemblyNameParts[1]))
40+
{
41+
throw new ConfigurationException($"Storage service '{FullyQualifiedAssemblyName}' is invalid. Please provide a fully qualified name.")
42+
{
43+
HelpLink = "https://docs.microsoft.com/en-us/dotnet/standard/assembly/find-fully-qualified-name"
44+
};
45+
}
46+
47+
return assemblyNameParts[1].Trim();
48+
}
49+
50+
public abstract IHealthChecksBuilder Configure(
51+
IHealthChecksBuilder builder,
52+
IServiceProvider serviceProvider,
53+
HealthStatus? failureStatus = null,
54+
IEnumerable<string>? tags = null,
55+
TimeSpan? timeout = null);
56+
}
57+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.IO.Abstractions;
18+
using Ardalis.GuardClauses;
19+
using Microsoft.Extensions.DependencyInjection;
20+
using Monai.Deploy.Storage.API;
21+
using Monai.Deploy.Storage.Configuration;
22+
23+
namespace Monai.Deploy.Storage
24+
{
25+
public static class IHealthChecksBuilderExtensions
26+
{
27+
/// <summary>
28+
/// Configures health check for the MONAI Deploy Storage Service.
29+
/// </summary>
30+
/// <param name="builder">Instance of <see cref="IHealthChecksBuilder"/>.</param>
31+
/// <param name="serviceProvider">Instance of <see cref="IServiceProvider"/>.</param>
32+
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
33+
/// <returns>Instance of <see cref="IHealthChecksBuilder"/>.</returns>
34+
/// <exception cref="ConfigurationException"></exception>
35+
public static IHealthChecksBuilder AddMOnaiDeployStorageHealthCheck(this IHealthChecksBuilder builder, IServiceProvider serviceProvider, string fullyQualifiedTypeName)
36+
=> AddMOnaiDeployStorageHealthCheck(builder, serviceProvider, fullyQualifiedTypeName, new FileSystem());
37+
38+
/// <summary>
39+
/// Configures health check for the MONAI Deploy Storage Service.
40+
/// </summary>
41+
/// <param name="builder">Instance of <see cref="IHealthChecksBuilder"/>.</param>
42+
/// <param name="serviceProvider">Instance of <see cref="IServiceProvider"/>.</param>
43+
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
44+
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/>.</param>
45+
/// <returns>Instance of <see cref="IHealthChecksBuilder"/>.</returns>
46+
/// <exception cref="ConfigurationException"></exception>
47+
public static IHealthChecksBuilder AddMOnaiDeployStorageHealthCheck(this IHealthChecksBuilder builder, IServiceProvider serviceProvider, string fullyQualifiedTypeName, IFileSystem fileSystem)
48+
{
49+
Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName));
50+
Guard.Against.Null(fileSystem, nameof(fileSystem));
51+
52+
ResolveEventHandler resolveEventHandler = (sender, args) =>
53+
{
54+
return IServiceCollectionExtensions.CurrentDomain_AssemblyResolve(args, fileSystem);
55+
};
56+
57+
AppDomain.CurrentDomain.AssemblyResolve += resolveEventHandler;
58+
59+
var storageServiceAssembly = IServiceCollectionExtensions.LoadAssemblyFromDisk(IServiceCollectionExtensions.GetAssemblyName(fullyQualifiedTypeName), fileSystem);
60+
var serviceRegistrationType = storageServiceAssembly.GetTypes().FirstOrDefault(p => p.IsSubclassOf(typeof(HealthCheckRegistrationBase)));
61+
62+
if (serviceRegistrationType is null || Activator.CreateInstance(serviceRegistrationType, fullyQualifiedTypeName) is not HealthCheckRegistrationBase healthCheckBuilder)
63+
{
64+
throw new ConfigurationException($"Service registrar cannot be found for the configured plug-in '{fullyQualifiedTypeName}'.");
65+
}
66+
67+
if (!IServiceCollectionExtensions.IsSupportedType(fullyQualifiedTypeName, storageServiceAssembly))
68+
{
69+
throw new ConfigurationException($"The configured type '{fullyQualifiedTypeName}' does not implement the {typeof(IStorageService).Name} interface.");
70+
}
71+
72+
healthCheckBuilder.Configure(builder, serviceProvider);
73+
74+
AppDomain.CurrentDomain.AssemblyResolve -= resolveEventHandler;
75+
return builder;
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)