Skip to content

Commit 49712fc

Browse files
committed
various refinements to ssa logic
- localizes sanitation to ssa matcher - further ensures values won't be logged Signed-off-by: Steve Hawkins <[email protected]>
1 parent 2827206 commit 49712fc

File tree

4 files changed

+47
-76
lines changed

4 files changed

+47
-76
lines changed

Diff for: docs/documentation/dependent-resources.md

+2
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ server. To bypass the matching feature completely, simply override the `match` m
370370
return `false`, thus telling JOSDK that the actual state never matches the desired one, making
371371
it always update the resources using SSA.
372372

373+
WARNING: Older versions of Kubernetes before 1.25 would create an additional resource version for every SSA update performed with certain resources - even though there were no actual changes in the stored resource. This behavior was seen with Secrets using stringData, Ingresses using empty string fields, and StatefulSets using volume claim templates. The operator framework has added built-in handling for the StatefulSet issue. If you encounter this issue, consider changing your desired state, turning off SSA for that resource, or even upgrading your Kubernetes version.
374+
373375
## Telling JOSDK how to find which secondary resources are associated with a given primary resource
374376

375377
[`KubernetesDependentResource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java)

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

+3-7
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ public R create(R desired, P primary, Context<P> context) {
114114
}
115115
}
116116
addMetadata(false, null, desired, primary);
117-
sanitizeDesired(desired, null, primary, context);
118117
final var resource = prepare(desired, primary, "Creating");
119118
return useSSA(context)
120119
? resource
@@ -131,7 +130,6 @@ public R update(R actual, R desired, P primary, Context<P> context) {
131130
}
132131
R updatedResource;
133132
addMetadata(false, actual, desired, primary);
134-
sanitizeDesired(desired, actual, primary, context);
135133
if (useSSA(context)) {
136134
updatedResource = prepare(desired, primary, "Updating")
137135
.fieldManager(context.getControllerConfiguration().fieldManager())
@@ -148,7 +146,6 @@ public R update(R actual, R desired, P primary, Context<P> context) {
148146
@Override
149147
public Result<R> match(R actualResource, P primary, Context<P> context) {
150148
final var desired = desired(primary, context);
151-
sanitizeDesired(desired, actualResource, primary, context);
152149
return match(actualResource, desired, primary, updaterMatcher, context);
153150
}
154151

@@ -192,11 +189,10 @@ protected void addMetadata(boolean forMatch, R actualResource, final R target, P
192189
addReferenceHandlingMetadata(target, primary);
193190
}
194191

195-
protected void sanitizeDesired(R desired, R actual, P primary, Context<P> context) {
196-
DesiredResourceSanitizer.sanitizeDesired(desired, actual, primary, context, useSSA(context));
197-
}
198-
199192
protected boolean useSSA(Context<P> context) {
193+
if (this instanceof ResourceUpdaterMatcher) {
194+
return false;
195+
}
200196
Optional<Boolean> useSSAConfig =
201197
configuration().flatMap(KubernetesDependentResourceConfig::useSSA);
202198
var configService = context.getControllerConfiguration().getConfigurationService();

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

+42-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,35 @@ 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+
if (desiredStatefulSet.getSpec().getVolumeClaimTemplates().get(i).getStatus() == null) {
119+
Optional
120+
.ofNullable(
121+
GenericKubernetesResource.get(actualMap, "spec", "volumeClaimTemplates", i))
122+
.map(Map.class::cast).ifPresent(m -> m.remove("status"));
123+
}
124+
}
125+
}
126+
}
127+
}
128+
95129
@SuppressWarnings("unchecked")
96130
private static void removeIrrelevantValues(Map<String, Object> desiredMap) {
97131
var metadata = (Map<String, Object>) desiredMap.get(METADATA_KEY);
@@ -153,8 +187,7 @@ private static void fillResultsAndTraverseFurther(Map<String, Object> result,
153187
var emptyMapValue = new HashMap<String, Object>();
154188
result.put(keyInActual, emptyMapValue);
155189
var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap());
156-
log.debug("key: {} actual map value: {} managedFieldValue: {}", keyInActual,
157-
actualMapValue, managedFieldValue);
190+
log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, managedFieldValue);
158191

159192
keepOnlyManagedFields(emptyMapValue, (Map<String, Object>) actualMapValue,
160193
(Map<String, Object>) managedFields.get(key), objectMapper);
@@ -282,29 +315,16 @@ private static java.util.Map.Entry<Integer, Map<String, Object>> selectListEntry
282315
}
283316
if (possibleTargets.isEmpty()) {
284317
throw new IllegalStateException(
285-
"Cannot find list element for key:" + key + ", in map: " + values);
318+
"Cannot find list element for key:" + key + ", in map: "
319+
+ values.stream().map(Map::keySet).collect(Collectors.toList()));
286320
}
287321
if (possibleTargets.size() > 1) {
288322
throw new IllegalStateException(
289-
"More targets found in list element for key:" + key + ", in map: " + values);
323+
"More targets found in list element for key:" + key + ", in map: "
324+
+ values.stream().map(Map::keySet).collect(Collectors.toList()));
290325
}
291326
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-
};
327+
return new AbstractMap.SimpleEntry<>(finalIndex, possibleTargets.get(0));
308328
}
309329

310330

@@ -318,14 +338,14 @@ private Optional<ManagedFieldsEntry> checkIfFieldManagerExists(R actual, String
318338
.collect(Collectors.toList());
319339
if (targetManagedFields.isEmpty()) {
320340
log.debug("No field manager exists for resource {} with name: {} and operation Apply ",
321-
actual, actual.getMetadata().getName());
341+
actual.getKind(), actual.getMetadata().getName());
322342
return Optional.empty();
323343
}
324344
// this should not happen in theory
325345
if (targetManagedFields.size() > 1) {
326346
throw new OperatorException(
327347
"More than one field manager exists with name: " + fieldManager + "in resource: " +
328-
actual + " with name: " + actual.getMetadata().getName());
348+
actual.getKind() + " with name: " + actual.getMetadata().getName());
329349
}
330350
return Optional.of(targetManagedFields.get(0));
331351
}

0 commit comments

Comments
 (0)