diff --git a/CHANGES.md b/CHANGES.md index e67b0b78c40..f151343137f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,11 @@ 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) +* [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) +* [Feature support the observe status access-key for pre-check and logging only](https://github.com/apolloconfig/apollo/pull/5236) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) diff --git a/README.md b/README.md index 2dbe99867c3..3b1a1f7d7ce 100644 --- a/README.md +++ b/README.md @@ -37,36 +37,31 @@ Demo Environment: * The same codebase could have different configurations when deployed in different clusters * With the namespace concept, it is easy to support multiple applications to share the same configurations, while also allowing them to customize the configurations * Multiple languages is provided in user interface(currently Chinese and English) - * **Configuration changes takes effect in real time (hot release)** * After the user modified the configuration and released it in Apollo, the sdk will receive the latest configurations in real time (1 second) and notify the application - * **Release version management** * Every configuration releases are versioned, which is friendly to support configuration rollback - * **Grayscale release** * Support grayscale configuration release, for example, after clicking release, it will only take effect for some application instances. After a period of observation, we could push the configurations to all application instances if there is no problem - +* **Global Search Configuration Items** + * A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used + * It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations * **Authorization management, release approval and operation audit** * Great authorization mechanism is designed for applications and configurations management, and the management of configurations is divided into two operations: editing and publishing, therefore greatly reducing human errors * All operations have audit logs for easy tracking of problems - * **Client side configuration information monitoring** * It's very easy to see which instances are using the configurations and what versions they are using - * **Rich SDKs available** * Provides native sdks of Java and .Net to facilitate application integration * Support Spring Placeholder, Annotation and Spring Boot ConfigurationProperties for easy application use (requires Spring 3.1.1+) * Http APIs are provided, so non-Java and .Net applications can integrate conveniently * Rich third party sdks are also available, e.g. Golang, Python, NodeJS, PHP, C, etc - * **Open platform API** * Apollo itself provides a unified configuration management interface, which supports features such as multi-environment, multi-data center configuration management, permissions, and process governance * However, for the sake of versatility, Apollo will not put too many restrictions on the modification of the configuration, as long as it conforms to the basic format, it can be saved. * In our research, we found that for some users, their configurations may have more complicated formats, such as xml, json, and the format needs to be verified * There are also some users such as DAL, which not only have a specific format, but also need to verify the entered value before saving, such as checking whether the database, username and password match * For this type of application, Apollo allows the application to modify and release configurations through open APIs, which has great authorization and permission control mechanism built in - * **Simple deployment** * As an infrastructure service, the configuration center has very high availability requirements, which forces Apollo to rely on external dependencies as little as possible * Currently, the only external dependency is MySQL, so the deployment is very simple. Apollo can run as long as Java and MySQL are installed diff --git a/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java b/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java index 1e95a2ee8fd..db8aee73e95 100644 --- a/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java +++ b/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java @@ -16,6 +16,8 @@ */ package com.ctrip.framework.apollo.adminservice.controller; +import static com.ctrip.framework.apollo.common.constants.AccessKeyMode.FILTER; + import com.ctrip.framework.apollo.biz.entity.AccessKey; import com.ctrip.framework.apollo.biz.service.AccessKeyService; import com.ctrip.framework.apollo.common.dto.AccessKeyDTO; @@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** @@ -61,9 +64,11 @@ public void delete(@PathVariable String appId, @PathVariable long id, String ope } @PutMapping(value = "/apps/{appId}/accesskeys/{id}/enable") - public void enable(@PathVariable String appId, @PathVariable long id, String operator) { + public void enable(@PathVariable String appId, @PathVariable long id, + @RequestParam(required = false, defaultValue = "" + FILTER) int mode, String operator) { AccessKey entity = new AccessKey(); entity.setId(id); + entity.setMode(mode); entity.setEnabled(true); entity.setDataChangeLastModifiedBy(operator); @@ -74,6 +79,7 @@ public void enable(@PathVariable String appId, @PathVariable long id, String ope public void disable(@PathVariable String appId, @PathVariable long id, String operator) { AccessKey entity = new AccessKey(); entity.setId(id); + entity.setMode(FILTER); entity.setEnabled(false); entity.setDataChangeLastModifiedBy(operator); diff --git a/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java b/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java index 2e7e25d5912..f628748d2fa 100644 --- a/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java +++ b/apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java @@ -27,6 +27,7 @@ import com.ctrip.framework.apollo.biz.service.ReleaseService; import com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder; import com.ctrip.framework.apollo.common.dto.ItemDTO; +import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; @@ -201,6 +202,14 @@ public List findDeletedItems(@PathVariable("appId") String appId, return Collections.emptyList(); } + @GetMapping("/items-search/key-and-value") + public PageDTO getItemInfoBySearch(@RequestParam(value = "key", required = false) String key, + @RequestParam(value = "value", required = false) String value, + Pageable limit) { + Page pageItemInfoDTO = itemService.getItemInfoBySearch(key, value, limit); + return new PageDTO<>(pageItemInfoDTO.getContent(), limit, pageItemInfoDTO.getTotalElements()); + } + @GetMapping("/items/{itemId}") public ItemDTO get(@PathVariable("itemId") long itemId) { Item item = itemService.findOne(itemId); diff --git a/apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemControllerTest.java b/apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemControllerTest.java index f406d01e0f9..84e791f6c1d 100644 --- a/apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemControllerTest.java +++ b/apollo-adminservice/src/test/java/com/ctrip/framework/apollo/adminservice/controller/ItemControllerTest.java @@ -21,18 +21,19 @@ import com.ctrip.framework.apollo.biz.entity.Commit; import com.ctrip.framework.apollo.biz.repository.CommitRepository; import com.ctrip.framework.apollo.biz.repository.ItemRepository; -import com.ctrip.framework.apollo.common.dto.AppDTO; -import com.ctrip.framework.apollo.common.dto.ClusterDTO; -import com.ctrip.framework.apollo.common.dto.ItemDTO; -import com.ctrip.framework.apollo.common.dto.NamespaceDTO; +import com.ctrip.framework.apollo.biz.service.ItemService; +import com.ctrip.framework.apollo.common.dto.*; + import java.util.List; import java.util.Objects; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; @@ -48,6 +49,9 @@ public class ItemControllerTest extends AbstractControllerTest { @Autowired private ItemRepository itemRepository; + @Autowired + private ItemService itemService; + @Test @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) @@ -58,7 +62,7 @@ public void testCreate() { ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); assert cluster != null; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), - NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); + NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); String itemKey = "test-key"; String itemValue = "test-value"; @@ -68,12 +72,12 @@ public void testCreate() { item.setDataChangeLastModifiedBy("apollo"); ResponseEntity response = restTemplate.postForEntity(itemBaseUrl(), - item, ItemDTO.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); + item, ItemDTO.class, app.getAppId(), cluster.getName(), namespace.getNamespaceName()); Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); Assert.assertEquals(itemKey, Objects.requireNonNull(response.getBody()).getKey()); List commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(), - Pageable.ofSize(10)); + Pageable.ofSize(10)); Assert.assertEquals(1, commitList.size()); Commit commit = commitList.get(0); @@ -93,15 +97,15 @@ public void testUpdate() { ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); assert cluster != null; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), - NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); + NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); String itemKey = "test-key"; String itemValue = "test-value-updated"; long itemId = itemRepository.findByKey(itemKey, Pageable.ofSize(1)) - .getContent() - .get(0) - .getId(); + .getContent() + .get(0) + .getId(); ItemDTO item = new ItemDTO(itemKey, itemValue, "", 1); item.setDataChangeLastModifiedBy("apollo"); @@ -115,7 +119,7 @@ public void testUpdate() { }); List commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(), - Pageable.ofSize(10)); + Pageable.ofSize(10)); assertThat(commitList).hasSize(2); } @@ -131,23 +135,44 @@ public void testDelete() { ClusterDTO cluster = restTemplate.getForObject(clusterBaseUrl(), ClusterDTO.class, app.getAppId(), "default"); assert cluster != null; NamespaceDTO namespace = restTemplate.getForObject(namespaceBaseUrl(), - NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); + NamespaceDTO.class, app.getAppId(), cluster.getName(), "application"); String itemKey = "test-key"; long itemId = itemRepository.findByKey(itemKey, Pageable.ofSize(1)) - .getContent() - .get(0) - .getId(); + .getContent() + .get(0) + .getId(); String deleteUrl = url( "/items/{itemId}?operator=apollo"); restTemplate.delete(deleteUrl, itemId); assertThat(itemRepository.findById(itemId).isPresent()) - .isFalse(); + .isFalse(); assert namespace != null; List commitList = commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(app.getAppId(), cluster.getName(), namespace.getNamespaceName(), - Pageable.ofSize(10)); + Pageable.ofSize(10)); assertThat(commitList).hasSize(2); } + + @Test + @Sql(scripts = "/controller/test-itemset.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/controller/cleanup.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) + public void testSearch() { + this.testCreate(); + + String itemKey = "test-key"; + String itemValue = "test-value"; + Page itemInfoDTOS = itemService.getItemInfoBySearch(itemKey, itemValue, PageRequest.of(0, 200)); + HttpHeaders headers = new HttpHeaders(); + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity> response = restTemplate.exchange( + url("/items-search/key-and-value?key={key}&value={value}&page={page}&size={size}"), + HttpMethod.GET, + entity, + new ParameterizedTypeReference>() {}, + itemKey, itemValue, 0, 200 + ); + assertThat(itemInfoDTOS.getContent().toString()).isEqualTo(response.getBody().getContent().toString()); + } } 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/entity/AccessKey.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/AccessKey.java index 3e927bbf914..a66ade65c15 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/AccessKey.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/AccessKey.java @@ -36,6 +36,9 @@ public class AccessKey extends BaseEntity { @Column(name = "`Secret`", nullable = false) private String secret; + @Column(name = "`Mode`") + private int mode; + @Column(name = "`IsEnabled`", columnDefinition = "Bit default '0'") private boolean enabled; @@ -55,6 +58,14 @@ public void setSecret(String secret) { this.secret = secret; } + public int getMode() { + return mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + public boolean isEnabled() { return enabled; } @@ -66,6 +77,6 @@ public void setEnabled(boolean enabled) { @Override public String toString() { return toStringHelper().add("appId", appId).add("secret", secret) - .add("enabled", enabled).toString(); + .add("mode", mode).add("enabled", enabled).toString(); } } diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ItemRepository.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ItemRepository.java index d482ce63e97..c866b902814 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ItemRepository.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ItemRepository.java @@ -18,11 +18,13 @@ import com.ctrip.framework.apollo.biz.entity.Item; +import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; import java.util.Date; import java.util.List; @@ -43,6 +45,21 @@ public interface ItemRepository extends PagingAndSortingRepository { Item findFirst1ByNamespaceIdOrderByLineNumDesc(Long namespaceId); + @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " + + "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " + + "WHERE i.key LIKE %:key% AND i.value LIKE %:value% AND i.isDeleted = 0") + Page findItemsByKeyAndValueLike(@Param("key") String key, @Param("value") String value, Pageable pageable); + + @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " + + "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " + + "WHERE i.key LIKE %:key% AND i.isDeleted = 0") + Page findItemsByKeyLike(@Param("key") String key, Pageable pageable); + + @Query("SELECT new com.ctrip.framework.apollo.common.dto.ItemInfoDTO(n.appId, n.clusterName, n.namespaceName, i.key, i.value) " + + "FROM Item i RIGHT JOIN Namespace n ON i.namespaceId = n.id " + + "WHERE i.value LIKE %:value% AND i.isDeleted = 0") + Page findItemsByValueLike(@Param("value") String value, Pageable pageable); + @Modifying @Query("update Item set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000), DataChange_LastModifiedBy = ?2 where NamespaceId = ?1 and IsDeleted = false") int deleteByNamespaceId(long namespaceId, String operator); 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 findItemsByNamespace(String appId, String clusterName, String return itemRepository.findByNamespaceId(namespace.getId(), pageable); } + public Page getItemInfoBySearch(String key, String value, Pageable limit) { + Page itemInfoDTOs; + if (key.isEmpty() && !value.isEmpty()) { + itemInfoDTOs = itemRepository.findItemsByValueLike(value, limit); + } else if (value.isEmpty() && !key.isEmpty()) { + itemInfoDTOs = itemRepository.findItemsByKeyLike(key, limit); + } else { + itemInfoDTOs = itemRepository.findItemsByKeyAndValueLike(key, value, limit); + } + return itemInfoDTOs; + } + @Transactional public Item save(Item entity) { checkItemKeyLength(entity.getKey()); 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 f95bd2e0a1e..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 @@ -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; @@ -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, @@ -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; @@ -94,6 +97,7 @@ public NamespaceService( this.namespaceBranchService = namespaceBranchService; this.namespaceLockService = namespaceLockService; this.instanceService = instanceService; + this.bizConfig = bizConfig; } @@ -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()); + } + } + entity.setId(0);//protection Namespace namespace = namespaceRepository.save(entity); diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemServiceTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemServiceTest.java index 3b0ed5f6a4b..6d8003f1d58 100644 --- a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemServiceTest.java +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/ItemServiceTest.java @@ -18,10 +18,13 @@ import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; import com.ctrip.framework.apollo.biz.entity.Item; +import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; import com.ctrip.framework.apollo.common.exception.BadRequestException; import org.junit.Assert; import org.junit.Test; 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; public class ItemServiceTest extends AbstractIntegrationTest { @@ -71,4 +74,26 @@ public void testUpdateItem() { Assert.assertEquals("v1-new", dbItem.getValue()); } + @Test + @Sql(scripts = {"/sql/namespace-test.sql","/sql/item-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + public void testSearchItem() { + ItemInfoDTO itemInfoDTO = new ItemInfoDTO(); + itemInfoDTO.setAppId("testApp"); + itemInfoDTO.setClusterName("default"); + itemInfoDTO.setNamespaceName("application"); + itemInfoDTO.setKey("k1"); + itemInfoDTO.setValue("v1"); + + String itemKey = "k1"; + String itemValue = "v1"; + Page ExpectedItemInfoDTOSByKeyAndValue = itemService.getItemInfoBySearch(itemKey, itemValue, PageRequest.of(0,200)); + Page ExpectedItemInfoDTOSByKey = itemService.getItemInfoBySearch(itemKey,"", PageRequest.of(0,200)); + Page ExpectedItemInfoDTOSByValue = itemService.getItemInfoBySearch("", itemValue, PageRequest.of(0,200)); + Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByKeyAndValue.getContent().get(0).toString()); + Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByKey.getContent().get(0).toString()); + Assert.assertEquals(itemInfoDTO.toString(), ExpectedItemInfoDTOSByValue.getContent().get(0).toString()); + + } + } 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/apollo-biz/src/test/resources/sql/accesskey-test.sql b/apollo-biz/src/test/resources/sql/accesskey-test.sql index 2ea1c96e9f2..36ffb2a1b6b 100644 --- a/apollo-biz/src/test/resources/sql/accesskey-test.sql +++ b/apollo-biz/src/test/resources/sql/accesskey-test.sql @@ -13,9 +13,9 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- -INSERT INTO "AccessKey" (`Id`, `AppId`, `Secret`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) +INSERT INTO "AccessKey" (`Id`, `AppId`, `Secret`, `Mode`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`) VALUES - (1, 'someAppId', 'someSecret', 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'), - (2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'), - (3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'), - (4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21'); + (1, 'someAppId', 'someSecret', 0, 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'), + (2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'), + (3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'), + (4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21'); diff --git a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/AccessKeyMode.java b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/AccessKeyMode.java new file mode 100644 index 00000000000..24844c407fa --- /dev/null +++ b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/AccessKeyMode.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.common.constants; + +public interface AccessKeyMode { + + int FILTER = 0; + + int OBSERVER = 1; + +} diff --git a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java index 4ba5a32d5ab..ecd7eb37511 100644 --- a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java +++ b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java @@ -24,6 +24,8 @@ public class AccessKeyDTO extends BaseDTO { private String appId; + private Integer mode; + private Boolean enabled; public Long getId() { @@ -50,6 +52,14 @@ public void setAppId(String appId) { this.appId = appId; } + public Integer getMode() { + return mode; + } + + public void setMode(Integer mode) { + this.mode = mode; + } + public Boolean getEnabled() { return enabled; } diff --git a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTO.java b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTO.java new file mode 100644 index 00000000000..a28794ed656 --- /dev/null +++ b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTO.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.common.dto; + + +public class ItemInfoDTO extends BaseDTO{ + private String appId; + private String clusterName; + private String namespaceName; + private String key; + private String value; + + public ItemInfoDTO() { + } + + public ItemInfoDTO(String appId, String clusterName, String namespaceName, String key, String value) { + this.appId = appId; + this.clusterName = clusterName; + this.namespaceName = namespaceName; + this.key = key; + this.value = value; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getNamespaceName() { + return namespaceName; + } + + public void setNamespaceName(String namespaceName) { + this.namespaceName = namespaceName; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "ItemInfoDTO{" + + "appId='" + appId + '\'' + + ", clusterName='" + clusterName + '\'' + + ", namespaceName='" + namespaceName + '\'' + + ", key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/SearchResponseEntity.java b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/SearchResponseEntity.java new file mode 100644 index 00000000000..0eb6cf446da --- /dev/null +++ b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/http/SearchResponseEntity.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.common.http; + +import org.springframework.http.HttpStatus; + +public class SearchResponseEntity { + + private T body; + private boolean hasMoreData; + private Object message; + private int code; + + public static SearchResponseEntity ok(T body){ + SearchResponseEntity SearchResponseEntity = new SearchResponseEntity<>(); + SearchResponseEntity.message = HttpStatus.OK.getReasonPhrase(); + SearchResponseEntity.code = HttpStatus.OK.value(); + SearchResponseEntity.body = body; + SearchResponseEntity.hasMoreData = false; + return SearchResponseEntity; + } + + public static SearchResponseEntity okWithMessage(T body, Object message){ + SearchResponseEntity SearchResponseEntity = new SearchResponseEntity<>(); + SearchResponseEntity.message = message; + SearchResponseEntity.code = HttpStatus.OK.value(); + SearchResponseEntity.body = body; + SearchResponseEntity.hasMoreData = true; + return SearchResponseEntity; + } + + public static SearchResponseEntity error(HttpStatus httpCode, Object message){ + SearchResponseEntity SearchResponseEntity = new SearchResponseEntity<>(); + SearchResponseEntity.message = message; + SearchResponseEntity.code = httpCode.value(); + return SearchResponseEntity; + } + + public int getCode() { + return code; + } + + public Object getMessage() { + return message; + } + + public T getBody() { + return body; + } + + public boolean isHasMoreData() {return hasMoreData;} + +} \ No newline at end of file diff --git a/apollo-common/src/test/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTOTest.java b/apollo-common/src/test/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTOTest.java new file mode 100644 index 00000000000..5a138b691cf --- /dev/null +++ b/apollo-common/src/test/java/com/ctrip/framework/apollo/common/dto/ItemInfoDTOTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + */ +package com.ctrip.framework.apollo.common.dto; + + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ItemInfoDTOTest { + + private ItemInfoDTO itemInfoDTO; + + @Before + public void setUp() { + itemInfoDTO = new ItemInfoDTO("testAppId", "testClusterName", "testNamespaceName", "testKey", "testValue"); + } + + @Test + public void testGetAppId_ShouldReturnCorrectAppId() { + assertEquals("testAppId", itemInfoDTO.getAppId()); + } + + @Test + public void testGetClusterName_ShouldReturnCorrectClusterName() { + assertEquals("testClusterName", itemInfoDTO.getClusterName()); + } + + @Test + public void testGetNamespaceName_ShouldReturnCorrectNamespaceName() { + assertEquals("testNamespaceName", itemInfoDTO.getNamespaceName()); + } + + @Test + public void testGetKey_ShouldReturnCorrectKey() { + assertEquals("testKey", itemInfoDTO.getKey()); + } + + @Test + public void testGetValue_ShouldReturnCorrectValue() { + assertEquals("testValue", itemInfoDTO.getValue()); + } + + @Test + public void testToString_ShouldReturnExpectedString() { + assertEquals("ItemInfoDTO{appId='testAppId', clusterName='testClusterName', namespaceName='testNamespaceName', key='testKey', value='testValue'}", itemInfoDTO.toString()); + } +} diff --git a/apollo-common/src/test/java/com/ctrip/framework/apollo/common/http/SearchResponseEntityTest.java b/apollo-common/src/test/java/com/ctrip/framework/apollo/common/http/SearchResponseEntityTest.java new file mode 100644 index 00000000000..14e250004eb --- /dev/null +++ b/apollo-common/src/test/java/com/ctrip/framework/apollo/common/http/SearchResponseEntityTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + */ +package com.ctrip.framework.apollo.common.http; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; + +import static org.junit.Assert.*; + +@RunWith(MockitoJUnitRunner.class) +public class SearchResponseEntityTest { + + @Test + public void testOk_WithValidBody_ShouldReturnOkResponse() { + String body = "test body"; + SearchResponseEntity response = SearchResponseEntity.ok(body); + + assertEquals(HttpStatus.OK.value(), response.getCode()); + assertEquals(HttpStatus.OK.getReasonPhrase(), response.getMessage()); + assertEquals(body, response.getBody()); + assertFalse(response.isHasMoreData()); + } + + @Test + public void testOkWithMessage_WithValidBodyAndMessage_ShouldReturnOkResponseWithMessage() { + String body = "test body"; + String message = "test message"; + SearchResponseEntity response = SearchResponseEntity.okWithMessage(body, message); + + assertEquals(HttpStatus.OK.value(), response.getCode()); + assertEquals(message, response.getMessage()); + assertEquals(body, response.getBody()); + assertTrue(response.isHasMoreData()); + } + + @Test + public void testError_WithValidCodeAndMessage_ShouldReturnErrorResponse() { + HttpStatus httpCode = HttpStatus.BAD_REQUEST; + String message = "error message"; + SearchResponseEntity response = SearchResponseEntity.error(httpCode, message); + + assertEquals(httpCode.value(), response.getCode()); + assertEquals(message, response.getMessage()); + assertEquals(null, response.getBody()); + assertFalse(response.isHasMoreData()); + } +} diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java index 25677b5626a..43329e135c4 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java @@ -17,9 +17,11 @@ package com.ctrip.framework.apollo.configservice.filter; import com.ctrip.framework.apollo.biz.config.BizConfig; +import com.ctrip.framework.apollo.common.utils.WebUtils; import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil; import com.ctrip.framework.apollo.core.signature.Signature; import com.ctrip.framework.apollo.core.utils.StringUtils; +import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.net.HttpHeaders; import java.io.IOException; import java.util.List; @@ -70,29 +72,60 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain List availableSecrets = accessKeyUtil.findAvailableSecret(appId); if (!CollectionUtils.isEmpty(availableSecrets)) { - String timestamp = request.getHeader(Signature.HTTP_HEADER_TIMESTAMP); - String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); - - // check timestamp, valid within 1 minute - if (!checkTimestamp(timestamp)) { - logger.warn("Invalid timestamp. appId={},timestamp={}", appId, timestamp); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed"); + if (!doCheck(request, response, appId, availableSecrets, false)) { return; } - - // check signature - String uri = request.getRequestURI(); - String query = request.getQueryString(); - if (!checkAuthorization(authorization, availableSecrets, timestamp, uri, query)) { - logger.warn("Invalid authorization. appId={},authorization={}", appId, authorization); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); - return; + } else { + // pre-check for observable secrets + List observableSecrets = accessKeyUtil.findObservableSecrets(appId); + if (!CollectionUtils.isEmpty(observableSecrets)) { + doCheck(request, response, appId, observableSecrets, true); } } chain.doFilter(request, response); } + /** + * Performs authentication checks(timestamp and signature) for the request. + * + * @param preCheck Boolean flag indicating whether this is a pre-check + * @return true if authentication checks is successful, false otherwise + */ + private boolean doCheck(HttpServletRequest req, HttpServletResponse resp, + String appId, List secrets, boolean preCheck) throws IOException { + + String timestamp = req.getHeader(Signature.HTTP_HEADER_TIMESTAMP); + String authorization = req.getHeader(HttpHeaders.AUTHORIZATION); + String ip = WebUtils.tryToGetClientIp(req); + + // check timestamp, valid within 1 minute + if (!checkTimestamp(timestamp)) { + if (preCheck) { + preCheckInvalidLogging(String.format("Invalid timestamp in pre-check. " + + "appId=%s,clientIp=%s,timestamp=%s", appId, ip, timestamp)); + } else { + logger.warn("Invalid timestamp. appId={},clientIp={},timestamp={}", appId, ip, timestamp); + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed"); + return false; + } + } + + // check signature + if (!checkAuthorization(authorization, secrets, timestamp, req.getRequestURI(), req.getQueryString())) { + if (preCheck) { + preCheckInvalidLogging(String.format("Invalid authorization in pre-check. " + + "appId=%s,clientIp=%s,authorization=%s", appId, ip, authorization)); + } else { + logger.warn("Invalid authorization. appId={},clientIp={},authorization={}", appId, ip, authorization); + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + return false; + } + } + + return true; + } + @Override public void destroy() { //nothing @@ -130,4 +163,9 @@ private boolean checkAuthorization(String authorization, List availableS } return false; } + + protected void preCheckInvalidLogging(String message) { + logger.warn(message); + Tracer.logEvent("Apollo.AccessKey.PreCheck", message); + } } diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java index 4dc3519f11d..46c449f445f 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java @@ -19,6 +19,7 @@ import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.AccessKey; import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository; +import com.ctrip.framework.apollo.common.constants.AccessKeyMode; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; @@ -37,11 +38,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -86,13 +87,21 @@ private void initialize() { } public List getAvailableSecrets(String appId) { + return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.FILTER); + } + + public List getObservableSecrets(String appId) { + return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.OBSERVER); + } + + public List getSecrets(String appId, Predicate filter) { List accessKeys = accessKeyCache.get(appId); if (CollectionUtils.isEmpty(accessKeys)) { return Collections.emptyList(); } return accessKeys.stream() - .filter(AccessKey::isEnabled) + .filter(filter) .map(AccessKey::getSecret) .collect(Collectors.toList()); } diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java index 910d203d964..472e1c37c43 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java @@ -46,6 +46,10 @@ public List findAvailableSecret(String appId) { return accessKeyServiceWithCache.getAvailableSecrets(appId); } + public List findObservableSecrets(String appId) { + return accessKeyServiceWithCache.getObservableSecrets(appId); + } + public String extractAppIdFromRequest(HttpServletRequest request) { String appId = null; String servletPath = request.getServletPath(); diff --git a/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java b/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java index b5676f9f149..5d060aa4263 100644 --- a/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java +++ b/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java @@ -17,7 +17,9 @@ package com.ctrip.framework.apollo.configservice.filter; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -26,6 +28,7 @@ import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil; import com.ctrip.framework.apollo.core.signature.Signature; import com.google.common.collect.Lists; +import java.util.Collections; import java.util.List; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; @@ -58,7 +61,7 @@ public class ClientAuthenticationFilterTest { @Before public void setUp() { - clientAuthenticationFilter = new ClientAuthenticationFilter(bizConfig, accessKeyUtil); + clientAuthenticationFilter = spy(new ClientAuthenticationFilter(bizConfig, accessKeyUtil)); } @Test @@ -141,6 +144,54 @@ public void testAuthorizedSuccessfully() throws Exception { clientAuthenticationFilter.doFilter(request, response, filterChain); + verifySuccessAndDoFilter(); + } + + @Test + public void testPreCheckInvalid() throws Exception { + String appId = "someAppId"; + String availableSignature = "someSignature"; + List secrets = Lists.newArrayList("someSecret"); + String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis() - 61 * 1000); + String errorAuthorization = "Apollo someAppId:wrongSignature"; + + when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); + when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList()); + when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets); + when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature); + when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp); + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(errorAuthorization); + when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60); + + clientAuthenticationFilter.doFilter(request, response, filterChain); + + verifySuccessAndDoFilter(); + verify(clientAuthenticationFilter, times(2)).preCheckInvalidLogging(anyString()); + } + + @Test + public void testPreCheckSuccessfully() throws Exception { + String appId = "someAppId"; + String availableSignature = "someSignature"; + List secrets = Lists.newArrayList("someSecret"); + String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis()); + String correctAuthorization = "Apollo someAppId:someSignature"; + + when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId); + when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList()); + when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets); + when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature); + when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp); + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(correctAuthorization); + when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60); + + clientAuthenticationFilter.doFilter(request, response, filterChain); + + verifySuccessAndDoFilter(); + verify(clientAuthenticationFilter, never()).preCheckInvalidLogging(anyString()); + } + + private void verifySuccessAndDoFilter() throws Exception { verify(response, never()).sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId"); verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed"); verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java index 3d3f23bff4a..b05c2cc77df 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java @@ -184,6 +184,9 @@ public static class ItemAPI extends API { private final ParameterizedTypeReference> openItemPageDTO = new ParameterizedTypeReference>() {}; + private final ParameterizedTypeReference> pageItemInfoDTO = + new ParameterizedTypeReference>() {}; + public List findItems(String appId, Env env, String clusterName, String namespaceName) { ItemDTO[] itemDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items", @@ -198,6 +201,15 @@ public List findDeletedItems(String appId, Env env, String clusterName, return Arrays.asList(itemDTOs); } + public PageDTO getPerEnvItemInfoBySearch(Env env, String key, String value, int page, int size){ + ResponseEntity> + entity = + restTemplate.get(env, + "items-search/key-and-value?key={key}&value={value}&page={page}&size={size}", + pageItemInfoDTO, key, value, page, size); + return entity.getBody(); + } + public ItemDTO loadItem(Env env, String appId, String clusterName, String namespaceName, String key) { return restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{key}", ItemDTO.class, appId, clusterName, namespaceName, key); @@ -305,9 +317,9 @@ public void delete(Env env, String appId, long id, String operator) { } @ApolloAuditLog(type = OpType.RPC, name = "AccessKey.enableInRemote") - public void enable(Env env, String appId, long id, String operator) { - restTemplate.put(env, "apps/{appId}/accesskeys/{id}/enable?operator={operator}", - null, appId, id, operator); + public void enable(Env env, String appId, long id, int mode, String operator) { + restTemplate.put(env, "apps/{appId}/accesskeys/{id}/enable?mode={mode}&operator={operator}", + null, appId, id, mode, operator); } @ApolloAuditLog(type = OpType.RPC, name = "AccessKey.disableInRemote") diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java index 93b9e93daf2..a5a4a9d81ea 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java @@ -86,6 +86,8 @@ public List portalSupportedEnvs() { return envs; } + public int getPerEnvSearchMaxResults() {return getIntProperty("apollo.portal.search.perEnvMaxResults", 200);} + /** * @return the relationship between environment and its meta server. empty if meet exception */ diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolver.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolver.java index 44080d7520d..9c3751f7bea 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolver.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolver.java @@ -21,15 +21,20 @@ import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.utils.BeanUtils; +import com.ctrip.framework.apollo.core.utils.StringUtils; import com.google.common.base.Strings; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import javax.validation.constraints.NotNull; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * normal property file resolver. @@ -45,12 +50,21 @@ public class PropertyResolver implements ConfigTextResolver { @Override public ItemChangeSets resolve(long namespaceId, String configText, List baseItems) { - Map oldLineNumMapItem = BeanUtils.mapByKey("lineNum", baseItems); Map oldKeyMapItem = BeanUtils.mapByKey("key", baseItems); - //remove comment and blank item map. oldKeyMapItem.remove(""); + // comment items + List baseCommentItems = new LinkedList<>(); + // blank items + List baseBlankItems = new LinkedList<>(); + if (!CollectionUtils.isEmpty(baseItems)) { + + baseCommentItems = baseItems.stream().filter(itemDTO -> isCommentItem(itemDTO)).sorted(Comparator.comparing(ItemDTO::getLineNum)).collect(Collectors.toCollection(LinkedList::new)); + + baseBlankItems = baseItems.stream().filter(itemDTO -> isBlankItem(itemDTO)).sorted(Comparator.comparing(ItemDTO::getLineNum)).collect(Collectors.toCollection(LinkedList::new)); + } + String[] newItems = configText.split(ITEM_SEPARATOR); Set repeatKeys = new HashSet<>(); if (isHasRepeatKey(newItems, repeatKeys)) { @@ -63,17 +77,25 @@ public ItemChangeSets resolve(long namespaceId, String configText, List for (String newItem : newItems) { newItem = newItem.trim(); newLineNumMapItem.put(lineCounter, newItem); - ItemDTO oldItemByLine = oldLineNumMapItem.get(lineCounter); //comment item if (isCommentItem(newItem)) { + ItemDTO oldItemDTO = null; + if (!CollectionUtils.isEmpty(baseCommentItems)) { + oldItemDTO = baseCommentItems.remove(0); + } - handleCommentLine(namespaceId, oldItemByLine, newItem, lineCounter, changeSets); + handleCommentLine(namespaceId, oldItemDTO, newItem, lineCounter, changeSets); //blank item } else if (isBlankItem(newItem)) { - handleBlankLine(namespaceId, oldItemByLine, lineCounter, changeSets); + ItemDTO oldItemDTO = null; + if (!CollectionUtils.isEmpty(baseBlankItems)) { + oldItemDTO = baseBlankItems.remove(0); + } + + handleBlankLine(namespaceId, oldItemDTO, lineCounter, changeSets); //normal item } else { @@ -83,7 +105,7 @@ public ItemChangeSets resolve(long namespaceId, String configText, List lineCounter++; } - deleteCommentAndBlankItem(oldLineNumMapItem, newLineNumMapItem, changeSets); + deleteCommentAndBlankItem(baseCommentItems, baseBlankItems, changeSets); deleteNormalKVItem(oldKeyMapItem, changeSets); return changeSets; @@ -97,7 +119,7 @@ private boolean isHasRepeatKey(String[] newItems, @NotNull Set repeatKey String[] kv = parseKeyValueFromItem(item); if (kv != null) { String key = kv[0].toLowerCase(); - if(!keys.add(key)){ + if (!keys.add(key)) { repeatKeys.add(key); } } else { @@ -122,16 +144,18 @@ private String[] parseKeyValueFromItem(String item) { } private void handleCommentLine(Long namespaceId, ItemDTO oldItemByLine, String newItem, int lineCounter, ItemChangeSets changeSets) { - String oldComment = oldItemByLine == null ? "" : oldItemByLine.getComment(); - //create comment. implement update comment by delete old comment and create new comment - if (!(isCommentItem(oldItemByLine) && newItem.equals(oldComment))) { + if (null == oldItemByLine) { changeSets.addCreateItem(buildCommentItem(0L, namespaceId, newItem, lineCounter)); + } else if (!StringUtils.equals(oldItemByLine.getComment(), newItem) || lineCounter != oldItemByLine.getLineNum()) { + changeSets.addUpdateItem(buildCommentItem(oldItemByLine.getId(), namespaceId, newItem, lineCounter)); } } private void handleBlankLine(Long namespaceId, ItemDTO oldItem, int lineCounter, ItemChangeSets changeSets) { - if (!isBlankItem(oldItem)) { + if (null == oldItem) { changeSets.addCreateItem(buildBlankItem(0L, namespaceId, lineCounter)); + } else if (lineCounter != oldItem.getLineNum()) { + changeSets.addUpdateItem(buildBlankItem(oldItem.getId(), namespaceId, lineCounter)); } } @@ -149,12 +173,12 @@ private void handleNormalLine(Long namespaceId, Map keyMapOldIt ItemDTO oldItem = keyMapOldItem.get(newKey); - if (oldItem == null) {//new item + //new item + if (oldItem == null) { changeSets.addCreateItem(buildNormalItem(0L, namespaceId, newKey, newValue, "", lineCounter)); - } else if (!newValue.equals(oldItem.getValue()) || lineCounter != oldItem.getLineNum()) {//update item - changeSets.addUpdateItem( - buildNormalItem(oldItem.getId(), namespaceId, newKey, newValue, oldItem.getComment(), - lineCounter)); + //update item + } else if (!StringUtils.equals(newValue, oldItem.getValue()) || lineCounter != oldItem.getLineNum()) { + changeSets.addUpdateItem(buildNormalItem(oldItem.getId(), namespaceId, newKey, newValue, oldItem.getComment(), lineCounter)); } keyMapOldItem.remove(newKey); } @@ -173,7 +197,7 @@ private boolean isBlankItem(ItemDTO item) { } private boolean isBlankItem(String line) { - return Strings.nullToEmpty(line).trim().isEmpty(); + return Strings.nullToEmpty(line).trim().isEmpty(); } private void deleteNormalKVItem(Map baseKeyMapItem, ItemChangeSets changeSets) { @@ -183,23 +207,11 @@ private void deleteNormalKVItem(Map baseKeyMapItem, ItemChangeS } } - private void deleteCommentAndBlankItem(Map oldLineNumMapItem, - Map newLineNumMapItem, + private void deleteCommentAndBlankItem(List baseCommentItems, + List baseBlankItems, ItemChangeSets changeSets) { - - for (Map.Entry entry : oldLineNumMapItem.entrySet()) { - int lineNum = entry.getKey(); - ItemDTO oldItem = entry.getValue(); - String newItem = newLineNumMapItem.get(lineNum); - - //1. old is blank by now is not - //2.old is comment by now is not exist or modified - //3.old is blank by now is not exist or modified - if ((isBlankItem(oldItem) && !isBlankItem(newItem)) - || (isCommentItem(oldItem) || isBlankItem(oldItem)) && (newItem == null || !newItem.equals(oldItem.getComment()))) { - changeSets.addDeleteItem(oldItem); - } - } + baseCommentItems.forEach(oldItemDTO -> changeSets.addDeleteItem(oldItemDTO)); + baseBlankItems.forEach(oldItemDTO -> changeSets.addDeleteItem(oldItemDTO)); } private ItemDTO buildCommentItem(Long id, Long namespaceId, String comment, int lineNum) { diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AccessKeyController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AccessKeyController.java index fb8518d7dec..b5ef6f123f5 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AccessKeyController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AccessKeyController.java @@ -16,6 +16,8 @@ */ package com.ctrip.framework.apollo.portal.controller; +import static com.ctrip.framework.apollo.common.constants.AccessKeyMode.FILTER; + import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.dto.AccessKeyDTO; @@ -77,9 +79,10 @@ public void delete(@PathVariable String appId, @ApolloAuditLog(type = OpType.UPDATE, name = "AccessKey.enable") public void enable(@PathVariable String appId, @PathVariable String env, - @PathVariable long id) { + @PathVariable long id, + @RequestParam(required = false, defaultValue = "" + FILTER) int mode) { String operator = userInfoHolder.getUser().getUserId(); - accessKeyService.enable(Env.valueOf(env), appId, id, operator); + accessKeyService.enable(Env.valueOf(env), appId, id, mode, operator); } @PreAuthorize(value = "@permissionValidator.isAppAdmin(#appId)") diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchController.java new file mode 100644 index 00000000000..bb44b22932f --- /dev/null +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchController.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.portal.controller; + + +import com.ctrip.framework.apollo.common.exception.BadRequestException; +import com.ctrip.framework.apollo.common.http.SearchResponseEntity; +import com.ctrip.framework.apollo.portal.component.config.PortalConfig; +import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; +import com.ctrip.framework.apollo.portal.service.GlobalSearchService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class GlobalSearchController { + private final GlobalSearchService globalSearchService; + private final PortalConfig portalConfig; + + public GlobalSearchController(final GlobalSearchService globalSearchService, final PortalConfig portalConfig) { + this.globalSearchService = globalSearchService; + this.portalConfig = portalConfig; + } + + @PreAuthorize(value = "@permissionValidator.isSuperAdmin()") + @GetMapping("/global-search/item-info/by-key-or-value") + public SearchResponseEntity> getItemInfoBySearch(@RequestParam(value = "key", required = false, defaultValue = "") String key, + @RequestParam(value = "value", required = false , defaultValue = "") String value) { + + if(key.isEmpty() && value.isEmpty()) { + throw new BadRequestException("Please enter at least one search criterion in either key or value."); + } + + return globalSearchService.getAllEnvItemInfoBySearch(key, value, 0, portalConfig.getPerEnvSearchMaxResults()); + } + +} diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ItemInfo.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ItemInfo.java new file mode 100644 index 00000000000..f4d8dbf3974 --- /dev/null +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/ItemInfo.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.portal.entity.vo; + +public class ItemInfo { + + private String appId; + private String envName; + private String clusterName; + private String namespaceName; + private String key; + private String value; + + public ItemInfo() { + } + + public ItemInfo(String appId, String envName, String clusterName, + String namespaceName, String key, String value) { + this.appId = appId; + this.envName = envName; + this.clusterName = clusterName; + this.namespaceName = namespaceName; + this.key = key; + this.value = value; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getEnvName() { + return envName; + } + + public void setEnvName(String envName) { + this.envName = envName; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getNamespaceName() { + return namespaceName; + } + + public void setNamespaceName(String namespaceName) { + this.namespaceName = namespaceName; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "ItemInfo{" + + "appId='" + appId + '\'' + + ", envName='" + envName + '\'' + + ", clusterName='" + clusterName + '\'' + + ", namespaceName='" + namespaceName + '\'' + + ", key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AccessKeyService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AccessKeyService.java index 26ba0ff92c0..9f7890ccf8e 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AccessKeyService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AccessKeyService.java @@ -49,8 +49,8 @@ public void deleteAccessKey(Env env, String appId, long id, String operator) { accessKeyAPI.delete(env, appId, id, operator); } - public void enable(Env env, String appId, long id, String operator) { - accessKeyAPI.enable(env, appId, id, operator); + public void enable(Env env, String appId, long id, int mode, String operator) { + accessKeyAPI.enable(env, appId, id, mode, operator); } public void disable(Env env, String appId, long id, String operator) { diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/GlobalSearchService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/GlobalSearchService.java new file mode 100644 index 00000000000..5c7cbdcf5ba --- /dev/null +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/GlobalSearchService.java @@ -0,0 +1,77 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.portal.service; + +import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; +import com.ctrip.framework.apollo.common.dto.PageDTO; +import com.ctrip.framework.apollo.common.http.SearchResponseEntity; +import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; +import com.ctrip.framework.apollo.portal.component.PortalSettings; +import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; +import com.ctrip.framework.apollo.portal.environment.Env; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +@Service +public class GlobalSearchService { + + private static final Logger LOGGER = LoggerFactory.getLogger(GlobalSearchService.class); + private final AdminServiceAPI.ItemAPI itemAPI; + private final PortalSettings portalSettings; + + public GlobalSearchService(AdminServiceAPI.ItemAPI itemAPI, PortalSettings portalSettings) { + this.itemAPI = itemAPI; + this.portalSettings = portalSettings; + } + + public SearchResponseEntity> getAllEnvItemInfoBySearch(String key, String value, int page, int size) { + List activeEnvs = portalSettings.getActiveEnvs(); + List envBeyondLimit = new ArrayList<>(); + AtomicBoolean hasMoreData = new AtomicBoolean(false); + List allEnvItemInfos = new ArrayList<>(); + activeEnvs.forEach(env -> { + PageDTO perEnvItemInfoDTOs = itemAPI.getPerEnvItemInfoBySearch(env, key, value, page, size); + if (!perEnvItemInfoDTOs.hasContent()) { + return; + } + perEnvItemInfoDTOs.getContent().forEach(itemInfoDTO -> { + try { + ItemInfo itemInfo = new ItemInfo(itemInfoDTO.getAppId(),env.getName(),itemInfoDTO.getClusterName(),itemInfoDTO.getNamespaceName(),itemInfoDTO.getKey(),itemInfoDTO.getValue()); + allEnvItemInfos.add(itemInfo); + } catch (Exception e) { + LOGGER.error("Error converting ItemInfoDTO to ItemInfo for item: {}", itemInfoDTO, e); + } + }); + if(perEnvItemInfoDTOs.getTotal() > size){ + envBeyondLimit.add(env.getName()); + hasMoreData.set(true); + } + }); + if(hasMoreData.get()){ + return SearchResponseEntity.okWithMessage(allEnvItemInfos,String.format( + "In %s , more than %d items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.", + String.join(" , ", envBeyondLimit), size)); + } + return SearchResponseEntity.ok(allEnvItemInfos); + } + +} diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ItemService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ItemService.java index b1677e18858..589052a6f82 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ItemService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ItemService.java @@ -50,6 +50,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Service public class ItemService { @@ -216,12 +217,13 @@ public void revokeItem(String appId, Env env, String clusterName, String namespa } List baseItems = itemAPI.findItems(appId, env, clusterName, namespaceName); Map oldKeyMapItem = BeanUtils.mapByKey("key", baseItems); - Map deletedItemDTOs = new HashMap<>(); + //remove comment and blank item map. + oldKeyMapItem.remove(""); //deleted items for comment - findDeletedItems(appId, env, clusterName, namespaceName).forEach(item -> { - deletedItemDTOs.put(item.getKey(),item); - }); + Map deletedItemDTOs = findDeletedItems(appId, env, clusterName, namespaceName).stream() + .filter(itemDTO -> !StringUtils.isEmpty(itemDTO.getKey())) + .collect(Collectors.toMap(itemDTO -> itemDTO.getKey(), v -> v, (v1, v2) -> v2)); ItemChangeSets changeSets = new ItemChangeSets(); AtomicInteger lineNum = new AtomicInteger(1); @@ -229,16 +231,15 @@ public void revokeItem(String appId, Env env, String clusterName, String namespa ItemDTO oldItem = oldKeyMapItem.get(key); if (oldItem == null) { ItemDTO deletedItemDto = deletedItemDTOs.computeIfAbsent(key, k -> new ItemDTO()); - changeSets.addCreateItem(buildNormalItem(0L, namespaceId,key,value,deletedItemDto.getComment(),lineNum.get())); - } else if (!oldItem.getValue().equals(value) || lineNum.get() != oldItem - .getLineNum()) { - changeSets.addUpdateItem(buildNormalItem(oldItem.getId(), namespaceId, key, - value, oldItem.getComment(), lineNum.get())); + int newLineNum = 0 == deletedItemDto.getLineNum() ? lineNum.get() : deletedItemDto.getLineNum(); + changeSets.addCreateItem(buildNormalItem(0L, namespaceId, key, value, deletedItemDto.getComment(), newLineNum)); + } else if (!StringUtils.equals(oldItem.getValue(), value) || lineNum.get() != oldItem.getLineNum()) { + changeSets.addUpdateItem(buildNormalItem(oldItem.getId(), namespaceId, key, value, oldItem.getComment(), oldItem.getLineNum())); } oldKeyMapItem.remove(key); lineNum.set(lineNum.get() + 1); }); - oldKeyMapItem.forEach((key, value) -> changeSets.addDeleteItem(oldKeyMapItem.get(key))); + oldKeyMapItem.forEach((key, value) -> changeSets.addDeleteItem(value)); changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId()); updateItems(appId, env, clusterName, namespaceName, changeSets); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java index 43f05e4de94..183a58b891e 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java @@ -309,7 +309,6 @@ private NamespaceBO transformNamespace2BO(Env env, NamespaceDTO namespace, boole //latest Release ReleaseDTO latestRelease; Map releaseItems = new HashMap<>(); - Map deletedItemDTOs = new HashMap<>(); latestRelease = releaseService.loadLatestRelease(appId, env, clusterName, namespaceName); if (latestRelease != null) { releaseItems = GSON.fromJson(latestRelease.getConfigurations(), GsonType.CONFIG); @@ -333,9 +332,9 @@ private NamespaceBO transformNamespace2BO(Env env, NamespaceDTO namespace, boole if (includeDeletedItems) { //deleted items - itemService.findDeletedItems(appId, env, clusterName, namespaceName).forEach(item -> { - deletedItemDTOs.put(item.getKey(), item); - }); + Map deletedItemDTOs = itemService.findDeletedItems(appId, env, clusterName, namespaceName).stream() + .filter(itemDTO -> !StringUtils.isEmpty(itemDTO.getKey())) + .collect(Collectors.toMap(itemDTO -> itemDTO.getKey(), v -> v, (v1, v2) -> v2)); List deletedItems = parseDeletedItems(items, releaseItems, deletedItemDTOs); itemBOs.addAll(deletedItems); @@ -385,6 +384,8 @@ private void fillAppNamespaceProperties(NamespaceBO namespace) { private List parseDeletedItems(List newItems, Map releaseItems, Map deletedItemDTOs) { Map newItemMap = BeanUtils.mapByKey("key", newItems); + //remove comment and blank item map. + newItemMap.remove(""); List deletedItems = new LinkedList<>(); for (Map.Entry entry : releaseItems.entrySet()) { diff --git a/apollo-portal/src/main/resources/static/app/access_key.html b/apollo-portal/src/main/resources/static/app/access_key.html index debca68fd0f..4cb1c9c4993 100644 --- a/apollo-portal/src/main/resources/static/app/access_key.html +++ b/apollo-portal/src/main/resources/static/app/access_key.html @@ -60,10 +60,11 @@

{{'Common.Environment' | translate }}: {{env}} {{accessKey.secret}} - {{accessKey.enabled ? ('AccessKey.Operator.Enabled' | translate) : ('AccessKey.Operator.Disabled' | translate) }} + {{'AccessKey.Operator.Enabled' | translate}} + {{'AccessKey.Operator.Observed' | translate}} + {{'AccessKey.Operator.Disabled' | translate}} {{accessKey.dataChangeLastModifiedBy}} {{accessKey.dataChangeLastModifiedTime | date: 'yyyy-MM-dd HH:mm:ss'}} - {{'AccessKey.Operator.Enable' | translate}} - {{'AccessKey.Operator.Disable' | translate}} - {{'AccessKey.Operator.Remove' | translate }} + {{'AccessKey.Operator.Enable' | translate}} + {{'AccessKey.Operator.Observe' | translate}} + {{'AccessKey.Operator.Disable' | translate}} + {{'AccessKey.Operator.Remove' | translate }} diff --git a/apollo-portal/src/main/resources/static/global_search_value.html b/apollo-portal/src/main/resources/static/global_search_value.html new file mode 100644 index 00000000000..1d59578914b --- /dev/null +++ b/apollo-portal/src/main/resources/static/global_search_value.html @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + {{'Global.Title' | translate }} + + + + +
+
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apollo-portal/src/main/resources/static/i18n/en.json b/apollo-portal/src/main/resources/static/i18n/en.json index dbe1d698520..69ae8e5e4cf 100644 --- a/apollo-portal/src/main/resources/static/i18n/en.json +++ b/apollo-portal/src/main/resources/static/i18n/en.json @@ -517,10 +517,11 @@ "ServiceConfig.KeyExistsAndSaveTip": "Key: '{{key}}' already exists. Click Save will override the configuration item.", "AccessKey.Tips.1": "Add up to 5 access keys per environment.", "AccessKey.Tips.2": "Once the environment has any enabled access key, the client will be required to configure access key, or the configurations cannot be obtained.", - "AccessKey.Tips.3": "Configure the access key to prevent unauthorized clients from obtaining the application configuration. The configuration method is as follows(only apollo-client version 1.6.0+):", - "AccessKey.Tips.3.1": "Via jvm parameter: apollo-client version >=1.9.0 is recommended to use -Dapollo.access-key.secret; other versions use -Dapollo.accesskey.secret", - "AccessKey.Tips.3.2": "Through the os environment variable: apollo-client version >=1.9.0 is recommended to use APOLLO_ACCESS_KEY_SECRET; other versions use APOLLO_ACCESSKEY_SECRET", - "AccessKey.Tips.3.3": "Via META-INF/app.properties or application.properties: apollo-client version >=1.9.0 is recommended to use apollo.access-key.secret; other versions use apollo.accesskey.secret(note that the multi-environment secret is different)", + "AccessKey.Tips.3": "Observed access keys are used for pre-check and logging only. Note: Once the environment has any enabled access key, the observed status will no longer take effect.", + "AccessKey.Tips.4": "Configure the access key to prevent unauthorized clients from obtaining the application configuration. The configuration method is as follows(only apollo-client version 1.6.0+):", + "AccessKey.Tips.4.1": "Via jvm parameter: apollo-client version >=1.9.0 is recommended to use -Dapollo.access-key.secret; other versions use -Dapollo.accesskey.secret", + "AccessKey.Tips.4.2": "Through the os environment variable: apollo-client version >=1.9.0 is recommended to use APOLLO_ACCESS_KEY_SECRET; other versions use APOLLO_ACCESSKEY_SECRET", + "AccessKey.Tips.4.3": "Via META-INF/app.properties or application.properties: apollo-client version >=1.9.0 is recommended to use apollo.access-key.secret; other versions use apollo.accesskey.secret(note that the multi-environment secret is different)", "AccessKey.NoAccessKeyServiceTips": "There are no access keys in this environment.", "AccessKey.ConfigAccessKeys.Secret": "Access Key Secret", "AccessKey.ConfigAccessKeys.Status": "Status", @@ -529,19 +530,24 @@ "AccessKey.ConfigAccessKeys.Operator": "Operation", "AccessKey.Operator.Disable": "Disable", "AccessKey.Operator.Enable": "Enable", + "AccessKey.Operator.Observe": "Observe", "AccessKey.Operator.Disabled": "Disabled", "AccessKey.Operator.Enabled": "Enabled", + "AccessKey.Operator.Observed": "Observed", "AccessKey.Operator.Remove": "Remove", "AccessKey.Operator.CreateSuccess": "Access key created successfully", "AccessKey.Operator.DisabledSuccess": "Access key disabled successfully", "AccessKey.Operator.EnabledSuccess": "Access key enabled successfully", + "AccessKey.Operator.ObservedSuccess": "Access key observed successfully", "AccessKey.Operator.RemoveSuccess": "Access key removed successfully", "AccessKey.Operator.CreateError": "Access key created failed", "AccessKey.Operator.DisabledError": "Access key disabled failed", "AccessKey.Operator.EnabledError": "Access key enabled failed", + "AccessKey.Operator.ObservedError": "Access key observed failed", "AccessKey.Operator.RemoveError": "Access key removed failed", "AccessKey.Operator.DisabledTips": "Are you sure you want to disable the access key?", "AccessKey.Operator.EnabledTips": "Are you sure you want to enable the access key?", + "AccessKey.Operator.ObservedTips": "Are you sure you want to observe the access key?", "AccessKey.Operator.RemoveTips": "Are you sure you want to remove the access key?", "AccessKey.LoadError": "Error Loading access keys", "SystemInfo.Title": "System Information", @@ -893,5 +899,28 @@ "ApolloAuditLog.ParentSpan": "parent operation", "ApolloAuditLog.FollowsFromSpan": "last operation", "ApolloAuditLog.FieldChangeHistory": "Field Change History", - "ApolloAuditLog.InfluenceEntity": "Audit entity influenced" + "ApolloAuditLog.InfluenceEntity": "Audit entity influenced", + "Global.Title": "Global Search for Value", + "Global.App": "App ID", + "Global.Env": "Env Name", + "Global.Cluster": "Cluster Name", + "Global.NameSpace": "NameSpace Name", + "Global.Key": "Key", + "Global.Value": "Value", + "Global.ValueSearch.Tips" : "(Fuzzy search, key can be the name or content of the configuration item, value is the value of the configuration item.)", + "Global.Operate" : "Operate", + "Global.Expand" : "Expand", + "Global.Abbreviate" : "Abbreviate", + "Global.JumpToEditPage" : "Jump to edit page", + "Item.GlobalSearchByKey": "Search by Key", + "Item.GlobalSearchByValue": "Search by Value", + "Item.GlobalSearch": "Search", + "Item.GlobalSearchSystemError": "System error, please try again or contact the system administrator", + "Item.GlobalSearch.Tips": "Search hint", + "ApolloGlobalSearch.NoData" : "No data yet, please search or add", + "Paging.TotalItems.part1" : "Total of", + "Paging.TotalItems.part2" : "records", + "Paging.DisplayNumber" : "per/Page", + "Paging.PageNumberOne" : "First", + "Paging.PageNumberLast" : "Last" } diff --git a/apollo-portal/src/main/resources/static/i18n/zh-CN.json b/apollo-portal/src/main/resources/static/i18n/zh-CN.json index e166237c306..8ae676eda59 100644 --- a/apollo-portal/src/main/resources/static/i18n/zh-CN.json +++ b/apollo-portal/src/main/resources/static/i18n/zh-CN.json @@ -516,11 +516,12 @@ "ServiceConfig.KeyNotExistsAndCreateTip": "Key: '{{key}}' 不存在,点击保存后会创建该配置项", "ServiceConfig.KeyExistsAndSaveTip": "Key: '{{key}}' 已存在,点击保存后会覆盖该配置项", "AccessKey.Tips.1": "每个环境最多可添加 5 个访问密钥", - "AccessKey.Tips.2": "一旦该环境有启用的访问密钥,客户端将被要求配置密钥,否则无法获取配置", - "AccessKey.Tips.3": "配置访问密钥防止非法客户端获取该应用配置,配置方式如下:(仅支持apollo-client 1.6.0+)", - "AccessKey.Tips.3.1": "通过JVM参数配置: apollo-client >=1.9.0 推荐使用 -Dapollo.access-key.secret; 其它版本使用 -Dapollo.accesskey.secret", - "AccessKey.Tips.3.2": "通过操作系统环境变量配置: apollo-client >=1.9.0 推荐使用 APOLLO_ACCESS_KEY_SECRET; 其它版本使用 APOLLO_ACCESSKEY_SECRET", - "AccessKey.Tips.3.3": "通过 META-INF/app.properties 或 application.properties配置: apollo-client >=1.9.0 推荐使用 apollo.access-key.secret; 其它版本使用 apollo.accesskey.secret(注意多环境 secret 不一样)", + "AccessKey.Tips.2": "一旦该环境有启用状态的访问密钥,客户端将被要求配置密钥,否则无法获取配置", + "AccessKey.Tips.3": "观察状态密钥用于预校验,只做日志记录不拦截配置获取,注意:一旦该环境有启用状态的访问密钥,观察状态将不再生效", + "AccessKey.Tips.4": "配置访问密钥防止非法客户端获取该应用配置,配置方式如下:(仅支持apollo-client 1.6.0+)", + "AccessKey.Tips.4.1": "通过JVM参数配置: apollo-client >=1.9.0 推荐使用 -Dapollo.access-key.secret; 其它版本使用 -Dapollo.accesskey.secret", + "AccessKey.Tips.4.2": "通过操作系统环境变量配置: apollo-client >=1.9.0 推荐使用 APOLLO_ACCESS_KEY_SECRET; 其它版本使用 APOLLO_ACCESSKEY_SECRET", + "AccessKey.Tips.4.3": "通过 META-INF/app.properties 或 application.properties配置: apollo-client >=1.9.0 推荐使用 apollo.access-key.secret; 其它版本使用 apollo.accesskey.secret(注意多环境 secret 不一样)", "AccessKey.NoAccessKeyServiceTips": "该环境没有配置访问密钥", "AccessKey.ConfigAccessKeys.Secret": "访问密钥", "AccessKey.ConfigAccessKeys.Status": "状态", @@ -529,19 +530,24 @@ "AccessKey.ConfigAccessKeys.Operator": "操作", "AccessKey.Operator.Disable": "禁用", "AccessKey.Operator.Enable": "启用", + "AccessKey.Operator.Observe": "观察", "AccessKey.Operator.Disabled": "已禁用", "AccessKey.Operator.Enabled": "已启用", + "AccessKey.Operator.Observed": "已观察", "AccessKey.Operator.Remove": "删除", "AccessKey.Operator.CreateSuccess": "访问密钥创建成功", "AccessKey.Operator.DisabledSuccess": "访问密钥禁用成功", "AccessKey.Operator.EnabledSuccess": "访问密钥启用成功", + "AccessKey.Operator.ObservedSuccess": "访问密钥观察成功", "AccessKey.Operator.RemoveSuccess": "访问密钥删除成功", "AccessKey.Operator.CreateError": "访问密钥创建失败", "AccessKey.Operator.DisabledError": "访问密钥禁用失败", "AccessKey.Operator.EnabledError": "访问密钥启用失败", + "AccessKey.Operator.ObservedError": "访问密钥观察失败", "AccessKey.Operator.RemoveError": "访问密钥删除失败", "AccessKey.Operator.DisabledTips": "是否确定禁用该访问密钥?", "AccessKey.Operator.EnabledTips": " 是否确定启用该访问密钥?", + "AccessKey.Operator.ObservedTips": " 是否确定观察该访问密钥?", "AccessKey.Operator.RemoveTips": " 是否确定删除该访问密钥?", "AccessKey.LoadError": "加载访问密钥出错", "SystemInfo.Title": "系统信息", @@ -893,5 +899,28 @@ "ApolloAuditLog.ParentSpan": "父操作", "ApolloAuditLog.FollowsFromSpan": "前操作", "ApolloAuditLog.FieldChangeHistory": "属性变动历史", - "ApolloAuditLog.InfluenceEntity": "影响的审计实体" + "ApolloAuditLog.InfluenceEntity": "影响的审计实体", + "Global.Title": "Value的全局搜索", + "Global.App": "应用ID", + "Global.Env": "环境", + "Global.Cluster": "集群名", + "Global.NameSpace": "命名空间", + "Global.Key": "Key", + "Global.Value": "Value", + "Global.ValueSearch.Tips" : "(模糊搜索,key可为配置项名称或content,value为配置项值)", + "Global.Operate" : "操作", + "Global.Expand" : "展开", + "Global.Abbreviate" : "缩略", + "Global.JumpToEditPage" : "跳转到编辑页面", + "Item.GlobalSearchByKey": "按照Key值检索", + "Item.GlobalSearchByValue": "按照Value值检索", + "Item.GlobalSearch": "查询", + "Item.GlobalSearchSystemError": "系统出错,请重试或联系系统负责人", + "Item.GlobalSearch.Tips": "搜索提示", + "ApolloGlobalSearch.NoData" : "暂无数据,请进行检索或者添加", + "Paging.TotalItems.part1" : "共", + "Paging.TotalItems.part2" : "条记录", + "Paging.DisplayNumber" : "条/页", + "Paging.PageNumberOne" : "首页", + "Paging.PageNumberLast" : "尾页" } diff --git a/apollo-portal/src/main/resources/static/img/nodata.png b/apollo-portal/src/main/resources/static/img/nodata.png new file mode 100644 index 00000000000..1cb236546ea Binary files /dev/null and b/apollo-portal/src/main/resources/static/img/nodata.png differ diff --git a/apollo-portal/src/main/resources/static/scripts/app.js b/apollo-portal/src/main/resources/static/scripts/app.js index bac2dd5fde8..88ecb81e522 100644 --- a/apollo-portal/src/main/resources/static/scripts/app.js +++ b/apollo-portal/src/main/resources/static/scripts/app.js @@ -64,6 +64,8 @@ var diff_item_module = angular.module('diff_item', ['app.service', 'apollo.direc var namespace_module = angular.module('namespace', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']); //server config var server_config_manage_module = angular.module('server_config_manage', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']); +// Value的全局检索 +var global_search_value_module = angular.module('global_search_value', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'ngSanitize']); //setting var setting_module = angular.module('setting', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']); //role diff --git a/apollo-portal/src/main/resources/static/scripts/controller/AccessKeyController.js b/apollo-portal/src/main/resources/static/scripts/controller/AccessKeyController.js index 7044ba54286..b34f939486f 100644 --- a/apollo-portal/src/main/resources/static/scripts/controller/AccessKeyController.js +++ b/apollo-portal/src/main/resources/static/scripts/controller/AccessKeyController.js @@ -136,17 +136,19 @@ function AccessKeyController($scope, $location, $translate, toastr, } } - function enable(id, env) { - var confirmTips = $translate.instant('AccessKey.Operator.EnabledTips', { + function enable(id, env, mode) { + mode = (mode === 1) ? 1 : 0; + var tipsPrefix = mode === 1 ? 'AccessKey.Operator.Observed' : 'AccessKey.Operator.Enabled'; + var confirmTips = $translate.instant(tipsPrefix + 'Tips', { appId: $scope.pageContext.appId }); if (confirm(confirmTips)) { - AccessKeyService.enable_access_key($scope.pageContext.appId, env, id) + AccessKeyService.enable_access_key($scope.pageContext.appId, env, id, mode) .then(function () { - toastr.success($translate.instant('AccessKey.Operator.EnabledSuccess', {env})); + toastr.success($translate.instant(tipsPrefix + 'Success', {env})); loadAccessKeys(env); }, function (result) { - toastr.error(AppUtil.errorMsg(result), $translate.instant('AccessKey.Operator.EnabledError', {env})); + toastr.error(AppUtil.errorMsg(result), $translate.instant(tipsPrefix + 'Error', {env})); }); } } diff --git a/apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js b/apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js new file mode 100644 index 00000000000..ee720aacc58 --- /dev/null +++ b/apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js @@ -0,0 +1,273 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +global_search_value_module.controller('GlobalSearchValueController', + ['$scope', '$window', '$translate', 'toastr', 'AppUtil', 'GlobalSearchValueService', 'PermissionService', GlobalSearchValueController]); + +function GlobalSearchValueController($scope, $window, $translate, toastr, AppUtil, GlobalSearchValueService, PermissionService) { + + $scope.allItemInfo = []; + $scope.pageItemInfo = []; + $scope.itemInfoSearchKey = ''; + $scope.itemInfoSearchValue = ''; + $scope.needToBeHighlightedKey = ''; + $scope.needToBeHighlightedValue = ''; + $scope.isShowHighlightKeyword = []; + $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; + $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; + $scope.isAllItemInfoDisplayValueInARow = []; + $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; + $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; + $scope.isPageItemInfoDisplayValueInARow = []; + $scope.currentPage = 1; + $scope.pageSize = '10'; + $scope.totalItems = 0; + $scope.totalPages = 0; + $scope.pagesArray = []; + $scope.tempKey = ''; + $scope.tempValue = ''; + + $scope.getItemInfoByKeyAndValue = getItemInfoByKeyAndValue; + $scope.highlightKeyword = highlightKeyword; + $scope.jumpToTheEditingPage = jumpToTheEditingPage; + $scope.isShowAllValue = isShowAllValue; + $scope.convertPageSizeToInt = convertPageSizeToInt; + $scope.changePage = changePage; + $scope.getPagesArray = getPagesArray; + $scope.determineDisplayKeyOrValueWithoutShowHighlightKeyword = determineDisplayKeyOrValueWithoutShowHighlightKeyword; + $scope.determineDisplayValueInARow = determineDisplayValueInARow; + + init(); + function init() { + initPermission(); + } + + function initPermission() { + PermissionService.has_root_permission() + .then(function (result) { + $scope.isRootUser = result.hasPermission; + }); + } + + function getItemInfoByKeyAndValue(itemInfoSearchKey, itemInfoSearchValue) { + $scope.currentPage = 1; + $scope.itemInfoSearchKey = itemInfoSearchKey || ''; + $scope.itemInfoSearchValue = itemInfoSearchValue || ''; + $scope.allItemInfo = []; + $scope.pageItemInfo = []; + $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; + $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; + $scope.isAllItemInfoDisplayValueInARow = []; + $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; + $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; + $scope.isPageItemInfoDisplayValueInARow = []; + $scope.tempKey = itemInfoSearchKey || ''; + $scope.tempValue = itemInfoSearchValue || ''; + $scope.isShowHighlightKeyword = []; + GlobalSearchValueService.findItemInfoByKeyAndValue($scope.itemInfoSearchKey, $scope.itemInfoSearchValue) + .then(handleSuccess).catch(handleError); + function handleSuccess(result) { + let allItemInfo = []; + let isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; + let isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; + let isAllItemInfoDisplayValueInARow = []; + if(($scope.itemInfoSearchKey === '') && !($scope.itemInfoSearchValue === '')){ + $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue; + $scope.needToBeHighlightedKey = ''; + result.body.forEach((itemInfo, index) => { + allItemInfo.push(itemInfo); + isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = '0'; + isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue); + isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue); + }); + }else if(!($scope.itemInfoSearchKey === '') && ($scope.itemInfoSearchValue === '')){ + $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey; + $scope.needToBeHighlightedValue = ''; + result.body.forEach((itemInfo, index) => { + allItemInfo.push(itemInfo); + isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey); + isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = '0'; + }); + }else{ + $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey; + $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue; + result.body.forEach((itemInfo, index) => { + allItemInfo.push(itemInfo); + isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue); + isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey); + isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue); + }); + } + $scope.totalItems = allItemInfo.length; + $scope.allItemInfo = allItemInfo; + $scope.totalPages = Math.ceil($scope.totalItems / parseInt($scope.pageSize, 10)); + const startIndex = ($scope.currentPage - 1) * parseInt($scope.pageSize, 10); + const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), allItemInfo.length); + $scope.pageItemInfo = allItemInfo.slice(startIndex, endIndex); + $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword; + $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword; + $scope.isAllItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow; + $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex); + $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex); + $scope.isPageItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex); + getPagesArray(); + if(result.hasMoreData){ + toastr.warning(result.message, $translate.instant('Item.GlobalSearch.Tips')); + } + } + + function handleError(error) { + $scope.itemInfo = []; + toastr.error(AppUtil.errorMsg(error), $translate.instant('Item.GlobalSearchSystemError')); + } + } + + function convertPageSizeToInt() { + getItemInfoByKeyAndValue($scope.tempKey, $scope.tempValue); + } + + function changePage(page) { + if (page >= 1 && page <= $scope.totalPages) { + $scope.currentPage = page; + $scope.isShowHighlightKeyword = []; + $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = []; + $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = []; + $scope.isPageItemInfoDisplayValueInARow = []; + $scope.itemInfoSearchKey = $scope.tempKey; + $scope.itemInfoSearchValue = $scope.tempValue; + const startIndex = ($scope.currentPage - 1)* parseInt($scope.pageSize, 10); + const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), $scope.totalItems); + $scope.pageItemInfo = $scope.allItemInfo.slice(startIndex, endIndex); + $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex); + $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex); + $scope.isPageItemInfoDisplayValueInARow = $scope.isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex); + getPagesArray(); + } + } + + function getPagesArray() { + const pageRange = 2; + let pagesArray = []; + let currentPage = $scope.currentPage; + let totalPages = $scope.totalPages; + if (totalPages <= (pageRange * 2) + 4) { + for (let i = 1; i <= totalPages; i++) { + pagesArray.push(i); + } + } else { + if (currentPage <= (pageRange + 2)) { + for (let i = 1; i <= pageRange * 2 + 2; i++) { + pagesArray.push(i); + } + pagesArray.push('...'); + pagesArray.push(totalPages); + } else if (currentPage >= (totalPages - (pageRange + 1))) { + for (let i = totalPages - pageRange * 2 - 1 ; i <= totalPages; i++) { + pagesArray.push(i); + } + pagesArray.unshift('...'); + pagesArray.unshift(1); + } else { + for (let i = (currentPage - pageRange); i <= currentPage + pageRange; i++) { + pagesArray.push(i); + } + pagesArray.unshift('...'); + pagesArray.unshift(1); + pagesArray.push('...'); + pagesArray.push(totalPages); + } + } + $scope.pagesArray = pagesArray; + } + + function determineDisplayValueInARow(value, highlight) { + var valueColumn = document.getElementById('valueColumn'); + var testElement = document.createElement('span'); + setupTestElement(testElement, valueColumn); + testElement.innerText = value; + document.body.appendChild(testElement); + const position = determinePosition(value, highlight); + let displayValue = '0'; + if (testElement.scrollWidth > testElement.offsetWidth) { + displayValue = position; + } else { + if (testElement.scrollWidth === testElement.offsetWidth) { + return '0'; + } + switch (position) { + case '1': + testElement.innerText = value + '...' + '| ' + $translate.instant('Global.Expand'); + break; + case '2': + testElement.innerText = '...' + value + '| ' + $translate.instant('Global.Expand'); + break; + case '3': + testElement.innerText = '...' + value + '...' + '| ' + $translate.instant('Global.Expand'); + break; + default: + return '0'; + } + if (testElement.scrollWidth === testElement.offsetWidth) { + displayValue = '0'; + } else { + displayValue = position; + } + } + document.body.removeChild(testElement); + return displayValue; + } + + function setupTestElement(element, valueColumn) { + element.style.visibility = 'hidden'; + element.style.position = 'absolute'; + element.style.whiteSpace = 'nowrap'; + element.style.display = 'inline-block'; + element.style.fontFamily = '"Open Sans", sans-serif'; + const devicePixelRatio = window.devicePixelRatio; + const zoomLevel = Math.round((window.outerWidth / window.innerWidth) * 100) / 100; + element.style.fontSize = 13 * devicePixelRatio * zoomLevel + 'px'; + element.style.padding = 8 * devicePixelRatio * zoomLevel + 'px'; + element.style.width = valueColumn.offsetWidth * devicePixelRatio * zoomLevel + 'px'; + } + + function determinePosition(value, highlight) { + const position = value.indexOf(highlight); + if (position === -1) return '-1'; + if (position === 0) return '1'; + if (position + highlight.length === value.length) return '2'; + return "3"; + } + + function determineDisplayKeyOrValueWithoutShowHighlightKeyword(keyorvalue, highlight) { + return keyorvalue === highlight ? '0' : '-1'; + } + + function jumpToTheEditingPage(appid,env,cluster){ + let url = AppUtil.prefixPath() + "/config.html#/appid=" + appid + "&" +"env=" + env + "&" + "cluster=" + cluster; + window.open(url, '_blank'); + } + + function highlightKeyword(fulltext,keyword) { + if (!keyword || keyword.length === 0) return fulltext; + let regex = new RegExp("(" + keyword + ")", "g"); + return fulltext.replace(regex, '$1'); + } + + function isShowAllValue(index){ + $scope.isShowHighlightKeyword[index] = !$scope.isShowHighlightKeyword[index]; + } + +} diff --git a/apollo-portal/src/main/resources/static/scripts/services/AccessKeyService.js b/apollo-portal/src/main/resources/static/scripts/services/AccessKeyService.js index 35a42c0fe0a..aca9c393735 100644 --- a/apollo-portal/src/main/resources/static/scripts/services/AccessKeyService.js +++ b/apollo-portal/src/main/resources/static/scripts/services/AccessKeyService.js @@ -31,7 +31,7 @@ appService.service('AccessKeyService', ['$resource', '$q', 'AppUtil', function ( }, enable_access_key: { method: 'PUT', - url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/accesskeys/:id/enable' + url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/accesskeys/:id/enable?mode=:mode' }, disable_access_key: { method: 'PUT', @@ -79,12 +79,13 @@ appService.service('AccessKeyService', ['$resource', '$q', 'AppUtil', function ( }); return d.promise; }, - enable_access_key: function (appId, env, id) { + enable_access_key: function (appId, env, id, mode) { var d = $q.defer(); access_key_resource.enable_access_key({ appId: appId, env: env, - id: id + id: id, + mode: mode }, {}, function (result) { d.resolve(result); diff --git a/apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js b/apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js new file mode 100644 index 00000000000..52a345449b7 --- /dev/null +++ b/apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +appService.service('GlobalSearchValueService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) { + let global_search_resource = $resource('', {}, { + get_item_Info_by_key_and_Value: { + isArray: false, + method: 'GET', + url: AppUtil.prefixPath() + '/global-search/item-info/by-key-or-value', + params: { + key: 'key', + value: 'value' + } + } + }); + return { + findItemInfoByKeyAndValue:function (key,value){ + let d = $q.defer(); + global_search_resource.get_item_Info_by_key_and_Value({key: key,value: value},function (result) { + d.resolve(result); + }, function (error) { + d.reject(error); + }); + return d.promise; + } + } +}]); diff --git a/apollo-portal/src/main/resources/static/views/common/footer.html b/apollo-portal/src/main/resources/static/views/common/footer.html index aa316cf251b..69c32b97050 100644 --- a/apollo-portal/src/main/resources/static/views/common/footer.html +++ b/apollo-portal/src/main/resources/static/views/common/footer.html @@ -20,7 +20,7 @@ Copyright 2024 Apollo Authors - github + GitHub

diff --git a/apollo-portal/src/main/resources/static/views/common/nav.html b/apollo-portal/src/main/resources/static/views/common/nav.html index c2f73b213dd..a78f2b68dd0 100644 --- a/apollo-portal/src/main/resources/static/views/common/nav.html +++ b/apollo-portal/src/main/resources/static/views/common/nav.html @@ -66,6 +66,7 @@
  • {{'Common.Nav.SystemInfo' | translate }}
  • {{'Common.Nav.ConfigExport' | translate }}
  • {{'ApolloAuditLog.Title' | translate }}
  • +
  • {{'Global.Title' | translate }}
  • diff --git a/apollo-portal/src/main/resources/static/views/component/namespace-panel-master-tab.html b/apollo-portal/src/main/resources/static/views/component/namespace-panel-master-tab.html index 6b8fa17f47d..c93bd8f90a4 100644 --- a/apollo-portal/src/main/resources/static/views/component/namespace-panel-master-tab.html +++ b/apollo-portal/src/main/resources/static/views/component/namespace-panel-master-tab.html @@ -624,7 +624,7 @@

    {{'Component.Namespace.Master.Items.Body.Link.NoCoverLinkItem' | translate } + ng-if="config.item.key && ((!config.isModified && !config.isDeleted) || config.oldValue)"> @@ -643,9 +643,11 @@
    {{'Component.Namespace.Master.Items.Body.Link.NoCoverLinkItem' | translate } - - + ng-click="(config.isModified || config.isDeleted) ? showText(config.oldValue) : showText(config.item.value)"> + + + + diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolverTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolverTest.java index 4c8664f7b47..ed18a70c448 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolverTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/txtresolver/PropertyResolverTest.java @@ -91,9 +91,9 @@ public void testDeleteItem() { @Test public void testDeleteCommentItem() { ItemChangeSets changeSets = resolver.resolve(1, "a=b\n\nb=c", mockBaseItemWith2Key1Comment1Blank()); - Assert.assertEquals(2, changeSets.getDeleteItems().size()); - Assert.assertEquals(2, changeSets.getUpdateItems().size()); - Assert.assertEquals(1, changeSets.getCreateItems().size()); + Assert.assertEquals(1, changeSets.getDeleteItems().size()); + Assert.assertEquals(3, changeSets.getUpdateItems().size()); + Assert.assertEquals(0, changeSets.getCreateItems().size()); } @Test @@ -120,17 +120,17 @@ public void testUpdateCommentItem() { + "a=b\n" +"\n" + "b=c", mockBaseItemWith2Key1Comment1Blank()); - Assert.assertEquals(1, changeSets.getDeleteItems().size()); - Assert.assertEquals(0, changeSets.getUpdateItems().size()); - Assert.assertEquals(1, changeSets.getCreateItems().size()); + Assert.assertEquals(0, changeSets.getDeleteItems().size()); + Assert.assertEquals(1, changeSets.getUpdateItems().size()); + Assert.assertEquals(0, changeSets.getCreateItems().size()); } @Test public void testAllSituation(){ ItemChangeSets changeSets = resolver.resolve(1, "#ww\nd=e\nb=c\na=b\n\nq=w\n#eee", mockBaseItemWith2Key1Comment1Blank()); - Assert.assertEquals(2, changeSets.getDeleteItems().size()); - Assert.assertEquals(2, changeSets.getUpdateItems().size()); - Assert.assertEquals(5, changeSets.getCreateItems().size()); + Assert.assertEquals(0, changeSets.getDeleteItems().size()); + Assert.assertEquals(4, changeSets.getUpdateItems().size()); + Assert.assertEquals(3, changeSets.getCreateItems().size()); } /** diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java new file mode 100644 index 00000000000..03231dcc59e --- /dev/null +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.portal.controller; + +/** + * @author hujiyuan 2024-08-10 + */ + +import com.ctrip.framework.apollo.common.http.SearchResponseEntity; +import com.ctrip.framework.apollo.portal.component.config.PortalConfig; +import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; +import com.ctrip.framework.apollo.portal.service.GlobalSearchService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.*; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(MockitoJUnitRunner.class) +public class GlobalSearchControllerTest { + + private MockMvc mockMvc; + + @Mock + private PortalConfig portalConfig; + + @Mock + private GlobalSearchService globalSearchService; + + @InjectMocks + private GlobalSearchController globalSearchController; + + private final int perEnvSearchMaxResults = 200; + + @Before + public void setUp() { + when(portalConfig.getPerEnvSearchMaxResults()).thenReturn(perEnvSearchMaxResults); + mockMvc = MockMvcBuilders.standaloneSetup(globalSearchController).build(); + } + + @Test + public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnEmptyItemInfos() throws Exception { + when(globalSearchService.getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(new ArrayList<>())); + mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value") + .contentType(MediaType.APPLICATION_JSON) + .param("key", "query-key") + .param("value", "query-value")) + .andExpect(status().isOk()) + .andExpect(content().json("{\"body\":[],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}")); + verify(portalConfig,times(1)).getPerEnvSearchMaxResults(); + verify(globalSearchService,times(1)).getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults)); + } + + @Test + public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos_ButOverPerEnvLimit() throws Exception { + List allEnvMockItemInfos = new ArrayList<>(); + allEnvMockItemInfos.add(new ItemInfo("appid1","env1","cluster1","namespace1","query-key","query-value")); + allEnvMockItemInfos.add(new ItemInfo("appid2","env2","cluster2","namespace2","query-key","query-value")); + when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.okWithMessage(allEnvMockItemInfos,"In DEV , PRO , more than "+perEnvSearchMaxResults+" items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.")); + mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value") + .contentType(MediaType.APPLICATION_JSON) + .param("key", "query-key") + .param("value", "query-value")) + .andExpect(status().isOk()) + .andExpect(content().json("{\"body\":[" + + " { \"appId\": \"appid1\",\n" + + " \"envName\": \"env1\",\n" + + " \"clusterName\": \"cluster1\",\n" + + " \"namespaceName\": \"namespace1\",\n" + + " \"key\": \"query-key\",\n" + + " \"value\": \"query-value\"}," + + " { \"appId\": \"appid2\",\n" + + " \"envName\": \"env2\",\n" + + " \"clusterName\": \"cluster2\",\n" + + " \"namespaceName\": \"namespace2\",\n" + + " \"key\": \"query-key\",\n" + + " \"value\": \"query-value\"}],\"hasMoreData\":true,\"message\":\"In DEV , PRO , more than 200 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.\",\"code\":200}")); + verify(portalConfig,times(1)).getPerEnvSearchMaxResults(); + verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults)); + } + + @Test + public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos() throws Exception { + List allEnvMockItemInfos = new ArrayList<>(); + allEnvMockItemInfos.add(new ItemInfo("appid1","env1","cluster1","namespace1","query-key","query-value")); + allEnvMockItemInfos.add(new ItemInfo("appid2","env2","cluster2","namespace2","query-key","query-value")); + when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(allEnvMockItemInfos)); + + mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value") + .contentType(MediaType.APPLICATION_JSON) + .param("key", "query-key") + .param("value", "query-value")) + .andExpect(status().isOk()) + .andExpect(content().json("{\"body\":[" + + " { \"appId\": \"appid1\",\n" + + " \"envName\": \"env1\",\n" + + " \"clusterName\": \"cluster1\",\n" + + " \"namespaceName\": \"namespace1\",\n" + + " \"key\": \"query-key\",\n" + + " \"value\": \"query-value\"}," + + " { \"appId\": \"appid2\",\n" + + " \"envName\": \"env2\",\n" + + " \"clusterName\": \"cluster2\",\n" + + " \"namespaceName\": \"namespace2\",\n" + + " \"key\": \"query-key\",\n" + + " \"value\": \"query-value\"}],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}")); + verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults)); + } + +} diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java new file mode 100644 index 00000000000..661a692447d --- /dev/null +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2024 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.portal.service; + +/** + * @author hujiyuan 2024-08-10 + */ + +import com.ctrip.framework.apollo.common.dto.ItemInfoDTO; +import com.ctrip.framework.apollo.common.dto.PageDTO; +import com.ctrip.framework.apollo.common.http.SearchResponseEntity; +import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; +import com.ctrip.framework.apollo.portal.component.PortalSettings; +import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo; +import com.ctrip.framework.apollo.portal.environment.Env; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.domain.PageRequest; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GlobalSearchServiceTest { + + @Mock + private AdminServiceAPI.ItemAPI itemAPI; + + @Mock + private PortalSettings portalSettings; + + @InjectMocks + private GlobalSearchService globalSearchService; + + private final List activeEnvs = new ArrayList<>(); + + @Before + public void setUp() { + when(portalSettings.getActiveEnvs()).thenReturn(activeEnvs); + } + + @Test + public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos() { + activeEnvs.add(Env.DEV); + activeEnvs.add(Env.PRO); + + ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp","TestCluster","TestNamespace","TestKey","TestValue"); + List mockItemInfoDTOs = new ArrayList<>(); + mockItemInfoDTOs.add(itemInfoDTO); + Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 1L)); + SearchResponseEntity> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1); + assertEquals(2, mockItemInfos.getBody().size()); + + List devMockItemInfos = new ArrayList<>(); + List proMockItemInfos = new ArrayList<>(); + List allEnvMockItemInfos = new ArrayList<>(); + devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); + proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); + allEnvMockItemInfos.addAll(devMockItemInfos); + allEnvMockItemInfos.addAll(proMockItemInfos); + + verify(itemAPI,times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1)); + verify(portalSettings,times(1)).getActiveEnvs(); + assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString()); + } + + @Test + public void testGet_PerEnv_ItemInfo_withKeyAndValue_BySearch_ReturnEmptyItemInfos() { + activeEnvs.add(Env.DEV); + activeEnvs.add(Env.PRO); + Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), anyString(), anyString(), eq(0), eq(1))) + .thenReturn(new PageDTO<>(new ArrayList<>(), PageRequest.of(0, 1), 0L)); + SearchResponseEntity> result = globalSearchService.getAllEnvItemInfoBySearch("NonExistentKey", "NonExistentValue", 0, 1); + assertEquals(0, result.getBody().size()); + } + + @Test + public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos_ButOverPerEnvLimit() { + activeEnvs.add(Env.DEV); + activeEnvs.add(Env.PRO); + + ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp","TestCluster","TestNamespace","TestKey","TestValue"); + List mockItemInfoDTOs = new ArrayList<>(); + mockItemInfoDTOs.add(itemInfoDTO); + Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 2L)); + SearchResponseEntity> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1); + assertEquals(2, mockItemInfos.getBody().size()); + + List devMockItemInfos = new ArrayList<>(); + List proMockItemInfos = new ArrayList<>(); + List allEnvMockItemInfos = new ArrayList<>(); + devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); + proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue")); + allEnvMockItemInfos.addAll(devMockItemInfos); + allEnvMockItemInfos.addAll(proMockItemInfos); + String message = "In DEV , PRO , more than 1 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope."; + verify(itemAPI,times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1)); + verify(portalSettings,times(1)).getActiveEnvs(); + assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString()); + assertEquals(message, mockItemInfos.getMessage()); + } + +} diff --git a/changes/changes-2.4.0.md b/changes/changes-2.4.0.md new file mode 100644 index 00000000000..3ebe2e98381 --- /dev/null +++ b/changes/changes-2.4.0.md @@ -0,0 +1,14 @@ +Changes by Version +================== +Release Notes. + +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 support the observe status access-key for pre-check and logging only](https://github.com/apolloconfig/apollo/pull/5236) + +------------------ +All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) 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/README.md b/docs/en/README.md index 46fb251756f..d76c4540aa2 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -37,6 +37,10 @@ Demo Environment: * **Grayscale release** * Support grayscale configuration release, for example, after clicking release, it will only take effect for some application instances. After a period of observation, we could push the configurations to all application instances if there is no problem +- **Global Search Configuration Items** + - A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used + - It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations + * **Authorization management, release approval and operation audit** * Great authorization mechanism is designed for applications and configurations management, and the management of configurations is divided into two operations: editing and publishing, therefore greatly reducing human errors * All operations have audit logs for easy tracking of problems diff --git a/docs/en/client/java-sdk-user-guide.md b/docs/en/client/java-sdk-user-guide.md index fb65ba35241..3b61cb62bf3 100644 --- a/docs/en/client/java-sdk-user-guide.md +++ b/docs/en/client/java-sdk-user-guide.md @@ -423,8 +423,6 @@ Starting from version 2.4.0, the observability of the client has been enhanced. `apollo.client.monitor.jmx.enabled`: Exposes Monitor data in JMX format. If enabled, tools like J-console and Jprofiler can be used to view the relevant information. The default is false. -![Monitor Configuration](https://cdn.jsdelivr.net/gh/Rawven/image@main/2024-08-24-14-59-01-image.png) - `apollo.client.monitor.exception-queue-size`: Sets the maximum number of exceptions that the Monitor can store. The default value is 25. `apollo.client.monitor.external.type`: **Non-standard configuration item**, used to activate the corresponding monitoring system's Exporter when exporting metric data. For example, if the apollo-plugin-client-prometheus is introduced, "prometheus" can be specified to enable it. The values available for configuration depend on the MetricsExporter SPI introduced (official or custom implementations). This design allows for easy extensibility. If multiple, incorrect, or no values are set, no Exporter will be enabled. @@ -570,7 +568,7 @@ apollo: After starting the application, you can view it using J-console or J-profiler; here, we use J-profiler as an example. -![](https://raw.githubusercontent.com/Rawven/image/main/20240828003803.png) +![](https://raw.githubusercontent.com/Rawven/image/main/20241020224657.png) #### 3.1.5.2 Exporting Metrics via Prometheus @@ -612,18 +610,31 @@ public class TestController { After starting the application, let Prometheus listen to this interface, and you will see request logs with a similar format. ``` +# TYPE apollo_client_thread_pool_active_task_count gauge +# HELP apollo_client_thread_pool_active_task_count apollo gauge metrics +apollo_client_thread_pool_active_task_count{thread_pool_name="RemoteConfigRepository"} 0.0 +apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfig"} 0.0 +# TYPE apollo_client_namespace_timeout gauge +# HELP apollo_client_namespace_timeout apollo gauge metrics +apollo_client_namespace_timeout 0.0 +# TYPE apollo_client_thread_pool_pool_size gauge +# HELP apollo_client_thread_pool_pool_size apollo gauge metrics +apollo_client_thread_pool_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 +apollo_client_thread_pool_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_remaining_capacity gauge # HELP apollo_client_thread_pool_queue_remaining_capacity apollo gauge metrics apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_thread_pool_core_pool_size gauge -# HELP apollo_client_thread_pool_core_pool_size apollo gauge metrics -apollo_client_thread_pool_core_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 -apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 -apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfig"} 0.0 +# TYPE apollo_client_exception_num counter +# HELP apollo_client_exception_num apollo counter metrics +apollo_client_exception_num_total 1404.0 +apollo_client_exception_num_created 1.729435502796E9 # TYPE apollo_client_thread_pool_largest_pool_size gauge # HELP apollo_client_thread_pool_largest_pool_size apollo gauge metrics apollo_client_thread_pool_largest_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 @@ -632,60 +643,47 @@ apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfigFile apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_size gauge # HELP apollo_client_thread_pool_queue_size apollo gauge metrics -apollo_client_thread_pool_queue_size{thread_pool_name="RemoteConfigRepository"} 2.0 +apollo_client_thread_pool_queue_size{thread_pool_name="RemoteConfigRepository"} 352.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_thread_pool_pool_size gauge -# HELP apollo_client_thread_pool_pool_size apollo gauge metrics -apollo_client_thread_pool_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 -apollo_client_thread_pool_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 -apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_namespace_item_num gauge -# HELP apollo_client_namespace_item_num apollo gauge metrics -apollo_client_namespace_item_num{namespace="application"} 8.0 -apollo_client_namespace_item_num{namespace="application1"} 2.0 -# TYPE apollo_client_thread_pool_completed_task_count gauge -# HELP apollo_client_thread_pool_completed_task_count apollo gauge metrics -apollo_client_thread_pool_completed_task_count{thread_pool_name="RemoteConfigRepository"} 2.0 -apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 0.0 -apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfig"} 0.0 +# TYPE apollo_client_namespace_usage counter +# HELP apollo_client_namespace_usage apollo counter metrics +apollo_client_namespace_usage_total{namespace="application"} 11.0 +apollo_client_namespace_usage_created{namespace="application"} 1.729435502791E9 +# TYPE apollo_client_thread_pool_core_pool_size gauge +# HELP apollo_client_thread_pool_core_pool_size apollo gauge metrics +apollo_client_thread_pool_core_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 +apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_not_found gauge # HELP apollo_client_namespace_not_found apollo gauge metrics -apollo_client_namespace_not_found 0.0 +apollo_client_namespace_not_found 351.0 # TYPE apollo_client_thread_pool_total_task_count gauge # HELP apollo_client_thread_pool_total_task_count apollo gauge metrics -apollo_client_thread_pool_total_task_count{thread_pool_name="RemoteConfigRepository"} 4.0 -apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_total_task_count{thread_pool_name="RemoteConfigRepository"} 353.0 +apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 4.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_namespace_usage counter -# HELP apollo_client_namespace_usage apollo counter metrics -apollo_client_namespace_usage_total{namespace="application"} 1.0 -apollo_client_namespace_usage_created{namespace="application"} 1.725899226271E9 -apollo_client_namespace_usage_total{namespace="application1"} 1.0 -apollo_client_namespace_usage_created{namespace="application1"} 1.72589922627E9 +# TYPE apollo_client_namespace_first_load_time_spend_in_ms gauge +# HELP apollo_client_namespace_first_load_time_spend_in_ms apollo gauge metrics +apollo_client_namespace_first_load_time_spend_in_ms{namespace="application"} 108.0 # TYPE apollo_client_thread_pool_maximum_pool_size gauge # HELP apollo_client_thread_pool_maximum_pool_size apollo gauge metrics apollo_client_thread_pool_maximum_pool_size{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfigFile"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfig"} 2.147483647E9 -# TYPE apollo_client_namespace_first_load_time_spend_in_ms gauge -# HELP apollo_client_namespace_first_load_time_spend_in_ms apollo gauge metrics -apollo_client_namespace_first_load_time_spend_in_ms{namespace="application"} 99.0 -apollo_client_namespace_first_load_time_spend_in_ms{namespace="application1"} 40.0 -# TYPE apollo_client_thread_pool_active_task_count gauge -# HELP apollo_client_thread_pool_active_task_count apollo gauge metrics -apollo_client_thread_pool_active_task_count{thread_pool_name="RemoteConfigRepository"} 0.0 -apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 -apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_namespace_timeout gauge -# HELP apollo_client_namespace_timeout apollo gauge metrics -apollo_client_namespace_timeout 0.0 +# TYPE apollo_client_namespace_item_num gauge +# HELP apollo_client_namespace_item_num apollo gauge metrics +apollo_client_namespace_item_num{namespace="application"} 9.0 +# TYPE apollo_client_thread_pool_completed_task_count gauge +# HELP apollo_client_thread_pool_completed_task_count apollo gauge metrics +apollo_client_thread_pool_completed_task_count{thread_pool_name="RemoteConfigRepository"} 1.0 +apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 3.0 +apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfig"} 0.0 # EOF ``` diff --git a/docs/en/deployment/distributed-deployment-guide.md b/docs/en/deployment/distributed-deployment-guide.md index 3251d4c5d5e..4dbe8dd7586 100644 --- a/docs/en/deployment/distributed-deployment-guide.md +++ b/docs/en/deployment/distributed-deployment-guide.md @@ -769,9 +769,9 @@ apollo.service.registry.cluster=same name with apollo Cluster ``` 2. (optional) If you want to customize Config Service and Admin Service's uri for Client, -for example when deploying on the intranet, -if you don't want to expose the intranet ip, -you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package + for example when deploying on the intranet, + if you don't want to expose the intranet ip, + you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package ```properties apollo.service.registry.uri=http://your-ip-or-domain:${server.port}/ ``` @@ -1447,6 +1447,14 @@ The default is true, which makes it easy to quickly search for configurations by If set to false, this feature is disabled +### 3.1.14 apollo.portal.search.perEnvMaxResults - set the Administrator Tool-Global Search for Value function's maximum number of search results for a single individual environment + +> For versions 2.4.0 and above + +Default is 200, which means that each environment will return up to 200 results in a single search operation. + +Modifying this parameter may affect the performance of the search function, so before modifying it, you should conduct sufficient testing and adjust the value of `apollo.portal.search.perEnvMaxResults` appropriately according to the actual business requirements and system resources to balance the performance and the number of search results. + ## 3.2 Adjusting ApolloConfigDB configuration Configuration items are uniformly stored in the ApolloConfigDB.ServerConfig table. It should be noted that each environment's ApolloConfigDB.ServerConfig needs to be configured separately, and the modification takes effect in real time for one minute afterwards. diff --git a/docs/en/design/apollo-design.md b/docs/en/design/apollo-design.md index f078c1881d2..0061f696f20 100644 --- a/docs/en/design/apollo-design.md +++ b/docs/en/design/apollo-design.md @@ -130,7 +130,7 @@ Why do we use Eureka as a service registry instead of the traditional zk and etc ### 1.3.2 Admin Service * Provide configuration management interface -* Provides interfaces for configuration modification, publishing, etc. +* Provides interfaces for configuration modification, publishing, retrieval, etc. * Interface service object is Portal ### 1.3.3 Meta Server diff --git a/docs/en/design/apollo-introduction.md b/docs/en/design/apollo-introduction.md index a001f5a3a81..f846e6be867 100644 --- a/docs/en/design/apollo-introduction.md +++ b/docs/en/design/apollo-introduction.md @@ -78,7 +78,13 @@ It is precisely based on the particularity of configuration that Apollo has been * **Client configuration information monitoring** * You can easily see which instances the configuration is being used on the interface +**Global Search Configuration Items** + +- A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used +- It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations + **Java and .Net native clients available** + * Provides native clients of Java and .Net for easy application integration * Support Spring Placeholder, Annotation and Spring Boot's ConfigurationProperties for easy application use (requires Spring 3.1.1+) * Also provides Http interface, non-Java and .Net applications can also be easily used diff --git a/docs/en/images/Configuration query-Non properties.png b/docs/en/images/Configuration query-Non properties.png new file mode 100644 index 00000000000..1a355074cbc Binary files /dev/null and b/docs/en/images/Configuration query-Non properties.png differ diff --git a/docs/en/images/Configuration query-properties.png b/docs/en/images/Configuration query-properties.png new file mode 100644 index 00000000000..2f8fd771109 Binary files /dev/null and b/docs/en/images/Configuration query-properties.png differ diff --git a/docs/en/images/System-parameterization-of-global-search-configuration-items.png b/docs/en/images/System-parameterization-of-global-search-configuration-items.png new file mode 100644 index 00000000000..4f47b58cd7b Binary files /dev/null and b/docs/en/images/System-parameterization-of-global-search-configuration-items.png differ diff --git a/docs/en/portal/apollo-user-guide.md b/docs/en/portal/apollo-user-guide.md index cb84a8e6faa..d084ab5a0aa 100644 --- a/docs/en/portal/apollo-user-guide.md +++ b/docs/en/portal/apollo-user-guide.md @@ -133,6 +133,20 @@ The rollback mechanism here is similar to the release system, where the rollback The rollback in Apollo is a similar mechanism. Clicking rollback rolls back the configuration published to the client to the previous published version, which means that the configuration read by the client will be restored to the previous version, but the configuration in the edited state on the page will not be rolled back, so that the developer can re-publish after fixing the configuration. +## 1.7 Configuration queries (administrator privileges) + +After a configuration has been added or modified, the administrator user can make a query for the configuration item it belongs to as well as jump to modifications by going to the `Administrator Tools - Global Search for Value` page. + +The query here is a fuzzy search, where at least one of the key and value of the configuration item is searched to find out in which application, environment, cluster, namespace the configuration is used. + +- Properties format configuration can be retrieved directly from the key and value + +![Configuration query-properties](../images/Configuration query-properties.png) + +- xml, json, yml, yaml, txt and other formats configuration, because the storage of content-value storage, so you can key = content, value = configuration item content, retrieval + +![Configuration query-Non properties](../images/Configuration query-Non properties.png) + # II. Public component access guide ## 2.1 Difference between public components and common applications @@ -482,6 +496,42 @@ Apollo has added an access key mechanism since version 1.6.0, so that only authe 3. Client-side [configure access key](en/client/java-sdk-user-guide?id=_1244-configuring-access-keys) . +## 6.3 System parameterization of global search configuration items + +Starting from version 2.4.0, apollo-portal adds the ability to globally search for configuration items by fuzzy retrieval of the key and value of a configuration item to find out which application, environment, cluster, or namespace the configuration item with the corresponding value is used in. In order to prevent memory overflow (OOM) problems when performing global view searches of configuration items, we introduce a system parameter `apollo.portal.search.perEnvMaxResults`, which is used to limit the number of maximum search results per environment configuration item in a single search. By default, this value is set to `200`, but administrators can adjust it to suit their actual needs. + +**Setting method:** + +1. Log in to the Apollo Configuration Center interface with a super administrator account. +2. Just go to the `Administrator Tools - System Parameters` page and add or modify the `apollo.portal.search.perEnvMaxResults` configuration item. + +Please note that modifications to system parameters may affect the performance of the search function, so you should perform adequate testing and ensure that you understand exactly what the parameters do before making changes. + +![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/README.md b/docs/zh/README.md index e54c216f0a3..09c04f85647 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -40,6 +40,10 @@ Java客户端不依赖任何框架,能够运行于所有Java运行时环境, * **灰度发布** * 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。 +- **配置项的全局视角搜索** + - 通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用。 + - 通过高亮显示、分页与跳转配置等操作,便于让管理员以及SRE角色快速、便捷地找到与更改资源的配置值。 + * **权限管理、发布审核、操作审计** * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。 * 所有的操作都有审计日志,可以方便的追踪问题。 diff --git a/docs/zh/client/java-sdk-user-guide.md b/docs/zh/client/java-sdk-user-guide.md index 4abd1b7d612..dd904cf0e32 100644 --- a/docs/zh/client/java-sdk-user-guide.md +++ b/docs/zh/client/java-sdk-user-guide.md @@ -408,8 +408,6 @@ apollo.label=YOUR-APOLLO-LABEL `apollo.client.monitor.jmx.enabled`:是否将Monitor数据以Jmx形式暴露,开启后可以通过J-console,Jprofiler等工具查看相关信息,默认为false -![](https://cdn.jsdelivr.net/gh/Rawven/image@main/2024-08-24-14-59-01-image.png) - `apollo.client.monitor.exception-queue-size`:设置Monitor存储Exception的最大数量,默认值为25 `apollo.client.monitor.external.type`:**非常规配置项**,用于导出指标数据时启用对应监控系统的Exporter,如引入apollo-plugin-client-prometheus则可填写prometheus进行启用,可填配置取决于用户引入的MetricsExporter的SPI使可用官方提供的或自己实现),这种设计是为了用户能更方便的扩展。多填,错填和不填则不启用任何Exporter。 @@ -539,7 +537,7 @@ apollo: 启动应用后,开启J-console或J-profiler即可查看,这里用J-profiler做例子 -![](https://raw.githubusercontent.com/Rawven/image/main/20240828003803.png) +![](https://raw.githubusercontent.com/Rawven/image/main/20241020224657.png) #### 3.1.5.2 以Prometheus形式导出指标 引入提供的官方依赖包 @@ -580,18 +578,31 @@ public class TestController { 启动应用后让Prometheus监听该接口,打印请求日志即可发现如下类似格式信息 ``` +# TYPE apollo_client_thread_pool_active_task_count gauge +# HELP apollo_client_thread_pool_active_task_count apollo gauge metrics +apollo_client_thread_pool_active_task_count{thread_pool_name="RemoteConfigRepository"} 0.0 +apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfig"} 0.0 +# TYPE apollo_client_namespace_timeout gauge +# HELP apollo_client_namespace_timeout apollo gauge metrics +apollo_client_namespace_timeout 0.0 +# TYPE apollo_client_thread_pool_pool_size gauge +# HELP apollo_client_thread_pool_pool_size apollo gauge metrics +apollo_client_thread_pool_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 +apollo_client_thread_pool_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_remaining_capacity gauge # HELP apollo_client_thread_pool_queue_remaining_capacity apollo gauge metrics apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_remaining_capacity{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_thread_pool_core_pool_size gauge -# HELP apollo_client_thread_pool_core_pool_size apollo gauge metrics -apollo_client_thread_pool_core_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 -apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 -apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfig"} 0.0 +# TYPE apollo_client_exception_num counter +# HELP apollo_client_exception_num apollo counter metrics +apollo_client_exception_num_total 1404.0 +apollo_client_exception_num_created 1.729435502796E9 # TYPE apollo_client_thread_pool_largest_pool_size gauge # HELP apollo_client_thread_pool_largest_pool_size apollo gauge metrics apollo_client_thread_pool_largest_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 @@ -600,60 +611,47 @@ apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfigFile apollo_client_thread_pool_largest_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_thread_pool_queue_size gauge # HELP apollo_client_thread_pool_queue_size apollo gauge metrics -apollo_client_thread_pool_queue_size{thread_pool_name="RemoteConfigRepository"} 2.0 +apollo_client_thread_pool_queue_size{thread_pool_name="RemoteConfigRepository"} 352.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_queue_size{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_thread_pool_pool_size gauge -# HELP apollo_client_thread_pool_pool_size apollo gauge metrics -apollo_client_thread_pool_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 -apollo_client_thread_pool_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 -apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_pool_size{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_namespace_item_num gauge -# HELP apollo_client_namespace_item_num apollo gauge metrics -apollo_client_namespace_item_num{namespace="application"} 8.0 -apollo_client_namespace_item_num{namespace="application1"} 2.0 -# TYPE apollo_client_thread_pool_completed_task_count gauge -# HELP apollo_client_thread_pool_completed_task_count apollo gauge metrics -apollo_client_thread_pool_completed_task_count{thread_pool_name="RemoteConfigRepository"} 2.0 -apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 0.0 -apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfig"} 0.0 +# TYPE apollo_client_namespace_usage counter +# HELP apollo_client_namespace_usage apollo counter metrics +apollo_client_namespace_usage_total{namespace="application"} 11.0 +apollo_client_namespace_usage_created{namespace="application"} 1.729435502791E9 +# TYPE apollo_client_thread_pool_core_pool_size gauge +# HELP apollo_client_thread_pool_core_pool_size apollo gauge metrics +apollo_client_thread_pool_core_pool_size{thread_pool_name="RemoteConfigRepository"} 1.0 +apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_core_pool_size{thread_pool_name="AbstractConfig"} 0.0 # TYPE apollo_client_namespace_not_found gauge # HELP apollo_client_namespace_not_found apollo gauge metrics -apollo_client_namespace_not_found 0.0 +apollo_client_namespace_not_found 351.0 # TYPE apollo_client_thread_pool_total_task_count gauge # HELP apollo_client_thread_pool_total_task_count apollo gauge metrics -apollo_client_thread_pool_total_task_count{thread_pool_name="RemoteConfigRepository"} 4.0 -apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 +apollo_client_thread_pool_total_task_count{thread_pool_name="RemoteConfigRepository"} 353.0 +apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 4.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfigFile"} 0.0 apollo_client_thread_pool_total_task_count{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_namespace_usage counter -# HELP apollo_client_namespace_usage apollo counter metrics -apollo_client_namespace_usage_total{namespace="application"} 1.0 -apollo_client_namespace_usage_created{namespace="application"} 1.725899226271E9 -apollo_client_namespace_usage_total{namespace="application1"} 1.0 -apollo_client_namespace_usage_created{namespace="application1"} 1.72589922627E9 +# TYPE apollo_client_namespace_first_load_time_spend_in_ms gauge +# HELP apollo_client_namespace_first_load_time_spend_in_ms apollo gauge metrics +apollo_client_namespace_first_load_time_spend_in_ms{namespace="application"} 108.0 # TYPE apollo_client_thread_pool_maximum_pool_size gauge # HELP apollo_client_thread_pool_maximum_pool_size apollo gauge metrics apollo_client_thread_pool_maximum_pool_size{thread_pool_name="RemoteConfigRepository"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractApolloClientMetricsExporter"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfigFile"} 2.147483647E9 apollo_client_thread_pool_maximum_pool_size{thread_pool_name="AbstractConfig"} 2.147483647E9 -# TYPE apollo_client_namespace_first_load_time_spend_in_ms gauge -# HELP apollo_client_namespace_first_load_time_spend_in_ms apollo gauge metrics -apollo_client_namespace_first_load_time_spend_in_ms{namespace="application"} 99.0 -apollo_client_namespace_first_load_time_spend_in_ms{namespace="application1"} 40.0 -# TYPE apollo_client_thread_pool_active_task_count gauge -# HELP apollo_client_thread_pool_active_task_count apollo gauge metrics -apollo_client_thread_pool_active_task_count{thread_pool_name="RemoteConfigRepository"} 0.0 -apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 1.0 -apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfigFile"} 0.0 -apollo_client_thread_pool_active_task_count{thread_pool_name="AbstractConfig"} 0.0 -# TYPE apollo_client_namespace_timeout gauge -# HELP apollo_client_namespace_timeout apollo gauge metrics -apollo_client_namespace_timeout 0.0 +# TYPE apollo_client_namespace_item_num gauge +# HELP apollo_client_namespace_item_num apollo gauge metrics +apollo_client_namespace_item_num{namespace="application"} 9.0 +# TYPE apollo_client_thread_pool_completed_task_count gauge +# HELP apollo_client_thread_pool_completed_task_count apollo gauge metrics +apollo_client_thread_pool_completed_task_count{thread_pool_name="RemoteConfigRepository"} 1.0 +apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractApolloClientMetricsExporter"} 3.0 +apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfigFile"} 0.0 +apollo_client_thread_pool_completed_task_count{thread_pool_name="AbstractConfig"} 0.0 # EOF ``` diff --git a/docs/zh/deployment/distributed-deployment-guide.md b/docs/zh/deployment/distributed-deployment-guide.md index 4d21e1c80aa..f83d62ac692 100644 --- a/docs/zh/deployment/distributed-deployment-guide.md +++ b/docs/zh/deployment/distributed-deployment-guide.md @@ -1392,6 +1392,14 @@ portal上“帮助”链接的地址,默认是Apollo github的wiki首页,可 如果设置为 false,则关闭此功能 +### 3.1.14 apollo.portal.search.perEnvMaxResults - 设置管理员工具-value的全局搜索功能单次单独环境最大搜索结果的数量 + +> 适用于2.4.0及以上版本 + +默认为200,意味着每个环境在单次搜索操作中最多返回200条结果 + +修改该参数可能会影响搜索功能的性能,因此在修改之前应该进行充分的测试,根据实际业务需求和系统资源情况,适当调整`apollo.portal.search.perEnvMaxResults`的值,以平衡性能和搜索结果的数量 + ## 3.2 调整ApolloConfigDB配置 配置项统一存储在ApolloConfigDB.ServerConfig表中,需要注意每个环境的ApolloConfigDB.ServerConfig都需要单独配置,修改完一分钟实时生效。 diff --git a/docs/zh/design/apollo-design.md b/docs/zh/design/apollo-design.md index e2724800c5f..7cd1b49b2c0 100644 --- a/docs/zh/design/apollo-design.md +++ b/docs/zh/design/apollo-design.md @@ -136,7 +136,7 @@ sequenceDiagram ### 1.3.2 Admin Service * 提供配置管理接口 -* 提供配置修改、发布等接口 +* 提供配置修改、发布、检索等接口 * 接口服务对象为Portal ### 1.3.3 Meta Server diff --git a/docs/zh/design/apollo-introduction.md b/docs/zh/design/apollo-introduction.md index e05da35ec54..166af68c3a4 100644 --- a/docs/zh/design/apollo-introduction.md +++ b/docs/zh/design/apollo-introduction.md @@ -69,26 +69,30 @@ Apollo支持4个维度管理Key-Value格式的配置: * **灰度发布** * 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例 +* **配置项的全局视角搜索** + * 通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用 + * 通过高亮显示、分页与跳转配置等操作,便于让管理员以及SRE角色快速、便捷地找到与更改资源的配置值 + * **权限管理、发布审核、操作审计** - * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。 - * 所有的操作都有审计日志,可以方便地追踪问题 + * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。 + * 所有的操作都有审计日志,可以方便地追踪问题 * **客户端配置信息监控** - * 可以在界面上方便地看到配置在被哪些实例使用 + * 可以在界面上方便地看到配置在被哪些实例使用 * **提供Java和.Net原生客户端** - * 提供了Java和.Net的原生客户端,方便应用集成 - * 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+) - * 同时提供了Http接口,非Java和.Net应用也可以方便地使用 + * 提供了Java和.Net的原生客户端,方便应用集成 + * 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+) + * 同时提供了Http接口,非Java和.Net应用也可以方便地使用 * **提供开放平台API** - * Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等 - * 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制 + * Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等 + * 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制 * **部署简单** - * 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少 - * 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来 - * Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数 + * 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少 + * 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来 + * Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数 # 3、Apollo at a glance diff --git a/docs/zh/images/Configuration query-Non properties.png b/docs/zh/images/Configuration query-Non properties.png new file mode 100644 index 00000000000..1a355074cbc Binary files /dev/null and b/docs/zh/images/Configuration query-Non properties.png differ diff --git a/docs/zh/images/Configuration query-properties.png b/docs/zh/images/Configuration query-properties.png new file mode 100644 index 00000000000..2f8fd771109 Binary files /dev/null and b/docs/zh/images/Configuration query-properties.png differ diff --git a/docs/zh/images/System-parameterization-of-global-search-configuration-items.png b/docs/zh/images/System-parameterization-of-global-search-configuration-items.png new file mode 100644 index 00000000000..4f47b58cd7b Binary files /dev/null and b/docs/zh/images/System-parameterization-of-global-search-configuration-items.png differ diff --git a/docs/zh/portal/apollo-user-guide.md b/docs/zh/portal/apollo-user-guide.md index 28ec5cc0fbc..cb38986bb2b 100644 --- a/docs/zh/portal/apollo-user-guide.md +++ b/docs/zh/portal/apollo-user-guide.md @@ -123,6 +123,20 @@ Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文 Apollo中的回滚也是类似的机制,点击回滚后是将发布到客户端的配置回滚到上一个已发布版本,也就是说客户端读取到的配置会恢复到上一个版本,但页面上编辑状态的配置是不会回滚的,从而开发可以在修复配置后重新发布。 +## 1.7 配置查询(管理员权限) + +在配置添加或修改后,管理员用户可以通过进入 `管理员工具 - Value的全局搜索` 页面,来对配置项进行所属查询以及跳转修改。 + +这里的查询为模糊检索,通过对配置项的key与value至少一项进行检索,找到该配置在哪个应用、环境、集群、命名空间中被使用。 + +- properties格式配置可以直接通过对key与value进行检索 + +![Configuration query-properties](../images/Configuration query-properties.png) + +- xml、json、yml、yaml、txt等格式配置,由于存储时以content-value进行存储,故可以通过key=content、value=配置项内容,进行检索 + +![Configuration query-Non properties](../images/Configuration query-Non properties.png) + # 二、公共组件接入指南 ## 2.1 公共组件和普通应用的区别 @@ -226,9 +240,9 @@ Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文 3. 关联成功后,页面会自动跳转到Namespace权限管理页面 1. 分配修改权限 -![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) + ![namespace-permission-edit](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-permission-edit.png) 2. 分配发布权限 -![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) + ![namespace-publish-permission](https://cdn.jsdelivr.net/gh/apolloconfig/apollo@master/doc/images/namespace-publish-permission.png) 4. 点击“返回”回到项目页面 @@ -448,13 +462,46 @@ Apollo目前提供Java客户端,具体信息请点击[Java客户端使用文 Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验证的客户端才能访问敏感配置。如果应用开启了访问密钥,客户端需要配置密钥,否则无法获取配置。 1. 项目管理员打开管理密钥页面 -![管理密钥入口](https://user-images.githubusercontent.com/837658/94990081-f4d3cd80-05ab-11eb-9470-fed5ec6de92e.png) + ![管理密钥入口](https://user-images.githubusercontent.com/837658/94990081-f4d3cd80-05ab-11eb-9470-fed5ec6de92e.png) 2. 为项目的每个环境生成访问密钥,注意默认是禁用的,建议在客户端都配置完成后再开启 -![密钥配置页面](https://user-images.githubusercontent.com/837658/94990150-788dba00-05ac-11eb-9a12-727fdb872e42.png) + ![密钥配置页面](https://user-images.githubusercontent.com/837658/94990150-788dba00-05ac-11eb-9a12-727fdb872e42.png) 3. 客户端侧[配置访问密钥](zh/client/java-sdk-user-guide#_1244-配置访问密钥) +## 6.3 全局搜索配置项的系统参数设置 + +从2.4.0版本开始,apollo-portal增加了全局搜索配置项的功能,通过对配置项的key与value进行的模糊检索,找到拥有对应值的配置项在哪个应用、环境、集群、命名空间中被使用。为了防止在进行配置项的全局视角搜索时出现内存溢出(OOM)的问题,我们引入了一个系统参数`apollo.portal.search.perEnvMaxResults`。这个参数用于限制每个环境配置项单次最大搜索结果的数量。默认情况下,这个值被设置为`200`,但管理员可以根据实际需求进行调整。 + +**设置方法:** + +1. 用超级管理员账号登录到Apollo配置中心的界面 +2. 进入`管理员工具 - 系统参数`页面新增或修改`apollo.portal.search.perEnvMaxResults`配置项即可 + +请注意,修改系统参数可能会影响搜索功能的性能,因此在修改之前应该进行充分的测试,并确保理解参数的具体作用。 + +![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 安全相关 @@ -485,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`,增强安全性 diff --git a/pom.xml b/pom.xml index 7ffab97f930..465a1c01343 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ 5.9.2 1.4.0 - 8.0.32 + 8.2.0 42.7.2 3.3.0 diff --git a/scripts/sql/profiles/h2-default/apolloconfigdb.sql b/scripts/sql/profiles/h2-default/apolloconfigdb.sql index 8a24f1c82c9..9fb40f0c21f 100644 --- a/scripts/sql/profiles/h2-default/apolloconfigdb.sql +++ b/scripts/sql/profiles/h2-default/apolloconfigdb.sql @@ -393,6 +393,7 @@ CREATE TABLE `AccessKey` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Secret` varchar(128) NOT NULL DEFAULT '' COMMENT 'Secret', + `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer', `IsEnabled` boolean NOT NULL DEFAULT FALSE COMMENT '1: enabled, 0: disabled', `IsDeleted` boolean NOT NULL DEFAULT FALSE COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/profiles/h2-default/delta/v230-v240/apolloconfigdb-v230-v240.sql b/scripts/sql/profiles/h2-default/delta/v230-v240/apolloconfigdb-v230-v240.sql new file mode 100644 index 00000000000..459c12ff9b4 --- /dev/null +++ b/scripts/sql/profiles/h2-default/delta/v230-v240/apolloconfigdb-v230-v240.sql @@ -0,0 +1,43 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo config db from v2.3.0 to v2.4.0 + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== +-- + +-- H2 Function +-- ------------------------------------------------------------ +CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; + +-- + +ALTER TABLE `AccessKey` ADD COLUMN `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer' AFTER `Secret`; + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== diff --git a/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql b/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql index 4d2e0c19118..dd9b4dcae8e 100644 --- a/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql +++ b/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql @@ -404,6 +404,7 @@ CREATE TABLE `AccessKey` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Secret` varchar(128) NOT NULL DEFAULT '' COMMENT 'Secret', + `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer', `IsEnabled` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: enabled, 0: disabled', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/profiles/mysql-database-not-specified/delta/v230-v240/apolloconfigdb-v230-v240.sql b/scripts/sql/profiles/mysql-database-not-specified/delta/v230-v240/apolloconfigdb-v230-v240.sql new file mode 100644 index 00000000000..c71b792aa6e --- /dev/null +++ b/scripts/sql/profiles/mysql-database-not-specified/delta/v230-v240/apolloconfigdb-v230-v240.sql @@ -0,0 +1,39 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo config db from v2.3.0 to v2.4.0 + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== +-- +-- + +ALTER TABLE `AccessKey` + ADD COLUMN `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer' AFTER `Secret`; + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== diff --git a/scripts/sql/profiles/mysql-default/apolloconfigdb.sql b/scripts/sql/profiles/mysql-default/apolloconfigdb.sql index 3736ea599f3..9416af7380a 100644 --- a/scripts/sql/profiles/mysql-default/apolloconfigdb.sql +++ b/scripts/sql/profiles/mysql-default/apolloconfigdb.sql @@ -409,6 +409,7 @@ CREATE TABLE `AccessKey` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Secret` varchar(128) NOT NULL DEFAULT '' COMMENT 'Secret', + `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer', `IsEnabled` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: enabled, 0: disabled', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/profiles/mysql-default/delta/v230-v240/apolloconfigdb-v230-v240.sql b/scripts/sql/profiles/mysql-default/delta/v230-v240/apolloconfigdb-v230-v240.sql new file mode 100644 index 00000000000..8bb5fdc1a0e --- /dev/null +++ b/scripts/sql/profiles/mysql-default/delta/v230-v240/apolloconfigdb-v230-v240.sql @@ -0,0 +1,41 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo config db from v2.3.0 to v2.4.0 + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== +-- +-- +-- Use Database +Use ApolloConfigDB; + +ALTER TABLE `AccessKey` + ADD COLUMN `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer' AFTER `Secret`; + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== diff --git a/scripts/sql/src/apolloconfigdb.sql b/scripts/sql/src/apolloconfigdb.sql index 9f85a8791ce..216dfdd9a33 100644 --- a/scripts/sql/src/apolloconfigdb.sql +++ b/scripts/sql/src/apolloconfigdb.sql @@ -397,6 +397,7 @@ CREATE TABLE `AccessKey` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Secret` varchar(128) NOT NULL DEFAULT '' COMMENT 'Secret', + `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer', `IsEnabled` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: enabled, 0: disabled', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/src/delta/v230-v240/apolloconfigdb-v230-v240.sql b/scripts/sql/src/delta/v230-v240/apolloconfigdb-v230-v240.sql new file mode 100644 index 00000000000..2e3cddfbbea --- /dev/null +++ b/scripts/sql/src/delta/v230-v240/apolloconfigdb-v230-v240.sql @@ -0,0 +1,25 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo config db from v2.3.0 to v2.4.0 + +-- ${gists.autoGeneratedDeclaration} +-- ${gists.h2Function} +-- ${gists.useDatabase} + +ALTER TABLE `AccessKey` + ADD COLUMN `Mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '密钥模式,0: filter,1: observer' AFTER `Secret`; + +-- ${gists.autoGeneratedDeclaration}