From 2a8b15adbe4a479079101e4ed8a1d9257d79fa69 Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Fri, 11 Apr 2025 15:04:23 -0400 Subject: [PATCH] rough in of tracking things that are defaults and normalizations Signed-off-by: Steve Hawkins --- .../KubernetesDependentResource.java | 20 ++- ...BasedGenericKubernetesResourceMatcher.java | 148 ++++++++++++++++-- 2 files changed, 145 insertions(+), 23 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 382ac7525c..f8e188999f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -69,12 +69,18 @@ public R create(R desired, P primary, Context

context) { } addMetadata(false, null, desired, primary, context); final var resource = prepare(context, desired, primary, "Creating"); - return useSSA(context) - ? resource - .fieldManager(context.getControllerConfiguration().fieldManager()) - .forceConflicts() - .serverSideApply() - : resource.create(); + var result = + useSSA(context) + ? resource + .fieldManager(context.getControllerConfiguration().fieldManager()) + .forceConflicts() + .serverSideApply() + : resource.create(); + if (useSSA(context)) { + SSABasedGenericKubernetesResourceMatcher.getInstance() + .findDefaultsAndNormalizations(result, desired, context); + } + return result; } public R update(R actual, R desired, P primary, Context

context) { @@ -92,6 +98,8 @@ public R update(R actual, R desired, P primary, Context

context) { .fieldManager(context.getControllerConfiguration().fieldManager()) .forceConflicts() .serverSideApply(); + SSABasedGenericKubernetesResourceMatcher.getInstance() + .findDefaultsAndNormalizations(actual, desired, context); } else { var updatedActual = GenericResourceUpdater.updateResource(actual, desired, context); updatedResource = prepare(context, updatedActual, primary, "Updating").update(); 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 eed766fc95..1b0172823e 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 @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -27,6 +28,7 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.LoggingUtils; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import com.github.difflib.DiffUtils; import com.github.difflib.UnifiedDiffUtils; @@ -60,7 +62,13 @@ public class SSABasedGenericKubernetesResourceMatcher { new SSABasedGenericKubernetesResourceMatcher<>(); private static final List IGNORED_METADATA = - List.of("creationTimestamp", "deletionTimestamp", "generation", "selfLink", "uid"); + List.of( + "creationTimestamp", + "deletionTimestamp", + "generation", + "selfLink", + "uid", + "resourceVersion"); @SuppressWarnings("unchecked") public static SSABasedGenericKubernetesResourceMatcher getInstance() { @@ -79,8 +87,126 @@ public static SSABasedGenericKubernetesResourceMatcher desired, Map actual) {} + + record CacheKey(ResourceID id, int hash) {} + + private LinkedHashMap defaultsAndNormalizations = + new LinkedHashMap(); + + public void findDefaultsAndNormalizations(R actual, R desired, Context context) { + SSAState state = getSSAState(actual, desired, context); + + if (state == null) { + // exception? + } + + // minimize by removing eveything that appears in both + minimizeMaps(state.desired, state.actual); + + if (!state.desired.isEmpty() || !state.actual.isEmpty()) { + defaultsAndNormalizations.put( + new CacheKey(ResourceID.fromResource(actual), desired.hashCode()), state); + } + } + + void minimizeMaps(Map actual, Map desired) { + for (Iterator> iter = desired.entrySet().iterator(); + iter.hasNext(); ) { + var entry = iter.next(); + var desiredValue = entry.getValue(); + var actualValue = actual.get(entry.getKey()); + if (Objects.equals(desiredValue, actualValue)) { + iter.remove(); + actual.remove(entry.getKey()); + } else if (desiredValue instanceof Map dm) { + if (actualValue instanceof Map am) { + minimizeMaps(am, dm); + if (am.isEmpty()) { + actual.remove(entry.getKey()); + } + } + if (dm.isEmpty()) { + iter.remove(); + } + } else if (desiredValue instanceof List dl) { + if (actualValue instanceof List al) { + minimizeLists(al, dl); + if ((dl.isEmpty() || dl.stream().allMatch(Objects::isNull)) + && (al.isEmpty() || al.stream().allMatch(Objects::isNull))) { + iter.remove(); + actual.remove(entry.getKey()); + } + } + } + } + } + + void minimizeLists(List actual, List desired) { + for (int i = 0; i < desired.size(); i++) { + Object desiredValue = desired.get(i); + if (actual.size() > i) { + Object actualValue = actual.get(i); + if (Objects.equals(desiredValue, actualValue)) { + actual.set(i, null); + desired.set(i, null); + } else if (desiredValue instanceof Map dm) { + if (actualValue instanceof Map am) { + minimizeMaps(am, dm); + if (am.isEmpty()) { + actual.set(i, null); + } + } + if (dm.isEmpty()) { + desired.set(i, null); + } + } else if (desiredValue instanceof List dl) { + if (actualValue instanceof List al) { + minimizeLists(al, dl); + if ((dl.isEmpty() || dl.stream().allMatch(Objects::isNull)) + && (al.isEmpty() || al.stream().allMatch(Objects::isNull))) { + actual.set(i, null); + desired.set(i, null); + } + } + } + } + } + } + public boolean matches(R actual, R desired, Context context) { + SSAState state = getSSAState(actual, desired, context); + + if (state == null) { + return false; + } + + SSAState overrides = defaultsAndNormalizations.get(new CacheKey(ResourceID.fromResource(actual), desired.hashCode())); + if (overrides != null) { + // if containsAll(overrides.desired, state.desired) && containsAll(overrides.actual, state.actual) + + // + } + + var matches = matches(state.actual(), state.desired(), actual, desired, context); + if (!matches && log.isDebugEnabled() && LoggingUtils.isNotSensitiveResource(desired)) { + var diff = + getDiff( + state.actual(), state.desired(), context.getClient().getKubernetesSerialization()); + 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 matches; + } + + @SuppressWarnings("unchecked") + private SSAState getSSAState(R actual, R desired, Context context) { var optionalManagedFieldsEntry = checkIfFieldManagerExists(actual, context.getControllerConfiguration().fieldManager()); // If no field is managed by our controller, that means the controller hasn't touched the @@ -88,7 +214,7 @@ public boolean matches(R actual, R desired, Context context) { // 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; + return null; } var managedFieldsEntry = optionalManagedFieldsEntry.orElseThrow(); @@ -109,20 +235,8 @@ public boolean matches(R actual, R desired, Context context) { objectMapper); removeIrrelevantValues(desiredMap); - - var matches = matches(prunedActual, desiredMap, actual, desired, context); - 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 matches; + removeIrrelevantValues(actualMap); + return new SSAState(desiredMap, prunedActual); } /**