/*************************GO-LICENSE-START********************************* * Copyright 2016 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.thoughtworks.go.config.registry.ConfigElementImplementationRegistry; import com.thoughtworks.go.domain.GoConfigRevision; import com.thoughtworks.go.service.ConfigRepository; import com.thoughtworks.go.util.CachedDigestUtils; import com.thoughtworks.go.util.TimeProvider; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.input.SAXBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.*; import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.UUID; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static com.thoughtworks.go.util.ExceptionUtils.bombIfNull; import static com.thoughtworks.go.util.XmlUtils.buildXmlDocument; /** * @understands how to migrate from a previous version of config */ @Component public class GoConfigMigration { private static final Logger LOG = Logger.getLogger(GoConfigMigration.class); private final String schemaVersion = "schemaVersion"; private final UpgradeFailedHandler upgradeFailed; private final ConfigRepository configRepository; private final TimeProvider timeProvider; private ConfigCache configCache; private final ConfigElementImplementationRegistry registry; public static final String UPGRADE = "Upgrade"; @Autowired public GoConfigMigration(final ConfigRepository configRepository, final TimeProvider timeProvider, ConfigCache configCache, ConfigElementImplementationRegistry registry) { this(new UpgradeFailedHandler() { public void handle(Exception e) { e.printStackTrace(); System.err.println( "There are errors in the Cruise config file. Please read the error message and correct the errors.\n" + "Once fixed, please restart Cruise.\nError: " + e.getMessage()); LOG.fatal( "There are errors in the Cruise config file. Please read the error message and correct the errors.\n" + "Once fixed, please restart Cruise.\nError: " + e.getMessage()); // Send exit signal in a separate thread otherwise it will deadlock jetty new Thread(new Runnable() { public void run() { System.exit(1); } }).start(); } }, configRepository, timeProvider, configCache, registry); } GoConfigMigration(UpgradeFailedHandler upgradeFailed, ConfigRepository configRepository, TimeProvider timeProvider, ConfigCache configCache, ConfigElementImplementationRegistry registry) { this.upgradeFailed = upgradeFailed; this.configRepository = configRepository; this.timeProvider = timeProvider; this.configCache = configCache; this.registry = registry; } // This method should be removed once upgrade is done using new com.thoughtworks.go.config.GoConfigMigrator#migrate() @Deprecated public GoConfigMigrationResult upgradeIfNecessary(File configFile, final String currentGoServerVersion) { try { return upgradeValidateAndVersion(configFile, true, currentGoServerVersion); } catch (Exception e) { upgradeFailed.handle(e); } return GoConfigMigrationResult.unexpectedFailure("Failed to upgrade"); } private GoConfigMigrationResult upgradeValidateAndVersion(File configFile, boolean shouldTryOlderVersion, String currentGoServerVersion) throws Exception { try { ByteArrayOutputStream stream = new ByteArrayOutputStream(); String xmlStringBeforeUpgrade = FileUtils.readFileToString(configFile); int currentVersion = getCurrentSchemaVersion(xmlStringBeforeUpgrade); String reloadedXml; if (shouldUpgrade(currentVersion)) { backup(configFile); reloadedXml = upgrade(xmlStringBeforeUpgrade, currentVersion); GoConfigHolder configHolder = reloadedConfig(stream, reloadedXml); reloadedXml = new String(stream.toByteArray()); configRepository.checkin(new GoConfigRevision(reloadedXml, CachedDigestUtils.md5Hex(reloadedXml), UPGRADE, currentGoServerVersion, timeProvider)); } else { GoConfigHolder configHolder = reloadedConfig(stream, xmlStringBeforeUpgrade); reloadedXml = new String(stream.toByteArray()); } FileUtils.writeStringToFile(configFile, reloadedXml); } catch (Exception e) { GoConfigRevision currentConfigRevision = configRepository.getCurrentRevision(); if (shouldTryOlderVersion && ifVersionedConfig(currentConfigRevision)) { GoConfigMigrationResult goConfigMigrationResult = revertFileToVersion(configFile, currentConfigRevision, e); upgradeValidateAndVersion(configFile, false, currentGoServerVersion); return goConfigMigrationResult; } else { log(shouldTryOlderVersion); throw e; } } return GoConfigMigrationResult.success(); } private GoConfigHolder reloadedConfig(ByteArrayOutputStream stream, String upgradedXmlString) throws Exception { GoConfigHolder configHolder = validateAfterMigrationFinished(upgradedXmlString); new MagicalGoConfigXmlWriter(configCache, registry).write(configHolder.configForEdit, stream, false); return configHolder; } public File revertFileToVersion(File configFile, GoConfigRevision currentConfigRevision) throws Exception { File backupFile = getBackupFile(configFile, "invalid."); try { backup(configFile, backupFile); FileUtils.writeStringToFile(configFile, currentConfigRevision.getContent()); } catch (IOException e1) { throw new RuntimeException(String.format("Could not write to config file '%s'.", configFile.getAbsolutePath()), e1); } return backupFile; } private GoConfigMigrationResult revertFileToVersion(File configFile, GoConfigRevision currentConfigRevision, Exception e) throws Exception { File backupFile = getBackupFile(configFile, "invalid."); try { backup(configFile, backupFile); FileUtils.writeStringToFile(configFile, currentConfigRevision.getContent()); } catch (IOException e1) { throw new RuntimeException(String.format("Could not write to config file '%s'.", configFile.getAbsolutePath()), e1); } String invalidConfigMessage = String.format("Go encountered an invalid configuration file while starting up. " + "The invalid configuration file has been renamed to ā€˜%sā€™ and a new configuration file has been automatically created using the last good configuration. Cause: '%s'", backupFile.getAbsolutePath(), e.getMessage()); return GoConfigMigrationResult.failedToUpgrade(invalidConfigMessage); } private void log(boolean shouldTryOlderVersion) { if (shouldTryOlderVersion) { LOG.warn("There is no versioned configuration to use."); } else { LOG.warn("The versioned config file could be invalid or migrating the versioned config resulted in an invalid configuration"); } } private boolean ifVersionedConfig(GoConfigRevision currentConfigRevision) { return currentConfigRevision != null; } public String upgradeIfNecessary(String content) { return upgrade(content, getCurrentSchemaVersion(content)); } private boolean shouldUpgrade(int currentVersion) { return currentVersion < GoConfigSchema.currentSchemaVersion(); } private GoConfigHolder validateAfterMigrationFinished(String content) throws Exception { return new MagicalGoConfigXmlLoader(configCache, registry).loadConfigHolder(content); } private void backup(File configFile) throws IOException { File backupFile = getBackupFile(configFile, ""); backup(configFile, backupFile); } private void backup(File configFile, File backupFile) throws IOException { FileUtils.copyFile(configFile, backupFile); LOG.info("Config file is backed up, location: " + backupFile.getAbsolutePath()); } File getBackupFile(File configFile, final String prefix) { String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(timeProvider.currentTime()); return new File(configFile + "." + prefix + timestamp); } private String upgrade(String content, int currentVersion) { int targetVersion = GoConfigSchema.currentSchemaVersion(); return upgrade(content, currentVersion, targetVersion); } private String upgrade(String content, int currentVersion, int targetVersion) { LOG.info("Upgrading config file from version " + currentVersion + " to version " + targetVersion); List<URL> upgradeScripts = upgradeScripts(currentVersion, targetVersion); for (URL upgradeScript : upgradeScripts) { validate(content); content = upgrade(content, upgradeScript); } validate(content); LOG.info("Finished upgrading config file"); return content; } private void validate(String content) { int currentVersion = getCurrentSchemaVersion(content); try { buildXmlDocument(new ByteArrayInputStream(content.getBytes()), GoConfigSchema.getResource(currentVersion), registry.xsds()); } catch (Exception e) { throw bomb("Cruise config file with version " + currentVersion + " is invalid. Unable to upgrade.", e); } } private String upgrade(String originalContent, URL upgradeScript) { InputStream xslt = null; try { xslt = upgradeScript.openStream(); ByteArrayOutputStream convertedConfig = new ByteArrayOutputStream(); transformer(upgradeScript.getPath(), xslt) .transform(new StreamSource(new ByteArrayInputStream(originalContent.getBytes())), new StreamResult(convertedConfig)); return convertedConfig.toString(); } catch (TransformerException e) { throw bomb("Couldn't transform configuration file using upgrade script " + upgradeScript.getPath(), e); } catch (IOException e) { throw bomb("Couldn't write converted config file", e); } finally { IOUtils.closeQuietly(xslt); } } private List<URL> upgradeScripts(int currentVersion, int targetVersion) { ArrayList<URL> xsls = new ArrayList<>(); for (int i = currentVersion + 1; i <= targetVersion; i++) { URL xsl = getResource("/upgrades/" + i + ".xsl"); bombIfNull(xsl, "Config File upgrade script named " + i + ".xsl is missing. Unable to perform upgrade."); xsls.add(xsl); } return xsls; } private URL getResource(String script) { return GoConfigMigration.class.getResource(script); } private Transformer transformer(String xsltName, InputStream xslt) { try { return TransformerFactory.newInstance().newTransformer(new StreamSource(xslt)); } catch (TransformerConfigurationException tce) { throw bomb("Couldn't parse XSL template " + xsltName, tce); } } private int getCurrentSchemaVersion(String content) { try { SAXBuilder builder = new SAXBuilder(); Document document = builder.build(new ByteArrayInputStream(content.getBytes())); Element root = document.getRootElement(); String currentVersion = root.getAttributeValue(schemaVersion) == null ? "0" : root.getAttributeValue(schemaVersion); return Integer.parseInt(currentVersion); } catch (Exception e) { throw bomb(e); } } public static interface UpgradeFailedHandler { void handle(Exception e); } }