From ad86f8b4dd351e320ab1b58a184399b7d2aee328 Mon Sep 17 00:00:00 2001 From: lepdou Date: Sun, 31 Oct 2021 17:14:29 +0800 Subject: [PATCH] support export/import configs --- CHANGES.md | 2 + .../controller/ItemController.java | 31 +- .../apollo/biz/service/ItemService.java | 21 + .../apollo/common/constants/GsonType.java | 2 + .../apollo/portal/api/AdminServiceAPI.java | 5 + .../controller/ConfigsExportController.java | 57 +- .../controller/ConfigsImportController.java | 53 +- .../portal/controller/InstanceController.java | 2 +- .../apollo/portal/entity/bo/ConfigBO.java | 16 +- .../entity/model/NamespaceTextModel.java | 9 + .../portal/service/AppNamespaceService.java | 30 ++ .../apollo/portal/service/AppService.java | 34 +- .../portal/service/ConfigsExportService.java | 317 +++++++---- .../portal/service/ConfigsImportService.java | 494 +++++++++++++++--- .../portal/service/InstanceService.java | 2 +- .../apollo/portal/service/ItemService.java | 18 +- .../portal/service/NamespaceService.java | 7 +- .../apollo/portal/util/ConfigFileUtils.java | 35 +- .../apollo/portal/util/NamespaceBOUtils.java | 52 +- .../src/main/resources/application.yml | 4 + .../src/main/resources/static/config.html | 3 + .../main/resources/static/config_export.html | 197 ++++--- .../src/main/resources/static/i18n/en.json | 27 +- .../src/main/resources/static/i18n/zh-CN.json | 27 +- .../src/main/resources/static/img/export.png | Bin 0 -> 4425 bytes .../src/main/resources/static/img/import.png | Bin 0 -> 4180 bytes .../controller/ConfigExportController.js | 109 ++++ .../import-namespace-modal-directive.js | 81 +++ .../directive/namespace-panel-directive.js | 12 + .../static/scripts/services/EventManager.js | 1 + .../static/scripts/services/ExportService.js | 42 ++ .../component/import-namespace-modal.html | 55 ++ .../component/namespace-panel-master-tab.html | 10 + .../service/ConfigsExportServiceTest.java | 258 +++++++++ .../portal/service/NamespaceServiceTest.java | 14 +- .../portal/util/ConfigFileUtilsTest.java | 2 +- 36 files changed, 1718 insertions(+), 311 deletions(-) create mode 100644 apollo-portal/src/main/resources/static/img/export.png create mode 100644 apollo-portal/src/main/resources/static/img/import.png create mode 100644 apollo-portal/src/main/resources/static/scripts/controller/ConfigExportController.js create mode 100644 apollo-portal/src/main/resources/static/scripts/directive/import-namespace-modal-directive.js create mode 100644 apollo-portal/src/main/resources/static/scripts/services/ExportService.js create mode 100644 apollo-portal/src/main/resources/static/views/component/import-namespace-modal.html create mode 100644 apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/ConfigsExportServiceTest.java diff --git a/CHANGES.md b/CHANGES.md index 468d756a1b1..9be795b1d8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,8 @@ Apollo 2.0.0 * [Bump version to 2.0.0 and drop java 1.7 support](https://github.com/apolloconfig/apollo/pull/4015) * [Optimize home page style](https://github.com/apolloconfig/apollo/pull/4052) * [Support Java 17](https://github.com/apolloconfig/apollo/pull/4060) +* [Support export/import configs by apollo env](https://github.com/apolloconfig/apollo/pull/3947) + ------------------ All issues and pull requests are [here](https://github.com/ctripcorp/apollo/milestone/8?closed=1) 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 c421eb17743..89ad4479d82 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 @@ -30,6 +30,8 @@ import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; +import com.ctrip.framework.apollo.core.utils.StringUtils; + import java.util.Collection; import java.util.Collections; import java.util.List; @@ -88,6 +90,33 @@ public ItemDTO create(@PathVariable("appId") String appId, return dto; } + @PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/comment_items") + public ItemDTO createComment(@PathVariable("appId") String appId, + @PathVariable("clusterName") String clusterName, + @PathVariable("namespaceName") String namespaceName, @RequestBody ItemDTO dto) { + if (!StringUtils.isBlank(dto.getKey()) || !StringUtils.isBlank(dto.getValue())) { + throw new BadRequestException("Comment item's key or value should be blank."); + } + if (StringUtils.isBlank(dto.getComment())) { + throw new BadRequestException("Comment item's comment should not be blank."); + } + + // check if comment existed + List allItems = itemService.findItemsWithOrdered(appId, clusterName, namespaceName); + for (Item item : allItems) { + if (StringUtils.isBlank(item.getKey()) && StringUtils.isBlank(item.getValue()) && + Objects.equals(item.getComment(), dto.getComment())) { + return BeanUtils.transform(ItemDTO.class, item); + } + } + + Item entity = BeanUtils.transform(Item.class, dto); + entity = itemService.saveComment(entity); + + return BeanUtils.transform(ItemDTO.class, entity); + } + + @PreAcquireNamespaceLock @PutMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{itemId}") public ItemDTO update(@PathVariable("appId") String appId, @@ -109,7 +138,7 @@ public ItemDTO update(@PathVariable("appId") String appId, Item entity = BeanUtils.transform(Item.class, itemDTO); ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder(); - + Item beforeUpdateItem = BeanUtils.transform(Item.class, managedEntity); //protect. only value,comment,lastModifiedBy can be modified diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemService.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemService.java index 39ade331f0c..8866864af22 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemService.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemService.java @@ -26,6 +26,7 @@ import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.core.utils.StringUtils; + import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -165,6 +166,26 @@ public Item save(Item entity) { return item; } + @Transactional + public Item saveComment(Item entity) { + entity.setKey(""); + entity.setValue(""); + entity.setId(0);//protection + + if (entity.getLineNum() == 0) { + Item lastItem = findLastOne(entity.getNamespaceId()); + int lineNum = lastItem == null ? 1 : lastItem.getLineNum() + 1; + entity.setLineNum(lineNum); + } + + Item item = itemRepository.save(entity); + + auditService.audit(Item.class.getSimpleName(), item.getId(), Audit.OP.INSERT, + item.getDataChangeCreatedBy()); + + return item; + } + @Transactional public Item update(Item item) { checkItemValueLength(item.getNamespaceId(), item.getValue()); diff --git a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/GsonType.java b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/GsonType.java index 3bbbc3b0b08..8526f0a0349 100644 --- a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/GsonType.java +++ b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/constants/GsonType.java @@ -19,6 +19,7 @@ import com.google.gson.reflect.TypeToken; import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO; +import com.ctrip.framework.apollo.common.dto.ItemDTO; import java.lang.reflect.Type; import java.util.List; @@ -30,4 +31,5 @@ public interface GsonType { Type RULE_ITEMS = new TypeToken>() {}.getType(); + Type ITEM_DTOS = new TypeToken>(){}.getType(); } 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 3409162a333..ebaa849e1fe 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 @@ -204,6 +204,11 @@ public ItemDTO createItem(String appId, Env env, String clusterName, String name item, ItemDTO.class, appId, clusterName, namespace); } + public ItemDTO createCommentItem(String appId, Env env, String clusterName, String namespace, ItemDTO item) { + return restTemplate.post(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/comment_items", + item, ItemDTO.class, appId, clusterName, namespace); + } + public void deleteItem(Env env, long itemId, String operator) { restTemplate.delete(env, "items/{itemId}?operator={operator}", itemId, operator); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsExportController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsExportController.java index 3cc73136440..110a895ceed 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsExportController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsExportController.java @@ -16,6 +16,9 @@ */ package com.ctrip.framework.apollo.portal.controller; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; + import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; @@ -23,14 +26,7 @@ import com.ctrip.framework.apollo.portal.service.ConfigsExportService; import com.ctrip.framework.apollo.portal.service.NamespaceService; import com.ctrip.framework.apollo.portal.util.NamespaceBOUtils; -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Date; -import java.util.List; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; + import org.apache.commons.lang.time.DateFormatUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,15 +35,26 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** * jian.tan */ @RestController public class ConfigsExportController { - private static final Logger logger = LoggerFactory.getLogger(ConfigsExportController.class); + private static final Logger logger = LoggerFactory.getLogger(ConfigsExportController.class); + private static final String ENV_SEPARATOR = ","; private final ConfigsExportService configsExportService; @@ -74,12 +81,17 @@ public ConfigsExportController( @PreAuthorize(value = "!@permissionValidator.shouldHideConfigToCurrentUser(#appId, #env, #namespaceName)") @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/export") public void exportItems(@PathVariable String appId, @PathVariable String env, - @PathVariable String clusterName, @PathVariable String namespaceName, - HttpServletResponse res) { + @PathVariable String clusterName, @PathVariable String namespaceName, + HttpServletResponse res) { List fileNameSplit = Splitter.on(".").splitToList(namespaceName); - String fileName = fileNameSplit.size() <= 1 ? Joiner.on(".") - .join(namespaceName, ConfigFileFormat.Properties.getValue()) : namespaceName; + String fileName = namespaceName; + + //properties file or public namespace has not suffix (.properties) + if (fileNameSplit.size() <= 1 || !ConfigFileFormat.isValidFormat(fileNameSplit.get(fileNameSplit.size() - 1))) { + fileName = Joiner.on(".").join(namespaceName, ConfigFileFormat.Properties.getValue()); + } + NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(appId, Env.valueOf (env), clusterName, namespaceName); @@ -96,21 +108,26 @@ public void exportItems(@PathVariable String appId, @PathVariable String env, } /** - * Export all configs in a compressed file. - * Just export namespace which current exists read permission. - * The permission check in service. + * Export all configs in a compressed file. Just export namespace which current exists read permission. The permission + * check in service. */ - @GetMapping("/export") - public void exportAll(HttpServletRequest request, HttpServletResponse response) throws IOException { + @GetMapping("/configs/export") + public void exportAll(@RequestParam(value = "envs") String envs, + HttpServletRequest request, HttpServletResponse response) throws IOException { // filename must contain the information of time final String filename = "apollo_config_export_" + DateFormatUtils.format(new Date(), "yyyy_MMdd_HH_mm_ss") + ".zip"; // log who download the configs - logger.info("Download configs, remote addr [{}], remote host [{}]. Filename is [{}]", request.getRemoteAddr(), request.getRemoteHost(), filename); + logger.info("Download configs, remote addr [{}], remote host [{}]. Filename is [{}]", request.getRemoteAddr(), + request.getRemoteHost(), filename); // set downloaded filename response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename); + List + exportEnvs = + Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(env -> Env.valueOf(env)).collect(Collectors.toList()); + try (OutputStream outputStream = response.getOutputStream()) { - configsExportService.exportAllTo(outputStream); + configsExportService.exportData(outputStream, exportEnvs); } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsImportController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsImportController.java index 672edf70962..2623d9518cc 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsImportController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigsImportController.java @@ -16,10 +16,13 @@ */ package com.ctrip.framework.apollo.portal.controller; +import com.google.common.base.Splitter; + import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.ConfigsImportService; import com.ctrip.framework.apollo.portal.util.ConfigFileUtils; -import java.io.IOException; + import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -27,6 +30,12 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.zip.ZipInputStream; + /** * Import the configs from file. * First version: move code from {@link ConfigsExportController} @@ -34,9 +43,11 @@ */ @RestController public class ConfigsImportController { + private static final String ENV_SEPARATOR = ","; private final ConfigsImportService configsImportService; + public ConfigsImportController( final ConfigsImportService configsImportService ) { @@ -53,13 +64,43 @@ public ConfigsImportController( @PreAuthorize(value = "@permissionValidator.hasModifyNamespacePermission(#appId, #namespaceName, #env)") @PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/import") public void importConfigFile(@PathVariable String appId, @PathVariable String env, - @PathVariable String clusterName, @PathVariable String namespaceName, - @RequestParam("file") MultipartFile file) throws IOException { + @PathVariable String clusterName, @PathVariable String namespaceName, + @RequestParam("file") MultipartFile file) throws IOException { // check file ConfigFileUtils.check(file); final String format = ConfigFileUtils.getFormat(file.getOriginalFilename()); - final String standardFilename = ConfigFileUtils.toFilename(appId, clusterName, namespaceName, - ConfigFileFormat.fromString(format)); - configsImportService.importOneConfigFromFile(env, standardFilename, file.getInputStream()); + final String standardFilename = ConfigFileUtils.toFilename(appId, clusterName, + namespaceName, + ConfigFileFormat.fromString(format)); + + configsImportService.forceImportNamespaceFromFile(Env.valueOf(env), standardFilename, file.getInputStream()); + } + + @PostMapping(value = "/configs/import", params = "conflictAction=cover") + public void importConfigByZipWithCoverConflictNamespace(@RequestParam(value = "envs") String envs, + @RequestParam("file") MultipartFile file) throws IOException { + + List + importEnvs = + Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(env -> Env.valueOf(env)).collect(Collectors.toList()); + + byte[] bytes = file.getBytes(); + try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) { + configsImportService.importDataFromZipFile(importEnvs, zipInputStream, false); + } + } + + @PostMapping(value = "/configs/import", params = "conflictAction=ignore") + public void importConfigByZipWithIgnoreConflictNamespace(@RequestParam(value = "envs") String envs, + @RequestParam("file") MultipartFile file) throws IOException { + + List + importEnvs = + Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(env -> Env.valueOf(env)).collect(Collectors.toList()); + + byte[] bytes = file.getBytes(); + try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) { + configsImportService.importDataFromZipFile(importEnvs, zipInputStream, true); + } } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/InstanceController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/InstanceController.java index 3cd86966374..45425b51d0b 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/InstanceController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/InstanceController.java @@ -69,7 +69,7 @@ public ResponseEntity getInstanceCountByNamespace(@PathVariable String e @RequestParam String clusterName, @RequestParam String namespaceName) { - int count = instanceService.getInstanceCountByNamepsace(appId, Env.valueOf(env), clusterName, namespaceName); + int count = instanceService.getInstanceCountByNamespace(appId, Env.valueOf(env), clusterName, namespaceName); return ResponseEntity.ok(new Number(count)); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ConfigBO.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ConfigBO.java index 0ae0596d7a2..349bd4ee672 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ConfigBO.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ConfigBO.java @@ -40,22 +40,25 @@ public class ConfigBO { private final ConfigFileFormat format; + private final boolean isPublic; + public ConfigBO(Env env, String ownerName, String appId, String clusterName, - String namespace, String configFileContent, ConfigFileFormat format) { + String namespace, boolean isPublic, String configFileContent, ConfigFileFormat format) { this.env = env; this.ownerName = ownerName; this.appId = appId; this.clusterName = clusterName; this.namespace = namespace; + this.isPublic = isPublic; this.configFileContent = configFileContent; this.format = format; } public ConfigBO(Env env, String ownerName, String appId, String clusterName, NamespaceBO namespaceBO) { this(env, ownerName, appId, clusterName, - namespaceBO.getBaseInfo().getNamespaceName(), - NamespaceBOUtils.convert2configFileContent(namespaceBO), - ConfigFileFormat.fromString(namespaceBO.getFormat()) + namespaceBO.getBaseInfo().getNamespaceName(), namespaceBO.isPublic(), + NamespaceBOUtils.convert2configFileContent(namespaceBO), + ConfigFileFormat.fromString(namespaceBO.getFormat()) ); } @@ -67,6 +70,7 @@ public String toString() { ", appId='" + appId + '\'' + ", clusterName='" + clusterName + '\'' + ", namespace='" + namespace + '\'' + + ", isPublic='" + isPublic + '\'' + ", configFileContent='" + configFileContent + '\'' + ", format=" + format + '}'; @@ -99,4 +103,8 @@ public String getConfigFileContent() { public ConfigFileFormat getFormat() { return format; } + + public boolean isPublic() { + return isPublic; + } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceTextModel.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceTextModel.java index b33c89d82de..239aff0b39a 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceTextModel.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceTextModel.java @@ -30,6 +30,7 @@ public class NamespaceTextModel implements Verifiable { private long namespaceId; private String format; private String configText; + private String operator; @Override @@ -92,4 +93,12 @@ public ConfigFileFormat getFormat() { public void setFormat(String format) { this.format = format; } + + public String getOperator() { + return operator; + } + + public void setOperator(String operator) { + this.operator = operator; + } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java index 281a75fe793..a299ed7f79d 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java @@ -25,6 +25,7 @@ import com.ctrip.framework.apollo.portal.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.google.common.base.Joiner; +import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -89,6 +90,11 @@ public List findByAppId(String appId) { return appNamespaceRepository.findByAppId(appId); } + public List findAll() { + Iterable appNamespaces = appNamespaceRepository.findAll(); + return Lists.newArrayList(appNamespaces); + } + @Transactional public void createDefaultAppNamespace(String appId) { if (!isAppNamespaceNameUnique(appId, ConfigConsts.NAMESPACE_APPLICATION)) { @@ -171,6 +177,30 @@ public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace, boolean return createdAppNamespace; } + @Transactional + public AppNamespace importAppNamespaceInLocal(AppNamespace appNamespace) { + // globally uniqueness check for public app namespace + if (appNamespace.isPublic()) { + checkAppNamespaceGlobalUniqueness(appNamespace); + } else { + // check private app namespace + if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()) != null) { + throw new BadRequestException("Private AppNamespace " + appNamespace.getName() + " already exists!"); + } + // should not have the same with public app namespace + checkPublicAppNamespaceGlobalUniqueness(appNamespace); + } + + AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace); + + String operator = appNamespace.getDataChangeCreatedBy(); + + roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(), operator); + roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(), operator); + + return createdAppNamespace; + } + private void checkAppNamespaceGlobalUniqueness(AppNamespace appNamespace) { checkPublicAppNamespaceGlobalUniqueness(appNamespace); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java index ae6bb8eea9d..1438e000cc7 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java @@ -21,6 +21,7 @@ import com.ctrip.framework.apollo.common.entity.App; 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.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.constant.TracerEventType; @@ -113,9 +114,11 @@ public AppDTO load(Env env, String appId) { } public void createAppInRemote(Env env, App app) { - String username = userInfoHolder.getUser().getUserId(); - app.setDataChangeCreatedBy(username); - app.setDataChangeLastModifiedBy(username); + if (StringUtils.isBlank(app.getDataChangeCreatedBy())) { + String username = userInfoHolder.getUser().getUserId(); + app.setDataChangeCreatedBy(username); + app.setDataChangeLastModifiedBy(username); + } AppDTO appDTO = BeanUtils.transform(AppDTO.class, app); appAPI.createApp(env, appDTO); @@ -150,6 +153,31 @@ public App createAppInLocal(App app) { return createdApp; } + @Transactional + public App importAppInLocal(App app) { + String appId = app.getAppId(); + App managedApp = appRepository.findByAppId(appId); + + if (managedApp != null) { + return app; + } + + UserInfo owner = userService.findByUserId(app.getOwnerName()); + if (owner == null) { + throw new BadRequestException("Application's owner not exist."); + } + + app.setOwnerEmail(owner.getEmail()); + + App createdApp = appRepository.save(app); + + roleInitializationService.initAppRoles(createdApp); + + Tracer.logEvent(TracerEventType.CREATE_APP, appId); + + return createdApp; + } + @Transactional public App updateAppInLocal(App app) { String appId = app.getAppId(); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsExportService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsExportService.java index 126cacc24f4..431ec7a9a5d 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsExportService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsExportService.java @@ -16,8 +16,13 @@ */ package com.ctrip.framework.apollo.portal.service; +import com.google.gson.Gson; + import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.entity.App; +import com.ctrip.framework.apollo.common.entity.AppNamespace; +import com.ctrip.framework.apollo.common.exception.BadRequestException; +import com.ctrip.framework.apollo.common.exception.ServiceException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.portal.component.PermissionValidator; import com.ctrip.framework.apollo.portal.component.PortalSettings; @@ -25,35 +30,40 @@ import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.util.ConfigFileUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + import java.io.IOException; import java.io.OutputStream; import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.function.BiFunction; -import java.util.function.BinaryOperator; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; @Service public class ConfigsExportService { private static final Logger logger = LoggerFactory.getLogger(ConfigsExportService.class); + private final Gson gson = new Gson(); + private final AppService appService; private final ClusterService clusterService; private final NamespaceService namespaceService; + private final AppNamespaceService appNamespaceService; + private final PortalSettings portalSettings; private final PermissionValidator permissionValidator; @@ -62,133 +72,242 @@ public ConfigsExportService( AppService appService, ClusterService clusterService, final @Lazy NamespaceService namespaceService, + final AppNamespaceService appNamespaceService, PortalSettings portalSettings, PermissionValidator permissionValidator) { this.appService = appService; this.clusterService = clusterService; this.namespaceService = namespaceService; + this.appNamespaceService = appNamespaceService; this.portalSettings = portalSettings; this.permissionValidator = permissionValidator; } /** - * write multiple namespace to a zip. use {@link Stream#reduce(Object, BiFunction, - * BinaryOperator)} to forbid concurrent write. + * Export all application which current user own them. + *

+ * File Struts: + *

* - * @param configBOStream namespace's stream - * @param outputStream receive zip file output stream - * @throws IOException if happen write problem - */ - private static void writeAsZipOutputStream( - Stream configBOStream, OutputStream outputStream) throws IOException { - try (final ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { - final Consumer configBOConsumer = - configBO -> { - try { - // TODO, Stream.reduce will cause some problems. Is There other way to speed up the - // downloading? - synchronized (zipOutputStream) { - write2ZipOutputStream(zipOutputStream, configBO); - } - } catch (IOException e) { - logger.error("Write error. {}", configBO); - throw new IllegalStateException(e); - } - }; - configBOStream.forEach(configBOConsumer); - } - } - - /** - * write {@link ConfigBO} as file to {@link ZipOutputStream}. Watch out the concurrent problem! - * zip output stream is same like cannot write concurrently! the name of file is determined by - * {@link ConfigFileUtils#toFilename(String, String, String, ConfigFileFormat)}. the path of file - * is determined by {@link ConfigFileUtils#toFilePath(String, String, Env, String)}. + * List + * List -> List -> List -> List + * -----------------> app.metadata + * -------------------------------------------> List * - * @param zipOutputStream zip file output stream - * @param configBO a namespace represent - * @return zip file output stream same as parameter zipOutputStream + * @param outputStream network file download stream to user */ - private static ZipOutputStream write2ZipOutputStream( - final ZipOutputStream zipOutputStream, final ConfigBO configBO) throws IOException { - final Env env = configBO.getEnv(); - final String ownerName = configBO.getOwnerName(); - final String appId = configBO.getAppId(); - final String clusterName = configBO.getClusterName(); - final String namespace = configBO.getNamespace(); - final String configFileContent = configBO.getConfigFileContent(); - final ConfigFileFormat configFileFormat = configBO.getFormat(); - final String configFilename = - ConfigFileUtils.toFilename(appId, clusterName, namespace, configFileFormat); - final String filePath = ConfigFileUtils.toFilePath(ownerName, appId, env, configFilename); - final ZipEntry zipEntry = new ZipEntry(filePath); - try { - zipOutputStream.putNextEntry(zipEntry); - zipOutputStream.write(configFileContent.getBytes()); - zipOutputStream.closeEntry(); - } catch (IOException e) { - logger.error("config export failed. {}", configBO); - throw new IOException("config export failed", e); + public void exportData(OutputStream outputStream, List exportEnvs) { + if (CollectionUtils.isEmpty(exportEnvs)) { + exportEnvs = portalSettings.getActiveEnvs(); } - return zipOutputStream; - } - /** @return the namespaces current user exists */ - private Stream makeStreamBy( - final Env env, final String ownerName, final String appId, final String clusterName) { - final List namespaceBOS = - namespaceService.findNamespaceBOs(appId, env, clusterName); - final Function function = - namespaceBO -> new ConfigBO(env, ownerName, appId, clusterName, namespaceBO); - return namespaceBOS.parallelStream().map(function); + exportApps(exportEnvs, outputStream); } - private Stream makeStreamBy(final Env env, final String ownerName, final String appId) { - final List clusterDTOS = clusterService.findClusters(env, appId); - final Function> function = - clusterDTO -> this.makeStreamBy(env, ownerName, appId, clusterDTO.getName()); - return clusterDTOS.parallelStream().flatMap(function); - } + private void exportApps(final Collection exportEnvs, OutputStream outputStream) { + List hasPermissionApps = findHasPermissionApps(); + + if (CollectionUtils.isEmpty(hasPermissionApps)) { + return; + } - private Stream makeStreamBy(final Env env, final List apps) { - final Function> function = - app -> this.makeStreamBy(env, app.getOwnerName(), app.getAppId()); + try (final ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + //write app info to zip + writeAppInfoToZip(hasPermissionApps, zipOutputStream); + + //export app namespace + exportAppNamespaces(zipOutputStream); - return apps.parallelStream().flatMap(function); + //export app's clusters + exportEnvs.parallelStream().forEach(env -> { + try { + this.exportClusters(env, hasPermissionApps, zipOutputStream); + } catch (Exception e) { + logger.error("export cluster error. env = {}", env, e); + } + }); + } catch (IOException e) { + logger.error("export config error", e); + throw new ServiceException("export config error", e); + } } - private Stream makeStreamBy(final Collection envs) { + private List findHasPermissionApps() { // get all apps final List apps = appService.findAll(); + if (CollectionUtils.isEmpty(apps)) { + return Collections.emptyList(); + } + // permission check final Predicate isAppAdmin = app -> { try { return permissionValidator.isAppAdmin(app.getAppId()); } catch (Exception e) { - logger.error("app = {}", app); - logger.error(app.getAppId()); + logger.error("permission check failed. app = {}", app); + return false; } - return false; }; // app admin permission filter - final List appsExistPermission = - apps.stream().filter(isAppAdmin).collect(Collectors.toList()); - return envs.parallelStream().flatMap(env -> this.makeStreamBy(env, appsExistPermission)); + return apps.stream().filter(isAppAdmin).collect(Collectors.toList()); } - /** - * Export all projects which current user own them. Permission check by {@link - * PermissionValidator#isAppAdmin(java.lang.String)} - * - * @param outputStream network file download stream to user - * @throws IOException if happen write problem - */ - public void exportAllTo(OutputStream outputStream) throws IOException { - final List activeEnvs = portalSettings.getActiveEnvs(); - final Stream configBOStream = this.makeStreamBy(activeEnvs); - writeAsZipOutputStream(configBOStream, outputStream); + private void writeAppInfoToZip(List apps, ZipOutputStream zipOutputStream) { + logger.info("to import app size = {}", apps.size()); + + final Consumer appConsumer = + app -> { + try { + synchronized (zipOutputStream) { + String fileName = ConfigFileUtils.genAppInfoPath(app); + String content = gson.toJson(app); + + writeToZip(fileName, content, zipOutputStream); + } + } catch (IOException e) { + logger.error("Write error. {}", app); + throw new ServiceException("Write app error. {}", e); + } + }; + + apps.forEach(appConsumer); } + + private void exportAppNamespaces(ZipOutputStream zipOutputStream) { + List appNamespaces = appNamespaceService.findAll(); + + logger.info("to import appnamespace size = " + appNamespaces.size()); + + Consumer appNamespaceConsumer = appNamespace -> { + try { + synchronized (zipOutputStream) { + String fileName = ConfigFileUtils.genAppNamespaceInfoPath(appNamespace); + String content = gson.toJson(appNamespace); + + writeToZip(fileName, content, zipOutputStream); + } + } catch (Exception e) { + logger.error("Write appnamespace error. {}", appNamespace); + throw new IllegalStateException(e); + } + }; + + appNamespaces.forEach(appNamespaceConsumer); + + } + + private void exportClusters(final Env env, final List exportApps, ZipOutputStream zipOutputStream) { + exportApps.parallelStream().forEach(exportApp -> { + try { + this.exportCluster(env, exportApp, zipOutputStream); + } catch (Exception e) { + logger.error("export cluster error. appId = {}", exportApp.getAppId(), e); + } + }); + } + + private void exportCluster(final Env env, final App exportApp, ZipOutputStream zipOutputStream) { + final List exportClusters = clusterService.findClusters(env, exportApp.getAppId()); + + if (CollectionUtils.isEmpty(exportClusters)) { + return; + } + + //write cluster info to zip + writeClusterInfoToZip(env, exportApp, exportClusters, zipOutputStream); + + //export namespaces + exportClusters.parallelStream().forEach(cluster -> { + try { + this.exportNamespaces(env, exportApp, cluster, zipOutputStream); + } catch (BadRequestException badRequestException) { + //ignore + } catch (Exception e) { + logger.error("export namespace error. appId = {}, cluster = {}", exportApp.getAppId(), cluster, e); + } + }); + } + + private void exportNamespaces(final Env env, final App exportApp, final ClusterDTO exportCluster, + ZipOutputStream zipOutputStream) { + String clusterName = exportCluster.getName(); + + List namespaceBOS = namespaceService.findNamespaceBOs(exportApp.getAppId(), env, clusterName); + + if (CollectionUtils.isEmpty(namespaceBOS)) { + return; + } + + Stream configBOStream = namespaceBOS.stream() + .map( + namespaceBO -> new ConfigBO(env, exportApp.getOwnerName(), exportApp.getAppId(), clusterName, namespaceBO)); + + writeNamespacesToZip(configBOStream, zipOutputStream); + } + + private void writeNamespacesToZip(Stream configBOStream, ZipOutputStream zipOutputStream) { + final Consumer configBOConsumer = + configBO -> { + try { + synchronized (zipOutputStream) { + String appId = configBO.getAppId(); + String clusterName = configBO.getClusterName(); + String namespace = configBO.getNamespace(); + String configFileContent = configBO.getConfigFileContent(); + ConfigFileFormat configFileFormat = configBO.getFormat(); + + String + configFileName = + ConfigFileUtils.toFilename(appId, clusterName, namespace, configFileFormat); + String filePath = + ConfigFileUtils.genNamespacePath(configBO.getOwnerName(), appId, configBO.getEnv(), configFileName); + + writeToZip(filePath, configFileContent, zipOutputStream); + } + } catch (IOException e) { + logger.error("Write error. {}", configBO); + throw new ServiceException("Write namespace error. {}", e); + } + }; + + configBOStream.forEach(configBOConsumer); + } + + private void writeClusterInfoToZip(Env env, App app, List exportClusters, + ZipOutputStream zipOutputStream) { + final Consumer clusterConsumer = + cluster -> { + try { + synchronized (zipOutputStream) { + String fileName = ConfigFileUtils.genClusterInfoPath(app, env, cluster); + String content = gson.toJson(cluster); + + writeToZip(fileName, content, zipOutputStream); + } + } catch (IOException e) { + logger.error("Write error. {}", cluster); + throw new ServiceException("Write error. {}", e); + } + }; + + exportClusters.forEach(clusterConsumer); + } + + private void writeToZip(String filePath, String content, ZipOutputStream zipOutputStream) + throws IOException { + final ZipEntry zipEntry = new ZipEntry(filePath); + try { + zipOutputStream.putNextEntry(zipEntry); + zipOutputStream.write(content.getBytes()); + zipOutputStream.closeEntry(); + } catch (IOException e) { + String errorMsg = "write content to zip error. file = " + filePath + ", content = " + content; + logger.error(errorMsg); + throw new IOException(errorMsg, e); + } + } + } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsImportService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsImportService.java index f5aa4b155ff..cd95fdda4eb 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsImportService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsImportService.java @@ -16,18 +16,43 @@ */ package com.ctrip.framework.apollo.portal.service; +import com.google.common.collect.Lists; +import com.google.gson.Gson; + +import com.ctrip.framework.apollo.common.constants.GsonType; +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.common.entity.App; +import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.ServiceException; -import com.ctrip.framework.apollo.portal.component.PermissionValidator; -import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel; +import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.portal.environment.Env; +import com.ctrip.framework.apollo.portal.listener.AppNamespaceCreationEvent; +import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.util.ConfigFileUtils; import com.ctrip.framework.apollo.portal.util.ConfigToFileUtils; -import java.io.IOException; -import java.io.InputStream; -import java.security.AccessControlException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpStatusCodeException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.rmi.ServerException; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * @author wxq @@ -35,103 +60,434 @@ @Service public class ConfigsImportService { - private final ItemService itemService; + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigsImportService.class); + + private Gson gson = new Gson(); - private final NamespaceService namespaceService; + private final ItemService itemService; + private final AppService appService; + private final ClusterService clusterService; + private final NamespaceService namespaceService; + private final AppNamespaceService appNamespaceService; + private final ApplicationEventPublisher publisher; + private final UserInfoHolder userInfoHolder; + private final RoleInitializationService roleInitializationService; + private String currentUser; - private final PermissionValidator permissionValidator; public ConfigsImportService( final ItemService itemService, + final AppService appService, + final ClusterService clusterService, final @Lazy NamespaceService namespaceService, - PermissionValidator permissionValidator) { + final AppNamespaceService appNamespaceService, + final ApplicationEventPublisher publisher, + final UserInfoHolder userInfoHolder, + final RoleInitializationService roleInitializationService) { this.itemService = itemService; + this.appService = appService; + this.clusterService = clusterService; this.namespaceService = namespaceService; - this.permissionValidator = permissionValidator; + this.appNamespaceService = appNamespaceService; + this.publisher = publisher; + this.userInfoHolder = userInfoHolder; + this.roleInitializationService = roleInitializationService; } /** - * move from {@link com.ctrip.framework.apollo.portal.controller.ConfigsImportController} + * force import, new items will overwrite existed items. */ - private void importConfig( - final String appId, - final String env, - final String clusterName, - final String namespaceName, - final long namespaceId, - final String format, - final String configText - ) { - final NamespaceTextModel model = new NamespaceTextModel(); - - model.setAppId(appId); - model.setEnv(env); - model.setClusterName(clusterName); - model.setNamespaceName(namespaceName); - model.setNamespaceId(namespaceId); - model.setFormat(format); - model.setConfigText(configText); - - itemService.updateConfigItemByText(model); + public void forceImportNamespaceFromFile(final Env env, final String standardFilename, + final InputStream zipInputStream) { + String configText; + try (InputStream in = zipInputStream) { + configText = ConfigToFileUtils.fileToString(in); + } catch (IOException e) { + throw new ServiceException("Read config file errors:{}", e); + } + + currentUser = userInfoHolder.getUser().getUserId(); + + this.importNamespaceFromText(env, standardFilename, configText, false); } /** - * import one config from file + * import all data include app、appnamespace、cluster、namespace、item */ - private void importOneConfigFromFile( - final String appId, - final String env, - final String clusterName, - final String namespaceName, - final String configText, - final String format - ) { - final NamespaceDTO namespaceDTO = namespaceService - .loadNamespaceBaseInfo(appId, Env.valueOf(env), clusterName, namespaceName); - this.importConfig(appId, env, clusterName, namespaceName, namespaceDTO.getId(), format, configText); + public void importDataFromZipFile(List importEnvs, ZipInputStream dataZip, boolean ignoreConflictNamespace) + throws IOException { + List toImportApps = Lists.newArrayList(); + List toImportAppNSs = Lists.newArrayList(); + List toImportClusters = Lists.newArrayList(); + List toImportNSs = Lists.newArrayList(); + + ZipEntry entry; + while ((entry = dataZip.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + + String filePath = entry.getName(); + String content = readContent(dataZip); + + String[] info = filePath.split("/"); + + String fileName; + if (info.length == 1) { + //app namespace metadata file. path format : ${namespaceName}.appnamespace.metadata + fileName = info[0]; + if (fileName.endsWith(ConfigFileUtils.APP_NAMESPACE_METADATA_FILE_SUFFIX)) { + toImportAppNSs.add(content); + } + } else if (info.length == 3) { + fileName = info[2]; + if (fileName.equals(ConfigFileUtils.APP_METADATA_FILENAME)) { + //app metadata file. path format : apollo/${appId}/app.metadata + toImportApps.add(content); + } + } else { + String env = info[2]; + fileName = info[3]; + if (fileName.endsWith(ConfigFileUtils.CLUSTER_METADATA_FILE_SUFFIX)) { + //cluster metadata file. path format : apollo/${appId}/${env}/${clusterName}.cluster.metadata + toImportClusters.add(content); + } else { + //namespace file.path format : apollo/${appId}/${env}/${appId}+${cluster}+${namespaceName} + //only import for selected envs. + for (Env importEnv : importEnvs) { + if (Objects.equals(importEnv.getName(), env)) { + toImportNSs.add(new NamespaceImportData(Env.valueOf(env), fileName, content, ignoreConflictNamespace)); + } + } + } + } + } + + try { + LOGGER.info("Import data. app = {}, appns = {}, cluster = {}, namespace = {}", toImportApps.size(), + toImportAppNSs.size(), + toImportClusters.size(), toImportNSs.size()); + + doImport(importEnvs, toImportApps, toImportAppNSs, toImportClusters, toImportNSs); + + } catch (Exception e) { + LOGGER.error("import config error.", e); + throw new ServerException("import config error.", e); + } + } + + private void doImport(List importEnvs, List toImportApps, List toImportAppNSs, + List toImportClusters, List toImportNSs) + throws InterruptedException { + currentUser = userInfoHolder.getUser().getUserId(); + + LOGGER.info("Start to import app. size = {}", toImportApps.size()); + + long startTime = System.currentTimeMillis(); + CountDownLatch appLatch = new CountDownLatch(toImportApps.size()); + toImportApps.parallelStream().forEach(app -> { + try { + importApp(app, importEnvs); + } catch (Exception e) { + LOGGER.error("import app error. app = {}", app, e); + } finally { + appLatch.countDown(); + } + }); + appLatch.await(); + + LOGGER.info("Finish to import app. duration = {}", System.currentTimeMillis() - startTime); + LOGGER.info("Start to import appnamespace. size = {}", toImportAppNSs.size()); + + startTime = System.currentTimeMillis(); + CountDownLatch appNSLatch = new CountDownLatch(toImportAppNSs.size()); + toImportAppNSs.parallelStream().forEach(appNS -> { + try { + importAppNamespace(appNS); + } catch (Exception e) { + LOGGER.error("import appnamespace error. appnamespace = {}", appNS, e); + } finally { + appNSLatch.countDown(); + } + }); + appNSLatch.await(); + + LOGGER.info("Finish to import appnamespace. duration = {}", System.currentTimeMillis() - startTime); + LOGGER.info("Start to import cluster. size = {}", toImportClusters.size()); + + startTime = System.currentTimeMillis(); + CountDownLatch clusterLatch = new CountDownLatch(toImportClusters.size()); + toImportClusters.parallelStream().forEach(cluster -> { + try { + importCluster(cluster, importEnvs); + } catch (Exception e) { + LOGGER.error("import cluster error. cluster = {}", cluster, e); + } finally { + clusterLatch.countDown(); + } + }); + clusterLatch.await(); + + LOGGER.info("Finish to import cluster. duration = {}", System.currentTimeMillis() - startTime); + LOGGER.info("Start to import namespace. size = {}", toImportNSs.size()); + + startTime = System.currentTimeMillis(); + CountDownLatch nsLatch = new CountDownLatch(toImportNSs.size()); + toImportNSs.parallelStream().forEach(namespace -> { + try { + importNamespaceFromText(namespace.getEnv(), namespace.getFileName(), namespace.getContent(), + namespace.isIgnoreConflictNamespace()); + } catch (Exception e) { + LOGGER.error("import namespace error. namespace = {}", namespace, e); + } finally { + nsLatch.countDown(); + } + }); + nsLatch.await(); + + LOGGER.info("Finish to import namespace. duration = {}", System.currentTimeMillis() - startTime); + } + + private void importApp(String appInfo, List importEnvs) { + App toImportApp = gson.fromJson(appInfo, App.class); + String appId = toImportApp.getAppId(); + + //imported app set owner to current user. + toImportApp.setOwnerName(currentUser); + toImportApp.setDataChangeCreatedBy(currentUser); + toImportApp.setDataChangeLastModifiedBy(currentUser); + toImportApp.setDataChangeCreatedTime(new Date()); + toImportApp.setDataChangeLastModifiedTime(new Date()); + + App managedApp = appService.load(appId); + if (managedApp == null) { + appService.importAppInLocal(toImportApp); + } + + importEnvs.parallelStream().forEach(env -> { + try { + appService.load(env, appId); + } catch (Exception e) { + //not existed + appService.createAppInRemote(env, toImportApp); + } + }); + } + + private void importAppNamespace(String appNamespace) { + AppNamespace toImportPubAppNS = gson.fromJson(appNamespace, AppNamespace.class); + + String appId = toImportPubAppNS.getAppId(); + String namespaceName = toImportPubAppNS.getName(); + boolean isPublic = toImportPubAppNS.isPublic(); + + AppNamespace + managedAppNamespace = + isPublic ? appNamespaceService.findPublicAppNamespace(namespaceName) + : appNamespaceService.findByAppIdAndName(appId, namespaceName); + + if (managedAppNamespace == null) { + managedAppNamespace = new AppNamespace(); + managedAppNamespace.setAppId(toImportPubAppNS.getAppId()); + managedAppNamespace.setPublic(isPublic); + managedAppNamespace.setFormat(toImportPubAppNS.getFormat()); + managedAppNamespace.setComment(toImportPubAppNS.getComment()); + managedAppNamespace.setDataChangeCreatedBy(currentUser); + managedAppNamespace.setDataChangeLastModifiedBy(currentUser); + managedAppNamespace.setName(namespaceName); + + AppNamespace createdAppNamespace = appNamespaceService.importAppNamespaceInLocal(managedAppNamespace); + + //application namespace will be auto created when creating app + if (!ConfigConsts.NAMESPACE_APPLICATION.equals(namespaceName)) { + publisher.publishEvent(new AppNamespaceCreationEvent(createdAppNamespace)); + } + } + } + + private void importCluster(String clusterInfo, List importEnvs) { + ClusterDTO toImportCluster = gson.fromJson(clusterInfo, ClusterDTO.class); + + toImportCluster.setDataChangeCreatedBy(currentUser); + toImportCluster.setDataChangeLastModifiedBy(currentUser); + toImportCluster.setDataChangeCreatedTime(new Date()); + toImportCluster.setDataChangeLastModifiedTime(new Date()); + + String appId = toImportCluster.getAppId(); + String clusterName = toImportCluster.getName(); + + importEnvs.parallelStream().forEach(env -> { + try { + clusterService.loadCluster(appId, env, clusterName); + } catch (Exception e) { + //not existed + clusterService.createCluster(env, toImportCluster); + } + }); } /** - * import a config file. - * the name of config file must be special like - * appId+cluster+namespace.format - * Example: + * import a config file. the name of config file must be special like appId+cluster+namespace.format Example: *

    *   123456+default+application.properties (appId is 123456, cluster is default, namespace is application, format is properties)
    *   654321+north+password.yml (appId is 654321, cluster is north, namespace is password, format is yml)
    * 
* so we can get the information of appId, cluster, namespace, format from the file name. - * @param env environment + * + * @param env environment * @param standardFilename appId+cluster+namespace.format - * @param configText config content + * @param configText config content */ - private void importOneConfigFromText( - final String env, - final String standardFilename, - final String configText - ) { + private void importNamespaceFromText(final Env env, final String standardFilename, final String configText, + boolean ignoreConflictNamespace) { final String appId = ConfigFileUtils.getAppId(standardFilename); final String clusterName = ConfigFileUtils.getClusterName(standardFilename); final String namespace = ConfigFileUtils.getNamespace(standardFilename); final String format = ConfigFileUtils.getFormat(standardFilename); - this.importOneConfigFromFile(appId, env, clusterName, namespace, configText, format); + + this.importNamespace(appId, env, clusterName, namespace, configText, format, ignoreConflictNamespace); } - /** - * @see ConfigsImportService#importOneConfigFromText(java.lang.String, java.lang.String, java.lang.String) - * @throws AccessControlException if has no modify namespace permission - */ - public void importOneConfigFromFile( - final String env, - final String standardFilename, - final InputStream inputStream - ) { - final String configText; - try(InputStream in = inputStream) { - configText = ConfigToFileUtils.fileToString(in); + private void importNamespace(final String appId, final Env env, + final String clusterName, final String namespaceName, + final String configText, final String format, + boolean ignoreConflictNamespace) { + NamespaceDTO namespaceDTO; + try { + namespaceDTO = namespaceService.loadNamespaceBaseInfo(appId, env, clusterName, namespaceName); + } catch (Exception e) { + //not existed + namespaceDTO = null; + } + + if (namespaceDTO == null) { + namespaceDTO = new NamespaceDTO(); + namespaceDTO.setAppId(appId); + namespaceDTO.setClusterName(clusterName); + namespaceDTO.setNamespaceName(namespaceName); + namespaceDTO.setDataChangeCreatedBy(currentUser); + namespaceDTO.setDataChangeLastModifiedBy(currentUser); + namespaceDTO = namespaceService.createNamespace(env, namespaceDTO); + + roleInitializationService.initNamespaceRoles(appId, namespaceName, currentUser); + roleInitializationService.initNamespaceEnvRoles(appId, namespaceName, currentUser); + } + + List itemDTOS = itemService.findItems(appId, env, clusterName, namespaceName); + // skip import if target namespace has existed items + if (!CollectionUtils.isEmpty(itemDTOS) && ignoreConflictNamespace) { + return; + } + + importItems(appId, env, clusterName, namespaceName, configText, namespaceDTO); + } + + private void importItems(String appId, Env env, String clusterName, String namespaceName, String configText, + NamespaceDTO namespaceDTO) { + List toImportItems = gson.fromJson(configText, GsonType.ITEM_DTOS); + + toImportItems.parallelStream().forEach(newItem -> { + String key = newItem.getKey(); + newItem.setNamespaceId(namespaceDTO.getId()); + newItem.setDataChangeCreatedBy(currentUser); + newItem.setDataChangeLastModifiedBy(currentUser); + newItem.setDataChangeCreatedTime(new Date()); + newItem.setDataChangeLastModifiedTime(new Date()); + + if (StringUtils.hasText(key)) { + //create or update normal item + try { + ItemDTO oldItem = itemService.loadItem(env, appId, clusterName, namespaceName, key); + newItem.setId(oldItem.getId()); + //existed + itemService.updateItem(appId, env, clusterName, namespaceName, newItem); + } catch (Exception e) { + if (e instanceof HttpStatusCodeException && ((HttpStatusCodeException) e).getStatusCode() + .equals(HttpStatus.NOT_FOUND)) { + //not existed + itemService.createItem(appId, env, clusterName, namespaceName, newItem); + } else { + LOGGER.error("Load or update item error. appId = {}, env = {}, cluster = {}, namespace = {}", appId, env, + clusterName, namespaceDTO, e); + } + } + } else if (StringUtils.hasText(newItem.getComment())){ + //create comment item + itemService.createCommentItem(appId, env, clusterName, namespaceName, newItem); + } + + }); + } + + + private String readContent(ZipInputStream zipInputStream) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int offset; + while ((offset = zipInputStream.read(buffer)) != -1) { + out.write(buffer, 0, offset); + } + return out.toString("UTF-8"); } catch (IOException e) { - throw new ServiceException("Read config file errors:{}", e); + LOGGER.error("Read file content from zip error.", e); + return null; + } + } + + static class NamespaceImportData { + + private Env env; + private String fileName; + private String content; + private boolean ignoreConflictNamespace; + + public NamespaceImportData(Env env, String fileName, String content, boolean ignoreConflictNamespace) { + this.env = env; + this.fileName = fileName; + this.content = content; + this.ignoreConflictNamespace = ignoreConflictNamespace; + } + + public Env getEnv() { + return env; + } + + public void setEnv(Env env) { + this.env = env; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public boolean isIgnoreConflictNamespace() { + return ignoreConflictNamespace; + } + + public void setIgnoreConflictNamespace(boolean ignoreConflictNamespace) { + this.ignoreConflictNamespace = ignoreConflictNamespace; + } + + @Override + public String toString() { + return "NamespaceImportData{" + + "env=" + env + + ", fileName='" + fileName + '\'' + + ", content='" + content + '\'' + + ", ignoreConflictNamespace=" + ignoreConflictNamespace + + '}'; } - this.importOneConfigFromText(env, standardFilename, configText); } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/InstanceService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/InstanceService.java index d13625b62d9..681d2ac0d1c 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/InstanceService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/InstanceService.java @@ -44,7 +44,7 @@ public PageDTO getByNamespace(Env env, String appId, String cluster return instanceAPI.getByNamespace(appId, env, clusterName, namespaceName, instanceAppId, page, size); } - public int getInstanceCountByNamepsace(String appId, Env env, String clusterName, String namespaceName){ + public int getInstanceCountByNamespace(String appId, Env env, String clusterName, String namespaceName){ return instanceAPI.getInstanceCountByNamespace(appId, env, clusterName, namespaceName); } 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 72011b195d5..d629a0b0a07 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 @@ -107,7 +107,12 @@ public void updateConfigItemByText(NamespaceTextModel model) { return; } - changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId()); + String operator = model.getOperator(); + if (StringUtils.isBlank(operator)) { + operator = userInfoHolder.getUser().getUserId(); + } + changeSets.setDataChangeLastModifiedBy(operator); + updateItems(appId, env, clusterName, namespaceName, changeSets); Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE_BY_TEXT, @@ -133,6 +138,17 @@ public ItemDTO createItem(String appId, Env env, String clusterName, String name return itemDTO; } + public ItemDTO createCommentItem(String appId, Env env, String clusterName, String namespaceName, ItemDTO item) { + NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName); + if (namespace == null) { + throw new BadRequestException( + "namespace:" + namespaceName + " not exist in env:" + env + ", cluster:" + clusterName); + } + item.setNamespaceId(namespace.getId()); + + return itemAPI.createCommentItem(appId, env, clusterName, namespaceName, item); + } + public void updateItem(String appId, Env env, String clusterName, String namespaceName, ItemDTO item) { itemAPI.updateItem(appId, env, clusterName, namespaceName, item.getId(), item); } 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 bf6616265bb..99000ac4592 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 @@ -101,7 +101,10 @@ public NamespaceDTO createNamespace(Env env, NamespaceDTO namespace) { if (StringUtils.isEmpty(namespace.getDataChangeCreatedBy())) { namespace.setDataChangeCreatedBy(userInfoHolder.getUser().getUserId()); } - namespace.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId()); + + if (StringUtils.isEmpty(namespace.getDataChangeLastModifiedBy())) { + namespace.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId()); + } NamespaceDTO createdNamespace = namespaceAPI.createNamespace(env, namespace); Tracer.logEvent(TracerEventType.CREATE_NAMESPACE, @@ -207,7 +210,7 @@ public NamespaceBO loadNamespaceBO(String appId, Env env, String clusterName, public boolean namespaceHasInstances(String appId, Env env, String clusterName, String namespaceName) { - return instanceService.getInstanceCountByNamepsace(appId, env, clusterName, namespaceName) > 0; + return instanceService.getInstanceCountByNamespace(appId, env, clusterName, namespaceName) > 0; } public boolean publicAppNamespaceHasAssociatedNamespace(String publicNamespaceName, Env env) { diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/ConfigFileUtils.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/ConfigFileUtils.java index 73ba87bee82..53ad52faef8 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/ConfigFileUtils.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/ConfigFileUtils.java @@ -16,14 +16,20 @@ */ package com.ctrip.framework.apollo.portal.util; +import com.ctrip.framework.apollo.common.dto.ClusterDTO; +import com.ctrip.framework.apollo.common.entity.App; +import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.controller.ConfigsImportController; import com.ctrip.framework.apollo.portal.environment.Env; + import com.google.common.base.Splitter; + import java.io.File; import java.util.List; + import org.springframework.web.multipart.MultipartFile; /** @@ -32,6 +38,10 @@ */ public class ConfigFileUtils { + public static final String APP_METADATA_FILENAME = "app.metadata"; + public static final String CLUSTER_METADATA_FILE_SUFFIX = ".cluster.metadata"; + public static final String APP_NAMESPACE_METADATA_FILE_SUFFIX = ".appnamespace.metadata"; + public static void check(MultipartFile file) { checkEmpty(file); final String originalFilename = file.getOriginalFilename(); @@ -168,7 +178,7 @@ public static String toFilename( * file path = ownerName/appId/env/configFilename * @return file path in compressed file */ - public static String toFilePath( + public static String genNamespacePath( final String ownerName, final String appId, final Env env, @@ -176,4 +186,27 @@ public static String toFilePath( ) { return String.join(File.separator, ownerName, appId, env.getName(), configFilename); } + + /** + * path = ownerName/appId/app.metadata + */ + public static String genAppInfoPath(App app) { + return String.join(File.separator, app.getOwnerName(), app.getAppId(), APP_METADATA_FILENAME); + } + + /** + * path = {appNamespace}.appnamespace.metadata + */ + public static String genAppNamespaceInfoPath(AppNamespace appNamespace) { + return String.join(File.separator, + appNamespace.getAppId() + "+" + appNamespace.getName() + APP_NAMESPACE_METADATA_FILE_SUFFIX); + } + + /** + * path = ownerName/appId/env/${clusterName}.metadata + */ + public static String genClusterInfoPath(App app, Env env, ClusterDTO cluster) { + return String.join(File.separator, app.getOwnerName(), app.getAppId(), env.getName(), + cluster.getName() + CLUSTER_METADATA_FILE_SUFFIX); + } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/NamespaceBOUtils.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/NamespaceBOUtils.java index bbb9dad8a19..e2fa42c9597 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/NamespaceBOUtils.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/NamespaceBOUtils.java @@ -16,24 +16,36 @@ */ package com.ctrip.framework.apollo.portal.util; +import com.google.common.collect.Lists; +import com.google.gson.Gson; + +import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.core.utils.PropertiesUtil; import com.ctrip.framework.apollo.portal.controller.ConfigsExportController; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; + +import org.springframework.util.CollectionUtils; + import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Properties; +import java.util.stream.Collectors; /** * @author wxq */ public class NamespaceBOUtils { + private static final Gson GSON = new Gson(); + /** - * namespace must not be {@link ConfigFileFormat#Properties}. - * the content of namespace in item's value which item's key is {@link ConfigConsts#CONFIG_FILE_CONTENT_KEY}. + * namespace must not be {@link ConfigFileFormat#Properties}. the content of namespace in item's value which item's + * key is {@link ConfigConsts#CONFIG_FILE_CONTENT_KEY}. + * * @param namespaceBO namespace * @return content of non-properties's namespace */ @@ -43,17 +55,18 @@ static String convertNonProperties2configFileContent(NamespaceBO namespaceBO) { String key = itemBO.getItem().getKey(); // special namespace format(not properties) if (ConfigConsts.CONFIG_FILE_CONTENT_KEY.equals(key)) { - return itemBO.getItem().getValue(); + ItemDTO dto = itemBO.getItem(); + dto.setId(0); + dto.setNamespaceId(0); + return GSON.toJson(Lists.newArrayList(dto)); } } - // If there is no items? - // return empty string "" return ""; } /** - * copy from old {@link ConfigsExportController}. - * convert {@link NamespaceBO} to a file content. + * copy from old {@link ConfigsExportController}. convert {@link NamespaceBO} to a file content. + * * @return content of config file * @throws IllegalStateException if convert properties to string fail */ @@ -66,23 +79,18 @@ public static String convert2configFileContent(NamespaceBO namespaceBO) { // it must be a properties format namespace List itemBOS = namespaceBO.getItems(); - // save the kev value pair - Properties properties = new Properties(); - for (ItemBO itemBO : itemBOS) { - String key = itemBO.getItem().getKey(); - String value = itemBO.getItem().getValue(); - // ignore comment, so the comment will lack - properties.put(key, value); + if (CollectionUtils.isEmpty(itemBOS)) { + return GSON.toJson(Collections.emptyList()); } - // use a special method convert properties to string - final String configFileContent; - try { - configFileContent = PropertiesUtil.toString(properties); - } catch (IOException e) { - throw new IllegalStateException("convert properties to string fail.", e); - } - return configFileContent; + List itemDTOS = itemBOS.stream().map(itemBO -> { + ItemDTO dto = itemBO.getItem(); + dto.setId(0); + dto.setNamespaceId(0); + return dto; + }).collect(Collectors.toList()); + + return GSON.toJson(itemDTOS); } } diff --git a/apollo-portal/src/main/resources/application.yml b/apollo-portal/src/main/resources/application.yml index 34b3fffedda..0398414ae23 100644 --- a/apollo-portal/src/main/resources/application.yml +++ b/apollo-portal/src/main/resources/application.yml @@ -27,6 +27,10 @@ spring: store-type: jdbc jdbc: initialize-schema: never + servlet: + multipart: + max-file-size: 200MB # import data configs + max-request-size: 200MB server: port: 8070 compression: diff --git a/apollo-portal/src/main/resources/static/config.html b/apollo-portal/src/main/resources/static/config.html index c11b66440df..8a17a4a493c 100644 --- a/apollo-portal/src/main/resources/static/config.html +++ b/apollo-portal/src/main/resources/static/config.html @@ -255,6 +255,8 @@

+ + {{'Config.CreateBranchTips.DialogTitle' | translate}} + diff --git a/apollo-portal/src/main/resources/static/config_export.html b/apollo-portal/src/main/resources/static/config_export.html index 27e22b16928..1e35c5e79d2 100644 --- a/apollo-portal/src/main/resources/static/config_export.html +++ b/apollo-portal/src/main/resources/static/config_export.html @@ -30,76 +30,139 @@ - -
-
-
-
-
- {{'ConfigExport.Title' | translate }} - - {{'ConfigExport.TitleTips' | translate}} - -
-
- - - + +
+
+
+
+ {{'ConfigExport.Title' | translate }} + {{'ConfigExport.TitleTips' | translate }} +
+
+
+ +
+ + + + + + + +
-
-
+
+ +
+ {{'ConfigExport.Export' | translate }} +

({{'ConfigExport.ExportTips' | translate }})

+
+
+ + +
+ +
+
+ +
+ + + + + + + +
+
+
+
+ +
+
+ + {{'ConfigExport.IgnoreExistedNamespace' | translate }} +
+
+ + {{'ConfigExport.OverwriteExistedNamespace' | translate }} +
+
+
+
+ +
+ +
+
+
+ +
+ {{'ConfigExport.Import' | translate }} +
+

({{'ConfigExport.ImportTips' | 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 b4962e574e2..39897dc503b 100644 --- a/apollo-portal/src/main/resources/static/i18n/en.json +++ b/apollo-portal/src/main/resources/static/i18n/en.json @@ -11,7 +11,7 @@ "Common.Nav.SystemConfig": "System Configuration", "Common.Nav.DeleteApp-Cluster-Namespace": "Delete Apps, Clusters, AppNamespace", "Common.Nav.SystemInfo": "System Information", - "Common.Nav.ConfigExport": "Config Export", + "Common.Nav.ConfigExport": "Config Export / Import", "Common.Nav.Logout": "Logout", "Common.Department": "Department", "Common.Cluster": "Cluster", @@ -175,6 +175,8 @@ "Component.Namespace.Master.Items.RequestPermission": "Apply for configuration permission", "Component.Namespace.Master.Items.RequestPermissionTips": "You do not have any configuration permission. Please apply.", "Component.Namespace.Master.Items.DeleteNamespace": "Delete Namespace", + "Component.Namespace.Master.Items.ExportNamespace": "Export Namespace", + "Component.Namespace.Master.Items.ImportNamespace": "Import Namespace", "Component.Namespace.Master.Items.NoPermissionTips": "You are not this project's administrator, nor you have edit or release permission for the namespace. Thus you cannot view the configuration.", "Component.Namespace.Master.Items.ItemList": "Table", "Component.Namespace.Master.Items.ItemListByText": "Text", @@ -693,9 +695,28 @@ "Config.Diff.DiffCluster": "Clusters to be compared", "Config.Diff.HasDiffComment": "Whether to compare comments or not", "Config.Diff.PleaseChooseTwoCluster": "Please select at least two clusters", - "ConfigExport.Title": "Config Export", - "ConfigExport.TitleTips" : "Super administrators will download the configuration of all projects, normal users will only download the configuration of their own projects", + "ConfigExport.Title": "Config Export/Import", + "ConfigExport.TitleTips" : "(The data (application, cluster and namespace) of one cluster can be migrated to another cluster by exporting and importing the configuration)", + "ConfigExport.SelectExportEnv" : "Select the environment to export", + "ConfigExport.SelectImportEnv" : "Select the environment to import", + "ConfigExport.ExportTips" : "In case of large amount of data, the export speed is slow. Please wait patiently", + "ConfigExport.ImportConflictLabel" : "How to deal with existing namespaces when importing", + "ConfigExport.IgnoreExistedNamespace" : "Ignore existing namespaces", + "ConfigExport.OverwriteExistedNamespace" : "Overwrite existing namespace", + "ConfigExport.UploadFile" : "Upload the exported file", + "ConfigExport.UploadFileTip" : "Please upload the exported compressed file", + "ConfigExport.ImportSuccess" : "Import success", + "ConfigExport.ImportingTip" : "Importing, please wait patiently. After importing, please check whether the namespace configuration is correct. If it is correct, publish the namespace to take effect", + "ConfigExport.ImportFailed" : "Import failed", + "ConfigExport.ExportSuccess" : "Exporting data. The data volume will cause slow speed. Please wait patiently", + "ConfigExport.ImportTips" : "After the import is completed, please check whether the namespace configuration is correct. After the check is correct, it needs to be published to take effect", + "ConfigExport.Export" : "Export", + "ConfigExport.Import" : "Import", "ConfigExport.Download": "Download", + "ConfigImport.Title": "Import Namespace", + "ConfigImport.Tip1": "When the configuration item conflicts, the imported value will overwrite the past value", + "ConfigImport.Tip2": "When the configuration items do not conflict, a new configuration item will be added", + "ConfigImport.Tip3": "After importing the configuration item, it needs to be released to take effect", "App.CreateProject": "Create Project", "App.AppIdTips": "(Application's unique identifiers)", "App.AppNameTips": "(Suggested format xx-yy-zz e.g. apollo-server)", 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 41dae012dd4..1b71868f05e 100644 --- a/apollo-portal/src/main/resources/static/i18n/zh-CN.json +++ b/apollo-portal/src/main/resources/static/i18n/zh-CN.json @@ -11,7 +11,7 @@ "Common.Nav.SystemConfig": "系统参数", "Common.Nav.DeleteApp-Cluster-Namespace": "删除应用、集群、AppNamespace", "Common.Nav.SystemInfo": "系统信息", - "Common.Nav.ConfigExport": "配置导出", + "Common.Nav.ConfigExport": "配置导出导入", "Common.Nav.Logout": "退出", "Common.Department": "部门", "Common.Cluster": "集群", @@ -175,6 +175,8 @@ "Component.Namespace.Master.Items.RequestPermission": "申请配置权限", "Component.Namespace.Master.Items.RequestPermissionTips": "您没有任何配置权限,请申请", "Component.Namespace.Master.Items.DeleteNamespace": "删除Namespace", + "Component.Namespace.Master.Items.ExportNamespace": "导出Namespace", + "Component.Namespace.Master.Items.ImportNamespace": "导入Namespace", "Component.Namespace.Master.Items.NoPermissionTips": "您不是该应用的管理员,也没有该Namespace的编辑或发布权限,无法查看配置信息。", "Component.Namespace.Master.Items.ItemList": "表格", "Component.Namespace.Master.Items.ItemListByText": "文本", @@ -693,9 +695,28 @@ "Config.Diff.DiffCluster": "要比较的集群", "Config.Diff.HasDiffComment": "是否比较注释", "Config.Diff.PleaseChooseTwoCluster": "请至少选择两个集群", - "ConfigExport.Title": "配置导出", - "ConfigExport.TitleTips" : "超级管理员会下载所有应用的配置,普通用户只会下载自己应用的配置", + "ConfigExport.Title": "配置导出导入", + "ConfigExport.TitleTips" : "(通过导出导入配置,把一个集群的数据(应用、集群、Namespace)迁移到另外一个集群)", + "ConfigExport.SelectExportEnv" : "选择导出的环境", + "ConfigExport.SelectImportEnv" : "选择导入的环境", + "ConfigExport.ImportConflictLabel" : "导入时该如何处理已存在的 Namespace", + "ConfigExport.ExportSuccess" : "正在导出数据,数据量大会导致速度慢,请耐心等待", + "ConfigExport.ExportTips" : "数据量大的情况下,导出速度较慢请耐心等待", + "ConfigExport.IgnoreExistedNamespace" : "跳过已存在的 Namespace", + "ConfigExport.OverwriteExistedNamespace" : "覆盖已存在的 Namespace", + "ConfigExport.UploadFile" : "上传导出的文件", + "ConfigExport.UploadFileTip" : "请上传导出的压缩文件", + "ConfigExport.ImportSuccess" : "导入成功", + "ConfigExport.ImportingTip" : "正在导入,请耐心等待。导入完成后,请检查 namespace 的配置是否正确,如果无误再发布 namespace", + "ConfigExport.ImportFailed" : "导入失败", + "ConfigExport.Export" : "导出", + "ConfigExport.Import" : "导入", + "ConfigExport.ImportTips" : "导入完成之后,请检查 namespace 的配置是否正确,检查无误后需要发布才能生效", "ConfigExport.Download": "下载", + "ConfigImport.Title": "导入 Namespace", + "ConfigImport.Tip1": "当配置项冲突时,导入的值将会覆盖已有的值", + "ConfigImport.Tip2": "当配置项不冲突时,则会新增配置项", + "ConfigImport.Tip3": "导入配置项后,需要发布才能生效", "App.CreateProject": "创建应用", "App.AppIdTips": "(应用唯一标识)", "App.AppNameTips": "(建议格式 xx-yy-zz 例:apollo-server)", diff --git a/apollo-portal/src/main/resources/static/img/export.png b/apollo-portal/src/main/resources/static/img/export.png new file mode 100644 index 0000000000000000000000000000000000000000..533c7aae3a45984c36504332ded4195c805a6212 GIT binary patch literal 4425 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 literal 0 HcmV?d00001 diff --git a/apollo-portal/src/main/resources/static/img/import.png b/apollo-portal/src/main/resources/static/img/import.png new file mode 100644 index 0000000000000000000000000000000000000000..9422ee34adea8c3db67900affb3db17c798d1c47 GIT binary patch literal 4180 zcmb`LX*^VK*vBVEjU~ny;ZK}l?6Q_EQ)Y~1A|m^eZG^GZSVOi!W^6GLvQ*aW6cLH= zw}t+;vWt+2u_dxSdGWkB=lYy;-@p6Z=emFAzCPbWxn-z(p5-4F5D0W0r-wD6 z$L`Yua+ZFsu&m6bhcmt=x)@O9fWS`>2+EGbqD>#%S<4AZ5U}8kEelS^qL<*`$OmUd zXRP@1Bh=vMzGqH@#plNOnDoZ1`1PmI&@&i`B_S{b5=bT&iz~W%LXgH9D0T-;RWM|8 zM>U&4ILS6b6{9+Dz_T)Lhi~-={1Y-w^KW^6(q9&AX5TE=jMC-cI*tTE%Jm{Xo&{?< zxG+G$#f;!2G>G)=K?Df=kq;E<3BJ$N(|O)F2^s+;vHjz9R|JH9P1H&i0Rv=_D4ncv zh-R|q6m$`C1~T9MV(_jq0~E=xf>pzSNUDl%wV8<^@c&JoPa0YIab0+855Gi-Rc*VP zu8b0#9ti)luHMjc@WWR5zN-5i$zYV{E{f_Kcrbq5x**nV z+T5JOQ)o&1XF>SDC65t1xQo93?IQQ+{LLK}w3dw&^1?T zRj|Q8GnkJ#T;#m;aXw63PvvNznK-^R4nyKtnr%& zu`!-hq!l({oIPBZREoH<8WXBhtKq(Ci&Ilm9iNZ?MxZc0mBKBvI+zNr(31$K+#8QP z;P|lo^sSRP&QE-?!P7@+$dH60KM1V&x{LREbWfAT8sg=pIJSG+8dX8}X#+Hgr4n0J zu!6ja9jT-6-k5yg5Q@XHE;eJp)LmQ8+cnRe*RQ35T1=>ccC=#sSrk7|J+p1D1kIPC zup_rTf3R~_UHL^jeCm|j8?6Op%pbX=H`Vs$uKi|fI9B{2g?#7L*ig>A@oM+M#eW|b zvd02vJJVwUBL8jI<|Ty2U)P65TMYlc4Wrdu=1_kSzSlAZkmqr|u-tTjcxkoXGkk2W zJ=Pg#_}OHVQz0C9X+HL+r81*+w~`^WG{CiC~oa2MU@cKEd8MYT!2^sF&|Nnys$_g6_1S1|KL0c2PInZ+NjB zjf=Po{tEmfxhUx%GxvOKS0}S~=_qKIQHh&3t#?6^dPq#wT34 z)V+5@vB^ItA#d+?t5M%rr_Wdm`5oV`x`p&ojiLaFmpA@ADWM9P#gqFp=yeuZu!=l) z2SH$GJSmF{Drcpyn2u9CKwPa0KCtS5>u>RD!(-Lvzc|?ckos4{JchpL->Y%Sn`s3z zVM+B8JtEx1gp5M9W&f7)jIL+*Ax;kPx6E|cTwZ!^)KP;h<4(b1r`Vzl52sUyPNL|8 zyX6D%Ro;ourJ?#;^4c;OOn)`4t130^uMj;w*KC;h>~VJ|`5n`rt4@$OZblT`#+&}<1W*iPR)vBFhbU`_1hlh=+^o3Ug6X1 zy}7U4v)A9jrTBs>ztPM_{1UB6x4Og=Kq2>MW(?1#JS3MS5uNPfpfAT+6bvW{qMjW# z9MRqcJ<(mUvMQ^$f4h?u@+=w(CKag3#d~}TV6s}TR(?|*g_SiNQ~*Z;l1ZP2Od!nk7uV7EIjDvSi=)FzYaEpSvfgN?i?NXCbtKk5`0iYg zu*q+svPb}m%mRobrq0LKV%MZY&aR3eAmE-bJ&!lrHWKqUuKjYLu+V2W&O{OMNDMe6 zZupM8w&XvL2FMe08jF4VPsDW(xrBq0t;MAC=}P148N-RJhFs4Yj8RYg`2Yxb&{x7J zXm0;;PIg9uQ06upim#%ZfiC|L)e!b+_h2@JHuqS`*v8&GKl8MAIH>raHQ$VVVVLBB z7RTuwxo)4%&El}0NeSU%rACG1*8_YPfivsYYz1 z_0=bp^~8(d8lu#NucjWg*Y&Y-Mkd0nUVJ-89TwS0wfH-X=s2-Ekw=8YT_MWy%PvBU z)wQe3{`OY80SLWuakp2;)}hBvPBX;XwalM~=+7q!FNbQqS<$uge$wMR22F?#emy*@ zW~!&{oUX=Zg;w*Y_R{1Cz$_E_w>ySj^qHio=JZ(7iyYLI1p|7rmMOJT^k^nVx#oia zp?y9QjD`w78=-8-%JRmCvU}%|wc@`H?CXnfgh)pMy8}8UuQwTmglEtO#X?s&hqKyp zPmBw13P2tK+yYzAIx)i{`}3RD&PAE%&m2kh1B)l@Xes5R0w0M z!+kHT$G;naJ&27A*9kLwdR+-DImhPTt?(h>fnVqSs*`@0utYf?V!}&6@&BYoX%HTT z9HD|sOT8~ZO!CH8s9KiR?RNqLDb_0HQecHg*QPt4EdY-0n52bY2t6P3PIC;q^D;9j z!}+CwL3zR5d`sgI~54qc{ z=-Mr2w2Ue;S!59E&B}OUOfe_eg3*`T`lu@-oozc{+~K$AQLFij@%y&)T}$G%E5ro3 zb}}1Z%)h2dR?fPVk8R>PJmQ8pZFrWC*Lu~ds5Q1bFfUG8FF&iMJ-0KRgBC~(>5jXu z^zBOu5?#Xd^P#B*EvzBO`K;s8$C%fZi5t@OKXHcrGM)}8j6X$klJ;)$?mfN~2LemR z?HKP`1ulK*O4J#wIIAx){l&m<5!kTN=4MC735TYJ=4u=~;-blgA8<0G1|9eoC!RHL zx6KU5!Obq8s#y*UKKHd?qg(I|2|k{3e#T>euHVAfKnwIHq<_`za53@N+=^=!g&(er zDncMmJt+}L>GLEl*tpqF8Y9V4xHH9NnTkA~*#{5OE_o`L;r`^v?|;{_un%)B1^VNO zOoMEuS8`-=#)C#*xF%&aZM z|7Z`(!Ix}q3xjL!!n^%;K7o0zzC8#%w;`eyGRgPk))NuPbZxJ7r7f8Z`1!|5U{(rwIshBrfSD#9;HbH z?=Zi1pCsJ#@zZ@a6ytWCYZ_i-nO?@?Qr&3o&& z;u(Z`D<(ZUG@RoV)SVk2TojrK@j5rQ#Ra9-b40Bl^v+VO&sj5Nmdf=he)1=XA|@^{ zv<(UDHa2;cbYU-AGg;pZLQDGN^L?n ztxKe}jmEokm4=#WEIkffBYl%HsmX6UjH&*rR^?MhPg@4o#*aodbi){2Q|-l&vc zwsjyzzI*yK9*DO{*?i+D*Hagal#7V4?QGSKTp#;+wtZd`2AMQf%r~lFo~bgaNzcMNImg>9|>Jhkk2g zQM$9B*bEAy-Y-xup}o=u-hV7X=cC*Qz8FOd7R)+#kQUOX<`XScKy9myNzG^bO4i$o zqJu#~-htrCj9P8g_x8XLm7|@jIZbP2eYV;1I|X`>D5Wm&~j0N?vv$jg8_-$*+VC1e0FuOF$1(?@MHek e|2{r4oYApA|CUKa^fvuB0SKpUh^@ppkp2gmM3ZU& literal 0 HcmV?d00001 diff --git a/apollo-portal/src/main/resources/static/scripts/controller/ConfigExportController.js b/apollo-portal/src/main/resources/static/scripts/controller/ConfigExportController.js new file mode 100644 index 00000000000..9e044b709b6 --- /dev/null +++ b/apollo-portal/src/main/resources/static/scripts/controller/ConfigExportController.js @@ -0,0 +1,109 @@ +/* + * Copyright 2021 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. + * + */ +config_export_module.controller('ConfigExportController', + ['$scope', '$location', '$window', '$http', '$translate', 'toastr', 'AppService', + 'EnvService', + 'ExportService', + 'AppUtil', + function ($scope, $location, $window, $http, $translate, toastr, AppService, + EnvService, + ExportService, + AppUtil) { + + $scope.conflictAction = 'ignore'; + + EnvService.find_all_envs().then(function (result) { + $scope.exportEnvs = []; + $scope.importEnvs = []; + result.forEach(function (env) { + $scope.exportEnvs.push({name: env, checked: false}); + $scope.importEnvs.push({name: env, checked: false}); + + }); + $(".apollo-container").removeClass("hidden"); + }, function (result) { + toastr.error(AppUtil.errorMsg(result), + $translate.instant('Cluster.LoadingEnvironmentError')); + }); + + $scope.switchChecked = function (env, $event) { + env.checked = !env.checked; + $event.stopPropagation(); + }; + + $scope.toggleEnvCheckedStatus = function (env) { + env.checked = !env.checked; + }; + + $scope.export = function () { + var selectedEnvs = [] + $scope.exportEnvs.forEach(function (env) { + if (env.checked) { + selectedEnvs.push(env.name); + } + }); + + if (selectedEnvs.length === 0) { + toastr.warning($translate.instant('Cluster.PleaseChooseEnvironment')); + return + } + + var selectedEnvStr = selectedEnvs.join(","); + $window.location.href = '/configs/export?envs=' + selectedEnvStr; + + toastr.success($translate.instant('ConfigExport.ExportSuccess')); + }; + + $scope.import = function () { + var selectedEnvs = [] + $scope.importEnvs.forEach(function (env) { + if (env.checked) { + selectedEnvs.push(env.name); + } + }); + + if (selectedEnvs.length === 0) { + toastr.warning($translate.instant('Cluster.PleaseChooseEnvironment')); + return + } + + var selectedEnvStr = selectedEnvs.join(","); + var file = document.getElementById("fileUpload").files[0]; + + if (file == null) { + toastr.warning($translate.instant('ConfigExport.UploadFileTip')) + return + } + + var form = new FormData(); + form.append('file', file); + $http({ + method: 'POST', + url: '/configs/import?envs=' + selectedEnvStr + "&conflictAction=" + + $scope.conflictAction, + data: form, + headers: {'Content-Type': undefined}, + transformRequest: angular.identity + }).success(function (data) { + toastr.success(data, $translate.instant('ConfigExport.ImportSuccess')) + }).error(function (data) { + toastr.error(data, $translate.instant('ConfigExport.ImportFailed')) + }) + toastr.info($translate.instant('ConfigExport.ImportingTip')) + } + + }]); diff --git a/apollo-portal/src/main/resources/static/scripts/directive/import-namespace-modal-directive.js b/apollo-portal/src/main/resources/static/scripts/directive/import-namespace-modal-directive.js new file mode 100644 index 00000000000..5c0f3642722 --- /dev/null +++ b/apollo-portal/src/main/resources/static/scripts/directive/import-namespace-modal-directive.js @@ -0,0 +1,81 @@ +/* + * Copyright 2021 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. + * + */ +directive_module.directive('importnamespacemodal', importNamespaceModalDirective); + +function importNamespaceModalDirective($window, $q, $translate, $http, toastr, AppUtil, EventManager, + PermissionService, UserService, NamespaceService) { + return { + restrict: 'E', + templateUrl: AppUtil.prefixPath() + '/views/component/import-namespace-modal.html', + transclude: true, + replace: true, + scope: { + env: '=' + }, + link: function (scope) { + + scope.doImportNamespace = doImportNamespace; + + EventManager.subscribe(EventManager.EventType.PRE_IMPORT_NAMESPACE, function (context) { + scope.toImportNamespace = context.namespace; + + showImportNamespaceConfirmDialog(); + + }); + + function showImportNamespaceConfirmDialog() { + AppUtil.showModal('#importNamespaceModal'); + } + + function doImportNamespace() { + var file = document.getElementById("fileUpload").files[0]; + + if (file == null) { + toastr.warning($translate.instant('ConfigExport.UploadFileTip')) + return + } + + var toImportNamespace = scope.toImportNamespace; + var form = new FormData(); + form.append('file', file); + $http({ + method: 'POST', + url: '/apps/' + toImportNamespace.baseInfo.appId + '/envs/' + scope.env + '/clusters/' + + toImportNamespace.baseInfo.clusterName + + '/namespaces/' + toImportNamespace.baseInfo.namespaceName + "/items/import", + data: form, + headers: {'Content-Type': undefined}, + transformRequest: angular.identity + }).success(function (data) { + toastr.success(data, $translate.instant('ConfigExport.ImportSuccess')) + + //refresh namespace + EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE, + { + namespace: toImportNamespace + }); + }).error(function (data) { + toastr.error(data, $translate.instant('ConfigExport.ImportFailed')) + }) + } + + } + } +} + + + diff --git a/apollo-portal/src/main/resources/static/scripts/directive/namespace-panel-directive.js b/apollo-portal/src/main/resources/static/scripts/directive/namespace-panel-directive.js index fb54964b737..f37bfd39e5c 100644 --- a/apollo-portal/src/main/resources/static/scripts/directive/namespace-panel-directive.js +++ b/apollo-portal/src/main/resources/static/scripts/directive/namespace-panel-directive.js @@ -88,6 +88,8 @@ function directive($window, $translate, toastr, AppUtil, EventManager, Permissio scope.editRuleItem = editRuleItem; scope.deleteNamespace = deleteNamespace; + scope.exportNamespace = exportNamespace; + scope.importNamespace = importNamespace; var subscriberId = EventManager.subscribe(EventManager.EventType.UPDATE_GRAY_RELEASE_RULES, function (context) { @@ -961,6 +963,16 @@ function directive($window, $translate, toastr, AppUtil, EventManager, Permissio EventManager.emit(EventManager.EventType.PRE_DELETE_NAMESPACE, { namespace: namespace }); } + function exportNamespace(namespace) { + $window.location.href = + AppUtil.prefixPath() + '/apps/' + scope.appId + "/envs/" + scope.env + "/clusters/" + scope.cluster + + "/namespaces/" + namespace.baseInfo.namespaceName + "/items/export" + } + + function importNamespace(namespace) { + EventManager.emit(EventManager.EventType.PRE_IMPORT_NAMESPACE, { namespace: namespace }); + } + //theme: https://github.com/ajaxorg/ace-builds/tree/ba3b91e04a5aa559d56ac70964f9054baa0f4caf/src-min scope.aceConfig = { $blockScrolling: Infinity, diff --git a/apollo-portal/src/main/resources/static/scripts/services/EventManager.js b/apollo-portal/src/main/resources/static/scripts/services/EventManager.js index 20b121e0de8..f6bbc9feb67 100644 --- a/apollo-portal/src/main/resources/static/scripts/services/EventManager.js +++ b/apollo-portal/src/main/resources/static/scripts/services/EventManager.js @@ -125,6 +125,7 @@ appService.service('EventManager', [function () { PUBLISH_DENY: 'publish_deny', EMERGENCY_PUBLISH: 'emergency_publish', PRE_DELETE_NAMESPACE: 'pre_delete_namespace', + PRE_IMPORT_NAMESPACE: 'pre_import_namespace', DELETE_NAMESPACE: 'delete_namespace', DELETE_NAMESPACE_FAILED: 'delete_namespace_failed', CHANGE_ENV_CLUSTER: "change_env_cluster", diff --git a/apollo-portal/src/main/resources/static/scripts/services/ExportService.js b/apollo-portal/src/main/resources/static/scripts/services/ExportService.js new file mode 100644 index 00000000000..929cdc76467 --- /dev/null +++ b/apollo-portal/src/main/resources/static/scripts/services/ExportService.js @@ -0,0 +1,42 @@ +/* + * Copyright 2021 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('ExportService', ['$resource', '$q', function ($resource, $q) { + var resource = $resource('', {}, { + importConfig: { + method: 'POST', + url: '/import', + headers: {'Content-Type': undefined}, + } + }); + return { + importConfig: function (envs, file) { + var form = new FormData(); + form.append('file', file); + var d = $q.defer(); + resource.importConfig({ + data: form, + envs: envs, + }, + function (result) { + d.resolve(result); + }, function (result) { + d.reject(result); + }); + return d.promise; + } + } +}]); diff --git a/apollo-portal/src/main/resources/static/views/component/import-namespace-modal.html b/apollo-portal/src/main/resources/static/views/component/import-namespace-modal.html new file mode 100644 index 00000000000..45239996b1e --- /dev/null +++ b/apollo-portal/src/main/resources/static/views/component/import-namespace-modal.html @@ -0,0 +1,55 @@ + 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 08424c8d4b1..a708a209de6 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 @@ -112,6 +112,16 @@