Skip to content

Commit f99a414

Browse files
committed
matchers ignore list (#1880)
Co-authored-by: Chris Laprun <[email protected]> � Conflicts: � operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java � operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java
1 parent d0d1528 commit f99a414

19 files changed

+513
-93
lines changed

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java

-19
This file was deleted.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public Optional<T> computedDesired() {
9292
* @return a {@link Result} encapsulating whether the resource matched its desired state and this
9393
* associated state if it was computed as part of the matching process. Use the static
9494
* convenience methods ({@link Result#nonComputed(boolean)} and
95-
* {@link Result#computed(boolean, Object)})
95+
* {@link Result#computed(boolean, Object)}) to create your return {@link Result}.
9696
*/
9797
Result<R> match(R actualResource, P primary, Context<P> context);
9898
}
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,61 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

3-
import java.util.Objects;
3+
import java.util.*;
44

5+
import io.fabric8.kubernetes.api.model.ConfigMap;
56
import io.fabric8.kubernetes.api.model.HasMetadata;
7+
import io.fabric8.kubernetes.api.model.Secret;
8+
import io.fabric8.zjsonpatch.JsonDiff;
9+
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
610
import io.javaoperatorsdk.operator.api.reconciler.Context;
711
import io.javaoperatorsdk.operator.processing.dependent.Matcher;
812
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.processors.GenericResourceUpdatePreProcessor;
913

14+
import com.fasterxml.jackson.databind.JsonNode;
15+
import com.fasterxml.jackson.databind.ObjectMapper;
16+
1017
public class GenericKubernetesResourceMatcher<R extends HasMetadata, P extends HasMetadata>
1118
implements Matcher<R, P> {
1219

20+
21+
private static final String ADD = "add";
22+
private static final String OP = "op";
23+
private static final String METADATA_LABELS = "/metadata/labels";
24+
private static final String METADATA_ANNOTATIONS = "/metadata/annotations";
25+
26+
private static final String PATH = "path";
27+
private static final String[] EMPTY_ARRAY = {};
1328
private final KubernetesDependentResource<R, P> dependentResource;
1429

1530
private GenericKubernetesResourceMatcher(KubernetesDependentResource<R, P> dependentResource) {
1631
this.dependentResource = dependentResource;
1732
}
1833

19-
@SuppressWarnings({"unchecked", "rawtypes"})
34+
@SuppressWarnings({"unchecked", "rawtypes", "unused"})
2035
static <R extends HasMetadata, P extends HasMetadata> Matcher<R, P> matcherFor(
2136
KubernetesDependentResource<R, P> dependentResource) {
2237
return new GenericKubernetesResourceMatcher(dependentResource);
2338
}
2439

40+
/**
41+
* {@inheritDoc}
42+
* <p/>
43+
* This implementation attempts to cover most common cases out of the box by considering
44+
* non-additive changes to the resource's spec (if the resource in question has a {@code spec}
45+
* field), making special provisions for {@link ConfigMap} and {@link Secret} resources. Additive
46+
* changes (i.e. a field is added that previously didn't exist) are not considered as triggering a
47+
* mismatch by default to account for validating webhooks that might add default values
48+
* automatically when not present or some other controller adding labels and/or annotations.
49+
* <p/>
50+
* It should be noted that this implementation is potentially intensive because it generically
51+
* attempts to cover common use cases by performing diffs on the JSON representation of objects.
52+
* If performance is a concern, it might be easier / simpler to provide a {@link Matcher}
53+
* implementation optimized for your use case.
54+
*/
2555
@Override
2656
public Result<R> match(R actualResource, P primary, Context<P> context) {
2757
var desired = dependentResource.desired(primary, context);
28-
return match(desired, actualResource, false, false);
29-
}
30-
31-
public static <R extends HasMetadata> Result<R> match(R desired, R actualResource,
32-
boolean considerMetadata) {
33-
return match(desired, actualResource, considerMetadata, false);
58+
return match(desired, actualResource, false, false, false);
3459
}
3560

3661
/**
@@ -39,10 +64,13 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
3964
*
4065
* @param desired the desired resource
4166
* @param actualResource the actual resource
42-
* @param considerMetadata {@code true} if labels and annotations will be checked for equality,
43-
* {@code false} otherwise (meaning that metadata changes will be ignored for matching
44-
* purposes)
45-
* @param equality if {@code false}, the algorithm checks if the properties in the desired
67+
* @param considerLabelsAndAnnotations {@code true} if labels and annotations will be checked for
68+
* equality, {@code false} otherwise (meaning that metadata changes will be ignored for
69+
* matching purposes)
70+
* @param labelsAndAnnotationsEquality if true labels and annotation match exactly in the actual
71+
* and desired state if false, additional elements are allowed in actual annotations.
72+
* Considered only if considerLabelsAndAnnotations is true.
73+
* @param specEquality if {@code false}, the algorithm checks if the properties in the desired
4674
* resource spec are same as in the actual resource spec. The reason is that admission
4775
* controllers and default Kubernetes controllers might add default values to some
4876
* properties which are not set in the desired resources' spec and comparing it with simple
@@ -53,61 +81,191 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
5381
* all properties and values are equal. This could be implemented also by overriding equals
5482
* method of spec, should be done as an optimization - this implementation does not require
5583
* that.
56-
*
57-
* @return results of matching
5884
* @param <R> resource
85+
* @return results of matching
5986
*/
60-
@SuppressWarnings("unchecked")
6187
public static <R extends HasMetadata> Result<R> match(R desired, R actualResource,
62-
boolean considerMetadata, boolean equality) {
63-
if (considerMetadata) {
64-
final var desiredMetadata = desired.getMetadata();
65-
final var actualMetadata = actualResource.getMetadata();
66-
final var matched =
67-
Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
68-
Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
69-
if (!matched) {
70-
return Result.computed(false, desired);
71-
}
72-
}
88+
boolean considerLabelsAndAnnotations, boolean labelsAndAnnotationsEquality,
89+
boolean specEquality) {
90+
return match(desired, actualResource, considerLabelsAndAnnotations,
91+
labelsAndAnnotationsEquality, specEquality, EMPTY_ARRAY);
92+
}
7393

74-
final ResourceUpdatePreProcessor<R> processor =
75-
GenericResourceUpdatePreProcessor.processorFor((Class<R>) desired.getClass());
76-
final var matched = processor.matches(actualResource, desired, equality);
77-
return Result.computed(matched, desired);
94+
/**
95+
* Determines whether the specified actual resource matches the specified desired resource,
96+
* possibly considering metadata and deeper equality checks.
97+
*
98+
* @param desired the desired resource
99+
* @param actualResource the actual resource
100+
* @param considerLabelsAndAnnotations {@code true} if labels and annotations will be checked for
101+
* equality, {@code false} otherwise (meaning that metadata changes will be ignored for
102+
* matching purposes)
103+
* @param labelsAndAnnotationsEquality if true labels and annotation match exactly in the actual
104+
* and desired state if false, additional elements are allowed in actual annotations.
105+
* Considered only if considerLabelsAndAnnotations is true.
106+
* @param ignorePaths are paths in the resource that are ignored on matching (basically an ignore
107+
* list). All changes with a target prefix path on a calculated JSON Patch between actual
108+
* and desired will be ignored. If there are other changes, non-present on ignore list
109+
* match fails.
110+
* @param <R> resource
111+
* @return results of matching
112+
*/
113+
public static <R extends HasMetadata> Result<R> match(R desired, R actualResource,
114+
boolean considerLabelsAndAnnotations, boolean labelsAndAnnotationsEquality,
115+
String... ignorePaths) {
116+
return match(desired, actualResource, considerLabelsAndAnnotations,
117+
labelsAndAnnotationsEquality, false, ignorePaths);
78118
}
79119

80120
/**
81121
* Determines whether the specified actual resource matches the desired state defined by the
82122
* specified {@link KubernetesDependentResource} based on the observed state of the associated
83123
* specified primary resource.
84124
*
85-
* @param dependentResource the {@link KubernetesDependentResource} implementation used to
86-
* computed the desired state associated with the specified primary resource
125+
* @param dependentResource the {@link KubernetesDependentResource} implementation used to compute
126+
* the desired state associated with the specified primary resource
87127
* @param actualResource the observed dependent resource for which we want to determine whether it
88128
* matches the desired state or not
89129
* @param primary the primary resource from which we want to compute the desired state
90130
* @param context the {@link Context} instance within which this method is called
91-
* @param considerMetadata {@code true} to consider the metadata of the actual resource when
92-
* determining if it matches the desired state, {@code false} if matching should occur only
93-
* considering the spec of the resources
94-
* @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
131+
* @param considerLabelsAndAnnotations {@code true} to consider the metadata of the actual
132+
* resource when determining if it matches the desired state, {@code false} if matching
133+
* should occur only considering the spec of the resources
134+
* @param labelsAndAnnotationsEquality if true labels and annotation match exactly in the actual
135+
* and desired state if false, additional elements are allowed in actual annotations.
136+
* Considered only if considerLabelsAndAnnotations is true.
95137
* @param <R> the type of resource we want to determine whether they match or not
96138
* @param <P> the type of primary resources associated with the secondary resources we want to
97139
* match
98-
* @param strongEquality if the resource should match exactly
140+
* @param ignorePaths are paths in the resource that are ignored on matching (basically an ignore
141+
* list). All changes with a target prefix path on a calculated JSON Patch between actual
142+
* and desired will be ignored. If there are other changes, non-present on ignore list
143+
* match fails.
144+
* @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
99145
*/
100146
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
101147
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
102-
Context<P> context, boolean considerMetadata, boolean strongEquality) {
148+
Context<P> context, boolean considerLabelsAndAnnotations,
149+
boolean labelsAndAnnotationsEquality,
150+
String... ignorePaths) {
151+
final var desired = dependentResource.desired(primary, context);
152+
return match(desired, actualResource, considerLabelsAndAnnotations,
153+
labelsAndAnnotationsEquality, ignorePaths);
154+
}
155+
156+
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
157+
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
158+
Context<P> context, boolean considerLabelsAndAnnotations,
159+
boolean labelsAndAnnotationsEquality,
160+
boolean specEquality) {
161+
final var desired = dependentResource.desired(primary, context);
162+
return match(desired, actualResource, considerLabelsAndAnnotations,
163+
labelsAndAnnotationsEquality, specEquality);
164+
}
165+
166+
@SuppressWarnings("unchecked")
167+
public static <R extends HasMetadata> Result<R> match(R desired, R actualResource,
168+
boolean considerMetadata, boolean labelsAndAnnotationsEquality, boolean specEquality,
169+
String... ignoredPaths) {
170+
final List<String> ignoreList =
171+
ignoredPaths != null && ignoredPaths.length > 0 ? Arrays.asList(ignoredPaths)
172+
: Collections.emptyList();
173+
174+
if (specEquality && !ignoreList.isEmpty()) {
175+
throw new IllegalArgumentException(
176+
"Equality should be false in case of ignore list provided");
177+
}
178+
final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper();
179+
180+
if (considerMetadata) {
181+
Optional<Result<R>> res =
182+
matchMetadata(desired, actualResource, labelsAndAnnotationsEquality, objectMapper);
183+
if (res.isPresent()) {
184+
return res.orElseThrow();
185+
}
186+
}
187+
188+
final ResourceUpdatePreProcessor<R> processor =
189+
GenericResourceUpdatePreProcessor.processorFor((Class<R>) desired.getClass());
190+
final var matched = processor.matches(actualResource, desired, specEquality, ignoredPaths);
191+
return Result.computed(matched, desired);
192+
}
193+
194+
private static <R extends HasMetadata> Optional<Result<R>> matchMetadata(R desired,
195+
R actualResource,
196+
boolean labelsAndAnnotationsEquality,
197+
ObjectMapper objectMapper) {
198+
199+
if (labelsAndAnnotationsEquality) {
200+
final var desiredMetadata = desired.getMetadata();
201+
final var actualMetadata = actualResource.getMetadata();
202+
203+
final var matched =
204+
Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
205+
Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
206+
if (!matched) {
207+
return Optional.of(Result.computed(false, desired));
208+
}
209+
} else {
210+
var desiredNode = objectMapper.valueToTree(desired);
211+
var actualNode = objectMapper.valueToTree(actualResource);
212+
var wholeDiffJsonPatch = JsonDiff.asJson(desiredNode, actualNode);
213+
var metadataJSonDiffs = getDiffsImpactingPathsWithPrefixes(wholeDiffJsonPatch,
214+
METADATA_LABELS,
215+
METADATA_ANNOTATIONS);
216+
if (!allDiffsAreAddOps(metadataJSonDiffs)) {
217+
return Optional.of(Result.computed(false, desired));
218+
}
219+
}
220+
return Optional.empty();
221+
}
222+
223+
static boolean nodeIsChildOf(JsonNode n, List<String> prefixes) {
224+
var path = getPath(n);
225+
return prefixes.stream().anyMatch(path::startsWith);
226+
}
227+
228+
static String getPath(JsonNode n) {
229+
return n.get(PATH).asText();
230+
}
231+
232+
static boolean allDiffsAreAddOps(List<JsonNode> metadataJSonDiffs) {
233+
if (metadataJSonDiffs.isEmpty()) {
234+
return true;
235+
}
236+
return metadataJSonDiffs.stream().allMatch(n -> ADD.equals(n.get(OP).asText()));
237+
}
238+
239+
public static List<JsonNode> getDiffsImpactingPathsWithPrefixes(JsonNode diffJsonPatch,
240+
String... prefixes) {
241+
if (prefixes != null && prefixes.length > 0) {
242+
var res = new ArrayList<JsonNode>();
243+
var prefixList = Arrays.asList(prefixes);
244+
for (int i = 0; i < diffJsonPatch.size(); i++) {
245+
var node = diffJsonPatch.get(i);
246+
if (nodeIsChildOf(node, prefixList)) {
247+
res.add(node);
248+
}
249+
}
250+
return res;
251+
}
252+
return Collections.emptyList();
253+
}
254+
255+
@Deprecated(forRemoval = true)
256+
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
257+
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
258+
Context<P> context, boolean considerLabelsAndAnnotations, boolean specEquality) {
103259
final var desired = dependentResource.desired(primary, context);
104-
return match(desired, actualResource, considerMetadata, strongEquality);
260+
return match(desired, actualResource, considerLabelsAndAnnotations, specEquality);
105261
}
106262

263+
@Deprecated(forRemoval = true)
107264
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
108265
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
109-
Context<P> context, boolean considerMetadata) {
266+
Context<P> context, boolean considerLabelsAndAnnotations, String... ignorePaths) {
110267
final var desired = dependentResource.desired(primary, context);
111-
return match(desired, actualResource, considerMetadata, false);
268+
return match(desired, actualResource, considerLabelsAndAnnotations, true, ignorePaths);
112269
}
270+
113271
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ public Result<R> match(R actualResource, P primary, Context<P> context) {
142142

143143
@SuppressWarnings("unused")
144144
public Result<R> match(R actualResource, R desired, P primary, Context<P> context) {
145-
return GenericKubernetesResourceMatcher.match(desired, actualResource, false);
145+
return GenericKubernetesResourceMatcher.match(desired, actualResource, false,
146+
false, false);
146147
}
147148

148149
protected void handleDelete(P primary, R secondary, Context<P> context) {

0 commit comments

Comments
 (0)