Skip to content

Commit

Permalink
Add dry-run option for apply operations
Browse files Browse the repository at this point in the history
  • Loading branch information
spyrkob committed Jul 12, 2024
1 parent 4ce69cb commit cb15571
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ArtifactChange> artifactUpdates = applyCandidateAction.findUpdates().getArtifactUpdates();
console.printArtifactChanges(artifactUpdates);
final List<FileConflict> conflicts = applyCandidateAction.getConflicts();
FileConflictPrinter.print(conflicts, console);

if (dryRun) {
return ReturnCodes.SUCCESS;
}

if (noConflictsOnly && !conflicts.isEmpty()) {
throw CliMessages.MESSAGES.cancelledByConfilcts();
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -268,6 +271,10 @@ public Integer call() throws Exception {
final List<FileConflict> conflicts = applyCandidateAction.getConflicts();
FileConflictPrinter.print(conflicts, console);

if (dryRun) {
return ReturnCodes.SUCCESS;
}

if (noConflictsOnly && !conflicts.isEmpty()) {
throw CliMessages.MESSAGES.cancelledByConfilcts();
}
Expand Down
2 changes: 1 addition & 1 deletion prospero-cli/src/main/resources/UsageMessages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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|@ \
Expand Down Expand Up @@ -196,6 +195,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);


}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -117,6 +121,61 @@ public boolean prepareUpdate(Path targetDir, List<Repository> repositories) thro
}
}

@Override
public Collection<FileConflict> 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<ArtifactChange> findUpdates(List<Repository> repositories) throws Exception {
try (UpdateAction updateAction = actionFactory.getUpdateAction(map(repositories, ProsperoInstallationManager::mapRepository))) {
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,24 @@
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;
import org.wildfly.prospero.api.SavedState;
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;
Expand Down Expand Up @@ -66,6 +72,9 @@ public class ProsperoInstallationManagerTest {
@Mock
private InstallationHistoryAction historyAction;

@Mock
private ApplyCandidateAction applyCandidateAction;

@Rule
public TemporaryFolder temp = new TemporaryFolder();

Expand Down Expand Up @@ -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<FileConflict> 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");

}
}

0 comments on commit cb15571

Please sign in to comment.