Feature repository specific data migration (#1526)

This adds a new migration mechanism for repository data. Instead of using UpdateSteps for all data migrations, repository data shall from now on be implemented with RepositoryUpdateSteps. The general logic stays the same. Executed updates are stored with the repository. Doing this, we can now execute updates on imported repositories without touching other data. This way we can import repositories even though they were exported with older versions of SCM-Manager or a plugin.
This commit is contained in:
René Pfeuffer
2021-02-10 08:12:48 +01:00
committed by GitHub
parent 7c50fd935c
commit e0d2630a08
24 changed files with 743 additions and 146 deletions

View File

@@ -27,6 +27,7 @@ package sonia.scm.importexport;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -39,9 +40,11 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.api.ImportFailedException;
import sonia.scm.repository.api.IncompatibleEnvironmentForImportException;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.api.UnbundleCommandBuilder;
import sonia.scm.update.UpdateEngine;
import java.io.File;
import java.io.FileInputStream;
@@ -49,16 +52,19 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Collection;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("UnstableApiUsage")
class FullScmRepositoryImporterTest {
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold("svn");
@@ -75,6 +81,8 @@ class FullScmRepositoryImporterTest {
private ScmEnvironmentCompatibilityChecker compatibilityChecker;
@Mock
private TarArchiveRepositoryStoreImporter storeImporter;
@Mock
private UpdateEngine updateEngine;
@InjectMocks
private FullScmRepositoryImporter fullImporter;
@@ -89,29 +97,57 @@ class FullScmRepositoryImporterTest {
void shouldNotImportRepositoryIfFileNotExists(@TempDir Path temp) throws IOException {
File emptyFile = new File(temp.resolve("empty").toString());
Files.touch(emptyFile);
assertThrows(ImportFailedException.class, () -> fullImporter.importFromStream(REPOSITORY, new FileInputStream(emptyFile)));
}
@Test
void shouldFailIfScmEnvironmentIsIncompatible() {
when(compatibilityChecker.check(any())).thenReturn(false);
FileInputStream inputStream = new FileInputStream(emptyFile);
assertThrows(
ImportFailedException.class,
() -> fullImporter.importFromStream(REPOSITORY, Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream())
() -> fullImporter.importFromStream(REPOSITORY, inputStream)
);
}
@Test
void shouldImportScmRepositoryArchive() throws IOException {
when(compatibilityChecker.check(any())).thenReturn(true);
when(repositoryManager.create(eq(REPOSITORY), any())).thenReturn(REPOSITORY);
void shouldFailIfScmEnvironmentIsIncompatible() throws IOException {
when(compatibilityChecker.check(any())).thenReturn(false);
Repository repository = fullImporter.importFromStream(REPOSITORY, Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream());
assertThat(repository).isEqualTo(REPOSITORY);
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class));
verify(repositoryManager).modify(REPOSITORY);
Collection<RepositoryPermission> updatedPermissions = REPOSITORY.getPermissions();
assertThat(updatedPermissions).hasSize(2);
InputStream importStream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
assertThrows(
IncompatibleEnvironmentForImportException.class,
() -> fullImporter.importFromStream(REPOSITORY, importStream)
);
}
@Nested
class WithValidEnvironment {
@BeforeEach
void setUpEnvironment() {
when(compatibilityChecker.check(any())).thenReturn(true);
when(repositoryManager.create(eq(REPOSITORY), any())).thenAnswer(invocation -> {
invocation.getArgument(1, Consumer.class).accept(REPOSITORY);
return REPOSITORY;
});
}
@Test
void shouldImportScmRepositoryArchive() throws IOException {
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
Repository repository = fullImporter.importFromStream(REPOSITORY, stream);
assertThat(repository).isEqualTo(REPOSITORY);
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class));
verify(repositoryManager).modify(REPOSITORY);
Collection<RepositoryPermission> updatedPermissions = REPOSITORY.getPermissions();
assertThat(updatedPermissions).hasSize(2);
verify(unbundleCommandBuilder).unbundle((InputStream) argThat(argument -> argument.getClass().equals(FullScmRepositoryImporter.NoneClosingInputStream.class)));
}
@Test
void shouldTriggerUpdateForImportedRepository() throws IOException {
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
fullImporter.importFromStream(REPOSITORY, stream);
verify(updateEngine).update(REPOSITORY.getId());
}
}
}

View File

@@ -35,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginManager;
import sonia.scm.version.Version;
import java.util.Collections;
import java.util.List;
@@ -61,7 +62,7 @@ class ScmEnvironmentCompatibilityCheckerTest {
}
@Test
void shouldReturnTrueIfEnvironmentIsCompatible() {
void shouldReturnTrueIfEnvironmentIsSame() {
when(scmContextProvider.getVersion()).thenReturn("2.0.0");
ImmutableList<EnvironmentPluginDescriptor> plugins = ImmutableList.of(
new EnvironmentPluginDescriptor("scm-first-plugin", "1.0.0"),
@@ -74,6 +75,20 @@ class ScmEnvironmentCompatibilityCheckerTest {
assertThat(compatible).isTrue();
}
@Test
void shouldReturnTrueIfEnvironmentIsNewer() {
when(scmContextProvider.getVersion()).thenReturn("2.1.0");
ImmutableList<EnvironmentPluginDescriptor> plugins = ImmutableList.of(
new EnvironmentPluginDescriptor("scm-first-plugin", "0.9.0"),
new EnvironmentPluginDescriptor("scm-second-plugin", "1.0.1")
);
ScmEnvironment env = createScmEnvironment("2.0.0", "linux", "64", plugins);
boolean compatible = checker.check(env);
assertThat(compatible).isTrue();
}
@Test
void shouldReturnFalseIfCoreVersionIncompatible() {
when(scmContextProvider.getVersion()).thenReturn("2.0.0");
@@ -124,5 +139,4 @@ class ScmEnvironmentCompatibilityCheckerTest {
scmEnvironment.setPlugins(environmentPluginsDescriptor);
return scmEnvironment;
}
}

View File

@@ -24,7 +24,10 @@
package sonia.scm.update;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sonia.scm.migration.RepositoryUpdateContext;
import sonia.scm.migration.RepositoryUpdateStep;
import sonia.scm.migration.UpdateStep;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
@@ -33,16 +36,35 @@ import sonia.scm.version.Version;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static sonia.scm.version.Version.parse;
class UpdateEngineTest {
ConfigurationEntryStoreFactory storeFactory = new InMemoryConfigurationEntryStoreFactory();
RepositoryUpdateIterator repositoryUpdateIterator = mock(RepositoryUpdateIterator.class, CALLS_REAL_METHODS);
List<String> processedUpdates = new ArrayList<>();
@BeforeEach
void mockRepositories() {
doAnswer(invocation -> {
Consumer<String> consumer = invocation.getArgument(0, Consumer.class);
consumer.accept("42");
consumer.accept("1337");
return null;
}).when(repositoryUpdateIterator).forEachRepository(any());
}
@Test
void shouldProcessStepsInCorrectOrder() {
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
@@ -51,13 +73,45 @@ class UpdateEngineTest {
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
updateEngine.update();
assertThat(processedUpdates)
.containsExactly("test:1.1.0", "test:1.1.1", "test:1.2.0");
}
@Test
void shouldProcessStepsInCorrectOrderWithRepositoryUpdates() {
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
LinkedHashSet<RepositoryUpdateStep> repositoryUpdateSteps = new LinkedHashSet<>();
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
updateEngine.update();
assertThat(processedUpdates)
.containsExactly("test:1.1.0-42", "test:1.1.0-1337", "test:1.1.1-42", "test:1.1.1-1337", "test:1.2.0");
}
@Test
void shouldProcessStepsForSingleRepository() {
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
LinkedHashSet<RepositoryUpdateStep> repositoryUpdateSteps = new LinkedHashSet<>();
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
updateEngine.update("1337");
assertThat(processedUpdates)
.containsExactly("test:1.1.0-1337", "test:1.1.1-1337");
}
@Test
void shouldProcessCoreStepsBeforeOther() {
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
@@ -65,7 +119,7 @@ class UpdateEngineTest {
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
updateSteps.add(new CoreFixedVersionUpdateStep("core", "1.2.0"));
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
updateEngine.update();
assertThat(processedUpdates)
@@ -73,41 +127,73 @@ class UpdateEngineTest {
}
@Test
void shouldRunStepsOnlyOnce() {
void shouldProcessGlobalStepsBeforeRepository() {
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, storeFactory, repositoryUpdateIterator);
updateEngine.update();
assertThat(processedUpdates)
.containsExactly("test:1.2.0", "test:1.2.0-42", "test:1.2.0-1337");
}
@Test
void shouldRunGlobalStepsOnlyOnce() {
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
updateEngine.update();
UpdateEngine firstUpdateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
firstUpdateEngine.update();
processedUpdates.clear();
updateEngine.update();
UpdateEngine secondUpdateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
secondUpdateEngine.update();
assertThat(processedUpdates).isEmpty();
}
@Test
void shouldRunRepositoryStepsOnlyOnce() {
LinkedHashSet<RepositoryUpdateStep> repositoryUpdateSteps = new LinkedHashSet<>();
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
firstUpdateEngine.update();
processedUpdates.clear();
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
secondUpdateEngine.update();
assertThat(processedUpdates)
.containsExactly("test:1.2.0-42", "test:1.2.0-1337");
}
@Test
void shouldRunStepsForDifferentTypesIndependently() {
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
updateEngine.update();
processedUpdates.clear();
updateSteps.add(new FixedVersionUpdateStep("other", "1.1.1"));
updateEngine = new UpdateEngine(updateSteps, storeFactory);
updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
updateEngine.update();
assertThat(processedUpdates).containsExactly("other:1.1.1");
}
class FixedVersionUpdateStep implements UpdateStep {
class FixedVersionUpdateStep implements UpdateStep, RepositoryUpdateStep {
private final String type;
private final String version;
@@ -130,6 +216,11 @@ class UpdateEngineTest {
public void doUpdate() {
processedUpdates.add(type + ":" + version);
}
@Override
public void doUpdate(RepositoryUpdateContext repositoryUpdateContext) throws Exception {
processedUpdates.add(type + ":" + version + "-" + repositoryUpdateContext.getRepositoryId());
}
}
class CoreFixedVersionUpdateStep extends FixedVersionUpdateStep implements CoreUpdateStep {