Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -121,7 +121,6 @@ private static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResou
}
});

// Use custom health check that also seeds the databases and containers
var healthCheckKey = $"{builder.Resource.Name}_check";
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureCosmosDB(
sp => cosmosClient ?? throw new InvalidOperationException("CosmosClient is not initialized."),
Expand Down
86 changes: 56 additions & 30 deletions src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static IResourceBuilder<AzureStorageResource> AddAzureStorage(this IDistr
};

var resource = new AzureStorageResource(name, configureInfrastructure);

return builder.AddResource(resource)
.WithDefaultRoleAssignments(StorageBuiltInRole.GetBuiltInRoleName,
StorageBuiltInRole.StorageBlobDataContributor,
Expand Down Expand Up @@ -131,34 +132,30 @@ 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);
if (connectionString is null)
{
throw new DistributedApplicationException($"BeforeResourceStartedEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
}
// The BlobServiceClient is created before the health check is run.
// We can't use ConnectionStringAvailableEvent here because the resource doesn't have a connection string, so
// we use BeforeResourceStartedEvent

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.");
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.");
}
// The ResourceReadyEvent of a resource is triggered after its health check is healthy.
// This means we can safely use this event to create the blob containers.

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

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

Expand All @@ -182,18 +179,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 @@ -308,7 +293,22 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResource
ArgumentException.ThrowIfNullOrEmpty(name);

var resource = new AzureBlobStorageResource(name, builder.Resource);
return builder.ApplicationBuilder.AddResource(resource);

string? connectionString = null;
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
{
connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
});

var healthCheckKey = $"{resource.Name}_check";

BlobServiceClient? blobServiceClient = null;
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(sp =>
{
return blobServiceClient ??= CreateBlobServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized."));
}, name: healthCheckKey);
Comment on lines +303 to +309
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do we need to duplicate the HC here? Or why do we need to keep the HC on lines:160-167?

Copy link
Member

Choose a reason for hiding this comment

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

Here it's on the Blobs resource it. Line 160 is on the Emulator resource. Doing it on the storage is not sufficient as the WaitForHealthyAsync doesn't bubble up to the parent resources.

If it were just for the existing tests we could probably not have this specific one. But it's more consistent to keep it if we do it for containers.


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

/// <summary>
Expand All @@ -326,10 +326,24 @@ 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);
string? connectionString = null;
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
{
connectionString = await resource.Parent.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
});

var healthCheckKey = $"{resource.Name}_check";

BlobServiceClient? blobServiceClient = null;
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(
sp => blobServiceClient ??= CreateBlobServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized.")),
optionsFactory: sp => new HealthChecks.Azure.Storage.Blobs.AzureBlobStorageHealthCheckOptions { ContainerName = blobContainerName },
name: healthCheckKey);

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

/// <summary>
Expand Down Expand Up @@ -362,6 +376,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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Azure.Core;
using Azure.Core.Extensions;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Specialized;
using HealthChecks.Azure.Storage.Blobs;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -55,7 +57,7 @@ protected override void BindSettingsToConfiguration(AzureBlobStorageContainerSet
}

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

protected override bool GetHealthCheckEnabled(AzureBlobStorageContainerSettings settings)
=> !settings.DisableHealthChecks;
Expand Down
9 changes: 4 additions & 5 deletions tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,10 @@ public async Task AddAzureStorageViaPublishMode()
};
});

var blob = storage.AddBlobs("blob");
var queue = storage.AddQueues("queue");
var table = storage.AddTables("table");

storage.Resource.Outputs["blobEndpoint"] = "https://myblob";
storage.Resource.Outputs["queueEndpoint"] = "https://myqueue";
storage.Resource.Outputs["tableEndpoint"] = "https://mytable";
Expand Down Expand Up @@ -1578,7 +1582,6 @@ param principalId string
Assert.Equal(expectedBicep, storageRolesManifest.BicepText);

// Check blob resource.
var blob = storage.AddBlobs("blob");

var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource;

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

// Check queue resource.
var queue = storage.AddQueues("queue");

var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource;

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

// Check table resource.
var table = storage.AddTables("table");

var connectionStringTableResource = (IResourceWithConnectionString)table.Resource;

Assert.Equal("https://mytable", await connectionStringTableResource.GetConnectionStringAsync());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,10 @@ public async Task VerifyAzureStorageEmulatorResource()

[Fact]
[RequiresDocker]
[QuarantinedTest("https://github.com/dotnet/aspire/issues/9139")]
Copy link
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
Member

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
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
Member

Choose a reason for hiding this comment

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

Copy link
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()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));

using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
var blobs = storage.AddBlobs("BlobConnection");
Expand All @@ -146,16 +147,16 @@ public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created()
using var app = builder.Build();
await app.StartAsync();

var rns = app.Services.GetRequiredService<ResourceNotificationService>();
await rns.WaitForResourceHealthyAsync(blobContainer.Resource.Name, cancellationToken: cts.Token);

var hb = Host.CreateApplicationBuilder();
hb.Configuration["ConnectionStrings:BlobConnection"] = await blobs.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
hb.AddAzureBlobClient("BlobConnection");

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

var rns = app.Services.GetRequiredService<ResourceNotificationService>();
await rns.WaitForResourceHealthyAsync(blobContainer.Resource.Name, CancellationToken.None);

var serviceClient = host.Services.GetRequiredService<BlobServiceClient>();
var blobContainerClient = serviceClient.GetBlobContainerClient("testblobcontainer");

Expand Down