Skip to content

Commit

Permalink
Implement ProxyConfig and ProxyConfigProvider callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
nulltoken committed May 30, 2022
1 parent 5e0ad23 commit 9a6f772
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/ReverseProxy/Configuration/IProxyConfigNotifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace Yarp.ReverseProxy.Configuration;

/// <summary>
/// Represents an optional configuration capability. When decorating a <see cref="IProxyConfig"/> type,
/// will allow config instances to be notified upon config applying.
/// </summary>
public interface IProxyConfigNotifier
{
/// <summary>
/// A callback that will be triggered once changes to the configuration have been successfully applied.
/// </summary>
void SuccessfulConfigChangeApplyingCallback();

/// <summary>
/// A callback that will be triggered once changes to the configuration have been tried to be applied but eventually failed.
/// </summary>
void ErroredConfigChangeApplyingCallback(Exception ex);
}
23 changes: 23 additions & 0 deletions src/ReverseProxy/Configuration/IProxyConfigProviderNotifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace Yarp.ReverseProxy.Configuration;

/// <summary>
/// Represents an optional configuration provider capability. When decorating a <see cref="IProxyConfigProvider"/> type,
/// will allow config provider instances to be notified upon config loading.
/// </summary>
public interface IProxyConfigProviderNotifier
{
/// <summary>
/// A callback that will be triggered once the configuration have been successfully loaded.
/// </summary>
void SuccessfulConfigLoadingCallback();

/// <summary>
/// A callback that will be triggered once the configuration have been tried to be loaded but eventually failed.
/// </summary>
void ErroredConfigLoadingCallback(Exception ex);
}
46 changes: 46 additions & 0 deletions src/ReverseProxy/Management/ProxyConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,23 @@ internal async Task<EndpointDataSource> InitialLoadAsync()
_configs[i] = new ConfigState(provider, config);
routes.AddRange(config.Routes ?? Array.Empty<RouteConfig>());
clusters.AddRange(config.Clusters ?? Array.Empty<ClusterConfig>());

if (provider is IProxyConfigProviderNotifier)
{
((IProxyConfigProviderNotifier)provider).SuccessfulConfigLoadingCallback();
}
}

await ApplyConfigAsync(routes, clusters);

foreach (var c in _configs)
{
if (c.LatestConfig is IProxyConfigNotifier)
{
((IProxyConfigNotifier)c.LatestConfig).SuccessfulConfigChangeApplyingCallback();
}
}

ListenForConfigChanges();
}
catch (Exception ex)
Expand All @@ -182,6 +195,9 @@ private async Task ReloadConfigAsync()
var sourcesChanged = false;
var routes = new List<RouteConfig>();
var clusters = new List<ClusterConfig>();

var notifiers = new List<IProxyConfigNotifier>();

foreach (var instance in _configs)
{
try
Expand All @@ -193,12 +209,30 @@ private async Task ReloadConfigAsync()
instance.LatestConfig = config;
instance.LoadFailed = false;
sourcesChanged = true;

if (instance.Provider is IProxyConfigProviderNotifier)
{
// When callback is defined, notify the configuration provider of the successful loading of the configuration.
((IProxyConfigProviderNotifier)instance.Provider).SuccessfulConfigLoadingCallback();
}
}

if (instance.LatestConfig is IProxyConfigNotifier)
{
// And register potentially existing config notifiers for later invocation
notifiers.Add(((IProxyConfigNotifier)instance.LatestConfig));
}
}
catch (Exception ex)
{
instance.LoadFailed = true;
Log.ErrorReloadingConfig(_logger, ex);

if (instance.Provider is IProxyConfigProviderNotifier)
{
// When callback is defined, notify the configuration provider of the unsuccessful loading of the configuration.
((IProxyConfigProviderNotifier)instance.Provider).ErroredConfigLoadingCallback(ex);
}
}

// If we didn't/couldn't get a new config then re-use the last one.
Expand All @@ -221,10 +255,22 @@ private async Task ReloadConfigAsync()
CreateEndpoints();
}
}

foreach (var notifier in notifiers)
{
// Notify all registered config callbacks of successful applying.
notifier.SuccessfulConfigChangeApplyingCallback();
}
}
catch (Exception ex)
{
Log.ErrorApplyingConfig(_logger, ex);

foreach (var notifier in notifiers)
{
// Notify all registered config callbacks of unsuccessful applying.
notifier.ErroredConfigChangeApplyingCallback(ex);
}
}
}

Expand Down
137 changes: 137 additions & 0 deletions test/ReverseProxy.Tests/Management/ProxyConfigManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,143 @@ public async Task BuildConfig_TwoOverlappingConfigs_Works()
Assert.Equal(TestAddress, destination.Model.Config.Address);
}

[Fact]
public async Task BuildConfig_CanBeNotifiedOfProxyConfigSuccessfullAndErroredLoading()
{
bool? hasConfig1Loaded = null;
bool? hasConfig2Loaded = null;

var configProvider1 = new InMemoryConfigProviderNotifier(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, () => { hasConfig1Loaded = true; }, (_) => { hasConfig1Loaded = false; });
var configProvider2 = new InMemoryConfigProviderNotifier(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, () => { hasConfig2Loaded = true; }, (_) => { hasConfig2Loaded = false; });

var services = CreateServices(new[] { configProvider1, configProvider2 });

var manager = services.GetRequiredService<ProxyConfigManager>();
await manager.InitialLoadAsync();

Assert.True(hasConfig1Loaded);
Assert.True(hasConfig2Loaded);

const string TestAddress = "https://localhost:123/";

var cluster1 = new ClusterConfig
{
ClusterId = "cluster1",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
{
{ "d1", new DestinationConfig { Address = TestAddress } }
}
};
var cluster2 = new ClusterConfig
{
ClusterId = "cluster2",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
{
{ "d2", new DestinationConfig { Address = TestAddress } }
}
};

var route1 = new RouteConfig
{
RouteId = "route1",
ClusterId = "cluster1",
Match = new RouteMatch { Path = "/" }
};
var route2 = new RouteConfig
{
RouteId = "route2",
ClusterId = "cluster2",
Match = new RouteMatch { Path = "/" }
};

hasConfig1Loaded = null;
hasConfig2Loaded = null;

configProvider1.Update(new List<RouteConfig>() { route1 }, new List<ClusterConfig>() { cluster1 });

Assert.True(hasConfig1Loaded);
Assert.Null(hasConfig2Loaded);

hasConfig1Loaded = null;

configProvider2.ShouldConfigLoadingFail = true;
configProvider2.Update(new List<RouteConfig>() { route2 }, new List<ClusterConfig>() { cluster2 });

Assert.Null(hasConfig1Loaded);
Assert.False(hasConfig2Loaded);
}

[Fact]
public async Task BuildConfig_CanBeNotifiedOfProxyConfigSuccessfullAndErroredUpdating()
{
bool? hasConfig1Updated = null;
bool? hasConfig2Updated = null;

var config1 = new InMemoryConfigNotifier(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, () => { hasConfig1Updated = true; }, (_) => { hasConfig1Updated = false; });
var config2 = new InMemoryConfigNotifier(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, () => { hasConfig2Updated = true; }, (_) => { hasConfig2Updated = false; });

var configProvider1 = new InMemoryConfigProvider(config1);
var configProvider2 = new InMemoryConfigProvider(config2);

var services = CreateServices(new[] { configProvider1, configProvider2 });

var manager = services.GetRequiredService<ProxyConfigManager>();
var dataSource = await manager.InitialLoadAsync();

Assert.True(hasConfig1Updated);
Assert.True(hasConfig2Updated);

const string TestAddress = "https://localhost:123/";

var cluster1 = new ClusterConfig
{
ClusterId = "cluster1",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
{
{ "d1", new DestinationConfig { Address = TestAddress } }
}
};
var cluster2 = new ClusterConfig
{
ClusterId = "cluster2",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
{
{ "d2", new DestinationConfig { Address = TestAddress } }
}
};

var route1 = new RouteConfig
{
RouteId = "route1",
ClusterId = "cluster1",
Match = new RouteMatch { Path = "/" }
};
var route2 = new RouteConfig
{
RouteId = "route2",
ClusterId = "cluster2",
// Missing Match here will be caught by the analysis
};

hasConfig1Updated = null;
hasConfig2Updated = null;

var config1b = new InMemoryConfigNotifier(new List<RouteConfig>() { route1 }, new List<ClusterConfig>() { cluster1 }, () => { hasConfig1Updated = true; }, (_) => { hasConfig1Updated = false; });
configProvider1.Update(config1b);

Assert.True(hasConfig1Updated);
Assert.True(hasConfig2Updated);

hasConfig1Updated = null;
hasConfig2Updated = null;

var config2b = new InMemoryConfigNotifier(new List<RouteConfig>() { route2 }, new List<ClusterConfig>() { cluster2 }, () => { hasConfig2Updated = true; }, (_) => { hasConfig2Updated = false; });
configProvider2.Update(config2b);

Assert.False(hasConfig1Updated);
Assert.False(hasConfig2Updated);
}

[Fact]
public async Task InitialLoadAsync_ProxyHttpClientOptionsSet_CreateAndSetHttpClient()
{
Expand Down

0 comments on commit 9a6f772

Please sign in to comment.