Repository navigation
Floci Azure: add WithStorage child resource and fix dev account key #2223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| namespace Aspire.Hosting.ApplicationModel; | ||
|
|
||
| /// <summary> | ||
| /// Represents the Blob, Queue and Table Storage APIs exposed by a Floci Azure emulator resource. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The resource has no health check of its own: Aspire applies the parent emulator's health check | ||
| /// to it, so it becomes healthy as soon as the Floci Azure container is. | ||
| /// </remarks> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="parent">The parent Floci Azure emulator resource.</param> | ||
| [AspireExport(ExposeProperties = true)] | ||
| public class FlociAzureStorageResource( | ||
| string name, | ||
| FlociAzureContainerResource parent) : Resource(name), | ||
| IResourceWithParent<FlociAzureContainerResource>, | ||
| IResourceWithConnectionString | ||
| { | ||
| internal const string DefaultName = "storage"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the parent Floci Azure emulator resource. | ||
| /// </summary> | ||
| public FlociAzureContainerResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent)); | ||
|
|
||
| /// <summary> | ||
| /// Gets the storage account name. | ||
| /// </summary> | ||
| public string AccountName => FlociAzureContainerResource.DefaultAccountName; | ||
|
|
||
| /// <summary> | ||
| /// Gets the service endpoint shared by the Blob, Queue and Table APIs. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The host is the endpoint's IPv4 address rather than <c>localhost</c>: the Azure Storage SDKs | ||
| /// only read the account name from the URL path when the host is an IP address, so a | ||
| /// <c>localhost</c> URL makes them treat <c>localhost</c> as the account name. | ||
| /// </remarks> | ||
| public ReferenceExpression ServiceEndpoint => | ||
| ReferenceExpression.Create( | ||
| $"{Parent.Scheme}://{Parent.PrimaryEndpoint.Property(EndpointProperty.IPV4Host)}:{Parent.Port}/{AccountName}"); | ||
|
|
||
| /// <summary> | ||
| /// Gets the storage connection string expression, carrying the <c>BlobEndpoint</c>, | ||
| /// <c>QueueEndpoint</c> and <c>TableEndpoint</c> entries so every Azure Storage SDK client | ||
| /// resolves to the emulator. | ||
| /// </summary> | ||
| public ReferenceExpression ConnectionStringExpression => | ||
| ReferenceExpression.Create( | ||
| $"DefaultEndpointsProtocol={Parent.Scheme};AccountName={AccountName};AccountKey={FlociAzureContainerResource.DefaultAccountKey};BlobEndpoint={ServiceEndpoint};QueueEndpoint={ServiceEndpoint};TableEndpoint={ServiceEndpoint};"); | ||
|
|
||
| IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties() => | ||
| Parent.CombineProperties([ | ||
| new("AccountName", ReferenceExpression.Create($"{AccountName}")), | ||
| new("BlobEndpoint", ServiceEndpoint), | ||
| new("QueueEndpoint", ServiceEndpoint), | ||
| new("TableEndpoint", ServiceEndpoint) | ||
| ]); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -254,6 +254,33 @@ public static IResourceBuilder<FlociAzureCosmosResource> WithCosmos( | |
| .WithParentRelationship(builder); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a child resource representing the Blob, Queue and Table Storage APIs exposed by the | ||
| /// Floci Azure emulator. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Reference the returned resource with Aspire's standard <c>WithReference</c> API to inject its | ||
| /// storage connection string (e.g. for <c>AddAzureBlobServiceClient</c>). The resource has no | ||
| /// health check of its own; it reports the parent emulator's health, so dependents can | ||
| /// <c>WaitFor</c> it directly. | ||
| /// </remarks> | ||
| /// <ats-summary>Adds a Storage child resource to the Floci Azure emulator</ats-summary> | ||
| /// <param name="builder">The Floci Azure resource builder.</param> | ||
| /// <param name="name">The name of the Storage resource (default: <c>storage</c>).</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{FlociAzureStorageResource}"/> for further configuration.</returns> | ||
| [AspireExport] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in d8dfaf8. The TypeScript AppHost now calls |
||
| public static IResourceBuilder<FlociAzureStorageResource> WithStorage( | ||
| this IResourceBuilder<FlociAzureContainerResource> builder, | ||
| [ResourceName] string name = FlociAzureStorageResource.DefaultName) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(name); | ||
|
|
||
| return builder.ApplicationBuilder | ||
| .AddResource(new FlociAzureStorageResource(name, builder.Resource)) | ||
| .WithParentRelationship(builder); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mounts the Docker socket into the Floci Azure container so that Azure Functions and other | ||
| /// container-backed services can launch sibling containers. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| using Aspire.Hosting; | ||
| using Aspire.Hosting.Utils; | ||
| using Azure.Storage.Blobs; | ||
| using Azure.Storage.Sas; | ||
| using CommunityToolkit.Aspire.Testing; | ||
|
|
||
| namespace CommunityToolkit.Aspire.Hosting.Floci.Tests; | ||
|
|
||
| [RequiresDocker] | ||
| public class AzureStorageFunctionalTests(ITestOutputHelper testOutputHelper) | ||
| { | ||
| [Fact] | ||
| public async Task StorageBecomesHealthyAndSupportsBlobSdkRoundTrip() | ||
| { | ||
| using var timeout = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); | ||
| timeout.CancelAfter(TimeSpan.FromMinutes(3)); | ||
| var cancellationToken = timeout.Token; | ||
|
|
||
| using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); | ||
| var azure = builder.AddFlociAzure("floci-az"); | ||
| var storage = azure.WithStorage(); | ||
|
|
||
| await using var app = await builder.BuildAsync(cancellationToken); | ||
| await app.StartAsync(cancellationToken); | ||
| await app.ResourceNotifications.WaitForResourceHealthyAsync(storage.Resource.Name, cancellationToken); | ||
|
|
||
| string? connectionString = await app.GetConnectionStringAsync(storage.Resource.Name, cancellationToken); | ||
| Assert.NotNull(connectionString); | ||
|
|
||
| var service = new BlobServiceClient(connectionString); | ||
| Assert.Equal(storage.Resource.AccountName, service.AccountName); | ||
|
|
||
| var container = service.GetBlobContainerClient($"test-{Guid.NewGuid():N}"); | ||
| await container.CreateAsync(cancellationToken: cancellationToken); | ||
| var blob = container.GetBlobClient("greeting.txt"); | ||
| await blob.UploadAsync(BinaryData.FromString("hello"), cancellationToken); | ||
|
|
||
| var download = await blob.DownloadContentAsync(cancellationToken); | ||
| Assert.Equal("hello", download.Value.Content.ToString()); | ||
|
|
||
| // SAS generation needs the account name from the URL path, which only works for IP hosts. | ||
| Assert.True(blob.CanGenerateSasUri); | ||
| var sasUri = blob.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddMinutes(5)); | ||
| Assert.StartsWith($"{service.Uri.Scheme}://127.0.0.1:", sasUri.ToString()); | ||
| var sasDownload = await new BlobClient(sasUri).DownloadContentAsync(cancellationToken); | ||
| Assert.Equal("hello", sasDownload.Value.Content.ToString()); | ||
|
|
||
| await container.DeleteAsync(cancellationToken: cancellationToken); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.