提交 7a27efe2 编写于 作者: J Jason Song

Merge pull request #167 from lepdou/lepdou_master

配置namespace之间同步
......@@ -20,6 +20,7 @@ INSERT INTO AppNamespace (AppId, Name) VALUES ('100003173', 'fx.apollo.portal');
INSERT INTO AppNamespace (AppID, Name) VALUES ('fxhermesproducer', 'fx.hermes.producer');
INSERT INTO Namespace (Id, AppId, ClusterName, NamespaceName) VALUES (1, '100003171', 'default', 'application');
INSERT INTO Namespace (Id, AppId, ClusterName, NamespaceName) VALUES (5, '100003171', 'cluster1', 'application');
INSERT INTO Namespace (Id, AppId, ClusterName, NamespaceName) VALUES (2, 'fxhermesproducer', 'default', 'fx.hermes.producer');
INSERT INTO Namespace (Id, AppId, ClusterName, NamespaceName) VALUES (3, '100003172', 'default', 'application');
INSERT INTO Namespace (Id, AppId, ClusterName, NamespaceName) VALUES (4, '100003173', 'default', 'application');
......@@ -27,7 +28,7 @@ INSERT INTO Namespace (Id, AppId, ClusterName, NamespaceName) VALUES (4, '100003
INSERT INTO Item (NamespaceId, `Key`, Value, Comment) VALUES (1, 'k1', 'v1', 'comment1');
INSERT INTO Item (NamespaceId, `Key`, Value, Comment) VALUES (1, 'k2', 'v2', 'comment2');
INSERT INTO Item (NamespaceId, `Key`, Value, Comment) VALUES (2, 'k3', 'v3', 'comment3');
INSERT INTO Item (NamespaceId, `Key`, Value, Comment) VALUES (5, 'k3', 'v4', 'comment4');
INSERT INTO Item (NamespaceId, `Key`, Value, Comment, LineNum) VALUES (5, 'k1', 'v4', 'comment4',1);
INSERT INTO RELEASE (ReleaseKey, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES ('TEST-RELEASE-KEY', 'REV1','First Release','100003171', 'default', 'application', '{"k1":"v1"}');
......@@ -24,6 +24,7 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
......
......@@ -14,6 +14,8 @@ import com.ctrip.apollo.portal.entity.form.NamespaceReleaseModel;
import com.ctrip.apollo.portal.service.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
......@@ -102,7 +104,20 @@ public class ConfigController {
throw new BadRequestException("request model is invalid");
}
return configService.compare(model.getSyncItems(), model.getSyncToNamespaces());
return configService.compare(model.getSyncToNamespaces(), model.getSyncItems());
}
@RequestMapping(value = "/namespaces/{namespaceName}/items", method = RequestMethod.PUT, consumes = {
"application/json"})
public ResponseEntity<Void> update(@RequestBody NamespaceSyncModel model){
if (model == null){
throw new BadRequestException("request payload shoud not be null");
}
if (model.isInvalid()) {
throw new BadRequestException("request model is invalid");
}
configService.syncItems(model.getSyncToNamespaces(), model.getSyncItems());
return ResponseEntity.status(HttpStatus.OK).build();
}
}
......@@ -47,4 +47,14 @@ public class NamespaceIdentifer implements Verifiable{
public boolean isInvalid() {
return StringUtils.isContainEmpty(env, clusterName, namespaceName);
}
@Override
public String toString() {
return "NamespaceIdentifer{" +
"appId='" + appId + '\'' +
", env='" + env + '\'' +
", clusterName='" + clusterName + '\'' +
", namespaceName='" + namespaceName + '\'' +
'}';
}
}
......@@ -54,11 +54,6 @@ public class ConfigService {
/**
* load cluster all namespace info with items
*
* @param appId
* @param env
* @param clusterName
* @return
*/
public List<NamespaceVO> findNampspaces(String appId, Env env, String clusterName) {
......@@ -76,7 +71,7 @@ public class ConfigService {
namespaceVOs.add(namespaceVO);
} catch (Exception e) {
logger.error("parse namespace error. app id:{}, env:{}, clusterName:{}, namespace:{}",
appId, env, clusterName, namespace.getNamespaceName(), e);
appId, env, clusterName, namespace.getNamespaceName(), e);
throw e;
}
}
......@@ -85,7 +80,7 @@ public class ConfigService {
}
@SuppressWarnings("unchecked")
private NamespaceVO parseNamespace(String appId, Env env, String clusterName, NamespaceDTO namespace) {
private NamespaceVO parseNamespace(String appId, Env env, String clusterName, NamespaceDTO namespace) {
NamespaceVO namespaceVO = new NamespaceVO();
namespaceVO.setNamespace(namespace);
......@@ -100,10 +95,10 @@ public class ConfigService {
try {
release = releaseAPI.loadLatestRelease(appId, env, clusterName, namespaceName);
releaseItems = gson.fromJson(release.getConfigurations(), Map.class);
}catch (HttpClientErrorException e){
if (e.getStatusCode() == HttpStatus.NOT_FOUND){
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
logger.warn(ExceptionUtils.toString(e));
}else {
} else {
throw e;
}
}
......@@ -142,7 +137,7 @@ public class ConfigService {
/**
* parse config text and update config items
*
*
* @return parse result
*/
public void updateConfigItemByText(NamespaceTextModel model) {
......@@ -154,8 +149,8 @@ public class ConfigService {
String configText = model.getConfigText();
ItemChangeSets changeSets = resolver.resolve(namespaceId, configText,
itemAPI.findItems(appId, env, clusterName, namespaceName));
if (changeSets.isEmpty()){
itemAPI.findItems(appId, env, clusterName, namespaceName));
if (changeSets.isEmpty()) {
return;
}
try {
......@@ -171,85 +166,107 @@ public class ConfigService {
/**
* createRelease config items
*
* @return
*/
public ReleaseDTO createRelease(NamespaceReleaseModel model) {
return releaseAPI.release(model.getAppId(), model.getEnv(), model.getClusterName(),
model.getNamespaceName(), model.getReleaseBy(), model.getReleaseComment());
model.getNamespaceName(), model.getReleaseBy(), model.getReleaseComment());
}
public List<ItemDTO> findItems(String appId, Env env, String clusterName, String namespaceName){
public List<ItemDTO> findItems(String appId, Env env, String clusterName, String namespaceName) {
return itemAPI.findItems(appId, env, clusterName, namespaceName);
}
public List<ItemDiffs> compare(List<ItemDTO> sourceItems, List<NamespaceIdentifer> comparedNamespaces){
public void syncItems(List<NamespaceIdentifer> comparedNamespaces, List<ItemDTO> sourceItems){
List<ItemDiffs> itemDiffs = compare(comparedNamespaces, sourceItems);
for (ItemDiffs itemDiff: itemDiffs){
NamespaceIdentifer namespaceIdentifer = itemDiff.getNamespace();
try {
itemAPI
.updateItems(namespaceIdentifer.getAppId(), namespaceIdentifer.getEnv(),
namespaceIdentifer.getClusterName(),
namespaceIdentifer.getNamespaceName(), itemDiff.getDiffs());
} catch (HttpClientErrorException e) {
logger.error("sync items error. namespace:{}", namespaceIdentifer);
throw new ServiceException(String.format("sync item error. env:%s, clusterName:%s", namespaceIdentifer.getEnv(),
namespaceIdentifer.getClusterName()), e);
}
}
}
public List<ItemDiffs> compare(List<NamespaceIdentifer> comparedNamespaces, List<ItemDTO> sourceItems) {
List<ItemDiffs> result = new LinkedList<>();
String appId, clusterName, namespaceName;
Env env;
for (NamespaceIdentifer namespace: comparedNamespaces){
appId = namespace.getAppId();
clusterName = namespace.getClusterName();
namespaceName = namespace.getNamespaceName();
env = namespace.getEnv();
NamespaceDTO namespaceDTO = null;
try {
namespaceDTO = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName);
} catch (NotFoundException e){
logger.warn("namespace not exist. appId:{}, env:{}, clusterName:{}, namespaceName:{}", appId, env, clusterName,
namespaceName);
throw new BadRequestException(String.format(
"namespace not exist. appId:%s, env:%s, clusterName:%s, namespaceName:%s", appId, env, clusterName,
namespaceName));
}
for (NamespaceIdentifer namespace : comparedNamespaces) {
ItemDiffs itemDiffs = new ItemDiffs(namespace);
ItemChangeSets changeSets = new ItemChangeSets();
itemDiffs.setDiffs(changeSets);
List<ItemDTO>
targetItems =
itemAPI.findItems(namespace.getAppId(), namespace.getEnv(),
namespace.getClusterName(), namespace.getNamespaceName());
long namespaceId = namespaceDTO.getId();
if (CollectionUtils.isEmpty(targetItems)){//all source items is added
int lineNum = 1;
for (ItemDTO sourceItem: sourceItems){
changeSets.addCreateItem(buildItem(namespaceId, lineNum++, sourceItem));
}
}else {
Map<String, ItemDTO> keyMapItem = BeanUtils.mapByKey("key", targetItems);
String key,sourceValue,sourceComment;
ItemDTO targetItem = null;
int maxLineNum = targetItems.size();//append to last
for (ItemDTO sourceItem: sourceItems){
key = sourceItem.getKey();
sourceValue = sourceItem.getValue();
sourceComment = sourceItem.getComment();
targetItem = keyMapItem.get(key);
if (targetItem == null) {//added items
changeSets.addCreateItem(buildItem(namespaceId, ++maxLineNum, sourceItem));
}else if (!sourceValue.equals(targetItem.getValue()) || !sourceComment.equals(targetItem.getComment())){//modified items
targetItem.setValue(sourceValue);
targetItem.setComment(sourceComment);
changeSets.addUpdateItem(targetItem);
}
}
}
itemDiffs.setDiffs(parseChangeSets(namespace, sourceItems));
result.add(itemDiffs);
}
return result;
}
private ItemDTO buildItem(long namespaceId, int lineNum, ItemDTO sourceItem){
private long getNamespaceId(NamespaceIdentifer namespaceIdentifer) {
String appId = namespaceIdentifer.getAppId();
String clusterName = namespaceIdentifer.getClusterName();
String namespaceName = namespaceIdentifer.getNamespaceName();
Env env = namespaceIdentifer.getEnv();
NamespaceDTO namespaceDTO = null;
try {
namespaceDTO = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName);
} catch (NotFoundException e) {
logger.warn("namespace not exist. appId:{}, env:{}, clusterName:{}, namespaceName:{}", appId, env, clusterName,
namespaceName);
throw new BadRequestException(String.format(
"namespace not exist. appId:%s, env:%s, clusterName:%s, namespaceName:%s", appId, env, clusterName,
namespaceName));
}
return namespaceDTO.getId();
}
private ItemChangeSets parseChangeSets(NamespaceIdentifer namespace, List<ItemDTO> sourceItems){
ItemChangeSets changeSets = new ItemChangeSets();
List<ItemDTO>
targetItems =
itemAPI.findItems(namespace.getAppId(), namespace.getEnv(),
namespace.getClusterName(), namespace.getNamespaceName());
long namespaceId = getNamespaceId(namespace);
if (CollectionUtils.isEmpty(targetItems)) {//all source items is added
int lineNum = 1;
for (ItemDTO sourceItem : sourceItems) {
changeSets.addCreateItem(buildItem(namespaceId, lineNum++, sourceItem));
}
} else {
Map<String, ItemDTO> keyMapItem = BeanUtils.mapByKey("key", targetItems);
String key, sourceValue, sourceComment;
ItemDTO targetItem = null;
int maxLineNum = targetItems.size();//append to last
for (ItemDTO sourceItem : sourceItems) {
key = sourceItem.getKey();
sourceValue = sourceItem.getValue();
sourceComment = sourceItem.getComment();
targetItem = keyMapItem.get(key);
if (targetItem == null) {//added items
changeSets.addCreateItem(buildItem(namespaceId, ++maxLineNum, sourceItem));
} else if (!sourceValue.equals(targetItem.getValue()) || !sourceComment
.equals(targetItem.getComment())) {//modified items
targetItem.setValue(sourceValue);
targetItem.setComment(sourceComment);
changeSets.addUpdateItem(targetItem);
}
}
}
return changeSets;
}
private ItemDTO buildItem(long namespaceId, int lineNum, ItemDTO sourceItem) {
ItemDTO createdItem = new ItemDTO();
BeanUtils.copyEntityProperties(sourceItem, createdItem);
createdItem.setLineNum(lineNum++);
......
......@@ -12,7 +12,7 @@ appUtil.service('AppUtil', [function () {
if (!path) {
return {};
}
if (path.startsWith("/")) {
if (path.indexOf('/') == 0) {
path = path.substring(1, path.length);
}
var params = path.split("&");
......
sync_item_module.controller("SyncItemController",
['$scope', '$location', 'toastr', 'AppService', 'AppUtil', 'ConfigService',
function ($scope, $location, toastr, AppService, AppUtil, ConfigService) {
['$scope', '$location', '$window', 'toastr', 'AppService', 'AppUtil', 'ConfigService',
function ($scope, $location, $window, toastr, AppService, AppUtil, ConfigService) {
var params = AppUtil.parseParams($location.$$url);
var currentUser = 'test_user';
......@@ -13,15 +13,16 @@ sync_item_module.controller("SyncItemController",
////// load env //////
AppService.load_nav_tree($scope.pageContext.appId).then(function (result) {
$scope.clusters = result.nodes;
$scope.clusters = [];
$scope.namespaceIdentifers = [];
result.nodes.forEach(function (node) {
var env = node.env;
node.clusters.forEach(function (cluster) {
cluster.env = env;
cluster.checked = false;
$scope.clusters.push(cluster);
})
if (env != $scope.pageContext.env || cluster.name != $scope.pageContext.clusterName){
$scope.namespaceIdentifers.push(cluster);
}
})
});
}, function (result) {
toastr.error(AppUtil.errorMsg(result), "加载环境出错");
......@@ -30,8 +31,8 @@ sync_item_module.controller("SyncItemController",
var envAllSelected = false;
$scope.toggleEnvsCheckedStatus = function () {
envAllSelected = !envAllSelected;
$scope.clusters.forEach(function (cluster) {
cluster.checked = envAllSelected;
$scope.namespaceIdentifers.forEach(function (namespaceIdentifer) {
namespaceIdentifer.checked = envAllSelected;
})
};
......@@ -39,9 +40,12 @@ sync_item_module.controller("SyncItemController",
ConfigService.find_items($scope.pageContext.appId, $scope.pageContext.env,
$scope.pageContext.clusterName, $scope.pageContext.namespaceName).then(function (result) {
$scope.sourceItems = result;
$scope.sourceItems.forEach(function (item) {
item.checked = false;
$scope.sourceItems = [];
result.forEach(function (item) {
if (item.key){
item.checked = false;
$scope.sourceItems.push(item);
}
})
}, function (result) {
......@@ -56,6 +60,44 @@ sync_item_module.controller("SyncItemController",
})
};
$scope.diff = function () {
ConfigService.diff($scope.pageContext.namespaceName, parseSyncSourceData()).then(function (result) {
$scope.diffs = result;
$scope.syncItemNextStep(1);
}, function (result) {
toastr.error(AppUtil.errorMsg(result));
});
};
$scope.syncItems = function () {
ConfigService.sync_items($scope.pageContext.namespaceName, parseSyncSourceData()).then(function (result) {
$scope.syncItemStep += 1;
}, function (result) {
toastr.error(AppUtil.errorMsg(result));
});
};
function parseSyncSourceData() {
var sourceData = {
syncToNamespaces: [],
syncItems: []
};
var namespaceName = $scope.pageContext.namespaceName;
$scope.namespaceIdentifers.forEach(function (namespaceIdentifer) {
if (namespaceIdentifer.checked){
namespaceIdentifer.clusterName = namespaceIdentifer.name;
namespaceIdentifer.namespaceName = namespaceName;
sourceData.syncToNamespaces.push(namespaceIdentifer);
}
});
$scope.sourceItems.forEach(function (item) {
if (item.checked) {
sourceData.syncItems.push(item);
}
});
return sourceData;
}
////// flow control ///////
$scope.syncItemStep = 1;
......@@ -63,14 +105,12 @@ sync_item_module.controller("SyncItemController",
$scope.syncItemStep += offset;
};
$scope.syncItems = function () {
$scope.syncItemStep += 1;
$scope.backToAppHomePage = function () {
$window.location.href = '/views/app.html?#appid=' + $scope.pageContext.appId;
};
$scope.destorySync = function () {
$scope.syncItemStep = 1;
$scope.switchSelect = function (o) {
o.checked = !o.checked;
}
}]);
......@@ -17,6 +17,16 @@ appService.service("ConfigService", ['$resource', '$q', function ($resource, $q)
release: {
method: 'POST',
url:'/apps/:appId/env/:env/clusters/:clusterName/namespaces/:namespaceName/release'
},
diff: {
method: 'POST',
url: '/namespaces/:namespaceName/diff',
isArray: true
},
sync_item: {
method: 'PUT',
url: '/namespaces/:namespaceName/items',
isArray: false
}
});
......@@ -87,6 +97,30 @@ appService.service("ConfigService", ['$resource', '$q', function ($resource, $q)
d.reject(result);
});
return d.promise;
},
diff: function (namespaceName, sourceData) {
var d = $q.defer();
config_source.diff({
namespaceName: namespaceName
}, sourceData, function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
});
return d.promise;
},
sync_items: function (namespaceName, sourceData) {
var d = $q.defer();
config_source.sync_item({
namespaceName: namespaceName
}, sourceData, function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
});
return d.promise;
}
}
......
......@@ -65,14 +65,6 @@
</div>
</section>
<a class="list-group-item" data-toggle="modal" data-target="#syncItems">
<div class="row">
<div class="col-md-2"><img src="../img/sync.png" class="i-20"></div>
<div class="col-md-7 hidden-xs">
<p class="apps-description">配置同步</p>
</div>
</div>
</a>
<a class="list-group-item" data-toggle="modal" data-target="#createEnvModal"
ng-show="missEnvs.length > 0">
<div class="row">
......
......@@ -29,12 +29,12 @@
ng-click="syncItemNextStep(-1)">上一步
</button>
<button type="button" class="btn btn-primary" ng-show="syncItemStep < 2"
ng-click="syncItemNextStep(1)">下一步
ng-click="diff()">下一步
</button>
<button type="button" class="btn btn-success" ng-show="syncItemStep == 2" ng-click="syncItems()">同步
</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" ng-show="syncItemStep == 3"
ng-click="destorySync()">返回
ng-click="backToAppHomePage()">返回
</button>
</div>
</div>
......@@ -55,10 +55,11 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="cluster in clusters">
<td width="10%"><input type="checkbox" ng-checked="cluster.checked"></td>
<td width="30%">{{cluster.env}}</td>
<td width="60%">{{cluster.name}}</td>
<tr ng-repeat="namespaceIdentifer in namespaceIdentifers">
<td width="10%"><input type="checkbox" ng-checked="namespaceIdentifer.checked"
ng-click="switchSelect(namespaceIdentifer)"></td>
<td width="30%">{{namespaceIdentifer.env}}</td>
<td width="60%">{{namespaceIdentifer.name}}</td>
</tr>
</tbody>
</table>
......@@ -85,10 +86,14 @@
</thead>
<tbody>
<tr ng-repeat="item in sourceItems">
<td width="10%"><input type="checkbox" ng-checked="item.checked"></td>
<td width="10%"><input type="checkbox" ng-checked="item.checked"
ng-click="switchSelect(item)"></td>
<td width="20%">{{item.key}}</td>
<td width="50%">{{item.value | limitTo: 36}} {{item.value.length > 36 ? '...' : ''}}</td>
<td width="20%">{{item.comment | limitTo: 15}}{{item.comment.length > 15 ? '...' : ''}}</td>
<td width="50%">{{item.value | limitTo: 36}} {{item.value.length > 36 ? '...' : ''}}
</td>
<td width="20%">{{item.comment | limitTo: 15}}{{item.comment.length > 15 ? '...' :
''}}
</td>
</tr>
</tbody>
</table>
......@@ -99,28 +104,29 @@
</div>
<!--step 2-->
<div class="row" ng-show="syncItemStep == 2">
<h4 class="text-center">环境:fat 集群:default</h4>
<div class="row" ng-show="syncItemStep == 2" ng-repeat="diff in diffs">
<h4 class="text-center">环境:{{diff.namespace.env}} 集群:{{diff.namespace.clusterName}}</h4>
<hr>
<div class="row" style="margin-top: 10px;">
<div class="row text-center" style="margin-top: 10px;" ng-show="diff.diffs.createItems.length == 0 && diff.diffs.updateItems.length == 0">
<h5>无更新的配置</h5>
</div>
<div class="row" style="margin-top: 10px;" ng-show="diff.diffs.createItems.length > 0">
<div class="form-horizontal">
<label class="col-sm-2 control-label">新增的配置</label>
<div class="col-sm-9">
<table class="table table-bordered table-hover">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<td>key</td>
<td>value</td>
<td>comment</td>
</tr>
</thead>
<tbody>
<tr>
<td width="30%">k1</td>
<td width="60%">v1</td>
</tr>
<tr>
<td width="30%">k1</td>
<td width="60%">v1</td>
<tr ng-repeat="createItem in diff.diffs.createItems">
<td width="30%">{{createItem.key}}</td>
<td width="40%">{{createItem.value}}</td>
<td width="30%">{{createItem.comment}}</td>
</tr>
</tbody>
</table>
......@@ -128,33 +134,28 @@
</div>
</div>
</div>
<div class="row" ng-show="syncItemStep == 2">
<div class="row" style="margin-top: 10px;">
<div class="form-horizontal">
<label class="col-sm-2 control-label">更新的配置</label>
<div class="col-sm-9">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<td>key</td>
<td>value</td>
</tr>
</thead>
<tbody>
<tr>
<td width="30%">k1</td>
<td width="60%">v1</td>
</tr>
<tr>
<td width="30%">k1</td>
<td width="60%">v1</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row" style="margin-top: 10px;" ng-show="diff.diffs.updateItems.length > 0">
<div class="form-horizontal">
<label class="col-sm-2 control-label">更新的配置</label>
<div class="col-sm-9">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<td>key</td>
<td>value</td>
<td>comment</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="updateItem in diff.diffs.updateItems">
<td width="30%">{{updateItem.key}}</td>
<td width="40%">{{updateItem.value}}</td>
<td width="30%">{{updateItem.comment}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
......
......@@ -143,7 +143,7 @@ public class ConfigServiceTest {
when(namespaceAPI.loadNamespace(appId, Env.valueOf(env), clusterName, namespaceName)).thenReturn(namespaceDTO);
when(itemAPI.findItems(appId, Env.valueOf(env), clusterName, namespaceName)).thenReturn(null);
List<ItemDiffs> itemDiffses = configService.compare(sourceItems, namespaceIdentifers);
List<ItemDiffs> itemDiffses = configService.compare(namespaceIdentifers, sourceItems);
assertEquals(1,itemDiffses.size());
ItemDiffs itemDiffs = itemDiffses.get(0);
......@@ -180,7 +180,7 @@ public class ConfigServiceTest {
when(namespaceAPI.loadNamespace(appId, Env.valueOf(env), clusterName, namespaceName)).thenReturn(namespaceDTO);
when(itemAPI.findItems(appId, Env.valueOf(env), clusterName, namespaceName)).thenReturn(targetItems);
List<ItemDiffs> itemDiffses = configService.compare(sourceItems, namespaceIdentifers);
List<ItemDiffs> itemDiffses = configService.compare(namespaceIdentifers, sourceItems);
assertEquals(1, itemDiffses.size());
ItemDiffs itemDiffs = itemDiffses.get(0);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册