diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml
index f0b30f33a3..0ead5bab7f 100644
--- a/operator-framework-core/pom.xml
+++ b/operator-framework-core/pom.xml
@@ -14,6 +14,10 @@
Core framework for implementing Kubernetes operators
+
+ io.github.java-diff-utils
+ java-diff-utils
+
io.fabric8
kubernetes-client
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
index bd4a6aa1d3..c65528eb12 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
@@ -5,6 +5,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -25,6 +26,9 @@
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.LoggingUtils;
+import com.github.difflib.DiffUtils;
+import com.github.difflib.UnifiedDiffUtils;
+
/**
* Matches the actual state on the server vs the desired state. Based on the managedFields of SSA.
*
@@ -103,20 +107,66 @@ public boolean matches(R actual, R desired, Context> context) {
removeIrrelevantValues(desiredMap);
- if (LoggingUtils.isNotSensitiveResource(desired)) {
- logDiff(prunedActual, desiredMap, objectMapper);
+ var matches = prunedActual.equals(desiredMap);
+
+ if (!matches && log.isDebugEnabled() && LoggingUtils.isNotSensitiveResource(desired)) {
+ var diff = getDiff(prunedActual, desiredMap, objectMapper);
+ log.debug(
+ "Diff between actual and desired state for resource: {} with name: {} in namespace: {} is: \n{}",
+ actual.getKind(), actual.getMetadata().getName(), actual.getMetadata().getNamespace(),
+ diff);
}
- return prunedActual.equals(desiredMap);
+ return matches;
}
- private void logDiff(Map prunedActualMap, Map desiredMap,
+ private String getDiff(Map prunedActualMap, Map desiredMap,
KubernetesSerialization serialization) {
- if (log.isDebugEnabled()) {
- var actualYaml = serialization.asYaml(prunedActualMap);
- var desiredYaml = serialization.asYaml(desiredMap);
- log.debug("Pruned actual yaml: \n {} \n desired yaml: \n {} ", actualYaml, desiredYaml);
+ var actualYaml = serialization.asYaml(sortMap(prunedActualMap));
+ var desiredYaml = serialization.asYaml(sortMap(desiredMap));
+ if (log.isTraceEnabled()) {
+ log.trace("Pruned actual resource: \n {} \ndesired resource: \n {} ", actualYaml,
+ desiredYaml);
+ }
+
+ var patch = DiffUtils.diff(actualYaml.lines().toList(), desiredYaml.lines().toList());
+ List unifiedDiff =
+ UnifiedDiffUtils.generateUnifiedDiff("", "", actualYaml.lines().toList(), patch, 1);
+ return String.join("\n", unifiedDiff);
+ }
+
+ @SuppressWarnings("unchecked")
+ Map sortMap(Map map) {
+ List sortedKeys = new ArrayList<>(map.keySet());
+ Collections.sort(sortedKeys);
+
+ Map sortedMap = new LinkedHashMap<>();
+ for (String key : sortedKeys) {
+ Object value = map.get(key);
+ if (value instanceof Map) {
+ sortedMap.put(key, sortMap((Map) value));
+ } else if (value instanceof List) {
+ sortedMap.put(key, sortListItems((List
+
+ io.github.java-diff-utils
+ java-diff-utils
+ ${java.diff.version}
+
org.slf4j
slf4j-api