Skip to content

Commit 5207700

Browse files
fix: infinite resource updates due empty EnvVars (#2803)
Signed-off-by: Antonio Fernandez Alhambra <antonio.alhambra@hivemq.com>
1 parent e3c828f commit 5207700

File tree

4 files changed

+254
-47
lines changed

4 files changed

+254
-47
lines changed
Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,34 @@
22

33
import java.util.List;
44
import java.util.Map;
5+
import java.util.Objects;
56
import java.util.Optional;
67

78
import io.fabric8.kubernetes.api.model.Container;
9+
import io.fabric8.kubernetes.api.model.EnvVar;
810
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
911
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
1012
import io.fabric8.kubernetes.api.model.Quantity;
1113
import io.fabric8.kubernetes.api.model.ResourceRequirements;
1214

1315
/**
14-
* Sanitizes the {@link ResourceRequirements} in the containers of a pair of {@link PodTemplateSpec}
15-
* instances.
16+
* Sanitizes the {@link ResourceRequirements} and the {@link EnvVar} in the containers of a pair of
17+
* {@link PodTemplateSpec} instances.
1618
*
1719
* <p>When the sanitizer finds a mismatch in the structure of the given templates, before it gets to
18-
* the nested resource limits and requests, it returns early without fixing the actual map. This is
19-
* an optimization because the given templates will anyway differ at this point. This means we do
20-
* not have to attempt to sanitize the resources for these use cases, since there will anyway be an
21-
* update of the K8s resource.
20+
* the nested fields, it returns early without fixing the actual map. This is an optimization
21+
* because the given templates will anyway differ at this point. This means we do not have to
22+
* attempt to sanitize the fields for these use cases, since there will anyway be an update of the
23+
* K8s resource.
2224
*
2325
* <p>The algorithm traverses the whole template structure because we need the actual and desired
24-
* {@link Quantity} instances to compare their numerical amount. Using the {@link
26+
* {@link Quantity} and {@link EnvVar} instances. Using the {@link
2527
* GenericKubernetesResource#get(Map, Object...)} shortcut would need to create new instances just
2628
* for the sanitization check.
2729
*/
28-
class ResourceRequirementsSanitizer {
30+
class PodTemplateSpecSanitizer {
2931

30-
static void sanitizeResourceRequirements(
32+
static void sanitizePodTemplateSpec(
3133
final Map<String, Object> actualMap,
3234
final PodTemplateSpec actualTemplate,
3335
final PodTemplateSpec desiredTemplate) {
@@ -37,31 +39,37 @@ static void sanitizeResourceRequirements(
3739
if (actualTemplate.getSpec() == null || desiredTemplate.getSpec() == null) {
3840
return;
3941
}
40-
sanitizeResourceRequirements(
42+
sanitizePodTemplateSpec(
4143
actualMap,
4244
actualTemplate.getSpec().getInitContainers(),
4345
desiredTemplate.getSpec().getInitContainers(),
4446
"initContainers");
45-
sanitizeResourceRequirements(
47+
sanitizePodTemplateSpec(
4648
actualMap,
4749
actualTemplate.getSpec().getContainers(),
4850
desiredTemplate.getSpec().getContainers(),
4951
"containers");
5052
}
5153

52-
private static void sanitizeResourceRequirements(
54+
private static void sanitizePodTemplateSpec(
5355
final Map<String, Object> actualMap,
5456
final List<Container> actualContainers,
5557
final List<Container> desiredContainers,
5658
final String containerPath) {
5759
int containers = desiredContainers.size();
5860
if (containers == actualContainers.size()) {
5961
for (int containerIndex = 0; containerIndex < containers; containerIndex++) {
60-
var desiredContainer = desiredContainers.get(containerIndex);
61-
var actualContainer = actualContainers.get(containerIndex);
62+
final var desiredContainer = desiredContainers.get(containerIndex);
63+
final var actualContainer = actualContainers.get(containerIndex);
6264
if (!desiredContainer.getName().equals(actualContainer.getName())) {
6365
return;
6466
}
67+
sanitizeEnvVars(
68+
actualMap,
69+
actualContainer.getEnv(),
70+
desiredContainer.getEnv(),
71+
containerPath,
72+
containerIndex);
6573
sanitizeResourceRequirements(
6674
actualMap,
6775
actualContainer.getResources(),
@@ -121,7 +129,7 @@ private static void sanitizeQuantities(
121129
m ->
122130
actualResource.forEach(
123131
(key, actualQuantity) -> {
124-
var desiredQuantity = desiredResource.get(key);
132+
final var desiredQuantity = desiredResource.get(key);
125133
if (desiredQuantity == null) {
126134
return;
127135
}
@@ -138,4 +146,53 @@ private static void sanitizeQuantities(
138146
}
139147
}));
140148
}
149+
150+
@SuppressWarnings("unchecked")
151+
private static void sanitizeEnvVars(
152+
final Map<String, Object> actualMap,
153+
final List<EnvVar> actualEnvVars,
154+
final List<EnvVar> desiredEnvVars,
155+
final String containerPath,
156+
final int containerIndex) {
157+
if (desiredEnvVars.isEmpty() || actualEnvVars.isEmpty()) {
158+
return;
159+
}
160+
Optional.ofNullable(
161+
GenericKubernetesResource.get(
162+
actualMap, "spec", "template", "spec", containerPath, containerIndex, "env"))
163+
.map(List.class::cast)
164+
.ifPresent(
165+
envVars ->
166+
actualEnvVars.forEach(
167+
actualEnvVar -> {
168+
final var actualEnvVarName = actualEnvVar.getName();
169+
final var actualEnvVarValue = actualEnvVar.getValue();
170+
// check if the actual EnvVar value string is not null or the desired EnvVar
171+
// already contains the same EnvVar name with a non empty EnvVar value
172+
final var isDesiredEnvVarEmpty =
173+
hasEnvVarNoEmptyValue(actualEnvVarName, desiredEnvVars);
174+
if (actualEnvVarValue != null || isDesiredEnvVarEmpty) {
175+
return;
176+
}
177+
envVars.stream()
178+
.filter(
179+
envVar ->
180+
((Map<String, String>) envVar)
181+
.get("name")
182+
.equals(actualEnvVarName))
183+
// add the actual EnvVar value with an empty string to prevent a
184+
// resource update
185+
.forEach(envVar -> ((Map<String, String>) envVar).put("value", ""));
186+
}));
187+
}
188+
189+
private static boolean hasEnvVarNoEmptyValue(
190+
final String envVarName, final List<EnvVar> envVars) {
191+
return envVars.stream()
192+
.anyMatch(
193+
envVar ->
194+
Objects.equals(envVarName, envVar.getName())
195+
&& envVar.getValue() != null
196+
&& !envVar.getValue().isEmpty());
197+
}
141198
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import com.github.difflib.DiffUtils;
3232
import com.github.difflib.UnifiedDiffUtils;
3333

34-
import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceRequirementsSanitizer.sanitizeResourceRequirements;
34+
import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.PodTemplateSpecSanitizer.sanitizePodTemplateSpec;
3535

3636
/**
3737
* Matches the actual state on the server vs the desired state. Based on the managedFields of SSA.
@@ -203,22 +203,22 @@ protected void sanitizeState(R actual, R desired, Map<String, Object> actualMap)
203203
}
204204
}
205205
}
206-
sanitizeResourceRequirements(actualMap, actualSpec.getTemplate(), desiredSpec.getTemplate());
206+
sanitizePodTemplateSpec(actualMap, actualSpec.getTemplate(), desiredSpec.getTemplate());
207207
} else if (actual instanceof Deployment actualDeployment
208208
&& desired instanceof Deployment desiredDeployment) {
209-
sanitizeResourceRequirements(
209+
sanitizePodTemplateSpec(
210210
actualMap,
211211
actualDeployment.getSpec().getTemplate(),
212212
desiredDeployment.getSpec().getTemplate());
213213
} else if (actual instanceof ReplicaSet actualReplicaSet
214214
&& desired instanceof ReplicaSet desiredReplicaSet) {
215-
sanitizeResourceRequirements(
215+
sanitizePodTemplateSpec(
216216
actualMap,
217217
actualReplicaSet.getSpec().getTemplate(),
218218
desiredReplicaSet.getSpec().getTemplate());
219219
} else if (actual instanceof DaemonSet actualDaemonSet
220220
&& desired instanceof DaemonSet desiredDaemonSet) {
221-
sanitizeResourceRequirements(
221+
sanitizePodTemplateSpec(
222222
actualMap,
223223
actualDaemonSet.getSpec().getTemplate(),
224224
desiredDaemonSet.getSpec().getTemplate());

0 commit comments

Comments
 (0)