diff --git a/src/Ocelot/Configuration/FileConfigurationController.cs b/src/Ocelot/Configuration/FileConfigurationController.cs index 707eb61d..3b88096f 100644 --- a/src/Ocelot/Configuration/FileConfigurationController.cs +++ b/src/Ocelot/Configuration/FileConfigurationController.cs @@ -1,69 +1,76 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Setter; -using Ocelot.Raft; -using Rafty.Concensus; - -namespace Ocelot.Configuration -{ - using Repository; - - [Authorize] - [Route("configuration")] - public class FileConfigurationController : Controller - { - private readonly IFileConfigurationRepository _repo; - private readonly IFileConfigurationSetter _setter; - private readonly IServiceProvider _provider; - - public FileConfigurationController(IFileConfigurationRepository repo, IFileConfigurationSetter setter, IServiceProvider provider) - { - _repo = repo; - _setter = setter; - _provider = provider; - } - - [HttpGet] - public async Task Get() - { - var response = await _repo.Get(); - - if(response.IsError) - { - return new BadRequestObjectResult(response.Errors); - } - - return new OkObjectResult(response.Data); - } - - [HttpPost] - public async Task Post([FromBody]FileConfiguration fileConfiguration) - { - //todo - this code is a bit shit sort it out.. - var test = _provider.GetService(typeof(INode)); - if (test != null) - { - var node = (INode)test; - var result = node.Accept(new UpdateFileConfiguration(fileConfiguration)); - if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse)) - { - return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub."); - } - - return new OkObjectResult(result.Command.Configuration); - } - - var response = await _setter.Set(fileConfiguration); - - if (response.IsError) - { - return new BadRequestObjectResult(response.Errors); - } - - return new OkObjectResult(fileConfiguration); - } - } -} +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Setter; +using Ocelot.Raft; +using Rafty.Concensus; + +namespace Ocelot.Configuration +{ + using Repository; + + [Authorize] + [Route("configuration")] + public class FileConfigurationController : Controller + { + private readonly IFileConfigurationRepository _repo; + private readonly IFileConfigurationSetter _setter; + private readonly IServiceProvider _provider; + + public FileConfigurationController(IFileConfigurationRepository repo, IFileConfigurationSetter setter, IServiceProvider provider) + { + _repo = repo; + _setter = setter; + _provider = provider; + } + + [HttpGet] + public async Task Get() + { + var response = await _repo.Get(); + + if(response.IsError) + { + return new BadRequestObjectResult(response.Errors); + } + + return new OkObjectResult(response.Data); + } + + [HttpPost] + public async Task Post([FromBody]FileConfiguration fileConfiguration) + { + try + { + //todo - this code is a bit shit sort it out.. + var test = _provider.GetService(typeof(INode)); + if (test != null) + { + var node = (INode)test; + var result = node.Accept(new UpdateFileConfiguration(fileConfiguration)); + if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse)) + { + return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub."); + } + + return new OkObjectResult(result.Command.Configuration); + } + + var response = await _setter.Set(fileConfiguration); + + if (response.IsError) + { + return new BadRequestObjectResult(response.Errors); + } + + return new OkObjectResult(fileConfiguration); + } + catch(Exception e) + { + return new BadRequestObjectResult($"{e.Message}:{e.StackTrace}"); + } + } + } +} diff --git a/src/Ocelot/Raft/HttpPeer.cs b/src/Ocelot/Raft/HttpPeer.cs index fcec77ea..18ecdee1 100644 --- a/src/Ocelot/Raft/HttpPeer.cs +++ b/src/Ocelot/Raft/HttpPeer.cs @@ -1,128 +1,129 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using Newtonsoft.Json; -using Ocelot.Configuration; -using Ocelot.Middleware; -using Rafty.Concensus; -using Rafty.FiniteStateMachine; - -namespace Ocelot.Raft -{ - [ExcludeFromCoverage] - public class HttpPeer : IPeer - { - private readonly string _hostAndPort; - private readonly HttpClient _httpClient; - private readonly JsonSerializerSettings _jsonSerializerSettings; - private readonly string _baseSchemeUrlAndPort; - private BearerToken _token; - private readonly IInternalConfiguration _config; - private readonly IIdentityServerConfiguration _identityServerConfiguration; - - public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration) - { - _identityServerConfiguration = identityServerConfiguration; - _config = config; - Id = hostAndPort; - _hostAndPort = hostAndPort; - _httpClient = httpClient; - _jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - _baseSchemeUrlAndPort = finder.Find(); - } - - public string Id {get; private set;} - - public RequestVoteResponse Request(RequestVote requestVote) - { - if(_token == null) - { - SetToken(); - } - - var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content).GetAwaiter().GetResult(); - if(response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); - } - else - { - return new RequestVoteResponse(false, requestVote.Term); - } - } - - public AppendEntriesResponse Request(AppendEntries appendEntries) - { - try - { - if(_token == null) - { - SetToken(); - } - - var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content).GetAwaiter().GetResult(); - if(response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(),_jsonSerializerSettings); - } - else - { - return new AppendEntriesResponse(appendEntries.Term, false); - } - } - catch(Exception ex) - { - Console.WriteLine(ex); - return new AppendEntriesResponse(appendEntries.Term, false); - } - } - - public Response Request(T command) - where T : ICommand - { - if(_token == null) - { - SetToken(); - } - - var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content).GetAwaiter().GetResult(); - if(response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); - } - else - { - return new ErrorResponse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), command); - } - } - - private void SetToken() - { - var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", _identityServerConfiguration.ApiName), - new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), - new KeyValuePair("scope", _identityServerConfiguration.ApiName), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - var response = _httpClient.PostAsync(tokenUrl, content).GetAwaiter().GetResult(); - var responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); - } - } -} +using System; +using System.Collections.Generic; +using System.Net.Http; +using Newtonsoft.Json; +using Ocelot.Configuration; +using Ocelot.Middleware; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class HttpPeer : IPeer + { + private readonly string _hostAndPort; + private readonly HttpClient _httpClient; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly string _baseSchemeUrlAndPort; + private BearerToken _token; + private readonly IInternalConfiguration _config; + private readonly IIdentityServerConfiguration _identityServerConfiguration; + + public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration) + { + _identityServerConfiguration = identityServerConfiguration; + _config = config; + Id = hostAndPort; + _hostAndPort = hostAndPort; + _httpClient = httpClient; + _jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + _baseSchemeUrlAndPort = finder.Find(); + } + + public string Id {get; private set;} + + public RequestVoteResponse Request(RequestVote requestVote) + { + if(_token == null) + { + SetToken(); + } + + var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content).GetAwaiter().GetResult(); + if(response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); + } + else + { + return new RequestVoteResponse(false, requestVote.Term); + } + } + + public AppendEntriesResponse Request(AppendEntries appendEntries) + { + try + { + if(_token == null) + { + SetToken(); + } + + var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content).GetAwaiter().GetResult(); + if(response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(),_jsonSerializerSettings); + } + else + { + return new AppendEntriesResponse(appendEntries.Term, false); + } + } + catch(Exception ex) + { + Console.WriteLine(ex); + return new AppendEntriesResponse(appendEntries.Term, false); + } + } + + public Response Request(T command) + where T : ICommand + { + if(_token == null) + { + SetToken(); + } + + var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content).GetAwaiter().GetResult(); + if(response.IsSuccessStatusCode) + { + var okResponse = JsonConvert.DeserializeObject>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); + return new OkResponse((T)okResponse.Command); + } + else + { + return new ErrorResponse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), command); + } + } + + private void SetToken() + { + var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", _identityServerConfiguration.ApiName), + new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), + new KeyValuePair("scope", _identityServerConfiguration.ApiName), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + var response = _httpClient.PostAsync(tokenUrl, content).GetAwaiter().GetResult(); + var responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); + } + } +} diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs index baad5091..bbb8ff41 100644 --- a/test/Ocelot.IntegrationTests/RaftTests.cs +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -1,393 +1,431 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.Raft; -using Rafty.Concensus; -using Rafty.Infrastructure; -using Shouldly; -using Xunit; -using static Rafty.Infrastructure.Wait; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Configuration; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; - -namespace Ocelot.IntegrationTests -{ - public class RaftTests : IDisposable - { - private readonly List _builders; - private readonly List _webHostBuilders; - private readonly List _threads; - private FilePeers _peers; - private readonly HttpClient _httpClient; - private readonly HttpClient _httpClientForAssertions; - private BearerToken _token; - private HttpResponseMessage _response; - private static readonly object _lock = new object(); - - public RaftTests() - { - _httpClientForAssertions = new HttpClient(); - _httpClient = new HttpClient(); - var ocelotBaseUrl = "http://localhost:5000"; - _httpClient.BaseAddress = new Uri(ocelotBaseUrl); - _webHostBuilders = new List(); - _builders = new List(); - _threads = new List(); - } - - public void Dispose() - { - foreach (var builder in _builders) - { - builder?.Dispose(); - } - - foreach (var peer in _peers.Peers) - { - File.Delete(peer.HostAndPort.Replace("/","").Replace(":","")); - File.Delete($"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"); - } - } - - [Fact(Skip = "This tests is flakey at the moment so ignoring will be fixed long term see https://github.com/TomPallister/Ocelot/issues/245")] - public void should_persist_command_to_five_servers() - { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - } - }; - - var updatedConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var command = new UpdateFileConfiguration(updatedConfiguration); - GivenThereIsAConfiguration(configuration); - GivenFiveServersAreRunning(); - GivenALeaderIsElected(); - GivenIHaveAnOcelotToken("/administration"); - WhenISendACommandIntoTheCluster(command); - ThenTheCommandIsReplicatedToAllStateMachines(command); - } - - [Fact(Skip = "This tests is flakey at the moment so ignoring will be fixed long term see https://github.com/TomPallister/Ocelot/issues/245")] - public void should_persist_command_to_five_servers_when_using_administration_api() - { - var configuration = new FileConfiguration - { - }; - - var updatedConfiguration = new FileConfiguration - { - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var command = new UpdateFileConfiguration(updatedConfiguration); - GivenThereIsAConfiguration(configuration); - GivenFiveServersAreRunning(); - GivenALeaderIsElected(); - GivenIHaveAnOcelotToken("/administration"); - GivenIHaveAddedATokenToMyRequest(); - WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); - ThenTheCommandIsReplicatedToAllStateMachines(command); - } - - private void WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) - { - var p = _peers.Peers.First(); - var json = JsonConvert.SerializeObject(command,new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }); - var httpContent = new StringContent(json); - httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - using(var httpClient = new HttpClient()) - { - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - var result = JsonConvert.DeserializeObject>(content); - result.Command.Configuration.ReRoutes.Count.ShouldBe(2); - } - - //dirty sleep to make sure command replicated... - var stopwatch = Stopwatch.StartNew(); - while(stopwatch.ElapsedMilliseconds < 10000) - { - } - } - - private void ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) - { - //dirty sleep to give a chance to replicate... - var stopwatch = Stopwatch.StartNew(); - while(stopwatch.ElapsedMilliseconds < 2000) - { - } - - bool CommandCalledOnAllStateMachines() - { - try - { - var passed = 0; - foreach (var peer in _peers.Peers) - { - var path = $"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"; - using(var connection = new SqliteConnection($"Data Source={path};")) - { - connection.Open(); - var sql = @"select count(id) from logs"; - using(var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(command.ExecuteScalar()); - index.ShouldBe(1); - } - } - - _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var result = _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration").Result; - var json = result.Content.ReadAsStringAsync().Result; - var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All}); - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var res = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.Configuration.ReRoutes[i].DownstreamHostAndPorts[j]; - res.Host.ShouldBe(expected.Host); - res.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamHttpMethod); - } - - passed++; - } - - return passed == 5; - } - catch(Exception e) - { - Console.WriteLine(e); - return false; - } - } - - var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines()); - commandOnAllStateMachines.ShouldBeTrue(); - } - - private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) - { - var json = JsonConvert.SerializeObject(updatedConfiguration); - var content = new StringContent(json); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - _response = _httpClient.PostAsync(url, content).Result; - } - - private void GivenIHaveAddedATokenToMyRequest() - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - private void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - - var response = _httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = _httpClient.GetAsync(configPath).Result; - response.EnsureSuccessStatusCode(); - } - - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); - - configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); - } - - private void GivenAServerIsRunning(string url) - { - lock(_lock) - { - IWebHostBuilder webHostBuilder = new WebHostBuilder(); - webHostBuilder.UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddJsonFile("peers.json", optional: true, reloadOnChange: true); - #pragma warning disable CS0618 - config.AddOcelotBaseUrl(url); - #pragma warning restore CS0618 - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - x.AddSingleton(new NodeId(url)); - x - .AddOcelot() - .AddAdministration("/administration", "secret") - .AddRafty(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - var builder = webHostBuilder.Build(); - builder.Start(); - - _webHostBuilders.Add(webHostBuilder); - _builders.Add(builder); - } - } - - private void GivenFiveServersAreRunning() - { - var bytes = File.ReadAllText("peers.json"); - _peers = JsonConvert.DeserializeObject(bytes); - - foreach (var peer in _peers.Peers) - { - var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort)); - thread.Start(); - _threads.Add(thread); - } - } - - private void GivenALeaderIsElected() - { - //dirty sleep to make sure we have a leader - var stopwatch = Stopwatch.StartNew(); - while(stopwatch.ElapsedMilliseconds < 20000) - { - } - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.Infrastructure; +using Shouldly; +using Xunit; +using static Rafty.Infrastructure.Wait; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +namespace Ocelot.IntegrationTests +{ + public class RaftTests : IDisposable + { + private readonly List _builders; + private readonly List _webHostBuilders; + private readonly List _threads; + private FilePeers _peers; + private readonly HttpClient _httpClient; + private readonly HttpClient _httpClientForAssertions; + private BearerToken _token; + private HttpResponseMessage _response; + private static readonly object _lock = new object(); + + public RaftTests() + { + _httpClientForAssertions = new HttpClient(); + _httpClient = new HttpClient(); + var ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(ocelotBaseUrl); + _webHostBuilders = new List(); + _builders = new List(); + _threads = new List(); + } + + public void Dispose() + { + foreach (var builder in _builders) + { + builder?.Dispose(); + } + + foreach (var peer in _peers.Peers) + { + File.Delete(peer.HostAndPort.Replace("/","").Replace(":","")); + File.Delete($"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"); + } + } + + [Fact] + public void should_persist_command_to_five_servers() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + GivenIHaveAnOcelotToken("/administration"); + WhenISendACommandIntoTheCluster(command); + ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + [Fact] + public void should_persist_command_to_five_servers_when_using_administration_api() + { + var configuration = new FileConfiguration + { + }; + + var updatedConfiguration = new FileConfiguration + { + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + GivenIHaveAnOcelotToken("/administration"); + GivenIHaveAddedATokenToMyRequest(); + WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); + ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + private void WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) + { + bool SendCommand() + { + var p = _peers.Peers.First(); + var json = JsonConvert.SerializeObject(command,new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }); + var httpContent = new StringContent(json); + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + using(var httpClient = new HttpClient()) + { + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + var errorResult = JsonConvert.DeserializeObject>(content); + + if(!string.IsNullOrEmpty(errorResult.Error)) + { + return false; + } + + var okResult = JsonConvert.DeserializeObject>(content); + + if(okResult.Command.Configuration.ReRoutes.Count == 2) + { + return true; + } + } + + return false; + } + + var commandSent = WaitFor(20000).Until(() => SendCommand()); + commandSent.ShouldBeTrue(); + } + + private void ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) + { + bool CommandCalledOnAllStateMachines() + { + try + { + var passed = 0; + foreach (var peer in _peers.Peers) + { + var path = $"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"; + using(var connection = new SqliteConnection($"Data Source={path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using(var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(command.ExecuteScalar()); + index.ShouldBe(1); + } + } + + _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var result = _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration").Result; + var json = result.Content.ReadAsStringAsync().Result; + var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All}); + response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) + { + var res = response.ReRoutes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.Configuration.ReRoutes[i].DownstreamHostAndPorts[j]; + res.Host.ShouldBe(expected.Host); + res.Port.ShouldBe(expected.Port); + } + + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamHttpMethod); + } + + passed++; + } + + return passed == 5; + } + catch(Exception e) + { + Console.WriteLine(e); + return false; + } + } + + var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines()); + commandOnAllStateMachines.ShouldBeTrue(); + } + + private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + bool SendCommand() + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + _response = _httpClient.PostAsync(url, content).Result; + var responseContent = _response.Content.ReadAsStringAsync().Result; + + //Console.ForegroundColor = ConsoleColor.Green; + //Console.WriteLine(responseContent); + //Console.WriteLine(_response.StatusCode); + //Console.ForegroundColor = ConsoleColor.White; + + if(responseContent == "There was a problem. This error message sucks raise an issue in GitHub.") + { + return false; + } + + if(string.IsNullOrEmpty(responseContent)) + { + return false; + } + + return _response.IsSuccessStatusCode; + } + + var commandSent = WaitFor(20000).Until(() => SendCommand()); + commandSent.ShouldBeTrue(); + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + bool AddToken() + { + try + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + if(!response.IsSuccessStatusCode) + { + return false; + } + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + return response.IsSuccessStatusCode; + } + catch(Exception e) + { + return false; + } + } + + var addToken = WaitFor(20000).Until(() => AddToken()); + addToken.ShouldBeTrue(); + + } + + private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void GivenAServerIsRunning(string url) + { + lock(_lock) + { + IWebHostBuilder webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddJsonFile("peers.json", optional: true, reloadOnChange: true); + #pragma warning disable CS0618 + config.AddOcelotBaseUrl(url); + #pragma warning restore CS0618 + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + x.AddSingleton(new NodeId(url)); + x + .AddOcelot() + .AddAdministration("/administration", "secret") + .AddRafty(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + var builder = webHostBuilder.Build(); + builder.Start(); + + _webHostBuilders.Add(webHostBuilder); + _builders.Add(builder); + } + } + + private void GivenFiveServersAreRunning() + { + var bytes = File.ReadAllText("peers.json"); + _peers = JsonConvert.DeserializeObject(bytes); + + foreach (var peer in _peers.Peers) + { + var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort)); + thread.Start(); + _threads.Add(thread); + } + } + } +}