diff --git a/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/CliConstants.java b/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/CliConstants.java index 95bdf7da4..67baaaf89 100644 --- a/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/CliConstants.java +++ b/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/CliConstants.java @@ -102,4 +102,6 @@ private Commands() { public static final String Y = "-y"; public static final String YES = "--yes"; public static final String NO_CONFLICTS_ONLY = "--no-conflicts-only"; + public static final String DRY_RUN = "--dry-run"; + } diff --git a/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/RevertCommand.java b/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/RevertCommand.java index 5966e18af..7317f3c5b 100644 --- a/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/RevertCommand.java +++ b/prospero-cli/src/main/java/org/wildfly/prospero/cli/commands/RevertCommand.java @@ -42,6 +42,7 @@ import org.wildfly.prospero.cli.FileConflictPrinter; import org.wildfly.prospero.cli.RepositoryDefinition; import org.wildfly.prospero.api.TemporaryFilesManager; +import org.wildfly.prospero.cli.ReturnCodes; import picocli.CommandLine; import static org.wildfly.prospero.cli.ReturnCodes.SUCCESS; @@ -52,12 +53,18 @@ ) public class RevertCommand extends AbstractParentCommand { - private static int applyCandidate(CliConsole console, ApplyCandidateAction applyCandidateAction, boolean yes, boolean noConflictsOnly) throws OperationException, ProvisioningException { + private static int applyCandidate(CliConsole console, ApplyCandidateAction applyCandidateAction, + boolean yes, boolean noConflictsOnly, boolean dryRun) + throws OperationException, ProvisioningException { List artifactUpdates = applyCandidateAction.findUpdates().getArtifactUpdates(); console.printArtifactChanges(artifactUpdates); final List conflicts = applyCandidateAction.getConflicts(); FileConflictPrinter.print(conflicts, console); + if (dryRun) { + return ReturnCodes.SUCCESS; + } + if (noConflictsOnly && !conflicts.isEmpty()) { throw CliMessages.MESSAGES.cancelledByConfilcts(); } @@ -128,7 +135,7 @@ public Integer call() throws Exception { validateRevertCandidate(installationDirectory, tempDirectory, applyCandidateAction); - applyCandidate(console, applyCandidateAction, yes, noConflictsOnly); + applyCandidate(console, applyCandidateAction, yes, noConflictsOnly, false); } catch (IOException e) { throw ProsperoLogger.ROOT_LOGGER.unableToCreateTemporaryDirectory(e); } @@ -157,6 +164,9 @@ public static class ApplyCommand extends AbstractCommand { @CommandLine.Option(names = {CliConstants.NO_CONFLICTS_ONLY}) boolean noConflictsOnly; + @CommandLine.Option(names = {CliConstants.DRY_RUN}) + boolean dryRun; + public ApplyCommand(CliConsole console, ActionFactory actionFactory) { super(console, actionFactory); } @@ -172,7 +182,7 @@ public Integer call() throws Exception { console.println(CliMessages.MESSAGES.revertStart(installationDirectory, applyCandidateAction.getCandidateRevision().getName())); console.println(""); - applyCandidate(console, applyCandidateAction, yes, noConflictsOnly); + applyCandidate(console, applyCandidateAction, yes, noConflictsOnly, dryRun); if(remove) { applyCandidateAction.removeCandidate(candidateDirectory.toFile()); } 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 d31ddcb4f..675181a3a 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 @@ -237,6 +237,9 @@ public static class ApplyCommand extends AbstractCommand { @CommandLine.Option(names = {CliConstants.NO_CONFLICTS_ONLY}) boolean noConflictsOnly; + @CommandLine.Option(names = {CliConstants.DRY_RUN}) + boolean dryRun; + public ApplyCommand(CliConsole console, ActionFactory actionFactory) { super(console, actionFactory); } @@ -268,6 +271,10 @@ public Integer call() throws Exception { final List conflicts = applyCandidateAction.getConflicts(); FileConflictPrinter.print(conflicts, console); + if (dryRun) { + return ReturnCodes.SUCCESS; + } + if (noConflictsOnly && !conflicts.isEmpty()) { throw CliMessages.MESSAGES.cancelledByConfilcts(); } diff --git a/prospero-cli/src/main/resources/UsageMessages.properties b/prospero-cli/src/main/resources/UsageMessages.properties index eed31d0f4..8e4cd917b 100644 --- a/prospero-cli/src/main/resources/UsageMessages.properties +++ b/prospero-cli/src/main/resources/UsageMessages.properties @@ -155,7 +155,6 @@ dir = Location of the existing application server. If not specified, current wor ${prospero.dist.name}.install.dir = Target directory where the application server will be provisioned. ${prospero.dist.name}.clone.recreate.dir = Target directory where the application server will be provisioned. -dry-run = Print components that can be upgraded, but do not perform the upgrades. fpl.0 = Maven coordinates of a Galleon feature pack. The specified feature pack is installed \ with default layers and packages. fpl.1 = When you use this option, you should also specify the @|bold --channels|@ or a combination of @|bold --manifest|@ \ @@ -199,6 +198,7 @@ ${prospero.dist.name}.update.subscribe.product = Specify the product name. This ${prospero.dist.name}.update.subscribe.version = Specify the version of the product. no-conflicts-only = Rejects the operation if any file conflicts are detected. If not used, the user will be asked to \ confirm automatic conflict resolution, unless @|bold --yes|@ option is used. +dry-run = Prints the changes that would be performed by executing the command, but does not perform any changes on the filesystem. # # Exit Codes diff --git a/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/ApplyUpdateCommandTest.java b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/ApplyUpdateCommandTest.java index 1f2f7c837..c39e202a7 100644 --- a/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/ApplyUpdateCommandTest.java +++ b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/ApplyUpdateCommandTest.java @@ -223,6 +223,21 @@ public void noConflictArgumentHasNoEffect_WhenNoConflictsAreFound() throws Excep verify(applyCandidateAction).applyUpdate(ApplyCandidateAction.Type.UPDATE); } + @Test + public void dryRun_DoesntCallApplyAction() throws Exception { + final Path updatePath = mockInstallation("update"); + final Path targetPath = mockInstallation("target"); + when(applyCandidateAction.getConflicts()).thenReturn(Collections.emptyList()); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.APPLY, + CliConstants.CANDIDATE_DIR, updatePath.toString(), + CliConstants.DIR, targetPath.toString(), + CliConstants.DRY_RUN); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + verify(applyCandidateAction, Mockito.never()).applyUpdate(any()); + } + private Path mockInstallation(String target) throws IOException, MetadataException, XMLStreamException { final Path targetPath = temp.newFolder(target).toPath(); MetadataTestUtils.createInstallationMetadata(targetPath).close(); diff --git a/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertApplyCommandTest.java b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertApplyCommandTest.java index 80a26fe19..5dfe1eaad 100644 --- a/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertApplyCommandTest.java +++ b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertApplyCommandTest.java @@ -154,4 +154,17 @@ public void noConflictArgumentHasNoEffect_WhenNoConflictsAreFound() throws Excep assertEquals(ReturnCodes.SUCCESS, exitCode); verify(applyCandidateAction).applyUpdate(ApplyCandidateAction.Type.REVERT); } + + @Test + public void dryRun_DoesntCallApplyAction() throws Exception { + when(applyCandidateAction.getConflicts()).thenReturn(Collections.emptyList()); + + int exitCode = commandLine.execute(CliConstants.Commands.REVERT, CliConstants.Commands.APPLY, + CliConstants.CANDIDATE_DIR, updateDir.toString(), + CliConstants.DIR, installationDir.toString(), + CliConstants.DRY_RUN); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + verify(applyCandidateAction, Mockito.never()).applyUpdate(any()); + } } 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 cab8ff124..bba33737a 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java @@ -26,6 +26,7 @@ import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; import org.wildfly.channel.InvalidChannelMetadataException; +import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.actions.FeaturesAddAction; import org.wildfly.prospero.api.exceptions.ArtifactPromoteException; import org.wildfly.prospero.api.exceptions.ChannelDefinitionException; @@ -392,4 +393,14 @@ public interface ProsperoLogger extends BasicLogger { @Message(id = 272, value = "Failed to apply the candidate changes due to: %s") String failedToApplyCandidate(String reason); + + @Message(id = 273, value = "The server [%s] has been modified after the candidate has been created [%s].") + InvalidUpdateCandidateException staleCandidate(Path originalServer, Path candiadate); + + @Message(id = 274, value = "The folder [%s] doesn't contain a server candidate.") + InvalidUpdateCandidateException notCandidate(Path candidateServer); + + @Message(id = 275, value = "The candidate at [%s] was not prepared for %s operation.") + InvalidUpdateCandidateException wrongCandidateOperation(Path candidateServer, ApplyCandidateAction.Type operationType); + } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/FeaturesAddAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/FeaturesAddAction.java index 925e1b175..6cd3aab1e 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/FeaturesAddAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/FeaturesAddAction.java @@ -753,9 +753,9 @@ public PrepareCandidateAction newPrepareCandidateActionInstance( } @Override - public ApplyCandidateAction newApplyCandidateActionInstance(Path candidateDir) + public ApplyCandidateAction newApplyCandidateActionInstance(Path candidatePath) throws ProvisioningException, OperationException { - return new ApplyCandidateAction(installDir, candidateDir); + return new ApplyCandidateAction(installDir, candidatePath); } } } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/spi/ProsperoInstallationManager.java b/prospero-common/src/main/java/org/wildfly/prospero/spi/ProsperoInstallationManager.java index 2f7e935b3..4e543abff 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/spi/ProsperoInstallationManager.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/spi/ProsperoInstallationManager.java @@ -5,8 +5,10 @@ import org.wildfly.channel.ChannelManifestCoordinate; import org.wildfly.channel.MavenCoordinate; import org.wildfly.installationmanager.ArtifactChange; +import org.wildfly.installationmanager.CandidateType; import org.wildfly.installationmanager.Channel; import org.wildfly.installationmanager.ChannelChange; +import org.wildfly.installationmanager.FileConflict; import org.wildfly.installationmanager.HistoryResult; import org.wildfly.installationmanager.InstallationChanges; import org.wildfly.installationmanager.ManifestVersion; @@ -16,11 +18,13 @@ import org.wildfly.installationmanager.spi.InstallationManager; import org.wildfly.installationmanager.spi.OsShell; import org.wildfly.prospero.ProsperoLogger; +import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.actions.InstallationExportAction; import org.wildfly.prospero.actions.InstallationHistoryAction; import org.wildfly.prospero.actions.MetadataAction; import org.wildfly.prospero.actions.UpdateAction; import org.wildfly.prospero.api.MavenOptions.Builder; +import org.wildfly.prospero.api.exceptions.InvalidUpdateCandidateException; import org.wildfly.prospero.galleon.GalleonCallbackAdapter; import org.wildfly.prospero.metadata.ManifestVersionRecord; import org.wildfly.prospero.spi.internal.CliProvider; @@ -117,6 +121,61 @@ public boolean prepareUpdate(Path targetDir, List repositories) thro } } + @Override + public Collection verifyCandidate(Path candidatePath, CandidateType candidateType) throws Exception { + final ApplyCandidateAction applyCandidateAction = actionFactory.getApplyCandidateAction(candidatePath); + final ApplyCandidateAction.Type operation; + switch (candidateType) { + case UPDATE: + operation = ApplyCandidateAction.Type.UPDATE; + break; + case REVERT: + operation = ApplyCandidateAction.Type.REVERT; + break; + default: + throw new IllegalArgumentException("Unsupported candidate type: " + candidateType); + } + + final ApplyCandidateAction.ValidationResult validationResult = applyCandidateAction.verifyCandidate(operation); + switch (validationResult) { + case OK: + // we're good, continue + break; + case STALE: + throw ProsperoLogger.ROOT_LOGGER.staleCandidate(installationDir, candidatePath); + case NO_CHANGES: + throw ProsperoLogger.ROOT_LOGGER.noChangesAvailable(installationDir, candidatePath); + case NOT_CANDIDATE: + throw ProsperoLogger.ROOT_LOGGER.notCandidate(candidatePath); + case WRONG_TYPE: + throw ProsperoLogger.ROOT_LOGGER.wrongCandidateOperation(candidatePath, operation); + default: + // unexpected validation type - include the error in the description + throw new InvalidUpdateCandidateException(String.format("The candidate server %s is invalid - %s.", candidatePath, validationResult)); + } + + return map(applyCandidateAction.getConflicts(), ProsperoInstallationManager::mapFileConflict); + } + + private static FileConflict mapFileConflict(org.wildfly.prospero.api.FileConflict fileConflict) { + return new FileConflict(Path.of(fileConflict.getRelativePath()), map(fileConflict.getUserChange()), map(fileConflict.getUpdateChange()), fileConflict.getResolution() == org.wildfly.prospero.api.FileConflict.Resolution.UPDATE); + } + + private static FileConflict.Status map(org.wildfly.prospero.api.FileConflict.Change change) { + switch (change) { + case MODIFIED: + return FileConflict.Status.MODIFIED; + case ADDED: + return FileConflict.Status.ADDED; + case REMOVED: + return FileConflict.Status.REMOVED; + case NONE: + return FileConflict.Status.NONE; + default: + throw new IllegalArgumentException("Unknown file conflict change: " + change); + } + } + @Override public List findUpdates(List repositories) throws Exception { try (UpdateAction updateAction = actionFactory.getUpdateAction(map(repositories, ProsperoInstallationManager::mapRepository))) { @@ -355,6 +414,10 @@ protected InstallationExportAction getInstallationExportAction() { return new InstallationExportAction(server); } + protected ApplyCandidateAction getApplyCandidateAction(Path candidateDir) throws ProvisioningException, OperationException { + return new ApplyCandidateAction(server, candidateDir); + } + org.wildfly.prospero.api.MavenOptions getMavenOptions() { return mavenOptions; } diff --git a/prospero-common/src/test/java/org/wildfly/prospero/spi/ProsperoInstallationManagerTest.java b/prospero-common/src/test/java/org/wildfly/prospero/spi/ProsperoInstallationManagerTest.java index dc08526dc..408e2c618 100644 --- a/prospero-common/src/test/java/org/wildfly/prospero/spi/ProsperoInstallationManagerTest.java +++ b/prospero-common/src/test/java/org/wildfly/prospero/spi/ProsperoInstallationManagerTest.java @@ -26,8 +26,11 @@ import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelManifestCoordinate; import org.wildfly.channel.Repository; +import org.wildfly.installationmanager.CandidateType; +import org.wildfly.installationmanager.FileConflict; import org.wildfly.installationmanager.InstallationChanges; import org.wildfly.installationmanager.MavenOptions; +import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.actions.InstallationHistoryAction; import org.wildfly.prospero.actions.UpdateAction; import org.wildfly.prospero.api.ChannelChange; @@ -35,9 +38,12 @@ import org.wildfly.prospero.updates.UpdateSet; import java.nio.file.Path; +import java.util.Collection; import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -66,6 +72,9 @@ public class ProsperoInstallationManagerTest { @Mock private InstallationHistoryAction historyAction; + @Mock + private ApplyCandidateAction applyCandidateAction; + @Rule public TemporaryFolder temp = new TemporaryFolder(); @@ -245,4 +254,37 @@ public void mapMavenOptions() throws Exception { assertTrue(mavenOptions.isNoLocalCache()); assertNull(mavenOptions.getLocalCache()); } + + @Test + public void testCheckUpdatesMapsConflicts() throws Exception { + final ProsperoInstallationManager mgr = new ProsperoInstallationManager(actionFactory); + + when(actionFactory.getApplyCandidateAction(any())).thenReturn(applyCandidateAction); + when(applyCandidateAction.verifyCandidate(any())).thenReturn(ApplyCandidateAction.ValidationResult.OK); + when(applyCandidateAction.getConflicts()).thenReturn(List.of( + org.wildfly.prospero.api.FileConflict.userModified("foo/bar").updateModified().userPreserved(), + org.wildfly.prospero.api.FileConflict.userModified("system/file_a").updateModified().overwritten(), + org.wildfly.prospero.api.FileConflict.userAdded("system/file_b").updateAdded().overwritten() + )); + + final Collection conflicts = mgr.verifyCandidate(Path.of("candidate"), CandidateType.UPDATE); + assertThat(conflicts) + .contains( + new FileConflict(Path.of("foo/bar"), FileConflict.Status.MODIFIED, FileConflict.Status.MODIFIED, false), + new FileConflict(Path.of("system/file_a"), FileConflict.Status.MODIFIED, FileConflict.Status.MODIFIED, true), + new FileConflict(Path.of("system/file_b"), FileConflict.Status.ADDED, FileConflict.Status.ADDED, true) + ); + } + + @Test + public void testCheckUpdatesThrowsVerificationExceptions() throws Exception { + final ProsperoInstallationManager mgr = new ProsperoInstallationManager(actionFactory); + + when(actionFactory.getApplyCandidateAction(any())).thenReturn(applyCandidateAction); + when(applyCandidateAction.verifyCandidate(any())).thenReturn(ApplyCandidateAction.ValidationResult.STALE); + + assertThatThrownBy(() -> mgr.verifyCandidate(Path.of("candidate"), CandidateType.UPDATE)) + .hasMessageContaining("has been modified after the candidate has been created"); + + } } \ No newline at end of file