Skip to content

Commit c67d088

Browse files
authored
fix: shut down operator when failing to start (#1137)
* fix: start timer as daemon to avoid prolonging the app's life * fix: exit operator on exception when starting Fixes #1136 * feat: extract missing CRD exception check to ReconcilerUtils Fixes #1139 * fix: propagate exceptions and stop on start exception * chore: regexp input is under control so marking as false positive
1 parent 59180ba commit c67d088

File tree

7 files changed

+107
-33
lines changed

7 files changed

+107
-33
lines changed

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,26 @@ public List<Controller> getControllers() {
8585
* and start the cluster monitoring processes.
8686
*/
8787
public void start() {
88-
controllers.shouldStart();
89-
90-
final var version = ConfigurationServiceProvider.instance().getVersion();
91-
log.info(
92-
"Operator SDK {} (commit: {}) built on {} starting...",
93-
version.getSdkVersion(),
94-
version.getCommit(),
95-
version.getBuiltTime());
96-
97-
final var clientVersion = Version.clientVersion();
98-
log.info("Client version: {}", clientVersion);
99-
100-
ExecutorServiceManager.init();
101-
controllers.start();
88+
try {
89+
controllers.shouldStart();
90+
91+
final var version = ConfigurationServiceProvider.instance().getVersion();
92+
log.info(
93+
"Operator SDK {} (commit: {}) built on {} starting...",
94+
version.getSdkVersion(),
95+
version.getCommit(),
96+
version.getBuiltTime());
97+
98+
final var clientVersion = Version.clientVersion();
99+
log.info("Client version: {}", clientVersion);
100+
101+
ExecutorServiceManager.init();
102+
controllers.start();
103+
} catch (Exception e) {
104+
log.error("Error starting operator", e);
105+
stop();
106+
throw e;
107+
}
102108
}
103109

104110
@Override
@@ -204,10 +210,6 @@ public synchronized void start() {
204210
}
205211

206212
public synchronized void stop() {
207-
if (!started) {
208-
return;
209-
}
210-
211213
this.controllers.values().parallelStream().forEach(closeable -> {
212214
log.debug("closing {}", closeable);
213215
closeable.stop();

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
import java.io.InputStream;
55
import java.lang.reflect.InvocationTargetException;
66
import java.lang.reflect.Method;
7+
import java.util.Arrays;
78
import java.util.Locale;
9+
import java.util.function.Predicate;
10+
import java.util.regex.Pattern;
11+
import java.util.stream.Collectors;
812

913
import io.fabric8.kubernetes.api.model.HasMetadata;
14+
import io.fabric8.kubernetes.client.KubernetesClientException;
1015
import io.fabric8.kubernetes.client.utils.Serialization;
1116
import io.javaoperatorsdk.operator.api.reconciler.Constants;
1217
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
@@ -115,4 +120,49 @@ public static <T> T loadYaml(Class<T> clazz, Class loader, String yaml) {
115120
}
116121
}
117122

123+
public static void handleKubernetesClientException(Exception e, String resourceTypeName) {
124+
if (e instanceof MissingCRDException) {
125+
throw ((MissingCRDException) e);
126+
}
127+
128+
if (e instanceof KubernetesClientException) {
129+
KubernetesClientException ke = (KubernetesClientException) e;
130+
if (404 == ke.getCode()) {
131+
// only throw MissingCRDException if the 404 error occurs on the target CRD
132+
if (resourceTypeName.equals(ke.getFullResourceName())
133+
|| matchesResourceType(resourceTypeName, ke)) {
134+
throw new MissingCRDException(resourceTypeName, null, e.getMessage(), e);
135+
}
136+
}
137+
}
138+
}
139+
140+
private static boolean matchesResourceType(String resourceTypeName,
141+
KubernetesClientException exception) {
142+
final var fullResourceName = exception.getFullResourceName();
143+
if (fullResourceName != null) {
144+
return resourceTypeName.equals(fullResourceName);
145+
} else {
146+
// extract matching information from URI in the message if available
147+
final var message = exception.getMessage();
148+
final var regex = Pattern
149+
.compile(".*http(s?)://[^/]*/api(s?)/(\\S*).*") // NOSONAR: input is controlled
150+
.matcher(message);
151+
if (regex.matches()) {
152+
var group = regex.group(3);
153+
if (group.endsWith(".")) {
154+
group = group.substring(0, group.length() - 1);
155+
}
156+
final var segments = Arrays.stream(group.split("/")).filter(Predicate.not(String::isEmpty))
157+
.collect(Collectors.toUnmodifiableList());
158+
if (segments.size() != 3) {
159+
return false;
160+
}
161+
final var targetResourceName = segments.get(2) + "." + segments.get(0);
162+
return resourceTypeName.equals(targetResourceName);
163+
}
164+
}
165+
return false;
166+
}
167+
118168
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ public void start() throws OperatorException {
332332

333333
log.info("'{}' controller started, pending event sources initialization", controllerName);
334334
} catch (MissingCRDException e) {
335+
stop();
335336
throwMissingCRDException(crdName, specVersion, controllerName);
336337
}
337338
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
import io.fabric8.kubernetes.api.model.HasMetadata;
99
import io.fabric8.kubernetes.client.KubernetesClientException;
1010
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
11-
import io.javaoperatorsdk.operator.MissingCRDException;
1211
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
1312
import io.javaoperatorsdk.operator.processing.Controller;
1413
import io.javaoperatorsdk.operator.processing.MDCUtils;
1514
import io.javaoperatorsdk.operator.processing.event.ResourceID;
1615
import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource;
1716

17+
import static io.javaoperatorsdk.operator.ReconcilerUtils.handleKubernetesClientException;
1818
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName;
1919
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID;
2020
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion;
@@ -50,7 +50,7 @@ public void start() {
5050
try {
5151
super.start();
5252
} catch (KubernetesClientException e) {
53-
handleKubernetesClientException(e);
53+
handleKubernetesClientException(e, controller.getConfiguration().getResourceTypeName());
5454
throw e;
5555
}
5656
}
@@ -90,17 +90,6 @@ public void onDelete(T resource, boolean b) {
9090
eventReceived(ResourceAction.DELETED, resource, null);
9191
}
9292

93-
private void handleKubernetesClientException(Exception e) {
94-
KubernetesClientException ke = (KubernetesClientException) e;
95-
if (404 == ke.getCode()) {
96-
// only throw MissingCRDException if the 404 error occurs on the target CRD
97-
final var targetCRDName = controller.getConfiguration().getResourceTypeName();
98-
if (targetCRDName.equals(ke.getFullResourceName())) {
99-
throw new MissingCRDException(targetCRDName, null, e.getMessage(), e);
100-
}
101-
}
102-
}
103-
10493
@Override
10594
public Optional<T> getSecondaryResource(T primary) {
10695
throw new IllegalStateException("This method should not be called here. Primary: " + primary);

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
1414
import io.fabric8.kubernetes.client.informers.cache.Cache;
1515
import io.javaoperatorsdk.operator.OperatorException;
16+
import io.javaoperatorsdk.operator.ReconcilerUtils;
1617
import io.javaoperatorsdk.operator.processing.LifecycleAware;
1718
import io.javaoperatorsdk.operator.processing.event.ResourceID;
1819
import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache;
@@ -31,7 +32,13 @@ public InformerWrapper(SharedIndexInformer<T> informer) {
3132

3233
@Override
3334
public void start() throws OperatorException {
34-
informer.run();
35+
try {
36+
informer.run();
37+
} catch (Exception e) {
38+
ReconcilerUtils.handleKubernetesClientException(e,
39+
HasMetadata.getFullResourceName(informer.getApiTypeClass()));
40+
throw e;
41+
}
3542
}
3643

3744
@Override

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class TimerEventSource<R extends HasMetadata>
2020
implements ResourceEventAware<R> {
2121
private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class);
2222

23-
private final Timer timer = new Timer();
23+
private final Timer timer = new Timer(true);
2424
private final AtomicBoolean running = new AtomicBoolean();
2525
private final Map<ResourceID, EventProducerTimeTask> onceTasks = new ConcurrentHashMap<>();
2626

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@
22

33
import org.junit.jupiter.api.Test;
44

5+
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
import io.fabric8.kubernetes.api.model.Namespaced;
57
import io.fabric8.kubernetes.api.model.Pod;
68
import io.fabric8.kubernetes.api.model.PodSpec;
79
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
810
import io.fabric8.kubernetes.api.model.apps.Deployment;
911
import io.fabric8.kubernetes.api.model.apps.DeploymentSpec;
12+
import io.fabric8.kubernetes.client.CustomResource;
13+
import io.fabric8.kubernetes.client.KubernetesClientException;
14+
import io.fabric8.kubernetes.model.annotation.Group;
15+
import io.fabric8.kubernetes.model.annotation.ShortNames;
16+
import io.fabric8.kubernetes.model.annotation.Version;
1017
import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler;
1118
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource;
1219

1320
import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultFinalizerName;
1421
import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultNameFor;
1522
import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultReconcilerName;
23+
import static io.javaoperatorsdk.operator.ReconcilerUtils.handleKubernetesClientException;
1624
import static io.javaoperatorsdk.operator.ReconcilerUtils.isFinalizerValid;
1725
import static org.assertj.core.api.Assertions.assertThat;
1826
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.junit.jupiter.api.Assertions.assertThrows;
1928
import static org.junit.jupiter.api.Assertions.assertTrue;
2029

2130
class ReconcilerUtilsTest {
@@ -89,4 +98,20 @@ private Deployment createTestDeployment() {
8998
podTemplateSpec.getSpec().setHostname("localhost");
9099
return deployment;
91100
}
101+
102+
@Test
103+
void handleKubernetesExceptionShouldThrowMissingCRDExceptionWhenAppropriate() {
104+
assertThrows(MissingCRDException.class, () -> handleKubernetesClientException(
105+
new KubernetesClientException(
106+
"Failure executing: GET at: https://kubernetes.docker.internal:6443/apis/tomcatoperator.io/v1/tomcats. Message: Not Found.",
107+
404, null),
108+
HasMetadata.getFullResourceName(Tomcat.class)));
109+
}
110+
111+
@Group("tomcatoperator.io")
112+
@Version("v1")
113+
@ShortNames("tc")
114+
private static class Tomcat extends CustomResource<Void, Void> implements Namespaced {
115+
116+
}
92117
}

0 commit comments

Comments
 (0)