Skip to content

Commit 7baf34b

Browse files
Update health check to ensure blob containers created at right time (#9159)
* Update health check to ensure blob containers created at right time Resolves #9139 Resolves #9145 * fixup! Update health check to ensure blob containers created at right time * fixup! Update health check to ensure blob containers created at right time * fixup! Update health check to ensure blob containers created at right time * Prevent multiple container checks * Use better variable name * Move container creation to RunAsEmulator * Register single hc for blobs * Reuse blobserviceclient * Fix registrations * Remove custom heathcheck * Remove custom healthcheck * Remove specific health checks * Add blob and container health checks * Fix test * Feedback * Improve comment --------- Co-authored-by: Sebastien Ros <sebastienros@gmail.com>
1 parent 86e1f2a commit 7baf34b

6 files changed

Lines changed: 68 additions & 81 deletions

File tree

src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ private static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResou
121121
}
122122
});
123123

124-
// Use custom health check that also seeds the databases and containers
125124
var healthCheckKey = $"{builder.Resource.Name}_check";
126125
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureCosmosDB(
127126
sp => cosmosClient ?? throw new InvalidOperationException("CosmosClient is not initialized."),

src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public static IResourceBuilder<AzureStorageResource> AddAzureStorage(this IDistr
9595
};
9696

9797
var resource = new AzureStorageResource(name, configureInfrastructure);
98+
9899
return builder.AddResource(resource)
99100
.WithDefaultRoleAssignments(StorageBuiltInRole.GetBuiltInRoleName,
100101
StorageBuiltInRole.StorageBlobDataContributor,
@@ -131,34 +132,30 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
131132
});
132133

133134
BlobServiceClient? blobServiceClient = null;
134-
135135
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (@event, ct) =>
136136
{
137-
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
138-
if (connectionString is null)
139-
{
140-
throw new DistributedApplicationException($"BeforeResourceStartedEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
141-
}
137+
// The BlobServiceClient is created before the health check is run.
138+
// We can't use ConnectionStringAvailableEvent here because the resource doesn't have a connection string, so
139+
// we use BeforeResourceStartedEvent
142140

141+
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null.");
143142
blobServiceClient = CreateBlobServiceClient(connectionString);
144143
});
145144

146145
builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(builder.Resource, async (@event, ct) =>
147146
{
148-
if (blobServiceClient is null)
149-
{
150-
throw new DistributedApplicationException($"BlobServiceClient was not created for the '{builder.Resource.Name}' resource.");
151-
}
147+
// The ResourceReadyEvent of a resource is triggered after its health check is healthy.
148+
// This means we can safely use this event to create the blob containers.
152149

153-
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
154-
if (connectionString is null)
150+
if (blobServiceClient is null)
155151
{
156-
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
152+
throw new InvalidOperationException("BlobServiceClient is not initialized.");
157153
}
158154

159-
foreach (var blobContainer in builder.Resource.BlobContainers)
155+
foreach (var container in builder.Resource.BlobContainers)
160156
{
161-
await blobServiceClient.GetBlobContainerClient(blobContainer.BlobContainerName).CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
157+
var blobContainerClient = blobServiceClient.GetBlobContainerClient(container.BlobContainerName);
158+
await blobContainerClient.CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
162159
}
163160
});
164161

@@ -182,18 +179,6 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
182179
configureContainer?.Invoke(surrogateBuilder);
183180

184181
return builder;
185-
186-
static BlobServiceClient CreateBlobServiceClient(string connectionString)
187-
{
188-
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
189-
{
190-
return new BlobServiceClient(uri, new DefaultAzureCredential());
191-
}
192-
else
193-
{
194-
return new BlobServiceClient(connectionString);
195-
}
196-
}
197182
}
198183

199184
/// <summary>
@@ -308,7 +293,22 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResource
308293
ArgumentException.ThrowIfNullOrEmpty(name);
309294

310295
var resource = new AzureBlobStorageResource(name, builder.Resource);
311-
return builder.ApplicationBuilder.AddResource(resource);
296+
297+
string? connectionString = null;
298+
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
299+
{
300+
connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
301+
});
302+
303+
var healthCheckKey = $"{resource.Name}_check";
304+
305+
BlobServiceClient? blobServiceClient = null;
306+
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(sp =>
307+
{
308+
return blobServiceClient ??= CreateBlobServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized."));
309+
}, name: healthCheckKey);
310+
311+
return builder.ApplicationBuilder.AddResource(resource).WithHealthCheck(healthCheckKey);
312312
}
313313

314314
/// <summary>
@@ -326,10 +326,24 @@ public static IResourceBuilder<AzureBlobStorageContainerResource> AddBlobContain
326326
blobContainerName ??= name;
327327

328328
AzureBlobStorageContainerResource resource = new(name, blobContainerName, builder.Resource);
329-
330329
builder.Resource.Parent.BlobContainers.Add(resource);
331330

332-
return builder.ApplicationBuilder.AddResource(resource);
331+
string? connectionString = null;
332+
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
333+
{
334+
connectionString = await resource.Parent.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
335+
});
336+
337+
var healthCheckKey = $"{resource.Name}_check";
338+
339+
BlobServiceClient? blobServiceClient = null;
340+
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(
341+
sp => blobServiceClient ??= CreateBlobServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized.")),
342+
optionsFactory: sp => new HealthChecks.Azure.Storage.Blobs.AzureBlobStorageHealthCheckOptions { ContainerName = blobContainerName },
343+
name: healthCheckKey);
344+
345+
return builder.ApplicationBuilder
346+
.AddResource(resource).WithHealthCheck(healthCheckKey);
333347
}
334348

335349
/// <summary>
@@ -362,6 +376,18 @@ public static IResourceBuilder<AzureQueueStorageResource> AddQueues(this IResour
362376
return builder.ApplicationBuilder.AddResource(resource);
363377
}
364378

379+
private static BlobServiceClient CreateBlobServiceClient(string connectionString)
380+
{
381+
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
382+
{
383+
return new BlobServiceClient(uri, new DefaultAzureCredential());
384+
}
385+
else
386+
{
387+
return new BlobServiceClient(connectionString);
388+
}
389+
}
390+
365391
/// <summary>
366392
/// Assigns the specified roles to the given resource, granting it the necessary permissions
367393
/// on the target Azure Storage account. This replaces the default role assignments for the resource.

src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.BlobStorageContainerComponent.AzureBlobStorageContainerHealthCheck.cs

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.BlobStorageContainerComponent.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using Azure.Core;
77
using Azure.Core.Extensions;
88
using Azure.Storage.Blobs;
9+
using Azure.Storage.Blobs.Specialized;
10+
using HealthChecks.Azure.Storage.Blobs;
911
using Microsoft.Extensions.Azure;
1012
using Microsoft.Extensions.Configuration;
1113
using Microsoft.Extensions.DependencyInjection;
@@ -55,7 +57,7 @@ protected override void BindSettingsToConfiguration(AzureBlobStorageContainerSet
5557
}
5658

5759
protected override IHealthCheck CreateHealthCheck(BlobContainerClient client, AzureBlobStorageContainerSettings settings)
58-
=> new AzureBlobStorageContainerHealthCheck(client);
60+
=> new AzureBlobStorageHealthCheck(client.GetParentBlobServiceClient(), new AzureBlobStorageHealthCheckOptions { ContainerName = client.Name });
5961

6062
protected override bool GetHealthCheckEnabled(AzureBlobStorageContainerSettings settings)
6163
=> !settings.DisableHealthChecks;

tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,10 @@ public async Task AddAzureStorageViaPublishMode()
15031503
};
15041504
});
15051505

1506+
var blob = storage.AddBlobs("blob");
1507+
var queue = storage.AddQueues("queue");
1508+
var table = storage.AddTables("table");
1509+
15061510
storage.Resource.Outputs["blobEndpoint"] = "https://myblob";
15071511
storage.Resource.Outputs["queueEndpoint"] = "https://myqueue";
15081512
storage.Resource.Outputs["tableEndpoint"] = "https://mytable";
@@ -1578,7 +1582,6 @@ param principalId string
15781582
Assert.Equal(expectedBicep, storageRolesManifest.BicepText);
15791583

15801584
// Check blob resource.
1581-
var blob = storage.AddBlobs("blob");
15821585

15831586
var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource;
15841587

@@ -1593,8 +1596,6 @@ param principalId string
15931596
Assert.Equal(expectedBlobManifest, blobManifest.ToString());
15941597

15951598
// Check queue resource.
1596-
var queue = storage.AddQueues("queue");
1597-
15981599
var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource;
15991600

16001601
Assert.Equal("https://myqueue", await connectionStringQueueResource.GetConnectionStringAsync());
@@ -1608,8 +1609,6 @@ param principalId string
16081609
Assert.Equal(expectedQueueManifest, queueManifest.ToString());
16091610

16101611
// Check table resource.
1611-
var table = storage.AddTables("table");
1612-
16131612
var connectionStringTableResource = (IResourceWithConnectionString)table.Resource;
16141613

16151614
Assert.Equal("https://mytable", await connectionStringTableResource.GetConnectionStringAsync());

tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,10 @@ public async Task VerifyAzureStorageEmulatorResource()
135135

136136
[Fact]
137137
[RequiresDocker]
138-
[QuarantinedTest("https://github.com/dotnet/aspire/issues/9139")]
139138
public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created()
140139
{
140+
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
141+
141142
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
142143
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
143144
var blobs = storage.AddBlobs("BlobConnection");
@@ -146,16 +147,16 @@ public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created()
146147
using var app = builder.Build();
147148
await app.StartAsync();
148149

150+
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
151+
await rns.WaitForResourceHealthyAsync(blobContainer.Resource.Name, cancellationToken: cts.Token);
152+
149153
var hb = Host.CreateApplicationBuilder();
150154
hb.Configuration["ConnectionStrings:BlobConnection"] = await blobs.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
151155
hb.AddAzureBlobClient("BlobConnection");
152156

153157
using var host = hb.Build();
154158
await host.StartAsync();
155159

156-
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
157-
await rns.WaitForResourceHealthyAsync(blobContainer.Resource.Name, CancellationToken.None);
158-
159160
var serviceClient = host.Services.GetRequiredService<BlobServiceClient>();
160161
var blobContainerClient = serviceClient.GetBlobContainerClient("testblobcontainer");
161162

0 commit comments

Comments
 (0)