diff --git a/CHANGES.md b/CHANGES.md index 53c1315cd16..a274d411129 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ Apollo 2.1.0 * [fix(#4474):'openjdk:8-jre-alpine' potentially causing wrong number of cpu cores](https://github.com/apolloconfig/apollo/pull/4475) * [Switching spring-session serialization mode to json for compatibility with spring-security version updates]() * [fix(#4483):Fixed overwrite JSON type configuration being empty](https://github.com/apolloconfig/apollo/pull/4486) +* [Allow users to delete AppNamespace](https://github.com/apolloconfig/apollo/pull/4499) * [fix the deleted at timestamp issue](https://github.com/apolloconfig/apollo/pull/4493) ------------------ diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceController.java index 19f5f5a00f9..9d2f286ef53 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceController.java @@ -25,6 +25,7 @@ import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.common.utils.InputValidator; import com.ctrip.framework.apollo.common.utils.RequestPrecondition; +import com.ctrip.framework.apollo.portal.entity.vo.NamespaceUsage; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PermissionValidator; @@ -38,6 +39,7 @@ import com.ctrip.framework.apollo.portal.service.RoleInitializationService; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.tracer.Tracer; +import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -168,16 +170,28 @@ public ResponseEntity createNamespace(@PathVariable String appId, } @PreAuthorize(value = "@permissionValidator.hasDeleteNamespacePermission(#appId)") - @DeleteMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName:.+}") - public ResponseEntity deleteNamespace(@PathVariable String appId, @PathVariable String env, - @PathVariable String clusterName, @PathVariable String namespaceName) { + @DeleteMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/linked-namespaces/{namespaceName:.+}") + public ResponseEntity deleteLinkedNamespace(@PathVariable String appId, @PathVariable String env, + @PathVariable String clusterName, @PathVariable String namespaceName) { namespaceService.deleteNamespace(appId, Env.valueOf(env), clusterName, namespaceName); return ResponseEntity.ok().build(); } - @PreAuthorize(value = "@permissionValidator.isSuperAdmin()") + @GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/linked-namespaces/{namespaceName}/usage") + public List findLinkedNamespaceUsage(@PathVariable String appId, @PathVariable String env, + @PathVariable String clusterName, @PathVariable String namespaceName) { + NamespaceUsage usage = namespaceService.getNamespaceUsageByEnv(appId, namespaceName, Env.valueOf(env), clusterName); + return Lists.newArrayList(usage); + } + + @GetMapping("/apps/{appId}/namespaces/{namespaceName}/usage") + public List findNamespaceUsage(@PathVariable String appId, @PathVariable String namespaceName) { + return namespaceService.getNamespaceUsageByAppId(appId, namespaceName); + } + + @PreAuthorize(value = "@permissionValidator.hasDeleteNamespacePermission(#appId)") @DeleteMapping("/apps/{appId}/appnamespaces/{namespaceName:.+}") public ResponseEntity deleteAppNamespace(@PathVariable String appId, @PathVariable String namespaceName) { diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceUsage.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceUsage.java new file mode 100644 index 00000000000..b4ecfbc677b --- /dev/null +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceUsage.java @@ -0,0 +1,100 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.portal.entity.vo; + +/** + * @author kl (http://kailing.pub) + * @since 2022/8/9 + */ +public class NamespaceUsage { + + private String namespaceName; + private String appId; + private String clusterName; + private String envName; + private int instanceCount; + private int branchInstanceCount; + private int linkedNamespaceCount; + + + public NamespaceUsage() { + } + + public NamespaceUsage(String namespaceName, String appId, String clusterName, + String envName) { + this.namespaceName = namespaceName; + this.appId = appId; + this.clusterName = clusterName; + this.envName = envName; + } + + public String getNamespaceName() { + return namespaceName; + } + + public void setNamespaceName(String namespaceName) { + this.namespaceName = namespaceName; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getEnvName() { + return envName; + } + + public void setEnvName(String envName) { + this.envName = envName; + } + + public int getInstanceCount() { + return instanceCount; + } + + public void setInstanceCount(int instanceCount) { + this.instanceCount = instanceCount; + } + + public int getBranchInstanceCount() { + return branchInstanceCount; + } + + public void setBranchInstanceCount(int branchInstanceCount) { + this.branchInstanceCount = branchInstanceCount; + } + + public int getLinkedNamespaceCount() { + return linkedNamespaceCount; + } + + public void setLinkedNamespaceCount(int linkedNamespaceCount) { + this.linkedNamespaceCount = linkedNamespaceCount; + } +} diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java index 19987c76b3f..fb1e8076cec 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppNamespaceService.java @@ -27,15 +27,14 @@ import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import java.util.List; +import java.util.Objects; +import java.util.Set; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import java.util.List; -import java.util.Objects; -import java.util.Set; - @Service public class AppNamespaceService { @@ -254,4 +253,5 @@ public AppNamespace deleteAppNamespace(String appId, String namespaceName) { public void batchDeleteByAppId(String appId, String operator) { appNamespaceRepository.batchDeleteByAppId(appId, operator); } + } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java index 38d8a9ae444..15a35ca4924 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/NamespaceService.java @@ -17,6 +17,7 @@ package com.ctrip.framework.apollo.portal.service; import com.ctrip.framework.apollo.common.constants.GsonType; +import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.PageDTO; @@ -28,6 +29,7 @@ import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; +import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.NamespaceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import com.ctrip.framework.apollo.portal.constant.RoleType; @@ -35,6 +37,7 @@ import com.ctrip.framework.apollo.portal.enricher.adapter.BaseDtoUserInfoEnrichedAdapter; import com.ctrip.framework.apollo.portal.entity.bo.ItemBO; import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO; +import com.ctrip.framework.apollo.portal.entity.vo.NamespaceUsage; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.util.RoleUtils; @@ -42,6 +45,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gson.Gson; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -80,19 +84,21 @@ public class NamespaceService { private final NamespaceBranchService branchService; private final RolePermissionService rolePermissionService; private final AdditionalUserInfoEnrichService additionalUserInfoEnrichService; + private final ClusterService clusterService; public NamespaceService( final PortalConfig portalConfig, final PortalSettings portalSettings, final UserInfoHolder userInfoHolder, - final AdminServiceAPI.NamespaceAPI namespaceAPI, + final NamespaceAPI namespaceAPI, final ItemService itemService, final ReleaseService releaseService, final AppNamespaceService appNamespaceService, final InstanceService instanceService, final @Lazy NamespaceBranchService branchService, final RolePermissionService rolePermissionService, - final AdditionalUserInfoEnrichService additionalUserInfoEnrichService) { + final AdditionalUserInfoEnrichService additionalUserInfoEnrichService, + ClusterService clusterService) { this.portalConfig = portalConfig; this.portalSettings = portalSettings; this.userInfoHolder = userInfoHolder; @@ -104,6 +110,7 @@ public NamespaceService( this.branchService = branchService; this.rolePermissionService = rolePermissionService; this.additionalUserInfoEnrichService = additionalUserInfoEnrichService; + this.clusterService = clusterService; } @@ -124,35 +131,46 @@ public NamespaceDTO createNamespace(Env env, NamespaceDTO namespace) { } - @Transactional - public void deleteNamespace(String appId, Env env, String clusterName, String namespaceName) { - + public List getNamespaceUsageByAppId(String appId, String namespaceName) { + List envs = portalSettings.getActiveEnvs(); AppNamespace appNamespace = appNamespaceService.findByAppIdAndName(appId, namespaceName); + List usages = new ArrayList<>(); + for (Env env : envs) { + List clusters = clusterService.findClusters(env, appId); + for (ClusterDTO cluster : clusters) { + String clusterName = cluster.getName(); + NamespaceUsage usage = this.getNamespaceUsageByEnv(appId, namespaceName, env, clusterName); + if (appNamespace != null && appNamespace.isPublic()) { + int associatedNamespace = this.getPublicAppNamespaceHasAssociatedNamespace(namespaceName, env); + usage.setLinkedNamespaceCount(associatedNamespace); + } - //1. check parent namespace has not instances - if (namespaceHasInstances(appId, env, clusterName, namespaceName)) { - throw new BadRequestException( - "Can not delete namespace because namespace has active instances"); + if(usage.getLinkedNamespaceCount() > 0 || usage.getBranchInstanceCount() > 0 || usage.getInstanceCount() > 0) { + usages.add(usage); + } + } } + return usages; + } - //2. check child namespace has not instances - NamespaceDTO childNamespace = branchService - .findBranchBaseInfo(appId, env, clusterName, namespaceName); - if (childNamespace != null && - namespaceHasInstances(appId, env, childNamespace.getClusterName(), namespaceName)) { - throw new BadRequestException( - "Can not delete namespace because namespace's branch has active instances"); - } + public NamespaceUsage getNamespaceUsageByEnv(String appId, String namespaceName, Env env, String clusterName) { + NamespaceUsage namespaceUsage = new NamespaceUsage(namespaceName, appId, clusterName, env.getName()); + int instanceCount = instanceService.getInstanceCountByNamespace(appId, env, clusterName, namespaceName); + namespaceUsage.setInstanceCount(instanceCount); - //3. check public namespace has not associated namespace - if (appNamespace != null && appNamespace.isPublic() && publicAppNamespaceHasAssociatedNamespace( - namespaceName, env)) { - throw new BadRequestException( - "Can not delete public namespace which has associated namespaces"); + NamespaceDTO branchNamespace = branchService.findBranchBaseInfo(appId, env, clusterName, namespaceName); + if(branchNamespace != null){ + String branchClusterName = branchNamespace.getClusterName(); + int branchInstanceCount = instanceService.getInstanceCountByNamespace(appId, env, branchClusterName, namespaceName); + namespaceUsage.setBranchInstanceCount(branchInstanceCount); } + return namespaceUsage; + } - String operator = userInfoHolder.getUser().getUserId(); + @Transactional + public void deleteNamespace(String appId, Env env, String clusterName, String namespaceName) { + String operator = userInfoHolder.getUser().getUserId(); namespaceAPI.deleteNamespace(env, appId, clusterName, namespaceName, operator); } @@ -236,13 +254,12 @@ public NamespaceBO loadNamespaceBO(String appId, Env env, String clusterName, return transformNamespace2BO(env, namespace); } - public boolean namespaceHasInstances(String appId, Env env, String clusterName, - String namespaceName) { - return instanceService.getInstanceCountByNamespace(appId, env, clusterName, namespaceName) > 0; + public boolean publicAppNamespaceHasAssociatedNamespace(String publicNamespaceName, Env env) { + return getPublicAppNamespaceHasAssociatedNamespace(publicNamespaceName, env) > 0; } - public boolean publicAppNamespaceHasAssociatedNamespace(String publicNamespaceName, Env env) { - return namespaceAPI.countPublicAppNamespaceAssociatedNamespaces(env, publicNamespaceName) > 0; + public int getPublicAppNamespaceHasAssociatedNamespace(String publicNamespaceName, Env env) { + return namespaceAPI.countPublicAppNamespaceAssociatedNamespaces(env, publicNamespaceName); } public NamespaceBO findPublicNamespaceForAssociatedNamespace(Env env, String appId, diff --git a/apollo-portal/src/main/resources/static/i18n/en.json b/apollo-portal/src/main/resources/static/i18n/en.json index de6b7132ce0..d2e8f053bb8 100644 --- a/apollo-portal/src/main/resources/static/i18n/en.json +++ b/apollo-portal/src/main/resources/static/i18n/en.json @@ -16,9 +16,12 @@ "Common.Department": "Department", "Common.Cluster": "Cluster", "Common.Environment": "Environment", + "Common.GrayscaleInstance": "GrayscaleInstance", + "Common.Instance": "Instance", "Common.Email": "Email", "Common.AppId": "App Id", "Common.Namespace": "Namespace", + "Common.LinkedNamespace": "LinkedNamespace", "Common.AppName": "App Name", "Common.AppOwner": "App Owner", "Common.AppOwnerLong": "App Owner", @@ -40,9 +43,12 @@ "Common.LoginExpiredTips": "Your login is expired. Please refresh the page and try again.", "Common.Operation": "Operation", "Common.Delete": "Delete", + "Common.ForceDelete": "Force Delete", "Component.DeleteNamespace.Title": "Delete Namespace", - "Component.DeleteNamespace.PublicContent": "Deleting namespace will cause the instances unable to get the configuration of this namespace. Are you sure to delete it?", - "Component.DeleteNamespace.PrivateContent": "Deleting a private Namespace will cause the instances unable to get the configuration of this namespace, and the page will prompt 'missing namespace' (unless the AppNamespace is deleted by admin tool). Are you sure to delete it?", + "Component.DeleteNamespace.PublicContent": "Caution, the public namespace for all environments will be deleted! This will cause the instances unable to get the configuration of this namespace. Are you sure you want to delete it?", + "Component.DeleteNamespace.PrivateContent": "Caution, the private namespace for all environments will be deleted! This will cause the instances unable to get the configuration of this namespace. Are you sure you want to delete it?", + "Component.DeleteNamespace.LinkedContent": "Caution, all the namespaces associated with the current environment will be deleted! This will cause the instances unable to get the configuration of this namespace. Are you sure you want to delete it?", + "Component.DeleteNamespace.ForceDeleteContent": "There are instances in use for the current namespace within 24 hours, are you sure to force delete the namespace?", "Component.GrayscalePublishRule.Title": "Edit Grayscale Rule", "Component.GrayscalePublishRule.AppId": "Grayscale AppId", "Component.GrayscalePublishRule.AcceptRule": "Grayscale Application Rule", @@ -428,8 +434,8 @@ "Delete.ClusterNameTips": "(Please query cluster information before deletion)", "Delete.ClusterInfo": "Cluster information", "Delete.DeleteNamespace": "Delete AppNamespace", - "Delete.DeleteNamespaceTips": "(Note that Namespace and AppNamespace in all environments will be deleted! If you just want to delete the namespace of some environment, let the user delete it on the project page!", - "Delete.DeleteNamespaceTips2": "Currently users can delete the associated namespace and private namespace by themselves, but they can not delete the AppNamespace. Because deleting AppNamespace has very large impacts, it is only allowed to be deleted by system administrators for the time being. For public Namespace, it is necessary to ensure that no application associates the AppNamespace", + "Delete.DeleteNamespaceTips": "(Note that Namespace and AppNamespace in all environments will be deleted!", + "Delete.DeleteNamespaceTips2": "For public Namespace, it is necessary to ensure that no application associates the AppNamespace", "Delete.AppNamespaceName": "AppNamespace name", "Delete.AppNamespaceNameTips": "(For non-properties namespaces, please add the suffix, such as apollo.xml)", "Delete.AppNamespaceInfo": "AppNamespace Information", diff --git a/apollo-portal/src/main/resources/static/i18n/zh-CN.json b/apollo-portal/src/main/resources/static/i18n/zh-CN.json index 7787a7a76be..ec4a4074eed 100644 --- a/apollo-portal/src/main/resources/static/i18n/zh-CN.json +++ b/apollo-portal/src/main/resources/static/i18n/zh-CN.json @@ -16,9 +16,12 @@ "Common.Department": "部门", "Common.Cluster": "集群", "Common.Environment": "环境", + "Common.GrayscaleInstance": "灰度实例", + "Common.Instance": "实例", "Common.Email": "邮箱", "Common.AppId": "AppId", "Common.Namespace": "Namespace", + "Common.LinkedNamespace": "关联的 Namespace", "Common.AppName": "应用名称", "Common.AppOwner": "负责人", "Common.AppOwnerLong": "应用负责人", @@ -40,9 +43,12 @@ "Common.LoginExpiredTips": "您的登录信息已过期,请刷新页面后重试", "Common.Operation": "操作", "Common.Delete": "删除", + "Common.ForceDelete": "强制删除", "Component.DeleteNamespace.Title": "删除 Namespace", - "Component.DeleteNamespace.PublicContent": "删除 Namespace 将导致实例获取不到此 Namespace 的配置,确定要删除吗?", - "Component.DeleteNamespace.PrivateContent": "删除私有 Namespace 将导致实例获取不到此 Namespace 的配置,且页面会提示缺失 Namespace(除非使用管理员工具删除 AppNamespace),确定要删除吗?", + "Component.DeleteNamespace.PublicContent": "注意,所有环境的公共 Namespace 都会被删除!这将导致实例以及关联的 Namespace 获取不到此 Namespace 的配置,确定要删除吗?", + "Component.DeleteNamespace.PrivateContent": "注意,所有环境的私有 Namespace 都会被删除!这将导致实例获取不到此 Namespace 的配置,确定要删除吗?", + "Component.DeleteNamespace.LinkedContent": "注意,当前环境关联的 Namespace 会被删除!这将导致实例获取不到此 Namespace 的配置,确定要删除吗?", + "Component.DeleteNamespace.ForceDeleteContent": "当前 Namespace 在 24H 内,存在使用中的实例,二次确认是否强制删除?", "Component.GrayscalePublishRule.Title": "编辑灰度规则", "Component.GrayscalePublishRule.AppId": "灰度的 AppId", "Component.GrayscalePublishRule.AcceptRule": "灰度应用规则", @@ -428,8 +434,8 @@ "Delete.ClusterNameTips": "(删除前请先查询应用集群信息)", "Delete.ClusterInfo": "集群信息", "Delete.DeleteNamespace": "删除 AppNamespace", - "Delete.DeleteNamespaceTips": "(注意,所有环境的 Namespace 和 AppNamespace 都会被删除!如果只是要删除某个环境的 Namespace,让用户到应用页面中自行删除!)", - "Delete.DeleteNamespaceTips2": "目前用户可以自行删除关联的 Namespace 和私有的 Namespace,不过无法删除 AppNamespace 元信息,因为删除 AppNamespace 影响面较大,所以现在暂时只允许系统管理员删除,对于公共 Namespace 需要确保没有应用关联了该 AppNamespace。", + "Delete.DeleteNamespaceTips": "(注意,所有环境的 Namespace 和 AppNamespace 都会被删除!)", + "Delete.DeleteNamespaceTips2": "对于公共 Namespace 需要确保没有应用关联了该 AppNamespace。", "Delete.AppNamespaceName": "AppNamespace 名称", "Delete.AppNamespaceNameTips": "(非 properties 类型的 Namespace 请加上类型后缀,例如 apollo.xml)", "Delete.AppNamespaceInfo": "AppNamespace 信息", diff --git a/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigNamespaceController.js b/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigNamespaceController.js index 31935448b11..d79d9791006 100644 --- a/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigNamespaceController.js +++ b/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigNamespaceController.js @@ -404,33 +404,6 @@ function controller($rootScope, $scope, $translate, toastr, AppUtil, EventManage } - EventManager.subscribe(EventManager.EventType.DELETE_NAMESPACE_FAILED, function (context) { - $scope.deleteNamespaceContext = context; - - if (context.reason == 'master_instance') { - AppUtil.showModal('#deleteNamespaceDenyForMasterInstanceDialog'); - } else if (context.reason == 'branch_instance') { - AppUtil.showModal('#deleteNamespaceDenyForBranchInstanceDialog'); - } else if (context.reason == 'public_namespace') { - var otherAppAssociatedNamespaces = context.otherAppAssociatedNamespaces; - var namespaceTips = []; - otherAppAssociatedNamespaces.forEach(function (namespace) { - var appId = namespace.appId; - var clusterName = namespace.clusterName; - var url = AppUtil.prefixPath() + '/config.html?#/appid=' + appId + '&env=' + $scope.pageContext.env + '&cluster=' - + clusterName; - - namespaceTips.push("AppId = " + appId + ", Cluster = " + clusterName - + ", Namespace = " + namespace.namespaceName + ""); - }); - - $scope.deleteNamespaceContext.detailReason = $translate.instant('Config.DeleteNamespaceFailedTips') + "
" + namespaceTips.join("
"); - - AppUtil.showModal('#deleteNamespaceDenyForPublicNamespaceDialog'); - } - - }); - EventManager.subscribe(EventManager.EventType.SYNTAX_CHECK_TEXT_FAILED, function (context) { $scope.syntaxCheckContext = context; diff --git a/apollo-portal/src/main/resources/static/scripts/directive/delete-namespace-modal-directive.js b/apollo-portal/src/main/resources/static/scripts/directive/delete-namespace-modal-directive.js index f21ddeb1b50..440bca5f7de 100644 --- a/apollo-portal/src/main/resources/static/scripts/directive/delete-namespace-modal-directive.js +++ b/apollo-portal/src/main/resources/static/scripts/directive/delete-namespace-modal-directive.js @@ -28,8 +28,6 @@ function deleteNamespaceModalDirective($window, $q, $translate, toastr, AppUtil, }, link: function (scope) { - - scope.doDeleteNamespace = doDeleteNamespace; EventManager.subscribe(EventManager.EventType.PRE_DELETE_NAMESPACE, function (context) { @@ -39,21 +37,25 @@ function deleteNamespaceModalDirective($window, $q, $translate, toastr, AppUtil, //1. check operator has master permission checkPermission(toDeleteNamespace).then(function () { - //2. check namespace's master branch has not instances - if (!checkMasterInstance(toDeleteNamespace)) { - return; - } - - //3. check namespace's gray branch has not instances - if (!checkBranchInstance(toDeleteNamespace)) { - return; - } - - if (!toDeleteNamespace.isPublic || toDeleteNamespace.isLinkedNamespace) { - showDeleteNamespaceConfirmDialog(); + if (toDeleteNamespace.isLinkedNamespace) { + NamespaceService.getLinkedNamespaceUsage(toDeleteNamespace.baseInfo.appId, scope.env, + toDeleteNamespace.baseInfo.clusterName, + toDeleteNamespace.baseInfo.namespaceName + ).then(function (usage) { + scope.toDeleteNamespace.namespaceUsage = usage; + if (usage[0].instanceCount > 0 || usage[0].branchInstanceCount > 0) { + scope.toDeleteNamespace.forceDeleteButton = true; + } + showDeleteNamespaceConfirmDialog(); + }); } else { - //5. check public namespace has not associated namespace - checkPublicNamespace(toDeleteNamespace).then(function () { + NamespaceService.getNamespaceUsage(toDeleteNamespace.baseInfo.appId, + toDeleteNamespace.baseInfo.namespaceName + ).then(function (usage) { + scope.toDeleteNamespace.namespaceUsage = usage; + if (usage.length > 0) { + scope.toDeleteNamespace.forceDeleteButton = true; + } showDeleteNamespaceConfirmDialog(); }); } @@ -98,73 +100,16 @@ function deleteNamespaceModalDirective($window, $q, $translate, toastr, AppUtil, return d.promise; } - function checkMasterInstance(namespace) { - if (namespace.instancesCount > 0) { - EventManager.emit(EventManager.EventType.DELETE_NAMESPACE_FAILED, { - namespace: namespace, - reason: 'master_instance' - }); - - return false; - } - - return true; - } - - function checkBranchInstance(namespace) { - if (namespace.hasBranch && namespace.branch.latestReleaseInstances.total > 0) { - EventManager.emit(EventManager.EventType.DELETE_NAMESPACE_FAILED, { - namespace: namespace, - reason: 'branch_instance' - }); - - return false; - } - - return true; - } - - function checkPublicNamespace(namespace) { - var d = $q.defer(); - - var publicAppId = namespace.baseInfo.appId; - NamespaceService.getPublicAppNamespaceAllNamespaces(scope.env, - namespace.baseInfo.namespaceName, - 0, 20) - .then(function (associatedNamespaces) { - var otherAppAssociatedNamespaces = []; - associatedNamespaces.forEach(function (associatedNamespace) { - if (associatedNamespace.appId != publicAppId) { - otherAppAssociatedNamespaces.push(associatedNamespace); - } - }); - - if (otherAppAssociatedNamespaces.length) { - EventManager.emit(EventManager.EventType.DELETE_NAMESPACE_FAILED, { - namespace: namespace, - reason: 'public_namespace', - otherAppAssociatedNamespaces: otherAppAssociatedNamespaces - }); - d.reject(); - } else { - d.resolve(); - } - - }); - - return d.promise; - - } - function showDeleteNamespaceConfirmDialog() { AppUtil.showModal('#deleteNamespaceModal'); } function doDeleteNamespace() { var toDeleteNamespace = scope.toDeleteNamespace; - NamespaceService.deleteNamespace(toDeleteNamespace.baseInfo.appId, scope.env, - toDeleteNamespace.baseInfo.clusterName, - toDeleteNamespace.baseInfo.namespaceName) + if(toDeleteNamespace.isLinkedNamespace){ + NamespaceService.deleteLinkedNamespace(toDeleteNamespace.baseInfo.appId, scope.env, + toDeleteNamespace.baseInfo.clusterName, + toDeleteNamespace.baseInfo.namespaceName) .then(function () { toastr.success($translate.instant('Common.Deleted')); @@ -175,7 +120,20 @@ function deleteNamespaceModalDirective($window, $q, $translate, toastr, AppUtil, }, function (result) { AppUtil.showErrorMsg(result, $translate.instant('Common.DeleteFailed')); }) + } else { + NamespaceService.deleteAppNamespace(toDeleteNamespace.baseInfo.appId, + toDeleteNamespace.baseInfo.namespaceName) + .then(function () { + toastr.success($translate.instant('Common.Deleted')); + + setTimeout(function () { + $window.location.reload(); + }, 1000); + }, function (result) { + AppUtil.showErrorMsg(result, $translate.instant('Common.DeleteFailed')); + }) + } } } diff --git a/apollo-portal/src/main/resources/static/scripts/services/EventManager.js b/apollo-portal/src/main/resources/static/scripts/services/EventManager.js index 74459f4896c..86b69b8fcd8 100644 --- a/apollo-portal/src/main/resources/static/scripts/services/EventManager.js +++ b/apollo-portal/src/main/resources/static/scripts/services/EventManager.js @@ -127,7 +127,6 @@ appService.service('EventManager', [function () { PRE_DELETE_NAMESPACE: 'pre_delete_namespace', PRE_IMPORT_NAMESPACE: 'pre_import_namespace', DELETE_NAMESPACE: 'delete_namespace', - DELETE_NAMESPACE_FAILED: 'delete_namespace_failed', CHANGE_ENV_CLUSTER: "change_env_cluster", SYNTAX_CHECK_TEXT_FAILED: "syntax_check_text_failed" } diff --git a/apollo-portal/src/main/resources/static/scripts/services/NamespaceService.js b/apollo-portal/src/main/resources/static/scripts/services/NamespaceService.js index f59125b33d5..dcc57bafb72 100644 --- a/apollo-portal/src/main/resources/static/scripts/services/NamespaceService.js +++ b/apollo-portal/src/main/resources/static/scripts/services/NamespaceService.js @@ -35,9 +35,9 @@ appService.service("NamespaceService", ['$resource', '$q', 'AppUtil', function ( method: 'GET', url: AppUtil.prefixPath() + '/apps/:appId/namespaces/publish_info' }, - deleteNamespace: { + deleteLinkedNamespace: { method: 'DELETE', - url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName' + url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/linked-namespaces/:namespaceName' }, getPublicAppNamespaceAllNamespaces: { method: 'GET', @@ -51,6 +51,16 @@ appService.service("NamespaceService", ['$resource', '$q', 'AppUtil', function ( deleteAppNamespace: { method: 'DELETE', url: AppUtil.prefixPath() + '/apps/:appId/appnamespaces/:namespaceName' + }, + getLinkedNamespaceUsage: { + method: 'GET', + url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/linked-namespaces/:namespaceName/usage', + isArray: true + }, + getNamespaceUsage: { + method: 'GET', + url: AppUtil.prefixPath() + '/apps/:appId/namespaces/:namespaceName/usage', + isArray: true } }); @@ -102,20 +112,20 @@ appService.service("NamespaceService", ['$resource', '$q', 'AppUtil', function ( return d.promise; } - function deleteNamespace(appId, env, clusterName, namespaceName) { + function deleteLinkedNamespace(appId, env, clusterName, namespaceName) { var d = $q.defer(); - namespace_source.deleteNamespace({ - appId: appId, - env: env, - clusterName: clusterName, - namespaceName: namespaceName - }, - function (result) { - d.resolve(result); - }, - function (result) { - d.reject(result); - }); + namespace_source.deleteLinkedNamespace({ + appId: appId, + env: env, + clusterName: clusterName, + namespaceName: namespaceName + }, + function (result) { + d.resolve(result); + }, + function (result) { + d.reject(result); + }); return d.promise; } @@ -169,15 +179,49 @@ appService.service("NamespaceService", ['$resource', '$q', 'AppUtil', function ( return d.promise; } + function getLinkedNamespaceUsage(appId, env, clusterName, namespaceName) { + var d = $q.defer(); + namespace_source.getLinkedNamespaceUsage({ + appId: appId, + env: env, + clusterName: clusterName, + namespaceName: namespaceName + }, + function (result) { + d.resolve(result); + }, + function (result) { + d.reject(result); + }); + return d.promise; + } + + function getNamespaceUsage(appId, namespaceName) { + var d = $q.defer(); + namespace_source.getNamespaceUsage({ + appId: appId, + namespaceName: namespaceName + }, + function (result) { + d.resolve(result); + }, + function (result) { + d.reject(result); + }); + return d.promise; + } + return { find_public_namespaces: find_public_namespaces, createNamespace: createNamespace, createAppNamespace: createAppNamespace, getNamespacePublishInfo: getNamespacePublishInfo, - deleteNamespace: deleteNamespace, + deleteLinkedNamespace: deleteLinkedNamespace, getPublicAppNamespaceAllNamespaces: getPublicAppNamespaceAllNamespaces, loadAppNamespace: loadAppNamespace, - deleteAppNamespace: deleteAppNamespace + deleteAppNamespace: deleteAppNamespace, + getLinkedNamespaceUsage: getLinkedNamespaceUsage, + getNamespaceUsage: getNamespaceUsage } }]); diff --git a/apollo-portal/src/main/resources/static/views/component/delete-namespace-modal.html b/apollo-portal/src/main/resources/static/views/component/delete-namespace-modal.html index 6ca3eb81709..e5eaa31ba43 100644 --- a/apollo-portal/src/main/resources/static/views/component/delete-namespace-modal.html +++ b/apollo-portal/src/main/resources/static/views/component/delete-namespace-modal.html @@ -24,20 +24,53 @@ - diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/NamespaceServiceTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/NamespaceServiceTest.java index 2bc8bbe813d..0ceab7b41d7 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/NamespaceServiceTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/NamespaceServiceTest.java @@ -16,12 +16,14 @@ */ package com.ctrip.framework.apollo.portal.service; +import com.ctrip.framework.apollo.common.dto.ClusterDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.NamespaceDTO; import com.ctrip.framework.apollo.common.dto.ReleaseDTO; import com.ctrip.framework.apollo.common.entity.AppNamespace; -import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.ctrip.framework.apollo.portal.component.PortalSettings; +import com.ctrip.framework.apollo.portal.entity.vo.NamespaceUsage; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.AbstractUnitTest; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; @@ -40,6 +42,7 @@ import java.util.Collections; import java.util.List; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; @@ -67,6 +70,10 @@ public class NamespaceServiceTest extends AbstractUnitTest { private UserInfoHolder userInfoHolder; @Mock private AdditionalUserInfoEnrichService additionalUserInfoEnrichService; + @Mock + private PortalSettings portalSettings; + @Mock + private ClusterService clusterService; @InjectMocks private NamespaceService namespaceService; @@ -156,51 +163,38 @@ public void testDeletePrivateNamespace() { verify(namespaceAPI, times(1)).deleteNamespace(testEnv, testAppId, testClusterName, testNamespaceName, operator); } - @Test(expected = BadRequestException.class) - public void testDeleteNamespaceHasInstance() { - AppNamespace publicNamespace = createAppNamespace(testAppId, testNamespaceName, true); - - when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(publicNamespace); - when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) - .thenReturn(10); - - namespaceService.deleteNamespace(testAppId, testEnv, testClusterName, testNamespaceName); - - } - - @Test(expected = BadRequestException.class) - public void testDeleteNamespaceBranchHasInstance() { - AppNamespace publicNamespace = createAppNamespace(testAppId, testNamespaceName, true); - String branchName = "branch"; - NamespaceDTO branch = createNamespace(testAppId, branchName, testNamespaceName); - - when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(publicNamespace); - when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) - .thenReturn(0); - when(branchService.findBranchBaseInfo(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(branch); - when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, branchName, testNamespaceName)).thenReturn(10); - - namespaceService.deleteNamespace(testAppId, testEnv, testClusterName, testNamespaceName); - - } - - @Test(expected = BadRequestException.class) - public void testDeleteNamespaceWithAssociatedNamespace() { + @Test + public void testGetNamespaceUsage() { AppNamespace publicNamespace = createAppNamespace(testAppId, testNamespaceName, true); String branchName = "branch"; NamespaceDTO branch = createNamespace(testAppId, branchName, testNamespaceName); + when(portalSettings.getActiveEnvs()).thenReturn(Lists.newArrayList(testEnv)); + ClusterDTO cluster = new ClusterDTO(); + cluster.setName(testClusterName); + cluster.setAppId(testAppId); + when(clusterService.findClusters(testEnv, testAppId)).thenReturn(Lists.newArrayList(cluster)); when(appNamespaceService.findByAppIdAndName(testAppId, testNamespaceName)).thenReturn(publicNamespace); when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, testClusterName, testNamespaceName)) - .thenReturn(0); + .thenReturn(8); when(branchService.findBranchBaseInfo(testAppId, testEnv, testClusterName, testNamespaceName)).thenReturn(branch); - when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, branchName, testNamespaceName)).thenReturn(0); + when(instanceService.getInstanceCountByNamespace(testAppId, testEnv, branchName, testNamespaceName)).thenReturn(9); when(appNamespaceService.findPublicAppNamespace(testNamespaceName)).thenReturn(publicNamespace); - when(namespaceAPI.countPublicAppNamespaceAssociatedNamespaces(testEnv, testNamespaceName)).thenReturn(10); + when(namespaceAPI.countPublicAppNamespaceAssociatedNamespaces(testEnv, testNamespaceName)).thenReturn(10); - namespaceService.deleteNamespace(testAppId, testEnv, testClusterName, testNamespaceName); + List usages = namespaceService.getNamespaceUsageByAppId(testAppId, testNamespaceName); + assertThat(usages).asList().hasSize(1); + assertThat(usages.get(0).getInstanceCount()).isEqualTo(8); + assertThat(usages.get(0).getBranchInstanceCount()).isEqualTo(9); + assertThat(usages.get(0).getLinkedNamespaceCount()).isEqualTo(10); + + NamespaceUsage usage = namespaceService.getNamespaceUsageByEnv(testAppId, testNamespaceName, testEnv, testClusterName); + assertThat(usage).isNotNull(); + assertThat(usage.getInstanceCount()).isEqualTo(8); + assertThat(usage.getBranchInstanceCount()).isEqualTo(9); + assertThat(usage.getLinkedNamespaceCount()).isEqualTo(0); } @Test