package com.ctrip.framework.apollo.biz.service; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gson.Gson; import com.ctrip.framework.apollo.biz.entity.Audit; import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule; import com.ctrip.framework.apollo.biz.entity.Item; import com.ctrip.framework.apollo.biz.entity.Namespace; import com.ctrip.framework.apollo.biz.entity.NamespaceLock; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.biz.repository.ReleaseRepository; import com.ctrip.framework.apollo.biz.utils.ReleaseKeyGenerator; import com.ctrip.framework.apollo.common.constants.GsonType; import com.ctrip.framework.apollo.common.constants.ReleaseOperation; import com.ctrip.framework.apollo.common.constants.ReleaseOperationContext; import com.ctrip.framework.apollo.common.dto.ItemChangeSets; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.common.utils.GrayReleaseRuleItemTransformer; import com.ctrip.framework.apollo.core.utils.StringUtils; import org.apache.commons.lang.time.FastDateFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class ReleaseService { private static final FastDateFormat TIMESTAMP_FORMAT = FastDateFormat.getInstance("yyyyMMddHHmmss"); private Gson gson = new Gson(); @Autowired private ReleaseRepository releaseRepository; @Autowired private ItemService itemService; @Autowired private AuditService auditService; @Autowired private NamespaceLockService namespaceLockService; @Autowired private NamespaceService namespaceService; @Autowired private NamespaceBranchService namespaceBranchService; @Autowired private ReleaseHistoryService releaseHistoryService; @Autowired private ItemSetService itemSetService; public Release findOne(long releaseId) { return releaseRepository.findOne(releaseId); } public Release findActiveOne(long releaseId) { return releaseRepository.findByIdAndIsAbandonedFalse(releaseId); } public List<Release> findByReleaseIds(Set<Long> releaseIds) { Iterable<Release> releases = releaseRepository.findAll(releaseIds); if (releases == null) { return Collections.emptyList(); } return Lists.newArrayList(releases); } public List<Release> findByReleaseKeys(Set<String> releaseKeys) { return releaseRepository.findByReleaseKeyIn(releaseKeys); } public Release findLatestActiveRelease(Namespace namespace) { return findLatestActiveRelease(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName()); } public Release findLatestActiveRelease(String appId, String clusterName, String namespaceName) { return releaseRepository.findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(appId, clusterName, namespaceName); } public List<Release> findAllReleases(String appId, String clusterName, String namespaceName, Pageable page) { List<Release> releases = releaseRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(appId, clusterName, namespaceName, page); if (releases == null) { return Collections.emptyList(); } return releases; } public List<Release> findActiveReleases(String appId, String clusterName, String namespaceName, Pageable page) { List<Release> releases = releaseRepository.findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(appId, clusterName, namespaceName, page); if (releases == null) { return Collections.emptyList(); } return releases; } @Transactional public Release mergeBranchChangeSetsAndRelease(Namespace namespace, String branchName, String releaseName, String releaseComment, boolean isEmergencyPublish, ItemChangeSets changeSets) { checkLock(namespace, isEmergencyPublish, changeSets.getDataChangeLastModifiedBy()); itemSetService.updateSet(namespace, changeSets); Release branchRelease = findLatestActiveRelease(namespace.getAppId(), branchName, namespace .getNamespaceName()); long branchReleaseId = branchRelease == null ? 0 : branchRelease.getId(); Map<String, String> operateNamespaceItems = getNamespaceItems(namespace); Map<String, Object> operationContext = Maps.newHashMap(); operationContext.put(ReleaseOperationContext.SOURCE_BRANCH, branchName); operationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, branchReleaseId); operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish); return masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems, changeSets.getDataChangeLastModifiedBy(), ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER, operationContext); } @Transactional public Release publish(Namespace namespace, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish) { checkLock(namespace, isEmergencyPublish, operator); Map<String, String> operateNamespaceItems = getNamespaceItems(namespace); Namespace parentNamespace = namespaceService.findParentNamespace(namespace); //branch release if (parentNamespace != null) { return publishBranchNamespace(parentNamespace, namespace, operateNamespaceItems, releaseName, releaseComment, operator, isEmergencyPublish); } Namespace childNamespace = namespaceService.findChildNamespace(namespace); Release previousRelease = null; if (childNamespace != null) { previousRelease = findLatestActiveRelease(namespace); } //master release Map<String, Object> operationContext = Maps.newHashMap(); operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish); Release release = masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems, operator, ReleaseOperation.NORMAL_RELEASE, operationContext); //merge to branch and auto release if (childNamespace != null) { mergeFromMasterAndPublishBranch(namespace, childNamespace, operateNamespaceItems, releaseName, releaseComment, operator, previousRelease, release, isEmergencyPublish); } return release; } private void checkLock(Namespace namespace, boolean isEmergencyPublish, String operator) { if (!isEmergencyPublish) { NamespaceLock lock = namespaceLockService.findLock(namespace.getId()); if (lock != null && lock.getDataChangeCreatedBy().equals(operator)) { throw new BadRequestException("Config can not be published by yourself."); } } } private void mergeFromMasterAndPublishBranch(Namespace parentNamespace, Namespace childNamespace, Map<String, String> parentNamespaceItems, String releaseName, String releaseComment, String operator, Release masterPreviousRelease, Release parentRelease, boolean isEmergencyPublish) { //create release for child namespace Map<String, String> childReleaseConfiguration = getNamespaceReleaseConfiguration(childNamespace); Map<String, String> parentNamespaceOldConfiguration = masterPreviousRelease == null ? null : gson.fromJson(masterPreviousRelease.getConfigurations(), GsonType.CONFIG); Map<String, String> childNamespaceToPublishConfigs = calculateChildNamespaceToPublishConfiguration(parentNamespaceOldConfiguration, parentNamespaceItems, childNamespace); //compare if (!childNamespaceToPublishConfigs.equals(childReleaseConfiguration)) { branchRelease(parentNamespace, childNamespace, releaseName, releaseComment, childNamespaceToPublishConfigs, parentRelease.getId(), operator, ReleaseOperation.MASTER_NORMAL_RELEASE_MERGE_TO_GRAY, isEmergencyPublish); } } private Release publishBranchNamespace(Namespace parentNamespace, Namespace childNamespace, Map<String, String> childNamespaceItems, String releaseName, String releaseComment, String operator, boolean isEmergencyPublish) { Release parentLatestRelease = findLatestActiveRelease(parentNamespace); Map<String, String> parentConfigurations = parentLatestRelease != null ? gson.fromJson(parentLatestRelease.getConfigurations(), GsonType.CONFIG) : new HashMap<>(); long baseReleaseId = parentLatestRelease == null ? 0 : parentLatestRelease.getId(); Map<String, String> childNamespaceToPublishConfigs = mergeConfiguration(parentConfigurations, childNamespaceItems); return branchRelease(parentNamespace, childNamespace, releaseName, releaseComment, childNamespaceToPublishConfigs, baseReleaseId, operator, ReleaseOperation.GRAY_RELEASE, isEmergencyPublish); } private Release masterRelease(Namespace namespace, String releaseName, String releaseComment, Map<String, String> configurations, String operator, int releaseOperation, Map<String, Object> operationContext) { Release lastActiveRelease = findLatestActiveRelease(namespace); long previousReleaseId = lastActiveRelease == null ? 0 : lastActiveRelease.getId(); Release release = createRelease(namespace, releaseName, releaseComment, configurations, operator); releaseHistoryService.createReleaseHistory(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName(), namespace.getClusterName(), release.getId(), previousReleaseId, releaseOperation, operationContext, operator); return release; } private Release branchRelease(Namespace parentNamespace, Namespace childNamespace, String releaseName, String releaseComment, Map<String, String> configurations, long baseReleaseId, String operator, int releaseOperation, boolean isEmergencyPublish) { Release previousRelease = findLatestActiveRelease(childNamespace.getAppId(), childNamespace.getClusterName(), childNamespace.getNamespaceName()); long previousReleaseId = previousRelease == null ? 0 : previousRelease.getId(); Map<String, Object> releaseOperationContext = Maps.newHashMap(); releaseOperationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, baseReleaseId); releaseOperationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish); Release release = createRelease(childNamespace, releaseName, releaseComment, configurations, operator); //update gray release rules GrayReleaseRule grayReleaseRule = namespaceBranchService.updateRulesReleaseId(childNamespace.getAppId(), parentNamespace.getClusterName(), childNamespace.getNamespaceName(), childNamespace.getClusterName(), release.getId(), operator); if (grayReleaseRule != null) { releaseOperationContext.put(ReleaseOperationContext.RULES, GrayReleaseRuleItemTransformer .batchTransformFromJSON(grayReleaseRule.getRules())); } releaseHistoryService.createReleaseHistory(parentNamespace.getAppId(), parentNamespace.getClusterName(), parentNamespace.getNamespaceName(), childNamespace.getClusterName(), release.getId(), previousReleaseId, releaseOperation, releaseOperationContext, operator); return release; } private Map<String, String> mergeConfiguration(Map<String, String> baseConfigurations, Map<String, String> coverConfigurations) { Map<String, String> result = new HashMap<>(); //copy base configuration for (Map.Entry<String, String> entry : baseConfigurations.entrySet()) { result.put(entry.getKey(), entry.getValue()); } //update and publish for (Map.Entry<String, String> entry : coverConfigurations.entrySet()) { result.put(entry.getKey(), entry.getValue()); } return result; } private Map<String, String> getNamespaceItems(Namespace namespace) { List<Item> items = itemService.findItemsWithoutOrdered(namespace.getId()); Map<String, String> configurations = new HashMap<String, String>(); for (Item item : items) { if (StringUtils.isEmpty(item.getKey())) { continue; } configurations.put(item.getKey(), item.getValue()); } return configurations; } private Map<String, String> getNamespaceReleaseConfiguration(Namespace namespace) { Release release = findLatestActiveRelease(namespace); Map<String, String> configuration = new HashMap<>(); if (release != null) { configuration = new Gson().fromJson(release.getConfigurations(), GsonType.CONFIG); } return configuration; } private Release createRelease(Namespace namespace, String name, String comment, Map<String, String> configurations, String operator) { Release release = new Release(); release.setReleaseKey(ReleaseKeyGenerator.generateReleaseKey(namespace)); release.setDataChangeCreatedTime(new Date()); release.setDataChangeCreatedBy(operator); release.setDataChangeLastModifiedBy(operator); release.setName(name); release.setComment(comment); release.setAppId(namespace.getAppId()); release.setClusterName(namespace.getClusterName()); release.setNamespaceName(namespace.getNamespaceName()); release.setConfigurations(gson.toJson(configurations)); release = releaseRepository.save(release); namespaceLockService.unlock(namespace.getId()); auditService.audit(Release.class.getSimpleName(), release.getId(), Audit.OP.INSERT, release.getDataChangeCreatedBy()); return release; } @Transactional public Release rollback(long releaseId, String operator) { Release release = findOne(releaseId); if (release == null) { throw new NotFoundException("release not found"); } if (release.isAbandoned()) { throw new BadRequestException("release is not active"); } String appId = release.getAppId(); String clusterName = release.getClusterName(); String namespaceName = release.getNamespaceName(); PageRequest page = new PageRequest(0, 2); List<Release> twoLatestActiveReleases = findActiveReleases(appId, clusterName, namespaceName, page); if (twoLatestActiveReleases == null || twoLatestActiveReleases.size() < 2) { throw new BadRequestException(String.format( "Can't rollback namespace(appId=%s, clusterName=%s, namespaceName=%s) because there is only one active release", appId, clusterName, namespaceName)); } release.setAbandoned(true); release.setDataChangeLastModifiedBy(operator); releaseRepository.save(release); releaseHistoryService.createReleaseHistory(appId, clusterName, namespaceName, clusterName, twoLatestActiveReleases.get(1).getId(), release.getId(), ReleaseOperation.ROLLBACK, null, operator); //publish child namespace if namespace has child rollbackChildNamespace(appId, clusterName, namespaceName, twoLatestActiveReleases, operator); return release; } private void rollbackChildNamespace(String appId, String clusterName, String namespaceName, List<Release> parentNamespaceTwoLatestActiveRelease, String operator) { Namespace parentNamespace = namespaceService.findOne(appId, clusterName, namespaceName); Namespace childNamespace = namespaceService.findChildNamespace(appId, clusterName, namespaceName); if (parentNamespace == null || childNamespace == null) { return; } Release abandonedRelease = parentNamespaceTwoLatestActiveRelease.get(0); Release parentNamespaceNewLatestRelease = parentNamespaceTwoLatestActiveRelease.get(1); Map<String, String> parentNamespaceAbandonedConfiguration = gson.fromJson(abandonedRelease.getConfigurations(), GsonType.CONFIG); Map<String, String> parentNamespaceNewLatestConfiguration = gson.fromJson(parentNamespaceNewLatestRelease.getConfigurations(), GsonType.CONFIG); Map<String, String> childNamespaceNewConfiguration = calculateChildNamespaceToPublishConfiguration(parentNamespaceAbandonedConfiguration, parentNamespaceNewLatestConfiguration, childNamespace); branchRelease(parentNamespace, childNamespace, TIMESTAMP_FORMAT.format(new Date()) + "-master-rollback-merge-to-gray", "", childNamespaceNewConfiguration, parentNamespaceNewLatestRelease.getId(), operator, ReleaseOperation.MATER_ROLLBACK_MERGE_TO_GRAY, false); } private Map<String, String> calculateChildNamespaceToPublishConfiguration( Map<String, String> parentNamespaceOldConfiguration, Map<String, String> parentNamespaceNewConfiguration, Namespace childNamespace) { //first. calculate child namespace modified configs Release childNamespaceLatestActiveRelease = findLatestActiveRelease(childNamespace); Map<String, String> childNamespaceLatestActiveConfiguration = childNamespaceLatestActiveRelease == null ? null : gson.fromJson(childNamespaceLatestActiveRelease .getConfigurations(), GsonType.CONFIG); Map<String, String> childNamespaceModifiedConfiguration = calculateBranchModifiedItemsAccordingToRelease( parentNamespaceOldConfiguration, childNamespaceLatestActiveConfiguration); //second. append child namespace modified configs to parent namespace new latest configuration return mergeConfiguration(parentNamespaceNewConfiguration, childNamespaceModifiedConfiguration); } private Map<String, String> calculateBranchModifiedItemsAccordingToRelease( Map<String, String> masterReleaseConfigs, Map<String, String> branchReleaseConfigs) { Map<String, String> modifiedConfigs = new HashMap<>(); if (CollectionUtils.isEmpty(branchReleaseConfigs)) { return modifiedConfigs; } if (CollectionUtils.isEmpty(masterReleaseConfigs)) { return branchReleaseConfigs; } for (Map.Entry<String, String> entry : branchReleaseConfigs.entrySet()) { if (!Objects.equals(entry.getValue(), masterReleaseConfigs.get(entry.getKey()))) { modifiedConfigs.put(entry.getKey(), entry.getValue()); } } return modifiedConfigs; } @Transactional public int batchDelete(String appId, String clusterName, String namespaceName, String operator) { return releaseRepository.batchDelete(appId, clusterName, namespaceName, operator); } }