Skip to content

Commit 528410e

Browse files
committed
various refinements to ssa logic
- localizes sanitation to ssa matcher - further ensures values won't be logged - will detect if a non-meaningful revision has been created Signed-off-by: Steve Hawkins <[email protected]>
1 parent 2827206 commit 528410e

File tree

4 files changed

+138
-77
lines changed

4 files changed

+138
-77
lines changed

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredResourceSanitizer.java

-47
This file was deleted.

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java

+13-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.fabric8.kubernetes.client.KubernetesClient;
1212
import io.fabric8.kubernetes.client.dsl.Resource;
1313
import io.javaoperatorsdk.operator.OperatorException;
14+
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
1415
import io.javaoperatorsdk.operator.api.config.dependent.Configured;
1516
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
1617
import io.javaoperatorsdk.operator.api.reconciler.Constants;
@@ -114,7 +115,6 @@ public R create(R desired, P primary, Context<P> context) {
114115
}
115116
}
116117
addMetadata(false, null, desired, primary);
117-
sanitizeDesired(desired, null, primary, context);
118118
final var resource = prepare(desired, primary, "Creating");
119119
return useSSA(context)
120120
? resource
@@ -131,7 +131,6 @@ public R update(R actual, R desired, P primary, Context<P> context) {
131131
}
132132
R updatedResource;
133133
addMetadata(false, actual, desired, primary);
134-
sanitizeDesired(desired, actual, primary, context);
135134
if (useSSA(context)) {
136135
updatedResource = prepare(desired, primary, "Updating")
137136
.fieldManager(context.getControllerConfiguration().fieldManager())
@@ -148,7 +147,6 @@ public R update(R actual, R desired, P primary, Context<P> context) {
148147
@Override
149148
public Result<R> match(R actualResource, P primary, Context<P> context) {
150149
final var desired = desired(primary, context);
151-
sanitizeDesired(desired, actualResource, primary, context);
152150
return match(actualResource, desired, primary, updaterMatcher, context);
153151
}
154152

@@ -192,19 +190,22 @@ protected void addMetadata(boolean forMatch, R actualResource, final R target, P
192190
addReferenceHandlingMetadata(target, primary);
193191
}
194192

195-
protected void sanitizeDesired(R desired, R actual, P primary, Context<P> context) {
196-
DesiredResourceSanitizer.sanitizeDesired(desired, actual, primary, context, useSSA(context));
193+
protected boolean useSSA(Context<P> context) {
194+
return useSSA(context.getControllerConfiguration());
197195
}
198196

199-
protected boolean useSSA(Context<P> context) {
197+
protected boolean useSSA(ControllerConfiguration<P> controllerConfiguration) {
198+
if (this instanceof ResourceUpdaterMatcher) {
199+
return false;
200+
}
200201
Optional<Boolean> useSSAConfig =
201202
configuration().flatMap(KubernetesDependentResourceConfig::useSSA);
202-
var configService = context.getControllerConfiguration().getConfigurationService();
203+
var configService = controllerConfiguration.getConfigurationService();
203204
// don't use SSA for certain resources by default, only if explicitly overriden
204205
if (useSSAConfig.isEmpty() && configService.defaultNonSSAResource().contains(resourceType())) {
205206
return false;
206207
}
207-
return useSSAConfig.orElse(context.getControllerConfiguration().getConfigurationService()
208+
return useSSAConfig.orElse(controllerConfiguration.getConfigurationService()
208209
.ssaBasedCreateUpdateMatchForDependentResources());
209210
}
210211

@@ -261,6 +262,10 @@ protected InformerEventSource<R, P> createEventSource(EventSourceContext<P> cont
261262
"Using default configuration for {} KubernetesDependentResource, call configureWith to provide configuration",
262263
resourceType().getSimpleName());
263264
}
265+
if (useSSA(context.getControllerConfiguration())) {
266+
onUpdateFilter = new MetadataAwareOnUpdateFilter<>(onUpdateFilter,
267+
context.getClient().getKubernetesSerialization());
268+
}
264269
return eventSource().orElseThrow();
265270
}
266271

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2021 Red Hat, Inc. and/or its affiliates and other contributors as indicated by
3+
* the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License. You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
16+
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import io.fabric8.kubernetes.api.model.HasMetadata;
22+
import io.fabric8.kubernetes.api.model.ObjectMeta;
23+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
24+
import io.fabric8.kubernetes.client.informers.cache.Cache;
25+
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
26+
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
27+
28+
import com.fasterxml.jackson.databind.node.ObjectNode;
29+
30+
/**
31+
* addresses the additional events noted on
32+
* https://github.com/fabric8io/kubernetes-client/issues/5215
33+
* <p>
34+
* Several of these circumstances seem to have been addressed by ~1.25+
35+
*/
36+
public class MetadataAwareOnUpdateFilter<T extends HasMetadata> implements OnUpdateFilter<T> {
37+
38+
private static final Logger log = LoggerFactory.getLogger(MetadataAwareOnUpdateFilter.class);
39+
40+
private final OnUpdateFilter<T> underlyingFilter;
41+
private final KubernetesSerialization kubernetesSerialization;
42+
43+
public MetadataAwareOnUpdateFilter(OnUpdateFilter<T> underlyingFilter,
44+
KubernetesSerialization kubernetesSerialization) {
45+
this.underlyingFilter = underlyingFilter;
46+
this.kubernetesSerialization = kubernetesSerialization;
47+
}
48+
49+
@Override
50+
public boolean accept(T newResource, T oldResource) {
51+
if (underlyingFilter != null && !underlyingFilter.accept(newResource, oldResource)) {
52+
return false;
53+
}
54+
55+
ObjectMeta newMetadata = newResource.getMetadata();
56+
ObjectMeta oldMetadata = oldResource.getMetadata();
57+
// quick check if anything meta has changed
58+
if (newMetadata != null && oldMetadata != null
59+
&& !minimizeObjectMeta(newMetadata).equals(minimizeObjectMeta(oldMetadata))) {
60+
return true;
61+
}
62+
// check everything else besides the metadata
63+
// since the hierarchy of model may not implement hashCode/equals, we'll convert to a generic
64+
// form
65+
// that should be less expensive than full serialization
66+
var newNode = kubernetesSerialization.convertValue(newResource, ObjectNode.class);
67+
newNode.remove("metadata");
68+
var oldNode = kubernetesSerialization.convertValue(oldResource, ObjectNode.class);
69+
oldNode.remove("metadata");
70+
if (newNode.equals(oldNode)) {
71+
// could check the previous annotation to see if it's our update
72+
log.warn("Ignoring revision with no actual changes for resource type {}, {}. "
73+
+ "If the operator sdk was responsible for this update, this is a situation not handled well by SSA.",
74+
newResource.getKind(), Cache.metaNamespaceKeyFunc(newResource));
75+
return false;
76+
}
77+
return true;
78+
}
79+
80+
private ObjectMeta minimizeObjectMeta(ObjectMeta newMetadata) {
81+
return new ObjectMetaBuilder(newMetadata).withManagedFields().withResourceVersion(null)
82+
.withUid(null).build();
83+
}
84+
85+
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java

+40-22
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import org.slf4j.Logger;
88
import org.slf4j.LoggerFactory;
99

10+
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
1011
import io.fabric8.kubernetes.api.model.HasMetadata;
1112
import io.fabric8.kubernetes.api.model.ManagedFieldsEntry;
13+
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
1214
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
1315
import io.javaoperatorsdk.operator.OperatorException;
1416
import io.javaoperatorsdk.operator.api.reconciler.Context;
@@ -74,6 +76,9 @@ public boolean matches(R actual, R desired, Context<?> context) {
7476
var objectMapper = context.getClient().getKubernetesSerialization();
7577

7678
var actualMap = objectMapper.convertValue(actual, Map.class);
79+
80+
sanitizeState(actual, desired, actualMap);
81+
7782
var desiredMap = objectMapper.convertValue(desired, Map.class);
7883
if (LoggingUtils.isNotSensitiveResource(desired)) {
7984
log.trace("Original actual: \n {} \n original desired: \n {} ", actual, desiredMap);
@@ -92,6 +97,33 @@ public boolean matches(R actual, R desired, Context<?> context) {
9297
return prunedActual.equals(desiredMap);
9398
}
9499

100+
/**
101+
* Correct for known issue with SSA
102+
*/
103+
@SuppressWarnings("unchecked")
104+
private void sanitizeState(R actual, R desired, Map<String, Object> actualMap) {
105+
if (desired instanceof StatefulSet) {
106+
StatefulSet desiredStatefulSet = (StatefulSet) desired;
107+
StatefulSet actualStatefulSet = (StatefulSet) actual;
108+
int claims = desiredStatefulSet.getSpec().getVolumeClaimTemplates().size();
109+
if (claims == actualStatefulSet.getSpec().getVolumeClaimTemplates().size()) {
110+
for (int i = 0; i < claims; i++) {
111+
if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getSpec()
112+
.getVolumeMode() == null) {
113+
Optional
114+
.ofNullable(GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates",
115+
i, "spec"))
116+
.map(Map.class::cast).ifPresent(m -> m.remove("volumeMode"));
117+
}
118+
Optional
119+
.ofNullable(
120+
GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i))
121+
.map(Map.class::cast).ifPresent(m -> m.remove("status"));
122+
}
123+
}
124+
}
125+
}
126+
95127
@SuppressWarnings("unchecked")
96128
private static void removeIrrelevantValues(Map<String, Object> desiredMap) {
97129
var metadata = (Map<String, Object>) desiredMap.get(METADATA_KEY);
@@ -153,8 +185,7 @@ private static void fillResultsAndTraverseFurther(Map<String, Object> result,
153185
var emptyMapValue = new HashMap<String, Object>();
154186
result.put(keyInActual, emptyMapValue);
155187
var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap());
156-
log.debug("key: {} actual map value: {} managedFieldValue: {}", keyInActual,
157-
actualMapValue, managedFieldValue);
188+
log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, managedFieldValue);
158189

159190
keepOnlyManagedFields(emptyMapValue, (Map<String, Object>) actualMapValue,
160191
(Map<String, Object>) managedFields.get(key), objectMapper);
@@ -282,29 +313,16 @@ private static java.util.Map.Entry<Integer, Map<String, Object>> selectListEntry
282313
}
283314
if (possibleTargets.isEmpty()) {
284315
throw new IllegalStateException(
285-
"Cannot find list element for key:" + key + ", in map: " + values);
316+
"Cannot find list element for key:" + key + ", in map: "
317+
+ values.stream().map(Map::keySet).collect(Collectors.toList()));
286318
}
287319
if (possibleTargets.size() > 1) {
288320
throw new IllegalStateException(
289-
"More targets found in list element for key:" + key + ", in map: " + values);
321+
"More targets found in list element for key:" + key + ", in map: "
322+
+ values.stream().map(Map::keySet).collect(Collectors.toList()));
290323
}
291324
final var finalIndex = index;
292-
return new Map.Entry<>() {
293-
@Override
294-
public Integer getKey() {
295-
return finalIndex;
296-
}
297-
298-
@Override
299-
public Map<String, Object> getValue() {
300-
return possibleTargets.get(0);
301-
}
302-
303-
@Override
304-
public Map<String, Object> setValue(Map<String, Object> stringObjectMap) {
305-
throw new IllegalStateException("should not be called");
306-
}
307-
};
325+
return new AbstractMap.SimpleEntry<>(finalIndex, possibleTargets.get(0));
308326
}
309327

310328

@@ -318,14 +336,14 @@ private Optional<ManagedFieldsEntry> checkIfFieldManagerExists(R actual, String
318336
.collect(Collectors.toList());
319337
if (targetManagedFields.isEmpty()) {
320338
log.debug("No field manager exists for resource {} with name: {} and operation Apply ",
321-
actual, actual.getMetadata().getName());
339+
actual.getKind(), actual.getMetadata().getName());
322340
return Optional.empty();
323341
}
324342
// this should not happen in theory
325343
if (targetManagedFields.size() > 1) {
326344
throw new OperatorException(
327345
"More than one field manager exists with name: " + fieldManager + "in resource: " +
328-
actual + " with name: " + actual.getMetadata().getName());
346+
actual.getKind() + " with name: " + actual.getMetadata().getName());
329347
}
330348
return Optional.of(targetManagedFields.get(0));
331349
}

0 commit comments

Comments
 (0)