Skip to content

Commit 18f396a

Browse files
committed
feat: stops operator if leader elector has no permission to lease object
1 parent 92f2f28 commit 18f396a

File tree

4 files changed

+125
-9
lines changed

4 files changed

+125
-9
lines changed

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.slf4j.LoggerFactory;
88

99
import io.fabric8.kubernetes.client.KubernetesClient;
10+
import io.fabric8.kubernetes.client.KubernetesClientException;
1011
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks;
1112
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfig;
1213
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElector;
@@ -20,16 +21,22 @@ public class LeaderElectionManager {
2021

2122
private static final Logger log = LoggerFactory.getLogger(LeaderElectionManager.class);
2223

24+
public static final String NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE =
25+
"No permission to lease resource.";
26+
2327
private LeaderElector leaderElector = null;
2428
private final ControllerManager controllerManager;
2529
private String identity;
2630
private CompletableFuture<?> leaderElectionFuture;
31+
private LeaderElectionConfig leaderElectionConfig;
32+
private KubernetesClient client;
2733

2834
public LeaderElectionManager(ControllerManager controllerManager) {
2935
this.controllerManager = controllerManager;
3036
}
3137

3238
public void init(LeaderElectionConfiguration config, KubernetesClient client) {
39+
this.client = client;
3340
this.identity = identity(config);
3441
final var leaseNamespace =
3542
config.getLeaseNamespace().orElseGet(
@@ -42,18 +49,19 @@ public void init(LeaderElectionConfiguration config, KubernetesClient client) {
4249
}
4350
final var lock = new LeaseLock(leaseNamespace, config.getLeaseName(), identity);
4451
// releaseOnCancel is not used in the underlying implementation
52+
leaderElectionConfig = new LeaderElectionConfig(
53+
lock,
54+
config.getLeaseDuration(),
55+
config.getRenewDeadline(),
56+
config.getRetryPeriod(),
57+
leaderCallbacks(),
58+
true,
59+
config.getLeaseName());
60+
4561
leaderElector =
4662
new LeaderElectorBuilder(
4763
client, ExecutorServiceManager.instance().executorService())
48-
.withConfig(
49-
new LeaderElectionConfig(
50-
lock,
51-
config.getLeaseDuration(),
52-
config.getRenewDeadline(),
53-
config.getRetryPeriod(),
54-
leaderCallbacks(),
55-
true,
56-
config.getLeaseName()))
64+
.withConfig(leaderElectionConfig)
5765
.build();
5866
}
5967

@@ -90,6 +98,7 @@ private String identity(LeaderElectionConfiguration config) {
9098

9199
public void start() {
92100
if (isLeaderElectionEnabled()) {
101+
checkLeaseAccess();
93102
leaderElectionFuture = leaderElector.start();
94103
}
95104
}
@@ -99,4 +108,16 @@ public void stop() {
99108
leaderElectionFuture.cancel(false);
100109
}
101110
}
111+
112+
private void checkLeaseAccess() {
113+
try {
114+
leaderElectionConfig.getLock().get(client);
115+
} catch (KubernetesClientException e) {
116+
if (e.getCode() == 403) {
117+
throw new OperatorException(NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE, e);
118+
} else {
119+
throw e;
120+
}
121+
}
122+
}
102123
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import io.fabric8.kubernetes.api.model.ConfigMap;
6+
import io.fabric8.kubernetes.api.model.rbac.Role;
7+
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
8+
import io.fabric8.kubernetes.client.ConfigBuilder;
9+
import io.fabric8.kubernetes.client.KubernetesClient;
10+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
11+
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration;
12+
import io.javaoperatorsdk.operator.api.reconciler.Context;
13+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
14+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
15+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
16+
17+
import static io.javaoperatorsdk.operator.LeaderElectionManager.NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE;
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.junit.jupiter.api.Assertions.assertThrows;
20+
21+
class LeaderElectionPermissionIT {
22+
23+
KubernetesClient adminClient = new KubernetesClientBuilder().build();
24+
25+
@Test
26+
void operatorStopsIfNoLeaderElectionPermission() {
27+
applyRole();
28+
applyRoleBinding();
29+
30+
var client = new KubernetesClientBuilder().withConfig(new ConfigBuilder()
31+
.withImpersonateUsername("leader-elector-stop-noaccess")
32+
.build()).build();
33+
34+
var operator = new Operator(client, o -> {
35+
o.withLeaderElectionConfiguration(
36+
new LeaderElectionConfiguration("lease1", "default"));
37+
o.withStopOnInformerErrorDuringStartup(false);
38+
});
39+
operator.register(new TestReconciler(), o -> o.settingNamespace("default"));
40+
41+
OperatorException exception = assertThrows(
42+
OperatorException.class,
43+
operator::start);
44+
45+
assertThat(exception.getCause().getMessage())
46+
.isEqualTo(NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE);
47+
}
48+
49+
50+
@ControllerConfiguration
51+
public static class TestReconciler implements Reconciler<ConfigMap> {
52+
@Override
53+
public UpdateControl<ConfigMap> reconcile(ConfigMap resource, Context<ConfigMap> context)
54+
throws Exception {
55+
throw new IllegalStateException("Should not get here");
56+
}
57+
}
58+
59+
private void applyRoleBinding() {
60+
var clusterRoleBinding = ReconcilerUtils
61+
.loadYaml(RoleBinding.class, this.getClass(),
62+
"leader-elector-stop-noaccess-role-binding.yaml");
63+
adminClient.resource(clusterRoleBinding).createOrReplace();
64+
}
65+
66+
private void applyRole() {
67+
var role = ReconcilerUtils
68+
.loadYaml(Role.class, this.getClass(), "leader-elector-stop-role-noaccess.yaml");
69+
adminClient.resource(role).createOrReplace();
70+
}
71+
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
# This cluster role binding allows anyone in the "manager" group to read secrets in any namespace.
3+
kind: RoleBinding
4+
metadata:
5+
name: informer-rbac-startup-global
6+
namespace: default
7+
subjects:
8+
- kind: User
9+
name: leader-elector-stop-noaccess
10+
apiGroup: rbac.authorization.k8s.io
11+
roleRef:
12+
kind: Role
13+
name: leader-elector-stop-noaccess
14+
apiGroup: rbac.authorization.k8s.io
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
kind: Role
3+
metadata:
4+
namespace: default
5+
name: leader-elector-stop-noaccess
6+
rules:
7+
- apiGroups: [ "" ]
8+
resources: [ "configmaps" ]
9+
verbs: [ "get", "watch", "list","post", "delete", "create" ]

0 commit comments

Comments
 (0)