Add update steps for namespaces

This adds a new update step API dedicated
to handle namespace related data.

Pushed-by: Rene Pfeuffer<rene.pfeuffer@cloudogu.com>
Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
Committed-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
This commit is contained in:
Rene Pfeuffer
2023-10-05 11:00:20 +02:00
parent 1a22307f18
commit 2ca68c43b3
17 changed files with 492 additions and 26 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Update steps for namespaces

View File

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

View File

@@ -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 <code>forNamespace</code> in a store
* factory. To migrate global data, use a {@link UpdateStep}.
* <p>For information about {@link #getAffectedDataType()} and {@link #getTargetVersion()}, see the package
* documentation.</p>
*
* @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;
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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<String> namespaceConsumer) {
Collection<String> 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);
}
}

View File

@@ -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(
"<repositories>",
" <namespace>hitchhike</namespace>",
"</repositories>"
));
}
@Test
void shouldFindNamespaces() {
Collection<String> foundNamespaces = new ArrayList<>();
new FileNamespaceUpdateIterator(locationResolver)
.forEachNamespace(foundNamespaces::add);
assertThat(foundNamespaces).containsExactly("hitchhike");
}
}

View File

@@ -37,7 +37,7 @@ import java.util.Map;
public class InMemoryByteConfigurationEntryStore<V> implements ConfigurationEntryStore<V> {
private final Class<V> type;
private Class<V> type;
private final KeyGenerator generator = new UUIDKeyGenerator();
private final Map<String, byte[]> store = new HashMap<>();
@@ -104,4 +104,8 @@ public class InMemoryByteConfigurationEntryStore<V> implements ConfigurationEntr
}
return null;
}
void overrideType(Class<V> type) {
this.type = type;
}
}

View File

@@ -51,6 +51,8 @@ public class InMemoryByteConfigurationEntryStoreFactory implements Configuration
@SuppressWarnings("unchecked")
public <T> ConfigurationEntryStore<T> get(Class<T> type, String name) {
return stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationEntryStore<>(type));
InMemoryByteConfigurationEntryStore<T> store = stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationEntryStore<>(type));
store.overrideType(type);
return store;
}
}

View File

@@ -34,7 +34,7 @@ import java.nio.charset.StandardCharsets;
*/
public class InMemoryByteConfigurationStore<T> implements ConfigurationStore<T> {
private final Class<T> type;
private Class<T> type;
byte[] store;
public InMemoryByteConfigurationStore(Class<T> type) {
@@ -73,4 +73,8 @@ public class InMemoryByteConfigurationStore<T> implements ConfigurationStore<T>
public void setRawXml(String xml) {
store = xml.getBytes(StandardCharsets.UTF_8);
}
void overrideType(Class<T> type) {
this.type = type;
}
}

View File

@@ -46,6 +46,8 @@ public class InMemoryByteConfigurationStoreFactory implements ConfigurationStore
@SuppressWarnings("unchecked")
public <T> ConfigurationStore<T> getStore(Class<T> type, String name) {
return stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationStore<>(type));
InMemoryByteConfigurationStore<T> store = stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationStore<>(type));
store.overrideType(type);
return store;
}
}

View File

@@ -37,7 +37,7 @@ import java.util.Map;
public class InMemoryByteDataStore<T> implements DataStore<T> {
private final Class<T> type;
private Class<T> type;
private final KeyGenerator generator = new UUIDKeyGenerator();
private final Map<String, byte[]> store = new HashMap<>();
@@ -104,4 +104,8 @@ public class InMemoryByteDataStore<T> implements DataStore<T> {
}
return null;
}
void overrideType(Class<T> type) {
this.type = type;
}
}

View File

@@ -47,6 +47,8 @@ public class InMemoryByteDataStoreFactory implements DataStoreFactory, InMemoryS
@SuppressWarnings("unchecked")
public <T> DataStore<T> getStore(Class<T> type, String name) {
return stores.computeIfAbsent(name, n -> new InMemoryByteDataStore<>(type));
InMemoryByteDataStore<T> store = stores.computeIfAbsent(name, n -> new InMemoryByteDataStore<>(type));
store.overrideType(type);
return store;
}
}

View File

@@ -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<UpdateStepRepositoryMetadataAccess<Path>>() {}).to(new TypeLiteral<MetadataStore>() {});

View File

@@ -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<UpdateStep> updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class);
Multibinder<RepositoryUpdateStep> repositoryUdateStepBinder = Multibinder.newSetBinder(binder(), RepositoryUpdateStep.class);
Multibinder<RepositoryUpdateStep> repositoryUpdateStepBinder = Multibinder.newSetBinder(binder(), RepositoryUpdateStep.class);
Multibinder<NamespaceUpdateStep> 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));
}
}

View File

@@ -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<UpdateStepWrapper> steps;
private final RepositoryUpdateIterator repositoryUpdateIterator;
private final NamespaceUpdateIterator namespaceUpdateIterator;
private final UpdateStepStore updateStepStore;
@Inject
public UpdateEngine(
Set<UpdateStep> globalSteps,
Set<RepositoryUpdateStep> repositorySteps,
Set<NamespaceUpdateStep> 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<UpdateStepWrapper> sortSteps(Set<UpdateStep> globalSteps, Set<RepositoryUpdateStep> repositorySteps) {
private List<UpdateStepWrapper> sortSteps(Set<UpdateStep> globalSteps, Set<RepositoryUpdateStep> repositorySteps, Set<NamespaceUpdateStep> namespaceSteps) {
LOG.trace("sorting available update steps:");
List<UpdateStepWrapper> 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);
}
}
}

View File

@@ -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<UpdateVersionInfo> store = createNamespaceStore(namespace);
storeNewVersion(store, updateStep);
}
void storeExecutedUpdate(UpdateStep updateStep) {
ConfigurationEntryStore<UpdateVersionInfo> 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<UpdateVersionInfo> 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<UpdateVersionInfo> createNamespaceStore(String namespace) {
return storeFactory
.withType(UpdateVersionInfo.class)
.withName(STORE_NAME)
.forNamespace(namespace)
.build();
}
}

View File

@@ -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<String> processedUpdates = new ArrayList<>();
@@ -66,6 +69,16 @@ class UpdateEngineTest {
}).when(repositoryUpdateIterator).forEachRepository(any());
}
@BeforeEach
void mockNamespaces() {
doAnswer(invocation -> {
Consumer<String> consumer = invocation.getArgument(0, Consumer.class);
consumer.accept("hitchhikers");
consumer.accept("vogons");
return null;
}).when(namespaceUpdateIterator).forEachNamespace(any());
}
@Test
void shouldProcessStepsInCorrectOrder() {
LinkedHashSet<UpdateStep> 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<UpdateStep> updateSteps = new LinkedHashSet<>();
LinkedHashSet<NamespaceUpdateStep> 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<UpdateStep> 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<UpdateStep> updateSteps = singleton(new FixedVersionUpdateStep("test", "1.2.0"));
Set<RepositoryUpdateStep> 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<NamespaceUpdateStep> 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<UpdateStep> 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 {