Skip to content

Commit ab111e7

Browse files
authored
Primary to Secondary Mapper (#1300)
1 parent d3f1b57 commit ab111e7

File tree

16 files changed

+397
-90
lines changed

16 files changed

+397
-90
lines changed

docs/documentation/features.md

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -377,22 +377,12 @@ public class TomcatReconciler implements Reconciler<Tomcat>, EventSourceInitiali
377377

378378
@Override
379379
public List<EventSource> prepareEventSources(EventSourceContext<Tomcat> context) {
380-
SharedIndexInformer<Deployment> deploymentInformer =
381-
kubernetesClient.apps()
382-
.deployments()
383-
.inAnyNamespace()
384-
.withLabel("app.kubernetes.io/managed-by", "tomcat-operator")
385-
.runnableInformer(0);
386-
387-
return List.of(
388-
new InformerEventSource<>(deploymentInformer, d -> {
389-
var ownerReferences = d.getMetadata().getOwnerReferences();
390-
if (!ownerReferences.isEmpty()) {
391-
return Set.of(new ResourceID(ownerReferences.get(0).getName(), d.getMetadata().getNamespace()));
392-
} else {
393-
return EMPTY_SET;
394-
}
395-
}));
380+
var configMapEventSource =
381+
new InformerEventSource<>(InformerConfiguration.from(Deployment.class, context)
382+
.withLabelSelector(SELECTOR)
383+
.withSecondaryToPrimaryMapper(Mappers.fromAnnotation(ANNOTATION_NAME,ANNOTATION_NAMESPACE)
384+
.build(), context));
385+
return EventSourceInitializer.nameEventSources(configMapEventSource);
396386
}
397387
...
398388
}
@@ -401,21 +391,38 @@ public class TomcatReconciler implements Reconciler<Tomcat>, EventSourceInitiali
401391
In the example above an `InformerEventSource` is registered (more on this specific eventsource later). Multiple things
402392
are going on here:
403393

404-
1. An `SharedIndexInformer` (class from fabric8 Kubernetes client) is created. This will watch and produce events for
394+
1. In the background `SharedIndexInformer` (class from fabric8 Kubernetes client) is created. This will watch and produce events for
405395
`Deployments` in every namespace, but will filter them based on label. So `Deployments` which are not managed by
406396
`tomcat-operator` (the label is not present on them) will not trigger a reconciliation.
407397
2. In the next step
408398
an [InformerEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java)
409-
is created, which wraps the `SharedIndexInformer`. In addition to that a mapping functions is provided, **this maps
410-
the event of the watched resource (in this case `Deployment`) to the custom resources to reconcile**. Not that in
411-
this case this is a simple task, since `Deployment` is already created with an owner reference. Therefore,
412-
the `ResourceID`
413-
what identifies the custom resource to reconcile is created from the owner reference.
399+
is created, which wraps the `SharedIndexInformer`. In addition to that a mapping functions is provided,
400+
with `withSecondaryToPrimaryMapper`, this maps the event of the watched resource (in this case `Deployment`) to the
401+
custom resources to reconcile. Note that usually this is covered by a default mapper , when `Deployment`
402+
is created with an owner reference, the default mapper gets the mapping information from there. Thus,
403+
the `ResourceID` what identifies the custom resource to reconcile is created from the owner reference.
404+
For sake of the example a mapper is added that maps secondary to primary resource based on annotations.
414405

415406
Note that a set of `ResourceID` is returned, this is usually just a set with one element. The possibility to specify
416407
multiple values are there to cover some rare corner cases. If an irrelevant resource is observed, an empty set can
417408
be returned to not reconcile any custom resource.
418409

410+
### Managing Relation between Primary and Secondary Resources
411+
412+
As already touched in previous section, a `SecondaryToPrimaryMapper` is required to map events to trigger reconciliation
413+
of the primary resource. By default, this is handled with a mapper that utilizes owner references. If an owner reference
414+
cannot be used (for example resources are in different namespace), other mapper can be provided, typically an annotation
415+
based on is provided.
416+
417+
Adding a `SecondaryToPrimaryMapper` is typically sufficient when there is a one-to-many relationship between primary and
418+
secondary resources. The secondary resources can be mapped to its primary owner, and this is enough information to also
419+
get the resource using the API from the context in reconciler: `context.getSecondaryResources(...)`. There are however
420+
cases when to map the other way around this mapper is not enough, a `PrimaryToSecondaryMapper` is required.
421+
This is typically when there is a many-to-one or many-to-many relationship between resources, thus the primary resource
422+
is referencing a secondary resources. In these cases the mentioned reverse mapper is required to work properly.
423+
See [PrimaryToSecondaryIT](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryIT.java)
424+
integration test for a sample.
425+
419426
### Built-in EventSources
420427

421428
There are multiple event-sources provided out of the box, the following are some more central ones:

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.javaoperatorsdk.operator.api.config.ResourceConfiguration;
99
import io.javaoperatorsdk.operator.api.config.Utils;
1010
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
11+
import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper;
1112
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
1213
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
1314

@@ -19,15 +20,18 @@ public interface InformerConfiguration<R extends HasMetadata>
1920
class DefaultInformerConfiguration<R extends HasMetadata> extends
2021
DefaultResourceConfiguration<R> implements InformerConfiguration<R> {
2122

23+
private final PrimaryToSecondaryMapper<?> primaryToSecondaryMapper;
2224
private final SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
2325
private final boolean followControllerNamespaceChanges;
2426

2527
protected DefaultInformerConfiguration(String labelSelector,
2628
Class<R> resourceClass,
29+
PrimaryToSecondaryMapper<?> primaryToSecondaryMapper,
2730
SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper,
2831
Set<String> namespaces, boolean followControllerNamespaceChanges) {
2932
super(labelSelector, resourceClass, namespaces);
3033
this.followControllerNamespaceChanges = followControllerNamespaceChanges;
34+
this.primaryToSecondaryMapper = primaryToSecondaryMapper;
3135
this.secondaryToPrimaryMapper =
3236
Objects.requireNonNullElse(secondaryToPrimaryMapper,
3337
Mappers.fromOwnerReference());
@@ -41,6 +45,10 @@ public SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper() {
4145
return secondaryToPrimaryMapper;
4246
}
4347

48+
@Override
49+
public <P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondaryMapper() {
50+
return (PrimaryToSecondaryMapper<P>) primaryToSecondaryMapper;
51+
}
4452
}
4553

4654
/**
@@ -53,9 +61,12 @@ public SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper() {
5361

5462
SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper();
5563

64+
<P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondaryMapper();
65+
5666
@SuppressWarnings("unused")
5767
class InformerConfigurationBuilder<R extends HasMetadata> {
5868

69+
private PrimaryToSecondaryMapper<?> primaryToSecondaryMapper;
5970
private SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
6071
private Set<String> namespaces;
6172
private String labelSelector;
@@ -66,6 +77,12 @@ private InformerConfigurationBuilder(Class<R> resourceClass) {
6677
this.resourceClass = resourceClass;
6778
}
6879

80+
public <P extends HasMetadata> InformerConfigurationBuilder<R> withPrimaryToSecondaryMapper(
81+
PrimaryToSecondaryMapper<P> primaryToSecondaryMapper) {
82+
this.primaryToSecondaryMapper = primaryToSecondaryMapper;
83+
return this;
84+
}
85+
6986
public InformerConfigurationBuilder<R> withSecondaryToPrimaryMapper(
7087
SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper) {
7188
this.secondaryToPrimaryMapper = secondaryToPrimaryMapper;
@@ -136,6 +153,7 @@ public InformerConfigurationBuilder<R> withLabelSelector(String labelSelector) {
136153

137154
public InformerConfiguration<R> build() {
138155
return new DefaultInformerConfiguration<>(labelSelector, resourceClass,
156+
primaryToSecondaryMapper,
139157
secondaryToPrimaryMapper,
140158
namespaces, inheritControllerNamespacesOnChange);
141159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.javaoperatorsdk.operator.processing.event.source;
2+
3+
import java.util.Set;
4+
5+
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
7+
8+
/**
9+
* Primary to Secondary mapper only needed in some cases, typically when there it many-to-one or
10+
* many-to-many relation between primary and secondary resources. If there is owner reference (or
11+
* reference with annotations) from secondary to primary this is not needed. See
12+
* PrimaryToSecondaryIT integration tests that handles many-to-many relationship.
13+
*
14+
* @param <P> primary resource type
15+
*/
16+
public interface PrimaryToSecondaryMapper<P extends HasMetadata> {
17+
18+
Set<ResourceID> toSecondaryResourceIDs(P primary);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.javaoperatorsdk.operator.processing.event.source.informer;
2+
3+
import java.util.*;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
6+
import io.fabric8.kubernetes.api.model.HasMetadata;
7+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
8+
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
9+
10+
class DefaultPrimaryToSecondaryIndex<R extends HasMetadata> implements PrimaryToSecondaryIndex<R> {
11+
12+
private SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
13+
private Map<ResourceID, Set<ResourceID>> index = new HashMap<>();
14+
15+
public DefaultPrimaryToSecondaryIndex(SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper) {
16+
this.secondaryToPrimaryMapper = secondaryToPrimaryMapper;
17+
}
18+
19+
@Override
20+
public synchronized void onAddOrUpdate(R resource) {
21+
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
22+
primaryResources.forEach(
23+
primaryResource -> {
24+
var resourceSet =
25+
index.computeIfAbsent(primaryResource, pr -> ConcurrentHashMap.newKeySet());
26+
resourceSet.add(ResourceID.fromResource(resource));
27+
});
28+
}
29+
30+
@Override
31+
public synchronized void onDelete(R resource) {
32+
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
33+
primaryResources.forEach(
34+
primaryResource -> {
35+
var secondaryResources = index.get(primaryResource);
36+
secondaryResources.remove(ResourceID.fromResource(resource));
37+
if (secondaryResources.isEmpty()) {
38+
index.remove(primaryResource);
39+
}
40+
});
41+
}
42+
43+
@Override
44+
public synchronized Set<ResourceID> getSecondaryResources(ResourceID primary) {
45+
var resourceIDs = index.get(primary);
46+
if (resourceIDs == null) {
47+
return Collections.emptySet();
48+
} else {
49+
return Collections.unmodifiableSet(resourceIDs);
50+
}
51+
}
52+
}

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.javaoperatorsdk.operator.processing.event.Event;
1717
import io.javaoperatorsdk.operator.processing.event.EventHandler;
1818
import io.javaoperatorsdk.operator.processing.event.ResourceID;
19+
import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper;
1920

2021
/**
2122
* <p>
@@ -74,20 +75,23 @@ public class InformerEventSource<R extends HasMetadata, P extends HasMetadata>
7475
private final EventRecorder<R> eventRecorder = new EventRecorder<>();
7576
// we need direct control for the indexer to propagate the just update resource also to the index
7677
private final PrimaryToSecondaryIndex<R> primaryToSecondaryIndex;
78+
private final PrimaryToSecondaryMapper<P> primaryToSecondaryMapper;
7779

7880
public InformerEventSource(
7981
InformerConfiguration<R> configuration, EventSourceContext<P> context) {
80-
super(context.getClient().resources(configuration.getResourceClass()), configuration);
81-
this.configuration = configuration;
82-
primaryToSecondaryIndex =
83-
new PrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper());
82+
this(configuration, context.getClient());
8483
}
8584

8685
public InformerEventSource(InformerConfiguration<R> configuration, KubernetesClient client) {
8786
super(client.resources(configuration.getResourceClass()), configuration);
8887
this.configuration = configuration;
89-
primaryToSecondaryIndex =
90-
new PrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper());
88+
primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper();
89+
if (primaryToSecondaryMapper == null) {
90+
primaryToSecondaryIndex =
91+
new DefaultPrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper());
92+
} else {
93+
primaryToSecondaryIndex = NOOPPrimaryToSecondaryIndex.getInstance();
94+
}
9195
}
9296

9397
@Override
@@ -177,8 +181,13 @@ private void propagateEvent(R object) {
177181

178182
@Override
179183
public Set<R> getSecondaryResources(P primary) {
180-
var secondaryIDs =
181-
primaryToSecondaryIndex.getSecondaryResources(ResourceID.fromResource(primary));
184+
Set<ResourceID> secondaryIDs;
185+
if (useSecondaryToPrimaryIndex()) {
186+
secondaryIDs =
187+
primaryToSecondaryIndex.getSecondaryResources(ResourceID.fromResource(primary));
188+
} else {
189+
secondaryIDs = primaryToSecondaryMapper.toSecondaryResourceIDs(primary);
190+
}
182191
return secondaryIDs.stream().map(this::get).flatMap(Optional::stream)
183192
.collect(Collectors.toSet());
184193
}
@@ -247,6 +256,10 @@ private void handleRecentResourceOperationAndStopEventRecording(R resource) {
247256
}
248257
}
249258

259+
private boolean useSecondaryToPrimaryIndex() {
260+
return this.primaryToSecondaryMapper == null;
261+
}
262+
250263
@Override
251264
public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID,
252265
R resource) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.javaoperatorsdk.operator.processing.event.source.informer;
2+
3+
import java.util.Set;
4+
5+
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
7+
8+
class NOOPPrimaryToSecondaryIndex<R extends HasMetadata>
9+
implements PrimaryToSecondaryIndex<R> {
10+
11+
@SuppressWarnings("rawtypes")
12+
private static final NOOPPrimaryToSecondaryIndex instance = new NOOPPrimaryToSecondaryIndex();
13+
14+
public static <T extends HasMetadata> NOOPPrimaryToSecondaryIndex<T> getInstance() {
15+
return instance;
16+
}
17+
18+
private NOOPPrimaryToSecondaryIndex() {}
19+
20+
@Override
21+
public void onAddOrUpdate(R resource) {
22+
// empty method because of noop implementation
23+
}
24+
25+
@Override
26+
public void onDelete(R resource) {
27+
// empty method because of noop implementation
28+
}
29+
30+
@Override
31+
public Set<ResourceID> getSecondaryResources(ResourceID primary) {
32+
throw new UnsupportedOperationException();
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,15 @@
11
package io.javaoperatorsdk.operator.processing.event.source.informer;
22

3-
import java.util.*;
4-
import java.util.concurrent.ConcurrentHashMap;
3+
import java.util.Set;
54

65
import io.fabric8.kubernetes.api.model.HasMetadata;
76
import io.javaoperatorsdk.operator.processing.event.ResourceID;
8-
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
97

10-
class PrimaryToSecondaryIndex<R extends HasMetadata> {
8+
public interface PrimaryToSecondaryIndex<R extends HasMetadata> {
119

12-
private SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
13-
private Map<ResourceID, Set<ResourceID>> index = new HashMap<>();
10+
void onAddOrUpdate(R resource);
1411

15-
public PrimaryToSecondaryIndex(SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper) {
16-
this.secondaryToPrimaryMapper = secondaryToPrimaryMapper;
17-
}
12+
void onDelete(R resource);
1813

19-
public synchronized void onAddOrUpdate(R resource) {
20-
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
21-
primaryResources.forEach(
22-
primaryResource -> {
23-
var resourceSet =
24-
index.computeIfAbsent(primaryResource, pr -> ConcurrentHashMap.newKeySet());
25-
resourceSet.add(ResourceID.fromResource(resource));
26-
});
27-
}
28-
29-
public synchronized void onDelete(R resource) {
30-
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
31-
primaryResources.forEach(
32-
primaryResource -> {
33-
var secondaryResources = index.get(primaryResource);
34-
secondaryResources.remove(ResourceID.fromResource(resource));
35-
if (secondaryResources.isEmpty()) {
36-
index.remove(primaryResource);
37-
}
38-
});
39-
}
40-
41-
public synchronized Set<ResourceID> getSecondaryResources(ResourceID primary) {
42-
var resourceIDs = index.get(primary);
43-
if (resourceIDs == null) {
44-
return Collections.emptySet();
45-
} else {
46-
return Collections.unmodifiableSet(resourceIDs);
47-
}
48-
}
14+
Set<ResourceID> getSecondaryResources(ResourceID primary);
4915
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
import static org.mockito.Mockito.mock;
1616
import static org.mockito.Mockito.when;
1717

18-
class PrimaryToSecondaryIndexTest {
18+
class DefaultPrimaryToSecondaryIndexTest {
1919

2020
private SecondaryToPrimaryMapper<ConfigMap> secondaryToPrimaryMapperMock =
2121
mock(SecondaryToPrimaryMapper.class);
22-
private PrimaryToSecondaryIndex<ConfigMap> primaryToSecondaryIndex =
23-
new PrimaryToSecondaryIndex<>(secondaryToPrimaryMapperMock);
22+
private DefaultPrimaryToSecondaryIndex<ConfigMap> primaryToSecondaryIndex =
23+
new DefaultPrimaryToSecondaryIndex<>(secondaryToPrimaryMapperMock);
2424

2525
private ResourceID primaryID1 = new ResourceID("id1", "default");
2626
private ResourceID primaryID2 = new ResourceID("id2", "default");

0 commit comments

Comments
 (0)