diff --git a/spring-cloud-kubernetes-fabric8-leader/src/main/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCallbacksAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-leader/src/main/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCallbacksAutoConfiguration.java
index 14f4255985..1d0c157cc2 100644
--- a/spring-cloud-kubernetes-fabric8-leader/src/main/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCallbacksAutoConfiguration.java
+++ b/spring-cloud-kubernetes-fabric8-leader/src/main/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCallbacksAutoConfiguration.java
@@ -40,7 +40,7 @@
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@ConditionalOnLeaderElectionEnabled
@AutoConfigureAfter({ Fabric8AutoConfiguration.class, KubernetesCommonsAutoConfiguration.class })
-final class Fabric8LeaderElectionCallbacksAutoConfiguration extends LeaderElectionCallbacks {
+public class Fabric8LeaderElectionCallbacksAutoConfiguration extends LeaderElectionCallbacks {
@Bean
@ConditionalOnMissingBean
diff --git a/spring-cloud-kubernetes-fabric8-leader/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderAutoConfigurationTests.java b/spring-cloud-kubernetes-fabric8-leader/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionAutoConfigurationTests.java
similarity index 99%
rename from spring-cloud-kubernetes-fabric8-leader/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderAutoConfigurationTests.java
rename to spring-cloud-kubernetes-fabric8-leader/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionAutoConfigurationTests.java
index 05918d2bbf..6afd5dade4 100644
--- a/spring-cloud-kubernetes-fabric8-leader/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderAutoConfigurationTests.java
+++ b/spring-cloud-kubernetes-fabric8-leader/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionAutoConfigurationTests.java
@@ -29,7 +29,7 @@
*
* @author wind57
*/
-class Fabric8LeaderAutoConfigurationTests {
+class Fabric8LeaderElectionAutoConfigurationTests {
/**
*
diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml
index 1908c2ead4..a1a9a6b108 100644
--- a/spring-cloud-kubernetes-integration-tests/pom.xml
+++ b/spring-cloud-kubernetes-integration-tests/pom.xml
@@ -108,13 +108,14 @@
spring-cloud-kubernetes-k8s-client-configuration-watcher
- spring-cloud-kubernetes-k8s-client-kafka-configmap-reload
+ spring-cloud-kubernetes-k8s-client-kafka-configmap-reload
- spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload
+ spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload
- spring-cloud-kubernetes-fabric8-leader-election
+ spring-cloud-kubernetes-fabric8-leader-election
+ spring-cloud-kubernetes-k8s-client-leader-election
-
+
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-leader-election/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCanceledAndNotRestartedIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-leader-election/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCanceledAndNotRestartedIT.java
index dee050c416..d18d0e82c3 100644
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-leader-election/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCanceledAndNotRestartedIT.java
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-leader-election/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionCanceledAndNotRestartedIT.java
@@ -66,6 +66,8 @@ void test(CapturedOutput output) {
// lease is going to reset
awaitUntil(10, 100, () -> getLease().getSpec().getHolderIdentity().isEmpty());
+ awaitUntil(10, 100, () -> output.getOut().contains("terminating leadership for : " + NAME));
+
}
}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/pom.xml
new file mode 100644
index 0000000000..68865a336c
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-integration-tests
+ 5.0.1-SNAPSHOT
+
+
+
+ true
+ true
+
+
+ spring-cloud-kubernetes-k8s-client-leader-election
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-test-support
+ test
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-client-leader
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/AbstractLeaderElection.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/AbstractLeaderElection.java
new file mode 100644
index 0000000000..d23e23a057
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/AbstractLeaderElection.java
@@ -0,0 +1,180 @@
+/*
+ * 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.io.IOException;
+import java.io.StringReader;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BooleanSupplier;
+
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoordinationV1Api;
+import io.kubernetes.client.openapi.models.V1Lease;
+import io.kubernetes.client.util.Config;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.testcontainers.k3s.K3sContainer;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.system.OutputCaptureExtension;
+import org.springframework.cloud.kubernetes.commons.leader.LeaderUtils;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.test.annotation.DirtiesContext;
+
+/**
+ * @author wind57
+ */
+@ExtendWith(OutputCaptureExtension.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.leader.election.enabled=true",
+ "spring.cloud.kubernetes.leader.election.lease-duration=6s",
+ "spring.cloud.kubernetes.leader.election.renew-deadline=5s",
+ "logging.level.org.springframework.cloud.kubernetes.commons.leader.election=debug",
+ "logging.level.org.springframework.cloud.kubernetes.client.leader.election=debug",
+ "logging.level.io.kubernetes.client.extended.leaderelection=debug" },
+ classes = { App.class, AbstractLeaderElection.TestConfig.class,
+ AbstractLeaderElection.PodReadyTestConfiguration.class })
+@DirtiesContext
+abstract class AbstractLeaderElection {
+
+ @Autowired
+ private ApiClient apiClient;
+
+ private static K3sContainer container;
+
+ private static MockedStatic LEADER_UTILS_MOCKED_STATIC;
+
+ static void beforeAll(String candidateIdentity) {
+ container = Commons.container();
+ container.start();
+
+ LEADER_UTILS_MOCKED_STATIC = Mockito.mockStatic(LeaderUtils.class);
+ LEADER_UTILS_MOCKED_STATIC.when(LeaderUtils::hostName).thenReturn(candidateIdentity);
+ }
+
+ @AfterAll
+ static void afterAll() {
+ LEADER_UTILS_MOCKED_STATIC.close();
+ }
+
+ void stopLeaderAndDeleteLease(KubernetesClientLeaderElectionInitiator initiator, boolean deleteLease) {
+ initiator.preDestroy();
+
+ if (deleteLease) {
+ CoordinationV1Api api = new CoordinationV1Api(apiClient);
+
+ try {
+ api.deleteNamespacedLease("spring-k8s-leader-election-lock", "default").execute();
+ }
+ catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ V1Lease getLease() {
+ CoordinationV1Api api = new CoordinationV1Api(apiClient);
+ try {
+ return api.readNamespacedLease("spring-k8s-leader-election-lock", "default").execute();
+ }
+ catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ V1Lease updateLease(V1Lease lease) {
+ CoordinationV1Api api = new CoordinationV1Api(apiClient);
+ try {
+ return api.replaceNamespacedLease("spring-k8s-leader-election-lock", "default", lease).execute();
+ }
+ catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @TestConfiguration
+ static class TestConfig {
+
+ @Bean
+ @Primary
+ ApiClient client() {
+ String kubeConfigYaml = container.getKubeConfigYaml();
+
+ ApiClient client;
+ try {
+ client = Config.fromConfig(new StringReader(kubeConfigYaml));
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return client;
+ }
+
+ }
+
+ @TestConfiguration
+ static class PodReadyTestConfiguration {
+
+ // readiness passes after 2 retries
+ @Bean
+ @Primary
+ @ConditionalOnProperty(value = "readiness.passes", havingValue = "true", matchIfMissing = false)
+ BooleanSupplier readinessSupplierPasses() {
+ AtomicInteger counter = new AtomicInteger(0);
+ return () -> {
+ if (counter.get() != 2) {
+ counter.incrementAndGet();
+ return false;
+ }
+ return true;
+ };
+ }
+
+ // readiness fails after 2 retries
+ @Bean
+ @Primary
+ @ConditionalOnProperty(value = "readiness.fails", havingValue = "true", matchIfMissing = false)
+ BooleanSupplier readinessSupplierFails() {
+ AtomicInteger counter = new AtomicInteger(0);
+ return () -> {
+ if (counter.get() != 2) {
+ counter.incrementAndGet();
+ return false;
+ }
+ throw new RuntimeException("readiness fails");
+ };
+ }
+
+ // readiness always fails
+ @Bean
+ @Primary
+ @ConditionalOnProperty(value = "readiness.never.finishes", havingValue = "true", matchIfMissing = false)
+ BooleanSupplier readinessNeverFinishes() {
+ return () -> false;
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/App.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/App.java
new file mode 100644
index 0000000000..7b5bcc26f1
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/App.java
@@ -0,0 +1,29 @@
+/*
+ * 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 org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class App {
+
+ public static void main(String[] args) {
+ SpringApplication.run(App.class, args);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/Assertions.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/Assertions.java
new file mode 100644
index 0000000000..1d0f752dee
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/Assertions.java
@@ -0,0 +1,89 @@
+/*
+ * 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.time.OffsetDateTime;
+import java.util.function.Supplier;
+
+import io.kubernetes.client.openapi.models.V1Lease;
+
+import org.springframework.boot.test.system.CapturedOutput;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+/**
+ * @author wind57
+ */
+final class Assertions {
+
+ private Assertions() {
+
+ }
+
+ /**
+ * lease was acquired and we renewed it, at least once.
+ */
+ static void assertAcquireAndRenew(CapturedOutput output, Supplier leaseSupplier,
+ String candidateIdentity) {
+ // we have become the leader
+ awaitUntil(60, 100, () -> output.getOut().contains(candidateIdentity + " is the new leader"));
+
+ // let's unwind some logs to see that the process is how we expect it to be
+
+ // 1. lease is used as the lock (comes from our code)
+ awaitUntil(5, 100, () -> output.getOut().contains("will use lease as the lock for leader election"));
+
+ // 2. we start leader initiator for our hostname (comes from our code)
+ awaitUntil(5, 100, () -> output.getOut().contains("starting leader initiator : " + candidateIdentity));
+
+ // 3. start leader election with the configured lock
+ awaitUntil(10, 100, () -> output.getOut()
+ .contains("Start leader election with lock default/spring-k8s-leader-election-lock"));
+
+ // 4. we try to acquire the lease
+ awaitUntil(5, 100, () -> output.getOut().contains("Attempting to acquire leader lease"));
+
+ // 5. lease has been acquired
+ awaitUntil(5, 100,
+ () -> output.getOut().contains("LeaderElection lock is currently held by " + candidateIdentity));
+
+ // 6. we are the leader
+ awaitUntil(10, 100, () -> output.getOut().contains("Successfully acquired lease, became leader"));
+
+ // 7. wait until a renewal happens
+ // this one means that we have extended our leadership
+ awaitUntil(10, 100, () -> output.getOut().contains("Successfully renewed lease"));
+
+ V1Lease lease = leaseSupplier.get();
+
+ OffsetDateTime currentAcquiredTime = lease.getSpec().getAcquireTime();
+ assertThat(currentAcquiredTime).isNotNull();
+ assertThat(lease.getSpec().getLeaseDurationSeconds()).isEqualTo(6);
+ assertThat(lease.getSpec().getLeaseTransitions()).isEqualTo(0);
+
+ OffsetDateTime currentRenewalTime = lease.getSpec().getRenewTime();
+ assertThat(currentRenewalTime).isNotNull();
+
+ // 8. renewal happens
+ awaitUntil(4, 500, () -> {
+ OffsetDateTime newRenewalTime = leaseSupplier.get().getSpec().getRenewTime();
+ return newRenewalTime.isAfter(currentRenewalTime);
+ });
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionCanceledAndNotRestartedIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionCanceledAndNotRestartedIT.java
new file mode 100644
index 0000000000..9e040693e4
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionCanceledAndNotRestartedIT.java
@@ -0,0 +1,71 @@
+/*
+ * 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 org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.springframework.cloud.kubernetes.client.leader.election.Assertions.assertAcquireAndRenew;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+/**
+ *
+ * - we acquire the leadership
+ * - leadership feature fails
+ *
+ *
+ * @author wind57
+ */
+@TestPropertySource(properties = { "spring.cloud.kubernetes.leader.election.wait-for-pod-ready=true",
+ "spring.cloud.kubernetes.leader.election.restart-on-failure=true", "readiness.passes=true" })
+class K8sClientLeaderElectionCanceledAndNotRestartedIT extends AbstractLeaderElection {
+
+ private static final String NAME = "leader-acquired-then-canceled-it";
+
+ @Autowired
+ private KubernetesClientLeaderElectionInitiator initiator;
+
+ @BeforeAll
+ static void beforeAll() {
+ AbstractLeaderElection.beforeAll(NAME);
+ }
+
+ @AfterEach
+ void afterEach() {
+ stopLeaderAndDeleteLease(initiator, true);
+ }
+
+ @Test
+ void test(CapturedOutput output) {
+
+ assertAcquireAndRenew(output, this::getLease, NAME);
+
+ // this will kill leadership and it will not be re-started
+ initiator.preDestroy();
+
+ awaitUntil(10, 100, () -> output.getOut().contains("leadership terminated for : " + NAME));
+
+ awaitUntil(10, 100, () -> output.getOut().contains("Giving up the lock"));
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionCompletedExceptionallyAndRestartedIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionCompletedExceptionallyAndRestartedIT.java
new file mode 100644
index 0000000000..c098671180
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionCompletedExceptionallyAndRestartedIT.java
@@ -0,0 +1,85 @@
+/*
+ * 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 org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.springframework.cloud.kubernetes.client.leader.election.Assertions.assertAcquireAndRenew;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+/**
+ *
+ * - we acquire the leadership
+ * - leadership feature fails
+ * - we retry and acquire it again
+ *
+ *
+ * @author wind57
+ */
+@TestPropertySource(properties = { "spring.cloud.kubernetes.leader.election.wait-for-pod-ready=true",
+ "spring.cloud.kubernetes.leader.election.restart-on-failure=true", "readiness.passes=true" })
+class K8sClientLeaderElectionCompletedExceptionallyAndRestartedIT extends AbstractLeaderElection {
+
+ private static final String NAME = "leader-completed-and-restarted-it";
+
+ @Autowired
+ private KubernetesClientLeaderElectionInitiator initiator;
+
+ @BeforeAll
+ static void beforeAll() {
+ AbstractLeaderElection.beforeAll(NAME);
+ }
+
+ @AfterEach
+ void afterEach() {
+ stopLeaderAndDeleteLease(initiator, true);
+ }
+
+ @Test
+ void test(CapturedOutput output) {
+
+ assertAcquireAndRenew(output, this::getLease, NAME);
+
+ // simulate that the lock is released
+ initiator.leaderElector().close();
+
+ // from the callback
+ awaitUntil(5, 50, () -> output.getOut().contains("id : " + NAME + " stopped being a leader"));
+
+ awaitUntil(5, 50, () -> output.getOut().contains("will re-start leader election for : " + NAME));
+
+ int afterLeaderFailure = output.getOut().indexOf("will re-start leader election for : " + NAME);
+
+ afterLeaderFailure(afterLeaderFailure, output);
+
+ }
+
+ private void afterLeaderFailure(int afterLeaderFailure, CapturedOutput output) {
+ awaitUntil(60, 100, () -> output.getOut().substring(afterLeaderFailure).contains(NAME + " is the new leader"));
+ awaitUntil(5, 100, () -> output.getOut().contains("Update lock to renew lease"));
+ awaitUntil(5, 100, () -> output.getOut().contains("TryAcquireOrRenew return success"));
+ awaitUntil(5, 100, () -> output.getOut().contains("Successfully renewed lease"));
+ awaitUntil(5, 100, () -> output.getOut().contains("Update lock to renew lease"));
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionConcurrentIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionConcurrentIT.java
new file mode 100644
index 0000000000..0ad66c7136
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionConcurrentIT.java
@@ -0,0 +1,230 @@
+/*
+ * 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.io.IOException;
+import java.io.StringReader;
+import java.time.Duration;
+import java.util.function.Consumer;
+
+import io.kubernetes.client.extended.leaderelection.LeaderElectionConfig;
+import io.kubernetes.client.extended.leaderelection.Lock;
+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.CoordinationV1Api;
+import io.kubernetes.client.util.Config;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.testcontainers.k3s.K3sContainer;
+
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.boot.test.system.OutputCaptureExtension;
+import org.springframework.cloud.kubernetes.commons.leader.election.LeaderElectionProperties;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
+
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+@ExtendWith(OutputCaptureExtension.class)
+class K8sClientLeaderElectionConcurrentIT {
+
+ private static final String LEASE_NAME = "lease-lock";
+
+ private static final LeaderElectionProperties PROPERTIES = new LeaderElectionProperties(false, false,
+ Duration.ofSeconds(15), "default", LEASE_NAME, Duration.ofSeconds(5), Duration.ofSeconds(2),
+ Duration.ofSeconds(5), false, true);
+
+ private static final String CANDIDATE_IDENTITY_ONE = "one";
+
+ private static final String CANDIDATE_IDENTITY_TWO = "two";
+
+ private KubernetesClientLeaderElectionInitiator one;
+
+ private KubernetesClientLeaderElectionInitiator two;
+
+ private static ApiClient apiClient;
+
+ @BeforeAll
+ static void beforeAll() {
+
+ K3sContainer container = Commons.container();
+ container.start();
+
+ String kubeConfigYaml = container.getKubeConfigYaml();
+
+ try {
+ apiClient = Config.fromConfig(new StringReader(kubeConfigYaml));
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @AfterAll
+ static void afterAll() {
+
+ CoordinationV1Api api = new CoordinationV1Api(apiClient);
+
+ try {
+ api.deleteNamespacedLease(LEASE_NAME, "default").execute();
+ }
+ catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @AfterEach
+ void afterEach() {
+ one.preDestroy();
+ two.preDestroy();
+ }
+
+ @Test
+ void test(CapturedOutput output) {
+
+ LeaderElectionConfig leaderElectionConfigOne = leaderElectionConfig(CANDIDATE_IDENTITY_ONE);
+ KubernetesClientLeaderElectionCallbacks callbacksOne = callbacks(CANDIDATE_IDENTITY_ONE);
+ one = new KubernetesClientLeaderElectionInitiator(CANDIDATE_IDENTITY_ONE, "default", leaderElectionConfigOne,
+ PROPERTIES, () -> true, callbacksOne);
+
+ LeaderElectionConfig leaderElectionConfigTwo = leaderElectionConfig(CANDIDATE_IDENTITY_TWO);
+ KubernetesClientLeaderElectionCallbacks callbacksTwo = callbacks(CANDIDATE_IDENTITY_TWO);
+ two = new KubernetesClientLeaderElectionInitiator(CANDIDATE_IDENTITY_TWO, "default", leaderElectionConfigTwo,
+ PROPERTIES, () -> true, callbacksTwo);
+
+ one.postConstruct();
+ two.postConstruct();
+
+ // both try to acquire the lock
+ awaitUntil(5, 100, () -> output.getOut().contains("starting leader initiator : one"));
+ awaitUntil(5, 100, () -> output.getOut().contains("starting leader initiator : two"));
+
+ // someone has become the leader
+ awaitUntil(5, 100, () -> output.getOut().contains("LeaderElection lock is currently held by"));
+
+ LeaderAndFollower leaderAndFollower = leaderAndFollower(leaderElectionConfigOne);
+ String leader = leaderAndFollower.leader();
+ String follower = leaderAndFollower.follower();
+
+ // someone has become the leader
+ awaitUntil(5, 100, () -> output.getOut().contains("LeaderElection lock is currently held by " + leader));
+ awaitUntil(3, 100, () -> output.getOut().contains("id : " + leader + " is the new leader"));
+
+ // the other elector says it can't acquire the lock
+ awaitUntil(10, 100, () -> output.getOut().contains("Lock is held by " + leader + " and has not yet expired"));
+ awaitUntil(10, 100, () -> output.getOut().contains("The tryAcquireOrRenew result is false"));
+
+ int beforeRelease = output.getOut().length();
+ failLeaderRenewal(leader, one, two);
+
+ /*
+ * we simulated above that renewal failed and leader future was canceled. In this
+ * case, the 'notLeader' picks up the leadership, the 'leader' is now a
+ * "follower", it re-tries to take leadership.
+ */
+ awaitUntil(10, 100,
+ () -> output.getOut().substring(beforeRelease).contains("id : " + follower + " is the new leader"));
+ awaitUntil(10, 100,
+ () -> output.getOut().substring(beforeRelease).contains("Failed to renew lease, lose leadership"));
+
+ // the other candidate still tries to become leader
+ awaitUntil(10, 100,
+ () -> output.getOut()
+ .substring(beforeRelease)
+ .contains("Lock is held by " + follower + " and has not yet expired"));
+
+ /*
+ * we simulate the renewal failure one more time. we know that leader = 'follower'
+ */
+ int failAgain = output.getOut().length();
+ failLeaderRenewal(follower, one, two);
+
+ awaitUntil(10, 100,
+ () -> output.getOut().substring(failAgain).contains("id : " + leader + " is the new leader"));
+ // the other candidate still tries to become leader
+ awaitUntil(10, 100,
+ () -> output.getOut()
+ .substring(beforeRelease)
+ .contains("Lock is held by " + leader + " and has not yet expired"));
+ }
+
+ private LeaderElectionConfig leaderElectionConfig(String holderIdentity) {
+
+ Lock lock = leaseLock(holderIdentity);
+
+ LeaderElectionConfig leaderElectionConfig = new LeaderElectionConfig();
+ leaderElectionConfig.setLock(lock);
+ leaderElectionConfig.setLeaseDuration(PROPERTIES.leaseDuration());
+ leaderElectionConfig.setRenewDeadline(PROPERTIES.renewDeadline());
+ leaderElectionConfig.setRetryPeriod(PROPERTIES.retryPeriod());
+
+ return leaderElectionConfig;
+ }
+
+ private LeaseLock leaseLock(String holderIdentity) {
+ return new LeaseLock("default", LEASE_NAME, holderIdentity, apiClient);
+ }
+
+ private KubernetesClientLeaderElectionCallbacks callbacks(String holderIdentity) {
+ KubernetesClientLeaderElectionCallbacksAutoConfiguration configuration = new KubernetesClientLeaderElectionCallbacksAutoConfiguration();
+
+ Runnable onStartLeadingCallback = configuration.onStartLeadingCallback(null, holderIdentity, PROPERTIES);
+ Runnable onStopLeadingCallback = configuration.onStopLeadingCallback(null, holderIdentity, PROPERTIES);
+ Consumer onNewLeaderCallback = configuration.onNewLeaderCallback(null, PROPERTIES);
+
+ return new KubernetesClientLeaderElectionCallbacks(onStartLeadingCallback, onStopLeadingCallback,
+ onNewLeaderCallback);
+ }
+
+ private LeaderAndFollower leaderAndFollower(LeaderElectionConfig leaderElectionConfig) {
+
+ boolean oneIsLeader;
+ try {
+ oneIsLeader = leaderElectionConfig.getLock().get().getHolderIdentity().equals(CANDIDATE_IDENTITY_ONE);
+ }
+ catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+
+ if (oneIsLeader) {
+ return new LeaderAndFollower("one", "two");
+ }
+ else {
+ return new LeaderAndFollower("two", "one");
+ }
+ }
+
+ private void failLeaderRenewal(String currentLeader, KubernetesClientLeaderElectionInitiator one,
+ KubernetesClientLeaderElectionInitiator two) {
+ if (currentLeader.equals("one")) {
+ one.leaderElector().close();
+ }
+ else {
+ two.leaderElector().close();
+ }
+ }
+
+ private record LeaderAndFollower(String leader, String follower) {
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionIsLostAndRestartedIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionIsLostAndRestartedIT.java
new file mode 100644
index 0000000000..c3c6e8a2f2
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionIsLostAndRestartedIT.java
@@ -0,0 +1,99 @@
+/*
+ * 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 io.kubernetes.client.openapi.models.V1Lease;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.springframework.cloud.kubernetes.client.leader.election.Assertions.assertAcquireAndRenew;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+/**
+ * We acquire leadership, then lose it, then acquire it back. This tests the "leaderFuture
+ * finished normally, will re-start it for" branch
+ *
+ * @author wind57
+ */
+@TestPropertySource(
+ properties = { "spring.cloud.kubernetes.leader.election.wait-for-pod-ready=true", "readiness.passes=true" })
+class K8sClientLeaderElectionIsLostAndRestartedIT extends AbstractLeaderElection {
+
+ private static final String NAME = "leader-lost-then-recovers-it";
+
+ @Autowired
+ private KubernetesClientLeaderElectionInitiator initiator;
+
+ @BeforeAll
+ static void beforeAll() {
+ AbstractLeaderElection.beforeAll(NAME);
+ }
+
+ @AfterEach
+ void afterEach() {
+ stopLeaderAndDeleteLease(initiator, true);
+ }
+
+ @Test
+ void test(CapturedOutput output) {
+
+ assertAcquireAndRenew(output, this::getLease, NAME);
+
+ // 8. simulate that leadership has changed
+ V1Lease lease = getLease();
+ lease.getSpec().setHolderIdentity("leader-lost-then-recovers-it-is-not-the-leader-anymore");
+ updateLease(lease);
+
+ // 9. we lost leadership
+ awaitUntil(10, 100, () -> output.getOut().contains("Failed to renew lease, lose leadership"));
+
+ // 10. callback confirms we lost leadership
+ awaitUntil(10, 100, () -> output.getOut().contains("id : " + NAME + " stopped being a leader"));
+
+ // 11. leader has changed
+ awaitUntil(10, 20, () -> output.getOut()
+ .contains(
+ "LeaderElection lock is currently held by leader-lost-then-recovers-it-is-not-the-leader-anymore"));
+
+ // 12. from our callback
+ awaitUntil(10, 100, () -> output.getOut()
+ .contains("id : leader-lost-then-recovers-it-is-not-the-leader-anymore is the new leader"));
+
+ // 13. leadership is restarted for us
+ awaitUntil(10, 100, () -> output.getOut().contains("will re-start leader election for : " + NAME));
+
+ int leadershipFinished = output.getOut().indexOf("will re-start leader election for : " + NAME);
+ afterLeadershipRestart(output, leadershipFinished);
+
+ }
+
+ private void afterLeadershipRestart(CapturedOutput output, int leadershipFinished) {
+
+ // 14. since the new leader is artificial, renew is not going to happen for it
+ awaitUntil(60, 100, () -> output.getOut().substring(leadershipFinished).contains(NAME + " is the new leader"));
+
+ // 15. callback is again triggered
+ awaitUntil(10, 100, () -> output.getOut().substring(leadershipFinished).contains(NAME + " is now a leader"));
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessCanceledIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessCanceledIT.java
new file mode 100644
index 0000000000..177dcec022
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessCanceledIT.java
@@ -0,0 +1,87 @@
+/*
+ * 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 org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+/**
+ * Readiness is canceled. This is the case when pod is shut down gracefully
+ *
+ * @author wind57
+ */
+@TestPropertySource(properties = { "readiness.never.finishes=true",
+ "spring.cloud.kubernetes.leader.election.wait-for-pod-ready=true" })
+class K8sClientLeaderElectionReadinessCanceledIT extends AbstractLeaderElection {
+
+ private static final String NAME = "readiness-canceled-it";
+
+ @Autowired
+ private KubernetesClientLeaderElectionInitiator initiator;
+
+ @BeforeAll
+ static void beforeAll() {
+ AbstractLeaderElection.beforeAll(NAME);
+ }
+
+ @AfterEach
+ void afterEach() {
+ stopLeaderAndDeleteLease(initiator, false);
+ }
+
+ @Test
+ void test(CapturedOutput output) {
+
+ // we are trying readiness at least once
+ awaitUntil(60, 500, () -> output.getOut()
+ .contains("Pod : " + NAME + " in namespace : " + "default is not ready, will retry in one second"));
+
+ initiator.preDestroy();
+
+ // 1. preDestroy method logs what it will do
+ assertThat(output.getOut()).contains("podReadyFuture will be canceled for : " + NAME);
+
+ // 2. readiness failed
+ assertThat(output.getOut()).contains("readiness failed for : " + NAME + ", leader election will not start");
+
+ // 3. will cancel the future that is supposed to do the readiness
+ assertThat(output.getOut()).contains("canceling scheduled future because completable future was cancelled");
+
+ // 4. podReadyWaitingExecutor is shut down also
+ assertThat(output.getOut()).contains("podReadyWaitingExecutor will be shutdown for : " + NAME);
+
+ // 5. the scheduled executor where pod readiness is checked is shut down also
+ awaitUntil(2, 100, () -> output.getOut().contains("Shutting down executor : podReadyExecutor"));
+
+ // we need to call preDestroy again, to make sure that leaderFuture was not
+ // started
+ initiator.preDestroy();
+
+ // 6. leader election is not started, since readiness does not finish
+ assertThat(output.getOut()).doesNotContain("starting leader election : " + NAME);
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessFailsIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessFailsIT.java
new file mode 100644
index 0000000000..82dc925551
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessFailsIT.java
@@ -0,0 +1,97 @@
+/*
+ * 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 org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+/**
+ * Readiness fails with an Exception, and we don't establish leadership
+ *
+ * @author wind57
+ */
+@TestPropertySource(
+ properties = { "readiness.fails=true", "spring.cloud.kubernetes.leader.election.wait-for-pod-ready=true" })
+class K8sClientLeaderElectionReadinessFailsIT extends AbstractLeaderElection {
+
+ private static final String NAME = "readiness-fails-it";
+
+ @Autowired
+ private KubernetesClientLeaderElectionInitiator initiator;
+
+ @BeforeAll
+ static void beforeAll() {
+ AbstractLeaderElection.beforeAll(NAME);
+ }
+
+ @AfterEach
+ void afterEach() {
+ stopLeaderAndDeleteLease(initiator, false);
+ }
+
+ /**
+ *
+ * - readiness fails after 2 seconds - leader election process is not started at all
+ *
+ */
+ @Test
+ void test(CapturedOutput output) {
+
+ // we do not start leader election at all
+ awaitUntil(60, 1000,
+ () -> output.getOut().contains("readiness failed for : " + NAME + ", leader election will not start"));
+
+ // 1. lease is used as the lock (comes from our code)
+ assertThat(output.getOut()).contains("will use lease as the lock for leader election");
+
+ // 2. leader initiator is started
+ assertThat(output.getOut()).contains("starting leader initiator : " + NAME);
+
+ // 3. wait for when pod is ready (we mock this one)
+ assertThat(output.getOut()).contains("will wait until pod " + NAME + " is ready");
+
+ // 4. we run readiness check in podReadyExecutor
+ assertThat(output.getOut()).contains("Scheduling command to run in : podReadyExecutor");
+
+ // 5. pod fails on the first two attempts
+ assertThat(output.getOut())
+ .contains("Pod : " + NAME + " in namespace : default is not ready, will retry in one second");
+
+ // 6. readiness fails
+ assertThat(output.getOut()).contains("exception waiting for pod : " + NAME);
+
+ // 7. we shut down the executor
+ assertThat(output.getOut()).contains("canceling scheduled future because readiness failed");
+
+ // 8. leader election did not even start properly
+ assertThat(output.getOut()).contains("pod readiness for : " + NAME + " failed with : readiness fails");
+
+ // 9. executor is shutdown, even when readiness failed
+ awaitUntil(60, 100,
+ () -> output.getOut().contains("readiness failed for : " + NAME + ", leader election will not start"));
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessPassesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessPassesIT.java
new file mode 100644
index 0000000000..a5fab8d35d
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/java/org/springframework/cloud/kubernetes/client/leader/election/K8sClientLeaderElectionReadinessPassesIT.java
@@ -0,0 +1,80 @@
+/*
+ * 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 org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.springframework.cloud.kubernetes.client.leader.election.Assertions.assertAcquireAndRenew;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities.awaitUntil;
+
+/**
+ * A simple test where we are the sole participant in the leader election and everything
+ * goes fine from start to end. It's a happy path scenario test.
+ *
+ * @author wind57
+ */
+
+@TestPropertySource(
+ properties = { "spring.cloud.kubernetes.leader.election.wait-for-pod-ready=true", "readiness.passes=true" })
+class K8sClientLeaderElectionReadinessPassesIT extends AbstractLeaderElection {
+
+ @Autowired
+ private KubernetesClientLeaderElectionInitiator initiator;
+
+ private static final String NAME = "readiness-passes-it";
+
+ @BeforeAll
+ static void beforeAll() {
+ AbstractLeaderElection.beforeAll(NAME);
+ }
+
+ @AfterEach
+ void afterEach() {
+ stopLeaderAndDeleteLease(initiator, true);
+ }
+
+ /**
+ *
+ * - readiness is checked
+ * - leader election process happens after that
+ * - we establish leadership and renew it
+ *
+ */
+ @Test
+ void test(CapturedOutput output) {
+
+ awaitUntil(10, 100, () -> output.getOut()
+ .contains("Pod : " + NAME + " in namespace : default is not ready, will retry in one second"));
+ awaitUntil(10, 100, () -> output.getOut().contains("Pod : " + NAME + " in namespace : default is ready"));
+ awaitUntil(10, 100, () -> output.getOut().contains(NAME + " is ready"));
+ awaitUntil(10, 100, () -> output.getOut().contains("canceling scheduled future because readiness succeeded"));
+
+ assertAcquireAndRenew(output, this::getLease, NAME);
+
+ // comes from the callback, where we post the spring lifecycle event
+ awaitUntil(5, 100, () -> output.getOut().contains(NAME + " is the new leader"));
+
+ awaitUntil(60, 100, () -> output.getOut().contains("Shutting down executor : podReadyExecutor"));
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..33cd90c3b9
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-leader-election/src/test/resources/logback-test.xml
@@ -0,0 +1,17 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+