diff --git a/CHANGES.md b/CHANGES.md index 2d9cd3d8735..11cd73eaff2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ Apollo 2.4.0 * [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182) * [Fix: Resolve issues with duplicate comments and blank lines in configuration management](https://github.com/apolloconfig/apollo/pull/5232) * [Fix link namespace published items show missing some items](https://github.com/apolloconfig/apollo/pull/5240) +* [Feature: Add limit and whitelist for namespace count per appid+cluster](https://github.com/apolloconfig/apollo/pull/5228) +* ------------------ 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 2652995f582..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 @@ -21,12 +21,14 @@ import com.ctrip.framework.apollo.common.config.RefreshablePropertySource; import com.google.common.base.Strings; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.springframework.stereotype.Component; @@ -36,6 +38,9 @@ public class BizConfig extends RefreshableConfig { private static final int DEFAULT_ITEM_KEY_LENGTH = 128; private static final int DEFAULT_ITEM_VALUE_LENGTH = 20000; + + private static final int DEFAULT_MAX_NAMESPACE_NUM = 200; + private static final int DEFAULT_APPNAMESPACE_CACHE_REBUILD_INTERVAL = 60; //60s private static final int DEFAULT_GRAY_RELEASE_RULE_SCAN_INTERVAL = 60; //60s private static final int DEFAULT_APPNAMESPACE_CACHE_SCAN_INTERVAL = 1; //1s @@ -99,6 +104,19 @@ 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", new String[0])); + } + public Map namespaceValueLengthLimitOverride() { String namespaceValueLengthOverrideString = getValue("namespace.value.length.limit.override"); Map namespaceValueLengthOverride = Maps.newHashMap(); diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/NamespaceRepository.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/NamespaceRepository.java index fc36a10524b..1c134275c8a 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/NamespaceRepository.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/NamespaceRepository.java @@ -44,4 +44,6 @@ public interface NamespaceRepository extends PagingAndSortingRepository= bizConfig.namespaceNumLimit()) { + throw new ServiceException("namespace[appId = " + entity.getAppId() + ", cluster= " + entity.getClusterName() + "] nowCount= " + nowCount + ", maxCount =" + bizConfig.namespaceNumLimit()); + } + } + entity.setId(0);//protection Namespace namespace = namespaceRepository.save(entity); 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..0f2307e371b 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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; 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; + + @MockBean + private BizConfig bizConfig; private String testApp = "testApp"; private String testCluster = "default"; @@ -134,4 +148,85 @@ 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() { + + 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() { + + when(bizConfig.namespaceNumLimit()).thenReturn(2); + + Namespace namespace = new Namespace(); + namespace.setAppId(testApp); + namespace.setClusterName(testCluster); + namespace.setNamespaceName("demo-namespace"); + namespaceService.save(namespace); + + Namespace namespace2 = new Namespace(); + namespace2.setAppId(testApp); + namespace2.setClusterName(testCluster); + namespace2.setNamespaceName("demo-namespace2"); + namespaceService.save(namespace2); + + 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() { + + 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); + + Namespace namespace2 = new Namespace(); + namespace2.setAppId(testApp); + namespace2.setClusterName(testCluster); + namespace2.setNamespaceName("demo-namespace2"); + namespaceService.save(namespace2); + + 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..d084ab5a0aa 100644 --- a/docs/en/portal/apollo-user-guide.md +++ b/docs/en/portal/apollo-user-guide.md @@ -509,6 +509,29 @@ 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 Parameter settings for limiting the number of namespaces in the appld+cluster dimension + +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) + +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) + +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) + + + # 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..cb38986bb2b 100644 --- a/docs/zh/portal/apollo-user-guide.md +++ b/docs/zh/portal/apollo-user-guide.md @@ -482,6 +482,26 @@ Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验 ![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png) + +## 6.4 appld+cluster维度下命名空间数量限制功能参数设置 +从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) + +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) + +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) + + + # 七、最佳实践 ## 7.1 安全相关 @@ -512,4 +532,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`,增强安全性