Skip to content

Commit 90ea84e

Browse files
authored
fix: change namespace starts processor on namespace change even if not leader (operator-framework#2344)
Signed-off-by: Attila Mészáros <csviri@gmail.com>
1 parent 6daf2dc commit 90ea84e

File tree

5 files changed

+157
-3
lines changed

5 files changed

+157
-3
lines changed

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

+11-3
Original file line numberDiff line numberDiff line change
@@ -368,17 +368,25 @@ private void validateCRDWithLocalModelIfRequired(Class<P> resClass, String contr
368368
}
369369
}
370370

371-
public void changeNamespaces(Set<String> namespaces) {
371+
public synchronized void changeNamespaces(Set<String> namespaces) {
372372
if (namespaces.contains(WATCH_CURRENT_NAMESPACE)) {
373373
throw new OperatorException("Unexpected value in target namespaces: " + namespaces);
374374
}
375375
if (namespaces.contains(Constants.WATCH_ALL_NAMESPACES) && namespaces.size() > 1) {
376376
throw new OperatorException(
377377
"Watching all namespaces, but additional specific namespace is present");
378378
}
379-
eventProcessor.stop();
379+
// if the processor was not running, for example because the controller
380+
// was not leading in a HA setup, we don't want to stop and
381+
// mainly start the processor on namespace change.
382+
boolean eventProcessorWasRunning = eventProcessor.isRunning();
383+
if (eventProcessorWasRunning) {
384+
eventProcessor.stop();
385+
}
380386
eventSourceManager.changeNamespaces(namespaces);
381-
eventProcessor.start();
387+
if (eventProcessorWasRunning) {
388+
eventProcessor.start();
389+
}
382390
}
383391

384392
public synchronized void startEventProcessing() {

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

+4
Original file line numberDiff line numberDiff line change
@@ -473,4 +473,8 @@ private String controllerName() {
473473
public synchronized boolean isUnderProcessing(ResourceID resourceID) {
474474
return isControllerUnderExecution(resourceStateManager.getOrCreate(resourceID));
475475
}
476+
477+
public synchronized boolean isRunning() {
478+
return running;
479+
}
476480
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.time.Duration;
4+
import java.time.ZonedDateTime;
5+
6+
import org.junit.jupiter.api.AfterAll;
7+
import org.junit.jupiter.api.BeforeAll;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.RegisterExtension;
11+
12+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
13+
import io.fabric8.kubernetes.api.model.coordination.v1.Lease;
14+
import io.fabric8.kubernetes.api.model.coordination.v1.LeaseSpecBuilder;
15+
import io.fabric8.kubernetes.client.KubernetesClient;
16+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
17+
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration;
18+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
19+
import io.javaoperatorsdk.operator.sample.leaderelectionchangenamespace.LeaderElectionChangeNamespaceCustomResource;
20+
import io.javaoperatorsdk.operator.sample.leaderelectionchangenamespace.LeaderElectionChangeNamespaceReconciler;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.awaitility.Awaitility.await;
24+
25+
public class LeaderElectionChangeNamespaceIT {
26+
27+
public static final String LEASE_NAME = "nschangelease";
28+
29+
@RegisterExtension
30+
LocallyRunOperatorExtension extension =
31+
LocallyRunOperatorExtension.builder()
32+
.withConfigurationService(o -> o.withLeaderElectionConfiguration(
33+
new LeaderElectionConfiguration(LEASE_NAME)))
34+
.withReconciler(new LeaderElectionChangeNamespaceReconciler())
35+
.build();
36+
37+
private static KubernetesClient client = new KubernetesClientBuilder().build();
38+
39+
@BeforeAll
40+
static void createLeaseManually() {
41+
client.resource(lease()).create();
42+
}
43+
44+
@AfterAll
45+
static void deleteLeaseManually() {
46+
client.resource(lease()).delete();
47+
}
48+
49+
@Test
50+
@DisplayName("If operator is not a leader, namespace change should not start processor")
51+
void noReconcileOnChangeNamespace() {
52+
extension.create(testResource());
53+
54+
var reconciler = extension.getReconcilerOfType(LeaderElectionChangeNamespaceReconciler.class);
55+
await().pollDelay(Duration.ofSeconds(1))
56+
.timeout(Duration.ofSeconds(3))
57+
.untilAsserted(() -> {
58+
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(0);
59+
});
60+
61+
extension.getRegisteredControllerForReconcile(LeaderElectionChangeNamespaceReconciler.class)
62+
.changeNamespaces("default", extension.getNamespace());
63+
64+
await().pollDelay(Duration.ofSeconds(1))
65+
.timeout(Duration.ofSeconds(3))
66+
.untilAsserted(() -> {
67+
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(0);
68+
});
69+
}
70+
71+
72+
LeaderElectionChangeNamespaceCustomResource testResource() {
73+
var resource = new LeaderElectionChangeNamespaceCustomResource();
74+
resource.setMetadata(new ObjectMetaBuilder()
75+
.withName("test1")
76+
.build());
77+
return resource;
78+
}
79+
80+
static Lease lease() {
81+
var lease = new Lease();
82+
lease.setMetadata(new ObjectMetaBuilder()
83+
.withName(LEASE_NAME)
84+
.withNamespace("default")
85+
.build());
86+
var time = ZonedDateTime.now();
87+
lease.setSpec(new LeaseSpecBuilder()
88+
.withAcquireTime(ZonedDateTime.now())
89+
.withRenewTime(time)
90+
.withAcquireTime(time)
91+
.withHolderIdentity("non-operator-identity")
92+
.withLeaseTransitions(0)
93+
.withLeaseDurationSeconds(30)
94+
.build());
95+
96+
return lease;
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.leaderelectionchangenamespace;
2+
3+
import io.fabric8.kubernetes.api.model.Namespaced;
4+
import io.fabric8.kubernetes.client.CustomResource;
5+
import io.fabric8.kubernetes.model.annotation.Group;
6+
import io.fabric8.kubernetes.model.annotation.ShortNames;
7+
import io.fabric8.kubernetes.model.annotation.Version;
8+
9+
@Group("sample.javaoperatorsdk")
10+
@Version("v1")
11+
@ShortNames("lcn")
12+
public class LeaderElectionChangeNamespaceCustomResource
13+
extends CustomResource<Void, Void>
14+
implements Namespaced {
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.javaoperatorsdk.operator.sample.leaderelectionchangenamespace;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
import io.javaoperatorsdk.operator.api.reconciler.Context;
6+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
7+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
8+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
9+
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;
10+
11+
@ControllerConfiguration()
12+
public class LeaderElectionChangeNamespaceReconciler
13+
implements Reconciler<LeaderElectionChangeNamespaceCustomResource>, TestExecutionInfoProvider {
14+
15+
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
16+
17+
@Override
18+
public UpdateControl<LeaderElectionChangeNamespaceCustomResource> reconcile(
19+
LeaderElectionChangeNamespaceCustomResource resource,
20+
Context<LeaderElectionChangeNamespaceCustomResource> context) {
21+
numberOfExecutions.addAndGet(1);
22+
return UpdateControl.noUpdate();
23+
}
24+
25+
public int getNumberOfExecutions() {
26+
return numberOfExecutions.get();
27+
}
28+
29+
}

0 commit comments

Comments
 (0)