diff --git a/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/UpdateTest.java b/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/UpdateTest.java index ef524051c..9d6a86490 100644 --- a/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/UpdateTest.java +++ b/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/UpdateTest.java @@ -18,6 +18,7 @@ package org.wildfly.prospero.it.commonapi; import org.apache.commons.io.FileUtils; +import org.assertj.core.api.Assertions; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.artifact.Artifact; @@ -36,6 +37,7 @@ import org.wildfly.channel.Stream; import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.api.exceptions.MetadataException; +import org.wildfly.prospero.api.exceptions.OperationException; import org.wildfly.prospero.metadata.ManifestVersionRecord; import org.wildfly.prospero.actions.UpdateAction; import org.wildfly.prospero.api.MavenOptions; @@ -78,6 +80,9 @@ public class UpdateTest extends WfCoreTestBase { public void setUp() throws Exception { super.setUp(); mockRepo = temp.newFolder("repo"); + + // remove cached manifests between tests + FileUtils.deleteQuietly(localCachePath.resolve(Path.of("test", "channel")).toFile()); } @After @@ -252,6 +257,43 @@ public void candidateFolderHasToBeEmpty() throws Exception { .message().contains("Can't install the server into a non empty directory"); } + @Test + public void rejectUpdateWhenManifestDowngradeIsDetected() throws Exception { + // deploy manifest file + File manifestFile = new File(MetadataTestUtils.class.getClassLoader().getResource(CHANNEL_BASE_CORE_19).toURI()); + deployManifestFile(mockRepo.toURI().toURL(), manifestFile, "1.0.1"); + + // provision using channel gav + final ProvisioningDefinition provisioningDefinition = defaultWfCoreDefinition() + .setChannelCoordinates(buildConfigWithMockRepo().toPath().toString()) + .setOverrideRepositories(Collections.emptyList()) // reset overrides from defaultWfCoreDefinition() + .build(); + installation.provision(provisioningDefinition.toProvisioningConfig(), + provisioningDefinition.resolveChannels(CHANNELS_RESOLVER_FACTORY)); + + Optional wildflyCliArtifact = readArtifactFromManifest("org.wildfly.core", "wildfly-cli"); + assertEquals(BASE_VERSION, wildflyCliArtifact.get().getVersion()); + + // delete the metadata file, so that the lower version of manifest can be resolved in an update + final Path manifestMetadata = mockRepo.toPath().resolve(Path.of("test", "channel", "maven-metadata.xml")); + Files.delete(manifestMetadata); + + // update manifest file + final File updatedManifest = upgradeTestArtifactIn(manifestFile); + deployManifestFile(mockRepo.toURI().toURL(), updatedManifest, "1.0.0"); + + + // update installation + Assertions.assertThatThrownBy(()-> + new UpdateAction(outputPath, mavenOptions, new AcceptingConsole(), Collections.emptyList()) + .performUpdate()) + .isInstanceOf(OperationException.class) + .hasMessageContaining("PRSP000276: Unable to perform the update"); + + wildflyCliArtifact = readArtifactFromManifest("org.wildfly.core", "wildfly-cli"); + assertEquals(BASE_VERSION, wildflyCliArtifact.get().getVersion()); + } + private File upgradeTestArtifactIn(File manifestFile) throws IOException, MetadataException { final ChannelManifest manifest = ManifestYamlSupport.parse(manifestFile); final List streams = manifest.getStreams().stream().map(s -> { diff --git a/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java b/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java index 96fdbaab2..b3adde67c 100644 --- a/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java +++ b/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java @@ -82,7 +82,7 @@ public class WfCoreTestBase { protected static Artifact resolvedUpgradeArtifact; protected static Artifact resolvedUpgradeClientArtifact; - private static Path localCachePath; + protected static Path localCachePath; protected Path outputPath; protected Path manifestPath; protected ProvisioningAction installation; diff --git a/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliConsole.java b/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliConsole.java index b984ef027..5b4c1470f 100644 --- a/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliConsole.java +++ b/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliConsole.java @@ -25,6 +25,7 @@ import java.util.Scanner; import org.apache.commons.lang3.StringUtils; +import org.wildfly.prospero.api.ChannelVersionChange; import org.wildfly.prospero.api.Console; import org.wildfly.prospero.api.ProvisioningProgressEvent; import org.wildfly.prospero.api.ArtifactChange; @@ -174,6 +175,18 @@ public void updatesFound(List artifactUpdates) { } } + public void manifestUpdate(List manifestChanges) { + if (manifestChanges.isEmpty()) { + println(CliMessages.MESSAGES.unableToChannelVersions()); + } else { + println(""); + println(CliMessages.MESSAGES.updatededChannelVersionsHeader()); + for (ChannelVersionChange change : manifestChanges) { + println(change.shortDescription()); + } + } + } + public void printArtifactChanges(List artifactUpdates) { if (!artifactUpdates.isEmpty()) { getStdOut().println(CliMessages.MESSAGES.changesFound()); diff --git a/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliMessages.java b/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliMessages.java index 792483b4a..1b291ca09 100644 --- a/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliMessages.java +++ b/prospero-cli/src/main/java/org/wildfly/prospero/cli/CliMessages.java @@ -34,6 +34,7 @@ import java.util.Set; import static java.lang.String.format; +import static org.wildfly.prospero.cli.commands.CliConstants.VERBOSE; public interface CliMessages { @@ -726,4 +727,16 @@ default OperationException cancelledByConfilcts() { default String downgradeDetected(){ return bundle.getString("prospero.perform.update.downgrade.detected"); } + + default String fullUpdatesOption(String paramName) { + return String.format(bundle.getString("prospero.updates.full_list_option"), VERBOSE); + } + + default String unableToChannelVersions() { + return bundle.getString("prospero.updates.channel_versions_unknown"); + } + + default String updatededChannelVersionsHeader() { + return bundle.getString("prospero.updates.channel_updates_header"); + } } diff --git a/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/UpdateCommand.java b/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/UpdateCommand.java index e9a5cbad0..90e38a0e1 100644 --- a/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/UpdateCommand.java +++ b/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/UpdateCommand.java @@ -17,6 +17,8 @@ package org.wildfly.prospero.cli.commands; +import static org.wildfly.prospero.cli.commands.CliConstants.VERBOSE; + import java.io.IOException; import java.net.MalformedURLException; import java.nio.file.Files; @@ -29,7 +31,6 @@ import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; -import org.eclipse.aether.artifact.Artifact; import org.jboss.galleon.ProvisioningException; import org.jboss.galleon.diff.FsDiff; import org.jboss.galleon.diff.FsEntry; @@ -40,7 +41,6 @@ import org.wildfly.channel.ChannelManifest; import org.wildfly.channel.ChannelManifestCoordinate; import org.wildfly.channel.Repository; -import org.wildfly.channel.version.VersionMatcher; import org.wildfly.prospero.ProsperoLogger; import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.actions.SubscribeNewServerAction; @@ -62,7 +62,6 @@ import org.wildfly.prospero.api.TemporaryFilesManager; import org.wildfly.prospero.galleon.FeaturePackLocationParser; import org.wildfly.prospero.galleon.GalleonUtils; -import org.wildfly.prospero.metadata.ManifestVersionRecord; import org.wildfly.prospero.metadata.ProsperoMetadataUtils; import org.wildfly.prospero.model.InstallationProfile; import org.wildfly.prospero.updates.UpdateSet; @@ -123,6 +122,7 @@ public Integer call() throws Exception { log.tracef("Perform full update"); console.println(CliMessages.MESSAGES.updateHeader(installationDir)); + try (UpdateAction updateAction = actionFactory.update(installationDir, mavenOptions, console, repositories)) { performUpdate(updateAction, yes, console, installationDir, noConflictsOnly); } @@ -138,18 +138,8 @@ private boolean performUpdate(UpdateAction updateAction, boolean yes, CliConsole Path targetDir = null; try { targetDir = Files.createTempDirectory("update-candidate"); - try (InstallationMetadata installationMetadata = updateAction.getInstallationMetadata()) { - if (installationMetadata.getManifestVersions().isPresent()) { - List mavenManifests = installationMetadata.getManifestVersions().get().getMavenManifests(); - final List manifestUpdates = updateAction.findCurrentChannelSessionManifests(); - if (!isManifestDowngraded(mavenManifests, manifestUpdates)) { - console.println(CliMessages.MESSAGES.downgradeDetected()); - return false; // Terminate the method if a downgrade is detected - } - } - } - if (buildUpdate(updateAction, targetDir, yes, console, () -> console.confirmUpdates())) { + if (buildUpdate(updateAction, targetDir, yes, console, () -> console.confirmUpdates(), verbose)) { console.println(""); console.buildUpdatesComplete(); @@ -217,7 +207,7 @@ public Integer call() throws Exception { try (UpdateAction updateAction = actionFactory.update(installationDir, mavenOptions, console, repositories)) { - if (buildUpdate(updateAction, candidateDirectory, yes, console, () -> console.confirmBuildUpdates())) { + if (buildUpdate(updateAction, candidateDirectory, yes, console, () -> console.confirmBuildUpdates(), verbose)) { console.println(""); console.buildUpdatesComplete(); console.println(CliMessages.MESSAGES.updateCandidateGenerated(candidateDirectory)); @@ -280,7 +270,9 @@ public Integer call() throws Exception { throw CliMessages.MESSAGES.notCandidate(candidateDir.toAbsolutePath()); } - console.updatesFound(applyCandidateAction.findUpdates().getArtifactUpdates()); + final UpdateSet updates = applyCandidateAction.findUpdates(); + printUpdates(console, updates, verbose); + final List conflicts = applyCandidateAction.getConflicts(); FileConflictPrinter.print(conflicts, console); @@ -329,8 +321,8 @@ public Integer call() throws Exception { RepositoryDefinition.from(temporaryRepositories), temporaryFiles); console.println(CliMessages.MESSAGES.checkUpdatesHeader(installationDir)); try (UpdateAction updateAction = actionFactory.update(installationDir, mavenOptions, console, repositories)) { - final UpdateSet updateSet = updateAction.findUpdates(); - console.updatesFound(updateSet.getArtifactUpdates()); + final UpdateSet updates = updateAction.findUpdates(); + printUpdates(console, updates, verbose); } final float totalTime = (System.currentTimeMillis() - startTime) / 1000f; @@ -466,9 +458,12 @@ public UpdateCommand(CliConsole console, ActionFactory actionFactory) { } - private static boolean buildUpdate(UpdateAction updateAction, Path updateDirectory, boolean yes, CliConsole console, Supplier confirmation) throws OperationException, ProvisioningException { + private static boolean buildUpdate(UpdateAction updateAction, Path updateDirectory, + boolean yes, CliConsole console, Supplier confirmation, + boolean verbose) throws OperationException, ProvisioningException { final UpdateSet updateSet = updateAction.findUpdates(); - console.updatesFound(updateSet.getArtifactUpdates()); + printUpdates(console, updateSet, verbose); + if (updateSet.isEmpty()) { return false; } @@ -482,6 +477,26 @@ private static boolean buildUpdate(UpdateAction updateAction, Path updateDirecto return true; } + private static void printUpdates(CliConsole console, UpdateSet updates, boolean verbose) throws OperationException { + if (updates.hasManifestDowngrade()) { + final String summary = String.join(";", updates.getManifestDowngradeDescriptions()); + throw ProsperoLogger.ROOT_LOGGER.manifestDowngrade(summary); + } + + // only print the full list of components if asked for or if the manifests versions are not complete + if (!updates.isAuthoritativeManifestVersions() || verbose) { + if (!updates.getManifestChanges().isEmpty()) { + console.manifestUpdate(updates.getManifestChanges()); + console.println(""); + } + console.updatesFound(updates.getArtifactUpdates()); + } else { + console.manifestUpdate(updates.getManifestChanges()); + console.println(""); + console.println(CliMessages.MESSAGES.fullUpdatesOption(VERBOSE)); + } + } + public static void verifyInstallationContainsOnlyProspero(Path dir) throws ArgumentParsingException { verifyDirectoryContainsInstallation(dir); @@ -506,37 +521,4 @@ public static Path detectProsperoInstallationPath() throws ArgumentParsingExcept return Paths.get(modulePath).toAbsolutePath().getParent(); } - - public static boolean isManifestDowngraded(List mavenManifests, List manifestUpdates ) { - - for (ManifestVersionRecord.MavenManifest installedManifest : mavenManifests) { - Artifact updateArtifact = null; - - // Find the corresponding update artifact for the installedManifest - for (Artifact manifestUpdate : manifestUpdates) { - if (manifestUpdate.getGroupId().equals(installedManifest.getGroupId()) && - manifestUpdate.getArtifactId().equals(installedManifest.getArtifactId())) { - updateArtifact = manifestUpdate; - break; - } - } - - if (updateArtifact != null) { - // Compare versions - String installedVersion = installedManifest.getVersion(); - String availableVersion = updateArtifact.getVersion(); - if (VersionMatcher.COMPARATOR.compare(installedVersion, availableVersion) < 0) { - log.debugf("Upgrade available for %s:%s: %s -> %s", - installedManifest.getGroupId(), installedManifest.getArtifactId(), installedVersion, availableVersion); - return true; - } else if (VersionMatcher.COMPARATOR.compare(installedVersion, availableVersion) > 0) { - log.debugf("Downgrade detected for " + installedManifest.getArtifactId() + ": " + installedVersion + " -> " + availableVersion); - return false; - } else { - System.out.println(installedManifest.getArtifactId() + " is up to date."); - } - } - } - return true; - } } diff --git a/prospero-cli/src/main/resources/UsageMessages.properties b/prospero-cli/src/main/resources/UsageMessages.properties index 78f938348..9efa03038 100644 --- a/prospero-cli/src/main/resources/UsageMessages.properties +++ b/prospero-cli/src/main/resources/UsageMessages.properties @@ -312,6 +312,10 @@ prospero.update.subscribe.conflict.prompt.continue=Copy metadata files. prospero.update.subscribe.conflict.prompt.cancel=Quit without generating metadata files. prospero.update.subscribe.meta.exists=Path `%s` contains a server installation provisioned by the %s already. +prospero.updates.full_list_option=To see the full list of component changes, use `%s` option. +prospero.updates.channel_versions_unknown=Unable to discover updated channel versions. Displaying updated components. +prospero.updates.channel_updates_header=Updates summary + prospero.history.no_updates=No changes found prospero.history.feature_pack.title=Feature Pack prospero.history.configuration_model.title=configuration model diff --git a/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/UpdateCommandTest.java b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/UpdateCommandTest.java index 5f6433df3..026743ee6 100644 --- a/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/UpdateCommandTest.java +++ b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/UpdateCommandTest.java @@ -36,11 +36,12 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.wildfly.channel.Repository; +import org.wildfly.prospero.ProsperoLogger; import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.actions.UpdateAction; import org.wildfly.prospero.api.ArtifactChange; +import org.wildfly.prospero.api.ChannelVersionChange; import org.wildfly.prospero.api.FileConflict; -import org.wildfly.prospero.api.InstallationMetadata; import org.wildfly.prospero.api.MavenOptions; import org.wildfly.prospero.cli.ActionFactory; import org.wildfly.prospero.cli.CliMessages; @@ -99,7 +100,6 @@ public void setUp() throws Exception { MetadataTestUtils.createInstallationMetadata(installationDir); MetadataTestUtils.createGalleonProvisionedState(installationDir, A_PROSPERO_FP); - when(updateAction.getInstallationMetadata()).thenReturn(InstallationMetadata.loadInstallation(installationDir)); } @After @@ -339,6 +339,118 @@ public void noConflictArgumentHasNoEffect_WhenNoConflictsAreFound() throws Excep verify(applyCandidateAction).applyUpdate(ApplyCandidateAction.Type.UPDATE); } + @Test + public void testBuildUpdateIsRejectedIfManifestDowngradeIsDetected() throws Exception { + System.setProperty(UpdateCommand.JBOSS_MODULE_PATH, installationDir.toString()); + when(updateAction.findUpdates()).thenReturn( + new UpdateSet( + List.of(change("1.0.0", "1.0.1")), + List.of(new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.1") + .setNewPhysicalVersion("1.0.0") + .build() + ), + false + ) + ); + final Path updatePath = tempFolder.newFolder().toPath(); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.PREPARE, CliConstants.CANDIDATE_DIR, updatePath.toString(), + CliConstants.DIR, installationDir.toAbsolutePath().toString()); + + assertEquals(ReturnCodes.PROCESSING_ERROR, exitCode); + Mockito.verify(actionFactory).update(eq(installationDir.toAbsolutePath()), any(), any(), any()); + Mockito.verify(updateAction, never()).buildUpdate(updatePath); + + // verify the error contains manifest downgrade + assertThat(getErrorOutput()) + .contains(ProsperoLogger.ROOT_LOGGER.manifestDowngrade("test: 1.0.1 -> 1.0.0").getMessage()); + } + + @Test + public void tesListUpdateShowsOnlySummaryIfManifestsAreAuthoritative() throws Exception { + System.setProperty(UpdateCommand.JBOSS_MODULE_PATH, installationDir.toString()); + when(updateAction.findUpdates()).thenReturn( + new UpdateSet( + List.of(change("1.0.0", "1.0.1")), + List.of(new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .build() + ), + true + ) + ); + final Path updatePath = tempFolder.newFolder().toPath(); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.LIST, + CliConstants.DIR, installationDir.toAbsolutePath().toString()); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + Mockito.verify(actionFactory).update(eq(installationDir.toAbsolutePath()), any(), any(), any()); + + // verify the error contains manifest downgrade + assertThat(getStandardOutput()) + .contains("test: 1.0.0 -> 1.0.1") + .doesNotContain("org.foo:bar"); + } + + @Test + public void tesListUpdateShowFullUpdatesIfManifestsIsNotAuthoritative() throws Exception { + System.setProperty(UpdateCommand.JBOSS_MODULE_PATH, installationDir.toString()); + when(updateAction.findUpdates()).thenReturn( + new UpdateSet( + List.of(change("1.0.0", "1.0.1")), + List.of(new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .build() + ), + false + ) + ); + final Path updatePath = tempFolder.newFolder().toPath(); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.LIST, + CliConstants.DIR, installationDir.toAbsolutePath().toString()); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + Mockito.verify(actionFactory).update(eq(installationDir.toAbsolutePath()), any(), any(), any()); + + // verify the error contains manifest downgrade + assertThat(getStandardOutput()) + .contains("test: 1.0.0 -> 1.0.1") + .contains("org.foo:bar"); + } + + @Test + public void tesListUpdateShowFullUpdatesIfManifestsIsAuthoritativeAndVerboseOption() throws Exception { + System.setProperty(UpdateCommand.JBOSS_MODULE_PATH, installationDir.toString()); + when(updateAction.findUpdates()).thenReturn( + new UpdateSet( + List.of(change("1.0.0", "1.0.1")), + List.of(new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .build() + ), + true + ) + ); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.LIST, + CliConstants.DIR, installationDir.toAbsolutePath().toString(), + CliConstants.VERBOSE); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + Mockito.verify(actionFactory).update(eq(installationDir.toAbsolutePath()), any(), any(), any()); + + // verify the error contains manifest downgrade + assertThat(getStandardOutput()) + .contains("test: 1.0.0 -> 1.0.1") + .contains("org.foo:bar"); + } + private ArtifactChange change(String oldVersion, String newVersion) { return ArtifactChange.updated(new DefaultArtifact("org.foo", "bar", null, oldVersion), new DefaultArtifact("org.foo", "bar", null, newVersion)); diff --git a/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java b/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java index bba33737a..8e355c62b 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java @@ -34,6 +34,7 @@ import org.wildfly.prospero.api.exceptions.InvalidUpdateCandidateException; import org.wildfly.prospero.api.exceptions.MetadataException; import org.wildfly.prospero.api.exceptions.NoChannelException; +import org.wildfly.prospero.api.exceptions.OperationException; import org.wildfly.prospero.api.exceptions.ProvisioningRuntimeException; import java.io.IOException; @@ -403,4 +404,6 @@ public interface ProsperoLogger extends BasicLogger { @Message(id = 275, value = "The candidate at [%s] was not prepared for %s operation.") InvalidUpdateCandidateException wrongCandidateOperation(Path candidateServer, ApplyCandidateAction.Type operationType); + @Message(id = 276, value = "Unable to perform the update. The resolved update is older than the current version of the server: [%s]") + OperationException manifestDowngrade(String downgradeDescription); } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/UpdateAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/UpdateAction.java index f2a2a695f..7bb0588ec 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/UpdateAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/UpdateAction.java @@ -22,13 +22,9 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; -import org.eclipse.aether.artifact.Artifact; -import org.eclipse.aether.artifact.DefaultArtifact; import org.jboss.galleon.util.PathsUtils; -import org.wildfly.channel.ChannelSession; import org.wildfly.channel.Channel; import org.wildfly.channel.Repository; import org.wildfly.prospero.ProsperoLogger; @@ -118,11 +114,17 @@ public boolean buildUpdate(Path targetDir) throws ProvisioningException, Operati targetDir = InstallFolderUtils.toRealPath(targetDir); final UpdateSet updateSet = findUpdates(); + if (updateSet.isEmpty()) { ProsperoLogger.ROOT_LOGGER.noUpdatesFound(installDir); return false; } + if (updateSet.hasManifestDowngrade()) { + final String summary = String.join(";", updateSet.getManifestDowngradeDescriptions()); + throw ProsperoLogger.ROOT_LOGGER.manifestDowngrade(summary); + } + ProsperoLogger.ROOT_LOGGER.updateCandidateStarted(installDir); try (PrepareCandidateAction prepareCandidateAction = new PrepareCandidateAction(installDir, mavenSessionManager, prosperoConfig); GalleonEnvironment galleonEnv = getGalleonEnv(targetDir)) { @@ -147,7 +149,7 @@ public boolean buildUpdate(Path targetDir) throws ProvisioningException, Operati public UpdateSet findUpdates() throws OperationException, ProvisioningException { ProsperoLogger.ROOT_LOGGER.checkingUpdates(); try (GalleonEnvironment galleonEnv = getGalleonEnv(installDir); - UpdateFinder updateFinder = new UpdateFinder(galleonEnv.getChannelSession())) { + UpdateFinder updateFinder = new UpdateFinder(galleonEnv.getChannelSession(), metadata)) { final UpdateSet updates = updateFinder.findUpdates(metadata.getArtifacts()); ProsperoLogger.ROOT_LOGGER.updatesFound(updates.getArtifactUpdates().size()); @@ -175,18 +177,4 @@ private ProsperoConfig addTemporaryRepositories(List repositories) { return new ProsperoConfig(channels, prosperoConfig.getMavenOptions()); } - - public List findCurrentChannelSessionManifests() throws ProvisioningException, OperationException { - try (GalleonEnvironment galleonEnv = getGalleonEnv(installDir); - ChannelSession channelSession = galleonEnv.getChannelSession()) { - - return channelSession.getRuntimeChannels().stream() - .map(runtimeChannel -> runtimeChannel.getChannelDefinition().getManifestCoordinate()) - .map(coordinate -> new DefaultArtifact(coordinate.getGroupId(), coordinate.getArtifactId(), coordinate.getExtension(), coordinate.getVersion())) - .collect(Collectors.toList()); - } - } - public InstallationMetadata getInstallationMetadata() { - return metadata; - } } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/api/ChannelVersionChange.java b/prospero-common/src/main/java/org/wildfly/prospero/api/ChannelVersionChange.java new file mode 100644 index 000000000..d3cfa0d7f --- /dev/null +++ b/prospero-common/src/main/java/org/wildfly/prospero/api/ChannelVersionChange.java @@ -0,0 +1,167 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.prospero.api; + +import java.util.Objects; +import java.util.Optional; + +import org.wildfly.channel.version.VersionMatcher; + +/** + * Represents a change in a versioned channel. For example when a new version of channel manifest is available. + * + * The channel has two versions - {@code physical} - usually a Maven version and {@code logical} - declared by the manifest itself. + */ +public class ChannelVersionChange { + private final String name; + private final String oldLogicalVersion; + private final String oldPhysicalVersion; + + private final String newLogicalVersion; + private final String newPhysicalVersion; + + private ChannelVersionChange(Builder builder) { + this.name = builder.name; + this.oldLogicalVersion = builder.oldLogicalVersion; + this.newLogicalVersion = builder.newLogicalVersion; + this.oldPhysicalVersion = builder.oldPhysicalVersion; + this.newPhysicalVersion = builder.newPhysicalVersion; + } + + public String getName() { + return name; + } + + public String getOldLogicalVersion() { + return oldLogicalVersion; + } + + public String getOldPhysicalVersion() { + return oldPhysicalVersion; + } + + public String getNewLogicalVersion() { + return newLogicalVersion; + } + + public String getNewPhysicalVersion() { + return newPhysicalVersion; + } + + public String shortDescription() { + final Optional oldVersion = Optional.ofNullable(getOldDisplayVersion()); + final Optional newVersion = Optional.ofNullable(getNewDisplayVersion()); + return String.format("%s: %s -> %s", name, oldVersion.orElse("[]"), newVersion.orElse("[]")); + } + + public String getOldDisplayVersion() { + if (showLogicalVersion()) { + return oldLogicalVersion; + } else { + return oldPhysicalVersion; + } + } + + public String getNewDisplayVersion() { + if (showLogicalVersion()) { + return newLogicalVersion; + } else { + return newPhysicalVersion; + } + } + + private boolean showLogicalVersion() { + if (oldPhysicalVersion != null && newPhysicalVersion != null) { + return oldLogicalVersion != null && newLogicalVersion != null; + } else if (oldPhysicalVersion != null) { + return oldLogicalVersion != null; + } else { + return newLogicalVersion != null; + } + } + + public boolean isDowngrade() { + if (newPhysicalVersion == null || oldPhysicalVersion == null) { + return false; + } else { + return VersionMatcher.COMPARATOR.compare(newPhysicalVersion, oldPhysicalVersion) < 0; + } + } + + @Override + public String toString() { + return "ChannelVersionChange{" + + "name='" + name + '\'' + + ", oldLogicalVersion='" + oldLogicalVersion + '\'' + + ", oldPhysicalVersion='" + oldPhysicalVersion + '\'' + + ", newLogicalVersion='" + newLogicalVersion + '\'' + + ", newPhysicalVersion='" + newPhysicalVersion + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChannelVersionChange that = (ChannelVersionChange) o; + return Objects.equals(name, that.name) && Objects.equals(oldLogicalVersion, that.oldLogicalVersion) && Objects.equals(oldPhysicalVersion, that.oldPhysicalVersion) && Objects.equals(newLogicalVersion, that.newLogicalVersion) && Objects.equals(newPhysicalVersion, that.newPhysicalVersion); + } + + @Override + public int hashCode() { + return Objects.hash(name, oldLogicalVersion, oldPhysicalVersion, newLogicalVersion, newPhysicalVersion); + } + + public static class Builder { + private String name; + private String oldLogicalVersion; + private String oldPhysicalVersion; + + private String newLogicalVersion; + private String newPhysicalVersion; + + public Builder(String name) { + this.name = name; + } + + public ChannelVersionChange build() { + return new ChannelVersionChange(this); + } + + public Builder setOldLogicalVersion(String oldLogicalVersion) { + this.oldLogicalVersion = oldLogicalVersion; + return this; + } + + public Builder setOldPhysicalVersion(String oldPhysicalVersion) { + this.oldPhysicalVersion = oldPhysicalVersion; + return this; + } + + public Builder setNewLogicalVersion(String newLogicalVersion) { + this.newLogicalVersion = newLogicalVersion; + return this; + } + + public Builder setNewPhysicalVersion(String newPhysicalVersion) { + this.newPhysicalVersion = newPhysicalVersion; + return this; + } + } + +} diff --git a/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateFinder.java b/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateFinder.java index b2d984b1d..eab084e31 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateFinder.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateFinder.java @@ -19,15 +19,27 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; +import org.wildfly.channel.Channel; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestCoordinate; import org.wildfly.channel.ChannelSession; +import org.wildfly.channel.MavenCoordinate; +import org.wildfly.channel.RuntimeChannel; import org.wildfly.channel.UnresolvedMavenArtifactException; import org.wildfly.channel.VersionResult; import org.wildfly.prospero.api.ArtifactChange; +import org.wildfly.prospero.api.ChannelVersionChange; +import org.wildfly.prospero.api.InstallationMetadata; import org.wildfly.prospero.api.exceptions.ArtifactResolutionException; +import org.wildfly.prospero.metadata.ManifestVersionRecord; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; @@ -40,10 +52,12 @@ public class UpdateFinder implements AutoCloseable { private final ChannelSession channelSession; private final ExecutorService executorService; + private final InstallationMetadata metadata; - public UpdateFinder(ChannelSession channelSession) { + public UpdateFinder(ChannelSession channelSession, InstallationMetadata metadata) { this.channelSession = channelSession; this.executorService = Executors.newWorkStealingPool(UPDATES_SEARCH_PARALLELISM); + this.metadata = metadata; } public UpdateSet findUpdates(List artifacts) throws ArtifactResolutionException { @@ -77,7 +91,7 @@ public UpdateSet findUpdates(List artifacts) throws ArtifactResolution .flatMap(Optional::stream) .collect(Collectors.toList()); - return new UpdateSet(updates); + return new UpdateSet(updates, findManifestUpdates(), areManifestVersionsAuthoritative()); } private Optional findUpdates(Artifact artifact) throws ArtifactResolutionException { @@ -102,9 +116,103 @@ private Optional findUpdates(Artifact artifact) throws ArtifactR } } + private List findManifestUpdates() { + final Optional manifestRecord = metadata.getManifestVersions(); + + Map currentManifests = new HashMap<>(); + for (ManifestVersionRecord.MavenManifest manifest : manifestRecord + .map(ManifestVersionRecord::getMavenManifests) + .orElse(Collections.emptyList())) { + currentManifests.put(manifest.getGroupId() + ":" + manifest.getArtifactId(), + new ManifestVersion(manifest.getVersion(), manifest.getDescription())); + } + + final ArrayList manifestChanges = new ArrayList<>(); + final Set removedGavs = currentManifests.keySet(); + + for (RuntimeChannel runtimeChannel : channelSession.getRuntimeChannels()) { + final Channel channelDefinition = runtimeChannel.getChannelDefinition(); + final ChannelManifestCoordinate manifestCoordinate = channelDefinition.getManifestCoordinate(); + if (manifestCoordinate == null || manifestCoordinate.getMaven() == null) { + // skip the no-manifest or URL-based channel + continue; + } + + final MavenCoordinate newManifest = manifestCoordinate.getMaven(); + final String ga = newManifest.getGroupId() + ":" + newManifest.getArtifactId(); + + final ChannelVersionChange.Builder builder = new ChannelVersionChange.Builder(channelDefinition.getName()) + .setNewPhysicalVersion(newManifest.getVersion()) + .setNewLogicalVersion(runtimeChannel.getChannelManifest().getName()); + + if (currentManifests.containsKey(ga)) { + final ManifestVersion oldManifest = currentManifests.get(ga); + + builder + .setOldPhysicalVersion(oldManifest.physicalVersion) + .setOldLogicalVersion(oldManifest.logicalVersion); + } + + removedGavs.remove(ga); + manifestChanges.add(builder.build()); + } + + for (String ga : removedGavs) { + final ManifestVersion oldManifest = currentManifests.get(ga); + final ChannelVersionChange.Builder builder = new ChannelVersionChange.Builder(ga) + .setOldPhysicalVersion(oldManifest.physicalVersion) + .setOldLogicalVersion(oldManifest.logicalVersion); + manifestChanges.add(builder.build()); + } + return manifestChanges; + } + + /* + * manifest updates are authoritative if all channels in the update: + * * are maven based (and we can get the versions) + * * are not using resolve-if-no-stream + * * are not using versionPatterns + */ + private boolean areManifestVersionsAuthoritative() { + if (metadata.getManifestVersions().isEmpty() || metadata.getManifestVersions().get().getMavenManifests().isEmpty()) { + return false; + } + + if (channelSession.getRuntimeChannels().isEmpty()) { + return false; + } + for (RuntimeChannel runtimeChannel : channelSession.getRuntimeChannels()) { + final Channel channelDefinition = runtimeChannel.getChannelDefinition(); + if (channelDefinition.getNoStreamStrategy() != Channel.NoStreamStrategy.NONE) { + return false; + } + if (channelDefinition.getManifestCoordinate() != null && channelDefinition.getManifestCoordinate().getMaven() == null) { + return false; + } + final ChannelManifest manifest = runtimeChannel.getChannelManifest(); + if (manifest == null) { + return false; + } + if (manifest.getStreams().stream().anyMatch(s->s.getVersionPattern() != null)) { + return false; + } + } + + return true; + } + @Override public void close() { this.executorService.shutdown(); } + private static class ManifestVersion { + private final String logicalVersion; + private final String physicalVersion; + + public ManifestVersion(String physicalVersion, String logicalVersion) { + this.logicalVersion = logicalVersion; + this.physicalVersion = physicalVersion; + } + } } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java b/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java index ceed66d34..39cef8817 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java @@ -18,24 +18,66 @@ package org.wildfly.prospero.updates; import org.wildfly.prospero.api.ArtifactChange; +import org.wildfly.prospero.api.ChannelVersionChange; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; public class UpdateSet { public static final UpdateSet EMPTY = new UpdateSet(Collections.emptyList()); private final List artifactUpdates; + private final List manifestChanges; + private final boolean authoritativeManifestVersions; + @Deprecated public UpdateSet(List updates) { + this(updates, Collections.emptyList(), false); + } + + @Deprecated + public UpdateSet(List updates, List manifestChanges) { + this(updates, manifestChanges, false); + } + + public UpdateSet(List updates, List manifestChanges, boolean authoritativeManifestVersions) { this.artifactUpdates = updates; + this.manifestChanges = manifestChanges; + this.authoritativeManifestVersions = authoritativeManifestVersions; + } + + public boolean hasManifestDowngrade() { + return getManifestChanges().stream().anyMatch(ChannelVersionChange::isDowngrade); } public List getArtifactUpdates() { return artifactUpdates; } + public List getManifestChanges() { + return manifestChanges; + } + public boolean isEmpty() { return artifactUpdates.isEmpty(); } + + /** + * set to true only if all the component changes can be identified based on manifest versions. + * For example this is not a case if manifest uses versionPatterns + * + * @return + */ + public boolean isAuthoritativeManifestVersions() { + return authoritativeManifestVersions; + } + + public List getManifestDowngradeDescriptions() { + final List downgrades = getManifestChanges().stream() + .filter(ChannelVersionChange::isDowngrade) + .map(ChannelVersionChange::shortDescription) + .collect(Collectors.toList()); + return downgrades; + } } diff --git a/prospero-common/src/test/java/org/wildfly/prospero/api/ChannelVersionChangeTest.java b/prospero-common/src/test/java/org/wildfly/prospero/api/ChannelVersionChangeTest.java new file mode 100644 index 000000000..8bbc20f80 --- /dev/null +++ b/prospero-common/src/test/java/org/wildfly/prospero/api/ChannelVersionChangeTest.java @@ -0,0 +1,123 @@ +package org.wildfly.prospero.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ChannelVersionChangeTest { + + @Test + public void printModifiedVersionWithPhysicalVersion_PhysicalVersionOnly() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .build(); + + assertEquals("test: 1.0.0 -> 1.0.1", change.shortDescription()); + } + + @Test + public void printModifiedVersionWithLogicalVersion_LogicalAndPhysicalVersion() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setOldLogicalVersion("Update 1.0") + .setNewPhysicalVersion("1.0.1") + .setNewLogicalVersion("Update 1.1") + .build(); + + assertEquals("test: Update 1.0 -> Update 1.1", change.shortDescription()); + } + + @Test + public void printModifiedVersionWithPhysicalVersion_NewLogicalVersionOnly() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .setNewLogicalVersion("Update 1.1") + .build(); + + assertEquals("test: 1.0.0 -> 1.0.1", change.shortDescription()); + } + + @Test + public void printModifiedVersionWithPhysicalVersion_OldLogicalVersionOnly() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .setOldLogicalVersion("Update 1.0") + .build(); + + assertEquals("test: 1.0.0 -> 1.0.1", change.shortDescription()); + } + + @Test + public void printAddedVersionWithPhysicalVersion_PhysicalVersionOnly() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setNewPhysicalVersion("1.0.1") + .build(); + + assertEquals("test: [] -> 1.0.1", change.shortDescription()); + } + + @Test + public void printAddedVersionWithPhysicalVersion_PhysicalAndLogicalVersion() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setNewPhysicalVersion("1.0.1") + .setNewLogicalVersion("Update 1.1") + .build(); + + assertEquals("test: [] -> Update 1.1", change.shortDescription()); + } + + @Test + public void printRemovedVersionWithPhysicalVersion_PhysicalVersionOnly() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .build(); + + assertEquals("test: 1.0.0 -> []", change.shortDescription()); + } + + @Test + public void printRemovedVersionWithPhysicalVersion_PhysicalAndLogicalVersion() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.0") + .setOldLogicalVersion("Update 1.0") + .build(); + + assertEquals("test: Update 1.0 -> []", change.shortDescription()); + } + + @Test + public void isDowngrade_IfPhysicalVersionsAreSet() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setOldPhysicalVersion("1.0.1") + .setNewPhysicalVersion("1.0.0") + .setOldLogicalVersion("Update 1.0") + .build(); + + assertTrue(change.isDowngrade()); + } + + @Test + public void addedChannelIsNotDowngrade() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setNewPhysicalVersion("1.0.0") + .setOldLogicalVersion("Update 1.0") + .build(); + + assertFalse(change.isDowngrade()); + } + + @Test + public void removedChannelIsNotDowngrade() throws Exception { + final ChannelVersionChange change = new ChannelVersionChange.Builder("test") + .setNewPhysicalVersion("1.0.0") + .setOldLogicalVersion("Update 1.0") + .build(); + + assertFalse(change.isDowngrade()); + } +} \ No newline at end of file diff --git a/prospero-common/src/test/java/org/wildfly/prospero/updates/UpdateFinderTest.java b/prospero-common/src/test/java/org/wildfly/prospero/updates/UpdateFinderTest.java index deb5c031a..c5a973c28 100644 --- a/prospero-common/src/test/java/org/wildfly/prospero/updates/UpdateFinderTest.java +++ b/prospero-common/src/test/java/org/wildfly/prospero/updates/UpdateFinderTest.java @@ -24,17 +24,30 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.wildfly.channel.ArtifactTransferException; +import org.wildfly.channel.Channel; +import org.wildfly.channel.ChannelManifest; import org.wildfly.channel.ChannelSession; +import org.wildfly.channel.RuntimeChannel; +import org.wildfly.channel.Stream; import org.wildfly.channel.VersionResult; import org.wildfly.prospero.api.ArtifactChange; +import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.regex.Pattern; + import org.jboss.galleon.api.Provisioning; +import org.wildfly.prospero.api.ChannelVersionChange; +import org.wildfly.prospero.api.InstallationMetadata; +import org.wildfly.prospero.metadata.ManifestVersionRecord; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; @SuppressWarnings("OptionalGetWithoutIsPresent") @@ -44,6 +57,8 @@ public class UpdateFinderTest { @Mock ChannelSession channelSession; @Mock + InstallationMetadata metadata; + @Mock Provisioning provMgr; @Test @@ -51,7 +66,7 @@ public void testDowngradeIsPossible() throws Exception { when(channelSession.findLatestMavenArtifactVersion("org.foo", "bar", "jar", "", null)) .thenReturn(new VersionResult("1.0.0", null)); - UpdateFinder finder = new UpdateFinder(channelSession); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); final List artifacts = Arrays.asList( new DefaultArtifact("org.foo", "bar", "jar", "1.0.1") ); @@ -70,7 +85,7 @@ public void testExcludeSameVersion() throws Exception { when(channelSession.findLatestMavenArtifactVersion("org.foo", "bar", "jar", "", null)) .thenReturn(new VersionResult("1.0.0", null)); - UpdateFinder finder = new UpdateFinder(channelSession); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); final List artifacts = Arrays.asList( new DefaultArtifact("org.foo", "bar", "jar", "1.0.0") ); @@ -84,7 +99,7 @@ public void testIncludeUpgradeVersion() throws Exception { when(channelSession.findLatestMavenArtifactVersion("org.foo", "bar", "jar", "", null)) .thenReturn(new VersionResult("1.0.1", null)); - UpdateFinder finder = new UpdateFinder(channelSession); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); final List artifacts = Arrays.asList( new DefaultArtifact("org.foo", "bar", "jar", "1.0.0") ); @@ -101,7 +116,7 @@ public void testRemoval() throws Exception { when(channelSession.findLatestMavenArtifactVersion("org.foo", "bar", "jar", "", null)) .thenThrow(new ArtifactTransferException("Exception", Collections.emptySet(), Collections.emptySet())); - UpdateFinder finder = new UpdateFinder(channelSession); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); final List artifacts = Arrays.asList( new DefaultArtifact("org.foo", "bar", "jar", "1.0.0") ); @@ -118,7 +133,7 @@ public void findUpdatesIncludesChannelNames() throws Exception { when(channelSession.findLatestMavenArtifactVersion("org.foo", "bar", "jar", "", null)) .thenReturn(new VersionResult("1.0.0", "test-channel")); - UpdateFinder finder = new UpdateFinder(channelSession); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); final List artifacts = Arrays.asList( new DefaultArtifact("org.foo", "bar", "jar", "1.0.1") ); @@ -131,4 +146,208 @@ public void findUpdatesIncludesChannelNames() throws Exception { assertEquals("1.0.1", actualUpdate.getOldVersion().get()); assertEquals("test-channel", actualUpdate.getChannelName().orElse(null)); } + + @Test + public void manifestUpdatesAreAuthoritativeIfAllTheChannelsAreMavenChannels() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder().build(), new ChannelManifest.Builder().build(), null) + )); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertTrue(updates.isAuthoritativeManifestVersions()); + } + + @Test + public void manifestUpdatesAreNotAuthoritativeIfThereAreNoChannelsDefined() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(Collections.emptyList()); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertFalse(updates.isAuthoritativeManifestVersions()); + } + + @Test + public void manifestUpdatesAreNotAuthoritativeIfOneOfTheChannelsHasNoStreamStrategy() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder() + .setResolveStrategy(Channel.NoStreamStrategy.LATEST) + .build(), new ChannelManifest.Builder().build(), null) + )); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertFalse(updates.isAuthoritativeManifestVersions()); + } + + @Test + public void manifestUpdatesAreNotAuthoritativeIfOneOfTheChannelsUsesUrlManifest() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder() + .setManifestUrl(new URL("http://test.te")) + .build(), new ChannelManifest.Builder().build(), null) + )); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertFalse(updates.isAuthoritativeManifestVersions()); + } + + @Test + public void manifestUpdatesAreNotAuthoritativeIfOneOfTheChannelsUsesVersionPatterns() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder().build(), + new ChannelManifest.Builder() + .addStreams(new Stream("org.foo", "bar", Pattern.compile(".*"))) + .build(), null) + )); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertFalse(updates.isAuthoritativeManifestVersions()); + } + + @Test + public void manifestUpdatesAreNotAuthoritativeIfUnableToEstablishCurrentVersions() throws Exception { + when(metadata.getManifestVersions()).thenReturn(Optional.empty()); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertFalse(updates.isAuthoritativeManifestVersions()); + } + + @Test + public void manifestUpdatesAreNotAuthoritativeIfUnableToEstablishCurrentChannels() throws Exception { + when(metadata.getManifestVersions()).thenReturn(Optional.of(new ManifestVersionRecord())); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertFalse(updates.isAuthoritativeManifestVersions()); + } + + @Test + public void listUpdatedMavenManifest() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder() + .setName("test-channel") + .setManifestCoordinate("org.foo", "bar", "1.0.1").build(), + new ChannelManifest.Builder().build(), null) + )); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertThat(updates.getManifestChanges()) + .containsOnly(new ChannelVersionChange.Builder("test-channel") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .setOldLogicalVersion("Update 1") + .build()); + } + + @Test + public void listsNewMavenManifestAsAdded() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder() + .setName("test-channel-1") + .setManifestCoordinate("org.foo", "bar", "1.0.1") + .build(), + new ChannelManifest.Builder().build(), null), + new RuntimeChannel(new Channel.Builder() + .setName("test-channel-2") + .setManifestCoordinate("org.foo", "new", "1.0.0") + .build(), + new ChannelManifest.Builder().build(), null) + ) + ); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertThat(updates.getManifestChanges()) + .containsOnly( + new ChannelVersionChange.Builder("test-channel-1") + .setOldPhysicalVersion("1.0.0") + .setNewPhysicalVersion("1.0.1") + .setOldLogicalVersion("Update 1") + .build(), + new ChannelVersionChange.Builder("test-channel-2") + .setNewPhysicalVersion("1.0.0") + .build() + ); + } + + @Test + public void listsRemovedMavenManifestAsRemoved() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(Collections.emptyList()); + mockCurrentManifestInformation(); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertThat(updates.getManifestChanges()) + .containsOnly( + new ChannelVersionChange.Builder("org.foo:bar") // note we don't have the channel info for removed manifests + .setOldPhysicalVersion("1.0.0") + .setOldLogicalVersion("Update 1") + .build() + ); + } + + @Test + public void listsNewMavenManifestWhenNoCurrentInfoAvailable() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder() + .setName("test-channel-1") + .setManifestCoordinate("org.foo", "bar", "1.0.1") + .build(), + new ChannelManifest.Builder().build(), null) + ) + ); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertThat(updates.getManifestChanges()) + .containsOnly( + new ChannelVersionChange.Builder("test-channel-1") + .setNewPhysicalVersion("1.0.1") + .build() + ); + } + + @Test + public void ignoresNonMavenManifest() throws Exception { + when(channelSession.getRuntimeChannels()).thenReturn(List.of( + new RuntimeChannel(new Channel.Builder() + .setName("test-channel") + .setManifestUrl(new URL("http://test.manifest")) + .build(), + new ChannelManifest.Builder().build(), null) + )); + UpdateFinder finder = new UpdateFinder(channelSession, metadata); + + final UpdateSet updates = finder.findUpdates(Collections.emptyList()); + + assertThat(updates.getManifestChanges()) + .isEmpty(); + } + + private void mockCurrentManifestInformation() { + when(metadata.getManifestVersions()).thenReturn(Optional.of(new ManifestVersionRecord("1.0.0", List.of(new ManifestVersionRecord.MavenManifest("org.foo", "bar", "1.0.0" ,"Update 1")), Collections.emptyList(), Collections.emptyList()))); + } } \ No newline at end of file