Convert to file-scoped namespace (#1382)

This commit is contained in:
Kahbazi 2021-11-15 21:45:32 +03:30 коммит произвёл GitHub
Родитель bbd2742c21
Коммит efd2a4c869
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
202 изменённых файлов: 10659 добавлений и 10861 удалений

Просмотреть файл

@ -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&lt;System.ValueTuple&lt;TItem, System.Boolean&gt;&gt;.</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&lt;System.ValueTuple&lt;TItem, System.Boolean&gt;&gt;.</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&lt;System.ValueTuple&lt;TItem, System.Boolean&gt;&gt;.</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&lt;System.ValueTuple&lt;TItem, System.Boolean&gt;&gt;.</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 Reservations 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 Reservations 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&lt;NamespacedName&gt;.</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&lt;NamespacedName&gt;.</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&lt;TResource&gt;.</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&lt;TResource&gt;.</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);
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше