diff --git a/eng/Versions.props b/eng/Versions.props
index e41f7dd83f..2278650564 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -3,9 +3,9 @@
9
1
- 0
+ 1
$(MajorVersion).$(MinorVersion).$(PatchVersion)
- preview.1
+ servicing
net8.0
$(DefaultTargetFramework);net9.0
diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
index d22f612ec2..35aedb8630 100644
--- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
+++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
@@ -19,6 +19,8 @@ namespace Aspire.Hosting;
///
public static class AzureEventHubsExtensions
{
+ private const UnixFileMode FileMode644 = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead;
+
///
/// Adds an Azure Event Hubs Namespace resource to the application model. This resource can be used to create Event Hub resources.
///
@@ -322,6 +324,12 @@ 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())
+ {
+ File.SetUnixFileMode(configJsonPath, FileMode644);
+ }
+
builder.WithAnnotation(new ContainerMountAnnotation(
configJsonPath,
AzureEventHubsEmulatorResource.EmulatorConfigJsonPath,
@@ -425,6 +433,7 @@ public static IResourceBuilder WithConfiguration
private static string WriteEmulatorConfigJson(AzureEventHubsResource emulatorResource)
{
+ // This temporary file is not used by the container, it will be copied and then deleted
var filePath = Path.GetTempFileName();
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Write);
diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs
index 9861a74229..61baec40cc 100644
--- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs
+++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs
@@ -20,6 +20,8 @@ namespace Aspire.Hosting;
///
public static class AzureServiceBusExtensions
{
+ private const UnixFileMode FileMode644 = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead;
+
///
/// Adds an Azure Service Bus Namespace resource to the application model. This resource can be used to create queue, topic, and subscription resources.
///
@@ -423,6 +425,12 @@ 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())
+ {
+ File.SetUnixFileMode(configJsonPath, FileMode644);
+ }
+
builder.WithAnnotation(new ContainerMountAnnotation(
configJsonPath,
AzureServiceBusEmulatorResource.EmulatorConfigJsonPath,
@@ -550,6 +558,7 @@ public static IResourceBuilder WithHostPort(thi
private static string WriteEmulatorConfigJson(AzureServiceBusResource emulatorResource)
{
+ // This temporary file is not used by the container, it will be copied and then deleted
var filePath = Path.GetTempFileName();
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Write);
diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
index e7af6de482..5b9e84ddf7 100644
--- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
+++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
@@ -17,7 +17,7 @@ public static class PostgresBuilderExtensions
{
private const string UserEnvVarName = "POSTGRES_USER";
private const string PasswordEnvVarName = "POSTGRES_PASSWORD";
-
+
///
/// Adds a PostgreSQL resource to the application model. A container is used for local development.
///
@@ -308,8 +308,8 @@ public static IResourceBuilder WithPgWeb(this IResourceB
if (!OperatingSystem.IsWindows())
{
var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
- UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
- UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
+ UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
File.SetUnixFileMode(serverFileMount.Source!, mode);
}
diff --git a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
index 4674604a80..b2f270e63d 100644
--- a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
+++ b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
@@ -101,25 +101,33 @@ public static IResourceBuilder WithDataVolume(this IRes
/// The source directory on the host to mount into the container.
/// A flag that indicates if this is a read-only mount.
/// The .
+ ///
+ /// The container starts up as non-root and the directory must be readable by the user that the container runs as.
+ /// https://learn.microsoft.com/sql/linux/sql-server-linux-docker-container-configure?view=sql-server-ver16&pivots=cs1-bash#mount-a-host-directory-as-data-volume
+ ///
public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(source);
- // c.f. https://learn.microsoft.com/sql/linux/sql-server-linux-docker-container-configure?view=sql-server-ver15&pivots=cs1-bash#mount-a-host-directory-as-data-volume
-
- foreach (var dir in new string[] { "data", "log", "secrets" })
+ if (!OperatingSystem.IsWindows())
+ {
+ return builder.WithBindMount(source, "/var/opt/mssql", isReadOnly);
+ }
+ else
{
- var path = Path.Combine(source, dir);
+ // c.f. https://learn.microsoft.com/sql/linux/sql-server-linux-docker-container-configure?view=sql-server-ver15&pivots=cs1-bash#mount-a-host-directory-as-data-volume
- if (!Directory.Exists(path))
+ foreach (var dir in new string[] { "data", "log", "secrets" })
{
+ var path = Path.Combine(source, dir);
+
Directory.CreateDirectory(path);
+
+ builder.WithBindMount(path, $"/var/opt/mssql/{dir}", isReadOnly);
}
- builder.WithBindMount(path, $"/var/opt/mssql/{dir}", isReadOnly);
+ return builder;
}
-
- return builder;
}
}
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs
index 1af15125d2..3ccc93360b 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,16 @@ 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.GroupRead | UnixFileMode.OtherRead;
+
+ 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..a81da01bab 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs
@@ -470,6 +470,16 @@ 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.GroupRead | UnixFileMode.OtherRead;
+
+ Assert.True(fileInfo.UnixFileMode.HasFlag(expectedUnixFileMode));
+ }
+
var configJsonContent = File.ReadAllText(volumeAnnotation.Source!);
Assert.Equal(/*json*/"""
diff --git a/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs b/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs
index e24fc32478..7461ce84a1 100644
--- a/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs
+++ b/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs
@@ -149,25 +149,23 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
else
{
bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- Directory.CreateDirectory(bindMountPath);
- sqlserver1.WithDataBindMount(bindMountPath);
+ Directory.CreateDirectory(bindMountPath);
if (!OperatingSystem.IsWindows())
{
- // Change permissions for non-root accounts (container user account)
- // c.f. https://learn.microsoft.com/sql/linux/sql-server-linux-docker-container-security?view=sql-server-ver16#storagepermissions
-
- // This is the minimal set to get the tests passing.
- const UnixFileMode MsSqlPermissions =
- UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
- UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
-
- File.SetUnixFileMode(bindMountPath, MsSqlPermissions);
- File.SetUnixFileMode($"{bindMountPath}/data", MsSqlPermissions);
- File.SetUnixFileMode($"{bindMountPath}/log", MsSqlPermissions);
- File.SetUnixFileMode($"{bindMountPath}/secrets", MsSqlPermissions);
+ // The docker container runs as a non-root user, so we need to grant other user's read/write permission
+ // to the bind mount directory.
+ // Note that we need to do this after creating the directory, because the umask is applied at the time of creation.
+ const UnixFileMode BindMountPermissions =
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
+
+ File.SetUnixFileMode(bindMountPath, BindMountPermissions);
}
+
+ sqlserver1.WithDataBindMount(bindMountPath);
}
using var app1 = builder1.Build();
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;
}
}