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);
}
}