Skip to content

Commit 81090a6

Browse files
authored
feat: do not start operator if leader elector has no permission to lease object (#1912)
1 parent b156052 commit 81090a6

File tree

4 files changed

+135
-11
lines changed

4 files changed

+135
-11
lines changed

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

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package io.javaoperatorsdk.operator;
22

3+
import java.util.Arrays;
34
import java.util.UUID;
45
import java.util.concurrent.CompletableFuture;
56

67
import org.slf4j.Logger;
78
import org.slf4j.LoggerFactory;
89

10+
import io.fabric8.kubernetes.api.model.authorization.v1.*;
911
import io.fabric8.kubernetes.client.KubernetesClient;
1012
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks;
1113
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfig;
@@ -20,18 +22,26 @@ public class LeaderElectionManager {
2022

2123
private static final Logger log = LoggerFactory.getLogger(LeaderElectionManager.class);
2224

25+
public static final String NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE =
26+
"No permission to lease resource.";
27+
2328
private LeaderElector leaderElector = null;
2429
private final ControllerManager controllerManager;
2530
private String identity;
2631
private CompletableFuture<?> leaderElectionFuture;
32+
private KubernetesClient client;
33+
private String leaseName;
34+
private String leaseNamespace;
2735

2836
public LeaderElectionManager(ControllerManager controllerManager) {
2937
this.controllerManager = controllerManager;
3038
}
3139

3240
public void init(LeaderElectionConfiguration config, KubernetesClient client) {
41+
this.client = client;
3342
this.identity = identity(config);
34-
final var leaseNamespace =
43+
this.leaseName = config.getLeaseName();
44+
leaseNamespace =
3545
config.getLeaseNamespace().orElseGet(
3646
() -> ConfigurationServiceProvider.instance().getClientConfiguration().getNamespace());
3747
if (leaseNamespace == null) {
@@ -40,20 +50,19 @@ public void init(LeaderElectionConfiguration config, KubernetesClient client) {
4050
log.error(message);
4151
throw new IllegalArgumentException(message);
4252
}
43-
final var lock = new LeaseLock(leaseNamespace, config.getLeaseName(), identity);
53+
final var lock = new LeaseLock(leaseNamespace, leaseName, identity);
4454
// releaseOnCancel is not used in the underlying implementation
4555
leaderElector =
4656
new LeaderElectorBuilder(
4757
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()))
58+
.withConfig(new LeaderElectionConfig(
59+
lock,
60+
config.getLeaseDuration(),
61+
config.getRenewDeadline(),
62+
config.getRetryPeriod(),
63+
leaderCallbacks(),
64+
true,
65+
config.getLeaseName()))
5766
.build();
5867
}
5968

@@ -90,6 +99,7 @@ private String identity(LeaderElectionConfiguration config) {
9099

91100
public void start() {
92101
if (isLeaderElectionEnabled()) {
102+
checkLeaseAccess();
93103
leaderElectionFuture = leaderElector.start();
94104
}
95105
}
@@ -99,4 +109,21 @@ public void stop() {
99109
leaderElectionFuture.cancel(false);
100110
}
101111
}
112+
113+
private void checkLeaseAccess() {
114+
var verbs = Arrays.asList("create", "update", "get");
115+
SelfSubjectRulesReview review = new SelfSubjectRulesReview();
116+
review.setSpec(new SelfSubjectRulesReviewSpecBuilder().withNamespace(leaseNamespace).build());
117+
var reviewResult = client.resource(review).create();
118+
log.debug("SelfSubjectRulesReview result: {}", reviewResult);
119+
var foundRule = reviewResult.getStatus().getResourceRules().stream()
120+
.filter(rule -> rule.getApiGroups().contains("coordination.k8s.io")
121+
&& rule.getResources().contains("leases")
122+
&& (rule.getVerbs().containsAll(verbs)) || rule.getVerbs().contains("*"))
123+
.findAny();
124+
if (foundRule.isEmpty()) {
125+
throw new OperatorException(NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE +
126+
" in namespace: " + leaseNamespace);
127+
}
128+
}
102129
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.authorization.v1.*;
7+
import io.fabric8.kubernetes.api.model.rbac.Role;
8+
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
9+
import io.fabric8.kubernetes.client.ConfigBuilder;
10+
import io.fabric8.kubernetes.client.KubernetesClient;
11+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
12+
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
13+
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration;
14+
import io.javaoperatorsdk.operator.api.reconciler.Context;
15+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
16+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
17+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
18+
19+
import static io.javaoperatorsdk.operator.LeaderElectionManager.NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
23+
class LeaderElectionPermissionIT {
24+
25+
KubernetesClient adminClient = new KubernetesClientBuilder().build();
26+
27+
@Test
28+
void operatorStopsIfNoLeaderElectionPermission() {
29+
applyRole();
30+
applyRoleBinding();
31+
32+
var client = new KubernetesClientBuilder().withConfig(new ConfigBuilder()
33+
.withImpersonateUsername("leader-elector-stop-noaccess")
34+
.build()).build();
35+
36+
var operator = new Operator(client, o -> {
37+
o.withLeaderElectionConfiguration(
38+
new LeaderElectionConfiguration("lease1", "default"));
39+
o.withStopOnInformerErrorDuringStartup(false);
40+
});
41+
operator.register(new TestReconciler(), o -> o.settingNamespace("default"));
42+
43+
OperatorException exception = assertThrows(
44+
OperatorException.class,
45+
operator::start);
46+
47+
assertThat(exception.getCause().getMessage())
48+
.contains(NO_PERMISSION_TO_LEASE_RESOURCE_MESSAGE);
49+
ConfigurationServiceProvider.reset();
50+
}
51+
52+
53+
@ControllerConfiguration
54+
public static class TestReconciler implements Reconciler<ConfigMap> {
55+
@Override
56+
public UpdateControl<ConfigMap> reconcile(ConfigMap resource, Context<ConfigMap> context)
57+
throws Exception {
58+
throw new IllegalStateException("Should not get here");
59+
}
60+
}
61+
62+
private void applyRoleBinding() {
63+
var clusterRoleBinding = ReconcilerUtils
64+
.loadYaml(RoleBinding.class, this.getClass(),
65+
"leader-elector-stop-noaccess-role-binding.yaml");
66+
adminClient.resource(clusterRoleBinding).createOrReplace();
67+
}
68+
69+
private void applyRole() {
70+
var role = ReconcilerUtils
71+
.loadYaml(Role.class, this.getClass(), "leader-elector-stop-role-noaccess.yaml");
72+
adminClient.resource(role).createOrReplace();
73+
}
74+
}
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)