Merge pull request #1254 from KeRan213539/exportAndImport

#1115 feature: configs export/import and clone
This commit is contained in:
Fury Zhu 2019-07-02 12:42:30 +08:00 committed by GitHub
commit 402b31adb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1638 additions and 36 deletions

View File

@ -18,6 +18,8 @@ package com.alibaba.nacos.config.server.controller;
import com.alibaba.nacos.config.server.constant.Constants;
import com.alibaba.nacos.config.server.exception.NacosException;
import com.alibaba.nacos.config.server.model.*;
import com.alibaba.nacos.config.server.result.ResultBuilder;
import com.alibaba.nacos.config.server.result.code.ResultCodeEnum;
import com.alibaba.nacos.config.server.service.AggrWhitelist;
import com.alibaba.nacos.config.server.service.ConfigDataChangeEvent;
import com.alibaba.nacos.config.server.service.ConfigSubService;
@ -25,15 +27,21 @@ import com.alibaba.nacos.config.server.service.PersistService;
import com.alibaba.nacos.config.server.service.trace.ConfigTraceService;
import com.alibaba.nacos.config.server.utils.*;
import com.alibaba.nacos.config.server.utils.event.EventDispatcher;
import com.google.common.base.Joiner;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -41,8 +49,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import static com.alibaba.nacos.core.utils.SystemUtils.LOCAL_IP;
@ -57,6 +64,14 @@ public class ConfigController {
private static final Logger log = LoggerFactory.getLogger(ConfigController.class);
private static final String NAMESPACE_PUBLIC_KEY = "public";
public static final String EXPORT_CONFIG_FILE_NAME = "nacos_config_export_";
public static final String EXPORT_CONFIG_FILE_NAME_EXT = ".zip";
public static final String EXPORT_CONFIG_FILE_NAME_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private final transient ConfigServletInner inner;
private final transient PersistService persistService;
@ -382,4 +397,192 @@ public class ConfigController {
return rr;
}
}
@RequestMapping(params = "export=true", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<byte[]> exportConfig(HttpServletRequest request,
HttpServletResponse response,
@RequestParam("group") String group,
@RequestParam(value = "appName", required = false) String appName,
@RequestParam(value = "tenant", required = false,
defaultValue = StringUtils.EMPTY) String tenant,
@RequestParam(value = "ids", required = false)List<Long> ids) {
ids.removeAll(Collections.singleton(null));
String idsStr = Joiner.on(",").join(ids);
List<ConfigInfo> dataList = persistService.findAllConfigInfo4Export(group, tenant, appName, idsStr);
List<ZipUtils.ZipItem> zipItemList = new ArrayList<>();
StringBuilder metaData = null;
for(ConfigInfo ci : dataList){
if(StringUtils.isNotBlank(ci.getAppName())){
// Handle appName
if(metaData == null){
metaData = new StringBuilder();
}
String metaDataId = ci.getDataId();
if(metaDataId.contains(".")){
metaDataId = metaDataId.substring(0,metaDataId.lastIndexOf("."))
+ "~" + metaDataId.substring(metaDataId.lastIndexOf(".") + 1);
}
metaData.append(ci.getGroup()).append(".").append(metaDataId).append(".app=")
// Fixed use of "\r\n" here
.append(ci.getAppName()).append("\r\n");
}
String itemName = ci.getGroup() + "/" + ci.getDataId() ;
zipItemList.add(new ZipUtils.ZipItem(itemName, ci.getContent()));
}
if(metaData != null){
zipItemList.add(new ZipUtils.ZipItem(".meta.yml", metaData.toString()));
}
HttpHeaders headers = new HttpHeaders();
String fileName=EXPORT_CONFIG_FILE_NAME + DateFormatUtils.format(new Date(), EXPORT_CONFIG_FILE_NAME_DATE_FORMAT) + EXPORT_CONFIG_FILE_NAME_EXT;
headers.add("Content-Disposition", "attachment;filename="+fileName);
return new ResponseEntity<byte[]>(ZipUtils.zip(zipItemList), headers, HttpStatus.OK);
}
@RequestMapping(params = "import=true", method = RequestMethod.POST)
@ResponseBody
public RestResult<Map<String, Object>> importAndPublishConfig(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "src_user", required = false) String srcUser,
@RequestParam(value = "namespace", required = false) String namespace,
@RequestParam(value = "policy", defaultValue = "ABORT")
SameConfigPolicy policy,
MultipartFile file) throws NacosException {
Map<String, Object> failedData = new HashMap<>(4);
if(StringUtils.isNotBlank(namespace)){
if(persistService.tenantInfoCountByTenantId(namespace) <= 0){
failedData.put("succCount", 0);
return ResultBuilder.buildResult(ResultCodeEnum.NAMESPACE_NOT_EXIST, failedData);
}
}
List<ConfigInfo> configInfoList = null;
try {
ZipUtils.UnZipResult unziped = ZipUtils.unzip(file.getBytes());
ZipUtils.ZipItem metaDataZipItem = unziped.getMetaDataItem();
Map<String, String> metaDataMap = new HashMap<>(16);
if(metaDataZipItem != null){
String metaDataStr = metaDataZipItem.getItemData();
String[] metaDataArr = metaDataStr.split("\r\n");
for(String metaDataItem : metaDataArr){
String[] metaDataItemArr = metaDataItem.split("=");
if(metaDataItemArr.length != 2){
failedData.put("succCount", 0);
return ResultBuilder.buildResult(ResultCodeEnum.METADATA_ILLEGAL, failedData);
}
metaDataMap.put(metaDataItemArr[0], metaDataItemArr[1]);
}
}
List<ZipUtils.ZipItem> itemList = unziped.getZipItemList();
if(itemList != null && !itemList.isEmpty()){
configInfoList = new ArrayList<>(itemList.size());
for(ZipUtils.ZipItem item : itemList){
String[] groupAdnDataId = item.getItemName().split("/");
if(!item.getItemName().contains("/") || groupAdnDataId.length != 2){
failedData.put("succCount", 0);
return ResultBuilder.buildResult(ResultCodeEnum.DATA_VALIDATION_FAILED, failedData);
}
String group = groupAdnDataId[0];
String dataId = groupAdnDataId[1];
String tempDataId = dataId;
if(tempDataId.contains(".")){
tempDataId = tempDataId.substring(0, tempDataId.lastIndexOf("."))
+ "~" + tempDataId.substring(tempDataId.lastIndexOf(".") + 1);
}
String metaDataId = group + "." + tempDataId + ".app";
ConfigInfo ci = new ConfigInfo();
ci.setTenant(namespace);
ci.setGroup(group);
ci.setDataId(dataId);
ci.setContent(item.getItemData());
if(metaDataMap.get(metaDataId) != null){
ci.setAppName(metaDataMap.get(metaDataId));
}
configInfoList.add(ci);
}
}
} catch (IOException e) {
failedData.put("succCount", 0);
log.error("parsing data failed", e);
return ResultBuilder.buildResult(ResultCodeEnum.PARSING_DATA_FAILED, failedData);
}
if (configInfoList == null || configInfoList.isEmpty()) {
failedData.put("succCount", 0);
return ResultBuilder.buildResult(ResultCodeEnum.DATA_EMPTY, failedData);
}
final String srcIp = RequestUtil.getRemoteIp(request);
String requestIpApp = RequestUtil.getAppName(request);
final Timestamp time = TimeUtils.getCurrentTime();
Map<String, Object> saveResult = persistService.batchInsertOrUpdate(configInfoList, srcUser, srcIp,
null, time, false, policy);
for (ConfigInfo configInfo : configInfoList) {
EventDispatcher.fireEvent(new ConfigDataChangeEvent(false, configInfo.getDataId(), configInfo.getGroup(),
configInfo.getTenant(), time.getTime()));
ConfigTraceService.logPersistenceEvent(configInfo.getDataId(), configInfo.getGroup(),
configInfo.getTenant(), requestIpApp, time.getTime(),
LOCAL_IP, ConfigTraceService.PERSISTENCE_EVENT_PUB, configInfo.getContent());
}
return ResultBuilder.buildSuccessResult("导入成功", saveResult);
}
@RequestMapping(params = "clone=true", method = RequestMethod.GET)
@ResponseBody
public RestResult<Map<String, Object>> cloneConfig(HttpServletRequest request,
HttpServletResponse response,
@RequestParam(value = "src_user", required = false) String srcUser,
@RequestParam(value = "tenant", required = true) String namespace,
@RequestParam(value = "ids", required = true) List<Long> ids,
@RequestParam(value = "policy", defaultValue = "ABORT")
SameConfigPolicy policy) throws NacosException {
Map<String, Object> failedData = new HashMap<>(4);
if(NAMESPACE_PUBLIC_KEY.equals(namespace.toLowerCase())){
namespace = "";
} else if(persistService.tenantInfoCountByTenantId(namespace) <= 0){
failedData.put("succCount", 0);
return ResultBuilder.buildResult(ResultCodeEnum.NAMESPACE_NOT_EXIST, failedData);
}
ids.removeAll(Collections.singleton(null));
String idsStr = Joiner.on(",").join(ids);
List<ConfigInfo> queryedDataList = persistService.findAllConfigInfo4Export(null, null, null, idsStr);
if(queryedDataList == null || queryedDataList.isEmpty()){
failedData.put("succCount", 0);
return ResultBuilder.buildResult(ResultCodeEnum.DATA_EMPTY, failedData);
}
List<ConfigInfo> configInfoList4Clone = new ArrayList<>(queryedDataList.size());
for(ConfigInfo ci : queryedDataList){
ConfigInfo ci4save = new ConfigInfo();
ci4save.setTenant(namespace);
ci4save.setGroup(ci.getGroup());
ci4save.setDataId(ci.getDataId());
ci4save.setContent(ci.getContent());
if(StringUtils.isNotBlank(ci.getAppName())){
ci4save.setAppName(ci.getAppName());
}
configInfoList4Clone.add(ci4save);
}
if (configInfoList4Clone.isEmpty()) {
failedData.put("succCount", 0);
return ResultBuilder.buildResult(ResultCodeEnum.DATA_EMPTY, failedData);
}
final String srcIp = RequestUtil.getRemoteIp(request);
String requestIpApp = RequestUtil.getAppName(request);
final Timestamp time = TimeUtils.getCurrentTime();
Map<String, Object> saveResult = persistService.batchInsertOrUpdate(configInfoList4Clone, srcUser, srcIp,
null, time, false, policy);
for (ConfigInfo configInfo : configInfoList4Clone) {
EventDispatcher.fireEvent(new ConfigDataChangeEvent(false, configInfo.getDataId(), configInfo.getGroup(),
configInfo.getTenant(), time.getTime()));
ConfigTraceService.logPersistenceEvent(configInfo.getDataId(), configInfo.getGroup(),
configInfo.getTenant(), requestIpApp, time.getTime(),
LOCAL_IP, ConfigTraceService.PERSISTENCE_EVENT_PUB, configInfo.getContent());
}
return ResultBuilder.buildSuccessResult("导入成功", saveResult);
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.config.server.enums;
/**
* @author klw
* @ClassName: FileTypeEnum
* @Description: config file type enum
* @date 2019/7/1 10:21
*/
public enum FileTypeEnum {
/**
* @author klw
* @Description: yaml file
*/
YML("yaml"),
/**
* @author klw
* @Description: yaml file
*/
YAML("yaml"),
/**
* @author klw
* @Description: text file
*/
TXT("text"),
/**
* @author klw
* @Description: text file
*/
TEXT("text"),
/**
* @author klw
* @Description: json file
*/
JSON("json"),
/**
* @author klw
* @Description: xml file
*/
XML("xml"),
/**
* @author klw
* @Description: html file
*/
HTM("html"),
/**
* @author klw
* @Description: html file
*/
HTML("html"),
/**
* @author klw
* @Description: properties file
*/
PROPERTIES("properties")
;
/**
* @author klw
* @Description: file type corresponding to file extension
*/
private String fileType;
FileTypeEnum(String fileType){
this.fileType = fileType;
}
public String getFileType(){
return this.fileType;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.config.server.model;
/**
* @author klw
* @ClassName: SameConfigPolicy
* @Description: processing policy of the same configuration
* @date 2019/5/21 10:55
*/
public enum SameConfigPolicy {
/**
* @Description: abort import on duplicate
*/
ABORT,
/**
* @Description: skipping on duplicate
*/
SKIP,
/**
* @Description: overwrite on duplicate
*/
OVERWRITE
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.config.server.result;
import com.alibaba.nacos.config.server.model.RestResult;
import com.alibaba.nacos.config.server.result.code.ResultCodeEnum;
import com.alibaba.nacos.config.server.result.core.IResultCode;
import org.springframework.util.Assert;
/**
* @author klw
* @ClassName: ResultBuilder
* @Description: util for generating com.alibaba.nacos.config.server.model.RestResult
* @date 2019/6/28 14:47
*/
public class ResultBuilder {
public static <T extends Object> RestResult<T> buildResult(IResultCode resultCode, T resultData){
Assert.notNull(resultCode, "the resultCode can not be null");
RestResult<T> rr = new RestResult<>(resultCode.getCode(), resultCode.getCodeMsg(), resultData);
return rr;
}
public static <T extends Object> RestResult<T> buildSuccessResult(T resultData){
return buildResult(ResultCodeEnum.SUCCESS, resultData);
}
public static <T extends Object> RestResult<T> buildSuccessResult(String successMsg, T resultData){
RestResult<T> rr = buildResult(ResultCodeEnum.SUCCESS, resultData);
rr.setMessage(successMsg);
return rr;
}
public static <T extends Object> RestResult<T> buildSuccessResult(){
return buildResult(ResultCodeEnum.SUCCESS, null);
}
public static <T extends Object> RestResult<T> buildSuccessResult(String successMsg){
RestResult<T> rr = buildResult(ResultCodeEnum.SUCCESS, null);
rr.setMessage(successMsg);
return rr;
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.config.server.result.code;
import com.alibaba.nacos.config.server.result.core.IResultCode;
/**
* @author klw
* @ClassName: ResultCodeEnum
* @Description: result code enum
* @date 2019/6/28 14:43
*/
public enum ResultCodeEnum implements IResultCode {
/** common code **/
SUCCESS(200, "处理成功"),
ERROR(500, "服务器内部错误"),
/** config use 100001 ~ 100999 **/
NAMESPACE_NOT_EXIST(100001, "目标 namespace 不存在"),
METADATA_ILLEGAL(100002, "导入的元数据非法"),
DATA_VALIDATION_FAILED(100003, "未读取到合法数据"),
PARSING_DATA_FAILED(100004, "解析数据失败"),
DATA_EMPTY(100005, "导入的文件数据为空"),
;
private int code;
private String msg;
ResultCodeEnum(int code, String codeMsg){
this.code = code;
this.msg = codeMsg;
}
@Override
public int getCode() {
return code;
}
@Override
public String getCodeMsg() {
return msg;
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.config.server.result.core;
/**
* @author klw
* @ClassName: IResultCode
* @Description: result code enum needs to be implemented this interface
* @date 2019/6/28 14:44
*/
public interface IResultCode {
/**
* get the result code
*
* @author klw
* @Date 2019/6/28 14:56
* @Param []
* @return java.lang.String
*/
int getCode();
/**
* get the result code's message
*
* @author klw
* @Date 2019/6/28 14:56
* @Param []
* @return java.lang.String
*/
String getCodeMsg();
}

View File

@ -15,25 +15,15 @@
*/
package com.alibaba.nacos.config.server.service;
import static com.alibaba.nacos.config.server.utils.LogUtil.defaultLog;
import static com.alibaba.nacos.config.server.utils.LogUtil.fatalLog;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import com.alibaba.nacos.config.server.enums.FileTypeEnum;
import com.alibaba.nacos.config.server.exception.NacosException;
import com.alibaba.nacos.config.server.model.*;
import com.alibaba.nacos.config.server.utils.LogUtil;
import com.alibaba.nacos.config.server.utils.MD5;
import com.alibaba.nacos.config.server.utils.PaginationHelper;
import com.alibaba.nacos.config.server.utils.ParamUtils;
import com.alibaba.nacos.config.server.utils.event.EventDispatcher;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
@ -53,12 +43,17 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import com.alibaba.nacos.config.server.utils.LogUtil;
import com.alibaba.nacos.config.server.utils.MD5;
import com.alibaba.nacos.config.server.utils.PaginationHelper;
import com.alibaba.nacos.config.server.utils.event.EventDispatcher;
import com.google.common.collect.Lists;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.sql.*;
import java.util.*;
import java.util.Map.Entry;
import static com.alibaba.nacos.config.server.utils.LogUtil.defaultLog;
import static com.alibaba.nacos.config.server.utils.LogUtil.fatalLog;
/**
* 数据库服务提供ConfigInfo在数据库的存取<br> 3.0开始增加数据版本号, 并将物理删除改为逻辑删除<br> 3.0增加数据库切换功能
@ -76,6 +71,16 @@ public class PersistService {
private DataSourceService dataSourceService;
private static final String SQL_FIND_ALL_CONFIG_INFO = "select data_id,group_id,tenant_id,app_name,content,type from config_info";
private static final String SQL_TENANT_INFO_COUNT_BY_TENANT_ID = "select count(1) from tenant_info where tenant_id = ?";
/**
* @author klw
* @Description: constant variables
*/
public static final String SPOT = ".";
@PostConstruct
public void init() {
dataSourceService = dynamicDataSource.getDataSource();
@ -1476,7 +1481,6 @@ public class PersistService {
*
* @param pageNo 页码(必须大于0)
* @param pageSize 每页大小(必须大于0)
* @param group
* @return ConfigInfo对象的集合
*/
public Page<ConfigInfo> findConfigInfoByApp(final int pageNo,
@ -3288,6 +3292,145 @@ public class PersistService {
return true;
}
/**
* query all configuration information according to group, appName, tenant (for export)
*
* @param group
* @return Collection of ConfigInfo objects
*/
public List<ConfigInfo> findAllConfigInfo4Export(final String group, final String tenant,
final String appName, final String ids) {
String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
StringBuilder where = new StringBuilder(" where ");
List<String> paramList = new ArrayList<>();
if(StringUtils.isNotBlank(ids)){
where.append(" id in (").append(ids).append(") ");
} else {
where.append(" tenant_id=? ");
paramList.add(tenantTmp);
if (StringUtils.isNotBlank(group)) {
where.append(" and group_id=? ");
paramList.add(group);
}
if (StringUtils.isNotBlank(appName)) {
where.append(" and app_name=? ");
paramList.add(appName);
}
}
try {
return this.jt.query(SQL_FIND_ALL_CONFIG_INFO + where, paramList.toArray(), CONFIG_INFO_ROW_MAPPER);
} catch (CannotGetJdbcConnectionException e) {
fatalLog.error("[db-error] " + e.toString(), e);
throw e;
}
}
/**
* batch operation,insert or update
* the format of the returned:
* succCount: number of successful imports
* skipCount: number of import skips (only with skip for the same configs)
* failData: import failed data (only with abort for the same configs)
* skipData: data skipped at import (only with skip for the same configs)
*/
public Map<String, Object> batchInsertOrUpdate(List<ConfigInfo> configInfoList, String srcUser, String srcIp,
Map<String, Object> configAdvanceInfo, Timestamp time, boolean notify, SameConfigPolicy policy) throws NacosException {
int succCount = 0;
int skipCount = 0;
List<Map<String, String>> failData = null;
List<Map<String, String>> skipData = null;
for (int i = 0; i < configInfoList.size(); i++) {
ConfigInfo configInfo = configInfoList.get(i);
try {
ParamUtils.checkParam(configInfo.getDataId(), configInfo.getGroup(), "datumId", configInfo.getContent());
} catch (NacosException e) {
defaultLog.error("data verification failed", e);
throw e;
}
ConfigInfo configInfo2Save = new ConfigInfo(configInfo.getDataId(), configInfo.getGroup(),
configInfo.getTenant(), configInfo.getAppName(), configInfo.getContent());
// simple judgment of file type based on suffix
String type = null;
if (configInfo.getDataId().contains(SPOT)) {
String extName = configInfo.getDataId().substring(configInfo.getDataId().lastIndexOf(SPOT) + 1).toLowerCase();
try{
type = FileTypeEnum.valueOf(extName).getFileType();
}catch (Exception ex){
type = FileTypeEnum.TEXT.getFileType();
}
}
if (configAdvanceInfo == null) {
configAdvanceInfo = new HashMap<>(16);
}
configAdvanceInfo.put("type", type);
try {
addConfigInfo(srcIp, srcUser, configInfo2Save, time, configAdvanceInfo, notify);
succCount++;
} catch (DataIntegrityViolationException ive) {
// uniqueness constraint conflict
if (SameConfigPolicy.ABORT.equals(policy)) {
failData = new ArrayList<>();
skipData = new ArrayList<>();
Map<String, String> faileditem = new HashMap<>(2);
faileditem.put("dataId", configInfo2Save.getDataId());
faileditem.put("group", configInfo2Save.getGroup());
failData.add(faileditem);
for (int j = (i + 1); j < configInfoList.size(); j++) {
ConfigInfo skipConfigInfo = configInfoList.get(j);
Map<String, String> skipitem = new HashMap<>(2);
skipitem.put("dataId", skipConfigInfo.getDataId());
skipitem.put("group", skipConfigInfo.getGroup());
skipData.add(skipitem);
}
break;
} else if (SameConfigPolicy.SKIP.equals(policy)) {
skipCount++;
if (skipData == null) {
skipData = new ArrayList<>();
}
Map<String, String> skipitem = new HashMap<>(2);
skipitem.put("dataId", configInfo2Save.getDataId());
skipitem.put("group", configInfo2Save.getGroup());
skipData.add(skipitem);
} else if (SameConfigPolicy.OVERWRITE.equals(policy)) {
succCount++;
updateConfigInfo(configInfo2Save, srcIp, srcUser, time, configAdvanceInfo, notify);
}
}
}
Map<String, Object> result = new HashMap<>(4);
result.put("succCount", succCount);
result.put("skipCount", skipCount);
if (failData != null && !failData.isEmpty()) {
result.put("failData", failData);
}
if (skipData != null && !skipData.isEmpty()) {
result.put("skipData", skipData);
}
return result;
}
/**
* query tenantInfo (namespace) existence based by tenantId
*
* @param tenantId
* @return count by tenantId
*/
public int tenantInfoCountByTenantId(String tenantId) {
Assert.hasText(tenantId, "tenantId can not be null");
List<String> paramList = new ArrayList<>();
paramList.add(tenantId);
Integer result = this.jt.queryForObject(SQL_TENANT_INFO_COUNT_BY_TENANT_ID, paramList.toArray(), Integer.class);
if (result == null) {
return 0;
}
return result.intValue();
}
static final TenantInfoRowMapper TENANT_INFO_ROW_MAPPER = new TenantInfoRowMapper();
static final UserRowMapper USER_ROW_MAPPER = new UserRowMapper();

View File

@ -15,6 +15,11 @@
*/
package com.alibaba.nacos.config.server.utils;
import java.io.IOException;
import java.io.InputStream;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -49,4 +54,9 @@ public class JSONUtils {
return mapper.readValue(s, typeReference);
}
public static <T> T deserializeObject(InputStream src, TypeReference<?> typeReference)
throws IOException {
return mapper.readValue(src, typeReference);
}
}

View File

@ -0,0 +1,142 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.config.server.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* @author klw
* @Description: ZipUtils for import and export
* @date 2019/5/14 16:59
*/
public class ZipUtils {
private static final Logger log = LoggerFactory.getLogger(ZipUtils.class);
public static class ZipItem{
private String itemName;
private String itemData;
public ZipItem(String itemName, String itemData) {
this.itemName = itemName;
this.itemData = itemData;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public String getItemData() {
return itemData;
}
public void setItemData(String itemData) {
this.itemData = itemData;
}
}
public static class UnZipResult{
private List<ZipItem> zipItemList;
private ZipItem metaDataItem;
public UnZipResult(List<ZipItem> zipItemList, ZipItem metaDataItem) {
this.zipItemList = zipItemList;
this.metaDataItem = metaDataItem;
}
public List<ZipItem> getZipItemList() {
return zipItemList;
}
public void setZipItemList(List<ZipItem> zipItemList) {
this.zipItemList = zipItemList;
}
public ZipItem getMetaDataItem() {
return metaDataItem;
}
public void setMetaDataItem(ZipItem metaDataItem) {
this.metaDataItem = metaDataItem;
}
}
public static byte[] zip(List<ZipItem> source){
byte[] result = null;
try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); ZipOutputStream zipOut = new ZipOutputStream(byteOut)){
for (ZipItem item : source) {
zipOut.putNextEntry(new ZipEntry(item.getItemName()));
zipOut.write(item.getItemData().getBytes(StandardCharsets.UTF_8));
}
zipOut.flush();
zipOut.finish();
result = byteOut.toByteArray();
} catch (IOException e) {
log.error("an error occurred while compressing data.", e);
}
return result;
}
public static UnZipResult unzip(byte[] source) {
List<ZipItem> itemList = new ArrayList<>();
ZipItem metaDataItem = null;
try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(source))) {
ZipEntry entry;
while ((entry = zipIn.getNextEntry()) != null && !entry.isDirectory()) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int offset;
while ((offset = zipIn.read(buffer)) != -1) {
out.write(buffer, 0, offset);
}
if(".meta.yml".equals(entry.getName())){
metaDataItem = new ZipItem(entry.getName(), out.toString("UTF-8"));
} else {
itemList.add(new ZipItem(entry.getName(), out.toString("UTF-8")));
}
} catch (IOException e) {
log.error("unzip error", e);
}
}
} catch (IOException e) {
log.error("unzip error", e);
}
return new UnZipResult(itemList, metaDataItem);
}
}

View File

@ -258,5 +258,6 @@
<appender-ref ref="startLog"/>
</logger>
</included>

View File

@ -252,6 +252,45 @@ const I18N_CONF = {
pleaseEnterTag: 'Enter Tag',
application: 'Application',
operation: 'Operation',
export: 'Export query results',
import: 'Import',
uploadBtn: 'Upload File',
importSucc: 'The import was successful',
importAbort: 'Import abort',
importSuccBegin: 'The import was successful,with ',
importSuccEnd: 'configuration items imported',
importFail: 'Import failed',
importDataValidationError: 'No legitimate data was read, please check the imported data file.',
metadataIllegal: 'The imported metadata file is illegal',
namespaceNotExist: 'namespace does not exist',
abortImport: 'Abort import',
skipImport: 'Skip',
overwriteImport: 'Overwrite',
importRemind:
'File upload will be imported directly into the configuration, please be careful!',
samePreparation: 'Same preparation',
targetNamespace: 'Target namespace',
conflictConfig: 'Conflict-detected configuration items',
failureEntries: 'Failure entries',
unprocessedEntries: 'Unprocessed entries',
skippedEntries: 'skipped entries',
exportSelected: 'Export selected configs',
clone: 'Clone',
exportSelectedAlertTitle: 'Export config',
exportSelectedAlertContent: 'please select the configuration to export',
cloneSucc: 'The clone was successful',
cloneAbort: 'Clone abort',
cloneSuccBegin: 'The clone was successful,with ',
cloneSuccEnd: 'configuration items cloned',
cloneFail: 'Clone failed',
getNamespaceFailed: 'get the namespace failed',
startCloning: 'Start Clone',
cloningConfiguration: 'Clone config',
source: 'Source :',
configurationNumber: 'Items:',
target: 'Target:',
selectNamespace: 'Select Namespace',
selectedEntry: '| Selected Entry',
},
NewConfig: {
newListingMain: 'Create Configuration',

View File

@ -251,6 +251,44 @@ const I18N_CONF = {
pleaseEnterTag: '请输入标签',
application: '归属应用:',
operation: '操作',
export: '导出查询结果',
import: '导入配置',
uploadBtn: '上传文件',
importSucc: '导入成功',
importAbort: '导入终止',
importSuccBegin: '导入成功,导入了',
importSuccEnd: '项配制',
importFail: '导入失败',
importDataValidationError: '未读取到合法数据请检查导入的数据文件',
metadataIllegal: '导入的元数据文件非法',
namespaceNotExist: 'namespace 不存在',
abortImport: '终止导入',
skipImport: '跳过',
overwriteImport: '覆盖',
importRemind: '文件上传后将直接导入配置请务必谨慎操作',
samePreparation: '相同配制',
targetNamespace: '目标空间',
conflictConfig: '检测到冲突的配置项',
failureEntries: '失败的条目',
unprocessedEntries: '未处理的条目',
skippedEntries: '跳过的条目',
exportSelected: '导出选中的配制',
clone: '克隆',
exportSelectedAlertTitle: '配制导出',
exportSelectedAlertContent: '请选择要导出的配制',
cloneSucc: '克隆成功',
cloneAbort: '克隆终止',
cloneSuccBegin: '克隆成功,克隆了',
cloneSuccEnd: '项配制',
cloneFail: '克隆失败',
getNamespaceFailed: '获取命名空间失败',
startCloning: '开始克隆',
cloningConfiguration: '克隆配制',
source: '源空间',
configurationNumber: '配置数量',
target: '目标空间',
selectNamespace: '请选择命名空间',
selectedEntry: '| 选中的条目',
},
NewConfig: {
newListingMain: '新建配置',

View File

@ -29,6 +29,9 @@ import {
Pagination,
Select,
Table,
Grid,
Upload,
Message,
} from '@alifd/next';
import BatchHandle from 'components/BatchHandle';
import RegionGroup from 'components/RegionGroup';
@ -41,6 +44,8 @@ import './index.scss';
import { LANGUAGE_KEY } from '../../../constants';
const { Panel } = Collapse;
const { Row, Col } = Grid;
const configsTableSelected = new Map();
@ConfigProvider.config
class ConfigurationManagement extends React.Component {
@ -667,6 +672,376 @@ class ConfigurationManagement extends React.Component {
});
}
exportData() {
let url = `v1/cs/configs?export=true&group=${this.group}&tenant=${getParams(
'namespace'
)}&appName=${this.appName}&ids=`;
window.location.href = url;
}
exportSelectedData() {
const { locale = {} } = this.props;
if (configsTableSelected.size === 0) {
Dialog.alert({
title: locale.exportSelectedAlertTitle,
content: locale.exportSelectedAlertContent,
});
} else {
let idsStr = '';
configsTableSelected.forEach((value, key, map) => {
idsStr = `${idsStr + key},`;
});
let url = `v1/cs/configs?export=true&group=&tenant=&appName=&ids=${idsStr}`;
window.location.href = url;
}
}
cloneSelectedDataConfirm() {
const { locale = {} } = this.props;
const self = this;
self.field.setValue('sameConfigPolicy', 'ABORT');
self.field.setValue('cloneTargetSpace', undefined);
if (configsTableSelected.size === 0) {
Dialog.alert({
title: locale.exportSelectedAlertTitle,
content: locale.exportSelectedAlertContent,
});
return;
}
request({
url: 'v1/console/namespaces?namespaceId=',
beforeSend() {
self.openLoading();
},
success(data) {
if (!data || data.code !== 200 || !data.data) {
Dialog.alert({
title: locale.getNamespaceFailed,
content: locale.getNamespaceFailed,
});
}
let namespaces = data.data;
let namespaceSelectData = [];
namespaces.forEach(item => {
if (self.state.nownamespace_id !== item.namespace) {
let dataItem = {};
if (item.namespaceShowName === 'public') {
dataItem.label = 'public | public';
dataItem.value = 'public';
} else {
dataItem.label = `${item.namespaceShowName} | ${item.namespace}`;
dataItem.value = item.namespace;
}
namespaceSelectData.push(dataItem);
}
});
const cloneConfirm = Dialog.confirm({
title: locale.cloningConfiguration,
footer: false,
content: (
<div>
<div style={{ marginBottom: 10 }}>
<span style={{ color: '#999', marginRight: 5 }}>{locale.source}</span>
<span style={{ color: '#49D2E7' }}>{self.state.nownamespace_name} </span>|{' '}
{self.state.nownamespace_id}
</div>
<div style={{ marginBottom: 10 }}>
<span style={{ color: '#999', marginRight: 5 }}>{locale.configurationNumber}</span>
<span style={{ color: '#49D2E7' }}>{configsTableSelected.size} </span>
{locale.selectedEntry}
</div>
<div style={{ marginBottom: 10 }}>
<span style={{ color: 'red', marginRight: 2, marginLeft: -10 }}>{'*'}</span>
<span style={{ color: '#999', marginRight: 5 }}>{locale.target}</span>
<Select
style={{ width: 450 }}
placeholder={locale.selectNamespace}
size={'medium'}
hasArrow
showSearch
hasClear={false}
mode="single"
dataSource={namespaceSelectData}
onChange={(value, actionType, item) => {
if (value) {
document.getElementById('cloneTargetSpaceSelectErr').style.display = 'none';
self.field.setValue('cloneTargetSpace', value);
}
}}
/>
<br />
<span id={'cloneTargetSpaceSelectErr'} style={{ color: 'red', display: 'none' }}>
{locale.selectNamespace}
</span>
</div>
<div style={{ marginBottom: 10 }}>
<span style={{ color: '#999', marginRight: 5 }}>{locale.samePreparation}:</span>
<Select
style={{ width: 130 }}
size={'medium'}
hasArrow
mode="single"
filterLocal={false}
defaultValue={'ABORT'}
dataSource={[
{
label: locale.abortImport,
value: 'ABORT',
},
{
label: locale.skipImport,
value: 'SKIP',
},
{
label: locale.overwriteImport,
value: 'OVERWRITE',
},
]}
hasClear={false}
onChange={(value, actionType, item) => {
if (value) {
self.field.setValue('sameConfigPolicy', value);
}
}}
/>
</div>
<div>
<Button
type={'primary'}
style={{ marginRight: 10 }}
onClick={() => {
if (!self.field.getValue('cloneTargetSpace')) {
document.getElementById('cloneTargetSpaceSelectErr').style.display = 'inline';
return;
} else {
document.getElementById('cloneTargetSpaceSelectErr').style.display = 'none';
}
let idsStr = '';
configsTableSelected.forEach((value, key, map) => {
idsStr = `${idsStr + key},`;
});
let cloneTargetSpace = self.field.getValue('cloneTargetSpace');
let sameConfigPolicy = self.field.getValue('sameConfigPolicy');
request({
url: `v1/cs/configs?clone=true&tenant=${cloneTargetSpace}&policy=${sameConfigPolicy}&ids=${idsStr}`,
beforeSend() {
self.openLoading();
},
success(ret) {
self.processImportAndCloneResult(ret, locale, cloneConfirm, false);
},
error(data) {
self.setState({
dataSource: [],
total: 0,
currentPage: 0,
});
},
complete() {
self.closeLoading();
},
});
}}
data-spm-click={'gostr=/aliyun;locaid=doClone'}
>
{locale.startCloning}
</Button>
</div>
</div>
),
});
},
error(data) {
self.setState({
dataSource: [],
total: 0,
currentPage: 0,
});
},
complete() {
self.closeLoading();
},
});
}
processImportAndCloneResult(ret, locale, confirm, isImport) {
const resultCode = ret.code;
console.log(ret);
if (resultCode === 200) {
confirm.hide();
if (ret.data.failData && ret.data.failData.length > 0) {
Dialog.alert({
title: isImport ? locale.importAbort : locale.cloneAbort,
content: (
<div style={{ width: '500px' }}>
<h4>
{locale.conflictConfig}{ret.data.failData[0].group}/{ret.data.failData[0].dataId}
</h4>
<div style={{ marginTop: 20 }}>
<h5>
{locale.failureEntries}: {ret.data.failData.length}
</h5>
<Table dataSource={ret.data.failData}>
<Table.Column title="Data Id" dataIndex="dataId" />
<Table.Column title="Group" dataIndex="group" />
</Table>
</div>
<div>
<h5>
{locale.unprocessedEntries}: {ret.data.skipData ? ret.data.skipData.length : 0}
</h5>
<Table dataSource={ret.data.skipData}>
<Table.Column title="Data Id" dataIndex="dataId" />
<Table.Column title="Group" dataIndex="group" />
</Table>
</div>
</div>
),
});
} else if (ret.data.skipCount && ret.data.skipCount > 0) {
Dialog.alert({
title: isImport ? locale.importSucc : locale.cloneSucc,
content: (
<div style={{ width: '500px' }}>
<div>
<h5>
{locale.skippedEntries}: {ret.data.skipData.length}
</h5>
<Table dataSource={ret.data.skipData}>
<Table.Column title="Data Id" dataIndex="dataId" />
<Table.Column title="Group" dataIndex="group" />
</Table>
</div>
</div>
),
});
} else {
let message = `${isImport ? locale.importSuccBegin : locale.cloneSuccBegin}${
ret.data.succCount
}${isImport ? locale.importSuccEnd : locale.cloneSuccEnd}`;
Message.success(message);
}
this.getData();
} else {
let alertContent = isImport ? locale.importFailMsg : locale.cloneFailMsg;
if (resultCode === 100001) {
alertContent = locale.namespaceNotExist;
}
if (resultCode === 100002) {
alertContent = locale.metadataIllegal;
}
if (resultCode === 100003 || resultCode === 100004 || resultCode === 100005) {
alertContent = locale.importDataValidationError;
}
Dialog.alert({
title: isImport ? locale.importFail : locale.cloneFail,
content: alertContent,
});
}
}
importData() {
const { locale = {} } = this.props;
const self = this;
self.field.setValue('sameConfigPolicy', 'ABORT');
const uploadProps = {
accept: 'application/zip',
action: `v1/cs/configs?import=true&namespace=${getParams('namespace')}`,
data: {
policy: self.field.getValue('sameConfigPolicy'),
},
beforeUpload(file, options) {
options.data = {
policy: self.field.getValue('sameConfigPolicy'),
};
return options;
},
onSuccess(ret) {
self.processImportAndCloneResult(ret.response, locale, importConfirm, true);
},
onError(err) {
Dialog.alert({
title: locale.importFail,
content: locale.importDataValidationError,
});
},
};
const importConfirm = Dialog.confirm({
title: locale.import,
footer: false,
content: (
<div>
<div style={{ marginBottom: 10 }}>
<span style={{ color: '#999', marginRight: 5 }}>{locale.targetNamespace}:</span>
<span style={{ color: '#49D2E7' }}>{this.state.nownamespace_name} </span>|{' '}
{this.state.nownamespace_id}
</div>
<div style={{ marginBottom: 10 }}>
<span style={{ color: '#999', marginRight: 5 }}>{locale.samePreparation}:</span>
<Select
style={{ width: 130 }}
size={'medium'}
hasArrow
mode="single"
filterLocal={false}
defaultValue={'ABORT'}
dataSource={[
{
label: locale.abortImport,
value: 'ABORT',
},
{
label: locale.skipImport,
value: 'SKIP',
},
{
label: locale.overwriteImport,
value: 'OVERWRITE',
},
]}
hasClear={false}
onChange={function(value, actionType, item) {
self.field.setValue('sameConfigPolicy', value);
}}
/>
</div>
<div style={{ marginBottom: 10 }}>
<Icon type="prompt" style={{ color: '#FFA003', marginRight: '10px' }} />
{locale.importRemind}
</div>
<div>
<Upload
name={'file'}
listType="text"
data-spm-click={'gostr=/aliyun;locaid=configsImport'}
{...uploadProps}
>
<Button type="primary">{locale.uploadBtn}</Button>
</Upload>
</div>
</div>
),
});
}
configsTableOnSelect(selected, record, records) {
if (selected) {
configsTableSelected.set(record.id, record);
} else {
configsTableSelected.delete(record.id);
}
}
configsTableOnSelectAll(selected, records) {
if (selected) {
records.forEach((record, i) => {
configsTableSelected.set(record.id, record);
});
} else {
configsTableSelected.clear();
}
}
render() {
const { locale = {} } = this.props;
return (
@ -792,6 +1167,26 @@ class ConfigurationManagement extends React.Component {
/>
</div>
</Form.Item>
<Form.Item label={''}>
<Button
type={'primary'}
style={{ marginRight: 10 }}
onClick={this.exportData.bind(this)}
data-spm-click={'gostr=/aliyun;locaid=configsExport'}
>
{locale.export}
</Button>
</Form.Item>
<Form.Item label={''}>
<Button
type={'primary'}
style={{ marginRight: 10 }}
onClick={this.importData.bind(this)}
data-spm-click={'gostr=/aliyun;locaid=configsExport'}
>
{locale.import}
</Button>
</Form.Item>
<br />
<Form.Item
style={this.inApp ? { display: 'none' } : {}}
@ -844,6 +1239,10 @@ class ConfigurationManagement extends React.Component {
fixedHeader
maxBodyHeight={400}
ref={'dataTable'}
rowSelection={{
onSelect: this.configsTableOnSelect,
onSelectAll: this.configsTableOnSelectAll,
}}
>
<Table.Column title={'Data Id'} dataIndex={'dataId'} />
<Table.Column title={'Group'} dataIndex={'group'} />
@ -856,6 +1255,25 @@ class ConfigurationManagement extends React.Component {
</Table>
{this.state.dataSource.length > 0 && (
<div style={{ marginTop: 10, overflow: 'hidden' }}>
<div style={{ float: 'left' }}>
<Button
type={'primary'}
style={{ marginLeft: 60, marginRight: 10 }}
onClick={this.exportSelectedData.bind(this)}
data-spm-click={'gostr=/aliyun;locaid=configsExport'}
>
{locale.exportSelected}
</Button>
<Button
type={'primary'}
style={{ marginRight: 10 }}
onClick={this.cloneSelectedDataConfirm.bind(this)}
data-spm-click={'gostr=/aliyun;locaid=configsClone'}
>
{locale.clone}
</Button>
</div>
<div style={{ float: 'right' }}>
<Pagination
style={{ float: 'right' }}
pageSizeList={[10, 20, 30]}
@ -867,6 +1285,7 @@ class ConfigurationManagement extends React.Component {
onChange={this.changePage.bind(this)}
/>
</div>
</div>
)}
</div>
<ShowCodeing ref={this.showcode} />

View File

@ -723,6 +723,12 @@
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.github.keran213539</groupId>
<artifactId>commonOkHttp</artifactId>
<version>0.4.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -93,6 +93,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.keran213539</groupId>
<artifactId>commonOkHttp</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,290 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.test.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.client.config.http.HttpAgent;
import com.alibaba.nacos.client.config.http.MetricsHttpAgent;
import com.alibaba.nacos.client.config.http.ServerHttpAgent;
import com.alibaba.nacos.client.config.impl.HttpSimpleClient;
import com.alibaba.nacos.config.server.Config;
import com.alibaba.nacos.config.server.utils.ZipUtils;
import com.github.keran213539.commonOkHttp.CommonOkHttpClient;
import com.github.keran213539.commonOkHttp.CommonOkHttpClientBuilder;
import com.github.keran213539.commonOkHttp.UploadByteFile;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import java.net.HttpURLConnection;
import java.util.*;
/**
* @author klw
* @date 2019/5/23 15:26
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Config.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ConfigExportAndImportAPI_ITCase {
private static final long TIME_OUT = 2000;
private static final String CONFIG_CONTROLLER_PATH = "/v1/cs/configs";
private CommonOkHttpClient httpClient = new CommonOkHttpClientBuilder().build();
@LocalServerPort
private int port;
private String SERVER_ADDR = null;
private HttpAgent agent = null;
@Before
public void setUp() throws Exception {
SERVER_ADDR = "http://127.0.0.1"+":"+ port + "/nacos";
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1"+":"+port);
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
agent.start();
Map<String, String> prarm = new HashMap<>(7);
prarm.put("dataId", "testNoAppname1.yml");
prarm.put("group", "DEFAULT_GROUP");
prarm.put("content", "test: test");
prarm.put("desc", "testNoAppname1");
prarm.put("type", "yaml");
Assert.assertEquals("true", httpClient.post(SERVER_ADDR + CONFIG_CONTROLLER_PATH , prarm,null));
prarm.put("dataId", "testNoAppname2.txt");
prarm.put("group", "TEST1_GROUP");
prarm.put("content", "test: test");
prarm.put("desc", "testNoAppname2");
prarm.put("type", "text");
Assert.assertEquals("true", httpClient.post(SERVER_ADDR + CONFIG_CONTROLLER_PATH , prarm,null));
prarm.put("dataId", "testHasAppname1.properties");
prarm.put("group", "DEFAULT_GROUP");
prarm.put("content", "test.test1.value=test");
prarm.put("desc", "testHasAppname1");
prarm.put("type", "properties");
prarm.put("appName", "testApp1");
Assert.assertEquals("true", httpClient.post(SERVER_ADDR + CONFIG_CONTROLLER_PATH , prarm,null));
}
@After
public void cleanup(){
HttpSimpleClient.HttpResult result;
try {
List<String> params2 = Arrays.asList("dataId", "testNoAppname1.yml", "group", "DEFAULT_GROUP", "beta", "false");
result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params2, agent.getEncode(), TIME_OUT);
Assert.assertEquals(HttpURLConnection.HTTP_OK, result.code);
List<String> params3 = Arrays.asList("dataId", "testNoAppname2.txt", "group", "TEST1_GROUP", "beta", "false");
result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params3, agent.getEncode(), TIME_OUT);
Assert.assertEquals(HttpURLConnection.HTTP_OK, result.code);
List<String> params4 = Arrays.asList("dataId", "testHasAppname1.properties", "group", "DEFAULT_GROUP", "beta", "false");
result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params4, agent.getEncode(), TIME_OUT);
Assert.assertEquals(HttpURLConnection.HTTP_OK, result.code);
List<String> params5 = Arrays.asList("dataId", "test1.yml", "group", "TEST_IMPORT", "beta", "false");
result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params4, agent.getEncode(), TIME_OUT);
Assert.assertEquals(HttpURLConnection.HTTP_OK, result.code);
List<String> params6 = Arrays.asList("dataId", "test2.txt", "group", "TEST_IMPORT", "beta", "false");
result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params4, agent.getEncode(), TIME_OUT);
Assert.assertEquals(HttpURLConnection.HTTP_OK, result.code);
List<String> params7 = Arrays.asList("dataId", "test3.properties", "group", "TEST_IMPORT", "beta", "false");
result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params4, agent.getEncode(), TIME_OUT);
Assert.assertEquals(HttpURLConnection.HTTP_OK, result.code);
} catch (Exception e) {
Assert.fail();
}
}
@Test(timeout = 3*TIME_OUT)
public void testExportByIds(){
String getDataUrl = "?search=accurate&dataId=&group=&appName=&config_tags=&pageNo=1&pageSize=10&tenant=&namespaceId=";
String queryResult = httpClient.get(SERVER_ADDR + CONFIG_CONTROLLER_PATH + getDataUrl, null);
JSONObject resultObj = JSON.parseObject(queryResult);
JSONArray resultConfigs = resultObj.getJSONArray("pageItems");
JSONObject config1 = resultConfigs.getJSONObject(0);
JSONObject config2 = resultConfigs.getJSONObject(1);
String exportByIdsUrl = "?export=true&tenant=&group=&appName=&ids=" + config1.getIntValue("id")
+ "," + config2.getIntValue("id");
byte[] zipData = httpClient.download(SERVER_ADDR + CONFIG_CONTROLLER_PATH + exportByIdsUrl, null);
ZipUtils.UnZipResult unZiped = ZipUtils.unzip(zipData);
List<ZipUtils.ZipItem> zipItemList = unZiped.getZipItemList();
Assert.assertEquals(2, zipItemList.size());
String config1Name = config1.getString("group") + "/" + config1.getString("dataId");
String config2Name = config2.getString("group") + "/" + config2.getString("dataId");
for(ZipUtils.ZipItem zipItem : zipItemList){
if(!(config1Name.equals(zipItem.getItemName()) || config2Name.equals(zipItem.getItemName()))){
Assert.fail();
}
}
}
@Test(timeout = 3*TIME_OUT)
public void testExportByGroup(){
String getDataUrl = "?search=accurate&dataId=&group=DEFAULT_GROUP&appName=&config_tags=&pageNo=1&pageSize=10&tenant=&namespaceId=";
String queryResult = httpClient.get(SERVER_ADDR + CONFIG_CONTROLLER_PATH + getDataUrl, null);
JSONObject resultObj = JSON.parseObject(queryResult);
JSONArray resultConfigs = resultObj.getJSONArray("pageItems");
Assert.assertEquals(2, resultConfigs.size());
JSONObject config1 = resultConfigs.getJSONObject(0);
JSONObject config2 = resultConfigs.getJSONObject(1);
String exportByIdsUrl = "?export=true&tenant=&group=DEFAULT_GROUP&appName=&ids=";
byte[] zipData = httpClient.download(SERVER_ADDR + CONFIG_CONTROLLER_PATH + exportByIdsUrl, null);
ZipUtils.UnZipResult unZiped = ZipUtils.unzip(zipData);
List<ZipUtils.ZipItem> zipItemList = unZiped.getZipItemList();
Assert.assertEquals(2, zipItemList.size());
String config1Name = config1.getString("group") + "/" + config1.getString("dataId");
String config2Name = config2.getString("group") + "/" + config2.getString("dataId");
for(ZipUtils.ZipItem zipItem : zipItemList){
if(!(config1Name.equals(zipItem.getItemName()) || config2Name.equals(zipItem.getItemName()))){
Assert.fail();
}
}
// verification metadata
Map<String, String> metaData = processMetaData(unZiped.getMetaDataItem());
String metaDataName = packageMetaName("DEFAULT_GROUP", "testHasAppname1.properties");
String appName = metaData.get(metaDataName);
Assert.assertNotNull(appName);
Assert.assertEquals("testApp1", appName);
}
@Test(timeout = 3*TIME_OUT)
public void testExportByGroupAndApp(){
String getDataUrl = "?search=accurate&dataId=&group=DEFAULT_GROUP&appName=testApp1&config_tags=&pageNo=1&pageSize=10&tenant=&namespaceId=";
String queryResult = httpClient.get(SERVER_ADDR + CONFIG_CONTROLLER_PATH + getDataUrl, null);
JSONObject resultObj = JSON.parseObject(queryResult);
JSONArray resultConfigs = resultObj.getJSONArray("pageItems");
Assert.assertEquals(1, resultConfigs.size());
JSONObject config1 = resultConfigs.getJSONObject(0);
String exportByIdsUrl = "?export=true&tenant=&group=DEFAULT_GROUP&appName=testApp1&ids=";
byte[] zipData = httpClient.download(SERVER_ADDR + CONFIG_CONTROLLER_PATH + exportByIdsUrl, null);
ZipUtils.UnZipResult unZiped = ZipUtils.unzip(zipData);
List<ZipUtils.ZipItem> zipItemList = unZiped.getZipItemList();
Assert.assertEquals(1, zipItemList.size());
String config1Name = config1.getString("group") + "/" + config1.getString("dataId");
for(ZipUtils.ZipItem zipItem : zipItemList){
if(!config1Name.equals(zipItem.getItemName())){
Assert.fail();
}
}
// verification metadata
Map<String, String> metaData = processMetaData(unZiped.getMetaDataItem());
String metaDataName = packageMetaName("DEFAULT_GROUP", "testHasAppname1.properties");
String appName = metaData.get(metaDataName);
Assert.assertNotNull(appName);
Assert.assertEquals("testApp1", appName);
}
@Test(timeout = 3*TIME_OUT)
public void testExportAll(){
String exportByIdsUrl = "?export=true&tenant=&group=&appName=&ids=";
byte[] zipData = httpClient.download(SERVER_ADDR + CONFIG_CONTROLLER_PATH + exportByIdsUrl, null);
ZipUtils.UnZipResult unZiped = ZipUtils.unzip(zipData);
List<ZipUtils.ZipItem> zipItemList = unZiped.getZipItemList();
String config1Name = "DEFAULT_GROUP/testNoAppname1.yml";
String config2Name = "TEST1_GROUP/testNoAppname2.txt";
String config3Name = "DEFAULT_GROUP/testHasAppname1.properties";
int successCount = 0;
for(ZipUtils.ZipItem zipItem : zipItemList){
if(config1Name.equals(zipItem.getItemName()) || config2Name.equals(zipItem.getItemName()) ||
config3Name.equals(zipItem.getItemName())){
successCount++;
}
}
Assert.assertEquals(3, successCount);
// verification metadata
Map<String, String> metaData = processMetaData(unZiped.getMetaDataItem());
String metaDataName = packageMetaName("DEFAULT_GROUP", "testHasAppname1.properties");
String appName = metaData.get(metaDataName);
Assert.assertNotNull(appName);
Assert.assertEquals("testApp1", appName);
}
@Test(timeout = 3*TIME_OUT)
public void testImport(){
List<ZipUtils.ZipItem> zipItemList = new ArrayList<>(3);
zipItemList.add(new ZipUtils.ZipItem("TEST_IMPORT/test1.yml", "test: test1"));
zipItemList.add(new ZipUtils.ZipItem("TEST_IMPORT/test2.txt", "test: test1"));
zipItemList.add(new ZipUtils.ZipItem("TEST_IMPORT/test3.properties", "test.test1.value=test"));
String metaDataStr = "TEST_IMPORT.test2~txt.app=testApp1\r\nTEST_IMPORT.test3~properties.app=testApp2";
zipItemList.add(new ZipUtils.ZipItem(".meta.yml", metaDataStr));
String importUrl = "?import=true&namespace=";
Map<String, String> importPrarm = new HashMap<>(1);
importPrarm.put("policy", "OVERWRITE");
UploadByteFile uploadByteFile = new UploadByteFile();
uploadByteFile.setFileName("testImport.zip");
uploadByteFile.setFileBytes(ZipUtils.zip(zipItemList));
uploadByteFile.setMediaType("application/zip");
uploadByteFile.setPrarmName("file");
httpClient.post(SERVER_ADDR + CONFIG_CONTROLLER_PATH + importUrl, importPrarm, Collections.singletonList(uploadByteFile), null);
String getDataUrl = "?search=accurate&dataId=&group=TEST_IMPORT&appName=&config_tags=&pageNo=1&pageSize=10&tenant=&namespaceId=";
String queryResult = httpClient.get(SERVER_ADDR + CONFIG_CONTROLLER_PATH + getDataUrl, null);
JSONObject resultObj = JSON.parseObject(queryResult);
JSONArray resultConfigs = resultObj.getJSONArray("pageItems");
Assert.assertEquals(3, resultConfigs.size());
for(int i = 0; i < resultConfigs.size(); i++){
JSONObject config = resultConfigs.getJSONObject(i);
if(!"TEST_IMPORT".equals(config.getString("group"))){
Assert.fail();
}
switch (config.getString("dataId")){
case "test1.yml":
case "test2.txt":
case "test3.properties":
break;
default:
Assert.fail();
}
}
}
private Map<String, String> processMetaData(ZipUtils.ZipItem metaDataZipItem){
Map<String, String> metaDataMap = new HashMap<>(16);
if(metaDataZipItem != null){
String metaDataStr = metaDataZipItem.getItemData();
String[] metaDataArr = metaDataStr.split("\r\n");
for(String metaDataItem : metaDataArr){
String[] metaDataItemArr = metaDataItem.split("=");
Assert.assertEquals(2, metaDataItemArr.length);
metaDataMap.put(metaDataItemArr[0], metaDataItemArr[1]);
}
}
return metaDataMap;
}
private String packageMetaName(String group, String dataId){
String tempDataId = dataId;
if(tempDataId.contains(".")){
tempDataId = tempDataId.substring(0, tempDataId.lastIndexOf("."))
+ "~" + tempDataId.substring(tempDataId.lastIndexOf(".") + 1);
}
return group + "." + tempDataId + ".app";
}
}