diff --git a/Directory.Packages.props b/Directory.Packages.props index 6754c03..5804944 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + @@ -44,6 +45,7 @@ + diff --git a/HealthChecks.sln b/HealthChecks.sln index 4bd8157..31ce847 100644 --- a/HealthChecks.sln +++ b/HealthChecks.sln @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.HealthChecks.Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.HealthChecks.Tests.Architecture", "tests\NetEvolve.HealthChecks.Tests.Architecture\NetEvolve.HealthChecks.Tests.Architecture.csproj", "{17BCA132-1FBB-46C1-B6A1-60F64969383D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.HealthChecks.AWS.SNS", "src\NetEvolve.HealthChecks.AWS.SNS\NetEvolve.HealthChecks.AWS.SNS.csproj", "{546D5904-1811-457F-8C48-3F78D7F0C803}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -315,6 +317,18 @@ Global {17BCA132-1FBB-46C1-B6A1-60F64969383D}.Release|x64.Build.0 = Release|Any CPU {17BCA132-1FBB-46C1-B6A1-60F64969383D}.Release|x86.ActiveCfg = Release|Any CPU {17BCA132-1FBB-46C1-B6A1-60F64969383D}.Release|x86.Build.0 = Release|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Debug|Any CPU.Build.0 = Debug|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Debug|x64.ActiveCfg = Debug|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Debug|x64.Build.0 = Debug|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Debug|x86.ActiveCfg = Debug|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Debug|x86.Build.0 = Debug|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Release|Any CPU.ActiveCfg = Release|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Release|Any CPU.Build.0 = Release|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Release|x64.ActiveCfg = Release|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Release|x64.Build.0 = Release|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Release|x86.ActiveCfg = Release|Any CPU + {546D5904-1811-457F-8C48-3F78D7F0C803}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,6 +354,7 @@ Global {66406BE8-0281-4C95-B90B-20CAE4516A16} = {E412EC77-2022-4A1D-AAC1-FDF1A4A45827} {2B089420-E791-44E7-B471-F6F527B33E1C} = {E412EC77-2022-4A1D-AAC1-FDF1A4A45827} {17BCA132-1FBB-46C1-B6A1-60F64969383D} = {E412EC77-2022-4A1D-AAC1-FDF1A4A45827} + {546D5904-1811-457F-8C48-3F78D7F0C803} = {EF615D18-42E2-48A4-8EBA-E652DC574C56} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {28B4CC2B-39E8-49C0-9687-78121BD83A53} diff --git a/src/NetEvolve.HealthChecks.AWS.SNS/CreationMode.cs b/src/NetEvolve.HealthChecks.AWS.SNS/CreationMode.cs new file mode 100644 index 0000000..9515cf8 --- /dev/null +++ b/src/NetEvolve.HealthChecks.AWS.SNS/CreationMode.cs @@ -0,0 +1,6 @@ +namespace NetEvolve.HealthChecks.AWS.SNS; + +public enum CreationMode +{ + BasicAuthentication = 0, +} diff --git a/src/NetEvolve.HealthChecks.AWS.SNS/DependencyInjectionExtensions.cs b/src/NetEvolve.HealthChecks.AWS.SNS/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..a0ccdc7 --- /dev/null +++ b/src/NetEvolve.HealthChecks.AWS.SNS/DependencyInjectionExtensions.cs @@ -0,0 +1,66 @@ +namespace NetEvolve.HealthChecks.AWS.SNS; + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using NetEvolve.Arguments; +using NetEvolve.HealthChecks.Abstractions; + +/// +/// Extensions methods for with custom Health Checks. +/// +public static class DependencyInjectionExtensions +{ + private static readonly string[] _defaultTags = ["aws", "sns", "message-queue"]; + + /// + /// Add a health check for AWS SimpleNotificationService (SNS). + /// + /// The . + /// The name of the . + /// An optional action to configure. + /// A list of additional tags that can be used to filter sets of health checks. Optional. + /// The is . + /// The is . + /// The is or whitespace. + /// The is already in use. + /// The is . + public static IHealthChecksBuilder AddSimpleNotificationService( + [NotNull] this IHealthChecksBuilder builder, + [NotNull] string name, + Action? options = null, + params string[] tags + ) + { + ArgumentNullException.ThrowIfNull(builder); + Argument.ThrowIfNullOrEmpty(name); + ArgumentNullException.ThrowIfNull(tags); + + if (!builder.IsServiceTypeRegistered()) + { + _ = builder + .Services.AddSingleton() + .AddSingleton() + .ConfigureOptions(); + } + + if (builder.IsNameAlreadyUsed(name)) + { + throw new ArgumentException($"Name `{name}` already in use.", nameof(name), null); + } + + if (options is not null) + { + _ = builder.Services.Configure(name, options); + } + + return builder.AddCheck( + name, + HealthStatus.Unhealthy, + _defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase) + ); + } + + private sealed partial class SimpleNotificationServiceCheckMarker { } +} diff --git a/src/NetEvolve.HealthChecks.AWS.SNS/NetEvolve.HealthChecks.AWS.SNS.csproj b/src/NetEvolve.HealthChecks.AWS.SNS/NetEvolve.HealthChecks.AWS.SNS.csproj new file mode 100644 index 0000000..44b417d --- /dev/null +++ b/src/NetEvolve.HealthChecks.AWS.SNS/NetEvolve.HealthChecks.AWS.SNS.csproj @@ -0,0 +1,19 @@ + + + + $(_ProjectTargetFrameworks) + + Contains HealthChecks for AWS Simple Notification Service (SNS). + $(PackageTags);aws;sns; + + + + + + + + + + + + diff --git a/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceConfigure.cs b/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceConfigure.cs new file mode 100644 index 0000000..8544d8b --- /dev/null +++ b/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceConfigure.cs @@ -0,0 +1,15 @@ +namespace NetEvolve.HealthChecks.AWS.SNS; + +using Microsoft.Extensions.Options; + +internal sealed class SimpleNotificationServiceConfigure + : IConfigureNamedOptions, + IValidateOptions +{ + public void Configure(string? name, SimpleNotificationServiceOptions options) { } + + public void Configure(SimpleNotificationServiceOptions options) { } + + public ValidateOptionsResult Validate(string? name, SimpleNotificationServiceOptions options) => + ValidateOptionsResult.Success; +} diff --git a/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceHealthCheck.cs b/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceHealthCheck.cs new file mode 100644 index 0000000..0628087 --- /dev/null +++ b/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceHealthCheck.cs @@ -0,0 +1,69 @@ +namespace NetEvolve.HealthChecks.AWS.SNS; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using NetEvolve.Extensions.Tasks; +using NetEvolve.HealthChecks.Abstractions; + +internal sealed class SimpleNotificationServiceHealthCheck + : ConfigurableHealthCheckBase +{ + public SimpleNotificationServiceHealthCheck( + IOptionsMonitor optionsMonitor + ) + : base(optionsMonitor) { } + + protected override async ValueTask ExecuteHealthCheckAsync( + string name, + HealthStatus failureStatus, + SimpleNotificationServiceOptions options, + CancellationToken cancellationToken + ) + { + using var client = CreateClient(options); + + var (isValid, topic) = await client + .GetSubscriptionAttributesAsync( + new GetSubscriptionAttributesRequest { SubscriptionArn = options.TopicName }, + cancellationToken + ) + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + return HealthCheckState(isValid && topic is not null, name); + } + + private static AmazonSimpleNotificationServiceClient CreateClient( + SimpleNotificationServiceOptions options + ) + { + var hasCredentials = options.GetCredentials() is not null; + var hasEndpoint = options.GetRegionEndpoint() is not null; + + var config = new AmazonSimpleNotificationServiceConfig + { + ServiceURL = options.ServiceUrl, + RegionEndpoint = RegionEndpoint.USEast1, + }; + + return (hasCredentials, hasEndpoint) switch + { + (true, true) => new AmazonSimpleNotificationServiceClient( + options.GetCredentials(), + options.GetRegionEndpoint() + ), + (true, false) => new AmazonSimpleNotificationServiceClient( + options.GetCredentials(), + config + ), + (false, true) => new AmazonSimpleNotificationServiceClient(options.GetRegionEndpoint()), + _ => throw new InvalidOperationException("Invalid ClientCreationMode."), + }; + } +} diff --git a/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceOptions.cs b/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceOptions.cs new file mode 100644 index 0000000..d643540 --- /dev/null +++ b/src/NetEvolve.HealthChecks.AWS.SNS/SimpleNotificationServiceOptions.cs @@ -0,0 +1,33 @@ +namespace NetEvolve.HealthChecks.AWS.SNS; + +using System; +using Amazon; +using Amazon.Runtime; + +public sealed class SimpleNotificationServiceOptions +{ + public string? AccessKey { get; set; } + + public CreationMode CreationMode { get; set; } + + public string? SecretKey { get; set; } + +#pragma warning disable CA1056 // URI-like properties should not be strings + public string? ServiceUrl { get; set; } +#pragma warning restore CA1056 // URI-like properties should not be strings + + public string? TopicName { get; set; } + + public int Timeout { get; set; } = 100; + + internal AWSCredentials? GetCredentials() + { + return CreationMode switch + { + CreationMode.BasicAuthentication => new BasicAWSCredentials(AccessKey, SecretKey), + _ => null, + }; + } + + internal RegionEndpoint? GetRegionEndpoint() => CreationMode == CreationMode ? null : null; +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs index 8646c7f..e8e5ad7 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs @@ -19,10 +19,15 @@ private static Architecture LoadArchitecture() { System.Reflection.Assembly[] assemblies = [ + // Apache typeof(Apache.Kafka.KafkaCheck).Assembly, + // AWS + typeof(AWS.SNS.SimpleNotificationServiceHealthCheck).Assembly, + // Azure typeof(Azure.Blobs.BlobContainerAvailableHealthCheck).Assembly, typeof(Azure.Queues.QueueClientAvailableHealthCheck).Assembly, typeof(Azure.Tables.TableClientAvailableHealthCheck).Assembly, + // others typeof(ClickHouse.ClickHouseCheck).Assembly, typeof(Dapr.DaprHealthCheck).Assembly, typeof(MySql.MySqlCheck).Assembly, diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj b/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj index eade4cb..b861184 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/AWS/LocalStackInstance.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/AWS/LocalStackInstance.cs new file mode 100644 index 0000000..f1cde20 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/AWS/LocalStackInstance.cs @@ -0,0 +1,24 @@ +namespace NetEvolve.HealthChecks.Tests.Integration.AWS; + +using System.Threading.Tasks; +using Testcontainers.LocalStack; +using TestContainer = Testcontainers.LocalStack.LocalStackContainer; + +public sealed class LocalStackInstance : IAsyncLifetime +{ + /// Access Key + /// + internal const string AccessKey = "LSIAQAAAAAAVNCBMPNSG"; + internal const string SecretKey = "test"; + + private readonly TestContainer _container = new LocalStackBuilder() + .WithEnvironment("AWS_ACCESS_KEY_ID", AccessKey) + .WithEnvironment("AWS_SECRET_ACCESS_KEY", SecretKey) + .Build(); + + internal string ConnectionString => _container.GetConnectionString(); + + public async Task DisposeAsync() => await _container.DisposeAsync().ConfigureAwait(false); + + public async Task InitializeAsync() => await _container.StartAsync().ConfigureAwait(false); +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/AWS/SNS/SimpleNotificationServiceHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/AWS/SNS/SimpleNotificationServiceHealthCheckTests.cs new file mode 100644 index 0000000..e80c7bd --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/AWS/SNS/SimpleNotificationServiceHealthCheckTests.cs @@ -0,0 +1,31 @@ +namespace NetEvolve.HealthChecks.Tests.Integration.AWS.SNS; + +using NetEvolve.HealthChecks.AWS.SNS; +using NodaTime; + +public class SimpleNotificationServiceHealthCheckTests + : HealthCheckTestBase, + IClassFixture +{ + private readonly LocalStackInstance _instance; + + public SimpleNotificationServiceHealthCheckTests(LocalStackInstance instance) + { + _instance = instance; + } + + [Fact] + public async Task AddSimpleNotificationService_UseOptionsCreate_ShouldReturnHealthy() => + await RunAndVerify(healthChecks => + { + _ = healthChecks.AddSimpleNotificationService( + "TestContainerHealthy", + options => + { + options.AccessKey = LocalStackInstance.AccessKey; + options.SecretKey = LocalStackInstance.SecretKey; + options.ServiceUrl = _instance.ConnectionString; + } + ); + }); +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj b/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj index d22a7ed..37d5552 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj @@ -19,6 +19,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/SimpleNotificationServiceHealthCheckTests.AddSimpleNotificationService_UseOptionsCreate_ShouldReturnHealthy.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/SimpleNotificationServiceHealthCheckTests.AddSimpleNotificationService_UseOptionsCreate_ShouldReturnHealthy.verified.txt new file mode 100644 index 0000000..6ce74a9 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/SimpleNotificationServiceHealthCheckTests.AddSimpleNotificationService_UseOptionsCreate_ShouldReturnHealthy.verified.txt @@ -0,0 +1,25 @@ +{ + results: [ + { + description: TestContainerHealthy: Unexpected error., + exception: { + innerExceptions: [ + { + message: Exception of type 'Amazon.Runtime.Internal.HttpErrorResponseException' was thrown., + type: HttpErrorResponseException + } + ], + message: The security token included in the request is invalid., + type: Amazon.SimpleNotificationService.AmazonSimpleNotificationServiceException + }, + name: TestContainerHealthy, + status: Unhealthy, + tags: [ + aws, + sns, + message-queue + ] + } + ], + status: Unhealthy +} \ No newline at end of file