Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Storage.Blobs;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Aspire.Hosting;

/// <summary>
/// Azure Blob Storage container health check.
/// </summary>
/// <param name="blobContainerClient">
/// The <see cref="BlobContainerClient"/> used to perform Azure Blob Storage container operations.
/// Azure SDK recommends treating clients as singletons <see href="https://devblogs.microsoft.com/azure-sdk/lifetime-management-and-thread-safety-guarantees-of-azure-sdk-net-clients/"/>,
/// so this should be the exact same instance used by other parts of the application.
/// </param>
internal sealed class AzureBlobStorageContainerHealthCheck(BlobContainerClient blobContainerClient) : IHealthCheck
{
/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
await blobContainerClient.ExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
return HealthCheckResult.Healthy();
}
catch (Exception ex)
{
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
}
}
}
83 changes: 48 additions & 35 deletions src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Azure.Provisioning.Storage;
using Azure.Storage.Blobs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -131,7 +132,6 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
});

BlobServiceClient? blobServiceClient = null;

builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (@event, ct) =>
{
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
Expand All @@ -143,27 +143,7 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
blobServiceClient = CreateBlobServiceClient(connectionString);
});

builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(builder.Resource, async (@event, ct) =>
{
if (blobServiceClient is null)
{
throw new DistributedApplicationException($"BlobServiceClient was not created for the '{builder.Resource.Name}' resource.");
}

var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
if (connectionString is null)
{
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
}

foreach (var blobContainer in builder.Resource.BlobContainers)
{
await blobServiceClient.GetBlobContainerClient(blobContainer.BlobContainerName).CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
}
});

var healthCheckKey = $"{builder.Resource.Name}_check";

builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(sp =>
Comment thread
sebastienros marked this conversation as resolved.
{
return blobServiceClient ?? throw new InvalidOperationException("BlobServiceClient is not initialized.");
Expand All @@ -182,18 +162,6 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
configureContainer?.Invoke(surrogateBuilder);

return builder;

static BlobServiceClient CreateBlobServiceClient(string connectionString)
{
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
return new BlobServiceClient(uri, new DefaultAzureCredential());
}
else
{
return new BlobServiceClient(connectionString);
}
}
}

/// <summary>
Expand Down Expand Up @@ -326,10 +294,43 @@ public static IResourceBuilder<AzureBlobStorageContainerResource> AddBlobContain
blobContainerName ??= name;

AzureBlobStorageContainerResource resource = new(name, blobContainerName, builder.Resource);

builder.Resource.Parent.BlobContainers.Add(resource);

return builder.ApplicationBuilder.AddResource(resource);
BlobServiceClient? blobServiceClient = null;
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
{
var parentResource = resource.Parent;
var parentConnectionString = await parentResource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
if (parentConnectionString is null)
{
throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{parentResource.Name}' resource but the connection string was null.");
}

blobServiceClient = CreateBlobServiceClient(parentConnectionString);
});

var healthCheckKey = $"{resource.Name}_check";
var healthCheckRegistration = new HealthCheckRegistration(
healthCheckKey,
sp =>
{
_ = blobServiceClient ?? throw new InvalidOperationException("BlobServiceClient is not initialized.");

var blobContainerClient = blobServiceClient.GetBlobContainerClient(blobContainerName);
blobContainerClient.CreateIfNotExists();
Comment thread
sebastienros marked this conversation as resolved.
Outdated

return new AzureBlobStorageContainerHealthCheck(blobContainerClient);
},
failureStatus: default,
tags: default);

builder.ApplicationBuilder.Services
.AddHealthChecks()
.Add(healthCheckRegistration);

return builder.ApplicationBuilder
.AddResource(resource)
.WithHealthCheck(healthCheckKey);
}

/// <summary>
Expand Down Expand Up @@ -362,6 +363,18 @@ public static IResourceBuilder<AzureQueueStorageResource> AddQueues(this IResour
return builder.ApplicationBuilder.AddResource(resource);
}

private static BlobServiceClient CreateBlobServiceClient(string connectionString)
{
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
return new BlobServiceClient(uri, new DefaultAzureCredential());
}
else
{
return new BlobServiceClient(connectionString);
}
}

/// <summary>
/// Assigns the specified roles to the given resource, granting it the necessary permissions
/// on the target Azure Storage account. This replaces the default role assignments for the resource.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ partial class BlobStorageContainerComponent
/// </param>
private sealed class AzureBlobStorageContainerHealthCheck(BlobContainerClient blobContainerClient) : IHealthCheck
{
private readonly BlobContainerClient _blobServiceClient = blobContainerClient ?? throw new ArgumentNullException(nameof(blobContainerClient));

/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
await _blobServiceClient.ExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
await blobContainerClient.ExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
return HealthCheckResult.Healthy();
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ public async Task VerifyAzureStorageEmulatorResource()

[Fact]
[RequiresDocker]
[QuarantinedTest("https://github.com/dotnet/aspire/issues/9139")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that this can be dropped? For quarantined tests we want to take it out after it has been green for a certain number of runs (~100 right now).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add it back then.

What else? Reopening the issues? Is tracking automatic or is there a process to follow to unquarantine like for aspnet?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, re-open the issue. And it will be tracked automatically. And I will take care of taking it out of quarantine for now. It will get semi-automated in medium term.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created()
{
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
Expand Down