diff --git a/CHANGES.md b/CHANGES.md index c2f6ac72..10017ab8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Apollo Java 2.4.0 * [Fix monitor arg cause npe](https://github.com/apolloconfig/apollo-java/pull/86) * [Fix the concurrent issue in SpringValueRegistry.scanAndClean](https://github.com/apolloconfig/apollo-java/pull/95) * [Feature support incremental configuration synchronization client](https://github.com/apolloconfig/apollo-java/pull/90) +* [Feature reduce conflicts when update configmap in k8](https://github.com/apolloconfig/apollo-java/pull/93) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/4?closed=1) diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/kubernetes/KubernetesManager.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/kubernetes/KubernetesManager.java index 4cf96f38..75f92e49 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/kubernetes/KubernetesManager.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/kubernetes/KubernetesManager.java @@ -18,27 +18,44 @@ import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.exceptions.ApolloConfigException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodList; import io.kubernetes.client.util.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; +/** + * Manages Kubernetes ConfigMap operations. + * Required Kubernetes permissions: + * - pods: [get, list] - For pod selection and write eligibility + * - configmaps: [get, create, update] - For ConfigMap operations + */ @Service public class KubernetesManager { private static final Logger logger = LoggerFactory.getLogger(KubernetesManager.class); + private static final String RUNNING_POD_FIELD_SELECTOR = "status.phase=Running"; + + private static final int MAX_SEARCH_NUM = 100; + private ApiClient client; private CoreV1Api coreV1Api; + private int propertyKubernetesMaxWritePods = 3; + private String localPodName = System.getenv("HOSTNAME"); public KubernetesManager() { try { @@ -51,8 +68,11 @@ public KubernetesManager() { } } - public KubernetesManager(CoreV1Api coreV1Api) { + @VisibleForTesting + public KubernetesManager(CoreV1Api coreV1Api, String localPodName, int propertyKubernetesMaxWritePods) { this.coreV1Api = coreV1Api; + this.localPodName = localPodName; + this.propertyKubernetesMaxWritePods = propertyKubernetesMaxWritePods; } private V1ConfigMap buildConfigMap(String name, String namespace, Map data) { @@ -132,6 +152,10 @@ public boolean updateConfigMap(String k8sNamespace, String name, Map metadata.getCreationTimestamp() != null) + .sorted(Comparator.comparing(V1ObjectMeta::getCreationTimestamp)) + .map(V1ObjectMeta::getName) + .limit(propertyKubernetesMaxWritePods) + .anyMatch(localPodName::equals); + } catch (Exception e) { + logger.info("Error determining write pod eligibility:{}", e.getMessage(), e); + return true; + } + } } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java index af18c988..09e0ba2f 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java @@ -617,7 +617,7 @@ private void initClientMonitorExceptionQueueSize() { public int getMonitorExceptionQueueSize() { return monitorExceptionQueueSize; } - + private boolean getPropertyBoolean(String propertyName, String envName, boolean defaultVal) { String enablePropertyNamesCache = System.getProperty(propertyName); if (Strings.isNullOrEmpty(enablePropertyNamesCache)) { diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/kubernetes/KubernetesManagerTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/kubernetes/KubernetesManagerTest.java index c756d35d..f8c3acee 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/kubernetes/KubernetesManagerTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/kubernetes/KubernetesManagerTest.java @@ -21,14 +21,30 @@ import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodList; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; +import java.time.OffsetDateTime; +import java.util.Collections; import java.util.HashMap; import java.util.Map; -import static org.mockito.Mockito.*; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class KubernetesManagerTest { @@ -38,7 +54,7 @@ public class KubernetesManagerTest { @Before public void setUp() { coreV1Api = mock(CoreV1Api.class); - kubernetesManager = new KubernetesManager(coreV1Api); + kubernetesManager = new KubernetesManager(coreV1Api, "localPodName", 3); MockInjector.setInstance(KubernetesManager.class, kubernetesManager); MockInjector.setInstance(CoreV1Api.class, coreV1Api); @@ -58,13 +74,13 @@ public void testCreateConfigMapSuccess() throws Exception { .metadata(new V1ObjectMeta().name(name).namespace(namespace)) .data(data); - when(coreV1Api.createNamespacedConfigMap(eq(namespace), eq(configMap), isNull(), isNull(), isNull(),isNull())).thenReturn(configMap); + when(coreV1Api.createNamespacedConfigMap(eq(namespace), eq(configMap), isNull(), isNull(), isNull(), isNull())).thenReturn(configMap); // act String result = kubernetesManager.createConfigMap(namespace, name, data); // assert - verify(coreV1Api, times(1)).createNamespacedConfigMap(eq(namespace), any(V1ConfigMap.class), isNull(), isNull(), isNull(),isNull()); + verify(coreV1Api, times(1)).createNamespacedConfigMap(eq(namespace), any(V1ConfigMap.class), isNull(), isNull(), isNull(), isNull()); assert name.equals(result); } @@ -82,7 +98,7 @@ public void testCreateConfigMapNullData() throws Exception { String result = kubernetesManager.createConfigMap(namespace, name, data); // assert - verify(coreV1Api, times(1)).createNamespacedConfigMap(eq(namespace), any(V1ConfigMap.class), isNull(), isNull(), isNull(),isNull()); + verify(coreV1Api, times(1)).createNamespacedConfigMap(eq(namespace), any(V1ConfigMap.class), isNull(), isNull(), isNull(), isNull()); assert name.equals(result); } @@ -135,20 +151,40 @@ public void testUpdateConfigMapSuccess() throws Exception { // arrange String namespace = "default"; String name = "testConfigMap"; - Map data = new HashMap<>(); - data.put("key", "value"); + + V1Pod pod = new V1Pod() + .metadata( + new V1ObjectMeta() + .name("localPodName") + .creationTimestamp(OffsetDateTime.now()) + .labels(Collections.singletonMap("app", "app"))); + V1PodList v1PodList = new V1PodList().addItemsItem(new V1Pod().metadata(pod.getMetadata())); + + Map existData = new HashMap<>(); + existData.put("key", "value"); V1ConfigMap configMap = new V1ConfigMap(); configMap.metadata(new V1ObjectMeta().name(name).namespace(namespace)); - configMap.data(data); + configMap.data(existData); + when(coreV1Api.readNamespacedPod("localPodName", namespace, null)).thenReturn(pod); + when(coreV1Api.listNamespacedPod(namespace, null, null, + null, null, "app=app", + null, null, null + , null, null)).thenReturn(v1PodList); when(coreV1Api.readNamespacedConfigMap(name, namespace, null)).thenReturn(configMap); when(coreV1Api.replaceNamespacedConfigMap(name, namespace, configMap, null, null, null, null)).thenReturn(configMap); // act - Boolean success = kubernetesManager.updateConfigMap(namespace, name, data); + HashMap updateData = new HashMap<>(existData); + updateData.put("newKey","newValue"); + boolean success = kubernetesManager.updateConfigMap(namespace, name, updateData); // assert assertTrue(success); + Mockito.verify(coreV1Api, Mockito.times(1)).listNamespacedPod(namespace, null, null, + null, "status.phase=Running", "app=app", + 100, null, null + , null, null); } /**