Raft round 2 (#182)

* brought in rafty

* moved raft classes into Ocelot and deleted from int project

* started to set up rafty in Ocelot

* RAFTY INSIDE OCELOT...WOOT

* more work adding rafty...just need to get auth working now

* rudimentary authenticated raft requests working

* asyn await stuff

* hacked rafty into the fileconfigurationcontroller...everything seems to be working roughly but I have a lot of refactoring to do

* updated to latest rafty that doesnt need an id

* hacky but all tests passing

* changed admin area set up to use builder not configuration.json, changed admin area auth to use client credentials

* missing code coverage

* ignore raft sectionf for code coverage

* ignore raft sectionf for code coverage

* back to normal filters

* try exclude attr

* missed these

* moved client secret to builder for authentication and updated docs

* lock to try and fix error accessing identity server created temprsa file on build server

* updated postman scripts and changed Ocelot to not always use type handling as this looked crap when manually accessing the configuration endpoint

* added rafty docs

* changes I missed

* added serialisation code we need for rafty to process commands when they proxy to leader

* moved controllers into their feature slices
This commit is contained in:
Tom Pallister
2018-01-01 18:40:39 +00:00
committed by GitHub
parent 194f76cf7f
commit f082f7318a
50 changed files with 1876 additions and 459 deletions

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace Ocelot.Authentication
{
class BearerToken
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
}

View File

@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using Ocelot.Cache;
using Ocelot.Configuration.Provider;
namespace Ocelot.Controllers
namespace Ocelot.Cache
{
[Authorize]
[Route("outputcache")]

View File

@ -1,53 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Validation;
using Ocelot.Configuration.Provider;
namespace Ocelot.Configuration.Authentication
{
public class OcelotResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly IHashMatcher _matcher;
private readonly IIdentityServerConfiguration _identityServerConfiguration;
public OcelotResourceOwnerPasswordValidator(IHashMatcher matcher, IIdentityServerConfiguration identityServerConfiguration)
{
_identityServerConfiguration = identityServerConfiguration;
_matcher = matcher;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
var user = _identityServerConfiguration.Users.FirstOrDefault(u => u.UserName == context.UserName);
if(user == null)
{
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"invalid custom credential");
}
else if(_matcher.Match(context.Password, user.Salt, user.Hash))
{
context.Result = new GrantValidationResult(
subject: "admin",
authenticationMethod: "custom");
}
else
{
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"invalid custom credential");
}
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
}
}
}

View File

@ -9,6 +9,7 @@ using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Validator;
using Ocelot.DependencyInjection;
using Ocelot.LoadBalancer;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Logging;
@ -35,6 +36,8 @@ namespace Ocelot.Configuration.Creator
private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator;
private readonly IRegionCreator _regionCreator;
private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator;
private readonly IAdministrationPath _adminPath;
public FileOcelotConfigurationCreator(
IOptions<FileConfiguration> options,
@ -49,9 +52,11 @@ namespace Ocelot.Configuration.Creator
IReRouteOptionsCreator fileReRouteOptionsCreator,
IRateLimitOptionsCreator rateLimitOptionsCreator,
IRegionCreator regionCreator,
IHttpHandlerOptionsCreator httpHandlerOptionsCreator
IHttpHandlerOptionsCreator httpHandlerOptionsCreator,
IAdministrationPath adminPath
)
{
_adminPath = adminPath;
_regionCreator = regionCreator;
_rateLimitOptionsCreator = rateLimitOptionsCreator;
_requestIdKeyCreator = requestIdKeyCreator;
@ -92,7 +97,7 @@ namespace Ocelot.Configuration.Creator
var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration);
var config = new OcelotConfiguration(reRoutes, fileConfiguration.GlobalConfiguration.AdministrationPath, serviceProviderConfiguration);
var config = new OcelotConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration);
return new OkResponse<IOcelotConfiguration>(config);
}

View File

@ -8,29 +8,16 @@ namespace Ocelot.Configuration.Creator
{
public static class IdentityServerConfigurationCreator
{
public static IdentityServerConfiguration GetIdentityServerConfiguration()
public static IdentityServerConfiguration GetIdentityServerConfiguration(string secret)
{
var username = Environment.GetEnvironmentVariable("OCELOT_USERNAME");
var hash = Environment.GetEnvironmentVariable("OCELOT_HASH");
var salt = Environment.GetEnvironmentVariable("OCELOT_SALT");
var credentialsSigningCertificateLocation = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE");
var credentialsSigningCertificatePassword = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD");
return new IdentityServerConfiguration(
"admin",
false,
SupportedTokens.Both,
"secret",
secret,
new List<string> { "admin", "openid", "offline_access" },
"Ocelot Administration",
true,
GrantTypes.ResourceOwnerPassword,
AccessTokenType.Jwt,
false,
new List<User>
{
new User("admin", username, hash, salt)
},
credentialsSigningCertificateLocation,
credentialsSigningCertificatePassword
);

View File

@ -12,7 +12,6 @@ namespace Ocelot.Configuration.File
public string RequestIdKey { get; set; }
public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;}
public string AdministrationPath {get;set;}
public FileRateLimitOptions RateLimitOptions { get; set; }
}

View File

@ -1,11 +1,15 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Provider;
using Ocelot.Configuration.Setter;
using Ocelot.Raft;
using Rafty.Concensus;
namespace Ocelot.Controllers
namespace Ocelot.Configuration
{
[Authorize]
[Route("configuration")]
@ -13,11 +17,13 @@ namespace Ocelot.Controllers
{
private readonly IFileConfigurationProvider _configGetter;
private readonly IFileConfigurationSetter _configSetter;
private readonly IServiceProvider _serviceProvider;
public FileConfigurationController(IFileConfigurationProvider getFileConfig, IFileConfigurationSetter configSetter)
public FileConfigurationController(IFileConfigurationProvider getFileConfig, IFileConfigurationSetter configSetter, IServiceProvider serviceProvider)
{
_configGetter = getFileConfig;
_configSetter = configSetter;
_serviceProvider = serviceProvider;
}
[HttpGet]
@ -36,9 +42,23 @@ namespace Ocelot.Controllers
[HttpPost]
public async Task<IActionResult> Post([FromBody]FileConfiguration fileConfiguration)
{
//todo - this code is a bit shit sort it out..
var test = _serviceProvider.GetService(typeof(INode));
if (test != null)
{
var node = (INode)test;
var result = node.Accept(new UpdateFileConfiguration(fileConfiguration));
if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse<UpdateFileConfiguration>))
{
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 _configSetter.Set(fileConfiguration);
if(response.IsError)
if (response.IsError)
{
return new BadRequestObjectResult(response.Errors);
}
@ -46,4 +66,4 @@ namespace Ocelot.Controllers
return new OkObjectResult(fileConfiguration);
}
}
}
}

View File

@ -7,16 +7,9 @@ namespace Ocelot.Configuration.Provider
public interface IIdentityServerConfiguration
{
string ApiName { get; }
string ApiSecret { get; }
bool RequireHttps { get; }
List<string> AllowedScopes { get; }
SupportedTokens SupportedTokens { get; }
string ApiSecret { get; }
string Description {get;}
bool Enabled {get;}
IEnumerable<string> AllowedGrantTypes {get;}
AccessTokenType AccessTokenType {get;}
bool RequireClientSecret {get;}
List<User> Users {get;}
string CredentialsSigningCertificateLocation { get; }
string CredentialsSigningCertificatePassword { get; }
}

View File

@ -9,27 +9,15 @@ namespace Ocelot.Configuration.Provider
public IdentityServerConfiguration(
string apiName,
bool requireHttps,
SupportedTokens supportedTokens,
string apiSecret,
List<string> allowedScopes,
string description,
bool enabled,
IEnumerable<string> grantType,
AccessTokenType accessTokenType,
bool requireClientSecret,
List<User> users, string credentialsSigningCertificateLocation, string credentialsSigningCertificatePassword)
string credentialsSigningCertificateLocation,
string credentialsSigningCertificatePassword)
{
ApiName = apiName;
RequireHttps = requireHttps;
SupportedTokens = supportedTokens;
ApiSecret = apiSecret;
AllowedScopes = allowedScopes;
Description = description;
Enabled = enabled;
AllowedGrantTypes = grantType;
AccessTokenType = accessTokenType;
RequireClientSecret = requireClientSecret;
Users = users;
CredentialsSigningCertificateLocation = credentialsSigningCertificateLocation;
CredentialsSigningCertificatePassword = credentialsSigningCertificatePassword;
}
@ -37,14 +25,7 @@ namespace Ocelot.Configuration.Provider
public string ApiName { get; private set; }
public bool RequireHttps { get; private set; }
public List<string> AllowedScopes { get; private set; }
public SupportedTokens SupportedTokens { get; private set; }
public string ApiSecret { get; private set; }
public string Description {get;private set;}
public bool Enabled {get;private set;}
public IEnumerable<string> AllowedGrantTypes {get;private set;}
public AccessTokenType AccessTokenType {get;private set;}
public bool RequireClientSecret {get;private set;}
public List<User> Users {get;private set;}
public string CredentialsSigningCertificateLocation { get; private set; }
public string CredentialsSigningCertificatePassword { get; private set; }
}

View File

@ -1,17 +0,0 @@
namespace Ocelot.Configuration.Provider
{
public class User
{
public User(string subject, string userName, string hash, string salt)
{
Subject = subject;
UserName = userName;
Hash = hash;
Salt = salt;
}
public string Subject { get; private set; }
public string UserName { get; private set; }
public string Hash { get; private set; }
public string Salt { get; private set; }
}
}

View File

@ -7,5 +7,6 @@ namespace Ocelot.DependencyInjection
{
IOcelotBuilder AddStoreOcelotConfigurationInConsul();
IOcelotBuilder AddCacheManager(Action<ConfigurationBuilderCachePart> settings);
IOcelotAdministrationBuilder AddAdministration(string path, string secret);
}
}

View File

@ -14,7 +14,6 @@ using Ocelot.Configuration.Provider;
using Ocelot.Configuration.Repository;
using Ocelot.Configuration.Setter;
using Ocelot.Configuration.Validator;
using Ocelot.Controllers;
using Ocelot.DownstreamRouteFinder.Finder;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.DownstreamUrlCreator;
@ -47,6 +46,12 @@ using Ocelot.Configuration.Builder;
using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Linq;
using Ocelot.Raft;
using Rafty.Concensus;
using Rafty.FiniteStateMachine;
using Rafty.Infrastructure;
using Rafty.Log;
using Newtonsoft.Json;
namespace Ocelot.DependencyInjection
{
@ -121,14 +126,6 @@ namespace Ocelot.DependencyInjection
_services.AddMemoryCache();
_services.TryAddSingleton<OcelotDiagnosticListener>();
//add identity server for admin area
var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration();
if (identityServerConfiguration != null)
{
AddIdentityServer(identityServerConfiguration);
}
//add asp.net services..
var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly;
@ -141,6 +138,24 @@ namespace Ocelot.DependencyInjection
_services.AddLogging();
_services.AddMiddlewareAnalysis();
_services.AddWebEncoders();
_services.AddSingleton<IAdministrationPath>(new NullAdministrationPath());
}
public IOcelotAdministrationBuilder AddAdministration(string path, string secret)
{
var administrationPath = new AdministrationPath(path);
//add identity server for admin area
var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(secret);
if (identityServerConfiguration != null)
{
AddIdentityServer(identityServerConfiguration, administrationPath);
}
var descriptor = new ServiceDescriptor(typeof(IAdministrationPath), administrationPath);
_services.Replace(descriptor);
return new OcelotAdministrationBuilder(_services, _configurationRoot);
}
public IOcelotBuilder AddStoreOcelotConfigurationInConsul()
@ -185,7 +200,7 @@ namespace Ocelot.DependencyInjection
return this;
}
private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration)
private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath)
{
_services.TryAddSingleton<IIdentityServerConfiguration>(identityServerConfiguration);
_services.TryAddSingleton<IHashMatcher, HashMatcher>();
@ -194,8 +209,7 @@ namespace Ocelot.DependencyInjection
o.IssuerUri = "Ocelot";
})
.AddInMemoryApiResources(Resources(identityServerConfiguration))
.AddInMemoryClients(Client(identityServerConfiguration))
.AddResourceOwnerValidator<OcelotResourceOwnerPasswordValidator>();
.AddInMemoryClients(Client(identityServerConfiguration));
//todo - refactor a method so we know why this is happening
var whb = _services.First(x => x.ServiceType == typeof(IWebHostBuilder));
@ -206,8 +220,7 @@ namespace Ocelot.DependencyInjection
_services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(o =>
{
var adminPath = _configurationRoot.GetValue("GlobalConfiguration:AdministrationPath", string.Empty);
o.Authority = baseSchemeUrlAndPort + adminPath;
o.Authority = baseSchemeUrlAndPort + adminPath.Path;
o.ApiName = identityServerConfiguration.ApiName;
o.RequireHttpsMetadata = identityServerConfiguration.RequireHttps;
o.SupportedTokens = SupportedTokens.Both;
@ -240,7 +253,7 @@ namespace Ocelot.DependencyInjection
Value = identityServerConfiguration.ApiSecret.Sha256()
}
}
}
},
};
}
@ -251,12 +264,65 @@ namespace Ocelot.DependencyInjection
new Client
{
ClientId = identityServerConfiguration.ApiName,
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> {new Secret(identityServerConfiguration.ApiSecret.Sha256())},
AllowedScopes = { identityServerConfiguration.ApiName }
}
};
}
}
public interface IOcelotAdministrationBuilder
{
IOcelotAdministrationBuilder AddRafty();
}
public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder
{
private IServiceCollection _services;
private IConfigurationRoot _configurationRoot;
public OcelotAdministrationBuilder(IServiceCollection services, IConfigurationRoot configurationRoot)
{
_configurationRoot = configurationRoot;
_services = services;
}
public IOcelotAdministrationBuilder AddRafty()
{
var settings = new InMemorySettings(4000, 5000, 100, 5000);
_services.AddSingleton<ILog, SqlLiteLog>();
_services.AddSingleton<IFiniteStateMachine, OcelotFiniteStateMachine>();
_services.AddSingleton<ISettings>(settings);
_services.AddSingleton<IPeersProvider, FilePeersProvider>();
_services.AddSingleton<INode, Node>();
_services.Configure<FilePeers>(_configurationRoot);
return this;
}
}
public interface IAdministrationPath
{
string Path {get;}
}
public class NullAdministrationPath : IAdministrationPath
{
public NullAdministrationPath()
{
Path = null;
}
public string Path {get;private set;}
}
public class AdministrationPath : IAdministrationPath
{
public AdministrationPath(string path)
{
Path = path;
}
public string Path {get;private set;}
}
}

View File

@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection;
using Ocelot.Authentication.Middleware;
using Ocelot.Cache.Middleware;
using Ocelot.Claims.Middleware;
using Ocelot.Controllers;
using Ocelot.DownstreamRouteFinder.Middleware;
using Ocelot.DownstreamUrlCreator.Middleware;
using Ocelot.Errors.Middleware;
@ -23,12 +22,15 @@ using Ocelot.RateLimit.Middleware;
namespace Ocelot.Middleware
{
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Authorisation.Middleware;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Ocelot.Configuration;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File;
@ -36,7 +38,10 @@ namespace Ocelot.Middleware
using Ocelot.Configuration.Repository;
using Ocelot.Configuration.Setter;
using Ocelot.LoadBalancer.Middleware;
using Ocelot.Raft;
using Ocelot.Responses;
using Rafty.Concensus;
using Rafty.Infrastructure;
public static class OcelotMiddlewareExtensions
{
@ -64,6 +69,11 @@ namespace Ocelot.Middleware
await CreateAdministrationArea(builder, configuration);
if(UsingRafty(builder))
{
SetUpRafty(builder);
}
ConfigureDiagnosticListener(builder);
// This is registered to catch any global exceptions that are not handled
@ -149,6 +159,26 @@ namespace Ocelot.Middleware
return builder;
}
private static bool UsingRafty(IApplicationBuilder builder)
{
var possible = builder.ApplicationServices.GetService(typeof(INode)) as INode;
if(possible != null)
{
return true;
}
return false;
}
private static void SetUpRafty(IApplicationBuilder builder)
{
var applicationLifetime = (IApplicationLifetime)builder.ApplicationServices.GetService(typeof(IApplicationLifetime));
applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder));
var node = (INode)builder.ApplicationServices.GetService(typeof(INode));
var nodeId = (NodeId)builder.ApplicationServices.GetService(typeof(NodeId));
node.Start(nodeId.Id);
}
private static async Task<IOcelotConfiguration> CreateConfiguration(IApplicationBuilder builder)
{
var deps = GetDependencies(builder);
@ -183,7 +213,7 @@ namespace Ocelot.Middleware
return response == null || response.IsError;
}
private static bool ConfigurationNotSetUp(Response<IOcelotConfiguration> ocelotConfiguration)
private static bool ConfigurationNotSetUp(Ocelot.Responses.Response<IOcelotConfiguration> ocelotConfiguration)
{
return ocelotConfiguration == null || ocelotConfiguration.Data == null || ocelotConfiguration.IsError;
}
@ -247,6 +277,7 @@ namespace Ocelot.Middleware
return new ErrorResponse(ocelotConfig.Errors);
}
config = await ocelotConfigurationRepository.AddOrReplace(ocelotConfig.Data);
//todo - this starts the poller if it has been registered...please this is so bad.
var hack = builder.ApplicationServices.GetService(typeof(ConsulFileConfigurationPoller));
}
@ -292,5 +323,11 @@ namespace Ocelot.Middleware
diagnosticListener.SubscribeWithAdapter(listener);
}
}
private static void OnShutdown(IApplicationBuilder app)
{
var node = (INode)app.ApplicationServices.GetService(typeof(INode));
node.Stop();
}
}
}

View File

@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
@ -11,39 +10,37 @@
<PackageId>Ocelot</PackageId>
<PackageTags>API Gateway;.NET core</PackageTags>
<PackageProjectUrl>https://github.com/TomPallister/Ocelot</PackageProjectUrl>
<PackageProjectUrl>https://github.com/TomPallister/Ocelot</PackageProjectUrl>
<PackageProjectUrl>https://github.com/TomPallister/Ocelot</PackageProjectUrl>
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<Authors>Tom Pallister</Authors>
<Authors>Tom Pallister</Authors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="7.2.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DiagnosticAdapter" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.0.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.0" />
<PackageReference Include="CacheManager.Core" Version="1.1.1" />
<PackageReference Include="CacheManager.Microsoft.Extensions.Configuration" Version="1.1.1" />
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Consul" Version="0.7.2.3" />
<PackageReference Include="Polly" Version="5.3.1" />
<PackageReference Include="IdentityServer4" Version="2.0.2" />
<PackageReference Include="FluentValidation" Version="7.2.1"/>
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.1.0"/>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.DiagnosticAdapter" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.0.0"/>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.0"/>
<PackageReference Include="CacheManager.Core" Version="1.1.1"/>
<PackageReference Include="CacheManager.Microsoft.Extensions.Configuration" Version="1.1.1"/>
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.1.1"/>
<PackageReference Include="Consul" Version="0.7.2.3"/>
<PackageReference Include="Polly" Version="5.3.1"/>
<PackageReference Include="IdentityServer4" Version="2.0.2"/>
<PackageReference Include="Rafty" Version="0.4.2"/>
</ItemGroup>
</Project>
</Project>

View File

@ -0,0 +1,7 @@
using System;
namespace Ocelot.Raft
{
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method|AttributeTargets.Property)]
public class ExcludeFromCoverageAttribute : Attribute{}
}

View File

@ -0,0 +1,15 @@
using Rafty.FiniteStateMachine;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class FakeCommand : ICommand
{
public FakeCommand(string value)
{
this.Value = value;
}
public string Value { get; private set; }
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.IO;
using Newtonsoft.Json;
using Rafty.FiniteStateMachine;
using Rafty.Infrastructure;
using Rafty.Log;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class FileFsm : IFiniteStateMachine
{
private string _id;
public FileFsm(NodeId nodeId)
{
_id = nodeId.Id.Replace("/","").Replace(":","");
}
public void Handle(LogEntry log)
{
try
{
var json = JsonConvert.SerializeObject(log.CommandData);
File.AppendAllText(_id, json);
}
catch(Exception exception)
{
Console.WriteLine(exception);
}
}
}
}

View File

@ -0,0 +1,8 @@
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class FilePeer
{
public string HostAndPort { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class FilePeers
{
public FilePeers()
{
Peers = new List<FilePeer>();
}
public List<FilePeer> Peers {get; set;}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using Ocelot.Configuration;
using Ocelot.Configuration.Provider;
using Rafty.Concensus;
using Rafty.Infrastructure;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class FilePeersProvider : IPeersProvider
{
private readonly IOptions<FilePeers> _options;
private List<IPeer> _peers;
private IWebHostBuilder _builder;
private IOcelotConfigurationProvider _provider;
private IIdentityServerConfiguration _identityServerConfig;
public FilePeersProvider(IOptions<FilePeers> options, IWebHostBuilder builder, IOcelotConfigurationProvider provider, IIdentityServerConfiguration identityServerConfig)
{
_identityServerConfig = identityServerConfig;
_provider = provider;
_builder = builder;
_options = options;
_peers = new List<IPeer>();
//todo - sort out async nonsense..
var config = _provider.Get().GetAwaiter().GetResult();
foreach (var item in _options.Value.Peers)
{
var httpClient = new HttpClient();
//todo what if this errors?
var httpPeer = new HttpPeer(item.HostAndPort, httpClient, _builder, config.Data, _identityServerConfig);
_peers.Add(httpPeer);
}
}
public List<IPeer> Get()
{
return _peers;
}
}
}

128
src/Ocelot/Raft/HttpPeer.cs Normal file
View File

@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json;
using Ocelot.Authentication;
using Ocelot.Configuration;
using Ocelot.Configuration.Provider;
using Rafty.Concensus;
using Rafty.FiniteStateMachine;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class HttpPeer : IPeer
{
private string _hostAndPort;
private HttpClient _httpClient;
private JsonSerializerSettings _jsonSerializerSettings;
private string _baseSchemeUrlAndPort;
private BearerToken _token;
private IOcelotConfiguration _config;
private IIdentityServerConfiguration _identityServerConfiguration;
public HttpPeer(string hostAndPort, HttpClient httpClient, IWebHostBuilder builder, IOcelotConfiguration config, IIdentityServerConfiguration identityServerConfiguration)
{
_identityServerConfiguration = identityServerConfiguration;
_config = config;
Id = hostAndPort;
_hostAndPort = hostAndPort;
_httpClient = httpClient;
_jsonSerializerSettings = new JsonSerializerSettings() {
TypeNameHandling = TypeNameHandling.All
};
_baseSchemeUrlAndPort = builder.GetSetting(WebHostDefaults.ServerUrlsKey);
}
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<RequestVoteResponse>(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<AppendEntriesResponse>(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<T> Request<T>(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<OkResponse<T>>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings);
}
else
{
return new ErrorResponse<T>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), command);
}
}
private void SetToken()
{
var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token";
var formData = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("client_id", _identityServerConfiguration.ApiName),
new KeyValuePair<string, string>("client_secret", _identityServerConfiguration.ApiSecret),
new KeyValuePair<string, string>("scope", _identityServerConfiguration.ApiName),
new KeyValuePair<string, string>("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<BearerToken>(responseContent);
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken);
}
}
}

View File

@ -0,0 +1,25 @@
using Ocelot.Configuration.Setter;
using Rafty.FiniteStateMachine;
using Rafty.Log;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class OcelotFiniteStateMachine : IFiniteStateMachine
{
private IFileConfigurationSetter _setter;
public OcelotFiniteStateMachine(IFileConfigurationSetter setter)
{
_setter = setter;
}
public void Handle(LogEntry log)
{
//todo - handle an error
//hack it to just cast as at the moment we know this is the only command :P
var hack = (UpdateFileConfiguration)log.CommandData;
_setter.Set(hack.Configuration).GetAwaiter().GetResult();;
}
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Ocelot.Logging;
using Ocelot.Raft;
using Rafty.Concensus;
using Rafty.FiniteStateMachine;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
[Authorize]
[Route("raft")]
public class RaftController : Controller
{
private readonly INode _node;
private IOcelotLogger _logger;
private string _baseSchemeUrlAndPort;
private JsonSerializerSettings _jsonSerialiserSettings;
public RaftController(INode node, IOcelotLoggerFactory loggerFactory, IWebHostBuilder builder)
{
_jsonSerialiserSettings = new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.All
};
_baseSchemeUrlAndPort = builder.GetSetting(WebHostDefaults.ServerUrlsKey);
_logger = loggerFactory.CreateLogger<RaftController>();
_node = node;
}
[Route("appendentries")]
public async Task<IActionResult> AppendEntries()
{
using(var reader = new StreamReader(HttpContext.Request.Body))
{
var json = await reader.ReadToEndAsync();
var appendEntries = JsonConvert.DeserializeObject<AppendEntries>(json, _jsonSerialiserSettings);
_logger.LogDebug($"{_baseSchemeUrlAndPort}/appendentries called, my state is {_node.State.GetType().FullName}");
var appendEntriesResponse = _node.Handle(appendEntries);
return new OkObjectResult(appendEntriesResponse);
}
}
[Route("requestvote")]
public async Task<IActionResult> RequestVote()
{
using(var reader = new StreamReader(HttpContext.Request.Body))
{
var json = await reader.ReadToEndAsync();
var requestVote = JsonConvert.DeserializeObject<RequestVote>(json, _jsonSerialiserSettings);
_logger.LogDebug($"{_baseSchemeUrlAndPort}/requestvote called, my state is {_node.State.GetType().FullName}");
var requestVoteResponse = _node.Handle(requestVote);
return new OkObjectResult(requestVoteResponse);
}
}
[Route("command")]
public async Task<IActionResult> Command()
{
try
{
using(var reader = new StreamReader(HttpContext.Request.Body))
{
var json = await reader.ReadToEndAsync();
var command = JsonConvert.DeserializeObject<ICommand>(json, _jsonSerialiserSettings);
_logger.LogDebug($"{_baseSchemeUrlAndPort}/command called, my state is {_node.State.GetType().FullName}");
var commandResponse = _node.Accept(command);
json = JsonConvert.SerializeObject(commandResponse, _jsonSerialiserSettings);
return StatusCode(200, json);
}
}
catch(Exception e)
{
_logger.LogError($"THERE WAS A PROBLEM ON NODE {_node.State.CurrentState.Id}", e);
throw e;
}
}
}
}

View File

@ -0,0 +1,279 @@
using System.IO;
using Rafty.Log;
using Microsoft.Data.Sqlite;
using Newtonsoft.Json;
using System;
using Rafty.Infrastructure;
using System.Collections.Generic;
namespace Ocelot.Raft
{
[ExcludeFromCoverage]
public class SqlLiteLog : ILog
{
private string _path;
private readonly object _lock = new object();
public SqlLiteLog(NodeId nodeId)
{
_path = $"{nodeId.Id.Replace("/","").Replace(":","")}.db";
if(!File.Exists(_path))
{
lock(_lock)
{
FileStream fs = File.Create(_path);
fs.Dispose();
}
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
var sql = @"create table logs (
id integer primary key,
data text not null
)";
using(var command = new SqliteCommand(sql, connection))
{
var result = command.ExecuteNonQuery();
}
}
}
}
public int LastLogIndex
{
get
{
lock(_lock)
{
var result = 1;
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
var sql = @"select id from logs order by id desc limit 1";
using(var command = new SqliteCommand(sql, connection))
{
var index = Convert.ToInt32(command.ExecuteScalar());
if(index > result)
{
result = index;
}
}
}
return result;
}
}
}
public long LastLogTerm
{
get
{
lock(_lock)
{
long result = 0;
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
var sql = @"select data from logs order by id desc limit 1";
using(var command = new SqliteCommand(sql, connection))
{
var data = Convert.ToString(command.ExecuteScalar());
var jsonSerializerSettings = new JsonSerializerSettings() {
TypeNameHandling = TypeNameHandling.All
};
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
if(log != null && log.Term > result)
{
result = log.Term;
}
}
}
return result;
}
}
}
public int Count
{
get
{
lock(_lock)
{
var result = 0;
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());
if(index > result)
{
result = index;
}
}
}
return result;
}
}
}
public int Apply(LogEntry log)
{
lock(_lock)
{
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
var jsonSerializerSettings = new JsonSerializerSettings() {
TypeNameHandling = TypeNameHandling.All
};
var data = JsonConvert.SerializeObject(log, jsonSerializerSettings);
//todo - sql injection dont copy this..
var sql = $"insert into logs (data) values ('{data}')";
using(var command = new SqliteCommand(sql, connection))
{
var result = command.ExecuteNonQuery();
}
sql = "select last_insert_rowid()";
using(var command = new SqliteCommand(sql, connection))
{
var result = command.ExecuteScalar();
return Convert.ToInt32(result);
}
}
}
}
public void DeleteConflictsFromThisLog(int index, LogEntry logEntry)
{
lock(_lock)
{
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
//todo - sql injection dont copy this..
var sql = $"select data from logs where id = {index};";
using(var command = new SqliteCommand(sql, connection))
{
var data = Convert.ToString(command.ExecuteScalar());
var jsonSerializerSettings = new JsonSerializerSettings() {
TypeNameHandling = TypeNameHandling.All
};
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
if(logEntry != null && log != null && logEntry.Term != log.Term)
{
//todo - sql injection dont copy this..
var deleteSql = $"delete from logs where id >= {index};";
using(var deleteCommand = new SqliteCommand(deleteSql, connection))
{
var result = deleteCommand.ExecuteNonQuery();
}
}
}
}
}
}
public LogEntry Get(int index)
{
lock(_lock)
{
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
//todo - sql injection dont copy this..
var sql = $"select data from logs where id = {index}";
using(var command = new SqliteCommand(sql, connection))
{
var data = Convert.ToString(command.ExecuteScalar());
var jsonSerializerSettings = new JsonSerializerSettings() {
TypeNameHandling = TypeNameHandling.All
};
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
return log;
}
}
}
}
public System.Collections.Generic.List<(int index, LogEntry logEntry)> GetFrom(int index)
{
lock(_lock)
{
var logsToReturn = new List<(int, LogEntry)>();
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
//todo - sql injection dont copy this..
var sql = $"select id, data from logs where id >= {index}";
using(var command = new SqliteCommand(sql, connection))
{
using(var reader = command.ExecuteReader())
{
while(reader.Read())
{
var id = Convert.ToInt32(reader[0]);
var data = (string)reader[1];
var jsonSerializerSettings = new JsonSerializerSettings() {
TypeNameHandling = TypeNameHandling.All
};
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
logsToReturn.Add((id, log));
}
}
}
}
return logsToReturn;
}
}
public long GetTermAtIndex(int index)
{
lock(_lock)
{
long result = 0;
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
//todo - sql injection dont copy this..
var sql = $"select data from logs where id = {index}";
using(var command = new SqliteCommand(sql, connection))
{
var data = Convert.ToString(command.ExecuteScalar());
var jsonSerializerSettings = new JsonSerializerSettings() {
TypeNameHandling = TypeNameHandling.All
};
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
if(log != null && log.Term > result)
{
result = log.Term;
}
}
}
return result;
}
}
public void Remove(int indexOfCommand)
{
lock(_lock)
{
using(var connection = new SqliteConnection($"Data Source={_path};"))
{
connection.Open();
//todo - sql injection dont copy this..
var deleteSql = $"delete from logs where id >= {indexOfCommand};";
using(var deleteCommand = new SqliteCommand(deleteSql, connection))
{
var result = deleteCommand.ExecuteNonQuery();
}
}
}
}
}
}

View File

@ -0,0 +1,15 @@
using Ocelot.Configuration.File;
using Rafty.FiniteStateMachine;
namespace Ocelot.Raft
{
public class UpdateFileConfiguration : ICommand
{
public UpdateFileConfiguration(FileConfiguration configuration)
{
Configuration = configuration;
}
public FileConfiguration Configuration {get;private set;}
}
}