From 8819654ae7a454f7b6489e6cacbcfff9b4635337 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Fri, 22 Mar 2024 07:59:23 -0400 Subject: [PATCH 1/3] fix: ignore server managed fields for SSA matching closes: #2290 Signed-off-by: Steven Hawkins --- .../kubernetes/SSABasedGenericKubernetesResourceMatcher.java | 4 ++++ .../deployment-with-managed-fields-additional-controller.yaml | 2 +- .../processing/dependent/kubernetes/nginx-deployment.yaml | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) 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 d699ff2822..eac85f0ec0 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 @@ -41,6 +41,9 @@ public class SSABasedGenericKubernetesResourceMatcher { public static final String APPLY_OPERATION = "Apply"; public static final String DOT_KEY = "."; + private static final List IGNORED_METADATA = Arrays.asList("creationTimestamp", "deletionTimestamp", "generation", + "selfLink", "uid"); + @SuppressWarnings("unchecked") public static SSABasedGenericKubernetesResourceMatcher getInstance() { return INSTANCE; @@ -131,6 +134,7 @@ private static void removeIrrelevantValues(Map desiredMap) { var metadata = (Map) desiredMap.get(METADATA_KEY); metadata.remove(NAME_KEY); metadata.remove(NAMESPACE_KEY); + IGNORED_METADATA.forEach(metadata::remove); if (metadata.isEmpty()) { desiredMap.remove(METADATA_KEY); } diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml index 5e0be88433..d82b5c8933 100644 --- a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml @@ -4,7 +4,7 @@ metadata: annotations: deployment.kubernetes.io/revision: "1" creationTimestamp: "2023-06-01T08:43:47Z" - generation: 1 + generation: 2 managedFields: - apiVersion: apps/v1 fieldsType: FieldsV1 diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml index dcf90a8fc7..5478ac1747 100644 --- a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml @@ -2,6 +2,7 @@ apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: "test" + generation: 1 spec: progressDeadlineSeconds: 600 revisionHistoryLimit: 10 From 32f6ce629a0f9b6b93c4bebca598a7a6dfd87afb Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Fri, 22 Mar 2024 08:24:30 -0400 Subject: [PATCH 2/3] formatting Signed-off-by: Steven Hawkins --- ...BasedGenericKubernetesResourceMatcher.java | 577 +++++++++--------- 1 file changed, 277 insertions(+), 300 deletions(-) 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 eac85f0ec0..10bd0c1337 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 @@ -20,14 +20,14 @@ * 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. + * 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 + * @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 @@ -35,327 +35,304 @@ // 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 = "."; - - private static final List IGNORED_METADATA = Arrays.asList("creationTimestamp", "deletionTimestamp", "generation", - "selfLink", "uid"); - - @SuppressWarnings("unchecked") - public static SSABasedGenericKubernetesResourceMatcher getInstance() { - return INSTANCE; - } - - 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); - - - @SuppressWarnings("unchecked") - public boolean matches(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 - // 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; + @SuppressWarnings("rawtypes") + private static final SSABasedGenericKubernetesResourceMatcher INSTANCE = new SSABasedGenericKubernetesResourceMatcher<>(); + public static final String APPLY_OPERATION = "Apply"; + public static final String DOT_KEY = "."; + + private static final List IGNORED_METADATA = Arrays.asList("creationTimestamp", "deletionTimestamp", + "generation", "selfLink", "uid"); + + @SuppressWarnings("unchecked") + public static SSABasedGenericKubernetesResourceMatcher getInstance() { + return INSTANCE; } - var managedFieldsEntry = optionalManagedFieldsEntry.orElseThrow(); + 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); + + @SuppressWarnings("unchecked") + public boolean matches(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 + // 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 objectMapper = context.getClient().getKubernetesSerialization(); + var managedFieldsEntry = optionalManagedFieldsEntry.orElseThrow(); - var actualMap = objectMapper.convertValue(actual, Map.class); + var objectMapper = context.getClient().getKubernetesSerialization(); - sanitizeState(actual, desired, actualMap); + var actualMap = objectMapper.convertValue(actual, Map.class); - var desiredMap = objectMapper.convertValue(desired, Map.class); - if (LoggingUtils.isNotSensitiveResource(desired)) { - log.trace("Original actual: \n {} \n original desired: \n {} ", actual, desiredMap); - } + sanitizeState(actual, desired, actualMap); - var prunedActual = new HashMap(actualMap.size()); - keepOnlyManagedFields(prunedActual, actualMap, - managedFieldsEntry.getFieldsV1().getAdditionalProperties(), objectMapper); + var desiredMap = objectMapper.convertValue(desired, Map.class); + if (LoggingUtils.isNotSensitiveResource(desired)) { + log.trace("Original actual: \n {} \n original desired: \n {} ", actual, desiredMap); + } - removeIrrelevantValues(desiredMap); + var prunedActual = new HashMap(actualMap.size()); + keepOnlyManagedFields(prunedActual, actualMap, managedFieldsEntry.getFieldsV1().getAdditionalProperties(), + objectMapper); - if (LoggingUtils.isNotSensitiveResource(desired)) { - log.debug("Pruned actual: \n {} \n desired: \n {} ", prunedActual, desiredMap); - } + removeIrrelevantValues(desiredMap); - return prunedActual.equals(desiredMap); - } - - /** - * Correct for known issue with SSA - */ - @SuppressWarnings("unchecked") - private void sanitizeState(R actual, R desired, Map actualMap) { - if (desired instanceof StatefulSet) { - StatefulSet desiredStatefulSet = (StatefulSet) desired; - StatefulSet actualStatefulSet = (StatefulSet) actual; - int claims = desiredStatefulSet.getSpec().getVolumeClaimTemplates().size(); - if (claims == actualStatefulSet.getSpec().getVolumeClaimTemplates().size()) { - for (int i = 0; i < claims; i++) { - if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getSpec() - .getVolumeMode() == null) { - Optional - .ofNullable(GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", - i, "spec")) - .map(Map.class::cast).ifPresent(m -> m.remove("volumeMode")); - } - if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getStatus() == null) { - Optional - .ofNullable( - GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i)) - .map(Map.class::cast).ifPresent(m -> m.remove("status")); - } + if (LoggingUtils.isNotSensitiveResource(desired)) { + log.debug("Pruned actual: \n {} \n desired: \n {} ", prunedActual, desiredMap); } - } + + return prunedActual.equals(desiredMap); } - } - - @SuppressWarnings("unchecked") - private static void removeIrrelevantValues(Map desiredMap) { - var metadata = (Map) desiredMap.get(METADATA_KEY); - metadata.remove(NAME_KEY); - metadata.remove(NAMESPACE_KEY); - IGNORED_METADATA.forEach(metadata::remove); - if (metadata.isEmpty()) { - desiredMap.remove(METADATA_KEY); + + /** + * Correct for known issue with SSA + */ + @SuppressWarnings("unchecked") + private void sanitizeState(R actual, R desired, Map actualMap) { + if (desired instanceof StatefulSet) { + StatefulSet desiredStatefulSet = (StatefulSet) desired; + StatefulSet actualStatefulSet = (StatefulSet) actual; + int claims = desiredStatefulSet.getSpec().getVolumeClaimTemplates().size(); + if (claims == actualStatefulSet.getSpec().getVolumeClaimTemplates().size()) { + for (int i = 0; i < claims; i++) { + if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getSpec() + .getVolumeMode() == null) { + Optional.ofNullable( + GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i, "spec")) + .map(Map.class::cast).ifPresent(m -> m.remove("volumeMode")); + } + if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getStatus() == null) { + Optional.ofNullable(GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i)) + .map(Map.class::cast).ifPresent(m -> m.remove("status")); + } + } + } + } } - desiredMap.remove(KIND_KEY); - desiredMap.remove(API_VERSION_KEY); - } - - @SuppressWarnings("unchecked") - private static void keepOnlyManagedFields(Map result, - Map actualMap, - Map managedFields, KubernetesSerialization objectMapper) { - - if (managedFields.isEmpty()) { - result.putAll(actualMap); - return; + + @SuppressWarnings("unchecked") + private static void removeIrrelevantValues(Map desiredMap) { + var metadata = (Map) desiredMap.get(METADATA_KEY); + metadata.remove(NAME_KEY); + metadata.remove(NAMESPACE_KEY); + IGNORED_METADATA.forEach(metadata::remove); + if (metadata.isEmpty()) { + desiredMap.remove(METADATA_KEY); + } + desiredMap.remove(KIND_KEY); + desiredMap.remove(API_VERSION_KEY); } - 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)); + + @SuppressWarnings("unchecked") + private static void keepOnlyManagedFields(Map result, Map actualMap, + Map managedFields, KubernetesSerialization objectMapper) { + + if (managedFields.isEmpty()) { + result.putAll(actualMap); + return; } - } else { - // .:{} is ignored, other should not be present - if (!DOT_KEY.equals(key)) { - throw new IllegalStateException("Key: " + key + " has no prefix: " + F_PREFIX); + 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, - KubernetesSerialization objectMapper, String key, String keyInActual, - Object managedFieldValue) { - var emptyMapValue = new HashMap(); - result.put(keyInActual, emptyMapValue); - var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap()); - log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, 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, - KubernetesSerialization objectMapper, String keyInActual, - Set> managedEntrySet) { - var valueList = new ArrayList<>(); - result.put(keyInActual, valueList); - var actualValueList = (List>) actualMap.get(keyInActual); - - SortedMap> targetValuesByIndex = new TreeMap<>(); - Map> managedEntryByIndex = new HashMap<>(); - - for (Map.Entry listEntry : managedEntrySet) { - if (DOT_KEY.equals(listEntry.getKey())) { - continue; - } - var actualListEntry = selectListEntryBasedOnKey(keyWithoutPrefix(listEntry.getKey()), - actualValueList, objectMapper); - targetValuesByIndex.put(actualListEntry.getKey(), actualListEntry.getValue()); - managedEntryByIndex.put(actualListEntry.getKey(), (Map) listEntry.getValue()); + + @SuppressWarnings("unchecked") + private static void fillResultsAndTraverseFurther(Map result, Map actualMap, + Map managedFields, KubernetesSerialization objectMapper, String key, String keyInActual, + Object managedFieldValue) { + var emptyMapValue = new HashMap(); + result.put(keyInActual, emptyMapValue); + var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap()); + log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, managedFieldValue); + + keepOnlyManagedFields(emptyMapValue, (Map) actualMapValue, + (Map) managedFields.get(key), objectMapper); + } + + private static boolean isNestedValue(Map managedFieldValue) { + return !managedFieldValue.isEmpty(); } - targetValuesByIndex.forEach((key, value) -> { - var emptyResMapValue = new HashMap(); - valueList.add(emptyResMapValue); - keepOnlyManagedFields(emptyResMapValue, value, managedEntryByIndex.get(key), objectMapper); - }); - } - - /** - * Set values, the "v:" prefix. Form in managed fields: "f:some-set":{"v:1":{}},"v:2":{},"v:3":{}} - * Note that this should be just used in very rare cases, actually was not able to produce a - * sample. Kubernetes developers who worked on this feature were not able to provide one either - * when prompted. Basically this method just adds the values from {@code "v:"} to the - * result. - */ - @SuppressWarnings("rawtypes") - private static void handleSetValues(Map result, Map actualMap, - KubernetesSerialization objectMapper, String keyInActual, - Set> managedEntrySet) { - var valueList = new ArrayList<>(); - result.put(keyInActual, valueList); - for (Map.Entry valueEntry : managedEntrySet) { - // not clear if this can happen - if (DOT_KEY.equals(valueEntry.getKey())) { - continue; - } - Class targetClass = null; - List values = (List) actualMap.get(keyInActual); - if (!(values.get(0) instanceof Map)) { - targetClass = values.get(0).getClass(); - } - - var value = parseKeyValue(keyWithoutPrefix(valueEntry.getKey()), targetClass, objectMapper); - valueList.add(value); + /** + * 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, + KubernetesSerialization objectMapper, String keyInActual, Set> managedEntrySet) { + var valueList = new ArrayList<>(); + result.put(keyInActual, valueList); + var actualValueList = (List>) actualMap.get(keyInActual); + + SortedMap> targetValuesByIndex = new TreeMap<>(); + Map> managedEntryByIndex = new HashMap<>(); + + for (Map.Entry listEntry : managedEntrySet) { + if (DOT_KEY.equals(listEntry.getKey())) { + continue; + } + var actualListEntry = selectListEntryBasedOnKey(keyWithoutPrefix(listEntry.getKey()), actualValueList, + objectMapper); + targetValuesByIndex.put(actualListEntry.getKey(), actualListEntry.getValue()); + managedEntryByIndex.put(actualListEntry.getKey(), (Map) listEntry.getValue()); + } + + targetValuesByIndex.forEach((key, value) -> { + var emptyResMapValue = new HashMap(); + valueList.add(emptyResMapValue); + keepOnlyManagedFields(emptyResMapValue, value, managedEntryByIndex.get(key), objectMapper); + }); } - } - - public static Object parseKeyValue(String stringValue, Class targetClass, - KubernetesSerialization objectMapper) { - stringValue = stringValue.trim(); - if (targetClass != null) { - return objectMapper.unmarshal(stringValue, targetClass); - } else { - return objectMapper.unmarshal(stringValue, Map.class); + + /** + * Set values, the "v:" prefix. Form in managed fields: "f:some-set":{"v:1":{}},"v:2":{},"v:3":{}} Note that this + * should be just used in very rare cases, actually was not able to produce a sample. Kubernetes developers who + * worked on this feature were not able to provide one either when prompted. Basically this method just adds the + * values from {@code "v:"} to the result. + */ + @SuppressWarnings("rawtypes") + private static void handleSetValues(Map result, Map actualMap, + KubernetesSerialization objectMapper, String keyInActual, Set> managedEntrySet) { + var valueList = new ArrayList<>(); + result.put(keyInActual, valueList); + for (Map.Entry valueEntry : managedEntrySet) { + // not clear if this can happen + if (DOT_KEY.equals(valueEntry.getKey())) { + continue; + } + Class targetClass = null; + List values = (List) actualMap.get(keyInActual); + if (!(values.get(0) instanceof Map)) { + targetClass = values.get(0).getClass(); + } + + var value = parseKeyValue(keyWithoutPrefix(valueEntry.getKey()), targetClass, objectMapper); + valueList.add(value); + } } - } - - private static boolean isSetValueField(Set> managedEntrySet) { - return isKeyPrefixedSkippingDotKey(managedEntrySet, V_PREFIX); - } - - private static boolean isListKeyEntrySet(Set> managedEntrySet) { - return isKeyPrefixedSkippingDotKey(managedEntrySet, K_PREFIX); - } - - /** - * Sometimes (not always) the first subfield of a managed field ("f:") is ".:{}", it looks that - * those are added when there are more subfields of a referenced field. See test samples. Does not - * seem to provide additional functionality, so can be just skipped for now. - */ - private static boolean isKeyPrefixedSkippingDotKey(Set> managedEntrySet, - String prefix) { - var iterator = managedEntrySet.iterator(); - var managedFieldEntry = iterator.next(); - if (managedFieldEntry.getKey().equals(DOT_KEY)) { - managedFieldEntry = iterator.next(); + + public static Object parseKeyValue(String stringValue, Class targetClass, KubernetesSerialization objectMapper) { + stringValue = stringValue.trim(); + if (targetClass != null) { + return objectMapper.unmarshal(stringValue, targetClass); + } else { + return objectMapper.unmarshal(stringValue, Map.class); + } } - return managedFieldEntry.getKey().startsWith(prefix); - } - - @SuppressWarnings("unchecked") - private static java.util.Map.Entry> selectListEntryBasedOnKey( - String key, - List> values, - KubernetesSerialization objectMapper) { - Map ids = objectMapper.unmarshal(key, Map.class); - List> possibleTargets = new ArrayList<>(1); - int index = -1; - for (int i = 0; i < values.size(); i++) { - var v = values.get(i); - if (v.entrySet().containsAll(ids.entrySet())) { - possibleTargets.add(v); - index = i; - } + + private static boolean isSetValueField(Set> managedEntrySet) { + return isKeyPrefixedSkippingDotKey(managedEntrySet, V_PREFIX); } - if (possibleTargets.isEmpty()) { - throw new IllegalStateException( - "Cannot find list element for key:" + key + ", in map: " - + values.stream().map(Map::keySet).collect(Collectors.toList())); + + private static boolean isListKeyEntrySet(Set> managedEntrySet) { + return isKeyPrefixedSkippingDotKey(managedEntrySet, K_PREFIX); } - if (possibleTargets.size() > 1) { - throw new IllegalStateException( - "More targets found in list element for key:" + key + ", in map: " - + values.stream().map(Map::keySet).collect(Collectors.toList())); + + /** + * Sometimes (not always) the first subfield of a managed field ("f:") is ".:{}", it looks that those are added when + * there are more subfields of a referenced field. See test samples. Does not seem to provide additional + * functionality, so can be just skipped for now. + */ + private static boolean isKeyPrefixedSkippingDotKey(Set> managedEntrySet, String prefix) { + var iterator = managedEntrySet.iterator(); + var managedFieldEntry = iterator.next(); + if (managedFieldEntry.getKey().equals(DOT_KEY)) { + managedFieldEntry = iterator.next(); + } + return managedFieldEntry.getKey().startsWith(prefix); } - final var finalIndex = index; - return new AbstractMap.SimpleEntry<>(finalIndex, possibleTargets.get(0)); - } - - - private Optional checkIfFieldManagerExists(R actual, String fieldManager) { - var targetManagedFields = actual.getMetadata().getManagedFields().stream() - // Only the apply operations are interesting for us since those were created properly be SSA - // Patch. An update can be present with same fieldManager when migrating and having the same - // field manager name. - .filter( - f -> f.getManager().equals(fieldManager) && f.getOperation().equals(APPLY_OPERATION)) - .collect(Collectors.toList()); - if (targetManagedFields.isEmpty()) { - log.debug("No field manager exists for resource {} with name: {} and operation Apply ", - actual.getKind(), actual.getMetadata().getName()); - return Optional.empty(); + + @SuppressWarnings("unchecked") + private static java.util.Map.Entry> selectListEntryBasedOnKey(String key, + List> values, KubernetesSerialization objectMapper) { + Map ids = objectMapper.unmarshal(key, Map.class); + List> possibleTargets = new ArrayList<>(1); + int index = -1; + for (int i = 0; i < values.size(); i++) { + var v = values.get(i); + if (v.entrySet().containsAll(ids.entrySet())) { + possibleTargets.add(v); + index = i; + } + } + if (possibleTargets.isEmpty()) { + throw new IllegalStateException("Cannot find list element for key:" + key + ", in map: " + + values.stream().map(Map::keySet).collect(Collectors.toList())); + } + if (possibleTargets.size() > 1) { + throw new IllegalStateException("More targets found in list element for key:" + key + ", in map: " + + values.stream().map(Map::keySet).collect(Collectors.toList())); + } + final var finalIndex = index; + return new AbstractMap.SimpleEntry<>(finalIndex, possibleTargets.get(0)); } - // this should not happen in theory - if (targetManagedFields.size() > 1) { - throw new OperatorException( - "More than one field manager exists with name: " + fieldManager + "in resource: " + - actual.getKind() + " with name: " + actual.getMetadata().getName()); + + private Optional checkIfFieldManagerExists(R actual, String fieldManager) { + var targetManagedFields = actual.getMetadata().getManagedFields().stream() + // Only the apply operations are interesting for us since those were created properly be SSA + // Patch. An update can be present with same fieldManager when migrating and having the same + // field manager name. + .filter(f -> f.getManager().equals(fieldManager) && f.getOperation().equals(APPLY_OPERATION)) + .collect(Collectors.toList()); + if (targetManagedFields.isEmpty()) { + log.debug("No field manager exists for resource {} with name: {} and operation Apply ", actual.getKind(), + actual.getMetadata().getName()); + return Optional.empty(); + } + // this should not happen in theory + if (targetManagedFields.size() > 1) { + throw new OperatorException("More than one field manager exists with name: " + fieldManager + + "in resource: " + actual.getKind() + " with name: " + actual.getMetadata().getName()); + } + return Optional.of(targetManagedFields.get(0)); } - return Optional.of(targetManagedFields.get(0)); - } - private static String keyWithoutPrefix(String key) { - return key.substring(2); - } + private static String keyWithoutPrefix(String key) { + return key.substring(2); + } } From e14db05a6e91fa687b06955d8e8dc01c240a81ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 25 Mar 2024 08:55:35 +0100 Subject: [PATCH 3/3] format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- ...BasedGenericKubernetesResourceMatcher.java | 577 +++++++++--------- 1 file changed, 300 insertions(+), 277 deletions(-) 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 10bd0c1337..bcfaa52d1a 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 @@ -20,14 +20,14 @@ * 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. + * 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 + * @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 @@ -35,304 +35,327 @@ // 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 = "."; - - private static final List IGNORED_METADATA = Arrays.asList("creationTimestamp", "deletionTimestamp", - "generation", "selfLink", "uid"); - - @SuppressWarnings("unchecked") - public static SSABasedGenericKubernetesResourceMatcher getInstance() { - return INSTANCE; + @SuppressWarnings("rawtypes") + private static final SSABasedGenericKubernetesResourceMatcher INSTANCE = + new SSABasedGenericKubernetesResourceMatcher<>(); + public static final String APPLY_OPERATION = "Apply"; + public static final String DOT_KEY = "."; + + private static final List IGNORED_METADATA = + Arrays.asList("creationTimestamp", "deletionTimestamp", + "generation", "selfLink", "uid"); + + @SuppressWarnings("unchecked") + public static SSABasedGenericKubernetesResourceMatcher getInstance() { + return INSTANCE; + } + + 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); + + @SuppressWarnings("unchecked") + public boolean matches(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 + // 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; } - 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); - - @SuppressWarnings("unchecked") - public boolean matches(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 - // 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 managedFieldsEntry = optionalManagedFieldsEntry.orElseThrow(); + var objectMapper = context.getClient().getKubernetesSerialization(); - var objectMapper = context.getClient().getKubernetesSerialization(); + var actualMap = objectMapper.convertValue(actual, Map.class); - var actualMap = objectMapper.convertValue(actual, Map.class); + sanitizeState(actual, desired, actualMap); - sanitizeState(actual, desired, actualMap); - - var desiredMap = objectMapper.convertValue(desired, Map.class); - if (LoggingUtils.isNotSensitiveResource(desired)) { - log.trace("Original actual: \n {} \n original desired: \n {} ", actual, desiredMap); - } - - var prunedActual = new HashMap(actualMap.size()); - keepOnlyManagedFields(prunedActual, actualMap, managedFieldsEntry.getFieldsV1().getAdditionalProperties(), - objectMapper); + var desiredMap = objectMapper.convertValue(desired, Map.class); + if (LoggingUtils.isNotSensitiveResource(desired)) { + log.trace("Original actual: \n {} \n original desired: \n {} ", actual, desiredMap); + } - removeIrrelevantValues(desiredMap); + var prunedActual = new HashMap(actualMap.size()); + keepOnlyManagedFields(prunedActual, actualMap, + managedFieldsEntry.getFieldsV1().getAdditionalProperties(), + objectMapper); - if (LoggingUtils.isNotSensitiveResource(desired)) { - log.debug("Pruned actual: \n {} \n desired: \n {} ", prunedActual, desiredMap); - } + removeIrrelevantValues(desiredMap); - return prunedActual.equals(desiredMap); + if (LoggingUtils.isNotSensitiveResource(desired)) { + log.debug("Pruned actual: \n {} \n desired: \n {} ", prunedActual, desiredMap); } - /** - * Correct for known issue with SSA - */ - @SuppressWarnings("unchecked") - private void sanitizeState(R actual, R desired, Map actualMap) { - if (desired instanceof StatefulSet) { - StatefulSet desiredStatefulSet = (StatefulSet) desired; - StatefulSet actualStatefulSet = (StatefulSet) actual; - int claims = desiredStatefulSet.getSpec().getVolumeClaimTemplates().size(); - if (claims == actualStatefulSet.getSpec().getVolumeClaimTemplates().size()) { - for (int i = 0; i < claims; i++) { - if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getSpec() - .getVolumeMode() == null) { - Optional.ofNullable( - GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i, "spec")) - .map(Map.class::cast).ifPresent(m -> m.remove("volumeMode")); - } - if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getStatus() == null) { - Optional.ofNullable(GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i)) - .map(Map.class::cast).ifPresent(m -> m.remove("status")); - } - } - } + return prunedActual.equals(desiredMap); + } + + /** + * Correct for known issue with SSA + */ + @SuppressWarnings("unchecked") + private void sanitizeState(R actual, R desired, Map actualMap) { + if (desired instanceof StatefulSet) { + StatefulSet desiredStatefulSet = (StatefulSet) desired; + StatefulSet actualStatefulSet = (StatefulSet) actual; + int claims = desiredStatefulSet.getSpec().getVolumeClaimTemplates().size(); + if (claims == actualStatefulSet.getSpec().getVolumeClaimTemplates().size()) { + for (int i = 0; i < claims; i++) { + if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getSpec() + .getVolumeMode() == null) { + Optional.ofNullable( + GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i, "spec")) + .map(Map.class::cast).ifPresent(m -> m.remove("volumeMode")); + } + if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getStatus() == null) { + Optional + .ofNullable( + GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i)) + .map(Map.class::cast).ifPresent(m -> m.remove("status")); + } } + } } - - @SuppressWarnings("unchecked") - private static void removeIrrelevantValues(Map desiredMap) { - var metadata = (Map) desiredMap.get(METADATA_KEY); - metadata.remove(NAME_KEY); - metadata.remove(NAMESPACE_KEY); - IGNORED_METADATA.forEach(metadata::remove); - if (metadata.isEmpty()) { - desiredMap.remove(METADATA_KEY); - } - desiredMap.remove(KIND_KEY); - desiredMap.remove(API_VERSION_KEY); + } + + @SuppressWarnings("unchecked") + private static void removeIrrelevantValues(Map desiredMap) { + var metadata = (Map) desiredMap.get(METADATA_KEY); + metadata.remove(NAME_KEY); + metadata.remove(NAMESPACE_KEY); + IGNORED_METADATA.forEach(metadata::remove); + if (metadata.isEmpty()) { + desiredMap.remove(METADATA_KEY); } - - @SuppressWarnings("unchecked") - private static void keepOnlyManagedFields(Map result, Map actualMap, - Map managedFields, KubernetesSerialization objectMapper) { - - if (managedFields.isEmpty()) { - result.putAll(actualMap); - return; + desiredMap.remove(KIND_KEY); + desiredMap.remove(API_VERSION_KEY); + } + + @SuppressWarnings("unchecked") + private static void keepOnlyManagedFields(Map result, + Map actualMap, + Map managedFields, KubernetesSerialization objectMapper) { + + 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)); } - 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); - } - } + } 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, KubernetesSerialization objectMapper, String key, String keyInActual, - Object managedFieldValue) { - var emptyMapValue = new HashMap(); - result.put(keyInActual, emptyMapValue); - var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap()); - log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, managedFieldValue); - - keepOnlyManagedFields(emptyMapValue, (Map) actualMapValue, - (Map) managedFields.get(key), objectMapper); - } - - private static boolean isNestedValue(Map managedFieldValue) { - return !managedFieldValue.isEmpty(); + } + + @SuppressWarnings("unchecked") + private static void fillResultsAndTraverseFurther(Map result, + Map actualMap, + Map managedFields, KubernetesSerialization objectMapper, String key, + String keyInActual, + Object managedFieldValue) { + var emptyMapValue = new HashMap(); + result.put(keyInActual, emptyMapValue); + var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap()); + log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, 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, + KubernetesSerialization objectMapper, String keyInActual, + Set> managedEntrySet) { + var valueList = new ArrayList<>(); + result.put(keyInActual, valueList); + var actualValueList = (List>) actualMap.get(keyInActual); + + SortedMap> targetValuesByIndex = new TreeMap<>(); + Map> managedEntryByIndex = new HashMap<>(); + + for (Map.Entry listEntry : managedEntrySet) { + if (DOT_KEY.equals(listEntry.getKey())) { + continue; + } + var actualListEntry = + selectListEntryBasedOnKey(keyWithoutPrefix(listEntry.getKey()), actualValueList, + objectMapper); + targetValuesByIndex.put(actualListEntry.getKey(), actualListEntry.getValue()); + managedEntryByIndex.put(actualListEntry.getKey(), (Map) listEntry.getValue()); } - /** - * 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, - KubernetesSerialization objectMapper, String keyInActual, Set> managedEntrySet) { - var valueList = new ArrayList<>(); - result.put(keyInActual, valueList); - var actualValueList = (List>) actualMap.get(keyInActual); - - SortedMap> targetValuesByIndex = new TreeMap<>(); - Map> managedEntryByIndex = new HashMap<>(); - - for (Map.Entry listEntry : managedEntrySet) { - if (DOT_KEY.equals(listEntry.getKey())) { - continue; - } - var actualListEntry = selectListEntryBasedOnKey(keyWithoutPrefix(listEntry.getKey()), actualValueList, - objectMapper); - targetValuesByIndex.put(actualListEntry.getKey(), actualListEntry.getValue()); - managedEntryByIndex.put(actualListEntry.getKey(), (Map) listEntry.getValue()); - } - - targetValuesByIndex.forEach((key, value) -> { - var emptyResMapValue = new HashMap(); - valueList.add(emptyResMapValue); - keepOnlyManagedFields(emptyResMapValue, value, managedEntryByIndex.get(key), objectMapper); - }); + targetValuesByIndex.forEach((key, value) -> { + var emptyResMapValue = new HashMap(); + valueList.add(emptyResMapValue); + keepOnlyManagedFields(emptyResMapValue, value, managedEntryByIndex.get(key), objectMapper); + }); + } + + /** + * Set values, the "v:" prefix. Form in managed fields: "f:some-set":{"v:1":{}},"v:2":{},"v:3":{}} + * Note that this should be just used in very rare cases, actually was not able to produce a + * sample. Kubernetes developers who worked on this feature were not able to provide one either + * when prompted. Basically this method just adds the values from {@code "v:"} to the + * result. + */ + @SuppressWarnings("rawtypes") + private static void handleSetValues(Map result, Map actualMap, + KubernetesSerialization objectMapper, String keyInActual, + Set> managedEntrySet) { + var valueList = new ArrayList<>(); + result.put(keyInActual, valueList); + for (Map.Entry valueEntry : managedEntrySet) { + // not clear if this can happen + if (DOT_KEY.equals(valueEntry.getKey())) { + continue; + } + Class targetClass = null; + List values = (List) actualMap.get(keyInActual); + if (!(values.get(0) instanceof Map)) { + targetClass = values.get(0).getClass(); + } + + var value = parseKeyValue(keyWithoutPrefix(valueEntry.getKey()), targetClass, objectMapper); + valueList.add(value); } - - /** - * Set values, the "v:" prefix. Form in managed fields: "f:some-set":{"v:1":{}},"v:2":{},"v:3":{}} Note that this - * should be just used in very rare cases, actually was not able to produce a sample. Kubernetes developers who - * worked on this feature were not able to provide one either when prompted. Basically this method just adds the - * values from {@code "v:"} to the result. - */ - @SuppressWarnings("rawtypes") - private static void handleSetValues(Map result, Map actualMap, - KubernetesSerialization objectMapper, String keyInActual, Set> managedEntrySet) { - var valueList = new ArrayList<>(); - result.put(keyInActual, valueList); - for (Map.Entry valueEntry : managedEntrySet) { - // not clear if this can happen - if (DOT_KEY.equals(valueEntry.getKey())) { - continue; - } - Class targetClass = null; - List values = (List) actualMap.get(keyInActual); - if (!(values.get(0) instanceof Map)) { - targetClass = values.get(0).getClass(); - } - - var value = parseKeyValue(keyWithoutPrefix(valueEntry.getKey()), targetClass, objectMapper); - valueList.add(value); - } + } + + public static Object parseKeyValue(String stringValue, Class targetClass, + KubernetesSerialization objectMapper) { + stringValue = stringValue.trim(); + if (targetClass != null) { + return objectMapper.unmarshal(stringValue, targetClass); + } else { + return objectMapper.unmarshal(stringValue, Map.class); } - - public static Object parseKeyValue(String stringValue, Class targetClass, KubernetesSerialization objectMapper) { - stringValue = stringValue.trim(); - if (targetClass != null) { - return objectMapper.unmarshal(stringValue, targetClass); - } else { - return objectMapper.unmarshal(stringValue, Map.class); - } + } + + private static boolean isSetValueField(Set> managedEntrySet) { + return isKeyPrefixedSkippingDotKey(managedEntrySet, V_PREFIX); + } + + private static boolean isListKeyEntrySet(Set> managedEntrySet) { + return isKeyPrefixedSkippingDotKey(managedEntrySet, K_PREFIX); + } + + /** + * Sometimes (not always) the first subfield of a managed field ("f:") is ".:{}", it looks that + * those are added when there are more subfields of a referenced field. See test samples. Does not + * seem to provide additional functionality, so can be just skipped for now. + */ + private static boolean isKeyPrefixedSkippingDotKey(Set> managedEntrySet, + String prefix) { + var iterator = managedEntrySet.iterator(); + var managedFieldEntry = iterator.next(); + if (managedFieldEntry.getKey().equals(DOT_KEY)) { + managedFieldEntry = iterator.next(); } - - private static boolean isSetValueField(Set> managedEntrySet) { - return isKeyPrefixedSkippingDotKey(managedEntrySet, V_PREFIX); + return managedFieldEntry.getKey().startsWith(prefix); + } + + @SuppressWarnings("unchecked") + private static java.util.Map.Entry> selectListEntryBasedOnKey( + String key, + List> values, KubernetesSerialization objectMapper) { + Map ids = objectMapper.unmarshal(key, Map.class); + List> possibleTargets = new ArrayList<>(1); + int index = -1; + for (int i = 0; i < values.size(); i++) { + var v = values.get(i); + if (v.entrySet().containsAll(ids.entrySet())) { + possibleTargets.add(v); + index = i; + } } - - private static boolean isListKeyEntrySet(Set> managedEntrySet) { - return isKeyPrefixedSkippingDotKey(managedEntrySet, K_PREFIX); + if (possibleTargets.isEmpty()) { + throw new IllegalStateException("Cannot find list element for key:" + key + ", in map: " + + values.stream().map(Map::keySet).collect(Collectors.toList())); } - - /** - * Sometimes (not always) the first subfield of a managed field ("f:") is ".:{}", it looks that those are added when - * there are more subfields of a referenced field. See test samples. Does not seem to provide additional - * functionality, so can be just skipped for now. - */ - private static boolean isKeyPrefixedSkippingDotKey(Set> managedEntrySet, String prefix) { - var iterator = managedEntrySet.iterator(); - var managedFieldEntry = iterator.next(); - if (managedFieldEntry.getKey().equals(DOT_KEY)) { - managedFieldEntry = iterator.next(); - } - return managedFieldEntry.getKey().startsWith(prefix); + if (possibleTargets.size() > 1) { + throw new IllegalStateException( + "More targets found in list element for key:" + key + ", in map: " + + values.stream().map(Map::keySet).collect(Collectors.toList())); } - - @SuppressWarnings("unchecked") - private static java.util.Map.Entry> selectListEntryBasedOnKey(String key, - List> values, KubernetesSerialization objectMapper) { - Map ids = objectMapper.unmarshal(key, Map.class); - List> possibleTargets = new ArrayList<>(1); - int index = -1; - for (int i = 0; i < values.size(); i++) { - var v = values.get(i); - if (v.entrySet().containsAll(ids.entrySet())) { - possibleTargets.add(v); - index = i; - } - } - if (possibleTargets.isEmpty()) { - throw new IllegalStateException("Cannot find list element for key:" + key + ", in map: " - + values.stream().map(Map::keySet).collect(Collectors.toList())); - } - if (possibleTargets.size() > 1) { - throw new IllegalStateException("More targets found in list element for key:" + key + ", in map: " - + values.stream().map(Map::keySet).collect(Collectors.toList())); - } - final var finalIndex = index; - return new AbstractMap.SimpleEntry<>(finalIndex, possibleTargets.get(0)); + final var finalIndex = index; + return new AbstractMap.SimpleEntry<>(finalIndex, possibleTargets.get(0)); + } + + private Optional checkIfFieldManagerExists(R actual, String fieldManager) { + var targetManagedFields = actual.getMetadata().getManagedFields().stream() + // Only the apply operations are interesting for us since those were created properly be SSA + // Patch. An update can be present with same fieldManager when migrating and having the same + // field manager name. + .filter( + f -> f.getManager().equals(fieldManager) && f.getOperation().equals(APPLY_OPERATION)) + .collect(Collectors.toList()); + if (targetManagedFields.isEmpty()) { + log.debug("No field manager exists for resource {} with name: {} and operation Apply ", + actual.getKind(), + actual.getMetadata().getName()); + return Optional.empty(); } - - private Optional checkIfFieldManagerExists(R actual, String fieldManager) { - var targetManagedFields = actual.getMetadata().getManagedFields().stream() - // Only the apply operations are interesting for us since those were created properly be SSA - // Patch. An update can be present with same fieldManager when migrating and having the same - // field manager name. - .filter(f -> f.getManager().equals(fieldManager) && f.getOperation().equals(APPLY_OPERATION)) - .collect(Collectors.toList()); - if (targetManagedFields.isEmpty()) { - log.debug("No field manager exists for resource {} with name: {} and operation Apply ", actual.getKind(), - actual.getMetadata().getName()); - return Optional.empty(); - } - // this should not happen in theory - if (targetManagedFields.size() > 1) { - throw new OperatorException("More than one field manager exists with name: " + fieldManager - + "in resource: " + actual.getKind() + " with name: " + actual.getMetadata().getName()); - } - return Optional.of(targetManagedFields.get(0)); + // this should not happen in theory + if (targetManagedFields.size() > 1) { + throw new OperatorException("More than one field manager exists with name: " + fieldManager + + "in resource: " + actual.getKind() + " with name: " + actual.getMetadata().getName()); } + return Optional.of(targetManagedFields.get(0)); + } - private static String keyWithoutPrefix(String key) { - return key.substring(2); - } + private static String keyWithoutPrefix(String key) { + return key.substring(2); + } }