Skip to content

Commit eff9835

Browse files
authored
Try to get lease namespace if unspecified (#1450)
1 parent c4454f6 commit eff9835

File tree

8 files changed

+215
-26
lines changed

8 files changed

+215
-26
lines changed

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

+30-12
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElector;
1313
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectorBuilder;
1414
import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock;
15-
import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.Lock;
1615
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
1716
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration;
1817

@@ -31,24 +30,43 @@ public LeaderElectionManager(ControllerManager controllerManager) {
3130

3231
public void init(LeaderElectionConfiguration config, KubernetesClient client) {
3332
this.identity = identity(config);
34-
Lock lock = new LeaseLock(config.getLeaseNamespace(), config.getLeaseName(), identity);
33+
final var leaseNamespace =
34+
config.getLeaseNamespace().orElseGet(
35+
() -> ConfigurationServiceProvider.instance().getClientConfiguration().getNamespace());
36+
if (leaseNamespace == null) {
37+
final var message =
38+
"Lease namespace is not set and cannot be inferred. Leader election cannot continue.";
39+
log.error(message);
40+
throw new IllegalArgumentException(message);
41+
}
42+
final var lock = new LeaseLock(leaseNamespace, config.getLeaseName(), identity);
3543
// releaseOnCancel is not used in the underlying implementation
36-
leaderElector = new LeaderElectorBuilder(client,
37-
ConfigurationServiceProvider.instance().getExecutorService())
38-
.withConfig(
39-
new LeaderElectionConfig(lock, config.getLeaseDuration(), config.getRenewDeadline(),
40-
config.getRetryPeriod(), leaderCallbacks(), true, config.getLeaseName()))
41-
.build();
44+
leaderElector =
45+
new LeaderElectorBuilder(
46+
client, ConfigurationServiceProvider.instance().getExecutorService())
47+
.withConfig(
48+
new LeaderElectionConfig(
49+
lock,
50+
config.getLeaseDuration(),
51+
config.getRenewDeadline(),
52+
config.getRetryPeriod(),
53+
leaderCallbacks(),
54+
true,
55+
config.getLeaseName()))
56+
.build();
4257
}
4358

4459
public boolean isLeaderElectionEnabled() {
4560
return leaderElector != null;
4661
}
4762

4863
private LeaderCallbacks leaderCallbacks() {
49-
return new LeaderCallbacks(this::startLeading, this::stopLeading, leader -> {
50-
log.info("New leader with identity: {}", leader);
51-
});
64+
return new LeaderCallbacks(
65+
this::startLeading,
66+
this::stopLeading,
67+
leader -> {
68+
log.info("New leader with identity: {}", leader);
69+
});
5270
}
5371

5472
private void startLeading() {
@@ -64,7 +82,7 @@ private void stopLeading() {
6482
}
6583

6684
private String identity(LeaderElectionConfiguration config) {
67-
String id = config.getIdentity().orElse(System.getenv("HOSTNAME"));
85+
var id = config.getIdentity().orElse(System.getenv("HOSTNAME"));
6886
if (id == null || id.isBlank()) {
6987
id = UUID.randomUUID().toString();
7088
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ public LeaderElectionConfiguration(String leaseName, String leaseNamespace) {
3535
RETRY_PERIOD_DEFAULT_VALUE, null);
3636
}
3737

38+
public LeaderElectionConfiguration(String leaseName) {
39+
this(
40+
leaseName,
41+
null,
42+
LEASE_DURATION_DEFAULT_VALUE,
43+
RENEW_DEADLINE_DEFAULT_VALUE,
44+
RETRY_PERIOD_DEFAULT_VALUE, null);
45+
}
46+
3847
public LeaderElectionConfiguration(
3948
String leaseName,
4049
String leaseNamespace,
@@ -59,8 +68,8 @@ public LeaderElectionConfiguration(
5968
this.identity = identity;
6069
}
6170

62-
public String getLeaseNamespace() {
63-
return leaseNamespace;
71+
public Optional<String> getLeaseNamespace() {
72+
return Optional.ofNullable(leaseNamespace);
6473
}
6574

6675
public String getLeaseName() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
7+
import org.junit.jupiter.api.AfterEach;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.io.TempDir;
11+
12+
import io.fabric8.kubernetes.client.KubernetesClient;
13+
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
14+
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration;
15+
16+
import static io.fabric8.kubernetes.client.Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY;
17+
import static io.fabric8.kubernetes.client.Config.KUBERNETES_NAMESPACE_FILE;
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
import static org.mockito.Mockito.mock;
21+
22+
class LeaderElectionManagerTest {
23+
24+
private ControllerManager controllerManager;
25+
private KubernetesClient kubernetesClient;
26+
private LeaderElectionManager leaderElectionManager;
27+
28+
@BeforeEach
29+
void setUp() {
30+
controllerManager = mock(ControllerManager.class);
31+
kubernetesClient = mock(KubernetesClient.class);
32+
leaderElectionManager = new LeaderElectionManager(controllerManager);
33+
}
34+
35+
@AfterEach
36+
void tearDown() {
37+
ConfigurationServiceProvider.reset();
38+
System.getProperties().remove(KUBERNETES_NAMESPACE_FILE);
39+
System.getProperties().remove(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY);
40+
}
41+
42+
@Test
43+
void testInit() {
44+
leaderElectionManager.init(new LeaderElectionConfiguration("test", "testns"), kubernetesClient);
45+
assertTrue(leaderElectionManager.isLeaderElectionEnabled());
46+
}
47+
48+
@Test
49+
void testInitInferLeaseNamespace(@TempDir Path tempDir) throws IOException {
50+
var namespace = "foo";
51+
var namespacePath = tempDir.resolve("namespace");
52+
Files.writeString(namespacePath, namespace);
53+
54+
System.setProperty(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
55+
System.setProperty(KUBERNETES_NAMESPACE_FILE, namespacePath.toString());
56+
57+
leaderElectionManager.init(new LeaderElectionConfiguration("test"), kubernetesClient);
58+
assertTrue(leaderElectionManager.isLeaderElectionEnabled());
59+
}
60+
61+
@Test
62+
void testFailedToInitInferLeaseNamespace() {
63+
System.setProperty(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
64+
assertThrows(
65+
IllegalArgumentException.class,
66+
() -> leaderElectionManager.init(new LeaderElectionConfiguration("test"),
67+
kubernetesClient));
68+
}
69+
70+
@Test
71+
void testFailedToInitInferLeaseNamespaceProbablyUsingKubeConfig() {
72+
assertThrows(
73+
IllegalArgumentException.class,
74+
() -> leaderElectionManager.init(new LeaderElectionConfiguration("test"),
75+
kubernetesClient));
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: leader-election-operator-2
5+
spec:
6+
serviceAccountName: leader-election-operator
7+
containers:
8+
- name: operator
9+
image: leader-election-operator
10+
imagePullPolicy: Never
11+
env:
12+
- name: POD_NAME
13+
valueFrom:
14+
fieldRef:
15+
fieldPath: metadata.name
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
apiVersion: v1
2+
kind: ServiceAccount
3+
metadata:
4+
name: leader-election-operator
5+
6+
---
7+
apiVersion: v1
8+
kind: Pod
9+
metadata:
10+
name: leader-election-operator-1
11+
spec:
12+
serviceAccountName: leader-election-operator
13+
containers:
14+
- name: operator
15+
image: leader-election-operator
16+
imagePullPolicy: Never
17+
env:
18+
- name: POD_NAME
19+
valueFrom:
20+
fieldRef:
21+
fieldPath: metadata.name
22+
23+
---
24+
apiVersion: rbac.authorization.k8s.io/v1
25+
kind: ClusterRoleBinding
26+
metadata:
27+
name: operator-admin
28+
subjects:
29+
- kind: ServiceAccount
30+
name: leader-election-operator
31+
roleRef:
32+
kind: ClusterRole
33+
name: leader-election-operator
34+
apiGroup: ""
35+
36+
---
37+
apiVersion: rbac.authorization.k8s.io/v1
38+
kind: ClusterRole
39+
metadata:
40+
name: leader-election-operator
41+
rules:
42+
- apiGroups:
43+
- "apiextensions.k8s.io"
44+
resources:
45+
- customresourcedefinitions
46+
verbs:
47+
- '*'
48+
- apiGroups:
49+
- "sample.javaoperatorsdk"
50+
resources:
51+
- leaderelectiontestcustomresources
52+
- leaderelectiontestcustomresources/status
53+
verbs:
54+
- '*'
55+
- apiGroups:
56+
- "coordination.k8s.io"
57+
resources:
58+
- "leases"
59+
verbs:
60+
- '*'

sample-operators/leader-election/pom.xml

+6-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
<version>${project.version}</version>
5252
<scope>test</scope>
5353
</dependency>
54+
<dependency>
55+
<groupId>org.junit.jupiter</groupId>
56+
<artifactId>junit-jupiter-params</artifactId>
57+
<scope>test</scope>
58+
</dependency>
5459
</dependencies>
5560
<build>
5661
<plugins>
@@ -75,4 +80,4 @@
7580
</plugins>
7681
</build>
7782

78-
</project>
83+
</project>

sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ public static void main(String[] args) {
1717

1818
log.info("Starting operator with identity: {}", identity);
1919

20+
LeaderElectionConfiguration leaderElectionConfiguration =
21+
namespace == null
22+
? new LeaderElectionConfiguration("leader-election-test")
23+
: new LeaderElectionConfiguration("leader-election-test", namespace, identity);
24+
2025
var client = new KubernetesClientBuilder().build();
21-
Operator operator = new Operator(client,
22-
c -> c.withLeaderElectionConfiguration(
23-
new LeaderElectionConfiguration("leader-election-test", namespace, identity)));
26+
Operator operator =
27+
new Operator(client, c -> c.withLeaderElectionConfiguration(leaderElectionConfiguration));
2428

2529
operator.register(new LeaderElectionTestReconciler(identity));
2630
operator.installShutdownHook();
2731
operator.start();
2832
}
29-
3033
}

sample-operators/leader-election/src/test/java/io/javaoperatorsdk/operator/sample/LeaderElectionE2E.java

+9-7
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
import org.junit.jupiter.api.AfterEach;
1515
import org.junit.jupiter.api.BeforeEach;
16-
import org.junit.jupiter.api.Test;
1716
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
17+
import org.junit.jupiter.params.ParameterizedTest;
18+
import org.junit.jupiter.params.provider.ValueSource;
1819
import org.slf4j.Logger;
1920
import org.slf4j.LoggerFactory;
2021

@@ -46,14 +47,15 @@ class LeaderElectionE2E {
4647
private String namespace;
4748
private KubernetesClient client;
4849

49-
@Test
50+
@ParameterizedTest
51+
@ValueSource(strings = {"namespace-inferred-", ""})
5052
// not for local mode by design
5153
@EnabledIfSystemProperty(named = "test.deployment", matches = "remote")
52-
void otherInstancesTakesOverWhenSteppingDown() {
54+
void otherInstancesTakesOverWhenSteppingDown(String yamlFilePrefix) {
5355
log.info("Applying custom resource");
5456
applyCustomResource();
5557
log.info("Deploying operator instances");
56-
deployOperatorsInOrder();
58+
deployOperatorsInOrder(yamlFilePrefix);
5759

5860
log.info("Awaiting custom resource reconciliations");
5961
await().pollDelay(Duration.ofSeconds(MINIMAL_SECONDS_FOR_RENEWAL))
@@ -130,16 +132,16 @@ void tearDown() {
130132
.untilAsserted(() -> assertThat(client.namespaces().withName(namespace).get()).isNull());
131133
}
132134

133-
private void deployOperatorsInOrder() {
135+
private void deployOperatorsInOrder(String yamlFilePrefix) {
134136
log.info("Installing 1st instance");
135-
applyResources("k8s/operator.yaml");
137+
applyResources("k8s/" + yamlFilePrefix + "operator.yaml");
136138
await().atMost(Duration.ofSeconds(POD_STARTUP_TIMEOUT)).untilAsserted(() -> {
137139
var pod = client.pods().inNamespace(namespace).withName(OPERATOR_1_POD_NAME).get();
138140
assertThat(pod.getStatus().getContainerStatuses().get(0).getReady()).isTrue();
139141
});
140142

141143
log.info("Installing 2nd instance");
142-
applyResources("k8s/operator-instance-2.yaml");
144+
applyResources("k8s/" + yamlFilePrefix + "operator-instance-2.yaml");
143145
await().atMost(Duration.ofSeconds(POD_STARTUP_TIMEOUT)).untilAsserted(() -> {
144146
var pod = client.pods().inNamespace(namespace).withName(OPERATOR_2_POD_NAME).get();
145147
assertThat(pod.getStatus().getContainerStatuses().get(0).getReady()).isTrue();

0 commit comments

Comments
 (0)