Skip to content

Commit 619d065

Browse files
fix: Fix infinite resource updates due empty EnvVars
Signed-off-by: Antonio Fernandez Alhambra <[email protected]>
1 parent 63bbec5 commit 619d065

File tree

4 files changed

+263
-59
lines changed

4 files changed

+263
-59
lines changed
Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

3-
import java.util.List;
4-
import java.util.Map;
5-
import java.util.Optional;
6-
73
import io.fabric8.kubernetes.api.model.Container;
4+
import io.fabric8.kubernetes.api.model.EnvVar;
85
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
96
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
107
import io.fabric8.kubernetes.api.model.Quantity;
118
import io.fabric8.kubernetes.api.model.ResourceRequirements;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Objects;
12+
import java.util.Optional;
1213

1314
/**
14-
* Sanitizes the {@link ResourceRequirements} in the containers of a pair of {@link PodTemplateSpec}
15-
* instances.
15+
* Sanitizes the {@link ResourceRequirements} and the {@link EnvVar} in the containers of a pair of
16+
* {@link PodTemplateSpec} instances.
1617
*
1718
* <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.
19+
* the nested fields, it returns early without fixing the actual map. This is an optimization
20+
* because the given templates will anyway differ at this point. This means we do not have to
21+
* attempt to sanitize the fields for these use cases, since there will anyway be an update of the
22+
* K8s resource.
2223
*
2324
* <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
25+
* {@link Quantity} and {@link EnvVar} instances. Using the {@link
2526
* GenericKubernetesResource#get(Map, Object...)} shortcut would need to create new instances just
2627
* for the sanitization check.
2728
*/
28-
class ResourceRequirementsSanitizer {
29+
class PodTemplateSpecSanitizer {
2930

30-
static void sanitizeResourceRequirements(
31+
static void sanitizePodTemplateSpec(
3132
final Map<String, Object> actualMap,
3233
final PodTemplateSpec actualTemplate,
3334
final PodTemplateSpec desiredTemplate) {
@@ -37,31 +38,37 @@ static void sanitizeResourceRequirements(
3738
if (actualTemplate.getSpec() == null || desiredTemplate.getSpec() == null) {
3839
return;
3940
}
40-
sanitizeResourceRequirements(
41+
sanitizePodTemplateSpec(
4142
actualMap,
4243
actualTemplate.getSpec().getInitContainers(),
4344
desiredTemplate.getSpec().getInitContainers(),
4445
"initContainers");
45-
sanitizeResourceRequirements(
46+
sanitizePodTemplateSpec(
4647
actualMap,
4748
actualTemplate.getSpec().getContainers(),
4849
desiredTemplate.getSpec().getContainers(),
4950
"containers");
5051
}
5152

52-
private static void sanitizeResourceRequirements(
53+
private static void sanitizePodTemplateSpec(
5354
final Map<String, Object> actualMap,
5455
final List<Container> actualContainers,
5556
final List<Container> desiredContainers,
5657
final String containerPath) {
5758
int containers = desiredContainers.size();
5859
if (containers == actualContainers.size()) {
5960
for (int containerIndex = 0; containerIndex < containers; containerIndex++) {
60-
var desiredContainer = desiredContainers.get(containerIndex);
61-
var actualContainer = actualContainers.get(containerIndex);
61+
final var desiredContainer = desiredContainers.get(containerIndex);
62+
final var actualContainer = actualContainers.get(containerIndex);
6263
if (!desiredContainer.getName().equals(actualContainer.getName())) {
6364
return;
6465
}
66+
sanitizeEnvVars(
67+
actualMap,
68+
actualContainer.getEnv(),
69+
desiredContainer.getEnv(),
70+
containerPath,
71+
containerIndex);
6572
sanitizeResourceRequirements(
6673
actualMap,
6774
actualContainer.getResources(),
@@ -121,7 +128,7 @@ private static void sanitizeQuantities(
121128
m ->
122129
actualResource.forEach(
123130
(key, actualQuantity) -> {
124-
var desiredQuantity = desiredResource.get(key);
131+
final var desiredQuantity = desiredResource.get(key);
125132
if (desiredQuantity == null) {
126133
return;
127134
}
@@ -138,4 +145,53 @@ private static void sanitizeQuantities(
138145
}
139146
}));
140147
}
148+
149+
@SuppressWarnings("unchecked")
150+
private static void sanitizeEnvVars(
151+
final Map<String, Object> actualMap,
152+
final List<EnvVar> actualEnvVars,
153+
final List<EnvVar> desiredEnvVars,
154+
final String containerPath,
155+
final int containerIndex) {
156+
if (desiredEnvVars.isEmpty() || actualEnvVars.isEmpty()) {
157+
return;
158+
}
159+
Optional.ofNullable(
160+
GenericKubernetesResource.get(
161+
actualMap, "spec", "template", "spec", containerPath, containerIndex, "env"))
162+
.map(List.class::cast)
163+
.ifPresent(
164+
envVars ->
165+
actualEnvVars.forEach(
166+
actualEnvVar -> {
167+
final var actualEnvVarName = actualEnvVar.getName();
168+
final var actualEnvVarValue = actualEnvVar.getValue();
169+
// check if the actual EnvVar value string is not null or the desired EnvVar
170+
// already contains the same EnvVar name with a non empty EnvVar value
171+
final var isDesiredEnvVarEmpty =
172+
hasEnvVarNoEmptyValue(actualEnvVarName, desiredEnvVars);
173+
if (actualEnvVarValue != null || isDesiredEnvVarEmpty) {
174+
return;
175+
}
176+
envVars.stream()
177+
.filter(
178+
envVar ->
179+
((Map<String, String>) envVar)
180+
.get("name")
181+
.equals(actualEnvVarName))
182+
// add the actual EnvVar value with an empty string to prevent a
183+
// resource update
184+
.forEach(envVar -> ((Map<String, String>) envVar).put("value", ""));
185+
}));
186+
}
187+
188+
private static boolean hasEnvVarNoEmptyValue(
189+
final String envVarName, final List<EnvVar> envVars) {
190+
return envVars.stream()
191+
.anyMatch(
192+
envVar ->
193+
Objects.equals(envVarName, envVar.getName())
194+
&& envVar.getValue() != null
195+
&& !envVar.getValue().isEmpty());
196+
}
141197
}

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 @@ private 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)