From b253cd110dc785543c629c39e5732ec1e4951e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 4 Jun 2019 16:37:53 +0200 Subject: [PATCH] Initial migration servlet --- .../scm/security/DefaultCipherHandler.java | 17 +++-- .../scm/boot/BootstrapContextListener.java | 48 +++++++++--- .../MigrationWizardContextListener.java | 22 ++++++ .../scm/update/MigrationWizardModule.java | 14 ++++ .../scm/update/MigrationWizardServlet.java | 75 +++++++++++++++++++ .../update/repository/MigrationStrategy.java | 2 +- .../repository/MigrationStrategyDao.java | 2 + .../repository/XmlRepositoryV1UpdateStep.java | 75 +++++++++++++------ .../repository-migration-restart.mustache | 10 +++ .../templates/repository-migration.mustache | 38 ++++++++++ 10 files changed, 264 insertions(+), 39 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java create mode 100644 scm-webapp/src/main/resources/templates/repository-migration-restart.mustache create mode 100644 scm-webapp/src/main/resources/templates/repository-migration.mustache diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java index 9c1fa590cc..8c3afbd90f 100644 --- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java +++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java @@ -161,10 +161,17 @@ public class DefaultCipherHandler implements CipherHandler { * @return decrypted value */ public String decode(char[] plainKey, String value) { - String result = null; - + Base64.Decoder decoder = Base64.getUrlDecoder(); try { - byte[] encodedInput = Base64.getUrlDecoder().decode(value); + return decode(plainKey, value, decoder); + } catch (IllegalArgumentException e) { + return decode(plainKey, value, Base64.getDecoder()); + } + } + + private String decode(char[] plainKey, String value, Base64.Decoder decoder) { + try { + byte[] encodedInput = decoder.decode(value); byte[] salt = new byte[SALT_LENGTH]; byte[] encoded = new byte[encodedInput.length - SALT_LENGTH]; @@ -180,12 +187,10 @@ public class DefaultCipherHandler implements CipherHandler { byte[] decoded = cipher.doFinal(encoded); - result = new String(decoded, ENCODING); + return new String(decoded, ENCODING); } catch (IOException | GeneralSecurityException ex) { throw new CipherException("could not decode string", ex); } - - return result; } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 3af8a76650..be7955ea28 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -46,6 +46,7 @@ import sonia.scm.ScmEventBusModule; import sonia.scm.ScmInitializerModule; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; +import sonia.scm.migration.UpdateException; import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.Plugin; import sonia.scm.plugin.PluginException; @@ -54,6 +55,7 @@ import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.SmpArchive; +import sonia.scm.update.MigrationWizardContextListener; import sonia.scm.update.UpdateEngine; import sonia.scm.util.ClassLoaders; import sonia.scm.util.IOUtil; @@ -110,14 +112,16 @@ public class BootstrapContextListener implements ServletContextListener { public void contextDestroyed(ServletContextEvent sce) { contextListener.contextDestroyed(sce); - for (PluginWrapper plugin : contextListener.getPlugins()) { - ClassLoader pcl = plugin.getClassLoader(); + if (contextListener instanceof ScmContextListener) { + for (PluginWrapper plugin : ((ScmContextListener) contextListener).getPlugins()) { + ClassLoader pcl = plugin.getClassLoader(); - if (pcl instanceof Closeable) { - try { - ((Closeable) pcl).close(); - } catch (IOException ex) { - logger.warn("could not close plugin classloader", ex); + if (pcl instanceof Closeable) { + try { + ((Closeable) pcl).close(); + } catch (IOException ex) { + logger.warn("could not close plugin classloader", ex); + } } } } @@ -151,7 +155,9 @@ public class BootstrapContextListener implements ServletContextListener { } private void createContextListener(File pluginDirectory) { + try { + renameOldPluginsFolder(pluginDirectory); if (!isCorePluginExtractionDisabled()) { extractCorePlugins(context, pluginDirectory); } else { @@ -166,14 +172,36 @@ public class BootstrapContextListener implements ServletContextListener { Injector bootstrapInjector = createBootstrapInjector(pluginLoader); - processUpdates(pluginLoader, bootstrapInjector); + MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); - contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + if (wizardContextListener.wizardNecessary()) { + contextListener = wizardContextListener; + } else { + processUpdates(pluginLoader, bootstrapInjector); + + contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + } } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } } + private void renameOldPluginsFolder(File pluginDirectory) { + if (new File(pluginDirectory, "classpath.xml").exists()) { + File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1"); + boolean renamed = pluginDirectory.renameTo(backupDirectory); + if (renamed) { + logger.warn("moved old plugins directory to {}", backupDirectory); + } else { + throw new UpdateException("could not rename existing v1 plugin directory"); + } + } + } + + private MigrationWizardContextListener prepareWizardIfNeeded(Injector bootstrapInjector) { + return new MigrationWizardContextListener(bootstrapInjector); + } + private Injector createBootstrapInjector(PluginLoader pluginLoader) { Module scmContextListenerModule = new ScmContextListenerModule(); BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); @@ -402,7 +430,7 @@ public class BootstrapContextListener implements ServletContextListener { private ServletContext context; /** Field description */ - private ScmContextListener contextListener; + private ServletContextListener contextListener; /** Field description */ private boolean registered = false; diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java new file mode 100644 index 0000000000..c98062c900 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java @@ -0,0 +1,22 @@ +package sonia.scm.update; + +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; + +public class MigrationWizardContextListener extends GuiceServletContextListener { + + private final Injector injector; + + public MigrationWizardContextListener(Injector bootstrapInjector) { + this.injector = bootstrapInjector.createChildInjector(new MigrationWizardModule()); + } + + public boolean wizardNecessary() { + return injector.getInstance(MigrationWizardServlet.class).wizardNecessary(); + } + + @Override + protected Injector getInjector() { + return injector; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java new file mode 100644 index 0000000000..3362496330 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -0,0 +1,14 @@ +package sonia.scm.update; + +import com.google.inject.servlet.ServletModule; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import java.util.List; + +class MigrationWizardModule extends ServletModule { + + @Override + protected void configureServlets() { + serve("/*").with(MigrationWizardServlet.class); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java new file mode 100644 index 0000000000..d65a8408a0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -0,0 +1,75 @@ +package sonia.scm.update; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import sonia.scm.boot.RestartEvent; +import sonia.scm.event.ScmEventBus; +import sonia.scm.update.repository.MigrationStrategy; +import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +@Singleton +class MigrationWizardServlet extends HttpServlet { + + private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; + private final MigrationStrategyDao migrationStrategyDao; + + @Inject + MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) { + this.repositoryV1UpdateStep = repositoryV1UpdateStep; + this.migrationStrategyDao = migrationStrategyDao; + } + + public boolean wizardNecessary() { + return !repositoryV1UpdateStep.missingMigrationStrategies().isEmpty(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + List missingMigrationStrategies = repositoryV1UpdateStep.missingMigrationStrategies(); + + resp.setStatus(200); + + HashMap model = new HashMap<>(); + + model.put("submitUrl", req.getRequestURI()); + model.put("repositories", missingMigrationStrategies); + model.put("strategies", getMigrationStrategies()); + + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache mustache = mf.compile("templates/repository-migration.mustache"); + mustache.execute(resp.getWriter(), model).flush(); + } + + private List getMigrationStrategies() { + return stream(MigrationStrategy.values()).map(Enum::name).collect(toList()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setStatus(200); + + req.getParameterMap().forEach( + (name, strategy) -> migrationStrategyDao.set(name, MigrationStrategy.valueOf(strategy[0])) + ); + + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache mustache = mf.compile("templates/repository-migration-restart.mustache"); + mustache.execute(resp.getWriter(), new Object()).flush(); + + ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java index c7bb2cba86..d4b6c5c0f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java @@ -4,7 +4,7 @@ import com.google.inject.Injector; import java.nio.file.Path; -enum MigrationStrategy { +public enum MigrationStrategy { COPY(CopyMigrationStrategy.class), MOVE(MoveMigrationStrategy.class), diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java index 15c931bf31..8ddb5e02df 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java @@ -4,8 +4,10 @@ import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import javax.inject.Inject; +import javax.inject.Singleton; import java.util.Optional; +@Singleton public class MigrationStrategyDao { private final RepositoryMigrationPlan plan; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 34c08fb16b..acb28ca432 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -32,7 +32,9 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; import static sonia.scm.version.Version.parse; @@ -109,6 +111,23 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { ); } + public List missingMigrationStrategies() { + if (!resolveV1File().exists()) { + LOG.info("no v1 repositories database file found"); + return emptyList(); + } + try { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class); + return readV1Database(jaxbContext) + .map(v1Database -> v1Database.repositoryList.repositories.stream()) + .orElse(Stream.empty()) + .filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent()) + .collect(Collectors.toList()); + } catch (JAXBException e) { + throw new UpdateException("could not read v1 repository database", e); + } + } + private void backupOldRepositoriesFile() { Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); Path oldRepositoriesFile = configDir.resolve("repositories.xml"); @@ -126,8 +145,8 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { Repository repository = new Repository( v1Repository.id, v1Repository.type, - getNamespace(v1Repository), - getName(v1Repository), + v1Repository.getNewNamespace(), + v1Repository.getNewName(), v1Repository.contact, v1Repository.description, createPermissions(v1Repository)); @@ -142,10 +161,14 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) { - return migrationStrategyDao.get(v1Repository.id) + return findMigrationStrategy(v1Repository) .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); } + private Optional findMigrationStrategy(V1Repository v1Repository) { + return migrationStrategyDao.get(v1Repository.id); + } + private RepositoryPermission[] createPermissions(V1Repository v1Repository) { if (v1Repository.permissions == null) { return new RepositoryPermission[0]; @@ -161,24 +184,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission); } - private String getNamespace(V1Repository v1Repository) { - String[] nameParts = getNameParts(v1Repository.name); - return nameParts.length > 1 ? nameParts[0] : v1Repository.type; - } - - private String getName(V1Repository v1Repository) { - String[] nameParts = getNameParts(v1Repository.name); - return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); - } - - private String concatPathElements(String[] nameParts) { - return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); - } - - private String[] getNameParts(String v1Name) { - return v1Name.split("/"); - } - private Optional readV1Database(JAXBContext jaxbContext) throws JAXBException { Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File()); if (unmarshal instanceof V1RepositoryDatabase) { @@ -205,7 +210,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") - private static class V1Repository { + public static class V1Repository { private String contact; private long creationDate; private Long lastModified; @@ -218,6 +223,32 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private List permissions; private V1Properties properties; + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getNewNamespace() { + String[] nameParts = getNameParts(name); + return nameParts.length > 1 ? nameParts[0] : type; + } + + public String getNewName() { + String[] nameParts = getNameParts(name); + return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); + } + + private String[] getNameParts(String v1Name) { + return v1Name.split("/"); + } + + private String concatPathElements(String[] nameParts) { + return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); + } + @Override public String toString() { return "V1Repository{" + diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache new file mode 100644 index 0000000000..e47945174a --- /dev/null +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -0,0 +1,10 @@ + + + + SCM-Manager Restart + + + +SCM-Manager will restart to migrate the data. + + diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache new file mode 100644 index 0000000000..641e718136 --- /dev/null +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -0,0 +1,38 @@ + + + + SCM-Manager Migration + + + +

SCM-Manager Migration

+You have migrated from SCM-Manager v1 to SCM-Manager v2. +
+ + + + + + + {{#repositories}} + + + + + + {{/repositories}} +
original namenew namespace/nameStrategy
+ {{name}} + + {{newNamespace}}/{{newName}} + + +
+ +
+ +