Skip to content

Commit 94c28af

Browse files
youngzilnobodyiam
andauthored
feat: add determine appid+cluster namespace num limit logic (#5228)
* feat: add determine appid+cluster namespace num limit logic * fix: 1. Added switch control function on and off 2. Add unit tests and usage documentation 3. Update the CHANGES.md 4. Function and code style optimization * fix:Optimize CHANGES.md、unit tests、usage documentation * fix:Optimize unit tests * Update CHANGES.md --------- Co-authored-by: Jason Song <[email protected]>
1 parent a1c7c96 commit 94c28af

File tree

10 files changed

+173
-2
lines changed

10 files changed

+173
-2
lines changed

Diff for: CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Apollo 2.4.0
1010
* [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182)
1111
* [Fix: Resolve issues with duplicate comments and blank lines in configuration management](https://github.com/apolloconfig/apollo/pull/5232)
1212
* [Fix link namespace published items show missing some items](https://github.com/apolloconfig/apollo/pull/5240)
13+
* [Feature: Add limit and whitelist for namespace count per appid+cluster](https://github.com/apolloconfig/apollo/pull/5228)
1314

1415
------------------
1516
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)

Diff for: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java

+18
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import com.ctrip.framework.apollo.common.config.RefreshablePropertySource;
2222
import com.google.common.base.Strings;
2323
import com.google.common.collect.Maps;
24+
import com.google.common.collect.Sets;
2425
import com.google.gson.Gson;
2526
import com.google.gson.reflect.TypeToken;
2627
import java.lang.reflect.Type;
2728
import java.util.Collections;
2829
import java.util.List;
2930
import java.util.Map;
31+
import java.util.Set;
3032
import java.util.concurrent.TimeUnit;
3133
import java.util.stream.Collectors;
3234
import org.springframework.stereotype.Component;
@@ -36,6 +38,9 @@ public class BizConfig extends RefreshableConfig {
3638

3739
private static final int DEFAULT_ITEM_KEY_LENGTH = 128;
3840
private static final int DEFAULT_ITEM_VALUE_LENGTH = 20000;
41+
42+
private static final int DEFAULT_MAX_NAMESPACE_NUM = 200;
43+
3944
private static final int DEFAULT_APPNAMESPACE_CACHE_REBUILD_INTERVAL = 60; //60s
4045
private static final int DEFAULT_GRAY_RELEASE_RULE_SCAN_INTERVAL = 60; //60s
4146
private static final int DEFAULT_APPNAMESPACE_CACHE_SCAN_INTERVAL = 1; //1s
@@ -99,6 +104,19 @@ public int itemValueLengthLimit() {
99104
return checkInt(limit, 5, Integer.MAX_VALUE, DEFAULT_ITEM_VALUE_LENGTH);
100105
}
101106

107+
public boolean isNamespaceNumLimitEnabled() {
108+
return getBooleanProperty("namespace.num.limit.enabled", false);
109+
}
110+
111+
public int namespaceNumLimit() {
112+
int limit = getIntProperty("namespace.num.limit", DEFAULT_MAX_NAMESPACE_NUM);
113+
return checkInt(limit, 0, Integer.MAX_VALUE, DEFAULT_MAX_NAMESPACE_NUM);
114+
}
115+
116+
public Set<String> namespaceNumLimitWhite() {
117+
return Sets.newHashSet(getArrayProperty("namespace.num.limit.white", new String[0]));
118+
}
119+
102120
public Map<Long, Integer> namespaceValueLengthLimitOverride() {
103121
String namespaceValueLengthOverrideString = getValue("namespace.value.length.limit.override");
104122
Map<Long, Integer> namespaceValueLengthOverride = Maps.newHashMap();

Diff for: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/NamespaceRepository.java

+2
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ public interface NamespaceRepository extends PagingAndSortingRepository<Namespac
4444

4545
int countByNamespaceNameAndAppIdNot(String namespaceName, String appId);
4646

47+
int countByAppIdAndClusterName(String appId, String clusterName);
48+
4749
}

Diff for: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/NamespaceService.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package com.ctrip.framework.apollo.biz.service;
1818

19+
import com.ctrip.framework.apollo.biz.config.BizConfig;
1920
import com.ctrip.framework.apollo.biz.entity.Audit;
2021
import com.ctrip.framework.apollo.biz.entity.Cluster;
2122
import com.ctrip.framework.apollo.biz.entity.Item;
@@ -68,6 +69,7 @@ public class NamespaceService {
6869
private final NamespaceLockService namespaceLockService;
6970
private final InstanceService instanceService;
7071
private final MessageSender messageSender;
72+
private final BizConfig bizConfig;
7173

7274
public NamespaceService(
7375
final ReleaseHistoryService releaseHistoryService,
@@ -81,7 +83,8 @@ public NamespaceService(
8183
final @Lazy ClusterService clusterService,
8284
final @Lazy NamespaceBranchService namespaceBranchService,
8385
final NamespaceLockService namespaceLockService,
84-
final InstanceService instanceService) {
86+
final InstanceService instanceService,
87+
final BizConfig bizConfig) {
8588
this.releaseHistoryService = releaseHistoryService;
8689
this.namespaceRepository = namespaceRepository;
8790
this.auditService = auditService;
@@ -94,6 +97,7 @@ public NamespaceService(
9497
this.namespaceBranchService = namespaceBranchService;
9598
this.namespaceLockService = namespaceLockService;
9699
this.instanceService = instanceService;
100+
this.bizConfig = bizConfig;
97101
}
98102

99103

@@ -349,6 +353,14 @@ public Namespace save(Namespace entity) {
349353
if (!isNamespaceUnique(entity.getAppId(), entity.getClusterName(), entity.getNamespaceName())) {
350354
throw new ServiceException("namespace not unique");
351355
}
356+
357+
if (bizConfig.isNamespaceNumLimitEnabled() && !bizConfig.namespaceNumLimitWhite().contains(entity.getAppId())) {
358+
int nowCount = namespaceRepository.countByAppIdAndClusterName(entity.getAppId(), entity.getClusterName());
359+
if (nowCount >= bizConfig.namespaceNumLimit()) {
360+
throw new ServiceException("namespace[appId = " + entity.getAppId() + ", cluster= " + entity.getClusterName() + "] nowCount= " + nowCount + ", maxCount =" + bizConfig.namespaceNumLimit());
361+
}
362+
}
363+
352364
entity.setId(0);//protection
353365
Namespace namespace = namespaceRepository.save(entity);
354366

Diff for: apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/NamespaceServiceIntegrationTest.java

+95
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.ctrip.framework.apollo.biz.service;
1818

1919
import com.ctrip.framework.apollo.biz.AbstractIntegrationTest;
20+
import com.ctrip.framework.apollo.biz.config.BizConfig;
2021
import com.ctrip.framework.apollo.biz.entity.Cluster;
2122
import com.ctrip.framework.apollo.biz.entity.Commit;
2223
import com.ctrip.framework.apollo.biz.entity.InstanceConfig;
@@ -25,23 +26,31 @@
2526
import com.ctrip.framework.apollo.biz.entity.Release;
2627
import com.ctrip.framework.apollo.biz.entity.ReleaseHistory;
2728
import com.ctrip.framework.apollo.biz.repository.InstanceConfigRepository;
29+
import com.ctrip.framework.apollo.biz.repository.NamespaceRepository;
2830
import com.ctrip.framework.apollo.common.entity.AppNamespace;
2931

32+
import com.ctrip.framework.apollo.common.exception.ServiceException;
3033
import java.text.ParseException;
3134
import java.text.SimpleDateFormat;
35+
import java.util.Arrays;
3236
import java.util.Date;
37+
import java.util.HashSet;
38+
import org.junit.Assert;
3339
import org.junit.Test;
3440
import org.springframework.beans.factory.annotation.Autowired;
41+
import org.springframework.boot.test.mock.mockito.MockBean;
3542
import org.springframework.data.domain.Page;
3643
import org.springframework.data.domain.PageRequest;
3744
import org.springframework.test.context.jdbc.Sql;
3845

3946
import java.util.List;
47+
import org.springframework.test.util.ReflectionTestUtils;
4048

4149
import static org.junit.Assert.assertEquals;
4250
import static org.junit.Assert.assertNotNull;
4351
import static org.junit.Assert.assertNull;
4452
import static org.junit.Assert.assertTrue;
53+
import static org.mockito.Mockito.when;
4554

4655
public class NamespaceServiceIntegrationTest extends AbstractIntegrationTest {
4756

@@ -62,6 +71,11 @@ public class NamespaceServiceIntegrationTest extends AbstractIntegrationTest {
6271
private ReleaseHistoryService releaseHistoryService;
6372
@Autowired
6473
private InstanceConfigRepository instanceConfigRepository;
74+
@Autowired
75+
private NamespaceRepository namespaceRepository;
76+
77+
@MockBean
78+
private BizConfig bizConfig;
6579

6680
private String testApp = "testApp";
6781
private String testCluster = "default";
@@ -134,4 +148,85 @@ public void testGetCommitsByModifiedTime() throws ParseException {
134148
}
135149

136150

151+
@Test
152+
@Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
153+
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
154+
public void testNamespaceNumLimit() {
155+
156+
when(bizConfig.isNamespaceNumLimitEnabled()).thenReturn(true);
157+
when(bizConfig.namespaceNumLimit()).thenReturn(2);
158+
159+
Namespace namespace = new Namespace();
160+
namespace.setAppId(testApp);
161+
namespace.setClusterName(testCluster);
162+
namespace.setNamespaceName("demo-namespace");
163+
namespaceService.save(namespace);
164+
165+
try {
166+
Namespace namespace2 = new Namespace();
167+
namespace2.setAppId(testApp);
168+
namespace2.setClusterName(testCluster);
169+
namespace2.setNamespaceName("demo-namespace2");
170+
namespaceService.save(namespace2);
171+
172+
Assert.fail();
173+
} catch (Exception e) {
174+
Assert.assertTrue(e instanceof ServiceException);
175+
}
176+
177+
int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster);
178+
Assert.assertEquals(2, nowCount);
179+
180+
}
181+
182+
@Test
183+
@Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
184+
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
185+
public void testNamespaceNumLimitFalse() {
186+
187+
when(bizConfig.namespaceNumLimit()).thenReturn(2);
188+
189+
Namespace namespace = new Namespace();
190+
namespace.setAppId(testApp);
191+
namespace.setClusterName(testCluster);
192+
namespace.setNamespaceName("demo-namespace");
193+
namespaceService.save(namespace);
194+
195+
Namespace namespace2 = new Namespace();
196+
namespace2.setAppId(testApp);
197+
namespace2.setClusterName(testCluster);
198+
namespace2.setNamespaceName("demo-namespace2");
199+
namespaceService.save(namespace2);
200+
201+
int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster);
202+
Assert.assertEquals(3, nowCount);
203+
204+
}
205+
206+
@Test
207+
@Sql(scripts = "/sql/namespace-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
208+
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
209+
public void testNamespaceNumLimitWhite() {
210+
211+
when(bizConfig.isNamespaceNumLimitEnabled()).thenReturn(true);
212+
when(bizConfig.namespaceNumLimit()).thenReturn(2);
213+
when(bizConfig.namespaceNumLimitWhite()).thenReturn(new HashSet<>(Arrays.asList(testApp)));
214+
215+
Namespace namespace = new Namespace();
216+
namespace.setAppId(testApp);
217+
namespace.setClusterName(testCluster);
218+
namespace.setNamespaceName("demo-namespace");
219+
namespaceService.save(namespace);
220+
221+
Namespace namespace2 = new Namespace();
222+
namespace2.setAppId(testApp);
223+
namespace2.setClusterName(testCluster);
224+
namespace2.setNamespaceName("demo-namespace2");
225+
namespaceService.save(namespace2);
226+
227+
int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster);
228+
Assert.assertEquals(3, nowCount);
229+
230+
}
231+
137232
}

Diff for: doc/images/namespace-num-limit-enabled.png

31.9 KB
Loading

Diff for: doc/images/namespace-num-limit-white.png

34.9 KB
Loading

Diff for: doc/images/namespace-num-limit.png

31.3 KB
Loading

Diff for: docs/en/portal/apollo-user-guide.md

+23
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,29 @@ Please note that modifications to system parameters may affect the performance o
509509

510510
![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png)
511511

512+
513+
514+
## 6.4 Parameter settings for limiting the number of namespaces in the appld+cluster dimension
515+
516+
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.
517+
518+
**Setting method:**
519+
520+
1. Log in to the Apollo Configuration Center interface with a super administrator account.
521+
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.
522+
523+
![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png)
524+
525+
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
526+
527+
![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png)
528+
529+
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.
530+
531+
![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png)
532+
533+
534+
512535
# VII. Best practices
513536

514537
## 7.1 Security Related

Diff for: docs/zh/portal/apollo-user-guide.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,26 @@ Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验
482482

483483
![System-parameterization-of-global-search-configuration-items](../images/System-parameterization-of-global-search-configuration-items.png)
484484

485+
486+
## 6.4 appld+cluster维度下命名空间数量限制功能参数设置
487+
从2.4.0版本开始,apollo-portal提供了appld+cluster维度下可以创建的命名空间数量上限校验的功能,此功能默认关闭,需要配置系统 `namespace.num.limit.enabled` 开启,同时提供了系统参数`namespace.num.limit`来动态配置appld+cluster维度下的Namespace数量上限值,默认为200个,考虑到一些基础组件如网关、消息队列、Redis、数据库等需要特殊处理,新增了系统参数`namespace.num.limit.white` 来配置校验白名单,不受Namespace数量上限的影响
488+
489+
**设置方法:**
490+
1. 用超级管理员账号登录到Apollo配置中心的界面
491+
2. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit.enabled` 配置项为true/false 即可开启/关闭此功能,默认关闭
492+
493+
![item-num-limit-enabled](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png)
494+
495+
3. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit` 配置项来配置单个appld+cluster下的namespace数量上限值,默认为200
496+
497+
![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png)
498+
499+
4. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit.white` 配置项来配置namespace数量上限校验的白名单,多个AppId使用英文逗号分隔
500+
501+
![item-num-limit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png)
502+
503+
504+
485505
# 七、最佳实践
486506

487507
## 7.1 安全相关
@@ -512,4 +532,4 @@ Apollo 支持细粒度的权限控制,请务必根据实际情况做好权限
512532
1. `apollo-configservice``apollo-adminservice`是基于内网可信网络设计的,所以出于安全考虑,禁止`apollo-configservice``apollo-adminservice`直接暴露在公网
513533
2. 对敏感配置可以考虑开启[访问秘钥](#_62-%e9%85%8d%e7%bd%ae%e8%ae%bf%e9%97%ae%e5%af%86%e9%92%a5),从而只有经过身份验证的客户端才能访问敏感配置
514534
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`才能访问对应接口,增强安全性
515-
4. 2.1.0及以上版本可以考虑为`eureka`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证),从而只有受控的`apollo-configservice``apollo-adminservice`可以注册到`eureka`,增强安全性
535+
4. 2.1.0及以上版本可以考虑为`eureka`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证),从而只有受控的`apollo-configservice``apollo-adminservice`可以注册到`eureka`,增强安全性

0 commit comments

Comments
 (0)