Convert to file-scoped namespace (#1382)
This commit is contained in:
Родитель
bbd2742c21
Коммит
efd2a4c869
|
@ -5,27 +5,26 @@ using k8s.Models;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds data needed from a <see cref="V1Endpoints"/> resource.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct Endpoints
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public Endpoints(V1Endpoints endpoints)
|
||||
{
|
||||
if (endpoints is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
namespace Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
Name = endpoints.Name();
|
||||
Subsets = endpoints.Subsets;
|
||||
/// <summary>
|
||||
/// Holds data needed from a <see cref="V1Endpoints"/> resource.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct Endpoints
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public Endpoints(V1Endpoints endpoints)
|
||||
{
|
||||
if (endpoints is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public IList<V1EndpointSubset> Subsets { get; }
|
||||
Name = endpoints.Name();
|
||||
Subsets = endpoints.Subsets;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public IList<V1EndpointSubset> Subsets { get; }
|
||||
}
|
||||
|
|
|
@ -8,18 +8,17 @@ using System.Collections.Generic;
|
|||
using System.Collections.Immutable;
|
||||
using Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Caching
|
||||
namespace Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// ICache service interface holds onto least amount of data necessary
|
||||
/// for <see cref="IReconciler"/> to process work.
|
||||
/// </summary>
|
||||
public interface ICache
|
||||
{
|
||||
/// <summary>
|
||||
/// ICache service interface holds onto least amount of data necessary
|
||||
/// for <see cref="IReconciler"/> to process work.
|
||||
/// </summary>
|
||||
public interface ICache
|
||||
{
|
||||
void Update(WatchEventType eventType, V1Ingress ingress);
|
||||
void Update(WatchEventType eventType, V1Service service);
|
||||
ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints);
|
||||
bool TryGetReconcileData(NamespacedName key, out ReconcileData data);
|
||||
void GetKeys(List<NamespacedName> keys);
|
||||
}
|
||||
void Update(WatchEventType eventType, V1Ingress ingress);
|
||||
void Update(WatchEventType eventType, V1Service service);
|
||||
ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints);
|
||||
bool TryGetReconcileData(NamespacedName key, out ReconcileData data);
|
||||
void GetKeys(List<NamespacedName> keys);
|
||||
}
|
||||
|
|
|
@ -9,70 +9,69 @@ using System.Collections.Generic;
|
|||
using System.Collections.Immutable;
|
||||
using Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Caching
|
||||
namespace Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// ICache service interface holds onto least amount of data necessary
|
||||
/// for <see cref="IReconciler"/> to process work.
|
||||
/// </summary>
|
||||
public class IngressCache : ICache
|
||||
{
|
||||
/// <summary>
|
||||
/// ICache service interface holds onto least amount of data necessary
|
||||
/// for <see cref="IReconciler"/> to process work.
|
||||
/// </summary>
|
||||
public class IngressCache : ICache
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<string, NamespaceCache> _namespaceCaches = new Dictionary<string, NamespaceCache>();
|
||||
|
||||
public void Update(WatchEventType eventType, V1Ingress ingress)
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<string, NamespaceCache> _namespaceCaches = new Dictionary<string, NamespaceCache>();
|
||||
|
||||
public void Update(WatchEventType eventType, V1Ingress ingress)
|
||||
if (ingress is null)
|
||||
{
|
||||
if (ingress is null)
|
||||
throw new ArgumentNullException(nameof(ingress));
|
||||
}
|
||||
|
||||
Namespace(ingress.Namespace()).Update(eventType, ingress);
|
||||
}
|
||||
|
||||
public void Update(WatchEventType eventType, V1Service service)
|
||||
{
|
||||
if (service is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
}
|
||||
|
||||
Namespace(service.Namespace()).Update(eventType, service);
|
||||
}
|
||||
|
||||
public ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints)
|
||||
{
|
||||
return Namespace(endpoints.Namespace()).Update(eventType, endpoints);
|
||||
}
|
||||
|
||||
public bool TryGetReconcileData(NamespacedName key, out ReconcileData data)
|
||||
{
|
||||
return Namespace(key.Namespace).TryLookup(key, out data);
|
||||
}
|
||||
|
||||
public void GetKeys(List<NamespacedName> keys)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
foreach (var (ns, cache) in _namespaceCaches)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ingress));
|
||||
}
|
||||
|
||||
Namespace(ingress.Namespace()).Update(eventType, ingress);
|
||||
}
|
||||
|
||||
public void Update(WatchEventType eventType, V1Service service)
|
||||
{
|
||||
if (service is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
}
|
||||
|
||||
Namespace(service.Namespace()).Update(eventType, service);
|
||||
}
|
||||
|
||||
public ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints)
|
||||
{
|
||||
return Namespace(endpoints.Namespace()).Update(eventType, endpoints);
|
||||
}
|
||||
|
||||
public bool TryGetReconcileData(NamespacedName key, out ReconcileData data)
|
||||
{
|
||||
return Namespace(key.Namespace).TryLookup(key, out data);
|
||||
}
|
||||
|
||||
public void GetKeys(List<NamespacedName> keys)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
foreach (var (ns, cache) in _namespaceCaches)
|
||||
{
|
||||
cache.GetKeys(ns, keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private NamespaceCache Namespace(string key)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_namespaceCaches.TryGetValue(key, out var value))
|
||||
{
|
||||
value = new NamespaceCache();
|
||||
_namespaceCaches.Add(key, value);
|
||||
}
|
||||
return value;
|
||||
cache.GetKeys(ns, keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private NamespaceCache Namespace(string key)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_namespaceCaches.TryGetValue(key, out var value))
|
||||
{
|
||||
value = new NamespaceCache();
|
||||
_namespaceCaches.Add(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,26 @@
|
|||
using k8s.Models;
|
||||
using System;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds data needed from a <see cref="V1Ingress"/> resource.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct IngressData
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public IngressData(V1Ingress ingress)
|
||||
{
|
||||
if (ingress is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ingress));
|
||||
}
|
||||
namespace Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
Spec = ingress.Spec;
|
||||
Metadata = ingress.Metadata;
|
||||
/// <summary>
|
||||
/// Holds data needed from a <see cref="V1Ingress"/> resource.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct IngressData
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public IngressData(V1Ingress ingress)
|
||||
{
|
||||
if (ingress is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ingress));
|
||||
}
|
||||
|
||||
public V1IngressSpec Spec { get; set; }
|
||||
public V1ObjectMeta Metadata { get; set; }
|
||||
Spec = ingress.Spec;
|
||||
Metadata = ingress.Metadata;
|
||||
}
|
||||
|
||||
public V1IngressSpec Spec { get; set; }
|
||||
public V1ObjectMeta Metadata { get; set; }
|
||||
}
|
||||
|
|
|
@ -10,221 +10,220 @@ using System.Collections.Immutable;
|
|||
using System.Linq;
|
||||
using Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-namespace cache data. Implicitly scopes name-based lookups to same namespace. Also
|
||||
/// intended to make updates faster because cross-reference dictionaries are not cluster-wide.
|
||||
/// </summary>
|
||||
public class NamespaceCache
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<string, ImmutableList<string>> _ingressToServiceNames = new Dictionary<string, ImmutableList<string>>();
|
||||
private readonly Dictionary<string, ImmutableList<string>> _serviceToIngressNames = new Dictionary<string, ImmutableList<string>>();
|
||||
private readonly Dictionary<string, IngressData> _ingressData = new Dictionary<string, IngressData>();
|
||||
private readonly Dictionary<string, ServiceData> _serviceData = new Dictionary<string, ServiceData>();
|
||||
private readonly Dictionary<string, Endpoints> _endpointsData = new Dictionary<string, Endpoints>();
|
||||
namespace Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
public void Update(WatchEventType eventType, V1Ingress ingress)
|
||||
/// <summary>
|
||||
/// Per-namespace cache data. Implicitly scopes name-based lookups to same namespace. Also
|
||||
/// intended to make updates faster because cross-reference dictionaries are not cluster-wide.
|
||||
/// </summary>
|
||||
public class NamespaceCache
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<string, ImmutableList<string>> _ingressToServiceNames = new Dictionary<string, ImmutableList<string>>();
|
||||
private readonly Dictionary<string, ImmutableList<string>> _serviceToIngressNames = new Dictionary<string, ImmutableList<string>>();
|
||||
private readonly Dictionary<string, IngressData> _ingressData = new Dictionary<string, IngressData>();
|
||||
private readonly Dictionary<string, ServiceData> _serviceData = new Dictionary<string, ServiceData>();
|
||||
private readonly Dictionary<string, Endpoints> _endpointsData = new Dictionary<string, Endpoints>();
|
||||
|
||||
public void Update(WatchEventType eventType, V1Ingress ingress)
|
||||
{
|
||||
if (ingress is null)
|
||||
{
|
||||
if (ingress is null)
|
||||
throw new ArgumentNullException(nameof(ingress));
|
||||
}
|
||||
|
||||
var serviceNames = ImmutableList<string>.Empty;
|
||||
|
||||
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
|
||||
{
|
||||
// If the ingress exists, list out the related services
|
||||
var spec = ingress.Spec;
|
||||
var defaultBackend = spec?.DefaultBackend;
|
||||
var defaultService = defaultBackend?.Service;
|
||||
if (!string.IsNullOrEmpty(defaultService?.Name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ingress));
|
||||
serviceNames = serviceNames.Add(defaultService.Name);
|
||||
}
|
||||
|
||||
var serviceNames = ImmutableList<string>.Empty;
|
||||
foreach (var rule in spec.Rules ?? Enumerable.Empty<V1IngressRule>())
|
||||
{
|
||||
var http = rule.Http;
|
||||
foreach (var path in http.Paths ?? Enumerable.Empty<V1HTTPIngressPath>())
|
||||
{
|
||||
var backend = path.Backend;
|
||||
var service = backend.Service;
|
||||
|
||||
if (!serviceNames.Contains(service.Name))
|
||||
{
|
||||
serviceNames = serviceNames.Add(service.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ingressName = ingress.Name();
|
||||
lock (_sync)
|
||||
{
|
||||
var serviceNamesPrevious = ImmutableList<string>.Empty;
|
||||
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
|
||||
{
|
||||
// If the ingress exists, list out the related services
|
||||
var spec = ingress.Spec;
|
||||
var defaultBackend = spec?.DefaultBackend;
|
||||
var defaultService = defaultBackend?.Service;
|
||||
if (!string.IsNullOrEmpty(defaultService?.Name))
|
||||
// If the ingress exists then remember details
|
||||
|
||||
_ingressData[ingressName] = new IngressData(ingress);
|
||||
|
||||
if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))
|
||||
{
|
||||
serviceNames = serviceNames.Add(defaultService.Name);
|
||||
}
|
||||
|
||||
foreach (var rule in spec.Rules ?? Enumerable.Empty<V1IngressRule>())
|
||||
{
|
||||
var http = rule.Http;
|
||||
foreach (var path in http.Paths ?? Enumerable.Empty<V1HTTPIngressPath>())
|
||||
{
|
||||
var backend = path.Backend;
|
||||
var service = backend.Service;
|
||||
|
||||
if (!serviceNames.Contains(service.Name))
|
||||
{
|
||||
serviceNames = serviceNames.Add(service.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ingressName = ingress.Name();
|
||||
lock (_sync)
|
||||
{
|
||||
var serviceNamesPrevious = ImmutableList<string>.Empty;
|
||||
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
|
||||
{
|
||||
// If the ingress exists then remember details
|
||||
|
||||
_ingressData[ingressName] = new IngressData(ingress);
|
||||
|
||||
if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))
|
||||
{
|
||||
_ingressToServiceNames[ingressName] = serviceNames;
|
||||
}
|
||||
else
|
||||
{
|
||||
serviceNamesPrevious = ImmutableList<string>.Empty;
|
||||
_ingressToServiceNames.Add(ingressName, serviceNames);
|
||||
}
|
||||
}
|
||||
else if (eventType == WatchEventType.Deleted)
|
||||
{
|
||||
// otherwise clear out details
|
||||
|
||||
_ingressData.Remove(ingressName);
|
||||
|
||||
if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))
|
||||
{
|
||||
_ingressToServiceNames.Remove(ingressName);
|
||||
}
|
||||
}
|
||||
|
||||
// update cross-reference for new ingress-to-services linkage not previously known
|
||||
foreach (var serviceName in serviceNames)
|
||||
{
|
||||
if (!serviceNamesPrevious.Contains(serviceName))
|
||||
{
|
||||
if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNamesPrevious))
|
||||
{
|
||||
_serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Add(ingressName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_serviceToIngressNames.Add(serviceName, ImmutableList<string>.Empty.Add(ingressName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove cross-reference for previous ingress-to-services linkage no longer present
|
||||
foreach (var serviceName in serviceNamesPrevious)
|
||||
{
|
||||
if (!serviceNames.Contains(serviceName))
|
||||
{
|
||||
_serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Remove(ingressName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(WatchEventType eventType, V1Service service)
|
||||
{
|
||||
if (service is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
}
|
||||
|
||||
var serviceName = service.Name();
|
||||
lock (_sync)
|
||||
{
|
||||
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
|
||||
{
|
||||
_serviceData[serviceName] = new ServiceData(service);
|
||||
}
|
||||
else if (eventType == WatchEventType.Deleted)
|
||||
{
|
||||
_serviceData.Remove(serviceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GetKeys(string ns, List<NamespacedName> keys)
|
||||
{
|
||||
if (keys is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(keys));
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
foreach (var name in _ingressData.Keys)
|
||||
{
|
||||
keys.Add(new NamespacedName(ns, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints)
|
||||
{
|
||||
if (endpoints is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
var serviceName = endpoints.Name();
|
||||
lock (_sync)
|
||||
{
|
||||
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
|
||||
{
|
||||
_endpointsData[serviceName] = new Endpoints(endpoints);
|
||||
}
|
||||
else if (eventType == WatchEventType.Deleted)
|
||||
{
|
||||
_endpointsData.Remove(serviceName);
|
||||
}
|
||||
|
||||
if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames))
|
||||
{
|
||||
return ingressNames;
|
||||
_ingressToServiceNames[ingressName] = serviceNames;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ImmutableList<string>.Empty;
|
||||
serviceNamesPrevious = ImmutableList<string>.Empty;
|
||||
_ingressToServiceNames.Add(ingressName, serviceNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryLookup(NamespacedName key, out ReconcileData data)
|
||||
{
|
||||
var endspointsList = new List<Endpoints>();
|
||||
var servicesList = new List<ServiceData>();
|
||||
|
||||
lock (_sync)
|
||||
else if (eventType == WatchEventType.Deleted)
|
||||
{
|
||||
if (!_ingressData.TryGetValue(key.Name, out var ingress))
|
||||
// otherwise clear out details
|
||||
|
||||
_ingressData.Remove(ingressName);
|
||||
|
||||
if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))
|
||||
{
|
||||
data = default;
|
||||
return false;
|
||||
_ingressToServiceNames.Remove(ingressName);
|
||||
}
|
||||
}
|
||||
|
||||
if (_ingressToServiceNames.TryGetValue(key.Name, out var serviceNames))
|
||||
// update cross-reference for new ingress-to-services linkage not previously known
|
||||
foreach (var serviceName in serviceNames)
|
||||
{
|
||||
if (!serviceNamesPrevious.Contains(serviceName))
|
||||
{
|
||||
foreach (var serviceName in serviceNames)
|
||||
if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNamesPrevious))
|
||||
{
|
||||
if (_serviceData.TryGetValue(serviceName, out var serviceData))
|
||||
{
|
||||
servicesList.Add(serviceData);
|
||||
}
|
||||
|
||||
if (_endpointsData.TryGetValue(serviceName, out var endpoints))
|
||||
{
|
||||
endspointsList.Add(endpoints);
|
||||
}
|
||||
_serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Add(ingressName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_serviceToIngressNames.Add(serviceName, ImmutableList<string>.Empty.Add(ingressName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_serviceData.Count == 0)
|
||||
// remove cross-reference for previous ingress-to-services linkage no longer present
|
||||
foreach (var serviceName in serviceNamesPrevious)
|
||||
{
|
||||
if (!serviceNames.Contains(serviceName))
|
||||
{
|
||||
data = default;
|
||||
return false;
|
||||
_serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Remove(ingressName);
|
||||
}
|
||||
|
||||
data = new ReconcileData(ingress, servicesList, endspointsList);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(WatchEventType eventType, V1Service service)
|
||||
{
|
||||
if (service is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
}
|
||||
|
||||
var serviceName = service.Name();
|
||||
lock (_sync)
|
||||
{
|
||||
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
|
||||
{
|
||||
_serviceData[serviceName] = new ServiceData(service);
|
||||
}
|
||||
else if (eventType == WatchEventType.Deleted)
|
||||
{
|
||||
_serviceData.Remove(serviceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GetKeys(string ns, List<NamespacedName> keys)
|
||||
{
|
||||
if (keys is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(keys));
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
foreach (var name in _ingressData.Keys)
|
||||
{
|
||||
keys.Add(new NamespacedName(ns, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints)
|
||||
{
|
||||
if (endpoints is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
var serviceName = endpoints.Name();
|
||||
lock (_sync)
|
||||
{
|
||||
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
|
||||
{
|
||||
_endpointsData[serviceName] = new Endpoints(endpoints);
|
||||
}
|
||||
else if (eventType == WatchEventType.Deleted)
|
||||
{
|
||||
_endpointsData.Remove(serviceName);
|
||||
}
|
||||
|
||||
if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames))
|
||||
{
|
||||
return ingressNames;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ImmutableList<string>.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryLookup(NamespacedName key, out ReconcileData data)
|
||||
{
|
||||
var endspointsList = new List<Endpoints>();
|
||||
var servicesList = new List<ServiceData>();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_ingressData.TryGetValue(key.Name, out var ingress))
|
||||
{
|
||||
data = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ingressToServiceNames.TryGetValue(key.Name, out var serviceNames))
|
||||
{
|
||||
foreach (var serviceName in serviceNames)
|
||||
{
|
||||
if (_serviceData.TryGetValue(serviceName, out var serviceData))
|
||||
{
|
||||
servicesList.Add(serviceData);
|
||||
}
|
||||
|
||||
if (_endpointsData.TryGetValue(serviceName, out var endpoints))
|
||||
{
|
||||
endspointsList.Add(endpoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_serviceData.Count == 0)
|
||||
{
|
||||
data = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
data = new ReconcileData(ingress, servicesList, endspointsList);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,26 @@
|
|||
using k8s.Models;
|
||||
using System;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds data needed from a <see cref="V1Service"/> resource.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct ServiceData
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public ServiceData(V1Service service)
|
||||
{
|
||||
if (service is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
}
|
||||
namespace Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
Spec = service.Spec;
|
||||
Metadata = service.Metadata;
|
||||
/// <summary>
|
||||
/// Holds data needed from a <see cref="V1Service"/> resource.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct ServiceData
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public ServiceData(V1Service service)
|
||||
{
|
||||
if (service is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
}
|
||||
|
||||
public V1ServiceSpec Spec { get; set; }
|
||||
public V1ObjectMeta Metadata { get; set; }
|
||||
Spec = service.Spec;
|
||||
Metadata = service.Metadata;
|
||||
}
|
||||
|
||||
public V1ServiceSpec Spec { get; set; }
|
||||
public V1ObjectMeta Metadata { get; set; }
|
||||
}
|
||||
|
|
|
@ -5,27 +5,26 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Controllers
|
||||
namespace Yarp.Kubernetes.Controller.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// DispatchController provides API used by callers to begin streaming
|
||||
/// information being sent out through the <see cref="IDispatcher"/> muxer.
|
||||
/// </summary>
|
||||
[Route("api/dispatch")]
|
||||
[ApiController]
|
||||
public class DispatchController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// DispatchController provides API used by callers to begin streaming
|
||||
/// information being sent out through the <see cref="IDispatcher"/> muxer.
|
||||
/// </summary>
|
||||
[Route("api/dispatch")]
|
||||
[ApiController]
|
||||
public class DispatchController : ControllerBase
|
||||
private readonly IDispatcher _dispatcher;
|
||||
|
||||
public DispatchController(IDispatcher dispatcher)
|
||||
{
|
||||
private readonly IDispatcher _dispatcher;
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public DispatchController(IDispatcher dispatcher)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
[HttpGet("/api/dispatch")]
|
||||
public Task<IActionResult> WatchAsync()
|
||||
{
|
||||
return Task.FromResult<IActionResult>(new DispatchActionResult(_dispatcher));
|
||||
}
|
||||
[HttpGet("/api/dispatch")]
|
||||
public Task<IActionResult> WatchAsync()
|
||||
{
|
||||
return Task.FromResult<IActionResult>(new DispatchActionResult(_dispatcher));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
using System.Collections.Generic;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
internal sealed class ClusterTransfer
|
||||
{
|
||||
internal sealed class ClusterTransfer
|
||||
{
|
||||
public Dictionary<string, DestinationConfig> Destinations { get; set; } = new Dictionary<string, DestinationConfig>();
|
||||
public string ClusterId { get; set; }
|
||||
}
|
||||
public Dictionary<string, DestinationConfig> Destinations { get; set; } = new Dictionary<string, DestinationConfig>();
|
||||
public string ClusterId { get; set; }
|
||||
}
|
||||
|
|
|
@ -5,23 +5,22 @@ using System.Collections.Generic;
|
|||
using Yarp.ReverseProxy.Configuration;
|
||||
using Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
{
|
||||
internal sealed class YarpIngressContext
|
||||
{
|
||||
public YarpIngressContext(IngressData ingress, List<ServiceData> services, List<Endpoints> endpoints)
|
||||
{
|
||||
Ingress = ingress;
|
||||
Services = services;
|
||||
Endpoints = endpoints;
|
||||
}
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
public YarpIngressOptions Options { get; set; } = new YarpIngressOptions();
|
||||
public Dictionary<string, ClusterTransfer> ClusterTransfers { get; set; } = new Dictionary<string, ClusterTransfer>();
|
||||
public List<RouteConfig> Routes { get; set; } = new List<RouteConfig>();
|
||||
public List<ClusterConfig> Clusters { get; set; } = new List<ClusterConfig>();
|
||||
public IngressData Ingress { get; }
|
||||
public List<ServiceData> Services { get; }
|
||||
public List<Endpoints> Endpoints { get; }
|
||||
internal sealed class YarpIngressContext
|
||||
{
|
||||
public YarpIngressContext(IngressData ingress, List<ServiceData> services, List<Endpoints> endpoints)
|
||||
{
|
||||
Ingress = ingress;
|
||||
Services = services;
|
||||
Endpoints = endpoints;
|
||||
}
|
||||
|
||||
public YarpIngressOptions Options { get; set; } = new YarpIngressOptions();
|
||||
public Dictionary<string, ClusterTransfer> ClusterTransfers { get; set; } = new Dictionary<string, ClusterTransfer>();
|
||||
public List<RouteConfig> Routes { get; set; } = new List<RouteConfig>();
|
||||
public List<ClusterConfig> Clusters { get; set; } = new List<ClusterConfig>();
|
||||
public IngressData Ingress { get; }
|
||||
public List<ServiceData> Services { get; }
|
||||
public List<Endpoints> Endpoints { get; }
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
internal sealed class YarpIngressOptions
|
||||
{
|
||||
internal sealed class YarpIngressOptions
|
||||
{
|
||||
public bool Https { get; set; }
|
||||
}
|
||||
public bool Https { get; set; }
|
||||
}
|
||||
|
|
|
@ -6,197 +6,196 @@ using Yarp.ReverseProxy.Configuration;
|
|||
using Yarp.Kubernetes.Controller.Caching;
|
||||
using Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Converters
|
||||
namespace Yarp.Kubernetes.Controller.Converters;
|
||||
|
||||
internal static class YarpParser
|
||||
{
|
||||
internal static class YarpParser
|
||||
internal static void CovertFromKubernetesIngress(YarpIngressContext context)
|
||||
{
|
||||
internal static void CovertFromKubernetesIngress(YarpIngressContext context)
|
||||
var spec = context.Ingress.Spec;
|
||||
var defaultBackend = spec?.DefaultBackend;
|
||||
var defaultService = defaultBackend?.Service;
|
||||
IList<V1EndpointSubset> defaultSubsets = default;
|
||||
|
||||
if (!string.IsNullOrEmpty(defaultService?.Name))
|
||||
{
|
||||
var spec = context.Ingress.Spec;
|
||||
var defaultBackend = spec?.DefaultBackend;
|
||||
var defaultService = defaultBackend?.Service;
|
||||
IList<V1EndpointSubset> defaultSubsets = default;
|
||||
|
||||
if (!string.IsNullOrEmpty(defaultService?.Name))
|
||||
{
|
||||
defaultSubsets = context.Endpoints.SingleOrDefault(x => x.Name == defaultService?.Name).Subsets;
|
||||
}
|
||||
|
||||
// cluster can contain multiple replicas for each destination, need to know the lookup base don endpoints
|
||||
var options = HandleAnnotations(context, context.Ingress.Metadata);
|
||||
|
||||
foreach (var rule in spec.Rules ?? Enumerable.Empty<V1IngressRule>())
|
||||
{
|
||||
HandleIngressRule(context, context.Endpoints, defaultSubsets, rule);
|
||||
}
|
||||
|
||||
CreateClusters(context);
|
||||
defaultSubsets = context.Endpoints.SingleOrDefault(x => x.Name == defaultService?.Name).Subsets;
|
||||
}
|
||||
|
||||
private static void CreateClusters(YarpIngressContext context)
|
||||
// cluster can contain multiple replicas for each destination, need to know the lookup base don endpoints
|
||||
var options = HandleAnnotations(context, context.Ingress.Metadata);
|
||||
|
||||
foreach (var rule in spec.Rules ?? Enumerable.Empty<V1IngressRule>())
|
||||
{
|
||||
foreach (var cluster in context.ClusterTransfers)
|
||||
HandleIngressRule(context, context.Endpoints, defaultSubsets, rule);
|
||||
}
|
||||
|
||||
CreateClusters(context);
|
||||
}
|
||||
|
||||
private static void CreateClusters(YarpIngressContext context)
|
||||
{
|
||||
foreach (var cluster in context.ClusterTransfers)
|
||||
{
|
||||
context.Clusters.Add(new ClusterConfig()
|
||||
{
|
||||
context.Clusters.Add(new ClusterConfig()
|
||||
Destinations = cluster.Value.Destinations,
|
||||
ClusterId = cluster.Value.ClusterId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIngressRule(YarpIngressContext context, List<Endpoints> endpoints, IList<V1EndpointSubset> defaultSubsets, V1IngressRule rule)
|
||||
{
|
||||
var http = rule.Http;
|
||||
foreach (var path in http.Paths ?? Enumerable.Empty<V1HTTPIngressPath>())
|
||||
{
|
||||
var service = context.Services.SingleOrDefault(s => s.Metadata.Name == path.Backend.Service.Name);
|
||||
var servicePort = service.Spec.Ports.SingleOrDefault(p => MatchesPort(p, path.Backend.Service.Port));
|
||||
HandleIngressRulePath(context, servicePort, endpoints, defaultSubsets, rule, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIngressRulePath(YarpIngressContext context, V1ServicePort servicePort, List<Endpoints> endpoints, IList<V1EndpointSubset> defaultSubsets, V1IngressRule rule, V1HTTPIngressPath path)
|
||||
{
|
||||
var backend = path.Backend;
|
||||
var ingressServiceBackend = backend.Service;
|
||||
var subsets = defaultSubsets;
|
||||
|
||||
var clusters = context.ClusterTransfers;
|
||||
var routes = context.Routes;
|
||||
|
||||
if (!string.IsNullOrEmpty(ingressServiceBackend?.Name))
|
||||
{
|
||||
subsets = endpoints.SingleOrDefault(x => x.Name == ingressServiceBackend?.Name).Subsets;
|
||||
}
|
||||
|
||||
// Each ingress rule path can only be for one service
|
||||
var key = ingressServiceBackend.Port.Number.HasValue ? $"{ingressServiceBackend?.Name}:{ingressServiceBackend?.Port.Number}" : $"{ingressServiceBackend?.Name}:{ingressServiceBackend?.Port.Name}";
|
||||
if (!clusters.ContainsKey(key))
|
||||
{
|
||||
clusters.Add(key, new ClusterTransfer());
|
||||
}
|
||||
|
||||
var cluster = clusters[key];
|
||||
cluster.ClusterId = key;
|
||||
|
||||
// make sure cluster is present
|
||||
foreach (var subset in subsets ?? Enumerable.Empty<V1EndpointSubset>())
|
||||
{
|
||||
foreach (var port in subset.Ports ?? Enumerable.Empty<V1EndpointPort>())
|
||||
{
|
||||
foreach (var address in subset.Addresses ?? Enumerable.Empty<V1EndpointAddress>())
|
||||
{
|
||||
Destinations = cluster.Value.Destinations,
|
||||
ClusterId = cluster.Value.ClusterId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIngressRule(YarpIngressContext context, List<Endpoints> endpoints, IList<V1EndpointSubset> defaultSubsets, V1IngressRule rule)
|
||||
{
|
||||
var http = rule.Http;
|
||||
foreach (var path in http.Paths ?? Enumerable.Empty<V1HTTPIngressPath>())
|
||||
{
|
||||
var service = context.Services.SingleOrDefault(s => s.Metadata.Name == path.Backend.Service.Name);
|
||||
var servicePort = service.Spec.Ports.SingleOrDefault(p => MatchesPort(p, path.Backend.Service.Port));
|
||||
HandleIngressRulePath(context, servicePort, endpoints, defaultSubsets, rule, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIngressRulePath(YarpIngressContext context, V1ServicePort servicePort, List<Endpoints> endpoints, IList<V1EndpointSubset> defaultSubsets, V1IngressRule rule, V1HTTPIngressPath path)
|
||||
{
|
||||
var backend = path.Backend;
|
||||
var ingressServiceBackend = backend.Service;
|
||||
var subsets = defaultSubsets;
|
||||
|
||||
var clusters = context.ClusterTransfers;
|
||||
var routes = context.Routes;
|
||||
|
||||
if (!string.IsNullOrEmpty(ingressServiceBackend?.Name))
|
||||
{
|
||||
subsets = endpoints.SingleOrDefault(x => x.Name == ingressServiceBackend?.Name).Subsets;
|
||||
}
|
||||
|
||||
// Each ingress rule path can only be for one service
|
||||
var key = ingressServiceBackend.Port.Number.HasValue ? $"{ingressServiceBackend?.Name}:{ingressServiceBackend?.Port.Number}" : $"{ingressServiceBackend?.Name}:{ingressServiceBackend?.Port.Name}";
|
||||
if (!clusters.ContainsKey(key))
|
||||
{
|
||||
clusters.Add(key, new ClusterTransfer());
|
||||
}
|
||||
|
||||
var cluster = clusters[key];
|
||||
cluster.ClusterId = key;
|
||||
|
||||
// make sure cluster is present
|
||||
foreach (var subset in subsets ?? Enumerable.Empty<V1EndpointSubset>())
|
||||
{
|
||||
foreach (var port in subset.Ports ?? Enumerable.Empty<V1EndpointPort>())
|
||||
{
|
||||
foreach (var address in subset.Addresses ?? Enumerable.Empty<V1EndpointAddress>())
|
||||
if (!MatchesPort(port, servicePort.TargetPort))
|
||||
{
|
||||
if (!MatchesPort(port, servicePort.TargetPort))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var protocol = context.Options.Https ? "https" : "http";
|
||||
var uri = $"{protocol}://{address.Ip}:{port.Port}";
|
||||
cluster.Destinations[uri] = new DestinationConfig()
|
||||
{
|
||||
Address = uri
|
||||
};
|
||||
|
||||
var pathMatch = FixupPathMatch(path);
|
||||
var host = rule.Host;
|
||||
|
||||
routes.Add(new RouteConfig()
|
||||
{
|
||||
Match = new RouteMatch()
|
||||
{
|
||||
Hosts = host != null ? new[] { host } : Array.Empty<string>(),
|
||||
Path = pathMatch
|
||||
},
|
||||
ClusterId = cluster.ClusterId,
|
||||
RouteId = path.Path
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var protocol = context.Options.Https ? "https" : "http";
|
||||
var uri = $"{protocol}://{address.Ip}:{port.Port}";
|
||||
cluster.Destinations[uri] = new DestinationConfig()
|
||||
{
|
||||
Address = uri
|
||||
};
|
||||
|
||||
var pathMatch = FixupPathMatch(path);
|
||||
var host = rule.Host;
|
||||
|
||||
routes.Add(new RouteConfig()
|
||||
{
|
||||
Match = new RouteMatch()
|
||||
{
|
||||
Hosts = host != null ? new[] { host } : Array.Empty<string>(),
|
||||
Path = pathMatch
|
||||
},
|
||||
ClusterId = cluster.ClusterId,
|
||||
RouteId = path.Path
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FixupPathMatch(V1HTTPIngressPath path)
|
||||
private static string FixupPathMatch(V1HTTPIngressPath path)
|
||||
{
|
||||
var pathMatch = path.Path;
|
||||
|
||||
// Prefix match is the default for implementation specific.
|
||||
if (string.Equals(path.PathType, "Prefix", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(path.PathType, "ImplementationSpecific", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var pathMatch = path.Path;
|
||||
|
||||
// Prefix match is the default for implementation specific.
|
||||
if (string.Equals(path.PathType, "Prefix", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(path.PathType, "ImplementationSpecific", StringComparison.OrdinalIgnoreCase))
|
||||
if (!pathMatch.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
if (!pathMatch.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
pathMatch += "/";
|
||||
}
|
||||
// remember for prefix matches, /foo/ works for either /foo or /foo/
|
||||
pathMatch += "{**catch-all}";
|
||||
pathMatch += "/";
|
||||
}
|
||||
|
||||
return pathMatch;
|
||||
// remember for prefix matches, /foo/ works for either /foo or /foo/
|
||||
pathMatch += "{**catch-all}";
|
||||
}
|
||||
|
||||
private static YarpIngressOptions HandleAnnotations(YarpIngressContext context, V1ObjectMeta metadata)
|
||||
return pathMatch;
|
||||
}
|
||||
|
||||
private static YarpIngressOptions HandleAnnotations(YarpIngressContext context, V1ObjectMeta metadata)
|
||||
{
|
||||
var options = context.Options;
|
||||
var annotations = metadata.Annotations;
|
||||
if (annotations == null)
|
||||
{
|
||||
var options = context.Options;
|
||||
var annotations = metadata.Annotations;
|
||||
if (annotations == null)
|
||||
{
|
||||
return options;
|
||||
}
|
||||
|
||||
if (annotations.TryGetValue("yarp.ingress.kubernetes.io/backend-protocol", out var http))
|
||||
{
|
||||
options.Https = http.Equals("https", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// metadata to support:
|
||||
// rewrite target
|
||||
// auth
|
||||
// http or https
|
||||
// default backend
|
||||
// CORS
|
||||
// GRPC
|
||||
// HTTP2
|
||||
// Conneciton limits
|
||||
// rate limits
|
||||
|
||||
// backend health checks.
|
||||
return options;
|
||||
}
|
||||
|
||||
private static bool MatchesPort(V1EndpointPort port1, IntstrIntOrString port2)
|
||||
if (annotations.TryGetValue("yarp.ingress.kubernetes.io/backend-protocol", out var http))
|
||||
{
|
||||
if (port1 == null || port2 == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (int.TryParse(port2, out var port2Number) && port2Number == port1.Port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (string.Equals(port2, port1.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
options.Https = http.Equals("https", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool MatchesPort(V1ServicePort port1, V1ServiceBackendPort port2)
|
||||
// metadata to support:
|
||||
// rewrite target
|
||||
// auth
|
||||
// http or https
|
||||
// default backend
|
||||
// CORS
|
||||
// GRPC
|
||||
// HTTP2
|
||||
// Conneciton limits
|
||||
// rate limits
|
||||
|
||||
// backend health checks.
|
||||
return options;
|
||||
}
|
||||
|
||||
private static bool MatchesPort(V1EndpointPort port1, IntstrIntOrString port2)
|
||||
{
|
||||
if (port1 == null || port2 == null)
|
||||
{
|
||||
if (port1 == null || port2 == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (port2.Number != null && port2.Number == port1.Port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (port2.Name != null && string.Equals(port2.Name, port1.Name, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (int.TryParse(port2, out var port2Number) && port2Number == port1.Port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (string.Equals(port2, port1.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool MatchesPort(V1ServicePort port1, V1ServiceBackendPort port2)
|
||||
{
|
||||
if (port1 == null || port2 == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (port2.Number != null && port2.Number == port1.Port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (port2.Name != null && string.Equals(port2.Name, port1.Name, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,98 +10,97 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yarp.Kubernetes.Protocol;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
/// <summary>
|
||||
/// DispatchActionResult is an IActionResult which registers itself as
|
||||
/// an IDispatchTarget with the provided IDispatcher. As long as the client
|
||||
/// is connected this result will continue to write data to the response.
|
||||
/// </summary>
|
||||
public class DispatchActionResult : IActionResult, IDispatchTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// DispatchActionResult is an IActionResult which registers itself as
|
||||
/// an IDispatchTarget with the provided IDispatcher. As long as the client
|
||||
/// is connected this result will continue to write data to the response.
|
||||
/// </summary>
|
||||
public class DispatchActionResult : IActionResult, IDispatchTarget
|
||||
private static readonly byte[] _newline = Encoding.UTF8.GetBytes(Environment.NewLine);
|
||||
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private Task _task = Task.CompletedTask;
|
||||
private readonly object _taskSync = new();
|
||||
private HttpContext _httpContext;
|
||||
|
||||
public DispatchActionResult(IDispatcher dispatcher)
|
||||
{
|
||||
private static readonly byte[] _newline = Encoding.UTF8.GetBytes(Environment.NewLine);
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private Task _task = Task.CompletedTask;
|
||||
private readonly object _taskSync = new();
|
||||
private HttpContext _httpContext;
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{_httpContext?.Connection.Id}:{_httpContext?.TraceIdentifier}";
|
||||
}
|
||||
|
||||
public DispatchActionResult(IDispatcher dispatcher)
|
||||
public async Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
var cancellationToken = context.HttpContext.RequestAborted;
|
||||
|
||||
_httpContext = context.HttpContext;
|
||||
_httpContext.Response.ContentType = "text/plain";
|
||||
_httpContext.Response.Headers["Connection"] = "close";
|
||||
await _httpContext.Response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_dispatcher.Attach(this);
|
||||
|
||||
try
|
||||
{
|
||||
return $"{_httpContext?.Connection.Id}:{_httpContext?.TraceIdentifier}";
|
||||
}
|
||||
|
||||
public async Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
if (context is null)
|
||||
var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new Message
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
MessageType = MessageType.Heartbeat
|
||||
});
|
||||
|
||||
var cancellationToken = context.HttpContext.RequestAborted;
|
||||
|
||||
_httpContext = context.HttpContext;
|
||||
_httpContext.Response.ContentType = "text/plain";
|
||||
_httpContext.Response.Headers["Connection"] = "close";
|
||||
await _httpContext.Response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_dispatcher.Attach(this);
|
||||
|
||||
try
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new Message
|
||||
{
|
||||
MessageType = MessageType.Heartbeat
|
||||
});
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(35), cancellationToken).ConfigureAwait(false);
|
||||
await SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// This is fine.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dispatcher.Detach(this);
|
||||
await Task.Delay(TimeSpan.FromSeconds(35), cancellationToken).ConfigureAwait(false);
|
||||
await SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] bytes, CancellationToken cancellationToken)
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
var result = Task.CompletedTask;
|
||||
|
||||
lock (_taskSync)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_task.IsCanceled || _task.IsFaulted)
|
||||
{
|
||||
result = _task;
|
||||
}
|
||||
else
|
||||
{
|
||||
_task = DoSendAsync(_task, bytes);
|
||||
}
|
||||
|
||||
async Task DoSendAsync(Task task, byte[] bytes)
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
await _httpContext.Response.BodyWriter.WriteAsync(bytes, cancellationToken);
|
||||
await _httpContext.Response.BodyWriter.WriteAsync(_newline, cancellationToken);
|
||||
await _httpContext.Response.BodyWriter.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await result.ConfigureAwait(false);
|
||||
// This is fine.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dispatcher.Detach(this);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] bytes, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = Task.CompletedTask;
|
||||
|
||||
lock (_taskSync)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_task.IsCanceled || _task.IsFaulted)
|
||||
{
|
||||
result = _task;
|
||||
}
|
||||
else
|
||||
{
|
||||
_task = DoSendAsync(_task, bytes);
|
||||
}
|
||||
|
||||
async Task DoSendAsync(Task task, byte[] bytes)
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
await _httpContext.Response.BodyWriter.WriteAsync(bytes, cancellationToken);
|
||||
await _httpContext.Response.BodyWriter.WriteAsync(_newline, cancellationToken);
|
||||
await _httpContext.Response.BodyWriter.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await result.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,67 +7,66 @@ using System.Collections.Immutable;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
/// <summary>
|
||||
/// IDispatcher is a service interface to bridge outgoing data to the
|
||||
/// current connections.
|
||||
/// </summary>
|
||||
public class Dispatcher : IDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// IDispatcher is a service interface to bridge outgoing data to the
|
||||
/// current connections.
|
||||
/// </summary>
|
||||
public class Dispatcher : IDispatcher
|
||||
private readonly ILogger<Dispatcher> _logger;
|
||||
private readonly object _targetsSync = new object();
|
||||
private ImmutableList<IDispatchTarget> _targets = ImmutableList<IDispatchTarget>.Empty;
|
||||
private Action<IDispatchTarget> _attached;
|
||||
|
||||
public Dispatcher(ILogger<Dispatcher> logger)
|
||||
{
|
||||
private readonly ILogger<Dispatcher> _logger;
|
||||
private readonly object _targetsSync = new object();
|
||||
private ImmutableList<IDispatchTarget> _targets = ImmutableList<IDispatchTarget>.Empty;
|
||||
private Action<IDispatchTarget> _attached;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Dispatcher(ILogger<Dispatcher> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
public void OnAttach(Action<IDispatchTarget> attached)
|
||||
{
|
||||
_attached = attached;
|
||||
}
|
||||
|
||||
public void OnAttach(Action<IDispatchTarget> attached)
|
||||
{
|
||||
_attached = attached;
|
||||
}
|
||||
|
||||
public void Attach(IDispatchTarget target)
|
||||
{
|
||||
public void Attach(IDispatchTarget target)
|
||||
{
|
||||
#pragma warning disable CA1303 // Do not pass literals as localized parameters
|
||||
_logger.LogDebug("Attaching {DispatchTarget}", target?.ToString());
|
||||
_logger.LogDebug("Attaching {DispatchTarget}", target?.ToString());
|
||||
#pragma warning restore CA1303 // Do not pass literals as localized parameters
|
||||
|
||||
lock (_targetsSync)
|
||||
{
|
||||
_targets = _targets.Add(target);
|
||||
}
|
||||
|
||||
_attached?.Invoke(target);
|
||||
lock (_targetsSync)
|
||||
{
|
||||
_targets = _targets.Add(target);
|
||||
}
|
||||
|
||||
public void Detach(IDispatchTarget target)
|
||||
{
|
||||
_attached?.Invoke(target);
|
||||
}
|
||||
|
||||
public void Detach(IDispatchTarget target)
|
||||
{
|
||||
#pragma warning disable CA1303 // Do not pass literals as localized parameters
|
||||
_logger.LogDebug("Detaching {DispatchTarget}", target?.ToString());
|
||||
_logger.LogDebug("Detaching {DispatchTarget}", target?.ToString());
|
||||
#pragma warning restore CA1303 // Do not pass literals as localized parameters
|
||||
|
||||
lock (_targetsSync)
|
||||
{
|
||||
_targets = _targets.Remove(target);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(IDispatchTarget specificTarget, byte[] utf8Bytes, CancellationToken cancellationToken)
|
||||
lock (_targetsSync)
|
||||
{
|
||||
if (specificTarget != null)
|
||||
_targets = _targets.Remove(target);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(IDispatchTarget specificTarget, byte[] utf8Bytes, CancellationToken cancellationToken)
|
||||
{
|
||||
if (specificTarget != null)
|
||||
{
|
||||
await specificTarget.SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var target in _targets)
|
||||
{
|
||||
await specificTarget.SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var target in _targets)
|
||||
{
|
||||
await target.SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await target.SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
/// <summary>
|
||||
/// IDispatchTarget is what an <see cref="IDispatcher"/> will use to
|
||||
/// dispatch information.
|
||||
/// </summary>
|
||||
public interface IDispatchTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// IDispatchTarget is what an <see cref="IDispatcher"/> will use to
|
||||
/// dispatch information.
|
||||
/// </summary>
|
||||
public interface IDispatchTarget
|
||||
{
|
||||
public Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken);
|
||||
}
|
||||
public Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -5,17 +5,16 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching
|
||||
namespace Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
/// <summary>
|
||||
/// IDispatcher is a service interface to bridge outgoing data to the
|
||||
/// current connections.
|
||||
/// </summary>
|
||||
public interface IDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// IDispatcher is a service interface to bridge outgoing data to the
|
||||
/// current connections.
|
||||
/// </summary>
|
||||
public interface IDispatcher
|
||||
{
|
||||
void Attach(IDispatchTarget target);
|
||||
void Detach(IDispatchTarget target);
|
||||
void OnAttach(Action<IDispatchTarget> attached);
|
||||
Task SendAsync(IDispatchTarget specificTarget, byte[] utf8Bytes, CancellationToken cancellationToken);
|
||||
}
|
||||
void Attach(IDispatchTarget target);
|
||||
void Detach(IDispatchTarget target);
|
||||
void OnAttach(Action<IDispatchTarget> attached);
|
||||
Task SendAsync(IDispatchTarget specificTarget, byte[] utf8Bytes, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -9,32 +9,31 @@ using Serilog;
|
|||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller
|
||||
namespace Yarp.Kubernetes.Controller;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static class Program
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
using var serilog = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(theme: AnsiConsoleTheme.Code)
|
||||
.CreateLogger();
|
||||
using var serilog = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(theme: AnsiConsoleTheme.Code)
|
||||
.CreateLogger();
|
||||
|
||||
ServiceClientTracing.IsEnabled = true;
|
||||
ServiceClientTracing.IsEnabled = true;
|
||||
|
||||
await Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.ClearProviders();
|
||||
logging.AddSerilog(serilog, dispose: false);
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
})
|
||||
.Build()
|
||||
.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
await Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.ClearProviders();
|
||||
logging.AddSerilog(serilog, dispose: false);
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
})
|
||||
.Build()
|
||||
.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,15 +7,14 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
/// <summary>
|
||||
/// IReconciler is a service interface called by the <see cref="IngressController"/> to process
|
||||
/// the work items as they are dequeued.
|
||||
/// </summary>
|
||||
public interface IReconciler
|
||||
{
|
||||
/// <summary>
|
||||
/// IReconciler is a service interface called by the <see cref="IngressController"/> to process
|
||||
/// the work items as they are dequeued.
|
||||
/// </summary>
|
||||
public interface IReconciler
|
||||
{
|
||||
void OnAttach(Action<IDispatchTarget> attached);
|
||||
Task ProcessAsync(IDispatchTarget target, NamespacedName key, ReconcileData data, CancellationToken cancellationToken);
|
||||
}
|
||||
void OnAttach(Action<IDispatchTarget> attached);
|
||||
Task ProcessAsync(IDispatchTarget target, NamespacedName key, ReconcileData data, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -18,209 +18,208 @@ using Microsoft.Kubernetes.Controller.RateLimiters;
|
|||
using Yarp.Kubernetes.Controller.Caching;
|
||||
using Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Controller receives notifications from informers. The data which is needed for processing is
|
||||
/// saved in a <see cref="ICache"/> instance and resources which need to be reconciled are
|
||||
/// added to an <see cref="IRateLimitingQueue{QueueItem}"/>. The background task dequeues
|
||||
/// items and passes them to an <see cref="IReconciler"/> service for processing.
|
||||
/// </summary>
|
||||
public class IngressController : BackgroundHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller receives notifications from informers. The data which is needed for processing is
|
||||
/// saved in a <see cref="ICache"/> instance and resources which need to be reconciled are
|
||||
/// added to an <see cref="IRateLimitingQueue{QueueItem}"/>. The background task dequeues
|
||||
/// items and passes them to an <see cref="IReconciler"/> service for processing.
|
||||
/// </summary>
|
||||
public class IngressController : BackgroundHostedService
|
||||
private readonly IResourceInformerRegistration[] _registrations;
|
||||
private readonly IRateLimitingQueue<QueueItem> _queue;
|
||||
private readonly ICache _cache;
|
||||
private readonly IReconciler _reconciler;
|
||||
|
||||
public IngressController(
|
||||
ICache cache,
|
||||
IReconciler reconciler,
|
||||
IResourceInformer<V1Ingress> ingressInformer,
|
||||
IResourceInformer<V1Service> serviceInformer,
|
||||
IResourceInformer<V1Endpoints> endpointsInformer,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger<IngressController> logger)
|
||||
: base(hostApplicationLifetime, logger)
|
||||
{
|
||||
private readonly IResourceInformerRegistration[] _registrations;
|
||||
private readonly IRateLimitingQueue<QueueItem> _queue;
|
||||
private readonly ICache _cache;
|
||||
private readonly IReconciler _reconciler;
|
||||
|
||||
public IngressController(
|
||||
ICache cache,
|
||||
IReconciler reconciler,
|
||||
IResourceInformer<V1Ingress> ingressInformer,
|
||||
IResourceInformer<V1Service> serviceInformer,
|
||||
IResourceInformer<V1Endpoints> endpointsInformer,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger<IngressController> logger)
|
||||
: base(hostApplicationLifetime, logger)
|
||||
if (ingressInformer is null)
|
||||
{
|
||||
if (ingressInformer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ingressInformer));
|
||||
}
|
||||
throw new ArgumentNullException(nameof(ingressInformer));
|
||||
}
|
||||
|
||||
if (serviceInformer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceInformer));
|
||||
}
|
||||
if (serviceInformer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceInformer));
|
||||
}
|
||||
|
||||
if (endpointsInformer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointsInformer));
|
||||
}
|
||||
if (endpointsInformer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointsInformer));
|
||||
}
|
||||
|
||||
if (hostApplicationLifetime is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(hostApplicationLifetime));
|
||||
}
|
||||
if (hostApplicationLifetime is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(hostApplicationLifetime));
|
||||
}
|
||||
|
||||
if (logger is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
if (logger is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_registrations = new[]
|
||||
{
|
||||
_registrations = new[]
|
||||
{
|
||||
ingressInformer.Register(Notification),
|
||||
serviceInformer.Register(Notification),
|
||||
endpointsInformer.Register(Notification),
|
||||
};
|
||||
|
||||
_queue = new RateLimitingQueue<QueueItem>(new MaxOfRateLimiter<QueueItem>(
|
||||
new BucketRateLimiter<QueueItem>(
|
||||
limiter: new Limiter(
|
||||
limit: new Limit(perSecond: 10),
|
||||
burst: 100)),
|
||||
new ItemExponentialFailureRateLimiter<QueueItem>(
|
||||
baseDelay: TimeSpan.FromMilliseconds(5),
|
||||
maxDelay: TimeSpan.FromSeconds(10))));
|
||||
_queue = new RateLimitingQueue<QueueItem>(new MaxOfRateLimiter<QueueItem>(
|
||||
new BucketRateLimiter<QueueItem>(
|
||||
limiter: new Limiter(
|
||||
limit: new Limit(perSecond: 10),
|
||||
burst: 100)),
|
||||
new ItemExponentialFailureRateLimiter<QueueItem>(
|
||||
baseDelay: TimeSpan.FromMilliseconds(5),
|
||||
maxDelay: TimeSpan.FromSeconds(10))));
|
||||
|
||||
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
_reconciler = reconciler ?? throw new ArgumentNullException(nameof(reconciler));
|
||||
_reconciler.OnAttach(TargetAttached);
|
||||
}
|
||||
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
_reconciler = reconciler ?? throw new ArgumentNullException(nameof(reconciler));
|
||||
_reconciler.OnAttach(TargetAttached);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from resource informers, and cause queue to become shut down.
|
||||
/// </summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected override void Dispose(bool disposing)
|
||||
/// <summary>
|
||||
/// Disconnects from resource informers, and cause queue to become shut down.
|
||||
/// </summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var registration in _registrations)
|
||||
{
|
||||
registration.Dispose();
|
||||
}
|
||||
|
||||
_queue.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called each time a new connection arrives on the /api/dispatch endpoint.
|
||||
/// All of the currently-known Ingress names are queued up to be sent
|
||||
/// to the new target.
|
||||
/// </summary>
|
||||
/// <param name="target">The interface to target a connected client.</param>
|
||||
private void TargetAttached(IDispatchTarget target)
|
||||
{
|
||||
var keys = new List<NamespacedName>();
|
||||
_cache.GetKeys(keys);
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_queue.Add(new QueueItem(key, target));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the informer with real-time resource updates.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Indicates if the resource new, updated, or deleted.</param>
|
||||
/// <param name="resource">The information as provided by the Kubernets API server.</param>
|
||||
private void Notification(WatchEventType eventType, V1Ingress resource)
|
||||
{
|
||||
_cache.Update(eventType, resource);
|
||||
_queue.Add(new QueueItem(NamespacedName.From(resource), null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the informer with real-time resource updates.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Indicates if the resource new, updated, or deleted.</param>
|
||||
/// <param name="resource">The information as provided by the Kubernets API server.</param>
|
||||
private void Notification(WatchEventType eventType, V1Service resource)
|
||||
{
|
||||
_cache.Update(eventType, resource);
|
||||
_queue.Add(new QueueItem(NamespacedName.From(resource), null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the informer with real-time resource updates.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Indicates if the resource new, updated, or deleted.</param>
|
||||
/// <param name="resource">The information as provided by the Kubernets API server.</param>
|
||||
private void Notification(WatchEventType eventType, V1Endpoints resource)
|
||||
{
|
||||
var ingressNames = _cache.Update(eventType, resource);
|
||||
foreach (var ingressName in ingressNames)
|
||||
{
|
||||
_queue.Add(new QueueItem(new NamespacedName(resource.Namespace(), ingressName), null));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once at startup by the hosting infrastructure. This function must remain running
|
||||
/// for the entire lifetime of an application.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Indicates when the web application is shutting down.</param>
|
||||
/// <returns>The Task representing the async function results.</returns>
|
||||
public override async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// First wait for all informers to fully List resources before processing begins.
|
||||
foreach (var registration in _registrations)
|
||||
{
|
||||
await registration.ReadyAsync(cancellationToken).ConfigureAwait(false);
|
||||
registration.Dispose();
|
||||
}
|
||||
|
||||
// At this point we know that all of the Ingress and Endpoint caches are at least in sync
|
||||
// with cluster's state as of the start of this controller.
|
||||
_queue.Dispose();
|
||||
}
|
||||
|
||||
// Now begin one loop to process work until an application shudown is requested.
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called each time a new connection arrives on the /api/dispatch endpoint.
|
||||
/// All of the currently-known Ingress names are queued up to be sent
|
||||
/// to the new target.
|
||||
/// </summary>
|
||||
/// <param name="target">The interface to target a connected client.</param>
|
||||
private void TargetAttached(IDispatchTarget target)
|
||||
{
|
||||
var keys = new List<NamespacedName>();
|
||||
_cache.GetKeys(keys);
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_queue.Add(new QueueItem(key, target));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the informer with real-time resource updates.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Indicates if the resource new, updated, or deleted.</param>
|
||||
/// <param name="resource">The information as provided by the Kubernets API server.</param>
|
||||
private void Notification(WatchEventType eventType, V1Ingress resource)
|
||||
{
|
||||
_cache.Update(eventType, resource);
|
||||
_queue.Add(new QueueItem(NamespacedName.From(resource), null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the informer with real-time resource updates.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Indicates if the resource new, updated, or deleted.</param>
|
||||
/// <param name="resource">The information as provided by the Kubernets API server.</param>
|
||||
private void Notification(WatchEventType eventType, V1Service resource)
|
||||
{
|
||||
_cache.Update(eventType, resource);
|
||||
_queue.Add(new QueueItem(NamespacedName.From(resource), null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the informer with real-time resource updates.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Indicates if the resource new, updated, or deleted.</param>
|
||||
/// <param name="resource">The information as provided by the Kubernets API server.</param>
|
||||
private void Notification(WatchEventType eventType, V1Endpoints resource)
|
||||
{
|
||||
var ingressNames = _cache.Update(eventType, resource);
|
||||
foreach (var ingressName in ingressNames)
|
||||
{
|
||||
_queue.Add(new QueueItem(new NamespacedName(resource.Namespace(), ingressName), null));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once at startup by the hosting infrastructure. This function must remain running
|
||||
/// for the entire lifetime of an application.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Indicates when the web application is shutting down.</param>
|
||||
/// <returns>The Task representing the async function results.</returns>
|
||||
public override async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// First wait for all informers to fully List resources before processing begins.
|
||||
foreach (var registration in _registrations)
|
||||
{
|
||||
await registration.ReadyAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// At this point we know that all of the Ingress and Endpoint caches are at least in sync
|
||||
// with cluster's state as of the start of this controller.
|
||||
|
||||
// Now begin one loop to process work until an application shudown is requested.
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Dequeue the next item to process
|
||||
var (item, shutdown) = await _queue.GetAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (shutdown)
|
||||
{
|
||||
// Dequeue the next item to process
|
||||
var (item, shutdown) = await _queue.GetAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (shutdown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
// Fetch the currently known information about this Ingress
|
||||
if (_cache.TryGetReconcileData(item.NamespacedName, out var data))
|
||||
{
|
||||
// Fetch the currently known information about this Ingress
|
||||
if (_cache.TryGetReconcileData(item.NamespacedName, out var data))
|
||||
{
|
||||
#pragma warning disable CA1303 // Do not pass literals as localized parameters
|
||||
Logger.LogInformation("Processing {IngressNamespace} {IngressName}", item.NamespacedName.Namespace, item.NamespacedName.Name);
|
||||
Logger.LogInformation("Processing {IngressNamespace} {IngressName}", item.NamespacedName.Namespace, item.NamespacedName.Name);
|
||||
#pragma warning restore CA1303 // Do not pass literals as localized parameters
|
||||
|
||||
await _reconciler.ProcessAsync(item.DispatchTarget, item.NamespacedName, data, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Tell the queue to forget any exponential backoff details like attempt count
|
||||
_queue.Forget(item);
|
||||
await _reconciler.ProcessAsync(item.DispatchTarget, item.NamespacedName, data, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Tell the queue to forget any exponential backoff details like attempt count
|
||||
_queue.Forget(item);
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch
|
||||
catch
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
{
|
||||
#pragma warning disable CA1303 // Do not pass literals as localized parameters
|
||||
Logger.LogInformation("Rescheduling {IngressNamespace} {IngressName}", item.NamespacedName.Namespace, item.NamespacedName.Name);
|
||||
Logger.LogInformation("Rescheduling {IngressNamespace} {IngressName}", item.NamespacedName.Namespace, item.NamespacedName.Name);
|
||||
#pragma warning restore CA1303 // Do not pass literals as localized parameters
|
||||
|
||||
// Any failure to process this item results in being re-queued after
|
||||
// a per-item exponential backoff delay combined with
|
||||
// and an overall retry rate of 10 per second
|
||||
_queue.AddRateLimited(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// calling Done after GetAsync informs the queue
|
||||
// that the item is no longer being actively processed
|
||||
_queue.Done(item);
|
||||
}
|
||||
// Any failure to process this item results in being re-queued after
|
||||
// a per-item exponential backoff delay combined with
|
||||
// and an overall retry rate of 10 per second
|
||||
_queue.AddRateLimited(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// calling Done after GetAsync informs the queue
|
||||
// that the item is no longer being actively processed
|
||||
_queue.Done(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,54 +6,53 @@ using System.Collections.Generic;
|
|||
using Microsoft.Kubernetes;
|
||||
using Yarp.Kubernetes.Controller.Dispatching;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
/// <summary>
|
||||
/// QueueItem acts as the "Key" for the _queue to manage items.
|
||||
/// </summary>
|
||||
public struct QueueItem : IEquatable<QueueItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// QueueItem acts as the "Key" for the _queue to manage items.
|
||||
/// </summary>
|
||||
public struct QueueItem : IEquatable<QueueItem>
|
||||
public QueueItem(NamespacedName namespacedName, IDispatchTarget dispatchTarget)
|
||||
{
|
||||
public QueueItem(NamespacedName namespacedName, IDispatchTarget dispatchTarget)
|
||||
{
|
||||
NamespacedName = namespacedName;
|
||||
DispatchTarget = dispatchTarget;
|
||||
}
|
||||
NamespacedName = namespacedName;
|
||||
DispatchTarget = dispatchTarget;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This identifies an Ingress which must be dispatched because it, or a related resource, has changed.
|
||||
/// </summary>
|
||||
public NamespacedName NamespacedName { get; }
|
||||
/// <summary>
|
||||
/// This identifies an Ingress which must be dispatched because it, or a related resource, has changed.
|
||||
/// </summary>
|
||||
public NamespacedName NamespacedName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This idenitifies a single target if the work item is caused by a new connection, otherwise null
|
||||
/// if the information should be sent to all current connections.
|
||||
/// </summary>
|
||||
public IDispatchTarget DispatchTarget { get; }
|
||||
/// <summary>
|
||||
/// This idenitifies a single target if the work item is caused by a new connection, otherwise null
|
||||
/// if the information should be sent to all current connections.
|
||||
/// </summary>
|
||||
public IDispatchTarget DispatchTarget { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is QueueItem item && Equals(item);
|
||||
}
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is QueueItem item && Equals(item);
|
||||
}
|
||||
|
||||
public bool Equals(QueueItem other)
|
||||
{
|
||||
return NamespacedName.Equals(other.NamespacedName) &&
|
||||
EqualityComparer<IDispatchTarget>.Default.Equals(DispatchTarget, other.DispatchTarget);
|
||||
}
|
||||
public bool Equals(QueueItem other)
|
||||
{
|
||||
return NamespacedName.Equals(other.NamespacedName) &&
|
||||
EqualityComparer<IDispatchTarget>.Default.Equals(DispatchTarget, other.DispatchTarget);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(NamespacedName, DispatchTarget);
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(NamespacedName, DispatchTarget);
|
||||
}
|
||||
|
||||
public static bool operator ==(QueueItem left, QueueItem right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
public static bool operator ==(QueueItem left, QueueItem right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(QueueItem left, QueueItem right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
public static bool operator !=(QueueItem left, QueueItem right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,25 +4,24 @@
|
|||
using System.Collections.Generic;
|
||||
using Yarp.Kubernetes.Controller.Caching;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// ReconcileData is the information returned from <see cref="ICache.TryGetReconcileData(Microsoft.Kubernetes.NamespacedName, out ReconcileData)"/>
|
||||
/// and needed by <see cref="IReconciler.ProcessAsync(Dispatching.IDispatchTarget, Microsoft.Kubernetes.NamespacedName, ReconcileData, System.Threading.CancellationToken)"/>.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct ReconcileData
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public ReconcileData(IngressData ingress, List<ServiceData> services, List<Endpoints> endpoints)
|
||||
{
|
||||
Ingress = ingress;
|
||||
ServiceList = services;
|
||||
EndpointsList = endpoints;
|
||||
}
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
public IngressData Ingress { get; }
|
||||
public List<ServiceData> ServiceList { get; }
|
||||
public List<Endpoints> EndpointsList { get; }
|
||||
/// <summary>
|
||||
/// ReconcileData is the information returned from <see cref="ICache.TryGetReconcileData(Microsoft.Kubernetes.NamespacedName, out ReconcileData)"/>
|
||||
/// and needed by <see cref="IReconciler.ProcessAsync(Dispatching.IDispatchTarget, Microsoft.Kubernetes.NamespacedName, ReconcileData, System.Threading.CancellationToken)"/>.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct ReconcileData
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
public ReconcileData(IngressData ingress, List<ServiceData> services, List<Endpoints> endpoints)
|
||||
{
|
||||
Ingress = ingress;
|
||||
ServiceList = services;
|
||||
EndpointsList = endpoints;
|
||||
}
|
||||
|
||||
public IngressData Ingress { get; }
|
||||
public List<ServiceData> ServiceList { get; }
|
||||
public List<Endpoints> EndpointsList { get; }
|
||||
}
|
||||
|
|
|
@ -11,61 +11,60 @@ using Yarp.Kubernetes.Controller.Converters;
|
|||
using Yarp.Kubernetes.Controller.Dispatching;
|
||||
using Yarp.Kubernetes.Protocol;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller.Services
|
||||
namespace Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
/// <summary>
|
||||
/// IReconciler is a service interface called by the <see cref="IngressController"/> to process
|
||||
/// the work items as they are dequeued.
|
||||
/// </summary>
|
||||
public partial class Reconciler : IReconciler
|
||||
{
|
||||
/// <summary>
|
||||
/// IReconciler is a service interface called by the <see cref="IngressController"/> to process
|
||||
/// the work items as they are dequeued.
|
||||
/// </summary>
|
||||
public partial class Reconciler : IReconciler
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private Action<IDispatchTarget> _attached;
|
||||
private readonly ILogger<Reconciler> _logger;
|
||||
|
||||
public Reconciler(IDispatcher dispatcher, ILogger<Reconciler> logger)
|
||||
{
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private Action<IDispatchTarget> _attached;
|
||||
private readonly ILogger<Reconciler> _logger;
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_dispatcher.OnAttach(Attached);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Reconciler(IDispatcher dispatcher, ILogger<Reconciler> logger)
|
||||
{
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_dispatcher.OnAttach(Attached);
|
||||
_logger = logger;
|
||||
}
|
||||
public void OnAttach(Action<IDispatchTarget> attached)
|
||||
{
|
||||
_attached = attached;
|
||||
}
|
||||
|
||||
public void OnAttach(Action<IDispatchTarget> attached)
|
||||
{
|
||||
_attached = attached;
|
||||
}
|
||||
private void Attached(IDispatchTarget target)
|
||||
{
|
||||
_attached?.Invoke(target);
|
||||
}
|
||||
|
||||
private void Attached(IDispatchTarget target)
|
||||
public async Task ProcessAsync(IDispatchTarget target, NamespacedName key, ReconcileData data, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_attached?.Invoke(target);
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(IDispatchTarget target, NamespacedName key, ReconcileData data, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
var message = new Message
|
||||
{
|
||||
var message = new Message
|
||||
{
|
||||
MessageType = MessageType.Update,
|
||||
Key = $"{key.Namespace}:{key.Name}"
|
||||
};
|
||||
var context = new YarpIngressContext(data.Ingress, data.ServiceList, data.EndpointsList);
|
||||
YarpParser.CovertFromKubernetesIngress(context);
|
||||
MessageType = MessageType.Update,
|
||||
Key = $"{key.Namespace}:{key.Name}"
|
||||
};
|
||||
var context = new YarpIngressContext(data.Ingress, data.ServiceList, data.EndpointsList);
|
||||
YarpParser.CovertFromKubernetesIngress(context);
|
||||
|
||||
message.Cluster = context.Clusters;
|
||||
message.Routes = context.Routes;
|
||||
message.Cluster = context.Clusters;
|
||||
message.Routes = context.Routes;
|
||||
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(message);
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(message);
|
||||
|
||||
_logger.LogInformation(JsonSerializer.Serialize(message));
|
||||
_logger.LogInformation(JsonSerializer.Serialize(message));
|
||||
|
||||
await _dispatcher.SendAsync(target, bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex.Message);
|
||||
throw;
|
||||
}
|
||||
await _dispatcher.SendAsync(target, bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,52 +10,51 @@ using Yarp.Kubernetes.Controller.Caching;
|
|||
using Yarp.Kubernetes.Controller.Dispatching;
|
||||
using Yarp.Kubernetes.Controller.Services;
|
||||
|
||||
namespace Yarp.Kubernetes.Controller
|
||||
namespace Yarp.Kubernetes.Controller;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
public class Startup
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
{
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
// Add components from the kubernetes controller framework
|
||||
services.AddKubernetesControllerRuntime();
|
||||
|
||||
// Add components implemented by this application
|
||||
services.AddHostedService<IngressController>();
|
||||
services.AddSingleton<ICache, IngressCache>();
|
||||
services.AddTransient<IReconciler, Reconciler>();
|
||||
services.AddSingleton<IDispatcher, Dispatcher>();
|
||||
|
||||
// Register the necessary Kubernetes resource informers
|
||||
services.RegisterResourceInformer<V1Ingress>();
|
||||
services.RegisterResourceInformer<V1Service>();
|
||||
services.RegisterResourceInformer<V1Endpoints>();
|
||||
|
||||
// Add ASP.NET Core controller support
|
||||
services.AddControllers();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
// Add components from the kubernetes controller framework
|
||||
services.AddKubernetesControllerRuntime();
|
||||
|
||||
// Add components implemented by this application
|
||||
services.AddHostedService<IngressController>();
|
||||
services.AddSingleton<ICache, IngressCache>();
|
||||
services.AddTransient<IReconciler, Reconciler>();
|
||||
services.AddSingleton<IDispatcher, Dispatcher>();
|
||||
|
||||
// Register the necessary Kubernetes resource informers
|
||||
services.RegisterResourceInformer<V1Ingress>();
|
||||
services.RegisterResourceInformer<V1Service>();
|
||||
services.RegisterResourceInformer<V1Endpoints>();
|
||||
|
||||
// Add ASP.NET Core controller support
|
||||
services.AddControllers();
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
});
|
||||
}
|
||||
endpoints.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
|
||||
namespace Yarp.Kubernetes.Protocol
|
||||
namespace Yarp.Kubernetes.Protocol;
|
||||
|
||||
public interface IUpdateConfig
|
||||
{
|
||||
public interface IUpdateConfig
|
||||
{
|
||||
void Update(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters);
|
||||
}
|
||||
void Update(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters);
|
||||
}
|
||||
|
|
|
@ -5,28 +5,27 @@ using System.Collections.Generic;
|
|||
using System.Text.Json.Serialization;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
|
||||
namespace Yarp.Kubernetes.Protocol
|
||||
namespace Yarp.Kubernetes.Protocol;
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
public enum MessageType
|
||||
{
|
||||
Heartbeat,
|
||||
Update,
|
||||
Remove,
|
||||
}
|
||||
Heartbeat,
|
||||
Update,
|
||||
Remove,
|
||||
}
|
||||
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types
|
||||
public struct Message
|
||||
public struct Message
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public MessageType MessageType { get; set; }
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public MessageType MessageType { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
public string Key { get; set; }
|
||||
|
||||
#pragma warning disable CA2227 // Collection properties should be read only
|
||||
public List<RouteConfig> Routes { get; set; }
|
||||
public List<RouteConfig> Routes { get; set; }
|
||||
|
||||
public List<ClusterConfig> Cluster { get; set; }
|
||||
public List<ClusterConfig> Cluster { get; set; }
|
||||
#pragma warning restore CA2227 // Collection properties should be read only
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,49 +6,48 @@ using System.Threading;
|
|||
using Microsoft.Extensions.Primitives;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
|
||||
namespace Yarp.Kubernetes.Protocol
|
||||
namespace Yarp.Kubernetes.Protocol;
|
||||
|
||||
public class MessageConfigProvider : IProxyConfigProvider, IUpdateConfig
|
||||
{
|
||||
public class MessageConfigProvider : IProxyConfigProvider, IUpdateConfig
|
||||
private volatile MessageConfig _config;
|
||||
|
||||
public MessageConfigProvider()
|
||||
{
|
||||
private volatile MessageConfig _config;
|
||||
_config = new MessageConfig(null, null);
|
||||
}
|
||||
|
||||
public MessageConfigProvider()
|
||||
{
|
||||
_config = new MessageConfig(null, null);
|
||||
}
|
||||
public IProxyConfig GetConfig() => _config;
|
||||
|
||||
public IProxyConfig GetConfig() => _config;
|
||||
|
||||
public void Update(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)
|
||||
{
|
||||
var oldConfig = _config;
|
||||
_config = new MessageConfig(routes, clusters);
|
||||
oldConfig.SignalChange();
|
||||
}
|
||||
public void Update(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)
|
||||
{
|
||||
var oldConfig = _config;
|
||||
_config = new MessageConfig(routes, clusters);
|
||||
oldConfig.SignalChange();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
|
||||
private class MessageConfig : IProxyConfig
|
||||
private class MessageConfig : IProxyConfig
|
||||
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||
|
||||
public MessageConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||
Routes = routes;
|
||||
Clusters = clusters;
|
||||
ChangeToken = new CancellationChangeToken(_cts.Token);
|
||||
}
|
||||
|
||||
public MessageConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)
|
||||
{
|
||||
Routes = routes;
|
||||
Clusters = clusters;
|
||||
ChangeToken = new CancellationChangeToken(_cts.Token);
|
||||
}
|
||||
public IReadOnlyList<RouteConfig> Routes { get; }
|
||||
|
||||
public IReadOnlyList<RouteConfig> Routes { get; }
|
||||
public IReadOnlyList<ClusterConfig> Clusters { get; }
|
||||
|
||||
public IReadOnlyList<ClusterConfig> Clusters { get; }
|
||||
public IChangeToken ChangeToken { get; }
|
||||
|
||||
public IChangeToken ChangeToken { get; }
|
||||
|
||||
internal void SignalChange()
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
internal void SignalChange()
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,21 +5,20 @@ using System;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
|
||||
namespace Yarp.Kubernetes.Protocol
|
||||
{
|
||||
public static class MessageConfigProviderExtensions
|
||||
{
|
||||
public static IReverseProxyBuilder LoadFromMessages(this IReverseProxyBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
namespace Yarp.Kubernetes.Protocol;
|
||||
|
||||
var provider = new MessageConfigProvider();
|
||||
builder.Services.AddSingleton<IProxyConfigProvider>(provider);
|
||||
builder.Services.AddSingleton<IUpdateConfig>(provider);
|
||||
return builder;
|
||||
public static class MessageConfigProviderExtensions
|
||||
{
|
||||
public static IReverseProxyBuilder LoadFromMessages(this IReverseProxyBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
var provider = new MessageConfigProvider();
|
||||
builder.Services.AddSingleton<IProxyConfigProvider>(provider);
|
||||
builder.Services.AddSingleton<IUpdateConfig>(provider);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,80 +13,79 @@ using Microsoft.Extensions.Options;
|
|||
using Microsoft.Kubernetes.Controller.Hosting;
|
||||
using Microsoft.Kubernetes.Controller.Rate;
|
||||
|
||||
namespace Yarp.Kubernetes.Protocol
|
||||
namespace Yarp.Kubernetes.Protocol;
|
||||
|
||||
public class Receiver : BackgroundHostedService
|
||||
{
|
||||
public class Receiver : BackgroundHostedService
|
||||
private readonly ReceiverOptions _options;
|
||||
private readonly Limiter _limiter;
|
||||
private readonly IUpdateConfig _proxyConfigProvider;
|
||||
|
||||
public Receiver(
|
||||
IOptions<ReceiverOptions> options,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger<Receiver> logger,
|
||||
IUpdateConfig proxyConfigProvider) : base(hostApplicationLifetime, logger)
|
||||
{
|
||||
private readonly ReceiverOptions _options;
|
||||
private readonly Limiter _limiter;
|
||||
private readonly IUpdateConfig _proxyConfigProvider;
|
||||
|
||||
public Receiver(
|
||||
IOptions<ReceiverOptions> options,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger<Receiver> logger,
|
||||
IUpdateConfig proxyConfigProvider) : base(hostApplicationLifetime, logger)
|
||||
if (options is null)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
_options = options.Value;
|
||||
|
||||
// two requests per second after third failure
|
||||
_limiter = new Limiter(new Limit(2), 3);
|
||||
_proxyConfigProvider = proxyConfigProvider;
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public override async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
_options = options.Value;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await _limiter.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
// two requests per second after third failure
|
||||
_limiter = new Limiter(new Limit(2), 3);
|
||||
_proxyConfigProvider = proxyConfigProvider;
|
||||
}
|
||||
|
||||
public override async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await _limiter.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
#pragma warning disable CA1303 // Do not pass literals as localized parameters
|
||||
Logger.LogInformation("Connecting with {ControllerUrl}", _options.ControllerUrl.ToString());
|
||||
Logger.LogInformation("Connecting with {ControllerUrl}", _options.ControllerUrl.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET
|
||||
using var stream = await client.GetStreamAsync(_options.ControllerUrl, cancellationToken).ConfigureAwait(false);
|
||||
using var stream = await client.GetStreamAsync(_options.ControllerUrl, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
using var stream = await client.GetStreamAsync(_options.ControllerUrl).ConfigureAwait(false);
|
||||
using var stream = await client.GetStreamAsync(_options.ControllerUrl).ConfigureAwait(false);
|
||||
#endif
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
|
||||
using var cancellation = cancellationToken.Register(stream.Close);
|
||||
while (true)
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
|
||||
using var cancellation = cancellationToken.Register(stream.Close);
|
||||
while (true)
|
||||
{
|
||||
var json = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
var json = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var message = System.Text.Json.JsonSerializer.Deserialize<Message>(json);
|
||||
Logger.LogInformation("Received {MessageType} for {MessageKey}", message.MessageType, message.Key);
|
||||
var message = System.Text.Json.JsonSerializer.Deserialize<Message>(json);
|
||||
Logger.LogInformation("Received {MessageType} for {MessageKey}", message.MessageType, message.Key);
|
||||
|
||||
Logger.LogInformation(json);
|
||||
Logger.LogInformation(message.MessageType.ToString());
|
||||
Logger.LogInformation(json);
|
||||
Logger.LogInformation(message.MessageType.ToString());
|
||||
|
||||
if (message.MessageType == MessageType.Update)
|
||||
{
|
||||
_proxyConfigProvider.Update(message.Routes, message.Cluster);
|
||||
}
|
||||
if (message.MessageType == MessageType.Update)
|
||||
{
|
||||
_proxyConfigProvider.Update(message.Routes, message.Cluster);
|
||||
}
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
Logger.LogInformation("Stream ended: {Message}", ex.Message);
|
||||
}
|
||||
#pragma warning restore CA1303 // Do not pass literals as localized parameters
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
Logger.LogInformation("Stream ended: {Message}", ex.Message);
|
||||
}
|
||||
#pragma warning restore CA1303 // Do not pass literals as localized parameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
using System;
|
||||
|
||||
namespace Yarp.Kubernetes.Protocol
|
||||
namespace Yarp.Kubernetes.Protocol;
|
||||
|
||||
public class ReceiverOptions
|
||||
{
|
||||
public class ReceiverOptions
|
||||
{
|
||||
public Uri ControllerUrl { get; set; }
|
||||
}
|
||||
public Uri ControllerUrl { get; set; }
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<Import Project="$(MSBuildThisFileDirectory)..\..\Directory.Build.props" Condition="Exists('$(MSBuildThisFileDirectory)..\..\Directory.Build.props')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<NoWarn>$(NoWarn);CA1716</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
@ -8,188 +8,187 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Hosting
|
||||
namespace Microsoft.Kubernetes.Controller.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Class BackgroundHostedService.
|
||||
/// Implements the <see cref="IHostedService" />
|
||||
/// Implements the <see cref="IDisposable" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="IHostedService" />
|
||||
/// <seealso cref="IDisposable" />
|
||||
public abstract class BackgroundHostedService : IHostedService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BackgroundHostedService.
|
||||
/// Implements the <see cref="IHostedService" />
|
||||
/// Implements the <see cref="IDisposable" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="IHostedService" />
|
||||
/// <seealso cref="IDisposable" />
|
||||
public abstract class BackgroundHostedService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly CancellationTokenRegistration _hostApplicationStoppingRegistration;
|
||||
private readonly CancellationTokenSource _runCancellation = new CancellationTokenSource();
|
||||
private readonly string _serviceTypeName;
|
||||
private bool _disposedValue;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly CancellationTokenRegistration _hostApplicationStoppingRegistration;
|
||||
private readonly CancellationTokenSource _runCancellation = new CancellationTokenSource();
|
||||
private readonly string _serviceTypeName;
|
||||
private bool _disposedValue;
|
||||
#pragma warning disable CA2213 // Disposable fields should be disposed
|
||||
private Task _runTask;
|
||||
private Task _runTask;
|
||||
#pragma warning restore CA2213 // Disposable fields should be disposed
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundHostedService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hostApplicationLifetime">The host application lifetime.</param>
|
||||
protected BackgroundHostedService(
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger logger)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundHostedService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hostApplicationLifetime">The host application lifetime.</param>
|
||||
protected BackgroundHostedService(
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger logger)
|
||||
{
|
||||
_hostApplicationLifetime = hostApplicationLifetime ?? throw new ArgumentNullException(nameof(hostApplicationLifetime));
|
||||
Logger = logger;
|
||||
|
||||
// register the stoppingToken to become cancelled as soon as the
|
||||
// shutdown sequence is initiated.
|
||||
_hostApplicationStoppingRegistration = _hostApplicationLifetime.ApplicationStopping.Register(_runCancellation.Cancel);
|
||||
|
||||
var serviceType = GetType();
|
||||
if (serviceType.IsGenericType)
|
||||
{
|
||||
_hostApplicationLifetime = hostApplicationLifetime ?? throw new ArgumentNullException(nameof(hostApplicationLifetime));
|
||||
Logger = logger;
|
||||
|
||||
// register the stoppingToken to become cancelled as soon as the
|
||||
// shutdown sequence is initiated.
|
||||
_hostApplicationStoppingRegistration = _hostApplicationLifetime.ApplicationStopping.Register(_runCancellation.Cancel);
|
||||
|
||||
var serviceType = GetType();
|
||||
if (serviceType.IsGenericType)
|
||||
{
|
||||
_serviceTypeName = $"{serviceType.Name.Split('`').First()}<{string.Join(",", serviceType.GenericTypeArguments.Select(type => type.Name))}>";
|
||||
}
|
||||
else
|
||||
{
|
||||
_serviceTypeName = serviceType.Name;
|
||||
}
|
||||
_serviceTypeName = $"{serviceType.Name.Split('`').First()}<{string.Join(",", serviceType.GenericTypeArguments.Select(type => type.Name))}>";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
protected ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the application host is ready to start the service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
else
|
||||
{
|
||||
// fork off a new async causality line beginning with the call to RunAsync
|
||||
_runTask = Task.Run(CallRunAsync);
|
||||
_serviceTypeName = serviceType.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// the rest of the startup sequence should proceed without delay
|
||||
return Task.CompletedTask;
|
||||
/// <summary>
|
||||
/// Gets or sets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
protected ILogger Logger { get; set; }
|
||||
|
||||
// entry-point to run async background work separated from the startup sequence
|
||||
async Task CallRunAsync()
|
||||
/// <summary>
|
||||
/// Triggered when the application host is ready to start the service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// fork off a new async causality line beginning with the call to RunAsync
|
||||
_runTask = Task.Run(CallRunAsync);
|
||||
|
||||
// the rest of the startup sequence should proceed without delay
|
||||
return Task.CompletedTask;
|
||||
|
||||
// entry-point to run async background work separated from the startup sequence
|
||||
async Task CallRunAsync()
|
||||
{
|
||||
// don't bother running in case of abnormally early shutdown
|
||||
_runCancellation.Token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
// don't bother running in case of abnormally early shutdown
|
||||
_runCancellation.Token.ThrowIfCancellationRequested();
|
||||
Logger?.LogInformation(
|
||||
new EventId(1, "RunStarting"),
|
||||
"Calling RunAsync for {BackgroundHostedService}",
|
||||
_serviceTypeName);
|
||||
|
||||
try
|
||||
{
|
||||
// call the overridden method
|
||||
await RunAsync(_runCancellation.Token).ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger?.LogInformation(
|
||||
new EventId(1, "RunStarting"),
|
||||
"Calling RunAsync for {BackgroundHostedService}",
|
||||
new EventId(2, "RunComplete"),
|
||||
"RunAsync completed for {BackgroundHostedService}",
|
||||
_serviceTypeName);
|
||||
|
||||
try
|
||||
{
|
||||
// call the overridden method
|
||||
await RunAsync(_runCancellation.Token).ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger?.LogInformation(
|
||||
new EventId(2, "RunComplete"),
|
||||
"RunAsync completed for {BackgroundHostedService}",
|
||||
_serviceTypeName);
|
||||
}
|
||||
}
|
||||
catch
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!_hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||
{
|
||||
if (!_hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||
{
|
||||
// For any exception the application is instructed to tear down.
|
||||
// this would normally happen if IHostedService.StartAsync throws, so it
|
||||
// is a safe assumption the intent of an unhandled exception from background
|
||||
// RunAsync is the same.
|
||||
_hostApplicationLifetime.StopApplication();
|
||||
// For any exception the application is instructed to tear down.
|
||||
// this would normally happen if IHostedService.StartAsync throws, so it
|
||||
// is a safe assumption the intent of an unhandled exception from background
|
||||
// RunAsync is the same.
|
||||
_hostApplicationLifetime.StopApplication();
|
||||
|
||||
Logger?.LogInformation(
|
||||
new EventId(3, "RequestedStopApplication"),
|
||||
"Called StopApplication for {BackgroundHostedService}",
|
||||
_serviceTypeName);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// stop as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
|
||||
/// <returns>A <see cref="Task" /> representing the result of the asynchronous operation.</returns>
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// signal for the RunAsync call to be completed
|
||||
_runCancellation.Cancel();
|
||||
|
||||
// join the result of the RunAsync causality line back into the results of
|
||||
// this StopAsync call. this await statement will not complete until CallRunAsync
|
||||
// method has unwound and returned. if RunAsync completed by throwing an exception
|
||||
// it will be rethrown by this await. rethrown Exceptions will pass through
|
||||
// Hosting and may be caught by Program.Main.
|
||||
await _runTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// this exception is ignored - it's a natural result of cancellation token
|
||||
}
|
||||
finally
|
||||
{
|
||||
_runTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the asynchronous background work.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>A <see cref="Task" /> representing the result of the asynchronous operation.</returns>
|
||||
public abstract Task RunAsync(CancellationToken cancellationToken);
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
_runCancellation.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// ignore redundant exception to allow shutdown sequence to progress uninterupted
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_hostApplicationStoppingRegistration.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// ignore redundant exception to allow shutdown sequence to progress uninterupted
|
||||
}
|
||||
Logger?.LogInformation(
|
||||
new EventId(3, "RequestedStopApplication"),
|
||||
"Called StopApplication for {BackgroundHostedService}",
|
||||
_serviceTypeName);
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// stop as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
|
||||
/// <returns>A <see cref="Task" /> representing the result of the asynchronous operation.</returns>
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// signal for the RunAsync call to be completed
|
||||
_runCancellation.Cancel();
|
||||
|
||||
// join the result of the RunAsync causality line back into the results of
|
||||
// this StopAsync call. this await statement will not complete until CallRunAsync
|
||||
// method has unwound and returned. if RunAsync completed by throwing an exception
|
||||
// it will be rethrown by this await. rethrown Exceptions will pass through
|
||||
// Hosting and may be caught by Program.Main.
|
||||
await _runTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// this exception is ignored - it's a natural result of cancellation token
|
||||
}
|
||||
finally
|
||||
{
|
||||
_runTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the asynchronous background work.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>A <see cref="Task" /> representing the result of the asynchronous operation.</returns>
|
||||
public abstract Task RunAsync(CancellationToken cancellationToken);
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
_runCancellation.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// ignore redundant exception to allow shutdown sequence to progress uninterupted
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_hostApplicationStoppingRegistration.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// ignore redundant exception to allow shutdown sequence to progress uninterupted
|
||||
}
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,25 +5,24 @@ using Microsoft.Extensions.Hosting;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Hosting
|
||||
namespace Microsoft.Kubernetes.Controller.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Delegates start and stop calls to service-specific interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The service interface to delegate onto.</typeparam>
|
||||
public class HostedServiceAdapter<TService> : IHostedService
|
||||
where TService : IHostedService
|
||||
{
|
||||
private readonly TService _service;
|
||||
|
||||
/// <summary>
|
||||
/// Delegates start and stop calls to service-specific interface.
|
||||
/// Initializes a new instance of the <see cref="HostedServiceAdapter{TInterface}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The service interface to delegate onto.</typeparam>
|
||||
public class HostedServiceAdapter<TService> : IHostedService
|
||||
where TService : IHostedService
|
||||
{
|
||||
private readonly TService _service;
|
||||
/// <param name="service">The service interface to delegate onto.</param>
|
||||
public HostedServiceAdapter(TService service) => _service = service;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HostedServiceAdapter{TInterface}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="service">The service interface to delegate onto.</param>
|
||||
public HostedServiceAdapter(TService service) => _service = service;
|
||||
public Task StartAsync(CancellationToken cancellationToken) => _service.StartAsync(cancellationToken);
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => _service.StartAsync(cancellationToken);
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => _service.StopAsync(cancellationToken);
|
||||
}
|
||||
public Task StopAsync(CancellationToken cancellationToken) => _service.StopAsync(cancellationToken);
|
||||
}
|
||||
|
|
|
@ -5,28 +5,27 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Kubernetes.Controller.Hosting;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Class ServiceCollectionHostedServiceAdapterExtensions.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionHostedServiceAdapterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ServiceCollectionHostedServiceAdapterExtensions.
|
||||
/// Registers the hosted service.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionHostedServiceAdapterExtensions
|
||||
/// <typeparam name="TService">The type of the t service.</typeparam>
|
||||
/// <param name="services">The services.</param>
|
||||
/// <returns>IServiceCollection.</returns>
|
||||
public static IServiceCollection RegisterHostedService<TService>(this IServiceCollection services)
|
||||
where TService : IHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the hosted service.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The type of the t service.</typeparam>
|
||||
/// <param name="services">The services.</param>
|
||||
/// <returns>IServiceCollection.</returns>
|
||||
public static IServiceCollection RegisterHostedService<TService>(this IServiceCollection services)
|
||||
where TService : IHostedService
|
||||
if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(HostedServiceAdapter<TService>)))
|
||||
{
|
||||
if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(HostedServiceAdapter<TService>)))
|
||||
{
|
||||
services = services.AddHostedService<HostedServiceAdapter<TService>>();
|
||||
}
|
||||
|
||||
return services;
|
||||
services = services.AddHostedService<HostedServiceAdapter<TService>>();
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,57 +8,56 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Informers
|
||||
namespace Microsoft.Kubernetes.Controller.Informers;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for resource event notifications.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of <see cref="IKubernetesObject{V1ObjectMeta}"/> being monitored.</typeparam>
|
||||
/// <param name="eventType">The type of change event which was received.</param>
|
||||
/// <param name="resource">The instance of the resource which was received.</param>
|
||||
public delegate void ResourceInformerCallback<TResource>(WatchEventType eventType, TResource resource) where TResource : class, IKubernetesObject<V1ObjectMeta>;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IResourceInformer is a service which generates
|
||||
/// notifications for a specific type
|
||||
/// of Kubernetes object. The callback eventType informs if the notification
|
||||
/// is because it is new, modified, or has been deleted.
|
||||
/// Implements the <see cref="IHostedService" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
/// <seealso cref="IObservable{T}" />
|
||||
/// <seealso cref="IHostedService" />
|
||||
public interface IResourceInformer<TResource> : IHostedService, IResourceInformer
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Callback for resource event notifications.
|
||||
/// Registers a callback for change notification. To ensure no events are missed the registration
|
||||
/// may be created in the constructor of a dependant <see cref="IHostedService"/>. The returned
|
||||
/// registration should be disposed when the receiver is ending its work.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of <see cref="IKubernetesObject{V1ObjectMeta}"/> being monitored.</typeparam>
|
||||
/// <param name="eventType">The type of change event which was received.</param>
|
||||
/// <param name="resource">The instance of the resource which was received.</param>
|
||||
public delegate void ResourceInformerCallback<TResource>(WatchEventType eventType, TResource resource) where TResource : class, IKubernetesObject<V1ObjectMeta>;
|
||||
/// <param name="callback">The delegate that is invoked with each resource notification.</param>
|
||||
/// <returns>A registration that should be disposed to end the notifications.</returns>
|
||||
IResourceInformerRegistration Register(ResourceInformerCallback<TResource> callback);
|
||||
}
|
||||
|
||||
public interface IResourceInformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a task that can be awaited to know when the initial listing of resources is complete.
|
||||
/// Once an await on this method it is safe to assume that all of the knowledge of this resource
|
||||
/// type has been made available, and everything going forward will be updatres.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task ReadyAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Interface IResourceInformer is a service which generates
|
||||
/// notifications for a specific type
|
||||
/// of Kubernetes object. The callback eventType informs if the notification
|
||||
/// is because it is new, modified, or has been deleted.
|
||||
/// Implements the <see cref="IHostedService" />.
|
||||
/// Registers a callback for change notification. To ensure no events are missed the registration
|
||||
/// may be created in the constructor of a dependant <see cref="IHostedService"/>. The returned
|
||||
/// registration should be disposed when the receiver is ending its work.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
/// <seealso cref="IObservable{T}" />
|
||||
/// <seealso cref="IHostedService" />
|
||||
public interface IResourceInformer<TResource> : IHostedService, IResourceInformer
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a callback for change notification. To ensure no events are missed the registration
|
||||
/// may be created in the constructor of a dependant <see cref="IHostedService"/>. The returned
|
||||
/// registration should be disposed when the receiver is ending its work.
|
||||
/// </summary>
|
||||
/// <param name="callback">The delegate that is invoked with each resource notification.</param>
|
||||
/// <returns>A registration that should be disposed to end the notifications.</returns>
|
||||
IResourceInformerRegistration Register(ResourceInformerCallback<TResource> callback);
|
||||
}
|
||||
|
||||
public interface IResourceInformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a task that can be awaited to know when the initial listing of resources is complete.
|
||||
/// Once an await on this method it is safe to assume that all of the knowledge of this resource
|
||||
/// type has been made available, and everything going forward will be updatres.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task ReadyAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback for change notification. To ensure no events are missed the registration
|
||||
/// may be created in the constructor of a dependant <see cref="IHostedService"/>. The returned
|
||||
/// registration should be disposed when the receiver is ending its work.
|
||||
/// </summary>
|
||||
/// <param name="callback">The delegate that is invoked with each resource notification.</param>
|
||||
/// <returns>A registration that should be disposed to end the notifications.</returns>
|
||||
IResourceInformerRegistration Register(ResourceInformerCallback<IKubernetesObject<V1ObjectMeta>> callback);
|
||||
}
|
||||
/// <param name="callback">The delegate that is invoked with each resource notification.</param>
|
||||
/// <returns>A registration that should be disposed to end the notifications.</returns>
|
||||
IResourceInformerRegistration Register(ResourceInformerCallback<IKubernetesObject<V1ObjectMeta>> callback);
|
||||
}
|
||||
|
|
|
@ -5,21 +5,20 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Informers
|
||||
namespace Microsoft.Kubernetes.Controller.Informers;
|
||||
|
||||
/// <summary>
|
||||
/// Returned by <see cref="IResourceInformer{TResource}.Register(ResourceInformerCallback{TResource})"/> to control the lifetime of an event
|
||||
/// notification connection. Call <see cref="IDisposable.Dispose()"/> when the lifetime of the notification receiver is ending.
|
||||
/// </summary>
|
||||
public interface IResourceInformerRegistration : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Returned by <see cref="IResourceInformer{TResource}.Register(ResourceInformerCallback{TResource})"/> to control the lifetime of an event
|
||||
/// notification connection. Call <see cref="IDisposable.Dispose()"/> when the lifetime of the notification receiver is ending.
|
||||
/// Returns a task that can be awaited to know when the initial listing of resources is complete.
|
||||
/// Once an await on this method it is safe to assume that all of the knowledge of this resource
|
||||
/// type has been made available, and everything going forward will be updatres.
|
||||
/// </summary>
|
||||
public interface IResourceInformerRegistration : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a task that can be awaited to know when the initial listing of resources is complete.
|
||||
/// Once an await on this method it is safe to assume that all of the knowledge of this resource
|
||||
/// type has been made available, and everything going forward will be updatres.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task ReadyAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task ReadyAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -6,33 +6,32 @@ using k8s.Models;
|
|||
using Microsoft.Kubernetes.Controller.Informers;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class KubernetesControllerExtensions
|
||||
{
|
||||
public static class KubernetesControllerExtensions
|
||||
public static IServiceCollection AddKubernetesControllerRuntime(this IServiceCollection services)
|
||||
{
|
||||
public static IServiceCollection AddKubernetesControllerRuntime(this IServiceCollection services)
|
||||
services = services.AddKubernetesCore();
|
||||
|
||||
if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IResourceInformer<>)))
|
||||
{
|
||||
services = services.AddKubernetesCore();
|
||||
|
||||
if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IResourceInformer<>)))
|
||||
{
|
||||
services = services.AddSingleton(typeof(IResourceInformer<>), typeof(ResourceInformer<>));
|
||||
}
|
||||
|
||||
return services;
|
||||
services = services.AddSingleton(typeof(IResourceInformer<>), typeof(ResourceInformer<>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the resource informer.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t related resource.</typeparam>
|
||||
/// <param name="services">The services.</param>
|
||||
/// <returns>IServiceCollection.</returns>
|
||||
public static IServiceCollection RegisterResourceInformer<TResource>(this IServiceCollection services)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
return services
|
||||
.RegisterHostedService<IResourceInformer<TResource>>();
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the resource informer.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t related resource.</typeparam>
|
||||
/// <param name="services">The services.</param>
|
||||
/// <returns>IServiceCollection.</returns>
|
||||
public static IServiceCollection RegisterResourceInformer<TResource>(this IServiceCollection services)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
return services
|
||||
.RegisterHostedService<IResourceInformer<TResource>>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,32 +3,31 @@
|
|||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Composition
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Class DelayingQueueBase is a delegating base class for <see cref="IDelayingQueue{TItem}" /> interface. These classes are
|
||||
/// ported from go, which favors composition over inheritance, so the pattern is followed.
|
||||
/// Implements the <see cref="WorkQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IDelayingQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="WorkQueueBase{TItem}" />
|
||||
/// <seealso cref="IDelayingQueue{TItem}" />
|
||||
public abstract class DelayingQueueBase<TItem> : WorkQueueBase<TItem>, IDelayingQueue<TItem>
|
||||
{
|
||||
private readonly IDelayingQueue<TItem> _base;
|
||||
|
||||
/// <summary>
|
||||
/// Class DelayingQueueBase is a delegating base class for <see cref="IDelayingQueue{TItem}" /> interface. These classes are
|
||||
/// ported from go, which favors composition over inheritance, so the pattern is followed.
|
||||
/// Implements the <see cref="WorkQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IDelayingQueue{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="DelayingQueueBase{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="WorkQueueBase{TItem}" />
|
||||
/// <seealso cref="IDelayingQueue{TItem}" />
|
||||
public abstract class DelayingQueueBase<TItem> : WorkQueueBase<TItem>, IDelayingQueue<TItem>
|
||||
/// <param name="base">The base.</param>
|
||||
protected DelayingQueueBase(IDelayingQueue<TItem> @base)
|
||||
: base(@base)
|
||||
{
|
||||
private readonly IDelayingQueue<TItem> _base;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelayingQueueBase{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="base">The base.</param>
|
||||
protected DelayingQueueBase(IDelayingQueue<TItem> @base)
|
||||
: base(@base)
|
||||
{
|
||||
_base = @base;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddAfter(TItem item, TimeSpan delay) => _base.AddAfter(item, delay);
|
||||
_base = @base;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddAfter(TItem item, TimeSpan delay) => _base.AddAfter(item, delay);
|
||||
}
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Composition
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Class RateLimitingQueueBase is a delegating base class for <see cref="IRateLimitingQueue{TItem}" /> interface. These classes are
|
||||
/// ported from go, which favors composition over inheritance, so the pattern is followed.
|
||||
/// Implements the <see cref="DelayingQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IRateLimitingQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="DelayingQueueBase{TItem}" />
|
||||
/// <seealso cref="IRateLimitingQueue{TItem}" />
|
||||
public abstract class RateLimitingQueueBase<TItem> : DelayingQueueBase<TItem>, IRateLimitingQueue<TItem>
|
||||
{
|
||||
private readonly IRateLimitingQueue<TItem> _base;
|
||||
|
||||
/// <summary>
|
||||
/// Class RateLimitingQueueBase is a delegating base class for <see cref="IRateLimitingQueue{TItem}" /> interface. These classes are
|
||||
/// ported from go, which favors composition over inheritance, so the pattern is followed.
|
||||
/// Implements the <see cref="DelayingQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IRateLimitingQueue{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="RateLimitingQueueBase{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="DelayingQueueBase{TItem}" />
|
||||
/// <seealso cref="IRateLimitingQueue{TItem}" />
|
||||
public abstract class RateLimitingQueueBase<TItem> : DelayingQueueBase<TItem>, IRateLimitingQueue<TItem>
|
||||
/// <param name="base">The base.</param>
|
||||
protected RateLimitingQueueBase(IRateLimitingQueue<TItem> @base)
|
||||
: base(@base)
|
||||
{
|
||||
private readonly IRateLimitingQueue<TItem> _base;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RateLimitingQueueBase{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="base">The base.</param>
|
||||
protected RateLimitingQueueBase(IRateLimitingQueue<TItem> @base)
|
||||
: base(@base)
|
||||
{
|
||||
_base = @base;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void AddRateLimited(TItem item) => _base.AddRateLimited(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Forget(TItem item) => _base.Forget(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int NumRequeues(TItem item) => _base.NumRequeues(item);
|
||||
_base = @base;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void AddRateLimited(TItem item) => _base.AddRateLimited(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Forget(TItem item) => _base.Forget(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int NumRequeues(TItem item) => _base.NumRequeues(item);
|
||||
}
|
||||
|
|
|
@ -4,62 +4,61 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Composition
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Class WorkQueueBase is a delegating base class for <see cref="IWorkQueue{TItem}" /> interface. These classes are
|
||||
/// ported from go, which favors composition over inheritance, so the pattern is followed.
|
||||
/// Implements the <see cref="IWorkQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IWorkQueue{TItem}" />
|
||||
public abstract class WorkQueueBase<TItem> : IWorkQueue<TItem>
|
||||
{
|
||||
private readonly IWorkQueue<TItem> _base;
|
||||
private bool _disposedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Class WorkQueueBase is a delegating base class for <see cref="IWorkQueue{TItem}" /> interface. These classes are
|
||||
/// ported from go, which favors composition over inheritance, so the pattern is followed.
|
||||
/// Implements the <see cref="IWorkQueue{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="WorkQueueBase{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IWorkQueue{TItem}" />
|
||||
public abstract class WorkQueueBase<TItem> : IWorkQueue<TItem>
|
||||
/// <param name="workQueue">The work queue.</param>
|
||||
public WorkQueueBase(IWorkQueue<TItem> workQueue) => _base = workQueue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Add(TItem item) => _base.Add(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Done(TItem item) => _base.Done(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken) => _base.GetAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int Len() => _base.Len();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void ShutDown() => _base.ShutDown();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool ShuttingDown() => _base.ShuttingDown();
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
private readonly IWorkQueue<TItem> _base;
|
||||
private bool _disposedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkQueueBase{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="workQueue">The work queue.</param>
|
||||
public WorkQueueBase(IWorkQueue<TItem> workQueue) => _base = workQueue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Add(TItem item) => _base.Add(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Done(TItem item) => _base.Done(item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken) => _base.GetAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int Len() => _base.Len();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void ShutDown() => _base.ShutDown();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool ShuttingDown() => _base.ShuttingDown();
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
if (disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_base.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
_base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
System.GC.SuppressFinalize(this);
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
System.GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,143 +11,142 @@ using System.Threading.Tasks;
|
|||
#pragma warning disable CA1001 // Type owns disposable fields but is not disposable
|
||||
#pragma warning disable CA2213 // Disposable fields should be disposed
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues
|
||||
namespace Microsoft.Kubernetes.Controller.Queues;
|
||||
|
||||
/// <summary>
|
||||
/// Class DelayingQueue is the default implementation of <see cref="IDelayingQueue{TItem}" />.
|
||||
/// Implements the <see cref="Composition.WorkQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IDelayingQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="Composition.WorkQueueBase{TItem}" />
|
||||
/// <seealso cref="IDelayingQueue{TItem}" />
|
||||
public class DelayingQueue<TItem> : Composition.WorkQueueBase<TItem>, IDelayingQueue<TItem>
|
||||
{
|
||||
private readonly CancellationTokenSource _shuttingDown = new CancellationTokenSource();
|
||||
private readonly Channel<WaitFor> _waitingForAddCh = new Channel<WaitFor>(new List<WaitFor>());
|
||||
private readonly Task _waitingLoopTask;
|
||||
private readonly ISystemClock _clock;
|
||||
|
||||
/// <summary>
|
||||
/// Class DelayingQueue is the default implementation of <see cref="IDelayingQueue{TItem}" />.
|
||||
/// Implements the <see cref="Composition.WorkQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IDelayingQueue{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="DelayingQueue{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="Composition.WorkQueueBase{TItem}" />
|
||||
/// <seealso cref="IDelayingQueue{TItem}" />
|
||||
public class DelayingQueue<TItem> : Composition.WorkQueueBase<TItem>, IDelayingQueue<TItem>
|
||||
public DelayingQueue(ISystemClock clock = default, IWorkQueue<TItem> @base = default)
|
||||
: base(@base ?? new WorkQueue<TItem>())
|
||||
{
|
||||
private readonly CancellationTokenSource _shuttingDown = new CancellationTokenSource();
|
||||
private readonly Channel<WaitFor> _waitingForAddCh = new Channel<WaitFor>(new List<WaitFor>());
|
||||
private readonly Task _waitingLoopTask;
|
||||
private readonly ISystemClock _clock;
|
||||
_waitingLoopTask = Task.Run(WaitingLoopAsync);
|
||||
_clock = clock ?? new SystemClock(); ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelayingQueue{TItem}" /> class.
|
||||
/// </summary>
|
||||
public DelayingQueue(ISystemClock clock = default, IWorkQueue<TItem> @base = default)
|
||||
: base(@base ?? new WorkQueue<TItem>())
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
public override void ShutDown()
|
||||
{
|
||||
_shuttingDown.Cancel();
|
||||
base.ShutDown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the after.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
public virtual void AddAfter(TItem item, TimeSpan delay)
|
||||
{
|
||||
// util\workqueue\delaying_queue.go:160
|
||||
if (ShuttingDown())
|
||||
{
|
||||
_waitingLoopTask = Task.Run(WaitingLoopAsync);
|
||||
_clock = clock ?? new SystemClock(); ;
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
public override void ShutDown()
|
||||
// COUNTER: retry
|
||||
if (delay.TotalMilliseconds <= 0)
|
||||
{
|
||||
_shuttingDown.Cancel();
|
||||
base.ShutDown();
|
||||
Add(item);
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the after.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
public virtual void AddAfter(TItem item, TimeSpan delay)
|
||||
_waitingForAddCh.Push(new WaitFor { Item = item, ReadyAt = _clock.UtcNow + delay });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background loop for delaying queue. Will continuously evaluate when next
|
||||
/// items should be added. Async await sleeps until that time. Also wakes up when
|
||||
/// new items are pushed onto queue, or when <see cref="ShutDown" /> is called.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
|
||||
public async Task WaitingLoopAsync()
|
||||
{
|
||||
// util\workqueue\delaying_queue.go:187
|
||||
var waitingForQueue = new Heap<WaitFor>(
|
||||
new List<WaitFor>(),
|
||||
Comparer<WaitFor>.Create(WaitFor.Compare));
|
||||
|
||||
while (!ShuttingDown())
|
||||
{
|
||||
// util\workqueue\delaying_queue.go:160
|
||||
if (ShuttingDown())
|
||||
var now = _clock.UtcNow;
|
||||
var nextReadyAtDelay = TimeSpan.FromSeconds(10);
|
||||
|
||||
// remove and add all of the items that are readyAt by now
|
||||
// stop and calculate delay for the first one note readyAt by now
|
||||
// default to 10 seconds delay if the queue is fully emptied
|
||||
while (waitingForQueue.TryPeek(out var waitEntry))
|
||||
{
|
||||
return;
|
||||
if (waitEntry.ReadyAt > now)
|
||||
{
|
||||
nextReadyAtDelay = waitEntry.ReadyAt - now;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(waitingForQueue.Pop().Item);
|
||||
}
|
||||
}
|
||||
|
||||
// COUNTER: retry
|
||||
if (delay.TotalMilliseconds <= 0)
|
||||
// await for a delay, shuttingDown signal, or when addition items need to be queued
|
||||
var nextReadyAtDelayTask = Task.Delay(nextReadyAtDelay, _shuttingDown.Token);
|
||||
var waitingForAddChTask = _waitingForAddCh.WaitAsync();
|
||||
var whenTask = await Task.WhenAny(
|
||||
waitingForAddChTask,
|
||||
nextReadyAtDelayTask);
|
||||
|
||||
// await the condition as well, just in case there's an exception to observe
|
||||
await whenTask;
|
||||
|
||||
if (whenTask == waitingForAddChTask)
|
||||
{
|
||||
Add(item);
|
||||
return;
|
||||
}
|
||||
|
||||
_waitingForAddCh.Push(new WaitFor { Item = item, ReadyAt = _clock.UtcNow + delay });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background loop for delaying queue. Will continuously evaluate when next
|
||||
/// items should be added. Async await sleeps until that time. Also wakes up when
|
||||
/// new items are pushed onto queue, or when <see cref="ShutDown" /> is called.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
|
||||
public async Task WaitingLoopAsync()
|
||||
{
|
||||
// util\workqueue\delaying_queue.go:187
|
||||
var waitingForQueue = new Heap<WaitFor>(
|
||||
new List<WaitFor>(),
|
||||
Comparer<WaitFor>.Create(WaitFor.Compare));
|
||||
|
||||
while (!ShuttingDown())
|
||||
{
|
||||
var now = _clock.UtcNow;
|
||||
var nextReadyAtDelay = TimeSpan.FromSeconds(10);
|
||||
|
||||
// remove and add all of the items that are readyAt by now
|
||||
// stop and calculate delay for the first one note readyAt by now
|
||||
// default to 10 seconds delay if the queue is fully emptied
|
||||
while (waitingForQueue.TryPeek(out var waitEntry))
|
||||
now = _clock.UtcNow;
|
||||
while (_waitingForAddCh.TryPop(out var waitEntry))
|
||||
{
|
||||
if (waitEntry.ReadyAt > now)
|
||||
{
|
||||
nextReadyAtDelay = waitEntry.ReadyAt - now;
|
||||
break;
|
||||
waitingForQueue.Push(waitEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(waitingForQueue.Pop().Item);
|
||||
}
|
||||
}
|
||||
|
||||
// await for a delay, shuttingDown signal, or when addition items need to be queued
|
||||
var nextReadyAtDelayTask = Task.Delay(nextReadyAtDelay, _shuttingDown.Token);
|
||||
var waitingForAddChTask = _waitingForAddCh.WaitAsync();
|
||||
var whenTask = await Task.WhenAny(
|
||||
waitingForAddChTask,
|
||||
nextReadyAtDelayTask);
|
||||
|
||||
// await the condition as well, just in case there's an exception to observe
|
||||
await whenTask;
|
||||
|
||||
if (whenTask == waitingForAddChTask)
|
||||
{
|
||||
now = _clock.UtcNow;
|
||||
while (_waitingForAddCh.TryPop(out var waitEntry))
|
||||
{
|
||||
if (waitEntry.ReadyAt > now)
|
||||
{
|
||||
waitingForQueue.Push(waitEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(waitEntry.Item);
|
||||
}
|
||||
Add(waitEntry.Item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct WaitFor holds an item while queued for a time in the future.
|
||||
/// </summary>
|
||||
private struct WaitFor
|
||||
{
|
||||
/// <summary>
|
||||
/// The item to be queued in the future.
|
||||
/// </summary>
|
||||
public TItem Item;
|
||||
|
||||
/// <summary>
|
||||
/// Struct WaitFor holds an item while queued for a time in the future.
|
||||
/// The soonest UTC time the item is ready to be queued.
|
||||
/// </summary>
|
||||
private struct WaitFor
|
||||
{
|
||||
/// <summary>
|
||||
/// The item to be queued in the future.
|
||||
/// </summary>
|
||||
public TItem Item;
|
||||
public DateTimeOffset ReadyAt;
|
||||
|
||||
/// <summary>
|
||||
/// The soonest UTC time the item is ready to be queued.
|
||||
/// </summary>
|
||||
public DateTimeOffset ReadyAt;
|
||||
|
||||
public static int Compare(WaitFor first, WaitFor second) => DateTimeOffset.Compare(first.ReadyAt, second.ReadyAt);
|
||||
}
|
||||
public static int Compare(WaitFor first, WaitFor second) => DateTimeOffset.Compare(first.ReadyAt, second.ReadyAt);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,21 +3,20 @@
|
|||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues
|
||||
namespace Microsoft.Kubernetes.Controller.Queues;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IDelayingQueue
|
||||
/// Implements the <see cref="IWorkQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IWorkQueue{TItem}" />
|
||||
public interface IDelayingQueue<TItem> : IWorkQueue<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IDelayingQueue
|
||||
/// Implements the <see cref="IWorkQueue{TItem}" />.
|
||||
/// Adds the after.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IWorkQueue{TItem}" />
|
||||
public interface IDelayingQueue<TItem> : IWorkQueue<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the after.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
void AddAfter(TItem item, TimeSpan delay);
|
||||
}
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
void AddAfter(TItem item, TimeSpan delay);
|
||||
}
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues
|
||||
namespace Microsoft.Kubernetes.Controller.Queues;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IRateLimitingQueue
|
||||
/// Implements the <see cref="IDelayingQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IDelayingQueue{TItem}" />
|
||||
public interface IRateLimitingQueue<TItem> : IDelayingQueue<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IRateLimitingQueue
|
||||
/// Implements the <see cref="IDelayingQueue{TItem}" />.
|
||||
/// AddRateLimited adds an item to the workqueue after the rate limiter says it's ok.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IDelayingQueue{TItem}" />
|
||||
public interface IRateLimitingQueue<TItem> : IDelayingQueue<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// AddRateLimited adds an item to the workqueue after the rate limiter says it's ok.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void AddRateLimited(TItem item);
|
||||
/// <param name="item">The item.</param>
|
||||
void AddRateLimited(TItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether it's for perm failing
|
||||
/// or for success, we'll stop the rate limiter from tracking it. This only clears the `rateLimiter`, you
|
||||
/// still have to call `Done` on the queue.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Forget(TItem item);
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether it's for perm failing
|
||||
/// or for success, we'll stop the rate limiter from tracking it. This only clears the `rateLimiter`, you
|
||||
/// still have to call `Done` on the queue.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Forget(TItem item);
|
||||
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many times the item was requeued.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
int NumRequeues(TItem item);
|
||||
}
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many times the item was requeued.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
int NumRequeues(TItem item);
|
||||
}
|
||||
|
|
|
@ -5,53 +5,52 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues
|
||||
namespace Microsoft.Kubernetes.Controller.Queues;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IWorkQueue holds a series of work item objects. When objects are removed from the queue they are noted
|
||||
/// as well in a processing set. If new items arrive while processing they are not added to the queue until
|
||||
/// the processing of that item is <see cref="Done" />. In this way processing the same item twice simultaneously due to
|
||||
/// incoming event notifications is not possible.
|
||||
/// Ported from https://github.com/kubernetes/client-go/blob/master/util/workqueue/queue.go.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
public interface IWorkQueue<TItem> : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IWorkQueue holds a series of work item objects. When objects are removed from the queue they are noted
|
||||
/// as well in a processing set. If new items arrive while processing they are not added to the queue until
|
||||
/// the processing of that item is <see cref="Done" />. In this way processing the same item twice simultaneously due to
|
||||
/// incoming event notifications is not possible.
|
||||
/// Ported from https://github.com/kubernetes/client-go/blob/master/util/workqueue/queue.go.
|
||||
/// Adds the specified item.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
public interface IWorkQueue<TItem> : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Add(TItem item);
|
||||
/// <param name="item">The item.</param>
|
||||
void Add(TItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Returns number of items actively waiting in queue.
|
||||
/// </summary>
|
||||
/// <returns>System.Int32.</returns>
|
||||
int Len();
|
||||
/// <summary>
|
||||
/// Returns number of items actively waiting in queue.
|
||||
/// </summary>
|
||||
/// <returns>System.Int32.</returns>
|
||||
int Len();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next item in the queue once it is available.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task<System.ValueTuple<TItem, System.Boolean>>.</returns>
|
||||
Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Gets the next item in the queue once it is available.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task<System.ValueTuple<TItem, System.Boolean>>.</returns>
|
||||
Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called after <see cref="GetAsync"/> to inform the queue that the item
|
||||
/// processing is complete.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Done(TItem item);
|
||||
/// <summary>
|
||||
/// Called after <see cref="GetAsync"/> to inform the queue that the item
|
||||
/// processing is complete.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Done(TItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
void ShutDown();
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
void ShutDown();
|
||||
|
||||
/// <summary>
|
||||
/// Shuttings down.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool ShuttingDown();
|
||||
}
|
||||
/// <summary>
|
||||
/// Shuttings down.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool ShuttingDown();
|
||||
}
|
||||
|
|
|
@ -3,58 +3,57 @@
|
|||
|
||||
using Microsoft.Kubernetes.Controller.RateLimiters;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues
|
||||
namespace Microsoft.Kubernetes.Controller.Queues;
|
||||
|
||||
/// <summary>
|
||||
/// Class RateLimitingQueue is the default implemenation of <see cref="IRateLimitingQueue{TItem}" /> interface.
|
||||
/// Implements the <see cref="Composition.DelayingQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IRateLimitingQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="Composition.DelayingQueueBase{TItem}" />
|
||||
/// <seealso cref="IRateLimitingQueue{TItem}" />
|
||||
public class RateLimitingQueue<TItem> : Composition.DelayingQueueBase<TItem>, IRateLimitingQueue<TItem>
|
||||
{
|
||||
private readonly IRateLimiter<TItem> _rateLimiter;
|
||||
|
||||
/// <summary>
|
||||
/// Class RateLimitingQueue is the default implemenation of <see cref="IRateLimitingQueue{TItem}" /> interface.
|
||||
/// Implements the <see cref="Composition.DelayingQueueBase{TItem}" />.
|
||||
/// Implements the <see cref="IRateLimitingQueue{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="RateLimitingQueue{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="Composition.DelayingQueueBase{TItem}" />
|
||||
/// <seealso cref="IRateLimitingQueue{TItem}" />
|
||||
public class RateLimitingQueue<TItem> : Composition.DelayingQueueBase<TItem>, IRateLimitingQueue<TItem>
|
||||
/// <param name="rateLimiter">The rate limiter.</param>
|
||||
public RateLimitingQueue(IRateLimiter<TItem> rateLimiter, IDelayingQueue<TItem> @base = default)
|
||||
: base(@base ?? new DelayingQueue<TItem>())
|
||||
{
|
||||
private readonly IRateLimiter<TItem> _rateLimiter;
|
||||
_rateLimiter = rateLimiter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RateLimitingQueue{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="rateLimiter">The rate limiter.</param>
|
||||
public RateLimitingQueue(IRateLimiter<TItem> rateLimiter, IDelayingQueue<TItem> @base = default)
|
||||
: base(@base ?? new DelayingQueue<TItem>())
|
||||
{
|
||||
_rateLimiter = rateLimiter;
|
||||
}
|
||||
/// <summary>
|
||||
/// AddRateLimited adds an item to the workqueue after the rate limiter says it's ok.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void AddRateLimited(TItem item)
|
||||
{
|
||||
AddAfter(item, _rateLimiter.ItemDelay(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AddRateLimited adds an item to the workqueue after the rate limiter says it's ok.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void AddRateLimited(TItem item)
|
||||
{
|
||||
AddAfter(item, _rateLimiter.ItemDelay(item));
|
||||
}
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether it's for perm failing
|
||||
/// or for success, we'll stop the rate limiter from tracking it. This only clears the `rateLimiter`, you
|
||||
/// still have to call `Done` on the queue.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Forget(TItem item)
|
||||
{
|
||||
_rateLimiter.Forget(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether it's for perm failing
|
||||
/// or for success, we'll stop the rate limiter from tracking it. This only clears the `rateLimiter`, you
|
||||
/// still have to call `Done` on the queue.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Forget(TItem item)
|
||||
{
|
||||
_rateLimiter.Forget(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numbers the requeues.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int NumRequeues(TItem item)
|
||||
{
|
||||
return _rateLimiter.NumRequeues(item);
|
||||
}
|
||||
/// <summary>
|
||||
/// Numbers the requeues.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int NumRequeues(TItem item)
|
||||
{
|
||||
return _rateLimiter.NumRequeues(item);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,111 +4,110 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Utils
|
||||
namespace Microsoft.Kubernetes.Controller.Queues.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Class Channel is a utility to facilitate pushing data from one thread to
|
||||
/// be popped by another async thread.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item this channel contains.</typeparam>
|
||||
public class Channel<T>
|
||||
{
|
||||
private readonly IList<T> _collection;
|
||||
private readonly object _sync = new object();
|
||||
private TaskCompletionSource<int> _tcs = new TaskCompletionSource<int>();
|
||||
|
||||
/// <summary>
|
||||
/// Class Channel is a utility to facilitate pushing data from one thread to
|
||||
/// be popped by another async thread.
|
||||
/// Initializes a new instance of the <see cref="Channel{T}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item this channel contains.</typeparam>
|
||||
public class Channel<T>
|
||||
/// <param name="collection">The collection.</param>
|
||||
public Channel(IList<T> collection)
|
||||
{
|
||||
private readonly IList<T> _collection;
|
||||
private readonly object _sync = new object();
|
||||
private TaskCompletionSource<int> _tcs = new TaskCompletionSource<int>();
|
||||
_collection = collection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Channel{T}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="collection">The collection.</param>
|
||||
public Channel(IList<T> collection)
|
||||
{
|
||||
_collection = collection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of elements contained in the <see cref="Channel{T}" />.
|
||||
/// </summary>
|
||||
/// <value>The number of elements.</value>
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _collection.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified item onto the channel. If the channel
|
||||
/// was previously empty the async Task is also marked compete to
|
||||
/// unblock an awaiting thread.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Push(T item)
|
||||
/// <summary>
|
||||
/// Gets the number of elements contained in the <see cref="Channel{T}" />.
|
||||
/// </summary>
|
||||
/// <value>The number of elements.</value>
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_collection.Add(item);
|
||||
if (_collection.Count == 1)
|
||||
{
|
||||
_tcs.SetResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pops an item off of the channel. Must not be called when channel is empty.
|
||||
/// </summary>
|
||||
/// <returns>The item.</returns>
|
||||
public T Pop()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
int index = _collection.Count - 1;
|
||||
var item = _collection[index];
|
||||
_collection.RemoveAt(index);
|
||||
if (index == 0)
|
||||
{
|
||||
_tcs = new TaskCompletionSource<int>();
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to Pop an item off of the channel.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if item is returned, <c>false</c> otherwise.</returns>
|
||||
public bool TryPop(out T item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_collection.Count == 0)
|
||||
{
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
item = Pop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to know when data is available on the channel.
|
||||
/// </summary>
|
||||
/// <returns>An awaitable Task.</returns>
|
||||
public Task WaitAsync()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _tcs.Task;
|
||||
return _collection.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified item onto the channel. If the channel
|
||||
/// was previously empty the async Task is also marked compete to
|
||||
/// unblock an awaiting thread.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Push(T item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_collection.Add(item);
|
||||
if (_collection.Count == 1)
|
||||
{
|
||||
_tcs.SetResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pops an item off of the channel. Must not be called when channel is empty.
|
||||
/// </summary>
|
||||
/// <returns>The item.</returns>
|
||||
public T Pop()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
int index = _collection.Count - 1;
|
||||
var item = _collection[index];
|
||||
_collection.RemoveAt(index);
|
||||
if (index == 0)
|
||||
{
|
||||
_tcs = new TaskCompletionSource<int>();
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to Pop an item off of the channel.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if item is returned, <c>false</c> otherwise.</returns>
|
||||
public bool TryPop(out T item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_collection.Count == 0)
|
||||
{
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
item = Pop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to know when data is available on the channel.
|
||||
/// </summary>
|
||||
/// <returns>An awaitable Task.</returns>
|
||||
public Task WaitAsync()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,156 +3,155 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues
|
||||
namespace Microsoft.Kubernetes.Controller.Queues;
|
||||
|
||||
/// <summary>
|
||||
/// Class Heap is a list adapter structure that can add data into the
|
||||
/// collection by <see cref="Push" /> in a way that can be removed in sorted
|
||||
/// order by <see cref="Pop" />.
|
||||
/// Package heap provides heap operations for any type that implements
|
||||
/// heap.Interface. A heap is a tree with the property that each node is the
|
||||
/// minimum-valued node in its subtree.
|
||||
/// The minimum element in the tree is the root, at index 0.
|
||||
/// A heap is a common way to implement a priority queue. To build a priority
|
||||
/// queue, implement the Heap interface with the (negative) priority as the
|
||||
/// ordering for the Less method, so Push adds items while Pop removes the
|
||||
/// highest-priority item from the queue. The Examples include such an
|
||||
/// implementation; the file example_pq_test.go has the complete source.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item on the heap.</typeparam>
|
||||
public class Heap<T>
|
||||
{
|
||||
private readonly IList<T> _list;
|
||||
private readonly IComparer<T> _comparer;
|
||||
|
||||
/// <summary>
|
||||
/// Class Heap is a list adapter structure that can add data into the
|
||||
/// collection by <see cref="Push" /> in a way that can be removed in sorted
|
||||
/// order by <see cref="Pop" />.
|
||||
/// Package heap provides heap operations for any type that implements
|
||||
/// heap.Interface. A heap is a tree with the property that each node is the
|
||||
/// minimum-valued node in its subtree.
|
||||
/// The minimum element in the tree is the root, at index 0.
|
||||
/// A heap is a common way to implement a priority queue. To build a priority
|
||||
/// queue, implement the Heap interface with the (negative) priority as the
|
||||
/// ordering for the Less method, so Push adds items while Pop removes the
|
||||
/// highest-priority item from the queue. The Examples include such an
|
||||
/// implementation; the file example_pq_test.go has the complete source.
|
||||
/// Initializes a new instance of the <see cref="Heap{T}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item on the heap.</typeparam>
|
||||
public class Heap<T>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="comparer">The comparer.</param>
|
||||
public Heap(IList<T> list, IComparer<T> comparer)
|
||||
{
|
||||
private readonly IList<T> _list;
|
||||
private readonly IComparer<T> _comparer;
|
||||
_list = list;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Heap{T}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="comparer">The comparer.</param>
|
||||
public Heap(IList<T> list, IComparer<T> comparer)
|
||||
/// <summary>
|
||||
/// Gets the count.
|
||||
/// </summary>
|
||||
/// <value>The count.</value>
|
||||
public int Count => _list.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Push pushes the element x onto the heap.
|
||||
/// The complexity is O(log n) where n = h.Len().
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Push(T item)
|
||||
{
|
||||
_list.Add(item);
|
||||
Up(_list.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pop removes and returns the minimum element (according to Less) from the heap.
|
||||
/// The complexity is O(log n) where n = h.Len().
|
||||
/// Pop is equivalent to Remove(h, 0).
|
||||
/// </summary>
|
||||
/// <returns>The minimum item.</returns>
|
||||
public T Pop()
|
||||
{
|
||||
var n = _list.Count - 1;
|
||||
Swap(0, n);
|
||||
Down(0, n);
|
||||
var item = _list[_list.Count - 1];
|
||||
_list.RemoveAt(_list.Count - 1);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum element without removing from the collection.
|
||||
/// </summary>
|
||||
/// <returns>The minimum item.</returns>
|
||||
public T Peek()
|
||||
{
|
||||
return _list[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum element if available without removing from the collection.
|
||||
/// </summary>
|
||||
/// <param name="item">The minimum item.</param>
|
||||
/// <returns><c>true</c> if item is available, <c>false</c> otherwise.</returns>
|
||||
public bool TryPeek(out T item)
|
||||
{
|
||||
if (Count > 0)
|
||||
{
|
||||
_list = list;
|
||||
_comparer = comparer;
|
||||
item = Peek();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count.
|
||||
/// </summary>
|
||||
/// <value>The count.</value>
|
||||
public int Count => _list.Count;
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push pushes the element x onto the heap.
|
||||
/// The complexity is O(log n) where n = h.Len().
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Push(T item)
|
||||
{
|
||||
_list.Add(item);
|
||||
Up(_list.Count - 1);
|
||||
}
|
||||
private void Swap(int i, int j)
|
||||
{
|
||||
(_list[i], _list[j]) = (_list[j], _list[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pop removes and returns the minimum element (according to Less) from the heap.
|
||||
/// The complexity is O(log n) where n = h.Len().
|
||||
/// Pop is equivalent to Remove(h, 0).
|
||||
/// </summary>
|
||||
/// <returns>The minimum item.</returns>
|
||||
public T Pop()
|
||||
{
|
||||
var n = _list.Count - 1;
|
||||
Swap(0, n);
|
||||
Down(0, n);
|
||||
var item = _list[_list.Count - 1];
|
||||
_list.RemoveAt(_list.Count - 1);
|
||||
return item;
|
||||
}
|
||||
private bool Less(int j, int i)
|
||||
{
|
||||
return _comparer.Compare(_list[j], _list[i]) < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum element without removing from the collection.
|
||||
/// </summary>
|
||||
/// <returns>The minimum item.</returns>
|
||||
public T Peek()
|
||||
private void Up(int j)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
return _list[0];
|
||||
}
|
||||
int i = (j - 1) / 2; // parent
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum element if available without removing from the collection.
|
||||
/// </summary>
|
||||
/// <param name="item">The minimum item.</param>
|
||||
/// <returns><c>true</c> if item is available, <c>false</c> otherwise.</returns>
|
||||
public bool TryPeek(out T item)
|
||||
{
|
||||
if (Count > 0)
|
||||
if (i == j || !Less(j, i))
|
||||
{
|
||||
item = Peek();
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Swap(int i, int j)
|
||||
{
|
||||
(_list[i], _list[j]) = (_list[j], _list[i]);
|
||||
}
|
||||
|
||||
private bool Less(int j, int i)
|
||||
{
|
||||
return _comparer.Compare(_list[j], _list[i]) < 0;
|
||||
}
|
||||
|
||||
private void Up(int j)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
int i = (j - 1) / 2; // parent
|
||||
|
||||
if (i == j || !Less(j, i))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Swap(i, j);
|
||||
j = i;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Down(int i0, int n)
|
||||
{
|
||||
int i = i0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int j1 = 2 * i + 1;
|
||||
|
||||
if (j1 >= n || j1 < 0)
|
||||
{
|
||||
// j1 < 0 after int overflow
|
||||
break;
|
||||
}
|
||||
|
||||
int j = j1; // left child
|
||||
|
||||
int j2 = j1 + 1;
|
||||
if (j2 < n && Less(j2, j1))
|
||||
{
|
||||
j = j2; // = 2*i + 2 // right child
|
||||
}
|
||||
|
||||
if (!Less(j, i))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Swap(i, j);
|
||||
i = j;
|
||||
}
|
||||
|
||||
return i > i0;
|
||||
Swap(i, j);
|
||||
j = i;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Down(int i0, int n)
|
||||
{
|
||||
int i = i0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int j1 = 2 * i + 1;
|
||||
|
||||
if (j1 >= n || j1 < 0)
|
||||
{
|
||||
// j1 < 0 after int overflow
|
||||
break;
|
||||
}
|
||||
|
||||
int j = j1; // left child
|
||||
|
||||
int j2 = j1 + 1;
|
||||
if (j2 < n && Less(j2, j1))
|
||||
{
|
||||
j = j2; // = 2*i + 2 // right child
|
||||
}
|
||||
|
||||
if (!Less(j, i))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Swap(i, j);
|
||||
i = j;
|
||||
}
|
||||
|
||||
return i > i0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,169 +9,168 @@ using System.Threading.Tasks;
|
|||
#pragma warning disable CA2213 // Disposable fields should be disposed
|
||||
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Queues
|
||||
namespace Microsoft.Kubernetes.Controller.Queues;
|
||||
|
||||
/// <summary>
|
||||
/// Class WorkQueue is the default implementation of a work queue.
|
||||
/// Implements the <see cref="IWorkQueue{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IWorkQueue{TItem}" />
|
||||
public class WorkQueue<TItem> : IWorkQueue<TItem>
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<TItem, object> _dirty = new Dictionary<TItem, object>();
|
||||
private readonly Dictionary<TItem, object> _processing = new Dictionary<TItem, object>();
|
||||
private readonly Queue<TItem> _queue = new Queue<TItem>();
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0);
|
||||
private readonly CancellationTokenSource _shuttingDown = new CancellationTokenSource();
|
||||
private bool _disposedValue = false; // To detect redundant calls
|
||||
|
||||
/// <summary>
|
||||
/// Class WorkQueue is the default implementation of a work queue.
|
||||
/// Implements the <see cref="IWorkQueue{TItem}" />.
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IWorkQueue{TItem}" />
|
||||
public class WorkQueue<TItem> : IWorkQueue<TItem>
|
||||
public void Dispose()
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<TItem, object> _dirty = new Dictionary<TItem, object>();
|
||||
private readonly Dictionary<TItem, object> _processing = new Dictionary<TItem, object>();
|
||||
private readonly Queue<TItem> _queue = new Queue<TItem>();
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0);
|
||||
private readonly CancellationTokenSource _shuttingDown = new CancellationTokenSource();
|
||||
private bool _disposedValue = false; // To detect redundant calls
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// Adds the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Add(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
if (_shuttingDown.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dirty.ContainsKey(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dirty.Add(item, null);
|
||||
if (_processing.ContainsKey(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_queue.Enqueue(item);
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Add(TItem item)
|
||||
/// <summary>
|
||||
/// Gets the specified cancellation token.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task<System.ValueTuple<TItem, System.Boolean>>.</returns>
|
||||
public async Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shuttingDown.Token))
|
||||
{
|
||||
lock (_sync)
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (_shuttingDown.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
return (default, true);
|
||||
}
|
||||
|
||||
if (_dirty.ContainsKey(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
_dirty.Add(item, null);
|
||||
if (_processing.ContainsKey(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
lock (_sync)
|
||||
{
|
||||
if (_queue.Count == 0 || _shuttingDown.IsCancellationRequested)
|
||||
{
|
||||
_semaphore.Release();
|
||||
return (default, true);
|
||||
}
|
||||
|
||||
var item = _queue.Dequeue();
|
||||
|
||||
_processing.Add(item, null);
|
||||
_dirty.Remove(item);
|
||||
|
||||
return (item, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dones the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Done(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_processing.Remove(item);
|
||||
if (_dirty.ContainsKey(item))
|
||||
{
|
||||
_queue.Enqueue(item);
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified cancellation token.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task<System.ValueTuple<TItem, System.Boolean>>.</returns>
|
||||
public async Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// Lengthes this instance.
|
||||
/// </summary>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int Len()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shuttingDown.Token))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (_shuttingDown.IsCancellationRequested)
|
||||
{
|
||||
return (default, true);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_queue.Count == 0 || _shuttingDown.IsCancellationRequested)
|
||||
{
|
||||
_semaphore.Release();
|
||||
return (default, true);
|
||||
}
|
||||
|
||||
var item = _queue.Dequeue();
|
||||
|
||||
_processing.Add(item, null);
|
||||
_dirty.Remove(item);
|
||||
|
||||
return (item, false);
|
||||
}
|
||||
return _queue.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dones the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Done(TItem item)
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
public void ShutDown()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_processing.Remove(item);
|
||||
if (_dirty.ContainsKey(item))
|
||||
{
|
||||
_queue.Enqueue(item);
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
_shuttingDown.Cancel();
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lengthes this instance.
|
||||
/// </summary>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int Len()
|
||||
/// <summary>
|
||||
/// Shuttings down.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
public bool ShuttingDown()
|
||||
{
|
||||
return _shuttingDown.IsCancellationRequested;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
lock (_sync)
|
||||
if (disposing)
|
||||
{
|
||||
return _queue.Count;
|
||||
_semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
public void ShutDown()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_shuttingDown.Cancel();
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuttings down.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
public bool ShuttingDown()
|
||||
{
|
||||
return _shuttingDown.IsCancellationRequested;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_semaphore.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,110 +4,109 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Rate
|
||||
namespace Microsoft.Kubernetes.Controller.Rate;
|
||||
|
||||
/// <summary>
|
||||
/// Struct Limit defines the maximum frequency of some events.
|
||||
/// Limit is represented as number of events per second.
|
||||
/// A zero Limit allows no events.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L19
|
||||
/// Implements the <see cref="IEquatable{T}" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="IEquatable{T}" />
|
||||
public struct Limit : IEquatable<Limit>
|
||||
{
|
||||
private readonly double _tokensPerSecond;
|
||||
|
||||
/// <summary>
|
||||
/// Struct Limit defines the maximum frequency of some events.
|
||||
/// Limit is represented as number of events per second.
|
||||
/// A zero Limit allows no events.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L19
|
||||
/// Implements the <see cref="IEquatable{T}" />.
|
||||
/// Initializes a new instance of the <see cref="Limit"/> struct.
|
||||
/// </summary>
|
||||
/// <seealso cref="IEquatable{T}" />
|
||||
public struct Limit : IEquatable<Limit>
|
||||
/// <param name="perSecond">The per second.</param>
|
||||
public Limit(double perSecond)
|
||||
{
|
||||
private readonly double _tokensPerSecond;
|
||||
_tokensPerSecond = perSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Limit"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="perSecond">The per second.</param>
|
||||
public Limit(double perSecond)
|
||||
{
|
||||
_tokensPerSecond = perSecond;
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets a predefined maximum <see cref="Limit"/>.
|
||||
/// </summary>
|
||||
/// <value>The maximum.</value>
|
||||
public static Limit Max { get; } = new Limit(double.MaxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a predefined maximum <see cref="Limit"/>.
|
||||
/// </summary>
|
||||
/// <value>The maximum.</value>
|
||||
public static Limit Max { get; } = new Limit(double.MaxValue);
|
||||
/// <summary>
|
||||
/// Implements the == operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator ==(Limit left, Limit right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the == operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator ==(Limit left, Limit right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
/// <summary>
|
||||
/// Implements the != operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator !=(Limit left, Limit right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the != operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator !=(Limit left, Limit right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
/// <summary>
|
||||
/// TokensFromDuration is a unit conversion function from a time duration to the number of tokens
|
||||
/// which could be accumulated during that duration at a rate of limit tokens per second.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L396.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration.</param>
|
||||
/// <returns>System.Double.</returns>
|
||||
public double TokensFromDuration(TimeSpan duration)
|
||||
{
|
||||
var sec = duration.Ticks / TimeSpan.TicksPerSecond * _tokensPerSecond;
|
||||
var nsec = duration.Ticks % TimeSpan.TicksPerSecond * _tokensPerSecond;
|
||||
return sec + nsec / TimeSpan.TicksPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TokensFromDuration is a unit conversion function from a time duration to the number of tokens
|
||||
/// which could be accumulated during that duration at a rate of limit tokens per second.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L396.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration.</param>
|
||||
/// <returns>System.Double.</returns>
|
||||
public double TokensFromDuration(TimeSpan duration)
|
||||
{
|
||||
var sec = duration.Ticks / TimeSpan.TicksPerSecond * _tokensPerSecond;
|
||||
var nsec = duration.Ticks % TimeSpan.TicksPerSecond * _tokensPerSecond;
|
||||
return sec + nsec / TimeSpan.TicksPerSecond;
|
||||
}
|
||||
/// <summary>
|
||||
/// Durations from tokens is a unit conversion function from the number of tokens to the duration
|
||||
/// of time it takes to accumulate them at a rate of limit tokens per second.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L389.
|
||||
/// </summary>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan DurationFromTokens(double tokens)
|
||||
{
|
||||
return TimeSpan.FromSeconds(tokens / _tokensPerSecond);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Durations from tokens is a unit conversion function from the number of tokens to the duration
|
||||
/// of time it takes to accumulate them at a rate of limit tokens per second.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L389.
|
||||
/// </summary>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan DurationFromTokens(double tokens)
|
||||
{
|
||||
return TimeSpan.FromSeconds(tokens / _tokensPerSecond);
|
||||
}
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="object" /> is equal to this instance.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Limit limit && Equals(limit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="object" /> is equal to this instance.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Limit limit && Equals(limit);
|
||||
}
|
||||
/// <summary>
|
||||
/// Indicates whether the current object is equal to another object of the same type.
|
||||
/// </summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <returns><see langword="true" /> if the current object is equal to the <paramref name="other" /> parameter; otherwise, <see langword="false" />.</returns>
|
||||
public bool Equals([AllowNull] Limit other)
|
||||
{
|
||||
return _tokensPerSecond == other._tokensPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the current object is equal to another object of the same type.
|
||||
/// </summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <returns><see langword="true" /> if the current object is equal to the <paramref name="other" /> parameter; otherwise, <see langword="false" />.</returns>
|
||||
public bool Equals([AllowNull] Limit other)
|
||||
{
|
||||
return _tokensPerSecond == other._tokensPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_tokensPerSecond);
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns a hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_tokensPerSecond);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,287 +6,286 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Rate
|
||||
namespace Microsoft.Kubernetes.Controller.Rate;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Class Limiter controls how frequently events are allowed to happen.
|
||||
/// It implements a "token bucket" of size b, initially full and refilled
|
||||
/// at rate r tokens per second.
|
||||
/// Informally, in any large enough time interval, the Limiter limits the
|
||||
/// rate to r tokens per second, with a maximum burst size of b events.
|
||||
/// As a special case, if r == Inf (the infinite rate), b is ignored.
|
||||
/// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The zero value is a valid Limiter, but it will reject all events.
|
||||
/// Use NewLimiter to create non-zero Limiters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Limiter has three main methods, Allow, Reserve, and Wait.
|
||||
/// Most callers should use Wait.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each of the three methods consumes a single token.
|
||||
/// They differ in their behavior when no token is available.
|
||||
/// If no token is available, Allow returns false.
|
||||
/// If no token is available, Reserve returns a reservation for a future token
|
||||
/// and the amount of time the caller must wait before using it.
|
||||
/// If no token is available, Wait blocks until one can be obtained
|
||||
/// or its associated context.Context is canceled.
|
||||
/// The methods AllowN, ReserveN, and WaitN consume n tokens.
|
||||
/// </para>
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L55.
|
||||
/// </summary>
|
||||
public class Limiter
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Limit _limit;
|
||||
private readonly ISystemClock _clock;
|
||||
private readonly int _burst;
|
||||
private double _tokens;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Class Limiter controls how frequently events are allowed to happen.
|
||||
/// It implements a "token bucket" of size b, initially full and refilled
|
||||
/// at rate r tokens per second.
|
||||
/// Informally, in any large enough time interval, the Limiter limits the
|
||||
/// rate to r tokens per second, with a maximum burst size of b events.
|
||||
/// As a special case, if r == Inf (the infinite rate), b is ignored.
|
||||
/// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The zero value is a valid Limiter, but it will reject all events.
|
||||
/// Use NewLimiter to create non-zero Limiters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Limiter has three main methods, Allow, Reserve, and Wait.
|
||||
/// Most callers should use Wait.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each of the three methods consumes a single token.
|
||||
/// They differ in their behavior when no token is available.
|
||||
/// If no token is available, Allow returns false.
|
||||
/// If no token is available, Reserve returns a reservation for a future token
|
||||
/// and the amount of time the caller must wait before using it.
|
||||
/// If no token is available, Wait blocks until one can be obtained
|
||||
/// or its associated context.Context is canceled.
|
||||
/// The methods AllowN, ReserveN, and WaitN consume n tokens.
|
||||
/// </para>
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L55.
|
||||
/// The last time the limiter's tokens field was updated.
|
||||
/// </summary>
|
||||
public class Limiter
|
||||
private DateTimeOffset _last;
|
||||
|
||||
/// <summary>
|
||||
/// the latest time of a rate-limited event (past or future).
|
||||
/// </summary>
|
||||
private DateTimeOffset _lastEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Limiter" /> class.
|
||||
/// Allows events up to <see cref="Limit" /><paramref name="limit" /> and permits bursts of
|
||||
/// at most <paramref name="burst" /> tokens.
|
||||
/// </summary>
|
||||
/// <param name="limit">The count per second which is allowed.</param>
|
||||
/// <param name="burst">The burst.</param>
|
||||
/// <param name="systemClock">Accessor for the current UTC time.</param>
|
||||
public Limiter(Limit limit, int burst, ISystemClock systemClock = default)
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Limit _limit;
|
||||
private readonly ISystemClock _clock;
|
||||
private readonly int _burst;
|
||||
private double _tokens;
|
||||
_limit = limit;
|
||||
_burst = burst;
|
||||
_clock = systemClock ?? new SystemClock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The last time the limiter's tokens field was updated.
|
||||
/// </summary>
|
||||
private DateTimeOffset _last;
|
||||
/// <summary>
|
||||
/// Check to allow one token effective immediately.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if a token is available and used, <c>false</c> otherwise.</returns>
|
||||
public bool Allow()
|
||||
{
|
||||
return AllowN(_clock.UtcNow, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// the latest time of a rate-limited event (past or future).
|
||||
/// </summary>
|
||||
private DateTimeOffset _lastEvent;
|
||||
/// <summary>
|
||||
/// Checks if a number of tokens are available by a given time.
|
||||
/// They are consumed if available.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <param name="number">The number.</param>
|
||||
/// <returns><c>true</c> if a number token is available and used, <c>false</c> otherwise.</returns>
|
||||
public bool AllowN(DateTimeOffset now, int number)
|
||||
{
|
||||
return ReserveImpl(now, number, TimeSpan.Zero).Ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Limiter" /> class.
|
||||
/// Allows events up to <see cref="Limit" /><paramref name="limit" /> and permits bursts of
|
||||
/// at most <paramref name="burst" /> tokens.
|
||||
/// </summary>
|
||||
/// <param name="limit">The count per second which is allowed.</param>
|
||||
/// <param name="burst">The burst.</param>
|
||||
/// <param name="systemClock">Accessor for the current UTC time.</param>
|
||||
public Limiter(Limit limit, int burst, ISystemClock systemClock = default)
|
||||
/// <summary>
|
||||
/// Reserves this instance.
|
||||
/// </summary>
|
||||
/// <returns>Reservation.</returns>
|
||||
public Reservation Reserve()
|
||||
{
|
||||
return Reserve(_clock.UtcNow, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
|
||||
/// The Limiter takes this Reservation into account when allowing future events.
|
||||
/// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size.
|
||||
/// Usage example:
|
||||
/// r := lim.ReserveN(time.Now(), 1)
|
||||
/// if !r.OK() {
|
||||
/// return
|
||||
/// }
|
||||
/// time.Sleep(r.Delay())
|
||||
/// Act()
|
||||
/// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
|
||||
/// If you need to respect a deadline or cancel the delay, use Wait instead.
|
||||
/// To drop or skip events exceeding rate limit, use Allow instead.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <param name="count">The number.</param>
|
||||
/// <returns>Reservation.</returns>
|
||||
public Reservation Reserve(DateTimeOffset now, int count)
|
||||
{
|
||||
return ReserveImpl(now, count, TimeSpan.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits the asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task WaitAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return WaitAsync(1, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// wait as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="count">The count.</param>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <exception cref="Exception">rate: Wait(count={count}) exceeds limiter's burst {burst}.</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task WaitAsync(int count, CancellationToken cancellationToken)
|
||||
{
|
||||
// https://github.com/golang/time/blob/master/rate/rate.go#L226
|
||||
int burst = default;
|
||||
Limit limit = default;
|
||||
lock (_sync)
|
||||
{
|
||||
_limit = limit;
|
||||
_burst = burst;
|
||||
_clock = systemClock ?? new SystemClock();
|
||||
burst = _burst;
|
||||
limit = _limit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check to allow one token effective immediately.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if a token is available and used, <c>false</c> otherwise.</returns>
|
||||
public bool Allow()
|
||||
if (count > burst && limit != Limit.Max)
|
||||
{
|
||||
return AllowN(_clock.UtcNow, 1);
|
||||
throw new Exception($"rate: Wait(count={count}) exceeds limiter's burst {burst}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a number of tokens are available by a given time.
|
||||
/// They are consumed if available.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <param name="number">The number.</param>
|
||||
/// <returns><c>true</c> if a number token is available and used, <c>false</c> otherwise.</returns>
|
||||
public bool AllowN(DateTimeOffset now, int number)
|
||||
{
|
||||
return ReserveImpl(now, number, TimeSpan.Zero).Ok;
|
||||
}
|
||||
// Check if ctx is already cancelled
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
/// <summary>
|
||||
/// Reserves this instance.
|
||||
/// </summary>
|
||||
/// <returns>Reservation.</returns>
|
||||
public Reservation Reserve()
|
||||
{
|
||||
return Reserve(_clock.UtcNow, 1);
|
||||
}
|
||||
// Determine wait limit
|
||||
var waitLimit = limit.DurationFromTokens(count);
|
||||
|
||||
/// <summary>
|
||||
/// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
|
||||
/// The Limiter takes this Reservation into account when allowing future events.
|
||||
/// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size.
|
||||
/// Usage example:
|
||||
/// r := lim.ReserveN(time.Now(), 1)
|
||||
/// if !r.OK() {
|
||||
/// return
|
||||
/// }
|
||||
/// time.Sleep(r.Delay())
|
||||
/// Act()
|
||||
/// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
|
||||
/// If you need to respect a deadline or cancel the delay, use Wait instead.
|
||||
/// To drop or skip events exceeding rate limit, use Allow instead.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <param name="count">The number.</param>
|
||||
/// <returns>Reservation.</returns>
|
||||
public Reservation Reserve(DateTimeOffset now, int count)
|
||||
while (true)
|
||||
{
|
||||
return ReserveImpl(now, count, TimeSpan.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits the asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task WaitAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return WaitAsync(1, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// wait as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="count">The count.</param>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <exception cref="Exception">rate: Wait(count={count}) exceeds limiter's burst {burst}.</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task WaitAsync(int count, CancellationToken cancellationToken)
|
||||
{
|
||||
// https://github.com/golang/time/blob/master/rate/rate.go#L226
|
||||
int burst = default;
|
||||
Limit limit = default;
|
||||
lock (_sync)
|
||||
var now = _clock.UtcNow;
|
||||
var r = ReserveImpl(now, count, waitLimit);
|
||||
if (r.Ok)
|
||||
{
|
||||
burst = _burst;
|
||||
limit = _limit;
|
||||
var delay = r.DelayFrom(now);
|
||||
if (delay > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (count > burst && limit != Limit.Max)
|
||||
{
|
||||
throw new Exception($"rate: Wait(count={count}) exceeds limiter's burst {burst}");
|
||||
}
|
||||
|
||||
// Check if ctx is already cancelled
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Determine wait limit
|
||||
var waitLimit = limit.DurationFromTokens(count);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var now = _clock.UtcNow;
|
||||
var r = ReserveImpl(now, count, waitLimit);
|
||||
if (r.Ok)
|
||||
{
|
||||
var delay = r.DelayFrom(now);
|
||||
if (delay > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(waitLimit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await Task.Delay(waitLimit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// reserveN is a helper method for AllowN, ReserveN, and WaitN.
|
||||
/// maxFutureReserve specifies the maximum reservation wait duration allowed.
|
||||
/// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <param name="number">The number.</param>
|
||||
/// <param name="maxFutureReserve">The maximum future reserve.</param>
|
||||
/// <returns>Reservation.</returns>
|
||||
private Reservation ReserveImpl(DateTimeOffset now, int number, TimeSpan maxFutureReserve)
|
||||
/// <summary>
|
||||
/// reserveN is a helper method for AllowN, ReserveN, and WaitN.
|
||||
/// maxFutureReserve specifies the maximum reservation wait duration allowed.
|
||||
/// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <param name="number">The number.</param>
|
||||
/// <param name="maxFutureReserve">The maximum future reserve.</param>
|
||||
/// <returns>Reservation.</returns>
|
||||
private Reservation ReserveImpl(DateTimeOffset now, int number, TimeSpan maxFutureReserve)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
lock (_sync)
|
||||
if (_limit == Limit.Max)
|
||||
{
|
||||
if (_limit == Limit.Max)
|
||||
{
|
||||
return new Reservation(
|
||||
clock: _clock,
|
||||
limiter: this,
|
||||
ok: true,
|
||||
tokens: number,
|
||||
timeToAct: now);
|
||||
}
|
||||
|
||||
var (newNow, last, tokens) = Advance(now);
|
||||
now = newNow;
|
||||
|
||||
// Calculate the remaining number of tokens resulting from the request.
|
||||
tokens -= number;
|
||||
|
||||
// Calculate the wait duration
|
||||
TimeSpan waitDuration = default;
|
||||
if (tokens < 0)
|
||||
{
|
||||
waitDuration = _limit.DurationFromTokens(-tokens);
|
||||
}
|
||||
|
||||
// Decide result
|
||||
var ok = number <= _burst && waitDuration <= maxFutureReserve;
|
||||
|
||||
// Prepare reservation
|
||||
if (ok)
|
||||
{
|
||||
var reservation = new Reservation(
|
||||
clock: _clock,
|
||||
limiter: this,
|
||||
ok: true,
|
||||
tokens: number,
|
||||
limit: _limit,
|
||||
timeToAct: now.Add(waitDuration));
|
||||
|
||||
_last = newNow;
|
||||
_tokens = tokens;
|
||||
_lastEvent = reservation.TimeToAct;
|
||||
|
||||
return reservation;
|
||||
}
|
||||
else
|
||||
{
|
||||
var reservation = new Reservation(
|
||||
clock: _clock,
|
||||
limiter: this,
|
||||
ok: false,
|
||||
limit: _limit);
|
||||
|
||||
_last = last;
|
||||
|
||||
return reservation;
|
||||
}
|
||||
return new Reservation(
|
||||
clock: _clock,
|
||||
limiter: this,
|
||||
ok: true,
|
||||
tokens: number,
|
||||
timeToAct: now);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// advance calculates and returns an updated state for lim resulting from the passage of time.
|
||||
/// lim is not changed.
|
||||
/// advance requires that lim.mu is held.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
private (DateTimeOffset newNow, DateTimeOffset newLast, double newTokens) Advance(DateTimeOffset now)
|
||||
{
|
||||
lock (_sync)
|
||||
var (newNow, last, tokens) = Advance(now);
|
||||
now = newNow;
|
||||
|
||||
// Calculate the remaining number of tokens resulting from the request.
|
||||
tokens -= number;
|
||||
|
||||
// Calculate the wait duration
|
||||
TimeSpan waitDuration = default;
|
||||
if (tokens < 0)
|
||||
{
|
||||
var last = _last;
|
||||
if (now < last)
|
||||
{
|
||||
last = now;
|
||||
}
|
||||
waitDuration = _limit.DurationFromTokens(-tokens);
|
||||
}
|
||||
|
||||
// Avoid making delta overflow below when last is very old.
|
||||
var maxElapsed = _limit.DurationFromTokens(_burst - _tokens);
|
||||
var elapsed = now - last;
|
||||
if (elapsed > maxElapsed)
|
||||
{
|
||||
elapsed = maxElapsed;
|
||||
}
|
||||
// Decide result
|
||||
var ok = number <= _burst && waitDuration <= maxFutureReserve;
|
||||
|
||||
// Calculate the new number of tokens, due to time that passed.
|
||||
var delta = _limit.TokensFromDuration(elapsed);
|
||||
var tokens = _tokens + delta;
|
||||
if (tokens > _burst)
|
||||
{
|
||||
tokens = _burst;
|
||||
}
|
||||
// Prepare reservation
|
||||
if (ok)
|
||||
{
|
||||
var reservation = new Reservation(
|
||||
clock: _clock,
|
||||
limiter: this,
|
||||
ok: true,
|
||||
tokens: number,
|
||||
limit: _limit,
|
||||
timeToAct: now.Add(waitDuration));
|
||||
|
||||
return (now, last, tokens);
|
||||
_last = newNow;
|
||||
_tokens = tokens;
|
||||
_lastEvent = reservation.TimeToAct;
|
||||
|
||||
return reservation;
|
||||
}
|
||||
else
|
||||
{
|
||||
var reservation = new Reservation(
|
||||
clock: _clock,
|
||||
limiter: this,
|
||||
ok: false,
|
||||
limit: _limit);
|
||||
|
||||
_last = last;
|
||||
|
||||
return reservation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// advance calculates and returns an updated state for lim resulting from the passage of time.
|
||||
/// lim is not changed.
|
||||
/// advance requires that lim.mu is held.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
private (DateTimeOffset newNow, DateTimeOffset newLast, double newTokens) Advance(DateTimeOffset now)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var last = _last;
|
||||
if (now < last)
|
||||
{
|
||||
last = now;
|
||||
}
|
||||
|
||||
// Avoid making delta overflow below when last is very old.
|
||||
var maxElapsed = _limit.DurationFromTokens(_burst - _tokens);
|
||||
var elapsed = now - last;
|
||||
if (elapsed > maxElapsed)
|
||||
{
|
||||
elapsed = maxElapsed;
|
||||
}
|
||||
|
||||
// Calculate the new number of tokens, due to time that passed.
|
||||
var delta = _limit.TokensFromDuration(elapsed);
|
||||
var tokens = _tokens + delta;
|
||||
if (tokens > _burst)
|
||||
{
|
||||
tokens = _burst;
|
||||
}
|
||||
|
||||
return (now, last, tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,85 +6,84 @@ using System;
|
|||
|
||||
#pragma warning disable CS0169 // The field 'Reservation.limit' is never used
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.Rate
|
||||
namespace Microsoft.Kubernetes.Controller.Rate;
|
||||
|
||||
/// <summary>
|
||||
/// Class Reservation holds information about events that are permitted by a Limiter to happen after a delay.
|
||||
/// A Reservation may be canceled, which may enable the Limiter to permit additional events.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L106.
|
||||
/// </summary>
|
||||
public class Reservation
|
||||
{
|
||||
private readonly ISystemClock _clock;
|
||||
private readonly Limiter _limiter;
|
||||
private readonly Limit _limit;
|
||||
private readonly double _tokens;
|
||||
|
||||
/// <summary>
|
||||
/// Class Reservation holds information about events that are permitted by a Limiter to happen after a delay.
|
||||
/// A Reservation may be canceled, which may enable the Limiter to permit additional events.
|
||||
/// https://github.com/golang/time/blob/master/rate/rate.go#L106.
|
||||
/// Initializes a new instance of the <see cref="Reservation"/> class.
|
||||
/// </summary>
|
||||
public class Reservation
|
||||
/// <param name="limiter">The limiter.</param>
|
||||
/// <param name="ok">if set to <c>true</c> [ok].</param>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <param name="timeToAct">The time to act.</param>
|
||||
/// <param name="limit">The limit.</param>
|
||||
public Reservation(
|
||||
ISystemClock clock,
|
||||
Limiter limiter,
|
||||
bool ok,
|
||||
double tokens = default,
|
||||
DateTimeOffset timeToAct = default,
|
||||
Limit limit = default)
|
||||
{
|
||||
private readonly ISystemClock _clock;
|
||||
private readonly Limiter _limiter;
|
||||
private readonly Limit _limit;
|
||||
private readonly double _tokens;
|
||||
_clock = clock;
|
||||
_limiter = limiter;
|
||||
Ok = ok;
|
||||
_tokens = tokens;
|
||||
TimeToAct = timeToAct;
|
||||
_limit = limit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Reservation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="limiter">The limiter.</param>
|
||||
/// <param name="ok">if set to <c>true</c> [ok].</param>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <param name="timeToAct">The time to act.</param>
|
||||
/// <param name="limit">The limit.</param>
|
||||
public Reservation(
|
||||
ISystemClock clock,
|
||||
Limiter limiter,
|
||||
bool ok,
|
||||
double tokens = default,
|
||||
DateTimeOffset timeToAct = default,
|
||||
Limit limit = default)
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this <see cref="Reservation"/> is ok.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if ok; otherwise, <c>false</c>.</value>
|
||||
public bool Ok { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time to act.
|
||||
/// </summary>
|
||||
/// <value>The time to act.</value>
|
||||
public DateTimeOffset TimeToAct { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Delays this instance.
|
||||
/// </summary>
|
||||
/// <returns>TimeSpanOffset.</returns>
|
||||
public TimeSpan Delay()
|
||||
{
|
||||
return DelayFrom(_clock.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delays from.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan DelayFrom(DateTimeOffset now)
|
||||
{
|
||||
// https://github.com/golang/time/blob/master/rate/rate.go#L134
|
||||
if (!Ok)
|
||||
{
|
||||
_clock = clock;
|
||||
_limiter = limiter;
|
||||
Ok = ok;
|
||||
_tokens = tokens;
|
||||
TimeToAct = timeToAct;
|
||||
_limit = limit;
|
||||
return TimeSpan.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this <see cref="Reservation"/> is ok.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if ok; otherwise, <c>false</c>.</value>
|
||||
public bool Ok { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time to act.
|
||||
/// </summary>
|
||||
/// <value>The time to act.</value>
|
||||
public DateTimeOffset TimeToAct { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Delays this instance.
|
||||
/// </summary>
|
||||
/// <returns>TimeSpanOffset.</returns>
|
||||
public TimeSpan Delay()
|
||||
var delay = TimeToAct - now;
|
||||
if (delay < TimeSpan.Zero)
|
||||
{
|
||||
return DelayFrom(_clock.UtcNow);
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delays from.
|
||||
/// </summary>
|
||||
/// <param name="now">The now.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan DelayFrom(DateTimeOffset now)
|
||||
{
|
||||
// https://github.com/golang/time/blob/master/rate/rate.go#L134
|
||||
if (!Ok)
|
||||
{
|
||||
return TimeSpan.MaxValue;
|
||||
}
|
||||
|
||||
var delay = TimeToAct - now;
|
||||
if (delay < TimeSpan.Zero)
|
||||
{
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
return delay;
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,55 +4,54 @@
|
|||
using Microsoft.Kubernetes.Controller.Rate;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters;
|
||||
|
||||
/// <summary>
|
||||
/// Class BucketRateLimiter adapts a standard bucket to the workqueue ratelimiter API.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L48
|
||||
/// Implements the <see cref="IRateLimiter{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IRateLimiter{TItem}" />
|
||||
public class BucketRateLimiter<TItem> : IRateLimiter<TItem>
|
||||
{
|
||||
private readonly Limiter _limiter;
|
||||
|
||||
/// <summary>
|
||||
/// Class BucketRateLimiter adapts a standard bucket to the workqueue ratelimiter API.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L48
|
||||
/// Implements the <see cref="IRateLimiter{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="BucketRateLimiter{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IRateLimiter{TItem}" />
|
||||
public class BucketRateLimiter<TItem> : IRateLimiter<TItem>
|
||||
/// <param name="limiter">The limiter.</param>
|
||||
public BucketRateLimiter(Limiter limiter)
|
||||
{
|
||||
private readonly Limiter _limiter;
|
||||
_limiter = limiter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BucketRateLimiter{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="limiter">The limiter.</param>
|
||||
public BucketRateLimiter(Limiter limiter)
|
||||
{
|
||||
_limiter = limiter;
|
||||
}
|
||||
/// <summary>
|
||||
/// When gets an item and gets to decide how long that item should wait.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan ItemDelay(TItem item)
|
||||
{
|
||||
return _limiter.Reserve().Delay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When gets an item and gets to decide how long that item should wait.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan ItemDelay(TItem item)
|
||||
{
|
||||
return _limiter.Reserve().Delay();
|
||||
}
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many failures the item has had.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int NumRequeues(TItem item)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many failures the item has had.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int NumRequeues(TItem item)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
|
||||
/// or for success, we'll stop tracking it.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Forget(TItem item)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
|
||||
/// or for success, we'll stop tracking it.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Forget(TItem item)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,34 +3,33 @@
|
|||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IRateLimiter.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L27.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
public interface IRateLimiter<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IRateLimiter.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L27.
|
||||
/// When gets an item and gets to decide how long that item should wait.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
public interface IRateLimiter<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// When gets an item and gets to decide how long that item should wait.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
TimeSpan ItemDelay(TItem item);
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
TimeSpan ItemDelay(TItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
|
||||
/// or for success, we'll stop tracking it.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Forget(TItem item);
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
|
||||
/// or for success, we'll stop tracking it.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Forget(TItem item);
|
||||
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many failures the item has had.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
int NumRequeues(TItem item);
|
||||
}
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many failures the item has had.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
int NumRequeues(TItem item);
|
||||
}
|
||||
|
|
|
@ -4,74 +4,73 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters;
|
||||
|
||||
/// <summary>
|
||||
/// Class ItemExponentialFailureRateLimiter does a simple baseDelay*2^{num-failures} limit
|
||||
/// dealing with max failures and expiration are up to the caller.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L67
|
||||
/// Implements the <see cref="IRateLimiter{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IRateLimiter{TItem}" />
|
||||
public class ItemExponentialFailureRateLimiter<TItem> : IRateLimiter<TItem>
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<TItem, int> _itemFailures = new Dictionary<TItem, int>();
|
||||
private readonly TimeSpan _baseDelay;
|
||||
private readonly TimeSpan _maxDelay;
|
||||
|
||||
/// <summary>
|
||||
/// Class ItemExponentialFailureRateLimiter does a simple baseDelay*2^{num-failures} limit
|
||||
/// dealing with max failures and expiration are up to the caller.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L67
|
||||
/// Implements the <see cref="IRateLimiter{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="ItemExponentialFailureRateLimiter{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IRateLimiter{TItem}" />
|
||||
public class ItemExponentialFailureRateLimiter<TItem> : IRateLimiter<TItem>
|
||||
/// <param name="baseDelay">The base delay.</param>
|
||||
/// <param name="maxDelay">The maximum delay.</param>
|
||||
public ItemExponentialFailureRateLimiter(TimeSpan baseDelay, TimeSpan maxDelay)
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
private readonly Dictionary<TItem, int> _itemFailures = new Dictionary<TItem, int>();
|
||||
private readonly TimeSpan _baseDelay;
|
||||
private readonly TimeSpan _maxDelay;
|
||||
_baseDelay = baseDelay;
|
||||
_maxDelay = maxDelay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemExponentialFailureRateLimiter{TItem}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="baseDelay">The base delay.</param>
|
||||
/// <param name="maxDelay">The maximum delay.</param>
|
||||
public ItemExponentialFailureRateLimiter(TimeSpan baseDelay, TimeSpan maxDelay)
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan ItemDelay(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_baseDelay = baseDelay;
|
||||
_maxDelay = maxDelay;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan ItemDelay(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
if (_itemFailures.TryGetValue(item, out var requeues))
|
||||
{
|
||||
if (_itemFailures.TryGetValue(item, out var requeues))
|
||||
{
|
||||
_itemFailures[item] = requeues + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_itemFailures.Add(item, 1);
|
||||
}
|
||||
|
||||
var backoff = _baseDelay.TotalMilliseconds * Math.Pow(2, requeues);
|
||||
if (backoff > _maxDelay.TotalMilliseconds)
|
||||
{
|
||||
backoff = _maxDelay.TotalMilliseconds;
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(backoff);
|
||||
_itemFailures[item] = requeues + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Forget(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
else
|
||||
{
|
||||
_itemFailures.Remove(item);
|
||||
_itemFailures.Add(item, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int NumRequeues(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
var backoff = _baseDelay.TotalMilliseconds * Math.Pow(2, requeues);
|
||||
if (backoff > _maxDelay.TotalMilliseconds)
|
||||
{
|
||||
return _itemFailures.TryGetValue(item, out var requeues) ? requeues : 0;
|
||||
backoff = _maxDelay.TotalMilliseconds;
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(backoff);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Forget(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_itemFailures.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int NumRequeues(TItem item)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _itemFailures.TryGetValue(item, out var requeues) ? requeues : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,94 +5,93 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters
|
||||
namespace Microsoft.Kubernetes.Controller.RateLimiters;
|
||||
|
||||
/// <summary>
|
||||
/// Class MaxOfRateLimiter calls every RateLimiter and returns the worst case response
|
||||
/// When used with a token bucket limiter, the burst could be apparently exceeded in cases where particular items
|
||||
/// were separately delayed a longer time.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L175
|
||||
/// Implements the <see cref="IRateLimiter{TItem}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IRateLimiter{TItem}" />
|
||||
public class MaxOfRateLimiter<TItem> : IRateLimiter<TItem>
|
||||
{
|
||||
private readonly IRateLimiter<TItem>[] _rateLimiters;
|
||||
|
||||
/// <summary>
|
||||
/// Class MaxOfRateLimiter calls every RateLimiter and returns the worst case response
|
||||
/// When used with a token bucket limiter, the burst could be apparently exceeded in cases where particular items
|
||||
/// were separately delayed a longer time.
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L175
|
||||
/// Implements the <see cref="IRateLimiter{TItem}" />.
|
||||
/// Initializes a new instance of the <see cref="MaxOfRateLimiter{TItem}"/> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the t item.</typeparam>
|
||||
/// <seealso cref="IRateLimiter{TItem}" />
|
||||
public class MaxOfRateLimiter<TItem> : IRateLimiter<TItem>
|
||||
/// <param name="rateLimiters">The rate limiters.</param>
|
||||
public MaxOfRateLimiter(params IRateLimiter<TItem>[] rateLimiters)
|
||||
{
|
||||
private readonly IRateLimiter<TItem>[] _rateLimiters;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MaxOfRateLimiter{TItem}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rateLimiters">The rate limiters.</param>
|
||||
public MaxOfRateLimiter(params IRateLimiter<TItem>[] rateLimiters)
|
||||
if (rateLimiters is null)
|
||||
{
|
||||
if (rateLimiters is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(rateLimiters));
|
||||
}
|
||||
|
||||
_rateLimiters = (IRateLimiter<TItem>[])rateLimiters.Clone();
|
||||
throw new ArgumentNullException(nameof(rateLimiters));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MaxOfRateLimiter{TItem}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rateLimiters">The rate limiters.</param>
|
||||
public MaxOfRateLimiter(IEnumerable<IRateLimiter<TItem>> rateLimiters)
|
||||
_rateLimiters = (IRateLimiter<TItem>[])rateLimiters.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MaxOfRateLimiter{TItem}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rateLimiters">The rate limiters.</param>
|
||||
public MaxOfRateLimiter(IEnumerable<IRateLimiter<TItem>> rateLimiters)
|
||||
{
|
||||
_rateLimiters = rateLimiters.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When gets an item and gets to decide how long that item should wait.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan ItemDelay(TItem item)
|
||||
{
|
||||
// util\workqueue\default_rate_limiters.go:179
|
||||
var result = TimeSpan.Zero;
|
||||
foreach (var rateLimiter in _rateLimiters)
|
||||
{
|
||||
_rateLimiters = rateLimiters.ToArray();
|
||||
var current = rateLimiter.ItemDelay(item);
|
||||
if (result < current)
|
||||
{
|
||||
result = current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When gets an item and gets to decide how long that item should wait.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>TimeSpan.</returns>
|
||||
public TimeSpan ItemDelay(TItem item)
|
||||
{
|
||||
// util\workqueue\default_rate_limiters.go:179
|
||||
var result = TimeSpan.Zero;
|
||||
foreach (var rateLimiter in _rateLimiters)
|
||||
{
|
||||
var current = rateLimiter.ItemDelay(item);
|
||||
if (result < current)
|
||||
{
|
||||
result = current;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many failures the item has had.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int NumRequeues(TItem item)
|
||||
{
|
||||
// util\workqueue\default_rate_limiters.go:185
|
||||
var result = 0;
|
||||
foreach (var rateLimiter in _rateLimiters)
|
||||
{
|
||||
result = Math.Max(result, rateLimiter.NumRequeues(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NumRequeues returns back how many failures the item has had.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int NumRequeues(TItem item)
|
||||
{
|
||||
// util\workqueue\default_rate_limiters.go:185
|
||||
var result = 0;
|
||||
foreach (var rateLimiter in _rateLimiters)
|
||||
{
|
||||
result = Math.Max(result, rateLimiter.NumRequeues(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
|
||||
/// or for success, we'll stop tracking it.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Forget(TItem item)
|
||||
/// <summary>
|
||||
/// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
|
||||
/// or for success, we'll stop tracking it.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Forget(TItem item)
|
||||
{
|
||||
// util\workqueue\default_rate_limiters.go:207
|
||||
foreach (var rateLimiter in _rateLimiters)
|
||||
{
|
||||
// util\workqueue\default_rate_limiters.go:207
|
||||
foreach (var rateLimiter in _rateLimiters)
|
||||
{
|
||||
rateLimiter.Forget(item);
|
||||
}
|
||||
rateLimiter.Forget(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -18,288 +18,287 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Client
|
||||
namespace Microsoft.Kubernetes.Client;
|
||||
|
||||
public interface IAnyResourceKind
|
||||
{
|
||||
public interface IAnyResourceKind
|
||||
{
|
||||
//
|
||||
// Summary:
|
||||
// list or watch cluster scoped custom objects
|
||||
//
|
||||
// Parameters:
|
||||
// group:
|
||||
// The custom resource's group name
|
||||
//
|
||||
// version:
|
||||
// The custom resource's version
|
||||
//
|
||||
// plural:
|
||||
// The custom resource's plural name. For TPRs this would be lowercase plural kind.
|
||||
//
|
||||
// continueParameter:
|
||||
// The continue option should be set when retrieving more results from the server.
|
||||
// Since this value is server defined, clients may only use the continue value from
|
||||
// a previous query result with identical query parameters (except for the value
|
||||
// of continue) and the server may reject a continue value it does not recognize.
|
||||
// If the specified continue value is no longer valid whether due to expiration
|
||||
// (generally five to fifteen minutes) or a configuration change on the server,
|
||||
// the server will respond with a 410 ResourceExpired error together with a continue
|
||||
// token. If the client needs a consistent list, it must restart their list without
|
||||
// the continue field. Otherwise, the client may send another list request with
|
||||
// the token received with the 410 error, the server will respond with a list starting
|
||||
// from the next key, but from the latest snapshot, which is inconsistent from the
|
||||
// previous list results - objects that are created, modified, or deleted after
|
||||
// the first list request will be included in the response, as long as their keys
|
||||
// are after the "next key". This field is not supported when watch is true. Clients
|
||||
// may start a watch from the last resourceVersion value returned by the server
|
||||
// and not miss any modifications.
|
||||
//
|
||||
// fieldSelector:
|
||||
// A selector to restrict the list of returned objects by their fields. Defaults
|
||||
// to everything.
|
||||
//
|
||||
// labelSelector:
|
||||
// A selector to restrict the list of returned objects by their labels. Defaults
|
||||
// to everything.
|
||||
//
|
||||
// limit:
|
||||
// limit is a maximum number of responses to return for a list call. If more items
|
||||
// exist, the server will set the `continue` field on the list metadata to a value
|
||||
// that can be used with the same initial query to retrieve the next set of results.
|
||||
// Setting a limit may return fewer than the requested amount of items (up to zero
|
||||
// items) in the event all requested objects are filtered out and clients should
|
||||
// only use the presence of the continue field to determine whether more results
|
||||
// are available. Servers may choose not to support the limit argument and will
|
||||
// return all of the available results. If limit is specified and the continue field
|
||||
// is empty, clients may assume that no more results are available. This field is
|
||||
// not supported if watch is true. The server guarantees that the objects returned
|
||||
// when using continue will be identical to issuing a single list call without a
|
||||
// limit - that is, no objects created, modified, or deleted after the first request
|
||||
// is issued will be included in any subsequent continued requests. This is sometimes
|
||||
// referred to as a consistent snapshot, and ensures that a client that is using
|
||||
// limit to receive smaller chunks of a very large result can ensure they see all
|
||||
// possible objects. If objects are updated during a chunked list the version of
|
||||
// the object that was present at the time the first list result was calculated
|
||||
// is returned.
|
||||
//
|
||||
// resourceVersion:
|
||||
// When specified with a watch call, shows changes that occur after that particular
|
||||
// version of a resource. Defaults to changes from the beginning of history. When
|
||||
// specified for list: - if unset, then the result is returned from remote storage
|
||||
// based on quorum-read flag; - if it's 0, then we simply return what we currently
|
||||
// have in cache, no guarantee; - if set to non zero, then the result is at least
|
||||
// as fresh as given rv.
|
||||
//
|
||||
// timeoutSeconds:
|
||||
// Timeout for the list/watch call. This limits the duration of the call, regardless
|
||||
// of any activity or inactivity.
|
||||
//
|
||||
// watch:
|
||||
// Watch for changes to the described resources and return them as a stream of add,
|
||||
// update, and remove notifications.
|
||||
//
|
||||
// pretty:
|
||||
// If 'true', then the output is pretty printed.
|
||||
//
|
||||
// customHeaders:
|
||||
// The headers that will be added to request.
|
||||
//
|
||||
// cancellationToken:
|
||||
// The cancellation token.
|
||||
Task<HttpOperationResponse<KubernetesList<TResource>>> ListClusterAnyResourceKindWithHttpMessagesAsync<TResource>(string group, string version, string plural, string continueParameter = null, string fieldSelector = null, string labelSelector = null, int? limit = null, string resourceVersion = null, int? timeoutSeconds = null, bool? watch = null, string pretty = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default) where TResource : k8s.IKubernetesObject;
|
||||
//
|
||||
// Summary:
|
||||
// list or watch cluster scoped custom objects
|
||||
//
|
||||
// Parameters:
|
||||
// group:
|
||||
// The custom resource's group name
|
||||
//
|
||||
// version:
|
||||
// The custom resource's version
|
||||
//
|
||||
// plural:
|
||||
// The custom resource's plural name. For TPRs this would be lowercase plural kind.
|
||||
//
|
||||
// continueParameter:
|
||||
// The continue option should be set when retrieving more results from the server.
|
||||
// Since this value is server defined, clients may only use the continue value from
|
||||
// a previous query result with identical query parameters (except for the value
|
||||
// of continue) and the server may reject a continue value it does not recognize.
|
||||
// If the specified continue value is no longer valid whether due to expiration
|
||||
// (generally five to fifteen minutes) or a configuration change on the server,
|
||||
// the server will respond with a 410 ResourceExpired error together with a continue
|
||||
// token. If the client needs a consistent list, it must restart their list without
|
||||
// the continue field. Otherwise, the client may send another list request with
|
||||
// the token received with the 410 error, the server will respond with a list starting
|
||||
// from the next key, but from the latest snapshot, which is inconsistent from the
|
||||
// previous list results - objects that are created, modified, or deleted after
|
||||
// the first list request will be included in the response, as long as their keys
|
||||
// are after the "next key". This field is not supported when watch is true. Clients
|
||||
// may start a watch from the last resourceVersion value returned by the server
|
||||
// and not miss any modifications.
|
||||
//
|
||||
// fieldSelector:
|
||||
// A selector to restrict the list of returned objects by their fields. Defaults
|
||||
// to everything.
|
||||
//
|
||||
// labelSelector:
|
||||
// A selector to restrict the list of returned objects by their labels. Defaults
|
||||
// to everything.
|
||||
//
|
||||
// limit:
|
||||
// limit is a maximum number of responses to return for a list call. If more items
|
||||
// exist, the server will set the `continue` field on the list metadata to a value
|
||||
// that can be used with the same initial query to retrieve the next set of results.
|
||||
// Setting a limit may return fewer than the requested amount of items (up to zero
|
||||
// items) in the event all requested objects are filtered out and clients should
|
||||
// only use the presence of the continue field to determine whether more results
|
||||
// are available. Servers may choose not to support the limit argument and will
|
||||
// return all of the available results. If limit is specified and the continue field
|
||||
// is empty, clients may assume that no more results are available. This field is
|
||||
// not supported if watch is true. The server guarantees that the objects returned
|
||||
// when using continue will be identical to issuing a single list call without a
|
||||
// limit - that is, no objects created, modified, or deleted after the first request
|
||||
// is issued will be included in any subsequent continued requests. This is sometimes
|
||||
// referred to as a consistent snapshot, and ensures that a client that is using
|
||||
// limit to receive smaller chunks of a very large result can ensure they see all
|
||||
// possible objects. If objects are updated during a chunked list the version of
|
||||
// the object that was present at the time the first list result was calculated
|
||||
// is returned.
|
||||
//
|
||||
// resourceVersion:
|
||||
// When specified with a watch call, shows changes that occur after that particular
|
||||
// version of a resource. Defaults to changes from the beginning of history. When
|
||||
// specified for list: - if unset, then the result is returned from remote storage
|
||||
// based on quorum-read flag; - if it's 0, then we simply return what we currently
|
||||
// have in cache, no guarantee; - if set to non zero, then the result is at least
|
||||
// as fresh as given rv.
|
||||
//
|
||||
// timeoutSeconds:
|
||||
// Timeout for the list/watch call. This limits the duration of the call, regardless
|
||||
// of any activity or inactivity.
|
||||
//
|
||||
// watch:
|
||||
// Watch for changes to the described resources and return them as a stream of add,
|
||||
// update, and remove notifications.
|
||||
//
|
||||
// pretty:
|
||||
// If 'true', then the output is pretty printed.
|
||||
//
|
||||
// customHeaders:
|
||||
// The headers that will be added to request.
|
||||
//
|
||||
// cancellationToken:
|
||||
// The cancellation token.
|
||||
Task<HttpOperationResponse<KubernetesList<TResource>>> ListClusterAnyResourceKindWithHttpMessagesAsync<TResource>(string group, string version, string plural, string continueParameter = null, string fieldSelector = null, string labelSelector = null, int? limit = null, string resourceVersion = null, int? timeoutSeconds = null, bool? watch = null, string pretty = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default) where TResource : k8s.IKubernetesObject;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a namespace scoped Custom object
|
||||
/// </summary>
|
||||
/// <param name='body'>
|
||||
/// The JSON schema of the Resource to create.
|
||||
/// </param>
|
||||
/// <param name='group'>
|
||||
/// The custom resource's group name
|
||||
/// </param>
|
||||
/// <param name='version'>
|
||||
/// The custom resource's version
|
||||
/// </param>
|
||||
/// <param name='namespaceParameter'>
|
||||
/// The custom resource's namespace
|
||||
/// </param>
|
||||
/// <param name='plural'>
|
||||
/// The custom resource's plural name. For TPRs this would be lowercase plural
|
||||
/// kind.
|
||||
/// </param>
|
||||
/// <param name='dryRun'>
|
||||
/// When present, indicates that modifications should not be persisted. An
|
||||
/// invalid or unrecognized dryRun directive will result in an error response
|
||||
/// and no further processing of the request. Valid values are: - All: all dry
|
||||
/// run stages will be processed
|
||||
/// </param>
|
||||
/// <param name='fieldManager'>
|
||||
/// fieldManager is a name associated with the actor or entity that is making
|
||||
/// these changes. The value must be less than or 128 characters long, and only
|
||||
/// contain printable characters, as defined by
|
||||
/// https://golang.org/pkg/unicode/#IsPrint.
|
||||
/// </param>
|
||||
/// <param name='pretty'>
|
||||
/// If 'true', then the output is pretty printed.
|
||||
/// </param>
|
||||
/// <param name='customHeaders'>
|
||||
/// Headers that will be added to request.
|
||||
/// </param>
|
||||
/// <param name='cancellationToken'>
|
||||
/// The cancellation token.
|
||||
/// </param>
|
||||
/// <exception cref="HttpOperationException">
|
||||
/// Thrown when the operation returned an invalid status code
|
||||
/// </exception>
|
||||
/// <exception cref="SerializationException">
|
||||
/// Thrown when unable to deserialize the response
|
||||
/// </exception>
|
||||
/// <exception cref="ValidationException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <return>
|
||||
/// A response object containing the response body and response headers.
|
||||
/// </return>
|
||||
public Task<HttpOperationResponse<object>> CreateAnyResourceKindWithHttpMessagesAsync<TResource>(TResource body, string group, string version, string namespaceParameter, string plural, string dryRun = default(string), string fieldManager = default(string), string pretty = default(string), Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken)) where TResource : IKubernetesObject;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// patch the specified namespace scoped custom object
|
||||
/// </summary>
|
||||
/// <param name='body'>
|
||||
/// The JSON schema of the Resource to patch.
|
||||
/// </param>
|
||||
/// <param name='group'>
|
||||
/// the custom resource's group
|
||||
/// </param>
|
||||
/// <param name='version'>
|
||||
/// the custom resource's version
|
||||
/// </param>
|
||||
/// <param name='namespaceParameter'>
|
||||
/// The custom resource's namespace
|
||||
/// </param>
|
||||
/// <param name='plural'>
|
||||
/// the custom resource's plural name. For TPRs this would be lowercase plural
|
||||
/// kind.
|
||||
/// </param>
|
||||
/// <param name='name'>
|
||||
/// the custom object's name
|
||||
/// </param>
|
||||
/// <param name='dryRun'>
|
||||
/// When present, indicates that modifications should not be persisted. An
|
||||
/// invalid or unrecognized dryRun directive will result in an error response
|
||||
/// and no further processing of the request. Valid values are: - All: all dry
|
||||
/// run stages will be processed
|
||||
/// </param>
|
||||
/// <param name='fieldManager'>
|
||||
/// fieldManager is a name associated with the actor or entity that is making
|
||||
/// these changes. The value must be less than or 128 characters long, and only
|
||||
/// contain printable characters, as defined by
|
||||
/// https://golang.org/pkg/unicode/#IsPrint. This field is required for apply
|
||||
/// requests (application/apply-patch) but optional for non-apply patch types
|
||||
/// (JsonPatch, MergePatch, StrategicMergePatch).
|
||||
/// </param>
|
||||
/// <param name='force'>
|
||||
/// Force is going to "force" Apply requests. It means user will re-acquire
|
||||
/// conflicting fields owned by other people. Force flag must be unset for
|
||||
/// non-apply patch requests.
|
||||
/// </param>
|
||||
/// <param name='customHeaders'>
|
||||
/// Headers that will be added to request.
|
||||
/// </param>
|
||||
/// <param name='cancellationToken'>
|
||||
/// The cancellation token.
|
||||
/// </param>
|
||||
/// <exception cref="HttpOperationException">
|
||||
/// Thrown when the operation returned an invalid status code
|
||||
/// </exception>
|
||||
/// <exception cref="SerializationException">
|
||||
/// Thrown when unable to deserialize the response
|
||||
/// </exception>
|
||||
/// <exception cref="ValidationException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <return>
|
||||
/// A response object containing the response body and response headers.
|
||||
/// </return>
|
||||
public Task<HttpOperationResponse<object>> PatchAnyResourceKindWithHttpMessagesAsync(V1Patch body, string group, string version, string namespaceParameter, string plural, string name, string dryRun = default(string), string fieldManager = default(string), bool? force = default(bool?), Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// Creates a namespace scoped Custom object
|
||||
/// </summary>
|
||||
/// <param name='body'>
|
||||
/// The JSON schema of the Resource to create.
|
||||
/// </param>
|
||||
/// <param name='group'>
|
||||
/// The custom resource's group name
|
||||
/// </param>
|
||||
/// <param name='version'>
|
||||
/// The custom resource's version
|
||||
/// </param>
|
||||
/// <param name='namespaceParameter'>
|
||||
/// The custom resource's namespace
|
||||
/// </param>
|
||||
/// <param name='plural'>
|
||||
/// The custom resource's plural name. For TPRs this would be lowercase plural
|
||||
/// kind.
|
||||
/// </param>
|
||||
/// <param name='dryRun'>
|
||||
/// When present, indicates that modifications should not be persisted. An
|
||||
/// invalid or unrecognized dryRun directive will result in an error response
|
||||
/// and no further processing of the request. Valid values are: - All: all dry
|
||||
/// run stages will be processed
|
||||
/// </param>
|
||||
/// <param name='fieldManager'>
|
||||
/// fieldManager is a name associated with the actor or entity that is making
|
||||
/// these changes. The value must be less than or 128 characters long, and only
|
||||
/// contain printable characters, as defined by
|
||||
/// https://golang.org/pkg/unicode/#IsPrint.
|
||||
/// </param>
|
||||
/// <param name='pretty'>
|
||||
/// If 'true', then the output is pretty printed.
|
||||
/// </param>
|
||||
/// <param name='customHeaders'>
|
||||
/// Headers that will be added to request.
|
||||
/// </param>
|
||||
/// <param name='cancellationToken'>
|
||||
/// The cancellation token.
|
||||
/// </param>
|
||||
/// <exception cref="HttpOperationException">
|
||||
/// Thrown when the operation returned an invalid status code
|
||||
/// </exception>
|
||||
/// <exception cref="SerializationException">
|
||||
/// Thrown when unable to deserialize the response
|
||||
/// </exception>
|
||||
/// <exception cref="ValidationException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <return>
|
||||
/// A response object containing the response body and response headers.
|
||||
/// </return>
|
||||
public Task<HttpOperationResponse<object>> CreateAnyResourceKindWithHttpMessagesAsync<TResource>(TResource body, string group, string version, string namespaceParameter, string plural, string dryRun = default(string), string fieldManager = default(string), string pretty = default(string), Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken)) where TResource : IKubernetesObject;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified namespace scoped custom object
|
||||
/// </summary>
|
||||
/// <param name='group'>
|
||||
/// the custom resource's group
|
||||
/// </param>
|
||||
/// <param name='version'>
|
||||
/// the custom resource's version
|
||||
/// </param>
|
||||
/// <param name='namespaceParameter'>
|
||||
/// The custom resource's namespace
|
||||
/// </param>
|
||||
/// <param name='plural'>
|
||||
/// the custom resource's plural name. For TPRs this would be lowercase plural
|
||||
/// kind.
|
||||
/// </param>
|
||||
/// <param name='name'>
|
||||
/// the custom object's name
|
||||
/// </param>
|
||||
/// <param name='body'>
|
||||
/// </param>
|
||||
/// <param name='gracePeriodSeconds'>
|
||||
/// The duration in seconds before the object should be deleted. Value must be
|
||||
/// non-negative integer. The value zero indicates delete immediately. If this
|
||||
/// value is nil, the default grace period for the specified type will be used.
|
||||
/// Defaults to a per object value if not specified. zero means delete
|
||||
/// immediately.
|
||||
/// </param>
|
||||
/// <param name='orphanDependents'>
|
||||
/// Deprecated: please use the PropagationPolicy, this field will be deprecated
|
||||
/// in 1.7. Should the dependent objects be orphaned. If true/false, the
|
||||
/// "orphan" finalizer will be added to/removed from the object's finalizers
|
||||
/// list. Either this field or PropagationPolicy may be set, but not both.
|
||||
/// </param>
|
||||
/// <param name='propagationPolicy'>
|
||||
/// Whether and how garbage collection will be performed. Either this field or
|
||||
/// OrphanDependents may be set, but not both. The default policy is decided by
|
||||
/// the existing finalizer set in the metadata.finalizers and the
|
||||
/// resource-specific default policy.
|
||||
/// </param>
|
||||
/// <param name='dryRun'>
|
||||
/// When present, indicates that modifications should not be persisted. An
|
||||
/// invalid or unrecognized dryRun directive will result in an error response
|
||||
/// and no further processing of the request. Valid values are: - All: all dry
|
||||
/// run stages will be processed
|
||||
/// </param>
|
||||
/// <param name='customHeaders'>
|
||||
/// Headers that will be added to request.
|
||||
/// </param>
|
||||
/// <param name='cancellationToken'>
|
||||
/// The cancellation token.
|
||||
/// </param>
|
||||
/// <exception cref="HttpOperationException">
|
||||
/// Thrown when the operation returned an invalid status code
|
||||
/// </exception>
|
||||
/// <exception cref="SerializationException">
|
||||
/// Thrown when unable to deserialize the response
|
||||
/// </exception>
|
||||
/// <exception cref="ValidationException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <return>
|
||||
/// A response object containing the response body and response headers.
|
||||
/// </return>
|
||||
public Task<HttpOperationResponse<object>> DeleteAnyResourceKindWithHttpMessagesAsync(string group, string version, string namespaceParameter, string plural, string name, V1DeleteOptions body = default(V1DeleteOptions), int? gracePeriodSeconds = default(int?), bool? orphanDependents = default(bool?), string propagationPolicy = default(string), string dryRun = default(string), Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// patch the specified namespace scoped custom object
|
||||
/// </summary>
|
||||
/// <param name='body'>
|
||||
/// The JSON schema of the Resource to patch.
|
||||
/// </param>
|
||||
/// <param name='group'>
|
||||
/// the custom resource's group
|
||||
/// </param>
|
||||
/// <param name='version'>
|
||||
/// the custom resource's version
|
||||
/// </param>
|
||||
/// <param name='namespaceParameter'>
|
||||
/// The custom resource's namespace
|
||||
/// </param>
|
||||
/// <param name='plural'>
|
||||
/// the custom resource's plural name. For TPRs this would be lowercase plural
|
||||
/// kind.
|
||||
/// </param>
|
||||
/// <param name='name'>
|
||||
/// the custom object's name
|
||||
/// </param>
|
||||
/// <param name='dryRun'>
|
||||
/// When present, indicates that modifications should not be persisted. An
|
||||
/// invalid or unrecognized dryRun directive will result in an error response
|
||||
/// and no further processing of the request. Valid values are: - All: all dry
|
||||
/// run stages will be processed
|
||||
/// </param>
|
||||
/// <param name='fieldManager'>
|
||||
/// fieldManager is a name associated with the actor or entity that is making
|
||||
/// these changes. The value must be less than or 128 characters long, and only
|
||||
/// contain printable characters, as defined by
|
||||
/// https://golang.org/pkg/unicode/#IsPrint. This field is required for apply
|
||||
/// requests (application/apply-patch) but optional for non-apply patch types
|
||||
/// (JsonPatch, MergePatch, StrategicMergePatch).
|
||||
/// </param>
|
||||
/// <param name='force'>
|
||||
/// Force is going to "force" Apply requests. It means user will re-acquire
|
||||
/// conflicting fields owned by other people. Force flag must be unset for
|
||||
/// non-apply patch requests.
|
||||
/// </param>
|
||||
/// <param name='customHeaders'>
|
||||
/// Headers that will be added to request.
|
||||
/// </param>
|
||||
/// <param name='cancellationToken'>
|
||||
/// The cancellation token.
|
||||
/// </param>
|
||||
/// <exception cref="HttpOperationException">
|
||||
/// Thrown when the operation returned an invalid status code
|
||||
/// </exception>
|
||||
/// <exception cref="SerializationException">
|
||||
/// Thrown when unable to deserialize the response
|
||||
/// </exception>
|
||||
/// <exception cref="ValidationException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <return>
|
||||
/// A response object containing the response body and response headers.
|
||||
/// </return>
|
||||
public Task<HttpOperationResponse<object>> PatchAnyResourceKindWithHttpMessagesAsync(V1Patch body, string group, string version, string namespaceParameter, string plural, string name, string dryRun = default(string), string fieldManager = default(string), bool? force = default(bool?), Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified namespace scoped custom object
|
||||
/// </summary>
|
||||
/// <param name='group'>
|
||||
/// the custom resource's group
|
||||
/// </param>
|
||||
/// <param name='version'>
|
||||
/// the custom resource's version
|
||||
/// </param>
|
||||
/// <param name='namespaceParameter'>
|
||||
/// The custom resource's namespace
|
||||
/// </param>
|
||||
/// <param name='plural'>
|
||||
/// the custom resource's plural name. For TPRs this would be lowercase plural
|
||||
/// kind.
|
||||
/// </param>
|
||||
/// <param name='name'>
|
||||
/// the custom object's name
|
||||
/// </param>
|
||||
/// <param name='body'>
|
||||
/// </param>
|
||||
/// <param name='gracePeriodSeconds'>
|
||||
/// The duration in seconds before the object should be deleted. Value must be
|
||||
/// non-negative integer. The value zero indicates delete immediately. If this
|
||||
/// value is nil, the default grace period for the specified type will be used.
|
||||
/// Defaults to a per object value if not specified. zero means delete
|
||||
/// immediately.
|
||||
/// </param>
|
||||
/// <param name='orphanDependents'>
|
||||
/// Deprecated: please use the PropagationPolicy, this field will be deprecated
|
||||
/// in 1.7. Should the dependent objects be orphaned. If true/false, the
|
||||
/// "orphan" finalizer will be added to/removed from the object's finalizers
|
||||
/// list. Either this field or PropagationPolicy may be set, but not both.
|
||||
/// </param>
|
||||
/// <param name='propagationPolicy'>
|
||||
/// Whether and how garbage collection will be performed. Either this field or
|
||||
/// OrphanDependents may be set, but not both. The default policy is decided by
|
||||
/// the existing finalizer set in the metadata.finalizers and the
|
||||
/// resource-specific default policy.
|
||||
/// </param>
|
||||
/// <param name='dryRun'>
|
||||
/// When present, indicates that modifications should not be persisted. An
|
||||
/// invalid or unrecognized dryRun directive will result in an error response
|
||||
/// and no further processing of the request. Valid values are: - All: all dry
|
||||
/// run stages will be processed
|
||||
/// </param>
|
||||
/// <param name='customHeaders'>
|
||||
/// Headers that will be added to request.
|
||||
/// </param>
|
||||
/// <param name='cancellationToken'>
|
||||
/// The cancellation token.
|
||||
/// </param>
|
||||
/// <exception cref="HttpOperationException">
|
||||
/// Thrown when the operation returned an invalid status code
|
||||
/// </exception>
|
||||
/// <exception cref="SerializationException">
|
||||
/// Thrown when unable to deserialize the response
|
||||
/// </exception>
|
||||
/// <exception cref="ValidationException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// Thrown when a required parameter is null
|
||||
/// </exception>
|
||||
/// <return>
|
||||
/// A response object containing the response body and response headers.
|
||||
/// </return>
|
||||
public Task<HttpOperationResponse<object>> DeleteAnyResourceKindWithHttpMessagesAsync(string group, string version, string namespaceParameter, string plural, string name, V1DeleteOptions body = default(V1DeleteOptions), int? gracePeriodSeconds = default(int?), bool? orphanDependents = default(bool?), string propagationPolicy = default(string), string dryRun = default(string), Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
}
|
||||
|
|
|
@ -5,75 +5,74 @@ using k8s.Models;
|
|||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.Kubernetes
|
||||
namespace Microsoft.Kubernetes;
|
||||
|
||||
public struct GroupApiVersionKind : IEquatable<GroupApiVersionKind>
|
||||
{
|
||||
public struct GroupApiVersionKind : IEquatable<GroupApiVersionKind>
|
||||
public GroupApiVersionKind(string group, string apiVersion, string kind, string pluralName)
|
||||
{
|
||||
public GroupApiVersionKind(string group, string apiVersion, string kind, string pluralName)
|
||||
{
|
||||
Group = group;
|
||||
ApiVersion = apiVersion;
|
||||
GroupApiVersion = string.IsNullOrEmpty(Group) ? apiVersion : $"{group}/{apiVersion}";
|
||||
Kind = kind;
|
||||
PluralName = pluralName;
|
||||
}
|
||||
Group = group;
|
||||
ApiVersion = apiVersion;
|
||||
GroupApiVersion = string.IsNullOrEmpty(Group) ? apiVersion : $"{group}/{apiVersion}";
|
||||
Kind = kind;
|
||||
PluralName = pluralName;
|
||||
}
|
||||
|
||||
public string Group { get; }
|
||||
public string Group { get; }
|
||||
|
||||
public string ApiVersion { get; }
|
||||
public string ApiVersion { get; }
|
||||
|
||||
public string GroupApiVersion { get; }
|
||||
public string GroupApiVersion { get; }
|
||||
|
||||
public string Kind { get; }
|
||||
public string Kind { get; }
|
||||
|
||||
public string PluralName { get; set; }
|
||||
public string PluralName { get; set; }
|
||||
|
||||
public string PluralNameGroup => string.IsNullOrEmpty(Group) ? PluralName : $"{PluralName}.{Group}";
|
||||
public string PluralNameGroup => string.IsNullOrEmpty(Group) ? PluralName : $"{PluralName}.{Group}";
|
||||
|
||||
public static GroupApiVersionKind From<TResource>() => From(typeof(TResource));
|
||||
public static GroupApiVersionKind From<TResource>() => From(typeof(TResource));
|
||||
|
||||
public static GroupApiVersionKind From(Type resourceType)
|
||||
{
|
||||
var entity = resourceType.GetTypeInfo().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
public static GroupApiVersionKind From(Type resourceType)
|
||||
{
|
||||
var entity = resourceType.GetTypeInfo().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
|
||||
return new GroupApiVersionKind(
|
||||
group: entity.Group,
|
||||
apiVersion: entity.ApiVersion,
|
||||
kind: entity.Kind,
|
||||
pluralName: entity.PluralName);
|
||||
}
|
||||
return new GroupApiVersionKind(
|
||||
group: entity.Group,
|
||||
apiVersion: entity.ApiVersion,
|
||||
kind: entity.Kind,
|
||||
pluralName: entity.PluralName);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is GroupApiVersionKind kind && Equals(kind);
|
||||
}
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is GroupApiVersionKind kind && Equals(kind);
|
||||
}
|
||||
|
||||
public bool Equals(GroupApiVersionKind other)
|
||||
{
|
||||
return Group == other.Group &&
|
||||
ApiVersion == other.ApiVersion &&
|
||||
Kind == other.Kind &&
|
||||
PluralName == other.PluralName;
|
||||
}
|
||||
public bool Equals(GroupApiVersionKind other)
|
||||
{
|
||||
return Group == other.Group &&
|
||||
ApiVersion == other.ApiVersion &&
|
||||
Kind == other.Kind &&
|
||||
PluralName == other.PluralName;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Group, ApiVersion, Kind, PluralName);
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Group, ApiVersion, Kind, PluralName);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Kind}.{GroupApiVersion}";
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Kind}.{GroupApiVersion}";
|
||||
}
|
||||
|
||||
public static bool operator ==(GroupApiVersionKind left, GroupApiVersionKind right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
public static bool operator ==(GroupApiVersionKind left, GroupApiVersionKind right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(GroupApiVersionKind left, GroupApiVersionKind right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
public static bool operator !=(GroupApiVersionKind left, GroupApiVersionKind right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,102 +6,101 @@ using k8s.Models;
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Kubernetes
|
||||
namespace Microsoft.Kubernetes;
|
||||
|
||||
/// <summary>
|
||||
/// Struct GroupKindNamespacedName is a value that acts as a dictionary key. It is a comparable
|
||||
/// combination of a Kubernetes resource's apiGroup and kind in addition to the metadata namespace and name.
|
||||
/// Implements the <see cref="IEquatable{T}" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="IEquatable{T}" />
|
||||
public struct GroupKindNamespacedName : IEquatable<GroupKindNamespacedName>
|
||||
{
|
||||
/// <summary>
|
||||
/// Struct GroupKindNamespacedName is a value that acts as a dictionary key. It is a comparable
|
||||
/// combination of a Kubernetes resource's apiGroup and kind in addition to the metadata namespace and name.
|
||||
/// Implements the <see cref="IEquatable{T}" />.
|
||||
/// Initializes a new instance of the <see cref="GroupKindNamespacedName"/> struct.
|
||||
/// </summary>
|
||||
/// <seealso cref="IEquatable{T}" />
|
||||
public struct GroupKindNamespacedName : IEquatable<GroupKindNamespacedName>
|
||||
/// <param name="group">The group.</param>
|
||||
/// <param name="kind">The kind.</param>
|
||||
/// <param name="namespacedName">Name of the namespaced.</param>
|
||||
public GroupKindNamespacedName(string group, string kind, NamespacedName namespacedName)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GroupKindNamespacedName"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="group">The group.</param>
|
||||
/// <param name="kind">The kind.</param>
|
||||
/// <param name="namespacedName">Name of the namespaced.</param>
|
||||
public GroupKindNamespacedName(string group, string kind, NamespacedName namespacedName)
|
||||
Group = group;
|
||||
Kind = kind;
|
||||
NamespacedName = namespacedName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the group.
|
||||
/// </summary>
|
||||
/// <value>The group.</value>
|
||||
public string Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind.
|
||||
/// </summary>
|
||||
/// <value>The kind.</value>
|
||||
public string Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the namespaced.
|
||||
/// </summary>
|
||||
/// <value>The name of the namespaced.</value>
|
||||
public NamespacedName NamespacedName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Implements the == operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator ==(GroupKindNamespacedName left, GroupKindNamespacedName right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the != operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator !=(GroupKindNamespacedName left, GroupKindNamespacedName right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets key values from the specified resource.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource.</param>
|
||||
/// <returns>GroupKindNamespacedName.</returns>
|
||||
public static GroupKindNamespacedName From(IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
if (resource is null)
|
||||
{
|
||||
Group = group;
|
||||
Kind = kind;
|
||||
NamespacedName = namespacedName;
|
||||
throw new ArgumentNullException(nameof(resource));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the group.
|
||||
/// </summary>
|
||||
/// <value>The group.</value>
|
||||
public string Group { get; }
|
||||
return new GroupKindNamespacedName(
|
||||
resource.ApiGroup(),
|
||||
resource.Kind,
|
||||
NamespacedName.From(resource));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind.
|
||||
/// </summary>
|
||||
/// <value>The kind.</value>
|
||||
public string Kind { get; }
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is GroupKindNamespacedName name && Equals(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the namespaced.
|
||||
/// </summary>
|
||||
/// <value>The name of the namespaced.</value>
|
||||
public NamespacedName NamespacedName { get; }
|
||||
public bool Equals([AllowNull] GroupKindNamespacedName other)
|
||||
{
|
||||
return Group == other.Group &&
|
||||
Kind == other.Kind &&
|
||||
NamespacedName.Equals(other.NamespacedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the == operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator ==(GroupKindNamespacedName left, GroupKindNamespacedName right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the != operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator !=(GroupKindNamespacedName left, GroupKindNamespacedName right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets key values from the specified resource.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource.</param>
|
||||
/// <returns>GroupKindNamespacedName.</returns>
|
||||
public static GroupKindNamespacedName From(IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
if (resource is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(resource));
|
||||
}
|
||||
|
||||
return new GroupKindNamespacedName(
|
||||
resource.ApiGroup(),
|
||||
resource.Kind,
|
||||
NamespacedName.From(resource));
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is GroupKindNamespacedName name && Equals(name);
|
||||
}
|
||||
|
||||
public bool Equals([AllowNull] GroupKindNamespacedName other)
|
||||
{
|
||||
return Group == other.Group &&
|
||||
Kind == other.Kind &&
|
||||
NamespacedName.Equals(other.NamespacedName);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Group, Kind, NamespacedName);
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Group, Kind, NamespacedName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,16 @@
|
|||
|
||||
using k8s;
|
||||
|
||||
namespace Microsoft.Kubernetes
|
||||
namespace Microsoft.Kubernetes;
|
||||
|
||||
/// <summary>
|
||||
/// Class KubernetesClientOptions.
|
||||
/// </summary>
|
||||
public class KubernetesClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Class KubernetesClientOptions.
|
||||
/// Gets or sets the configuration.
|
||||
/// </summary>
|
||||
public class KubernetesClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration.
|
||||
/// </summary>
|
||||
/// <value>The configuration.</value>
|
||||
public KubernetesClientConfiguration Configuration { get; set; }
|
||||
}
|
||||
/// <value>The configuration.</value>
|
||||
public KubernetesClientConfiguration Configuration { get; set; }
|
||||
}
|
||||
|
|
|
@ -6,126 +6,125 @@ using k8s.Models;
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Kubernetes
|
||||
namespace Microsoft.Kubernetes;
|
||||
|
||||
/// <summary>
|
||||
/// Struct NamespacedName is a value that acts as a dictionary key. It is a comparable
|
||||
/// combination of a metadata namespace and name.
|
||||
/// Implements the <see cref="IEquatable{T}" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="IEquatable{T}" />
|
||||
public struct NamespacedName : IEquatable<NamespacedName>
|
||||
{
|
||||
/// <summary>
|
||||
/// Struct NamespacedName is a value that acts as a dictionary key. It is a comparable
|
||||
/// combination of a metadata namespace and name.
|
||||
/// Implements the <see cref="IEquatable{T}" />.
|
||||
/// Initializes a new instance of the <see cref="NamespacedName"/> struct.
|
||||
/// </summary>
|
||||
/// <seealso cref="IEquatable{T}" />
|
||||
public struct NamespacedName : IEquatable<NamespacedName>
|
||||
/// <param name="namespace">The namespace.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
[Newtonsoft.Json.JsonConstructor]
|
||||
public NamespacedName(string @namespace, string name)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamespacedName"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="namespace">The namespace.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
[Newtonsoft.Json.JsonConstructor]
|
||||
public NamespacedName(string @namespace, string name)
|
||||
Namespace = @namespace;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamespacedName"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
public NamespacedName(string name)
|
||||
{
|
||||
Namespace = null;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the namespace.
|
||||
/// </summary>
|
||||
/// <value>The namespace.</value>
|
||||
public string Namespace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Implements the == operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator ==(NamespacedName left, NamespacedName right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the != operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator !=(NamespacedName left, NamespacedName right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets key values from the specified metadata.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The metadata.</param>
|
||||
/// <returns>NamespacedName.</returns>
|
||||
public static NamespacedName From(IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
if (resource is null)
|
||||
{
|
||||
Namespace = @namespace;
|
||||
Name = name;
|
||||
throw new ArgumentNullException(nameof(resource));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamespacedName"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
public NamespacedName(string name)
|
||||
{
|
||||
Namespace = null;
|
||||
Name = name;
|
||||
}
|
||||
return new NamespacedName(resource.Namespace(), resource.Name());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the namespace.
|
||||
/// </summary>
|
||||
/// <value>The namespace.</value>
|
||||
public string Namespace { get; }
|
||||
/// <summary>
|
||||
/// Gets key values from the specified metadata.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The metadata.</param>
|
||||
/// <param name="ownerReference">The owner reference.</param>
|
||||
/// <param name="clusterScoped">if set to <c>true</c> [cluster scoped].</param>
|
||||
/// <returns>NamespacedName.</returns>
|
||||
public static NamespacedName From(V1ObjectMeta metadata, [NotNull] V1OwnerReference ownerReference, bool? clusterScoped = null)
|
||||
{
|
||||
_ = metadata ?? throw new ArgumentNullException(nameof(metadata));
|
||||
_ = ownerReference ?? throw new ArgumentNullException(nameof(ownerReference));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; }
|
||||
return new NamespacedName(
|
||||
clusterScoped ?? false ? null : metadata.NamespaceProperty,
|
||||
ownerReference.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the == operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator ==(NamespacedName left, NamespacedName right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is NamespacedName name && Equals(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the != operator.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>The result of the operator.</returns>
|
||||
public static bool operator !=(NamespacedName left, NamespacedName right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
public bool Equals([AllowNull] NamespacedName other)
|
||||
{
|
||||
return Namespace == other.Namespace && Name == other.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets key values from the specified metadata.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The metadata.</param>
|
||||
/// <returns>NamespacedName.</returns>
|
||||
public static NamespacedName From(IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
if (resource is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(resource));
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Namespace, Name);
|
||||
}
|
||||
|
||||
return new NamespacedName(resource.Namespace(), resource.Name());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets key values from the specified metadata.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The metadata.</param>
|
||||
/// <param name="ownerReference">The owner reference.</param>
|
||||
/// <param name="clusterScoped">if set to <c>true</c> [cluster scoped].</param>
|
||||
/// <returns>NamespacedName.</returns>
|
||||
public static NamespacedName From(V1ObjectMeta metadata, [NotNull] V1OwnerReference ownerReference, bool? clusterScoped = null)
|
||||
{
|
||||
_ = metadata ?? throw new ArgumentNullException(nameof(metadata));
|
||||
_ = ownerReference ?? throw new ArgumentNullException(nameof(ownerReference));
|
||||
|
||||
return new NamespacedName(
|
||||
clusterScoped ?? false ? null : metadata.NamespaceProperty,
|
||||
ownerReference.Name);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is NamespacedName name && Equals(name);
|
||||
}
|
||||
|
||||
public bool Equals([AllowNull] NamespacedName other)
|
||||
{
|
||||
return Namespace == other.Namespace && Name == other.Name;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Namespace, Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="string" /> that represents this instance.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name}.{Namespace}";
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="string" /> that represents this instance.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name}.{Namespace}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public sealed class DefaultResourceKind : IResourceKind
|
||||
{
|
||||
public sealed class DefaultResourceKind : IResourceKind
|
||||
{
|
||||
public static IResourceKind Unknown { get; } = new DefaultResourceKind();
|
||||
public static IResourceKind Unknown { get; } = new DefaultResourceKind();
|
||||
|
||||
public string ApiVersion => default;
|
||||
public string ApiVersion => default;
|
||||
|
||||
public string Kind => default;
|
||||
public string Kind => default;
|
||||
|
||||
public IResourceKindElement Schema => DefaultResourceKindElement.Unknown;
|
||||
}
|
||||
public IResourceKindElement Schema => DefaultResourceKindElement.Unknown;
|
||||
}
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public sealed class DefaultResourceKindElement : IResourceKindElement
|
||||
{
|
||||
public sealed class DefaultResourceKindElement : IResourceKindElement
|
||||
public DefaultResourceKindElement(ElementMergeStrategy replacePrimative)
|
||||
{
|
||||
public DefaultResourceKindElement(ElementMergeStrategy replacePrimative)
|
||||
{
|
||||
MergeStrategy = replacePrimative;
|
||||
}
|
||||
|
||||
public static IResourceKindElement Unknown { get; } = new DefaultResourceKindElement(ElementMergeStrategy.Unknown);
|
||||
|
||||
public static IResourceKindElement ReplacePrimative { get; } = new DefaultResourceKindElement(ElementMergeStrategy.ReplacePrimative);
|
||||
|
||||
public ElementMergeStrategy MergeStrategy { get; }
|
||||
|
||||
public string MergeKey => string.Empty;
|
||||
|
||||
public IResourceKindElement GetPropertyElementType(string name) => Unknown;
|
||||
|
||||
public IResourceKindElement GetCollectionElementType() => Unknown;
|
||||
MergeStrategy = replacePrimative;
|
||||
}
|
||||
|
||||
public static IResourceKindElement Unknown { get; } = new DefaultResourceKindElement(ElementMergeStrategy.Unknown);
|
||||
|
||||
public static IResourceKindElement ReplacePrimative { get; } = new DefaultResourceKindElement(ElementMergeStrategy.ReplacePrimative);
|
||||
|
||||
public ElementMergeStrategy MergeStrategy { get; }
|
||||
|
||||
public string MergeKey => string.Empty;
|
||||
|
||||
public IResourceKindElement GetPropertyElementType(string name) => Unknown;
|
||||
|
||||
public IResourceKindElement GetCollectionElementType() => Unknown;
|
||||
}
|
||||
|
|
|
@ -1,48 +1,47 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public enum ElementMergeStrategy
|
||||
{
|
||||
public enum ElementMergeStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown json schema are handled by MergeObject, ReplacePrimative, and ReplaceListOfPrimative.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
/// <summary>
|
||||
/// Unknown json schema are handled by MergeObject, ReplacePrimative, and ReplaceListOfPrimative.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Updating object by matching property names.
|
||||
/// </summary>
|
||||
MergeObject,
|
||||
/// <summary>
|
||||
/// Updating object by matching property names.
|
||||
/// </summary>
|
||||
MergeObject,
|
||||
|
||||
/// <summary>
|
||||
/// Updating primative by replacing when different.
|
||||
/// </summary>
|
||||
ReplacePrimative,
|
||||
/// <summary>
|
||||
/// Updating primative by replacing when different.
|
||||
/// </summary>
|
||||
ReplacePrimative,
|
||||
|
||||
/// <summary>
|
||||
/// Updating dictionary by matching keys.
|
||||
/// </summary>
|
||||
MergeMap,
|
||||
/// <summary>
|
||||
/// Updating dictionary by matching keys.
|
||||
/// </summary>
|
||||
MergeMap,
|
||||
|
||||
/// <summary>
|
||||
/// Merging list or primatives adding or removing when changed but leaving unmanaged values alone.
|
||||
/// </summary>
|
||||
MergeListOfPrimative,
|
||||
/// <summary>
|
||||
/// Merging list or primatives adding or removing when changed but leaving unmanaged values alone.
|
||||
/// </summary>
|
||||
MergeListOfPrimative,
|
||||
|
||||
/// <summary>
|
||||
/// Updating list of primatives by replacing entirely when differences exist.
|
||||
/// </summary>
|
||||
ReplaceListOfPrimative,
|
||||
/// <summary>
|
||||
/// Updating list of primatives by replacing entirely when differences exist.
|
||||
/// </summary>
|
||||
ReplaceListOfPrimative,
|
||||
|
||||
/// <summary>
|
||||
/// Merging list of objects when the "merge key" property for object matching is known.
|
||||
/// </summary>
|
||||
MergeListOfObject,
|
||||
/// <summary>
|
||||
/// Merging list of objects when the "merge key" property for object matching is known.
|
||||
/// </summary>
|
||||
MergeListOfObject,
|
||||
|
||||
/// <summary>
|
||||
/// Updating list of object by replacing entirely when differences exist.
|
||||
/// </summary>
|
||||
ReplaceListOfObject,
|
||||
}
|
||||
/// <summary>
|
||||
/// Updating list of object by replacing entirely when differences exist.
|
||||
/// </summary>
|
||||
ReplaceListOfObject,
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public interface IResourceKind
|
||||
{
|
||||
public interface IResourceKind
|
||||
{
|
||||
string ApiVersion { get; }
|
||||
string ApiVersion { get; }
|
||||
|
||||
string Kind { get; }
|
||||
string Kind { get; }
|
||||
|
||||
IResourceKindElement Schema { get; }
|
||||
}
|
||||
IResourceKindElement Schema { get; }
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public interface IResourceKindElement
|
||||
{
|
||||
public interface IResourceKindElement
|
||||
{
|
||||
ElementMergeStrategy MergeStrategy { get; }
|
||||
ElementMergeStrategy MergeStrategy { get; }
|
||||
|
||||
public string MergeKey { get; }
|
||||
public string MergeKey { get; }
|
||||
|
||||
IResourceKindElement GetPropertyElementType(string name);
|
||||
IResourceKindElement GetPropertyElementType(string name);
|
||||
|
||||
IResourceKindElement GetCollectionElementType();
|
||||
}
|
||||
IResourceKindElement GetCollectionElementType();
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public interface IResourceKindManager
|
||||
{
|
||||
public interface IResourceKindManager
|
||||
{
|
||||
public Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind);
|
||||
}
|
||||
public Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind);
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public interface IResourceKindProvider
|
||||
{
|
||||
public interface IResourceKindProvider
|
||||
{
|
||||
public Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind);
|
||||
}
|
||||
public Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind);
|
||||
}
|
||||
|
|
|
@ -5,28 +5,27 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds
|
||||
namespace Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
public class ResourceKindManager : IResourceKindManager
|
||||
{
|
||||
public class ResourceKindManager : IResourceKindManager
|
||||
private readonly IEnumerable<IResourceKindProvider> _providers;
|
||||
|
||||
public ResourceKindManager(IEnumerable<IResourceKindProvider> providers)
|
||||
{
|
||||
private readonly IEnumerable<IResourceKindProvider> _providers;
|
||||
_providers = providers ?? throw new ArgumentNullException(nameof(providers));
|
||||
}
|
||||
|
||||
public ResourceKindManager(IEnumerable<IResourceKindProvider> providers)
|
||||
public async Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind)
|
||||
{
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
_providers = providers ?? throw new ArgumentNullException(nameof(providers));
|
||||
}
|
||||
|
||||
public async Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind)
|
||||
{
|
||||
foreach (var provider in _providers)
|
||||
var resourceKind = await provider.GetResourceKindAsync(apiVersion, kind);
|
||||
if (resourceKind != null)
|
||||
{
|
||||
var resourceKind = await provider.GetResourceKindAsync(apiVersion, kind);
|
||||
if (resourceKind != null)
|
||||
{
|
||||
return resourceKind;
|
||||
}
|
||||
return resourceKind;
|
||||
}
|
||||
return DefaultResourceKind.Unknown;
|
||||
}
|
||||
return DefaultResourceKind.Unknown;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
|
||||
using Microsoft.Kubernetes.ResourceKinds;
|
||||
|
||||
namespace Microsoft.Kubernetes.Resources
|
||||
namespace Microsoft.Kubernetes.Resources;
|
||||
|
||||
public class CreatePatchParameters
|
||||
{
|
||||
public class CreatePatchParameters
|
||||
{
|
||||
public IResourceKind ResourceKind { get; set; }
|
||||
public object ApplyResource { get; set; }
|
||||
public object LastAppliedResource { get; set; }
|
||||
public object LiveResource { get; set; }
|
||||
}
|
||||
public IResourceKind ResourceKind { get; set; }
|
||||
public object ApplyResource { get; set; }
|
||||
public object LastAppliedResource { get; set; }
|
||||
public object LiveResource { get; set; }
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
using Microsoft.AspNetCore.JsonPatch;
|
||||
|
||||
namespace Microsoft.Kubernetes.Resources
|
||||
namespace Microsoft.Kubernetes.Resources;
|
||||
|
||||
public interface IResourcePatcher
|
||||
{
|
||||
public interface IResourcePatcher
|
||||
{
|
||||
JsonPatchDocument CreateJsonPatch(CreatePatchParameters parameters);
|
||||
}
|
||||
JsonPatchDocument CreateJsonPatch(CreatePatchParameters parameters);
|
||||
}
|
||||
|
|
|
@ -1,44 +1,43 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.Resources
|
||||
namespace Microsoft.Kubernetes.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IResourceSerializers provides common methods to convert between objects, json, and yaml
|
||||
/// representations of resource objects.
|
||||
/// </summary>
|
||||
public interface IResourceSerializers
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IResourceSerializers provides common methods to convert between objects, json, and yaml
|
||||
/// representations of resource objects.
|
||||
/// Converts a given object to a different resource type.
|
||||
/// </summary>
|
||||
public interface IResourceSerializers
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a given object to a different resource type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The resource model to target.</typeparam>
|
||||
/// <param name="resource">The source object to convert.</param>
|
||||
/// <returns>TResource.</returns>
|
||||
TResource Convert<TResource>(object resource);
|
||||
/// <typeparam name="TResource">The resource model to target.</typeparam>
|
||||
/// <param name="resource">The source object to convert.</param>
|
||||
/// <returns>TResource.</returns>
|
||||
TResource Convert<TResource>(object resource);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes from yaml string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The resource model to deserialize.</typeparam>
|
||||
/// <param name="yaml">The yaml content to parse.</param>
|
||||
/// <returns>TResource.</returns>
|
||||
TResource DeserializeYaml<TResource>(string yaml);
|
||||
/// <summary>
|
||||
/// Deserializes from yaml string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The resource model to deserialize.</typeparam>
|
||||
/// <param name="yaml">The yaml content to parse.</param>
|
||||
/// <returns>TResource.</returns>
|
||||
TResource DeserializeYaml<TResource>(string yaml);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes from json string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The resource model to deserialize.</typeparam>
|
||||
/// <param name="json">The json content to parse.</param>
|
||||
/// <returns>T.</returns>
|
||||
TResource DeserializeJson<TResource>(string json);
|
||||
/// <summary>
|
||||
/// Deserializes from json string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The resource model to deserialize.</typeparam>
|
||||
/// <param name="json">The json content to parse.</param>
|
||||
/// <returns>T.</returns>
|
||||
TResource DeserializeJson<TResource>(string json);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes to json string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The resource model to serialize.</typeparam>
|
||||
/// <param name="resource">The resource object to serialize.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string SerializeJson(object resource);
|
||||
}
|
||||
/// <summary>
|
||||
/// Serializes to json string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The resource model to serialize.</typeparam>
|
||||
/// <param name="resource">The resource object to serialize.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string SerializeJson(object resource);
|
||||
}
|
||||
|
|
|
@ -8,421 +8,420 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Kubernetes.Resources
|
||||
namespace Microsoft.Kubernetes.Resources;
|
||||
|
||||
public class ResourcePatcher : IResourcePatcher
|
||||
{
|
||||
public class ResourcePatcher : IResourcePatcher
|
||||
private readonly IEqualityComparer<JToken> _tokenEqualityComparer = new JTokenEqualityComparer();
|
||||
|
||||
public JsonPatchDocument CreateJsonPatch(CreatePatchParameters parameters)
|
||||
{
|
||||
private readonly IEqualityComparer<JToken> _tokenEqualityComparer = new JTokenEqualityComparer();
|
||||
|
||||
public JsonPatchDocument CreateJsonPatch(CreatePatchParameters parameters)
|
||||
if (parameters is null)
|
||||
{
|
||||
if (parameters is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(parameters));
|
||||
}
|
||||
|
||||
var context = new Context(parameters);
|
||||
|
||||
var patch = AccumulatePatch(new JsonPatchDocument(), context);
|
||||
|
||||
return patch;
|
||||
throw new ArgumentNullException(nameof(parameters));
|
||||
}
|
||||
|
||||
private JsonPatchDocument AccumulatePatch(JsonPatchDocument patch, Context context)
|
||||
var context = new Context(parameters);
|
||||
|
||||
var patch = AccumulatePatch(new JsonPatchDocument(), context);
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument AccumulatePatch(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
return context.Element.MergeStrategy switch
|
||||
{
|
||||
return context.Element.MergeStrategy switch
|
||||
{
|
||||
ElementMergeStrategy.Unknown => MergeApplyAny(patch, context),
|
||||
ElementMergeStrategy.ReplacePrimative => ReplacePrimative(patch, context),
|
||||
ElementMergeStrategy.Unknown => MergeApplyAny(patch, context),
|
||||
ElementMergeStrategy.ReplacePrimative => ReplacePrimative(patch, context),
|
||||
|
||||
ElementMergeStrategy.MergeObject => MergeObject(patch, context),
|
||||
ElementMergeStrategy.MergeMap => MergeMap(patch, context),
|
||||
ElementMergeStrategy.MergeObject => MergeObject(patch, context),
|
||||
ElementMergeStrategy.MergeMap => MergeMap(patch, context),
|
||||
|
||||
ElementMergeStrategy.MergeListOfPrimative => MergeListOfPrimative(patch, context),
|
||||
ElementMergeStrategy.ReplaceListOfPrimative => ReplaceListOfPrimative(patch, context),
|
||||
ElementMergeStrategy.MergeListOfObject => MergeListOfObject(patch, context),
|
||||
ElementMergeStrategy.ReplaceListOfObject => ReplaceListOfObject(patch, context),
|
||||
ElementMergeStrategy.MergeListOfPrimative => MergeListOfPrimative(patch, context),
|
||||
ElementMergeStrategy.ReplaceListOfPrimative => ReplaceListOfPrimative(patch, context),
|
||||
ElementMergeStrategy.MergeListOfObject => MergeListOfObject(patch, context),
|
||||
ElementMergeStrategy.ReplaceListOfObject => ReplaceListOfObject(patch, context),
|
||||
|
||||
_ => throw new Exception("Unhandled merge strategy"),
|
||||
};
|
||||
_ => throw new Exception("Unhandled merge strategy"),
|
||||
};
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeApplyAny(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
if (context.ApplyToken is JObject)
|
||||
{
|
||||
return MergeObject(patch, context);
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeApplyAny(JsonPatchDocument patch, Context context)
|
||||
else if (context.ApplyToken is JArray)
|
||||
{
|
||||
if (context.ApplyToken is JObject)
|
||||
return ReplaceListOfObjectOrPrimative(patch, context);
|
||||
}
|
||||
else if (context.ApplyToken is JValue)
|
||||
{
|
||||
return ReplacePrimative(patch, context);
|
||||
}
|
||||
else if (context.ApplyToken == null && context.LastAppliedToken != null)
|
||||
{
|
||||
return patch.Remove(context.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static FormatException NewFormatException(Context context)
|
||||
{
|
||||
return new FormatException($"{context.Kind.Kind}.{context.Kind.ApiVersion} {context.Path} type {context.ApplyToken?.Type} is incorrect for {context.Element?.MergeStrategy}");
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplacePrimative(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
if (context.ApplyToken is JValue apply)
|
||||
{
|
||||
if (context.LiveToken is JValue live &&
|
||||
_tokenEqualityComparer.Equals(apply, live))
|
||||
{
|
||||
return MergeObject(patch, context);
|
||||
}
|
||||
else if (context.ApplyToken is JArray)
|
||||
{
|
||||
return ReplaceListOfObjectOrPrimative(patch, context);
|
||||
}
|
||||
else if (context.ApplyToken is JValue)
|
||||
{
|
||||
return ReplacePrimative(patch, context);
|
||||
}
|
||||
else if (context.ApplyToken == null && context.LastAppliedToken != null)
|
||||
{
|
||||
return patch.Remove(context.Path);
|
||||
// live value is correct
|
||||
}
|
||||
else
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
// live value is different, or live is not a primative value
|
||||
patch = patch.Replace(context.Path, apply);
|
||||
}
|
||||
}
|
||||
|
||||
private static FormatException NewFormatException(Context context)
|
||||
else
|
||||
{
|
||||
return new FormatException($"{context.Kind.Kind}.{context.Kind.ApiVersion} {context.Path} type {context.ApplyToken?.Type} is incorrect for {context.Element?.MergeStrategy}");
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplacePrimative(JsonPatchDocument patch, Context context)
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeObject(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
var apply = (JObject)context.ApplyToken;
|
||||
var lastApplied = context.LastAppliedToken as JObject;
|
||||
var live = context.LiveToken as JObject;
|
||||
|
||||
if (live == null)
|
||||
{
|
||||
if (context.ApplyToken is JValue apply)
|
||||
return patch.Replace(context.Path, apply);
|
||||
}
|
||||
|
||||
foreach (var applyProperty in apply.Properties())
|
||||
{
|
||||
var name = applyProperty.Name;
|
||||
var path = $"{context.Path}/{EscapePath(name)}";
|
||||
|
||||
var liveProperty = live.Property(name, StringComparison.Ordinal);
|
||||
|
||||
if (liveProperty == null)
|
||||
{
|
||||
if (context.LiveToken is JValue live &&
|
||||
_tokenEqualityComparer.Equals(apply, live))
|
||||
{
|
||||
// live value is correct
|
||||
}
|
||||
else
|
||||
{
|
||||
// live value is different, or live is not a primative value
|
||||
patch = patch.Replace(context.Path, apply);
|
||||
}
|
||||
patch = patch.Add(path, applyProperty.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
var lastAppliedProperty = lastApplied?.Property(name, StringComparison.Ordinal);
|
||||
|
||||
return patch;
|
||||
var nested = context.Push(
|
||||
path,
|
||||
context.Element.GetPropertyElementType(name),
|
||||
applyProperty.Value,
|
||||
lastAppliedProperty?.Value,
|
||||
liveProperty.Value);
|
||||
|
||||
patch = AccumulatePatch(patch, nested);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeObject(JsonPatchDocument patch, Context context)
|
||||
foreach (var liveProperty in live.Properties())
|
||||
{
|
||||
var apply = (JObject)context.ApplyToken;
|
||||
var lastApplied = context.LastAppliedToken as JObject;
|
||||
var live = context.LiveToken as JObject;
|
||||
var name = liveProperty.Name;
|
||||
var applyProperty = apply.Property(name, StringComparison.Ordinal);
|
||||
|
||||
if (live == null)
|
||||
if (applyProperty == null)
|
||||
{
|
||||
return patch.Replace(context.Path, apply);
|
||||
}
|
||||
|
||||
foreach (var applyProperty in apply.Properties())
|
||||
{
|
||||
var name = applyProperty.Name;
|
||||
var path = $"{context.Path}/{EscapePath(name)}";
|
||||
|
||||
var liveProperty = live.Property(name, StringComparison.Ordinal);
|
||||
|
||||
if (liveProperty == null)
|
||||
var lastAppliedProperty = lastApplied?.Property(name, StringComparison.Ordinal);
|
||||
if (lastAppliedProperty != null)
|
||||
{
|
||||
patch = patch.Add(path, applyProperty.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastAppliedProperty = lastApplied?.Property(name, StringComparison.Ordinal);
|
||||
|
||||
var nested = context.Push(
|
||||
path,
|
||||
context.Element.GetPropertyElementType(name),
|
||||
applyProperty.Value,
|
||||
lastAppliedProperty?.Value,
|
||||
liveProperty.Value);
|
||||
|
||||
patch = AccumulatePatch(patch, nested);
|
||||
var path = $"{context.Path}/{EscapePath(name)}";
|
||||
patch = patch.Remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var liveProperty in live.Properties())
|
||||
{
|
||||
var name = liveProperty.Name;
|
||||
var applyProperty = apply.Property(name, StringComparison.Ordinal);
|
||||
|
||||
if (applyProperty == null)
|
||||
{
|
||||
var lastAppliedProperty = lastApplied?.Property(name, StringComparison.Ordinal);
|
||||
if (lastAppliedProperty != null)
|
||||
{
|
||||
var path = $"{context.Path}/{EscapePath(name)}";
|
||||
patch = patch.Remove(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeMap(JsonPatchDocument patch, Context context)
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeMap(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
var apply = (JObject)context.ApplyToken;
|
||||
var lastApplied = context.LastAppliedToken as JObject;
|
||||
var live = context.LiveToken as JObject;
|
||||
|
||||
if (live == null)
|
||||
{
|
||||
var apply = (JObject)context.ApplyToken;
|
||||
var lastApplied = context.LastAppliedToken as JObject;
|
||||
var live = context.LiveToken as JObject;
|
||||
|
||||
if (live == null)
|
||||
{
|
||||
return patch.Replace(context.Path, apply);
|
||||
}
|
||||
|
||||
var collectionElement = context.Element.GetCollectionElementType();
|
||||
|
||||
foreach (var applyProperty in apply.Properties())
|
||||
{
|
||||
var key = applyProperty.Name;
|
||||
var path = $"{context.Path}/{EscapePath(key)}";
|
||||
|
||||
var liveProperty = live.Property(key, StringComparison.Ordinal);
|
||||
|
||||
if (liveProperty == null)
|
||||
{
|
||||
patch = patch.Add(path, applyProperty.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastAppliedProperty = lastApplied?.Property(key, StringComparison.Ordinal);
|
||||
|
||||
var propertyContext = context.Push(
|
||||
path,
|
||||
collectionElement,
|
||||
applyProperty.Value,
|
||||
lastAppliedProperty?.Value,
|
||||
liveProperty.Value);
|
||||
|
||||
patch = AccumulatePatch(patch, propertyContext);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var liveProperty in live.Properties())
|
||||
{
|
||||
var name = liveProperty.Name;
|
||||
var applyProperty = apply.Property(name, StringComparison.Ordinal);
|
||||
|
||||
if (applyProperty == null)
|
||||
{
|
||||
var lastAppliedProperty = lastApplied?.Property(name, StringComparison.Ordinal);
|
||||
if (lastAppliedProperty != null)
|
||||
{
|
||||
var path = $"{context.Path}/{EscapePath(name)}";
|
||||
patch = patch.Remove(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return patch;
|
||||
return patch.Replace(context.Path, apply);
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeListOfPrimative(JsonPatchDocument patch, Context context)
|
||||
var collectionElement = context.Element.GetCollectionElementType();
|
||||
|
||||
foreach (var applyProperty in apply.Properties())
|
||||
{
|
||||
if (!(context.ApplyToken is JArray applyArray))
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
var key = applyProperty.Name;
|
||||
var path = $"{context.Path}/{EscapePath(key)}";
|
||||
|
||||
if (!(context.LiveToken is JArray liveArray))
|
||||
{
|
||||
// live is not an array, so replace it
|
||||
return patch.Replace(context.Path, applyArray);
|
||||
}
|
||||
var liveProperty = live.Property(key, StringComparison.Ordinal);
|
||||
|
||||
List<JToken> lastAppliedList;
|
||||
if (context.LastAppliedToken is JArray lastAppliedArray)
|
||||
if (liveProperty == null)
|
||||
{
|
||||
lastAppliedList = lastAppliedArray.ToList();
|
||||
patch = patch.Add(path, applyProperty.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
lastAppliedList = new List<JToken>();
|
||||
var lastAppliedProperty = lastApplied?.Property(key, StringComparison.Ordinal);
|
||||
|
||||
var propertyContext = context.Push(
|
||||
path,
|
||||
collectionElement,
|
||||
applyProperty.Value,
|
||||
lastAppliedProperty?.Value,
|
||||
liveProperty.Value);
|
||||
|
||||
patch = AccumulatePatch(patch, propertyContext);
|
||||
}
|
||||
}
|
||||
|
||||
var applyEnumerator = applyArray.GetEnumerator();
|
||||
var applyIndex = 0;
|
||||
var applyAvailable = applyIndex < applyArray.Count;
|
||||
var applyValue = applyAvailable ? applyArray[applyIndex] : null;
|
||||
foreach (var liveProperty in live.Properties())
|
||||
{
|
||||
var name = liveProperty.Name;
|
||||
var applyProperty = apply.Property(name, StringComparison.Ordinal);
|
||||
|
||||
var liveIndex = 0;
|
||||
foreach (var liveValue in liveArray)
|
||||
if (applyProperty == null)
|
||||
{
|
||||
// match live value to remaining last applied values
|
||||
var lastAppliedIndex = lastAppliedList.FindIndex(lastAppliedValue => _tokenEqualityComparer.Equals(lastAppliedValue, liveValue));
|
||||
var wasLastApplied = lastAppliedIndex != -1;
|
||||
if (wasLastApplied)
|
||||
var lastAppliedProperty = lastApplied?.Property(name, StringComparison.Ordinal);
|
||||
if (lastAppliedProperty != null)
|
||||
{
|
||||
// remove from last applied list to preserve the number of live values that are accounted for
|
||||
lastAppliedList.RemoveAt(lastAppliedIndex);
|
||||
}
|
||||
|
||||
if (applyAvailable && _tokenEqualityComparer.Equals(applyValue, liveValue))
|
||||
{
|
||||
// next live value matches next apply value in order, take no action and advance
|
||||
liveIndex++;
|
||||
applyIndex++;
|
||||
applyAvailable = applyIndex < applyArray.Count;
|
||||
applyValue = applyAvailable ? applyArray[applyIndex] : null;
|
||||
}
|
||||
else if (wasLastApplied)
|
||||
{
|
||||
// next live value matches last applied, but is either removed or does not match next apply value
|
||||
patch = patch.Remove($"{context.Path}/{liveIndex}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// next live value is not controlled by last applied, so take no action and advance live
|
||||
liveIndex++;
|
||||
var path = $"{context.Path}/{EscapePath(name)}";
|
||||
patch = patch.Remove(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var path = $"{context.Path}/-";
|
||||
while (applyAvailable)
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeListOfPrimative(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
if (!(context.ApplyToken is JArray applyArray))
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
|
||||
if (!(context.LiveToken is JArray liveArray))
|
||||
{
|
||||
// live is not an array, so replace it
|
||||
return patch.Replace(context.Path, applyArray);
|
||||
}
|
||||
|
||||
List<JToken> lastAppliedList;
|
||||
if (context.LastAppliedToken is JArray lastAppliedArray)
|
||||
{
|
||||
lastAppliedList = lastAppliedArray.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
lastAppliedList = new List<JToken>();
|
||||
}
|
||||
|
||||
var applyEnumerator = applyArray.GetEnumerator();
|
||||
var applyIndex = 0;
|
||||
var applyAvailable = applyIndex < applyArray.Count;
|
||||
var applyValue = applyAvailable ? applyArray[applyIndex] : null;
|
||||
|
||||
var liveIndex = 0;
|
||||
foreach (var liveValue in liveArray)
|
||||
{
|
||||
// match live value to remaining last applied values
|
||||
var lastAppliedIndex = lastAppliedList.FindIndex(lastAppliedValue => _tokenEqualityComparer.Equals(lastAppliedValue, liveValue));
|
||||
var wasLastApplied = lastAppliedIndex != -1;
|
||||
if (wasLastApplied)
|
||||
{
|
||||
// remaining apply values are appended
|
||||
patch = patch.Add(path, applyValue);
|
||||
// remove from last applied list to preserve the number of live values that are accounted for
|
||||
lastAppliedList.RemoveAt(lastAppliedIndex);
|
||||
}
|
||||
|
||||
if (applyAvailable && _tokenEqualityComparer.Equals(applyValue, liveValue))
|
||||
{
|
||||
// next live value matches next apply value in order, take no action and advance
|
||||
liveIndex++;
|
||||
applyIndex++;
|
||||
applyAvailable = applyIndex < applyArray.Count;
|
||||
applyValue = applyAvailable ? applyArray[applyIndex] : null;
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplaceListOfPrimative(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
return ReplaceListOfObjectOrPrimative(patch, context);
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeListOfObject(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
if (!(context.ApplyToken is JArray apply))
|
||||
else if (wasLastApplied)
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
|
||||
if (!(context.LiveToken is JArray live))
|
||||
{
|
||||
// live is not an array, so replace it
|
||||
return patch.Replace(context.Path, apply);
|
||||
}
|
||||
|
||||
var applyItems = apply.Select((item, index) => (name: item[context.Element.MergeKey]?.Value<string>(), index, item)).ToArray();
|
||||
var liveItems = live.Select((item, index) => (name: item[context.Element.MergeKey]?.Value<string>(), index, item)).ToArray();
|
||||
var lastAppliedItems = context.LastAppliedToken?.Select((item, index) => (name: item[context.Element.MergeKey]?.Value<string>(), index, item))?.ToArray() ?? Array.Empty<(string name, int index, JToken item)>();
|
||||
|
||||
var element = context.Element.GetCollectionElementType();
|
||||
|
||||
foreach (var (name, _, applyToken) in applyItems)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new Exception("Merge key is required on object");
|
||||
}
|
||||
|
||||
var (_, index, liveToken) = liveItems.SingleOrDefault(item => item.name == name);
|
||||
var (_, _, lastAppliedToken) = lastAppliedItems.SingleOrDefault(item => item.name == name);
|
||||
|
||||
if (liveToken != null)
|
||||
{
|
||||
var itemContext = context.Push(
|
||||
path: $"{context.Path}/{index}",
|
||||
element: element,
|
||||
apply: applyToken,
|
||||
lastApplied: lastAppliedToken,
|
||||
live: liveToken);
|
||||
|
||||
patch = AccumulatePatch(patch, itemContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
patch = patch.Add($"{context.Path}/-", applyToken);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (name, _, lastApplyToken) in lastAppliedItems)
|
||||
{
|
||||
var (_, index, liveToken) = liveItems.SingleOrDefault(item => item.name == name);
|
||||
var (_, _, applyToken) = applyItems.SingleOrDefault(item => item.name == name);
|
||||
|
||||
if (applyToken == null && liveToken != null)
|
||||
{
|
||||
patch = patch.Remove($"{context.Path}/{index}");
|
||||
}
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplaceListOfObject(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
return ReplaceListOfObjectOrPrimative(patch, context);
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplaceListOfObjectOrPrimative(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
if (context.ApplyToken is JArray apply)
|
||||
{
|
||||
if (context.LiveToken is JArray live &&
|
||||
_tokenEqualityComparer.Equals(apply, live))
|
||||
{
|
||||
// live is correct
|
||||
}
|
||||
else
|
||||
{
|
||||
// live array has any differences, or live is not an array
|
||||
patch = patch.Replace(context.Path, context.ApplyToken);
|
||||
}
|
||||
// next live value matches last applied, but is either removed or does not match next apply value
|
||||
patch = patch.Remove($"{context.Path}/{liveIndex}");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
// next live value is not controlled by last applied, so take no action and advance live
|
||||
liveIndex++;
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
private static string EscapePath(string name)
|
||||
var path = $"{context.Path}/-";
|
||||
while (applyAvailable)
|
||||
{
|
||||
return name.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.Ordinal);
|
||||
// remaining apply values are appended
|
||||
patch = patch.Add(path, applyValue);
|
||||
|
||||
applyIndex++;
|
||||
applyAvailable = applyIndex < applyArray.Count;
|
||||
applyValue = applyAvailable ? applyArray[applyIndex] : null;
|
||||
}
|
||||
|
||||
private struct Context
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplaceListOfPrimative(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
return ReplaceListOfObjectOrPrimative(patch, context);
|
||||
}
|
||||
|
||||
private JsonPatchDocument MergeListOfObject(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
if (!(context.ApplyToken is JArray apply))
|
||||
{
|
||||
public Context(CreatePatchParameters context)
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
|
||||
if (!(context.LiveToken is JArray live))
|
||||
{
|
||||
// live is not an array, so replace it
|
||||
return patch.Replace(context.Path, apply);
|
||||
}
|
||||
|
||||
var applyItems = apply.Select((item, index) => (name: item[context.Element.MergeKey]?.Value<string>(), index, item)).ToArray();
|
||||
var liveItems = live.Select((item, index) => (name: item[context.Element.MergeKey]?.Value<string>(), index, item)).ToArray();
|
||||
var lastAppliedItems = context.LastAppliedToken?.Select((item, index) => (name: item[context.Element.MergeKey]?.Value<string>(), index, item))?.ToArray() ?? Array.Empty<(string name, int index, JToken item)>();
|
||||
|
||||
var element = context.Element.GetCollectionElementType();
|
||||
|
||||
foreach (var (name, _, applyToken) in applyItems)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
Path = string.Empty;
|
||||
Kind = context.ResourceKind ?? DefaultResourceKind.Unknown;
|
||||
Element = context.ResourceKind?.Schema ?? DefaultResourceKindElement.Unknown;
|
||||
ApplyToken = (JToken)context.ApplyResource;
|
||||
LastAppliedToken = (JToken)context.LastAppliedResource;
|
||||
LiveToken = (JToken)context.LiveResource;
|
||||
throw new Exception("Merge key is required on object");
|
||||
}
|
||||
|
||||
public Context(string path, IResourceKind kind, IResourceKindElement element, JToken apply, JToken lastApplied, JToken live) : this()
|
||||
var (_, index, liveToken) = liveItems.SingleOrDefault(item => item.name == name);
|
||||
var (_, _, lastAppliedToken) = lastAppliedItems.SingleOrDefault(item => item.name == name);
|
||||
|
||||
if (liveToken != null)
|
||||
{
|
||||
Path = path;
|
||||
Kind = kind;
|
||||
Element = element;
|
||||
ApplyToken = apply;
|
||||
LastAppliedToken = lastApplied;
|
||||
LiveToken = live;
|
||||
var itemContext = context.Push(
|
||||
path: $"{context.Path}/{index}",
|
||||
element: element,
|
||||
apply: applyToken,
|
||||
lastApplied: lastAppliedToken,
|
||||
live: liveToken);
|
||||
|
||||
patch = AccumulatePatch(patch, itemContext);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public IResourceKind Kind { get; }
|
||||
|
||||
public IResourceKindElement Element { get; }
|
||||
|
||||
public JToken ApplyToken { get; }
|
||||
|
||||
public JToken LastAppliedToken { get; }
|
||||
|
||||
public JToken LiveToken { get; }
|
||||
|
||||
public Context Push(string path, IResourceKindElement element, JToken apply, JToken lastApplied, JToken live)
|
||||
else
|
||||
{
|
||||
return new Context(path, Kind, element, apply, lastApplied, live);
|
||||
patch = patch.Add($"{context.Path}/-", applyToken);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (name, _, lastApplyToken) in lastAppliedItems)
|
||||
{
|
||||
var (_, index, liveToken) = liveItems.SingleOrDefault(item => item.name == name);
|
||||
var (_, _, applyToken) = applyItems.SingleOrDefault(item => item.name == name);
|
||||
|
||||
if (applyToken == null && liveToken != null)
|
||||
{
|
||||
patch = patch.Remove($"{context.Path}/{index}");
|
||||
}
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplaceListOfObject(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
return ReplaceListOfObjectOrPrimative(patch, context);
|
||||
}
|
||||
|
||||
private JsonPatchDocument ReplaceListOfObjectOrPrimative(JsonPatchDocument patch, Context context)
|
||||
{
|
||||
if (context.ApplyToken is JArray apply)
|
||||
{
|
||||
if (context.LiveToken is JArray live &&
|
||||
_tokenEqualityComparer.Equals(apply, live))
|
||||
{
|
||||
// live is correct
|
||||
}
|
||||
else
|
||||
{
|
||||
// live array has any differences, or live is not an array
|
||||
patch = patch.Replace(context.Path, context.ApplyToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw NewFormatException(context);
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
private static string EscapePath(string name)
|
||||
{
|
||||
return name.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private struct Context
|
||||
{
|
||||
public Context(CreatePatchParameters context)
|
||||
{
|
||||
Path = string.Empty;
|
||||
Kind = context.ResourceKind ?? DefaultResourceKind.Unknown;
|
||||
Element = context.ResourceKind?.Schema ?? DefaultResourceKindElement.Unknown;
|
||||
ApplyToken = (JToken)context.ApplyResource;
|
||||
LastAppliedToken = (JToken)context.LastAppliedResource;
|
||||
LiveToken = (JToken)context.LiveResource;
|
||||
}
|
||||
|
||||
public Context(string path, IResourceKind kind, IResourceKindElement element, JToken apply, JToken lastApplied, JToken live) : this()
|
||||
{
|
||||
Path = path;
|
||||
Kind = kind;
|
||||
Element = element;
|
||||
ApplyToken = apply;
|
||||
LastAppliedToken = lastApplied;
|
||||
LiveToken = live;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public IResourceKind Kind { get; }
|
||||
|
||||
public IResourceKindElement Element { get; }
|
||||
|
||||
public JToken ApplyToken { get; }
|
||||
|
||||
public JToken LastAppliedToken { get; }
|
||||
|
||||
public JToken LiveToken { get; }
|
||||
|
||||
public Context Push(string path, IResourceKindElement element, JToken apply, JToken lastApplied, JToken live)
|
||||
{
|
||||
return new Context(path, Kind, element, apply, lastApplied, live);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,98 +8,97 @@ using System.Collections.Generic;
|
|||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Microsoft.Kubernetes.Resources
|
||||
namespace Microsoft.Kubernetes.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Class ResourceSerializers implements the resource serializers interface.
|
||||
/// Implements the <see cref="IResourceSerializers" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="IResourceSerializers" />
|
||||
public class ResourceSerializers : IResourceSerializers
|
||||
{
|
||||
private readonly IDeserializer _yamlDeserializer;
|
||||
private readonly JsonSerializerSettings _serializationSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Class ResourceSerializers implements the resource serializers interface.
|
||||
/// Implements the <see cref="IResourceSerializers" />.
|
||||
/// Initializes a new instance of the <see cref="ResourceSerializers"/> class.
|
||||
/// </summary>
|
||||
/// <seealso cref="IResourceSerializers" />
|
||||
public class ResourceSerializers : IResourceSerializers
|
||||
public ResourceSerializers()
|
||||
{
|
||||
private readonly IDeserializer _yamlDeserializer;
|
||||
private readonly JsonSerializerSettings _serializationSettings;
|
||||
_yamlDeserializer = new DeserializerBuilder()
|
||||
.WithNodeTypeResolver(new NonStringScalarTypeResolver())
|
||||
.Build();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResourceSerializers"/> class.
|
||||
/// </summary>
|
||||
public ResourceSerializers()
|
||||
_serializationSettings = new JsonSerializerSettings
|
||||
{
|
||||
_yamlDeserializer = new DeserializerBuilder()
|
||||
.WithNodeTypeResolver(new NonStringScalarTypeResolver())
|
||||
.Build();
|
||||
|
||||
_serializationSettings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.None,
|
||||
DateFormatHandling = DateFormatHandling.IsoDateFormat,
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
|
||||
ContractResolver = new ReadOnlyJsonContractResolver(),
|
||||
Converters = new List<JsonConverter>
|
||||
Formatting = Formatting.None,
|
||||
DateFormatHandling = DateFormatHandling.IsoDateFormat,
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
|
||||
ContractResolver = new ReadOnlyJsonContractResolver(),
|
||||
Converters = new List<JsonConverter>
|
||||
{
|
||||
new Iso8601TimeSpanConverter(),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public T DeserializeYaml<T>(string yaml)
|
||||
public T DeserializeYaml<T>(string yaml)
|
||||
{
|
||||
var resource = _yamlDeserializer.Deserialize<object>(yaml);
|
||||
|
||||
return Convert<T>(resource);
|
||||
}
|
||||
|
||||
public T DeserializeJson<T>(string json)
|
||||
{
|
||||
return SafeJsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
|
||||
public string SerializeJson(object resource)
|
||||
{
|
||||
return SafeJsonConvert.SerializeObject(resource, _serializationSettings);
|
||||
}
|
||||
|
||||
public TResource Convert<TResource>(object resource)
|
||||
{
|
||||
var json = SafeJsonConvert.SerializeObject(resource, _serializationSettings);
|
||||
|
||||
return DeserializeJson<TResource>(json);
|
||||
}
|
||||
|
||||
private class NonStringScalarTypeResolver : INodeTypeResolver
|
||||
{
|
||||
bool INodeTypeResolver.Resolve(NodeEvent nodeEvent, ref Type currentType)
|
||||
{
|
||||
var resource = _yamlDeserializer.Deserialize<object>(yaml);
|
||||
|
||||
return Convert<T>(resource);
|
||||
}
|
||||
|
||||
public T DeserializeJson<T>(string json)
|
||||
{
|
||||
return SafeJsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
|
||||
public string SerializeJson(object resource)
|
||||
{
|
||||
return SafeJsonConvert.SerializeObject(resource, _serializationSettings);
|
||||
}
|
||||
|
||||
public TResource Convert<TResource>(object resource)
|
||||
{
|
||||
var json = SafeJsonConvert.SerializeObject(resource, _serializationSettings);
|
||||
|
||||
return DeserializeJson<TResource>(json);
|
||||
}
|
||||
|
||||
private class NonStringScalarTypeResolver : INodeTypeResolver
|
||||
{
|
||||
bool INodeTypeResolver.Resolve(NodeEvent nodeEvent, ref Type currentType)
|
||||
if (currentType == typeof(object) && nodeEvent is Scalar)
|
||||
{
|
||||
if (currentType == typeof(object) && nodeEvent is Scalar)
|
||||
var scalar = nodeEvent as Scalar;
|
||||
if (scalar.IsPlainImplicit)
|
||||
{
|
||||
var scalar = nodeEvent as Scalar;
|
||||
if (scalar.IsPlainImplicit)
|
||||
// TODO: should use the correct boolean parser (which accepts yes/no) instead of bool.tryparse
|
||||
if (bool.TryParse(scalar.Value, out var _))
|
||||
{
|
||||
// TODO: should use the correct boolean parser (which accepts yes/no) instead of bool.tryparse
|
||||
if (bool.TryParse(scalar.Value, out var _))
|
||||
{
|
||||
currentType = typeof(bool);
|
||||
return true;
|
||||
}
|
||||
currentType = typeof(bool);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (int.TryParse(scalar.Value, out var _))
|
||||
{
|
||||
currentType = typeof(int);
|
||||
return true;
|
||||
}
|
||||
if (int.TryParse(scalar.Value, out var _))
|
||||
{
|
||||
currentType = typeof(int);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (double.TryParse(scalar.Value, out var _))
|
||||
{
|
||||
currentType = typeof(double);
|
||||
return true;
|
||||
}
|
||||
if (double.TryParse(scalar.Value, out var _))
|
||||
{
|
||||
currentType = typeof(double);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,28 +14,28 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.CustomResources
|
||||
{
|
||||
/// <summary>
|
||||
/// Class CustomResourceDefinitionGenerator generates CRD documents for .NET types.
|
||||
/// Implements the <see cref="ICustomResourceDefinitionGenerator" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="ICustomResourceDefinitionGenerator" />.
|
||||
public class CustomResourceDefinitionGenerator : ICustomResourceDefinitionGenerator
|
||||
{
|
||||
private readonly JsonSchemaGeneratorSettings _jsonSchemaGeneratorSettings;
|
||||
private readonly JsonSerializerSettings _serializerSettings;
|
||||
namespace Microsoft.Kubernetes.CustomResources;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomResourceDefinitionGenerator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
public CustomResourceDefinitionGenerator()
|
||||
/// <summary>
|
||||
/// Class CustomResourceDefinitionGenerator generates CRD documents for .NET types.
|
||||
/// Implements the <see cref="ICustomResourceDefinitionGenerator" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="ICustomResourceDefinitionGenerator" />.
|
||||
public class CustomResourceDefinitionGenerator : ICustomResourceDefinitionGenerator
|
||||
{
|
||||
private readonly JsonSchemaGeneratorSettings _jsonSchemaGeneratorSettings;
|
||||
private readonly JsonSerializerSettings _serializerSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomResourceDefinitionGenerator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
public CustomResourceDefinitionGenerator()
|
||||
{
|
||||
_jsonSchemaGeneratorSettings = new JsonSchemaGeneratorSettings()
|
||||
{
|
||||
_jsonSchemaGeneratorSettings = new JsonSchemaGeneratorSettings()
|
||||
{
|
||||
SchemaType = SchemaType.OpenApi3,
|
||||
TypeMappers =
|
||||
SchemaType = SchemaType.OpenApi3,
|
||||
TypeMappers =
|
||||
{
|
||||
new ObjectTypeMapper(
|
||||
typeof(V1ObjectMeta),
|
||||
|
@ -44,164 +44,163 @@ namespace Microsoft.Kubernetes.CustomResources
|
|||
Type = JsonObjectType.Object,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
_serializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.None,
|
||||
DateFormatHandling = DateFormatHandling.IsoDateFormat,
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
|
||||
ContractResolver = new ReadOnlyJsonContractResolver(),
|
||||
Converters = new List<JsonConverter>
|
||||
_serializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.None,
|
||||
DateFormatHandling = DateFormatHandling.IsoDateFormat,
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
|
||||
ContractResolver = new ReadOnlyJsonContractResolver(),
|
||||
Converters = new List<JsonConverter>
|
||||
{
|
||||
new Iso8601TimeSpanConverter(),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// generate custom resource definition as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
public Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync(Type resourceType, string scope, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
/// <summary>
|
||||
/// generate custom resource definition as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
public Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync(Type resourceType, string scope, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var names = GroupApiVersionKind.From(resourceType);
|
||||
var schema = GenerateJsonSchema(resourceType);
|
||||
var names = GroupApiVersionKind.From(resourceType);
|
||||
var schema = GenerateJsonSchema(resourceType);
|
||||
|
||||
return Task.FromResult(new V1CustomResourceDefinition(
|
||||
apiVersion: $"{V1CustomResourceDefinition.KubeGroup}/{V1CustomResourceDefinition.KubeApiVersion}",
|
||||
kind: V1CustomResourceDefinition.KubeKind,
|
||||
metadata: new V1ObjectMeta(
|
||||
name: names.PluralNameGroup),
|
||||
spec: new V1CustomResourceDefinitionSpec(
|
||||
group: names.Group,
|
||||
names: new V1CustomResourceDefinitionNames(
|
||||
kind: names.Kind,
|
||||
plural: names.PluralName),
|
||||
scope: scope,
|
||||
versions: new List<V1CustomResourceDefinitionVersion>
|
||||
{
|
||||
return Task.FromResult(new V1CustomResourceDefinition(
|
||||
apiVersion: $"{V1CustomResourceDefinition.KubeGroup}/{V1CustomResourceDefinition.KubeApiVersion}",
|
||||
kind: V1CustomResourceDefinition.KubeKind,
|
||||
metadata: new V1ObjectMeta(
|
||||
name: names.PluralNameGroup),
|
||||
spec: new V1CustomResourceDefinitionSpec(
|
||||
group: names.Group,
|
||||
names: new V1CustomResourceDefinitionNames(
|
||||
kind: names.Kind,
|
||||
plural: names.PluralName),
|
||||
scope: scope,
|
||||
versions: new List<V1CustomResourceDefinitionVersion>
|
||||
{
|
||||
new V1CustomResourceDefinitionVersion(
|
||||
name: names.ApiVersion,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: new V1CustomResourceValidation(schema)),
|
||||
})));
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// generate custom resource definition as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
public Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync<TResource>(string scope, CancellationToken cancellationToken = default)
|
||||
/// <summary>
|
||||
/// generate custom resource definition as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
public Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync<TResource>(string scope, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GenerateCustomResourceDefinitionAsync(typeof(TResource), scope, cancellationToken);
|
||||
}
|
||||
|
||||
private V1JSONSchemaProps GenerateJsonSchema(Type resourceType)
|
||||
{
|
||||
// start with JsonSchema
|
||||
var schema = JsonSchema.FromType(resourceType, _jsonSchemaGeneratorSettings);
|
||||
|
||||
// convert to JToken to make alterations
|
||||
var rootToken = JObject.Parse(schema.ToJson());
|
||||
rootToken = RewriteObject(rootToken);
|
||||
rootToken.Remove("$schema");
|
||||
rootToken.Remove("definitions");
|
||||
|
||||
// convert to k8s.Models.V1JSONSchemaProps to return
|
||||
using var reader = new JTokenReader(rootToken);
|
||||
return JsonSerializer
|
||||
.Create(_serializerSettings)
|
||||
.Deserialize<V1JSONSchemaProps>(reader);
|
||||
}
|
||||
|
||||
private JObject RewriteObject(JObject sourceObject)
|
||||
{
|
||||
var targetObject = new JObject();
|
||||
|
||||
var queue = new Queue<JObject>();
|
||||
queue.Enqueue(sourceObject);
|
||||
while (queue.Count != 0)
|
||||
{
|
||||
return GenerateCustomResourceDefinitionAsync(typeof(TResource), scope, cancellationToken);
|
||||
}
|
||||
|
||||
private V1JSONSchemaProps GenerateJsonSchema(Type resourceType)
|
||||
{
|
||||
// start with JsonSchema
|
||||
var schema = JsonSchema.FromType(resourceType, _jsonSchemaGeneratorSettings);
|
||||
|
||||
// convert to JToken to make alterations
|
||||
var rootToken = JObject.Parse(schema.ToJson());
|
||||
rootToken = RewriteObject(rootToken);
|
||||
rootToken.Remove("$schema");
|
||||
rootToken.Remove("definitions");
|
||||
|
||||
// convert to k8s.Models.V1JSONSchemaProps to return
|
||||
using var reader = new JTokenReader(rootToken);
|
||||
return JsonSerializer
|
||||
.Create(_serializerSettings)
|
||||
.Deserialize<V1JSONSchemaProps>(reader);
|
||||
}
|
||||
|
||||
private JObject RewriteObject(JObject sourceObject)
|
||||
{
|
||||
var targetObject = new JObject();
|
||||
|
||||
var queue = new Queue<JObject>();
|
||||
queue.Enqueue(sourceObject);
|
||||
while (queue.Count != 0)
|
||||
sourceObject = queue.Dequeue();
|
||||
foreach (var property in sourceObject.Properties())
|
||||
{
|
||||
sourceObject = queue.Dequeue();
|
||||
foreach (var property in sourceObject.Properties())
|
||||
if (property.Name == "$ref")
|
||||
{
|
||||
if (property.Name == "$ref")
|
||||
// resolve the target of the "$ref"
|
||||
var reference = sourceObject;
|
||||
foreach (var part in property.Value.Value<string>().Split("/"))
|
||||
{
|
||||
// resolve the target of the "$ref"
|
||||
var reference = sourceObject;
|
||||
foreach (var part in property.Value.Value<string>().Split("/"))
|
||||
if (part == "#")
|
||||
{
|
||||
if (part == "#")
|
||||
{
|
||||
reference = (JObject)reference.Root;
|
||||
}
|
||||
else
|
||||
{
|
||||
reference = (JObject)reference[part];
|
||||
}
|
||||
reference = (JObject)reference.Root;
|
||||
}
|
||||
else
|
||||
{
|
||||
reference = (JObject)reference[part];
|
||||
}
|
||||
|
||||
// the referenced object should be merged into the current target
|
||||
queue.Enqueue(reference);
|
||||
|
||||
// and $ref property is not added
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.Name == "additionalProperties" &&
|
||||
property.Value.Type == JTokenType.Boolean &&
|
||||
property.Value.Value<bool>() == false)
|
||||
{
|
||||
// don't add this property when it has a default value
|
||||
continue;
|
||||
}
|
||||
// the referenced object should be merged into the current target
|
||||
queue.Enqueue(reference);
|
||||
|
||||
if (property.Name == "oneOf" &&
|
||||
property.Value.Type == JTokenType.Array &&
|
||||
property.Value.Children().Count() == 1)
|
||||
{
|
||||
// a single oneOf array item should be merged into current object
|
||||
queue.Enqueue(RewriteObject(property.Value.Children().Cast<JObject>().Single()));
|
||||
// and $ref property is not added
|
||||
continue;
|
||||
}
|
||||
|
||||
// and don't add the oneOf property
|
||||
continue;
|
||||
}
|
||||
if (property.Name == "additionalProperties" &&
|
||||
property.Value.Type == JTokenType.Boolean &&
|
||||
property.Value.Value<bool>() == false)
|
||||
{
|
||||
// don't add this property when it has a default value
|
||||
continue;
|
||||
}
|
||||
|
||||
// all other properties are added after the value is rewritten recursively
|
||||
if (!targetObject.ContainsKey(property.Name))
|
||||
{
|
||||
targetObject.Add(property.Name, RewriteToken(property.Value));
|
||||
}
|
||||
if (property.Name == "oneOf" &&
|
||||
property.Value.Type == JTokenType.Array &&
|
||||
property.Value.Children().Count() == 1)
|
||||
{
|
||||
// a single oneOf array item should be merged into current object
|
||||
queue.Enqueue(RewriteObject(property.Value.Children().Cast<JObject>().Single()));
|
||||
|
||||
// and don't add the oneOf property
|
||||
continue;
|
||||
}
|
||||
|
||||
// all other properties are added after the value is rewritten recursively
|
||||
if (!targetObject.ContainsKey(property.Name))
|
||||
{
|
||||
targetObject.Add(property.Name, RewriteToken(property.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return targetObject;
|
||||
}
|
||||
|
||||
private JToken RewriteToken(JToken sourceToken)
|
||||
return targetObject;
|
||||
}
|
||||
|
||||
private JToken RewriteToken(JToken sourceToken)
|
||||
{
|
||||
if (sourceToken is JObject sourceObject)
|
||||
{
|
||||
if (sourceToken is JObject sourceObject)
|
||||
{
|
||||
return RewriteObject(sourceObject);
|
||||
}
|
||||
else if (sourceToken is JArray sourceArray)
|
||||
{
|
||||
return new JArray(sourceArray.Select(RewriteToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
return sourceToken;
|
||||
}
|
||||
return RewriteObject(sourceObject);
|
||||
}
|
||||
else if (sourceToken is JArray sourceArray)
|
||||
{
|
||||
return new JArray(sourceArray.Select(RewriteToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
return sourceToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,75 +10,74 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.CustomResources
|
||||
namespace Microsoft.Kubernetes.CustomResources;
|
||||
|
||||
public class CustomResourceDefinitionUpdater<TResource> : IHostedService
|
||||
{
|
||||
public class CustomResourceDefinitionUpdater<TResource> : IHostedService
|
||||
private readonly IKubernetes _client;
|
||||
private readonly ICustomResourceDefinitionGenerator _generator;
|
||||
private readonly CustomResourceDefinitionUpdaterOptions<TResource> _options;
|
||||
|
||||
public CustomResourceDefinitionUpdater(
|
||||
IKubernetes client,
|
||||
ICustomResourceDefinitionGenerator generator,
|
||||
IOptions<CustomResourceDefinitionUpdaterOptions<TResource>> options)
|
||||
{
|
||||
private readonly IKubernetes _client;
|
||||
private readonly ICustomResourceDefinitionGenerator _generator;
|
||||
private readonly CustomResourceDefinitionUpdaterOptions<TResource> _options;
|
||||
|
||||
public CustomResourceDefinitionUpdater(
|
||||
IKubernetes client,
|
||||
ICustomResourceDefinitionGenerator generator,
|
||||
IOptions<CustomResourceDefinitionUpdaterOptions<TResource>> options)
|
||||
if (options is null)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
_generator = generator ?? throw new ArgumentNullException(nameof(generator));
|
||||
_options = options.Value;
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
_generator = generator ?? throw new ArgumentNullException(nameof(generator));
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var scope = _options.Scope ?? "Namespaced";
|
||||
|
||||
var crd = await _generator.GenerateCustomResourceDefinitionAsync<TResource>(scope, cancellationToken);
|
||||
|
||||
await CreateOrReplaceCustomResourceDefinitionAsync(crd, cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task<V1CustomResourceDefinition> CreateOrReplaceCustomResourceDefinitionAsync(
|
||||
V1CustomResourceDefinition customResourceDefinition,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: log messages from here
|
||||
|
||||
if (customResourceDefinition is null)
|
||||
{
|
||||
var scope = _options.Scope ?? "Namespaced";
|
||||
|
||||
var crd = await _generator.GenerateCustomResourceDefinitionAsync<TResource>(scope, cancellationToken);
|
||||
|
||||
await CreateOrReplaceCustomResourceDefinitionAsync(crd, cancellationToken);
|
||||
throw new ArgumentNullException(nameof(customResourceDefinition));
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
var existingList = await _client.ListCustomResourceDefinitionAsync(
|
||||
fieldSelector: $"metadata.name={customResourceDefinition.Name()}",
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
var existingCustomResourceDefinition = existingList?.Items?.SingleOrDefault();
|
||||
|
||||
if (existingCustomResourceDefinition != null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
customResourceDefinition.Metadata.ResourceVersion = existingCustomResourceDefinition.ResourceVersion();
|
||||
|
||||
private async Task<V1CustomResourceDefinition> CreateOrReplaceCustomResourceDefinitionAsync(
|
||||
V1CustomResourceDefinition customResourceDefinition,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: log messages from here
|
||||
|
||||
if (customResourceDefinition is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(customResourceDefinition));
|
||||
}
|
||||
|
||||
var existingList = await _client.ListCustomResourceDefinitionAsync(
|
||||
fieldSelector: $"metadata.name={customResourceDefinition.Name()}",
|
||||
return await _client.ReplaceCustomResourceDefinitionAsync(
|
||||
customResourceDefinition,
|
||||
customResourceDefinition.Name(),
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await _client.CreateCustomResourceDefinitionAsync(
|
||||
customResourceDefinition,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
var existingCustomResourceDefinition = existingList?.Items?.SingleOrDefault();
|
||||
|
||||
if (existingCustomResourceDefinition != null)
|
||||
{
|
||||
customResourceDefinition.Metadata.ResourceVersion = existingCustomResourceDefinition.ResourceVersion();
|
||||
|
||||
return await _client.ReplaceCustomResourceDefinitionAsync(
|
||||
customResourceDefinition,
|
||||
customResourceDefinition.Name(),
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await _client.CreateCustomResourceDefinitionAsync(
|
||||
customResourceDefinition,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.Kubernetes.CustomResources
|
||||
{
|
||||
public class CustomResourceDefinitionUpdaterOptions<TResource> : CustomResourceDefinitionUpdaterOptions
|
||||
{
|
||||
}
|
||||
namespace Microsoft.Kubernetes.CustomResources;
|
||||
|
||||
public class CustomResourceDefinitionUpdaterOptions
|
||||
{
|
||||
public string Scope { get; set; }
|
||||
}
|
||||
public class CustomResourceDefinitionUpdaterOptions<TResource> : CustomResourceDefinitionUpdaterOptions
|
||||
{
|
||||
}
|
||||
|
||||
public class CustomResourceDefinitionUpdaterOptions
|
||||
{
|
||||
public string Scope { get; set; }
|
||||
}
|
||||
|
|
|
@ -6,30 +6,29 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.CustomResources
|
||||
namespace Microsoft.Kubernetes.CustomResources;
|
||||
|
||||
/// <summary>
|
||||
/// Interface ICustomResourceDefinitionGenerator is an dependency injection service used to
|
||||
/// generate a Kubernetes CustomResourceDefinition from a serializable resource class.
|
||||
/// </summary>
|
||||
public interface ICustomResourceDefinitionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface ICustomResourceDefinitionGenerator is an dependency injection service used to
|
||||
/// generate a Kubernetes CustomResourceDefinition from a serializable resource class.
|
||||
/// Generates the custom resource definition from the <typeparamref name="TResource"/> class.
|
||||
/// The class should be an <see cref="k8s.IKubernetesObject"/> object with ApiVersion, Type, and Metadata properties.
|
||||
/// </summary>
|
||||
public interface ICustomResourceDefinitionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the custom resource definition from the <typeparamref name="TResource"/> class.
|
||||
/// The class should be an <see cref="k8s.IKubernetesObject"/> object with ApiVersion, Type, and Metadata properties.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync<TResource>(string scope, CancellationToken cancellationToken = default);
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync<TResource>(string scope, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Generates the custom resource definition from the <typeparamref name="TResource"/> class.
|
||||
/// The class should be an <see cref="k8s.IKubernetesObject"/> object with ApiVersion, Type, and Metadata properties.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync(Type resourceType, string scope, CancellationToken cancellationToken = default);
|
||||
}
|
||||
/// <summary>
|
||||
/// Generates the custom resource definition from the <typeparamref name="TResource"/> class.
|
||||
/// The class should be an <see cref="k8s.IKubernetesObject"/> object with ApiVersion, Type, and Metadata properties.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the resource to generate.</typeparam>
|
||||
/// <param name="scope">The scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.</param>
|
||||
/// <returns>The generated V1CustomResourceDefinition instance.</returns>
|
||||
Task<V1CustomResourceDefinition> GenerateCustomResourceDefinitionAsync(Type resourceType, string scope, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
|
@ -5,34 +5,33 @@ using Microsoft.Kubernetes.CustomResources;
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class KubernetesCustomResourceExtensions
|
||||
{
|
||||
public static class KubernetesCustomResourceExtensions
|
||||
public static IServiceCollection AddKubernetesCustomResources(this IServiceCollection services)
|
||||
{
|
||||
public static IServiceCollection AddKubernetesCustomResources(this IServiceCollection services)
|
||||
if (!services.Any(services => services.ServiceType == typeof(ICustomResourceDefinitionGenerator)))
|
||||
{
|
||||
if (!services.Any(services => services.ServiceType == typeof(ICustomResourceDefinitionGenerator)))
|
||||
{
|
||||
services = services.AddTransient<ICustomResourceDefinitionGenerator, CustomResourceDefinitionGenerator>();
|
||||
}
|
||||
return services;
|
||||
services = services.AddTransient<ICustomResourceDefinitionGenerator, CustomResourceDefinitionGenerator>();
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCustomResourceDefinitionUpdater<TResource>(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddKubernetesCustomResources()
|
||||
.AddHostedService<CustomResourceDefinitionUpdater<TResource>>();
|
||||
}
|
||||
public static IServiceCollection AddCustomResourceDefinitionUpdater<TResource>(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddKubernetesCustomResources()
|
||||
.AddHostedService<CustomResourceDefinitionUpdater<TResource>>();
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCustomResourceDefinitionUpdater<TResource>(
|
||||
this IServiceCollection services,
|
||||
Action<CustomResourceDefinitionUpdaterOptions> configure)
|
||||
{
|
||||
return services
|
||||
.AddKubernetesCustomResources()
|
||||
.AddHostedService<CustomResourceDefinitionUpdater<TResource>>()
|
||||
.Configure<CustomResourceDefinitionUpdaterOptions<TResource>>(options => configure(options));
|
||||
}
|
||||
public static IServiceCollection AddCustomResourceDefinitionUpdater<TResource>(
|
||||
this IServiceCollection services,
|
||||
Action<CustomResourceDefinitionUpdaterOptions> configure)
|
||||
{
|
||||
return services
|
||||
.AddKubernetesCustomResources()
|
||||
.AddHostedService<CustomResourceDefinitionUpdater<TResource>>()
|
||||
.Configure<CustomResourceDefinitionUpdaterOptions<TResource>>(options => configure(options));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,37 +4,36 @@
|
|||
using k8s;
|
||||
using k8s.Models;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Caches
|
||||
namespace Microsoft.Kubernetes.Operator.Caches;
|
||||
|
||||
/// <summary>
|
||||
/// Updates an empty or existing work item.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the operator resource.</typeparam>
|
||||
/// <param name="workItem">An existing or default work item.</param>
|
||||
/// <returns></returns>
|
||||
public delegate OperatorCacheWorkItem<TResource> UpdateWorkItem<TResource>(OperatorCacheWorkItem<TResource> workItem)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new();
|
||||
|
||||
/// <summary>
|
||||
/// Interface IOperatorCache.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
public interface IOperatorCache<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates an empty or existing work item.
|
||||
/// Updates the work item.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the operator resource.</typeparam>
|
||||
/// <param name="workItem">An existing or default work item.</param>
|
||||
/// <returns></returns>
|
||||
public delegate OperatorCacheWorkItem<TResource> UpdateWorkItem<TResource>(OperatorCacheWorkItem<TResource> workItem)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new();
|
||||
/// <param name="namespacedName">Name of the namespaced.</param>
|
||||
/// <param name="update">The update.</param>
|
||||
void UpdateWorkItem(NamespacedName namespacedName, UpdateWorkItem<TResource> update);
|
||||
|
||||
/// <summary>
|
||||
/// Interface IOperatorCache.
|
||||
/// Tries the get work item.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
public interface IOperatorCache<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the work item.
|
||||
/// </summary>
|
||||
/// <param name="namespacedName">Name of the namespaced.</param>
|
||||
/// <param name="update">The update.</param>
|
||||
void UpdateWorkItem(NamespacedName namespacedName, UpdateWorkItem<TResource> update);
|
||||
|
||||
/// <summary>
|
||||
/// Tries the get work item.
|
||||
/// </summary>
|
||||
/// <param name="namespacedName">Name of the namespaced.</param>
|
||||
/// <param name="workItem">The work item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool TryGetWorkItem(NamespacedName namespacedName, out OperatorCacheWorkItem<TResource> workItem);
|
||||
}
|
||||
/// <param name="namespacedName">Name of the namespaced.</param>
|
||||
/// <param name="workItem">The work item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool TryGetWorkItem(NamespacedName namespacedName, out OperatorCacheWorkItem<TResource> workItem);
|
||||
}
|
||||
|
|
|
@ -6,62 +6,61 @@ using k8s.Models;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Caches
|
||||
{
|
||||
/// <summary>
|
||||
/// Class OperatorCache.
|
||||
/// Implements the <see cref="IOperatorCache{TResource}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
/// <seealso cref="IOperatorCache{TResource}" />
|
||||
public class OperatorCache<TResource> : IOperatorCache<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
private readonly Dictionary<NamespacedName, OperatorCacheWorkItem<TResource>> _workItems = new Dictionary<NamespacedName, OperatorCacheWorkItem<TResource>>();
|
||||
private readonly object _workItemsSync = new object();
|
||||
namespace Microsoft.Kubernetes.Operator.Caches;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetWorkItem(NamespacedName namespacedName, out OperatorCacheWorkItem<TResource> workItem)
|
||||
/// <summary>
|
||||
/// Class OperatorCache.
|
||||
/// Implements the <see cref="IOperatorCache{TResource}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
/// <seealso cref="IOperatorCache{TResource}" />
|
||||
public class OperatorCache<TResource> : IOperatorCache<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
private readonly Dictionary<NamespacedName, OperatorCacheWorkItem<TResource>> _workItems = new Dictionary<NamespacedName, OperatorCacheWorkItem<TResource>>();
|
||||
private readonly object _workItemsSync = new object();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetWorkItem(NamespacedName namespacedName, out OperatorCacheWorkItem<TResource> workItem)
|
||||
{
|
||||
lock (_workItemsSync)
|
||||
{
|
||||
lock (_workItemsSync)
|
||||
{
|
||||
return _workItems.TryGetValue(namespacedName, out workItem);
|
||||
}
|
||||
return _workItems.TryGetValue(namespacedName, out workItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateWorkItem(NamespacedName namespacedName, UpdateWorkItem<TResource> update)
|
||||
{
|
||||
if (update is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(update));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateWorkItem(NamespacedName namespacedName, UpdateWorkItem<TResource> update)
|
||||
lock (_workItemsSync)
|
||||
{
|
||||
if (update is null)
|
||||
if (_workItems.TryGetValue(namespacedName, out var workItem))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(update));
|
||||
}
|
||||
|
||||
lock (_workItemsSync)
|
||||
{
|
||||
if (_workItems.TryGetValue(namespacedName, out var workItem))
|
||||
// alter an existing entry
|
||||
workItem = update(workItem);
|
||||
if (workItem.IsEmpty)
|
||||
{
|
||||
// alter an existing entry
|
||||
workItem = update(workItem);
|
||||
if (workItem.IsEmpty)
|
||||
{
|
||||
// remove if result has no information
|
||||
_workItems.Remove(namespacedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise update struct in dictionary
|
||||
_workItems[namespacedName] = workItem;
|
||||
}
|
||||
// remove if result has no information
|
||||
_workItems.Remove(namespacedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
workItem = update(OperatorCacheWorkItem<TResource>.Empty);
|
||||
if (workItem.IsEmpty == false)
|
||||
{
|
||||
// add if result has information
|
||||
_workItems.Add(namespacedName, workItem);
|
||||
}
|
||||
// otherwise update struct in dictionary
|
||||
_workItems[namespacedName] = workItem;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
workItem = update(OperatorCacheWorkItem<TResource>.Empty);
|
||||
if (workItem.IsEmpty == false)
|
||||
{
|
||||
// add if result has information
|
||||
_workItems.Add(namespacedName, workItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,85 +7,84 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Caches
|
||||
namespace Microsoft.Kubernetes.Operator.Caches;
|
||||
|
||||
/// <summary>
|
||||
/// Class OperatorCacheWorkItem contains the information needed to reconcile a resource's desired
|
||||
/// state with what's known to exist in the cluster.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the operator resource.</typeparam>
|
||||
public struct OperatorCacheWorkItem<TResource> : IEquatable<OperatorCacheWorkItem<TResource>> where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Class OperatorCacheWorkItem contains the information needed to reconcile a resource's desired
|
||||
/// state with what's known to exist in the cluster.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the operator resource.</typeparam>
|
||||
public struct OperatorCacheWorkItem<TResource> : IEquatable<OperatorCacheWorkItem<TResource>> where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
public static readonly OperatorCacheWorkItem<TResource> Empty = new OperatorCacheWorkItem<TResource>(
|
||||
resource: default,
|
||||
related: ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>>.Empty);
|
||||
|
||||
public OperatorCacheWorkItem(
|
||||
TResource resource,
|
||||
ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> related)
|
||||
{
|
||||
public static readonly OperatorCacheWorkItem<TResource> Empty = new OperatorCacheWorkItem<TResource>(
|
||||
resource: default,
|
||||
related: ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>>.Empty);
|
||||
Resource = resource;
|
||||
Related = related;
|
||||
}
|
||||
|
||||
public OperatorCacheWorkItem(
|
||||
TResource resource,
|
||||
ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> related)
|
||||
{
|
||||
Resource = resource;
|
||||
Related = related;
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets the operator resource.
|
||||
/// </summary>
|
||||
/// <value>The operator resource.</value>
|
||||
public TResource Resource { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operator resource.
|
||||
/// </summary>
|
||||
/// <value>The operator resource.</value>
|
||||
public TResource Resource { get; }
|
||||
/// <summary>
|
||||
/// Gets the related resource which are owned by the operator resource.
|
||||
/// </summary>
|
||||
/// <value>The related resources.</value>
|
||||
public ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> Related { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the related resource which are owned by the operator resource.
|
||||
/// </summary>
|
||||
/// <value>The related resources.</value>
|
||||
public ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> Related { get; }
|
||||
public bool IsEmpty => Resource == null && Related.IsEmpty;
|
||||
|
||||
public bool IsEmpty => Resource == null && Related.IsEmpty;
|
||||
/// <summary>
|
||||
/// Returns a WorkItem with Resource assigned to new value.
|
||||
/// </summary>
|
||||
/// <param name="resource">The new operator resource.</param>
|
||||
/// <returns></returns>
|
||||
public OperatorCacheWorkItem<TResource> SetResource(TResource resource)
|
||||
{
|
||||
return new OperatorCacheWorkItem<TResource>(resource, Related);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a WorkItem with Resource assigned to new value.
|
||||
/// </summary>
|
||||
/// <param name="resource">The new operator resource.</param>
|
||||
/// <returns></returns>
|
||||
public OperatorCacheWorkItem<TResource> SetResource(TResource resource)
|
||||
{
|
||||
return new OperatorCacheWorkItem<TResource>(resource, Related);
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns a WorkItem with Related assigned to new value.
|
||||
/// </summary>
|
||||
/// <param name="resource">The new related resources.</param>
|
||||
/// <returns></returns>
|
||||
public OperatorCacheWorkItem<TResource> SetRelated(ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> related)
|
||||
{
|
||||
return new OperatorCacheWorkItem<TResource>(Resource, related);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a WorkItem with Related assigned to new value.
|
||||
/// </summary>
|
||||
/// <param name="resource">The new related resources.</param>
|
||||
/// <returns></returns>
|
||||
public OperatorCacheWorkItem<TResource> SetRelated(ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> related)
|
||||
{
|
||||
return new OperatorCacheWorkItem<TResource>(Resource, related);
|
||||
}
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is OperatorCacheWorkItem<TResource> item && Equals(item);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is OperatorCacheWorkItem<TResource> item && Equals(item);
|
||||
}
|
||||
public bool Equals(OperatorCacheWorkItem<TResource> other)
|
||||
{
|
||||
return EqualityComparer<TResource>.Default.Equals(Resource, other.Resource) &&
|
||||
EqualityComparer<ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>>>.Default.Equals(Related, other.Related);
|
||||
}
|
||||
|
||||
public bool Equals(OperatorCacheWorkItem<TResource> other)
|
||||
{
|
||||
return EqualityComparer<TResource>.Default.Equals(Resource, other.Resource) &&
|
||||
EqualityComparer<ImmutableDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>>>.Default.Equals(Related, other.Related);
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Resource, Related);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Resource, Related);
|
||||
}
|
||||
public static bool operator ==(OperatorCacheWorkItem<TResource> left, OperatorCacheWorkItem<TResource> right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator ==(OperatorCacheWorkItem<TResource> left, OperatorCacheWorkItem<TResource> right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(OperatorCacheWorkItem<TResource> left, OperatorCacheWorkItem<TResource> right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
public static bool operator !=(OperatorCacheWorkItem<TResource> left, OperatorCacheWorkItem<TResource> right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,34 +7,33 @@ using Microsoft.Extensions.Options;
|
|||
using Microsoft.Kubernetes.Controller.Informers;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator
|
||||
namespace Microsoft.Kubernetes.Operator;
|
||||
|
||||
public class ConfigureOperatorOptions<TOperatorResource, TRelatedResource> : IConfigureNamedOptions<OperatorOptions>
|
||||
where TRelatedResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
public class ConfigureOperatorOptions<TOperatorResource, TRelatedResource> : IConfigureNamedOptions<OperatorOptions>
|
||||
where TRelatedResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
private static GroupApiVersionKind _names = GroupApiVersionKind.From<TOperatorResource>();
|
||||
private readonly IResourceInformer<TRelatedResource> _resourceInformer;
|
||||
|
||||
public ConfigureOperatorOptions(IResourceInformer<TRelatedResource> resourceInformer)
|
||||
{
|
||||
private static GroupApiVersionKind _names = GroupApiVersionKind.From<TOperatorResource>();
|
||||
private readonly IResourceInformer<TRelatedResource> _resourceInformer;
|
||||
_resourceInformer = resourceInformer;
|
||||
}
|
||||
|
||||
public ConfigureOperatorOptions(IResourceInformer<TRelatedResource> resourceInformer)
|
||||
public void Configure(string name, OperatorOptions options)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
_resourceInformer = resourceInformer;
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public void Configure(string name, OperatorOptions options)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (string.Equals(name, _names.PluralNameGroup, StringComparison.Ordinal))
|
||||
{
|
||||
options.Informers.Add(_resourceInformer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(OperatorOptions options)
|
||||
if (string.Equals(name, _names.PluralNameGroup, StringComparison.Ordinal))
|
||||
{
|
||||
options.Informers.Add(_resourceInformer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(OperatorOptions options)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,40 +5,39 @@ using k8s;
|
|||
using k8s.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Generators
|
||||
namespace Microsoft.Kubernetes.Operator.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Class GenerateResult is returned from "IOperatorGenerator.GenerateAsync"/>
|
||||
/// to determine how the operator should proceed.
|
||||
/// </summary>
|
||||
public class GenerateResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GenerateResult is returned from "IOperatorGenerator.GenerateAsync"/>
|
||||
/// to determine how the operator should proceed.
|
||||
/// </summary>
|
||||
public class GenerateResult
|
||||
public GenerateResult()
|
||||
{
|
||||
public GenerateResult()
|
||||
{
|
||||
Resources = new List<IKubernetesObject<V1ObjectMeta>>();
|
||||
}
|
||||
|
||||
public GenerateResult(IList<IKubernetesObject<V1ObjectMeta>> resources)
|
||||
{
|
||||
Resources = resources ?? throw new System.ArgumentNullException(nameof(resources));
|
||||
}
|
||||
|
||||
public GenerateResult(bool shouldReconcile, IList<IKubernetesObject<V1ObjectMeta>> resources)
|
||||
{
|
||||
ShouldReconcile = shouldReconcile;
|
||||
Resources = resources ?? throw new System.ArgumentNullException(nameof(resources));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the operator should reconcile.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if <see cref="Resources"/> should be applied to cluster; otherwise, <c>false</c>.</value>
|
||||
public bool ShouldReconcile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of generated resources to reconcile.
|
||||
/// </summary>
|
||||
/// <value>The resources.</value>
|
||||
public IList<IKubernetesObject<V1ObjectMeta>> Resources { get; }
|
||||
Resources = new List<IKubernetesObject<V1ObjectMeta>>();
|
||||
}
|
||||
|
||||
public GenerateResult(IList<IKubernetesObject<V1ObjectMeta>> resources)
|
||||
{
|
||||
Resources = resources ?? throw new System.ArgumentNullException(nameof(resources));
|
||||
}
|
||||
|
||||
public GenerateResult(bool shouldReconcile, IList<IKubernetesObject<V1ObjectMeta>> resources)
|
||||
{
|
||||
ShouldReconcile = shouldReconcile;
|
||||
Resources = resources ?? throw new System.ArgumentNullException(nameof(resources));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the operator should reconcile.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if <see cref="Resources"/> should be applied to cluster; otherwise, <c>false</c>.</value>
|
||||
public bool ShouldReconcile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of generated resources to reconcile.
|
||||
/// </summary>
|
||||
/// <value>The resources.</value>
|
||||
public IList<IKubernetesObject<V1ObjectMeta>> Resources { get; }
|
||||
}
|
||||
|
|
|
@ -3,19 +3,18 @@
|
|||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Generators
|
||||
namespace Microsoft.Kubernetes.Operator.Generators;
|
||||
|
||||
public interface IOperatorGenerator<TResource>
|
||||
{
|
||||
public interface IOperatorGenerator<TResource>
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the specified resource. The resource document as the input is the source
|
||||
/// of truth, and the same set of child resources should be generated in response every
|
||||
/// time this method is called. The generated resources are then reconciled against the
|
||||
/// current state of the cluster, and any differences result in Create/Patch/Delete operations
|
||||
/// to bring the cluster in line with the desired state.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource which this particular operator takes as input.</param>
|
||||
/// <returns>GenerateResult that determines what should be reconciled.</returns>
|
||||
Task<GenerateResult> GenerateAsync(TResource resource);
|
||||
}
|
||||
/// <summary>
|
||||
/// Generates the specified resource. The resource document as the input is the source
|
||||
/// of truth, and the same set of child resources should be generated in response every
|
||||
/// time this method is called. The generated resources are then reconciled against the
|
||||
/// current state of the cluster, and any differences result in Create/Patch/Delete operations
|
||||
/// to bring the cluster in line with the desired state.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource which this particular operator takes as input.</param>
|
||||
/// <returns>GenerateResult that determines what should be reconciled.</returns>
|
||||
Task<GenerateResult> GenerateAsync(TResource resource);
|
||||
}
|
||||
|
|
|
@ -7,11 +7,10 @@ using System;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator
|
||||
namespace Microsoft.Kubernetes.Operator;
|
||||
|
||||
public interface IOperatorHandler<TResource> : IDisposable
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
{
|
||||
public interface IOperatorHandler<TResource> : IDisposable
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
{
|
||||
Task RunAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
Task RunAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -10,72 +10,71 @@ using Microsoft.Kubernetes.Operator.Reconcilers;
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class KubernetesOperatorExtensions
|
||||
{
|
||||
public static class KubernetesOperatorExtensions
|
||||
public static IServiceCollection AddKubernetesOperatorRuntime(this IServiceCollection services)
|
||||
{
|
||||
public static IServiceCollection AddKubernetesOperatorRuntime(this IServiceCollection services)
|
||||
services = services.AddKubernetesControllerRuntime();
|
||||
|
||||
if (!services.Any(services => services.ServiceType == typeof(IOperatorHandler<>)))
|
||||
{
|
||||
services = services.AddKubernetesControllerRuntime();
|
||||
|
||||
if (!services.Any(services => services.ServiceType == typeof(IOperatorHandler<>)))
|
||||
{
|
||||
services = services.AddSingleton(typeof(IOperatorHandler<>), typeof(OperatorHandler<>));
|
||||
}
|
||||
|
||||
if (!services.Any(services => services.ServiceType == typeof(IOperatorCache<>)))
|
||||
{
|
||||
services = services.AddSingleton(typeof(IOperatorCache<>), typeof(OperatorCache<>));
|
||||
}
|
||||
|
||||
if (!services.Any(services => services.ServiceType == typeof(IOperatorReconciler<>)))
|
||||
{
|
||||
services = services.AddSingleton(typeof(IOperatorReconciler<>), typeof(OperatorReconciler<>));
|
||||
}
|
||||
|
||||
return services;
|
||||
services = services.AddSingleton(typeof(IOperatorHandler<>), typeof(OperatorHandler<>));
|
||||
}
|
||||
|
||||
public static OperatorServiceCollectionBuilder<TResource> AddOperator<TResource>(this IServiceCollection services)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
if (!services.Any(services => services.ServiceType == typeof(IOperatorCache<>)))
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services = services
|
||||
.AddKubernetesOperatorRuntime()
|
||||
.AddHostedService<OperatorHostedService<TResource>>()
|
||||
.RegisterOperatorResourceInformer<TResource, TResource>();
|
||||
|
||||
return new OperatorServiceCollectionBuilder<TResource>(services);
|
||||
services = services.AddSingleton(typeof(IOperatorCache<>), typeof(OperatorCache<>));
|
||||
}
|
||||
|
||||
public static IServiceCollection AddOperator<TResource>(this IServiceCollection services, Action<OperatorServiceCollectionBuilder<TResource>> configure)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
if (!services.Any(services => services.ServiceType == typeof(IOperatorReconciler<>)))
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configure is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
var operatorServices = services.AddOperator<TResource>();
|
||||
configure(operatorServices);
|
||||
return operatorServices.Services;
|
||||
services = services.AddSingleton(typeof(IOperatorReconciler<>), typeof(OperatorReconciler<>));
|
||||
}
|
||||
|
||||
public static IServiceCollection RegisterOperatorResourceInformer<TOperatorResource, TRelatedResource>(this IServiceCollection services)
|
||||
where TRelatedResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
return services;
|
||||
}
|
||||
|
||||
public static OperatorServiceCollectionBuilder<TResource> AddOperator<TResource>(this IServiceCollection services)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
return services
|
||||
.RegisterResourceInformer<TRelatedResource>()
|
||||
.AddTransient<IConfigureOptions<OperatorOptions>, ConfigureOperatorOptions<TOperatorResource, TRelatedResource>>();
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services = services
|
||||
.AddKubernetesOperatorRuntime()
|
||||
.AddHostedService<OperatorHostedService<TResource>>()
|
||||
.RegisterOperatorResourceInformer<TResource, TResource>();
|
||||
|
||||
return new OperatorServiceCollectionBuilder<TResource>(services);
|
||||
}
|
||||
|
||||
public static IServiceCollection AddOperator<TResource>(this IServiceCollection services, Action<OperatorServiceCollectionBuilder<TResource>> configure)
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configure is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
var operatorServices = services.AddOperator<TResource>();
|
||||
configure(operatorServices);
|
||||
return operatorServices.Services;
|
||||
}
|
||||
|
||||
public static IServiceCollection RegisterOperatorResourceInformer<TOperatorResource, TRelatedResource>(this IServiceCollection services)
|
||||
where TRelatedResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
return services
|
||||
.RegisterResourceInformer<TRelatedResource>()
|
||||
.AddTransient<IConfigureOptions<OperatorOptions>, ConfigureOperatorOptions<TOperatorResource, TRelatedResource>>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,254 +15,253 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator
|
||||
namespace Microsoft.Kubernetes.Operator;
|
||||
|
||||
public class OperatorHandler<TResource> : IOperatorHandler<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
public class OperatorHandler<TResource> : IOperatorHandler<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
private static GroupApiVersionKind _names = GroupApiVersionKind.From<TResource>();
|
||||
private readonly List<IResourceInformerRegistration> _registrations = new List<IResourceInformerRegistration>();
|
||||
private readonly IRateLimitingQueue<NamespacedName> _queue;
|
||||
private readonly IOperatorCache<TResource> _cache;
|
||||
private readonly IOperatorReconciler<TResource> _reconciler;
|
||||
private readonly ILogger<OperatorHandler<TResource>> _logger;
|
||||
private bool _disposedValue;
|
||||
|
||||
public OperatorHandler(
|
||||
IOptionsSnapshot<OperatorOptions> optionsProvider,
|
||||
IOperatorCache<TResource> cache,
|
||||
IOperatorReconciler<TResource> reconciler,
|
||||
ILogger<OperatorHandler<TResource>> logger)
|
||||
{
|
||||
private static GroupApiVersionKind _names = GroupApiVersionKind.From<TResource>();
|
||||
private readonly List<IResourceInformerRegistration> _registrations = new List<IResourceInformerRegistration>();
|
||||
private readonly IRateLimitingQueue<NamespacedName> _queue;
|
||||
private readonly IOperatorCache<TResource> _cache;
|
||||
private readonly IOperatorReconciler<TResource> _reconciler;
|
||||
private readonly ILogger<OperatorHandler<TResource>> _logger;
|
||||
private bool _disposedValue;
|
||||
|
||||
public OperatorHandler(
|
||||
IOptionsSnapshot<OperatorOptions> optionsProvider,
|
||||
IOperatorCache<TResource> cache,
|
||||
IOperatorReconciler<TResource> reconciler,
|
||||
ILogger<OperatorHandler<TResource>> logger)
|
||||
if (optionsProvider is null)
|
||||
{
|
||||
if (optionsProvider is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(optionsProvider));
|
||||
}
|
||||
|
||||
var options = optionsProvider.Get(_names.PluralNameGroup);
|
||||
|
||||
foreach (var informer in options.Informers)
|
||||
{
|
||||
_registrations.Add(informer.Register(Notification));
|
||||
}
|
||||
|
||||
var rateLimiter = options.NewRateLimiter();
|
||||
_queue = options.NewRateLimitingQueue(rateLimiter);
|
||||
_cache = cache;
|
||||
_reconciler = reconciler;
|
||||
_logger = logger;
|
||||
throw new ArgumentNullException(nameof(optionsProvider));
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
var options = optionsProvider.Get(_names.PluralNameGroup);
|
||||
|
||||
foreach (var informer in options.Informers)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
_registrations.Add(informer.Register(Notification));
|
||||
}
|
||||
|
||||
var rateLimiter = options.NewRateLimiter();
|
||||
_queue = options.NewRateLimitingQueue(rateLimiter);
|
||||
_cache = cache;
|
||||
_reconciler = reconciler;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (disposing)
|
||||
foreach (var registration in _registrations)
|
||||
{
|
||||
foreach (var registration in _registrations)
|
||||
registration.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Notification(WatchEventType eventType, IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
if (resource is TResource customResource)
|
||||
{
|
||||
OnPrimaryResourceWatchEvent(eventType, customResource);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnRelatedResourceWatchEvent(eventType, resource);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrimaryResourceWatchEvent(WatchEventType watchEventType, TResource resource)
|
||||
{
|
||||
var key = NamespacedName.From(resource);
|
||||
|
||||
_cache.UpdateWorkItem(key, workItem =>
|
||||
{
|
||||
if (watchEventType == WatchEventType.Added || watchEventType == WatchEventType.Modified)
|
||||
{
|
||||
workItem = workItem.SetResource(resource);
|
||||
}
|
||||
else if (watchEventType == WatchEventType.Deleted)
|
||||
{
|
||||
workItem = workItem.SetResource(default);
|
||||
}
|
||||
|
||||
_queue.Add(key);
|
||||
return workItem;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnRelatedResourceWatchEvent(WatchEventType watchEventType, IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
// Check each owner reference on the notified resource
|
||||
foreach (var ownerReference in resource.OwnerReferences() ?? Enumerable.Empty<V1OwnerReference>())
|
||||
{
|
||||
// If this operator's resource type is an owner
|
||||
if (string.Equals(ownerReference.Kind, _names.Kind, StringComparison.Ordinal) &&
|
||||
string.Equals(ownerReference.ApiVersion, _names.GroupApiVersion, StringComparison.Ordinal))
|
||||
{
|
||||
// Then hold on to the resource's current state under the owner's cache entry
|
||||
|
||||
var resourceKey = new NamespacedName(
|
||||
@namespace: resource.Namespace(),
|
||||
name: ownerReference.Name);
|
||||
|
||||
var relatedKey = GroupKindNamespacedName.From(resource);
|
||||
|
||||
_cache.UpdateWorkItem(resourceKey, workItem =>
|
||||
{
|
||||
if (watchEventType == WatchEventType.Added || watchEventType == WatchEventType.Modified)
|
||||
{
|
||||
registration.Dispose();
|
||||
workItem = workItem.SetRelated(workItem.Related.SetItem(relatedKey, resource));
|
||||
}
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Notification(WatchEventType eventType, IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
if (resource is TResource customResource)
|
||||
{
|
||||
OnPrimaryResourceWatchEvent(eventType, customResource);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnRelatedResourceWatchEvent(eventType, resource);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrimaryResourceWatchEvent(WatchEventType watchEventType, TResource resource)
|
||||
{
|
||||
var key = NamespacedName.From(resource);
|
||||
|
||||
_cache.UpdateWorkItem(key, workItem =>
|
||||
{
|
||||
if (watchEventType == WatchEventType.Added || watchEventType == WatchEventType.Modified)
|
||||
{
|
||||
workItem = workItem.SetResource(resource);
|
||||
}
|
||||
else if (watchEventType == WatchEventType.Deleted)
|
||||
{
|
||||
workItem = workItem.SetResource(default);
|
||||
}
|
||||
|
||||
_queue.Add(key);
|
||||
return workItem;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnRelatedResourceWatchEvent(WatchEventType watchEventType, IKubernetesObject<V1ObjectMeta> resource)
|
||||
{
|
||||
// Check each owner reference on the notified resource
|
||||
foreach (var ownerReference in resource.OwnerReferences() ?? Enumerable.Empty<V1OwnerReference>())
|
||||
{
|
||||
// If this operator's resource type is an owner
|
||||
if (string.Equals(ownerReference.Kind, _names.Kind, StringComparison.Ordinal) &&
|
||||
string.Equals(ownerReference.ApiVersion, _names.GroupApiVersion, StringComparison.Ordinal))
|
||||
{
|
||||
// Then hold on to the resource's current state under the owner's cache entry
|
||||
|
||||
var resourceKey = new NamespacedName(
|
||||
@namespace: resource.Namespace(),
|
||||
name: ownerReference.Name);
|
||||
|
||||
var relatedKey = GroupKindNamespacedName.From(resource);
|
||||
|
||||
_cache.UpdateWorkItem(resourceKey, workItem =>
|
||||
else if (watchEventType == WatchEventType.Deleted)
|
||||
{
|
||||
if (watchEventType == WatchEventType.Added || watchEventType == WatchEventType.Modified)
|
||||
{
|
||||
workItem = workItem.SetRelated(workItem.Related.SetItem(relatedKey, resource));
|
||||
}
|
||||
else if (watchEventType == WatchEventType.Deleted)
|
||||
{
|
||||
workItem = workItem.SetRelated(workItem.Related.Remove(relatedKey));
|
||||
}
|
||||
workItem = workItem.SetRelated(workItem.Related.Remove(relatedKey));
|
||||
}
|
||||
|
||||
_queue.Add(resourceKey);
|
||||
return workItem;
|
||||
});
|
||||
}
|
||||
_queue.Add(resourceKey);
|
||||
return workItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken cancellationToken)
|
||||
public async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(new EventId(1, "WaitingForInformers"), "Waiting for resource informers to finish synchronizing.");
|
||||
foreach (var registration in _registrations)
|
||||
{
|
||||
_logger.LogInformation(new EventId(1, "WaitingForInformers"), "Waiting for resource informers to finish synchronizing.");
|
||||
foreach (var registration in _registrations)
|
||||
{
|
||||
await registration.ReadyAsync(cancellationToken);
|
||||
}
|
||||
_logger.LogInformation(new EventId(2, "InformersReady"), "All resource informers are ready.");
|
||||
await registration.ReadyAsync(cancellationToken);
|
||||
}
|
||||
_logger.LogInformation(new EventId(2, "InformersReady"), "All resource informers are ready.");
|
||||
|
||||
while (await ProcessNextWorkItemAsync(cancellationToken))
|
||||
{
|
||||
// loop until complete
|
||||
}
|
||||
while (await ProcessNextWorkItemAsync(cancellationToken))
|
||||
{
|
||||
// loop until complete
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// processNextWorkItem will read a single work item off the workqueue and attempt to process it, by calling the reconcileHandler.
|
||||
/// </summary>
|
||||
private async Task<bool> ProcessNextWorkItemAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// pkg\internal\controller\controller.go:194
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Stop working
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// processNextWorkItem will read a single work item off the workqueue and attempt to process it, by calling the reconcileHandler.
|
||||
/// </summary>
|
||||
private async Task<bool> ProcessNextWorkItemAsync(CancellationToken cancellationToken)
|
||||
var (key, shutdown) = await _queue.GetAsync(cancellationToken);
|
||||
if (shutdown || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// pkg\internal\controller\controller.go:194
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Stop working
|
||||
return false;
|
||||
}
|
||||
|
||||
var (key, shutdown) = await _queue.GetAsync(cancellationToken);
|
||||
if (shutdown || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Stop working
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await ReconcileWorkItemAsync(key, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// We call Done here so the workqueue knows we have finished
|
||||
// processing this item. We also must remember to call Forget if we
|
||||
// do not want this work item being re-queued. For example, we do
|
||||
// not call Forget if a transient error occurs, instead the item is
|
||||
// put back on the workqueue and attempted again after a back-off
|
||||
// period.
|
||||
_queue.Done(key);
|
||||
}
|
||||
// Stop working
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> ReconcileWorkItemAsync(NamespacedName key, CancellationToken cancellationToken)
|
||||
try
|
||||
{
|
||||
// pkg\internal\controller\controller.go:194
|
||||
if (!_cache.TryGetWorkItem(key, out var workItem))
|
||||
{
|
||||
// no knowledge of this resource at all. forget it ever happened.
|
||||
_queue.Forget(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
ReconcileResult result;
|
||||
try
|
||||
{
|
||||
result = await _reconciler.ReconcileAsync(
|
||||
new ReconcileParameters<TResource>(workItem.Resource, workItem.Related),
|
||||
cancellationToken);
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception error)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
result = new ReconcileResult
|
||||
{
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
|
||||
if (result.Error != null)
|
||||
{
|
||||
// TODO: LOG Reconciler error
|
||||
|
||||
_logger.LogInformation(
|
||||
new EventId(3, "ErrorRetry"),
|
||||
"Scheduling retry for {ItemName}.{ItemNamespace}: {ErrorMessage}",
|
||||
key.Name,
|
||||
key.Namespace,
|
||||
result.Error.Message);
|
||||
|
||||
_queue.AddRateLimited(key);
|
||||
return true;
|
||||
}
|
||||
else if (result.RequeueAfter > TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(4, "DelayRetry"),
|
||||
"Scheduling retry in {DelayTime} for {ItemName}.{ItemNamespace}",
|
||||
result.RequeueAfter,
|
||||
key.Name,
|
||||
key.Namespace);
|
||||
|
||||
_queue.Forget(key);
|
||||
_queue.AddAfter(key, result.RequeueAfter);
|
||||
|
||||
// TODO: COUNTER
|
||||
return true;
|
||||
}
|
||||
else if (result.Requeue)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(5, "BackoffRetry"),
|
||||
"Scheduling backoff retry for {ItemName}.{ItemNamespace}",
|
||||
key.Name,
|
||||
key.Namespace);
|
||||
|
||||
_queue.AddRateLimited(key);
|
||||
return true;
|
||||
}
|
||||
return await ReconcileWorkItemAsync(key, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// We call Done here so the workqueue knows we have finished
|
||||
// processing this item. We also must remember to call Forget if we
|
||||
// do not want this work item being re-queued. For example, we do
|
||||
// not call Forget if a transient error occurs, instead the item is
|
||||
// put back on the workqueue and attempted again after a back-off
|
||||
// period.
|
||||
_queue.Done(key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ReconcileWorkItemAsync(NamespacedName key, CancellationToken cancellationToken)
|
||||
{
|
||||
// pkg\internal\controller\controller.go:194
|
||||
if (!_cache.TryGetWorkItem(key, out var workItem))
|
||||
{
|
||||
// no knowledge of this resource at all. forget it ever happened.
|
||||
_queue.Forget(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
ReconcileResult result;
|
||||
try
|
||||
{
|
||||
result = await _reconciler.ReconcileAsync(
|
||||
new ReconcileParameters<TResource>(workItem.Resource, workItem.Related),
|
||||
cancellationToken);
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception error)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
result = new ReconcileResult
|
||||
{
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
|
||||
if (result.Error != null)
|
||||
{
|
||||
// TODO: LOG Reconciler error
|
||||
|
||||
_logger.LogInformation(
|
||||
new EventId(3, "ErrorRetry"),
|
||||
"Scheduling retry for {ItemName}.{ItemNamespace}: {ErrorMessage}",
|
||||
key.Name,
|
||||
key.Namespace,
|
||||
result.Error.Message);
|
||||
|
||||
_queue.AddRateLimited(key);
|
||||
return true;
|
||||
}
|
||||
else if (result.RequeueAfter > TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(4, "DelayRetry"),
|
||||
"Scheduling retry in {DelayTime} for {ItemName}.{ItemNamespace}",
|
||||
result.RequeueAfter,
|
||||
key.Name,
|
||||
key.Namespace);
|
||||
|
||||
_queue.Forget(key);
|
||||
_queue.AddAfter(key, result.RequeueAfter);
|
||||
|
||||
// TODO: COUNTER
|
||||
return true;
|
||||
}
|
||||
else if (result.Requeue)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(5, "BackoffRetry"),
|
||||
"Scheduling backoff retry for {ItemName}.{ItemNamespace}",
|
||||
key.Name,
|
||||
key.Namespace);
|
||||
|
||||
_queue.AddRateLimited(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
_queue.Forget(key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,25 +9,24 @@ using Microsoft.Kubernetes.Controller.Hosting;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator
|
||||
namespace Microsoft.Kubernetes.Operator;
|
||||
|
||||
public class OperatorHostedService<TResource> : BackgroundHostedService
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
public class OperatorHostedService<TResource> : BackgroundHostedService
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
private readonly IOperatorHandler<TResource> _handler;
|
||||
|
||||
public OperatorHostedService(
|
||||
IOperatorHandler<TResource> handler,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger<OperatorHostedService<TResource>> logger)
|
||||
: base(hostApplicationLifetime, logger)
|
||||
{
|
||||
private readonly IOperatorHandler<TResource> _handler;
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public OperatorHostedService(
|
||||
IOperatorHandler<TResource> handler,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger<OperatorHostedService<TResource>> logger)
|
||||
: base(hostApplicationLifetime, logger)
|
||||
{
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _handler.RunAsync(cancellationToken);
|
||||
}
|
||||
public override Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _handler.RunAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,57 +8,56 @@ using Microsoft.Kubernetes.Controller.RateLimiters;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator
|
||||
namespace Microsoft.Kubernetes.Operator;
|
||||
|
||||
public class OperatorOptions
|
||||
{
|
||||
public class OperatorOptions
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets factory for new ratelimitingqueue.
|
||||
/// </summary>
|
||||
/// <value>The new rate limiting queue.</value>
|
||||
public Func<IRateLimiter<NamespacedName>, IRateLimitingQueue<NamespacedName>> NewRateLimitingQueue { get; set; } = NewRateLimitingQueueDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets factory for new ratelimiter.
|
||||
/// </summary>
|
||||
/// <value>The new rate limiter.</value>
|
||||
public Func<IRateLimiter<NamespacedName>> NewRateLimiter { get; set; } = NewRateLimiterDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the informers.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The resource informers which have been registered for a given operator.
|
||||
/// </value>
|
||||
public List<IResourceInformer> Informers { get; } = new List<IResourceInformer>();
|
||||
|
||||
/// <summary>
|
||||
/// NewRateLimitingQueueDefault is the default factory method of a rate limiting work queue.
|
||||
/// </summary>
|
||||
/// <param name="rateLimiter">The rate limiter.</param>
|
||||
/// <returns></returns>
|
||||
private static IRateLimitingQueue<NamespacedName> NewRateLimitingQueueDefault(IRateLimiter<NamespacedName> rateLimiter)
|
||||
{
|
||||
return new RateLimitingQueue<NamespacedName>(rateLimiter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets factory for new ratelimitingqueue.
|
||||
/// </summary>
|
||||
/// <value>The new rate limiting queue.</value>
|
||||
public Func<IRateLimiter<NamespacedName>, IRateLimitingQueue<NamespacedName>> NewRateLimitingQueue { get; set; } = NewRateLimitingQueueDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets factory for new ratelimiter.
|
||||
/// </summary>
|
||||
/// <value>The new rate limiter.</value>
|
||||
public Func<IRateLimiter<NamespacedName>> NewRateLimiter { get; set; } = NewRateLimiterDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the informers.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The resource informers which have been registered for a given operator.
|
||||
/// </value>
|
||||
public List<IResourceInformer> Informers { get; } = new List<IResourceInformer>();
|
||||
|
||||
/// <summary>
|
||||
/// NewRateLimitingQueueDefault is the default factory method of a rate limiting work queue.
|
||||
/// </summary>
|
||||
/// <param name="rateLimiter">The rate limiter.</param>
|
||||
/// <returns></returns>
|
||||
private static IRateLimitingQueue<NamespacedName> NewRateLimitingQueueDefault(IRateLimiter<NamespacedName> rateLimiter)
|
||||
{
|
||||
return new RateLimitingQueue<NamespacedName>(rateLimiter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NewRateLimiterDefault is the default factory method of a rate limiter for a workqueue. It has
|
||||
/// both overall and per-item rate limiting. The overall is a token bucket and the per-item is exponential
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L39.
|
||||
/// </summary>
|
||||
/// <returns>IRateLimiter<NamespacedName>.</returns>
|
||||
private static IRateLimiter<NamespacedName> NewRateLimiterDefault()
|
||||
{
|
||||
return new MaxOfRateLimiter<NamespacedName>(
|
||||
new BucketRateLimiter<NamespacedName>(
|
||||
limiter: new Limiter(
|
||||
limit: new Limit(perSecond: 10),
|
||||
burst: 100)),
|
||||
new ItemExponentialFailureRateLimiter<NamespacedName>(
|
||||
baseDelay: TimeSpan.FromMilliseconds(5),
|
||||
maxDelay: TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
/// <summary>
|
||||
/// NewRateLimiterDefault is the default factory method of a rate limiter for a workqueue. It has
|
||||
/// both overall and per-item rate limiting. The overall is a token bucket and the per-item is exponential
|
||||
/// https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L39.
|
||||
/// </summary>
|
||||
/// <returns>IRateLimiter<NamespacedName>.</returns>
|
||||
private static IRateLimiter<NamespacedName> NewRateLimiterDefault()
|
||||
{
|
||||
return new MaxOfRateLimiter<NamespacedName>(
|
||||
new BucketRateLimiter<NamespacedName>(
|
||||
limiter: new Limiter(
|
||||
limit: new Limit(perSecond: 10),
|
||||
burst: 100)),
|
||||
new ItemExponentialFailureRateLimiter<NamespacedName>(
|
||||
baseDelay: TimeSpan.FromMilliseconds(5),
|
||||
maxDelay: TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,56 +8,55 @@ using Microsoft.Kubernetes.Operator;
|
|||
using Microsoft.Kubernetes.Operator.Generators;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Class OperatorServiceCollection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOperatorResource">The type of the t resource.</typeparam>
|
||||
public class OperatorServiceCollectionBuilder<TOperatorResource>
|
||||
where TOperatorResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
private IServiceCollection _services;
|
||||
|
||||
/// <summary>
|
||||
/// Class OperatorServiceCollection.
|
||||
/// Initializes a new instance of the <see cref="OperatorServiceCollectionBuilder{TResource}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOperatorResource">The type of the t resource.</typeparam>
|
||||
public class OperatorServiceCollectionBuilder<TOperatorResource>
|
||||
where TOperatorResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
/// <param name="services">The services.</param>
|
||||
public OperatorServiceCollectionBuilder(IServiceCollection services)
|
||||
{
|
||||
private IServiceCollection _services;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperatorServiceCollectionBuilder{TResource}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="services">The services.</param>
|
||||
public OperatorServiceCollectionBuilder(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets or sets the services.
|
||||
/// </summary>
|
||||
/// <value>The services.</value>
|
||||
public IServiceCollection Services => _services;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the services.
|
||||
/// </summary>
|
||||
/// <value>The services.</value>
|
||||
public IServiceCollection Services => _services;
|
||||
/// <summary>
|
||||
/// Withes the related resource.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRelatedResource">The type of the t related resource.</typeparam>
|
||||
/// <returns>OperatorServiceCollection<TResource>.</returns>
|
||||
public OperatorServiceCollectionBuilder<TOperatorResource> WithRelatedResource<TRelatedResource>()
|
||||
where TRelatedResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
_services = _services.RegisterOperatorResourceInformer<TOperatorResource, TRelatedResource>();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Withes the related resource.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRelatedResource">The type of the t related resource.</typeparam>
|
||||
/// <returns>OperatorServiceCollection<TResource>.</returns>
|
||||
public OperatorServiceCollectionBuilder<TOperatorResource> WithRelatedResource<TRelatedResource>()
|
||||
where TRelatedResource : class, IKubernetesObject<V1ObjectMeta>, new()
|
||||
{
|
||||
_services = _services.RegisterOperatorResourceInformer<TOperatorResource, TRelatedResource>();
|
||||
return this;
|
||||
}
|
||||
public OperatorServiceCollectionBuilder<TOperatorResource> WithGenerator<TGenerator>()
|
||||
where TGenerator : class, IOperatorGenerator<TOperatorResource>
|
||||
{
|
||||
_services = _services.AddTransient<IOperatorGenerator<TOperatorResource>, TGenerator>();
|
||||
return this;
|
||||
}
|
||||
|
||||
public OperatorServiceCollectionBuilder<TOperatorResource> WithGenerator<TGenerator>()
|
||||
where TGenerator : class, IOperatorGenerator<TOperatorResource>
|
||||
{
|
||||
_services = _services.AddTransient<IOperatorGenerator<TOperatorResource>, TGenerator>();
|
||||
return this;
|
||||
}
|
||||
|
||||
public OperatorServiceCollectionBuilder<TOperatorResource> Configure(Action<OperatorOptions> configureOptions)
|
||||
{
|
||||
var names = GroupApiVersionKind.From<TOperatorResource>();
|
||||
_services = _services.Configure(names.PluralNameGroup, configureOptions);
|
||||
return this;
|
||||
}
|
||||
public OperatorServiceCollectionBuilder<TOperatorResource> Configure(Action<OperatorOptions> configureOptions)
|
||||
{
|
||||
var names = GroupApiVersionKind.From<TOperatorResource>();
|
||||
_services = _services.Configure(names.PluralNameGroup, configureOptions);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,22 +6,20 @@ using k8s.Models;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Reconcilers
|
||||
{
|
||||
namespace Microsoft.Kubernetes.Operator.Reconcilers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IOperatorReconciler.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
public interface IOperatorReconciler<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IOperatorReconciler.
|
||||
/// Reconciles the specified resource.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
public interface IOperatorReconciler<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
{
|
||||
/// <summary>
|
||||
/// Reconciles the specified resource.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The information about desired and current state of cluster resources.</param>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Results of the reconcile.</returns>
|
||||
Task<ReconcileResult> ReconcileAsync(ReconcileParameters<TResource> parameters, CancellationToken cancellationToken);
|
||||
}
|
||||
/// <param name="parameters">The information about desired and current state of cluster resources.</param>
|
||||
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>Results of the reconcile.</returns>
|
||||
Task<ReconcileResult> ReconcileAsync(ReconcileParameters<TResource> parameters, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -20,358 +20,357 @@ using System.Threading.Tasks;
|
|||
|
||||
#pragma warning disable CA2213 // Disposable fields should be disposed
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Reconcilers
|
||||
namespace Microsoft.Kubernetes.Operator.Reconcilers;
|
||||
|
||||
/// <summary>
|
||||
/// Class OperatorReconciler.
|
||||
/// Implements the <see cref="IOperatorReconciler{TResource}" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
/// <seealso cref="IOperatorReconciler{TResource}" />
|
||||
public class OperatorReconciler<TResource> : IOperatorReconciler<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
{
|
||||
private readonly IResourceSerializers _resourceSerializers;
|
||||
private readonly IAnyResourceKind _resourceClient;
|
||||
private readonly IOperatorGenerator<TResource> _generator;
|
||||
private readonly IResourceKindManager _resourceKindManager;
|
||||
private readonly IResourcePatcher _resourcePatcher;
|
||||
private readonly ILogger<OperatorReconciler<TResource>> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Class OperatorReconciler.
|
||||
/// Implements the <see cref="IOperatorReconciler{TResource}" />.
|
||||
/// Initializes a new instance of the <see cref="OperatorReconciler{TResource}" /> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResource">The type of the t resource.</typeparam>
|
||||
/// <seealso cref="IOperatorReconciler{TResource}" />
|
||||
public class OperatorReconciler<TResource> : IOperatorReconciler<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
/// <param name="resourceSerializers">The resource serializers.</param>
|
||||
/// <param name="resourcePatcher">The patch generator.</param>
|
||||
/// <param name="generator">The generator.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public OperatorReconciler(
|
||||
IResourceSerializers resourceSerializers,
|
||||
IKubernetes client,
|
||||
IOperatorGenerator<TResource> generator,
|
||||
IResourceKindManager resourceKindManager,
|
||||
IResourcePatcher resourcePatcher,
|
||||
ILogger<OperatorReconciler<TResource>> logger)
|
||||
{
|
||||
private readonly IResourceSerializers _resourceSerializers;
|
||||
private readonly IAnyResourceKind _resourceClient;
|
||||
private readonly IOperatorGenerator<TResource> _generator;
|
||||
private readonly IResourceKindManager _resourceKindManager;
|
||||
private readonly IResourcePatcher _resourcePatcher;
|
||||
private readonly ILogger<OperatorReconciler<TResource>> _logger;
|
||||
_resourceSerializers = resourceSerializers;
|
||||
_resourceClient = client.AnyResourceKind();
|
||||
_generator = generator;
|
||||
_resourceKindManager = resourceKindManager;
|
||||
_resourcePatcher = resourcePatcher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum UpdateType provides a dimension value for logs and counters.
|
||||
/// </summary>
|
||||
public enum UpdateType
|
||||
{
|
||||
/// <summary>
|
||||
/// A resource is being created.
|
||||
/// </summary>
|
||||
Creating,
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperatorReconciler{TResource}" /> class.
|
||||
/// A resource is being deleted.
|
||||
/// </summary>
|
||||
/// <param name="resourceSerializers">The resource serializers.</param>
|
||||
/// <param name="resourcePatcher">The patch generator.</param>
|
||||
/// <param name="generator">The generator.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public OperatorReconciler(
|
||||
IResourceSerializers resourceSerializers,
|
||||
IKubernetes client,
|
||||
IOperatorGenerator<TResource> generator,
|
||||
IResourceKindManager resourceKindManager,
|
||||
IResourcePatcher resourcePatcher,
|
||||
ILogger<OperatorReconciler<TResource>> logger)
|
||||
{
|
||||
_resourceSerializers = resourceSerializers;
|
||||
_resourceClient = client.AnyResourceKind();
|
||||
_generator = generator;
|
||||
_resourceKindManager = resourceKindManager;
|
||||
_resourcePatcher = resourcePatcher;
|
||||
_logger = logger;
|
||||
}
|
||||
Deleting,
|
||||
|
||||
/// <summary>
|
||||
/// Enum UpdateType provides a dimension value for logs and counters.
|
||||
/// A resource is being patched.
|
||||
/// </summary>
|
||||
public enum UpdateType
|
||||
Patching,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ReconcileResult> ReconcileAsync(ReconcileParameters<TResource> parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
if (parameters is null)
|
||||
{
|
||||
/// <summary>
|
||||
/// A resource is being created.
|
||||
/// </summary>
|
||||
Creating,
|
||||
|
||||
/// <summary>
|
||||
/// A resource is being deleted.
|
||||
/// </summary>
|
||||
Deleting,
|
||||
|
||||
/// <summary>
|
||||
/// A resource is being patched.
|
||||
/// </summary>
|
||||
Patching,
|
||||
throw new ArgumentNullException(nameof(parameters));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ReconcileResult> ReconcileAsync(ReconcileParameters<TResource> parameters, CancellationToken cancellationToken)
|
||||
if (parameters.Resource == null)
|
||||
{
|
||||
if (parameters is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(parameters));
|
||||
}
|
||||
// Custom resource has been deleted - k8s garbage collection will do the rest of the work
|
||||
return default;
|
||||
}
|
||||
|
||||
if (parameters.Resource == null)
|
||||
var resource = parameters.Resource;
|
||||
|
||||
var liveResources = parameters.RelatedResources;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug(
|
||||
new EventId(1, "ReconcileStarting"),
|
||||
"Reconcile starting for {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
resource.Kind,
|
||||
resource.Name(),
|
||||
resource.Namespace());
|
||||
|
||||
var generatedResources = await _generator.GenerateAsync(resource);
|
||||
|
||||
if (generatedResources.ShouldReconcile == false)
|
||||
{
|
||||
// Custom resource has been deleted - k8s garbage collection will do the rest of the work
|
||||
// the resource is not in a state which can be reconciled
|
||||
// return default results and let the next informer notification
|
||||
// cause a reconciliation
|
||||
return default;
|
||||
}
|
||||
|
||||
var resource = parameters.Resource;
|
||||
|
||||
var liveResources = parameters.RelatedResources;
|
||||
|
||||
try
|
||||
foreach (var generatedResource in generatedResources.Resources)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
new EventId(1, "ReconcileStarting"),
|
||||
"Reconcile starting for {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
resource.Kind,
|
||||
resource.Name(),
|
||||
resource.Namespace());
|
||||
|
||||
var generatedResources = await _generator.GenerateAsync(resource);
|
||||
|
||||
if (generatedResources.ShouldReconcile == false)
|
||||
var generatedKey = GroupKindNamespacedName.From(generatedResource);
|
||||
if (!liveResources.TryGetValue(generatedKey, out var liveResource))
|
||||
{
|
||||
// the resource is not in a state which can be reconciled
|
||||
// return default results and let the next informer notification
|
||||
// cause a reconciliation
|
||||
return default;
|
||||
var ownerReference = new V1OwnerReference(
|
||||
apiVersion: resource.ApiVersion,
|
||||
kind: resource.Kind,
|
||||
name: resource.Name(),
|
||||
uid: resource.Uid());
|
||||
|
||||
await CreateGeneratedResourceAsync(generatedResource, ownerReference, cancellationToken);
|
||||
}
|
||||
|
||||
foreach (var generatedResource in generatedResources.Resources)
|
||||
else
|
||||
{
|
||||
var generatedKey = GroupKindNamespacedName.From(generatedResource);
|
||||
if (!liveResources.TryGetValue(generatedKey, out var liveResource))
|
||||
{
|
||||
var ownerReference = new V1OwnerReference(
|
||||
apiVersion: resource.ApiVersion,
|
||||
kind: resource.Kind,
|
||||
name: resource.Name(),
|
||||
uid: resource.Uid());
|
||||
var patch = await CalculateJsonPatchDocumentAsync(generatedResource, liveResource);
|
||||
|
||||
await CreateGeneratedResourceAsync(generatedResource, ownerReference, cancellationToken);
|
||||
}
|
||||
else
|
||||
if (patch.Operations.Any())
|
||||
{
|
||||
var patch = await CalculateJsonPatchDocumentAsync(generatedResource, liveResource);
|
||||
|
||||
if (patch.Operations.Any())
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await PatchResourceAsync(generatedResource, liveResource, patch, cancellationToken);
|
||||
}
|
||||
catch (ReconcilerException ex) when (ex.Status.Reason == "Invalid" && (ex.Status.Details?.Causes?.Any() ?? false))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
new EventId(7, "PatchDeleteFallback"),
|
||||
"Deleting existing resource due to patch failure: {PatchMessage}",
|
||||
ex.Status.Message);
|
||||
await PatchResourceAsync(generatedResource, liveResource, patch, cancellationToken);
|
||||
}
|
||||
catch (ReconcilerException ex) when (ex.Status.Reason == "Invalid" && (ex.Status.Details?.Causes?.Any() ?? false))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
new EventId(7, "PatchDeleteFallback"),
|
||||
"Deleting existing resource due to patch failure: {PatchMessage}",
|
||||
ex.Status.Message);
|
||||
|
||||
await DeleteLiveResourceAsync(liveResource, cancellationToken);
|
||||
return default;
|
||||
}
|
||||
await DeleteLiveResourceAsync(liveResource, cancellationToken);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var generatedResourceKeys = generatedResources.Resources.ToDictionary(GroupKindNamespacedName.From);
|
||||
foreach (var (liveResourceKey, liveResource) in liveResources)
|
||||
{
|
||||
if (!generatedResourceKeys.ContainsKey(liveResourceKey))
|
||||
{
|
||||
// existing resource has an ownerReference which is no longer generated by this operator
|
||||
// it's deleted here because k8s garbage collection won't know to remove it
|
||||
await DeleteLiveResourceAsync(liveResource, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
finally
|
||||
|
||||
var generatedResourceKeys = generatedResources.Resources.ToDictionary(GroupKindNamespacedName.From);
|
||||
foreach (var (liveResourceKey, liveResource) in liveResources)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
new EventId(2, "ReconcileComplete"),
|
||||
"Reconcile completed for {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
resource.Kind,
|
||||
resource.Name(),
|
||||
resource.Namespace());
|
||||
if (!generatedResourceKeys.ContainsKey(liveResourceKey))
|
||||
{
|
||||
// existing resource has an ownerReference which is no longer generated by this operator
|
||||
// it's deleted here because k8s garbage collection won't know to remove it
|
||||
await DeleteLiveResourceAsync(liveResource, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateGeneratedResourceAsync(
|
||||
IKubernetesObject<V1ObjectMeta> generatedResource,
|
||||
V1OwnerReference ownerReference,
|
||||
CancellationToken cancellationToken)
|
||||
return default;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(3, "CreatingResource"),
|
||||
"{UpdateType} resource {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
UpdateType.Creating,
|
||||
generatedResource.Kind,
|
||||
generatedResource.Name(),
|
||||
generatedResource.Namespace());
|
||||
|
||||
generatedResource.SetAnnotation(
|
||||
"kubectl.kubernetes.io/last-applied-configuration",
|
||||
_resourceSerializers.SerializeJson(generatedResource));
|
||||
|
||||
generatedResource.AddOwnerReference(ownerReference);
|
||||
|
||||
var kubernetesEntity = generatedResource.GetType().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
|
||||
using var resultResource = await _resourceClient.CreateAnyResourceKindWithHttpMessagesAsync(
|
||||
body: generatedResource,
|
||||
group: generatedResource.ApiGroup(),
|
||||
version: generatedResource.ApiGroupVersion(),
|
||||
namespaceParameter: generatedResource.Namespace() ?? string.Empty,
|
||||
plural: kubernetesEntity?.PluralName,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
if (TryNewReconcilerException(ex, out var reconcilerException))
|
||||
{
|
||||
throw reconcilerException;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
_logger.LogDebug(
|
||||
new EventId(2, "ReconcileComplete"),
|
||||
"Reconcile completed for {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
resource.Kind,
|
||||
resource.Name(),
|
||||
resource.Namespace());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteLiveResourceAsync(
|
||||
IKubernetesObject<V1ObjectMeta> existingResource,
|
||||
CancellationToken cancellationToken)
|
||||
private async Task CreateGeneratedResourceAsync(
|
||||
IKubernetesObject<V1ObjectMeta> generatedResource,
|
||||
V1OwnerReference ownerReference,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(4, "DeletingResource"),
|
||||
new EventId(3, "CreatingResource"),
|
||||
"{UpdateType} resource {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
UpdateType.Deleting,
|
||||
existingResource.Kind,
|
||||
existingResource.Name(),
|
||||
existingResource.Namespace());
|
||||
UpdateType.Creating,
|
||||
generatedResource.Kind,
|
||||
generatedResource.Name(),
|
||||
generatedResource.Namespace());
|
||||
|
||||
var kubernetesEntity = existingResource.GetType().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
generatedResource.SetAnnotation(
|
||||
"kubectl.kubernetes.io/last-applied-configuration",
|
||||
_resourceSerializers.SerializeJson(generatedResource));
|
||||
|
||||
var deleteOptions = new V1DeleteOptions(
|
||||
apiVersion: V1DeleteOptions.KubeApiVersion,
|
||||
kind: V1DeleteOptions.KubeKind,
|
||||
preconditions: new V1Preconditions(
|
||||
resourceVersion: existingResource.ResourceVersion()));
|
||||
generatedResource.AddOwnerReference(ownerReference);
|
||||
|
||||
using var response = await _resourceClient.DeleteAnyResourceKindWithHttpMessagesAsync(
|
||||
group: existingResource.ApiGroup(),
|
||||
version: existingResource.ApiGroupVersion(),
|
||||
namespaceParameter: existingResource.Namespace() ?? string.Empty,
|
||||
var kubernetesEntity = generatedResource.GetType().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
|
||||
using var resultResource = await _resourceClient.CreateAnyResourceKindWithHttpMessagesAsync(
|
||||
body: generatedResource,
|
||||
group: generatedResource.ApiGroup(),
|
||||
version: generatedResource.ApiGroupVersion(),
|
||||
namespaceParameter: generatedResource.Namespace() ?? string.Empty,
|
||||
plural: kubernetesEntity?.PluralName,
|
||||
name: existingResource.Name(),
|
||||
body: deleteOptions,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private async Task PatchResourceAsync(
|
||||
IKubernetesObject<V1ObjectMeta> generatedResource,
|
||||
IKubernetesObject<V1ObjectMeta> liveResource,
|
||||
JsonPatchDocument patch,
|
||||
CancellationToken cancellationToken)
|
||||
catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
try
|
||||
if (TryNewReconcilerException(ex, out var reconcilerException))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(5, "ConfiguringResource"),
|
||||
"{UpdateType} resource {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
UpdateType.Patching,
|
||||
generatedResource.Kind,
|
||||
generatedResource.Name(),
|
||||
generatedResource.Namespace());
|
||||
|
||||
_logger.LogDebug(
|
||||
new EventId(6, "PatchDocument"),
|
||||
"Json patch contains {JsonPatch}",
|
||||
new LazySerialization(patch));
|
||||
|
||||
// ensure PATCH is idempotent - will fail if cache out of date, will fail if applied more than once
|
||||
patch = patch.Test(
|
||||
"/metadata/resourceVersion",
|
||||
liveResource.Metadata.ResourceVersion);
|
||||
|
||||
// lastly, update the json annotation which is the basis for subsequent patch generation
|
||||
patch = patch.Replace(
|
||||
"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration",
|
||||
_resourceSerializers.SerializeJson(generatedResource));
|
||||
|
||||
var kubernetesEntity = generatedResource.GetType().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
|
||||
using var response = await _resourceClient.PatchAnyResourceKindWithHttpMessagesAsync(
|
||||
body: new V1Patch(patch, V1Patch.PatchType.JsonPatch),
|
||||
group: kubernetesEntity.Group,
|
||||
version: kubernetesEntity.ApiVersion,
|
||||
namespaceParameter: generatedResource.Namespace(),
|
||||
plural: kubernetesEntity.PluralName,
|
||||
name: generatedResource.Name(),
|
||||
cancellationToken: cancellationToken);
|
||||
throw reconcilerException;
|
||||
}
|
||||
catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.UnprocessableEntity)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteLiveResourceAsync(
|
||||
IKubernetesObject<V1ObjectMeta> existingResource,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(4, "DeletingResource"),
|
||||
"{UpdateType} resource {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
UpdateType.Deleting,
|
||||
existingResource.Kind,
|
||||
existingResource.Name(),
|
||||
existingResource.Namespace());
|
||||
|
||||
var kubernetesEntity = existingResource.GetType().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
|
||||
var deleteOptions = new V1DeleteOptions(
|
||||
apiVersion: V1DeleteOptions.KubeApiVersion,
|
||||
kind: V1DeleteOptions.KubeKind,
|
||||
preconditions: new V1Preconditions(
|
||||
resourceVersion: existingResource.ResourceVersion()));
|
||||
|
||||
using var response = await _resourceClient.DeleteAnyResourceKindWithHttpMessagesAsync(
|
||||
group: existingResource.ApiGroup(),
|
||||
version: existingResource.ApiGroupVersion(),
|
||||
namespaceParameter: existingResource.Namespace() ?? string.Empty,
|
||||
plural: kubernetesEntity?.PluralName,
|
||||
name: existingResource.Name(),
|
||||
body: deleteOptions,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private async Task PatchResourceAsync(
|
||||
IKubernetesObject<V1ObjectMeta> generatedResource,
|
||||
IKubernetesObject<V1ObjectMeta> liveResource,
|
||||
JsonPatchDocument patch,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
new EventId(5, "ConfiguringResource"),
|
||||
"{UpdateType} resource {ItemKind}/{ItemName}.{ItemNamespace}",
|
||||
UpdateType.Patching,
|
||||
generatedResource.Kind,
|
||||
generatedResource.Name(),
|
||||
generatedResource.Namespace());
|
||||
|
||||
_logger.LogDebug(
|
||||
new EventId(6, "PatchDocument"),
|
||||
"Json patch contains {JsonPatch}",
|
||||
new LazySerialization(patch));
|
||||
|
||||
// ensure PATCH is idempotent - will fail if cache out of date, will fail if applied more than once
|
||||
patch = patch.Test(
|
||||
"/metadata/resourceVersion",
|
||||
liveResource.Metadata.ResourceVersion);
|
||||
|
||||
// lastly, update the json annotation which is the basis for subsequent patch generation
|
||||
patch = patch.Replace(
|
||||
"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration",
|
||||
_resourceSerializers.SerializeJson(generatedResource));
|
||||
|
||||
var kubernetesEntity = generatedResource.GetType().GetCustomAttribute<KubernetesEntityAttribute>();
|
||||
|
||||
using var response = await _resourceClient.PatchAnyResourceKindWithHttpMessagesAsync(
|
||||
body: new V1Patch(patch, V1Patch.PatchType.JsonPatch),
|
||||
group: kubernetesEntity.Group,
|
||||
version: kubernetesEntity.ApiVersion,
|
||||
namespaceParameter: generatedResource.Namespace(),
|
||||
plural: kubernetesEntity.PluralName,
|
||||
name: generatedResource.Name(),
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
if (TryNewReconcilerException(ex, out var reconcilerException))
|
||||
{
|
||||
if (TryNewReconcilerException(ex, out var reconcilerException))
|
||||
{
|
||||
throw reconcilerException;
|
||||
}
|
||||
throw;
|
||||
throw reconcilerException;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JsonPatchDocument> CalculateJsonPatchDocumentAsync(IKubernetesObject<V1ObjectMeta> applyResource, IKubernetesObject<V1ObjectMeta> liveResource)
|
||||
{
|
||||
var parameters = new CreatePatchParameters
|
||||
{
|
||||
ApplyResource = _resourceSerializers.Convert<object>(applyResource),
|
||||
LiveResource = _resourceSerializers.Convert<object>(liveResource),
|
||||
};
|
||||
|
||||
var lastAppliedConfiguration = liveResource.GetAnnotation("kubectl.kubernetes.io/last-applied-configuration");
|
||||
if (!string.IsNullOrEmpty(lastAppliedConfiguration))
|
||||
{
|
||||
parameters.LastAppliedResource = _resourceSerializers.DeserializeJson<object>(lastAppliedConfiguration);
|
||||
}
|
||||
|
||||
private async Task<JsonPatchDocument> CalculateJsonPatchDocumentAsync(IKubernetesObject<V1ObjectMeta> applyResource, IKubernetesObject<V1ObjectMeta> liveResource)
|
||||
parameters.ResourceKind = await _resourceKindManager.GetResourceKindAsync(
|
||||
apiVersion: applyResource.ApiVersion,
|
||||
kind: applyResource.Kind).ConfigureAwait(false);
|
||||
|
||||
return _resourcePatcher.CreateJsonPatch(parameters);
|
||||
}
|
||||
|
||||
private bool TryNewReconcilerException(HttpOperationException innerException, out ReconcilerException reconcilerException)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parameters = new CreatePatchParameters
|
||||
var status = _resourceSerializers.DeserializeJson<V1Status>(innerException.Response.Content);
|
||||
if (status.Kind == V1Status.KubeKind)
|
||||
{
|
||||
ApplyResource = _resourceSerializers.Convert<object>(applyResource),
|
||||
LiveResource = _resourceSerializers.Convert<object>(liveResource),
|
||||
};
|
||||
|
||||
var lastAppliedConfiguration = liveResource.GetAnnotation("kubectl.kubernetes.io/last-applied-configuration");
|
||||
if (!string.IsNullOrEmpty(lastAppliedConfiguration))
|
||||
{
|
||||
parameters.LastAppliedResource = _resourceSerializers.DeserializeJson<object>(lastAppliedConfiguration);
|
||||
reconcilerException = new ReconcilerException(status, innerException);
|
||||
return true;
|
||||
}
|
||||
|
||||
parameters.ResourceKind = await _resourceKindManager.GetResourceKindAsync(
|
||||
apiVersion: applyResource.ApiVersion,
|
||||
kind: applyResource.Kind).ConfigureAwait(false);
|
||||
|
||||
return _resourcePatcher.CreateJsonPatch(parameters);
|
||||
reconcilerException = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryNewReconcilerException(HttpOperationException innerException, out ReconcilerException reconcilerException)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = _resourceSerializers.DeserializeJson<V1Status>(innerException.Response.Content);
|
||||
if (status.Kind == V1Status.KubeKind)
|
||||
{
|
||||
reconcilerException = new ReconcilerException(status, innerException);
|
||||
return true;
|
||||
}
|
||||
reconcilerException = default;
|
||||
return false;
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch
|
||||
catch
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
// Don't allow deserialization errors to replace original exception
|
||||
reconcilerException = default;
|
||||
return false;
|
||||
}
|
||||
{
|
||||
// Don't allow deserialization errors to replace original exception
|
||||
reconcilerException = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct LazySerialization enables the json string to be created only when debug logging is enabled.
|
||||
/// </summary>
|
||||
private struct LazySerialization
|
||||
{
|
||||
private readonly JsonPatchDocument _patch;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LazySerialization"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="patch">The patch.</param>
|
||||
public LazySerialization(JsonPatchDocument patch)
|
||||
{
|
||||
_patch = patch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct LazySerialization enables the json string to be created only when debug logging is enabled.
|
||||
/// Returns a <see cref="string" /> that represents this instance.
|
||||
/// </summary>
|
||||
private struct LazySerialization
|
||||
/// <returns>A <see cref="string" /> that represents this instance.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
private readonly JsonPatchDocument _patch;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LazySerialization"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="patch">The patch.</param>
|
||||
public LazySerialization(JsonPatchDocument patch)
|
||||
{
|
||||
_patch = patch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="string" /> that represents this instance.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonConvert.SerializeObject(_patch, Formatting.None);
|
||||
}
|
||||
return JsonConvert.SerializeObject(_patch, Formatting.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,17 @@ using k8s.Models;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator
|
||||
{
|
||||
public class ReconcileParameters<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
{
|
||||
public ReconcileParameters(TResource resource, IDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> relatedResources)
|
||||
{
|
||||
Resource = resource;
|
||||
RelatedResources = relatedResources ?? throw new ArgumentNullException(nameof(relatedResources));
|
||||
}
|
||||
namespace Microsoft.Kubernetes.Operator;
|
||||
|
||||
public TResource Resource { get; }
|
||||
public IDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> RelatedResources { get; }
|
||||
public class ReconcileParameters<TResource>
|
||||
where TResource : class, IKubernetesObject<V1ObjectMeta>
|
||||
{
|
||||
public ReconcileParameters(TResource resource, IDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> relatedResources)
|
||||
{
|
||||
Resource = resource;
|
||||
RelatedResources = relatedResources ?? throw new ArgumentNullException(nameof(relatedResources));
|
||||
}
|
||||
|
||||
public TResource Resource { get; }
|
||||
public IDictionary<GroupKindNamespacedName, IKubernetesObject<V1ObjectMeta>> RelatedResources { get; }
|
||||
}
|
||||
|
|
|
@ -4,39 +4,38 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator
|
||||
namespace Microsoft.Kubernetes.Operator;
|
||||
|
||||
public struct ReconcileResult : IEquatable<ReconcileResult>
|
||||
{
|
||||
public struct ReconcileResult : IEquatable<ReconcileResult>
|
||||
public bool Requeue { get; set; }
|
||||
public TimeSpan RequeueAfter { get; set; }
|
||||
public Exception Error { get; set; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
public bool Requeue { get; set; }
|
||||
public TimeSpan RequeueAfter { get; set; }
|
||||
public Exception Error { get; set; }
|
||||
return obj is ReconcileResult result && Equals(result);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ReconcileResult result && Equals(result);
|
||||
}
|
||||
public bool Equals(ReconcileResult other)
|
||||
{
|
||||
return Requeue == other.Requeue &&
|
||||
RequeueAfter.Equals(other.RequeueAfter) &&
|
||||
EqualityComparer<Exception>.Default.Equals(Error, other.Error);
|
||||
}
|
||||
|
||||
public bool Equals(ReconcileResult other)
|
||||
{
|
||||
return Requeue == other.Requeue &&
|
||||
RequeueAfter.Equals(other.RequeueAfter) &&
|
||||
EqualityComparer<Exception>.Default.Equals(Error, other.Error);
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Requeue, RequeueAfter, Error);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Requeue, RequeueAfter, Error);
|
||||
}
|
||||
public static bool operator ==(ReconcileResult left, ReconcileResult right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator ==(ReconcileResult left, ReconcileResult right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ReconcileResult left, ReconcileResult right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
public static bool operator !=(ReconcileResult left, ReconcileResult right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,41 +5,40 @@ using k8s.Models;
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Microsoft.Kubernetes.Operator.Reconcilers
|
||||
namespace Microsoft.Kubernetes.Operator.Reconcilers;
|
||||
|
||||
[Serializable]
|
||||
public class ReconcilerException : Exception
|
||||
{
|
||||
[Serializable]
|
||||
public class ReconcilerException : Exception
|
||||
public ReconcilerException()
|
||||
{
|
||||
public ReconcilerException()
|
||||
{
|
||||
}
|
||||
|
||||
public ReconcilerException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ReconcilerException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public ReconcilerException(V1Status status)
|
||||
: base((status ?? throw new ArgumentNullException(nameof(status))).Message)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public ReconcilerException(V1Status status, Exception innerException)
|
||||
: base((status ?? throw new ArgumentNullException(nameof(status))).Message, innerException)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
|
||||
protected ReconcilerException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
public V1Status Status { get; }
|
||||
}
|
||||
|
||||
public ReconcilerException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ReconcilerException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public ReconcilerException(V1Status status)
|
||||
: base((status ?? throw new ArgumentNullException(nameof(status))).Message)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public ReconcilerException(V1Status status, Exception innerException)
|
||||
: base((status ?? throw new ArgumentNullException(nameof(status))).Message, innerException)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
|
||||
protected ReconcilerException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
public V1Status Status { get; }
|
||||
}
|
||||
|
|
|
@ -7,131 +7,130 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKindProvider.OpenApi
|
||||
namespace Microsoft.Kubernetes.ResourceKindProvider.OpenApi;
|
||||
|
||||
public class OpenApiResourceKind : IResourceKind
|
||||
{
|
||||
public class OpenApiResourceKind : IResourceKind
|
||||
private readonly Dictionary<JsonSchema, IResourceKindElement> _elements = new Dictionary<JsonSchema, IResourceKindElement>();
|
||||
private readonly object _elementsSync = new object();
|
||||
|
||||
public OpenApiResourceKind(string apiVersion, string kind, JsonSchema jsonSchema)
|
||||
{
|
||||
private readonly Dictionary<JsonSchema, IResourceKindElement> _elements = new Dictionary<JsonSchema, IResourceKindElement>();
|
||||
private readonly object _elementsSync = new object();
|
||||
|
||||
public OpenApiResourceKind(string apiVersion, string kind, JsonSchema jsonSchema)
|
||||
if (string.IsNullOrEmpty(apiVersion))
|
||||
{
|
||||
if (string.IsNullOrEmpty(apiVersion))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(apiVersion)}' cannot be null or empty", nameof(apiVersion));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(kind))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(kind)}' cannot be null or empty", nameof(kind));
|
||||
}
|
||||
|
||||
if (jsonSchema is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(jsonSchema));
|
||||
}
|
||||
|
||||
ApiVersion = apiVersion;
|
||||
Kind = kind;
|
||||
Schema = BindElement(jsonSchema.ActualSchema);
|
||||
throw new ArgumentException($"'{nameof(apiVersion)}' cannot be null or empty", nameof(apiVersion));
|
||||
}
|
||||
|
||||
public string ApiVersion { get; }
|
||||
|
||||
public string Kind { get; }
|
||||
|
||||
public IResourceKindElement Schema { get; }
|
||||
|
||||
internal IResourceKindElement BindElement(JsonSchema schema)
|
||||
if (string.IsNullOrEmpty(kind))
|
||||
{
|
||||
lock (_elementsSync)
|
||||
throw new ArgumentException($"'{nameof(kind)}' cannot be null or empty", nameof(kind));
|
||||
}
|
||||
|
||||
if (jsonSchema is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(jsonSchema));
|
||||
}
|
||||
|
||||
ApiVersion = apiVersion;
|
||||
Kind = kind;
|
||||
Schema = BindElement(jsonSchema.ActualSchema);
|
||||
}
|
||||
|
||||
public string ApiVersion { get; }
|
||||
|
||||
public string Kind { get; }
|
||||
|
||||
public IResourceKindElement Schema { get; }
|
||||
|
||||
internal IResourceKindElement BindElement(JsonSchema schema)
|
||||
{
|
||||
lock (_elementsSync)
|
||||
{
|
||||
if (_elements.TryGetValue(schema, out var element))
|
||||
{
|
||||
if (_elements.TryGetValue(schema, out var element))
|
||||
{
|
||||
return element;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
if (IsPrimative(schema))
|
||||
{
|
||||
element = DefaultResourceKindElement.ReplacePrimative;
|
||||
}
|
||||
else if (schema.IsArray)
|
||||
{
|
||||
var itemSchema = schema.Item.ActualSchema;
|
||||
var hasMergePatchStrategy = HasPatchStrategy(schema, "merge");
|
||||
if (IsPrimative(schema))
|
||||
{
|
||||
element = DefaultResourceKindElement.ReplacePrimative;
|
||||
}
|
||||
else if (schema.IsArray)
|
||||
{
|
||||
var itemSchema = schema.Item.ActualSchema;
|
||||
var hasMergePatchStrategy = HasPatchStrategy(schema, "merge");
|
||||
|
||||
if (IsPrimative(itemSchema))
|
||||
if (IsPrimative(itemSchema))
|
||||
{
|
||||
if (hasMergePatchStrategy)
|
||||
{
|
||||
if (hasMergePatchStrategy)
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeListOfPrimative);
|
||||
}
|
||||
else
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.ReplaceListOfPrimative);
|
||||
}
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeListOfPrimative);
|
||||
}
|
||||
else
|
||||
{
|
||||
var collectionTypeSchema = schema.Item.HasReference ? schema.Item.Reference : schema.Item;
|
||||
|
||||
if (hasMergePatchStrategy && HasPatchMergeKey(schema, out var patchMergeKey))
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeListOfObject, mergeKey: patchMergeKey as string);
|
||||
}
|
||||
else
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.ReplaceListOfObject);
|
||||
}
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.ReplaceListOfPrimative);
|
||||
}
|
||||
}
|
||||
else if (schema.IsDictionary)
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeMap);
|
||||
}
|
||||
else if (schema.IsObject)
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("Unable to process schema.");
|
||||
var collectionTypeSchema = schema.Item.HasReference ? schema.Item.Reference : schema.Item;
|
||||
|
||||
if (hasMergePatchStrategy && HasPatchMergeKey(schema, out var patchMergeKey))
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeListOfObject, mergeKey: patchMergeKey as string);
|
||||
}
|
||||
else
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.ReplaceListOfObject);
|
||||
}
|
||||
}
|
||||
|
||||
_elements[schema] = element;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasPatchStrategy(JsonSchema schema, string value)
|
||||
{
|
||||
return
|
||||
schema.ExtensionData != null &&
|
||||
schema.ExtensionData.TryGetValue("x-kubernetes-patch-strategy", out var patchStrategy) &&
|
||||
(patchStrategy as string ?? string.Empty).Split(',').Any(part => part == value);
|
||||
}
|
||||
|
||||
private static bool HasPatchMergeKey(JsonSchema schema, out string mergeKey)
|
||||
{
|
||||
if (schema.ExtensionData != null &&
|
||||
schema.ExtensionData.TryGetValue("x-kubernetes-patch-merge-key", out var value) &&
|
||||
value is string stringValue)
|
||||
else if (schema.IsDictionary)
|
||||
{
|
||||
mergeKey = stringValue;
|
||||
return true;
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeMap);
|
||||
}
|
||||
else if (schema.IsObject)
|
||||
{
|
||||
element = new OpenApiResourceKindElement(this, schema, ElementMergeStrategy.MergeObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("Unable to process schema.");
|
||||
}
|
||||
|
||||
mergeKey = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPrimative(JsonSchema schema)
|
||||
{
|
||||
return
|
||||
schema.Type == JsonObjectType.String ||
|
||||
schema.Type == JsonObjectType.Integer ||
|
||||
schema.Type == JsonObjectType.Boolean ||
|
||||
schema.Type == JsonObjectType.Number;
|
||||
_elements[schema] = element;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasPatchStrategy(JsonSchema schema, string value)
|
||||
{
|
||||
return
|
||||
schema.ExtensionData != null &&
|
||||
schema.ExtensionData.TryGetValue("x-kubernetes-patch-strategy", out var patchStrategy) &&
|
||||
(patchStrategy as string ?? string.Empty).Split(',').Any(part => part == value);
|
||||
}
|
||||
|
||||
private static bool HasPatchMergeKey(JsonSchema schema, out string mergeKey)
|
||||
{
|
||||
if (schema.ExtensionData != null &&
|
||||
schema.ExtensionData.TryGetValue("x-kubernetes-patch-merge-key", out var value) &&
|
||||
value is string stringValue)
|
||||
{
|
||||
mergeKey = stringValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
mergeKey = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPrimative(JsonSchema schema)
|
||||
{
|
||||
return
|
||||
schema.Type == JsonObjectType.String ||
|
||||
schema.Type == JsonObjectType.Integer ||
|
||||
schema.Type == JsonObjectType.Boolean ||
|
||||
schema.Type == JsonObjectType.Number;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,67 +5,66 @@ using Microsoft.Kubernetes.ResourceKinds;
|
|||
using NJsonSchema;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKindProvider.OpenApi
|
||||
namespace Microsoft.Kubernetes.ResourceKindProvider.OpenApi;
|
||||
|
||||
public class OpenApiResourceKindElement : IResourceKindElement
|
||||
{
|
||||
public class OpenApiResourceKindElement : IResourceKindElement
|
||||
private readonly OpenApiResourceKind _resourceKind;
|
||||
private readonly JsonSchema _jsonSchema;
|
||||
private readonly Lazy<IResourceKindElement> _collectionElementType;
|
||||
|
||||
|
||||
public OpenApiResourceKindElement(
|
||||
OpenApiResourceKind resourceKind,
|
||||
JsonSchema jsonSchema,
|
||||
ElementMergeStrategy mergeStrategy,
|
||||
string mergeKey = default)
|
||||
{
|
||||
private readonly OpenApiResourceKind _resourceKind;
|
||||
private readonly JsonSchema _jsonSchema;
|
||||
private readonly Lazy<IResourceKindElement> _collectionElementType;
|
||||
_collectionElementType = new Lazy<IResourceKindElement>(BindCollectionElementType);
|
||||
_resourceKind = resourceKind;
|
||||
_jsonSchema = jsonSchema;
|
||||
MergeStrategy = mergeStrategy;
|
||||
MergeKey = mergeKey;
|
||||
}
|
||||
|
||||
public ElementMergeStrategy MergeStrategy { get; }
|
||||
|
||||
public OpenApiResourceKindElement(
|
||||
OpenApiResourceKind resourceKind,
|
||||
JsonSchema jsonSchema,
|
||||
ElementMergeStrategy mergeStrategy,
|
||||
string mergeKey = default)
|
||||
public string MergeKey { get; }
|
||||
|
||||
public IResourceKindElement GetPropertyElementType(string name)
|
||||
{
|
||||
// TODO: Cache properties by name
|
||||
|
||||
if (_jsonSchema.ActualProperties.TryGetValue(name, out var property))
|
||||
{
|
||||
_collectionElementType = new Lazy<IResourceKindElement>(BindCollectionElementType);
|
||||
_resourceKind = resourceKind;
|
||||
_jsonSchema = jsonSchema;
|
||||
MergeStrategy = mergeStrategy;
|
||||
MergeKey = mergeKey;
|
||||
return _resourceKind.BindElement(property.ActualSchema);
|
||||
}
|
||||
|
||||
public ElementMergeStrategy MergeStrategy { get; }
|
||||
return DefaultResourceKindElement.Unknown;
|
||||
}
|
||||
|
||||
public string MergeKey { get; }
|
||||
public IResourceKindElement GetCollectionElementType() => _collectionElementType.Value;
|
||||
|
||||
public IResourceKindElement GetPropertyElementType(string name)
|
||||
private IResourceKindElement BindCollectionElementType()
|
||||
{
|
||||
return MergeStrategy switch
|
||||
{
|
||||
// TODO: Cache properties by name
|
||||
// array strategies
|
||||
ElementMergeStrategy.MergeListOfObject => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
ElementMergeStrategy.MergeListOfPrimative => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
ElementMergeStrategy.ReplaceListOfObject => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
ElementMergeStrategy.ReplaceListOfPrimative => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
|
||||
if (_jsonSchema.ActualProperties.TryGetValue(name, out var property))
|
||||
{
|
||||
return _resourceKind.BindElement(property.ActualSchema);
|
||||
}
|
||||
// dictionary strategy
|
||||
ElementMergeStrategy.MergeMap => _resourceKind.BindElement(_jsonSchema.AdditionalPropertiesSchema.ActualSchema),
|
||||
|
||||
return DefaultResourceKindElement.Unknown;
|
||||
}
|
||||
// non-collection strategies
|
||||
ElementMergeStrategy.MergeObject => DefaultResourceKindElement.Unknown,
|
||||
ElementMergeStrategy.ReplacePrimative => DefaultResourceKindElement.Unknown,
|
||||
ElementMergeStrategy.Unknown => DefaultResourceKindElement.Unknown,
|
||||
|
||||
public IResourceKindElement GetCollectionElementType() => _collectionElementType.Value;
|
||||
|
||||
private IResourceKindElement BindCollectionElementType()
|
||||
{
|
||||
return MergeStrategy switch
|
||||
{
|
||||
// array strategies
|
||||
ElementMergeStrategy.MergeListOfObject => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
ElementMergeStrategy.MergeListOfPrimative => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
ElementMergeStrategy.ReplaceListOfObject => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
ElementMergeStrategy.ReplaceListOfPrimative => _resourceKind.BindElement(_jsonSchema.Item.ActualSchema),
|
||||
|
||||
// dictionary strategy
|
||||
ElementMergeStrategy.MergeMap => _resourceKind.BindElement(_jsonSchema.AdditionalPropertiesSchema.ActualSchema),
|
||||
|
||||
// non-collection strategies
|
||||
ElementMergeStrategy.MergeObject => DefaultResourceKindElement.Unknown,
|
||||
ElementMergeStrategy.ReplacePrimative => DefaultResourceKindElement.Unknown,
|
||||
ElementMergeStrategy.Unknown => DefaultResourceKindElement.Unknown,
|
||||
|
||||
// unexpected enum value
|
||||
_ => throw new InvalidOperationException($"Merge strategy '${MergeStrategy}' does not support GetCollectionElementType.")
|
||||
};
|
||||
}
|
||||
// unexpected enum value
|
||||
_ => throw new InvalidOperationException($"Merge strategy '${MergeStrategy}' does not support GetCollectionElementType.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,108 +11,107 @@ using System.IO;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.ResourceKinds.OpenApi
|
||||
namespace Microsoft.Kubernetes.ResourceKinds.OpenApi;
|
||||
|
||||
public class OpenApiResourceKindProvider : IResourceKindProvider
|
||||
{
|
||||
public class OpenApiResourceKindProvider : IResourceKindProvider
|
||||
private readonly Dictionary<(string apiVersion, string kind), OpenApiResourceKind> _resourceKinds = new Dictionary<(string apiVersion, string kind), OpenApiResourceKind>();
|
||||
private readonly object _resourceKindsSync = new object();
|
||||
private readonly Lazy<Task<IDictionary<string, JsonSchema>>> _lazyDefinitions;
|
||||
private readonly Lazy<Task<ApiVersionKindSchemasDictionary>> _lazyApiVersionKindSchemas;
|
||||
private readonly ILogger<OpenApiResourceKindProvider> _logger;
|
||||
|
||||
public OpenApiResourceKindProvider(ILogger<OpenApiResourceKindProvider> logger)
|
||||
{
|
||||
private readonly Dictionary<(string apiVersion, string kind), OpenApiResourceKind> _resourceKinds = new Dictionary<(string apiVersion, string kind), OpenApiResourceKind>();
|
||||
private readonly object _resourceKindsSync = new object();
|
||||
private readonly Lazy<Task<IDictionary<string, JsonSchema>>> _lazyDefinitions;
|
||||
private readonly Lazy<Task<ApiVersionKindSchemasDictionary>> _lazyApiVersionKindSchemas;
|
||||
private readonly ILogger<OpenApiResourceKindProvider> _logger;
|
||||
_lazyDefinitions = new Lazy<Task<IDictionary<string, JsonSchema>>>(LoadDefinitions, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
_lazyApiVersionKindSchemas = new Lazy<Task<ApiVersionKindSchemasDictionary>>(LoadApiVersionKindSchemas, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public OpenApiResourceKindProvider(ILogger<OpenApiResourceKindProvider> logger)
|
||||
public async Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind)
|
||||
{
|
||||
var key = (apiVersion, kind);
|
||||
lock (_resourceKindsSync)
|
||||
{
|
||||
_lazyDefinitions = new Lazy<Task<IDictionary<string, JsonSchema>>>(LoadDefinitions, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
_lazyApiVersionKindSchemas = new Lazy<Task<ApiVersionKindSchemasDictionary>>(LoadApiVersionKindSchemas, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
_logger = logger;
|
||||
if (_resourceKinds.TryGetValue(key, out var cachedResourceKind))
|
||||
{
|
||||
return cachedResourceKind;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IResourceKind> GetResourceKindAsync(string apiVersion, string kind)
|
||||
var apiVersionKindSchemas = await _lazyApiVersionKindSchemas.Value;
|
||||
if (!apiVersionKindSchemas.TryGetValue(key, out var schema))
|
||||
{
|
||||
var key = (apiVersion, kind);
|
||||
lock (_resourceKindsSync)
|
||||
return null;
|
||||
}
|
||||
|
||||
var resourceKind = new OpenApiResourceKind(apiVersion, kind, schema);
|
||||
|
||||
lock (_resourceKindsSync)
|
||||
{
|
||||
if (!_resourceKinds.TryAdd(key, resourceKind))
|
||||
{
|
||||
if (_resourceKinds.TryGetValue(key, out var cachedResourceKind))
|
||||
resourceKind = _resourceKinds[key];
|
||||
}
|
||||
}
|
||||
return resourceKind;
|
||||
}
|
||||
|
||||
public async Task<IDictionary<string, JsonSchema>> LoadDefinitions()
|
||||
{
|
||||
using var stream = typeof(OpenApiResourceKindProvider).Assembly.GetManifestResourceStream(typeof(OpenApiResourceKindProvider), "swagger.json");
|
||||
if (stream == null)
|
||||
{
|
||||
_logger.LogError(
|
||||
new EventId(1, "MissingEmbeddedStream"),
|
||||
"Assembly {AssemblyName} does not contain embedded stream {EmbeddedNamespace}.{EmbeddedName}",
|
||||
typeof(OpenApiResourceKindProvider).Assembly.GetName().Name,
|
||||
typeof(OpenApiResourceKindProvider).FullName,
|
||||
"swagger.json");
|
||||
|
||||
throw new FileNotFoundException(
|
||||
"Embedded stream not found",
|
||||
$"{typeof(OpenApiResourceKindProvider).FullName}.swagger.json");
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
var openApiDocument = await OpenApiDocument.FromJsonAsync(json);
|
||||
return openApiDocument.Definitions;
|
||||
}
|
||||
|
||||
private async Task<ApiVersionKindSchemasDictionary> LoadApiVersionKindSchemas()
|
||||
{
|
||||
var definitions = await _lazyDefinitions.Value;
|
||||
|
||||
var schemas = new ApiVersionKindSchemasDictionary();
|
||||
|
||||
foreach (var (_, definition) in definitions)
|
||||
{
|
||||
if (definition.ExtensionData?.TryGetValue("x-kubernetes-group-version-kind", out var _) ?? false)
|
||||
{
|
||||
var groupVersionKindElements = (object[])definition.ExtensionData["x-kubernetes-group-version-kind"];
|
||||
var groupVersionKind = (Dictionary<string, object>)groupVersionKindElements[0];
|
||||
|
||||
var group = (string)groupVersionKind["group"];
|
||||
var version = (string)groupVersionKind["version"];
|
||||
var kind = (string)groupVersionKind["kind"];
|
||||
|
||||
if (string.IsNullOrEmpty(group))
|
||||
{
|
||||
return cachedResourceKind;
|
||||
schemas[(version, kind)] = definition;
|
||||
}
|
||||
else
|
||||
{
|
||||
schemas[($"{group}/{version}", kind)] = definition;
|
||||
}
|
||||
}
|
||||
|
||||
var apiVersionKindSchemas = await _lazyApiVersionKindSchemas.Value;
|
||||
if (!apiVersionKindSchemas.TryGetValue(key, out var schema))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resourceKind = new OpenApiResourceKind(apiVersion, kind, schema);
|
||||
|
||||
lock (_resourceKindsSync)
|
||||
{
|
||||
if (!_resourceKinds.TryAdd(key, resourceKind))
|
||||
{
|
||||
resourceKind = _resourceKinds[key];
|
||||
}
|
||||
}
|
||||
return resourceKind;
|
||||
}
|
||||
|
||||
public async Task<IDictionary<string, JsonSchema>> LoadDefinitions()
|
||||
{
|
||||
using var stream = typeof(OpenApiResourceKindProvider).Assembly.GetManifestResourceStream(typeof(OpenApiResourceKindProvider), "swagger.json");
|
||||
if (stream == null)
|
||||
{
|
||||
_logger.LogError(
|
||||
new EventId(1, "MissingEmbeddedStream"),
|
||||
"Assembly {AssemblyName} does not contain embedded stream {EmbeddedNamespace}.{EmbeddedName}",
|
||||
typeof(OpenApiResourceKindProvider).Assembly.GetName().Name,
|
||||
typeof(OpenApiResourceKindProvider).FullName,
|
||||
"swagger.json");
|
||||
return schemas;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(
|
||||
"Embedded stream not found",
|
||||
$"{typeof(OpenApiResourceKindProvider).FullName}.swagger.json");
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
var openApiDocument = await OpenApiDocument.FromJsonAsync(json);
|
||||
return openApiDocument.Definitions;
|
||||
}
|
||||
|
||||
private async Task<ApiVersionKindSchemasDictionary> LoadApiVersionKindSchemas()
|
||||
{
|
||||
var definitions = await _lazyDefinitions.Value;
|
||||
|
||||
var schemas = new ApiVersionKindSchemasDictionary();
|
||||
|
||||
foreach (var (_, definition) in definitions)
|
||||
{
|
||||
if (definition.ExtensionData?.TryGetValue("x-kubernetes-group-version-kind", out var _) ?? false)
|
||||
{
|
||||
var groupVersionKindElements = (object[])definition.ExtensionData["x-kubernetes-group-version-kind"];
|
||||
var groupVersionKind = (Dictionary<string, object>)groupVersionKindElements[0];
|
||||
|
||||
var group = (string)groupVersionKind["group"];
|
||||
var version = (string)groupVersionKind["version"];
|
||||
var kind = (string)groupVersionKind["kind"];
|
||||
|
||||
if (string.IsNullOrEmpty(group))
|
||||
{
|
||||
schemas[(version, kind)] = definition;
|
||||
}
|
||||
else
|
||||
{
|
||||
schemas[($"{group}/{version}", kind)] = definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schemas;
|
||||
}
|
||||
|
||||
internal sealed class ApiVersionKindSchemasDictionary : Dictionary<(string apiVersion, string kind), JsonSchema>
|
||||
{
|
||||
}
|
||||
internal sealed class ApiVersionKindSchemasDictionary : Dictionary<(string apiVersion, string kind), JsonSchema>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,40 +6,39 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.Kubernetes.Testing.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Testing
|
||||
namespace Microsoft.Kubernetes.Testing;
|
||||
|
||||
[Route("api/{version}/{plural}")]
|
||||
public class ResourceApiController : Controller
|
||||
{
|
||||
[Route("api/{version}/{plural}")]
|
||||
public class ResourceApiController : Controller
|
||||
private readonly ITestCluster _testCluster;
|
||||
|
||||
public ResourceApiController(ITestCluster testCluster)
|
||||
{
|
||||
private readonly ITestCluster _testCluster;
|
||||
_testCluster = testCluster;
|
||||
}
|
||||
|
||||
public ResourceApiController(ITestCluster testCluster)
|
||||
{
|
||||
_testCluster = testCluster;
|
||||
}
|
||||
[FromRoute]
|
||||
public string Version { get; set; }
|
||||
|
||||
[FromRoute]
|
||||
public string Version { get; set; }
|
||||
|
||||
[FromRoute]
|
||||
public string Plural { get; set; }
|
||||
[FromRoute]
|
||||
public string Plural { get; set; }
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListAsync(ListParameters parameters)
|
||||
{
|
||||
var list = await _testCluster.ListResourcesAsync(string.Empty, Version, Plural, parameters);
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListAsync(ListParameters parameters)
|
||||
{
|
||||
var list = await _testCluster.ListResourcesAsync(string.Empty, Version, Plural, parameters);
|
||||
|
||||
var result = new KubernetesList<ResourceObject>(
|
||||
apiVersion: Version,
|
||||
kind: "PodList",
|
||||
metadata: new V1ListMeta(
|
||||
continueProperty: list.Continue,
|
||||
remainingItemCount: null,
|
||||
resourceVersion: list.ResourceVersion),
|
||||
items: list.Items);
|
||||
var result = new KubernetesList<ResourceObject>(
|
||||
apiVersion: Version,
|
||||
kind: "PodList",
|
||||
metadata: new V1ListMeta(
|
||||
continueProperty: list.Continue,
|
||||
remainingItemCount: null,
|
||||
resourceVersion: list.ResourceVersion),
|
||||
items: list.Items);
|
||||
|
||||
return new ObjectResult(result);
|
||||
}
|
||||
return new ObjectResult(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,42 +6,41 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.Kubernetes.Testing.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Testing
|
||||
namespace Microsoft.Kubernetes.Testing;
|
||||
|
||||
[Route("apis/{group}/{version}/{plural}")]
|
||||
public class ResourceApiGroupController : Controller
|
||||
{
|
||||
[Route("apis/{group}/{version}/{plural}")]
|
||||
public class ResourceApiGroupController : Controller
|
||||
private readonly ITestCluster _testCluster;
|
||||
|
||||
public ResourceApiGroupController(ITestCluster testCluster)
|
||||
{
|
||||
private readonly ITestCluster _testCluster;
|
||||
_testCluster = testCluster;
|
||||
}
|
||||
|
||||
public ResourceApiGroupController(ITestCluster testCluster)
|
||||
{
|
||||
_testCluster = testCluster;
|
||||
}
|
||||
[FromRoute]
|
||||
public string Group { get; set; }
|
||||
|
||||
[FromRoute]
|
||||
public string Group { get; set; }
|
||||
[FromRoute]
|
||||
public string Version { get; set; }
|
||||
|
||||
[FromRoute]
|
||||
public string Version { get; set; }
|
||||
[FromRoute]
|
||||
public string Plural { get; set; }
|
||||
|
||||
[FromRoute]
|
||||
public string Plural { get; set; }
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListAsync(ListParameters parameters)
|
||||
{
|
||||
var list = await _testCluster.ListResourcesAsync(Group, Version, Plural, parameters);
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListAsync(ListParameters parameters)
|
||||
{
|
||||
var list = await _testCluster.ListResourcesAsync(Group, Version, Plural, parameters);
|
||||
var result = new KubernetesList<ResourceObject>(
|
||||
apiVersion: $"{Group}/{Version}",
|
||||
kind: "DeploymentList",
|
||||
metadata: new V1ListMeta(
|
||||
continueProperty: list.Continue,
|
||||
remainingItemCount: null,
|
||||
resourceVersion: list.ResourceVersion),
|
||||
items: list.Items);
|
||||
|
||||
var result = new KubernetesList<ResourceObject>(
|
||||
apiVersion: $"{Group}/{Version}",
|
||||
kind: "DeploymentList",
|
||||
metadata: new V1ListMeta(
|
||||
continueProperty: list.Continue,
|
||||
remainingItemCount: null,
|
||||
resourceVersion: list.ResourceVersion),
|
||||
items: list.Items);
|
||||
|
||||
return new ObjectResult(result);
|
||||
}
|
||||
return new ObjectResult(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,11 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.Kubernetes.Testing.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kubernetes.Testing
|
||||
{
|
||||
public interface ITestCluster
|
||||
{
|
||||
Task UnhandledRequest(HttpContext context);
|
||||
namespace Microsoft.Kubernetes.Testing;
|
||||
|
||||
Task<ListResult> ListResourcesAsync(string group, string version, string plural, ListParameters parameters);
|
||||
}
|
||||
public interface ITestCluster
|
||||
{
|
||||
Task UnhandledRequest(HttpContext context);
|
||||
|
||||
Task<ListResult> ListResourcesAsync(string group, string version, string plural, ListParameters parameters);
|
||||
}
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче