mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 06:22:50 +08:00
Merge branch 'master' into pitming-feature/MethodTransformer
This commit is contained in:
commit
d879721978
@ -1,10 +1,10 @@
|
|||||||
[<img src="https://threemammals.com/ocelot_logo.png">](https://threemammals.com/ocelot)
|
[<img src="https://threemammals.com/images/ocelot_logo.png">](https://threemammals.com/ocelot)
|
||||||
|
|
||||||
[](https://circleci.com/gh/ThreeMammals/Ocelot/tree/master)
|
[](https://circleci.com/gh/ThreeMammals/Ocelot/tree/master)
|
||||||
|
|
||||||
[](https://coveralls.io/github/ThreeMammals/Ocelot?branch=master)
|
[](https://coveralls.io/github/ThreeMammals/Ocelot?branch=master)
|
||||||
|
|
||||||
[Slack](threemammals.slack.com)
|
[Slack](https://threemammals.slack.com)
|
||||||
|
|
||||||
# Ocelot
|
# Ocelot
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ Here is an example ReRoute configuration, You don't need to set all of these thi
|
|||||||
"UpstreamHttpMethod": [
|
"UpstreamHttpMethod": [
|
||||||
"Get"
|
"Get"
|
||||||
],
|
],
|
||||||
"DownstreamHttpMethod": "".
|
"DownstreamHttpMethod": "",
|
||||||
"AddHeadersToRequest": {},
|
"AddHeadersToRequest": {},
|
||||||
"AddClaimsToRequest": {},
|
"AddClaimsToRequest": {},
|
||||||
"RouteClaimsRequirement": {},
|
"RouteClaimsRequirement": {},
|
||||||
@ -229,3 +229,53 @@ MaxConnectionsPerServer
|
|||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
This controls how many connections the internal HttpClient will open. This can be set at ReRoute or global level.
|
This controls how many connections the internal HttpClient will open. This can be set at ReRoute or global level.
|
||||||
|
|
||||||
|
React to Configuration Changes
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Resolve IOcelotConfigurationChangeTokenSource from the DI container if you wish to react to changes to the Ocelot configuration via the Ocelot.Administration API or ocelot.json being reloaded from the disk. You may either poll the change token's HasChanged property, or register a callback with the RegisterChangeCallback method.
|
||||||
|
|
||||||
|
Polling the HasChanged property
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. code-block:: csharp
|
||||||
|
public class ConfigurationNotifyingService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IOcelotConfigurationChangeTokenSource _tokenSource;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
public ConfigurationNotifyingService(IOcelotConfigurationChangeTokenSource tokenSource, ILogger logger)
|
||||||
|
{
|
||||||
|
_tokenSource = tokenSource;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (_tokenSource.ChangeToken.HasChanged)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Configuration updated");
|
||||||
|
}
|
||||||
|
await Task.Delay(1000, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Registering a callback
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. code-block:: csharp
|
||||||
|
public class MyDependencyInjectedClass : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IOcelotConfigurationChangeTokenSource _tokenSource;
|
||||||
|
private readonly IDisposable _callbackHolder;
|
||||||
|
public MyClass(IOcelotConfigurationChangeTokenSource tokenSource)
|
||||||
|
{
|
||||||
|
_tokenSource = tokenSource;
|
||||||
|
_callbackHolder = tokenSource.ChangeToken.RegisterChangeCallback(_ => Console.WriteLine("Configuration changed"), null);
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_callbackHolder.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ Then add the following to your ConfigureServices method.
|
|||||||
s.AddOcelot()
|
s.AddOcelot()
|
||||||
.AddKubernetes();
|
.AddKubernetes();
|
||||||
|
|
||||||
If you have services deployed in kubernetes you will normally use the naming service to access them. Default usePodServiceAccount = True, which means that ServiceAccount using Pod to access the service of the k8s cluster needs to be ServiceAccount based on RABC authorization
|
If you have services deployed in kubernetes you will normally use the naming service to access them. Default usePodServiceAccount = True, which means that ServiceAccount using Pod to access the service of the k8s cluster needs to be ServiceAccount based on RBAC authorization
|
||||||
|
|
||||||
.. code-block::csharp
|
.. code-block::csharp
|
||||||
public static class OcelotBuilderExtensions
|
public static class OcelotBuilderExtensions
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@ -6,8 +6,8 @@
|
|||||||
<Folder Include="wwwroot\" />
|
<Folder Include="wwwroot\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.App"/>
|
<PackageReference Include="Ocelot" Version="14.0.3" />
|
||||||
<PackageReference Include="Ocelot" Version="12.0.1"/>
|
<PackageReference Include="Ocelot.Administration" Version="14.0.3" />
|
||||||
<PackageReference Include="Ocelot.Administration" Version="0.1.0"/>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
@ -1,15 +1,10 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Ocelot.Administration;
|
||||||
using Ocelot.DependencyInjection;
|
using Ocelot.DependencyInjection;
|
||||||
using Ocelot.Middleware;
|
using Ocelot.Middleware;
|
||||||
using Ocelot.Administration;
|
using System.IO;
|
||||||
|
|
||||||
namespace AdministrationApi
|
namespace AdministrationApi
|
||||||
{
|
{
|
||||||
|
@ -27,7 +27,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseMvc();
|
app.UseRouting();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapDefaultControllerRoute();
|
||||||
|
endpoints.MapControllers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace Ocelot.Configuration.ChangeTracking
|
||||||
|
{
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="IChangeToken" /> source which is activated when Ocelot's configuration is changed.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOcelotConfigurationChangeTokenSource
|
||||||
|
{
|
||||||
|
IChangeToken ChangeToken { get; }
|
||||||
|
|
||||||
|
void Activate();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
namespace Ocelot.Configuration.ChangeTracking
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
public class OcelotConfigurationChangeToken : IChangeToken
|
||||||
|
{
|
||||||
|
public const double PollingIntervalSeconds = 1;
|
||||||
|
|
||||||
|
private readonly ICollection<CallbackWrapper> _callbacks = new List<CallbackWrapper>();
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
private DateTime? _timeChanged;
|
||||||
|
|
||||||
|
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var wrapper = new CallbackWrapper(callback, state, _callbacks, _lock);
|
||||||
|
_callbacks.Add(wrapper);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Activate()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_timeChanged = DateTime.UtcNow;
|
||||||
|
foreach (var wrapper in _callbacks)
|
||||||
|
{
|
||||||
|
wrapper.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token stays active for PollingIntervalSeconds after a change (could be parameterised) - otherwise HasChanged would be true forever.
|
||||||
|
// Taking suggestions for better ways to reset HasChanged back to false.
|
||||||
|
public bool HasChanged => _timeChanged.HasValue && (DateTime.UtcNow - _timeChanged.Value).TotalSeconds < PollingIntervalSeconds;
|
||||||
|
|
||||||
|
public bool ActiveChangeCallbacks => true;
|
||||||
|
|
||||||
|
private class CallbackWrapper : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ICollection<CallbackWrapper> _callbacks;
|
||||||
|
private readonly object _lock;
|
||||||
|
|
||||||
|
public CallbackWrapper(Action<object> callback, object state, ICollection<CallbackWrapper> callbacks, object @lock)
|
||||||
|
{
|
||||||
|
_callbacks = callbacks;
|
||||||
|
_lock = @lock;
|
||||||
|
Callback = callback;
|
||||||
|
State = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invoke()
|
||||||
|
{
|
||||||
|
Callback.Invoke(State);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_callbacks.Remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<object> Callback { get; }
|
||||||
|
|
||||||
|
public object State { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
namespace Ocelot.Configuration.ChangeTracking
|
||||||
|
{
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
public class OcelotConfigurationChangeTokenSource : IOcelotConfigurationChangeTokenSource
|
||||||
|
{
|
||||||
|
private readonly OcelotConfigurationChangeToken _changeToken = new OcelotConfigurationChangeToken();
|
||||||
|
|
||||||
|
public IChangeToken ChangeToken => _changeToken;
|
||||||
|
|
||||||
|
public void Activate()
|
||||||
|
{
|
||||||
|
_changeToken.Activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
namespace Ocelot.Configuration.ChangeTracking
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Ocelot.Configuration.Repository;
|
||||||
|
|
||||||
|
public class OcelotConfigurationMonitor : IOptionsMonitor<IInternalConfiguration>
|
||||||
|
{
|
||||||
|
private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;
|
||||||
|
private readonly IInternalConfigurationRepository _repo;
|
||||||
|
|
||||||
|
public OcelotConfigurationMonitor(IInternalConfigurationRepository repo, IOcelotConfigurationChangeTokenSource changeTokenSource)
|
||||||
|
{
|
||||||
|
_changeTokenSource = changeTokenSource;
|
||||||
|
_repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IInternalConfiguration Get(string name)
|
||||||
|
{
|
||||||
|
return _repo.Get().Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable OnChange(Action<IInternalConfiguration, string> listener)
|
||||||
|
{
|
||||||
|
return _changeTokenSource.ChangeToken.RegisterChangeCallback(_ => listener(CurrentValue, ""), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IInternalConfiguration CurrentValue => _repo.Get().Data;
|
||||||
|
}
|
||||||
|
}
|
@ -4,18 +4,21 @@ using Ocelot.Configuration.File;
|
|||||||
using Ocelot.Responses;
|
using Ocelot.Responses;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
|
|
||||||
namespace Ocelot.Configuration.Repository
|
namespace Ocelot.Configuration.Repository
|
||||||
{
|
{
|
||||||
public class DiskFileConfigurationRepository : IFileConfigurationRepository
|
public class DiskFileConfigurationRepository : IFileConfigurationRepository
|
||||||
{
|
{
|
||||||
|
private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;
|
||||||
private readonly string _environmentFilePath;
|
private readonly string _environmentFilePath;
|
||||||
private readonly string _ocelotFilePath;
|
private readonly string _ocelotFilePath;
|
||||||
private static readonly object _lock = new object();
|
private static readonly object _lock = new object();
|
||||||
private const string ConfigurationFileName = "ocelot";
|
private const string ConfigurationFileName = "ocelot";
|
||||||
|
|
||||||
public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment)
|
public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource)
|
||||||
{
|
{
|
||||||
|
_changeTokenSource = changeTokenSource;
|
||||||
_environmentFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json";
|
_environmentFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json";
|
||||||
|
|
||||||
_ocelotFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}.json";
|
_ocelotFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}.json";
|
||||||
@ -56,6 +59,7 @@ namespace Ocelot.Configuration.Repository
|
|||||||
System.IO.File.WriteAllText(_ocelotFilePath, jsonConfiguration);
|
System.IO.File.WriteAllText(_ocelotFilePath, jsonConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_changeTokenSource.Activate();
|
||||||
return Task.FromResult<Response>(new OkResponse());
|
return Task.FromResult<Response>(new OkResponse());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Ocelot.Responses;
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
|
using Ocelot.Responses;
|
||||||
|
|
||||||
namespace Ocelot.Configuration.Repository
|
namespace Ocelot.Configuration.Repository
|
||||||
{
|
{
|
||||||
@ -10,6 +11,12 @@ namespace Ocelot.Configuration.Repository
|
|||||||
private static readonly object LockObject = new object();
|
private static readonly object LockObject = new object();
|
||||||
|
|
||||||
private IInternalConfiguration _internalConfiguration;
|
private IInternalConfiguration _internalConfiguration;
|
||||||
|
private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;
|
||||||
|
|
||||||
|
public InMemoryInternalConfigurationRepository(IOcelotConfigurationChangeTokenSource changeTokenSource)
|
||||||
|
{
|
||||||
|
_changeTokenSource = changeTokenSource;
|
||||||
|
}
|
||||||
|
|
||||||
public Response<IInternalConfiguration> Get()
|
public Response<IInternalConfiguration> Get()
|
||||||
{
|
{
|
||||||
@ -23,6 +30,7 @@ namespace Ocelot.Configuration.Repository
|
|||||||
_internalConfiguration = internalConfiguration;
|
_internalConfiguration = internalConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_changeTokenSource.Activate();
|
||||||
return new OkResponse();
|
return new OkResponse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
|
|
||||||
namespace Ocelot.DependencyInjection
|
namespace Ocelot.DependencyInjection
|
||||||
{
|
{
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Ocelot.Authorisation;
|
using Ocelot.Authorisation;
|
||||||
using Ocelot.Cache;
|
using Ocelot.Cache;
|
||||||
using Ocelot.Claims;
|
using Ocelot.Claims;
|
||||||
@ -112,6 +115,8 @@ namespace Ocelot.DependencyInjection
|
|||||||
Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();
|
Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();
|
||||||
Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>();
|
Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>();
|
||||||
Services.TryAddSingleton<ICacheKeyGenerator, CacheKeyGenerator>();
|
Services.TryAddSingleton<ICacheKeyGenerator, CacheKeyGenerator>();
|
||||||
|
Services.TryAddSingleton<IOcelotConfigurationChangeTokenSource, OcelotConfigurationChangeTokenSource>();
|
||||||
|
Services.TryAddSingleton<IOptionsMonitor<IInternalConfiguration>, OcelotConfigurationMonitor>();
|
||||||
|
|
||||||
// see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
|
// see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
|
||||||
// could maybe use a scoped data repository
|
// could maybe use a scoped data repository
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Ocelot.Configuration.File;
|
using Ocelot.Configuration.File;
|
||||||
using System;
|
using System;
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
using TestStack.BDDfy;
|
using TestStack.BDDfy;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -54,6 +55,33 @@ namespace Ocelot.AcceptanceTests
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_trigger_change_token_on_change()
|
||||||
|
{
|
||||||
|
this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningReloadingConfig(true))
|
||||||
|
.And(x => _steps.GivenIHaveAChangeToken())
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(_anotherConfig))
|
||||||
|
.And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken))
|
||||||
|
.Then(x => _steps.TheChangeTokenShouldBeActive(true))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_not_trigger_change_token_with_no_change()
|
||||||
|
{
|
||||||
|
this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningReloadingConfig(false))
|
||||||
|
.And(x => _steps.GivenIHaveAChangeToken())
|
||||||
|
.And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken)) // Wait for prior activation to expire.
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(_anotherConfig))
|
||||||
|
.And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken))
|
||||||
|
.Then(x => _steps.TheChangeTokenShouldBeActive(false))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int MillisecondsToWaitForChangeToken = (int) (OcelotConfigurationChangeToken.PollingIntervalSeconds*1000) - 100;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_steps.Dispose();
|
_steps.Dispose();
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Ocelot.AcceptanceTests
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
|
|
||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
{
|
{
|
||||||
using Caching;
|
using Caching;
|
||||||
using Configuration.Repository;
|
using Configuration.Repository;
|
||||||
@ -54,6 +56,7 @@
|
|||||||
private IWebHostBuilder _webHostBuilder;
|
private IWebHostBuilder _webHostBuilder;
|
||||||
private WebHostBuilder _ocelotBuilder;
|
private WebHostBuilder _ocelotBuilder;
|
||||||
private IWebHost _ocelotHost;
|
private IWebHost _ocelotHost;
|
||||||
|
private IOcelotConfigurationChangeTokenSource _changeToken;
|
||||||
|
|
||||||
public Steps()
|
public Steps()
|
||||||
{
|
{
|
||||||
@ -216,6 +219,11 @@
|
|||||||
_ocelotClient = _ocelotServer.CreateClient();
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void GivenIHaveAChangeToken()
|
||||||
|
{
|
||||||
|
_changeToken = _ocelotServer.Host.Services.GetRequiredService<IOcelotConfigurationChangeTokenSource>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step.
|
/// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1135,6 +1143,11 @@
|
|||||||
_ocelotClient = _ocelotServer.CreateClient();
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TheChangeTokenShouldBeActive(bool itShouldBeActive)
|
||||||
|
{
|
||||||
|
_changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive);
|
||||||
|
}
|
||||||
|
|
||||||
public void GivenOcelotIsRunningWithLogger()
|
public void GivenOcelotIsRunningWithLogger()
|
||||||
{
|
{
|
||||||
_webHostBuilder = new WebHostBuilder();
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
@ -21,6 +21,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using TestStack.BDDfy;
|
using TestStack.BDDfy;
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Ocelot.IntegrationTests
|
namespace Ocelot.IntegrationTests
|
||||||
@ -278,6 +279,51 @@ namespace Ocelot.IntegrationTests
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_activate_change_token_when_configuration_is_updated()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration(),
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIPostOnTheApiGateway("/administration/configuration", configuration))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => TheChangeTokenShouldBeActive())
|
||||||
|
.And(x => ThenTheResponseShouldBe(configuration))
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.And(x => ThenTheResponseShouldBe(configuration))
|
||||||
|
.And(_ => ThenTheConfigurationIsSavedCorrectly(configuration))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TheChangeTokenShouldBeActive()
|
||||||
|
{
|
||||||
|
_builder.Services.GetRequiredService<IOcelotConfigurationChangeTokenSource>().ChangeToken.HasChanged.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
private void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expected)
|
private void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expected)
|
||||||
{
|
{
|
||||||
var ocelotJsonPath = $"{AppContext.BaseDirectory}ocelot.json";
|
var ocelotJsonPath = $"{AppContext.BaseDirectory}ocelot.json";
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
namespace Ocelot.UnitTests.Configuration.ChangeTracking
|
||||||
|
{
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class OcelotConfigurationChangeTokenSourceTests
|
||||||
|
{
|
||||||
|
private readonly IOcelotConfigurationChangeTokenSource _source;
|
||||||
|
|
||||||
|
public OcelotConfigurationChangeTokenSourceTests()
|
||||||
|
{
|
||||||
|
_source = new OcelotConfigurationChangeTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_activate_change_token()
|
||||||
|
{
|
||||||
|
this.Given(_ => GivenIActivateTheChangeTokenSource())
|
||||||
|
.Then(_ => ThenTheChangeTokenShouldBeActivated())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIActivateTheChangeTokenSource()
|
||||||
|
{
|
||||||
|
_source.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheChangeTokenShouldBeActivated()
|
||||||
|
{
|
||||||
|
_source.ChangeToken.HasChanged.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.UnitTests.Configuration.ChangeTracking
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Shouldly;
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
|
||||||
|
public class OcelotConfigurationChangeTokenTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void should_call_callback_with_state()
|
||||||
|
{
|
||||||
|
this.Given(_ => GivenIHaveAChangeToken())
|
||||||
|
.And(_ => AndIRegisterACallback())
|
||||||
|
.Then(_ => ThenIShouldGetADisposableWrapper())
|
||||||
|
.Given(_ => GivenIActivateTheToken())
|
||||||
|
.Then(_ => ThenTheCallbackShouldBeCalled())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_not_call_callback_if_it_is_disposed()
|
||||||
|
{
|
||||||
|
this.Given(_ => GivenIHaveAChangeToken())
|
||||||
|
.And(_ => AndIRegisterACallback())
|
||||||
|
.Then(_ => ThenIShouldGetADisposableWrapper())
|
||||||
|
.And(_ => GivenIActivateTheToken())
|
||||||
|
.And(_ => AndIDisposeTheCallbackWrapper())
|
||||||
|
.And(_ => GivenIActivateTheToken())
|
||||||
|
.Then(_ => ThenTheCallbackShouldNotBeCalled())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private OcelotConfigurationChangeToken _changeToken;
|
||||||
|
private IDisposable _callbackWrapper;
|
||||||
|
private int _callbackCounter;
|
||||||
|
private readonly object _callbackInitialState = new object();
|
||||||
|
private object _callbackState;
|
||||||
|
|
||||||
|
private void Callback(object state)
|
||||||
|
{
|
||||||
|
_callbackCounter++;
|
||||||
|
_callbackState = state;
|
||||||
|
_changeToken.HasChanged.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAChangeToken()
|
||||||
|
{
|
||||||
|
_changeToken = new OcelotConfigurationChangeToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AndIRegisterACallback()
|
||||||
|
{
|
||||||
|
_callbackWrapper = _changeToken.RegisterChangeCallback(Callback, _callbackInitialState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenIShouldGetADisposableWrapper()
|
||||||
|
{
|
||||||
|
_callbackWrapper.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIActivateTheToken()
|
||||||
|
{
|
||||||
|
_callbackCounter = 0;
|
||||||
|
_callbackState = null;
|
||||||
|
_changeToken.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheCallbackShouldBeCalled()
|
||||||
|
{
|
||||||
|
_callbackCounter.ShouldBe(1);
|
||||||
|
_callbackState.ShouldNotBeNull();
|
||||||
|
_callbackState.ShouldBeSameAs(_callbackInitialState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheCallbackShouldNotBeCalled()
|
||||||
|
{
|
||||||
|
_callbackCounter.ShouldBe(0);
|
||||||
|
_callbackState.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AndIDisposeTheCallbackWrapper()
|
||||||
|
{
|
||||||
|
_callbackState = null;
|
||||||
|
_callbackCounter = 0;
|
||||||
|
_callbackWrapper.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
using Ocelot.Configuration.File;
|
using Ocelot.Configuration.File;
|
||||||
using Ocelot.Configuration.Repository;
|
using Ocelot.Configuration.Repository;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
@ -16,6 +17,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
public class DiskFileConfigurationRepositoryTests : IDisposable
|
public class DiskFileConfigurationRepositoryTests : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Mock<IWebHostEnvironment> _hostingEnvironment;
|
private readonly Mock<IWebHostEnvironment> _hostingEnvironment;
|
||||||
|
private readonly Mock<IOcelotConfigurationChangeTokenSource> _changeTokenSource;
|
||||||
private IFileConfigurationRepository _repo;
|
private IFileConfigurationRepository _repo;
|
||||||
private string _environmentSpecificPath;
|
private string _environmentSpecificPath;
|
||||||
private string _ocelotJsonPath;
|
private string _ocelotJsonPath;
|
||||||
@ -35,7 +37,9 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
_semaphore.Wait();
|
_semaphore.Wait();
|
||||||
_hostingEnvironment = new Mock<IWebHostEnvironment>();
|
_hostingEnvironment = new Mock<IWebHostEnvironment>();
|
||||||
_hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName);
|
_hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName);
|
||||||
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object);
|
_changeTokenSource = new Mock<IOcelotConfigurationChangeTokenSource>(MockBehavior.Strict);
|
||||||
|
_changeTokenSource.Setup(m => m.Activate());
|
||||||
|
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -70,6 +74,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
.When(_ => WhenISetTheConfiguration())
|
.When(_ => WhenISetTheConfiguration())
|
||||||
.Then(_ => ThenTheConfigurationIsStoredAs(config))
|
.Then(_ => ThenTheConfigurationIsStoredAs(config))
|
||||||
.And(_ => ThenTheConfigurationJsonIsIndented(config))
|
.And(_ => ThenTheConfigurationJsonIsIndented(config))
|
||||||
|
.And(x => AndTheChangeTokenIsActivated())
|
||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +122,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
{
|
{
|
||||||
_environmentName = null;
|
_environmentName = null;
|
||||||
_hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName);
|
_hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName);
|
||||||
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object);
|
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration)
|
private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration)
|
||||||
@ -210,6 +215,11 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AndTheChangeTokenIsActivated()
|
||||||
|
{
|
||||||
|
_changeTokenSource.Verify(m => m.Activate(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
private FileConfiguration FakeFileConfigurationForSet()
|
private FileConfiguration FakeFileConfigurationForSet()
|
||||||
{
|
{
|
||||||
var reRoutes = new List<FileReRoute>
|
var reRoutes = new List<FileReRoute>
|
||||||
@ -222,11 +232,11 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
{
|
{
|
||||||
Host = "123.12.12.12",
|
Host = "123.12.12.12",
|
||||||
Port = 80,
|
Port = 80,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
DownstreamScheme = "https",
|
DownstreamScheme = "https",
|
||||||
DownstreamPathTemplate = "/asdfs/test/{test}"
|
DownstreamPathTemplate = "/asdfs/test/{test}",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var globalConfiguration = new FileGlobalConfiguration
|
var globalConfiguration = new FileGlobalConfiguration
|
||||||
|
@ -9,6 +9,7 @@ using Ocelot.Errors;
|
|||||||
using Ocelot.Responses;
|
using Ocelot.Responses;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
using TestStack.BDDfy;
|
using TestStack.BDDfy;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -104,8 +105,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
|
|
||||||
private void ThenTheConfigurationRepositoryIsCalledCorrectly()
|
private void ThenTheConfigurationRepositoryIsCalledCorrectly()
|
||||||
{
|
{
|
||||||
_configRepo
|
_configRepo.Verify(x => x.AddOrReplace(_configuration.Data), Times.Once);
|
||||||
.Verify(x => x.AddOrReplace(_configuration.Data), Times.Once);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ using Ocelot.Responses;
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Configuration.ChangeTracking;
|
||||||
using TestStack.BDDfy;
|
using TestStack.BDDfy;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -16,10 +18,13 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
private IInternalConfiguration _config;
|
private IInternalConfiguration _config;
|
||||||
private Response _result;
|
private Response _result;
|
||||||
private Response<IInternalConfiguration> _getResult;
|
private Response<IInternalConfiguration> _getResult;
|
||||||
|
private readonly Mock<IOcelotConfigurationChangeTokenSource> _changeTokenSource;
|
||||||
|
|
||||||
public InMemoryConfigurationRepositoryTests()
|
public InMemoryConfigurationRepositoryTests()
|
||||||
{
|
{
|
||||||
_repo = new InMemoryInternalConfigurationRepository();
|
_changeTokenSource = new Mock<IOcelotConfigurationChangeTokenSource>(MockBehavior.Strict);
|
||||||
|
_changeTokenSource.Setup(m => m.Activate());
|
||||||
|
_repo = new InMemoryInternalConfigurationRepository(_changeTokenSource.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -28,6 +33,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial", "adminath")))
|
this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial", "adminath")))
|
||||||
.When(x => x.WhenIAddOrReplaceTheConfig())
|
.When(x => x.WhenIAddOrReplaceTheConfig())
|
||||||
.Then(x => x.ThenNoErrorsAreReturned())
|
.Then(x => x.ThenNoErrorsAreReturned())
|
||||||
|
.And(x => AndTheChangeTokenIsActivated())
|
||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +77,11 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
_result.IsError.ShouldBeFalse();
|
_result.IsError.ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AndTheChangeTokenIsActivated()
|
||||||
|
{
|
||||||
|
_changeTokenSource.Verify(m => m.Activate(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
private class FakeConfig : IInternalConfiguration
|
private class FakeConfig : IInternalConfiguration
|
||||||
{
|
{
|
||||||
private readonly string _downstreamTemplatePath;
|
private readonly string _downstreamTemplatePath;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user