Commit 1cf49996 authored by Jason Song's avatar Jason Song Committed by GitHub

Merge pull request #1279 from nobodyiam/delete-app-cluster-namespace-page

add admin page to delete app, cluster and app namespace
parents 65da1611 44ce0e4e
...@@ -52,9 +52,14 @@ public class ClusterController { ...@@ -52,9 +52,14 @@ public class ClusterController {
@RequestMapping(value = "apps/{appId}/envs/{env}/clusters/{clusterName:.+}", method = RequestMethod.DELETE) @RequestMapping(value = "apps/{appId}/envs/{env}/clusters/{clusterName:.+}", method = RequestMethod.DELETE)
public ResponseEntity<Void> deleteCluster(@PathVariable String appId, @PathVariable String env, public ResponseEntity<Void> deleteCluster(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName){ @PathVariable String clusterName){
clusterService.deleteCluster(Env.valueOf(env), appId, clusterName); clusterService.deleteCluster(Env.fromString(env), appId, clusterName);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@RequestMapping(value = "apps/{appId}/envs/{env}/clusters/{clusterName:.+}", method = RequestMethod.GET)
public ClusterDTO loadCluster(@PathVariable("appId") String appId, @PathVariable String env, @PathVariable("clusterName") String clusterName) {
return clusterService.loadCluster(appId, Env.fromString(env), clusterName);
}
} }
package com.ctrip.framework.apollo.portal.controller; package com.ctrip.framework.apollo.portal.controller;
import com.ctrip.framework.apollo.common.dto.AppNamespaceDTO;
import com.ctrip.framework.apollo.common.utils.BeanUtils;
import com.ctrip.framework.apollo.portal.listener.AppNamespaceDeletionEvent; import com.ctrip.framework.apollo.portal.listener.AppNamespaceDeletionEvent;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
...@@ -144,6 +146,18 @@ public class NamespaceController { ...@@ -144,6 +146,18 @@ public class NamespaceController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@RequestMapping(value = "/apps/{appId}/appnamespaces/{namespaceName:.+}", method = RequestMethod.GET)
public AppNamespaceDTO findAppNamespace(@PathVariable String appId, @PathVariable String namespaceName) {
AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName);
if (appNamespace == null) {
throw new BadRequestException(
String.format("AppNamespace not exists. AppId = %s, NamespaceName = %s", appId, namespaceName));
}
return BeanUtils.transfrom(AppNamespaceDTO.class, appNamespace);
}
@PreAuthorize(value = "@permissionValidator.hasCreateAppNamespacePermission(#appId, #appNamespace)") @PreAuthorize(value = "@permissionValidator.hasCreateAppNamespacePermission(#appId, #appNamespace)")
@RequestMapping(value = "/apps/{appId}/appnamespaces", method = RequestMethod.POST) @RequestMapping(value = "/apps/{appId}/appnamespaces", method = RequestMethod.POST)
public AppNamespace createAppNamespace(@PathVariable String appId, @RequestBody AppNamespace appNamespace) { public AppNamespace createAppNamespace(@PathVariable String appId, @RequestBody AppNamespace appNamespace) {
......
...@@ -74,26 +74,22 @@ public class NamespaceService { ...@@ -74,26 +74,22 @@ public class NamespaceService {
@Transactional @Transactional
public void deleteNamespace(String appId, Env env, String clusterName, String namespaceName) { public void deleteNamespace(String appId, Env env, String clusterName, String namespaceName) {
//1. check private namespace
AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName); AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName);
if (appNamespace != null && !appNamespace.isPublic()) {
throw new BadRequestException("Private namespace can not be deleted");
}
//2. check parent namespace has not instances //1. check parent namespace has not instances
if (namespaceHasInstances(appId, env, clusterName, namespaceName)) { if (namespaceHasInstances(appId, env, clusterName, namespaceName)) {
throw new BadRequestException("Can not delete namespace because namespace has active instances"); throw new BadRequestException("Can not delete namespace because namespace has active instances");
} }
//3. check child namespace has not instances //2. check child namespace has not instances
NamespaceDTO childNamespace = branchService.findBranchBaseInfo(appId, env, clusterName, namespaceName); NamespaceDTO childNamespace = branchService.findBranchBaseInfo(appId, env, clusterName, namespaceName);
if (childNamespace != null && if (childNamespace != null &&
namespaceHasInstances(appId, env, childNamespace.getClusterName(), namespaceName)) { namespaceHasInstances(appId, env, childNamespace.getClusterName(), namespaceName)) {
throw new BadRequestException("Can not delete namespace because namespace's branch has active instances"); throw new BadRequestException("Can not delete namespace because namespace's branch has active instances");
} }
//4. check public namespace has not associated namespace //3. check public namespace has not associated namespace
if (appNamespace != null && publicAppNamespaceHasAssociatedNamespace(namespaceName, env)) { if (appNamespace != null && appNamespace.isPublic() && publicAppNamespaceHasAssociatedNamespace(namespaceName, env)) {
throw new BadRequestException("Can not delete public namespace which has associated namespaces"); throw new BadRequestException("Can not delete public namespace which has associated namespaces");
} }
......
...@@ -288,8 +288,7 @@ ...@@ -288,8 +288,7 @@
apollo-detail="'发现有 <b>' + deleteNamespaceContext.namespace.instancesCount + apollo-detail="'发现有 <b>' + deleteNamespaceContext.namespace.instancesCount +
'</b> 个实例正在使用Namespace(' + deleteNamespaceContext.namespace.baseInfo.namespaceName + '</b> 个实例正在使用Namespace(' + deleteNamespaceContext.namespace.baseInfo.namespaceName +
'),删除Namespace将导致实例获取不到配置。<br> '),删除Namespace将导致实例获取不到配置。<br>
请到 <ins>“实例列表”</ins> 确认实例信息,如已确认删除Namespace将不会导致实例异常, 请到 <ins>“实例列表”</ins> 确认实例信息,如确认相关实例都已经不再使用该Namespace配置,可以联系Apollo相关负责人删除实例信息(InstanceConfig)或等待实例24小时自动过期后再来删除。'"
请联系Apollo相关负责人删除Namespace'"
apollo-confirm="continueDeleteNamespace"> apollo-confirm="continueDeleteNamespace">
</apolloconfirmdialog> </apolloconfirmdialog>
...@@ -298,8 +297,7 @@ ...@@ -298,8 +297,7 @@
apollo-detail="'发现有 <b>' + deleteNamespaceContext.namespace.branch.latestReleaseInstances.total apollo-detail="'发现有 <b>' + deleteNamespaceContext.namespace.branch.latestReleaseInstances.total
+ '</b> 个实例正在使用Namespace(' + deleteNamespaceContext.namespace.baseInfo.namespaceName + + '</b> 个实例正在使用Namespace(' + deleteNamespaceContext.namespace.baseInfo.namespaceName +
')灰度版本的配置,删除Namespace将导致实例获取不到配置。<br> ')灰度版本的配置,删除Namespace将导致实例获取不到配置。<br>
请到 <ins>“灰度版本” => “实例列表”</ins> 确认实例信息,如已确认删除Namespace将不会导致实例异常, 请到 <ins>“灰度版本” => “实例列表”</ins> 确认实例信息,如确认相关实例都已经不再使用该Namespace配置,可以联系Apollo相关负责人删除实例信息(InstanceConfig)或等待实例24小时自动过期后再来删除。'"
请联系Apollo相关负责人删除Namespace'"
apollo-confirm="continueDeleteNamespace"> apollo-confirm="continueDeleteNamespace">
</apolloconfirmdialog> </apolloconfirmdialog>
......
...@@ -34,6 +34,5 @@ var open_manage_module = angular.module('open_manage', ['app.service', 'apollo.d ...@@ -34,6 +34,5 @@ var open_manage_module = angular.module('open_manage', ['app.service', 'apollo.d
var user_module = angular.module('user', ['apollo.directive', 'toastr', 'app.service', 'app.util', 'angular-loading-bar', 'valdr']); var user_module = angular.module('user', ['apollo.directive', 'toastr', 'app.service', 'app.util', 'angular-loading-bar', 'valdr']);
//login //login
var login_module = angular.module('login', ['toastr', 'app.util']); var login_module = angular.module('login', ['toastr', 'app.util']);
//delete app cluster namespace
var delete_app_cluster_namespace_module = angular.module('delete_app_cluster_namespace', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
delete_app_cluster_namespace_module.controller('DeleteAppClusterNamespaceController',
['$scope', 'toastr', 'AppUtil', 'AppService', 'ClusterService', 'NamespaceService', 'PermissionService',
DeleteAppClusterNamespaceController]);
function DeleteAppClusterNamespaceController($scope, toastr, AppUtil, AppService, ClusterService, NamespaceService, PermissionService) {
$scope.app = {};
$scope.deleteAppBtnDisabled = true;
$scope.getAppInfo = getAppInfo;
$scope.deleteApp = deleteApp;
$scope.cluster = {};
$scope.deleteClusterBtnDisabled = true;
$scope.getClusterInfo = getClusterInfo;
$scope.deleteCluster = deleteCluster;
$scope.appNamespace = {};
$scope.deleteAppNamespaceBtnDisabled = true;
$scope.getAppNamespaceInfo = getAppNamespaceInfo;
$scope.deleteAppNamespace = deleteAppNamespace;
initPermission();
function initPermission() {
PermissionService.has_root_permission()
.then(function (result) {
$scope.isRootUser = result.hasPermission;
})
}
function getAppInfo() {
if (!$scope.app.appId) {
toastr.warning("请输入appId");
return;
}
$scope.app.info = "";
AppService.load($scope.app.appId).then(function (result) {
if (!result.appId) {
toastr.warning("AppId: " + $scope.app.appId + " 不存在!");
$scope.deleteAppBtnDisabled = true;
return;
}
$scope.app.info = "应用名:" + result.name + " 部门:" + result.orgName + '(' + result.orgId + ')' + " 负责人:" + result.ownerName;
$scope.deleteAppBtnDisabled = false;
}, function (result) {
AppUtil.showErrorMsg(result);
});
}
function deleteApp() {
if (!$scope.app.appId) {
toastr.warning("请输入appId");
return;
}
if (confirm("确认删除AppId: " + $scope.app.appId + "")) {
AppService.delete_app($scope.app.appId).then(function (result) {
toastr.success("删除成功");
$scope.deleteAppBtnDisabled = true;
}, function (result) {
AppUtil.showErrorMsg(result);
})
}
}
function getClusterInfo() {
if (!$scope.cluster.appId || !$scope.cluster.env || !$scope.cluster.name) {
toastr.warning("请输入appId、环境和集群名称");
return;
}
$scope.cluster.info = "";
ClusterService.load_cluster($scope.cluster.appId, $scope.cluster.env, $scope.cluster.name).then(function (result) {
$scope.cluster.info = "AppId:" + result.appId+ " 环境:" + $scope.cluster.env + " 集群名称:" + result.name;
$scope.deleteClusterBtnDisabled = false;
}, function (result) {
AppUtil.showErrorMsg(result);
});
}
function deleteCluster() {
if (!$scope.cluster.appId || !$scope.cluster.env || !$scope.cluster.name) {
toastr.warning("请输入appId、环境和集群名称");
return;
}
if (confirm("确认删除集群?appId: " + $scope.cluster.appId + " 环境:" + $scope.cluster.env + " 集群名称:" + $scope.cluster.name)) {
ClusterService.delete_cluster($scope.cluster.appId, $scope.cluster.env, $scope.cluster.name).then(function (result) {
toastr.success("删除成功");
$scope.deleteClusterBtnDisabled = true;
}, function (result) {
AppUtil.showErrorMsg(result);
})
}
}
function getAppNamespaceInfo() {
if (!$scope.appNamespace.appId || !$scope.appNamespace.name) {
toastr.warning("请输入appId和AppNamespace名称");
return;
}
$scope.appNamespace.info = "";
NamespaceService.loadAppNamespace($scope.appNamespace.appId, $scope.appNamespace.name).then(function (result) {
$scope.appNamespace.info = "AppId:" + result.appId+ " AppNamespace名称:" + result.name + " isPublic:" + result.isPublic;
$scope.deleteAppNamespaceBtnDisabled = false;
}, function (result) {
AppUtil.showErrorMsg(result);
});
}
function deleteAppNamespace() {
if (!$scope.appNamespace.appId || !$scope.appNamespace.name) {
toastr.warning("请输入appId和AppNamespace名称");
return;
}
if (confirm("确认删除所有环境的AppNamespace和Namespace?appId: " + $scope.appNamespace.appId + " 环境:所有环境" + " AppNamespace名称:" + $scope.appNamespace.name)) {
NamespaceService.deleteAppNamespace($scope.appNamespace.appId, $scope.appNamespace.name).then(function (result) {
toastr.success("删除成功");
$scope.deleteAppNamespaceBtnDisabled = true;
}, function (result) {
AppUtil.showErrorMsg(result);
})
}
}
}
...@@ -18,25 +18,20 @@ function deleteNamespaceModalDirective($window, $q, toastr, AppUtil, EventManage ...@@ -18,25 +18,20 @@ function deleteNamespaceModalDirective($window, $q, toastr, AppUtil, EventManage
var toDeleteNamespace = context.namespace; var toDeleteNamespace = context.namespace;
scope.toDeleteNamespace = toDeleteNamespace; scope.toDeleteNamespace = toDeleteNamespace;
//1. check namespace is not private //1. check operator has master permission
if (!checkNotPrivateNamespace(toDeleteNamespace)) {
return;
}
//2. check operator has master permission
checkPermission(toDeleteNamespace).then(function () { checkPermission(toDeleteNamespace).then(function () {
//3. check namespace's master branch has not instances //2. check namespace's master branch has not instances
if (!checkMasterInstance(toDeleteNamespace)) { if (!checkMasterInstance(toDeleteNamespace)) {
return; return;
} }
//4. check namespace's gray branch has not instances //3. check namespace's gray branch has not instances
if (!checkBranchInstance(toDeleteNamespace)) { if (!checkBranchInstance(toDeleteNamespace)) {
return; return;
} }
if (toDeleteNamespace.isLinkedNamespace) { if (!toDeleteNamespace.isPublic || toDeleteNamespace.isLinkedNamespace) {
showDeleteNamespaceConfirmDialog(); showDeleteNamespaceConfirmDialog();
} else { } else {
//5. check public namespace has not associated namespace //5. check public namespace has not associated namespace
...@@ -48,15 +43,6 @@ function deleteNamespaceModalDirective($window, $q, toastr, AppUtil, EventManage ...@@ -48,15 +43,6 @@ function deleteNamespaceModalDirective($window, $q, toastr, AppUtil, EventManage
}); });
function checkNotPrivateNamespace(namespace) {
if (!namespace.isPublic) {
toastr.error("不能删除私有的Namespace", "删除失败");
return false;
}
return true;
}
function checkPermission(namespace) { function checkPermission(namespace) {
var d = $q.defer(); var d = $q.defer();
...@@ -153,7 +139,6 @@ function deleteNamespaceModalDirective($window, $q, toastr, AppUtil, EventManage ...@@ -153,7 +139,6 @@ function deleteNamespaceModalDirective($window, $q, toastr, AppUtil, EventManage
function showDeleteNamespaceConfirmDialog() { function showDeleteNamespaceConfirmDialog() {
AppUtil.showModal('#deleteNamespaceModal'); AppUtil.showModal('#deleteNamespaceModal');
} }
function doDeleteNamespace() { function doDeleteNamespace() {
......
...@@ -34,6 +34,10 @@ appService.service('AppService', ['$resource', '$q', function ($resource, $q) { ...@@ -34,6 +34,10 @@ appService.service('AppService', ['$resource', '$q', function ($resource, $q) {
find_miss_envs: { find_miss_envs: {
method: 'GET', method: 'GET',
url: '/apps/:appId/miss_envs' url: '/apps/:appId/miss_envs'
},
delete_app: {
method: 'DELETE',
isArray: false
} }
}); });
return { return {
...@@ -123,6 +127,17 @@ appService.service('AppService', ['$resource', '$q', function ($resource, $q) { ...@@ -123,6 +127,17 @@ appService.service('AppService', ['$resource', '$q', function ($resource, $q) {
d.reject(result); d.reject(result);
}); });
return d.promise; return d.promise;
},
delete_app: function (appId) {
var d = $q.defer();
app_resource.delete_app({
appId: appId
}, function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
});
return d.promise;
} }
} }
}]); }]);
...@@ -3,6 +3,14 @@ appService.service('ClusterService', ['$resource', '$q', function ($resource, $q ...@@ -3,6 +3,14 @@ appService.service('ClusterService', ['$resource', '$q', function ($resource, $q
create_cluster: { create_cluster: {
method: 'POST', method: 'POST',
url: 'apps/:appId/envs/:env/clusters' url: 'apps/:appId/envs/:env/clusters'
},
load_cluster: {
method: 'GET',
url: 'apps/:appId/envs/:env/clusters/:clusterName'
},
delete_cluster: {
method: 'DELETE',
url: 'apps/:appId/envs/:env/clusters/:clusterName'
} }
}); });
return { return {
...@@ -18,6 +26,34 @@ appService.service('ClusterService', ['$resource', '$q', function ($resource, $q ...@@ -18,6 +26,34 @@ appService.service('ClusterService', ['$resource', '$q', function ($resource, $q
d.reject(result); d.reject(result);
}); });
return d.promise; return d.promise;
},
load_cluster: function (appId, env, clusterName) {
var d = $q.defer();
cluster_resource.load_cluster({
appId: appId,
env: env,
clusterName: clusterName
},
function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
});
return d.promise;
},
delete_cluster: function (appId, env, clusterName) {
var d = $q.defer();
cluster_resource.delete_cluster({
appId: appId,
env: env,
clusterName: clusterName
},
function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
});
return d.promise;
} }
} }
}]); }]);
...@@ -27,6 +27,14 @@ appService.service("NamespaceService", ['$resource', '$q', function ($resource, ...@@ -27,6 +27,14 @@ appService.service("NamespaceService", ['$resource', '$q', function ($resource,
method: 'GET', method: 'GET',
url: '/envs/:env/appnamespaces/:publicNamespaceName/namespaces', url: '/envs/:env/appnamespaces/:publicNamespaceName/namespaces',
isArray: true isArray: true
},
loadAppNamespace: {
method: 'GET',
url: '/apps/:appId/appnamespaces/:namespaceName'
},
deleteAppNamespace: {
method: 'DELETE',
url: '/apps/:appId/appnamespaces/:namespaceName'
} }
}); });
...@@ -112,13 +120,47 @@ appService.service("NamespaceService", ['$resource', '$q', function ($resource, ...@@ -112,13 +120,47 @@ appService.service("NamespaceService", ['$resource', '$q', function ($resource,
} }
function loadAppNamespace(appId, namespaceName) {
var d = $q.defer();
namespace_source.loadAppNamespace({
appId: appId,
namespaceName: namespaceName
},
function (result) {
d.resolve(result);
},
function (result) {
d.reject(result);
});
return d.promise;
}
function deleteAppNamespace(appId, namespaceName) {
var d = $q.defer();
namespace_source.deleteAppNamespace({
appId: appId,
namespaceName: namespaceName
},
function (result) {
d.resolve(result);
},
function (result) {
d.reject(result);
});
return d.promise;
}
return { return {
find_public_namespaces: find_public_namespaces, find_public_namespaces: find_public_namespaces,
createNamespace: createNamespace, createNamespace: createNamespace,
createAppNamespace: createAppNamespace, createAppNamespace: createAppNamespace,
getNamespacePublishInfo: getNamespacePublishInfo, getNamespacePublishInfo: getNamespacePublishInfo,
deleteNamespace: deleteNamespace, deleteNamespace: deleteNamespace,
getPublicAppNamespaceAllNamespaces: getPublicAppNamespaceAllNamespaces getPublicAppNamespaceAllNamespaces: getPublicAppNamespaceAllNamespaces,
loadAppNamespace: loadAppNamespace,
deleteAppNamespace: deleteAppNamespace
} }
}]); }]);
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
<li><a href="/user-manage.html" target="_blank">用户管理</a></li> <li><a href="/user-manage.html" target="_blank">用户管理</a></li>
<li><a href="/open/manage.html" target="_blank">开放平台授权管理</a></li> <li><a href="/open/manage.html" target="_blank">开放平台授权管理</a></li>
<li><a href="/server_config.html" target="_blank">系统参数</a></li> <li><a href="/server_config.html" target="_blank">系统参数</a></li>
<li><a href="/delete_app_cluster_namespace.html" target="_blank">删除应用、集群、AppNamespace</a></li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
......
...@@ -8,9 +8,12 @@ ...@@ -8,9 +8,12 @@
删除Namespace 删除Namespace
</h4> </h4>
</div> </div>
<div class="modal-body form-horizontal"> <div class="modal-body form-horizontal" ng-show="toDeleteNamespace.isPublic">
删除Namespace将导致实例获取不到此Namespace的配置,确定要删除吗? 删除Namespace将导致实例获取不到此Namespace的配置,确定要删除吗?
</div> </div>
<div class="modal-body form-horizontal" ng-show="!toDeleteNamespace.isPublic">
删除私有Namespace将导致实例获取不到此Namespace的配置,而且无法在当前集群重新创建该Namespace(除非使用管理员工具把所有环境的AppNamespace删除后重建),确定要删除吗?
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"> <button type="button" class="btn btn-default" data-dismiss="modal">
取消 取消
......
...@@ -111,13 +111,18 @@ public class NamespaceServiceTest extends AbstractUnitTest { ...@@ -111,13 +111,18 @@ public class NamespaceServiceTest extends AbstractUnitTest {
} }
@Test(expected = BadRequestException.class) @Test
public void testDeletePrivateNamespace() { public void testDeletePrivateNamespace() {
String operator = "user";
AppNamespace privateNamespace = createAppNamespace(testAppId, testNamespaceName, false); AppNamespace privateNamespace = createAppNamespace(testAppId, testNamespaceName, false);
when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(privateNamespace); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(privateNamespace);
when(userInfoHolder.getUser()).thenReturn(createUser(operator));
namespaceService.deleteNamespace(testAppId, testEnv, testClusterName, testNamespaceName); namespaceService.deleteNamespace(testAppId, testEnv, testClusterName, testNamespaceName);
verify(namespaceAPI, times(1)).deleteNamespace(testEnv, testAppId, testClusterName, testNamespaceName, operator);
} }
@Test(expected = BadRequestException.class) @Test(expected = BadRequestException.class)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment