Skip to content

Commit 93cc8bb

Browse files
metacosmcsviri
andcommitted
feat: make it possible to also check annotations/labels when matching (#1393)
Fixes #1392 Co-authored-by: csviri <[email protected]>
1 parent 4676ce6 commit 93cc8bb

File tree

4 files changed

+251
-96
lines changed

4 files changed

+251
-96
lines changed

docs/documentation/dependent-resources.md

+109-80
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,35 @@ possible to not implement any of these traits and therefore create read-only dep
8686
that will trigger your reconciler whenever a user interacts with them but that are never
8787
modified by your reconciler itself.
8888

89+
[`AbstractSimpleDependentResource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java)
90+
and [`KubernetesDependentResource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java)
91+
sub-classes can also implement
92+
the [`Matcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java)
93+
interface to customize how the SDK decides whether or not the actual state of the dependent
94+
matches the desired state. This makes it convenient to use these abstract base classes for your
95+
implementation, only customizing the matching logic. Note that in many cases, there is no need
96+
to customize that logic as the SDK already provides convenient default implementations in the
97+
form
98+
of [`DesiredEqualsMatcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java)
99+
and
100+
[`GenericKubernetesResourceMatcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java)
101+
implementations, respectively. If you want to provide custom logic, you only need your
102+
`DependentResource` implementation to implement the `Matcher` interface as below, which shows
103+
how to customize the default matching logic for Kubernetes resource to also consider annotations
104+
and labels, which are ignored by default:
105+
106+
```java
107+
public class MyDependentResource extends KubernetesDependentResource<MyDependent, MyPrimary>
108+
implements Matcher<MyDependent, MyPrimary> {
109+
// your implementation
110+
111+
public Result<MyDependent> match(MyDependent actualResource, MyPrimary primary,
112+
Context<MyPrimary> context) {
113+
return GenericKubernetesResourceMatcher.match(this, actualResource, primary, context, true);
114+
}
115+
}
116+
```
117+
89118
### Batteries included: convenient DependentResource implementations!
90119

91120
JOSDK also offers several other convenient implementations building on top of
@@ -116,7 +145,7 @@ Deleted (or set to be garbage collected). The following example shows how to cre
116145
@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR)
117146
class DeploymentDependentResource extends CRUDKubernetesDependentResource<Deployment, WebPage> {
118147

119-
public DeploymentDependentResource() {
148+
public DeploymentDependentResource() {
120149
super(Deployment.class);
121150
}
122151

@@ -169,26 +198,26 @@ instances are managed by JOSDK, an example of which can be seen below:
169198
```java
170199

171200
@ControllerConfiguration(
172-
labelSelector = SELECTOR,
173-
dependents = {
174-
@Dependent(type = ConfigMapDependentResource.class),
175-
@Dependent(type = DeploymentDependentResource.class),
176-
@Dependent(type = ServiceDependentResource.class)
177-
})
201+
labelSelector = SELECTOR,
202+
dependents = {
203+
@Dependent(type = ConfigMapDependentResource.class),
204+
@Dependent(type = DeploymentDependentResource.class),
205+
@Dependent(type = ServiceDependentResource.class)
206+
})
178207
public class WebPageManagedDependentsReconciler
179-
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage> {
208+
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage> {
180209

181-
// omitted code
210+
// omitted code
182211

183-
@Override
184-
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context)
185-
throws Exception {
212+
@Override
213+
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context)
214+
throws Exception {
186215

187-
final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow()
188-
.getMetadata().getName();
189-
webPage.setStatus(createStatus(name));
190-
return UpdateControl.patchStatus(webPage);
191-
}
216+
final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow()
217+
.getMetadata().getName();
218+
webPage.setStatus(createStatus(name));
219+
return UpdateControl.patchStatus(webPage);
220+
}
192221

193222
}
194223
```
@@ -215,69 +244,69 @@ an `Ingress`:
215244

216245
@ControllerConfiguration
217246
public class WebPageStandaloneDependentsReconciler
218-
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage>,
219-
EventSourceInitializer<WebPage> {
220-
221-
private KubernetesDependentResource<ConfigMap, WebPage> configMapDR;
222-
private KubernetesDependentResource<Deployment, WebPage> deploymentDR;
223-
private KubernetesDependentResource<Service, WebPage> serviceDR;
224-
private KubernetesDependentResource<Service, WebPage> ingressDR;
225-
226-
public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) {
227-
// 1.
228-
createDependentResources(kubernetesClient);
229-
}
230-
231-
@Override
232-
public List<EventSource> prepareEventSources(EventSourceContext<WebPage> context) {
233-
// 2.
234-
return List.of(
235-
configMapDR.initEventSource(context),
236-
deploymentDR.initEventSource(context),
237-
serviceDR.initEventSource(context));
238-
}
239-
240-
@Override
241-
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context)
242-
throws Exception {
243-
244-
// 3.
245-
if (!isValidHtml(webPage.getHtml())) {
246-
return UpdateControl.patchStatus(setInvalidHtmlErrorMessage(webPage));
247-
}
248-
249-
// 4.
250-
configMapDR.reconcile(webPage, context);
251-
deploymentDR.reconcile(webPage, context);
252-
serviceDR.reconcile(webPage, context);
253-
254-
// 5.
255-
if (Boolean.TRUE.equals(webPage.getSpec().getExposed())) {
256-
ingressDR.reconcile(webPage, context);
257-
} else {
258-
ingressDR.delete(webPage, context);
259-
}
260-
261-
// 6.
262-
webPage.setStatus(
263-
createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()));
264-
return UpdateControl.patchStatus(webPage);
265-
}
266-
267-
private void createDependentResources(KubernetesClient client) {
268-
this.configMapDR = new ConfigMapDependentResource();
269-
this.deploymentDR = new DeploymentDependentResource();
270-
this.serviceDR = new ServiceDependentResource();
271-
this.ingressDR = new IngressDependentResource();
272-
273-
Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> {
274-
dr.setKubernetesClient(client);
275-
dr.configureWith(new KubernetesDependentResourceConfig()
276-
.setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR));
277-
});
278-
}
279-
280-
// omitted code
247+
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage>,
248+
EventSourceInitializer<WebPage> {
249+
250+
private KubernetesDependentResource<ConfigMap, WebPage> configMapDR;
251+
private KubernetesDependentResource<Deployment, WebPage> deploymentDR;
252+
private KubernetesDependentResource<Service, WebPage> serviceDR;
253+
private KubernetesDependentResource<Service, WebPage> ingressDR;
254+
255+
public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) {
256+
// 1.
257+
createDependentResources(kubernetesClient);
258+
}
259+
260+
@Override
261+
public List<EventSource> prepareEventSources(EventSourceContext<WebPage> context) {
262+
// 2.
263+
return List.of(
264+
configMapDR.initEventSource(context),
265+
deploymentDR.initEventSource(context),
266+
serviceDR.initEventSource(context));
267+
}
268+
269+
@Override
270+
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context)
271+
throws Exception {
272+
273+
// 3.
274+
if (!isValidHtml(webPage.getHtml())) {
275+
return UpdateControl.patchStatus(setInvalidHtmlErrorMessage(webPage));
276+
}
277+
278+
// 4.
279+
configMapDR.reconcile(webPage, context);
280+
deploymentDR.reconcile(webPage, context);
281+
serviceDR.reconcile(webPage, context);
282+
283+
// 5.
284+
if (Boolean.TRUE.equals(webPage.getSpec().getExposed())) {
285+
ingressDR.reconcile(webPage, context);
286+
} else {
287+
ingressDR.delete(webPage, context);
288+
}
289+
290+
// 6.
291+
webPage.setStatus(
292+
createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()));
293+
return UpdateControl.patchStatus(webPage);
294+
}
295+
296+
private void createDependentResources(KubernetesClient client) {
297+
this.configMapDR = new ConfigMapDependentResource();
298+
this.deploymentDR = new DeploymentDependentResource();
299+
this.serviceDR = new ServiceDependentResource();
300+
this.ingressDR = new IngressDependentResource();
301+
302+
Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> {
303+
dr.setKubernetesClient(client);
304+
dr.configureWith(new KubernetesDependentResourceConfig()
305+
.setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR));
306+
});
307+
}
308+
309+
// omitted code
281310
}
282311
```
283312

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java

+62
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,68 @@
55
import io.fabric8.kubernetes.api.model.HasMetadata;
66
import io.javaoperatorsdk.operator.api.reconciler.Context;
77

8+
/**
9+
* Implement this interface to provide custom matching logic when determining whether secondary
10+
* resources match their desired state. This is used by some default implementations of the
11+
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} interface, notably
12+
* {@link io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource}.
13+
*
14+
* @param <R> the type associated with the secondary resources we want to match
15+
* @param <P> the type associated with the primary resources with which the related
16+
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource}
17+
* implementation is associated
18+
*/
819
public interface Matcher<R, P extends HasMetadata> {
20+
21+
/**
22+
* Abstracts the matching result letting implementations also return the desired state if it has
23+
* been computed as part of their logic. This allows the SDK to avoid re-computing it if not
24+
* needed.
25+
*
26+
* @param <R> the type associated with the secondary resources we want to match
27+
*/
928
interface Result<R> {
29+
30+
/**
31+
* Whether or not the actual resource matched the desired state
32+
*
33+
* @return {@code true} if the observed resource matched the desired state, {@code false}
34+
* otherwise
35+
*/
1036
boolean matched();
1137

38+
/**
39+
* Retrieves the associated desired state if it has been computed during the matching process or
40+
* empty if not.
41+
*
42+
* @return an {@link Optional} holding the desired state if it has been computed during the
43+
* matching process or {@link Optional#empty()} if not
44+
*/
1245
default Optional<R> computedDesired() {
1346
return Optional.empty();
1447
}
1548

49+
/**
50+
* Creates a result stating only whether the resource matched the desired state without having
51+
* computed the desired state.
52+
*
53+
* @param matched whether the actual resource matched the desired state
54+
* @return a {@link Result} with an empty computed desired state
55+
* @param <T> the type of resources being matched
56+
*/
1657
static <T> Result<T> nonComputed(boolean matched) {
1758
return () -> matched;
1859
}
1960

61+
/**
62+
* Creates a result stating whether the resource matched and the associated computed desired
63+
* state so that the SDK can use it downstream without having to recompute it.
64+
*
65+
* @param matched whether the actual resource matched the desired state
66+
* @param computedDesired the associated desired state as computed during the matching process
67+
* @return a {@link Result} with the associated desired state
68+
* @param <T> the type of resources being matched
69+
*/
2070
static <T> Result<T> computed(boolean matched, T computedDesired) {
2171
return new Result<>() {
2272
@Override
@@ -32,5 +82,17 @@ public Optional<T> computedDesired() {
3282
}
3383
}
3484

85+
/**
86+
* Determines whether the specified secondary resource matches the desired state as defined from
87+
* the specified primary resource, given the specified {@link Context}.
88+
*
89+
* @param actualResource the resource we want to determine whether it's matching the desired state
90+
* @param primary the primary resource from which the desired state is inferred
91+
* @param context the context in which the resource is being matched
92+
* @return a {@link Result} encapsulating whether the resource matched its desired state and this
93+
* associated state if it was computed as part of the matching process. Use the static
94+
* convenience methods ({@link Result#nonComputed(boolean)} and
95+
* {@link Result#computed(boolean, Object)})
96+
*/
3597
Result<R> match(R actualResource, P primary, Context<P> context);
3698
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

3+
import java.util.Objects;
4+
35
import io.fabric8.kubernetes.api.model.ConfigMap;
46
import io.fabric8.kubernetes.api.model.HasMetadata;
57
import io.fabric8.kubernetes.api.model.Secret;
@@ -41,8 +43,44 @@ static <R extends HasMetadata, P extends HasMetadata> Matcher<R, P> matcherFor(
4143

4244
@Override
4345
public Result<R> match(R actualResource, P primary, Context<P> context) {
44-
final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper();
46+
return match(dependentResource, actualResource, primary, context, false);
47+
}
48+
49+
/**
50+
* Determines whether the specified actual resource matches the desired state defined by the
51+
* specified {@link KubernetesDependentResource} based on the observed state of the associated
52+
* specified primary resource.
53+
*
54+
* @param dependentResource the {@link KubernetesDependentResource} implementation used to
55+
* computed the desired state associated with the specified primary resource
56+
* @param actualResource the observed dependent resource for which we want to determine whether it
57+
* matches the desired state or not
58+
* @param primary the primary resource from which we want to compute the desired state
59+
* @param context the {@link Context} instance within which this method is called
60+
* @param considerMetadata {@code true} to consider the metadata of the actual resource when
61+
* determining if it matches the desired state, {@code false} if matching should occur only
62+
* considering the spec of the resources
63+
* @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
64+
* @param <R> the type of resource we want to determine whether they match or not
65+
* @param <P> the type of primary resources associated with the secondary resources we want to
66+
* match
67+
*/
68+
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
69+
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
70+
Context<P> context, boolean considerMetadata) {
4571
final var desired = dependentResource.desired(primary, context);
72+
if (considerMetadata) {
73+
final var desiredMetadata = desired.getMetadata();
74+
final var actualMetadata = actualResource.getMetadata();
75+
final var matched =
76+
Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
77+
Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
78+
if (!matched) {
79+
return Result.computed(false, desired);
80+
}
81+
}
82+
83+
final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper();
4684

4785
// reflection will be replaced by this:
4886
// https://github.com/fabric8io/kubernetes-client/issues/3816

0 commit comments

Comments
 (0)