Skip to content

Commit 09411ec

Browse files
Add cache-level predicate support for informers
Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
1 parent 7601bd2 commit 09411ec

6 files changed

Lines changed: 290 additions & 7 deletions

File tree

e2e/src/test/java/io/kubernetes/client/e2e/informer/NamespaceInformerTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
import io.kubernetes.client.informer.SharedInformerFactory;
2020
import io.kubernetes.client.informer.cache.Lister;
2121
import io.kubernetes.client.openapi.ApiClient;
22+
import io.kubernetes.client.openapi.apis.CoreV1Api;
2223
import io.kubernetes.client.openapi.models.V1Namespace;
2324
import io.kubernetes.client.openapi.models.V1NamespaceList;
25+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
2426
import io.kubernetes.client.util.ClientBuilder;
2527
import io.kubernetes.client.util.generic.GenericKubernetesApi;
28+
import java.util.stream.Collectors;
2629
import org.junit.jupiter.api.Test;
2730

2831
class NamespaceInformerTest {
@@ -57,4 +60,61 @@ void listWatchingNamespaces() throws Exception {
5760
informerFactory.stopAllRegisteredInformers(true);
5861
}
5962
}
63+
64+
@Test
65+
void listWatchingNamespacesWithCachePredicate() throws Exception {
66+
ApiClient client = ClientBuilder.defaultClient();
67+
CoreV1Api coreV1Api = new CoreV1Api(client);
68+
SharedInformerFactory informerFactory = new SharedInformerFactory(client);
69+
String selectedNamespace = "e2e-cache-selected";
70+
String ignoredNamespace = "e2e-cache-ignored";
71+
72+
coreV1Api
73+
.createNamespace(
74+
new V1Namespace()
75+
.metadata(
76+
new V1ObjectMeta()
77+
.name(selectedNamespace)
78+
.labels(java.util.Map.of("cache-filter", "keep"))))
79+
.execute();
80+
coreV1Api
81+
.createNamespace(
82+
new V1Namespace()
83+
.metadata(
84+
new V1ObjectMeta()
85+
.name(ignoredNamespace)
86+
.labels(java.util.Map.of("cache-filter", "drop"))))
87+
.execute();
88+
89+
GenericKubernetesApi<V1Namespace, V1NamespaceList> api =
90+
new GenericKubernetesApi<>(V1Namespace.class, V1NamespaceList.class, "", "v1", "namespaces", client);
91+
SharedIndexInformer<V1Namespace> nsInformer =
92+
informerFactory.sharedIndexInformerFor(
93+
api,
94+
V1Namespace.class,
95+
0,
96+
ns ->
97+
ns.getMetadata() != null
98+
&& ns.getMetadata().getLabels() != null
99+
&& "keep".equals(ns.getMetadata().getLabels().get("cache-filter")));
100+
101+
try {
102+
informerFactory.startAllRegisteredInformers();
103+
await().untilAsserted(() -> assertThat(nsInformer.hasSynced()).isTrue());
104+
await()
105+
.untilAsserted(
106+
() -> {
107+
java.util.List<String> cachedNamespaceNames =
108+
nsInformer.getIndexer().list().stream()
109+
.map(ns -> ns.getMetadata().getName())
110+
.collect(Collectors.toList());
111+
assertThat(cachedNamespaceNames).contains(selectedNamespace);
112+
assertThat(cachedNamespaceNames).doesNotContain(ignoredNamespace);
113+
});
114+
} finally {
115+
informerFactory.stopAllRegisteredInformers(true);
116+
coreV1Api.deleteNamespace(selectedNamespace).execute();
117+
coreV1Api.deleteNamespace(ignoredNamespace).execute();
118+
}
119+
}
60120
}

util/src/main/java/io/kubernetes/client/informer/SharedInformerFactory.java

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.concurrent.Executors;
3838
import java.util.concurrent.Future;
3939
import java.util.function.BiConsumer;
40+
import java.util.function.Predicate;
4041

4142
/**
4243
* SharedInformerFactory class constructs and caches informers for api types.
@@ -200,6 +201,17 @@ SharedIndexInformer<ApiType> sharedIndexInformerFor(
200201
Class<ApiType> apiTypeClass,
201202
long resyncPeriodInMillis,
202203
BiConsumer<Class<ApiType>, Throwable> exceptionHandler) {
204+
return sharedIndexInformerFor(
205+
listerWatcher, apiTypeClass, resyncPeriodInMillis, exceptionHandler, obj -> true);
206+
}
207+
208+
public synchronized <ApiType extends KubernetesObject, ApiListType extends KubernetesListObject>
209+
SharedIndexInformer<ApiType> sharedIndexInformerFor(
210+
ListerWatcher<ApiType, ApiListType> listerWatcher,
211+
Class<ApiType> apiTypeClass,
212+
long resyncPeriodInMillis,
213+
BiConsumer<Class<ApiType>, Throwable> exceptionHandler,
214+
Predicate<ApiType> cachePredicate) {
203215
Type apiType = TypeToken.get(apiTypeClass).getType();
204216

205217
if(informers.containsKey(apiType) && reuseExistingCachedInformer) {
@@ -208,7 +220,11 @@ SharedIndexInformer<ApiType> sharedIndexInformerFor(
208220

209221
SharedIndexInformer<ApiType> informer =
210222
new DefaultSharedIndexInformer<>(
211-
apiTypeClass, listerWatcher, resyncPeriodInMillis, new Cache<>(), exceptionHandler);
223+
apiTypeClass,
224+
listerWatcher,
225+
resyncPeriodInMillis,
226+
new Cache<>(cachePredicate),
227+
exceptionHandler);
212228

213229
this.informers.putIfAbsent(apiType, informer);
214230
return informer;
@@ -233,6 +249,21 @@ SharedIndexInformer<ApiType> sharedIndexInformerFor(
233249
genericKubernetesApi, apiTypeClass, resyncPeriodInMillis, Namespaces.NAMESPACE_ALL);
234250
}
235251

252+
public synchronized <ApiType extends KubernetesObject, ApiListType extends KubernetesListObject>
253+
SharedIndexInformer<ApiType> sharedIndexInformerFor(
254+
GenericKubernetesApi<ApiType, ApiListType> genericKubernetesApi,
255+
Class<ApiType> apiTypeClass,
256+
long resyncPeriodInMillis,
257+
Predicate<ApiType> cachePredicate) {
258+
return sharedIndexInformerFor(
259+
genericKubernetesApi,
260+
apiTypeClass,
261+
resyncPeriodInMillis,
262+
Namespaces.NAMESPACE_ALL,
263+
null,
264+
cachePredicate);
265+
}
266+
236267
/**
237268
* Working the same as {@link SharedInformerFactory#sharedIndexInformerFor} above.
238269
*
@@ -253,7 +284,27 @@ SharedIndexInformer<ApiType> sharedIndexInformerFor(
253284
long resyncPeriodInMillis,
254285
String namespace) {
255286
return sharedIndexInformerFor(
256-
genericKubernetesApi, apiTypeClass, resyncPeriodInMillis, namespace, null);
287+
genericKubernetesApi,
288+
apiTypeClass,
289+
resyncPeriodInMillis,
290+
namespace,
291+
(BiConsumer<Class<ApiType>, Throwable>) null);
292+
}
293+
294+
public synchronized <ApiType extends KubernetesObject, ApiListType extends KubernetesListObject>
295+
SharedIndexInformer<ApiType> sharedIndexInformerFor(
296+
GenericKubernetesApi<ApiType, ApiListType> genericKubernetesApi,
297+
Class<ApiType> apiTypeClass,
298+
long resyncPeriodInMillis,
299+
String namespace,
300+
Predicate<ApiType> cachePredicate) {
301+
return sharedIndexInformerFor(
302+
genericKubernetesApi,
303+
apiTypeClass,
304+
resyncPeriodInMillis,
305+
namespace,
306+
null,
307+
cachePredicate);
257308
}
258309

259310
/**
@@ -277,10 +328,27 @@ SharedIndexInformer<ApiType> sharedIndexInformerFor(
277328
long resyncPeriodInMillis,
278329
String namespace,
279330
BiConsumer<Class<ApiType>, Throwable> exceptionHandler) {
331+
return sharedIndexInformerFor(
332+
genericKubernetesApi,
333+
apiTypeClass,
334+
resyncPeriodInMillis,
335+
namespace,
336+
exceptionHandler,
337+
obj -> true);
338+
}
339+
340+
public synchronized <ApiType extends KubernetesObject, ApiListType extends KubernetesListObject>
341+
SharedIndexInformer<ApiType> sharedIndexInformerFor(
342+
GenericKubernetesApi<ApiType, ApiListType> genericKubernetesApi,
343+
Class<ApiType> apiTypeClass,
344+
long resyncPeriodInMillis,
345+
String namespace,
346+
BiConsumer<Class<ApiType>, Throwable> exceptionHandler,
347+
Predicate<ApiType> cachePredicate) {
280348
ListerWatcher<ApiType, ApiListType> listerWatcher =
281349
listerWatcherFor(genericKubernetesApi, namespace);
282350
return sharedIndexInformerFor(
283-
listerWatcher, apiTypeClass, resyncPeriodInMillis, exceptionHandler);
351+
listerWatcher, apiTypeClass, resyncPeriodInMillis, exceptionHandler, cachePredicate);
284352
}
285353

286354
private <ApiType extends KubernetesObject, ApiListType extends KubernetesListObject>

util/src/main/java/io/kubernetes/client/informer/cache/Cache.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Map;
2222
import java.util.Set;
2323
import java.util.function.Function;
24+
import java.util.function.Predicate;
2425
import org.apache.commons.collections4.CollectionUtils;
2526
import org.apache.commons.collections4.MapUtils;
2627

@@ -43,11 +44,22 @@ public class Cache<ApiType extends KubernetesObject> implements Indexer<ApiType>
4344
/** indices stores objects' keys by their indices */
4445
private Map<String, Map<String, Set<String>>> indices = new HashMap<>();
4546

47+
private Predicate<ApiType> storePredicate;
48+
4649
public Cache() {
4750
this(
4851
Caches.NAMESPACE_INDEX,
4952
Caches::metaNamespaceIndexFunc,
50-
Caches::deletionHandlingMetaNamespaceKeyFunc);
53+
Caches::deletionHandlingMetaNamespaceKeyFunc,
54+
obj -> true);
55+
}
56+
57+
public Cache(Predicate<ApiType> storePredicate) {
58+
this(
59+
Caches.NAMESPACE_INDEX,
60+
Caches::metaNamespaceIndexFunc,
61+
Caches::deletionHandlingMetaNamespaceKeyFunc,
62+
storePredicate);
5163
}
5264

5365
/**
@@ -61,9 +73,18 @@ public Cache(
6173
String indexName,
6274
Function<ApiType, List<String>> indexFunc,
6375
Function<ApiType, String> keyFunc) {
76+
this(indexName, indexFunc, keyFunc, obj -> true);
77+
}
78+
79+
public Cache(
80+
String indexName,
81+
Function<ApiType, List<String>> indexFunc,
82+
Function<ApiType, String> keyFunc,
83+
Predicate<ApiType> storePredicate) {
6484
this.indexers.put(indexName, indexFunc);
6585
this.keyFunc = keyFunc;
6686
this.indices.put(indexName, new HashMap<>());
87+
this.storePredicate = storePredicate;
6788
}
6889

6990
/**
@@ -73,6 +94,9 @@ public Cache(
7394
*/
7495
@Override
7596
public void add(ApiType obj) {
97+
if (!storePredicate.test(obj)) {
98+
return;
99+
}
76100
String key = keyFunc.apply(obj);
77101
synchronized (this) {
78102
ApiType oldObj = this.items.get(key);
@@ -91,6 +115,13 @@ public void update(ApiType obj) {
91115
String key = keyFunc.apply(obj);
92116
synchronized (this) {
93117
ApiType oldObj = this.items.get(key);
118+
if (!storePredicate.test(obj)) {
119+
if (oldObj != null) {
120+
this.deleteFromIndices(oldObj, key);
121+
this.items.remove(key);
122+
}
123+
return;
124+
}
94125
this.items.put(key, obj);
95126
updateIndices(oldObj, obj, key);
96127
}
@@ -123,6 +154,9 @@ public void delete(ApiType obj) {
123154
public synchronized void replace(List<ApiType> list, String resourceVersion) {
124155
Map<String, ApiType> newItems = new HashMap<>();
125156
for (ApiType item : list) {
157+
if (!storePredicate.test(item)) {
158+
continue;
159+
}
126160
String key = keyFunc.apply(item);
127161
newItems.put(key, item);
128162
}

util/src/main/java/io/kubernetes/client/informer/impl/DefaultSharedIndexInformer.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,19 @@ public void handleDeltas(Deque<MutablePair<DeltaFIFO.DeltaType, KubernetesObject
284284
Object oldObj = this.indexer.get((ApiType) obj);
285285
if (oldObj != null) {
286286
this.indexer.update((ApiType) obj);
287-
this.processor.distribute(
288-
new ProcessorListener.UpdateNotification(oldObj, obj), isSync);
287+
Object storedObj = this.indexer.get((ApiType) obj);
288+
if (storedObj != null) {
289+
this.processor.distribute(
290+
new ProcessorListener.UpdateNotification(oldObj, obj), isSync);
291+
} else {
292+
this.processor.distribute(new ProcessorListener.DeleteNotification(oldObj), false);
293+
}
289294
} else {
290295
this.indexer.add((ApiType) obj);
291-
this.processor.distribute(new ProcessorListener.AddNotification(obj), isSync);
296+
Object storedObj = this.indexer.get((ApiType) obj);
297+
if (storedObj != null) {
298+
this.processor.distribute(new ProcessorListener.AddNotification(obj), isSync);
299+
}
292300
}
293301
break;
294302
case Deleted:

util/src/test/java/io/kubernetes/client/informer/cache/CacheTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.function.Function;
27+
import org.junit.jupiter.api.Test;
2728
import org.junit.jupiter.params.ParameterizedTest;
2829
import org.junit.jupiter.params.provider.MethodSource;
2930

@@ -190,4 +191,52 @@ void addIndexers(KubernetesObject obj, String index) {
190191
List<V1Pod> nodeNameIndexedPods = podCache.byIndex(nodeIndex, "node1");
191192
assertThat(nodeNameIndexedPods).hasSize(1);
192193
}
194+
195+
@Test
196+
void cachePredicateShouldFilterAddAndReplace() {
197+
Cache<V1Pod> podCache =
198+
new Cache<>(pod -> "keep".equals(pod.getMetadata().getLabels().get("scope")));
199+
V1Pod kept =
200+
new V1Pod()
201+
.metadata(
202+
new V1ObjectMeta()
203+
.namespace("ns")
204+
.name("kept")
205+
.labels(Map.of("scope", "keep")));
206+
V1Pod dropped =
207+
new V1Pod()
208+
.metadata(
209+
new V1ObjectMeta()
210+
.namespace("ns")
211+
.name("dropped")
212+
.labels(Map.of("scope", "drop")));
213+
214+
podCache.add(kept);
215+
podCache.add(dropped);
216+
assertThat(podCache.list()).containsExactly(kept);
217+
218+
podCache.replace(Arrays.asList(kept, dropped), "1");
219+
assertThat(podCache.list()).containsExactly(kept);
220+
}
221+
222+
@Test
223+
void cachePredicateShouldEvictOnUpdateWhenObjectNoLongerMatches() {
224+
Cache<V1Pod> podCache =
225+
new Cache<>(pod -> "keep".equals(pod.getMetadata().getLabels().get("scope")));
226+
V1Pod pod =
227+
new V1Pod()
228+
.metadata(
229+
new V1ObjectMeta()
230+
.namespace("ns")
231+
.name("pod")
232+
.labels(new HashMap<>(Map.of("scope", "keep"))));
233+
234+
podCache.add(pod);
235+
assertThat(podCache.list()).hasSize(1);
236+
237+
pod.getMetadata().setLabels(Map.of("scope", "drop"));
238+
podCache.update(pod);
239+
240+
assertThat(podCache.list()).isEmpty();
241+
}
193242
}

0 commit comments

Comments
 (0)