Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add determine appid+cluster namespace num limit logic #5228

Merged
merged 8 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
youngzil marked this conversation as resolved.
Show resolved Hide resolved
*
nobodyiam marked this conversation as resolved.
Show resolved Hide resolved
------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<String> namespaceNumLimitWhite() {
return Sets.newHashSet(getArrayProperty("namespace.num.limit.white", new String[0]));
}

public Map<Long, Integer> namespaceValueLengthLimitOverride() {
String namespaceValueLengthOverrideString = getValue("namespace.value.length.limit.override");
Map<Long, Integer> namespaceValueLengthOverride = Maps.newHashMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ public interface NamespaceRepository extends PagingAndSortingRepository<Namespac

int countByNamespaceNameAndAppIdNot(String namespaceName, String appId);

int countByAppIdAndClusterName(String appId, String clusterName);

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package com.ctrip.framework.apollo.biz.service;

import com.ctrip.framework.apollo.biz.config.BizConfig;
import com.ctrip.framework.apollo.biz.entity.Audit;
import com.ctrip.framework.apollo.biz.entity.Cluster;
import com.ctrip.framework.apollo.biz.entity.Item;
Expand Down Expand Up @@ -68,6 +69,7 @@ public class NamespaceService {
private final NamespaceLockService namespaceLockService;
private final InstanceService instanceService;
private final MessageSender messageSender;
private final BizConfig bizConfig;

public NamespaceService(
final ReleaseHistoryService releaseHistoryService,
Expand All @@ -81,7 +83,8 @@ public NamespaceService(
final @Lazy ClusterService clusterService,
final @Lazy NamespaceBranchService namespaceBranchService,
final NamespaceLockService namespaceLockService,
final InstanceService instanceService) {
final InstanceService instanceService,
final BizConfig bizConfig) {
this.releaseHistoryService = releaseHistoryService;
this.namespaceRepository = namespaceRepository;
this.auditService = auditService;
Expand All @@ -94,6 +97,7 @@ public NamespaceService(
this.namespaceBranchService = namespaceBranchService;
this.namespaceLockService = namespaceLockService;
this.instanceService = instanceService;
this.bizConfig = bizConfig;
}


Expand Down Expand Up @@ -349,6 +353,14 @@ public Namespace save(Namespace entity) {
if (!isNamespaceUnique(entity.getAppId(), entity.getClusterName(), entity.getNamespaceName())) {
throw new ServiceException("namespace not unique");
}

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());
}
}

youngzil marked this conversation as resolved.
Show resolved Hide resolved
entity.setId(0);//protection
Namespace namespace = namespaceRepository.save(entity);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -62,6 +71,11 @@ public class NamespaceServiceIntegrationTest extends AbstractIntegrationTest {
private ReleaseHistoryService releaseHistoryService;
@Autowired
private InstanceConfigRepository instanceConfigRepository;
@Autowired
private NamespaceRepository namespaceRepository;

@Mock
private BizConfig bizConfig;
youngzil marked this conversation as resolved.
Show resolved Hide resolved

private String testApp = "testApp";
private String testCluster = "default";
Expand Down Expand Up @@ -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);
youngzil marked this conversation as resolved.
Show resolved Hide resolved


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);
}
youngzil marked this conversation as resolved.
Show resolved Hide resolved

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);
}
youngzil marked this conversation as resolved.
Show resolved Hide resolved

int nowCount = namespaceRepository.countByAppIdAndClusterName(testApp, testCluster);
Assert.assertEquals(3, nowCount);

}

}
Binary file added doc/images/namespace-num-limit-enabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/namespace-num-limit-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/namespace-num-limit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions docs/en/portal/apollo-user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 单个命名空间下的配置项数量限制
youngzil marked this conversation as resolved.
Show resolved Hide resolved

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]&#40;https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png&#41;)
youngzil marked this conversation as resolved.
Show resolved Hide resolved
![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]&#40;https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png&#41;)
![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]&#40;https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png&#41;)
![item-num-limit](../../../doc/images/namespace-num-limit-white.png)




# VII. Best practices

## 7.1 Security Related
Expand Down
24 changes: 23 additions & 1 deletion docs/zh/portal/apollo-user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]&#40;https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-enabled.png&#41;)
![item-num-limit-enabled](../../../doc/images/namespace-num-limit-enabled.png)
3. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit` 配置项来配置单个appld+cluster下的namespace数量上限值,默认为200

[//]: # ( ![item-num-limit]&#40;https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit.png&#41;)
![item-num-limit](../../../doc/images/namespace-num-limit.png)

4. 进入 `管理员工具 - 系统参数 - ConfigDB 配置管理` 页面新增或修改 `namespace.num.limit.white` 配置项来配置namespace数量上限校验的白名单,多个AppId使用英文逗号分隔

[//]: # ( ![item-num-limit]&#40;https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-num-limit-white.png&#41;)
![item-num-limit](../../../doc/images/namespace-num-limit-white.png)



# 七、最佳实践

## 7.1 安全相关
Expand Down Expand Up @@ -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`,增强安全性
4. 2.1.0及以上版本可以考虑为`eureka`开启[访问控制](zh/deployment/distributed-deployment-guide?id=_329-apolloeurekaserversecurityenabled-配置是否开启eureka-server的登录认证),从而只有受控的`apollo-configservice`和`apollo-adminservice`可以注册到`eureka`,增强安全性
Loading