From 2ca68c43b37c9a6740e2bef193f6f5ab9be719e3 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 5 Oct 2023 11:00:20 +0200 Subject: [PATCH] Add update steps for namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new update step API dedicated to handle namespace related data. Pushed-by: Rene Pfeuffer Co-authored-by: René Pfeuffer Committed-by: René Pfeuffer --- gradle/changelog/namespace_update_steps.yaml | 2 + .../scm/migration/NamespaceUpdateContext.java | 46 +++++++++++ .../scm/migration/NamespaceUpdateStep.java | 52 +++++++++++++ .../scm/update/NamespaceUpdateIterator.java | 75 ++++++++++++++++++ .../store/FileNamespaceUpdateIterator.java | 70 +++++++++++++++++ .../FileNamespaceUpdateIteratorTest.java | 68 ++++++++++++++++ .../InMemoryByteConfigurationEntryStore.java | 6 +- ...oryByteConfigurationEntryStoreFactory.java | 4 +- .../store/InMemoryByteConfigurationStore.java | 6 +- ...InMemoryByteConfigurationStoreFactory.java | 4 +- .../scm/store/InMemoryByteDataStore.java | 6 +- .../store/InMemoryByteDataStoreFactory.java | 4 +- .../lifecycle/modules/BootstrapModule.java | 3 + .../lifecycle/modules/UpdateStepModule.java | 10 ++- .../java/sonia/scm/update/UpdateEngine.java | 67 ++++++++++++++-- .../sonia/scm/update/UpdateStepStore.java | 18 +++++ .../sonia/scm/update/UpdateEngineTest.java | 77 ++++++++++++++++--- 17 files changed, 492 insertions(+), 26 deletions(-) create mode 100644 gradle/changelog/namespace_update_steps.yaml create mode 100644 scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateContext.java create mode 100644 scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateStep.java create mode 100644 scm-core/src/main/java/sonia/scm/update/NamespaceUpdateIterator.java create mode 100644 scm-dao-xml/src/main/java/sonia/scm/store/FileNamespaceUpdateIterator.java create mode 100644 scm-dao-xml/src/test/java/sonia/scm/store/FileNamespaceUpdateIteratorTest.java diff --git a/gradle/changelog/namespace_update_steps.yaml b/gradle/changelog/namespace_update_steps.yaml new file mode 100644 index 0000000000..03360527b3 --- /dev/null +++ b/gradle/changelog/namespace_update_steps.yaml @@ -0,0 +1,2 @@ +- type: added + description: Update steps for namespaces diff --git a/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateContext.java b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateContext.java new file mode 100644 index 0000000000..5dd8364b6c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateContext.java @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.migration; + +/** + * Data for the namespace, whose data that should be migrated. + * + * @since 2.47.0 + */ +public final class NamespaceUpdateContext { + + private final String namespace; + + public NamespaceUpdateContext(String namespace) { + this.namespace = namespace; + } + + /** + * The name of the namespace, whose data should be migrated. + */ + public String getNamespace() { + return namespace; + } +} diff --git a/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateStep.java b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateStep.java new file mode 100644 index 0000000000..78be05d79a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateStep.java @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.migration; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * This is the main interface for "namespace specific" data migration/update. Using this interface, SCM-Manager + * provides the possibility to change data structures between versions for a given type of data. This class should be + * used only for namespace specific data (eg. the store is created with a call of forNamespace in a store + * factory. To migrate global data, use a {@link UpdateStep}. + *

For information about {@link #getAffectedDataType()} and {@link #getTargetVersion()}, see the package + * documentation.

+ * + * @see sonia.scm.migration + * + * @since 2.47.0 + */ +@ExtensionPoint +public interface NamespaceUpdateStep extends UpdateStepTarget { + /** + * Implement this to update the data to the new version for a specific namespace. If any {@link Exception} is thrown, + * SCM-Manager will not start up. + * + * @param namespaceUpdateContext A context providing specifics about the namespace, whose data should be migrated + * (eg. its name). + */ + @SuppressWarnings("java:S112") // we suppress this one, because an implementation should feel free to throw any exception it deems necessary + void doUpdate(NamespaceUpdateContext namespaceUpdateContext) throws Exception; +} diff --git a/scm-core/src/main/java/sonia/scm/update/NamespaceUpdateIterator.java b/scm-core/src/main/java/sonia/scm/update/NamespaceUpdateIterator.java new file mode 100644 index 0000000000..f93cd5c9f6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/NamespaceUpdateIterator.java @@ -0,0 +1,75 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.update; + +import sonia.scm.migration.UpdateException; + +import java.util.function.Consumer; + +/** + * Implementations of this interface can be used to iterate all namespaces in update steps. + * + * @since 2.47.0 + */ +public interface NamespaceUpdateIterator { + + /** + * Calls the given consumer with each namespace. + * + * @since 2.47.0 + */ + void forEachNamespace(Consumer namespace); + + /** + * Equivalent to {@link #forEachNamespace(Consumer)} with the difference, that you can throw exceptions in the given + * update code, that will then be wrapped in a {@link UpdateException}. + * + * @since 2.47.0 + */ + default void updateEachNamespace(Updater updater) { + forEachNamespace( + namespace -> { + try { + updater.update(namespace); + } catch (Exception e) { + throw new UpdateException("failed to update namespace " + namespace, e); + } + } + ); + } + + /** + * Simple callback with the name of an existing namespace with the possibility to throw exceptions. + * + * @since 2.47.0 + */ + interface Updater { + /** + * Implements the update logic for a single namespace, denoted by its name. + */ + @SuppressWarnings("java:S112") // We explicitly want to allow arbitrary exceptions here + void update(String namespace) throws Exception; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileNamespaceUpdateIterator.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileNamespaceUpdateIterator.java new file mode 100644 index 0000000000..366d0d1b0b --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileNamespaceUpdateIterator.java @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.store; + +import sonia.scm.migration.UpdateException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.NamespaceUpdateIterator; + +import javax.inject.Inject; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashSet; +import java.util.function.Consumer; + +public class FileNamespaceUpdateIterator implements NamespaceUpdateIterator { + + private final RepositoryLocationResolver locationResolver; + private final JAXBContext jaxbContext; + + @Inject + public FileNamespaceUpdateIterator(RepositoryLocationResolver locationResolver) { + this.locationResolver = locationResolver; + try { + jaxbContext = JAXBContext.newInstance(Repository.class); + } catch (JAXBException ex) { + throw new IllegalStateException("failed to create jaxb context for repository", ex); + } + } + + @Override + public void forEachNamespace(Consumer namespaceConsumer) { + Collection namespaces = new HashSet<>(); + locationResolver + .forClass(Path.class) + .forAllLocations((repositoryId, path) -> { + try { + Repository metadata = (Repository) jaxbContext.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile()); + namespaces.add(metadata.getNamespace()); + } catch (JAXBException e) { + throw new UpdateException("Failed to read metadata for repository " + repositoryId, e); + } + }); + namespaces.forEach(namespaceConsumer); + } +} diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileNamespaceUpdateIteratorTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileNamespaceUpdateIteratorTest.java new file mode 100644 index 0000000000..5b66e4cafb --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileNamespaceUpdateIteratorTest.java @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.store; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.TempDirRepositoryLocationResolver; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FileNamespaceUpdateIteratorTest { + + private TempDirRepositoryLocationResolver locationResolver; + + @BeforeEach + void initLocationResolver(@TempDir Path tempDir) throws IOException { + locationResolver = new TempDirRepositoryLocationResolver(tempDir.toFile()); + Files.write(tempDir.resolve("metadata.xml"), asList( + "", + " hitchhike", + "" + )); + } + + @Test + void shouldFindNamespaces() { + Collection foundNamespaces = new ArrayList<>(); + new FileNamespaceUpdateIterator(locationResolver) + .forEachNamespace(foundNamespaces::add); + + assertThat(foundNamespaces).containsExactly("hitchhike"); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java index 20fa3d664f..3f748243bf 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java @@ -37,7 +37,7 @@ import java.util.Map; public class InMemoryByteConfigurationEntryStore implements ConfigurationEntryStore { - private final Class type; + private Class type; private final KeyGenerator generator = new UUIDKeyGenerator(); private final Map store = new HashMap<>(); @@ -104,4 +104,8 @@ public class InMemoryByteConfigurationEntryStore implements ConfigurationEntr } return null; } + + void overrideType(Class type) { + this.type = type; + } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java index eeec98ec13..92ffaa97d2 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java @@ -51,6 +51,8 @@ public class InMemoryByteConfigurationEntryStoreFactory implements Configuration @SuppressWarnings("unchecked") public ConfigurationEntryStore get(Class type, String name) { - return stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationEntryStore<>(type)); + InMemoryByteConfigurationEntryStore store = stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationEntryStore<>(type)); + store.overrideType(type); + return store; } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java index c7c75ace57..a8bdc44b96 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java @@ -34,7 +34,7 @@ import java.nio.charset.StandardCharsets; */ public class InMemoryByteConfigurationStore implements ConfigurationStore { - private final Class type; + private Class type; byte[] store; public InMemoryByteConfigurationStore(Class type) { @@ -73,4 +73,8 @@ public class InMemoryByteConfigurationStore implements ConfigurationStore public void setRawXml(String xml) { store = xml.getBytes(StandardCharsets.UTF_8); } + + void overrideType(Class type) { + this.type = type; + } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java index 4c6ffb6934..f63d4675a6 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java @@ -46,6 +46,8 @@ public class InMemoryByteConfigurationStoreFactory implements ConfigurationStore @SuppressWarnings("unchecked") public ConfigurationStore getStore(Class type, String name) { - return stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationStore<>(type)); + InMemoryByteConfigurationStore store = stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationStore<>(type)); + store.overrideType(type); + return store; } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java index 46b1a1623a..1736b1d0d4 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java @@ -37,7 +37,7 @@ import java.util.Map; public class InMemoryByteDataStore implements DataStore { - private final Class type; + private Class type; private final KeyGenerator generator = new UUIDKeyGenerator(); private final Map store = new HashMap<>(); @@ -104,4 +104,8 @@ public class InMemoryByteDataStore implements DataStore { } return null; } + + void overrideType(Class type) { + this.type = type; + } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java index 348255b609..05997ae87d 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java @@ -47,6 +47,8 @@ public class InMemoryByteDataStoreFactory implements DataStoreFactory, InMemoryS @SuppressWarnings("unchecked") public DataStore getStore(Class type, String name) { - return stores.computeIfAbsent(name, n -> new InMemoryByteDataStore<>(type)); + InMemoryByteDataStore store = stores.computeIfAbsent(name, n -> new InMemoryByteDataStore<>(type)); + store.overrideType(type); + return store; } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index 07fe9d2443..f1b87f2253 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -61,6 +61,7 @@ import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.DataStoreFactory; import sonia.scm.store.DefaultBlobDirectoryAccess; import sonia.scm.store.FileBlobStoreFactory; +import sonia.scm.store.FileNamespaceUpdateIterator; import sonia.scm.store.FileRepositoryUpdateIterator; import sonia.scm.store.FileStoreUpdateStepUtilFactory; import sonia.scm.store.JAXBConfigurationEntryStoreFactory; @@ -68,6 +69,7 @@ import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.store.JAXBDataStoreFactory; import sonia.scm.store.JAXBPropertyFileAccess; import sonia.scm.update.BlobDirectoryAccess; +import sonia.scm.update.NamespaceUpdateIterator; import sonia.scm.update.PropertyFileAccess; import sonia.scm.update.RepositoryUpdateIterator; import sonia.scm.update.StoreUpdateStepUtilFactory; @@ -124,6 +126,7 @@ public class BootstrapModule extends AbstractModule { bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class); bind(BlobDirectoryAccess.class, DefaultBlobDirectoryAccess.class); bind(RepositoryUpdateIterator.class, FileRepositoryUpdateIterator.class); + bind(NamespaceUpdateIterator.class, FileNamespaceUpdateIterator.class); bind(StoreUpdateStepUtilFactory.class, FileStoreUpdateStepUtilFactory.class); bind(new TypeLiteral>() {}).to(new TypeLiteral() {}); diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java index 9a5415ea03..77a8c3c427 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java @@ -26,6 +26,7 @@ package sonia.scm.lifecycle.modules; import com.google.inject.AbstractModule; import com.google.inject.multibindings.Multibinder; +import sonia.scm.migration.NamespaceUpdateStep; import sonia.scm.migration.RepositoryUpdateStep; import sonia.scm.migration.UpdateStep; import sonia.scm.plugin.PluginLoader; @@ -41,7 +42,8 @@ public class UpdateStepModule extends AbstractModule { @Override protected void configure() { Multibinder updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class); - Multibinder repositoryUdateStepBinder = Multibinder.newSetBinder(binder(), RepositoryUpdateStep.class); + Multibinder repositoryUpdateStepBinder = Multibinder.newSetBinder(binder(), RepositoryUpdateStep.class); + Multibinder namespaceUpdateStepBinder = Multibinder.newSetBinder(binder(), NamespaceUpdateStep.class); pluginLoader .getExtensionProcessor() .byExtensionPoint(UpdateStep.class) @@ -49,6 +51,10 @@ public class UpdateStepModule extends AbstractModule { pluginLoader .getExtensionProcessor() .byExtensionPoint(RepositoryUpdateStep.class) - .forEach(stepClass -> repositoryUdateStepBinder.addBinding().to(stepClass)); + .forEach(stepClass -> repositoryUpdateStepBinder.addBinding().to(stepClass)); + pluginLoader + .getExtensionProcessor() + .byExtensionPoint(NamespaceUpdateStep.class) + .forEach(stepClass -> namespaceUpdateStepBinder.addBinding().to(stepClass)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java index 8adda1ff26..dff597698a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java +++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java @@ -26,6 +26,8 @@ package sonia.scm.update; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.migration.NamespaceUpdateContext; +import sonia.scm.migration.NamespaceUpdateStep; import sonia.scm.migration.RepositoryUpdateContext; import sonia.scm.migration.RepositoryUpdateStep; import sonia.scm.migration.UpdateException; @@ -46,28 +48,33 @@ public class UpdateEngine { public static final Logger LOG = LoggerFactory.getLogger(UpdateEngine.class); - private final List steps; private final RepositoryUpdateIterator repositoryUpdateIterator; + private final NamespaceUpdateIterator namespaceUpdateIterator; private final UpdateStepStore updateStepStore; @Inject public UpdateEngine( Set globalSteps, Set repositorySteps, + Set namespaceSteps, RepositoryUpdateIterator repositoryUpdateIterator, - UpdateStepStore updateStepStore) { + NamespaceUpdateIterator namespaceUpdateIterator, UpdateStepStore updateStepStore) { this.repositoryUpdateIterator = repositoryUpdateIterator; + this.namespaceUpdateIterator = namespaceUpdateIterator; this.updateStepStore = updateStepStore; - this.steps = sortSteps(globalSteps, repositorySteps); + this.steps = sortSteps(globalSteps, repositorySteps, namespaceSteps); } - private List sortSteps(Set globalSteps, Set repositorySteps) { + private List sortSteps(Set globalSteps, Set repositorySteps, Set namespaceSteps) { LOG.trace("sorting available update steps:"); List sortedSteps = concat( - globalSteps.stream().filter(this::notRunYet).map(GlobalUpdateStepWrapper::new), - repositorySteps.stream().map(RepositoryUpdateStepWrapper::new)) + concat( + globalSteps.stream().filter(this::notRunYet).map(GlobalUpdateStepWrapper::new), + repositorySteps.stream().map(RepositoryUpdateStepWrapper::new)), + namespaceSteps.stream().map(NamespaceUpdateStepWrapper::new) + ) .sorted( Comparator .comparing(UpdateStepWrapper::getTargetVersion) @@ -248,4 +255,52 @@ public class UpdateEngine { return updateStepStore.notRunYet(repositoryId, delegate); } } + + private class NamespaceUpdateStepWrapper extends UpdateStepWrapper { + + private final NamespaceUpdateStep delegate; + + public NamespaceUpdateStepWrapper(NamespaceUpdateStep delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override + public boolean isGlobalUpdateStep() { + return false; + } + + @Override + boolean isCoreUpdate() { + return false; + } + + @Override + void doUpdate() { + namespaceUpdateIterator.updateEachNamespace(this::doUpdate); + } + + @Override + void doUpdate(String namespace) throws Exception { + if (notRunYet(namespace)) { + LOG.info("running update step for type {} and version {} (class {}) for namespace {}", + delegate.getAffectedDataType(), + delegate.getTargetVersion(), + delegate.getClass().getName(), + namespace + ); + delegate.doUpdate(new NamespaceUpdateContext(namespace)); + updateStepStore.storeExecutedUpdate(namespace, delegate); + } + } + + private boolean notRunYet(String namespace) { + LOG.trace("checking whether to run update step for type {} and version {} on namespace {}", + delegate.getAffectedDataType(), + delegate.getTargetVersion(), + namespace + ); + return updateStepStore.notRunYet(namespace, delegate); + } + } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java index a44354912b..c2146b42f3 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java +++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java @@ -26,6 +26,7 @@ package sonia.scm.update; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.migration.NamespaceUpdateStep; import sonia.scm.migration.RepositoryUpdateStep; import sonia.scm.migration.UpdateStep; import sonia.scm.migration.UpdateStepTarget; @@ -52,6 +53,11 @@ class UpdateStepStore { storeNewVersion(store, updateStep); } + void storeExecutedUpdate(String namespace, NamespaceUpdateStep updateStep) { + ConfigurationEntryStore store = createNamespaceStore(namespace); + storeNewVersion(store, updateStep); + } + void storeExecutedUpdate(UpdateStep updateStep) { ConfigurationEntryStore store = createGlobalStore(); storeNewVersion(store, updateStep); @@ -65,6 +71,10 @@ class UpdateStepStore { return notRunYet(createRepositoryStore(repositoryId), updateStep); } + boolean notRunYet(String namespace, NamespaceUpdateStep updateStep) { + return notRunYet(createNamespaceStore(namespace), updateStep); + } + private void storeNewVersion(ConfigurationEntryStore store, UpdateStepTarget updateStep) { UpdateVersionInfo newVersionInfo = new UpdateVersionInfo(updateStep.getTargetVersion().getParsedVersion()); store.put(updateStep.getAffectedDataType(), newVersionInfo); @@ -99,4 +109,12 @@ class UpdateStepStore { .forRepository(repositoryId) .build(); } + + private ConfigurationEntryStore createNamespaceStore(String namespace) { + return storeFactory + .withType(UpdateVersionInfo.class) + .withName(STORE_NAME) + .forNamespace(namespace) + .build(); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java index 5a8f007ac0..d4cd9ce3f6 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java @@ -26,6 +26,8 @@ package sonia.scm.update; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import sonia.scm.migration.NamespaceUpdateContext; +import sonia.scm.migration.NamespaceUpdateStep; import sonia.scm.migration.RepositoryUpdateContext; import sonia.scm.migration.RepositoryUpdateStep; import sonia.scm.migration.UpdateStep; @@ -52,6 +54,7 @@ class UpdateEngineTest { ConfigurationEntryStoreFactory storeFactory = new InMemoryByteConfigurationEntryStoreFactory(); RepositoryUpdateIterator repositoryUpdateIterator = mock(RepositoryUpdateIterator.class, CALLS_REAL_METHODS); + NamespaceUpdateIterator namespaceUpdateIterator = mock(NamespaceUpdateIterator.class, CALLS_REAL_METHODS); UpdateStepStore updateStepStore = new UpdateStepStore(storeFactory); List processedUpdates = new ArrayList<>(); @@ -66,6 +69,16 @@ class UpdateEngineTest { }).when(repositoryUpdateIterator).forEachRepository(any()); } + @BeforeEach + void mockNamespaces() { + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0, Consumer.class); + consumer.accept("hitchhikers"); + consumer.accept("vogons"); + return null; + }).when(namespaceUpdateIterator).forEachNamespace(any()); + } + @Test void shouldProcessStepsInCorrectOrder() { LinkedHashSet updateSteps = new LinkedHashSet<>(); @@ -74,7 +87,7 @@ class UpdateEngineTest { updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); updateSteps.add(new FixedVersionUpdateStep("test", "1.1.0")); - UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore); + UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); updateEngine.update(); assertThat(processedUpdates) @@ -90,7 +103,7 @@ class UpdateEngineTest { updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0")); - UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore); + UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); updateEngine.update(); assertThat(processedUpdates) @@ -106,13 +119,29 @@ class UpdateEngineTest { repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0")); - UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore); + UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); updateEngine.update("1337"); assertThat(processedUpdates) .containsExactly("test:1.1.0-1337", "test:1.1.1-1337"); } + @Test + void shouldProcessStepsForSingleNamespace() { + LinkedHashSet updateSteps = new LinkedHashSet<>(); + LinkedHashSet namespaceUpdateSteps = new LinkedHashSet<>(); + + updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); + namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); + namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0")); + + UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), namespaceUpdateSteps, repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); + updateEngine.update("hitchhikers"); + + assertThat(processedUpdates) + .containsExactly("test:1.1.0/hitchhikers", "test:1.1.1/hitchhikers"); + } + @Test void shouldProcessCoreStepsBeforeOther() { LinkedHashSet updateSteps = new LinkedHashSet<>(); @@ -120,7 +149,7 @@ class UpdateEngineTest { updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); updateSteps.add(new CoreFixedVersionUpdateStep("core", "1.2.0")); - UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore); + UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); updateEngine.update(); assertThat(processedUpdates) @@ -132,7 +161,7 @@ class UpdateEngineTest { Set updateSteps = singleton(new FixedVersionUpdateStep("test", "1.2.0")); Set repositoryUpdateSteps = singleton(new FixedVersionUpdateStep("test", "1.2.0")); - UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore); + UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); updateEngine.update(); assertThat(processedUpdates) @@ -145,12 +174,12 @@ class UpdateEngineTest { updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); - UpdateEngine firstUpdateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore); + UpdateEngine firstUpdateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); firstUpdateEngine.update(); processedUpdates.clear(); - UpdateEngine secondUpdateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore); + UpdateEngine secondUpdateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); secondUpdateEngine.update(); assertThat(processedUpdates).isEmpty(); @@ -162,39 +191,58 @@ class UpdateEngineTest { repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); - UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore); + UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); firstUpdateEngine.update(); processedUpdates.clear(); repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); - UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore); + UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); secondUpdateEngine.update(); assertThat(processedUpdates) .containsExactly("test:1.2.0-42", "test:1.2.0-1337"); } + @Test + void shouldRunNamespaceStepsOnlyOnce() { + LinkedHashSet namespaceUpdateSteps = new LinkedHashSet<>(); + + namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); + + UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), emptySet(), namespaceUpdateSteps, repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); + firstUpdateEngine.update(); + + processedUpdates.clear(); + + namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); + UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), emptySet(), namespaceUpdateSteps, repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); + secondUpdateEngine.update(); + + assertThat(processedUpdates) + .containsExactly("test:1.2.0/hitchhikers", "test:1.2.0/vogons"); + } + @Test void shouldRunStepsForDifferentTypesIndependently() { LinkedHashSet updateSteps = new LinkedHashSet<>(); updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); - UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore); + UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); updateEngine.update(); processedUpdates.clear(); updateSteps.add(new FixedVersionUpdateStep("other", "1.1.1")); - updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore); + updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore); updateEngine.update(); assertThat(processedUpdates).containsExactly("other:1.1.1"); } - class FixedVersionUpdateStep implements UpdateStep, RepositoryUpdateStep { + class FixedVersionUpdateStep implements UpdateStep, RepositoryUpdateStep, NamespaceUpdateStep { private final String type; private final String version; @@ -222,6 +270,11 @@ class UpdateEngineTest { public void doUpdate(RepositoryUpdateContext repositoryUpdateContext) throws Exception { processedUpdates.add(type + ":" + version + "-" + repositoryUpdateContext.getRepositoryId()); } + + @Override + public void doUpdate(NamespaceUpdateContext namespaceUpdateContext) throws Exception { + processedUpdates.add(type + ":" + version + "/" + namespaceUpdateContext.getNamespace()); + } } class CoreFixedVersionUpdateStep extends FixedVersionUpdateStep implements CoreUpdateStep {