Skip to content

Commit d4842f1

Browse files
committed
[SPARK-57106] Disallow watched namespace changes via dynamic config
1 parent 9de9156 commit d4842f1

4 files changed

Lines changed: 12 additions & 121 deletions

File tree

spark-operator/src/main/java/org/apache/spark/k8s/operator/SparkOperator.java

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.time.Duration;
2727
import java.util.ArrayList;
2828
import java.util.Arrays;
29-
import java.util.HashSet;
3029
import java.util.List;
3130
import java.util.Set;
3231
import java.util.concurrent.ExecutorService;
@@ -37,7 +36,6 @@
3736
import io.fabric8.kubernetes.client.KubernetesClient;
3837
import io.fabric8.kubernetes.client.http.Interceptor;
3938
import io.javaoperatorsdk.operator.Operator;
40-
import io.javaoperatorsdk.operator.RegisteredController;
4139
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider;
4240
import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider;
4341
import lombok.extern.slf4j.Slf4j;
@@ -73,7 +71,6 @@ public class SparkOperator {
7371
private final SparkClusterSubmissionWorker clusterSubmissionWorker;
7472
private final SparkAppStatusRecorder sparkAppStatusRecorder;
7573
private final SparkClusterStatusRecorder sparkClusterStatusRecorder;
76-
protected Set<RegisteredController<?>> registeredSparkControllers;
7774
protected Set<String> watchedNamespaces;
7875
private final MetricsSystem metricsSystem;
7976
private final SentinelManager<SparkApplication> sparkApplicationSentinelManager;
@@ -95,7 +92,6 @@ public SparkOperator() {
9592
this.sparkAppStatusRecorder =
9693
new SparkAppStatusRecorder(getAppStatusListener(), recorderSource);
9794
this.sparkClusterStatusRecorder = new SparkClusterStatusRecorder(getClusterStatusListener());
98-
this.registeredSparkControllers = new HashSet<>();
9995
this.watchedNamespaces = getWatchedNamespaces();
10096
this.sparkApplicationSentinelManager = new SentinelManager<>();
10197
this.sparkClusterSentinelManager = new SentinelManager<>();
@@ -161,16 +157,14 @@ static void runSystemGc() {
161157
*/
162158
protected Operator registerSparkOperator() {
163159
Operator op = new Operator(this::overrideOperatorConfigs);
164-
registeredSparkControllers.add(
165-
op.register(
166-
new SparkAppReconciler(
167-
appSubmissionWorker, sparkAppStatusRecorder, sparkApplicationSentinelManager),
168-
this::overrideControllerConfigs));
169-
registeredSparkControllers.add(
170-
op.register(
171-
new SparkClusterReconciler(
172-
clusterSubmissionWorker, sparkClusterStatusRecorder, sparkClusterSentinelManager),
173-
this::overrideControllerConfigs));
160+
op.register(
161+
new SparkAppReconciler(
162+
appSubmissionWorker, sparkAppStatusRecorder, sparkApplicationSentinelManager),
163+
this::overrideControllerConfigs);
164+
op.register(
165+
new SparkClusterReconciler(
166+
clusterSubmissionWorker, sparkClusterStatusRecorder, sparkClusterSentinelManager),
167+
this::overrideControllerConfigs);
174168
return op;
175169
}
176170

@@ -188,8 +182,7 @@ protected Operator registerSparkOperatorConfMonitor() {
188182
operatorNamespace,
189183
confSelector);
190184
op.register(
191-
new SparkOperatorConfigMapReconciler(
192-
this::updateWatchingNamespaces, unused -> getWatchedNamespaces()),
185+
new SparkOperatorConfigMapReconciler(),
193186
c -> {
194187
c.withRateLimiter(SparkOperatorConf.getOperatorRateLimiter());
195188
c.settingNamespaces(operatorNamespace);
@@ -198,38 +191,6 @@ protected Operator registerSparkOperatorConfMonitor() {
198191
return op;
199192
}
200193

201-
/**
202-
* Updates the set of namespaces that the operator is watching.
203-
*
204-
* @param namespaces The new set of namespaces to watch.
205-
* @return True if the namespaces were updated, false otherwise.
206-
*/
207-
protected boolean updateWatchingNamespaces(Set<String> namespaces) {
208-
if (watchedNamespaces.equals(namespaces)) {
209-
log.info("No watched namespace change detected");
210-
return false;
211-
}
212-
if (watchedNamespaces.isEmpty()) {
213-
log.info("Cannot update watch namespaces for operator started at cluster level.");
214-
return false;
215-
}
216-
if (namespaces == null || namespaces.isEmpty()) {
217-
log.error("Cannot updating namespaces to empty");
218-
return false;
219-
}
220-
registeredSparkControllers.forEach(
221-
c -> {
222-
if (c.allowsNamespaceChanges()) {
223-
log.info("Updating operator namespaces to {}", namespaces);
224-
c.changeNamespaces(namespaces);
225-
} else {
226-
log.error("Controller does not allow namespace change, skipping namespace change.");
227-
}
228-
});
229-
this.watchedNamespaces = new HashSet<>(namespaces);
230-
return true;
231-
}
232-
233194
/**
234195
* Overrides the default configuration for the operator.
235196
*

spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConfigMapReconciler.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,23 @@
1919

2020
package org.apache.spark.k8s.operator.config;
2121

22-
import java.util.Set;
23-
import java.util.function.Function;
24-
2522
import io.fabric8.kubernetes.api.model.ConfigMap;
2623
import io.javaoperatorsdk.operator.api.reconciler.Context;
2724
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
2825
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
2926
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
3027
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
31-
import lombok.RequiredArgsConstructor;
3228
import lombok.extern.slf4j.Slf4j;
3329

3430
/**
3531
* This serves dynamic configuration for Spark Operator. When enabled, Operator assumes config file
3632
* is located in given config map. It would keep watch the config map and apply changes when update
37-
* is detected.
33+
* is detected. The set of watched namespaces is intentionally excluded from dynamic refresh and can
34+
* only be changed by restarting the operator with updated Helm values.
3835
*/
3936
@ControllerConfiguration
40-
@RequiredArgsConstructor
4137
@Slf4j
4238
public class SparkOperatorConfigMapReconciler implements Reconciler<ConfigMap> {
43-
private final Function<Set<String>, Boolean> namespaceUpdater;
44-
private final Function<Void, Set<String>> watchedNamespacesGetter;
4539

4640
/**
4741
* Updates the error status of the ConfigMap reconciliation.
@@ -70,7 +64,6 @@ public ErrorStatusUpdateControl<ConfigMap> updateErrorStatus(
7064
public UpdateControl<ConfigMap> reconcile(ConfigMap resource, Context<ConfigMap> context)
7165
throws Exception {
7266
SparkOperatorConfManager.INSTANCE.refresh(resource.getData());
73-
namespaceUpdater.apply(watchedNamespacesGetter.apply(null));
7467
return UpdateControl.noUpdate();
7568
}
7669
}

spark-operator/src/test/java/org/apache/spark/k8s/operator/SparkOperatorTest.java

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,12 @@
2626
import static org.mockito.Mockito.mockConstruction;
2727
import static org.mockito.Mockito.mockStatic;
2828
import static org.mockito.Mockito.verify;
29-
import static org.mockito.Mockito.verifyNoMoreInteractions;
30-
import static org.mockito.Mockito.when;
3129

3230
import java.util.Set;
3331
import java.util.function.Consumer;
3432

3533
import io.fabric8.kubernetes.client.KubernetesClient;
3634
import io.javaoperatorsdk.operator.Operator;
37-
import io.javaoperatorsdk.operator.RegisteredController;
3835
import org.junit.jupiter.api.Assertions;
3936
import org.junit.jupiter.api.Test;
4037
import org.mockito.MockedConstruction;
@@ -49,7 +46,6 @@
4946
import org.apache.spark.k8s.operator.metrics.source.KubernetesMetricsInterceptor;
5047
import org.apache.spark.k8s.operator.probe.ProbeService;
5148
import org.apache.spark.k8s.operator.reconciler.SparkAppReconciler;
52-
import org.apache.spark.k8s.operator.reconciler.SparkClusterReconciler;
5349
import org.apache.spark.k8s.operator.utils.Utils;
5450

5551
class SparkOperatorTest {
@@ -86,7 +82,6 @@ void testOperatorConstructionWithDynamicConfigEnabled() {
8682
mockUtils.when(Utils::getWatchedNamespaces).thenReturn(Set.of("namespace-1"));
8783

8884
SparkOperator sparkOperator = new SparkOperator();
89-
Assertions.assertEquals(1, sparkOperator.registeredSparkControllers.size());
9085
Assertions.assertEquals(2, operatorConstruction.constructed().size());
9186
Assertions.assertEquals(1, sparkAppReconcilerConstruction.constructed().size());
9287
Assertions.assertEquals(1, configReconcilerConstruction.constructed().size());
@@ -129,7 +124,6 @@ void testOperatorConstructionWithDynamicConfigDisabled() {
129124
.when(() -> KubernetesClientFactory.buildKubernetesClient(any()))
130125
.thenReturn(mockClient);
131126
SparkOperator sparkOperator = new SparkOperator();
132-
Assertions.assertEquals(1, sparkOperator.registeredSparkControllers.size());
133127
Assertions.assertEquals(1, operatorConstruction.constructed().size());
134128
Assertions.assertEquals(1, sparkAppReconcilerConstruction.constructed().size());
135129
Assertions.assertEquals(1, probeServiceConstruction.constructed().size());
@@ -140,57 +134,4 @@ void testOperatorConstructionWithDynamicConfigDisabled() {
140134
setConfigKey(SparkOperatorConf.DYNAMIC_CONFIG_ENABLED, dynamicConfigEnabled);
141135
}
142136
}
143-
144-
@SuppressWarnings("PMD.UnusedLocalVariable")
145-
@Test
146-
void testUpdateWatchedNamespacesWithDynamicConfigEnabled() {
147-
MetricsSystem mockMetricsSystem = mock(MetricsSystem.class);
148-
KubernetesClient mockClient = mock(KubernetesClient.class);
149-
var registeredController = mock(RegisteredController.class);
150-
when(registeredController.allowsNamespaceChanges()).thenReturn(true);
151-
boolean dynamicConfigEnabled = SparkOperatorConf.DYNAMIC_CONFIG_ENABLED.getValue();
152-
153-
try (MockedStatic<MetricsSystemFactory> mockMetricsSystemFactory =
154-
mockStatic(MetricsSystemFactory.class);
155-
MockedStatic<KubernetesClientFactory> mockKubernetesClientFactory =
156-
mockStatic(KubernetesClientFactory.class);
157-
MockedStatic<Utils> mockUtils = mockStatic(Utils.class);
158-
MockedConstruction<Operator> operatorConstruction =
159-
mockConstruction(
160-
Operator.class,
161-
(mock, context) -> {
162-
when(mock.register(any(SparkAppReconciler.class), any(Consumer.class)))
163-
.thenReturn(registeredController);
164-
when(mock.register(any(SparkClusterReconciler.class), any(Consumer.class)))
165-
.thenReturn(registeredController);
166-
});
167-
MockedConstruction<SparkAppReconciler> sparkAppReconcilerConstruction =
168-
mockConstruction(SparkAppReconciler.class);
169-
MockedConstruction<SparkOperatorConfigMapReconciler> configReconcilerConstruction =
170-
mockConstruction(SparkOperatorConfigMapReconciler.class);
171-
MockedConstruction<ProbeService> probeServiceConstruction =
172-
mockConstruction(ProbeService.class);
173-
MockedConstruction<MetricsService> metricsServiceConstruction =
174-
mockConstruction(MetricsService.class);
175-
MockedConstruction<KubernetesMetricsInterceptor> interceptorMockedConstruction =
176-
mockConstruction(KubernetesMetricsInterceptor.class)) {
177-
setConfigKey(SparkOperatorConf.DYNAMIC_CONFIG_ENABLED, true);
178-
mockMetricsSystemFactory
179-
.when(MetricsSystemFactory::createMetricsSystem)
180-
.thenReturn(mockMetricsSystem);
181-
mockKubernetesClientFactory
182-
.when(() -> KubernetesClientFactory.buildKubernetesClient(any()))
183-
.thenReturn(mockClient);
184-
mockUtils.when(Utils::getWatchedNamespaces).thenReturn(Set.of("namespace-1"));
185-
SparkOperator sparkOperator = new SparkOperator();
186-
Set<String> updatedNamespaces = Set.of("namespace-1", "namespace-2");
187-
Assertions.assertTrue(sparkOperator.updateWatchingNamespaces(updatedNamespaces));
188-
Assertions.assertEquals(updatedNamespaces, sparkOperator.watchedNamespaces);
189-
verify(registeredController).allowsNamespaceChanges();
190-
verify(registeredController).changeNamespaces(updatedNamespaces);
191-
verifyNoMoreInteractions(registeredController);
192-
} finally {
193-
setConfigKey(SparkOperatorConf.DYNAMIC_CONFIG_ENABLED, dynamicConfigEnabled);
194-
}
195-
}
196137
}

spark-operator/src/test/java/org/apache/spark/k8s/operator/config/SparkOperatorConfigMapReconcilerTest.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
import static org.apache.spark.k8s.operator.config.SparkOperatorConf.RECONCILER_INTERVAL_SECONDS;
2323
import static org.assertj.core.api.Assertions.assertThat;
2424
import static org.awaitility.Awaitility.await;
25-
import static org.mockito.Mockito.mock;
2625

2726
import java.util.Map;
28-
import java.util.function.Function;
2927

3028
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3129
import io.fabric8.kubeapitest.junit.EnableKubeAPIServer;
@@ -50,10 +48,8 @@ class SparkOperatorConfigMapReconcilerTest {
5048
Operator operator;
5149

5250
@BeforeEach
53-
@SuppressWarnings("unchecked")
5451
void startController() {
55-
var reconciler =
56-
new SparkOperatorConfigMapReconciler(mock(Function.class), mock(Function.class));
52+
var reconciler = new SparkOperatorConfigMapReconciler();
5753
operator = new Operator(o -> o.withKubernetesClient(client));
5854
operator.register(reconciler);
5955
operator.start();

0 commit comments

Comments
 (0)