diff --git a/cm.yaml b/cm.yaml
new file mode 100644
index 0000000000..9e9bab6716
--- /dev/null
+++ b/cm.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: test1
+ namespace: default
+ ownerReferences:
+ - apiVersion: v1
+ kind: ConfigMap
+ name: kube-root-ca.crt
+ uid: 1ef74cb4-dbbd-45ef-9caf-aa76186594ea
+data:
+ key1: "val1"
+# key2: "val2"
+
diff --git a/cm2.yaml b/cm2.yaml
new file mode 100644
index 0000000000..3c7c14c111
--- /dev/null
+++ b/cm2.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: test1
+ namespace: default
+data:
+ key3: "val3"
+
+
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
index bd939d4b32..d92294d048 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
@@ -33,6 +33,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
+import static io.javaoperatorsdk.operator.api.config.ControllerConfiguration.CONTROLLER_NAME_AS_FIELD_MANAGER;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET;
public class BaseConfigurationService extends AbstractConfigurationService {
@@ -135,6 +136,10 @@ protected
ControllerConfiguration
configFor(Reconcile
timeUnit = reconciliationInterval.timeUnit();
}
+ final var dependentFieldManager =
+ annotation.fieldManager().equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name
+ : annotation.fieldManager();
+
final var config = new ResolvedControllerConfiguration
(
resourceClass, name, generationAware,
associatedReconcilerClass, retry, rateLimiter,
@@ -152,7 +157,8 @@ protected
ControllerConfiguration
configFor(Reconcile
io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::labelSelector,
Constants.NO_VALUE_SET),
null,
- Utils.instantiate(annotation.itemStore(), ItemStore.class, context), this);
+ Utils.instantiate(annotation.itemStore(), ItemStore.class, context), dependentFieldManager,
+ this);
ResourceEventFilter
answer = deprecatedEventFilter(annotation);
config.setEventFilter(answer != null ? answer : ResourceEventFilters.passthrough());
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
index 52d78fb1e9..1ab358155e 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
@@ -264,4 +264,28 @@ static ConfigurationService newOverriddenConfigurationService(
default ExecutorServiceManager getExecutorServiceManager() {
return new ExecutorServiceManager(this);
}
+
+ /**
+ * Allows to revert to the 4.3 behavior when it comes to creating or updating Kubernetes Dependent
+ * Resources when set to {@code false}. The default approach how these resources are
+ * created/updated was change to use
+ * Server-Side
+ * Apply (SSA) by default. Note that the legacy approach, and this setting, might be removed
+ * in the future.
+ */
+ default boolean ssaBasedCreateUpdateForDependentResources() {
+ return true;
+ }
+
+ /**
+ * Allows to revert to the 4.3 generic matching algorithm for Kubernetes Dependent Resources when
+ * set to {@code false}. Version 4.4 introduced a new generic matching algorithm for Kubernetes
+ * Dependent Resources which is quite complex. As a consequence, we introduced this setting to
+ * allow folks to revert to the previous matching algorithm if needed. Note, however, that the
+ * legacy algorithm, and this setting, might be removed in the future.
+ */
+ default boolean ssaBasedDefaultMatchingForDependentResources() {
+ return true;
+ }
+
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
index 8d9f4200bc..b2b4b93b12 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
@@ -32,6 +32,8 @@ public class ConfigurationServiceOverrider {
private Boolean stopOnInformerErrorDuringStartup;
private Duration cacheSyncTimeout;
private ResourceClassResolver resourceClassResolver;
+ private Boolean ssaBasedCreateUpdateForDependentResources;
+ private Boolean ssaBasedDefaultMatchingForDependentResources;
ConfigurationServiceOverrider(ConfigurationService original) {
this.original = original;
@@ -139,6 +141,18 @@ public ConfigurationServiceOverrider withResourceClassResolver(
return this;
}
+ public ConfigurationServiceOverrider withSSABasedCreateUpdateForDependentResources(
+ boolean value) {
+ this.ssaBasedCreateUpdateForDependentResources = value;
+ return this;
+ }
+
+ public ConfigurationServiceOverrider withSSABasedDefaultMatchingForDependentResources(
+ boolean value) {
+ this.ssaBasedDefaultMatchingForDependentResources = value;
+ return this;
+ }
+
public ConfigurationService build() {
return new BaseConfigurationService(original.getVersion(), cloner, objectMapper) {
@Override
@@ -248,6 +262,20 @@ public ResourceClassResolver getResourceClassResolver() {
return resourceClassResolver != null ? resourceClassResolver
: super.getResourceClassResolver();
}
+
+ @Override
+ public boolean ssaBasedCreateUpdateForDependentResources() {
+ return ssaBasedCreateUpdateForDependentResources != null
+ ? ssaBasedCreateUpdateForDependentResources
+ : super.ssaBasedCreateUpdateForDependentResources();
+ }
+
+ @Override
+ public boolean ssaBasedDefaultMatchingForDependentResources() {
+ return ssaBasedDefaultMatchingForDependentResources != null
+ ? ssaBasedDefaultMatchingForDependentResources
+ : super.ssaBasedDefaultMatchingForDependentResources();
+ }
};
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java
index 60a053d0a8..13ddd995ad 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java
@@ -22,6 +22,10 @@ public interface ControllerConfiguration
extends Resource
@SuppressWarnings("rawtypes")
RateLimiter DEFAULT_RATE_LIMITER = LinearRateLimiter.deactivatedRateLimiter();
+ /**
+ * Will use the controller name as fieldManager if set.
+ */
+ String CONTROLLER_NAME_AS_FIELD_MANAGER = "use_controller_name";
default String getName() {
return ensureValidName(null, getAssociatedReconcilerClassName());
@@ -124,4 +128,16 @@ default Class
getResourceClass() {
default Set getEffectiveNamespaces() {
return ResourceConfiguration.super.getEffectiveNamespaces(getConfigurationService());
}
+
+ /**
+ * Retrieves the name used to assign as field manager for
+ * Server-Side
+ * Apply (SSA) operations. If unset, the sanitized controller name will be used.
+ *
+ * @return the name used as field manager for SSA operations
+ */
+ default String fieldManager() {
+ return getName();
+ }
+
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java
index 5d96c2abf6..3b2dcd8289 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java
@@ -39,6 +39,7 @@ public class ControllerConfigurationOverrider {
private Map configurations;
private ItemStore itemStore;
private String name;
+ private String fieldManager;
private ControllerConfigurationOverrider(ControllerConfiguration original) {
this.finalizer = original.getFinalizerName();
@@ -54,6 +55,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) {
this.original = original;
this.rateLimiter = original.getRateLimiter();
this.name = original.getName();
+ this.fieldManager = original.fieldManager();
}
public ControllerConfigurationOverrider withFinalizer(String finalizer) {
@@ -168,6 +170,12 @@ public ControllerConfigurationOverrider withName(String name) {
return this;
}
+ public ControllerConfigurationOverrider withFieldManager(
+ String dependentFieldManager) {
+ this.fieldManager = dependentFieldManager;
+ return this;
+ }
+
public ControllerConfigurationOverrider replacingNamedDependentResourceConfig(String name,
Object dependentResourceConfig) {
@@ -190,7 +198,7 @@ public ControllerConfiguration build() {
generationAware, original.getAssociatedReconcilerClassName(), retry, rateLimiter,
reconciliationMaxInterval, onAddFilter, onUpdateFilter, genericFilter,
original.getDependentResources(),
- namespaces, finalizer, labelSelector, configurations, itemStore,
+ namespaces, finalizer, labelSelector, configurations, itemStore, fieldManager,
original.getConfigurationService());
overridden.setEventFilter(customResourcePredicate);
return overridden;
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java
index f91e9c506a..0b8b7306f8 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java
@@ -32,6 +32,7 @@ public class ResolvedControllerConfiguration
private final Map configurations;
private final ItemStore itemStore;
private final ConfigurationService configurationService;
+ private final String fieldManager;
private ResourceEventFilter
eventFilter;
private List dependentResources;
@@ -44,7 +45,8 @@ public ResolvedControllerConfiguration(Class resourceClass, ControllerConfigu
other.genericFilter().orElse(null),
other.getDependentResources(), other.getNamespaces(),
other.getFinalizerName(), other.getLabelSelector(), Collections.emptyMap(),
- other.getItemStore().orElse(null), other.getConfigurationService());
+ other.getItemStore().orElse(null), other.fieldManager(),
+ other.getConfigurationService());
}
public static Duration getMaxReconciliationInterval(long interval, TimeUnit timeUnit) {
@@ -72,10 +74,12 @@ public ResolvedControllerConfiguration(Class
resourceClass, String name,
List dependentResources,
Set namespaces, String finalizer, String labelSelector,
Map configurations, ItemStore itemStore,
+ String fieldManager,
ConfigurationService configurationService) {
this(resourceClass, name, generationAware, associatedReconcilerClassName, retry, rateLimiter,
maxReconciliationInterval, onAddFilter, onUpdateFilter, genericFilter,
- namespaces, finalizer, labelSelector, configurations, itemStore, configurationService);
+ namespaces, finalizer, labelSelector, configurations, itemStore, fieldManager,
+ configurationService);
setDependentResources(dependentResources);
}
@@ -85,6 +89,7 @@ protected ResolvedControllerConfiguration(Class
resourceClass, String name,
OnAddFilter
onAddFilter, OnUpdateFilter
onUpdateFilter, GenericFilter
genericFilter,
Set namespaces, String finalizer, String labelSelector,
Map configurations, ItemStore itemStore,
+ String fieldManager,
ConfigurationService configurationService) {
super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter,
itemStore);
@@ -99,13 +104,14 @@ protected ResolvedControllerConfiguration(Class
resourceClass, String name,
this.itemStore = itemStore;
this.finalizer =
ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName());
+ this.fieldManager = fieldManager;
}
protected ResolvedControllerConfiguration(Class
resourceClass, String name,
Class extends Reconciler> reconcilerClas, ConfigurationService configurationService) {
this(resourceClass, name, false, getAssociatedReconcilerClassName(reconcilerClas), null, null,
null, null, null, null, null,
- null, null, null, null, configurationService);
+ null, null, null, null, null, configurationService);
}
@Override
@@ -182,4 +188,9 @@ public Object getConfigurationFor(DependentResourceSpec spec) {
public Optional> getItemStore() {
return Optional.ofNullable(itemStore);
}
+
+ @Override
+ public String fieldManager() {
+ return fieldManager;
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java
index e2c0de896b..dcd5484b0a 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java
@@ -17,6 +17,8 @@
import io.javaoperatorsdk.operator.processing.retry.GenericRetry;
import io.javaoperatorsdk.operator.processing.retry.Retry;
+import static io.javaoperatorsdk.operator.api.config.ControllerConfiguration.CONTROLLER_NAME_AS_FIELD_MANAGER;
+
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@@ -47,7 +49,7 @@
* Specified which namespaces this Controller monitors for custom resources events. If no
* namespace is specified then the controller will monitor all namespaces by default.
*
- * @return the list of namespaces this controller monitors
+ * @return the array of namespaces this controller monitors
*/
String[] namespaces() default Constants.WATCH_ALL_NAMESPACES;
@@ -108,7 +110,7 @@ MaxReconciliationInterval maxReconciliationInterval() default @MaxReconciliation
* Optional list of {@link Dependent} configurations which associate a resource type to a
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} implementation
*
- * @return the list of {@link Dependent} configurations
+ * @return the array of {@link Dependent} configurations
*/
Dependent[] dependents() default {};
@@ -129,4 +131,13 @@ MaxReconciliationInterval maxReconciliationInterval() default @MaxReconciliation
Class extends RateLimiter> rateLimiter() default LinearRateLimiter.class;
Class extends ItemStore> itemStore() default ItemStore.class;
+
+ /**
+ * Retrieves the name used to assign as field manager for
+ * Server-Side
+ * Apply (SSA) operations. If unset, the sanitized controller name will be used.
+ *
+ * @return the name used as field manager for SSA operations
+ */
+ String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java
index 0cee7c2413..f0ca32bd98 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java
@@ -19,8 +19,8 @@ public class GenericKubernetesResourceMatcher kubernetesDependentResourceConfig;
+
@SuppressWarnings("unchecked")
public KubernetesDependentResource(Class resourceType) {
super(resourceType);
@@ -128,16 +129,41 @@ protected R handleUpdate(R actual, R desired, P primary, Context context) {
@SuppressWarnings("unused")
public R create(R target, P primary, Context
context) {
- return prepare(target, primary, "Creating").create();
+ if (!context.getControllerConfiguration().getConfigurationService()
+ .ssaBasedCreateUpdateForDependentResources()) {
+ return prepare(target, primary, "Creating").create();
+ } else {
+ return prepare(target, primary, "Creating")
+ .fieldManager(context.getControllerConfiguration().fieldManager())
+ .forceConflicts()
+ .serverSideApply();
+ }
}
public R update(R actual, R target, P primary, Context
context) {
- var updatedActual = processor.replaceSpecOnActual(actual, target, context);
- return prepare(updatedActual, primary, "Updating").replace();
+ if (!context.getControllerConfiguration().getConfigurationService()
+ .ssaBasedCreateUpdateForDependentResources()) {
+ var updatedActual = processor.replaceSpecOnActual(actual, target, context);
+ return prepare(updatedActual, primary, "Updating").replace();
+ } else {
+ target.getMetadata().setResourceVersion(actual.getMetadata().getResourceVersion());
+ return prepare(target, primary, "Updating")
+ .fieldManager(context.getControllerConfiguration().fieldManager())
+ .forceConflicts().serverSideApply();
+ }
}
public Result match(R actualResource, P primary, Context context) {
- return GenericKubernetesResourceMatcher.match(this, actualResource, primary, context, false);
+ if (!context.getControllerConfiguration().getConfigurationService()
+ .ssaBasedDefaultMatchingForDependentResources()) {
+ return GenericKubernetesResourceMatcher.match(this, actualResource, primary, context, false);
+ } else {
+ final var desired = desired(primary, context);
+ addReferenceHandlingMetadata(desired, primary);
+ var matches = SSABasedGenericKubernetesResourceMatcher.getInstance().matches(actualResource,
+ desired, context);
+ return Result.computed(matches, desired);
+ }
}
@SuppressWarnings("unused")
@@ -164,11 +190,7 @@ protected Resource prepare(R desired, P primary, String actionName) {
desired.getClass(),
ResourceID.fromResource(desired));
- if (addOwnerReference()) {
- desired.addOwnerReference(primary);
- } else if (useDefaultAnnotationsToIdentifyPrimary()) {
- addDefaultSecondaryToPrimaryMapperAnnotations(desired, primary);
- }
+ addReferenceHandlingMetadata(desired, primary);
if (desired instanceof Namespaced) {
return client.resource(desired).inNamespace(desired.getMetadata().getNamespace());
@@ -177,6 +199,14 @@ protected Resource prepare(R desired, P primary, String actionName) {
}
}
+ protected void addReferenceHandlingMetadata(R desired, P primary) {
+ if (addOwnerReference()) {
+ desired.addOwnerReference(primary);
+ } else if (useDefaultAnnotationsToIdentifyPrimary()) {
+ addDefaultSecondaryToPrimaryMapperAnnotations(desired, primary);
+ }
+ }
+
@Override
@SuppressWarnings("unchecked")
protected InformerEventSource createEventSource(EventSourceContext context) {
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
new file mode 100644
index 0000000000..50158cad18
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
@@ -0,0 +1,359 @@
+package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ManagedFieldsEntry;
+import io.javaoperatorsdk.operator.OperatorException;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Matches the actual state on the server vs the desired state. Based on the managedFields of SSA.
+ *
+ *
+ * The basis of algorithm is to extract the fields managed we convert resources to Map/List
+ * composition. The actual resource (from the server) is pruned, all the fields which are not
+ * mentioed in managedFields of the target manager is removed. Some irrelevant fields are also
+ * removed from desired. And the two resulted Maps are compared for equality. The implementation is
+ * a bit nasty since have to deal with some specific cases of managedFields format.
+ *
+ *
+ * @param matched resource type
+ */
+// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#fieldsv1-v1-meta
+// https://github.com/kubernetes-sigs/structured-merge-diff
+// https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-field-management.html
+// see also: https://kubernetes.slack.com/archives/C0123CNN8F3/p1686141087220719
+public class SSABasedGenericKubernetesResourceMatcher {
+
+ @SuppressWarnings("rawtypes")
+ private static final SSABasedGenericKubernetesResourceMatcher INSTANCE =
+ new SSABasedGenericKubernetesResourceMatcher<>();
+ public static final String APPLY_OPERATION = "Apply";
+ public static final String DOT_KEY = ".";
+
+ @SuppressWarnings("unchecked")
+ public static SSABasedGenericKubernetesResourceMatcher getInstance() {
+ return INSTANCE;
+ }
+
+ private static final TypeReference> typeRef = new TypeReference<>() {};
+
+ private static final String F_PREFIX = "f:";
+ private static final String K_PREFIX = "k:";
+ private static final String V_PREFIX = "v:";
+ private static final String METADATA_KEY = "metadata";
+ private static final String NAME_KEY = "name";
+ private static final String NAMESPACE_KEY = "namespace";
+ private static final String KIND_KEY = "kind";
+ private static final String API_VERSION_KEY = "apiVersion";
+
+ private static final Logger log =
+ LoggerFactory.getLogger(SSABasedGenericKubernetesResourceMatcher.class);
+
+
+ public boolean matches(R actual, R desired, Context> context) {
+ try {
+ var optionalManagedFieldsEntry =
+ checkIfFieldManagerExists(actual, context.getControllerConfiguration().fieldManager());
+ // If no field is managed by our controller, that means the controller hasn't touched the
+ // resource yet and the resource probably doesn't match the desired state. Not matching here
+ // means that the resource will need to be updated and since this will be done using SSA, the
+ // fields our controller cares about will become managed by it
+ if (optionalManagedFieldsEntry.isEmpty()) {
+ return false;
+ }
+
+ var managedFieldsEntry = optionalManagedFieldsEntry.orElseThrow();
+
+ var objectMapper =
+ context.getControllerConfiguration().getConfigurationService().getObjectMapper();
+
+ var actualMap = objectMapper.convertValue(actual, typeRef);
+ var desiredMap = objectMapper.convertValue(desired, typeRef);
+
+ log.trace("Original actual: \n {} \n original desired: \n {} ", actual, desiredMap);
+
+ var prunedActual = new HashMap(actualMap.size());
+ keepOnlyManagedFields(prunedActual, actualMap,
+ managedFieldsEntry.getFieldsV1().getAdditionalProperties(), objectMapper);
+
+ removeIrrelevantValues(desiredMap);
+
+ log.debug("Pruned actual: \n {} \n desired: \n {} ", prunedActual, desiredMap);
+
+ return prunedActual.equals(desiredMap);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void removeIrrelevantValues(Map desiredMap) {
+ var metadata = (Map) desiredMap.get(METADATA_KEY);
+ metadata.remove(NAME_KEY);
+ metadata.remove(NAMESPACE_KEY);
+ if (metadata.isEmpty()) {
+ desiredMap.remove(METADATA_KEY);
+ }
+ desiredMap.remove(KIND_KEY);
+ desiredMap.remove(API_VERSION_KEY);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void keepOnlyManagedFields(Map result,
+ Map actualMap,
+ Map managedFields, ObjectMapper objectMapper) throws JsonProcessingException {
+
+ if (managedFields.isEmpty()) {
+ result.putAll(actualMap);
+ return;
+ }
+ for (Map.Entry entry : managedFields.entrySet()) {
+ String key = entry.getKey();
+ if (key.startsWith(F_PREFIX)) {
+ String keyInActual = keyWithoutPrefix(key);
+ var managedFieldValue = (Map) entry.getValue();
+ if (isNestedValue(managedFieldValue)) {
+ var managedEntrySet = managedFieldValue.entrySet();
+
+ // two special cases "k:" and "v:" prefixes
+ if (isListKeyEntrySet(managedEntrySet)) {
+ handleListKeyEntrySet(result, actualMap, objectMapper, keyInActual, managedEntrySet);
+ } else if (isSetValueField(managedEntrySet)) {
+ handleSetValues(result, actualMap, objectMapper, keyInActual, managedEntrySet);
+ } else {
+ // basically if we should traverse further
+ fillResultsAndTraverseFurther(result, actualMap, managedFields, objectMapper, key,
+ keyInActual, managedFieldValue);
+ }
+ } else {
+ // this should handle the case when the value is complex in the actual map (not just a
+ // simple value).
+ result.put(keyInActual, actualMap.get(keyInActual));
+ }
+ } else {
+ // .:{} is ignored, other should not be present
+ if (!DOT_KEY.equals(key)) {
+ throw new IllegalStateException("Key: " + key + " has no prefix: " + F_PREFIX);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void fillResultsAndTraverseFurther(Map result,
+ Map actualMap, Map managedFields, ObjectMapper objectMapper,
+ String key, String keyInActual, Object managedFieldValue) throws JsonProcessingException {
+ var emptyMapValue = new HashMap();
+ result.put(keyInActual, emptyMapValue);
+ var actualMapValue = actualMap.get(keyInActual);
+ log.debug("key: {} actual map value: {} managedFieldValue: {}", keyInActual,
+ actualMapValue, managedFieldValue);
+
+ keepOnlyManagedFields(emptyMapValue, (Map) actualMapValue,
+ (Map) managedFields.get(key), objectMapper);
+ }
+
+ private static boolean isNestedValue(Map, ?> managedFieldValue) {
+ return !managedFieldValue.isEmpty();
+ }
+
+ /**
+ * List entries referenced by key, or when "k:" prefix is used. It works in a way that it selects
+ * the target element based on the field(s) in "k:" for example when there is a list of element of
+ * owner references, the uid can serve as a key for a list element:
+ * "k:{"uid":"1ef74cb4-dbbd-45ef-9caf-aa76186594ea"}". It selects the element and recursively
+ * processes it. Note that in these lists the order matters and seems that if there are more keys
+ * ("k:"), the ordering of those in the managed fields are not the same as the value order. So
+ * this also explicitly orders the result based on the value order in the resource not the key
+ * order in managed field.
+ */
+ @SuppressWarnings("unchecked")
+ private static void handleListKeyEntrySet(Map result,
+ Map actualMap,
+ ObjectMapper objectMapper, String keyInActual,
+ Set> managedEntrySet) {
+ var valueList = new ArrayList<>();
+ result.put(keyInActual, valueList);
+ var actualValueList = (List