Skip to content

Commit 30bddf1

Browse files
committed
feat: ignore list support for kubernetes dependent matchers
1 parent 0ea7794 commit 30bddf1

File tree

3 files changed

+126
-41
lines changed

3 files changed

+126
-41
lines changed

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

Lines changed: 0 additions & 19 deletions
This file was deleted.

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

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

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

55
import io.fabric8.kubernetes.api.model.ConfigMap;
66
import io.fabric8.kubernetes.api.model.HasMetadata;
77
import io.fabric8.kubernetes.api.model.Secret;
88
import io.fabric8.zjsonpatch.JsonDiff;
9-
import io.javaoperatorsdk.operator.ReconcilerUtils;
109
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
1110
import io.javaoperatorsdk.operator.api.reconciler.Context;
1211
import io.javaoperatorsdk.operator.processing.dependent.Matcher;
1312

13+
import com.fasterxml.jackson.databind.JsonNode;
14+
1415
public class GenericKubernetesResourceMatcher<R extends HasMetadata, P extends HasMetadata>
1516
implements Matcher<R, P> {
1617

@@ -63,17 +64,50 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
6364
*/
6465
public static <R extends HasMetadata> Result<R> match(R desired, R actualResource,
6566
boolean considerMetadata, boolean equality) {
67+
return match(desired, actualResource, considerMetadata, equality, Collections.emptyList());
68+
}
69+
70+
public static <R extends HasMetadata> Result<R> match(R desired, R actualResource,
71+
boolean considerMetadata, String... ignoreList) {
72+
return match(desired, actualResource, considerMetadata, false, Arrays.asList(ignoreList));
73+
}
74+
75+
76+
private static <R extends HasMetadata> Result<R> match(R desired, R actualResource,
77+
boolean considerMetadata, boolean equality, List<String> ignoreList) {
78+
if (equality && !ignoreList.isEmpty()) {
79+
throw new IllegalArgumentException(
80+
"Equality should be false in case of ignore list provided");
81+
}
82+
83+
final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper();
84+
85+
var desiredNode = objectMapper.valueToTree(desired);
86+
var actualNode = objectMapper.valueToTree(actualResource);
87+
var wholeDiffJsonPatch = JsonDiff.asJson(desiredNode, actualNode);
88+
89+
var considerIgnoreList = !equality && !ignoreList.isEmpty();
90+
6691
if (considerMetadata) {
67-
final var desiredMetadata = desired.getMetadata();
68-
final var actualMetadata = actualResource.getMetadata();
69-
final var matched =
70-
Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
71-
Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
72-
if (!matched) {
73-
return Result.computed(false, desired);
92+
if (equality) {
93+
final var desiredMetadata = desired.getMetadata();
94+
final var actualMetadata = actualResource.getMetadata();
95+
96+
final var matched =
97+
Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
98+
Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
99+
if (!matched) {
100+
return Result.computed(false, desired);
101+
}
102+
} else {
103+
var metadataJSonDiffs = getDiffsWithPathSuffix(wholeDiffJsonPatch,
104+
"/metadata/labels",
105+
"/metadata/annotations");
106+
if (!allDiffsAreAddOps(metadataJSonDiffs)) {
107+
return Result.computed(false, desired);
108+
}
74109
}
75110
}
76-
77111
if (desired instanceof ConfigMap) {
78112
return Result.computed(
79113
ResourceComparators.compareConfigMapData((ConfigMap) desired, (ConfigMap) actualResource),
@@ -83,29 +117,60 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
83117
ResourceComparators.compareSecretData((Secret) desired, (Secret) actualResource),
84118
desired);
85119
} else {
86-
final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper();
87-
88120
// reflection will be replaced by this:
89121
// https://github.com/fabric8io/kubernetes-client/issues/3816
90-
var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desired));
91-
var actualSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(actualResource));
92-
var diffJsonPatch = JsonDiff.asJson(desiredSpecNode, actualSpecNode);
122+
var specDiffJsonPatch = getDiffsWithPathSuffix(wholeDiffJsonPatch, "/spec");
93123
// In case of equality is set to true, no diffs are allowed, so we return early if diffs exist
94124
// On contrary (if equality is false), "add" is allowed for cases when for some
95125
// resources Kubernetes fills-in values into spec.
96-
if (equality && diffJsonPatch.size() > 0) {
126+
if (equality && !specDiffJsonPatch.isEmpty()) {
97127
return Result.computed(false, desired);
98128
}
99-
for (int i = 0; i < diffJsonPatch.size(); i++) {
100-
String operation = diffJsonPatch.get(i).get("op").asText();
101-
if (!operation.equals("add")) {
129+
if (considerIgnoreList) {
130+
if (!allDiffsOnIgnoreList(specDiffJsonPatch, ignoreList)) {
131+
return Result.computed(false, desired);
132+
}
133+
} else {
134+
if (!allDiffsAreAddOps(specDiffJsonPatch)) {
102135
return Result.computed(false, desired);
103136
}
104137
}
105138
return Result.computed(true, desired);
106139
}
107140
}
108141

142+
private static boolean allDiffsAreAddOps(List<JsonNode> metadataJSonDiffs) {
143+
if (metadataJSonDiffs.isEmpty()) {
144+
return true;
145+
}
146+
return metadataJSonDiffs.stream().allMatch(n -> "add".equals(n.get("op").asText()));
147+
}
148+
149+
private static boolean allDiffsOnIgnoreList(List<JsonNode> metadataJSonDiffs,
150+
List<String> ignoreList) {
151+
if (metadataJSonDiffs.isEmpty()) {
152+
return true;
153+
}
154+
return metadataJSonDiffs.stream().allMatch(n -> {
155+
var path = n.get("path").asText();
156+
return ignoreList.stream().anyMatch(path::startsWith);
157+
});
158+
}
159+
160+
private static List<JsonNode> getDiffsWithPathSuffix(JsonNode diffJsonPatch,
161+
String... ignorePaths) {
162+
var res = new ArrayList<JsonNode>();
163+
var prefixList = Arrays.asList(ignorePaths);
164+
for (int i = 0; i < diffJsonPatch.size(); i++) {
165+
var node = diffJsonPatch.get(i);
166+
String path = diffJsonPatch.get(i).get("path").asText();
167+
if (prefixList.stream().anyMatch(path::startsWith)) {
168+
res.add(node);
169+
}
170+
}
171+
return res;
172+
}
173+
109174
/**
110175
* Determines whether the specified actual resource matches the desired state defined by the
111176
* specified {@link KubernetesDependentResource} based on the observed state of the associated
@@ -133,6 +198,34 @@ public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
133198
return match(desired, actualResource, considerMetadata, strongEquality);
134199
}
135200

201+
/**
202+
* Determines whether the specified actual resource matches the desired state defined by the
203+
* specified {@link KubernetesDependentResource} based on the observed state of the associated
204+
* specified primary resource.
205+
*
206+
* @param dependentResource the {@link KubernetesDependentResource} implementation used to compute
207+
* the desired state associated with the specified primary resource
208+
* @param actualResource the observed dependent resource for which we want to determine whether it
209+
* matches the desired state or not
210+
* @param primary the primary resource from which we want to compute the desired state
211+
* @param context the {@link Context} instance within which this method is called
212+
* @param considerMetadata {@code true} to consider the metadata of the actual resource when
213+
* determining if it matches the desired state, {@code false} if matching should occur only
214+
* considering the spec of the resources
215+
* @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
216+
* @param <R> the type of resource we want to determine whether they match or not
217+
* @param <P> the type of primary resources associated with the secondary resources we want to
218+
* match
219+
* @param ignorePaths are paths in the resource that are ignored on matching. Anny related change
220+
* on a calculated JSON Patch between actual and desired will be ignored.
221+
*/
222+
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
223+
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
224+
Context<P> context, boolean considerMetadata, String... ignorePaths) {
225+
final var desired = dependentResource.desired(primary, context);
226+
return match(desired, actualResource, considerMetadata, ignorePaths);
227+
}
228+
136229
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
137230
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
138231
Context<P> context, boolean considerMetadata) {

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ void checksIfDesiredValuesAreTheSame() {
3535
final var matcher =
3636
GenericKubernetesResourceMatcher.matcherFor(Deployment.class, dependentResource);
3737
assertThat(matcher.match(actual, null, context).matched()).isTrue();
38-
assertThat(matcher.match(actual, null, context).computedDesired().isPresent()).isTrue();
39-
assertThat(matcher.match(actual, null, context).computedDesired().get()).isEqualTo(desired);
38+
assertThat(matcher.match(actual, null, context).computedDesired()).isPresent();
39+
assertThat(matcher.match(actual, null, context).computedDesired()).contains(desired);
4040

4141
actual.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val");
4242
assertThat(matcher.match(actual, null, context).matched())
@@ -59,6 +59,11 @@ void checksIfDesiredValuesAreTheSame() {
5959
.withFailMessage("Changed values are not ok")
6060
.isFalse();
6161

62+
assertThat(GenericKubernetesResourceMatcher
63+
.match(dependentResource, actual, null, context, false, "/spec/replicas").matched())
64+
.withFailMessage("Ignored paths are not matched")
65+
.isTrue();
66+
6267
actual = new DeploymentBuilder(createDeployment())
6368
.editOrNewMetadata()
6469
.addToAnnotations("test", "value")
@@ -70,9 +75,15 @@ void checksIfDesiredValuesAreTheSame() {
7075
.isTrue();
7176

7277
assertThat(GenericKubernetesResourceMatcher
73-
.match(dependentResource, actual, null, context, true).matched())
78+
.match(dependentResource, actual, null, context, true, true).matched())
7479
.withFailMessage("Annotations should matter when metadata is considered")
7580
.isFalse();
81+
82+
assertThat(GenericKubernetesResourceMatcher
83+
.match(dependentResource, actual, null, context, true, false).matched())
84+
.withFailMessage("Non strong equality on labels and annotations")
85+
.isTrue();
86+
7687
}
7788

7889
Deployment createDeployment() {

0 commit comments

Comments
 (0)