From ffb620e697d66df3b6e9fd088f49cc060b8df1cd Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Wed, 23 Sep 2026 21:20:42 +0100 Subject: [PATCH 1/4] Use endpoint hosts for service binding Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ListmonkBuilderExtensions.cs | 8 +-- .../OpenTelemetryCollectorExtensions.cs | 12 +++-- .../StreamlitAppHostingExtension.cs | 9 +--- .../RavenDBBuilderExtensions.cs | 3 +- .../RedPandaBuilderExtensions.cs | 51 ++++++++----------- ...leDiffusionCppResourceBuilderExtensions.cs | 9 ++-- .../K6BuilderExtensions.cs | 8 ++- .../AddListmonkTests.cs | 4 +- .../K6PublicApiTests.cs | 8 +-- 9 files changed, 51 insertions(+), 61 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs index ad38e0874..7d65b3592 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs @@ -67,16 +67,16 @@ public static IResourceBuilder AddListmonk( .WithHttpEndpoint(port: port, targetPort: ListmonkPort, name: ListmonkResource.PrimaryEndpointName) .WithEntrypoint("sh") .WithArgs("-c", "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''") - .WithEnvironment(AppAddressEnvVarName, "0.0.0.0:9000") + .WithEnvironment(AppAddressEnvVarName, ReferenceExpression.Create($"{resource.PrimaryEndpoint.Property(EndpointProperty.IPV4Host)}:{resource.PrimaryEndpoint.Property(EndpointProperty.TargetPort)}")) .WithHttpHealthCheck("/health") .WithIconName("MailMultiple"); } /// - /// Configures the listmonk web server address. + /// Configures the listmonk web server bind host, overriding the default which is derived from the resource's endpoint. /// /// The listmonk resource builder. - /// The address value for LISTMONK_app__address, for example 0.0.0.0:9000. + /// The host value to bind the listmonk web server to, for example 0.0.0.0. /// The . [AspireExport] public static IResourceBuilder WithAppAddress(this IResourceBuilder builder, string address) @@ -84,7 +84,7 @@ public static IResourceBuilder WithAppAddress(this IResourceBu ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(address); - return builder.WithEnvironment(AppAddressEnvVarName, address); + return builder.WithEndpoint(ListmonkResource.PrimaryEndpointName, endpoint => endpoint.TargetHost = address); } /// diff --git a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs index 546e8a0ce..2d42ee0c4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs @@ -66,11 +66,13 @@ public static IResourceBuilder AddOpenTelemetryC const int healthPort = 13233; resourceBuilder.WithEndpoint(targetPort: healthPort, name: "health", scheme: "http") .WithHttpHealthCheck("/health", endpointName: "health") - .WithArgs( - "--feature-gates=confmap.enableMergeAppendOption", - $"--config=yaml:extensions::health_check/aspire::endpoint: 0.0.0.0:{healthPort}", - "--config=yaml:service::extensions: [ health_check/aspire ]" - ); + .WithArgs(context => + { + EndpointReference healthEndpoint = resourceBuilder.GetEndpoint("health"); + context.Args.Add("--feature-gates=confmap.enableMergeAppendOption"); + context.Args.Add(ReferenceExpression.Create($"--config=yaml:extensions::health_check/aspire::endpoint: {healthEndpoint.Property(EndpointProperty.IPV4Host)}:{healthEndpoint.Property(EndpointProperty.TargetPort)}")); + context.Args.Add("--config=yaml:service::extensions: [ health_check/aspire ]"); + }); } return resourceBuilder; diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs index 104705104..2460a4826 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs @@ -75,14 +75,7 @@ public static IResourceBuilder AddStreamlitApp( // Configure server address context.Args.Add("--server.address"); - if (builder.ExecutionContext.IsPublishMode) - { - context.Args.Add("0.0.0.0"); - } - else - { - context.Args.Add(endpoint.EndpointAnnotation.TargetHost); - } + context.Args.Add(endpoint.Property(EndpointProperty.IPV4Host)); }); // Create a StreamlitAppResource wrapping the PythonAppResource diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs index f90272512..f83c5480c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs @@ -183,8 +183,7 @@ private static Dictionary GetEnvironmentVariablesFromServerSetti private static void ConfigureEnvironmentVariables(EnvironmentCallbackContext context, RavenDBServerResource serverResource, Dictionary? environmentVariables = null) { - var tcpPort = serverResource.TcpEndpoint.IsAllocated ? serverResource.TcpEndpoint.Port : serverResource.TcpEndpoint.TargetPort; - context.EnvironmentVariables.TryAdd("RAVEN_ServerUrl_Tcp", $"{serverResource.TcpEndpoint.Scheme}://0.0.0.0:{tcpPort}"); + context.EnvironmentVariables.TryAdd("RAVEN_ServerUrl_Tcp", ReferenceExpression.Create($"{serverResource.TcpEndpoint.Scheme}://{serverResource.TcpEndpoint.Property(EndpointProperty.IPV4Host)}:{serverResource.TcpEndpoint.Property(EndpointProperty.TargetPort)}")); if (environmentVariables is null) { diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs index 55a6754a8..fa803bf20 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs @@ -306,41 +306,36 @@ private static void ConfigureRedPandaArgs(CommandLineArgsCallbackContext context // Two Kafka listeners: an "internal" listener for container-to-container traffic over the // Aspire container network, and an "external" listener that is reachable from the host. + // Use fresh (unpinned) endpoint references for binding so the address resolves relative to the + // broker itself, rather than the host-pinned references used for advertising below. + var internalBindEndpoint = resource.GetEndpoint(RedPandaServerResource.InternalEndpointName); + var kafkaBindEndpoint = resource.GetEndpoint(RedPandaServerResource.PrimaryEndpointName); context.Args.Add("--kafka-addr"); - context.Args.Add($"internal://0.0.0.0:{RedPandaServerResource.KafkaInternalBrokerPort},external://0.0.0.0:{RedPandaServerResource.KafkaBrokerPort}"); + context.Args.Add(ReferenceExpression.Create( + $"internal://{internalBindEndpoint.Property(EndpointProperty.IPV4Host)}:{internalBindEndpoint.Property(EndpointProperty.TargetPort)},external://{kafkaBindEndpoint.Property(EndpointProperty.IPV4Host)}:{kafkaBindEndpoint.Property(EndpointProperty.TargetPort)}")); // Advertised listeners tell clients how to reach the broker after the initial connection. - var internalPort = RedPandaServerResource.KafkaInternalBrokerPort.ToString(CultureInfo.InvariantCulture); - var advertised = context.ExecutionContext.IsRunMode - // In run mode, the internal listener is reached over the default Aspire container network using - // the resource name, and the external listener is reached from the host on the mapped port. - ? ReferenceExpression.Create( - $"internal://{resource.Name}:{internalPort},external://localhost:{resource.PrimaryEndpoint.Property(EndpointProperty.Port)}") - : ReferenceExpression.Create( - $"internal://{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)},external://{resource.PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}"); + // Reading from endpoint properties (rather than branching on run/publish mode) lets the + // framework resolve the correct host/port for the network context automatically. + var advertised = ReferenceExpression.Create( + $"internal://{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)},external://{resource.PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}"); context.Args.Add("--advertise-kafka-addr"); context.Args.Add(advertised); context.Args.Add("--schema-registry-addr"); - context.Args.Add($"0.0.0.0:{RedPandaServerResource.SchemaRegistryPort}"); + context.Args.Add(ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.IPV4Host)}:{resource.SchemaRegistryEndpoint.Property(EndpointProperty.TargetPort)}")); } private static void ConfigureConsoleContainer(EnvironmentCallbackContext context, RedPandaServerResource resource) { - // The console runs in its own container, so it reaches Redpanda over the default Aspire container - // network in run mode (using the resource name + target ports) and over the host otherwise. - var brokers = context.ExecutionContext.IsRunMode - ? ReferenceExpression.Create($"{resource.Name}:{resource.InternalEndpoint.Property(EndpointProperty.TargetPort)}") - : ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); + // The console runs in its own container; reading from endpoint properties resolves the correct + // host/port for whichever network the console reaches Redpanda over. + var brokers = ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); - var schemaRegistry = context.ExecutionContext.IsRunMode - ? ReferenceExpression.Create($"http://{resource.Name}:{resource.SchemaRegistryEndpoint.Property(EndpointProperty.TargetPort)}") - : ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); + var schemaRegistry = ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); - var adminApi = context.ExecutionContext.IsRunMode - ? ReferenceExpression.Create($"http://{resource.Name}:{resource.AdminEndpoint.Property(EndpointProperty.TargetPort)}") - : ReferenceExpression.Create($"{resource.AdminEndpoint.Property(EndpointProperty.Scheme)}://{resource.AdminEndpoint.Property(EndpointProperty.HostAndPort)}"); + var adminApi = ReferenceExpression.Create($"{resource.AdminEndpoint.Property(EndpointProperty.Scheme)}://{resource.AdminEndpoint.Property(EndpointProperty.HostAndPort)}"); context.EnvironmentVariables["KAFKA_BROKERS"] = brokers; context.EnvironmentVariables["KAFKA_SCHEMAREGISTRY_ENABLED"] = "true"; @@ -351,15 +346,11 @@ private static void ConfigureConsoleContainer(EnvironmentCallbackContext context private static void ConfigureKafkaUiContainer(EnvironmentCallbackContext context, RedPandaServerResource resource) { - // Kafka UI runs in its own container, so it reaches Redpanda over the default Aspire container - // network in run mode (using the resource name + target ports) and over the host otherwise. - var bootstrapServers = context.ExecutionContext.IsRunMode - ? ReferenceExpression.Create($"{resource.Name}:{resource.InternalEndpoint.Property(EndpointProperty.TargetPort)}") - : ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); - - var schemaRegistry = context.ExecutionContext.IsRunMode - ? ReferenceExpression.Create($"http://{resource.Name}:{resource.SchemaRegistryEndpoint.Property(EndpointProperty.TargetPort)}") - : ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); + // Kafka UI runs in its own container; reading from endpoint properties resolves the correct + // host/port for whichever network the UI reaches Redpanda over. + var bootstrapServers = ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); + + var schemaRegistry = ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); context.EnvironmentVariables["KAFKA_CLUSTERS_0_NAME"] = resource.Name; context.EnvironmentVariables["KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS"] = bootstrapServers; diff --git a/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs index 12f185579..070dd010f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs @@ -56,11 +56,10 @@ public static IResourceBuilder AddStableDiffusionCpp .WithImageSHA256(GetImageSha256(imageVariant)) .WithImageRegistry(StableDiffusionCppContainerImageTags.Registry) .WithEntrypoint("/sd-server") - .WithArgs( - "--listen-ip", "0.0.0.0", - "--listen-port", StableDiffusionCppResource.HttpTargetPort.ToString(), - "--lora-model-dir", "/models/loras", - "--hires-upscalers-dir", "/models/upscalers") + .WithArgs("--listen-ip", resource.PrimaryEndpoint.Property(EndpointProperty.IPV4Host)) + .WithArgs("--listen-port", resource.PrimaryEndpoint.Property(EndpointProperty.TargetPort)) + .WithArgs("--lora-model-dir", "/models/loras") + .WithArgs("--hires-upscalers-dir", "/models/upscalers") .WithBindMount(modelsDirectory, "/models") .WithBindMount(outputDirectory, "/output") .WithHttpEndpoint( diff --git a/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs index 3947ee1e7..609497cc3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs @@ -105,7 +105,13 @@ public static IResourceBuilder WithScript( ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(scriptPath); - var args = new List { "run", "--address", $"0.0.0.0:{K6Port}" }; + var endpoint = builder.Resource.PrimaryEndpoint; + var args = new List + { + "run", + "--address", + ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.TargetPort)}") + }; if (virtualUsers is not null) { diff --git a/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs index 860a04195..0daeb3664 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs @@ -68,11 +68,11 @@ public async Task WithAppAddressAddsAppAddressConfiguration() using var appBuilder = TestDistributedApplicationBuilder.Create(); var listmonk = appBuilder.AddListmonk("listmonk") - .WithAppAddress("0.0.0.0:9100"); + .WithAppAddress("myhost"); var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); - Assert.Equal("0.0.0.0:9100", config["LISTMONK_app__address"]); + Assert.Equal("myhost:9000", config["LISTMONK_app__address"]); } [Fact] diff --git a/tests/CommunityToolkit.Aspire.Hosting.k6.Tests/K6PublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.k6.Tests/K6PublicApiTests.cs index de6f8b908..b4f02eade 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.k6.Tests/K6PublicApiTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.k6.Tests/K6PublicApiTests.cs @@ -83,7 +83,7 @@ public async Task WithScriptShouldOmitVusAndDurationFlagsWhenNull() resourceBuilder.WithScript("scripts/main.js"); - Assert.Equal(["run", "--address", "0.0.0.0:6565", "scripts/main.js"], await GetArgsAsync(resourceBuilder)); + Assert.Equal(["run", "--address", "{k6.bindings.http.host}:{k6.bindings.http.targetPort}", "scripts/main.js"], await GetArgsAsync(resourceBuilder)); } [Fact] @@ -94,7 +94,7 @@ public async Task WithScriptShouldEmitEachFlagIndependently() resourceBuilder.WithScript("scripts/main.js", duration: "1m"); - Assert.Equal(["run", "--address", "0.0.0.0:6565", "--duration", "1m", "scripts/main.js"], await GetArgsAsync(resourceBuilder)); + Assert.Equal(["run", "--address", "{k6.bindings.http.host}:{k6.bindings.http.targetPort}", "--duration", "1m", "scripts/main.js"], await GetArgsAsync(resourceBuilder)); } [Fact] @@ -105,7 +105,7 @@ public async Task WithScriptShouldEmitVusAndDurationWhenSet() resourceBuilder.WithScript("scripts/main.js", 5, "30s"); - Assert.Equal(["run", "--address", "0.0.0.0:6565", "--vus", "5", "--duration", "30s", "scripts/main.js"], await GetArgsAsync(resourceBuilder)); + Assert.Equal(["run", "--address", "{k6.bindings.http.host}:{k6.bindings.http.targetPort}", "--vus", "5", "--duration", "30s", "scripts/main.js"], await GetArgsAsync(resourceBuilder)); } private static async Task> GetArgsAsync(IResourceBuilder resourceBuilder) @@ -116,7 +116,7 @@ private static async Task> GetArgsAsync(IResourceBuilder()]; + return [.. args.Select(a => a is ReferenceExpression expression ? expression.ValueExpression : (string)a)]; } [Fact] From 1dfa36cb015f71c46aa90d7e32fbd5ca940fc253 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Wed, 23 Sep 2026 22:58:50 +0100 Subject: [PATCH 2/4] Refactor endpoint resolution Use structured endpoint properties and caller-aware resolution across hosting integrations, and update endpoint allocation tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DbxBuilderExtensions.cs | 243 +++++++++++------- .../DbxConnectionConfig.cs | 11 +- .../README.md | 2 +- .../K3sBuilderExtensions.cs | 17 +- .../K3sClusterResource.cs | 2 +- .../K3sReadinessHealthCheck.cs | 14 +- .../README.md | 7 +- .../ListmonkBuilderExtensions.cs | 11 +- .../NgrokExtensions.cs | 33 ++- .../README.md | 4 +- .../OpenTelemetryCollectorExtensions.cs | 15 +- .../RavenDBBuilderExtensions.cs | 3 +- .../RedPandaBuilderExtensions.cs | 20 +- .../RedisBuilderExtensions.cs | 19 +- .../RustFsBuilderExtensions.cs | 4 +- .../K3sReadinessHealthCheckTests.cs | 22 +- .../AddListmonkTests.cs | 78 ++++-- .../ResourceCreationTests.cs | 9 +- .../ResourceCreationTests.cs | 36 ++- .../AddRustFsTests.cs | 8 +- 20 files changed, 347 insertions(+), 211 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxBuilderExtensions.cs index 6d529bac9..642b53355 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxBuilderExtensions.cs @@ -1,100 +1,143 @@ -using Aspire.Hosting.ApplicationModel; -using System.Text.Json; - -#pragma warning disable ASPIREATS001 // AspireExport is experimental - -namespace Aspire.Hosting; - -/// -/// Provides extension methods for dbx resources to an . -/// -public static class DbxBuilderExtensions -{ - private const string DBX_STATIC_DIR = "/app/static"; - private const string DBX_DATA_DIR = "/app/data"; - - /// - /// Configures the host port that the dbx resource is exposed on instead of using randomly assigned port. - /// - /// The resource builder for dbx. - /// The port to bind on the host. If is used random port will be assigned. - /// The resource builder for dbx. - [AspireExport] - public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) - { - ArgumentNullException.ThrowIfNull(builder); - - return builder.WithEndpoint(DbxContainerResource.PrimaryEndpointName, endpoint => - { - endpoint.Port = port; - }); - } - - /// - /// Adds a dbx container resource to the application. - /// - /// The resource builder. - /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. Optional; defaults to dbx. - /// The host port to bind the underlying container to. - /// - /// Multiple calls will return the same resource builder instance. - /// - /// A reference to the . - [AspireExport] - public static IResourceBuilder AddDbx(this IDistributedApplicationBuilder builder, [ResourceName] string name = "dbx", int? port = null) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(name); - - if (builder.Resources.OfType().SingleOrDefault() is { } existingDbxResource) - { - var builderForExistingResource = builder.CreateResourceBuilder(existingDbxResource); - return builderForExistingResource; - } - - var dbxContainer = new DbxContainerResource(name); - var dbxContainerBuilder = builder.AddResource(dbxContainer) - .WithImage(DbxContainerImageTags.Image, DbxContainerImageTags.Tag) - .WithImageRegistry(DbxContainerImageTags.Registry) - .WithHttpEndpoint(targetPort: 4224, port: port, name: DbxContainerResource.PrimaryEndpointName) - .WithUrlForEndpoint(DbxContainerResource.PrimaryEndpointName, e => e.DisplayText = "dbx Dashboard") - .WithIconName("WindowDatabase") - .WithEnvironment("DBX_DISABLE_PASSWORD", "true") - .ExcludeFromManifest(); - - dbxContainerBuilder.WithContainerFiles( - destinationPath: DBX_DATA_DIR, - callback: (context, _) => - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower - }; - var connectionsContents = JsonSerializer.Serialize(dbxContainer.Connections, options); - - var secrets = dbxContainer.Connections - .ToDictionary(connection => $"connection:{connection.Id}:password", connection => connection.Password); - var secretsContents = JsonSerializer.Serialize(secrets); - - IEnumerable files = [ - new ContainerFile - { - Contents = connectionsContents, - Name = "connections.json", - }, - new ContainerFile - { - Contents = secretsContents, - Name = "secrets.json", - } - ]; - - return Task.FromResult(files); - } - ); - - return dbxContainerBuilder; - } -} - -#pragma warning restore ASPIREATS001 +using Aspire.Hosting.ApplicationModel; +using System.Text.Json; + +#pragma warning disable ASPIREATS001 // AspireExport is experimental + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for dbx resources to an . +/// +public static class DbxBuilderExtensions +{ + private const string DBX_STATIC_DIR = "/app/static"; + private const string DBX_DATA_DIR = "/app/data"; + + /// + /// Configures the host port that the dbx resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for dbx. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for dbx. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(DbxContainerResource.PrimaryEndpointName, endpoint => + { + endpoint.Port = port; + }); + } + + /// + /// Adds a dbx container resource to the application. + /// + /// The resource builder. + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. Optional; defaults to dbx. + /// The host port to bind the underlying container to. + /// + /// Multiple calls will return the same resource builder instance. + /// + /// A reference to the . + [AspireExport] + public static IResourceBuilder AddDbx(this IDistributedApplicationBuilder builder, [ResourceName] string name = "dbx", int? port = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + if (builder.Resources.OfType().SingleOrDefault() is { } existingDbxResource) + { + var builderForExistingResource = builder.CreateResourceBuilder(existingDbxResource); + return builderForExistingResource; + } + + var dbxContainer = new DbxContainerResource(name); + var dbxContainerBuilder = builder.AddResource(dbxContainer) + .WithImage(DbxContainerImageTags.Image, DbxContainerImageTags.Tag) + .WithImageRegistry(DbxContainerImageTags.Registry) + .WithHttpEndpoint(targetPort: 4224, port: port, name: DbxContainerResource.PrimaryEndpointName) + .WithUrlForEndpoint(DbxContainerResource.PrimaryEndpointName, e => e.DisplayText = "dbx Dashboard") + .WithIconName("WindowDatabase") + .WithEnvironment("DBX_DISABLE_PASSWORD", "true") + .ExcludeFromManifest(); + + dbxContainerBuilder.WithContainerFiles( + destinationPath: DBX_DATA_DIR, + callback: async (context, cancellationToken) => + { + foreach (DbxConnectionConfig connection in dbxContainer.Connections) + { + await PopulateEndpointValuesAsync(connection, dbxContainer, cancellationToken) + .ConfigureAwait(false); + } + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower + }; + var connectionsContents = JsonSerializer.Serialize(dbxContainer.Connections, options); + + var secrets = dbxContainer.Connections + .ToDictionary(connection => $"connection:{connection.Id}:password", connection => connection.Password); + var secretsContents = JsonSerializer.Serialize(secrets); + + IEnumerable files = [ + new ContainerFile + { + Contents = connectionsContents, + Name = "connections.json", + }, + new ContainerFile + { + Contents = secretsContents, + Name = "secrets.json", + } + ]; + + return files; + } + ); + + return dbxContainerBuilder; + } + + private static async Task PopulateEndpointValuesAsync( + DbxConnectionConfig connection, + DbxContainerResource dbxContainer, + CancellationToken cancellationToken) + { + if (connection.Endpoint is not null) + { + ValueProviderContext valueProviderContext = new() + { + Caller = dbxContainer, + }; + + if (string.IsNullOrWhiteSpace(connection.Host)) + { + connection.Host = (await connection.Endpoint + .Property(EndpointProperty.Host) + .GetValueAsync(valueProviderContext, cancellationToken) + .ConfigureAwait(false))!; + } + + if (connection.Port == 0) + { + string port = (await connection.Endpoint + .Property(EndpointProperty.Port) + .GetValueAsync(valueProviderContext, cancellationToken) + .ConfigureAwait(false))!; + connection.Port = ushort.Parse(port, System.Globalization.CultureInfo.InvariantCulture); + } + } + + if (string.IsNullOrWhiteSpace(connection.Host) || connection.Port == 0) + { + throw new DistributedApplicationException( + $"The dbx connection '{connection.Name}' must specify either an endpoint or explicit host and port values."); + } + } +} + +#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxConnectionConfig.cs b/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxConnectionConfig.cs index 3c54e70c2..9bfab57dc 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxConnectionConfig.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Dbx/DbxConnectionConfig.cs @@ -26,11 +26,18 @@ public sealed class DbxConnectionConfig /// Gets or sets the host address. [JsonPropertyName("host")] - public required string Host { get; set; } + public string Host { get; set; } = string.Empty; /// Gets or sets the port number. [JsonPropertyName("port")] - public required ushort Port { get; set; } + public ushort Port { get; set; } + + /// + /// Gets or sets the endpoint used to populate and + /// when their explicit values are not set. + /// + [JsonIgnore] + public EndpointReference? Endpoint { get; set; } /// Gets or sets the username for authentication. [JsonPropertyName("username")] diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md index 96d9a2a8b..534fd6bea 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md @@ -142,7 +142,7 @@ builder.AddAzureServiceBusClient("servicebus"); AMQP and AMQPS must use different host ports. Repeated calls on the same emulator return the existing child only when the resource name and any specified ports match. -For container consumers, the Service Bus reference helper adds `floci-servicebus-host.internal:host-gateway` to the container's host mappings and uses the sidecar's published AMQP port. This requires a container runtime supporting Docker's `--add-host=...:host-gateway` option. Use the Service Bus reference helper instead of passing the child's raw endpoint or connection string expression to a container. +The Service Bus reference helper resolves the proxyless AMQP endpoint from the consuming resource's network context. Host processes receive the loopback address, while container consumers receive Aspire's allocated IPv4 host address; no container-runtime host alias is required. Service Bus support is run-only. `WithServiceBus()` throws in publish mode because the Artemis sidecar has no deployable backing resource in the Aspire model. In an AppHost that also publishes, call it inside `if (builder.ExecutionContext.IsRunMode)` and reference a deployable Service Bus resource in the publish branch. diff --git a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs index 08798d6e2..813c5d9c2 100644 --- a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs @@ -19,7 +19,7 @@ public static class K3sBuilderExtensions /// The distributed application builder. /// /// The resource name. Also used as the DNS hostname by which containers in the DCP network - /// reach the cluster's API server (e.g. https://{name}:6443). + /// reach the cluster's API server through its Aspire endpoint. /// /// /// Host port to bind the Kubernetes API server (port 6443) to. @@ -74,7 +74,7 @@ public static IResourceBuilder AddK3sCluster( // AppHostDirectory/.k3s/{name}/ holds three sub-directories: // cluster/ — bind-mounted into the k3s container; k3s writes kubeconfig.yaml here // local/ — rewritten by the health check with server: https://localhost:{port} - // container/ — rewritten by the health check with server: https://{name}:6443 + // container/ — rewritten by the health check using the container-network endpoint var kubeconfigDir = Path.Combine(builder.AppHostDirectory, ".k3s", name); var clusterDir = Path.Combine(kubeconfigDir, "cluster"); Directory.CreateDirectory(clusterDir); @@ -120,7 +120,9 @@ public static IResourceBuilder AddK3sCluster( // ({resourceName}), and any forwarded address (0.0.0.0). .WithArgs($"--tls-san=127.0.0.1") .WithArgs($"--tls-san=localhost") - .WithArgs($"--tls-san={name}") + .WithArgs( + ReferenceExpression.Create( + $"--tls-san={resource.ApiEndpoint.Property(EndpointProperty.Host)}")) .WithArgs("--tls-san=0.0.0.0") // Disable components not needed for local development. @@ -448,7 +450,7 @@ public static IResourceBuilder WithDataVolume( /// /// The same builder, for chaining. /// - /// Agent nodes connect to the server via DCP DNS (https://{name}:6443) and use + /// Agent nodes connect to the server through the API endpoint resolved in their resource context and use /// k3s's built-in retry loop, so no explicit WaitFor is needed. The cluster health /// check waits for 1 + count nodes to reach Ready state before reporting /// healthy. Use with @@ -603,8 +605,8 @@ exec k3s "$@" // ── Agent node creation ─────────────────────────────────────────────────── // Shared by AddK3sCluster (via options.AgentCount) and WithAgentCount. - // Agents use DCP DNS (K3S_URL=https://{name}:6443) and retry until the server is - // reachable — no WaitFor to avoid a deadlock where the cluster health check waits + // Agents resolve the cluster API endpoint from their own container context and retry until + // the server is reachable — no WaitFor to avoid a deadlock where the cluster health check waits // for nodes to be Ready while nodes wait for the cluster to be healthy. private static void AddAgentNodes( IResourceBuilder clusterBuilder, @@ -636,7 +638,7 @@ private static void AddAgentNodes( .WithArgs("agent") .WithArgs("-v", "0") .WithArgs("--kubelet-arg=v=0") - .WithEnvironment("K3S_URL", $"https://{name}:6443") + .WithEnvironment("K3S_URL", resource.ApiEndpoint) .WithEnvironment("K3S_TOKEN", $"aspire-k3s-{name}-token") .WithEnvironment("K3S_NODE_NAME", agentName) .WithContainerRuntimeArgs("--privileged") @@ -729,6 +731,7 @@ internal static void ApplyServiceUrlContainerOverride(IResource dependent, K3sSe ctx.EnvironmentVariables[envKey] = $"{scheme}://host.docker.internal:{ep.HostPort}"; })); } + } #pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sClusterResource.cs b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sClusterResource.cs index 003b3056d..298d41ae0 100644 --- a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sClusterResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sClusterResource.cs @@ -37,7 +37,7 @@ public sealed class K3sClusterResource(string name) /// /// cluster/kubeconfig.yaml — raw file written by k3s (bind-mounted) /// local/kubeconfig.yaml — server: https://localhost:{port} (host processes) - /// container/kubeconfig.yaml — server: https://{name}:6443 (DCP-network containers) + /// container/kubeconfig.yaml — the API endpoint resolved for the Aspire container network /// /// internal string? KubeconfigDirectory { get; set; } diff --git a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sReadinessHealthCheck.cs b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sReadinessHealthCheck.cs index e85680adf..949c9323e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sReadinessHealthCheck.cs +++ b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sReadinessHealthCheck.cs @@ -1,3 +1,4 @@ +using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using k8s; using k8s.KubeConfigModels; @@ -13,7 +14,7 @@ namespace CommunityToolkit.Aspire.Hosting; /// it rewrites the server URL for two variants: /// /// local/kubeconfig.yaml — server: https://localhost:{allocatedPort} (host processes) -/// container/kubeconfig.yaml — server: https://{name}:6443 (DCP-network containers) +/// container/kubeconfig.yaml — the API endpoint resolved for the Aspire container network /// /// Then creates a short-lived client (disposed after each check) /// to call ListNodeAsync, confirming that all expected nodes are Ready. @@ -148,7 +149,16 @@ await WriteKubeconfigAsync(localPath, BuildConfigYaml(parsed, $"https://localhos Directory.CreateDirectory(containerDir); await WriteKubeconfigAsync( Path.Combine(containerDir, "kubeconfig.yaml"), - BuildConfigYaml(parsed, $"https://{resource.Name}:6443"), + BuildConfigYaml( + parsed, + await resource.ApiEndpoint.GetValueAsync( + new ValueProviderContext + { + Network = KnownNetworkIdentifiers.DefaultAspireContainerNetwork, + }, + ct).ConfigureAwait(false) + ?? throw new DistributedApplicationException( + "The k3s API endpoint could not be resolved for the Aspire container network.")), ct).ConfigureAwait(false); return localPath; diff --git a/src/CommunityToolkit.Aspire.Hosting.K3s/README.md b/src/CommunityToolkit.Aspire.Hosting.K3s/README.md index 7be04c162..706a4beb8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.K3s/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.K3s/README.md @@ -237,14 +237,15 @@ await builder.build().run(); ## Reaching Aspire services from k3s pods k3s pods run on the internal pod network (`10.42.0.0/16`). Flannel masquerades outbound -pod traffic through the k3s container's DCP network IP, so pods can reach DCP services -using `host.docker.internal` and the host-mapped port. +pod traffic through the k3s container's DCP network IP, so use the referenced service's +IPv4 host and port rather than assuming a container-runtime host name. ```csharp var postgres = builder.AddPostgres("db"); cluster.AddHelmRelease("my-operator", "my-operator-chart") - .WithHelmValue("database.host", "host.docker.internal") + .WithHelmValue("database.host", + postgres.GetEndpoint("tcp").Property(EndpointProperty.IPV4Host)) .WithHelmValue("database.port", postgres.GetEndpoint("tcp").Property(EndpointProperty.Port)); ``` diff --git a/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs index 7d65b3592..67d7037d1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs @@ -61,13 +61,18 @@ public static IResourceBuilder AddListmonk( ArgumentException.ThrowIfNullOrEmpty(name); var resource = new ListmonkResource(name); + EndpointReference bindEndpoint = new( + resource, + ListmonkResource.PrimaryEndpointName, + KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + return builder.AddResource(resource) .WithImage(ListmonkContainerImageTags.Image, ListmonkContainerImageTags.Tag) .WithImageRegistry(ListmonkContainerImageTags.Registry) .WithHttpEndpoint(port: port, targetPort: ListmonkPort, name: ListmonkResource.PrimaryEndpointName) .WithEntrypoint("sh") .WithArgs("-c", "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''") - .WithEnvironment(AppAddressEnvVarName, ReferenceExpression.Create($"{resource.PrimaryEndpoint.Property(EndpointProperty.IPV4Host)}:{resource.PrimaryEndpoint.Property(EndpointProperty.TargetPort)}")) + .WithEnvironment(AppAddressEnvVarName, ReferenceExpression.Create($"{bindEndpoint.Property(EndpointProperty.IPV4Host)}:{bindEndpoint.Property(EndpointProperty.TargetPort)}")) .WithHttpHealthCheck("/health") .WithIconName("MailMultiple"); } @@ -104,8 +109,8 @@ public static IResourceBuilder WithReference( var postgres = database.Resource.Parent; return builder - .WithEnvironment(DatabaseHostEnvVarName, postgres.Name) - .WithEnvironment(DatabasePortEnvVarName, ReferenceExpression.Create($"{postgres.PrimaryEndpoint.Property(EndpointProperty.TargetPort)}")) + .WithEnvironment(DatabaseHostEnvVarName, postgres.PrimaryEndpoint.Property(EndpointProperty.Host)) + .WithEnvironment(DatabasePortEnvVarName, postgres.PrimaryEndpoint.Property(EndpointProperty.Port)) .WithEnvironment(DatabaseUserEnvVarName, postgres.UserNameReference) .WithEnvironment(DatabasePasswordEnvVarName, postgres.PasswordParameter) .WithEnvironment(DatabaseNameEnvVarName, ReferenceExpression.Create($"{database.Resource.DatabaseName}")) diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs index eac67ef31..ab3a8e807 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs @@ -1,6 +1,5 @@ using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.Ngrok; -using System.Runtime.InteropServices; using System.Text; #pragma warning disable ASPIREATS001 // AspireExport is experimental @@ -12,9 +11,6 @@ namespace Aspire.Hosting; /// public static class NgrokExtensions { - private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - private static readonly bool IsOsx = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - /// /// Configures a container resource for grok which is pre-configured to connect to the resource that this method is used on. /// @@ -75,7 +71,13 @@ public static IResourceBuilder AddNgrok( .OfType() .SelectMany(annotation => annotation.Endpoints.Select(ngrokEndpoint => (endpointReference: annotation.Resource.GetEndpoint(ngrokEndpoint.EndpointName), ngrokEndpoint))) .ToList(); - await CreateNgrokConfigurationFileAsync(configurationFolder, name, endpointTuples, configurationVersion ?? 3); + await CreateNgrokConfigurationFileAsync( + configurationFolder, + name, + resource, + endpointTuples, + configurationVersion ?? 3, + ct).ConfigureAwait(false); }); return resourceBuilder; } @@ -186,8 +188,10 @@ private static IResourceBuilder AddTunnelEndpoint( private static async Task CreateNgrokConfigurationFileAsync( string configurationFolder, string name, + NgrokResource ngrokResource, IList<(EndpointReference, NgrokEndpoint)> endpointTuples, - int configurationVersion) + int configurationVersion, + CancellationToken cancellationToken) { var ngrokConfig = new StringBuilder(); ngrokConfig.AppendLine($"version: {configurationVersion}"); @@ -206,7 +210,8 @@ private static async Task CreateNgrokConfigurationFileAsync( { ngrokConfig.AppendLine($" - {label.Key}={label.Value}"); } - ngrokConfig.AppendLine($" addr: {GetUpstreamUrl(endpointReference)}"); + ngrokConfig.AppendLine( + $" addr: {await GetUpstreamUrlAsync(endpointReference, ngrokResource, cancellationToken).ConfigureAwait(false)}"); } break; case 3: @@ -222,7 +227,8 @@ private static async Task CreateNgrokConfigurationFileAsync( if (!string.IsNullOrWhiteSpace(ngrokEndpoint.Url)) ngrokConfig.AppendLine($" url: {ngrokEndpoint.Url}"); ngrokConfig.AppendLine(" upstream:"); - ngrokConfig.AppendLine($" url: {GetUpstreamUrl(endpointReference)}"); + ngrokConfig.AppendLine( + $" url: {await GetUpstreamUrlAsync(endpointReference, ngrokResource, cancellationToken).ConfigureAwait(false)}"); } } break; @@ -233,11 +239,14 @@ private static async Task CreateNgrokConfigurationFileAsync( await File.WriteAllTextAsync(Path.Combine(configurationFolder, $"{name}.yml"), ngrokConfig.ToString()); } - private static string GetUpstreamUrl(EndpointReference endpoint) + private static async Task GetUpstreamUrlAsync( + EndpointReference endpoint, + NgrokResource ngrokResource, + CancellationToken cancellationToken) { - var isLocal = endpoint.Host.Equals("localhost", StringComparison.InvariantCultureIgnoreCase); - var host = (IsWindows || IsOsx) && isLocal ? "host.docker.internal" : endpoint.Host; - return $"{endpoint.Scheme}://{host}:{endpoint.Port}"; + return (await endpoint.GetValueAsync( + new ValueProviderContext { Caller = ngrokResource }, + cancellationToken).ConfigureAwait(false))!; } } diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/README.md b/src/CommunityToolkit.Aspire.Hosting.Ngrok/README.md index 43570c3fa..aae0fc91d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/README.md @@ -48,7 +48,7 @@ This will return a JSON response with the ngrok tunneled endpoints. "public_url": "https://", "proto": "https", "config": { - "addr": "http://host.docker.internal:5165", + "addr": "http://my-http.dev.internal:5165", "inspect": true }, // ... @@ -59,7 +59,7 @@ This will return a JSON response with the ngrok tunneled endpoints. "public_url": "https://0849-94-134-176-242.ngrok-free.app", "proto": "https", "config": { - "addr": "http://host.docker.internal:3657", + "addr": "http://other-http.dev.internal:3657", "inspect": true }, // ... diff --git a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs index 2d42ee0c4..40e08d31c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs @@ -65,14 +65,13 @@ public static IResourceBuilder AddOpenTelemetryC { const int healthPort = 13233; resourceBuilder.WithEndpoint(targetPort: healthPort, name: "health", scheme: "http") - .WithHttpHealthCheck("/health", endpointName: "health") - .WithArgs(context => - { - EndpointReference healthEndpoint = resourceBuilder.GetEndpoint("health"); - context.Args.Add("--feature-gates=confmap.enableMergeAppendOption"); - context.Args.Add(ReferenceExpression.Create($"--config=yaml:extensions::health_check/aspire::endpoint: {healthEndpoint.Property(EndpointProperty.IPV4Host)}:{healthEndpoint.Property(EndpointProperty.TargetPort)}")); - context.Args.Add("--config=yaml:service::extensions: [ health_check/aspire ]"); - }); + .WithHttpHealthCheck("/health", endpointName: "health"); + + EndpointReference healthEndpoint = resourceBuilder.GetEndpoint("health"); + resourceBuilder.WithArgs( + "--feature-gates=confmap.enableMergeAppendOption", + ReferenceExpression.Create($"--config=yaml:extensions::health_check/aspire::endpoint: {healthEndpoint.Property(EndpointProperty.IPV4Host)}:{healthEndpoint.Property(EndpointProperty.TargetPort)}"), + "--config=yaml:service::extensions: [ health_check/aspire ]"); } return resourceBuilder; diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs index f83c5480c..c6306ec00 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs @@ -183,7 +183,8 @@ private static Dictionary GetEnvironmentVariablesFromServerSetti private static void ConfigureEnvironmentVariables(EnvironmentCallbackContext context, RavenDBServerResource serverResource, Dictionary? environmentVariables = null) { - context.EnvironmentVariables.TryAdd("RAVEN_ServerUrl_Tcp", ReferenceExpression.Create($"{serverResource.TcpEndpoint.Scheme}://{serverResource.TcpEndpoint.Property(EndpointProperty.IPV4Host)}:{serverResource.TcpEndpoint.Property(EndpointProperty.TargetPort)}")); + EndpointReference tcpEndpoint = serverResource.TcpEndpoint; + context.EnvironmentVariables.TryAdd("RAVEN_ServerUrl_Tcp", ReferenceExpression.Create($"{tcpEndpoint.Scheme}://{tcpEndpoint.Property(EndpointProperty.IPV4Host)}:{tcpEndpoint.Property(EndpointProperty.TargetPort)}")); if (environmentVariables is null) { diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs index fa803bf20..17a2212bf 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs @@ -331,29 +331,19 @@ private static void ConfigureConsoleContainer(EnvironmentCallbackContext context { // The console runs in its own container; reading from endpoint properties resolves the correct // host/port for whichever network the console reaches Redpanda over. - var brokers = ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); - - var schemaRegistry = ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); - - var adminApi = ReferenceExpression.Create($"{resource.AdminEndpoint.Property(EndpointProperty.Scheme)}://{resource.AdminEndpoint.Property(EndpointProperty.HostAndPort)}"); - - context.EnvironmentVariables["KAFKA_BROKERS"] = brokers; + context.EnvironmentVariables["KAFKA_BROKERS"] = resource.InternalEndpoint.Property(EndpointProperty.HostAndPort); context.EnvironmentVariables["KAFKA_SCHEMAREGISTRY_ENABLED"] = "true"; - context.EnvironmentVariables["KAFKA_SCHEMAREGISTRY_URLS"] = schemaRegistry; + context.EnvironmentVariables["KAFKA_SCHEMAREGISTRY_URLS"] = resource.SchemaRegistryEndpoint; context.EnvironmentVariables["REDPANDA_ADMINAPI_ENABLED"] = "true"; - context.EnvironmentVariables["REDPANDA_ADMINAPI_URLS"] = adminApi; + context.EnvironmentVariables["REDPANDA_ADMINAPI_URLS"] = resource.AdminEndpoint; } private static void ConfigureKafkaUiContainer(EnvironmentCallbackContext context, RedPandaServerResource resource) { // Kafka UI runs in its own container; reading from endpoint properties resolves the correct // host/port for whichever network the UI reaches Redpanda over. - var bootstrapServers = ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); - - var schemaRegistry = ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); - context.EnvironmentVariables["KAFKA_CLUSTERS_0_NAME"] = resource.Name; - context.EnvironmentVariables["KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS"] = bootstrapServers; - context.EnvironmentVariables["KAFKA_CLUSTERS_0_SCHEMAREGISTRY"] = schemaRegistry; + context.EnvironmentVariables["KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS"] = resource.InternalEndpoint.Property(EndpointProperty.HostAndPort); + context.EnvironmentVariables["KAFKA_CLUSTERS_0_SCHEMAREGISTRY"] = resource.SchemaRegistryEndpoint; } } diff --git a/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs index 1e8ee08c0..566afc1c2 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs @@ -116,16 +116,17 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context, { var redisResource = builder.Resource; - var name = redisResource.Name; - var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(name); + var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(redisResource.Name); var label = $"LABEL_{connectionId}"; - // DbGate assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address + var endpoint = redisResource.PrimaryEndpoint; var redisUrl = redisResource.PasswordParameter is not null ? - ReferenceExpression.Create($"rediss://:{redisResource.PasswordParameter}@{name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}") : - ReferenceExpression.Create($"rediss://{name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}"); + ReferenceExpression.Create( + $"rediss://:{redisResource.PasswordParameter}@{endpoint.Property(EndpointProperty.Host)}:{endpoint.Property(EndpointProperty.Port)}") : + ReferenceExpression.Create( + $"rediss://{endpoint.Property(EndpointProperty.Host)}:{endpoint.Property(EndpointProperty.Port)}"); - context.EnvironmentVariables.Add(label, name); + context.EnvironmentVariables.Add(label, redisResource.Name); context.EnvironmentVariables.Add($"URL_{connectionId}", redisUrl); context.EnvironmentVariables.Add($"ENGINE_{connectionId}", "redis@dbgate-plugin-redis"); @@ -146,15 +147,15 @@ IResourceBuilder builder ) { var redisResource = builder.Resource; - + var endpoint = redisResource.GetEndpoint("secondary"); + dbxBuilder.Resource.AddConnection( new DbxConnectionConfig { Id = redisResource.Name, Name = redisResource.Name, DbType = DbxDatabaseType.Redis, - Host = redisResource.Name, - Port = ushort.Parse(redisResource.GetEndpoint("secondary").TargetPort!.Value.ToString()), + Endpoint = endpoint, Username = string.Empty, Password = redisResource.PasswordParameter is not null ? await redisResource.PasswordParameter.GetValueAsync(context.CancellationToken) ?? string.Empty diff --git a/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs index 4f8a94a7b..352cf74c1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs @@ -51,8 +51,10 @@ public static IResourceBuilder AddRustFs( .WithImage(RustFsContainerImageTags.Image, RustFsContainerImageTags.Tag) .WithImageRegistry(RustFsContainerImageTags.Registry) .WithHttpEndpoint(name: RustFsResource.PrimaryEndpointName, port: port, + env: "RUSTFS_ADDRESS", targetPort: RustFsResource.PrimaryTargetPort) .WithHttpEndpoint(name: RustFsResource.ConsoleEndpointName, port: consolePort, + env: "RUSTFS_CONSOLE_ADDRESS", targetPort: RustFsResource.ConsoleTargetPort) .WithUrlForEndpoint(RustFsResource.PrimaryEndpointName, annot => { @@ -63,8 +65,6 @@ public static IResourceBuilder AddRustFs( annot.DisplayText = "Console"; }) .WithEnvironment("STORAGE_TYPE", "rustfs") - .WithEnvironment("RUSTFS_ADDRESS", ":" + RustFsResource.PrimaryTargetPort.ToString()) - .WithEnvironment("RUSTFS_CONSOLE_ADDRESS", ":" + RustFsResource.ConsoleTargetPort.ToString()) .WithEnvironment(AccessKeyEnvVarName, $"{resource.AccessKey}") .WithEnvironment(SecretKeyEnvVarName, $"{resource.SecretKey}") .WithHttpHealthCheck("/health", 200, RustFsResource.PrimaryEndpointName) diff --git a/tests/CommunityToolkit.Aspire.Hosting.K3s.Tests/K3sReadinessHealthCheckTests.cs b/tests/CommunityToolkit.Aspire.Hosting.K3s.Tests/K3sReadinessHealthCheckTests.cs index 2a6e65743..47301ce32 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.K3s.Tests/K3sReadinessHealthCheckTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.K3s.Tests/K3sReadinessHealthCheckTests.cs @@ -190,14 +190,14 @@ public async Task CheckCoreAsync_LocalVariantContainsLocalhostUrl() } [Fact] - public async Task CheckCoreAsync_ContainerVariantContainsClusterNameUrl() + public async Task CheckCoreAsync_ContainerVariantUsesContainerNetworkEndpoint() { var (healthCheck, dir, _) = MakeCheck(writeKubeconfig: true, nodeCount: 1, agentCount: 0); await healthCheck.CheckCoreAsync(port: 6443); var containerYaml = await File.ReadAllTextAsync(Path.Combine(dir, "container", "kubeconfig.yaml")); - Assert.Contains("https://k8s:6443", containerYaml); + Assert.Contains("https://k8s.dev.internal:6443", containerYaml); } // ── BuildConfigYaml ─────────────────────────────────────────────────────── @@ -334,7 +334,23 @@ private static (K3sReadinessHealthCheck check, string dir, Mock moc if (writeKubeconfig) File.WriteAllText(Path.Combine(clusterDir, "kubeconfig.yaml"), MinimalKubeconfig); - var cluster = new K3sClusterResource("k8s") { KubeconfigDirectory = dir }; + var appBuilder = DistributedApplication.CreateBuilder(); + var cluster = appBuilder.AddK3sCluster("k8s").Resource; + cluster.KubeconfigDirectory = dir; + var originalApiEndpoint = cluster.Annotations + .OfType() + .Single(endpoint => endpoint.Name == K3sClusterResource.ApiServerEndpointName); + originalApiEndpoint.AllocatedEndpoint = new AllocatedEndpoint(originalApiEndpoint, "localhost", 6443); + originalApiEndpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint( + KnownNetworkIdentifiers.DefaultAspireContainerNetwork, + new AllocatedEndpoint( + originalApiEndpoint, + "k8s.dev.internal", + 6443, + EndpointBindingMode.SingleAddress, + null, + KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + for (var i = 0; i < agentCount; i++) { cluster.AgentCount++; diff --git a/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs index 0daeb3664..bbf51264e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Listmonk.Tests/AddListmonkTests.cs @@ -36,9 +36,9 @@ public async Task AddListmonkContainerWithDefaultsAddsAnnotationMetadata() Assert.Equal(ListmonkContainerImageTags.Image, containerAnnotation.Image); Assert.Equal(ListmonkContainerImageTags.Registry, containerAnnotation.Registry); - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource); - Assert.Equal("0.0.0.0:9000", config["LISTMONK_app__address"]); + Assert.Equal("192.0.2.1:9000", config["LISTMONK_app__address"]); } [Fact] @@ -46,14 +46,15 @@ public async Task WithReferenceConfiguresPostgresDatabase() { using var appBuilder = TestDistributedApplicationBuilder.Create(); - var database = appBuilder.AddPostgres("mailing-postgres") - .AddDatabase("listmonkdb"); + var postgres = appBuilder.AddPostgres("mailing-postgres"); + AllocateContainerEndpoint(postgres.Resource.PrimaryEndpoint.EndpointAnnotation, "mailing-postgres.dev.internal", 15432); + + var database = postgres.AddDatabase("listmonkdb"); var listmonk = appBuilder.AddListmonk("listmonk") .WithReference(database); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource); - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); - - Assert.Equal("mailing-postgres", config["LISTMONK_db__host"]); + Assert.Equal("mailing-postgres.dev.internal", config["LISTMONK_db__host"]); Assert.Equal("5432", config["LISTMONK_db__port"]); Assert.Equal("postgres", config["LISTMONK_db__user"]); Assert.Equal("listmonkdb", config["LISTMONK_db__database"]); @@ -68,11 +69,13 @@ public async Task WithAppAddressAddsAppAddressConfiguration() using var appBuilder = TestDistributedApplicationBuilder.Create(); var listmonk = appBuilder.AddListmonk("listmonk") - .WithAppAddress("myhost"); + .WithAppAddress("192.0.2.1"); - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); + Assert.Equal("192.0.2.1", listmonk.Resource.PrimaryEndpoint.EndpointAnnotation.TargetHost); - Assert.Equal("myhost:9000", config["LISTMONK_app__address"]); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource, "192.0.2.1"); + + Assert.Equal("192.0.2.1:9000", config["LISTMONK_app__address"]); } [Fact] @@ -80,8 +83,10 @@ public async Task WithDatabaseConfigurationMethodsAddEnvironmentVariables() { using var appBuilder = TestDistributedApplicationBuilder.Create(); - var database = appBuilder.AddPostgres("postgres") - .AddDatabase("listmonkdb"); + var postgres = appBuilder.AddPostgres("postgres"); + AllocateContainerEndpoint(postgres.Resource.PrimaryEndpoint.EndpointAnnotation, "postgres.dev.internal", 15432); + + var database = postgres.AddDatabase("listmonkdb"); var listmonk = appBuilder.AddListmonk("listmonk") .WithReference(database) .WithDatabaseSslMode("require") @@ -89,8 +94,7 @@ public async Task WithDatabaseConfigurationMethodsAddEnvironmentVariables() .WithDatabaseMaxIdleConnections(10) .WithDatabaseMaxLifetime("600s") .WithDatabaseParameters("application_name=listmonk gssencmode=disable"); - - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource); Assert.Equal("require", config["LISTMONK_db__ssl_mode"]); Assert.Equal("50", config["LISTMONK_db__max_open"]); @@ -106,8 +110,7 @@ public async Task WithTimeZoneAddsTimeZoneEnvironmentVariable() var listmonk = appBuilder.AddListmonk("listmonk") .WithTimeZone("Europe/Kiev"); - - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource); Assert.Equal("Europe/Kiev", config["TZ"]); } @@ -120,8 +123,7 @@ public async Task WithUserIdAndWithGroupIdAddContainerUserEnvironmentVariables() var listmonk = appBuilder.AddListmonk("listmonk") .WithUserId(1000) .WithGroupId(1001); - - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource); Assert.Equal("1000", config["PUID"]); Assert.Equal("1001", config["PGID"]); @@ -135,8 +137,7 @@ public async Task WithAdminCredentialsAddsFirstRunAdminEnvironmentVariables() var adminPassword = appBuilder.AddParameter("admin-password", "SuperSecret123!", secret: true); var listmonk = appBuilder.AddListmonk("listmonk") .WithAdminCredentials("admin", adminPassword); - - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource); Assert.Equal("admin", config["LISTMONK_ADMIN_USER"]); Assert.Equal("SuperSecret123!", config["LISTMONK_ADMIN_PASSWORD"]); @@ -151,13 +152,46 @@ public async Task WithAdminUserAndPasswordAddFirstRunAdminEnvironmentVariables() var listmonk = appBuilder.AddListmonk("listmonk") .WithAdminUser("admin") .WithAdminPassword(adminPassword); - - var config = await listmonk.Resource.GetEnvironmentVariablesAsync(); + var config = await EvaluateEnvironmentVariablesAsync(listmonk.Resource); Assert.Equal("admin", config["LISTMONK_ADMIN_USER"]); Assert.Equal("SuperSecret123!", config["LISTMONK_ADMIN_PASSWORD"]); } + private static ValueTask> EvaluateEnvironmentVariablesAsync( + ListmonkResource resource, + string host = "192.0.2.1") + { + resource.PrimaryEndpoint.EndpointAnnotation.AllocatedEndpoint = + new AllocatedEndpoint( + resource.PrimaryEndpoint.EndpointAnnotation, + "localhost", + 19000); + resource.PrimaryEndpoint.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint( + KnownNetworkIdentifiers.DefaultAspireContainerNetwork, + new AllocatedEndpoint( + resource.PrimaryEndpoint.EndpointAnnotation, + host, + resource.PrimaryEndpoint.EndpointAnnotation.TargetPort!.Value, + EndpointBindingMode.SingleAddress, + networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + + return resource.GetEnvironmentVariablesAsync(); + } + + private static void AllocateContainerEndpoint(EndpointAnnotation endpoint, string containerHost, int hostPort) + { + endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", hostPort); + endpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint( + KnownNetworkIdentifiers.DefaultAspireContainerNetwork, + new AllocatedEndpoint( + endpoint, + containerHost, + endpoint.TargetPort!.Value, + EndpointBindingMode.SingleAddress, + networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + } + [Fact] public void WithUploadsVolumeAddsVolumeAnnotation() { diff --git a/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs index f58563905..4980deaa2 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs @@ -365,8 +365,8 @@ public void DevCertificateLogicIsNotTriggeredWhenForceNonSecureReceiverEnabled() } // Should not contain TLS certificate configuration args - Assert.DoesNotContain(context.Args.Cast(), a => a.Contains("receivers::otlp::protocols::http::tls::cert_file")); - Assert.DoesNotContain(context.Args.Cast(), a => a.Contains("receivers::otlp::protocols::grpc::tls::cert_file")); + Assert.DoesNotContain(context.Args.OfType(), a => a.Contains("receivers::otlp::protocols::http::tls::cert_file")); + Assert.DoesNotContain(context.Args.OfType(), a => a.Contains("receivers::otlp::protocols::grpc::tls::cert_file")); } [Fact] @@ -468,7 +468,10 @@ public void CanDisableHealthcheckOnCollectorResource(bool disableHealthcheck) } Assert.Contains("--feature-gates=confmap.enableMergeAppendOption", argsContext.Args); - Assert.Contains("--config=yaml:extensions::health_check/aspire::endpoint: 0.0.0.0:13233", argsContext.Args); + ReferenceExpression healthEndpointArgument = Assert.Single(argsContext.Args.OfType()); + Assert.Equal( + "--config=yaml:extensions::health_check/aspire::endpoint: {collector.bindings.health.host}:{collector.bindings.health.targetPort}", + healthEndpointArgument.ValueExpression); Assert.Contains("--config=yaml:service::extensions: [ health_check/aspire ]", argsContext.Args); } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs index ed6a56217..aab7f2ad7 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs @@ -11,7 +11,7 @@ public async Task WithDbGateAddsAnnotations() var builder = DistributedApplication.CreateBuilder(); var redisResourceBuilder = builder.AddRedis("redis") - .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)) + .WithEndpoint("tcp", e => AllocateEndpoint(e, "redis.dev.internal", 27017)) .WithDbGate(); var redisResource = redisResourceBuilder.Resource; @@ -43,10 +43,9 @@ public async Task WithDbGateAddsAnnotations() }, async item => { - var redisUrl = redisResource.PasswordParameter is not null ? - $"rediss://:{await redisResource.PasswordParameter.GetValueAsync(default)}@{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort}" : $"rediss://{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort}"; + var password = await redisResource.PasswordParameter!.GetValueAsync(default); Assert.Equal("URL_redis", item.Key); - Assert.Equal(redisUrl, item.Value); + Assert.Equal($"rediss://:{password}@redis.dev.internal:6379", item.Value); }, item => { @@ -113,13 +112,13 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() var builder = DistributedApplication.CreateBuilder(); var redisResourceBuilder1 = builder.AddRedis("redis1") - .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)) + .WithEndpoint("tcp", e => AllocateEndpoint(e, "redis1.dev.internal", 27017)) .WithDbGate(); var redisResource1 = redisResourceBuilder1.Resource; var redisResourceBuilder2 = builder.AddRedis("redis2") - .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27018)) + .WithEndpoint("tcp", e => AllocateEndpoint(e, "redis2.dev.internal", 27018)) .WithDbGate(); var redisResource2 = redisResourceBuilder2.Resource; @@ -151,11 +150,9 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() }, async item => { - var redisUrl = redisResource1.PasswordParameter is not null ? - $"rediss://:{await redisResource1.PasswordParameter.GetValueAsync(default)}@{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}" : $"rediss://{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}"; - + var password = await redisResource1.PasswordParameter!.GetValueAsync(default); Assert.Equal("URL_redis1", item.Key); - Assert.Equal(redisUrl, item.Value); + Assert.Equal($"rediss://:{password}@redis1.dev.internal:6379", item.Value); }, item => { @@ -169,11 +166,9 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() }, async item => { - var redisUrl = redisResource2.PasswordParameter is not null ? - $"rediss://:{await redisResource2.PasswordParameter.GetValueAsync(default)}@{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}" : $"rediss://{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}"; - + var password = await redisResource2.PasswordParameter!.GetValueAsync(default); Assert.Equal("URL_redis2", item.Key); - Assert.Equal(redisUrl, item.Value); + Assert.Equal($"rediss://:{password}@redis2.dev.internal:6379", item.Value); }, item => { @@ -181,4 +176,17 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() Assert.Equal("redis@dbgate-plugin-redis", item.Value); }); } + + private static void AllocateEndpoint(EndpointAnnotation endpoint, string containerHost, int hostPort) + { + endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", hostPort); + endpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint( + KnownNetworkIdentifiers.DefaultAspireContainerNetwork, + new AllocatedEndpoint( + endpoint, + containerHost, + endpoint.TargetPort ?? 6379, + EndpointBindingMode.SingleAddress, + networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs index f3e9a9ab7..327cc1ac0 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting; +using CommunityToolkit.Aspire.Testing; namespace CommunityToolkit.Aspire.Hosting.RustFs.Tests; @@ -53,7 +54,7 @@ public void RustFsResourceHasHealthCheck() } [Fact] - public void RustFsResourceHasCorrectEndpoints() + public async Task RustFsResourceHasCorrectEndpoints() { var builder = DistributedApplication.CreateBuilder(); @@ -74,6 +75,11 @@ public void RustFsResourceHasCorrectEndpoints() var consoleEndpoint = Assert.Single(endpoints, e => e.Name == "console"); Assert.Equal(9001, consoleEndpoint.TargetPort); + + var config = await resource.GetEnvironmentVariablesAsync(); + + Assert.Equal("9000", config["RUSTFS_ADDRESS"]); + Assert.Equal("9001", config["RUSTFS_CONSOLE_ADDRESS"]); } [Fact] From 2ddadc5a8a16ea68b12b0fb0149c7da8c7b2fd07 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Wed, 23 Sep 2026 23:35:21 +0100 Subject: [PATCH 3/4] Add WithHostPort()/WithHostPort() fluent extensions across hosting integrations Adds the idiomatic host-port configuration pattern (matching Adminer, DbGate, Elasticvue, etc.) to every AddXyz() extension that takes a port parameter but was missing a matching fluent helper: ActiveMQ, Azure.DataApiBuilder, Flagd, Floci (Aws/Azure/Gcp), GoFeatureFlag, K3s, KurrentDB, LavinMQ, Listmonk, Logto, MailPit, McpInspector, Meilisearch, Mosquitto, Ngrok, Ollama, PapercutSmtp, Posta, RavenDB, RustFs, Sftp, Solr, StableDiffusionCpp, SurrealDb, Umami, Zitadel, and k6. Single-endpoint resources get WithHostPort(); multi-endpoint resources get WithHostPort() for each endpoint (e.g. WithAmqpHostPort, WithManagementHostPort, WithAdminHostPort, WithSmtpHostPort). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ActiveMQBuilderExtensions.cs | 30 +++++++++++++ .../DataApiBuilderHostingExtension.cs | 16 +++++++ .../FlagdBuilderExtensions.cs | 28 +++++++++++++ .../FlociHostingExtension.Aws.cs | 17 +++++++- .../FlociHostingExtension.Azure.cs | 16 +++++++ .../FlociHostingExtension.Gcp.cs | 17 +++++++- .../GoFeatureFlagBuilderExtensions.cs | 14 +++++++ .../K3sBuilderExtensions.cs | 14 +++++++ .../KurrentDBBuilderExtensions.cs | 14 +++++++ .../LavinMQHostingExtension.cs | 28 +++++++++++++ .../ListmonkBuilderExtensions.cs | 14 +++++++ .../LogtoBuilderExtensions.cs | 30 ++++++++++++- .../MailPitHostingExtension.cs | 28 +++++++++++++ .../McpInspectorResourceBuilderExtensions.cs | 28 +++++++++++++ .../MeilisearchBuilderExtensions.cs | 14 +++++++ .../MosquittoBuilderExtensions.cs | 14 +++++++ .../NgrokExtensions.cs | 14 +++++++ ...lamaResourceBuilderExtensions.OpenWebUI.cs | 2 +- .../OllamaResourceBuilderExtensions.cs | 30 ++++++++++++- .../PapercutSmtpHostingExtension.cs | 28 +++++++++++++ .../PostaHostingExtensions.cs | 42 +++++++++++++++++++ .../RavenDBBuilderExtensions.cs | 30 ++++++++++++- .../RustFsBuilderExtensions.cs | 28 +++++++++++++ .../SftpHostingExtensions.cs | 14 +++++++ .../SolrBuilderExtensions.cs | 14 +++++++ ...leDiffusionCppResourceBuilderExtensions.cs | 14 +++++++ .../SurrealDbBuilderExtensions.cs | 16 ++++++- .../UmamiBuilderExtensions.cs | 14 +++++++ .../ZitadelHostingExtensions.cs | 14 +++++++ .../K6BuilderExtensions.cs | 14 +++++++ 30 files changed, 589 insertions(+), 7 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.ActiveMQ/ActiveMQBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.ActiveMQ/ActiveMQBuilderExtensions.cs index fffdb2e30..4a28a376e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.ActiveMQ/ActiveMQBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.ActiveMQ/ActiveMQBuilderExtensions.cs @@ -86,6 +86,36 @@ public static IResourceBuilder AddActiveMQArtemis return builder.Build(port, scheme, webPort, activeMq); } + /// + /// Configures the host port that the ActiveMQ resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for ActiveMQ. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for ActiveMQ. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + where T : ActiveMQServerResourceBase + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(ActiveMQServerResourceBase.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the ActiveMQ web console is exposed on instead of using randomly assigned port. + /// + /// The resource builder for ActiveMQ. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for ActiveMQ. + [AspireExport] + public static IResourceBuilder WithWebHostPort(this IResourceBuilder builder, int? port) + where T : ActiveMQServerResourceBase + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint("web", endpoint => endpoint.Port = port); + } + private static IResourceBuilder Build(this IDistributedApplicationBuilder builder, int? port, string scheme, int? webPort, T activeMq) where T : ActiveMQServerResourceBase { diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs index 0e483a433..4440933a3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs @@ -95,6 +95,22 @@ internal static IResourceBuilder AddDataAPIBuil int? httpPort = null) => builder.AddDataAPIBuilder(name, httpPort, configFilePaths ?? []); + /// + /// Configures the host port that the Data API Builder resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Data API Builder. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Data API Builder. + [AspireExport] + public static IResourceBuilder WithHostPort( + this IResourceBuilder builder, + int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(DataApiBuilderContainerResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + private static IResourceBuilder WithDataApiBuilderDefaults( this IResourceBuilder builder) => builder.WithOtlpExporter() diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 770b1b86a..e4509715f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -48,6 +48,34 @@ public static IResourceBuilder AddFlagd( .WithArgs("start"); } + /// + /// Configures the host port that the flagd resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for flagd. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for flagd. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(FlagdResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the flagd OFREP endpoint is exposed on instead of using randomly assigned port. + /// + /// The resource builder for flagd. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for flagd. + [AspireExport] + public static IResourceBuilder WithOfrepHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(FlagdResource.OfrepEndpointName, endpoint => endpoint.Port = port); + } + /// /// Configures logging level for flagd. If a flag or targeting rule isn't proceeding the way you'd expect this can be enabled to get more verbose logging. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Aws.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Aws.cs index 42edea2ea..d59cfa173 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Aws.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Aws.cs @@ -53,6 +53,22 @@ public static IResourceBuilder AddFlociAws( return flociBuilder; } + /// + /// Configures the host port that the Floci AWS resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Floci AWS. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Floci AWS. + [AspireExport("withHostPortAws", MethodName = "withHostPort")] + public static IResourceBuilder WithHostPort( + this IResourceBuilder builder, + int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(FlociAwsContainerResource.AwsEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a reference to a Floci AWS emulator resource, injecting the standard /// ConnectionStrings__{name} entry plus the AWS SDK environment variables @@ -142,4 +158,3 @@ public static IResourceBuilder WithDataBindMount( => WithDataBindMountCore(builder, source, isReadOnly); } - diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs index ab11328d3..5a406f3d6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs @@ -46,6 +46,22 @@ public static IResourceBuilder AddFlociAzure( return flociBuilder; } + /// + /// Configures the host port that the Floci Azure resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Floci Azure. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Floci Azure. + [AspireExport("withHostPortAzure", MethodName = "withHostPort")] + public static IResourceBuilder WithHostPort( + this IResourceBuilder builder, + int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(builder.Resource.EndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a reference to a Floci Azure emulator resource, injecting the standard /// ConnectionStrings__{name} entry plus AZURE_STORAGE_CONNECTION_STRING — a diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Gcp.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Gcp.cs index 808a9818b..ebfced40c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Gcp.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Gcp.cs @@ -45,6 +45,22 @@ public static IResourceBuilder AddFlociGcp( return flociBuilder; } + /// + /// Configures the host port that the Floci GCP resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Floci GCP. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Floci GCP. + [AspireExport("withHostPortGcp", MethodName = "withHostPort")] + public static IResourceBuilder WithHostPort( + this IResourceBuilder builder, + int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(builder.Resource.EndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a reference to a Floci GCP emulator resource, injecting the standard /// ConnectionStrings__{name} entry plus the *_EMULATOR_HOST environment variables @@ -139,4 +155,3 @@ public static IResourceBuilder WithFlociUI( return builder; } } - diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagBuilderExtensions.cs index 0fbaa7162..9f5488ef6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagBuilderExtensions.cs @@ -68,6 +68,20 @@ public static IResourceBuilder AddGoFeatureFlag( .WithIconName("Flag"); } + /// + /// Configures the host port that the GO Feature Flag resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for GO Feature Flag. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for GO Feature Flag. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(GoFeatureFlagResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a named volume for the data folder to a GO Feature flag container resource. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs index 813c5d9c2..e3388e3b5 100644 --- a/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.K3s/K3sBuilderExtensions.cs @@ -270,6 +270,20 @@ public static IResourceBuilder AddK3sCluster( return resourceBuilder; } + /// + /// Configures the host port that the k3s cluster resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for the k3s cluster. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for the k3s cluster. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(K3sClusterResource.ApiServerEndpointName, endpoint => endpoint.Port = port); + } + /// /// Sets the k3s image version used by the cluster server and all its agent nodes. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs index 2cf9017b8..668a0a5c6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs @@ -16,6 +16,20 @@ namespace Aspire.Hosting; /// public static class KurrentDBBuilderExtensions { + /// + /// Configures the host port that the KurrentDB resource is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for KurrentDB. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for KurrentDB. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(KurrentDBResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + private const string DataTargetFolder = "/var/lib/kurrentdb"; /// diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQHostingExtension.cs index 9a88eb408..c8d905133 100644 --- a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQHostingExtension.cs @@ -14,6 +14,34 @@ namespace Aspire.Hosting; /// public static class LavinMQHostingExtension { + /// + /// Configures the host port that the LavinMQ AMQP endpoint is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for LavinMQ. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for LavinMQ. + [AspireExport] + public static IResourceBuilder WithAmqpHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(LavinMQContainerResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the LavinMQ management endpoint is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for LavinMQ. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for LavinMQ. + [AspireExport] + public static IResourceBuilder WithManagementHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(LavinMQContainerResource.ManagementEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a LavinMQ container resource to the distributed application builder. /// Configures the resource with specified parameters and sets up health checks for the resource. diff --git a/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs index 67d7037d1..b56c97608 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Listmonk/ListmonkBuilderExtensions.cs @@ -7,6 +7,20 @@ namespace Aspire.Hosting; /// public static class ListmonkBuilderExtensions { + /// + /// Configures the host port that the listmonk resource is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for listmonk. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for listmonk. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(ListmonkResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + private const int ListmonkPort = 9000; private const string UploadsPath = "/listmonk/uploads"; private const string AppAddressEnvVarName = "LISTMONK_app__address"; diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index 0c1085d6f..db26fb2a8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.DependencyInjection; #pragma warning disable ASPIREATS001 // AspireExport is experimental @@ -11,6 +11,34 @@ namespace Aspire.Hosting; /// public static class LogtoBuilderExtensions { + /// + /// Configures the host port that the Logto resource is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for Logto. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for Logto. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(LogtoResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the Logto admin endpoint is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for Logto. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for Logto. + [AspireExport] + public static IResourceBuilder WithAdminHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(LogtoResource.AdminEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a Logto resource to the Aspire distributed application by configuring it /// with the specified name, associated PostgreSQL server resource, and database name. diff --git a/src/CommunityToolkit.Aspire.Hosting.MailPit/MailPitHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.MailPit/MailPitHostingExtension.cs index ca378774a..30b2d267f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.MailPit/MailPitHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.MailPit/MailPitHostingExtension.cs @@ -10,6 +10,34 @@ namespace Aspire.Hosting; /// public static class MailPitHostingExtension { + /// + /// Configures the host port that the MailPit HTTP endpoint is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for MailPit. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for MailPit. + [AspireExport] + public static IResourceBuilder WithHttpHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(MailPitContainerResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the MailPit SMTP endpoint is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for MailPit. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for MailPit. + [AspireExport] + public static IResourceBuilder WithSmtpHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(MailPitContainerResource.SmtpEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a MailPit container resource to the . /// diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs index 0fe116e74..5173ad52c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs @@ -11,6 +11,34 @@ namespace Aspire.Hosting; /// public static class McpInspectorResourceBuilderExtensions { + /// + /// Configures the host port that the MCP Inspector client endpoint is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for MCP Inspector. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for MCP Inspector. + [AspireExport] + public static IResourceBuilder WithClientHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(McpInspectorResource.ClientEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the MCP Inspector server endpoint is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for MCP Inspector. + /// The port to bind on the host. If is used a random port will be assigned. + /// The resource builder for MCP Inspector. + [AspireExport] + public static IResourceBuilder WithServerHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(McpInspectorResource.ServerProxyEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a MCP Inspector container resource to the . /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Meilisearch/MeilisearchBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Meilisearch/MeilisearchBuilderExtensions.cs index e995a6222..908803ad4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Meilisearch/MeilisearchBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Meilisearch/MeilisearchBuilderExtensions.cs @@ -18,6 +18,20 @@ public static class MeilisearchBuilderExtensions { private const int MeilisearchPort = 7700; + /// + /// Configures the host port that the Meilisearch resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Meilisearch. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Meilisearch. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(MeilisearchResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds an Meilisearch container resource to the application model. /// The default image is and the tag is . diff --git a/src/CommunityToolkit.Aspire.Hosting.Mosquitto/MosquittoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Mosquitto/MosquittoBuilderExtensions.cs index 30bc56278..04630b93f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Mosquitto/MosquittoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Mosquitto/MosquittoBuilderExtensions.cs @@ -25,6 +25,20 @@ persistence true persistence_location /mosquitto/data/ """; + /// + /// Configures the host port that the Mosquitto resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Mosquitto. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Mosquitto. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(MosquittoServerResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a Mosquitto container resource to the application. Mosquitto is an open source MQTT /// message broker, so the resource can be referenced by any MQTT client integration. diff --git a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs index ab3a8e807..ce7175005 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ngrok/NgrokExtensions.cs @@ -11,6 +11,20 @@ namespace Aspire.Hosting; /// public static class NgrokExtensions { + /// + /// Configures the host port that the ngrok resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for ngrok. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for ngrok. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint("http", endpoint => endpoint.Port = port); + } + /// /// Configures a container resource for grok which is pre-configured to connect to the resource that this method is used on. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.OpenWebUI.cs b/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.OpenWebUI.cs index 42638d054..55171d8e6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.OpenWebUI.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.OpenWebUI.cs @@ -86,7 +86,7 @@ public static IResourceBuilder WithDataVolume(this IResourceB /// The resource builder for Open WebUI. /// The port to bind on the host. If is used random port will be assigned. /// The resource builder for Open WebUI. - [AspireExport] + [AspireExport("withOpenWebUIHostPort", MethodName = "withHostPort")] public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.cs index 54c813288..903620283 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaResourceBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; using CommunityToolkit.Aspire.Hosting.Ollama; using Microsoft.Extensions.DependencyInjection; @@ -15,6 +15,34 @@ namespace Aspire.Hosting; /// public static partial class OllamaResourceBuilderExtensions { + /// + /// Configures the host port that the Ollama resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Ollama. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Ollama. + [AspireExport("withOllamaHostPort", MethodName = "withHostPort")] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(OllamaResource.OllamaEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the Ollama local resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Ollama local. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Ollama local. + [AspireExport("withOllamaLocalHostPort", MethodName = "withHostPort")] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(OllamaExecutableResource.OllamaEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds an Ollama container resource to the application model. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs index fb49a5839..b96481aaf 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs @@ -10,6 +10,34 @@ namespace Aspire.Hosting; /// public static class PapercutSmtpHostingExtension { + /// + /// Configures the host port that the Papercut SMTP HTTP endpoint is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Papercut SMTP. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Papercut SMTP. + [AspireExport] + public static IResourceBuilder WithHttpHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(PapercutSmtpContainerResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the Papercut SMTP SMTP endpoint is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Papercut SMTP. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Papercut SMTP. + [AspireExport] + public static IResourceBuilder WithSmtpHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(PapercutSmtpContainerResource.SmtpEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds Papercut SMTP to the application model. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 92fc883f9..d7692bf4c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -11,6 +11,48 @@ public static class PostaHostingExtensions { private const string DataContainerPath = "/data"; + /// + /// Configures the host port that the Posta resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Posta. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Posta. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(PostaResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the Posta SMTP endpoint is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Posta. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Posta. + [AspireExport] + public static IResourceBuilder WithSmtpHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(PostaResource.SmtpEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the Posta SMTP relay endpoint is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Posta. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Posta. + [AspireExport] + public static IResourceBuilder WithSmtpRelayHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(PostaResource.SmtpRelayEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a Posta container resource to the and configures PostgreSQL and Redis references. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs index c6306ec00..82856c304 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.RavenDB; using Microsoft.Extensions.DependencyInjection; using Raven.Client.Documents; @@ -15,6 +15,34 @@ namespace Aspire.Hosting; /// public static class RavenDBBuilderExtensions { + /// + /// Configures the host port that the RavenDB resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for RavenDB. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for RavenDB. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(builder.Resource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the RavenDB TCP endpoint is exposed on instead of using randomly assigned port. + /// + /// The resource builder for RavenDB. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for RavenDB. + [AspireExport] + public static IResourceBuilder WithTcpHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(builder.Resource.TcpEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a RavenDB server resource to the application model. A container is used for local development. /// This overload simplifies the configuration by creating an unsecured RavenDB server resource with default settings. diff --git a/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs index 352cf74c1..b3cbd2475 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs @@ -16,6 +16,34 @@ public static class RustFsBuilderExtensions private const string AccessKeyEnvVarName = "RUSTFS_ACCESS_KEY"; private const string SecretKeyEnvVarName = "RUSTFS_SECRET_KEY"; + /// + /// Configures the host port that the RustFs resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for RustFs. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for RustFs. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(RustFsResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + + /// + /// Configures the host port that the RustFs console endpoint is exposed on instead of using randomly assigned port. + /// + /// The resource builder for RustFs. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for RustFs. + [AspireExport] + public static IResourceBuilder WithConsoleHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(RustFsResource.ConsoleEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a RustFs container to the application model. The default image is "rustfs/rustfs". /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Sftp/SftpHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Sftp/SftpHostingExtensions.cs index 9881177bd..346b714f6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Sftp/SftpHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Sftp/SftpHostingExtensions.cs @@ -10,6 +10,20 @@ namespace Aspire.Hosting; /// public static class SftpHostingExtensions { + /// + /// Configures the host port that the SFTP resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for SFTP. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for SFTP. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(SftpContainerResource.SftpEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds atmoz SFTP to the application model. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Solr/SolrBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrBuilderExtensions.cs index d6e543a82..81ded5700 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Solr/SolrBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrBuilderExtensions.cs @@ -11,6 +11,20 @@ namespace Aspire.Hosting; /// public static class SolrBuilderExtensions { + /// + /// Configures the host port that the Solr resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Solr. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Solr. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(SolrResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds an Apache Solr container resource to the distributed application. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs index 070dd010f..1330d4abf 100644 --- a/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.StableDiffusionCpp/StableDiffusionCppResourceBuilderExtensions.cs @@ -9,6 +9,20 @@ namespace Aspire.Hosting; /// public static class StableDiffusionCppResourceBuilderExtensions { + /// + /// Configures the host port that the stable-diffusion.cpp resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for stable-diffusion.cpp. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for stable-diffusion.cpp. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(StableDiffusionCppResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a stable-diffusion.cpp container resource using an official image. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs index b9568fe82..55fa4a98a 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; @@ -23,6 +23,20 @@ public static class SurrealDbBuilderExtensions private const string PasswordEnvVarName = "SURREAL_PASS"; private const string ImportFileEnvVarName = "SURREAL_IMPORT_FILE"; + /// + /// Configures the host port that the SurrealDB resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for SurrealDB. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for SurrealDB. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(SurrealDbServerResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a SurrealDB resource to the application model. A container is used for local development. /// The default image is and the tag is . diff --git a/src/CommunityToolkit.Aspire.Hosting.Umami/UmamiBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Umami/UmamiBuilderExtensions.cs index 70e299a59..57cc6cd9d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Umami/UmamiBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Umami/UmamiBuilderExtensions.cs @@ -17,6 +17,20 @@ public static class UmamiBuilderExtensions private const string SecretEnvVarName = "APP_SECRET"; private const string DatabaseStorageEnvVarName = "DATABASE_URL"; + /// + /// Configures the host port that the Umami resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Umami. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Umami. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(UmamiResource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a Umami resource to the application model. A container is used for local development. /// The default image is and the tag is . diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index 1b7e7c977..63c96eb4b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -11,6 +11,20 @@ namespace Aspire.Hosting; /// public static class ZitadelHostingExtensions { + /// + /// Configures the host port that the Zitadel resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Zitadel. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Zitadel. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(ZitadelResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a Zitadel container resource to the . /// diff --git a/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs index 609497cc3..a2ea4f500 100644 --- a/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs @@ -16,6 +16,20 @@ public static class K6BuilderExtensions { private const int K6Port = 6565; + /// + /// Configures the host port that the Grafana k6 resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Grafana k6. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Grafana k6. + [AspireExport] + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(K6Resource.PrimaryEndpointName, endpoint => endpoint.Port = port); + } + /// /// Adds a Grafana k6 container resource to the application model. /// The default image is and the tag is . From 1787e1dcaf7decdd132326fc7f06b5a8a408246b Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Thu, 24 Sep 2026 00:24:53 +0100 Subject: [PATCH 4/4] Fix endpoint allocation test regressions Use endpoint properties for RustFS bind addresses and fake endpoint allocations in unit tests so endpoint resolution remains deterministic without reverting the endpoint-refactor behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../RustFsBuilderExtensions.cs | 9 +++++++-- .../AddDbGateTests.cs | 2 +- .../AddRustFsTests.cs | 20 +++++++++++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs index b3cbd2475..ffdbaed8e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RustFs/RustFsBuilderExtensions.cs @@ -79,10 +79,8 @@ public static IResourceBuilder AddRustFs( .WithImage(RustFsContainerImageTags.Image, RustFsContainerImageTags.Tag) .WithImageRegistry(RustFsContainerImageTags.Registry) .WithHttpEndpoint(name: RustFsResource.PrimaryEndpointName, port: port, - env: "RUSTFS_ADDRESS", targetPort: RustFsResource.PrimaryTargetPort) .WithHttpEndpoint(name: RustFsResource.ConsoleEndpointName, port: consolePort, - env: "RUSTFS_CONSOLE_ADDRESS", targetPort: RustFsResource.ConsoleTargetPort) .WithUrlForEndpoint(RustFsResource.PrimaryEndpointName, annot => { @@ -98,6 +96,13 @@ public static IResourceBuilder AddRustFs( .WithHttpHealthCheck("/health", 200, RustFsResource.PrimaryEndpointName) .WithIconName("HardDrive"); + var primaryEndpoint = resourceBuilder.GetEndpoint(RustFsResource.PrimaryEndpointName); + var consoleEndpoint = resourceBuilder.GetEndpoint(RustFsResource.ConsoleEndpointName); + + resourceBuilder + .WithEnvironment("RUSTFS_ADDRESS", ReferenceExpression.Create($"{primaryEndpoint.Property(EndpointProperty.IPV4Host)}:{primaryEndpoint.Property(EndpointProperty.TargetPort)}")) + .WithEnvironment("RUSTFS_CONSOLE_ADDRESS", ReferenceExpression.Create($"{consoleEndpoint.Property(EndpointProperty.IPV4Host)}:{consoleEndpoint.Property(EndpointProperty.TargetPort)}")); + return resourceBuilder; } diff --git a/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs b/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs index f1c64e62f..859bd7f1d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs @@ -527,7 +527,7 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes() static void UpdateResourceEndpoint(IResourceWithEndpoints resource) { var endpoint = resource.GetEndpoint("tcp").EndpointAnnotation; - var ae = new AllocatedEndpoint(endpoint, "storage.dev.internal", 10000, EndpointBindingMode.SingleAddress, null, KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + var ae = new AllocatedEndpoint(endpoint, resource.Name, endpoint.TargetPort ?? 10000, EndpointBindingMode.SingleAddress, null, KnownNetworkIdentifiers.DefaultAspireContainerNetwork); endpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, ae); } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs index 327cc1ac0..202efda02 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/AddRustFsTests.cs @@ -72,14 +72,30 @@ public async Task RustFsResourceHasCorrectEndpoints() var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http"); Assert.Equal(9000, primaryEndpoint.TargetPort); + primaryEndpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint( + KnownNetworkIdentifiers.DefaultAspireContainerNetwork, + new AllocatedEndpoint( + primaryEndpoint, + "127.0.0.1", + primaryEndpoint.TargetPort!.Value, + EndpointBindingMode.SingleAddress, + networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); var consoleEndpoint = Assert.Single(endpoints, e => e.Name == "console"); Assert.Equal(9001, consoleEndpoint.TargetPort); + consoleEndpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint( + KnownNetworkIdentifiers.DefaultAspireContainerNetwork, + new AllocatedEndpoint( + consoleEndpoint, + "127.0.0.1", + consoleEndpoint.TargetPort!.Value, + EndpointBindingMode.SingleAddress, + networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); var config = await resource.GetEnvironmentVariablesAsync(); - Assert.Equal("9000", config["RUSTFS_ADDRESS"]); - Assert.Equal("9001", config["RUSTFS_CONSOLE_ADDRESS"]); + Assert.Equal("127.0.0.1:9000", config["RUSTFS_ADDRESS"]); + Assert.Equal("127.0.0.1:9001", config["RUSTFS_CONSOLE_ADDRESS"]); } [Fact]