-
Notifications
You must be signed in to change notification settings - Fork 524
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
Add custom application model for EventHubs emulator #7005
Changes from 4 commits
ebb6645
c2c3424
f3aded3
fd36a41
77fbc9c
00a5fd1
a667a7a
dbe2c26
1ff9b22
283b511
71238be
454fb7c
86ad563
c8edd15
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 |
---|---|---|
|
@@ -8,8 +8,9 @@ | |
using Aspire.Hosting.Utils; | ||
using Azure.Messaging.EventHubs.Producer; | ||
using Azure.Provisioning; | ||
using Azure.Provisioning.EventHubs; | ||
using AzureProvisioning = Azure.Provisioning.EventHubs; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using System.Text.Json.Nodes; | ||
|
||
namespace Aspire.Hosting; | ||
|
||
|
@@ -37,9 +38,9 @@ public static IResourceBuilder<AzureEventHubsResource> AddAzureEventHubs( | |
}; | ||
infrastructure.Add(skuParameter); | ||
|
||
var eventHubsNamespace = new EventHubsNamespace(infrastructure.AspireResource.GetBicepIdentifier()) | ||
var eventHubsNamespace = new AzureProvisioning.EventHubsNamespace(infrastructure.AspireResource.GetBicepIdentifier()) | ||
{ | ||
Sku = new EventHubsSku() | ||
Sku = new AzureProvisioning.EventHubsSku() | ||
{ | ||
Name = skuParameter | ||
}, | ||
|
@@ -52,20 +53,24 @@ public static IResourceBuilder<AzureEventHubsResource> AddAzureEventHubs( | |
var principalIdParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalId, typeof(string)); | ||
infrastructure.Add(principalIdParameter); | ||
|
||
infrastructure.Add(eventHubsNamespace.CreateRoleAssignment(EventHubsBuiltInRole.AzureEventHubsDataOwner, principalTypeParameter, principalIdParameter)); | ||
infrastructure.Add(eventHubsNamespace.CreateRoleAssignment(AzureProvisioning.EventHubsBuiltInRole.AzureEventHubsDataOwner, principalTypeParameter, principalIdParameter)); | ||
|
||
infrastructure.Add(new ProvisioningOutput("eventHubsEndpoint", typeof(string)) { Value = eventHubsNamespace.ServiceBusEndpoint }); | ||
|
||
var azureResource = (AzureEventHubsResource)infrastructure.AspireResource; | ||
|
||
foreach (var hub in azureResource.Hubs) | ||
{ | ||
var hubResource = new EventHub(Infrastructure.NormalizeBicepIdentifier(hub)) | ||
var cdkHub = hub.ToProvisioningEntity(); | ||
cdkHub.Parent = eventHubsNamespace; | ||
infrastructure.Add(cdkHub); | ||
|
||
foreach (var consumerGroup in hub.ConsumerGroups) | ||
{ | ||
Parent = eventHubsNamespace, | ||
Name = hub | ||
}; | ||
infrastructure.Add(hubResource); | ||
var cdkConsumerGroup = consumerGroup.ToProvisioningEntity(); | ||
cdkConsumerGroup.Parent = cdkHub; | ||
infrastructure.Add(cdkConsumerGroup); | ||
} | ||
} | ||
}; | ||
|
||
|
@@ -80,9 +85,34 @@ public static IResourceBuilder<AzureEventHubsResource> AddAzureEventHubs( | |
/// <param name="builder">The Azure Event Hubs resource builder.</param> | ||
/// <param name="name">The name of the Event Hub.</param> | ||
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
[Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(WithHub)} instead to add an Azure Event Hub.")] | ||
public static IResourceBuilder<AzureEventHubsResource> AddEventHub(this IResourceBuilder<AzureEventHubsResource> builder, [ResourceName] string name) | ||
{ | ||
builder.Resource.Hubs.Add(name); | ||
return builder.WithHub(name); | ||
} | ||
|
||
/// <summary> | ||
/// Adds an Azure Event Hubs hub resource to the application model. This resource requires an <see cref="AzureEventHubsResource"/> to be added to the application model. | ||
/// </summary> | ||
/// <remarks> | ||
/// This version of the package defaults to the <inheritdoc cref="EventHubsEmulatorContainerImageTags.Tag"/> tag of the <inheritdoc cref="EventHubsEmulatorContainerImageTags.Registry"/>/<inheritdoc cref="EventHubsEmulatorContainerImageTags.Image"/> container image. | ||
/// </remarks> | ||
/// <param name="builder">The Azure Event Hubs resource builder.</param> | ||
/// <param name="name">The name of the Event Hub.</param> | ||
/// <param name="configure">An optional method that can be used for customizing the <see cref="EventHub"/>.</param> | ||
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
public static IResourceBuilder<AzureEventHubsResource> WithHub(this IResourceBuilder<AzureEventHubsResource> builder, [ResourceName] string name, Action<EventHub>? configure = null) | ||
{ | ||
var hub = builder.Resource.Hubs.FirstOrDefault(x => x.Name == name); | ||
|
||
if (hub == null) | ||
{ | ||
hub = new EventHub(name); | ||
builder.Resource.Hubs.Add(hub); | ||
} | ||
|
||
configure?.Invoke(hub); | ||
|
||
return builder; | ||
} | ||
|
||
|
@@ -118,15 +148,13 @@ public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(this IResou | |
return builder; | ||
} | ||
|
||
// Add emulator container | ||
var configHostFile = Path.GetTempFileName(); | ||
if (!OperatingSystem.IsWindows()) | ||
{ | ||
File.SetUnixFileMode(configHostFile, | ||
UnixFileMode.UserRead | UnixFileMode.UserWrite | ||
| UnixFileMode.GroupRead | UnixFileMode.GroupWrite | ||
| UnixFileMode.OtherRead | UnixFileMode.OtherWrite); | ||
} | ||
var configHostFile = Path.Combine(Directory.CreateTempSubdirectory("AspireEventHubsEmulator").FullName, "Config.json"); | ||
|
||
var customMountAnnotation = new ContainerMountAnnotation( | ||
configHostFile, | ||
AzureEventHubsEmulatorResource.EmulatorConfigJsonPath, | ||
ContainerMountType.BindMount, | ||
isReadOnly: false); | ||
sebastienros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
builder | ||
.WithEndpoint(name: "emulator", targetPort: 5672) | ||
|
@@ -136,11 +164,7 @@ public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(this IResou | |
Image = EventHubsEmulatorContainerImageTags.Image, | ||
Tag = EventHubsEmulatorContainerImageTags.Tag | ||
}) | ||
.WithAnnotation(new ContainerMountAnnotation( | ||
configHostFile, | ||
AzureEventHubsEmulatorResource.EmulatorConfigJsonPath, | ||
ContainerMountType.BindMount, | ||
isReadOnly: false)); | ||
.WithAnnotation(customMountAnnotation); | ||
|
||
// Create a separate storage emulator for the Event Hub one | ||
var storageResource = builder.ApplicationBuilder | ||
|
@@ -169,9 +193,9 @@ public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(this IResou | |
// For the purposes of the health check we only need to know a hub name. If we don't have a hub | ||
// name we can't configure a valid producer client connection so we should throw. What good is | ||
// an event hub namespace without an event hub? :) | ||
if (builder.Resource.Hubs is { Count: > 0 } && builder.Resource.Hubs[0] is string hub) | ||
if (builder.Resource.Hubs is { Count: > 0 } && builder.Resource.Hubs[0] is { } hub) | ||
{ | ||
var healthCheckConnectionString = $"{connectionString};EntityPath={hub};"; | ||
var healthCheckConnectionString = $"{connectionString};EntityPath={hub.Name};"; | ||
client = new EventHubProducerClient(healthCheckConnectionString); | ||
} | ||
else | ||
|
@@ -207,30 +231,39 @@ public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(this IResou | |
|
||
foreach (var emulatorResource in eventHubsEmulatorResources) | ||
{ | ||
var configFileMount = emulatorResource.Annotations.OfType<ContainerMountAnnotation>().Single(v => v.Target == AzureEventHubsEmulatorResource.EmulatorConfigJsonPath); | ||
// A custom file mount with read-only access is used to mount the emulator configuration file. If it's not found, the read-write mount we defined on the container is used. | ||
var configFileMount = emulatorResource.Annotations.OfType<ContainerMountAnnotation>().LastOrDefault(v => v.Target == AzureEventHubsEmulatorResource.EmulatorConfigJsonPath); | ||
|
||
// If the latest mount for EmulatorConfigJsonPath is our custom one then we can generate it. | ||
if (configFileMount != customMountAnnotation) | ||
{ | ||
continue; | ||
} | ||
using var stream = new FileStream(configFileMount.Source!, FileMode.Create); | ||
using var writer = new Utf8JsonWriter(stream); | ||
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); | ||
|
||
if (!OperatingSystem.IsWindows()) | ||
{ | ||
File.SetUnixFileMode(configHostFile, | ||
UnixFileMode.UserRead | UnixFileMode.UserWrite | ||
| UnixFileMode.GroupRead | UnixFileMode.GroupWrite | ||
| UnixFileMode.OtherRead | UnixFileMode.OtherWrite); | ||
} | ||
|
||
writer.WriteStartObject(); // { | ||
writer.WriteStartObject("UserConfig"); // "UserConfig": { | ||
writer.WriteStartArray("NamespaceConfig"); // "NamespaceConfig": [ | ||
writer.WriteStartObject(); // { | ||
writer.WriteString("Type", "EventHub"); // "Type": "EventHub", | ||
writer.WriteString("Type", "EventHub"); | ||
|
||
// This name is currently required by the emulator | ||
writer.WriteString("Name", "emulatorNs1"); // "Name": "emulatorNs1" | ||
writer.WriteString("Name", "emulatorNs1"); | ||
writer.WriteStartArray("Entities"); // "Entities": [ | ||
|
||
foreach (var hub in emulatorResource.Hubs) | ||
{ | ||
// The default consumer group ('$default') is automatically created | ||
|
||
writer.WriteStartObject(); // { | ||
writer.WriteString("Name", hub); // "Name": "hub", | ||
writer.WriteString("PartitionCount", "2"); // "PartitionCount": "2", | ||
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. We used to have "2" as the default, but Is this intentional? 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. AFAIR it was 2 because of the sample they provided. But since the schema states [1-32] I decided to use 1 when the value is not set explicitly. |
||
writer.WriteStartArray("ConsumerGroups"); // "ConsumerGroups": [ | ||
writer.WriteEndArray(); // ] | ||
writer.WriteStartObject(); | ||
hub.WriteJsonObjectProperties(writer); | ||
writer.WriteEndObject(); // } | ||
} | ||
|
||
|
@@ -246,8 +279,38 @@ public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(this IResou | |
|
||
} | ||
|
||
return Task.CompletedTask; | ||
// Apply ConfigJsonAnnotation modifications | ||
foreach (var emulatorResource in eventHubsEmulatorResources) | ||
{ | ||
var configFileMount = emulatorResource.Annotations.OfType<ContainerMountAnnotation>().LastOrDefault(v => v.Target == AzureEventHubsEmulatorResource.EmulatorConfigJsonPath); | ||
|
||
// At this point there should be a mount for the Config.json file. | ||
if (configFileMount == null) | ||
{ | ||
throw new InvalidOperationException("The configuration file mount is not set."); | ||
} | ||
|
||
var configJsonAnnotations = emulatorResource.Annotations.OfType<ConfigJsonAnnotation>(); | ||
|
||
foreach (var annotation in configJsonAnnotations) | ||
{ | ||
using var readStream = new FileStream(configFileMount.Source!, FileMode.Open, FileAccess.Read); | ||
var jsonObject = JsonNode.Parse(readStream); | ||
readStream.Close(); | ||
|
||
using var writeStream = new FileStream(configFileMount.Source!, FileMode.Open, FileAccess.Write); | ||
using var writer = new Utf8JsonWriter(writeStream, new JsonWriterOptions { Indented = true }); | ||
|
||
if (jsonObject == null) | ||
{ | ||
throw new InvalidOperationException("The configuration file mount could not be parsed."); | ||
} | ||
annotation.Configure(jsonObject); | ||
jsonObject.WriteTo(writer); | ||
} | ||
} | ||
|
||
return Task.CompletedTask; | ||
}); | ||
|
||
return builder; | ||
|
@@ -272,16 +335,53 @@ public static IResourceBuilder<AzureEventHubsEmulatorResource> WithDataVolume(th | |
=> builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data", isReadOnly: false); | ||
|
||
/// <summary> | ||
/// Configures the gateway port for the Azure Event Hubs emulator. | ||
/// Configures the host port for the Azure Event Hubs emulator is exposed on instead of using randomly assigned port. | ||
/// </summary> | ||
/// <param name="builder">Builder for the Azure Event Hubs emulator container</param> | ||
/// <param name="port">Host port to bind to the emulator gateway port.</param> | ||
/// <param name="port">The port to bind on the host. If <see langword="null"/> is used, a random port will be assigned.</param> | ||
/// <returns>Azure Event Hubs emulator resource builder.</returns> | ||
[Obsolete("Use WithHostPort instead.")] | ||
public static IResourceBuilder<AzureEventHubsEmulatorResource> WithGatewayPort(this IResourceBuilder<AzureEventHubsEmulatorResource> builder, int? port) | ||
{ | ||
return WithHostPort(builder, port); | ||
} | ||
|
||
/// <summary> | ||
/// Configures the host port for the Azure Event Hubs emulator is exposed on instead of using randomly assigned port. | ||
/// </summary> | ||
/// <param name="builder">Builder for the Azure Event Hubs emulator container</param> | ||
/// <param name="port">The port to bind on the host. If <see langword="null"/> is used, a random port will be assigned.</param> | ||
/// <returns>Azure Event Hubs emulator resource builder.</returns> | ||
public static IResourceBuilder<AzureEventHubsEmulatorResource> WithHostPort(this IResourceBuilder<AzureEventHubsEmulatorResource> builder, int? port) | ||
{ | ||
return builder.WithEndpoint("emulator", endpoint => | ||
{ | ||
endpoint.Port = port; | ||
}); | ||
} | ||
|
||
/// <summary> | ||
/// Adds a bind mount for the configuration file of an Azure Service Bus emulator resource. | ||
/// </summary> | ||
/// <param name="builder">The builder for the <see cref="AzureEventHubsEmulatorResource"/>.</param> | ||
/// <param name="path">Path to the file on the AppHost where the emulator configuration is located.</param> | ||
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
public static IResourceBuilder<AzureEventHubsEmulatorResource> WithConfigurationFile(this IResourceBuilder<AzureEventHubsEmulatorResource> builder, string path) | ||
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. Do we have a test for this method? 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. It was also missing in SB, so I added it. At the same time I tried to simplify the implementation. However I intentionally didn't add support for a custom file provided for config, AND configuration hooks. I would assume if you provide a file, you could have updated it yourself. Hooks are useful when the emulator is generating a file from the resource model and you can't update some section from this model. |
||
=> builder.WithBindMount(path, AzureEventHubsEmulatorResource.EmulatorConfigJsonPath, isReadOnly: false); | ||
sebastienros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// Alters the JSON configuration document used by the emulator. | ||
/// </summary> | ||
/// <param name="builder">The builder for the <see cref="AzureEventHubsEmulatorResource"/>.</param> | ||
/// <param name="configJson">A callback to update the JSON object representation of the configuration.</param> | ||
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
public static IResourceBuilder<AzureEventHubsEmulatorResource> ConfigureEmulator(this IResourceBuilder<AzureEventHubsEmulatorResource> builder, Action<JsonNode> configJson) | ||
{ | ||
ArgumentNullException.ThrowIfNull(builder); | ||
ArgumentNullException.ThrowIfNull(configJson); | ||
|
||
builder.WithAnnotation(new ConfigJsonAnnotation(configJson)); | ||
|
||
return builder; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Text.Json.Nodes; | ||
using Aspire.Hosting.ApplicationModel; | ||
|
||
namespace Aspire.Hosting.Azure.EventHubs; | ||
|
||
/// <summary> | ||
/// Represents an annotation for updating the JSON content of a mounted document. | ||
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. What does "a mounted document" mean? 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. The Config.json file (json document) that is mounted to the container using ContainerMountType.BindMount. |
||
/// </summary> | ||
internal sealed class ConfigJsonAnnotation : IResourceAnnotation | ||
{ | ||
public ConfigJsonAnnotation(Action<JsonNode> configure) | ||
{ | ||
Configure = configure; | ||
} | ||
|
||
public Action<JsonNode> Configure { get; } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eerhardt Longer term we should have a service for doing this that gets cleaned up on exit. @karolz-ms maybe something DCP can provide since it controls the clean up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely. For example, I can imagine us having the capability for Containers where you can say "here is a blob of JSON that I need to appear as file inside the container, under this path". Sort of like Kubernetes ConfigMaps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #7029 to track.