From ba197ed36b340de582f7c46f6a0cfb975e592a9c Mon Sep 17 00:00:00 2001 From: luke <348358584@qq.com> Date: Sun, 26 Jan 2025 18:50:12 +0800 Subject: [PATCH] feat: Email notification when releasing by OpenAPI (#5324) --- CHANGES.md | 2 + .../v1/controller/ReleaseController.java | 176 +++++++++++------- changes/changes-2.4.0.md | 1 - 3 files changed, 110 insertions(+), 69 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3591e1fd14f..b1bca735d35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ Apollo 2.4.0 * [Feature: add JSON formatting function in apollo-portal](https://github.com/apolloconfig/apollo/pull/5287) * [Fix: add missing url patterns for AdminServiceAuthenticationFilter](https://github.com/apolloconfig/apollo/pull/5291) * [Fix: support java.time.Instant serialization with gson](https://github.com/apolloconfig/apollo/pull/5298) +* [Feature: support to assign users management authority by the cluster (modify, publish)](https://github.com/apolloconfig/apollo/pull/5302) +* [Feature: notification by email when releasing by OpenApi also](https://github.com/apolloconfig/apollo/pull/5324) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ReleaseController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ReleaseController.java index cb0d0ef9e14..b12895bb5af 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ReleaseController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ReleaseController.java @@ -30,10 +30,12 @@ import com.ctrip.framework.apollo.openapi.util.OpenApiBeanUtils; import com.ctrip.framework.apollo.portal.entity.model.NamespaceGrayDelReleaseModel; import com.ctrip.framework.apollo.portal.entity.model.NamespaceReleaseModel; +import com.ctrip.framework.apollo.portal.listener.ConfigPublishEvent; import com.ctrip.framework.apollo.portal.service.NamespaceBranchService; import com.ctrip.framework.apollo.portal.service.ReleaseService; import com.ctrip.framework.apollo.portal.spi.UserService; import javax.servlet.http.HttpServletRequest; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; @@ -54,18 +56,21 @@ public class ReleaseController { private final NamespaceBranchService namespaceBranchService; private final ConsumerPermissionValidator consumerPermissionValidator; private final ReleaseOpenApiService releaseOpenApiService; + private final ApplicationEventPublisher publisher; public ReleaseController( final ReleaseService releaseService, final UserService userService, final NamespaceBranchService namespaceBranchService, final ConsumerPermissionValidator consumerPermissionValidator, - ReleaseOpenApiService releaseOpenApiService) { + ReleaseOpenApiService releaseOpenApiService, + ApplicationEventPublisher publisher) { this.releaseService = releaseService; this.userService = userService; this.namespaceBranchService = namespaceBranchService; this.consumerPermissionValidator = consumerPermissionValidator; this.releaseOpenApiService = releaseOpenApiService; + this.publisher = publisher; } @PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)") @@ -83,7 +88,19 @@ public OpenReleaseDTO createRelease(@PathVariable String appId, @PathVariable St throw BadRequestException.userNotExists(model.getReleasedBy()); } - return this.releaseOpenApiService.publishNamespace(appId, env, clusterName, namespaceName, model); + OpenReleaseDTO releaseDTO = this.releaseOpenApiService.publishNamespace(appId, env, + clusterName, namespaceName, model); + + ConfigPublishEvent event = ConfigPublishEvent.instance(); + event.withAppId(appId) + .withCluster(clusterName) + .withNamespace(namespaceName) + .withReleaseId(releaseDTO.getId()) + .setNormalPublishEvent(true) + .setEnv(Env.valueOf(env)); + publisher.publishEvent(event); + + return releaseDTO; } @GetMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/latest") @@ -93,78 +110,91 @@ public OpenReleaseDTO loadLatestActiveRelease(@PathVariable String appId, @PathV return this.releaseOpenApiService.getLatestActiveRelease(appId, env, clusterName, namespaceName); } - @PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)") - @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge") - public OpenReleaseDTO merge(@PathVariable String appId, @PathVariable String env, - @PathVariable String clusterName, @PathVariable String namespaceName, - @PathVariable String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch, - @RequestBody NamespaceReleaseDTO model, HttpServletRequest request) { - RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(model.getReleasedBy(), model - .getReleaseTitle()), - "Params(releaseTitle and releasedBy) can not be empty"); - - if (userService.findByUserId(model.getReleasedBy()) == null) { - throw BadRequestException.userNotExists(model.getReleasedBy()); - } - - ReleaseDTO mergedRelease = namespaceBranchService.merge(appId, Env.valueOf(env.toUpperCase()), clusterName, namespaceName, branchName, - model.getReleaseTitle(), model.getReleaseComment(), - model.isEmergencyPublish(), deleteBranch, model.getReleasedBy()); - - return OpenApiBeanUtils.transformFromReleaseDTO(mergedRelease); + @PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)") + @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge") + public OpenReleaseDTO merge(@PathVariable String appId, @PathVariable String env, + @PathVariable String clusterName, @PathVariable String namespaceName, + @PathVariable String branchName, + @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch, + @RequestBody NamespaceReleaseDTO model, HttpServletRequest request) { + RequestPrecondition.checkArguments( + !StringUtils.isContainEmpty(model.getReleasedBy(), model.getReleaseTitle()), + "Params(releaseTitle and releasedBy) can not be empty"); + + if (userService.findByUserId(model.getReleasedBy()) == null) { + throw BadRequestException.userNotExists(model.getReleasedBy()); } - @PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)") - @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/releases") - public OpenReleaseDTO createGrayRelease(@PathVariable String appId, - @PathVariable String env, @PathVariable String clusterName, - @PathVariable String namespaceName, @PathVariable String branchName, - @RequestBody NamespaceReleaseDTO model, - HttpServletRequest request) { - RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(model.getReleasedBy(), model - .getReleaseTitle()), - "Params(releaseTitle and releasedBy) can not be empty"); - - if (userService.findByUserId(model.getReleasedBy()) == null) { - throw BadRequestException.userNotExists(model.getReleasedBy()); - } - - NamespaceReleaseModel releaseModel = BeanUtils.transform(NamespaceReleaseModel.class, model); - - releaseModel.setAppId(appId); - releaseModel.setEnv(Env.valueOf(env).toString()); - releaseModel.setClusterName(branchName); - releaseModel.setNamespaceName(namespaceName); - - return OpenApiBeanUtils.transformFromReleaseDTO(releaseService.publish(releaseModel)); + ReleaseDTO mergedRelease = namespaceBranchService.merge(appId, Env.valueOf(env.toUpperCase()), + clusterName, namespaceName, branchName, model.getReleaseTitle(), model.getReleaseComment(), + model.isEmergencyPublish(), deleteBranch, model.getReleasedBy()); + + ConfigPublishEvent event = ConfigPublishEvent.instance(); + event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) + .withReleaseId(mergedRelease.getId()).setMergeEvent(true).setEnv(Env.valueOf(env)); + publisher.publishEvent(event); + + return OpenApiBeanUtils.transformFromReleaseDTO(mergedRelease); + } + + @PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)") + @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/releases") + public OpenReleaseDTO createGrayRelease(@PathVariable String appId, @PathVariable String env, + @PathVariable String clusterName, @PathVariable String namespaceName, + @PathVariable String branchName, @RequestBody NamespaceReleaseDTO model, + HttpServletRequest request) { + RequestPrecondition.checkArguments( + !StringUtils.isContainEmpty(model.getReleasedBy(), model.getReleaseTitle()), + "Params(releaseTitle and releasedBy) can not be empty"); + + if (userService.findByUserId(model.getReleasedBy()) == null) { + throw BadRequestException.userNotExists(model.getReleasedBy()); } - @PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)") - @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/gray-del-releases") - public OpenReleaseDTO createGrayDelRelease(@PathVariable String appId, - @PathVariable String env, @PathVariable String clusterName, - @PathVariable String namespaceName, @PathVariable String branchName, - @RequestBody NamespaceGrayDelReleaseDTO model, - HttpServletRequest request) { - RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(model.getReleasedBy(), model - .getReleaseTitle()), - "Params(releaseTitle and releasedBy) can not be empty"); - RequestPrecondition.checkArguments(model.getGrayDelKeys() != null, - "Params(grayDelKeys) can not be null"); - - if (userService.findByUserId(model.getReleasedBy()) == null) { - throw BadRequestException.userNotExists(model.getReleasedBy()); - } - - NamespaceGrayDelReleaseModel releaseModel = BeanUtils.transform(NamespaceGrayDelReleaseModel.class, model); - releaseModel.setAppId(appId); - releaseModel.setEnv(env.toUpperCase()); - releaseModel.setClusterName(branchName); - releaseModel.setNamespaceName(namespaceName); - - return OpenApiBeanUtils.transformFromReleaseDTO(releaseService.publish(releaseModel, releaseModel.getReleasedBy())); + NamespaceReleaseModel releaseModel = BeanUtils.transform(NamespaceReleaseModel.class, model); + + releaseModel.setAppId(appId); + releaseModel.setEnv(Env.valueOf(env).toString()); + releaseModel.setClusterName(branchName); + releaseModel.setNamespaceName(namespaceName); + + ReleaseDTO releaseDTO = releaseService.publish(releaseModel); + + ConfigPublishEvent event = ConfigPublishEvent.instance(); + event.withAppId(appId).withCluster(clusterName).withNamespace(namespaceName) + .withReleaseId(releaseDTO.getId()).setGrayPublishEvent(true).setEnv(Env.valueOf(env)); + publisher.publishEvent(event); + + return OpenApiBeanUtils.transformFromReleaseDTO(releaseDTO); + } + + @PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName, #env)") + @PostMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/gray-del-releases") + public OpenReleaseDTO createGrayDelRelease(@PathVariable String appId, @PathVariable String env, + @PathVariable String clusterName, @PathVariable String namespaceName, + @PathVariable String branchName, @RequestBody NamespaceGrayDelReleaseDTO model, + HttpServletRequest request) { + RequestPrecondition.checkArguments( + !StringUtils.isContainEmpty(model.getReleasedBy(), model.getReleaseTitle()), + "Params(releaseTitle and releasedBy) can not be empty"); + RequestPrecondition.checkArguments(model.getGrayDelKeys() != null, + "Params(grayDelKeys) can not be null"); + + if (userService.findByUserId(model.getReleasedBy()) == null) { + throw BadRequestException.userNotExists(model.getReleasedBy()); } + NamespaceGrayDelReleaseModel releaseModel = BeanUtils.transform( + NamespaceGrayDelReleaseModel.class, model); + releaseModel.setAppId(appId); + releaseModel.setEnv(env.toUpperCase()); + releaseModel.setClusterName(branchName); + releaseModel.setNamespaceName(namespaceName); + + return OpenApiBeanUtils.transformFromReleaseDTO( + releaseService.publish(releaseModel, releaseModel.getReleasedBy())); + } + @PutMapping(path = "/releases/{releaseId}/rollback") public void rollback(@PathVariable String env, @PathVariable long releaseId, @RequestParam String operator, HttpServletRequest request) { @@ -185,6 +215,16 @@ public void rollback(@PathVariable String env, throw new AccessDeniedException("Forbidden operation. you don't have release permission"); } + ConfigPublishEvent event = ConfigPublishEvent.instance(); + event.withAppId(release.getAppId()) + .withCluster(release.getClusterName()) + .withNamespace(release.getNamespaceName()) + .withReleaseId(release.getId()) + .setRollbackEvent(true) + .setEnv(Env.valueOf(env)); + publisher.publishEvent(event); + + this.releaseOpenApiService.rollbackRelease(env, releaseId, operator); } diff --git a/changes/changes-2.4.0.md b/changes/changes-2.4.0.md index 3ebe2e98381..25fe3e1fb99 100644 --- a/changes/changes-2.4.0.md +++ b/changes/changes-2.4.0.md @@ -9,6 +9,5 @@ Apollo 2.4.0 * [Feature support portal restTemplate Client connection pool config](https://github.com/apolloconfig/apollo/pull/5200) * [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182) * [Feature support the observe status access-key for pre-check and logging only](https://github.com/apolloconfig/apollo/pull/5236) - ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)