/*
* Copyright 2017 ThoughtWorks, Inc.
*
* 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.thoughtworks.go.config;
import com.rits.cloning.Cloner;
import com.thoughtworks.go.config.commands.EntityConfigUpdateCommand;
import com.thoughtworks.go.config.exceptions.*;
import com.thoughtworks.go.config.registry.ConfigElementImplementationRegistry;
import com.thoughtworks.go.config.remote.ConfigOrigin;
import com.thoughtworks.go.config.remote.PartialConfig;
import com.thoughtworks.go.config.update.FullConfigUpdateCommand;
import com.thoughtworks.go.domain.GoConfigRevision;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.util.ServerVersion;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.serverhealth.ServerHealthService;
import com.thoughtworks.go.serverhealth.ServerHealthState;
import com.thoughtworks.go.service.ConfigRepository;
import com.thoughtworks.go.util.*;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import static com.thoughtworks.go.util.ExceptionUtils.bomb;
/**
* This class find the location of cruise-config.xml and turn that into stream
* and passing it into MagicLoader or MagicWriter.
*/
@Component
public class GoFileConfigDataSource {
private static final Logger LOGGER = LoggerFactory.getLogger(GoFileConfigDataSource.class.getName());
private final Charset UTF_8 = Charset.forName("UTF-8");
private final CachedGoPartials cachedGoPartials;
private ReloadStrategy reloadStrategy = new ReloadIfModified();
private final MagicalGoConfigXmlWriter magicalGoConfigXmlWriter;
private final MagicalGoConfigXmlLoader magicalGoConfigXmlLoader;
private final ConfigRepository configRepository;
private SystemEnvironment systemEnvironment;
private GoConfigMigration upgrader;
private final TimeProvider timeProvider;
private ServerVersion serverVersion;
private Cloner cloner = new Cloner();
public static final String FILESYSTEM = "Filesystem";
private ServerHealthService serverHealthService;
private ConfigElementImplementationRegistry configElementImplementationRegistry;
private final FullConfigSaveMergeFlow fullConfigSaveMergeFlow;
private final FullConfigSaveNormalFlow fullConfigSaveNormalFlow;
private GoConfigFileReader goConfigFileReader;
private GoConfigFileWriter goConfigFileWriter;
/* Will only upgrade cruise config file on application startup. */
@Autowired
public GoFileConfigDataSource(GoConfigMigration upgrader, ConfigRepository configRepository, SystemEnvironment systemEnvironment,
TimeProvider timeProvider, ConfigCache configCache, ServerVersion serverVersion,
ConfigElementImplementationRegistry configElementImplementationRegistry,
ServerHealthService serverHealthService, CachedGoPartials cachedGoPartials,
FullConfigSaveMergeFlow fullConfigSaveMergeFlow, FullConfigSaveNormalFlow fullConfigSaveNormalFlow) {
this(upgrader, configRepository, systemEnvironment, timeProvider, serverVersion,
new MagicalGoConfigXmlLoader(configCache, configElementImplementationRegistry),
new MagicalGoConfigXmlWriter(configCache, configElementImplementationRegistry), serverHealthService,
cachedGoPartials, fullConfigSaveMergeFlow, fullConfigSaveNormalFlow,
new GoConfigFileReader(systemEnvironment), new GoConfigFileWriter(systemEnvironment));
this.configElementImplementationRegistry = configElementImplementationRegistry;
}
GoFileConfigDataSource(GoConfigMigration upgrader, ConfigRepository configRepository, SystemEnvironment systemEnvironment,
TimeProvider timeProvider, ServerVersion serverVersion, MagicalGoConfigXmlLoader magicalGoConfigXmlLoader,
MagicalGoConfigXmlWriter magicalGoConfigXmlWriter, ServerHealthService serverHealthService,
CachedGoPartials cachedGoPartials, FullConfigSaveMergeFlow fullConfigSaveMergeFlow,
FullConfigSaveNormalFlow fullConfigSaveNormalFlow, GoConfigFileReader goConfigFileReader,
GoConfigFileWriter goConfigFileWriter) {
this.configRepository = configRepository;
this.systemEnvironment = systemEnvironment;
this.upgrader = upgrader;
this.timeProvider = timeProvider;
this.serverVersion = serverVersion;
this.magicalGoConfigXmlLoader = magicalGoConfigXmlLoader;
this.magicalGoConfigXmlWriter = magicalGoConfigXmlWriter;
this.serverHealthService = serverHealthService;
this.cachedGoPartials = cachedGoPartials;
this.fullConfigSaveMergeFlow = fullConfigSaveMergeFlow;
this.fullConfigSaveNormalFlow = fullConfigSaveNormalFlow;
this.goConfigFileReader = goConfigFileReader;
this.goConfigFileWriter = goConfigFileWriter;
}
public GoFileConfigDataSource reloadEveryTime() {
this.reloadStrategy = new AlwaysReload();
return this;
}
public GoFileConfigDataSource reloadIfModified() {
this.reloadStrategy = new ReloadIfModified();
return this;
}
public File fileLocation() {
return new File(systemEnvironment.getCruiseConfigFile());
}
public GoConfigHolder load() throws Exception {
File configFile = fileLocation();
ReloadStrategy.ReloadTestResult result = reloadStrategy.requiresReload(configFile);
if (!result.requiresReload) {
reloadStrategy.hasLatest(result);
return null;
}
synchronized (this) {
result = reloadStrategy.requiresReload(configFile);
if (!result.requiresReload) {
reloadStrategy.hasLatest(result);
return null;
}
reloadStrategy.performingReload(result);
LOGGER.info("Config file changed at " + result.modifiedTime);
LOGGER.info("Reloading config file: " + configFile);
if(systemEnvironment.optimizeFullConfigSave()) {
LOGGER.debug("Starting config reload using the optimized flow.");
return forceLoad();
} else {
encryptPasswords(configFile);
LOGGER.debug("Detected change in config file.");
return forceLoad(configFile);
}
}
}
private void encryptPasswords(File configFile) throws Exception {
String currentContent = FileUtils.readFileToString(configFile);
GoConfigHolder configHolder = magicalGoConfigXmlLoader.loadConfigHolder(currentContent);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
magicalGoConfigXmlWriter.write(configHolder.configForEdit, stream, true);
String postEncryptContent = new String(stream.toByteArray());
if (!currentContent.equals(postEncryptContent)) {
LOGGER.debug("[Encrypt] Writing config to file");
FileUtils.writeStringToFile(configFile, postEncryptContent);
}
}
synchronized GoConfigHolder forceLoad() throws Exception {
File configFile = goConfigFileReader.fileLocation();
CruiseConfig cruiseConfig = this.magicalGoConfigXmlLoader.deserializeConfig(goConfigFileReader.configXml());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Reloading config file: {}", configFile.getAbsolutePath());
}
GoConfigHolder goConfigHolder;
try {
try {
goConfigHolder = fullConfigSaveNormalFlow.execute(new FullConfigUpdateCommand(cruiseConfig, null), cachedGoPartials.lastKnownPartials(), FILESYSTEM);
} catch (GoConfigInvalidException e) {
if (!canUpdateConfigWithLastValidPartials())
throw e;
goConfigHolder = fullConfigSaveNormalFlow.execute(new FullConfigUpdateCommand(cruiseConfig, null), cachedGoPartials.lastValidPartials(), FILESYSTEM);
}
reloadStrategy.latestState(goConfigHolder.config);
return goConfigHolder;
} catch (Exception e) {
LOGGER.error("Unable to load config file: {} {}", configFile.getAbsolutePath(), e.getMessage(), e);
if (configFile.exists()) {
LOGGER.warn("--- {} ---", configFile.getAbsolutePath());
LOGGER.warn(FileUtil.readContentFromFile(configFile));
LOGGER.warn("------");
}
LOGGER.debug("", e);
throw e;
}
}
synchronized GoConfigHolder forceLoad(File configFile) throws Exception {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Reloading config file: " + configFile.getAbsolutePath());
}
GoConfigHolder holder;
try {
try {
List<PartialConfig> lastKnownPartials = cloner.deepClone(cachedGoPartials.lastKnownPartials());
holder = internalLoad(FileUtils.readFileToString(configFile), new ConfigModifyingUser(FILESYSTEM), lastKnownPartials);
} catch (GoConfigInvalidException e) {
if (!canUpdateConfigWithLastValidPartials()) {
throw e;
} else {
List<PartialConfig> lastValidPartials = cloner.deepClone(cachedGoPartials.lastValidPartials());
holder = internalLoad(FileUtils.readFileToString(configFile), new ConfigModifyingUser(FILESYSTEM), lastValidPartials);
}
}
return holder;
} catch (Exception e) {
LOGGER.error("Unable to load config file: " + configFile.getAbsolutePath() + " " + e.getMessage(), e);
if (configFile.exists()) {
LOGGER.warn("--- {} ---", configFile.getAbsolutePath());
LOGGER.warn(FileUtil.readContentFromFile(configFile));
LOGGER.warn("------");
}
LOGGER.debug("", e);
throw e;
}
}
@Deprecated
public synchronized GoConfigHolder write(String configFileContent, boolean shouldMigrate) throws Exception {
File configFile = fileLocation();
try {
if (shouldMigrate) {
configFileContent = upgrader.upgradeIfNecessary(configFileContent);
}
GoConfigHolder configHolder = internalLoad(configFileContent, new ConfigModifyingUser(), new ArrayList<>());
String toWrite = configAsXml(configHolder.configForEdit, false);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Writing config file: " + configFile.getAbsolutePath());
}
writeToConfigXmlFile(toWrite);
return configHolder;
} catch (Exception e) {
LOGGER.error("Unable to write config file: " + configFile.getAbsolutePath()
+ "\n" + e.getMessage(), e);
throw e;
}
}
private void writeToConfigXmlFile(String content) {
this.goConfigFileWriter.writeToConfigXmlFile(content);
}
public synchronized EntityConfigSaveResult writeEntityWithLock(EntityConfigUpdateCommand updatingCommand, GoConfigHolder configHolder, Username currentUser) {
CruiseConfig modifiedConfig = cloner.deepClone(configHolder.configForEdit);
try {
updatingCommand.update(modifiedConfig);
} catch (Exception e) {
bomb(e);
}
List<PartialConfig> lastValidPartials = cachedGoPartials.lastValidPartials();
List<PartialConfig> lastKnownPartials = cachedGoPartials.lastKnownPartials();
if (lastKnownPartials.isEmpty() || areKnownPartialsSameAsValidPartials(lastKnownPartials, lastValidPartials)) {
return trySavingEntity(updatingCommand, currentUser, modifiedConfig, lastValidPartials);
}
try {
return trySavingEntity(updatingCommand, currentUser, modifiedConfig, lastValidPartials);
} catch (GoConfigInvalidException e) {
StringBuilder errorMessageBuilder = new StringBuilder();
try {
String message = String.format(
"Merged update operation failed on VALID %s partials. Falling back to using LAST KNOWN %s partials. Exception message was: [%s %s]",
lastValidPartials.size(), lastKnownPartials.size(), e.getMessage(), e.getAllErrorMessages());
errorMessageBuilder.append(message);
LOGGER.warn(message, e);
updatingCommand.clearErrors();
modifiedConfig.setPartials(lastKnownPartials);
String configAsXml = configAsXml(modifiedConfig, false);
GoConfigHolder holder = internalLoad(configAsXml, new ConfigModifyingUser(currentUser.getUsername().toString()), lastKnownPartials);
LOGGER.info("Update operation on merged configuration succeeded with {} KNOWN partials. Now there are {} LAST KNOWN partials",
lastKnownPartials.size(), cachedGoPartials.lastKnownPartials().size());
return new EntityConfigSaveResult(holder.config, holder);
} catch (Exception exceptionDuringFallbackValidation) {
String message = String.format(
"Merged config update operation failed using fallback LAST KNOWN %s partials. Exception message was: %s",
lastKnownPartials.size(), exceptionDuringFallbackValidation.getMessage());
LOGGER.warn(message, exceptionDuringFallbackValidation);
errorMessageBuilder.append(System.lineSeparator());
errorMessageBuilder.append(message);
throw new GoConfigInvalidException(e.getCruiseConfig(), errorMessageBuilder.toString());
}
}
}
private EntityConfigSaveResult trySavingEntity(EntityConfigUpdateCommand updatingCommand, Username currentUser, CruiseConfig modifiedConfig, List<PartialConfig> partials) {
modifiedConfig.setPartials(partials);
CruiseConfig preprocessedConfig = cloner.deepClone(modifiedConfig);
MagicalGoConfigXmlLoader.preprocess(preprocessedConfig);
if (updatingCommand.isValid(preprocessedConfig)) {
try {
LOGGER.info("[Configuration Changed] Saving updated configuration.");
String configAsXml = configAsXml(modifiedConfig, true);
String md5 = CachedDigestUtils.md5Hex(configAsXml);
MagicalGoConfigXmlLoader.setMd5(modifiedConfig, md5);
MagicalGoConfigXmlLoader.setMd5(preprocessedConfig, md5);
writeToConfigXmlFile(configAsXml);
checkinConfigToGitRepo(partials, preprocessedConfig, configAsXml, md5, currentUser.getUsername().toString());
LOGGER.debug("[Config Save] Done writing with lock");
return new EntityConfigSaveResult(updatingCommand.getPreprocessedEntityConfig(), new GoConfigHolder(preprocessedConfig, modifiedConfig));
} catch (Exception e) {
throw new RuntimeException("failed to save : " + e.getMessage());
}
} else {
throw new GoConfigInvalidException(preprocessedConfig, "Validation failed.");
}
}
// This method should be removed once we have API's for all entities which should use writeEntityWithLock and full config save should use writeFullConfigWithLock
@Deprecated
public synchronized GoConfigSaveResult writeWithLock(UpdateConfigCommand updatingCommand, GoConfigHolder configHolder) {
try {
// Need to convert to xml before we try to write it to the config file.
// If our cruiseConfig fails XSD validation, we don't want to write it incorrectly.
GoConfigHolder validatedConfigHolder;
List<PartialConfig> lastKnownPartials = cachedGoPartials.lastKnownPartials();
List<PartialConfig> lastValidPartials = cachedGoPartials.lastValidPartials();
try {
validatedConfigHolder = trySavingConfig(updatingCommand, configHolder, lastKnownPartials);
updateMergedConfigForEdit(validatedConfigHolder, lastKnownPartials);
} catch (Exception e) {
if (lastKnownPartials.isEmpty() || areKnownPartialsSameAsValidPartials(lastKnownPartials, lastValidPartials)) {
throw e;
} else {
LOGGER.warn(
"Merged config update operation failed on LATEST {} partials. Falling back to using LAST VALID {} partials. Exception message was: {}",
lastKnownPartials.size(), lastValidPartials.size(), e.getMessage(), e);
try {
validatedConfigHolder = trySavingConfig(updatingCommand, configHolder, lastValidPartials);
updateMergedConfigForEdit(validatedConfigHolder, lastValidPartials);
LOGGER.info("Update operation on merged configuration succeeded with old {} LAST VALID partials.", lastValidPartials.size());
} catch (GoConfigInvalidException fallbackFailed) {
LOGGER.warn(
"Merged config update operation failed using fallback LAST VALID {} partials. Exception message was: {}",
lastValidPartials.size(), fallbackFailed.getMessage(), fallbackFailed);
throw new GoConfigInvalidMergeException("Fallback merge failed", lastValidPartials, fallbackFailed);
}
}
}
ConfigSaveState configSaveState = shouldMergeConfig(updatingCommand, configHolder) ? ConfigSaveState.MERGED : ConfigSaveState.UPDATED;
return new GoConfigSaveResult(validatedConfigHolder, configSaveState);
} catch (ConfigFileHasChangedException e) {
LOGGER.warn("Configuration file could not be merged successfully after a concurrent edit: " + e.getMessage(), e);
throw e;
} catch (GoConfigInvalidException e) {
LOGGER.warn("Configuration file is invalid: " + e.getMessage(), e);
throw bomb(e.getMessage(), e);
} catch (Exception e) {
LOGGER.error("Configuration file is not valid: " + e.getMessage(), e);
throw bomb(e.getMessage(), e);
} finally {
LOGGER.debug("[Config Save] Done writing with lock");
}
}
public synchronized GoConfigSaveResult writeFullConfigWithLock(FullConfigUpdateCommand updatingCommand, GoConfigHolder configHolder) {
try {
GoConfigHolder validatedConfigHolder;
try {
validatedConfigHolder = trySavingConfigWithLastKnownPartials(updatingCommand, configHolder);
} catch (Exception e) {
if (!canUpdateConfigWithLastValidPartials())
throw e;
LOGGER.warn("Merged config update operation failed on LATEST {} partials. Falling back to using LAST VALID {} partials." +
" Exception message was: {}", cachedGoPartials.lastKnownPartials().size(), cachedGoPartials.lastValidPartials().size(), e.getMessage(), e);
validatedConfigHolder = trySavingConfigWithLastValidPartials(updatingCommand, configHolder);
}
ConfigSaveState configSaveState = shouldMergeConfig(updatingCommand, configHolder) ? ConfigSaveState.MERGED : ConfigSaveState.UPDATED;
return new GoConfigSaveResult(validatedConfigHolder, configSaveState);
} catch (ConfigFileHasChangedException e) {
LOGGER.warn("Configuration file could not be merged successfully after a concurrent edit: {}", e.getMessage(), e);
throw e;
} catch (GoConfigInvalidException e) {
LOGGER.warn("Configuration file is invalid: {}", e.getMessage(), e);
throw bomb(e.getMessage(), e);
} catch (Exception e) {
LOGGER.error("Configuration file is not valid: {}", e.getMessage(), e);
throw bomb(e.getMessage(), e);
} finally {
LOGGER.debug("[Config Save] Done writing with lock");
}
}
private GoConfigHolder trySavingConfigWithLastKnownPartials(FullConfigUpdateCommand updateCommand, GoConfigHolder configHolder) throws Exception {
LOGGER.debug("[Config Save] Trying to save config with Last Known Partials");
return trySavingFullConfig(updateCommand, configHolder, cachedGoPartials.lastKnownPartials());
}
private GoConfigHolder trySavingConfigWithLastValidPartials(FullConfigUpdateCommand updateCommand, GoConfigHolder configHolder) throws Exception {
List<PartialConfig> lastValidPartials = cachedGoPartials.lastValidPartials();
GoConfigHolder goConfigHolder;
try {
goConfigHolder = trySavingFullConfig(updateCommand, configHolder, cachedGoPartials.lastValidPartials());
LOGGER.debug("Update operation on merged configuration succeeded with old {} LAST VALID partials.", lastValidPartials.size());
} catch (GoConfigInvalidException fallbackFailed) {
LOGGER.warn(
"Merged config update operation failed using fallback LAST VALID {} partials. Exception message was: {}",
lastValidPartials.size(), fallbackFailed.getMessage(), fallbackFailed);
throw new GoConfigInvalidMergeException("Fallback merge failed", lastValidPartials, fallbackFailed);
}
return goConfigHolder;
}
private boolean canUpdateConfigWithLastValidPartials() {
List<PartialConfig> lastKnownPartials = cachedGoPartials.lastKnownPartials();
List<PartialConfig> lastValidPartials = cachedGoPartials.lastValidPartials();
return (!lastKnownPartials.isEmpty() && !areKnownPartialsSameAsValidPartials(lastKnownPartials, lastValidPartials));
}
protected boolean areKnownPartialsSameAsValidPartials(List<PartialConfig> lastKnownPartials, List<PartialConfig> lastValidPartials) {
if (lastKnownPartials.size() != lastValidPartials.size()) {
return false;
}
final ArrayList<ConfigOrigin> validConfigOrigins = ListUtil.map(lastValidPartials, new ListUtil.Transformer<PartialConfig, ConfigOrigin>() {
@Override
public ConfigOrigin transform(PartialConfig partialConfig) {
return partialConfig.getOrigin();
}
});
PartialConfig invalidKnownPartial = ListUtil.find(lastKnownPartials, new ListUtil.Condition() {
@Override
public <T> boolean isMet(T item) {
return !validConfigOrigins.contains(((PartialConfig) item).getOrigin());
}
});
return invalidKnownPartial == null;
}
private void updateMergedConfigForEdit(GoConfigHolder validatedConfigHolder, List<PartialConfig> partialConfigs) {
if (partialConfigs.isEmpty()) return;
CruiseConfig mergedCruiseConfigForEdit = cloner.deepClone(validatedConfigHolder.configForEdit);
mergedCruiseConfigForEdit.merge(partialConfigs, true);
validatedConfigHolder.mergedConfigForEdit = mergedCruiseConfigForEdit;
}
private GoConfigHolder trySavingFullConfig(FullConfigUpdateCommand updatingCommand, GoConfigHolder configHolder, List<PartialConfig> partials) throws Exception {
String userName = getConfigUpdatingUser(updatingCommand).getUserName();
GoConfigHolder goConfigHolder;
LOGGER.debug("[Config Save] ==-- Getting modified config");
if (shouldMergeConfig(updatingCommand, configHolder)) {
if (!systemEnvironment.get(SystemEnvironment.ENABLE_CONFIG_MERGE_FEATURE)) {
throw new ConfigMergeException(ConfigFileHasChangedException.CONFIG_CHANGED_PLEASE_REFRESH);
}
goConfigHolder = this.fullConfigSaveMergeFlow.execute(updatingCommand, partials, userName);
} else {
goConfigHolder = this.fullConfigSaveNormalFlow.execute(updatingCommand, partials, userName);
}
reloadStrategy.latestState(goConfigHolder.config);
return goConfigHolder;
}
private GoConfigHolder trySavingConfig(UpdateConfigCommand updatingCommand, GoConfigHolder configHolder, List<PartialConfig> partials) throws Exception {
String configAsXml;
GoConfigHolder validatedConfigHolder;
LOGGER.debug("[Config Save] ==-- Getting modified config");
if (shouldMergeConfig(updatingCommand, configHolder)) {
if (!systemEnvironment.get(SystemEnvironment.ENABLE_CONFIG_MERGE_FEATURE)) {
throw new ConfigMergeException(ConfigFileHasChangedException.CONFIG_CHANGED_PLEASE_REFRESH);
}
configAsXml = getMergedConfig((NoOverwriteUpdateConfigCommand) updatingCommand, configHolder.configForEdit.getMd5(), partials);
try {
validatedConfigHolder = internalLoad(configAsXml, getConfigUpdatingUser(updatingCommand), partials);
} catch (Exception e) {
LOGGER.info("[CONFIG_MERGE] Post merge validation failed, latest-md5: {}", configHolder.configForEdit.getMd5());
throw new ConfigMergePostValidationException(e.getMessage(), e);
}
} else {
configAsXml = getUnmergedConfig(updatingCommand, configHolder, partials);
validatedConfigHolder = internalLoad(configAsXml, getConfigUpdatingUser(updatingCommand), partials);
}
LOGGER.info("[Configuration Changed] Saving updated configuration.");
writeToConfigXmlFile(configAsXml);
return validatedConfigHolder;
}
private ConfigModifyingUser getConfigUpdatingUser(UpdateConfigCommand updatingCommand) {
return updatingCommand instanceof UserAware ? ((UserAware) updatingCommand).user() : new ConfigModifyingUser();
}
private String getUnmergedConfig(UpdateConfigCommand updatingCommand, GoConfigHolder configHolder, List<PartialConfig> partials) throws Exception {
CruiseConfig deepCloneForEdit = cloner.deepClone(configHolder.configForEdit);
deepCloneForEdit.setPartials(partials);
CruiseConfig config = updatingCommand.update(deepCloneForEdit);
String configAsXml = configAsXml(config, false);
if (deepCloneForEdit.getPartials().size() < partials.size())
throw new RuntimeException("should never be called");
return configAsXml;
}
private boolean shouldMergeConfig(UpdateConfigCommand updatingCommand, GoConfigHolder configHolder) {
LOGGER.debug("[Config Save] Checking whether config should be merged");
if (updatingCommand instanceof NoOverwriteUpdateConfigCommand) {
NoOverwriteUpdateConfigCommand noOverwriteCommand = (NoOverwriteUpdateConfigCommand) updatingCommand;
if (!configHolder.configForEdit.getMd5().equals(noOverwriteCommand.unmodifiedMd5())) {
return true;
}
}
return false;
}
private String getMergedConfig(NoOverwriteUpdateConfigCommand noOverwriteCommand, String latestMd5, List<PartialConfig> partials) throws Exception {
LOGGER.debug("[Config Save] Getting merged config");
String oldMd5 = noOverwriteCommand.unmodifiedMd5();
CruiseConfig modifiedConfig = getOldConfigAndMutateWithChanges(noOverwriteCommand, oldMd5);
modifiedConfig.setPartials(partials);
String modifiedConfigAsXml = convertMutatedConfigToXml(modifiedConfig, latestMd5);
GoConfigRevision configRevision = new GoConfigRevision(modifiedConfigAsXml, "temporary-md5-for-branch", getConfigUpdatingUser(noOverwriteCommand).getUserName(),
serverVersion.version(), timeProvider);
String mergedConfigXml = configRepository.getConfigMergedWithLatestRevision(configRevision, oldMd5);
LOGGER.debug("[Config Save] -=- Done converting merged config to XML");
return mergedConfigXml;
}
private String convertMutatedConfigToXml(CruiseConfig modifiedConfig, String latestMd5) throws Exception {
try {
return configAsXml(modifiedConfig, false);
} catch (Exception e) {
LOGGER.info("[CONFIG_MERGE] Pre merge validation failed, latest-md5: {}", latestMd5);
throw new ConfigMergePreValidationException(e.getMessage(), e);
}
}
private CruiseConfig getOldConfigAndMutateWithChanges(NoOverwriteUpdateConfigCommand noOverwriteCommand, String oldMd5) throws Exception {
LOGGER.debug("[Config Save] --- Mutating old config");
String configXmlAtOldMd5 = configRepository.getRevision(oldMd5).getContent();
CruiseConfig cruiseConfigAtOldMd5 = magicalGoConfigXmlLoader.fromXmlPartial(configXmlAtOldMd5, BasicCruiseConfig.class);
CruiseConfig config = noOverwriteCommand.update(cruiseConfigAtOldMd5);
LOGGER.debug("[Config Save] --- Done mutating old config");
return config;
}
private GoConfigHolder internalLoad(final String content, final ConfigModifyingUser configModifyingUser, final List<PartialConfig> partials) throws Exception {
GoConfigHolder configHolder = magicalGoConfigXmlLoader.loadConfigHolder(content, new MagicalGoConfigXmlLoader.Callback() {
@Override
public void call(CruiseConfig cruiseConfig) {
cruiseConfig.setPartials(partials);
}
});
CruiseConfig config = configHolder.config;
checkinConfigToGitRepo(partials, config, content, configHolder.configForEdit.getMd5(), configModifyingUser.getUserName());
return configHolder;
}
private void checkinConfigToGitRepo(List<PartialConfig> partials, CruiseConfig config, String configAsXml, String md5, String currentUser) throws Exception {
reloadStrategy.latestState(config);
LOGGER.debug("[Config Save] === Checking in the valid XML to config.git");
configRepository.checkin(new GoConfigRevision(configAsXml, md5, currentUser, serverVersion.version(), timeProvider));
LOGGER.debug("[Config Save] === Done checking in to config.git");
cachedGoPartials.markAsValid(partials);
}
public String configAsXml(CruiseConfig config, boolean skipPreprocessingAndValidation) throws Exception {
LOGGER.debug("[Config Save] === Converting config to XML");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
magicalGoConfigXmlWriter.write(config, outputStream, skipPreprocessingAndValidation);
LOGGER.debug("[Config Save] === Done converting config to XML");
return outputStream.toString();
}
public void upgradeIfNecessary() {
GoConfigMigrationResult migrationResult = this.upgrader.upgradeIfNecessary(fileLocation(), serverVersion.version());
if (migrationResult.isUpgradeFailure()) {
String message = migrationResult.message();
serverHealthService.update(ServerHealthState.warning("Invalid Configuration", message, HealthStateType.general(HealthStateScope.forInvalidConfig())));
LOGGER.warn(message);
}
}
public String getFileLocation() {
return fileLocation().getAbsolutePath();
}
static class GoConfigSaveResult {
private final GoConfigHolder configHolder;
private final ConfigSaveState configSaveState;
GoConfigSaveResult(GoConfigHolder holder, ConfigSaveState configSaveState) {
configHolder = holder;
this.configSaveState = configSaveState;
}
public GoConfigHolder getConfigHolder() {
return configHolder;
}
public ConfigSaveState getConfigSaveState() {
return configSaveState;
}
}
private interface ReloadStrategy {
class ReloadTestResult {
final boolean requiresReload;
final long fileSize;
final long modifiedTime;
public ReloadTestResult(boolean requiresReload, long fileSize, long modifiedTime) {
this.requiresReload = requiresReload;
this.fileSize = fileSize;
this.modifiedTime = modifiedTime;
}
}
ReloadTestResult requiresReload(File configFile);
void latestState(CruiseConfig config);
void hasLatest(ReloadTestResult reloadTestResult);
void performingReload(ReloadTestResult reloadTestResult);
}
private static class AlwaysReload implements ReloadStrategy {
public ReloadTestResult requiresReload(File configFile) {
return new ReloadTestResult(true, 0, 0);
}
public void latestState(CruiseConfig config) {
}
public void hasLatest(ReloadTestResult result) {
}
public void performingReload(ReloadTestResult result) {
}
}
static class ReloadIfModified implements ReloadStrategy {
private long lastModified;
private long prevSize;
private volatile String md5 = "";
private long length(File configFile) {
return configFile.length();
}
private long lastModified(File configFile) {
return configFile.lastModified();
}
private boolean requiresReload(File configFile, long currentLastModified, long currentSize) {
return doFileAttributesDiffer(currentLastModified, currentSize) && doesFileContentDiffer(configFile);
}
boolean doesFileContentDiffer(File configFile) {
return !md5.equals(getConfigFileMd5(configFile));
}
boolean doFileAttributesDiffer(long currentLastModified, long currentSize) {
return currentLastModified != lastModified ||
prevSize != currentSize;
}
public ReloadTestResult requiresReload(File configFile) {
long lastModified = lastModified(configFile);
long length = length(configFile);
boolean requiresReload = requiresReload(configFile, lastModified, length);
return new ReloadTestResult(requiresReload, length, lastModified);
}
private String getConfigFileMd5(File configFile) {
String newMd5;
try (FileInputStream inputStream = new FileInputStream(configFile)) {
newMd5 = CachedDigestUtils.md5Hex(inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
return newMd5;
}
public void latestState(CruiseConfig config) {
md5 = config.getMd5();
}
public void hasLatest(ReloadTestResult result) {
rememberLatestFileAttributes(result);
}
private void rememberLatestFileAttributes(ReloadTestResult result) {
synchronized (this) {
lastModified = result.modifiedTime;
prevSize = result.fileSize;
}
}
public void performingReload(ReloadTestResult result) {
rememberLatestFileAttributes(result);
}
}
}