Skip to content

Commit

Permalink
feat: Some Azure Services (#218)
Browse files Browse the repository at this point in the history
* feat: Added `Azure.Blobs`

* chore: Additional Blob related changes

* chore: Added missing test

* fix: Parallelization of the tests so that we obtain uniform test results.

* chore: Added tests for Mode `SharedKey`

* fix: Removed whitespace terror

* chore: Simplified blob implementation and tests

* chore: Polished xml summaries

* fix: Mark `.Tests.Integration` Assemblies with `CollectionBehavior(CollectionBehavior.CollectionPerAssembly)`

* fix: Added missing packages

* style: Updated formatting

* fix: `new-project.ps1`

* chore: Renamed `ClientCreationMode` into `BlobClientCreationMode`

* fix: Namespaces

* feat: Added `NetEvolve.HealthChecks.Azure.Queues`

* fix(style): Reformatted files

* fix: Renamed `QueueName`

* fix: Further renames

* chore: Recreated test files

* fix: GNARF Kafka/Redpanda

* chore: Renamed

* feat: Added `NetEvolve.HealthChecks.Azure.Tables`

* fix: whitespace formatting
  • Loading branch information
samtrion authored May 22, 2024
1 parent a864bae commit 8fd7f9a
Show file tree
Hide file tree
Showing 143 changed files with 7,404 additions and 17 deletions.
9 changes: 8 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="9.25.0.90414" Condition=" '$(BuildingInsideVisualStudio)' == 'true' " />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Azure.Data.Tables" Version="12.8.3" />
<PackageVersion Include="Azure.Identity" Version="1.11.3" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.20.0" />
<PackageVersion Include="Azure.Storage.Queues" Version="12.18.0" />
<PackageVersion Include="ClickHouse.Client" Version="7.5.0" />
<PackageVersion Include="Confluent.Kafka" Version="2.4.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
Expand All @@ -23,6 +27,7 @@
<PackageVersion Update="Microsoft.AspNetCore.TestHost" Version="6.0.30" Condition=" '$(TargetFramework)' == 'net6.0' " />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.1.5" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.5" />
<PackageVersion Include="Microsoft.Extensions.Azure" Version="1.7.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="MySql.Data" Version="8.4.0" />
<PackageVersion Include="MySqlConnector" Version="2.3.7" />
Expand All @@ -32,7 +37,9 @@
<PackageVersion Include="Npgsql" Version="8.0.3" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="3.21.130" />
<PackageVersion Include="Polyfill" Version="5.2.2" />
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
<PackageVersion Include="Testcontainers.Azurite" Version="3.8.0" />
<PackageVersion Include="Testcontainers.ClickHouse" Version="3.8.0" />
<PackageVersion Include="Testcontainers.Kafka" Version="3.8.0" />
<PackageVersion Include="Testcontainers.MsSql" Version="3.8.0" />
Expand All @@ -45,4 +52,4 @@
<PackageVersion Include="xunit" Version="2.8.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
</ItemGroup>
</Project>
</Project>
165 changes: 165 additions & 0 deletions HealthChecks.sln

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions new-project.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ param (

[Parameter(Mandatory = $false)]
[Switch]
$EnableProjectGrouping
$DisableArchitectureTests = $false
)

. .\eng\scripts\new-project.ps1
Expand All @@ -47,4 +47,6 @@ New-Project `
-DisableIntegrationTests $DisableIntegrationTests `
-SolutionFile "./HealthChecks.sln" `
-OutputDirectory (Get-Location) `
-EnableProjectGrouping $EnableProjectGrouping
-EnableProjectGrouping $true `
-EnableAdvProjectGrouping $false `
-DisableArchitectureTests $DisableArchitectureTests
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace NetEvolve.HealthChecks;
namespace NetEvolve.HealthChecks.Abstractions;

using System;
using System.Collections.Generic;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using NetEvolve.Arguments;
using NetEvolve.HealthChecks.Abstractions;

/// <summary>
/// Extensions methods for <see cref="IHealthChecksBuilder"/> with custom Health Checks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public void AddKafka_WhenArgumentBuilderNull_ThrowArgumentNullException()
void Act() => _ = builder.AddKafka("Test");

// Assert
var ex = Assert.Throws<ArgumentNullException>("builder", Act);
Assert.Equal("Value cannot be null. (Parameter 'builder')", ex.Message);
_ = Assert.Throws<ArgumentNullException>("builder", Act);
}

[Fact]
Expand All @@ -39,8 +38,7 @@ public void AddKafka_WhenArgumentNameNull_ThrowArgumentNullException()
void Act() => _ = builder.AddKafka(name);

// Assert
var ex = Assert.Throws<ArgumentNullException>("name", Act);
Assert.Equal("Value cannot be null. (Parameter 'name')", ex.Message);
_ = Assert.Throws<ArgumentNullException>("name", Act);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ public void Configure_WhenArgumentNameNull_ThrowArgumentNullException()
void Act() => configure.Configure(name, options);

// Assert
var ex = Assert.Throws<ArgumentNullException>("name", Act);
Assert.Equal("Value cannot be null. (Parameter 'name')", ex.Message);
_ = Assert.Throws<ArgumentNullException>("name", Act);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace NetEvolve.HealthChecks.Azure.Blobs;

using System;
using global::Azure.Identity;
using global::Azure.Storage.Blobs;

/// <summary>
/// Describes the mode used to create the <see cref="BlobServiceClient"/>.
/// </summary>
public enum BlobClientCreationMode
{
/// <summary>
/// The default mode. The <see cref="BlobServiceClient"/> is loading the preregistered instance from the <see cref="IServiceProvider"/>.
/// </summary>
ServiceProvider = 0,

/// <summary>
/// The <see cref="BlobServiceClient"/> is created using the <see cref="DefaultAzureCredential"/>.
/// </summary>
DefaultAzureCredentials = 1,

/// <summary>
/// The <see cref="BlobServiceClient"/> is created using the <see cref="BlobContainerAvailableOptions.ConnectionString"/>.
/// </summary>
ConnectionString = 2,

/// <summary>
/// The <see cref="BlobServiceClient"/> is created using the <see cref="BlobContainerAvailableOptions.AccountName"/>
/// and <see cref="BlobContainerAvailableOptions.AccountKey"/>. As well as the <see cref="BlobContainerAvailableOptions.ServiceUri"/>.
/// </summary>
SharedKey = 3,

/// <summary>
/// The <see cref="BlobServiceClient"/> is created using the <see cref="BlobContainerAvailableOptions.ServiceUri"/>.
/// </summary>
AzureSasCredential = 4
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
namespace NetEvolve.HealthChecks.Azure.Blobs;

using System;
using System.Threading;
using global::Azure.Storage.Blobs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NetEvolve.Arguments;
using static Microsoft.Extensions.Options.ValidateOptionsResult;

internal sealed class BlobContainerAvailableConfigure
: IConfigureNamedOptions<BlobContainerAvailableOptions>,
IValidateOptions<BlobContainerAvailableOptions>
{
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;

public BlobContainerAvailableConfigure(
IConfiguration configuration,
IServiceProvider serviceProvider
)
{
_configuration = configuration;
_serviceProvider = serviceProvider;
}

public void Configure(string? name, BlobContainerAvailableOptions options)
{
Argument.ThrowIfNullOrWhiteSpace(name);
_configuration.Bind($"HealthChecks:AzureBlobContainer:{name}", options);
}

public void Configure(BlobContainerAvailableOptions options) =>
Configure(Options.DefaultName, options);

public ValidateOptionsResult Validate(string? name, BlobContainerAvailableOptions options)
{
if (string.IsNullOrWhiteSpace(name))
{
return Fail("The name cannot be null or whitespace.");
}

if (options is null)
{
return Fail("The option cannot be null.");
}

if (options.Timeout < Timeout.Infinite)
{
return Fail("The timeout cannot be less than infinite (-1).");
}

if (string.IsNullOrWhiteSpace(options.ContainerName))
{
return Fail("The container name cannot be null or whitespace.");
}

var mode = options.Mode;

return options.Mode switch
{
BlobClientCreationMode.ServiceProvider => ValidateModeServiceProvider(),
BlobClientCreationMode.ConnectionString => ValidateModeConnectionString(options),
BlobClientCreationMode.DefaultAzureCredentials
=> ValidateModeDefaultAzureCredentials(options),
BlobClientCreationMode.SharedKey => ValidateModeSharedKey(options),
BlobClientCreationMode.AzureSasCredential => ValidateModeAzureSasCredential(options),
_ => Fail($"The mode `{mode}` is not supported."),
};
}

private static ValidateOptionsResult ValidateModeAzureSasCredential(
BlobContainerAvailableOptions options
)
{
if (options.ServiceUri is null)
{
return Fail(
$"The service url cannot be null when using `{nameof(BlobClientCreationMode.AzureSasCredential)}` mode."
);
}

if (!options.ServiceUri.IsAbsoluteUri)
{
return Fail(
$"The service url must be an absolute url when using `{nameof(BlobClientCreationMode.AzureSasCredential)}` mode."
);
}

if (string.IsNullOrWhiteSpace(options.ServiceUri.Query))
{
return Fail(
$"The sas query token cannot be null or whitespace when using `{nameof(BlobClientCreationMode.AzureSasCredential)}` mode."
);
}

return Success;
}

private static ValidateOptionsResult ValidateModeSharedKey(
BlobContainerAvailableOptions options
)
{
if (options.ServiceUri is null)
{
return Fail(
$"The service url cannot be null when using `{nameof(BlobClientCreationMode.SharedKey)}` mode."
);
}

if (!options.ServiceUri.IsAbsoluteUri)
{
return Fail(
$"The service url must be an absolute url when using `{nameof(BlobClientCreationMode.SharedKey)}` mode."
);
}

if (string.IsNullOrWhiteSpace(options.AccountName))
{
return Fail(
$"The account name cannot be null or whitespace when using `{nameof(BlobClientCreationMode.SharedKey)}` mode."
);
}

if (string.IsNullOrWhiteSpace(options.AccountKey))
{
return Fail(
$"The account key cannot be null or whitespace when using `{nameof(BlobClientCreationMode.SharedKey)}` mode."
);
}

return Success;
}

private static ValidateOptionsResult ValidateModeDefaultAzureCredentials(
BlobContainerAvailableOptions options
)
{
if (options.ServiceUri is null)
{
return Fail(
$"The service url cannot be null when using `{nameof(BlobClientCreationMode.DefaultAzureCredentials)}` mode."
);
}

if (!options.ServiceUri.IsAbsoluteUri)
{
return Fail(
$"The service url must be an absolute url when using `{nameof(BlobClientCreationMode.DefaultAzureCredentials)}` mode."
);
}

return Success;
}

private static ValidateOptionsResult ValidateModeConnectionString(
BlobContainerAvailableOptions options
)
{
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
return Fail(
$"The connection string cannot be null or whitespace when using `{nameof(BlobClientCreationMode.ConnectionString)}` mode."
);
}

return Success;
}

private ValidateOptionsResult ValidateModeServiceProvider()
{
if (_serviceProvider.GetService<BlobServiceClient>() is null)
{
return Fail(
$"No service of type `{nameof(BlobServiceClient)}` registered. Please execute `builder.AddAzureClients()`."
);
}

return Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace NetEvolve.HealthChecks.Azure.Blobs;

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using NetEvolve.Extensions.Tasks;
using NetEvolve.HealthChecks.Abstractions;

internal sealed class BlobContainerAvailableHealthCheck
: ConfigurableHealthCheckBase<BlobContainerAvailableOptions>
{
private readonly IServiceProvider _serviceProvider;

public BlobContainerAvailableHealthCheck(
IServiceProvider serviceProvider,
IOptionsMonitor<BlobContainerAvailableOptions> optionsMonitor
)
: base(optionsMonitor) => _serviceProvider = serviceProvider;

protected override async ValueTask<HealthCheckResult> ExecuteHealthCheckAsync(
string name,
HealthStatus failureStatus,
BlobContainerAvailableOptions options,
CancellationToken cancellationToken
)
{
var blobClient = ClientCreation.GetBlobServiceClient(name, options, _serviceProvider);

var blobTask = blobClient
.GetBlobContainersAsync(cancellationToken: cancellationToken)
.AsPages(pageSizeHint: 1)
.GetAsyncEnumerator(cancellationToken)
.MoveNextAsync();

var (isValid, result) = await blobTask
.WithTimeoutAsync(options.Timeout, cancellationToken)
.ConfigureAwait(false);

var container = blobClient.GetBlobContainerClient(options.ContainerName);

var containerExists = await container.ExistsAsync(cancellationToken).ConfigureAwait(false);
if (!containerExists)
{
return HealthCheckResult.Unhealthy(
$"{name}: Container `{options.ContainerName}` does not exist."
);
}

(var containerInTime, _) = await container
.GetPropertiesAsync(cancellationToken: cancellationToken)
.WithTimeoutAsync(options.Timeout, cancellationToken)
.ConfigureAwait(false);

return (isValid && result && containerInTime)
? HealthCheckResult.Healthy($"{name}: Healthy")
: HealthCheckResult.Degraded($"{name}: Degraded");
}
}
Loading

0 comments on commit 8fd7f9a

Please sign in to comment.