Skip to content

Commit 1c47ecf

Browse files
committed
refactor: make Operator only deal with ConfiguredController management
Introduced ConfiguredController to gather in one entity ResourceController, its associated configuration and Kubernetes client so that they can be passed around more easily in an always-associated manner (as, right now, configuration can be disassociated from the controller). Another aspect is that it allows us to simplify the management of the lifecycle as ConfiguredControllers can be started/stopped and the Operator doesn't need to bother about the details.
1 parent 8f137cf commit 1c47ecf

File tree

2 files changed

+184
-116
lines changed

2 files changed

+184
-116
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,33 @@
11
package io.javaoperatorsdk.operator;
22

3-
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
4-
import io.fabric8.kubernetes.client.CustomResource;
5-
import io.fabric8.kubernetes.client.KubernetesClient;
6-
import io.fabric8.kubernetes.client.Version;
7-
import io.javaoperatorsdk.operator.api.ResourceController;
8-
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
9-
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
10-
import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager;
113
import java.io.Closeable;
124
import java.io.IOException;
135
import java.util.ArrayList;
146
import java.util.List;
15-
import java.util.Objects;
7+
168
import org.slf4j.Logger;
179
import org.slf4j.LoggerFactory;
1810

11+
import io.fabric8.kubernetes.client.CustomResource;
12+
import io.fabric8.kubernetes.client.KubernetesClient;
13+
import io.fabric8.kubernetes.client.Version;
14+
import io.javaoperatorsdk.operator.api.ResourceController;
15+
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
16+
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
17+
import io.javaoperatorsdk.operator.processing.ConfiguredController;
18+
1919
@SuppressWarnings("rawtypes")
2020
public class Operator implements AutoCloseable {
2121
private static final Logger log = LoggerFactory.getLogger(Operator.class);
2222
private final KubernetesClient k8sClient;
2323
private final ConfigurationService configurationService;
24-
private final List<Closeable> closeables;
2524
private final Object lock;
26-
private final List<ControllerRef> controllers;
25+
private final List<ConfiguredController> controllers;
2726
private volatile boolean started;
2827

2928
public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) {
3029
this.k8sClient = k8sClient;
3130
this.configurationService = configurationService;
32-
this.closeables = new ArrayList<>();
3331
this.lock = new Object();
3432
this.controllers = new ArrayList<>();
3533
this.started = false;
@@ -82,9 +80,7 @@ public void start() {
8280
throw new OperatorException("Error retrieving the server version", e);
8381
}
8482

85-
for (ControllerRef ref : controllers) {
86-
startController(ref.controller, ref.configuration);
87-
}
83+
controllers.forEach(ConfiguredController::start);
8884

8985
started = true;
9086
}
@@ -101,7 +97,7 @@ public void close() {
10197
log.info(
10298
"Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion());
10399

104-
for (Closeable closeable : this.closeables) {
100+
for (Closeable closeable : this.controllers) {
105101
try {
106102
log.debug("closing {}", closeable);
107103
closeable.close();
@@ -143,32 +139,6 @@ public <R extends CustomResource> void register(ResourceController<R> controller
143139
public <R extends CustomResource> void register(
144140
ResourceController<R> controller, ControllerConfiguration<R> configuration)
145141
throws OperatorException {
146-
synchronized (lock) {
147-
if (!started) {
148-
this.controllers.add(new ControllerRef(controller, configuration));
149-
} else {
150-
this.controllers.add(new ControllerRef(controller, configuration));
151-
startController(controller, configuration);
152-
}
153-
}
154-
}
155-
156-
/**
157-
* Registers the specified controller with this operator, overriding its default configuration by
158-
* the specified one (usually created via
159-
* {@link io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider#override(ControllerConfiguration)},
160-
* passing it the controller's original configuration.
161-
*
162-
* @param controller the controller to register
163-
* @param configuration the configuration with which we want to register the controller, if {@code
164-
* null}, the controller's original configuration is used
165-
* @param <R> the {@code CustomResource} type associated with the controller
166-
* @throws OperatorException if a problem occurred during the registration process
167-
*/
168-
private <R extends CustomResource> void startController(
169-
ResourceController<R> controller, ControllerConfiguration<R> configuration)
170-
throws OperatorException {
171-
172142
final var existing = configurationService.getConfigurationFor(controller);
173143
if (existing == null) {
174144
log.warn(
@@ -181,39 +151,13 @@ private <R extends CustomResource> void startController(
181151
if (configuration == null) {
182152
configuration = existing;
183153
}
184-
185-
final Class<R> resClass = configuration.getCustomResourceClass();
186-
final String controllerName = configuration.getName();
187-
final var crdName = configuration.getCRDName();
188-
final var specVersion = "v1";
189-
190-
// check that the custom resource is known by the cluster if configured that way
191-
final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config
192-
if (configurationService.checkCRDAndValidateLocalModel()) {
193-
crd = k8sClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get();
194-
if (crd == null) {
195-
throwMissingCRDException(crdName, specVersion, controllerName);
154+
synchronized (lock) {
155+
final var configuredController =
156+
new ConfiguredController(controller, configuration, k8sClient);
157+
this.controllers.add(configuredController);
158+
if (started) {
159+
configuredController.start();
196160
}
197-
198-
// Apply validations that are not handled by fabric8
199-
CustomResourceUtils.assertCustomResource(resClass, crd);
200-
}
201-
202-
try {
203-
DefaultEventSourceManager eventSourceManager =
204-
new DefaultEventSourceManager(
205-
controller, configuration, k8sClient.customResources(resClass));
206-
controller.init(eventSourceManager);
207-
closeables.add(eventSourceManager);
208-
} catch (MissingCRDException e) {
209-
throwMissingCRDException(crdName, specVersion, controllerName);
210-
}
211-
212-
if (failOnMissingCurrentNS(configuration)) {
213-
throw new OperatorException(
214-
"Controller '"
215-
+ controllerName
216-
+ "' is configured to watch the current namespace but it couldn't be inferred from the current configuration.");
217161
}
218162

219163
final var watchedNS =
@@ -222,49 +166,9 @@ private <R extends CustomResource> void startController(
222166
: configuration.getEffectiveNamespaces();
223167
log.info(
224168
"Registered Controller: '{}' for CRD: '{}' for namespace(s): {}",
225-
controllerName,
226-
resClass,
169+
configuration.getName(),
170+
configuration.getCustomResourceClass(),
227171
watchedNS);
228172
}
229173
}
230-
231-
private void throwMissingCRDException(String crdName, String specVersion, String controllerName) {
232-
throw new MissingCRDException(
233-
crdName,
234-
specVersion,
235-
"'"
236-
+ crdName
237-
+ "' "
238-
+ specVersion
239-
+ " CRD was not found on the cluster, controller '"
240-
+ controllerName
241-
+ "' cannot be registered");
242-
}
243-
244-
/**
245-
* Determines whether we should fail because the current namespace is request as target namespace
246-
* but is missing
247-
*
248-
* @return {@code true} if the current namespace is requested but is missing, {@code false}
249-
* otherwise
250-
*/
251-
private static <R extends CustomResource> boolean failOnMissingCurrentNS(
252-
ControllerConfiguration<R> configuration) {
253-
if (configuration.watchCurrentNamespace()) {
254-
final var effectiveNamespaces = configuration.getEffectiveNamespaces();
255-
return effectiveNamespaces.size() == 1
256-
&& effectiveNamespaces.stream().allMatch(Objects::isNull);
257-
}
258-
return false;
259-
}
260-
261-
private static class ControllerRef {
262-
public final ResourceController controller;
263-
public final ControllerConfiguration configuration;
264-
265-
public ControllerRef(ResourceController controller, ControllerConfiguration configuration) {
266-
this.controller = controller;
267-
this.configuration = configuration;
268-
}
269-
}
270174
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package io.javaoperatorsdk.operator.processing;
2+
3+
import java.io.Closeable;
4+
import java.io.IOException;
5+
import java.util.Objects;
6+
7+
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
8+
import io.fabric8.kubernetes.client.CustomResource;
9+
import io.fabric8.kubernetes.client.KubernetesClient;
10+
import io.javaoperatorsdk.operator.CustomResourceUtils;
11+
import io.javaoperatorsdk.operator.MissingCRDException;
12+
import io.javaoperatorsdk.operator.OperatorException;
13+
import io.javaoperatorsdk.operator.api.Context;
14+
import io.javaoperatorsdk.operator.api.DeleteControl;
15+
import io.javaoperatorsdk.operator.api.ResourceController;
16+
import io.javaoperatorsdk.operator.api.UpdateControl;
17+
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
18+
import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager;
19+
import io.javaoperatorsdk.operator.processing.event.EventSourceManager;
20+
21+
public class ConfiguredController<R extends CustomResource<?, ?>> implements ResourceController<R>,
22+
Closeable {
23+
private final ResourceController<R> controller;
24+
private final ControllerConfiguration<R> configuration;
25+
private final KubernetesClient k8sClient;
26+
private EventSourceManager manager;
27+
28+
public ConfiguredController(ResourceController<R> controller,
29+
ControllerConfiguration<R> configuration,
30+
KubernetesClient k8sClient) {
31+
this.controller = controller;
32+
this.configuration = configuration;
33+
this.k8sClient = k8sClient;
34+
}
35+
36+
@Override
37+
public DeleteControl deleteResource(R resource, Context<R> context) {
38+
return controller.deleteResource(resource, context);
39+
}
40+
41+
@Override
42+
public UpdateControl<R> createOrUpdateResource(R resource, Context<R> context) {
43+
return controller.createOrUpdateResource(resource, context);
44+
}
45+
46+
@Override
47+
public void init(EventSourceManager eventSourceManager) {
48+
this.manager = eventSourceManager;
49+
controller.init(eventSourceManager);
50+
}
51+
52+
@Override
53+
public boolean equals(Object o) {
54+
if (this == o) {
55+
return true;
56+
}
57+
if (o == null || getClass() != o.getClass()) {
58+
return false;
59+
}
60+
61+
ConfiguredController<?> that = (ConfiguredController<?>) o;
62+
return configuration.getName().equals(that.configuration.getName());
63+
}
64+
65+
@Override
66+
public int hashCode() {
67+
return configuration.getName().hashCode();
68+
}
69+
70+
@Override
71+
public String toString() {
72+
return "'" + configuration.getName() + "' Controller";
73+
}
74+
75+
public ResourceController<R> getController() {
76+
return controller;
77+
}
78+
79+
public ControllerConfiguration<R> getConfiguration() {
80+
return configuration;
81+
}
82+
83+
public KubernetesClient getClient() {
84+
return k8sClient;
85+
}
86+
87+
/**
88+
* Registers the specified controller with this operator, overriding its default configuration by
89+
* the specified one (usually created via
90+
* {@link io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider#override(ControllerConfiguration)},
91+
* passing it the controller's original configuration.
92+
*
93+
* @throws OperatorException if a problem occurred during the registration process
94+
*/
95+
public void start() throws OperatorException {
96+
final Class<R> resClass = configuration.getCustomResourceClass();
97+
final String controllerName = configuration.getName();
98+
final var crdName = configuration.getCRDName();
99+
final var specVersion = "v1";
100+
101+
// check that the custom resource is known by the cluster if configured that way
102+
final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config
103+
if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) {
104+
crd = k8sClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get();
105+
if (crd == null) {
106+
throwMissingCRDException(crdName, specVersion, controllerName);
107+
}
108+
109+
// Apply validations that are not handled by fabric8
110+
CustomResourceUtils.assertCustomResource(resClass, crd);
111+
}
112+
113+
try {
114+
DefaultEventSourceManager eventSourceManager =
115+
new DefaultEventSourceManager(
116+
controller, configuration, k8sClient.customResources(resClass));
117+
controller.init(eventSourceManager);
118+
} catch (MissingCRDException e) {
119+
throwMissingCRDException(crdName, specVersion, controllerName);
120+
}
121+
122+
if (failOnMissingCurrentNS()) {
123+
throw new OperatorException(
124+
"Controller '"
125+
+ controllerName
126+
+ "' is configured to watch the current namespace but it couldn't be inferred from the current configuration.");
127+
}
128+
}
129+
130+
private void throwMissingCRDException(String crdName, String specVersion, String controllerName) {
131+
throw new MissingCRDException(
132+
crdName,
133+
specVersion,
134+
"'"
135+
+ crdName
136+
+ "' "
137+
+ specVersion
138+
+ " CRD was not found on the cluster, controller '"
139+
+ controllerName
140+
+ "' cannot be registered");
141+
}
142+
143+
/**
144+
* Determines whether we should fail because the current namespace is request as target namespace
145+
* but is missing
146+
*
147+
* @return {@code true} if the current namespace is requested but is missing, {@code false}
148+
* otherwise
149+
*/
150+
private boolean failOnMissingCurrentNS() {
151+
if (configuration.watchCurrentNamespace()) {
152+
final var effectiveNamespaces = configuration.getEffectiveNamespaces();
153+
return effectiveNamespaces.size() == 1
154+
&& effectiveNamespaces.stream().allMatch(Objects::isNull);
155+
}
156+
return false;
157+
}
158+
159+
160+
@Override
161+
public void close() throws IOException {
162+
manager.close();
163+
}
164+
}

0 commit comments

Comments
 (0)