diff --git a/CHANGES.md b/CHANGES.md index ed756c3f80e..0354e920e1f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Apollo 2.4.0 * [Update the server config link in system info page](https://github.com/apolloconfig/apollo/pull/5204) * [Feature support portal restTemplate Client connection pool config](https://github.com/apolloconfig/apollo/pull/5200) * [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182) - +* [Feature added determine appid+cluster namespace num limit logic](https://github.com/apolloconfig/apollo/pull/5227) +* ------------------ -All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) \ No newline at end of file +All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java index baf8ec3187e..fe26e81bc45 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java @@ -40,7 +40,6 @@ public class BizConfig extends RefreshableConfig { private static final int DEFAULT_ITEM_VALUE_LENGTH = 20000; private static final int DEFAULT_MAX_NAMESPACE_NUM = 200; - private static final String[] DEFAULT_NAMESPACE_NUM_LIMIT_WHITE = new String[]{}; private static final int DEFAULT_APPNAMESPACE_CACHE_REBUILD_INTERVAL = 60; //60s private static final int DEFAULT_GRAY_RELEASE_RULE_SCAN_INTERVAL = 60; //60s @@ -105,13 +104,17 @@ public int itemValueLengthLimit() { return checkInt(limit, 5, Integer.MAX_VALUE, DEFAULT_ITEM_VALUE_LENGTH); } + public boolean isNamespaceNumLimitEnabled() { + return getBooleanProperty("namespace.num.limit.enabled", false); + } + public int namespaceNumLimit() { int limit = getIntProperty("namespace.num.limit", DEFAULT_MAX_NAMESPACE_NUM); return checkInt(limit, 0, Integer.MAX_VALUE, DEFAULT_MAX_NAMESPACE_NUM); } public Set namespaceNumLimitWhite() { - return Sets.newHashSet(getArrayProperty("namespace.num.limit.white", DEFAULT_NAMESPACE_NUM_LIMIT_WHITE)); + return Sets.newHashSet(getArrayProperty("namespace.num.limit.white", new String[0])); } public Map namespaceValueLengthLimitOverride() { diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceService.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceService.java index 8c67de77d6b..93832beeafb 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceService.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceService.java @@ -354,9 +354,9 @@ public Namespace save(Namespace entity) { throw new ServiceException("namespace not unique"); } - if (!bizConfig.namespaceNumLimitWhite().contains(entity.getAppId())){ - int nowCount = namespaceRepository.countByAppIdAndClusterName(entity.getAppId(), entity.getClusterName()) ; - if(nowCount >= bizConfig.namespaceNumLimit()) { + if (bizConfig.isNamespaceNumLimitEnabled() && !bizConfig.namespaceNumLimitWhite().contains(entity.getAppId())) { + int nowCount = namespaceRepository.countByAppIdAndClusterName(entity.getAppId(), entity.getClusterName()); + if (nowCount >= bizConfig.namespaceNumLimit()) { throw new ServiceException("namespace[appId = " + entity.getAppId() + ", cluster= " + entity.getClusterName() + "] nowCount= " + nowCount + ", maxCount =" + bizConfig.namespaceNumLimit()); } } diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceServiceIntegrationTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceServiceIntegrationTest.java index 3bc57305138..acc9c99554e 100644 --- a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceServiceIntegrationTest.java +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceServiceIntegrationTest.java @@ -17,6 +17,7 @@ package com.ctrip.framework.apollo.biz.service; import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; +import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Cluster; import com.ctrip.framework.apollo.biz.entity.Commit; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; @@ -25,23 +26,31 @@ import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.entity.ReleaseHistory; import com.ctrip.framework.apollo.biz.repository.InstanceConfigRepository; +import com.ctrip.framework.apollo.biz.repository.NamespaceRepository; import com.ctrip.framework.apollo.common.entity.AppNamespace; +import com.ctrip.framework.apollo.common.exception.ServiceException; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; +import org.junit.Assert; import org.junit.Test; +import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.jdbc.Sql; import java.util.List; +import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; public class NamespaceServiceIntegrationTest extends AbstractIntegrationTest { @@ -62,6 +71,11 @@ public class NamespaceServiceIntegrationTest extends AbstractIntegrationTest { private ReleaseHistoryService releaseHistoryService; @Autowired private InstanceConfigRepository instanceConfigRepository; + @Autowired + private NamespaceRepository namespaceRepository; + + @Mock + private BizConfig bizConfig; private String testApp = "testApp"; private String testCluster = "default"; @@ -134,4 +148,97 @@ public void testGetCommitsByModifiedTime() throws ParseException { } + @Test + @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + public void testNamespaceNumLimit() { + + ReflectionTestUtils.setField(namespaceService, "bizConfig", bizConfig); + when(bizConfig.isNamespaceNumLimitEnabled()).thenReturn(true); + when(bizConfig.namespaceNumLimit()).thenReturn(2); + + + Namespace namespace = new Namespace(); + namespace.setAppId(testApp); + namespace.setClusterName(testCluster); + namespace.setNamespaceName("demo-namespace"); + namespaceService.save(namespace); + + try { + Namespace namespace2 = new Namespace(); + namespace2.setAppId(testApp); + namespace2.setClusterName(testCluster); + namespace2.setNamespaceName("demo-namespace2"); + namespaceService.save(namespace2); + + Assert.fail(); + } catch (Exception e) { + Assert.assertTrue(e instanceof ServiceException); + } + + int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster); + Assert.assertEquals(2, nowCount); + + } + + @Test + @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + public void testNamespaceNumLimitFalse() { + + ReflectionTestUtils.setField(namespaceService, "bizConfig", bizConfig); + when(bizConfig.namespaceNumLimit()).thenReturn(2); + + Namespace namespace = new Namespace(); + namespace.setAppId(testApp); + namespace.setClusterName(testCluster); + namespace.setNamespaceName("demo-namespace"); + namespaceService.save(namespace); + + try { + Namespace namespace2 = new Namespace(); + namespace2.setAppId(testApp); + namespace2.setClusterName(testCluster); + namespace2.setNamespaceName("demo-namespace2"); + namespaceService.save(namespace2); + } catch (Exception e) { + Assert.assertTrue(e instanceof ServiceException); + } + + int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster); + Assert.assertEquals(3, nowCount); + + } + + @Test + @Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + public void testNamespaceNumLimitWhite() { + + ReflectionTestUtils.setField(namespaceService, "bizConfig", bizConfig); + when(bizConfig.isNamespaceNumLimitEnabled()).thenReturn(true); + when(bizConfig.namespaceNumLimit()).thenReturn(2); + when(bizConfig.namespaceNumLimitWhite()).thenReturn(new HashSet<>(Arrays.asList(testApp))); + + Namespace namespace = new Namespace(); + namespace.setAppId(testApp); + namespace.setClusterName(testCluster); + namespace.setNamespaceName("demo-namespace"); + namespaceService.save(namespace); + + try { + Namespace namespace2 = new Namespace(); + namespace2.setAppId(testApp); + namespace2.setClusterName(testCluster); + namespace2.setNamespaceName("demo-namespace2"); + namespaceService.save(namespace2); + } catch (Exception e) { + Assert.assertTrue(e instanceof ServiceException); + } + + int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster); + Assert.assertEquals(3, nowCount); + + } + } diff --git a/doc/images/namespace-num-limit-enabled.png b/doc/images/namespace-num-limit-enabled.png new file mode 100644 index 00000000000..a415132aa12 Binary files /dev/null and b/doc/images/namespace-num-limit-enabled.png differ diff --git a/doc/images/namespace-num-limit-white.png b/doc/images/namespace-num-limit-white.png new file mode 100644 index 00000000000..34bb884ca90 Binary files /dev/null and b/doc/images/namespace-num-limit-white.png differ diff --git a/doc/images/namespace-num-limit.png b/doc/images/namespace-num-limit.png new file mode 100644 index 00000000000..45f22e79956 Binary files /dev/null and b/doc/images/namespace-num-limit.png differ diff --git a/docs/en/portal/apollo-user-guide.md b/docs/en/portal/apollo-user-guide.md index 56674a741fa..69d296efaa1 100644 --- a/docs/en/portal/apollo-user-guide.md +++ b/docs/en/portal/apollo-user-guide.md @@ -509,6 +509,33 @@ Please note that modifications to system parameters may affect the performance o ![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png) + + +## 6.4 单个命名空间下的配置项数量限制 + +Starting from version 2.4.0, apollo-portal provides the function of checking the upper limit of the number of namespaces that can be created under the appld+cluster dimension. This function is disabled by default and needs to be enabled by configuring the system `namespace.num.limit.enabled`. At the same time, the system parameter `namespace.num.limit` is provided to dynamically configure the upper limit of the number of Namespaces under the appld+cluster dimension. The default value is 200. Considering that some basic components such as gateways, message queues, Redis, and databases require special processing, a new system parameter `namespace.num.limit.white` is added to configure the verification whitelist, which is not affected by the upper limit of the number of Namespaces. + +**Setting method:** + +1. Log in to the Apollo Configuration Center interface with a super administrator account. +2. Go to the `Administrator Tools - System Parameters - ConfigDB Configuration Management` page and add or modify the `namespace.num.limit.enabled` configuration item to true/false to enable/disable this function. It is disabled by default. + +[//]: # ( ![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png)) +![item-num-limit-enabled](../../../doc/images/namespace-num-limit-enabled.png) + +3. Go to the `Administrator Tools - System Parameters - ConfigDB Configuration Management` page to add or modify the `namespace.num.limit` configuration item to configure the upper limit of the number of namespaces under a single appld+cluster. The default value is 200 + +[//]: # ( ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png)) +![item-num-limit](../../../doc/images/namespace-num-limit.png) + +4. Go to `Administrator Tools - System Parameters - ConfigDB Configuration Management` page to add or modify the `namespace.num.limit.white` configuration item to configure the whitelist for namespace quantity limit verification. Multiple AppIds are separated by English commas. + +[//]: # ( ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png)) +![item-num-limit](../../../doc/images/namespace-num-limit-white.png) + + + + # VII. Best practices ## 7.1 Security Related diff --git a/docs/zh/portal/apollo-user-guide.md b/docs/zh/portal/apollo-user-guide.md index ef42e470222..a69d11e5f0d 100644 --- a/docs/zh/portal/apollo-user-guide.md +++ b/docs/zh/portal/apollo-user-guide.md @@ -482,6 +482,28 @@ Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验 ![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png) + +## 6.4 单个命名空间下的配置项数量限制 +从2.4.0版本开始,apollo-portal提供了appld+cluster维度下可以创建的命名空间数量上限校验的功能,此功能默认关闭,需要配置系统 `namespace.num.limit.enabled` 开启,同时提供了系统参数`namespace.num.limit`来动态配置appld+cluster维度下的Namespace数量上限值,默认为200个,考虑到一些基础组件如网关、消息队列、Redis、数据库等需要特殊处理,新增了系统参数`namespace.num.limit.white` 来配置校验白名单,不受Namespace数量上限的影响 + +**设置方法:** +1. 用超级管理员账号登录到Apollo配置中心的界面 +2. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit.enabled` 配置项为true/false 即可开启/关闭此功能,默认关闭 + +[//]: # ( ![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png)) +![item-num-limit-enabled](../../../doc/images/namespace-num-limit-enabled.png) +3. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit` 配置项来配置单个appld+cluster下的namespace数量上限值,默认为200 + +[//]: # ( ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png)) +![item-num-limit](../../../doc/images/namespace-num-limit.png) + +4. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit.white` 配置项来配置namespace数量上限校验的白名单,多个AppId使用英文逗号分隔 + +[//]: # ( ![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png)) +![item-num-limit](../../../doc/images/namespace-num-limit-white.png) + + + # 七、最佳实践 ## 7.1 安全相关 @@ -512,4 +534,4 @@ Apollo 支持细粒度的权限控制,请务必根据实际情况做好权限 1. `apollo-configservice`和`apollo-adminservice`是基于内网可信网络设计的,所以出于安全考虑,禁止`apollo-configservice`和`apollo-adminservice`直接暴露在公网 2. 对敏感配置可以考虑开启[访问秘钥](#_62-%e9%85%8d%e7%bd%ae%e8%ae%bf%e9%97%ae%e5%af%86%e9%92%a5),从而只有经过身份验证的客户端才能访问敏感配置 3. 1.7.1及以上版本可以考虑为`apollo-adminservice`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_326-admin-serviceaccesscontrolenabled-配置apollo-adminservice是否开启访问控制),从而只有[受控的](zh/deployment/distributed-deployment-guide?id=_3112-admin-serviceaccesstokens-设置apollo-portal访问各环境apollo-adminservice所需的access-token)`apollo-portal`才能访问对应接口,增强安全性 -4. 2.1.0及以上版本可以考虑为`eureka`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证),从而只有受控的`apollo-configservice`和`apollo-adminservice`可以注册到`eureka`,增强安全性 \ No newline at end of file +4. 2.1.0及以上版本可以考虑为`eureka`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证),从而只有受控的`apollo-configservice`和`apollo-adminservice`可以注册到`eureka`,增强安全性