|
| 1 | +--- |
| 2 | +title: Accessing resources in caches |
| 3 | +weight: 48 |
| 4 | +--- |
| 5 | + |
| 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 thar are related |
| 8 | +to cached resources. |
| 9 | + |
| 10 | +In Kubernetes world, the component that does this is called 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. |
| 13 | + |
| 14 | +EventSource is a generalized concept of Informer to non-Kubernetes resources. Thus, |
| 15 | +to cache external resources, and trigger reconciliation if those change. |
| 16 | + |
| 17 | +## The InformerEventSource |
| 18 | + |
| 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). |
| 24 | + |
| 25 | +Such capabilities are: |
| 26 | +- maintaining and index to which primary are the secondary resources in 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. |
| 31 | + |
| 32 | +### Associating Secondary Resources to Primary Resource |
| 33 | + |
| 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 (mostly) 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) |
| 40 | + |
| 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 revers 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. |
| 45 | + |
| 46 | +## Unified API for Related Resources |
| 47 | + |
| 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 | + |
| 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 |
| 57 | +on top of indexes, those operations are very fast, usually O(1). |
| 58 | + |
| 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 | + |
| 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. |
| 65 | + |
| 66 | +## Getting Resources Directly from Event Sources |
| 67 | + |
| 68 | +Note that nothing stops you to directly access the resources in the cache (so not just through `getSecondaryResources(...)`): |
| 69 | + |
| 70 | +```java |
| 71 | +public class WebPageReconciler implements Reconciler<WebPage> { |
| 72 | + |
| 73 | + InformerEventSource<ConfigMap, WebPage> configMapEventSource; |
| 74 | + |
| 75 | + @Override |
| 76 | + 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 |
| 80 | + } |
| 81 | + |
| 82 | + @Override |
| 83 | + public List<EventSource<?, WebPage>> prepareEventSources(EventSourceContext<WebPage> context) { |
| 84 | + configMapEventSource = new InformerEventSource<>( |
| 85 | + InformerEventSourceConfiguration.from(ConfigMap.class, WebPage.class) |
| 86 | + .withLabelSelector(SELECTOR) |
| 87 | + .build(), |
| 88 | + context); |
| 89 | + |
| 90 | + return List.of(configMapEventSource); |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +## The Use Case for PrimaryToSecondaryMapper |
| 96 | + |
| 97 | +As we discussed, we provide an 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). It is the union of both. |
| 101 | + |
| 102 | +The issue is if we want to trigger reconciliation for a resource, that does not have an owner reference or other direct |
| 103 | +association with the primary resource. |
| 104 | +Typically, if you have ConfigMap where you have input parameters for a set of primary resources, |
| 105 | +and the primary is actually referencing the secondary resource. Like having the name of the ConfigMap in the spec part |
| 106 | +of the primary resource. |
| 107 | + |
| 108 | +As an example we provide, have a primary resource a `Job` that references a `Cluster` resource. |
| 109 | +So multiple `Job` can reference the same `Cluster`, and we want to trigger `Job` reconciliation if cluster changes. |
| 110 | +See full sample [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary). |
| 111 | +But the `Cluster` (the secondary resource) does not reference the `Jobs`. |
| 112 | + |
| 113 | +Even writing a `SecondaryToPrimaryMapper` is not trivial in this case, if the cluster is updated, we want to trigger |
| 114 | +all `Jobs` that are referencing it. So we have to efficiently get the list of jobs, and return their ResourceIDs in |
| 115 | +the mapper. So we need an index that maps `Cluster` to `Jobs`. Here we can use indexing capabilities of the Informers: |
| 116 | + |
| 117 | +```java |
| 118 | + |
| 119 | +@Override |
| 120 | +public List<EventSource<?, Job>> prepareEventSources(EventSourceContext<Job> context) { |
| 121 | + |
| 122 | + context.getPrimaryCache() |
| 123 | + .addIndexer(JOB_CLUSTER_INDEX, |
| 124 | + (job -> List.of(indexKey(job.getSpec().getClusterName(), job.getMetadata().getNamespace())))); |
| 125 | + |
| 126 | + // omitted details |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +where index key is a String that uniquely idetifies a Cluster: |
| 131 | + |
| 132 | +```java |
| 133 | +private String indexKey(String clusterName, String namespace) { |
| 134 | + return clusterName + "#" + namespace; |
| 135 | + } |
| 136 | +``` |
| 137 | + |
| 138 | +In the InformerEventSource for the cluster now we can get all the `Jobs` for the `Cluster` using this index: |
| 139 | + |
| 140 | +```java |
| 141 | + |
| 142 | + InformerEventSource<Job,Cluster> clusterInformer = |
| 143 | + new InformerEventSource( |
| 144 | + InformerEventSourceConfiguration.from(Cluster.class, Job.class) |
| 145 | + .withSecondaryToPrimaryMapper( |
| 146 | + cluster -> |
| 147 | + context |
| 148 | + .getPrimaryCache() |
| 149 | + .byIndex( |
| 150 | + JOB_CLUSTER_INDEX, |
| 151 | + indexKey( |
| 152 | + cluster.getMetadata().getName(), |
| 153 | + cluster.getMetadata().getNamespace())) |
| 154 | + .stream() |
| 155 | + .map(ResourceID::fromResource) |
| 156 | + .collect(Collectors.toSet())) |
| 157 | + .withNamespacesInheritedFromController().build(), context); |
| 158 | +``` |
| 159 | + |
| 160 | +This will trigger all the related `Jobs` if a cluster changes. Also, the maintaining the `PrimaryToSecondaryIndex`. |
| 161 | +So we can use the `getSecondaryResources` in the `Job` reconciler to access the cluster. |
| 162 | +However, there is an issue, what if now there is a new `Job` created? The new job does not propagate |
| 163 | +automatically to `PrimaryToSecondaryIndex` in the `InformerEventSource` of the `Cluster`. That re-indexing |
| 164 | +happens where there is an event received for the `Cluster` and triggers all the `Jobs` again. |
| 165 | +Until that would happen again you could not use `getSecondaryResources` for the new `Job`. |
| 166 | + |
| 167 | +You could access the Cluster directly from cache though in the reconciler: |
| 168 | + |
| 169 | +```java |
| 170 | +@Override |
| 171 | +public UpdateControl<Job> reconcile(Job resource, Context<Job> context) { |
| 172 | + |
| 173 | + clusterInformer.get(new ResourceID(job.getSpec().getClusterName(), job.getMetadata().getNamespace())); |
| 174 | + |
| 175 | + // omitted details |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +But if still want to use the unified API (thus `context.getSecondaryResources()`), we can add |
| 180 | +`PrimaryToSecondaryMapper`: |
| 181 | + |
| 182 | +```java |
| 183 | +clusterInformer.withPrimaryToSecondaryMapper( (PrimaryToSecondaryMapper<Job>) |
| 184 | + job -> Set.of(new ResourceID( job.getSpec().getClusterName(),job.getMetadata().getNamespace()))); |
| 185 | +``` |
| 186 | + |
| 187 | +That will get the `Cluster` for the `Job` from the cache of `Cluster`'s `InformerEventSource`. |
| 188 | +So it won't use the `PrimaryToSecondaryIndex`, that might be outdated, but instead will use the `PrimaryToSecondaryMapper` to get |
| 189 | +the target `Cluster` ids. |
0 commit comments