Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 21c8dac

Browse files
authoredMay 11, 2023
fix: avoid NPE is given spec is null (#1899)
Also optimizes case where resource is a CustomResource. Fixes #1897
1 parent 55629a0 commit 21c8dac

File tree

3 files changed

+84
-22
lines changed

3 files changed

+84
-22
lines changed
 

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public class ReconcilerUtils {
2424

2525
private static final String FINALIZER_NAME_SUFFIX = "/finalizer";
2626
protected static final String MISSING_GROUP_SUFFIX = ".javaoperatorsdk.io";
27+
private static final String GET_SPEC = "getSpec";
28+
private static final String SET_SPEC = "setSpec";
29+
private static final Pattern API_URI_PATTERN =
30+
Pattern.compile(".*http(s?)://[^/]*/api(s?)/(\\S*).*"); // NOSONAR: input is controlled
2731

2832
// prevent instantiation of util class
2933
private ReconcilerUtils() {}
@@ -94,25 +98,55 @@ public static boolean specsEqual(HasMetadata r1, HasMetadata r2) {
9498

9599
// will be replaced with: https://github.com/fabric8io/kubernetes-client/issues/3816
96100
public static Object getSpec(HasMetadata resource) {
101+
// optimize CustomResource case
102+
if (resource instanceof CustomResource) {
103+
CustomResource cr = (CustomResource) resource;
104+
return cr.getSpec();
105+
}
106+
97107
try {
98-
Method getSpecMethod = resource.getClass().getMethod("getSpec");
108+
Method getSpecMethod = resource.getClass().getMethod(GET_SPEC);
99109
return getSpecMethod.invoke(resource);
100110
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
101-
throw new IllegalStateException("No spec found on resource", e);
111+
throw noSpecException(resource, e);
102112
}
103113
}
104114

115+
@SuppressWarnings("unchecked")
105116
public static Object setSpec(HasMetadata resource, Object spec) {
117+
// optimize CustomResource case
118+
if (resource instanceof CustomResource) {
119+
CustomResource cr = (CustomResource) resource;
120+
cr.setSpec(spec);
121+
return null;
122+
}
123+
106124
try {
107125
Class<? extends HasMetadata> resourceClass = resource.getClass();
108-
Method setSpecMethod =
109-
resource.getClass().getMethod("setSpec", getSpecClass(resourceClass, spec));
126+
127+
// if given spec is null, find the method just using its name
128+
Method setSpecMethod;
129+
if (spec != null) {
130+
setSpecMethod = resourceClass.getMethod(SET_SPEC, spec.getClass());
131+
} else {
132+
setSpecMethod = Arrays.stream(resourceClass.getMethods())
133+
.filter(method -> SET_SPEC.equals(method.getName()))
134+
.findFirst()
135+
.orElseThrow(() -> noSpecException(resource, null));
136+
}
137+
110138
return setSpecMethod.invoke(resource, spec);
111139
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
112-
throw new IllegalStateException("No spec found on resource", e);
140+
throw noSpecException(resource, e);
113141
}
114142
}
115143

144+
private static IllegalStateException noSpecException(HasMetadata resource,
145+
ReflectiveOperationException e) {
146+
return new IllegalStateException("No spec found on resource " + resource.getClass().getName(),
147+
e);
148+
}
149+
116150
public static <T> T loadYaml(Class<T> clazz, Class loader, String yaml) {
117151
try (InputStream is = loader.getResourceAsStream(yaml)) {
118152
if (Builder.class.isAssignableFrom(clazz)) {
@@ -149,9 +183,7 @@ private static boolean matchesResourceType(String resourceTypeName,
149183
} else {
150184
// extract matching information from URI in the message if available
151185
final var message = exception.getMessage();
152-
final var regex = Pattern
153-
.compile(".*http(s?)://[^/]*/api(s?)/(\\S*).*") // NOSONAR: input is controlled
154-
.matcher(message);
186+
final var regex = API_URI_PATTERN.matcher(message);
155187
if (regex.matches()) {
156188
var group = regex.group(3);
157189
if (group.endsWith(".")) {
@@ -168,14 +200,4 @@ private static boolean matchesResourceType(String resourceTypeName,
168200
}
169201
return false;
170202
}
171-
172-
// CustomResouce has a parameterized parameter type
173-
private static Class getSpecClass(Class<? extends HasMetadata> resourceClass, Object spec) {
174-
if (CustomResource.class.isAssignableFrom(resourceClass)) {
175-
return Object.class;
176-
} else {
177-
return spec.getClass();
178-
}
179-
}
180-
181203
}

‎operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import io.fabric8.kubernetes.api.model.ContainerBuilder;
88
import io.fabric8.kubernetes.api.model.HasMetadata;
9+
import io.fabric8.kubernetes.api.model.Namespace;
910
import io.fabric8.kubernetes.api.model.Namespaced;
1011
import io.fabric8.kubernetes.api.model.Pod;
1112
import io.fabric8.kubernetes.api.model.PodSpec;
@@ -85,6 +86,16 @@ void getsSpecWithReflection() {
8586
assertThat(spec.getReplicas()).isEqualTo(5);
8687
}
8788

89+
@Test
90+
void properlyHandlesNullSpec() {
91+
Namespace ns = new Namespace();
92+
93+
final var spec = ReconcilerUtils.getSpec(ns);
94+
assertThat(spec).isNull();
95+
96+
ReconcilerUtils.setSpec(ns, null);
97+
}
98+
8899
@Test
89100
void setsSpecWithReflection() {
90101
Deployment deployment = new Deployment();

‎operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

33
import java.util.HashMap;
4+
import java.util.List;
45

56
import org.junit.jupiter.api.BeforeAll;
67
import org.junit.jupiter.api.Test;
78

9+
import io.fabric8.kubernetes.api.model.Namespace;
10+
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
11+
import io.fabric8.kubernetes.api.model.NamespaceSpec;
812
import io.fabric8.kubernetes.api.model.apps.Deployment;
913
import io.javaoperatorsdk.operator.ReconcilerUtils;
1014
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
@@ -25,12 +29,9 @@ static void setUp() {
2529
when(context.getControllerConfiguration()).thenReturn(controllerConfiguration);
2630
}
2731

28-
ResourceUpdatePreProcessor<Deployment> processor =
29-
GenericResourceUpdatePreProcessor.processorFor(Deployment.class);
30-
31-
3232
@Test
3333
void preservesValues() {
34+
var processor = GenericResourceUpdatePreProcessor.processorFor(Deployment.class);
3435
var desired = createDeployment();
3536
var actual = createDeployment();
3637
actual.getMetadata().setLabels(new HashMap<>());
@@ -45,6 +46,34 @@ void preservesValues() {
4546
assertThat(result.getSpec().getRevisionHistoryLimit()).isEqualTo(10);
4647
}
4748

49+
@Test
50+
void checkNamespaces() {
51+
var processor = GenericResourceUpdatePreProcessor.processorFor(Namespace.class);
52+
var desired = new NamespaceBuilder().withNewMetadata().withName("foo").endMetadata().build();
53+
var actual = new NamespaceBuilder().withNewMetadata().withName("foo").endMetadata().build();
54+
actual.getMetadata().setLabels(new HashMap<>());
55+
actual.getMetadata().getLabels().put("additionalActualKey", "value");
56+
actual.getMetadata().setResourceVersion("1234");
57+
58+
var result = processor.replaceSpecOnActual(actual, desired, context);
59+
assertThat(result.getMetadata().getLabels().get("additionalActualKey")).isEqualTo("value");
60+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("1234");
61+
62+
desired.setSpec(new NamespaceSpec(List.of("halkyon.io/finalizer")));
63+
64+
result = processor.replaceSpecOnActual(actual, desired, context);
65+
assertThat(result.getMetadata().getLabels().get("additionalActualKey")).isEqualTo("value");
66+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("1234");
67+
assertThat(result.getSpec().getFinalizers()).containsExactly("halkyon.io/finalizer");
68+
69+
desired = new NamespaceBuilder().withNewMetadata().withName("foo").endMetadata().build();
70+
71+
result = processor.replaceSpecOnActual(actual, desired, context);
72+
assertThat(result.getMetadata().getLabels().get("additionalActualKey")).isEqualTo("value");
73+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("1234");
74+
assertThat(result.getSpec()).isNull();
75+
}
76+
4877
Deployment createDeployment() {
4978
return ReconcilerUtils.loadYaml(
5079
Deployment.class, GenericResourceUpdatePreProcessorTest.class, "nginx-deployment.yaml");

0 commit comments

Comments
 (0)
Please sign in to comment.