Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generalization of the update matcher #2014

Merged
merged 7 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.HasMetadata;
Expand All @@ -20,8 +17,10 @@ public class GenericKubernetesResourceMatcher<R extends HasMetadata, P extends H
implements Matcher<R, P> {

private static final String SPEC = "/spec";
private static final String METADATA = "/metadata";
private static final String ADD = "add";
private static final String OP = "op";
private static final List<String> IGNORED_FIELDS = List.of("/apiVersion", "/kind", "/status");
public static final String METADATA_LABELS = "/metadata/labels";
public static final String METADATA_ANNOTATIONS = "/metadata/annotations";

Expand Down Expand Up @@ -184,87 +183,39 @@ public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(R d
"Equality should be false in case of ignore list provided");
}

if (considerMetadata) {
Optional<Result<R>> res =
matchMetadata(desired, actualResource, labelsAndAnnotationsEquality, context);
if (res.isPresent()) {
return res.orElseThrow();
}
}

final var matched = matchSpec(actualResource, desired, specEquality, context, ignoredPaths);
return Result.computed(matched, desired);
}

private static <R extends HasMetadata> boolean matchSpec(R actual, R desired, boolean equality,
Context<?> context,
String[] ignoredPaths) {

final var kubernetesSerialization = context.getClient().getKubernetesSerialization();
var desiredNode = kubernetesSerialization.convertValue(desired, JsonNode.class);
var actualNode = kubernetesSerialization.convertValue(actual, JsonNode.class);
var actualNode = kubernetesSerialization.convertValue(actualResource, JsonNode.class);
var wholeDiffJsonPatch = JsonDiff.asJson(desiredNode, actualNode);

final List<String> ignoreList =
ignoredPaths != null && ignoredPaths.length > 0 ? Arrays.asList(ignoredPaths)
: Collections.emptyList();
// reflection will be replaced by this:
// https://github.com/fabric8io/kubernetes-client/issues/3816
var specDiffJsonPatch = getDiffsImpactingPathsWithPrefixes(wholeDiffJsonPatch, SPEC);
// In case of equality is set to true, no diffs are allowed, so we return early if diffs exist
// On contrary (if equality is false), "add" is allowed for cases when for some
// resources Kubernetes fills-in values into spec.
if (equality && !specDiffJsonPatch.isEmpty()) {
return false;
}
if (!equality && !ignoreList.isEmpty()) {
if (!allDiffsOnIgnoreList(specDiffJsonPatch, ignoreList)) {
return false;
}
} else {
if (!allDiffsAreAddOps(specDiffJsonPatch)) {
return false;
boolean matched = true;
for (int i = 0; i < wholeDiffJsonPatch.size() && matched; i++) {
var node = wholeDiffJsonPatch.get(i);
if (nodeIsChildOf(node, List.of(SPEC))) {
matched = match(specEquality, node, ignoreList);
} else if (nodeIsChildOf(node, List.of(METADATA))) {
// conditionally consider labels and annotations
if (considerMetadata
&& nodeIsChildOf(node, List.of(METADATA_LABELS, METADATA_ANNOTATIONS))) {
matched = match(labelsAndAnnotationsEquality, node, Collections.emptyList());
}
} else if (!nodeIsChildOf(node, IGNORED_FIELDS)) {
matched = match(true, node, ignoreList);
}
}
return true;

return Result.computed(matched, desired);
}

private static boolean allDiffsOnIgnoreList(List<JsonNode> metadataJSonDiffs,
List<String> ignoreList) {
if (metadataJSonDiffs.isEmpty()) {
private static boolean match(boolean equality, JsonNode diff,
final List<String> ignoreList) {
if (equality) {
return false;
}
return metadataJSonDiffs.stream().allMatch(n -> nodeIsChildOf(n, ignoreList));
}

private static <R extends HasMetadata, P extends HasMetadata> Optional<Result<R>> matchMetadata(
R desired,
R actualResource,
boolean labelsAndAnnotationsEquality, Context<P> context) {

if (labelsAndAnnotationsEquality) {
final var desiredMetadata = desired.getMetadata();
final var actualMetadata = actualResource.getMetadata();

final var matched =
Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
if (!matched) {
return Optional.of(Result.computed(false, desired));
}
} else {
final var objectMapper = context.getClient().getKubernetesSerialization();
var desiredNode = objectMapper.convertValue(desired, JsonNode.class);
var actualNode = objectMapper.convertValue(actualResource, JsonNode.class);
var wholeDiffJsonPatch = JsonDiff.asJson(desiredNode, actualNode);
var metadataJSonDiffs = getDiffsImpactingPathsWithPrefixes(wholeDiffJsonPatch,
METADATA_LABELS,
METADATA_ANNOTATIONS);
if (!allDiffsAreAddOps(metadataJSonDiffs)) {
return Optional.of(Result.computed(false, desired));
}
if (!ignoreList.isEmpty()) {
return nodeIsChildOf(diff, ignoreList);
}
return Optional.empty();
return ADD.equals(diff.get(OP).asText());
}

static boolean nodeIsChildOf(JsonNode n, List<String> prefixes) {
Expand All @@ -276,29 +227,6 @@ static String getPath(JsonNode n) {
return n.get(PATH).asText();
}

static boolean allDiffsAreAddOps(List<JsonNode> metadataJSonDiffs) {
if (metadataJSonDiffs.isEmpty()) {
return true;
}
return metadataJSonDiffs.stream().allMatch(n -> ADD.equals(n.get(OP).asText()));
}

public static List<JsonNode> getDiffsImpactingPathsWithPrefixes(JsonNode diffJsonPatch,
String... prefixes) {
if (prefixes != null && prefixes.length > 0) {
var res = new ArrayList<JsonNode>();
var prefixList = Arrays.asList(prefixes);
for (int i = 0; i < diffJsonPatch.size(); i++) {
var node = diffJsonPatch.get(i);
if (nodeIsChildOf(node, prefixList)) {
res.add(node);
}
}
return res;
}
return Collections.emptyList();
}

@Deprecated(forRemoval = true)
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,40 @@

import java.util.Map;

import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSlice;
import io.fabric8.kubernetes.api.model.rbac.ClusterRole;
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding;
import io.fabric8.kubernetes.api.model.rbac.Role;
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
import io.javaoperatorsdk.operator.ReconcilerUtils;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceUpdaterMatcher;

public class GenericResourceUpdaterMatcher<R extends HasMetadata> implements
ResourceUpdaterMatcher<R> {

private static final String METADATA = "metadata";
private static final ResourceUpdaterMatcher<?> INSTANCE = new GenericResourceUpdaterMatcher<>();

@SuppressWarnings("rawtypes")
private static final Map<Class, ResourceUpdaterMatcher> processors = Map.of(
Secret.class, new SecretResourceUpdaterMatcher(),
ConfigMap.class, new ConfigMapResourceUpdaterMatcher(),
ServiceAccount.class, new ServiceAccountResourceUpdaterMatcher(),
Role.class, new RoleResourceUpdaterMatcher(),
ClusterRole.class, new ClusterRoleResourceUpdaterMatcher(),
RoleBinding.class, new RoleBindingResourceUpdaterMatcher(),
ClusterRoleBinding.class, new ClusterRoleBindingResourceUpdaterMatcher(),
Endpoints.class, new EndpointsResourceUpdaterMatcher(),
EndpointSlice.class, new EndpointSliceResourceUpdateMatcher());

protected GenericResourceUpdaterMatcher() {}

@SuppressWarnings("unchecked")
public static <R extends HasMetadata> ResourceUpdaterMatcher<R> updaterMatcherFor(
Class<R> resourceType) {
final var processor = processors.get(resourceType);
return processor != null ? processor : (ResourceUpdaterMatcher<R>) INSTANCE;
return (ResourceUpdaterMatcher<R>) INSTANCE;
}

@SuppressWarnings("unchecked")
@Override
public R updateResource(R actual, R desired, Context<?> context) {
var clonedActual = context.getControllerConfiguration().getConfigurationService()
.getResourceCloner().clone(actual);
KubernetesSerialization kubernetesSerialization =
context.getClient().getKubernetesSerialization();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is opening potential issues where the cloning occurs in a different manner than here if, for some reason, the configured resource cloner has a different serialization set up… Should we remove the cloner altogether?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this was the only instance of the cloner being used, it could be removed or deprecated. There is also a clone method on KubernetesSerialization.

Map<String, Object> actualMap = kubernetesSerialization.convertValue(actual, Map.class);
shawkins marked this conversation as resolved.
Show resolved Hide resolved
Map<String, Object> desiredMap = kubernetesSerialization.convertValue(desired, Map.class);
// replace all top level fields from actual with desired, but merge metadata separately
var metadata = actualMap.remove(METADATA);
actualMap.replaceAll((k, v) -> desiredMap.get(k));
actualMap.putAll(desiredMap);
actualMap.put(METADATA, metadata);
var clonedActual = (R) kubernetesSerialization.convertValue(actualMap, desired.getClass());
updateLabelsAndAnnotation(clonedActual, desired);
updateClonedActual(clonedActual, desired);
return clonedActual;
}

Expand All @@ -53,15 +45,6 @@ public boolean matches(R actual, R desired, Context<?> context) {
false, false, context).matched();
}

protected void updateClonedActual(R actual, R desired) {
updateSpec(actual, desired);
}

public static <K extends HasMetadata> void updateSpec(K actual, K desired) {
var desiredSpec = ReconcilerUtils.getSpec(desired);
ReconcilerUtils.setSpec(actual, desiredSpec);
}

public static <K extends HasMetadata> void updateLabelsAndAnnotation(K actual, K desired) {
actual.getMetadata().getLabels().putAll(desired.getMetadata().getLabels());
actual.getMetadata().getAnnotations().putAll(desired.getMetadata().getAnnotations());
Expand Down

This file was deleted.

Loading
Loading