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 d02b35ae..1e4fcf04 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 @@ -63,7 +63,9 @@ public String createConfigMap(String configMapNamespace, String name, Mapidc>default,所以不需要额外层级设置了 - if (StringUtils.isBlank(cluster)){ + // cluster 就是用户定义>idc>default,所以已经不需要额外层级设置了 + if (StringUtils.isBlank(cluster)) { configMapKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join("default", namespace); return; } configMapKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(cluster, namespace); } - void setConfigMapName(String appId, boolean syncImmediately){ + public String getConfigMapKey() { + return configMapKey; + } + + public String getConfigMapName() { + return configMapName; + } + + void setConfigMapName(String appId, boolean syncImmediately) { configMapName = appId; + // 初始化configmap this.checkConfigMapName(configMapName); if (syncImmediately) { this.sync(); @@ -108,14 +116,13 @@ void setConfigMapName(String appId, boolean syncImmediately){ private void checkConfigMapName(String configMapName) { if (StringUtils.isBlank(configMapName)) { - throw new ApolloConfigException("ConfigMap name cannot be null"); + throw new IllegalArgumentException("ConfigMap name cannot be null"); } // 判断configMap是否存在,若存在直接返回,若不存在尝试创建 - if(kubernetesManager.checkConfigMapExist(configMapNamespace, configMapName)){ + if (kubernetesManager.checkConfigMapExist(configMapNamespace, configMapName)) { return; } // TODO 初步理解这里只生成就可以,后续update事件再写入新值 - Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "createK8sConfigMap"); transaction.addData("configMapName", configMapName); try { @@ -130,6 +137,12 @@ private void checkConfigMapName(String configMapName) { } } + /** + * TODO 测试点: + * 1. 从上游成功恢复(开启文件存储) + * 2. 从上游成功恢复(没开启文件存储,从remote) + * 3. 从k8s成功恢复 + */ @Override public Properties getConfig() { if (configMapProperties == null) { @@ -142,6 +155,7 @@ public Properties getConfig() { /** * Update the memory when the configuration center changes + * * @param upstreamConfigRepository the upstream repo */ @Override @@ -198,7 +212,6 @@ protected void sync() { } // 职责明确: manager层进行序列化和解析,把key传进去 - // repo这里只负责更新内存, Properties和appConfig格式的兼容 public Properties loadFromK8sConfigMap() throws IOException { Preconditions.checkNotNull(configMapName, "ConfigMap name cannot be null"); @@ -207,8 +220,8 @@ public Properties loadFromK8sConfigMap() throws IOException { // 从ConfigMap获取整个配置信息的JSON字符串 String jsonConfig = kubernetesManager.getValueFromConfigMap(configMapNamespace, configUtil.getAppId(), configMapKey); if (jsonConfig == null) { - // TODO 待修改,重试访问保底configmap - jsonConfig = kubernetesManager.getValueFromConfigMap(configMapNamespace, configMapName, Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join("default", namespace)); + // TODO 待修改,先重试访问idc再default保底 + jsonConfig = kubernetesManager.getValueFromConfigMap(configMapNamespace, configMapName, Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(configUtil, namespace)); } // 确保获取到的配置信息不为空 @@ -223,7 +236,8 @@ public Properties loadFromK8sConfigMap() throws IOException { // 使用Gson将JSON字符串转换为Map对象 if (jsonConfig != null && !jsonConfig.isEmpty()) { Gson gson = new Gson(); - Type type = new TypeToken>() {}.getType(); + Type type = new TypeToken>() { + }.getType(); Map configMap = gson.fromJson(jsonConfig, type); // 将Map中的键值对填充到Properties对象中 for (Map.Entry entry : configMap.entrySet()) { @@ -243,22 +257,31 @@ private boolean trySyncFromUpstream() { return false; } try { - // TODO 从上游数据恢复的逻辑 // 拉新数据,并将新数据更新到configMap updateConfigMapProperties(upstream.getConfig(), upstream.getSourceType()); return true; } catch (Throwable ex) { Tracer.logError(ex); - logger - .warn("Sync config from upstream repository {} failed, reason: {}", upstream.getClass(), - ExceptionUtil.getDetailMessage(ex)); + logger.warn("Sync config from upstream repository {} failed, reason: {}", upstream.getClass(), + ExceptionUtil.getDetailMessage(ex)); } return false; } + // 从上游数据恢复,并更新configmap + private synchronized void updateConfigMapProperties(Properties newProperties, ConfigSourceType sourceType) { + this.sourceType = sourceType; + if (newProperties.equals(configMapProperties)) { + return; + } + this.configMapProperties = newProperties; + persistConfigMap(configMapProperties); + } + /** * Update the memory - * @param namespace the namespace of this repository change + * + * @param namespace the namespace of this repository change * @param newProperties the properties after change */ @Override @@ -272,15 +295,6 @@ public void onRepositoryChange(String namespace, Properties newProperties) { this.fireRepositoryChange(namespace, newProperties); } - private synchronized void updateConfigMapProperties(Properties newProperties, ConfigSourceType sourceType) { - this.sourceType = sourceType; - if (newProperties.equals(configMapProperties)) { - return; - } - this.configMapProperties = newProperties; - persistConfigMap(configMapProperties); - } - public void persistConfigMap(Properties properties) { // 将Properties中的值持久化到configmap中,并使用事务管理 Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "persistK8sConfigMap"); @@ -308,7 +322,6 @@ public void persistConfigMap(Properties properties) { } finally { transaction.complete(); } - transaction.complete(); } } diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/K8sConfigMapConfigRepositoryTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/K8sConfigMapConfigRepositoryTest.java index ba38fc99..81f04739 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/K8sConfigMapConfigRepositoryTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/K8sConfigMapConfigRepositoryTest.java @@ -17,23 +17,31 @@ package com.ctrip.framework.apollo.internals; import com.ctrip.framework.apollo.Kubernetes.KubernetesManager; +import com.ctrip.framework.apollo.build.MockInjector; import com.ctrip.framework.apollo.enums.ConfigSourceType; import com.ctrip.framework.apollo.exceptions.ApolloConfigException; import com.ctrip.framework.apollo.util.ConfigUtil; +import com.ctrip.framework.apollo.util.factory.PropertiesFactory; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import java.util.Base64; import java.util.Properties; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; +import static org.springframework.test.util.ReflectionTestUtils.setField; -/** - * TODO (未完成)K8sConfigMapConfigRepository单元测试 - */ public class K8sConfigMapConfigRepositoryTest { - private String someNamespace; private ConfigRepository upstreamRepo; private Properties someProperties; @@ -44,26 +52,125 @@ public class K8sConfigMapConfigRepositoryTest { private ConfigSourceType someSourceType; @Mock + private CoreV1Api coreV1Api; + @Mock + private ApiClient client; + + @InjectMocks private KubernetesManager kubernetesManager; @Mock private ConfigUtil configUtil; - private K8sConfigMapConfigRepository k8sConfigMapConfigRepository; - @Before public void setUp() { - MockitoAnnotations.initMocks(this); - when(configUtil.getAppId()).thenReturn("testAppId"); - when(configUtil.getCluster()).thenReturn("default"); - when(configUtil.getConfigMapNamespace()).thenReturn("default"); + someNamespace = "someName"; + // 初始化上游数据源 someProperties = new Properties(); defaultKey = "defaultKey"; defaultValue = "defaultValue"; someProperties.setProperty(defaultKey, defaultValue); + someSourceType = ConfigSourceType.LOCAL; + upstreamRepo = mock(ConfigRepository.class); + when(upstreamRepo.getConfig()).thenReturn(someProperties); + when(upstreamRepo.getSourceType()).thenReturn(someSourceType); + + // mock configutil类 + MockitoAnnotations.initMocks(this); + when(configUtil.getAppId()).thenReturn("testAppId"); + when(configUtil.getCluster()).thenReturn("default"); + when(configUtil.getConfigMapNamespace()).thenReturn("default"); + + MockInjector.setInstance(ConfigUtil.class, new MockConfigUtil()); + PropertiesFactory propertiesFactory = mock(PropertiesFactory.class); + when(propertiesFactory.getPropertiesInstance()).thenAnswer(new Answer() { + @Override + public Properties answer(InvocationOnMock invocation) { + return new Properties(); + } + }); + MockInjector.setInstance(PropertiesFactory.class, propertiesFactory); + } + + @After + public void tearDown() throws Exception { + MockInjector.reset(); + } + + @Test(expected = ApolloConfigException.class) + public void testConstructorWithNullNamespace() { + new K8sConfigMapConfigRepository(null); + } + + @Test + public void testSetConfigMapKey() { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); + repo.setConfigMapKey(someCluster, someNamespace); + assertEquals(someCluster + someNamespace, repo.getConfigMapKey()); + } + + @Test + public void testSetConfigMapName() { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); + repo.setConfigMapName(someAppId, false); + assertEquals(someAppId, repo.getConfigMapName()); + } + + @Test(expected = ApolloConfigException.class) + public void testSetConfigMapNameWithNullAppId() { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); + repo.setConfigMapName(null, false); + } + + + @Test + public void testOnRepositoryChange() throws Exception { + RepositoryChangeListener someListener = mock(RepositoryChangeListener.class); + + // 创建一个 LocalFileConfigRepository 实例,作为上游仓库 + LocalFileConfigRepository upstreamRepo = mock(LocalFileConfigRepository.class); + when(upstreamRepo.getSourceType()).thenReturn(ConfigSourceType.LOCAL); + + // 创建一个模拟的 KubernetesManager + KubernetesManager mockKubernetesManager = mock(KubernetesManager.class); + when(mockKubernetesManager.checkConfigMapExist(anyString(), anyString())).thenReturn(true); + doNothing().when(mockKubernetesManager).createConfigMap(anyString(), anyString(), any()); + + K8sConfigMapConfigRepository k8sConfigMapConfigRepository = new K8sConfigMapConfigRepository("someNamespace", upstreamRepo); + k8sConfigMapConfigRepository.initialize(); + + // 设置本地缓存目录并添加监听器 + k8sConfigMapConfigRepository.addChangeListener(someListener); + k8sConfigMapConfigRepository.getConfig(); + + Properties anotherProperties = new Properties(); + anotherProperties.put("anotherKey", "anotherValue"); + ConfigSourceType anotherSourceType = ConfigSourceType.LOCAL; + when(upstreamRepo.getSourceType()).thenReturn(anotherSourceType); - k8sConfigMapConfigRepository = new K8sConfigMapConfigRepository("namespace", null); + // 调用 onRepositoryChange 方法,模拟仓库配置发生变化 + k8sConfigMapConfigRepository.onRepositoryChange("someNamespace", anotherProperties); + + // 使用 ArgumentCaptor 捕获监听器的调用参数 + final ArgumentCaptor captor = ArgumentCaptor.forClass(Properties.class); + verify(someListener, times(1)).onRepositoryChange(eq("someNamespace"), captor.capture()); + + // 断言捕获的配置与 anotherProperties 相同 + assertEquals(anotherProperties, captor.getValue()); + // 断言 K8sConfigMapConfigRepository 的源类型更新为 anotherSourceType + assertEquals(anotherSourceType, k8sConfigMapConfigRepository.getSourceType()); + } + + /** + * 测试persistConfigMap方法成功持久化配置信息 + */ + @Test + public void testPersistConfigMap() { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); + doNothing().when(kubernetesManager).updateConfigMap(anyString(), anyString(), anyMap()); + repo.persistConfigMap(someProperties); + verify(kubernetesManager, times(1)).updateConfigMap(anyString(), anyString(), anyMap()); } /** @@ -71,100 +178,111 @@ public void setUp() { */ @Test public void testSyncSuccessFromUpstream() throws Throwable { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); + // arrange ConfigRepository upstream = mock(ConfigRepository.class); Properties upstreamProperties = new Properties(); upstreamProperties.setProperty("key", "value"); when(upstream.getConfig()).thenReturn(upstreamProperties); when(upstream.getSourceType()).thenReturn(ConfigSourceType.REMOTE); - k8sConfigMapConfigRepository.setUpstreamRepository(upstream); + repo.setUpstreamRepository(upstream); + +// // mock KubernetesManager +// when(kubernetesManager.createConfigMap(anyString(), anyString(), anyMap())) +// .thenReturn(true); +// setField(repo, "kubernetesManager", kubernetesManager); // act - k8sConfigMapConfigRepository.sync(); + repo.sync(); // assert verify(upstream, times(1)).getConfig(); } - /** - * 测试sync方法从上游数据源同步失败,成功从Kubernetes的ConfigMap中加载 - */ @Test - public void testSyncFailFromUpstreamSuccessFromConfigMap() throws Throwable { - // arrange - ConfigRepository upstream = mock(ConfigRepository.class); - when(upstream.getConfig()).thenThrow(new RuntimeException("Upstream sync failed")); - k8sConfigMapConfigRepository.setUpstreamRepository(upstream); - when(kubernetesManager.getValueFromConfigMap(anyString(), anyString(), anyString())).thenReturn("encodedConfig"); + public void testSyncFromUpstreamWithFileStorage() throws Exception { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); - // act - k8sConfigMapConfigRepository.sync(); - // assert - verify(kubernetesManager, times(1)).getValueFromConfigMap(anyString(), anyString(), anyString()); + Properties upstreamProperties = new Properties(); + upstreamProperties.setProperty("key1", "value1"); + + when(upstreamRepo.getConfig()).thenReturn(upstreamProperties); + when(upstreamRepo.getSourceType()).thenReturn(ConfigSourceType.LOCAL); + + repo.sync(); + + Properties config = repo.getConfig(); + assertEquals("value1", config.getProperty("key1")); + assertEquals(ConfigSourceType.LOCAL, repo.getSourceType()); } - /** - * 测试loadFromK8sConfigMap方法成功加载配置信息 - */ @Test - public void testLoadFromK8sConfigMapSuccess() throws Throwable { - // arrange - when(kubernetesManager.getValueFromConfigMap(anyString(), anyString(), anyString())).thenReturn("encodedConfig"); + public void testSyncFromUpstreamWithRemote() throws Exception { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); - // act - Properties properties = k8sConfigMapConfigRepository.loadFromK8sConfigMap(); + Properties upstreamProperties = new Properties(); + upstreamProperties.setProperty("key2", "value2"); - // assert - verify(kubernetesManager, times(1)).getValueFromConfigMap(anyString(), anyString(), anyString()); - // 这里应该有更具体的断言来验证properties的内容,但由于编码和解码逻辑未给出,此处省略 + when(upstreamRepo.getConfig()).thenReturn(upstreamProperties); + when(upstreamRepo.getSourceType()).thenReturn(ConfigSourceType.REMOTE); + + repo.sync(); + + Properties config = repo.getConfig(); + assertEquals("value2", config.getProperty("key2")); + assertEquals(ConfigSourceType.REMOTE, repo.getSourceType()); } - /** - * 测试loadFromK8sConfigMap方法在加载配置信息时发生异常 - */ - @Test(expected = ApolloConfigException.class) - public void testLoadFromK8sConfigMapException() throws Throwable { - // arrange - when(kubernetesManager.getValueFromConfigMap(anyString(), anyString(), anyString())).thenThrow(new RuntimeException("Load failed")); + @Test + public void testSyncFromK8s() throws Exception { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); - // act - k8sConfigMapConfigRepository.loadFromK8sConfigMap(); + Properties k8sProperties = new Properties(); + k8sProperties.setProperty("key3", "value3"); - // assert - // 预期抛出ApolloConfigException + when(kubernetesManager.getValueFromConfigMap(anyString(), anyString(), anyString())) + .thenReturn(Base64.getEncoder().encodeToString("{\"key3\":\"value3\"}".getBytes())); + + repo.sync(); + + Properties config = repo.getConfig(); + assertEquals("value3", config.getProperty("key3")); + assertEquals(ConfigSourceType.CONFIGMAP, repo.getSourceType()); } + /** - * 测试persistConfigMap方法成功持久化配置信息 + * 测试sync方法从上游数据源同步失败,成功从Kubernetes的ConfigMap中加载 */ @Test - public void testPersistConfigMapSuccess() throws Throwable { + public void testSyncFailFromUpstreamSuccessFromConfigMap() throws Throwable { + K8sConfigMapConfigRepository repo = new K8sConfigMapConfigRepository(someNamespace); // arrange - Properties properties = new Properties(); - properties.setProperty("key", "value"); + ConfigRepository upstream = mock(ConfigRepository.class); + when(upstream.getConfig()).thenThrow(new RuntimeException("Upstream sync failed")); + repo.setUpstreamRepository(upstream); + when(kubernetesManager.getValueFromConfigMap(anyString(), anyString(), anyString())).thenReturn("encodedConfig"); // act - k8sConfigMapConfigRepository.persistConfigMap(properties); + repo.sync(); // assert - verify(kubernetesManager, times(1)).updateConfigMap(anyString(), anyString(), anyMap()); + verify(kubernetesManager, times(1)).getValueFromConfigMap(anyString(), anyString(), anyString()); } - /** - * 测试persistConfigMap方法在持久化配置信息时发生异常 - */ - @Test(expected = ApolloConfigException.class) - public void testPersistConfigMapException() throws Throwable { - // arrange - Properties properties = new Properties(); - properties.setProperty("key", "value"); - doThrow(new RuntimeException("Persist failed")).when(kubernetesManager).updateConfigMap(anyString(), anyString(), anyMap()); - // act - k8sConfigMapConfigRepository.persistConfigMap(properties); + public static class MockConfigUtil extends ConfigUtil { + @Override + public String getAppId() { + return someAppId; + } - // assert - // 预期抛出ApolloConfigException + @Override + public String getCluster() { + return someCluster; + } } + } diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/LocalFileConfigRepositoryTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/LocalFileConfigRepositoryTest.java index 8bf7b7be..dd9a75fa 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/LocalFileConfigRepositoryTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/LocalFileConfigRepositoryTest.java @@ -64,6 +64,7 @@ public void setUp() throws Exception { someBaseDir.mkdir(); someNamespace = "someName"; + someProperties = new Properties(); defaultKey = "defaultKey"; defaultValue = "defaultValue";