Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.javaoperatorsdk.operator;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

Expand All @@ -12,10 +16,13 @@
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElector;
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectorBuilder;
import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock;
import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.Lock;
import io.fabric8.kubernetes.client.utils.Utils;
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration;

import static io.fabric8.kubernetes.client.Config.KUBERNETES_NAMESPACE_FILE;
import static io.fabric8.kubernetes.client.Config.KUBERNETES_NAMESPACE_PATH;

public class LeaderElectionManager {

private static final Logger log = LoggerFactory.getLogger(LeaderElectionManager.class);
Expand All @@ -31,24 +38,42 @@ public LeaderElectionManager(ControllerManager controllerManager) {

public void init(LeaderElectionConfiguration config, KubernetesClient client) {
this.identity = identity(config);
Lock lock = new LeaseLock(config.getLeaseNamespace(), config.getLeaseName(), identity);
final var leaseNamespace =
config.getLeaseNamespace().or(LeaderElectionManager::tryGetInClusterNamespace);
if (leaseNamespace.isEmpty()) {
final var message =
"Lease namespace is not set and cannot be inferred. Leader election cannot continue.";
log.error(message);
throw new IllegalArgumentException(message);
}
final var lock = new LeaseLock(leaseNamespace.orElseThrow(), config.getLeaseName(), identity);
// releaseOnCancel is not used in the underlying implementation
leaderElector = new LeaderElectorBuilder(client,
ConfigurationServiceProvider.instance().getExecutorService())
.withConfig(
new LeaderElectionConfig(lock, config.getLeaseDuration(), config.getRenewDeadline(),
config.getRetryPeriod(), leaderCallbacks(), true, config.getLeaseName()))
.build();
leaderElector =
new LeaderElectorBuilder(
client, ConfigurationServiceProvider.instance().getExecutorService())
.withConfig(
new LeaderElectionConfig(
lock,
config.getLeaseDuration(),
config.getRenewDeadline(),
config.getRetryPeriod(),
leaderCallbacks(),
true,
config.getLeaseName()))
.build();
}

public boolean isLeaderElectionEnabled() {
return leaderElector != null;
}

private LeaderCallbacks leaderCallbacks() {
return new LeaderCallbacks(this::startLeading, this::stopLeading, leader -> {
log.info("New leader with identity: {}", leader);
});
return new LeaderCallbacks(
this::startLeading,
this::stopLeading,
leader -> {
log.info("New leader with identity: {}", leader);
});
}

private void startLeading() {
Expand All @@ -64,13 +89,36 @@ private void stopLeading() {
}

private String identity(LeaderElectionConfiguration config) {
String id = config.getIdentity().orElse(System.getenv("HOSTNAME"));
var id = config.getIdentity().orElse(System.getenv("HOSTNAME"));
if (id == null || id.isBlank()) {
id = UUID.randomUUID().toString();
}
return id;
}

private static Optional<String> tryGetInClusterNamespace() {
log.info("Trying to get namespace from Kubernetes service account namespace path...");

final var serviceAccountNamespace =
Utils.getSystemPropertyOrEnvVar(KUBERNETES_NAMESPACE_FILE, KUBERNETES_NAMESPACE_PATH);
final var serviceAccountNamespacePath = Path.of(serviceAccountNamespace);

final var serviceAccountNamespaceExists = Files.isRegularFile(serviceAccountNamespacePath);
if (serviceAccountNamespaceExists) {
log.info("Found service account namespace at: [{}].", serviceAccountNamespace);
try {
return Optional.of(Files.readString(serviceAccountNamespacePath));
} catch (IOException e) {
log.error(
"Error reading service account namespace from: [" + serviceAccountNamespace + "].", e);
return Optional.empty();
}
} else {
log.warn("Did not find service account namespace at: [{}].", serviceAccountNamespace);
return Optional.empty();
}
}

public void start() {
if (isLeaderElectionEnabled()) {
leaderElectionFuture = leaderElector.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public LeaderElectionConfiguration(String leaseName, String leaseNamespace) {
RETRY_PERIOD_DEFAULT_VALUE, null);
}

public LeaderElectionConfiguration(String leaseName) {
this(
leaseName,
null,
LEASE_DURATION_DEFAULT_VALUE,
RENEW_DEADLINE_DEFAULT_VALUE,
RETRY_PERIOD_DEFAULT_VALUE, null);
}

public LeaderElectionConfiguration(
String leaseName,
String leaseNamespace,
Expand All @@ -59,8 +68,8 @@ public LeaderElectionConfiguration(
this.identity = identity;
}

public String getLeaseNamespace() {
return leaseNamespace;
public Optional<String> getLeaseNamespace() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is a breaking change. I can mark it as deprecated and provide another method getOptionalLeaseNamespace, if needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh normally I would prefer backwards compatible with deprecated. getOptionalLeaseNamespace does not sound too good, maybe just stick with this this time.

Copy link
Collaborator

@metacosm metacosm Sep 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably would be better, yes. Then again, I don't know if anyone is using Leader Election already…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for users, direct usage of this API will be rare even they do use leader election feature. Probably only in unit test.

return Optional.ofNullable(leaseNamespace);
}

public String getLeaseName() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.javaoperatorsdk.operator;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration;

import static io.fabric8.kubernetes.client.Config.KUBERNETES_NAMESPACE_FILE;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

class LeaderElectionManagerTest {

private ControllerManager controllerManager;
private KubernetesClient kubernetesClient;
private LeaderElectionManager leaderElectionManager;

@BeforeEach
void setUp() {
controllerManager = mock(ControllerManager.class);
kubernetesClient = mock(KubernetesClient.class);
leaderElectionManager = new LeaderElectionManager(controllerManager);
}

@AfterEach
void tearDown() {
System.getProperties().remove(KUBERNETES_NAMESPACE_FILE);
}

@Test
void testInit() {
leaderElectionManager.init(new LeaderElectionConfiguration("test", "testns"), kubernetesClient);
assertTrue(leaderElectionManager.isLeaderElectionEnabled());
}

@Test
void testInitInferLeaseNamespace(@TempDir Path tempDir) throws IOException {
var namespace = "foo";
var namespacePath = tempDir.resolve("namespace");
Files.writeString(namespacePath, namespace);

System.setProperty(KUBERNETES_NAMESPACE_FILE, namespacePath.toString());

leaderElectionManager.init(new LeaderElectionConfiguration("test"), kubernetesClient);
assertTrue(leaderElectionManager.isLeaderElectionEnabled());
}

@Test
void testFailedToInitInferLeaseNamespace() {
assertThrows(
IllegalArgumentException.class,
() -> leaderElectionManager.init(new LeaderElectionConfiguration("test"),
kubernetesClient));
}
}