diff --git a/pom.xml b/pom.xml index bd7497a0..1eccc721 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,7 @@ 3.8.16.Final 1.7.0.Final 7.1.0.Final - 1.0.2.Final + 2.0.0.Beta1-SNAPSHOT 2.4.1.Final 1.2.1.Final 5.12.0 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 2610ba4e..0f7231ad 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 @@ -695,4 +695,10 @@ default String getProfile() { default OperationException sizeOfChannel(Path channelFile) { return new OperationException(format(bundle.getString("prospero.channels.error.morethanonechannel.found"), channelFile)); } + + default OperationException cancelledByConfilcts() { + return new OperationException(format( + bundle.getString("prospero.updates.apply.candidate.cancel_conflicts"), + CliConstants.NO_CONFLICTS_ONLY)); + } } 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 6fa3b30e..67baaaf8 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 @@ -101,4 +101,7 @@ private Commands() { public static final String VV = "-vv"; 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 b86b8f95..7317f3c5 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,22 @@ ) public class RevertCommand extends AbstractParentCommand { - private static int applyCandidate(CliConsole console, ApplyCandidateAction applyCandidateAction, boolean yes) 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(); + } + if (!yes && !artifactUpdates.isEmpty() && !console.confirm(CliMessages.MESSAGES.continueWithRevert(), CliMessages.MESSAGES.applyingChanges(), CliMessages.MESSAGES.revertCancelled())) { return SUCCESS; @@ -92,6 +103,9 @@ public static class PerformCommand extends AbstractMavenCommand { @CommandLine.Option(names = {CliConstants.Y, CliConstants.YES}) boolean yes; + @CommandLine.Option(names = {CliConstants.NO_CONFLICTS_ONLY}) + boolean noConflictsOnly; + public PerformCommand(CliConsole console, ActionFactory actionFactory) { super(console, actionFactory); } @@ -121,7 +135,7 @@ public Integer call() throws Exception { validateRevertCandidate(installationDirectory, tempDirectory, applyCandidateAction); - applyCandidate(console, applyCandidateAction, yes); + applyCandidate(console, applyCandidateAction, yes, noConflictsOnly, false); } catch (IOException e) { throw ProsperoLogger.ROOT_LOGGER.unableToCreateTemporaryDirectory(e); } @@ -147,6 +161,12 @@ public static class ApplyCommand extends AbstractCommand { @CommandLine.Option(names = {CliConstants.Y, CliConstants.YES}) boolean yes; + @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); } @@ -162,7 +182,7 @@ public Integer call() throws Exception { console.println(CliMessages.MESSAGES.revertStart(installationDirectory, applyCandidateAction.getCandidateRevision().getName())); console.println(""); - applyCandidate(console, applyCandidateAction, yes); + 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 d1cdc898..675181a3 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 @@ -89,6 +89,9 @@ public static class PerformCommand extends AbstractMavenCommand { @CommandLine.Option(names = {CliConstants.Y, CliConstants.YES}) boolean yes; + @CommandLine.Option(names = {CliConstants.NO_CONFLICTS_ONLY}) + boolean noConflictsOnly; + public PerformCommand(CliConsole console, ActionFactory actionFactory) { super(console, actionFactory); } @@ -119,7 +122,7 @@ public Integer call() throws Exception { console.println(CliMessages.MESSAGES.updateHeader(installationDir)); try (UpdateAction updateAction = actionFactory.update(installationDir, mavenOptions, console, repositories)) { - performUpdate(updateAction, yes, console, installationDir); + performUpdate(updateAction, yes, console, installationDir, noConflictsOnly); } } @@ -129,7 +132,7 @@ public Integer call() throws Exception { return ReturnCodes.SUCCESS; } - private boolean performUpdate(UpdateAction updateAction, boolean yes, CliConsole console, Path installDir) throws OperationException, ProvisioningException { + private boolean performUpdate(UpdateAction updateAction, boolean yes, CliConsole console, Path installDir, boolean noConflictsOnly) throws OperationException, ProvisioningException { Path targetDir = null; try { targetDir = Files.createTempDirectory("update-candidate"); @@ -141,6 +144,11 @@ private boolean performUpdate(UpdateAction updateAction, boolean yes, CliConsole final List conflicts = applyCandidateAction.getConflicts(); if (!conflicts.isEmpty()) { FileConflictPrinter.print(conflicts, console); + + if (noConflictsOnly) { + throw CliMessages.MESSAGES.cancelledByConfilcts(); + } + if (!yes && !console.confirm(CliMessages.MESSAGES.continueWithUpdate(), "", CliMessages.MESSAGES.updateCancelled())) { return false; } @@ -226,6 +234,12 @@ public static class ApplyCommand extends AbstractCommand { @CommandLine.Option(names = {CliConstants.Y, CliConstants.YES}) boolean yes; + @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); } @@ -257,6 +271,14 @@ 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(); + } + // there always should be updates, so confirm update if (!yes && !console.confirm(CliMessages.MESSAGES.continueWithUpdate(), CliMessages.MESSAGES.applyingUpdates(), CliMessages.MESSAGES.updateCancelled())) { return ReturnCodes.SUCCESS; diff --git a/prospero-cli/src/main/java/org/wildfly/prospero/cli/spi/CliProviderImpl.java b/prospero-cli/src/main/java/org/wildfly/prospero/cli/spi/CliProviderImpl.java index 4224f2d3..f0a37700 100644 --- a/prospero-cli/src/main/java/org/wildfly/prospero/cli/spi/CliProviderImpl.java +++ b/prospero-cli/src/main/java/org/wildfly/prospero/cli/spi/CliProviderImpl.java @@ -49,20 +49,22 @@ public String getScriptName(OsShell shell) { } @Override - public String getApplyUpdateCommand(Path installationPath, Path candidatePath) { + public String getApplyUpdateCommand(Path installationPath, Path candidatePath, boolean noConflictsOnly) { return CliConstants.Commands.UPDATE + " " + CliConstants.Commands.APPLY + " " + CliConstants.DIR + " " + escape(installationPath.toAbsolutePath()) + " " + CliConstants.CANDIDATE_DIR + " " + escape(candidatePath.toAbsolutePath()) + " " + CliConstants.YES + " " + + (noConflictsOnly ? CliConstants.NO_CONFLICTS_ONLY + " " : "") + CliConstants.REMOVE; } @Override - public String getApplyRevertCommand(Path installationPath, Path candidatePath) { + public String getApplyRevertCommand(Path installationPath, Path candidatePath, boolean noConflictsOnly) { return CliConstants.Commands.REVERT + " " + CliConstants.Commands.APPLY + " " + CliConstants.DIR + " " + escape(installationPath.toAbsolutePath()) + " " + CliConstants.CANDIDATE_DIR + " " + escape(candidatePath.toAbsolutePath()) + " " + CliConstants.YES + " " + + (noConflictsOnly ? CliConstants.NO_CONFLICTS_ONLY + " " : "") + CliConstants.REMOVE; } diff --git a/prospero-cli/src/main/resources/UsageMessages.properties b/prospero-cli/src/main/resources/UsageMessages.properties index acfd47fe..ecfef6ce 100644 --- a/prospero-cli/src/main/resources/UsageMessages.properties +++ b/prospero-cli/src/main/resources/UsageMessages.properties @@ -152,7 +152,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|@ \ @@ -194,6 +193,9 @@ package-stability-level.1 = Valid options are ${COMPLETION-CANDIDATES}. ${prospero.dist.name}.update.prepare.candidate-dir = Target directory where the candidate server will be provisioned. The existing server is not updated. ${prospero.dist.name}.update.subscribe.product = Specify the product name. This must be a known feature pack supported by ${prospero.dist.name}. ${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 @@ -278,6 +280,8 @@ prospero.updates.apply.validation.candidate.wrong_type=Unable to apply candidate prospero.updates.apply.validation.candidate.not_candidate=Unable to apply candidate.%n Installation at [%s] doesn't have a candidate marker file. prospero.updates.apply.candidate.remove=Remove the candidate directory after applying update. +prospero.updates.apply.candidate.cancel_conflicts = Potential conflicts exist in the installation. Resolve the conflicts in the listed files, or \ + use [%s=false] to preserve user changes where possible. prospero.updates.build.candidate.header=Building update candidate for %s%n prospero.updates.build.candidate.complete=Update candidate generated in %s 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 6bd1c71e..c39e202a 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 @@ -24,6 +24,7 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.api.FileConflict; @@ -43,6 +44,7 @@ import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -188,6 +190,54 @@ public void testAskForConfirmationIfConflictsPresent() throws Exception { assertEquals(1, askedConfirmation); } + @Test + public void noConflictArgumentFailsCommand_WhenConflictsAreFound() throws Exception { + final Path updatePath = mockInstallation("update"); + final Path targetPath = mockInstallation("target"); + when(applyCandidateAction.getConflicts()).thenReturn(List.of(mock(FileConflict.class))); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.APPLY, + CliConstants.CANDIDATE_DIR, updatePath.toString(), + CliConstants.DIR, targetPath.toString(), + CliConstants.NO_CONFLICTS_ONLY); + + assertEquals(ReturnCodes.PROCESSING_ERROR, exitCode); + assertThat(getErrorOutput()) + .contains(CliMessages.MESSAGES.cancelledByConfilcts().getMessage()); + + verify(applyCandidateAction, Mockito.never()).applyUpdate(any()); + } + + @Test + public void noConflictArgumentHasNoEffect_WhenNoConflictsAreFound() 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.NO_CONFLICTS_ONLY); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + 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 dce61428..5dfe1eaa 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 @@ -25,10 +25,12 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.api.Console; import org.wildfly.prospero.actions.InstallationHistoryAction; +import org.wildfly.prospero.api.FileConflict; import org.wildfly.prospero.api.SavedState; import org.wildfly.prospero.api.exceptions.OperationException; import org.wildfly.prospero.cli.AbstractConsoleTest; @@ -40,9 +42,13 @@ import java.nio.file.Path; import java.util.Collections; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -119,4 +125,46 @@ public void callApplyOperation() throws Exception { assertEquals(ReturnCodes.SUCCESS, exitCode); verify(applyCandidateAction).applyUpdate(ApplyCandidateAction.Type.REVERT); } + + @Test + public void noConflictArgumentFailsCommand_WhenConflictsAreFound() throws Exception { + when(applyCandidateAction.getConflicts()).thenReturn(List.of(mock(FileConflict.class))); + + int exitCode = commandLine.execute(CliConstants.Commands.REVERT, CliConstants.Commands.APPLY, + CliConstants.DIR, installationDir.toString(), + CliConstants.CANDIDATE_DIR, updateDir.toString(), + CliConstants.NO_CONFLICTS_ONLY); + + assertEquals(ReturnCodes.PROCESSING_ERROR, exitCode); + assertThat(getErrorOutput()) + .contains(CliMessages.MESSAGES.cancelledByConfilcts().getMessage()); + + verify(applyCandidateAction, Mockito.never()).applyUpdate(any()); + } + + @Test + public void noConflictArgumentHasNoEffect_WhenNoConflictsAreFound() throws Exception { + when(applyCandidateAction.getConflicts()).thenReturn(Collections.emptyList()); + + int exitCode = commandLine.execute(CliConstants.Commands.REVERT, CliConstants.Commands.APPLY, + CliConstants.DIR, installationDir.toString(), + CliConstants.CANDIDATE_DIR, updateDir.toString(), + CliConstants.NO_CONFLICTS_ONLY); + + 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-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertPerformCommandTest.java b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertPerformCommandTest.java index 843f9f8d..652e285f 100644 --- a/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertPerformCommandTest.java +++ b/prospero-cli/src/test/java/org/wildfly/prospero/cli/commands/RevertPerformCommandTest.java @@ -31,11 +31,13 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.wildfly.channel.Repository; import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.api.Console; import org.wildfly.prospero.actions.InstallationHistoryAction; +import org.wildfly.prospero.api.FileConflict; import org.wildfly.prospero.api.MavenOptions; import org.wildfly.prospero.api.SavedState; import org.wildfly.prospero.api.exceptions.OperationException; @@ -50,6 +52,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -154,6 +157,35 @@ public void passRemoteRepositories() throws Exception { .containsExactly("http://temp.repo.te"); } + @Test + public void noConflictArgumentFailsCommand_WhenConflictsAreFound() throws Exception { + when(applyCandidateAction.getConflicts()).thenReturn(List.of(mock(FileConflict.class))); + + int exitCode = commandLine.execute(CliConstants.Commands.REVERT, CliConstants.Commands.PERFORM, + CliConstants.DIR, installationDir.toString(), + CliConstants.REVISION, "abcd", + CliConstants.NO_CONFLICTS_ONLY); + + assertEquals(ReturnCodes.PROCESSING_ERROR, exitCode); + assertThat(getErrorOutput()) + .contains(CliMessages.MESSAGES.cancelledByConfilcts().getMessage()); + + verify(applyCandidateAction, Mockito.never()).applyUpdate(any()); + } + + @Test + public void noConflictArgumentHasNoEffect_WhenNoConflictsAreFound() throws Exception { + when(applyCandidateAction.getConflicts()).thenReturn(Collections.emptyList()); + + int exitCode = commandLine.execute(CliConstants.Commands.REVERT, CliConstants.Commands.PERFORM, + CliConstants.DIR, installationDir.toString(), + CliConstants.REVISION, "abcd", + CliConstants.NO_CONFLICTS_ONLY); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + verify(applyCandidateAction).applyUpdate(ApplyCandidateAction.Type.REVERT); + } + @Override protected MavenOptions getCapturedMavenOptions() throws Exception { verify(historyAction).prepareRevert(eq(new SavedState("abcd")), mavenOptions.capture(), any(), any()); 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 7b76e405..855a40f5 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 @@ -39,6 +39,7 @@ import org.wildfly.prospero.actions.ApplyCandidateAction; import org.wildfly.prospero.actions.UpdateAction; import org.wildfly.prospero.api.ArtifactChange; +import org.wildfly.prospero.api.FileConflict; import org.wildfly.prospero.api.MavenOptions; import org.wildfly.prospero.cli.ActionFactory; import org.wildfly.prospero.cli.CliMessages; @@ -51,7 +52,9 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -303,6 +306,37 @@ public void spliRepositoriesFromArgument() throws Exception { } + @Test + public void noConflictArgumentFailsCommand_WhenConflictsAreFound() throws Exception { + when(updateAction.findUpdates()).thenReturn(new UpdateSet(List.of(change("1.0.0", "1.0.1")))); + when(updateAction.buildUpdate(any())).thenReturn(true); + when(applyCandidateAction.getConflicts()).thenReturn(List.of(mock(FileConflict.class))); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.PERFORM, + CliConstants.DIR, installationDir.toString(), + CliConstants.NO_CONFLICTS_ONLY); + + assertEquals(ReturnCodes.PROCESSING_ERROR, exitCode); + assertThat(getErrorOutput()) + .contains(CliMessages.MESSAGES.cancelledByConfilcts().getMessage()); + + verify(applyCandidateAction, Mockito.never()).applyUpdate(any()); + } + + @Test + public void noConflictArgumentHasNoEffect_WhenNoConflictsAreFound() throws Exception { + when(updateAction.findUpdates()).thenReturn(new UpdateSet(List.of(change("1.0.0", "1.0.1")))); + when(updateAction.buildUpdate(any())).thenReturn(true); + when(applyCandidateAction.getConflicts()).thenReturn(Collections.emptyList()); + + int exitCode = commandLine.execute(CliConstants.Commands.UPDATE, CliConstants.Commands.PERFORM, + CliConstants.DIR, installationDir.toString(), + CliConstants.NO_CONFLICTS_ONLY); + + assertEquals(ReturnCodes.SUCCESS, exitCode); + verify(applyCandidateAction).applyUpdate(ApplyCandidateAction.Type.UPDATE); + } + 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 da92cd14..8517672e 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; @@ -389,4 +390,15 @@ public interface ProsperoLogger extends BasicLogger { @Message(id = 271, value = "Unable to evaluate symbolic link %s.") RuntimeException unableToEvaluateSymbolicLink(Path symlink, @Cause IOException e); + + @Message(id = 272, value = "The server [%s] has been modified after the candidate has been created [%s].") + InvalidUpdateCandidateException staleCandidate(Path originalServer, Path candiadate); + + @Message(id = 273, value = "The folder [%s] doesn't contain a server candidate.") + InvalidUpdateCandidateException notCandidate(Path candidateServer); + + @Message(id = 274, 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 fc6b49c8..1b9a9b5f 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 @@ -744,9 +744,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 1568b69a..4e543abf 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))) { @@ -197,24 +256,34 @@ public String generateApplyRevertCommand(Path scriptHome, Path candidatePath) th @Override public String generateApplyUpdateCommand(Path scriptHome, Path candidatePath, OsShell shell) throws OperationNotAvailableException { + return generateApplyUpdateCommand(scriptHome, candidatePath, shell, false); + } + + @Override + public String generateApplyRevertCommand(Path scriptHome, Path candidatePath, OsShell shell) throws OperationNotAvailableException { + return generateApplyUpdateCommand(scriptHome, candidatePath, shell, false); + } + + @Override + public String generateApplyUpdateCommand(Path scriptHome, Path candidatePath, OsShell shell, boolean noConflictsOnly) throws OperationNotAvailableException { final Optional cliProviderLoader = ServiceLoader.load(CliProvider.class).findFirst(); if (cliProviderLoader.isEmpty()) { throw new OperationNotAvailableException("Installation manager does not support CLI operations."); } final CliProvider cliProvider = cliProviderLoader.get(); - return escape(scriptHome.resolve(cliProvider.getScriptName(shell))) + " " + cliProvider.getApplyUpdateCommand(installationDir, candidatePath); + return escape(scriptHome.resolve(cliProvider.getScriptName(shell))) + " " + cliProvider.getApplyUpdateCommand(installationDir, candidatePath, false); } @Override - public String generateApplyRevertCommand(Path scriptHome, Path candidatePath, OsShell shell) throws OperationNotAvailableException { + public String generateApplyRevertCommand(Path scriptHome, Path candidatePath, OsShell shell, boolean noConflictsOnly) throws OperationNotAvailableException { final Optional cliProviderLoader = ServiceLoader.load(CliProvider.class).findFirst(); if (cliProviderLoader.isEmpty()) { throw new OperationNotAvailableException("Installation manager does not support CLI operations."); } final CliProvider cliProvider = cliProviderLoader.get(); - return escape(scriptHome.resolve(cliProvider.getScriptName(shell))) + " " + cliProvider.getApplyRevertCommand(installationDir, candidatePath); + return escape(scriptHome.resolve(cliProvider.getScriptName(shell))) + " " + cliProvider.getApplyRevertCommand(installationDir, candidatePath, noConflictsOnly); } @Override @@ -345,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/main/java/org/wildfly/prospero/spi/internal/CliProvider.java b/prospero-common/src/main/java/org/wildfly/prospero/spi/internal/CliProvider.java index d29d6ac7..a4f4a7c2 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/spi/internal/CliProvider.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/spi/internal/CliProvider.java @@ -37,16 +37,18 @@ public interface CliProvider { * * @param installationPath * @param candidatePath + * @param noConflictsOnly - whether to append the no-conflicts-only flag * @return */ - String getApplyUpdateCommand(Path installationPath, Path candidatePath); + String getApplyUpdateCommand(Path installationPath, Path candidatePath, boolean noConflictsOnly); /** * generates command used to apply a revert candidate in {@code candidatePath} into {@code installationPath} * * @param installationPath * @param candidatePath + * @param noConflictsOnly - whether to append the no-conflicts-only flag * @return */ - String getApplyRevertCommand(Path installationPath, Path candidatePath); + String getApplyRevertCommand(Path installationPath, Path candidatePath, boolean noConflictsOnly); } 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 dc08526d..408e2c61 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