Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/leader-election.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ management.info.leader.enabled=false

'''

There is another way you can configure leader election, and it comes with native support in the fabric8 library (k8s native client support is not yet implemented). In the long run, this will be the default way to configure leader election, while the previous one will be dropped. You can treat this one much like the JDK's "preview" features.
There is another way you can configure leader election, and it comes with native support in both fabric8 and kubernetes client. In the long run, this will be the default way to configure leader election, while the previous one will be dropped. You can treat this one much like the JDK's "preview" features.

To be able to use it, you need to set the property:

Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
<module>spring-cloud-starter-kubernetes-fabric8-loadbalancer</module>
<module>spring-cloud-kubernetes-discovery</module>
<module>spring-cloud-starter-kubernetes-discoveryclient</module>
<module>spring-cloud-kubernetes-client-leader</module>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

new module that adds support for k8s client leader election

</modules>

<dependencyManagement>
Expand Down
61 changes: 61 additions & 0 deletions spring-cloud-kubernetes-client-leader/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes</artifactId>
<version>5.0.1-SNAPSHOT</version>
</parent>

<artifactId>spring-cloud-kubernetes-client-leader</artifactId>
<name>K8s Client Spring Cloud Kubernetes :: Leader</name>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-client-autoconfig</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-webtestclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.kubernetes.client.leader.election;

import java.util.List;
import java.util.Objects;
import java.util.function.BooleanSupplier;

import io.kubernetes.client.extended.leaderelection.LeaderElectionConfig;
import io.kubernetes.client.extended.leaderelection.Lock;
import io.kubernetes.client.extended.leaderelection.resourcelock.ConfigMapLock;
import io.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
import io.kubernetes.client.openapi.models.V1APIResource;
import io.kubernetes.client.openapi.models.V1Pod;
import io.kubernetes.client.openapi.models.V1PodCondition;

import org.springframework.boot.actuate.autoconfigure.info.ConditionalOnEnabledInfoContributor;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.commons.leader.election.ConditionalOnLeaderElectionEnabled;
import org.springframework.cloud.kubernetes.commons.leader.election.LeaderElectionProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.log.LogAccessor;

import static org.springframework.cloud.kubernetes.commons.leader.LeaderUtils.COORDINATION_GROUP;
import static org.springframework.cloud.kubernetes.commons.leader.LeaderUtils.COORDINATION_VERSION;

/**
* @author wind57
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LeaderElectionProperties.class)
@ConditionalOnBean(ApiClient.class)
@ConditionalOnLeaderElectionEnabled
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@AutoConfigureAfter(KubernetesClientLeaderElectionCallbacksAutoConfiguration.class)
class KubernetesClientLeaderElectionAutoConfiguration {

private static final LogAccessor LOG = new LogAccessor(KubernetesClientLeaderElectionAutoConfiguration.class);

@Bean
@ConditionalOnClass(InfoContributor.class)
@ConditionalOnEnabledInfoContributor("leader.election")
KubernetesClientLeaderElectionInfoContributor kubernetesClientLeaderElectionInfoContributor(
String candidateIdentity, LeaderElectionConfig leaderElectionConfig) {
return new KubernetesClientLeaderElectionInfoContributor(candidateIdentity, leaderElectionConfig);
}

@Bean
@ConditionalOnMissingBean
KubernetesClientLeaderElectionInitiator kubernetesClientLeaderElectionInitiator(String candidateIdentity,
String podNamespace, LeaderElectionConfig leaderElectionConfig,
LeaderElectionProperties leaderElectionProperties, BooleanSupplier podReadySupplier,
KubernetesClientLeaderElectionCallbacks callbacks) {
return new KubernetesClientLeaderElectionInitiator(candidateIdentity, podNamespace, leaderElectionConfig,
leaderElectionProperties, podReadySupplier, callbacks);
}

@Bean
@ConditionalOnMissingBean
BooleanSupplier kubernetesClientPodReadySupplier(CoreV1Api coreV1Api, String candidateIdentity,
String podNamespace) {
return () -> {
try {
V1Pod pod = coreV1Api.readNamespacedPod(candidateIdentity, podNamespace).execute();
return isPodReady(pod);
}
catch (ApiException e) {
throw new RuntimeException(e);
}
};
}

@Bean
@ConditionalOnMissingBean
LeaderElectionConfig kubernetesClientLeaderElectionConfig(LeaderElectionProperties properties, Lock lock) {
return new LeaderElectionConfig(lock, properties.leaseDuration(), properties.renewDeadline(),
properties.retryPeriod());
}

@Bean
@ConditionalOnMissingBean
Lock kubernetesClientLeaderElectionLock(ApiClient apiClient, LeaderElectionProperties properties,
String candidateIdentity) {

CustomObjectsApi customObjectsApi = new CustomObjectsApi(apiClient);
boolean leaseSupported;
try {
List<V1APIResource> resources = customObjectsApi.getAPIResources(COORDINATION_GROUP, COORDINATION_VERSION)
.execute()
.getResources();

leaseSupported = resources.stream().map(V1APIResource::getKind).anyMatch("Lease"::equals);
}
catch (ApiException e) {
throw new RuntimeException(e);
}

if (leaseSupported) {
if (properties.useConfigMapAsLock()) {
LOG.info(() -> "leases are supported on the cluster, but config map will be used "
+ "(because 'spring.cloud.kubernetes.leader.election.use-config-map-as-lock=true')");
return new ConfigMapLock(properties.lockNamespace(), properties.lockName(), candidateIdentity);
}
else {
LOG.info(() -> "will use lease as the lock for leader election");
return new LeaseLock(properties.lockNamespace(), properties.lockName(), candidateIdentity, apiClient);
}
}
else {
LOG.info(() -> "will use configmap as the lock for leader election");
return new ConfigMapLock(properties.lockNamespace(), properties.lockName(), candidateIdentity, apiClient);
}
}

// following two methods are a verbatim copy of the fabric8 implementation
private static boolean isPodReady(V1Pod pod) {
Objects.requireNonNull(pod, "Pod can't be null.");
V1PodCondition condition = getPodReadyCondition(pod);

if (condition == null) {
return false;
}
return condition.getStatus().equalsIgnoreCase("True");
}

private static V1PodCondition getPodReadyCondition(V1Pod pod) {
if (pod.getStatus() == null || pod.getStatus().getConditions() == null) {
return null;
}

for (V1PodCondition condition : pod.getStatus().getConditions()) {
if ("Ready".equals(condition.getType())) {
return condition;
}
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.kubernetes.client.leader.election;

import java.util.function.Consumer;

/**
* @author wind57
*/
record KubernetesClientLeaderElectionCallbacks(Runnable onStartLeadingCallback, Runnable onStopLeadingCallback,
Consumer<String> onNewLeaderCallback) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.kubernetes.client.leader.election;

import java.util.function.Consumer;

import io.kubernetes.client.openapi.ApiClient;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
import org.springframework.cloud.kubernetes.commons.leader.election.ConditionalOnLeaderElectionEnabled;
import org.springframework.cloud.kubernetes.commons.leader.election.LeaderElectionCallbacks;
import org.springframework.cloud.kubernetes.commons.leader.election.LeaderElectionProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author wind57
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LeaderElectionProperties.class)
@ConditionalOnBean(ApiClient.class)
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@ConditionalOnLeaderElectionEnabled
@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class })
public class KubernetesClientLeaderElectionCallbacksAutoConfiguration extends LeaderElectionCallbacks {

@Bean
@ConditionalOnMissingBean
KubernetesClientLeaderElectionCallbacks kubernetesClientLeaderElectionCallbacks(Runnable onStartLeadingCallback,
Runnable onStopLeadingCallback, Consumer<String> onNewLeaderCallback) {
return new KubernetesClientLeaderElectionCallbacks(onStartLeadingCallback, onStopLeadingCallback,
onNewLeaderCallback);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.kubernetes.client.leader.election;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import io.kubernetes.client.extended.leaderelection.LeaderElectionConfig;
import io.kubernetes.client.openapi.ApiException;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.core.log.LogAccessor;

/**
* @author wind57
*/
final class KubernetesClientLeaderElectionInfoContributor implements InfoContributor {

private static final LogAccessor LOG = new LogAccessor(KubernetesClientLeaderElectionInfoContributor.class);

private final String candidateIdentity;

private final LeaderElectionConfig leaderElectionConfig;

KubernetesClientLeaderElectionInfoContributor(String candidateIdentity, LeaderElectionConfig leaderElectionConfig) {
this.candidateIdentity = candidateIdentity;
this.leaderElectionConfig = leaderElectionConfig;
}

@Override
public void contribute(Info.Builder builder) {
Map<String, Object> details = new HashMap<>();
try {
Optional.ofNullable(leaderElectionConfig.getLock().get()).ifPresentOrElse(leaderRecord -> {
boolean isLeader = candidateIdentity.equals(leaderRecord.getHolderIdentity());
details.put("leaderId", candidateIdentity);
details.put("isLeader", isLeader);
}, () -> details.put("leaderId", "Unknown"));
}
catch (ApiException e) {
LOG.error(e, "error in leader election info contributor");
}

builder.withDetail("leaderElection", details);
}

}
Loading
Loading