diff --git a/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs b/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs index b0758008e8..fd9b8b77d9 100644 --- a/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs +++ b/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs @@ -175,6 +175,9 @@ public sealed class ConsulDiscoveryOptions /// /// Gets or sets a value indicating whether to register with the port number ASP.NET Core is listening on. Default value: true. + /// + /// This property is ignored when or is explicitly configured, or when is + /// true. /// public bool UseAspNetCoreUrls { get; set; } = true; } diff --git a/src/Discovery/src/Consul/ConfigurationSchema.json b/src/Discovery/src/Consul/ConfigurationSchema.json index 3111a531f0..a00e7f69bf 100644 --- a/src/Discovery/src/Consul/ConfigurationSchema.json +++ b/src/Discovery/src/Consul/ConfigurationSchema.json @@ -188,7 +188,7 @@ }, "UseAspNetCoreUrls": { "type": "boolean", - "description": "Gets or sets a value indicating whether to register with the port number ASP.NET Core is listening on. Default value: true." + "description": "Gets or sets a value indicating whether to register with the port number ASP.NET Core is listening on. Default value: true.\n\nThis property is ignored when 'Steeltoe.Discovery.Consul.Configuration.ConsulDiscoveryOptions.Port' or 'Steeltoe.Discovery.Consul.Configuration.ConsulDiscoveryOptions.Scheme' is explicitly configured, or when 'Steeltoe.Discovery.Consul.Configuration.ConsulDiscoveryOptions.UseNetworkInterfaces' is true." }, "UseNetworkInterfaces": { "type": "boolean", diff --git a/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs b/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs index 6ad1371335..91dcd274cc 100644 --- a/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs +++ b/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs @@ -62,7 +62,7 @@ public void PostConfigure(string? name, ConsulDiscoveryOptions options) options.HostName = options.IPAddress; } - if (options.Port == 0) + if (options is { UseAspNetCoreUrls: true, Port: 0, Scheme: null, UseNetworkInterfaces: false }) { ICollection addresses = _configuration.GetListenAddresses(); SetSchemeWithPortFromListenAddresses(options, addresses); @@ -79,38 +79,33 @@ private string GetServiceName(ConsulDiscoveryOptions options) private void SetSchemeWithPortFromListenAddresses(ConsulDiscoveryOptions options, IEnumerable listenOnAddresses) { - // Try to pull some values out of server configuration to override defaults, but only if not using NetUtils. - // If NetUtils are configured, the user probably wants to define their own behavior. - if (options is { UseAspNetCoreUrls: true, Port: 0, Scheme: null }) - { - int? listenHttpPort = null; - int? listenHttpsPort = null; + int? listenHttpPort = null; + int? listenHttpsPort = null; - foreach (string address in listenOnAddresses) - { - BindingAddress bindingAddress = BindingAddress.Parse(address); - - if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) - { - listenHttpPort = bindingAddress.Port; - } - else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) - { - listenHttpsPort = bindingAddress.Port; - } - } + foreach (string address in listenOnAddresses) + { + BindingAddress bindingAddress = BindingAddress.Parse(address); - if (listenHttpsPort != null) + if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) { - options.Port = listenHttpsPort.Value; - options.Scheme = "https"; + listenHttpPort = bindingAddress.Port; } - else if (listenHttpPort != null) + else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) { - options.Port = listenHttpPort.Value; - options.Scheme = "http"; + listenHttpsPort = bindingAddress.Port; } } + + if (listenHttpsPort != null) + { + options.Port = listenHttpsPort.Value; + options.Scheme = "https"; + } + else if (listenHttpPort != null) + { + options.Port = listenHttpPort.Value; + options.Scheme = "http"; + } } private string GetInstanceId(ConsulDiscoveryOptions options) diff --git a/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs b/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs index 08e0f3f51e..24b391e3da 100644 --- a/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs +++ b/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs @@ -20,7 +20,11 @@ public sealed partial class EurekaInstanceOptions internal const string DefaultStatusPageUrlPath = "/info"; internal const string DefaultHealthCheckUrlPath = "/health"; - private bool UseAspNetCoreUrls => !Platform.IsCloudFoundry || IsContainerToContainerMethod() || IsForceHostNameMethod(); + private bool IsAnyPortConfigured => + this is { IsNonSecurePortEnabled: true, NonSecurePort: not null } or { IsSecurePortEnabled: true, SecurePort: not null }; + + private bool IsSuitableRegistrationMethod => !Platform.IsCloudFoundry || IsContainerToContainerMethod() || IsForceHostNameMethod(); + internal bool ShouldSetPortsFromListenAddresses => UseAspNetCoreUrls && IsSuitableRegistrationMethod && !IsAnyPortConfigured; internal TimeSpan LeaseRenewalInterval => TimeSpan.FromSeconds(LeaseRenewalIntervalInSeconds); internal TimeSpan LeaseExpirationDuration => TimeSpan.FromSeconds(LeaseExpirationDurationInSeconds); @@ -205,6 +209,13 @@ public sealed partial class EurekaInstanceOptions Name = DataCenterName.MyOwn }; + /// + /// Gets or sets a value indicating whether to register with the port number(s) ASP.NET Core is listening on. Default value: true. + /// + /// This property is ignored when or is explicitly configured. + /// + public bool UseAspNetCoreUrls { get; set; } = true; + /// /// Gets or sets a value indicating whether is used to determine and /// . Default value: false. @@ -231,72 +242,69 @@ internal bool IsForceHostNameMethod() internal void SetPortsFromListenAddresses(IEnumerable listenOnAddresses, string source, ILogger logger) { - if (UseAspNetCoreUrls) + int? listenHttpPort = null; + int? listenHttpsPort = null; + + foreach (string address in listenOnAddresses) { - int? listenHttpPort = null; - int? listenHttpsPort = null; + BindingAddress bindingAddress = BindingAddress.Parse(address); - foreach (string address in listenOnAddresses) + if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) { - BindingAddress bindingAddress = BindingAddress.Parse(address); - - if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) - { - listenHttpPort = bindingAddress.Port; - } - else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) - { - listenHttpsPort = bindingAddress.Port; - } + listenHttpPort = bindingAddress.Port; + } + else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) + { + listenHttpsPort = bindingAddress.Port; } + } - int? nonSecurePort = IsNonSecurePortEnabled ? NonSecurePort : null; - int? securePort = IsSecurePortEnabled ? SecurePort : null; + int? nonSecurePort = IsNonSecurePortEnabled ? NonSecurePort : null; + int? securePort = IsSecurePortEnabled ? SecurePort : null; - if (nonSecurePort != listenHttpPort) + if (nonSecurePort != listenHttpPort) + { + if (listenHttpPort != null) { - if (listenHttpPort != null) + if (nonSecurePort == null) { - if (nonSecurePort == null) - { - LogActivatingNonSecurePort(logger, listenHttpPort, source); - } - else - { - LogChangingNonSecurePort(logger, listenHttpPort, source); - } - - NonSecurePort = listenHttpPort.Value; - IsNonSecurePortEnabled = true; + LogActivatingNonSecurePort(logger, listenHttpPort, source); } - else if (nonSecurePort != null) + else { - LogDeactivatingNonSecurePort(logger, source); - IsNonSecurePortEnabled = false; + LogChangingNonSecurePort(logger, listenHttpPort, source); } + + NonSecurePort = listenHttpPort.Value; + IsNonSecurePortEnabled = true; + } + else if (nonSecurePort != null) + { + LogDeactivatingNonSecurePort(logger, source); + IsNonSecurePortEnabled = false; } + } - if (securePort != listenHttpsPort) + if (securePort != listenHttpsPort) + { + if (listenHttpsPort != null) { - if (listenHttpsPort != null) + if (securePort == null) { - if (securePort == null) - { - LogActivatingSecurePort(logger, listenHttpsPort, source); - } - else - { - LogChangingSecurePort(logger, listenHttpsPort, source); - } - - SecurePort = listenHttpsPort.Value; - IsSecurePortEnabled = true; + LogActivatingSecurePort(logger, listenHttpsPort, source); } - else if (securePort != null) + else { - LogDeactivatingSecurePort(logger, source); - IsSecurePortEnabled = false; + LogChangingSecurePort(logger, listenHttpsPort, source); } + + SecurePort = listenHttpsPort.Value; + IsSecurePortEnabled = true; + } + else if (securePort != null) + { + LogDeactivatingSecurePort(logger, source); + IsSecurePortEnabled = false; } } } diff --git a/src/Discovery/src/Eureka/ConfigurationSchema.json b/src/Discovery/src/Eureka/ConfigurationSchema.json index 2e1c600204..95634e3416 100644 --- a/src/Discovery/src/Eureka/ConfigurationSchema.json +++ b/src/Discovery/src/Eureka/ConfigurationSchema.json @@ -258,6 +258,10 @@ "type": "string", "description": "Gets or sets the relative path to the status page for the instance. The status page URL is then constructed out of the 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.HostName' and the type of communication - secure or non-secure, as specified in 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.SecurePort' and 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.NonSecurePort'. It is normally used for informational purposes for other services to find out about the status of the instance. Users can provide a simple HTML page indicating what the current status of the instance is. Default value: /info." }, + "UseAspNetCoreUrls": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to register with the port number(s) ASP.NET Core is listening on. Default value: true.\n\nThis property is ignored when 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.NonSecurePort' or 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.SecurePort' is explicitly configured." + }, "UseNetworkInterfaces": { "type": "boolean", "description": "Gets or sets a value indicating whether 'System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces' is used to determine 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.IPAddress' and 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.HostName'. Default value: false." diff --git a/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs b/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs index e58cefb6bf..da517432b7 100644 --- a/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs +++ b/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs @@ -136,7 +136,7 @@ public void PostConfigure(string? name, EurekaInstanceOptions options) { ArgumentNullException.ThrowIfNull(options); - if (_listenState.ListenOnAddresses != null) + if (_listenState.ListenOnAddresses != null && options.ShouldSetPortsFromListenAddresses) { options.SetPortsFromListenAddresses(_listenState.ListenOnAddresses, "address features", _optionsLogger); } diff --git a/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs b/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs index 3348501e08..38de08bce5 100644 --- a/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs +++ b/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs @@ -81,11 +81,11 @@ private static void ConfigureEurekaClientOptions(IServiceCollection services) private static void ConfigureEurekaInstanceOptions(IServiceCollection services) { + DynamicPortAssignmentHostedService.Wire(services); + services.AddOptions().BindConfiguration(EurekaInstanceOptions.ConfigurationPrefix); services.AddOptions().BindConfiguration(InetOptions.ConfigurationPrefix); services.AddSingleton, PostConfigureEurekaInstanceOptions>(); - - DynamicPortAssignmentHostedService.Wire(services); } private static void AddEurekaServices(IServiceCollection services) diff --git a/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs b/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs index 3ef1f30b96..b33208d520 100644 --- a/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs +++ b/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs @@ -140,7 +140,7 @@ private void SetPorts(EurekaInstanceOptions options) options.IsSecurePortEnabled = true; } - if (options.NonSecurePort == null && options.SecurePort == null) + if (options.ShouldSetPortsFromListenAddresses) { var optionsLogger = _serviceProvider.GetRequiredService>(); ICollection addresses = _configuration.GetListenAddresses(); diff --git a/src/Discovery/src/Eureka/PublicAPI.Unshipped.txt b/src/Discovery/src/Eureka/PublicAPI.Unshipped.txt index 7dc5c58110..e09ccfe57f 100644 --- a/src/Discovery/src/Eureka/PublicAPI.Unshipped.txt +++ b/src/Discovery/src/Eureka/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseAspNetCoreUrls.get -> bool +Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseAspNetCoreUrls.set -> void diff --git a/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs b/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs index 45f9666e17..b3adc4687e 100644 --- a/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs +++ b/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs @@ -4,9 +4,11 @@ using System.Runtime.InteropServices; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Steeltoe.Common.TestResources; +using Steeltoe.Discovery.Eureka.Configuration; namespace Steeltoe.Discovery.Eureka.Test; @@ -38,4 +40,147 @@ public async Task Applies_dynamically_assigned_ports_after_startup() infoManager.Instance.IsSecurePortEnabled.Should().BeTrue(); infoManager.Instance.SecurePort.Should().BePositive(); } + + [Fact] + public async Task Applies_dynamically_assigned_ports_when_kestrel_overrides_urls_config() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["urls"] = "http://*:5000" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.Configuration.AddInMemoryCollection(appSettings); + + builder.WebHost.ConfigureKestrel(options => + { + options.ListenAnyIP(0); + }); + + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsNonSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.NonSecurePort.Should().NotBe(5000); + infoManager.Instance.NonSecurePort.Should().BePositive(); + infoManager.Instance.IsSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.SecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_override_explicitly_configured_secure_port() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["Eureka:Instance:SecurePort"] = "443", + ["Eureka:Instance:SecurePortEnabled"] = "true" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.SecurePort.Should().Be(443); + infoManager.Instance.IsNonSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.NonSecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_override_explicitly_configured_non_secure_port() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["Eureka:Instance:Port"] = "80", + ["Eureka:Instance:NonSecurePortEnabled"] = "true" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsNonSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.NonSecurePort.Should().Be(80); + infoManager.Instance.IsSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.SecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_override_ports_configured_via_code() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + builder.Services.Configure(options => + { + options.SecurePort = 8443; + options.IsSecurePortEnabled = true; + }); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.SecurePort.Should().Be(8443); + infoManager.Instance.IsNonSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.NonSecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_apply_dynamic_ports_when_UseAspNetCoreUrls_is_false() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["Eureka:Instance:UseAspNetCoreUrls"] = "false" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsNonSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.NonSecurePort.Should().Be(0); + infoManager.Instance.IsSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.SecurePort.Should().Be(0); + } } diff --git a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs index d5a1a9ee6f..180fe7fbf9 100644 --- a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs @@ -43,6 +43,7 @@ public void Constructor_Initializes_Defaults() instanceOptions.SecureHealthCheckUrl.Should().BeNull(); instanceOptions.AutoScalingGroupName.Should().BeNull(); instanceOptions.DataCenterInfo.Name.Should().Be(DataCenterName.MyOwn); + instanceOptions.UseAspNetCoreUrls.Should().BeTrue(); instanceOptions.UseNetworkInterfaces.Should().BeFalse(); }