Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b369781

Browse files
csvirimetacosm
andcommittedDec 9, 2022
feat: cache object pruning (#1630)
Co-authored-by: Chris Laprun <[email protected]>
1 parent 9598629 commit b369781

27 files changed

+611
-53
lines changed
 

‎docs/documentation/features.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,29 @@ setting, where this flag usually needs to be set to false, in order to control t
711711
See also an example implementation in the
712712
[WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/3e2e7c4c834ef1c409d636156b988125744ca911/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java#L38-L43)
713713

714-
## Monitoring with Micrometer
714+
## Optimization of Caches
715+
716+
** Cache pruning is an experimental feature. Might a subject of change or even removal in the future. **
717+
718+
Operators using informers will initially cache the data for all known resources when starting up
719+
so that access to resources can be performed quickly. Consequently, the memory required for the
720+
operator to run and startup time will both increase quite dramatically when dealing with large
721+
clusters with numerous resources.
722+
723+
It is thus possible to configure the operator to cache only pruned versions of the resources to
724+
alleviate the memory usage of the primary and secondary caches. This setup, however, has
725+
implications on how reconcilers deal with resources since they will only work with partial
726+
objects. As a consequence, resources need to be updated using PATCH operations only, sending
727+
only required changes.
728+
729+
To see how to use, and how to handle related caveats regarding how to deal with pruned objects
730+
that leverage
731+
[server side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) patches,
732+
please check the provided
733+
[integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/c688524e64205690ba15587e7ed96a64dc231430/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java)
734+
and associates reconciler.
735+
736+
Pruned caches are currently not supported with the Dependent Resources feature.
715737

716738
## Automatic Generation of CRDs
717739

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Optional;
99
import java.util.Set;
1010
import java.util.function.Function;
11+
import java.util.function.UnaryOperator;
1112
import java.util.stream.Collectors;
1213

1314
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -83,6 +84,14 @@ public Set<String> getNamespaces() {
8384
DEFAULT_NAMESPACES_SET.toArray(String[]::new)));
8485
}
8586

87+
@Override
88+
@SuppressWarnings("unchecked")
89+
public Optional<UnaryOperator<P>> cachePruneFunction() {
90+
return Optional.ofNullable(
91+
Utils.instantiate(annotation.cachePruneFunction(), UnaryOperator.class,
92+
Utils.contextFor(this, null, null)));
93+
}
94+
8695
@Override
8796
@SuppressWarnings("unchecked")
8897
public Class<P> getResourceClass() {

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.List;
77
import java.util.Optional;
88
import java.util.Set;
9+
import java.util.function.UnaryOperator;
910
import java.util.stream.Collectors;
1011

1112
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -38,6 +39,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
3839
private OnUpdateFilter<R> onUpdateFilter;
3940
private GenericFilter<R> genericFilter;
4041
private RateLimiter rateLimiter;
42+
private UnaryOperator<R> cachePruneFunction;
4143

4244
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
4345
finalizer = original.getFinalizerName();
@@ -56,6 +58,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
5658
dependentResources.forEach(drs -> namedDependentResourceSpecs.put(drs.getName(), drs));
5759
this.original = original;
5860
this.rateLimiter = original.getRateLimiter();
61+
this.cachePruneFunction = original.cachePruneFunction().orElse(null);
5962
}
6063

6164
public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
@@ -158,6 +161,12 @@ public ControllerConfigurationOverrider<R> withGenericFilter(GenericFilter<R> ge
158161
return this;
159162
}
160163

164+
public ControllerConfigurationOverrider<R> withCachePruneFunction(
165+
UnaryOperator<R> cachePruneFunction) {
166+
this.cachePruneFunction = cachePruneFunction;
167+
return this;
168+
}
169+
161170
@SuppressWarnings("unchecked")
162171
public ControllerConfigurationOverrider<R> replacingNamedDependentResourceConfig(String name,
163172
Object dependentResourceConfig) {
@@ -208,7 +217,7 @@ public ControllerConfiguration<R> build() {
208217
onUpdateFilter,
209218
genericFilter,
210219
rateLimiter,
211-
newDependentSpecs);
220+
newDependentSpecs, cachePruneFunction);
212221
}
213222

214223
public static <R extends HasMetadata> ControllerConfigurationOverrider<R> override(

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.List;
66
import java.util.Optional;
77
import java.util.Set;
8+
import java.util.function.UnaryOperator;
89

910
import io.fabric8.kubernetes.api.model.HasMetadata;
1011
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
@@ -49,8 +50,10 @@ public DefaultControllerConfiguration(
4950
OnUpdateFilter<R> onUpdateFilter,
5051
GenericFilter<R> genericFilter,
5152
RateLimiter rateLimiter,
52-
List<DependentResourceSpec> dependents) {
53-
super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces);
53+
List<DependentResourceSpec> dependents,
54+
UnaryOperator<R> cachePruneFunction) {
55+
super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces,
56+
cachePruneFunction);
5457
this.associatedControllerClassName = associatedControllerClassName;
5558
this.name = name;
5659
this.crdName = crdName;
@@ -116,4 +119,5 @@ public Optional<Duration> maxReconciliationInterval() {
116119
public RateLimiter getRateLimiter() {
117120
return rateLimiter;
118121
}
122+
119123
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Optional;
44
import java.util.Set;
5+
import java.util.function.UnaryOperator;
56

67
import io.fabric8.kubernetes.api.model.HasMetadata;
78
import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
@@ -19,18 +20,23 @@ public class DefaultResourceConfiguration<R extends HasMetadata>
1920
private final OnAddFilter<R> onAddFilter;
2021
private final OnUpdateFilter<R> onUpdateFilter;
2122
private final GenericFilter<R> genericFilter;
23+
private final UnaryOperator<R> cachePruneFunction;
2224

2325
public DefaultResourceConfiguration(String labelSelector, Class<R> resourceClass,
2426
OnAddFilter<R> onAddFilter,
2527
OnUpdateFilter<R> onUpdateFilter, GenericFilter<R> genericFilter, String... namespaces) {
2628
this(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter,
2729
namespaces == null || namespaces.length == 0 ? DEFAULT_NAMESPACES_SET
28-
: Set.of(namespaces));
30+
: Set.of(namespaces),
31+
null);
2932
}
3033

3134
public DefaultResourceConfiguration(String labelSelector, Class<R> resourceClass,
3235
OnAddFilter<R> onAddFilter,
33-
OnUpdateFilter<R> onUpdateFilter, GenericFilter<R> genericFilter, Set<String> namespaces) {
36+
OnUpdateFilter<R> onUpdateFilter,
37+
GenericFilter<R> genericFilter,
38+
Set<String> namespaces,
39+
UnaryOperator<R> cachePruneFunction) {
3440
this.labelSelector = labelSelector;
3541
this.resourceClass = resourceClass;
3642
this.onAddFilter = onAddFilter;
@@ -39,6 +45,7 @@ public DefaultResourceConfiguration(String labelSelector, Class<R> resourceClass
3945
this.namespaces =
4046
namespaces == null || namespaces.isEmpty() ? DEFAULT_NAMESPACES_SET
4147
: namespaces;
48+
this.cachePruneFunction = cachePruneFunction;
4249
}
4350

4451
@Override
@@ -56,6 +63,11 @@ public Set<String> getNamespaces() {
5663
return namespaces;
5764
}
5865

66+
@Override
67+
public Optional<UnaryOperator<R>> cachePruneFunction() {
68+
return Optional.ofNullable(this.cachePruneFunction);
69+
}
70+
5971
@Override
6072
public Class<R> getResourceClass() {
6173
return resourceClass;

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import java.util.Collections;
44
import java.util.Optional;
55
import java.util.Set;
6+
import java.util.function.UnaryOperator;
67

78
import io.fabric8.kubernetes.api.model.HasMetadata;
89
import io.javaoperatorsdk.operator.OperatorException;
910
import io.javaoperatorsdk.operator.ReconcilerUtils;
1011
import io.javaoperatorsdk.operator.api.reconciler.Constants;
12+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
1113
import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
1214
import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter;
1315
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
@@ -108,4 +110,11 @@ default Set<String> getEffectiveNamespaces() {
108110
}
109111
return targetNamespaces;
110112
}
113+
114+
/**
115+
* See {@link ControllerConfiguration#cachePruneFunction()} for details.
116+
*/
117+
default Optional<UnaryOperator<R>> cachePruneFunction() {
118+
return Optional.empty();
119+
}
111120
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Objects;
44
import java.util.Optional;
55
import java.util.Set;
6+
import java.util.function.UnaryOperator;
67

78
import io.fabric8.kubernetes.api.model.HasMetadata;
89
import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration;
@@ -29,6 +30,7 @@ class DefaultInformerConfiguration<R extends HasMetadata> extends
2930
private final SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
3031
private final boolean followControllerNamespaceChanges;
3132
private final OnDeleteFilter<R> onDeleteFilter;
33+
private final UnaryOperator<R> cachePruneFunction;
3234

3335
protected DefaultInformerConfiguration(String labelSelector,
3436
Class<R> resourceClass,
@@ -38,15 +40,18 @@ protected DefaultInformerConfiguration(String labelSelector,
3840
OnAddFilter<R> onAddFilter,
3941
OnUpdateFilter<R> onUpdateFilter,
4042
OnDeleteFilter<R> onDeleteFilter,
41-
GenericFilter<R> genericFilter) {
42-
super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces);
43+
GenericFilter<R> genericFilter,
44+
UnaryOperator<R> cachePruneFunction) {
45+
super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces,
46+
cachePruneFunction);
4347
this.followControllerNamespaceChanges = followControllerNamespaceChanges;
4448

4549
this.primaryToSecondaryMapper = primaryToSecondaryMapper;
4650
this.secondaryToPrimaryMapper =
4751
Objects.requireNonNullElse(secondaryToPrimaryMapper,
4852
Mappers.fromOwnerReference());
4953
this.onDeleteFilter = onDeleteFilter;
54+
this.cachePruneFunction = cachePruneFunction;
5055
}
5156

5257
@Override
@@ -67,6 +72,11 @@ public Optional<OnDeleteFilter<R>> onDeleteFilter() {
6772
public <P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondaryMapper() {
6873
return (PrimaryToSecondaryMapper<P>) primaryToSecondaryMapper;
6974
}
75+
76+
@Override
77+
public Optional<UnaryOperator<R>> cachePruneFunction() {
78+
return Optional.ofNullable(this.cachePruneFunction);
79+
}
7080
}
7181

7282
/**
@@ -102,6 +112,7 @@ class InformerConfigurationBuilder<R extends HasMetadata> {
102112
private OnDeleteFilter<R> onDeleteFilter;
103113
private GenericFilter<R> genericFilter;
104114
private boolean inheritControllerNamespacesOnChange = false;
115+
private UnaryOperator<R> cachePruneFunction;
105116

106117
private InformerConfigurationBuilder(Class<R> resourceClass) {
107118
this.resourceClass = resourceClass;
@@ -202,12 +213,18 @@ public InformerConfigurationBuilder<R> withGenericFilter(GenericFilter<R> generi
202213
return this;
203214
}
204215

216+
public InformerConfigurationBuilder<R> withCachePruneFunction(
217+
UnaryOperator<R> cachePruneFunction) {
218+
this.cachePruneFunction = cachePruneFunction;
219+
return this;
220+
}
221+
205222
public InformerConfiguration<R> build() {
206223
return new DefaultInformerConfiguration<>(labelSelector, resourceClass,
207224
primaryToSecondaryMapper,
208225
secondaryToPrimaryMapper,
209226
namespaces, inheritControllerNamespacesOnChange, onAddFilter, onUpdateFilter,
210-
onDeleteFilter, genericFilter);
227+
onDeleteFilter, genericFilter, cachePruneFunction);
211228
}
212229
}
213230

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.lang.annotation.Retention;
66
import java.lang.annotation.RetentionPolicy;
77
import java.lang.annotation.Target;
8+
import java.util.function.UnaryOperator;
89

910
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
1011
import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter;
@@ -118,4 +119,25 @@ MaxReconciliationInterval maxReconciliationInterval() default @MaxReconciliation
118119
* accessible no-arg constructor.
119120
*/
120121
Class<? extends RateLimiter> rateLimiter() default LinearRateLimiter.class;
122+
123+
/**
124+
* <p>
125+
* <b>This is an experimental feature, might be a subject of change and even removal in the
126+
* future.</b>
127+
* </p>
128+
* <p>
129+
* In order to optimize cache, thus set null on some attributes, this function can be set. Note
130+
* that this has subtle implications how updates on the resources should be handled. Notably only
131+
* patching of the resource can be used from that point, since update would remove not cached
132+
* parts of the resource.
133+
* </p>
134+
* <p>
135+
* Note that this feature does not work with Dependent Resources.
136+
* </p>
137+
*
138+
*
139+
*
140+
* @return function to remove parts of the resource.
141+
*/
142+
Class<? extends UnaryOperator> cachePruneFunction() default UnaryOperator.class;
121143
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.javaoperatorsdk.operator.processing.event;
22

3+
import java.util.function.Function;
4+
35
import org.slf4j.Logger;
46
import org.slf4j.LoggerFactory;
57

@@ -10,6 +12,8 @@
1012
import io.fabric8.kubernetes.client.KubernetesClientException;
1113
import io.fabric8.kubernetes.client.dsl.MixedOperation;
1214
import io.fabric8.kubernetes.client.dsl.Resource;
15+
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
16+
import io.fabric8.kubernetes.client.dsl.base.PatchType;
1317
import io.javaoperatorsdk.operator.OperatorException;
1418
import io.javaoperatorsdk.operator.api.ObservedGenerationAware;
1519
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
@@ -82,7 +86,7 @@ private PostExecutionControl<P> handleDispatch(ExecutionScope<P> executionScope)
8286
Context<P> context =
8387
new DefaultContext<>(executionScope.getRetryInfo(), controller, originalResource);
8488
if (markedForDeletion) {
85-
return handleCleanup(resourceForExecution, context);
89+
return handleCleanup(originalResource, resourceForExecution, context);
8690
} else {
8791
return handleReconcile(executionScope, resourceForExecution, originalResource, context);
8892
}
@@ -109,7 +113,8 @@ private PostExecutionControl<P> handleReconcile(
109113
* finalizer add. This will make sure that the resources are not created before there is a
110114
* finalizer.
111115
*/
112-
var updatedResource = updateCustomResourceWithFinalizer(originalResource);
116+
var updatedResource =
117+
updateCustomResourceWithFinalizer(resourceForExecution, originalResource);
113118
return PostExecutionControl.onlyFinalizerAdded(updatedResource);
114119
} else {
115120
try {
@@ -276,7 +281,8 @@ private void updatePostExecutionControlWithReschedule(
276281
}
277282

278283

279-
private PostExecutionControl<P> handleCleanup(P resource, Context<P> context) {
284+
private PostExecutionControl<P> handleCleanup(P originalResource, P resource,
285+
Context<P> context) {
280286
log.debug(
281287
"Executing delete for resource: {} with version: {}",
282288
getName(resource),
@@ -289,7 +295,8 @@ private PostExecutionControl<P> handleCleanup(P resource, Context<P> context) {
289295
// cleanup is finished, nothing left to done
290296
final var finalizerName = configuration().getFinalizerName();
291297
if (deleteControl.isRemoveFinalizer() && resource.hasFinalizer(finalizerName)) {
292-
P customResource = removeFinalizer(resource, finalizerName);
298+
P customResource = conflictRetryingPatch(resource, originalResource,
299+
r -> r.removeFinalizer(finalizerName));
293300
return PostExecutionControl.customResourceFinalizerRemoved(customResource);
294301
}
295302
}
@@ -304,11 +311,13 @@ private PostExecutionControl<P> handleCleanup(P resource, Context<P> context) {
304311
return postExecutionControl;
305312
}
306313

307-
private P updateCustomResourceWithFinalizer(P resource) {
314+
private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) {
308315
log.debug(
309-
"Adding finalizer for resource: {} version: {}", getUID(resource), getVersion(resource));
310-
resource.addFinalizer(configuration().getFinalizerName());
311-
return customResourceFacade.updateResource(resource);
316+
"Adding finalizer for resource: {} version: {}", getUID(originalResource),
317+
getVersion(originalResource));
318+
319+
return conflictRetryingPatch(resourceForExecution, originalResource,
320+
r -> r.addFinalizer(configuration().getFinalizerName()));
312321
}
313322

314323
private P updateCustomResource(P resource) {
@@ -321,20 +330,21 @@ ControllerConfiguration<P> configuration() {
321330
return controller.getConfiguration();
322331
}
323332

324-
public P removeFinalizer(P resource, String finalizer) {
333+
public P conflictRetryingPatch(P resource, P originalResource,
334+
Function<P, Boolean> modificationFunction) {
325335
if (log.isDebugEnabled()) {
326336
log.debug("Removing finalizer on resource: {}", ResourceID.fromResource(resource));
327337
}
328338
int retryIndex = 0;
329339
while (true) {
330340
try {
331-
var removed = resource.removeFinalizer(finalizer);
332-
if (!removed) {
341+
var modified = modificationFunction.apply(resource);
342+
if (Boolean.FALSE.equals(modified)) {
333343
return resource;
334344
}
335-
return customResourceFacade.updateResource(resource);
345+
return customResourceFacade.serverSideApplyLockResource(resource, originalResource);
336346
} catch (KubernetesClientException e) {
337-
log.trace("Exception during finalizer removal for resource: {}", resource);
347+
log.trace("Exception during patch for resource: {}", resource);
338348
retryIndex++;
339349
// only retry on conflict (HTTP 409), otherwise fail
340350
if (e.getCode() != 409) {
@@ -343,7 +353,7 @@ public P removeFinalizer(P resource, String finalizer) {
343353
if (retryIndex >= MAX_FINALIZER_REMOVAL_RETRY) {
344354
throw new OperatorException(
345355
"Exceeded maximum (" + MAX_FINALIZER_REMOVAL_RETRY
346-
+ ") retry attempts to remove finalizer '" + finalizer + "' for resource "
356+
+ ") retry attempts to patch resource: "
347357
+ ResourceID.fromResource(resource));
348358
}
349359
resource = customResourceFacade.getResource(resource.getMetadata().getNamespace(),
@@ -370,12 +380,18 @@ public R getResource(String namespace, String name) {
370380
}
371381
}
372382

383+
public R serverSideApplyLockResource(R resource, R originalResource) {
384+
var patchContext = PatchContext.of(PatchType.SERVER_SIDE_APPLY);
385+
patchContext.setForce(true);
386+
return resource(originalResource).patch(patchContext,
387+
resource);
388+
}
389+
373390
public R updateResource(R resource) {
374391
log.debug(
375392
"Trying to replace resource {}, version: {}",
376393
getName(resource),
377394
resource.getMetadata().getResourceVersion());
378-
379395
return resource(resource).lockResourceVersion(resource.getMetadata().getResourceVersion())
380396
.replace();
381397
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,11 @@ private InformerWrapper<T> createEventSourceForNamespace(String namespace) {
109109
private InformerWrapper<T> createEventSource(
110110
FilterWatchListDeletable<T, KubernetesResourceList<T>, Resource<T>> filteredBySelectorClient,
111111
ResourceEventHandler<T> eventHandler, String namespaceIdentifier) {
112+
var informer = filteredBySelectorClient.runnableInformer(0);
113+
configuration.cachePruneFunction()
114+
.ifPresent(f -> informer.itemStore(new TransformingItemStore<>(f)));
112115
var source =
113-
new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0), namespaceIdentifier);
116+
new InformerWrapper<>(informer, namespaceIdentifier);
114117
source.addEventHandler(eventHandler);
115118
sources.put(namespaceIdentifier, source);
116119
return source;

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public InformerWrapper(SharedIndexInformer<T> informer, String namespaceIdentifi
3939
this.informer = informer;
4040
this.namespaceIdentifier = namespaceIdentifier;
4141
this.cache = (Cache<T>) informer.getStore();
42-
4342
}
4443

4544
@Override

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ public abstract class ManagedInformerEventSource<R extends HasMetadata, P extend
3636

3737
private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class);
3838

39-
protected TemporaryResourceCache<R> temporaryResourceCache = new TemporaryResourceCache<>(this);
39+
protected TemporaryResourceCache<R> temporaryResourceCache;
4040
protected InformerManager<R, C> cache = new InformerManager<>();
4141
protected C configuration;
4242

4343
protected ManagedInformerEventSource(
4444
MixedOperation<R, KubernetesResourceList<R>, Resource<R>> client, C configuration) {
4545
super(configuration.getResourceClass());
46+
temporaryResourceCache = new TemporaryResourceCache<>(this,
47+
configuration.cachePruneFunction().orElse(null));
4648
manager().initSources(client, configuration, this);
4749
this.configuration = configuration;
4850
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Map;
44
import java.util.Optional;
55
import java.util.concurrent.ConcurrentHashMap;
6+
import java.util.function.UnaryOperator;
67

78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
@@ -34,26 +35,29 @@ public class TemporaryResourceCache<T extends HasMetadata> {
3435

3536
private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class);
3637

38+
private UnaryOperator<T> cachePruneFunction;
3739
private final Map<ResourceID, T> cache = new ConcurrentHashMap<>();
3840
private final ManagedInformerEventSource<T, ?, ?> managedInformerEventSource;
3941

40-
public TemporaryResourceCache(ManagedInformerEventSource<T, ?, ?> managedInformerEventSource) {
42+
public TemporaryResourceCache(ManagedInformerEventSource<T, ?, ?> managedInformerEventSource,
43+
UnaryOperator<T> cachePruneFunction) {
4144
this.managedInformerEventSource = managedInformerEventSource;
45+
this.cachePruneFunction = cachePruneFunction;
4246
}
4347

4448
public synchronized void removeResourceFromCache(T resource) {
4549
cache.remove(ResourceID.fromResource(resource));
4650
}
4751

4852
public synchronized void unconditionallyCacheResource(T newResource) {
49-
cache.put(ResourceID.fromResource(newResource), newResource);
53+
putToCache(newResource, null);
5054
}
5155

5256
public synchronized void putAddedResource(T newResource) {
5357
ResourceID resourceID = ResourceID.fromResource(newResource);
5458
if (managedInformerEventSource.get(resourceID).isEmpty()) {
5559
log.debug("Putting resource to cache with ID: {}", resourceID);
56-
cache.put(resourceID, newResource);
60+
putToCache(newResource, resourceID);
5761
} else {
5862
log.debug("Won't put resource into cache found already informer cache: {}", resourceID);
5963
}
@@ -70,14 +74,21 @@ public synchronized void putUpdatedResource(T newResource, String previousResour
7074
if (informerCacheResource.get().getMetadata().getResourceVersion()
7175
.equals(previousResourceVersion)) {
7276
log.debug("Putting resource to temporal cache with id: {}", resourceId);
73-
cache.put(resourceId, newResource);
77+
putToCache(newResource, resourceId);
7478
} else {
7579
// if something is in cache it's surely obsolete now
7680
log.debug("Trying to remove an obsolete resource from cache for id: {}", resourceId);
7781
cache.remove(resourceId);
7882
}
7983
}
8084

85+
private void putToCache(T resource, ResourceID resourceID) {
86+
if (cachePruneFunction != null) {
87+
resource = cachePruneFunction.apply(resource);
88+
}
89+
cache.put(resourceID == null ? ResourceID.fromResource(resource) : resourceID, resource);
90+
}
91+
8192
public synchronized Optional<T> getResourceFromCache(ResourceID resourceID) {
8293
return Optional.ofNullable(cache.get(resourceID));
8394
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package io.javaoperatorsdk.operator.processing.event.source.informer;
2+
3+
import java.util.concurrent.ConcurrentHashMap;
4+
import java.util.function.Function;
5+
import java.util.function.UnaryOperator;
6+
import java.util.stream.Stream;
7+
8+
import io.fabric8.kubernetes.api.model.HasMetadata;
9+
import io.fabric8.kubernetes.client.informers.cache.Cache;
10+
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
11+
12+
public class TransformingItemStore<R extends HasMetadata> implements ItemStore<R> {
13+
14+
private Function<R, String> keyFunction;
15+
private UnaryOperator<R> transformationFunction;
16+
private ConcurrentHashMap<String, R> store = new ConcurrentHashMap<>();
17+
18+
public TransformingItemStore(UnaryOperator<R> transformationFunction) {
19+
this(Cache::metaNamespaceKeyFunc, transformationFunction);
20+
}
21+
22+
public TransformingItemStore(Function<R, String> keyFunction,
23+
UnaryOperator<R> transformationFunction) {
24+
this.keyFunction = keyFunction;
25+
this.transformationFunction = transformationFunction;
26+
}
27+
28+
@Override
29+
public String getKey(R obj) {
30+
return keyFunction.apply(obj);
31+
}
32+
33+
@Override
34+
public R put(String key, R obj) {
35+
var originalName = obj.getMetadata().getName();
36+
var originalNamespace = obj.getMetadata().getNamespace();
37+
var originalResourceVersion = obj.getMetadata().getResourceVersion();
38+
39+
var transformed = transformationFunction.apply(obj);
40+
41+
transformed.getMetadata().setName(originalName);
42+
transformed.getMetadata().setNamespace(originalNamespace);
43+
transformed.getMetadata().setResourceVersion(originalResourceVersion);
44+
return store.put(key, transformed);
45+
}
46+
47+
@Override
48+
public R remove(String key) {
49+
return store.remove(key);
50+
}
51+
52+
@Override
53+
public Stream<String> keySet() {
54+
return store.keySet().stream();
55+
}
56+
57+
@Override
58+
public Stream<R> values() {
59+
return store.values().stream();
60+
}
61+
62+
@Override
63+
public R get(String key) {
64+
return store.get(key);
65+
}
66+
67+
@Override
68+
public int size() {
69+
return store.size();
70+
}
71+
72+
@Override
73+
public boolean isFullState() {
74+
return false;
75+
}
76+
}

‎operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ private static class TestControllerConfiguration<R extends HasMetadata>
5858
public TestControllerConfiguration(Reconciler<R> controller, Class<R> crClass) {
5959
super(null, getControllerName(controller),
6060
CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass,
61-
null, null, null, null, null, null);
61+
null, null, null, null, null, null, null);
6262
this.controller = controller;
6363
}
6464

‎operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
import io.javaoperatorsdk.operator.MockKubernetesClient;
2323
import io.javaoperatorsdk.operator.OperatorException;
2424
import io.javaoperatorsdk.operator.TestUtils;
25-
import io.javaoperatorsdk.operator.api.config.Cloner;
26-
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
27-
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
28-
import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration;
25+
import io.javaoperatorsdk.operator.api.config.*;
2926
import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
3027
import io.javaoperatorsdk.operator.api.reconciler.Context;
3128
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
@@ -137,8 +134,9 @@ void addFinalizerOnNewResource() {
137134
verify(reconciler, never())
138135
.reconcile(ArgumentMatchers.eq(testCustomResource), any());
139136
verify(customResourceFacade, times(1))
140-
.updateResource(
141-
argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER)));
137+
.serverSideApplyLockResource(
138+
argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER)),
139+
any());
142140
assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue();
143141
}
144142

@@ -218,7 +216,8 @@ void removesDefaultFinalizerOnDeleteIfSet() {
218216
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
219217

220218
assertThat(postExecControl.isFinalizerRemoved()).isTrue();
221-
verify(customResourceFacade, times(1)).updateResource(testCustomResource);
219+
verify(customResourceFacade, times(1)).serverSideApplyLockResource(testCustomResource,
220+
testCustomResource);
222221
}
223222

224223
@Test
@@ -227,7 +226,7 @@ void retriesFinalizerRemovalWithFreshResource() {
227226
markForDeletion(testCustomResource);
228227
var resourceWithFinalizer = TestUtils.testCustomResource();
229228
resourceWithFinalizer.addFinalizer(DEFAULT_FINALIZER);
230-
when(customResourceFacade.updateResource(testCustomResource))
229+
when(customResourceFacade.serverSideApplyLockResource(testCustomResource, testCustomResource))
231230
.thenThrow(new KubernetesClientException(null, 409, null))
232231
.thenReturn(testCustomResource);
233232
when(customResourceFacade.getResource(any(), any())).thenReturn(resourceWithFinalizer);
@@ -236,15 +235,15 @@ void retriesFinalizerRemovalWithFreshResource() {
236235
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
237236

238237
assertThat(postExecControl.isFinalizerRemoved()).isTrue();
239-
verify(customResourceFacade, times(2)).updateResource(any());
238+
verify(customResourceFacade, times(2)).serverSideApplyLockResource(any(), any());
240239
verify(customResourceFacade, times(1)).getResource(any(), any());
241240
}
242241

243242
@Test
244243
void throwsExceptionIfFinalizerRemovalRetryExceeded() {
245244
testCustomResource.addFinalizer(DEFAULT_FINALIZER);
246245
markForDeletion(testCustomResource);
247-
when(customResourceFacade.updateResource(any()))
246+
when(customResourceFacade.serverSideApplyLockResource(any(), any()))
248247
.thenThrow(new KubernetesClientException(null, 409, null));
249248
when(customResourceFacade.getResource(any(), any()))
250249
.thenAnswer((Answer<TestCustomResource>) invocationOnMock -> createResourceWithFinalizer());
@@ -256,7 +255,9 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() {
256255
assertThat(postExecControl.getRuntimeException()).isPresent();
257256
assertThat(postExecControl.getRuntimeException().get())
258257
.isInstanceOf(OperatorException.class);
259-
verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).updateResource(any());
258+
verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).serverSideApplyLockResource(
259+
any(),
260+
any());
260261
verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY - 1)).getResource(any(),
261262
any());
262263
}
@@ -265,15 +266,15 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() {
265266
void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() {
266267
testCustomResource.addFinalizer(DEFAULT_FINALIZER);
267268
markForDeletion(testCustomResource);
268-
when(customResourceFacade.updateResource(any()))
269+
when(customResourceFacade.serverSideApplyLockResource(any(), any()))
269270
.thenThrow(new KubernetesClientException(null, 400, null));
270271

271272
var res =
272273
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
273274

274275
assertThat(res.getRuntimeException()).isPresent();
275276
assertThat(res.getRuntimeException().get()).isInstanceOf(KubernetesClientException.class);
276-
verify(customResourceFacade, times(1)).updateResource(any());
277+
verify(customResourceFacade, times(1)).serverSideApplyLockResource(any(), any());
277278
verify(customResourceFacade, never()).getResource(any(), any());
278279
}
279280

@@ -337,13 +338,14 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() {
337338
void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() {
338339
removeFinalizers(testCustomResource);
339340
reconciler.reconcile = (r, c) -> UpdateControl.noUpdate();
340-
when(customResourceFacade.updateResource(any())).thenReturn(testCustomResource);
341+
when(customResourceFacade.serverSideApplyLockResource(any(), any()))
342+
.thenReturn(testCustomResource);
341343

342344
var postExecControl =
343345
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
344346

345347
assertEquals(1, testCustomResource.getMetadata().getFinalizers().size());
346-
verify(customResourceFacade, times(1)).updateResource(any());
348+
verify(customResourceFacade, times(1)).serverSideApplyLockResource(any(), any());
347349
assertThat(postExecControl.updateIsStatusPatch()).isFalse();
348350
assertThat(postExecControl.getUpdatedCustomResource()).isPresent();
349351
}
@@ -639,6 +641,24 @@ void canSkipSchedulingMaxDelayIf() {
639641
assertThat(control.getReScheduleDelay()).isNotPresent();
640642
}
641643

644+
@Test
645+
void retriesAddingFinalizer() {
646+
removeFinalizers(testCustomResource);
647+
reconciler.reconcile = (r, c) -> UpdateControl.noUpdate();
648+
when(customResourceFacade.serverSideApplyLockResource(any(), any()))
649+
.thenThrow(new KubernetesClientException(null, 409, null))
650+
.thenReturn(testCustomResource);
651+
when(customResourceFacade.getResource(any(), any()))
652+
.then((Answer<TestCustomResource>) invocationOnMock -> {
653+
testCustomResource.getFinalizers().clear();
654+
return testCustomResource;
655+
});
656+
657+
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
658+
659+
verify(customResourceFacade, times(2)).serverSideApplyLockResource(any(), any());
660+
}
661+
642662
private ObservedGenCustomResource createObservedGenCustomResource() {
643663
ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource();
644664
observedGenCustomResource.setMetadata(new ObjectMeta());

‎operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public ControllerConfig(String finalizer, boolean generationAware,
145145
eventFilter,
146146
customResourceClass,
147147
null,
148-
null, null, null, null, null);
148+
null, null, null, null, null, null);
149149
}
150150
}
151151

‎operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public TestConfiguration(boolean generationAware, OnAddFilter<TestCustomResource
188188
null,
189189
TestCustomResource.class,
190190
null,
191-
onAddFilter, onUpdateFilter, genericFilter, null, null);
191+
onAddFilter, onUpdateFilter, genericFilter, null, null, null);
192192
}
193193
}
194194
}

‎operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package io.javaoperatorsdk.operator.processing.event.source.informer;
22

3+
import java.util.Map;
34
import java.util.Optional;
45

56
import org.junit.jupiter.api.Test;
67

78
import io.fabric8.kubernetes.api.model.ConfigMap;
8-
import io.fabric8.kubernetes.api.model.ObjectMeta;
9+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
910
import io.javaoperatorsdk.operator.processing.event.ResourceID;
1011

1112
import static org.assertj.core.api.Assertions.assertThat;
@@ -18,7 +19,7 @@ class TemporaryResourceCacheTest {
1819
public static final String RESOURCE_VERSION = "1";
1920
private InformerEventSource<ConfigMap, ?> informerEventSource = mock(InformerEventSource.class);
2021
private TemporaryResourceCache<ConfigMap> temporaryResourceCache =
21-
new TemporaryResourceCache<>(informerEventSource);
22+
new TemporaryResourceCache<>(informerEventSource, null);
2223

2324

2425
@Test
@@ -79,6 +80,29 @@ void removesResourceFromCache() {
7980
.isNotPresent();
8081
}
8182

83+
@Test
84+
void objectIsTransformedBeforePutIntoCache() {
85+
temporaryResourceCache =
86+
new TemporaryResourceCache<>(informerEventSource, r -> {
87+
r.getMetadata().setLabels(null);
88+
return r;
89+
});
90+
91+
temporaryResourceCache.putAddedResource(testResource());
92+
assertLabelsIsEmpty(temporaryResourceCache);
93+
94+
temporaryResourceCache.unconditionallyCacheResource(testResource());
95+
assertLabelsIsEmpty(temporaryResourceCache);
96+
97+
temporaryResourceCache.unconditionallyCacheResource(testResource());
98+
assertLabelsIsEmpty(temporaryResourceCache);
99+
}
100+
101+
private void assertLabelsIsEmpty(TemporaryResourceCache<ConfigMap> temporaryResourceCache) {
102+
assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource()))
103+
.orElseThrow().getMetadata().getLabels()).isNull();
104+
}
105+
82106
private ConfigMap propagateTestResourceToCache() {
83107
var testResource = testResource();
84108
when(informerEventSource.get(any())).thenReturn(Optional.empty());
@@ -90,7 +114,9 @@ private ConfigMap propagateTestResourceToCache() {
90114

91115
ConfigMap testResource() {
92116
ConfigMap configMap = new ConfigMap();
93-
configMap.setMetadata(new ObjectMeta());
117+
configMap.setMetadata(new ObjectMetaBuilder()
118+
.withLabels(Map.of("k", "v"))
119+
.build());
94120
configMap.getMetadata().setName("test");
95121
configMap.getMetadata().setNamespace("default");
96122
configMap.getMetadata().setResourceVersion(RESOURCE_VERSION);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.javaoperatorsdk.operator.processing.event.source.informer;
2+
3+
import java.util.Map;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import io.fabric8.kubernetes.api.model.ConfigMap;
8+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
9+
10+
import static io.fabric8.kubernetes.client.informers.cache.Cache.metaNamespaceKeyFunc;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class TransformingItemStoreTest {
14+
15+
@Test
16+
void cachedObjectTransformed() {
17+
TransformingItemStore<ConfigMap> transformingItemStore = new TransformingItemStore<>(r -> {
18+
r.getMetadata().setLabels(null);
19+
return r;
20+
});
21+
22+
var cm = configMap();
23+
cm.getMetadata().setLabels(Map.of("k", "v"));
24+
transformingItemStore.put(metaNamespaceKeyFunc(cm), cm);
25+
26+
assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getLabels())
27+
.isNull();
28+
}
29+
30+
@Test
31+
void preservesSelectedAttributes() {
32+
TransformingItemStore<ConfigMap> transformingItemStore = new TransformingItemStore<>(r -> {
33+
r.getMetadata().setName(null);
34+
r.getMetadata().setNamespace(null);
35+
r.getMetadata().setResourceVersion(null);
36+
return r;
37+
});
38+
var cm = configMap();
39+
transformingItemStore.put(metaNamespaceKeyFunc(cm), cm);
40+
41+
assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getName())
42+
.isNotNull();
43+
assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getNamespace())
44+
.isNotNull();
45+
assertThat(
46+
transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getResourceVersion())
47+
.isNotNull();
48+
}
49+
50+
ConfigMap configMap() {
51+
var cm = new ConfigMap();
52+
cm.setMetadata(new ObjectMetaBuilder()
53+
.withName("test1")
54+
.withNamespace("default").withResourceVersion("1")
55+
.build());
56+
return cm;
57+
}
58+
59+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.time.Duration;
4+
import java.util.Map;
5+
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.fabric8.kubernetes.api.model.ConfigMap;
10+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
11+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
12+
import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneCustomResource;
13+
import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneReconciler;
14+
import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneSpec;
15+
16+
import static io.javaoperatorsdk.operator.sample.cacheprune.CachePruneReconciler.DATA_KEY;
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.awaitility.Awaitility.await;
19+
20+
class CachePruneIT {
21+
22+
public static final String DEFAULT_DATA = "default_data";
23+
public static final String TEST_RESOURCE_NAME = "test1";
24+
public static final String UPDATED_DATA = "updated_data";
25+
26+
@RegisterExtension
27+
LocallyRunOperatorExtension operator =
28+
LocallyRunOperatorExtension.builder()
29+
.withReconciler(new CachePruneReconciler()).build();
30+
31+
@Test
32+
void pruningRelatedBehavior() {
33+
var res = operator.create(testResource());
34+
await().untilAsserted(() -> {
35+
assertState(DEFAULT_DATA);
36+
});
37+
38+
res.getSpec().setData(UPDATED_DATA);
39+
var updated = operator.replace(res);
40+
41+
await().untilAsserted(() -> {
42+
assertState(UPDATED_DATA);
43+
});
44+
45+
operator.delete(updated);
46+
47+
await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> {
48+
var actual = operator.get(CachePruneCustomResource.class, TEST_RESOURCE_NAME);
49+
var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
50+
assertThat(configMap).isNull();
51+
assertThat(actual).isNull();
52+
});
53+
}
54+
55+
void assertState(String expectedData) {
56+
var actual = operator.get(CachePruneCustomResource.class, TEST_RESOURCE_NAME);
57+
assertThat(actual.getMetadata()).isNotNull();
58+
assertThat(actual.getMetadata().getFinalizers()).isNotEmpty();
59+
assertThat(actual.getStatus().getCreated()).isTrue();
60+
assertThat(actual.getMetadata().getLabels()).isNotEmpty();
61+
var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
62+
assertThat(configMap.getData()).containsEntry(DATA_KEY, expectedData);
63+
assertThat(configMap.getMetadata().getLabels()).isNotEmpty();
64+
}
65+
66+
CachePruneCustomResource testResource() {
67+
var res = new CachePruneCustomResource();
68+
res.setMetadata(new ObjectMetaBuilder()
69+
.withName(TEST_RESOURCE_NAME)
70+
.withLabels(Map.of("sampleLabel", "val"))
71+
.build());
72+
res.setSpec(new CachePruneSpec());
73+
res.getSpec().setData(DEFAULT_DATA);
74+
return res;
75+
}
76+
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.cacheprune;
2+
3+
import io.fabric8.kubernetes.api.model.Namespaced;
4+
import io.fabric8.kubernetes.client.CustomResource;
5+
import io.fabric8.kubernetes.model.annotation.Group;
6+
import io.fabric8.kubernetes.model.annotation.ShortNames;
7+
import io.fabric8.kubernetes.model.annotation.Version;
8+
9+
@Group("sample.javaoperatorsdk")
10+
@Version("v1")
11+
@ShortNames("cpr")
12+
public class CachePruneCustomResource
13+
extends CustomResource<CachePruneSpec, CachePruneStatus>
14+
implements Namespaced {
15+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package io.javaoperatorsdk.operator.sample.cacheprune;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import io.fabric8.kubernetes.api.model.ConfigMap;
7+
import io.fabric8.kubernetes.api.model.ObjectMeta;
8+
import io.fabric8.kubernetes.client.KubernetesClient;
9+
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
10+
import io.fabric8.kubernetes.client.dsl.base.PatchType;
11+
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
12+
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
13+
import io.javaoperatorsdk.operator.api.reconciler.*;
14+
import io.javaoperatorsdk.operator.junit.KubernetesClientAware;
15+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
16+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
17+
18+
@ControllerConfiguration(cachePruneFunction = LabelRemovingPruneFunction.class)
19+
public class CachePruneReconciler
20+
implements Reconciler<CachePruneCustomResource>,
21+
EventSourceInitializer<CachePruneCustomResource>,
22+
Cleaner<CachePruneCustomResource>, KubernetesClientAware {
23+
24+
public static final String DATA_KEY = "data";
25+
public static final String FIELD_MANAGER = "controller";
26+
public static final String SECONDARY_CREATE_FIELD_MANAGER = "creator";
27+
private KubernetesClient client;
28+
29+
@Override
30+
public UpdateControl<CachePruneCustomResource> reconcile(
31+
CachePruneCustomResource resource,
32+
Context<CachePruneCustomResource> context) {
33+
var configMap = context.getSecondaryResource(ConfigMap.class);
34+
configMap.ifPresentOrElse(cm -> {
35+
if (!cm.getMetadata().getLabels().isEmpty()) {
36+
throw new AssertionError("Labels should be null");
37+
}
38+
if (!cm.getData().get(DATA_KEY)
39+
.equals(resource.getSpec().getData())) {
40+
var cloned = ConfigurationServiceProvider.instance().getResourceCloner().clone(cm);
41+
cloned.getData().put(DATA_KEY, resource.getSpec().getData());
42+
var patchContext = patchContextWithFieldManager(FIELD_MANAGER);
43+
// setting new field manager since we don't control label anymore:
44+
// since not the whole object is present in cache SSA would remove labels if the controller
45+
// is not the manager.
46+
// Note that JSON Merge Patch (or others would also work here, without this "hack".
47+
patchContext.setForce(true);
48+
patchContext.setFieldManager(FIELD_MANAGER);
49+
client.configMaps().resource(cm)
50+
.patch(patchContext, cloned);
51+
}
52+
}, () -> client.configMaps().resource(configMap(resource))
53+
.patch(patchContextWithFieldManager(SECONDARY_CREATE_FIELD_MANAGER)));
54+
55+
resource.setStatus(new CachePruneStatus());
56+
resource.getStatus().setCreated(true);
57+
return UpdateControl.patchStatus(resource);
58+
}
59+
60+
private PatchContext patchContextWithFieldManager(String fieldManager) {
61+
PatchContext patchContext = new PatchContext();
62+
// using server side apply
63+
patchContext.setPatchType(PatchType.SERVER_SIDE_APPLY);
64+
patchContext.setFieldManager(fieldManager);
65+
return patchContext;
66+
}
67+
68+
@Override
69+
public Map<String, EventSource> prepareEventSources(
70+
EventSourceContext<CachePruneCustomResource> context) {
71+
InformerEventSource<ConfigMap, CachePruneCustomResource> configMapEventSource =
72+
new InformerEventSource<>(InformerConfiguration.from(ConfigMap.class, context)
73+
.withCachePruneFunction(new LabelRemovingPruneFunction<>())
74+
.build(),
75+
context);
76+
return EventSourceInitializer.nameEventSources(configMapEventSource);
77+
}
78+
79+
ConfigMap configMap(CachePruneCustomResource resource) {
80+
ConfigMap configMap = new ConfigMap();
81+
configMap.setMetadata(new ObjectMeta());
82+
configMap.getMetadata().setName(resource.getMetadata().getName());
83+
configMap.getMetadata().setNamespace(resource.getMetadata().getNamespace());
84+
configMap.setData(Map.of(DATA_KEY, resource.getSpec().getData()));
85+
HashMap<String, String> labels = new HashMap<>();
86+
labels.put("mylabel", "val");
87+
configMap.getMetadata().setLabels(labels);
88+
configMap.addOwnerReference(resource);
89+
return configMap;
90+
}
91+
92+
@Override
93+
public KubernetesClient getKubernetesClient() {
94+
return client;
95+
}
96+
97+
@Override
98+
public void setKubernetesClient(KubernetesClient kubernetesClient) {
99+
this.client = kubernetesClient;
100+
}
101+
102+
@Override
103+
public DeleteControl cleanup(CachePruneCustomResource resource,
104+
Context<CachePruneCustomResource> context) {
105+
return DeleteControl.defaultDelete();
106+
}
107+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.cacheprune;
2+
3+
public class CachePruneSpec {
4+
5+
private String data;
6+
7+
public String getData() {
8+
return data;
9+
}
10+
11+
public CachePruneSpec setData(String data) {
12+
this.data = data;
13+
return this;
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.cacheprune;
2+
3+
public class CachePruneStatus {
4+
5+
private Boolean created;
6+
7+
public Boolean getCreated() {
8+
return created;
9+
}
10+
11+
public CachePruneStatus setCreated(Boolean created) {
12+
this.created = created;
13+
return this;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.javaoperatorsdk.operator.sample.cacheprune;
2+
3+
import java.util.function.UnaryOperator;
4+
5+
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
7+
public class LabelRemovingPruneFunction<R extends HasMetadata> implements UnaryOperator<R> {
8+
@Override
9+
public R apply(R r) {
10+
r.getMetadata().setLabels(null);
11+
return r;
12+
}
13+
}

‎sample-operators/webpage/src/main/resources/log4j2.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Configuration status="WARN">
33
<Appenders>
44
<Console name="Console" target="SYSTEM_OUT">
5-
<PatternLayout pattern="%style{%d}{yellow} %style{%-30c{1.}}{cyan} %highlight{[%-5level] %msg%n%throwable}{INFO=black}"/>
5+
<PatternLayout pattern="%style{%d}{yellow} %tn %style{%-30c{1.}}{cyan} %highlight{[%-5level] %msg%n%throwable}{INFO=black}"/>
66
</Console>
77
</Appenders>
88
<Loggers>

0 commit comments

Comments
 (0)
Please sign in to comment.