From 65da88644f8f535b63a5b168745f0fa99d587b39 Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Sun, 8 Jan 2017 15:45:33 -0500 Subject: [PATCH] Added Couchbase.Extensions.DnsDiscovery package for DNS SRV discovery (#1) --- Couchbase.Extensions.sln | 14 + Couchbase.Extensions.sln.DotSettings | 2 +- README.md | 48 +++ appveyor.yml | 17 +- .../Couchbase.Extensions.DnsDiscovery.xproj | 21 ++ .../ICouchbaseDnsLookup.cs | 9 + .../Internal/CouchbaseDnsLookup.cs | 109 +++++++ .../Internal/ILookupClientAdapter.cs | 16 + .../Internal/LookupClientAdapter.cs | 43 +++ .../Properties/AssemblyInfo.cs | 22 ++ .../ServiceCollectionExtensions.cs | 51 +++ .../project.json | 31 ++ ...se.Extensions.DnsDiscovery.UnitTests.xproj | 21 ++ .../Internal/CouchbaseDnsLookupTests.cs | 295 ++++++++++++++++++ .../Program.cs | 14 + .../Properties/AssemblyInfo.cs | 19 ++ .../ServiceCollectionExtensionsTests.cs | 66 ++++ .../project.json | 23 ++ tests/TestApp/.dockerignore | 2 + tests/TestApp/Dockerfile | 6 + tests/TestApp/Properties/launchSettings.json | 4 + tests/TestApp/Startup.cs | 12 +- tests/TestApp/docker-compose.dev.debug.yml | 17 + tests/TestApp/docker-compose.dev.release.yml | 9 + tests/TestApp/docker-compose.yml | 10 + tests/TestApp/project.json | 46 +-- 26 files changed, 901 insertions(+), 26 deletions(-) create mode 100644 src/Couchbase.Extensions.DnsDiscovery/Couchbase.Extensions.DnsDiscovery.xproj create mode 100644 src/Couchbase.Extensions.DnsDiscovery/ICouchbaseDnsLookup.cs create mode 100644 src/Couchbase.Extensions.DnsDiscovery/Internal/CouchbaseDnsLookup.cs create mode 100644 src/Couchbase.Extensions.DnsDiscovery/Internal/ILookupClientAdapter.cs create mode 100644 src/Couchbase.Extensions.DnsDiscovery/Internal/LookupClientAdapter.cs create mode 100644 src/Couchbase.Extensions.DnsDiscovery/Properties/AssemblyInfo.cs create mode 100644 src/Couchbase.Extensions.DnsDiscovery/ServiceCollectionExtensions.cs create mode 100644 src/Couchbase.Extensions.DnsDiscovery/project.json create mode 100644 tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Couchbase.Extensions.DnsDiscovery.UnitTests.xproj create mode 100644 tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Internal/CouchbaseDnsLookupTests.cs create mode 100644 tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Program.cs create mode 100644 tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Properties/AssemblyInfo.cs create mode 100644 tests/Couchbase.Extensions.DnsDiscovery.UnitTests/ServiceCollectionExtensionsTests.cs create mode 100644 tests/Couchbase.Extensions.DnsDiscovery.UnitTests/project.json create mode 100644 tests/TestApp/.dockerignore create mode 100644 tests/TestApp/Dockerfile create mode 100644 tests/TestApp/docker-compose.dev.debug.yml create mode 100644 tests/TestApp/docker-compose.dev.release.yml create mode 100644 tests/TestApp/docker-compose.yml diff --git a/Couchbase.Extensions.sln b/Couchbase.Extensions.sln index 1516667..82de0b5 100644 --- a/Couchbase.Extensions.sln +++ b/Couchbase.Extensions.sln @@ -20,6 +20,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Couchbase.Extensions.Depend EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TestApp", "tests\TestApp\TestApp.xproj", "{4C92B1B9-CE3E-458D-8CBA-359A473E1DBF}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Couchbase.Extensions.DnsDiscovery", "src\Couchbase.Extensions.DnsDiscovery\Couchbase.Extensions.DnsDiscovery.xproj", "{71A8FC7A-A848-4414-BFAE-86E8A08E59A0}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Couchbase.Extensions.DnsDiscovery.UnitTests", "tests\Couchbase.Extensions.DnsDiscovery.UnitTests\Couchbase.Extensions.DnsDiscovery.UnitTests.xproj", "{B743B8D9-1F91-4160-BFED-BFD23EA79CE6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +46,14 @@ Global {4C92B1B9-CE3E-458D-8CBA-359A473E1DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU {4C92B1B9-CE3E-458D-8CBA-359A473E1DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C92B1B9-CE3E-458D-8CBA-359A473E1DBF}.Release|Any CPU.Build.0 = Release|Any CPU + {71A8FC7A-A848-4414-BFAE-86E8A08E59A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71A8FC7A-A848-4414-BFAE-86E8A08E59A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71A8FC7A-A848-4414-BFAE-86E8A08E59A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71A8FC7A-A848-4414-BFAE-86E8A08E59A0}.Release|Any CPU.Build.0 = Release|Any CPU + {B743B8D9-1F91-4160-BFED-BFD23EA79CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B743B8D9-1F91-4160-BFED-BFD23EA79CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B743B8D9-1F91-4160-BFED-BFD23EA79CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B743B8D9-1F91-4160-BFED-BFD23EA79CE6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -51,5 +63,7 @@ Global {DD43EE36-EE8A-42E6-B837-AD5F32FE7F6F} = {727F95F9-8FD9-423C-ACC6-B23E0D6CCC4C} {BACE8433-B111-44E7-A8BE-ECA7A48CAC6A} = {727F95F9-8FD9-423C-ACC6-B23E0D6CCC4C} {4C92B1B9-CE3E-458D-8CBA-359A473E1DBF} = {727F95F9-8FD9-423C-ACC6-B23E0D6CCC4C} + {71A8FC7A-A848-4414-BFAE-86E8A08E59A0} = {EC47DFB9-AB39-41F8-BD7C-7B25BBCDEBD1} + {B743B8D9-1F91-4160-BFED-BFD23EA79CE6} = {727F95F9-8FD9-423C-ACC6-B23E0D6CCC4C} EndGlobalSection EndGlobal diff --git a/Couchbase.Extensions.sln.DotSettings b/Couchbase.Extensions.sln.DotSettings index ef6e123..6f4cbfd 100644 --- a/Couchbase.Extensions.sln.DotSettings +++ b/Couchbase.Extensions.sln.DotSettings @@ -1,3 +1,3 @@  - <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Couchbase.Extensions.DependencyInjection.*Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="Couchbase.Extensions.DependencyInjection" ModuleVersionMask="*" ClassMask="Couchbase.Extensions.DependencyInjection.Internal.ClusterProvider" FunctionMask="CreateCluster" IsEnabled="True" /><Filter ModuleMask="TestApp" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> + <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="*Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="Couchbase.Extensions.DependencyInjection" ModuleVersionMask="*" ClassMask="Couchbase.Extensions.DependencyInjection.Internal.ClusterProvider" FunctionMask="CreateCluster" IsEnabled="True" /><Filter ModuleMask="TestApp" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="Couchbase.Extensions.DnsDiscovery" ModuleVersionMask="*" ClassMask="Couchbase.Extensions.DnsDiscovery.Internal.LookupClientAdapter" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> <data><AttributeFilter ClassMask="Couchbase.Extensions.DependencyInjection.Internal.ExcludeFromCodeCoverageAttribute" IsEnabled="True" /></data> \ No newline at end of file diff --git a/README.md b/README.md index ccc131e..a405f87 100644 --- a/README.md +++ b/README.md @@ -143,3 +143,51 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF } ``` +## Couchbase.Extensions.DnsDiscovery + +A .Net Core compatible DNS SRV mechanism for discovering a Couchbase cluster dynamically. + +### Configuration + +To use, call `AddCouchbaseDnsDiscovery` during the service registration process, usually in your `Startup` class. You may choose whether or not to add this step based on the environment. Be sure to add the basic Couchbase configuration first. + +```csharp +public class Startup +{ + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + Environment = env; + } + + public IConfigurationRoot Configuration { get; } + public IHostingEnvironment Environment { get; } + + public void ConfigureServices(IServiceCollection services) + { + // Register Couchbase with configuration section + services.AddCouchbase(Configuration.GetSection("Couchbase")); + + if (Environment.IsProduction()) + { + // Lookup DNS SRV records using the query _couchbase._tcp.services.local + services.AddCouchbaseDnsDiscovery("_couchbase._tcp.services.local"); + + // Note: the record name above could also be retreived from configuration + } + + // Register other services, like .AddMvc() + } +``` + +The above configuration will perform a DNS SRV query (only in the Production environment). The results will be used to replace the Servers list in the Couchbase client configuration. Any servers listed in the configuration section will be thrown out. + +Note that only the servers with the highest priority in the DNS SRV response will be used. For example, if the response returns 2 servers with a priority of 10 and 2 different servers with a priority of 20, the first set will be used to initialize the cluster. SRV record failover is not supported. + +Additionally, due to the nature of Couchbase clusters, the weight fields in the DNS SRV records are ignored. However, the port is used and should normally be set to 8091. diff --git a/appveyor.yml b/appveyor.yml index e3c2dda..f015e86 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,7 @@ version: '{build}' +branches: + only: + - master configuration: Release environment: DOTNET_CLI_TELEMETRY_OPTOUT: 1 @@ -12,16 +15,26 @@ build: verbosity: minimal after_build: - cmd: dotnet pack --configuration %CONFIGURATION% "src/Couchbase.Extensions.DependencyInjection" +- cmd: dotnet pack --configuration %CONFIGURATION% "src/Couchbase.Extensions.DnsDiscovery" test_script: - cmd: dotnet test "tests\Couchbase.Extensions.DependencyInjection.UnitTests" +- cmd: dotnet test "tests\Couchbase.Extensions.DnsDiscovery.UnitTests" artifacts: - path: src/Couchbase.Extensions.DependencyInjection/bin/%CONFIGURATION%/*.nupkg - name: NuGet + name: DependencyInjection +- path: src/Couchbase.Extensions.DnsDiscovery/bin/%CONFIGURATION%/*.nupkg + name: DnsDiscovery deploy: - provider: NuGet api_key: secure: KzT1ESVyB5LB0Ovg+dPUmvZYQJ0XYoBEe9DW1pBDKzv0y/7y2RlJ1xt16ZI5dnVE - artifact: NuGet + artifact: DependencyInjection + on: + APPVEYOR_REPO_TAG: true +- provider: NuGet + api_key: + secure: KzT1ESVyB5LB0Ovg+dPUmvZYQJ0XYoBEe9DW1pBDKzv0y/7y2RlJ1xt16ZI5dnVE + artifact: DnsDiscovery on: APPVEYOR_REPO_TAG: true notifications: diff --git a/src/Couchbase.Extensions.DnsDiscovery/Couchbase.Extensions.DnsDiscovery.xproj b/src/Couchbase.Extensions.DnsDiscovery/Couchbase.Extensions.DnsDiscovery.xproj new file mode 100644 index 0000000..002b94d --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/Couchbase.Extensions.DnsDiscovery.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 71a8fc7a-a848-4414-bfae-86e8a08e59a0 + Couchbase.Extensions.DnsDiscovery + .\obj + .\bin\ + v4.5.2 + + + + 2.0 + + + diff --git a/src/Couchbase.Extensions.DnsDiscovery/ICouchbaseDnsLookup.cs b/src/Couchbase.Extensions.DnsDiscovery/ICouchbaseDnsLookup.cs new file mode 100644 index 0000000..4119b01 --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/ICouchbaseDnsLookup.cs @@ -0,0 +1,9 @@ +using Couchbase.Configuration.Client; + +namespace Couchbase.Extensions.DnsDiscovery +{ + public interface ICouchbaseDnsLookup + { + void Apply(CouchbaseClientDefinition clientDefinition, string recordName); + } +} diff --git a/src/Couchbase.Extensions.DnsDiscovery/Internal/CouchbaseDnsLookup.cs b/src/Couchbase.Extensions.DnsDiscovery/Internal/CouchbaseDnsLookup.cs new file mode 100644 index 0000000..3e57a20 --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/Internal/CouchbaseDnsLookup.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Couchbase.Configuration.Client; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Logging; + +namespace Couchbase.Extensions.DnsDiscovery.Internal +{ + internal class CouchbaseDnsLookup : ICouchbaseDnsLookup + { + private readonly ILookupClientAdapter _lookupClient; + private readonly ILogger _logger; + + public CouchbaseDnsLookup(ILookupClientAdapter lookupClient, ILogger logger) + { + if (lookupClient == null) + { + throw new ArgumentNullException(nameof(lookupClient)); + } + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _lookupClient = lookupClient; + _logger = logger; + } + + public void Apply(CouchbaseClientDefinition clientDefinition, string recordName) + { + if (clientDefinition == null) + { + throw new ArgumentNullException(nameof(clientDefinition)); + } + if (recordName == null) + { + throw new ArgumentNullException(nameof(recordName)); + } + + try + { + // Ensure an empty collection of servers before resolving + if (clientDefinition.Servers == null) + { + clientDefinition.Servers = new List(); + } + else + { + clientDefinition.Servers.Clear(); + } + + _logger.LogInformation("Looking up Couchbase servers using record '{0}'", recordName); + + List servers; + var syncContextCache = SynchronizationContext.Current; + try + { + // Ensure that we're outside any sync context before waiting on an async result to prevent deadlocks + SynchronizationContext.SetSynchronizationContext(null); + + servers = _lookupClient + .QuerySrvAsync(recordName).Result + .OrderBy(p => p.Priority) + .ToList(); + } + finally + { + if (syncContextCache != null) + { + SynchronizationContext.SetSynchronizationContext(syncContextCache); + } + } + + if (!servers.Any()) + { + _logger.LogError("No SRV records returned for query '{0}'", recordName); + return; + } + + var firstPriority = servers.First().Priority; + foreach (var server in servers.Where(p => p.Priority == firstPriority)) + { + var uri = new Uri($"http://{FormatTargetDns(server.Target)}:{server.Port}/pools"); + + _logger.LogInformation("Got Couchbase server '{0}'", uri); + + clientDefinition.Servers.Add(uri); + } + } + catch (Exception ex) + { + _logger.LogError(0, ex, "Exception getting Couchbase servers for '{0}'", recordName); + } + } + + private string FormatTargetDns(string target) + { + if (target.EndsWith(".")) + { + return target.Substring(0, target.Length - 1); + } + + return target; + } + } +} diff --git a/src/Couchbase.Extensions.DnsDiscovery/Internal/ILookupClientAdapter.cs b/src/Couchbase.Extensions.DnsDiscovery/Internal/ILookupClientAdapter.cs new file mode 100644 index 0000000..c9558fc --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/Internal/ILookupClientAdapter.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DnsClient.Protocol; + +namespace Couchbase.Extensions.DnsDiscovery.Internal +{ + /// + /// Mockable adapter for DnsClient.LookupClient + /// + internal interface ILookupClientAdapter + { + Task> QuerySrvAsync(string query); + } +} diff --git a/src/Couchbase.Extensions.DnsDiscovery/Internal/LookupClientAdapter.cs b/src/Couchbase.Extensions.DnsDiscovery/Internal/LookupClientAdapter.cs new file mode 100644 index 0000000..9958c17 --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/Internal/LookupClientAdapter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Logging; + +namespace Couchbase.Extensions.DnsDiscovery.Internal +{ + /// + /// Mockable adapter for DnsClient.LookupClient + /// + internal class LookupClientAdapter : ILookupClientAdapter + { + private readonly ILogger _logger; + private readonly LookupClient _lookupClient = new LookupClient(); + + public LookupClientAdapter(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger; + } + + public async Task> QuerySrvAsync(string query) + { + var response = await _lookupClient.QueryAsync(query, QueryType.SRV); + + if (response.HasError) + { + _logger.LogError("DNS query error '{0}'", response.ErrorMessage); + + return new SrvRecord[] {}; + } + + return response.Answers.SrvRecords(); + } + } +} diff --git a/src/Couchbase.Extensions.DnsDiscovery/Properties/AssemblyInfo.cs b/src/Couchbase.Extensions.DnsDiscovery/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..25c8b0b --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Couchbase.Extensions.DnsDiscovery")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("71a8fc7a-a848-4414-bfae-86e8a08e59a0")] + +[assembly: InternalsVisibleTo("Couchbase.Extensions.DnsDiscovery.UnitTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Couchbase.Extensions.DnsDiscovery/ServiceCollectionExtensions.cs b/src/Couchbase.Extensions.DnsDiscovery/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f8fe275 --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/ServiceCollectionExtensions.cs @@ -0,0 +1,51 @@ +using System; +using Couchbase.Configuration.Client; +using Couchbase.Extensions.DnsDiscovery.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Couchbase.Extensions.DnsDiscovery +{ + /// + /// Extensions to for Couchbase DNS discovery. + /// + public static class ServiceCollectionExtensions + { + /// + /// Add Couchbase dependencies to the . + /// + /// The . + /// Name of the DNS SRV record for lookup. + /// The . + /// + /// This method should be called after adding any other Couchbase configuration + /// to the service provider. + /// + public static IServiceCollection AddCouchbaseDnsDiscovery(this IServiceCollection services, + string recordName) + { + if (recordName == null) + { + throw new ArgumentNullException(nameof(recordName)); + } + + // Register CouchbaseDnsLookup + services.TryAddTransient(); + services.TryAddTransient(); + + // Register the action to alter CouchbaseClientDefinition options + return services.AddTransient>(serviceProvider => + { + // Get the ICouchbaseDnsLookup implementation + var lookup = serviceProvider.GetRequiredService(); + + // Return action that calls Apply on the ICouchbaseDnsLookup implementation + return new ConfigureOptions(clientDefinition => + { + lookup.Apply(clientDefinition, recordName); + }); + }); + } + } +} diff --git a/src/Couchbase.Extensions.DnsDiscovery/project.json b/src/Couchbase.Extensions.DnsDiscovery/project.json new file mode 100644 index 0000000..140857e --- /dev/null +++ b/src/Couchbase.Extensions.DnsDiscovery/project.json @@ -0,0 +1,31 @@ +{ + "version": "1.0.0-beta2", + "authors": [ "Brant Burnett" ], + + "packOptions": { + "summary": "A .Net Core compatible DNS SRV mechanism for discovering a Couchbase cluster dynamically.", + "tags": [ "Couchbase", "netcore", "dns", "microservices" ], + "projectUrl": "https://github.com/brantburnett/Couchbase.Extensions", + "licenseUrl": "https://www.apache.org/licenses/LICENSE-2.0", + "iconUrl": "https://raw.githubusercontent.com/couchbaselabs/Linq2Couchbase/master/Packaging/couchbase-logo.png", + "owners": [ "btburnett3" ], + "repository": { + "type": "git", + "url": "https://github.com/brantburnett/Couchbase.Extensions" + } + }, + + "dependencies": { + "CouchbaseNetClient": "2.4.0-dp3", + "DnsClient": "1.0.1", + "NETStandard.Library": "1.6.1", + "Microsoft.Extensions.Logging": "1.0.1", + "Microsoft.Extensions.Options": "1.0.1" + }, + + "frameworks": { + "netstandard1.5": { + "imports": "dnxcore50" + } + } +} diff --git a/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Couchbase.Extensions.DnsDiscovery.UnitTests.xproj b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Couchbase.Extensions.DnsDiscovery.UnitTests.xproj new file mode 100644 index 0000000..73a066a --- /dev/null +++ b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Couchbase.Extensions.DnsDiscovery.UnitTests.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + b743b8d9-1f91-4160-bfed-bfd23ea79ce6 + Couchbase.Extensions.DnsDiscovery.UnitTests + .\obj + .\bin\ + v4.5.2 + + + + 2.0 + + + diff --git a/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Internal/CouchbaseDnsLookupTests.cs b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Internal/CouchbaseDnsLookupTests.cs new file mode 100644 index 0000000..8e23cd6 --- /dev/null +++ b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Internal/CouchbaseDnsLookupTests.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Couchbase.Configuration.Client; +using Couchbase.Extensions.DnsDiscovery.Internal; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Couchbase.Extensions.DnsDiscovery.UnitTests.Internal +{ + public class CouchbaseDnsLookupTests + { + private const string RecordName = "_couchbase._tcp.services.local"; + + private static readonly Uri ServerRecord1ExpectedUrl = + new Uri("http://couchbaseserver1.services.local:8091/pools"); + + private static readonly Uri ServerRecord2ExpectedUrl = + new Uri("http://couchbaseserver2.services.local:8091/pools"); + + private static readonly SrvRecord SrvRecord1Priority10 = new SrvRecord( + new ResourceRecordInfo(RecordName, ResourceRecordType.SRV, QueryClass.IN, 60, 0), + 10, 10, 8091, new DnsName("couchbaseserver1.services.local.")); + + private static readonly SrvRecord SrvRecord2Priority10 = new SrvRecord( + new ResourceRecordInfo(RecordName, ResourceRecordType.SRV, QueryClass.IN, 60, 0), + 10, 10, 8091, new DnsName("couchbaseserver2.services.local.")); + + private static readonly SrvRecord SrvRecord3Priority20 = new SrvRecord( + new ResourceRecordInfo(RecordName, ResourceRecordType.SRV, QueryClass.IN, 60, 0), + 20, 10, 8091, new DnsName("couchbaseserver3.services.local.")); + + private static readonly SrvRecord SrvRecord4Priority20 = new SrvRecord( + new ResourceRecordInfo(RecordName, ResourceRecordType.SRV, QueryClass.IN, 60, 0), + 20, 10, 8091, new DnsName("couchbaseserver4.services.local.")); + + #region ctor + + [Fact] + public void ctor_NoLookupClient_Exception() + { + // Arrange + + var logger = new Mock>(); + + // Act/Assert + + var ex = Assert.Throws(() => new CouchbaseDnsLookup(null, logger.Object)); + + Assert.Equal("lookupClient", ex.ParamName); + } + + + [Fact] + public void ctor_NoLogger_Exception() + { + // Arrange + + var lookupClient = new Mock(); + + // Act/Assert + + var ex = Assert.Throws(() => new CouchbaseDnsLookup(lookupClient.Object, null)); + + Assert.Equal("logger", ex.ParamName); + } + + #endregion + + #region Apply + + [Fact] + public void Apply_NoClientDefinition_Exception() + { + // Arrange + + var lookupClient = new Mock(); + var logger = new Mock>(); + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act/Assert + + var ex = Assert.Throws(() => lookup.Apply(null, RecordName)); + + Assert.Equal("clientDefinition", ex.ParamName); + } + + [Fact] + public void Apply_NoRecordName_Exception() + { + // Arrange + + var lookupClient = new Mock(); + var logger = new Mock>(); + var clientDefinition = new CouchbaseClientDefinition(); + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act/Assert + + var ex = Assert.Throws(() => lookup.Apply(clientDefinition, null)); + + Assert.Equal("recordName", ex.ParamName); + } + + [Fact] + public void Apply_GotResults_ReturnsServersInOrder() + { + // Arrange + + var lookupClient = new Mock(); + lookupClient + .Setup(m => m.QuerySrvAsync(RecordName)) + .Returns(Task.FromResult(new[] + { + SrvRecord3Priority20, + SrvRecord4Priority20, + SrvRecord1Priority10, + SrvRecord2Priority10 + }.AsEnumerable())); + + var logger = new Mock>(); + + var clientDefinition = new CouchbaseClientDefinition(); + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act + + lookup.Apply(clientDefinition, RecordName); + + // Assert + + Assert.Equal(2, clientDefinition.Servers.Count); + Assert.Equal(ServerRecord1ExpectedUrl, clientDefinition.Servers[0]); + Assert.Equal(ServerRecord2ExpectedUrl, clientDefinition.Servers[1]); + } + + [Fact] + public void Apply_GotResults_ReturnsServersForHighestPriorityOnly() + { + // Arrange + + var lookupClient = new Mock(); + lookupClient + .Setup(m => m.QuerySrvAsync(RecordName)) + .Returns(Task.FromResult(new[] + { + SrvRecord1Priority10, + SrvRecord2Priority10 + }.AsEnumerable())); + + var logger = new Mock>(); + + var clientDefinition = new CouchbaseClientDefinition(); + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act + + lookup.Apply(clientDefinition, RecordName); + + // Assert + + Assert.Equal(2, clientDefinition.Servers.Count); + Assert.Equal(ServerRecord1ExpectedUrl, clientDefinition.Servers[0]); + Assert.Equal(ServerRecord2ExpectedUrl, clientDefinition.Servers[1]); + } + + [Fact] + public void Apply_HasServerList_ClearsListFirst() + { + // Arrange + + var lookupClient = new Mock(); + lookupClient + .Setup(m => m.QuerySrvAsync(RecordName)) + .Returns(Task.FromResult(new [] + { + SrvRecord1Priority10, + SrvRecord2Priority10 + }.AsEnumerable())); + + var logger = new Mock>(); + + var clientDefinition = new CouchbaseClientDefinition + { + Servers = new List + { + new Uri("http://shouldremove/") + } + }; + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act + + lookup.Apply(clientDefinition, RecordName); + + // Assert + + Assert.Equal(2, clientDefinition.Servers.Count); + } + + [Fact] + public void Apply_NoResults_ReturnsEmptyList() + { + // Arrange + + var lookupClient = new Mock(); + lookupClient + .Setup(m => m.QuerySrvAsync(RecordName)) + .Returns(Task.FromResult(new SrvRecord[] {}.AsEnumerable())); + + var logger = new Mock>(); + + var clientDefinition = new CouchbaseClientDefinition(); + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act + + lookup.Apply(clientDefinition, RecordName); + + // Assert + + Assert.Equal(0, clientDefinition.Servers.Count); + } + + [Fact] + public void Apply_UnhandledException_ReturnsEmptyList() + { + // Arrange + + var lookupClient = new Mock(); + lookupClient + .Setup(m => m.QuerySrvAsync(RecordName)) + .ThrowsAsync(new Exception("Badness Happened")); + + var logger = new Mock>(); + + var clientDefinition = new CouchbaseClientDefinition(); + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act + + lookup.Apply(clientDefinition, RecordName); + + // Assert + + Assert.Equal(0, clientDefinition.Servers.Count); + } + + [Fact] + public void Apply_UnhandledException_LogsError() + { + // Arrange + + var lookupClient = new Mock(); + lookupClient + .Setup(m => m.QuerySrvAsync(RecordName)) + .ThrowsAsync(new Exception("Badness Happened")); + + var logger = new Mock>(); + logger.Setup( + m => + m.Log(LogLevel.Error, It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny>())); + + var clientDefinition = new CouchbaseClientDefinition(); + + var lookup = new CouchbaseDnsLookup(lookupClient.Object, logger.Object); + + // Act + + lookup.Apply(clientDefinition, RecordName); + + // Assert + + logger.Verify( + m => + m.Log(LogLevel.Error, It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny>()), + Times.AtLeastOnce); + } + + #endregion + } +} diff --git a/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Program.cs b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Program.cs new file mode 100644 index 0000000..2759593 --- /dev/null +++ b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Program.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Couchbase.Extensions.DnsDiscovery.UnitTests +{ + public class Program + { + public static void Main(string[] args) + { + } + } +} diff --git a/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Properties/AssemblyInfo.cs b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dbbd942 --- /dev/null +++ b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Couchbase.Extensions.DnsDiscovery.UnitTests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b743b8d9-1f91-4160-bfed-bfd23ea79ce6")] diff --git a/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/ServiceCollectionExtensionsTests.cs b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..203a5d4 --- /dev/null +++ b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using Couchbase.Configuration.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Couchbase.Extensions.DnsDiscovery.UnitTests +{ + public class ServiceCollectionExtensionsTests + { + #region AddCouchbaseDnsDiscovery + + [Fact] + public void AddCouchbaseDnsDiscovery_NullRecordName_Exception() + { + // Arrange + + var services = new ServiceCollection(); + + // Act/Assert + + var ex = Assert.Throws(() => services.AddCouchbaseDnsDiscovery(null)); + + Assert.Equal("recordName", ex.ParamName); + } + + [Fact] + public void AddCouchbaseDnsDiscovery_WithRecordName_RegistersOption() + { + // Arrange + + const string recordName = "_couchbase._tcp.services.local"; + + var serverList = new List(); + + var lookup = new Mock(); + lookup + .Setup(m => m.Apply(It.IsAny(), recordName)) + .Callback((clientDefinition, tempRecordName) => + { + clientDefinition.Servers = serverList; + }); + + var services = new ServiceCollection(); + services.AddOptions(); + services.AddSingleton(lookup.Object); + services.AddCouchbaseDnsDiscovery(recordName); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + + var result = serviceProvider.GetService>(); + + // Assert + + Assert.NotNull(result); + Assert.Equal(serverList, result.Value.Servers); + } + + #endregion + } +} + diff --git a/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/project.json b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/project.json new file mode 100644 index 0000000..890e6ee --- /dev/null +++ b/tests/Couchbase.Extensions.DnsDiscovery.UnitTests/project.json @@ -0,0 +1,23 @@ +{ + "version": "1.0.0-*", + + "dependencies": { + "Couchbase.Extensions.DnsDiscovery": "1.0.0-*", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.1" + }, + "Microsoft.Extensions.DependencyInjection": "1.0.1", + "Moq": "4.6.38-alpha", + "xunit": "2.2.0-beta4-build3444" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + }, + + "testRunner": "xunit" +} diff --git a/tests/TestApp/.dockerignore b/tests/TestApp/.dockerignore new file mode 100644 index 0000000..63d337d --- /dev/null +++ b/tests/TestApp/.dockerignore @@ -0,0 +1,2 @@ +docker-compose.yml +Dockerfile diff --git a/tests/TestApp/Dockerfile b/tests/TestApp/Dockerfile new file mode 100644 index 0000000..372f139 --- /dev/null +++ b/tests/TestApp/Dockerfile @@ -0,0 +1,6 @@ +FROM microsoft/aspnetcore:1.0.1 +ENTRYPOINT ["dotnet", "TestApp.dll"] +ARG source=. +WORKDIR /app +EXPOSE 80 +COPY $source . diff --git a/tests/TestApp/Properties/launchSettings.json b/tests/TestApp/Properties/launchSettings.json index 1f77162..70655c7 100644 --- a/tests/TestApp/Properties/launchSettings.json +++ b/tests/TestApp/Properties/launchSettings.json @@ -22,6 +22,10 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "Docker": { + "launchBrowser": true, + "launchUrl": "http://localhost:{ServicePort}" } } } \ No newline at end of file diff --git a/tests/TestApp/Startup.cs b/tests/TestApp/Startup.cs index 52bb3f5..ea33839 100644 --- a/tests/TestApp/Startup.cs +++ b/tests/TestApp/Startup.cs @@ -1,4 +1,5 @@ using Couchbase.Extensions.DependencyInjection; +using Couchbase.Extensions.DnsDiscovery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -19,9 +20,11 @@ public Startup(IHostingEnvironment env) .AddEnvironmentVariables(); Configuration = builder.Build(); + Environment = env; } public IConfigurationRoot Configuration { get; } + public IHostingEnvironment Environment { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) @@ -31,17 +34,22 @@ public void ConfigureServices(IServiceCollection services) .AddCouchbase(Configuration.GetSection("Couchbase")) .AddCouchbaseBucket("travel-sample"); + if (Environment.IsProduction()) + { + services.AddCouchbaseDnsDiscovery("_couchbase._tcp.services.local"); + } + services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IApplicationLifetime applicationLifetime) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); - if (env.IsDevelopment()) + if (Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); diff --git a/tests/TestApp/docker-compose.dev.debug.yml b/tests/TestApp/docker-compose.dev.debug.yml new file mode 100644 index 0000000..24e4d62 --- /dev/null +++ b/tests/TestApp/docker-compose.dev.debug.yml @@ -0,0 +1,17 @@ +version: '2' + +services: + testapp: + build: + args: + source: obj/Docker/empty/ + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - DOTNET_USE_POLLING_FILE_WATCHER=1 + volumes: + - .:/app + - ~/.nuget/packages:/root/.nuget/packages:ro + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null diff --git a/tests/TestApp/docker-compose.dev.release.yml b/tests/TestApp/docker-compose.dev.release.yml new file mode 100644 index 0000000..6d0c2b6 --- /dev/null +++ b/tests/TestApp/docker-compose.dev.release.yml @@ -0,0 +1,9 @@ +version: '2' + +services: + testapp: + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + volumes: + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null diff --git a/tests/TestApp/docker-compose.yml b/tests/TestApp/docker-compose.yml new file mode 100644 index 0000000..5233037 --- /dev/null +++ b/tests/TestApp/docker-compose.yml @@ -0,0 +1,10 @@ +version: '2' + +services: + testapp: + image: user/testapp${TAG} + build: + context: . + dockerfile: Dockerfile + ports: + - "80" diff --git a/tests/TestApp/project.json b/tests/TestApp/project.json index ce414a1..364e15b 100644 --- a/tests/TestApp/project.json +++ b/tests/TestApp/project.json @@ -1,35 +1,34 @@ -{ +{ "dependencies": { "Couchbase.Extensions.DependencyInjection": "1.0.0-*", + "Couchbase.Extensions.DnsDiscovery": "1.0.0-*", "Microsoft.NETCore.App": { "version": "1.0.1", "type": "platform" }, - "Microsoft.AspNetCore.Diagnostics": "1.0.0", + "Microsoft.AspNetCore.Diagnostics": "1.0.1", "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.AspNetCore.Routing": "1.0.1", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", + "Microsoft.AspNetCore.Server.IISIntegration": "1.0.1", "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", - "Microsoft.AspNetCore.StaticFiles": "1.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", - "Microsoft.Extensions.Configuration.Json": "1.0.0", - "Microsoft.Extensions.Logging": "1.0.0", - "Microsoft.Extensions.Logging.Console": "1.0.0", - "Microsoft.Extensions.Logging.Debug": "1.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", + "Microsoft.AspNetCore.StaticFiles": "1.0.1", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.1", + "Microsoft.Extensions.Configuration.Json": "1.0.1", + "Microsoft.Extensions.Logging": "1.0.1", + "Microsoft.Extensions.Logging.Console": "1.0.1", + "Microsoft.Extensions.Logging.Debug": "1.0.1", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.1", "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0" }, - "tools": { "BundlerMinifier.Core": "2.0.238", "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final", "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" }, - "frameworks": { "netcoreapp1.0": { "imports": [ @@ -38,29 +37,34 @@ ] } }, - "buildOptions": { "emitEntryPoint": true, - "preserveCompilationContext": true + "preserveCompilationContext": true, + "debugType": "portable" }, - "runtimeOptions": { "configProperties": { "System.GC.Server": true } }, - "publishOptions": { "include": [ "wwwroot", "**/*.cshtml", "appsettings.json", - "web.config" + "web.config", + "docker-compose.yml", + "Dockerfile", + ".dockerignore" ] }, - "scripts": { - "prepublish": [ "bower install", "dotnet bundle" ], - "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] + "prepublish": [ + "bower install", + "dotnet bundle" + ], + "postpublish": [ + "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" + ] } -} +} \ No newline at end of file