diff --git a/CHANGELOG.md b/CHANGELOG.md index cf381195409..0d39038a089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ #### Improvements * Fix #4675: adding a fully client side timeout for informer watches * Fix #3805: DeletionTimestamp and Finalizer support in Mock server. +* Fix #4638: adding a way to set the full object meta on a leadership election lock, this can be used to set owner references * Fix #4644: generate CRDs in parallel and optimize code * Fix #4659: added a generic support(apiversion, kind) method in addition to the class based check * Fix #4724: Private configuration classes cause trouble with Java native (reflection) diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java index bad72483adf..57f9e2d6e77 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java @@ -33,6 +33,10 @@ public ConfigMapLock(String configMapNamespace, String configMapName, String ide super(configMapNamespace, configMapName, identity); } + public ConfigMapLock(ObjectMeta meta, String identity) { + super(meta, identity); + } + @Override protected Class getKind() { return ConfigMap.class; diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java index 1463b09e449..8e4c7cae748 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java @@ -29,6 +29,10 @@ public LeaseLock(String leaseNamespace, String leaseName, String identity) { super(leaseNamespace, leaseName, identity); } + public LeaseLock(ObjectMeta meta, String identity) { + super(meta, identity); + } + @Override protected Class getKind() { return Lease.class; diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ResourceLock.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ResourceLock.java index 3a1caaf1196..d321efc9085 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ResourceLock.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ResourceLock.java @@ -29,13 +29,17 @@ public abstract class ResourceLock implements Lock { - private final String namespace; - private final String name; + private final ObjectMeta meta; private final String identity; public ResourceLock(String namespace, String name, String identity) { - this.namespace = Objects.requireNonNull(namespace, "namespace is required"); - this.name = Objects.requireNonNull(name, "name is required"); + this(new ObjectMetaBuilder().withNamespace(namespace).withName(name).build(), identity); + } + + public ResourceLock(ObjectMeta meta, String identity) { + this.meta = meta; + Objects.requireNonNull(meta.getNamespace(), "namespace is required"); + Objects.requireNonNull(meta.getName(), "name is required"); this.identity = Objects.requireNonNull(identity, "identity is required"); } @@ -47,7 +51,7 @@ public LeaderElectionRecord get(KubernetesClient client) { } private Optional getResource(KubernetesClient client) { - return Optional.ofNullable(client.resources(getKind()).inNamespace(namespace).withName(name).get()); + return Optional.ofNullable(client.resources(getKind()).inNamespace(meta.getNamespace()).withName(meta.getName()).get()); } @Override @@ -66,7 +70,6 @@ public void update(KubernetesClient client, LeaderElectionRecord leaderElectionR * * @param leaderElectionRecord * @param meta not null - * @param current may be null * @return */ protected abstract T toResource(LeaderElectionRecord leaderElectionRecord, ObjectMeta meta); @@ -80,7 +83,7 @@ protected LeaderElectionRecord toRecordInternal(T resource) { protected abstract LeaderElectionRecord toRecord(T resource); protected ObjectMeta getObjectMeta(Serializable version) { - return new ObjectMetaBuilder().withNamespace(namespace).withName(name).withResourceVersion((String) version).build(); + return new ObjectMetaBuilder(meta).withResourceVersion((String) version).build(); } /** @@ -96,7 +99,7 @@ public String identity() { */ @Override public String describe() { - return String.format("%sLock: %s - %s (%s)", getKind().getSimpleName(), namespace, name, identity); + return String.format("%sLock: %s - %s (%s)", getKind().getSimpleName(), meta.getNamespace(), meta.getName(), identity); } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java index a6f2bb8bc47..6e7d63a279d 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java @@ -16,62 +16,33 @@ package io.fabric8.kubernetes.client.extended.leaderelection.resourcelock; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.ConfigMapList; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.ReplaceDeletable; -import io.fabric8.kubernetes.client.dsl.Resource; -import org.junit.jupiter.api.AfterEach; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; -import org.mockito.Answers; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Collections; -import java.util.HashMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; class ConfigMapLockTest { private KubernetesClient kc; - private MixedOperation> configMaps; - private ConfigMapBuilder configMapBuilder; - private ConfigMapBuilder.MetadataNested metadata; @BeforeEach void setUp() { kc = mock(KubernetesClient.class, RETURNS_DEEP_STUBS); - configMaps = mock(MixedOperation.class, RETURNS_DEEP_STUBS); - configMapBuilder = Mockito.mock(ConfigMapBuilder.class, RETURNS_DEEP_STUBS); - metadata = mock(ConfigMapBuilder.MetadataNested.class, RETURNS_DEEP_STUBS); - when(kc.configMaps().inNamespace(anyString())).thenReturn(configMaps); - when(configMapBuilder.editOrNewMetadata()).thenReturn(metadata); - } - - @AfterEach - void tearDown() { - metadata = null; - configMapBuilder = null; - configMaps = null; - kc = null; } @Test @@ -102,7 +73,6 @@ void missingIdentityShouldThrowException() { void getWithExistingConfigMapShouldReturnLeaderElectionRecord() { // Given final ConfigMap cm = new ConfigMap(); - when(configMaps.withName(ArgumentMatchers.eq("name")).get()).thenReturn(cm); cm.setMetadata(new ObjectMetaBuilder() .withAnnotations( Collections.singletonMap("control-plane.alpha.kubernetes.io/leader", @@ -110,7 +80,7 @@ void getWithExistingConfigMapShouldReturnLeaderElectionRecord() { .withResourceVersion("313373").build()); final ConfigMapLock lock = new ConfigMapLock("namespace", "name", "1337"); // When - final LeaderElectionRecord result = lock.get(kc); + final LeaderElectionRecord result = lock.toRecordInternal(cm); // Then assertNotNull(result); assertEquals("313373", result.getVersion()); @@ -128,18 +98,12 @@ void createWithValidLeaderElectionRecordShouldSendPostRequest() throws Exception // When lock.create(kc, record); // Then - verify(configMaps.withName("name"), times(1)).create(any(ConfigMap.class)); + verify(kc.resource(any(ConfigMap.class))).create(); } @Test - void updateWithValidLeaderElectionRecordShouldSendPutRequest() throws Exception { + void updateWithValidLeaderElectionRecordShouldSendPatchRequest() throws Exception { // Given - final Resource configMapResource = configMaps.withName("name"); - final ReplaceDeletable replaceable = mock(ReplaceDeletable.class, Answers.RETURNS_DEEP_STUBS); - when(configMapResource.lockResourceVersion(any())).thenReturn(replaceable); - final ConfigMap configMapInTheCluster = new ConfigMap(); - configMapInTheCluster.setMetadata(new ObjectMetaBuilder().withAnnotations(new HashMap<>()).build()); - when(configMapResource.get()).thenReturn(configMapInTheCluster); final LeaderElectionRecord record = new LeaderElectionRecord( "1337", Duration.ofSeconds(1), ZonedDateTime.now(), ZonedDateTime.now(), 0); record.setVersion("313373"); @@ -147,7 +111,7 @@ void updateWithValidLeaderElectionRecordShouldSendPutRequest() throws Exception // When lock.update(kc, record); // Then - verify(replaceable, times(1)).replace(eq(configMapInTheCluster)); + verify(kc.resource(any(ConfigMap.class))).patch(any(PatchContext.class)); } @Test diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLockTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLockTest.java index 77bb18a1c63..c2d9ef8a167 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLockTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLockTest.java @@ -18,16 +18,11 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.coordination.v1.Lease; import io.fabric8.kubernetes.api.model.coordination.v1.LeaseBuilder; -import io.fabric8.kubernetes.api.model.coordination.v1.LeaseList; -import io.fabric8.kubernetes.api.model.coordination.v1.LeaseSpec; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.ReplaceDeletable; -import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; -import org.mockito.Answers; import java.time.Duration; import java.time.ZoneId; @@ -37,32 +32,17 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; class LeaseLockTest { private KubernetesClient kc; - private MixedOperation> leases; - private LeaseBuilder leaserBuilder; - private LeaseBuilder.MetadataNested metadata; - private LeaseBuilder.SpecNested spec; @BeforeEach void setUp() { kc = mock(KubernetesClient.class, RETURNS_DEEP_STUBS); - leases = mock(MixedOperation.class, RETURNS_DEEP_STUBS); - leaserBuilder = mock(LeaseBuilder.class, RETURNS_DEEP_STUBS); - metadata = mock(LeaseBuilder.MetadataNested.class, RETURNS_DEEP_STUBS); - spec = mock(LeaseBuilder.SpecNested.class, RETURNS_DEEP_STUBS); - when(kc.leases().inNamespace(anyString())).thenReturn(leases); - when(leaserBuilder.withNewMetadata()).thenReturn(metadata); - when(leaserBuilder.withNewSpec()).thenReturn(spec); } @Test @@ -99,11 +79,10 @@ void getWithExistingLeaseShouldReturnLeaderElectionRecord() { .withRenewTime(ZonedDateTime.of(2015, 10, 21, 7, 28, 0, 0, ZoneId.of("UTC"))) .withLeaseTransitions(0) .endSpec().build(); - when(leases.withName(eq("name")).get()).thenReturn(lease); lease.setMetadata(new ObjectMetaBuilder().withResourceVersion("313373").build()); final LeaseLock lock = new LeaseLock("namespace", "name", "1337"); // When - final LeaderElectionRecord result = lock.get(kc); + final LeaderElectionRecord result = lock.toRecordInternal(lease); // Then assertNotNull(result); assertEquals("313373", result.getVersion()); @@ -121,27 +100,21 @@ void createWithValidLeaderElectionRecordShouldSendPostRequest() throws Exception // When lock.create(kc, record); // Then - verify(leases.withName("name"), times(1)).create(any(Lease.class)); + verify(kc.resource(any(Lease.class))).create(); } @Test - void updateWithValidLeaderElectionRecordShouldSendPutRequest() throws Exception { + void updateWithValidLeaderElectionRecordShouldSendPatchRequest() throws Exception { // Given - final Resource leaseResource = leases.withName("name"); - final ReplaceDeletable replaceable = mock(ReplaceDeletable.class, Answers.RETURNS_DEEP_STUBS); - when(leaseResource.lockResourceVersion(any())).thenReturn(replaceable); - final Lease leaseInTheCluster = new Lease(); - leaseInTheCluster.setSpec(new LeaseSpec()); - when(leaseResource.get()).thenReturn(leaseInTheCluster); final LeaderElectionRecord record = new LeaderElectionRecord( "1337", Duration.ofSeconds(1), ZonedDateTime.now(), ZonedDateTime.now(), 0); record.setVersion("313373"); final LeaseLock lock = new LeaseLock("namespace", "name", "1337"); + Lease lease = lock.toResource(record, lock.getObjectMeta("313373")); // When lock.update(kc, record); // Then - verify(replaceable, times(1)).replace(eq(leaseInTheCluster)); - assertEquals("1337", leaseInTheCluster.getSpec().getHolderIdentity()); + verify(kc.resource(lease)).patch(any(PatchContext.class)); } @Test diff --git a/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/LeaderElectionExamples.java b/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/LeaderElectionExamples.java index f9b1b0da861..e3a16f69522 100644 --- a/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/LeaderElectionExamples.java +++ b/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/LeaderElectionExamples.java @@ -186,7 +186,7 @@ private LeaderElector leader(String id, Function lockSupplier) { .withLeaderCallbacks(new LeaderCallbacks( () -> System.out.printf("\r%1$s: I just became leader!!!%n", id), () -> { - leaderReference.updateAndGet(s -> id.equals(s)?null:s); + leaderReference.updateAndGet(s -> id.equals(s) ? null : s); System.out.printf("\r%1$s: I just lost my leadership :(%n", id); }, leaderReference::set)) diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/LeaderIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/LeaderIT.java new file mode 100644 index 00000000000..75cf2c64d20 --- /dev/null +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/LeaderIT.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 + * + * http://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 io.fabric8.kubernetes; + +import io.fabric8.junit.jupiter.api.RequireK8sSupport; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.coordination.v1.Lease; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks; +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfigBuilder; +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElector; +import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@RequireK8sSupport(Lease.class) +class LeaderIT { + + KubernetesClient client; + + @Test + void testLeadershipCycle() throws InterruptedException, ExecutionException, TimeoutException { + final String lockIdentity = UUID.randomUUID().toString(); + CompletableFuture leading = new CompletableFuture<>(); + CompletableFuture stopped = new CompletableFuture<>(); + CompletableFuture changed = new CompletableFuture<>(); + LeaderElector leader = client.leaderElector() + .withConfig( + new LeaderElectionConfigBuilder() + .withReleaseOnCancel() + .withName("Sample Leader Election configuration") + .withLeaseDuration(Duration.ofSeconds(30L)) + .withLock(new LeaseLock(new ObjectMetaBuilder().withName("sample").withNamespace(client.getNamespace()).build(), + lockIdentity)) + .withRenewDeadline(Duration.ofSeconds(10L)) + .withRetryPeriod(Duration.ofSeconds(2L)) + .withLeaderCallbacks(new LeaderCallbacks( + () -> leading.complete(null), + () -> stopped.complete(null), + changed::complete)) + .build()) + .build(); + CompletableFuture f = leader.start(); + + leading.get(10, TimeUnit.SECONDS); + assertTrue(!stopped.isDone()); + f.cancel(true); + stopped.get(10, TimeUnit.SECONDS); + assertEquals(lockIdentity, changed.getNow(null)); + } + +}