From cc040acc6c9db6245104d68236da93c48562cc41 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Tue, 17 Dec 2019 10:54:46 +0100 Subject: [PATCH] Add possibility of customizing how discovery metadata are created (#10) --- src/ServiceModel.ECS/ECSMetadataExtensions.cs | 62 ++++++++ src/ServiceModel.ECS/ServiceModel.ECS.csproj | 5 + .../Discovery/AnnouncementService.cs | 6 +- .../CustomAutoDataAttribute.cs | 18 +++ .../ECSMetadataExtensionsTests.cs | 148 +++++++++++++++--- .../Tests.ServiceModel.ECS.csproj | 7 + 6 files changed, 224 insertions(+), 22 deletions(-) diff --git a/src/ServiceModel.ECS/ECSMetadataExtensions.cs b/src/ServiceModel.ECS/ECSMetadataExtensions.cs index 9e08d51..c1ea8c3 100644 --- a/src/ServiceModel.ECS/ECSMetadataExtensions.cs +++ b/src/ServiceModel.ECS/ECSMetadataExtensions.cs @@ -1,12 +1,19 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.ServiceModel.Description; +using System.ServiceModel.Discovery; using EMG.Extensions.Configuration.Model; using EMG.Utilities.ServiceModel.Configuration; +using EMG.Utilities.ServiceModel.Discovery; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace EMG.Utilities { public static class ECSMetadataExtensions { + [Obsolete("Use AddECSContainerSupport instead.")] public static HttpEndpointAddress UseECS(this HttpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions = null) { if (Environment.GetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey) == null) @@ -34,6 +41,7 @@ public static HttpEndpointAddress UseECS(this HttpEndpointAddress endpointAddres return EndpointAddress.ForHttp(containerMetadata.HostPrivateIPv4Address, endpointAddress.Path, portMapping.HostPort, endpointAddress.IsSecure); } + [Obsolete("Use AddECSContainerSupport instead.")] public static NetTcpEndpointAddress UseECS(this NetTcpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions = null) { if (Environment.GetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey) == null) @@ -61,5 +69,59 @@ public static NetTcpEndpointAddress UseECS(this NetTcpEndpointAddress endpointAd return EndpointAddress.ForNetTcp(portMapping.HostPort, containerMetadata.HostPrivateIPv4Address, endpointAddress.Path); } + + public static IServiceCollection AddECSContainerSupport(this IServiceCollection services, IConfiguration configuration) + { + if (Environment.GetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey) != null) + { + services.Configure(o => o.EndpointDiscoveryMetadata = GetEndpointMetadataFromECS); + } + + return services; + + EndpointDiscoveryMetadata GetEndpointMetadataFromECS(ServiceEndpoint endpoint) + { + if (endpoint.Address is null) + { + throw new ArgumentNullException(nameof(endpoint),"endpoint address should not be null"); + } + + var containerMetadata = configuration.Get(); + + var endpointMetadata = EndpointDiscoveryMetadata.FromServiceEndpoint(endpoint); + + if (endpointMetadata is null) + { + return null; + } + + if (containerMetadata?.HostPrivateIPv4Address is null) + { + return endpointMetadata; + } + + var uriBuilder = new UriBuilder(endpointMetadata.Address.Uri) + { + Host = containerMetadata.HostPrivateIPv4Address, + Port = GetMappedPortOrDefault(containerMetadata.PortMappings, endpointMetadata.Address.Uri.Port) + }; + + endpointMetadata.Address = new System.ServiceModel.EndpointAddress(uriBuilder.Uri.ToString()); + + return endpointMetadata; + } + + int GetMappedPortOrDefault(IEnumerable mappings, int port) + { + var hostPortByContainerPort = mappings.ToDictionary(k => k.ContainerPort, v => v.HostPort); + + if (hostPortByContainerPort.TryGetValue(port, out var mappedPort)) + { + return mappedPort; + } + + return port; + } + } } } \ No newline at end of file diff --git a/src/ServiceModel.ECS/ServiceModel.ECS.csproj b/src/ServiceModel.ECS/ServiceModel.ECS.csproj index ca2f5cd..6c2b5ca 100644 --- a/src/ServiceModel.ECS/ServiceModel.ECS.csproj +++ b/src/ServiceModel.ECS/ServiceModel.ECS.csproj @@ -19,4 +19,9 @@ + + + + + diff --git a/src/ServiceModel/ServiceModel/Discovery/AnnouncementService.cs b/src/ServiceModel/ServiceModel/Discovery/AnnouncementService.cs index 4e31c7c..a6384e0 100644 --- a/src/ServiceModel/ServiceModel/Discovery/AnnouncementService.cs +++ b/src/ServiceModel/ServiceModel/Discovery/AnnouncementService.cs @@ -22,6 +22,8 @@ public class AnnouncementServiceOptions public TimeSpan Interval { get; set; } public Binding Binding { get; set; } + + public Func EndpointDiscoveryMetadata { get; set; } = System.ServiceModel.Discovery.EndpointDiscoveryMetadata.FromServiceEndpoint; } public class AnnouncementService : IAnnouncementService @@ -54,7 +56,7 @@ from endpoint in endpointsToAnnounce { using (var client = CreateClient()) { - var metadata = EndpointDiscoveryMetadata.FromServiceEndpoint(endpoint); + var metadata = _options.EndpointDiscoveryMetadata(endpoint); if (metadata != null) { @@ -73,7 +75,7 @@ private void UnannounceService(IReadOnlyList endpoints) { foreach (var endpoint in endpoints) { - var metadata = EndpointDiscoveryMetadata.FromServiceEndpoint(endpoint); + var metadata = _options.EndpointDiscoveryMetadata(endpoint); if (metadata != null) { diff --git a/tests/Tests.ServiceModel.ECS/CustomAutoDataAttribute.cs b/tests/Tests.ServiceModel.ECS/CustomAutoDataAttribute.cs index 284069c..af6c338 100644 --- a/tests/Tests.ServiceModel.ECS/CustomAutoDataAttribute.cs +++ b/tests/Tests.ServiceModel.ECS/CustomAutoDataAttribute.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.ServiceModel.Description; using AutoFixture; using AutoFixture.AutoMoq; using AutoFixture.NUnit3; using EMG.Extensions.Configuration.Model; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace Tests { @@ -33,6 +36,10 @@ public static IFixture CreateFixture() GenerateDelegates = true }); + fixture.Customize(o => o + .With(p => p.PortMappings, (Generator g) => g.Take(1).ToArray()) + ); + fixture.Customize(o => o.FromFactory((ConfigurationBuilder builder, ECSContainerMetadata metadata) => { builder.AddObject(metadata); @@ -40,6 +47,17 @@ public static IFixture CreateFixture() return builder.Build(); })); + fixture.Customize(o => o.FromFactory(() => + { + var services = new ServiceCollection(); + + services.AddOptions(); + + return services; + })); + + fixture.Customize(o => o.Without(p => p.Address)); + return fixture; } } diff --git a/tests/Tests.ServiceModel.ECS/ECSMetadataExtensionsTests.cs b/tests/Tests.ServiceModel.ECS/ECSMetadataExtensionsTests.cs index c8f418c..1be1e27 100644 --- a/tests/Tests.ServiceModel.ECS/ECSMetadataExtensionsTests.cs +++ b/tests/Tests.ServiceModel.ECS/ECSMetadataExtensionsTests.cs @@ -1,21 +1,34 @@ using Moq; using NUnit.Framework; using System; +using System.ServiceModel.Description; using AutoFixture.NUnit3; using EMG.Extensions.Configuration.Model; using EMG.Utilities; using EMG.Utilities.ServiceModel.Configuration; +using EMG.Utilities.ServiceModel.Discovery; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using ECSMetadataExtensions = EMG.Utilities.ECSMetadataExtensions; +using EndpointAddress = System.ServiceModel.EndpointAddress; + +// ReSharper disable InvokeAsExtensionMethod +#pragma warning disable 618 namespace Tests { public class ECSMetadataExtensionsTests { + private static void AddFakeContainerMetadataFileKey() + { + Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, "some_path"); + } + [Test, CustomAutoData] - public void UseECS_returns_private_IPV4_address([Frozen] ECSContainerMetadata metadata, HttpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions, string filePath) + public void UseECS_returns_private_IPV4_address([Frozen] ECSContainerMetadata metadata, HttpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configuration, configureOptions); @@ -23,9 +36,9 @@ public void UseECS_returns_private_IPV4_address([Frozen] ECSContainerMetadata me } [Test, CustomAutoData] - public void UseECS_returns_private_IPV4_address([Frozen] ECSContainerMetadata metadata, NetTcpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions, string filePath) + public void UseECS_returns_private_IPV4_address([Frozen] ECSContainerMetadata metadata, NetTcpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configuration, configureOptions); @@ -33,9 +46,9 @@ public void UseECS_returns_private_IPV4_address([Frozen] ECSContainerMetadata me } [Test, CustomAutoData] - public void UseECS_uses_configureOptions(HttpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions, string filePath) + public void UseECS_uses_configureOptions(HttpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); ECSMetadataExtensions.UseECS(endpointAddress, configuration, configureOptions); @@ -43,9 +56,9 @@ public void UseECS_uses_configureOptions(HttpEndpointAddress endpointAddress, IC } [Test, CustomAutoData] - public void UseECS_uses_configureOptions(NetTcpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions, string filePath) + public void UseECS_uses_configureOptions(NetTcpEndpointAddress endpointAddress, IConfiguration configuration, Action configureOptions) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); ECSMetadataExtensions.UseECS(endpointAddress, configuration, configureOptions); @@ -53,9 +66,9 @@ public void UseECS_uses_configureOptions(NetTcpEndpointAddress endpointAddress, } [Test, CustomAutoData] - public void UseECS_uses_selected_port_mapping(HttpEndpointAddress endpointAddress, IConfiguration configuration, PortMapping mapping, string filePath) + public void UseECS_uses_selected_port_mapping(HttpEndpointAddress endpointAddress, IConfiguration configuration, PortMapping mapping) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configuration, options => options.PortMappingSelector = items => mapping); @@ -63,9 +76,9 @@ public void UseECS_uses_selected_port_mapping(HttpEndpointAddress endpointAddres } [Test, CustomAutoData] - public void UseECS_uses_selected_port_mapping(NetTcpEndpointAddress endpointAddress, IConfiguration configuration, PortMapping mapping, string filePath) + public void UseECS_uses_selected_port_mapping(NetTcpEndpointAddress endpointAddress, IConfiguration configuration, PortMapping mapping) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configuration, options => options.PortMappingSelector = items => mapping); @@ -89,9 +102,9 @@ public void UseECS_returns_same_address_if_ECSContainerMetadataFileKey_not_avail } [Test, CustomAutoData] - public void UseECS_returns_same_address_if_metadata_not_found_in_configuration(HttpEndpointAddress endpointAddress, ConfigurationBuilder configurationBuilder, string filePath) + public void UseECS_returns_same_address_if_metadata_not_found_in_configuration(HttpEndpointAddress endpointAddress, ConfigurationBuilder configurationBuilder) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configurationBuilder.Build()); @@ -99,9 +112,9 @@ public void UseECS_returns_same_address_if_metadata_not_found_in_configuration(H } [Test, CustomAutoData] - public void UseECS_returns_same_address_if_metadata_not_found_in_configuration(NetTcpEndpointAddress endpointAddress, ConfigurationBuilder configurationBuilder, string filePath) + public void UseECS_returns_same_address_if_metadata_not_found_in_configuration(NetTcpEndpointAddress endpointAddress, ConfigurationBuilder configurationBuilder) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configurationBuilder.Build()); @@ -109,9 +122,9 @@ public void UseECS_returns_same_address_if_metadata_not_found_in_configuration(N } [Test, CustomAutoData] - public void UseECS_returns_same_address_if_port_mapping_is_null(HttpEndpointAddress endpointAddress, IConfiguration configuration, string filePath) + public void UseECS_returns_same_address_if_port_mapping_is_null(HttpEndpointAddress endpointAddress, IConfiguration configuration) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configuration, options => options.PortMappingSelector = list => null); @@ -119,9 +132,9 @@ public void UseECS_returns_same_address_if_port_mapping_is_null(HttpEndpointAddr } [Test, CustomAutoData] - public void UseECS_returns_same_address_if_port_mapping_is_null(NetTcpEndpointAddress endpointAddress, IConfiguration configuration, string filePath) + public void UseECS_returns_same_address_if_port_mapping_is_null(NetTcpEndpointAddress endpointAddress, IConfiguration configuration) { - Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, filePath); + AddFakeContainerMetadataFileKey(); var newEndpoint = ECSMetadataExtensions.UseECS(endpointAddress, configuration, options => options.PortMappingSelector = list => null); @@ -133,5 +146,100 @@ public void TearDown() { Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, null); } + + + [Test, CustomAutoData] + public void AddECSContainerSupport_configures_AnnouncementServiceOptions(IServiceCollection services, IConfiguration configuration) + { + AddFakeContainerMetadataFileKey(); + + ECSMetadataExtensions.AddECSContainerSupport(services, configuration); + + Mock.Get(services).Verify(p => p.Add(It.Is(sd => sd.ServiceType == typeof(IConfigureOptions)))); + } + + [Test, CustomAutoData] + public void AddECSContainerSupport_does_nothing_if_no_ECS_metadata_file_key_available(IServiceCollection services, IConfiguration configuration) + { + Environment.SetEnvironmentVariable(EMG.Extensions.Configuration.ECSMetadataExtensions.ECSContainerMetadataFileKey, null); + + ECSMetadataExtensions.AddECSContainerSupport(services, configuration); + + Mock.Get(services).Verify(p => p.Add(It.IsAny()), Times.Never); + } + + [Test, CustomAutoData] + public void AddECSContainerSupport_configuration_throws_if_endpoint_has_no_address(ServiceCollection services, IConfiguration configuration, ServiceEndpoint endpoint) + { + AddFakeContainerMetadataFileKey(); + + ECSMetadataExtensions.AddECSContainerSupport(services, configuration); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>(); + + Assert.Throws(() => options.Value.EndpointDiscoveryMetadata(endpoint)); + } + + [Test, CustomAutoData] + public void AddECSContainerSupport_configuration_returns_metadata_with_replaced_address_when_ecs_metadata_is_valid(ServiceCollection services, [Frozen] ECSContainerMetadata containerMetadata, IConfiguration configuration, ServiceEndpoint endpoint, string address) + { + AddFakeContainerMetadataFileKey(); + + var expectedAddress = new Uri($"http://{containerMetadata.HostPrivateIPv4Address}:{containerMetadata.PortMappings[0].HostPort}/{address}"); + + endpoint.Address = new EndpointAddress($"http://localhost:{containerMetadata.PortMappings[0].ContainerPort}/{address}"); + + ECSMetadataExtensions.AddECSContainerSupport(services, configuration); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>(); + + var result = options.Value.EndpointDiscoveryMetadata(endpoint); + + Assert.That(result.Address.Uri, Is.EqualTo(expectedAddress).IgnoreCase); + } + + [Test, CustomAutoData] + public void AddECSContainerSupport_configuration_returns_metadata_with_same_address_when_ecs_metadata_has_no_ipv4_address(ServiceCollection services, IConfiguration configuration, ServiceEndpoint endpoint, int port, string address) + { + AddFakeContainerMetadataFileKey(); + + configuration[nameof(ECSContainerMetadata.HostPrivateIPv4Address)] = null; + + endpoint.Address = new EndpointAddress($"http://localhost:{port}/{address}"); + + ECSMetadataExtensions.AddECSContainerSupport(services, configuration); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>(); + + var result = options.Value.EndpointDiscoveryMetadata(endpoint); + + Assert.That(result.Address.Uri, Is.EqualTo(endpoint.Address.Uri)); + } + + [Test, CustomAutoData] + public void AddECSContainerSupport_configuration_returns_metadata_with_same_if_no_mapping(ServiceCollection services, [Frozen] ECSContainerMetadata containerMetadata, IConfiguration configuration, ServiceEndpoint endpoint, int port, string address) + { + AddFakeContainerMetadataFileKey(); + + var expectedAddress = new Uri($"http://{containerMetadata.HostPrivateIPv4Address}:{port}/{address}"); + + endpoint.Address = new EndpointAddress($"http://localhost:{port}/{address}"); + + ECSMetadataExtensions.AddECSContainerSupport(services, configuration); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>(); + + var result = options.Value.EndpointDiscoveryMetadata(endpoint); + + Assert.That(result.Address.Uri, Is.EqualTo(expectedAddress).IgnoreCase); + } } } \ No newline at end of file diff --git a/tests/Tests.ServiceModel.ECS/Tests.ServiceModel.ECS.csproj b/tests/Tests.ServiceModel.ECS/Tests.ServiceModel.ECS.csproj index ac5ebeb..acf4b67 100644 --- a/tests/Tests.ServiceModel.ECS/Tests.ServiceModel.ECS.csproj +++ b/tests/Tests.ServiceModel.ECS/Tests.ServiceModel.ECS.csproj @@ -24,4 +24,11 @@ + + + + ..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\System.ServiceModel.Discovery.dll + + +