diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj
index 758f5549f..3e54e61a0 100644
--- a/src/KubernetesClient/KubernetesClient.csproj
+++ b/src/KubernetesClient/KubernetesClient.csproj
@@ -45,4 +45,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/KubernetesClient/Util/Common/CallGeneratorParams.cs b/src/KubernetesClient/Util/Common/CallGeneratorParams.cs
new file mode 100644
index 000000000..fbdc6ac2d
--- /dev/null
+++ b/src/KubernetesClient/Util/Common/CallGeneratorParams.cs
@@ -0,0 +1,16 @@
+namespace k8s.Util.Common
+{
+ public class CallGeneratorParams
+ {
+ public bool Watch { get; }
+ public string ResourceVersion { get; }
+ public int? TimeoutSeconds { get; }
+
+ public CallGeneratorParams(bool watch, string resourceVersion, int? timeoutSeconds)
+ {
+ Watch = watch;
+ ResourceVersion = resourceVersion;
+ TimeoutSeconds = timeoutSeconds;
+ }
+ }
+}
diff --git a/src/KubernetesClient/Util/Common/CollectionsExtensions.cs b/src/KubernetesClient/Util/Common/CollectionsExtensions.cs
new file mode 100644
index 000000000..4043f53df
--- /dev/null
+++ b/src/KubernetesClient/Util/Common/CollectionsExtensions.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+
+namespace k8s.Util.Common
+{
+ internal static class CollectionsExtensions
+ {
+ public static void AddRange(this HashSet hashSet, ICollection items)
+ {
+ if (items == null)
+ {
+ return;
+ }
+
+ foreach (var item in items)
+ {
+ hashSet?.Add(item);
+ }
+ }
+
+ internal static TValue ComputeIfAbsent(this IDictionary dictionary, TKey key, Func mappingFunction)
+ {
+ if (dictionary is null)
+ {
+ throw new ArgumentNullException(nameof(dictionary));
+ }
+
+ if (dictionary.TryGetValue(key, out var value))
+ {
+ return value;
+ }
+
+ if (mappingFunction == null)
+ {
+ throw new ArgumentNullException(nameof(mappingFunction));
+ }
+
+ var newKey = mappingFunction(key);
+ dictionary[key] = newKey;
+ return newKey;
+ }
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/Cache.cs b/src/KubernetesClient/Util/Informer/Cache/Cache.cs
new file mode 100644
index 000000000..b1ae42aad
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/Cache.cs
@@ -0,0 +1,431 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using k8s.Models;
+using k8s.Util.Common;
+
+namespace k8s.Util.Informer.Cache
+{
+ ///
+ /// Cache is a C# port of Java's Cache which is a port of k/client-go's ThreadSafeStore. It basically saves and indexes all the entries.
+ ///
+ /// The type of K8s object to save
+ public class Cache : IIndexer
+ where TApiType : class, IKubernetesObject
+ {
+ ///
+ /// keyFunc defines how to map index objects into indices
+ ///
+ private Func _keyFunc;
+
+ ///
+ /// indexers stores index functions by their names
+ ///
+ /// The indexer name(string) is a label marking the different ways it can be calculated.
+ /// The default label is "namespace". The default func is to look in the object's metadata and combine the
+ /// namespace and name values, as namespace/name.
+ ///
+ private readonly Dictionary>> _indexers = new Dictionary>>();
+
+ ///
+ /// indices stores objects' keys by their indices
+ ///
+ /// Similar to 'indexers', an indice has the same label as its corresponding indexer except it's value
+ /// is the result of the func.
+ /// if the indexer func is to calculate the namespace and name values as namespace/name, then the indice HashSet
+ /// holds those values.
+ ///
+ private Dictionary>> _indices = new Dictionary>>();
+
+ ///
+ /// items stores object instances
+ ///
+ /// Indices hold the HashSet of calculated keys (namespace/name) for a given resource and items map each of
+ /// those keys to actual K8s object that was originally returned.
+ private Dictionary _items = new Dictionary();
+
+ ///
+ /// object used to track locking
+ ///
+ /// methods interacting with the store need to lock to secure the thread for race conditions,
+ /// learn more: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement
+ private readonly object _lock = new object();
+
+ public Cache()
+ : this(Caches.NamespaceIndex, Caches.MetaNamespaceIndexFunc, Caches.DeletionHandlingMetaNamespaceKeyFunc)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Constructor.
+ ///
+ /// the index name, an unique name representing the index
+ /// the index func by which we map multiple object to an index for querying
+ /// the key func by which we map one object to an unique key for storing
+ public Cache(string indexName, Func> indexFunc, Func keyFunc)
+ {
+ _indexers[indexName] = indexFunc;
+ _keyFunc = keyFunc;
+ _indices[indexName] = new Dictionary>();
+ }
+
+ ///
+ /// Add objects.
+ ///
+ /// the obj
+ public void Add(TApiType obj)
+ {
+ var key = _keyFunc(obj);
+
+ lock (_lock)
+ {
+ var oldObj = _items.GetValueOrDefault(key);
+ _items[key] = obj;
+ UpdateIndices(oldObj, obj, key);
+ }
+ }
+
+ ///
+ /// Update the object.
+ ///
+ /// the obj
+ public void Update(TApiType obj)
+ {
+ var key = _keyFunc(obj);
+
+ lock (_lock)
+ {
+ var oldObj = _items.GetValueOrDefault(key);
+ _items[key] = obj;
+ UpdateIndices(oldObj, obj, key);
+ }
+ }
+
+ ///
+ /// Delete the object.
+ ///
+ /// the obj
+ public void Delete(TApiType obj)
+ {
+ var key = _keyFunc(obj);
+ lock (_lock)
+ {
+ if (!_items.TryGetValue(key, out var value))
+ {
+ return;
+ }
+
+ DeleteFromIndices(value, key);
+ _items.Remove(key);
+ }
+ }
+
+ ///
+ /// Replace the content in the cache completely.
+ ///
+ /// the list
+ /// optional, unused param from interface
+ /// list is null
+ public void Replace(IEnumerable list, string resourceVersion = default)
+ {
+ if (list is null)
+ {
+ throw new ArgumentNullException(nameof(list));
+ }
+
+ var newItems = new Dictionary();
+ foreach (var item in list)
+ {
+ var key = _keyFunc(item);
+ newItems[key] = item;
+ }
+
+ lock (_lock)
+ {
+ _items = newItems;
+
+ // rebuild any index
+ _indices = new Dictionary>>();
+ foreach (var (key, value) in _items)
+ {
+ UpdateIndices(default, value, key);
+ }
+ }
+ }
+
+ ///
+ /// Resync.
+ ///
+ public void Resync()
+ {
+ // Do nothing by default
+ }
+
+ ///
+ /// List keys.
+ ///
+ /// the list
+ public IEnumerable ListKeys()
+ {
+ return _items.Select(item => item.Key);
+ }
+
+ ///
+ /// Get object t.
+ ///
+ /// the obj
+ /// the t
+ public TApiType Get(TApiType obj)
+ {
+ var key = _keyFunc(obj);
+
+ lock (_lock)
+ {
+ // Todo: to make this lock striped or reader/writer (or use ConcurrentDictionary)
+ return _items.GetValueOrDefault(key);
+ }
+ }
+
+ ///
+ /// List all objects in the cache.
+ ///
+ /// all items
+ public IEnumerable List()
+ {
+ lock (_lock)
+ {
+ return _items.Select(item => item.Value);
+ }
+ }
+
+ ///
+ /// Get object t.
+ ///
+ /// the key
+ /// the get by key
+ public TApiType GetByKey(string key)
+ {
+ lock (_lock)
+ {
+ _items.TryGetValue(key, out var value);
+ return value;
+ }
+ }
+
+ ///
+ /// Get objects.
+ ///
+ /// the index name
+ /// the obj
+ /// the list
+ /// indexers does not contain the provided index name
+ public IEnumerable Index(string indexName, TApiType obj)
+ {
+ if (!_indexers.ContainsKey(indexName))
+ {
+ throw new ArgumentException($"index {indexName} doesn't exist!", nameof(indexName));
+ }
+
+ lock (_lock)
+ {
+ var indexFunc = _indexers[indexName];
+ var indexKeys = indexFunc(obj);
+ var index = _indices.GetValueOrDefault(indexName);
+ if (index is null || index.Count == 0)
+ {
+ return new List();
+ }
+
+ var returnKeySet = new HashSet();
+ foreach (var set in indexKeys.Select(indexKey => index.GetValueOrDefault(indexKey)).Where(set => set != null && set.Count != 0))
+ {
+ returnKeySet.AddRange(set);
+ }
+
+ var items = new List(returnKeySet.Count);
+ items.AddRange(returnKeySet.Select(absoluteKey => _items[absoluteKey]));
+
+ return items;
+ }
+ }
+
+ ///
+ /// Index keys list.
+ ///
+ /// the index name
+ /// the index key
+ /// the list
+ /// indexers does not contain the provided index name
+ /// indices collection does not contain the provided index name
+ public IEnumerable IndexKeys(string indexName, string indexKey)
+ {
+ if (!_indexers.ContainsKey(indexName))
+ {
+ throw new ArgumentException($"index {indexName} doesn't exist!", nameof(indexName));
+ }
+
+ lock (_lock)
+ {
+ var index = _indices.GetValueOrDefault(indexName);
+
+ if (index is null)
+ {
+ throw new KeyNotFoundException($"no value could be found for name '{indexName}'");
+ }
+
+ return index[indexKey];
+ }
+ }
+
+ ///
+ /// By index list.
+ ///
+ /// the index name
+ /// the index key
+ /// the list
+ /// indexers does not contain the provided index name
+ /// indices collection does not contain the provided index name
+ public IEnumerable ByIndex(string indexName, string indexKey)
+ {
+ if (!_indexers.ContainsKey(indexName))
+ {
+ throw new ArgumentException($"index {indexName} doesn't exist!", nameof(indexName));
+ }
+
+ var index = _indices.GetValueOrDefault(indexName);
+
+ if (index is null)
+ {
+ throw new KeyNotFoundException($"no value could be found for name '{indexName}'");
+ }
+
+ var set = index[indexKey];
+ return set is null ? new List() : set.Select(key => _items[key]);
+ }
+
+ ///
+ /// Return the indexers registered with the cache.
+ ///
+ /// registered indexers
+ public IDictionary>> GetIndexers() => _indexers;
+
+ ///
+ /// Add additional indexers to the cache.
+ ///
+ /// indexers to add
+ /// newIndexers is null
+ /// items collection is not empty
+ /// conflict between keys in existing index and new indexers provided
+ public void AddIndexers(IDictionary>> newIndexers)
+ {
+ if (newIndexers is null)
+ {
+ throw new ArgumentNullException(nameof(newIndexers));
+ }
+
+ if (_items.Any())
+ {
+ throw new InvalidOperationException("cannot add indexers to a non-empty cache");
+ }
+
+ var oldKeys = _indexers.Keys;
+ var newKeys = newIndexers.Keys;
+ var intersection = oldKeys.Intersect(newKeys);
+
+ if (intersection.Any())
+ {
+ throw new ArgumentException("indexer conflict: " + intersection);
+ }
+
+ foreach (var (key, value) in newIndexers)
+ {
+ AddIndexFunc(key, value);
+ }
+ }
+
+ ///
+ /// UpdateIndices modifies the objects location in the managed indexes, if this is an update, you
+ /// must provide an oldObj.
+ ///
+ /// UpdateIndices must be called from a function that already has a lock on the cache.
+ /// the old obj
+ /// the new obj
+ /// the key
+ private void UpdateIndices(TApiType oldObj, TApiType newObj, string key)
+ {
+ // if we got an old object, we need to remove it before we can add
+ // it again.
+ if (oldObj != null)
+ {
+ DeleteFromIndices(oldObj, key);
+ }
+
+ foreach (var (indexName, indexFunc) in _indexers)
+ {
+ var indexValues = indexFunc(newObj);
+ if (indexValues is null || indexValues.Count == 0)
+ {
+ continue;
+ }
+
+ var index = _indices.ComputeIfAbsent(indexName, _ => new Dictionary>());
+
+ foreach (var indexValue in indexValues)
+ {
+ HashSet indexSet = index.ComputeIfAbsent(indexValue, k => new HashSet());
+ indexSet.Add(key);
+
+ index[indexValue] = indexSet;
+ }
+ }
+ }
+
+ ///
+ /// DeleteFromIndices removes the object from each of the managed indexes.
+ ///
+ /// It is intended to be called from a function that already has a lock on the cache.
+ /// the old obj
+ /// the key
+ private void DeleteFromIndices(TApiType oldObj, string key)
+ {
+ foreach (var (s, indexFunc) in _indexers)
+ {
+ var indexValues = indexFunc(oldObj);
+ if (indexValues is null || indexValues.Count == 0)
+ {
+ continue;
+ }
+
+ var index = _indices.GetValueOrDefault(s);
+ if (index is null)
+ {
+ continue;
+ }
+
+ foreach (var indexSet in indexValues.Select(indexValue => index[indexValue]))
+ {
+ indexSet?.Remove(key);
+ }
+ }
+ }
+
+ ///
+ /// Add index func.
+ ///
+ /// the index name
+ /// the index func
+ public void AddIndexFunc(string indexName, Func> indexFunc)
+ {
+ _indices[indexName] = new Dictionary>();
+ _indexers[indexName] = indexFunc;
+ }
+
+ public Func KeyFunc => _keyFunc;
+
+ public void SetKeyFunc(Func keyFunc)
+ {
+ _keyFunc = keyFunc;
+ }
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/Caches.cs b/src/KubernetesClient/Util/Informer/Cache/Caches.cs
new file mode 100644
index 000000000..74e31739a
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/Caches.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using k8s.Models;
+
+namespace k8s.Util.Informer.Cache
+{
+ ///
+ /// A set of helper utilities for constructing a cache.
+ ///
+ public static class Caches
+ {
+ ///
+ /// NamespaceIndex is the default index function for caching objects
+ ///
+ public const string NamespaceIndex = "namespace";
+
+ ///
+ /// deletionHandlingMetaNamespaceKeyFunc checks for DeletedFinalStateUnknown objects before calling
+ /// metaNamespaceKeyFunc.
+ ///
+ /// specific object
+ /// the type parameter
+ /// if obj is null
+ /// the key
+ public static string DeletionHandlingMetaNamespaceKeyFunc(TApiType obj)
+ where TApiType : class, IKubernetesObject
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (obj.GetType() == typeof(DeletedFinalStateUnknown))
+ {
+ var deleteObj = obj as DeletedFinalStateUnknown;
+ return deleteObj.GetKey();
+ }
+
+ return MetaNamespaceKeyFunc(obj);
+ }
+
+ ///
+ /// MetaNamespaceKeyFunc is a convenient default KeyFunc which knows how to make keys for API
+ /// objects which implement V1ObjectMeta Interface. The key uses the format <namespace>/<name>
+ /// unless <namespace> is empty, then it's just <name>.
+ ///
+ /// specific object
+ /// the key
+ /// if obj is null
+ /// if metadata can't be found on obj
+ public static string MetaNamespaceKeyFunc(IKubernetesObject obj)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (!string.IsNullOrEmpty(obj.Metadata.NamespaceProperty))
+ {
+ return obj.Metadata.NamespaceProperty + "/" + obj.Metadata.Name;
+ }
+
+ return obj.Metadata.Name;
+ }
+
+ ///
+ /// MetaNamespaceIndexFunc is a default index function that indexes based on an object's namespace.
+ ///
+ /// specific object
+ /// the type parameter
+ /// the indexed value
+ /// if obj is null
+ /// if metadata can't be found on obj
+ public static List MetaNamespaceIndexFunc(TApiType obj)
+ where TApiType : IKubernetesObject
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ return obj.Metadata is null ? new List() : new List() { obj.Metadata.NamespaceProperty };
+ }
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/DeletedFinalStateUnknown.cs b/src/KubernetesClient/Util/Informer/Cache/DeletedFinalStateUnknown.cs
new file mode 100644
index 000000000..7e8a99553
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/DeletedFinalStateUnknown.cs
@@ -0,0 +1,47 @@
+using k8s.Models;
+
+namespace k8s.Util.Informer.Cache
+{
+ // DeletedFinalStateUnknown is placed into a DeltaFIFO in the case where
+ // an object was deleted but the watch deletion event was missed. In this
+ // case we don't know the final "resting" state of the object, so there's
+ // a chance the included `Obj` is stale.
+ public class DeletedFinalStateUnknown : IKubernetesObject
+ where TApi : class, IKubernetesObject
+ {
+ private readonly string _key;
+ private readonly TApi _obj;
+
+ public DeletedFinalStateUnknown(string key, TApi obj)
+ {
+ _key = key;
+ _obj = obj;
+ }
+
+ public string GetKey() => _key;
+
+ ///
+ /// Gets get obj.
+ ///
+ /// the get obj
+ public TApi GetObj() => _obj;
+
+ public V1ObjectMeta Metadata
+ {
+ get => _obj.Metadata;
+ set => _obj.Metadata = value;
+ }
+
+ public string ApiVersion
+ {
+ get => _obj.ApiVersion;
+ set => _obj.ApiVersion = value;
+ }
+
+ public string Kind
+ {
+ get => _obj.Kind;
+ set => _obj.Kind = value;
+ }
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/DeltaType.cs b/src/KubernetesClient/Util/Informer/Cache/DeltaType.cs
new file mode 100644
index 000000000..b8f10c4fa
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/DeltaType.cs
@@ -0,0 +1,30 @@
+namespace k8s.Util.Informer.Cache
+{
+ public enum DeltaType
+ {
+ ///
+ /// Item added
+ ///
+ Added,
+
+ ///
+ /// Item updated
+ ///
+ Updated,
+
+ ///
+ /// Item deleted
+ ///
+ Deleted,
+
+ ///
+ /// Item synchronized
+ ///
+ Sync,
+
+ ///
+ /// Item replaced
+ ///
+ Replaced,
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/IIndexer.cs b/src/KubernetesClient/Util/Informer/Cache/IIndexer.cs
new file mode 100644
index 000000000..7da66d248
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/IIndexer.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using k8s.Models;
+
+namespace k8s.Util.Informer.Cache
+{
+ public interface IIndexer : IStore
+ where TApiType : class, IKubernetesObject
+ {
+ ///
+ /// Retrieve list of objects that match on the named indexing function.
+ ///
+ /// specific indexing function
+ /// .
+ /// matched objects
+ IEnumerable Index(string indexName, TApiType obj);
+
+ ///
+ /// IndexKeys returns the set of keys that match on the named indexing function.
+ ///
+ /// specific indexing function
+ /// specific index key
+ /// matched keys
+ IEnumerable IndexKeys(string indexName, string indexKey);
+
+ ///
+ /// ByIndex lists object that match on the named indexing function with the exact key.
+ ///
+ /// specific indexing function
+ /// specific index key
+ /// matched objects
+ IEnumerable ByIndex(string indexName, string indexKey);
+
+ ///
+ /// Return the indexers registered with the store.
+ ///
+ /// registered indexers
+ IDictionary>> GetIndexers();
+
+ ///
+ /// Add additional indexers to the store.
+ ///
+ /// indexers to add
+ void AddIndexers(IDictionary>> indexers);
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/IStore.cs b/src/KubernetesClient/Util/Informer/Cache/IStore.cs
new file mode 100644
index 000000000..8fb935a57
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/IStore.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using k8s.Models;
+
+namespace k8s.Util.Informer.Cache
+{
+ public interface IStore
+ where TApiType : class, IKubernetesObject
+ {
+ ///
+ /// add inserts an item into the store.
+ ///
+ /// specific obj
+ void Add(TApiType obj);
+
+ ///
+ /// update sets an item in the store to its updated state.
+ ///
+ /// specific obj
+ void Update(TApiType obj);
+
+ ///
+ /// delete removes an item from the store.
+ ///
+ /// specific obj
+ void Delete(TApiType obj);
+
+ ///
+ /// Replace will delete the contents of 'c', using instead the given list.
+ ///
+ /// list of objects
+ /// specific resource version
+ void Replace(IEnumerable list, string resourceVersion);
+
+ ///
+ /// resync will send a resync event for each item.
+ ///
+ void Resync();
+
+ ///
+ /// listKeys returns a list of all the keys of the object currently in the store.
+ ///
+ /// list of all keys
+ IEnumerable ListKeys();
+
+ ///
+ /// get returns the requested item.
+ ///
+ /// specific obj
+ /// the requested item if exist
+ TApiType Get(TApiType obj);
+
+ ///
+ /// getByKey returns the request item with specific key.
+ ///
+ /// specific key
+ /// the request item
+ TApiType GetByKey(string key);
+
+ ///
+ /// list returns a list of all the items.
+ ///
+ /// list of all the items
+ IEnumerable List();
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/Lister.cs b/src/KubernetesClient/Util/Informer/Cache/Lister.cs
new file mode 100644
index 000000000..4e11e99be
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/Lister.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using k8s.Models;
+
+namespace k8s.Util.Informer.Cache
+{
+ ///
+ /// Lister interface is used to list cached items from a running informer.
+ ///
+ /// the type
+ public class Lister
+ where TApiType : class, IKubernetesObject
+ {
+ private readonly string _namespace;
+ private readonly string _indexName;
+ private readonly IIndexer _indexer;
+
+ public Lister(IIndexer indexer, string @namespace = default, string indexName = Caches.NamespaceIndex)
+ {
+ _indexer = indexer;
+ _namespace = @namespace;
+ _indexName = indexName;
+ }
+
+ public IEnumerable List()
+ {
+ return string.IsNullOrEmpty(_namespace) ? _indexer.List() : _indexer.ByIndex(_indexName, _namespace);
+ }
+
+ public TApiType Get(string name)
+ {
+ var key = name;
+ if (!string.IsNullOrEmpty(_namespace))
+ {
+ key = _namespace + "/" + name;
+ }
+
+ return _indexer.GetByKey(key);
+ }
+
+ public Lister Namespace(string @namespace)
+ {
+ return new Lister(_indexer, @namespace, Caches.NamespaceIndex);
+ }
+ }
+}
diff --git a/src/KubernetesClient/Util/Informer/Cache/MutablePair.cs b/src/KubernetesClient/Util/Informer/Cache/MutablePair.cs
new file mode 100644
index 000000000..03851112d
--- /dev/null
+++ b/src/KubernetesClient/Util/Informer/Cache/MutablePair.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+
+namespace k8s.Util.Informer.Cache
+{
+ public class MutablePair
+ {
+ protected bool Equals(MutablePair other)
+ {
+ if (other is null)
+ {
+ throw new ArgumentNullException(nameof(other));
+ }
+
+ return EqualityComparer.Default.Equals(Left, other.Left) && EqualityComparer.Default.Equals(Right, other.Right);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, obj))
+ {
+ return true;
+ }
+
+ return obj.GetType() == this.GetType() && Equals((MutablePair)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (EqualityComparer.Default.GetHashCode(Left) * 397) ^ EqualityComparer.Default.GetHashCode(Right);
+ }
+ }
+
+ public TRight Right { get; }
+
+ public TLeft Left { get; }
+
+ public MutablePair()
+ {
+ }
+
+ public MutablePair(TLeft left, TRight right)
+ {
+ Left = left;
+ Right = right;
+ }
+ }
+}
diff --git a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
index af8efadc6..eaa58ae7d 100755
--- a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
+++ b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
@@ -8,6 +8,7 @@
+
@@ -41,4 +42,8 @@
+
+
+
+
diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/CacheTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/CacheTest.cs
new file mode 100644
index 000000000..9eae0e9ce
--- /dev/null
+++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/CacheTest.cs
@@ -0,0 +1,336 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using k8s.Util.Informer.Cache;
+using k8s.Models;
+using Xunit;
+
+namespace k8s.Tests.Util.Informer.Cache
+{
+ public class CacheTest
+ {
+ [Fact(DisplayName = "Create default cache success")]
+ private void CreateCacheSuccess()
+ {
+ var cache = new Cache();
+ cache.Should().NotBeNull();
+ cache.GetIndexers().ContainsKey(Caches.NamespaceIndex).Should().BeTrue();
+ // Todo: validate all defaults gor set up
+ }
+
+ [Fact(DisplayName = "Add cache item success")]
+ private void AddCacheItemSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+
+ cache.Add(aPod);
+
+ cache.Get(aPod).Equals(aPod).Should().BeTrue();
+ }
+
+ [Fact(DisplayName = "Update cache item success")]
+ private void UpdateCacheItemSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+
+ var cache = new Cache();
+
+ cache.Add(aPod);
+ aPod.Kind = "another-kind";
+ cache.Update(aPod);
+
+ cache.Get(aPod).Kind.Equals(aPod.Kind).Should().BeTrue();
+ }
+
+ [Fact(DisplayName = "Delete cache item success")]
+ private void DeleteCacheItemSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+
+ var cache = new Cache();
+
+ cache.Add(aPod);
+ cache.Delete(aPod);
+
+ // Todo: check indices for removed item
+ cache.Get(aPod).Should().BeNull();
+ }
+
+ [Fact(DisplayName = "Replace cache items success")]
+ private void ReplaceCacheItemsSuccess()
+ {
+ var pods = Util.CreatePods(3);
+ var aPod = pods.First();
+ var anotherPod = pods.Skip(1).First();
+ var yetAnotherPod = pods.Skip(2).First();
+
+ var cache = new Cache();
+
+ cache.Add(aPod);
+ cache.Replace(new[] { anotherPod, yetAnotherPod });
+
+ // Todo: check indices for replaced items
+ cache.Get(anotherPod).Should().NotBeNull();
+ cache.Get(yetAnotherPod).Should().NotBeNull();
+ }
+
+ [Fact(DisplayName = "List item keys success")]
+ public void ListItemKeysSuccess()
+ {
+ var pods = Util.CreatePods(3);
+ var aPod = pods.First();
+ var anotherPod = pods.Skip(1).First();
+ var cache = new Cache();
+
+ cache.Add(aPod);
+ cache.Add(anotherPod);
+
+ var keys = cache.ListKeys();
+
+ keys.Should().Contain($"{aPod.Metadata.NamespaceProperty}/{aPod.Metadata.Name}");
+ keys.Should().Contain($"{anotherPod.Metadata.NamespaceProperty}/{anotherPod.Metadata.Name}");
+ }
+
+ [Fact(DisplayName = "Get item doesn't exist")]
+ public void GetItemNotExist()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+
+ var item = cache.Get(aPod);
+ item.Should().BeNull();
+ }
+
+ [Fact(DisplayName = "Get item success")]
+ public void GetItemSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+
+ cache.Add(aPod);
+ var item = cache.Get(aPod);
+ item.Equals(aPod).Should().BeTrue();
+ }
+
+ [Fact(DisplayName = "List items success")]
+ public void ListItemSuccess()
+ {
+ var pods = Util.CreatePods(3);
+ var aPod = pods.First();
+ var anotherPod = pods.Skip(1).First();
+ var yetAnotherPod = pods.Skip(2).First();
+
+ var cache = new Cache();
+
+ cache.Add(aPod);
+ cache.Add(anotherPod);
+ cache.Add(yetAnotherPod);
+
+ var items = cache.List();
+ items.Should().HaveCount(3);
+ items.Should().Contain(aPod);
+ items.Should().Contain(anotherPod);
+ items.Should().Contain(yetAnotherPod);
+ }
+
+ [Fact(DisplayName = "Get item by key success")]
+ public void GetItemByKeySuccess()
+ {
+ var pod = Util.CreatePods(1).First();
+ var cache = new Cache();
+
+ cache.Add(pod);
+ var item = cache.GetByKey($"{pod.Metadata.NamespaceProperty}/{pod.Metadata.Name}");
+ item.Should().NotBeNull();
+ }
+
+ [Fact(DisplayName = "Index items no index")]
+ public void IndexItemsNoIndex()
+ {
+ var pod = Util.CreatePods(1).First();
+
+ var cache = new Cache();
+
+ cache.Add(pod);
+
+ Assert.Throws(() => { cache.Index("asdf", pod); });
+ }
+
+ [Fact(DisplayName = "Index items success")]
+ public void IndexItemsSuccess()
+ {
+ var pod = Util.CreatePods(1).First();
+
+ var cache = new Cache();
+
+ cache.Add(pod);
+
+ var items = cache.Index("namespace", pod);
+
+ items.Should().Contain(pod);
+ }
+
+ [Fact(DisplayName = "Get index keys no index")]
+ public void GetIndexKeysNoIndex()
+ {
+ var cache = new Cache();
+
+ Assert.Throws(() => { cache.IndexKeys("a", "b"); });
+ }
+
+ [Fact(DisplayName = "Get index keys no indice item")]
+ public void GetIndexKeysNoIndiceItem()
+ {
+ var cache = new Cache();
+
+ Assert.Throws(() => { cache.IndexKeys("namespace", "b"); });
+ }
+
+ [Fact(DisplayName = "Get index keys success")]
+ public void GetIndexKeysSuccess()
+ {
+ var pod = Util.CreatePods(1).First();
+
+ var cache = new Cache();
+
+ cache.Add(pod);
+ var keys = cache.IndexKeys("namespace", pod.Metadata.NamespaceProperty);
+
+ keys.Should().NotBeNull();
+ keys.Should().Contain(Caches.MetaNamespaceKeyFunc(pod));
+ }
+
+ [Fact(DisplayName = "List by index no index")]
+ public void ListByIndexNoIndex()
+ {
+ var cache = new Cache();
+
+ Assert.Throws(() => { cache.ByIndex("a", "b"); });
+ }
+
+ [Fact(DisplayName = "List by index no indice item")]
+ public void ListByIndexNoIndiceItem()
+ {
+ var cache = new Cache();
+
+ Assert.Throws(() => { cache.ByIndex("namespace", "b"); });
+ }
+
+ [Fact(DisplayName = "List by index success")]
+ public void ListByIndexSuccess()
+ {
+ var pod = Util.CreatePods(1).First();
+
+ var cache = new Cache();
+
+ cache.Add(pod);
+ var items = cache.ByIndex("namespace", pod.Metadata.NamespaceProperty);
+
+ items.Should().Contain(pod);
+ }
+
+ /* Add Indexers */
+ [Fact(DisplayName = "Add null indexers")]
+ public void AddNullIndexers()
+ {
+ var cache = new Cache();
+ Assert.Throws(() => { cache.AddIndexers(null); });
+ }
+
+ [Fact(DisplayName = "Add indexers with conflict")]
+ public void AddIndexersConflict()
+ {
+ var cache = new Cache();
+ Dictionary>> initialIndexers = new Dictionary>>()
+ {
+ { "1", pod => new List() },
+ { "2", pod => new List() },
+ };
+ Dictionary>> conflictIndexers = new Dictionary>>()
+ {
+ { "1", pod => new List() },
+ };
+
+ cache.AddIndexers(initialIndexers);
+ Assert.Throws(() => { cache.AddIndexers(conflictIndexers); });
+ }
+
+ [Fact(DisplayName = "Add indexers success")]
+ public void AddIndexersSuccess()
+ {
+ var cache = new Cache();
+ Dictionary>> indexers = new Dictionary>>()
+ {
+ { "2", pod => new List() { pod.Name() } },
+ { "3", pod => new List() { pod.Name() } },
+ };
+
+ cache.AddIndexers(indexers);
+
+ var savedIndexers = cache.GetIndexers();
+ savedIndexers.Should().HaveCount(indexers.Count + 1); // blank cache constructor will add a default index
+ savedIndexers.Should().Contain(indexers);
+
+ // Todo: check indicies collection for new indexname keys
+ }
+
+ /* Add Index Function */
+ [Fact(DisplayName = "Add index function success")]
+ public void AddIndexFuncSuccess()
+ {
+ var cache = new Cache();
+ cache.AddIndexFunc("1", pod => new List() { pod.Name() });
+
+ var savedIndexers = cache.GetIndexers();
+ savedIndexers.Should().HaveCount(2);
+
+ // Todo: check indicies collection for new indexname keys
+ }
+
+ /* Get Key Function */
+ [Fact(DisplayName = "Get default key function success")]
+ public void GetDefaultKeyFuncSuccess()
+ {
+ var pod = new V1Pod()
+ {
+ Metadata = new V1ObjectMeta()
+ {
+ Name = "a-name",
+ NamespaceProperty = "the-namespace",
+ },
+ };
+ var cache = new Cache();
+ var defaultReturnValue = Caches.DeletionHandlingMetaNamespaceKeyFunc(pod);
+
+ var funcReturnValue = cache.KeyFunc(pod);
+
+ Assert.True(defaultReturnValue.Equals(funcReturnValue));
+ }
+
+ /* Set Key Function */
+ [Fact(DisplayName = "Set key function success")]
+ public void SetKeyFuncSuccess()
+ {
+ var aPod = new V1Pod()
+ {
+ Kind = "some-kind",
+ Metadata = new V1ObjectMeta()
+ {
+ Name = "a-name",
+ NamespaceProperty = "the-namespace",
+ },
+ };
+ var cache = new Cache();
+ var newFunc = new Func((pod) => pod.Kind);
+ var defaultReturnValue = newFunc(aPod);
+
+ cache.SetKeyFunc(newFunc);
+
+ var funcReturnValue = cache.KeyFunc(aPod);
+
+ Assert.True(defaultReturnValue.Equals(funcReturnValue));
+ }
+ }
+}
diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/CachesTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/CachesTest.cs
new file mode 100644
index 000000000..dd2cfb730
--- /dev/null
+++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/CachesTest.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Linq;
+using FluentAssertions;
+using k8s.Models;
+using Xunit;
+using k8s.Util.Informer.Cache;
+
+namespace k8s.Tests.Util.Informer.Cache
+{
+ public class CachesTest
+ {
+ [Fact(DisplayName = "Check for default DeletedFinalStateUnknown")]
+ public void CheckDefaultDeletedFinalStateUnknown()
+ {
+ var aPod = Util.CreatePods(1).First();
+ Caches.DeletionHandlingMetaNamespaceKeyFunc(aPod).Should().Be($"{aPod.Metadata.NamespaceProperty}/{aPod.Metadata.Name}");
+ }
+
+ [Fact(DisplayName = "Check for obj DeletedFinalStateUnknown")]
+ public void CheckObjDeletedFinalStateUnknown()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var key = "a-key";
+ var deletedPod = new DeletedFinalStateUnknown(key, aPod);
+
+ var returnKey = Caches.DeletionHandlingMetaNamespaceKeyFunc(deletedPod);
+
+ // returnKey.Should().Be(key);
+ }
+
+ [Fact(DisplayName = "Get default namespace key null")]
+ public void GetDefaultNamespaceKeyNull()
+ {
+ Assert.Throws(() => { Caches.MetaNamespaceKeyFunc(null); });
+ }
+
+ [Fact(DisplayName = "Get default namespace key success")]
+ public void GetDefaultNamespaceKeySuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ Caches.MetaNamespaceKeyFunc(aPod).Should().Be($"{aPod.Metadata.NamespaceProperty}/{aPod.Metadata.Name}");
+ }
+
+ [Fact(DisplayName = "Get default namespace index null")]
+ public void GetDefaultNamespaceIndexNull()
+ {
+ Assert.Throws(() => { Caches.MetaNamespaceIndexFunc(null); });
+ }
+
+ [Fact(DisplayName = "Get default namespace index success")]
+ public void GetDefaultNamespaceIndexSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var indexes = Caches.MetaNamespaceIndexFunc(aPod);
+
+ indexes.Should().NotBeNull();
+ indexes.Should().Contain(aPod.Metadata.NamespaceProperty);
+ }
+ }
+}
diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/ListerTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/ListerTest.cs
new file mode 100644
index 000000000..1d1860e80
--- /dev/null
+++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/ListerTest.cs
@@ -0,0 +1,95 @@
+using System.Linq;
+using FluentAssertions;
+using k8s.Models;
+using Xunit;
+using k8s.Util.Informer.Cache;
+
+namespace k8s.Tests.Util.Informer.Cache
+{
+ public class ListerTest
+ {
+ [Fact(DisplayName = "Create default lister success")]
+ private void CreateListerDefaultsSuccess()
+ {
+ var cache = new Cache();
+ var lister = new Lister(cache);
+
+ lister.Should().NotBeNull();
+ }
+
+ [Fact(DisplayName = "List with null namespace success")]
+ private void ListNullNamespaceSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+ var lister = new Lister(cache);
+
+ cache.Add(aPod);
+ var pods = lister.List();
+
+ pods.Should().HaveCount(1);
+ pods.Should().Contain(aPod);
+ // Can't 'Get' the pod due to no namespace specified in Lister constructor
+ }
+
+ [Fact(DisplayName = "List with custom namespace success")]
+ private void ListCustomNamespaceSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+ var lister = new Lister(cache, aPod.Metadata.NamespaceProperty);
+
+ cache.Add(aPod);
+ var pods = lister.List();
+
+ pods.Should().HaveCount(1);
+ pods.Should().Contain(aPod);
+ lister.Get(aPod.Metadata.Name).Should().Be(aPod);
+ }
+
+ [Fact(DisplayName = "Get with null namespace success")]
+ private void GetNullNamespaceSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+ var lister = new Lister(cache);
+
+ cache.Add(aPod);
+ var pod = lister.Get(aPod.Metadata.Name);
+
+ // it's null because the namespace was not set in Lister constructor, but the pod did have a namespace.
+ // So it can't build the right key name for lookup in Cache
+ pod.Should().BeNull();
+ }
+
+ [Fact(DisplayName = "Get with custom namespace success")]
+ private void GetCustomNamespaceSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+ var lister = new Lister(cache, aPod.Metadata.NamespaceProperty);
+
+ cache.Add(aPod);
+ var pod = lister.Get(aPod.Metadata.Name);
+
+ pod.Should().Be(aPod);
+ }
+
+ [Fact(DisplayName = "Set custom namespace success")]
+ private void SetCustomNamespaceSuccess()
+ {
+ var aPod = Util.CreatePods(1).First();
+ var cache = new Cache();
+ var lister = new Lister(cache);
+
+ cache.Add(aPod);
+ var pod = lister.Get(aPod.Metadata.Name);
+ pod.Should().BeNull();
+
+ lister = lister.Namespace(aPod.Metadata.NamespaceProperty);
+
+ pod = lister.Get(aPod.Metadata.Name);
+ pod.Should().Be(aPod);
+ }
+ }
+}
diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/ReflectorTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/ReflectorTest.cs
new file mode 100644
index 000000000..da7f18778
--- /dev/null
+++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/ReflectorTest.cs
@@ -0,0 +1,33 @@
+using FluentAssertions;
+using k8s.Models;
+using k8s.Util.Informer.Cache;
+using Microsoft.Extensions.Logging;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace k8s.Tests.Util.Informer.Cache
+{
+ public class ReflectorTest
+ {
+ private readonly ITestOutputHelper _ouputHelper;
+
+ public ReflectorTest(ITestOutputHelper outputHelper)
+ {
+ _ouputHelper = outputHelper;
+ }
+
+ [Fact(DisplayName = "Create default reflector success")]
+ public void CreateReflectorSuccess()
+ {
+ /*using var apiClient = new Kubernetes(_clientConfiguration);
+ var cache = new Cache();
+ var queue = new DeltaFifo(Caches.MetaNamespaceKeyFunc, cache, _deltasLogger);
+ var listerWatcher = new ListWatcher(apiClient, ListAllPods);
+ var logger = LoggerFactory.Create(builder => builder.AddXUnit(_ouputHelper).SetMinimumLevel(LogLevel.Trace)).CreateLogger();
+ var reflector = new k8s.Util.Cache.Reflector(listerWatcher, queue, logger);
+
+ reflector.Should().NotBeNull();*/
+ }
+ }
+}
diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/Util.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/Util.cs
new file mode 100644
index 000000000..7b10c14ae
--- /dev/null
+++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/Util.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using k8s.Models;
+
+namespace k8s.Tests.Util.Informer.Cache
+{
+ internal static class Util
+ {
+ internal static IEnumerable CreatePods(int cnt)
+ {
+ var pods = new List();
+ for (var i = 0; i < cnt; i++)
+ {
+ pods.Add(new V1Pod()
+ {
+ ApiVersion = "Pod/V1",
+ Kind = "Pod",
+ Metadata = new V1ObjectMeta()
+ {
+ Name = Guid.NewGuid().ToString(),
+ NamespaceProperty = "the-namespace",
+ ResourceVersion = "1",
+ },
+ });
+ }
+
+ return pods;
+ }
+
+ internal static V1PodList CreatePostList(int cnt)
+ {
+ return new V1PodList()
+ {
+ ApiVersion = "Pod/V1",
+ Kind = "Pod",
+ Metadata = new V1ListMeta()
+ {
+ ResourceVersion = "1",
+ },
+ Items = CreatePods(cnt).ToList(),
+ };
+ }
+ }
+}
diff --git a/util/src/KubernetesClient.Util/KubernetesClient.Util.csproj b/util/src/KubernetesClient.Util/KubernetesClient.Util.csproj
new file mode 100644
index 000000000..febeee354
--- /dev/null
+++ b/util/src/KubernetesClient.Util/KubernetesClient.Util.csproj
@@ -0,0 +1,42 @@
+
+
+
+ 9.0
+ The Kubernetes Project Authors
+ 2017 The Kubernetes Project Authors
+ Supprting utilities for the kubernetes open source container orchestrator client library.
+
+ Apache-2.0
+ https://github.com/kubernetes-client/csharp
+ https://raw.githubusercontent.com/kubernetes/kubernetes/master/logo/logo.png
+ kubernetes;docker;containers;
+
+ netstandard2.1;net5.0
+ k8s.Util
+ true
+ true
+
+
+ true
+
+
+ true
+ snupkg
+ true
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/util/src/KubernetesClient.Util/KubernetesClient.Util.csproj.DotSettings b/util/src/KubernetesClient.Util/KubernetesClient.Util.csproj.DotSettings
new file mode 100644
index 000000000..6162834d1
--- /dev/null
+++ b/util/src/KubernetesClient.Util/KubernetesClient.Util.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ CSharp90
\ No newline at end of file
diff --git a/util/tests/KubernetesClient.Util.Tests/KubernetesClient.Util.Tests.csproj b/util/tests/KubernetesClient.Util.Tests/KubernetesClient.Util.Tests.csproj
new file mode 100644
index 000000000..ccd876b5a
--- /dev/null
+++ b/util/tests/KubernetesClient.Util.Tests/KubernetesClient.Util.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+