Skip to content

Commit 65541d6

Browse files
committed
docs: improve wording
Signed-off-by: Chris Laprun <claprun@redhat.com>
1 parent 143f424 commit 65541d6

File tree

1 file changed

+102
-94
lines changed

1 file changed

+102
-94
lines changed
Lines changed: 102 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,75 @@
11
---
2-
title: Accessing resources in caches
2+
title: Working with EventSource caches
33
weight: 48
44
---
55

6-
As described in [Event sources and related topics](eventing.md), event sources are the backbone
7-
for caching resources and triggering the reconciliation for primary resources that are related
8-
to cached resources.
6+
As described in [Event sources and related topics](eventing.md) event sources are the backbone
7+
for caching resources and triggering reconciliation of primary resources related
8+
to these secondary resources.
99

10-
In the Kubernetes world, the component that does this is called an Informer. Without going into
11-
the details (there are plenty of good documents online regarding informers), its responsibility
12-
is to watch resources, cache them, and emit an event if the resource changed.
10+
In Kubernetes parlance, `Informers` handle that responsibility. Without going into
11+
the details (there are plenty of good documents online regarding this topics), informers
12+
watch resources, cache them, and emit an event whenever watched resources change.
1313

14-
EventSource is a generalized concept of Informer to non-Kubernetes resources. Thus,
15-
to cache external resources, and trigger reconciliation if those change.
14+
`EventSource` generalizes this concept to also cover non-Kubernetes resources. Thus,
15+
allowing caching of external resources, and triggering reconciliation when those change.
1616

1717
## The InformerEventSource
1818

19-
The underlying informer implementation comes from the fabric8 client, called [DefaultSharedIndexInformer](https://github.com/fabric8io/kubernetes-client/blob/main/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/informers/impl/DefaultSharedIndexInformer.java).
20-
[InformerEventSource](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java)
21-
in Java Operator SDK wraps informers from fabric8 client.
22-
The purpose of such wrapping is to add additional capabilities required for controllers.
23-
(In general, Informers are not used only for implementing controllers).
19+
The underlying informer implementation comes from the Fabric8 client,
20+
called [DefaultSharedIndexInformer](https://github.com/fabric8io/kubernetes-client/blob/main/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/informers/impl/DefaultSharedIndexInformer.java).
21+
[InformerEventSource](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java)
22+
in Java Operator SDK wraps informers from Fabric8 client, thus presenting a unified front to deal with Kubernetes and
23+
non-Kubernetes resources with the `EventSource` architecture.
2424

25-
Such capabilities are:
26-
- maintaining an index to which primary are the secondary resources in the informer cache are related to.
27-
- setting up multiple informers for the same type if needed (you need informer per namespace if the informer
28-
is not watching the whole cluster)
29-
- Dynamically adding/removing watched namespaces.
30-
- Some others, what is out of the scope of this document.
25+
However, `InformerEventSource` also provide additional capabilities such as:
3126

32-
### Associating Secondary Resources to Primary Resource
27+
- recording the relations between primary and secondary resources so that the event source knows which primary resource
28+
to trigger a reconciler with whenever one of the cached secondary resources cached by the informer changes,
29+
- setting up multiple informers for the same type if needed, for example to transparently watch multiple namespaces,
30+
without you having to worry about it,
31+
- dynamically adding/removing watched namespaces, if needed
32+
- and more, outside of the scope of this document.
3333

34-
The question is, how to trigger reconciliation of a primary resources (your custom resource),
35-
when Informer receives a new resource.
36-
For this purpose the framework uses [`SecondaryToPrimaryMapper`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java)
37-
that tells (usually) based on the resource which primary resource reconciliation to trigger.
38-
The mapping is usually done based on the owner reference or annotation on the secondary resource.
39-
(But not always, as we will see)
34+
### Associating Secondary Resources to Primary Resource
4035

41-
It is important to realize that if a resource triggers the reconciliation of a primary resource, that
42-
resource naturally will be used during reconciliation. So the reconciler will need to access them.
43-
Therefore, InformerEventSource maintains a reverse index [PrimaryToSecondaryIndex](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/DefaultPrimaryToSecondaryIndex.java),
44-
based on the result of the `SecondaryToPrimaryMapper` result.
36+
Event sources need to trigger the appropriate reconciler, providing the correct primary resource, whenever one of their
37+
handled secondary resources changes. It is thus core to an event source's role to identify which primary resource (
38+
usually, your custom resource) is potentially impacted by that change.
39+
The framework uses [`SecondaryToPrimaryMapper`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java)
40+
for this purpose. For `InformerEventSources`, which target Kubernetes resources, this mapping is typically done using
41+
either the owner reference or an annotation on the secondary resource. For external resources, other mechanisms need to
42+
be used and there are also cases where the default mechanisms provided by the SDK do not work, even for Kubernetes
43+
resources.
44+
45+
However, once the event source has triggered a primary resource reconciliation, the associated reconciler needs to
46+
access the secondary resources which changes caused the reconciliation. Indeed, the information from the secondary
47+
resources might be needed during the reconciliation. For that purpose,
48+
`InformerEventSource` maintains a reverse
49+
index [PrimaryToSecondaryIndex](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/DefaultPrimaryToSecondaryIndex.java),
50+
based on the result of the `SecondaryToPrimaryMapper`result.
4551

4652
## Unified API for Related Resources
4753

48-
To access all related resources for a primary resource, the framework provides an API to access the related
49-
secondary resources using:
50-
51-
```java
52-
Context.getSecondaryResources(Class<R> expectedType);
53-
```
54+
To access all related resources for a primary resource, the framework provides an API to access the related
55+
secondary resources using the `Set<R> getSecondaryResources(Class<R> expectedType)` method of the `Context` object
56+
provided as part of the `reconcile` method.
5457

55-
That will list all the related resources of a certain type, based on the `InformerEventSource`'s `PrimaryToSecondaryIndex`.
56-
Based on that index, it reads the resources from the Informers cache. Note that since all those steps work
58+
For `InformerEventSource`, this will leverage the associated `PrimaryToSecondaryIndex`. Resources are then retrieved
59+
from the informer's cache. Note that since all those steps work
5760
on top of indexes, those operations are very fast, usually O(1).
5861

59-
We mostly talk about InformerEventSource, but this works in similar ways for generalized EventSources concept, since
60-
the [`EventSource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java#L93)
61-
actually implements the `Set<R> getSecondaryResources(P primary);` method. That is just called from the context.
62+
While we've focused mostly on `InformerEventSource`, this concept can be extended to all `EventSources`, since
63+
[`EventSource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java#L93)
64+
actually implements the `Set<R> getSecondaryResources(P primary)` method that can be called from the `Context`.
6265

63-
It is a bit more complex than that, since there can be multiple event sources for the same type, in that case
64-
the union of the results is returned.
66+
As there can be multiple event sources for the same resource types, things are a little more complex: the union of each
67+
event source results is returned.
6568

6669
## Getting Resources Directly from Event Sources
6770

68-
Note that nothing stops you to directly access the resources in the cache (so not just through `getSecondaryResources(...)`):
71+
Note that nothing prevents you from directly accessing resources in the cache without going through
72+
`getSecondaryResources(...)`:
6973

7074
```java
7175
public class WebPageReconciler implements Reconciler<WebPage> {
@@ -74,19 +78,19 @@ public class WebPageReconciler implements Reconciler<WebPage> {
7478

7579
@Override
7680
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {
77-
// accessing resource directly from an event source
78-
var mySecondaryResource = configMapEventSource.get(new ResourceID("name","namespace"));
79-
// details omitted
81+
// accessing resource directly from an event source
82+
var mySecondaryResource = configMapEventSource.get(new ResourceID("name", "namespace"));
83+
// details omitted
8084
}
81-
85+
8286
@Override
8387
public List<EventSource<?, WebPage>> prepareEventSources(EventSourceContext<WebPage> context) {
84-
configMapEventSource = new InformerEventSource<>(
88+
configMapEventSource = new InformerEventSource<>(
8589
InformerEventSourceConfiguration.from(ConfigMap.class, WebPage.class)
8690
.withLabelSelector(SELECTOR)
8791
.build(),
8892
context);
89-
93+
9094
return List.of(configMapEventSource);
9195
}
9296
}
@@ -95,25 +99,28 @@ public class WebPageReconciler implements Reconciler<WebPage> {
9599
## The Use Case for PrimaryToSecondaryMapper
96100

97101
As we discussed, we provide a unified API to access related resources using `Context.getSecondaryResources(...)`.
98-
This method was on purpose uses `Secondary` resource, since those are not only child resources - how
99-
resources that are created by the reconciler are sometimes called in Kubernetes world - and usually have owner references for the custom resources;
100-
neither related resources which are usually resources that serves as input for the primary (not managed).
101-
It is the union of both.
102-
103-
The issue arises when we want to trigger reconciliation for a resource, that does not have an owner reference or other direct
104-
association with the primary resource.
105-
Typically, if you have ConfigMap where you have input parameters for a set of primary resources,
106-
and the primary is actually referencing the secondary resource.
107-
In other words, having the name of the ConfigMap in the spec part of the primary resource.
108-
109-
As an example we provide, have a primary resource a `Job` that references a `Cluster` resource.
110-
So multiple `Job` can reference the same `Cluster`, and we want to trigger `Job` reconciliation if cluster changes.
111-
See full sample [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary).
112-
But the `Cluster` (the secondary resource) does not reference the `Jobs`.
113-
114-
Even writing a `SecondaryToPrimaryMapper` is not trivial in this case, if the cluster is updated, we want to trigger
115-
all `Jobs` that are referencing it. So we have to efficiently get the list of jobs, and return their ResourceIDs in
116-
the mapper. So we need an index that maps `Cluster` to `Jobs`. Here we can use indexing capabilities of the Informers:
102+
The name `Secondary` refers to resources that a reconciler needs to take into account to properly reconcile a primary
103+
resource. These resources cover more than only `child` resources as resources created by a reconciler are sometimes
104+
called and which usually have a owner reference pointing to the primary (and, typically, custom) resource. These also
105+
cover `related` resources (which might or might not be managed by Kubernetes) that serve as input for reconciliations.
106+
107+
There are cases where the SDK needs more information than what is readily available, in particular when some of these
108+
secondary resources do not have owner references or anything direct link to the primary resource they are associated
109+
with.
110+
111+
As an example we provide, consider a `Job` primary resource which can be assigned to run on a cluster, represented by a
112+
`Cluster` resource.
113+
Multiple jobs can run on a given cluster so multiple `Job` resources can reference the same `Cluster` resource. However,
114+
a `Cluster` resource should not know about `Job` resources as this information is not part of what a cluster *is*.
115+
However, when a cluster changes, we might want to redirect the associated jobs to other clusters. Our reconciler
116+
therefore needs to figure out which `Job` (primary) resources are associated with the changed `Cluster` (secondary)
117+
resource.
118+
See full
119+
sample [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary).
120+
121+
Even writing a `SecondaryToPrimaryMapper` is not trivial in this case, if the cluster is updated, we want to trigger
122+
all `Job`s that are referencing it. So we have to efficiently get the list of jobs, and return their ResourceIDs in
123+
the mapper. So we need an index that maps `Cluster` to `Job`s. Here we can use indexing capabilities of the Informers:
117124

118125
```java
119126

@@ -123,56 +130,56 @@ public List<EventSource<?, Job>> prepareEventSources(EventSourceContext<Job> con
123130
context.getPrimaryCache()
124131
.addIndexer(JOB_CLUSTER_INDEX,
125132
(job -> List.of(indexKey(job.getSpec().getClusterName(), job.getMetadata().getNamespace()))));
126-
133+
127134
// omitted details
128135
}
129136
```
130137

131-
where index key is a String that uniquely idetifies a Cluster:
138+
where index key is a String that uniquely identifies a Cluster:
132139

133140
```java
134141
private String indexKey(String clusterName, String namespace) {
135142
return clusterName + "#" + namespace;
136-
}
143+
}
137144
```
138145

139146
In the InformerEventSource for the cluster now we can get all the `Jobs` for the `Cluster` using this index:
140147

141148
```java
142149

143-
InformerEventSource<Job,Cluster> clusterInformer =
144-
new InformerEventSource(
145-
InformerEventSourceConfiguration.from(Cluster.class, Job.class)
146-
.withSecondaryToPrimaryMapper(
147-
cluster ->
148-
context
149-
.getPrimaryCache()
150-
.byIndex(
151-
JOB_CLUSTER_INDEX,
152-
indexKey(
153-
cluster.getMetadata().getName(),
154-
cluster.getMetadata().getNamespace()))
155-
.stream()
156-
.map(ResourceID::fromResource)
157-
.collect(Collectors.toSet()))
158-
.withNamespacesInheritedFromController().build(), context);
150+
InformerEventSource<Job, Cluster> clusterInformer =
151+
new InformerEventSource(
152+
InformerEventSourceConfiguration.from(Cluster.class, Job.class)
153+
.withSecondaryToPrimaryMapper(
154+
cluster ->
155+
context.getPrimaryCache()
156+
.byIndex(
157+
JOB_CLUSTER_INDEX,
158+
indexKey(
159+
cluster.getMetadata().getName(),
160+
cluster.getMetadata().getNamespace()))
161+
.stream()
162+
.map(ResourceID::fromResource)
163+
.collect(Collectors.toSet()))
164+
.withNamespacesInheritedFromController().build(), context);
159165
```
160166

161167
This will trigger all the related `Jobs` if a cluster changes. Also, the maintaining the `PrimaryToSecondaryIndex`.
162168
So we can use the `getSecondaryResources` in the `Job` reconciler to access the cluster.
163169
However, there is an issue, what if now there is a new `Job` created? The new job does not propagate
164170
automatically to `PrimaryToSecondaryIndex` in the `InformerEventSource` of the `Cluster`. That re-indexing
165-
happens where there is an event received for the `Cluster` and triggers all the `Jobs` again.
171+
happens where there is an event received for the `Cluster` and triggers all the `Jobs` again.
166172
Until that would happen again you could not use `getSecondaryResources` for the new `Job`.
167173

168174
You could access the Cluster directly from cache though in the reconciler:
169175

170176
```java
177+
171178
@Override
172179
public UpdateControl<Job> reconcile(Job resource, Context<Job> context) {
173180

174181
clusterInformer.get(new ResourceID(job.getSpec().getClusterName(), job.getMetadata().getNamespace()));
175-
182+
176183
// omitted details
177184
}
178185
```
@@ -181,10 +188,11 @@ But if you still want to use the unified API (thus `context.getSecondaryResource
181188
`PrimaryToSecondaryMapper`:
182189

183190
```java
184-
clusterInformer.withPrimaryToSecondaryMapper( (PrimaryToSecondaryMapper<Job>)
185-
job -> Set.of(new ResourceID( job.getSpec().getClusterName(),job.getMetadata().getNamespace())));
191+
clusterInformer.withPrimaryToSecondaryMapper( job ->
192+
Set.of(new ResourceID(job.getSpec().getClusterName(), job.getMetadata().getNamespace())));
186193
```
187194

188195
That will get the `Cluster` for the `Job` from the cache of `Cluster`'s `InformerEventSource`.
189-
So it won't use the `PrimaryToSecondaryIndex`, that might be outdated, but instead will use the `PrimaryToSecondaryMapper` to get
196+
So it won't use the `PrimaryToSecondaryIndex`, that might be outdated, but instead will use the
197+
`PrimaryToSecondaryMapper` to get
190198
the target `Cluster` ids.

0 commit comments

Comments
 (0)