diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index d22f612ec2..2794bcd11d 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -322,6 +322,15 @@ public static IResourceBuilder RunAsEmulator(this IResou // Deterministic file path for the configuration file based on its content var configJsonPath = aspireStore.GetFileNameWithContent($"{builder.Resource.Name}-Config.json", tempConfigFile); + // The docker container runs as a non-root user, so we need to grant other user's read/write permission + if (!OperatingSystem.IsWindows()) + { + var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + File.SetUnixFileMode(configJsonPath, mode); + } + builder.WithAnnotation(new ContainerMountAnnotation( configJsonPath, AzureEventHubsEmulatorResource.EmulatorConfigJsonPath, diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs index 9861a74229..fd2428befd 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs @@ -423,6 +423,15 @@ public static IResourceBuilder RunAsEmulator(this IReso // Deterministic file path for the configuration file based on its content var configJsonPath = aspireStore.GetFileNameWithContent($"{builder.Resource.Name}-Config.json", tempConfigFile); + // The docker container runs as a non-root user, so we need to grant other user's read/write permission + if (!OperatingSystem.IsWindows()) + { + var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + File.SetUnixFileMode(configJsonPath, mode); + } + builder.WithAnnotation(new ContainerMountAnnotation( configJsonPath, AzureServiceBusEmulatorResource.EmulatorConfigJsonPath, diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs index 1af15125d2..1b3072891e 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs @@ -110,6 +110,46 @@ public async Task VerifyAzureEventHubsEmulatorResource(bool referenceHub) } } + [Fact] + [RequiresDocker] + public async Task AzureEventHubsNs_ProducesAndConsumes() + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); + var eventHubns = builder.AddAzureEventHubs("eventhubns") + .RunAsEmulator(); + var eventHub = eventHubns.AddHub("hub"); + + using var app = builder.Build(); + await app.StartAsync(); + + var hb = Host.CreateApplicationBuilder(); + + hb.Configuration["ConnectionStrings:eventhubns"] = await eventHubns.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + hb.AddAzureEventHubProducerClient("eventhubns", settings => settings.EventHubName = "hub"); + hb.AddAzureEventHubConsumerClient("eventhubns", settings => settings.EventHubName = "hub"); + + using var host = hb.Build(); + await host.StartAsync(); + + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceHealthyAsync(eventHubns.Resource.Name, cts.Token); + + var producerClient = host.Services.GetRequiredService(); + var consumerClient = host.Services.GetRequiredService(); + + // If no exception is thrown when awaited, the Event Hubs service has acknowledged + // receipt and assumed responsibility for delivery of the set of events to its partition. + await producerClient.SendAsync([new EventData(Encoding.UTF8.GetBytes("hello worlds"))], cts.Token); + + await foreach (var partitionEvent in consumerClient.ReadEventsAsync(new ReadEventOptions { MaximumWaitTime = TimeSpan.FromSeconds(5) })) + { + Assert.Equal("hello worlds", Encoding.UTF8.GetString(partitionEvent.Data.EventBody.ToArray())); + break; + } + } + [Fact] public void AzureEventHubsUseEmulatorCallbackWithWithDataBindMountResultsInBindMountAnnotationWithDefaultPath() { @@ -393,6 +433,18 @@ public async Task AzureEventHubsEmulatorResourceGeneratesConfigJsonWithCustomiza var configJsonContent = File.ReadAllText(volumeAnnotation.Source!); + if (!OperatingSystem.IsWindows()) + { + // Ensure the configuration file has correct attributes + var fileInfo = new FileInfo(volumeAnnotation.Source!); + + var expectedUnixFileMode = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + Assert.True(fileInfo.UnixFileMode.HasFlag(expectedUnixFileMode)); + } + Assert.Equal(/*json*/""" { "UserConfig": { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs index 7b59a30128..60c0864538 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs @@ -215,7 +215,7 @@ public async Task VerifyWaitForOnServiceBusEmulatorBlocksDependentResources() await app.StopAsync(); } - [Fact(Skip = "Azure ServiceBus emulator is not reliable in CI - https://github.com/dotnet/aspire/issues/7066")] + [Fact] [RequiresDocker] public async Task VerifyAzureServiceBusEmulatorResource() { @@ -470,6 +470,18 @@ public async Task AzureServiceBusEmulatorResourceGeneratesConfigJson() var serviceBusEmulatorResource = builder.Resources.OfType().Single(x => x is { } serviceBusResource && serviceBusResource.IsEmulator); var volumeAnnotation = serviceBusEmulatorResource.Annotations.OfType().Single(); + if (!OperatingSystem.IsWindows()) + { + // Ensure the configuration file has correct attributes + var fileInfo = new FileInfo(volumeAnnotation.Source!); + + var expectedUnixFileMode = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + Assert.True(fileInfo.UnixFileMode.HasFlag(expectedUnixFileMode)); + } + var configJsonContent = File.ReadAllText(volumeAnnotation.Source!); Assert.Equal(/*json*/""" diff --git a/tests/Shared/DistributedApplicationTestingBuilderExtensions.cs b/tests/Shared/DistributedApplicationTestingBuilderExtensions.cs index 2ab4fa0501..918fc20027 100644 --- a/tests/Shared/DistributedApplicationTestingBuilderExtensions.cs +++ b/tests/Shared/DistributedApplicationTestingBuilderExtensions.cs @@ -23,9 +23,13 @@ public static IDistributedApplicationTestingBuilder WithTestAndResourceLogging(t builder.Services.AddLogging(builder => builder.AddFilter("Aspire.Hosting", LogLevel.Trace)); return builder; } + public static IDistributedApplicationTestingBuilder WithTempAspireStore(this IDistributedApplicationTestingBuilder builder) { - builder.Configuration["Aspire:Store:Path"] = Path.GetTempPath(); + // We create the Aspire Store in a folder with user-only access. This way non-root containers won't be able + // to access the files unless they correctly assign the required permissions for the container to work. + + builder.Configuration["Aspire:Store:Path"] = Directory.CreateTempSubdirectory().FullName; return builder; } }