Skip to content

Commit

Permalink
feat: Email notification when releasing by OpenAPI (#5324)
Browse files Browse the repository at this point in the history
  • Loading branch information
spaceluke authored Jan 26, 2025
1 parent 6c41ef0 commit ba197ed
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 69 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)")
Expand All @@ -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")
Expand All @@ -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) {
Expand All @@ -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);
}

Expand Down
1 change: 0 additions & 1 deletion changes/changes-2.4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit ba197ed

Please sign in to comment.