diff --git a/integration-tests/src/test/java/org/wildfly/prospero/it/cli/ApplyUpdateTest.java b/integration-tests/src/test/java/org/wildfly/prospero/it/cli/ApplyUpdateTest.java index 47e55bc2e..f358541a6 100644 --- a/integration-tests/src/test/java/org/wildfly/prospero/it/cli/ApplyUpdateTest.java +++ b/integration-tests/src/test/java/org/wildfly/prospero/it/cli/ApplyUpdateTest.java @@ -137,6 +137,49 @@ public void generateUpdateAndApplyUsingRepositoryArchive() throws Exception { assertEquals(WfCoreTestBase.UPGRADE_VERSION, wildflyCliStream.get().getVersion()); } + @Test + public void generateUpdateAndApplyIntoSymbolicLink() throws Exception { + final Path manifestPath = temp.newFile().toPath(); + final Path provisionConfig = temp.newFile().toPath(); + final Path updatePath = tempDir.newFolder("update-candidate").toPath(); + MetadataTestUtils.copyManifest("manifests/wfcore-base.yaml", manifestPath); + MetadataTestUtils.prepareChannel(provisionConfig, List.of(manifestPath.toUri().toURL()), defaultRemoteRepositories()); + + final Path targetLink = Files.createSymbolicLink(temp.newFolder().toPath().resolve("target-link"), targetDir.toPath()); + final Path candidateLink = Files.createSymbolicLink(temp.newFolder().toPath().resolve("update-candidate-link"), updatePath); + + install(provisionConfig, targetLink); + + upgradeStreamInManifest(manifestPath, resolvedUpgradeArtifact); + + final URL temporaryRepo = mockTemporaryRepo(true); + + // generate update-candidate + ExecutionUtils.prosperoExecution(CliConstants.Commands.UPDATE, CliConstants.Commands.PREPARE, + CliConstants.REPOSITORIES, temporaryRepo.toString(), + CliConstants.CANDIDATE_DIR, candidateLink.toAbsolutePath().toString(), + CliConstants.Y, + CliConstants.DIR, targetDir.getAbsolutePath()) + .execute() + .assertReturnCode(ReturnCodes.SUCCESS); + + // verify the original server has not been modified + Optional wildflyCliStream = getInstalledArtifact(resolvedUpgradeArtifact.getArtifactId(), targetDir.toPath()); + assertEquals(WfCoreTestBase.BASE_VERSION, wildflyCliStream.get().getVersion()); + + // apply update-candidate + ExecutionUtils.prosperoExecution(CliConstants.Commands.UPDATE, CliConstants.Commands.APPLY, + CliConstants.CANDIDATE_DIR, candidateLink.toAbsolutePath().toString(), + CliConstants.Y, + CliConstants.DIR, targetDir.getAbsolutePath()) + .execute() + .assertReturnCode(ReturnCodes.SUCCESS); + + // verify the original server has been modified + wildflyCliStream = getInstalledArtifact(resolvedUpgradeArtifact.getArtifactId(), targetDir.toPath()); + assertEquals(WfCoreTestBase.UPGRADE_VERSION, wildflyCliStream.get().getVersion()); + } + private Path createRepositoryArchive(URL temporaryRepo) throws URISyntaxException, IOException { final Path repoPath = Path.of(temporaryRepo.toURI()); final Path root = tempDir.newFolder("repo-root").toPath(); 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 495a8c81f..da92cd14d 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java @@ -386,4 +386,7 @@ public interface ProsperoLogger extends BasicLogger { @Message(id = 270, value = "Unable to compare the hash content between the installation %s and candidate installation %s.") MetadataException unableToCompareHashDirs(Path installationDir, Path updateDir, @Cause Exception e); + + @Message(id = 271, value = "Unable to evaluate symbolic link %s.") + RuntimeException unableToEvaluateSymbolicLink(Path symlink, @Cause IOException e); } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/ApplyCandidateAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/ApplyCandidateAction.java index 19a616b24..91495669a 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/ApplyCandidateAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/ApplyCandidateAction.java @@ -125,7 +125,8 @@ public static Type from (final String text) { public ApplyCandidateAction(Path installationDir, Path updateDir) throws ProvisioningException, OperationException { this.updateDir = updateDir; - this.installationDir = installationDir; + this.installationDir = InstallFolderUtils.toRealPath(installationDir); + updateDir = InstallFolderUtils.toRealPath(updateDir); try { this.systemPaths = SystemPaths.load(updateDir); 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 11a46ed01..fc6b49c8f 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 @@ -97,9 +97,10 @@ public FeaturesAddAction(MavenOptions mavenOptions, Path installDir, List repositories, Console console, CandidateActionsFactory candidateActionsFactory, FeaturePackTemplateManager featurePackTemplateManager) throws MetadataException, ProvisioningException { - this.installDir = installDir; + this.installDir = InstallFolderUtils.toRealPath(installDir); + this.console = console; - this.metadata = InstallationMetadata.loadInstallation(installDir); + this.metadata = InstallationMetadata.loadInstallation(this.installDir); this.prosperoConfig = addTemporaryRepositories(repositories); final MavenOptions mergedOptions = prosperoConfig.getMavenOptions().merge(mavenOptions); @@ -134,6 +135,8 @@ public void addFeaturePack(String featurePackCoord, Set defaultConfigN verifyFeaturePackCoord(featurePackCoord); Objects.requireNonNull(defaultConfigNames); + candidatePath = InstallFolderUtils.toRealPath(candidatePath); + FeaturePackLocation fpl = FeaturePackLocationParser.resolveFpl(featurePackCoord); if (ProsperoLogger.ROOT_LOGGER.isTraceEnabled()) { diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallFolderUtils.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallFolderUtils.java index 8a04d020b..42c08fa51 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallFolderUtils.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallFolderUtils.java @@ -19,6 +19,7 @@ import org.wildfly.prospero.ProsperoLogger; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -40,6 +41,42 @@ static void verifyIsEmptyDir(Path directory) { } } + /** + * convert the {@code symlink} to a real path. If any of the parent folders are a symlink, they will be + * converted to real path as well. + * + * @param symlink + * @return + */ + static Path toRealPath(Path symlink) { + /* + * There's an issue when trying to copy artifacts to a folder that is symlink + * This only happens when the last segment of path is symlink itself, but to be consistent, we replace the + * symlink anywhere in the path. This way we know we're always operating on a real path from this point on. + */ + Path path = symlink; + + // find a symlink (if any) in the path and its parents + while (path != null && !(Files.exists(path) && Files.isSymbolicLink(path))) { + path = path.getParent(); + } + + // if no symlinks were found we got to the root of the path (null) and we don't need to anything + if (path == null) { + return symlink; + } else { + // get the subfolder path between the symlink and the actual path + final Path relativized = path.relativize(symlink); + try { + // evaluate the symlink and append the relative path to get back to the starting folder + return path.toRealPath().resolve(relativized); + } catch (IOException e) { + // we know the file at path does exist, so if we got an exception here, that's an I/O error + throw ProsperoLogger.ROOT_LOGGER.unableToEvaluateSymbolicLink(symlink, e); + } + } + } + private static boolean isWritable(final Path path) { Path absPath = path.toAbsolutePath(); if (Files.exists(absPath)) { diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationExportAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationExportAction.java index 2e685d906..0f650e018 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationExportAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationExportAction.java @@ -30,7 +30,7 @@ public class InstallationExportAction { private final Path installationDir; public InstallationExportAction(Path installationDir) { - this.installationDir = installationDir; + this.installationDir = InstallFolderUtils.toRealPath(installationDir); } public static void main(String[] args) throws Exception { @@ -45,6 +45,8 @@ public void export(Path exportPath) throws IOException, MetadataException { throw ProsperoLogger.ROOT_LOGGER.installationDirDoesNotExist(installationDir); } + exportPath = InstallFolderUtils.toRealPath(exportPath); + try (InstallationMetadata metadataBundle = InstallationMetadata.loadInstallation(installationDir)) { metadataBundle.exportMetadataBundle(exportPath); diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationHistoryAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationHistoryAction.java index c56e1b287..727508f19 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationHistoryAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationHistoryAction.java @@ -51,7 +51,7 @@ public class InstallationHistoryAction { private final Console console; public InstallationHistoryAction(Path installation, Console console) { - this.installation = installation; + this.installation = InstallFolderUtils.toRealPath(installation); this.console = console; } @@ -111,6 +111,8 @@ public void prepareRevert(SavedState savedState, MavenOptions mavenOptions, List InstallFolderUtils.verifyIsWritable(targetDir); } + targetDir = InstallFolderUtils.toRealPath(targetDir); + try (InstallationMetadata metadata = InstallationMetadata.loadInstallation(installation)) { ProsperoLogger.ROOT_LOGGER.revertCandidateStarted(installation); diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationRestoreAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationRestoreAction.java index f619629ce..6f0c107a5 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationRestoreAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/InstallationRestoreAction.java @@ -48,7 +48,7 @@ public class InstallationRestoreAction { private final MavenSessionManager mavenSessionManager; public InstallationRestoreAction(Path installDir, MavenOptions mavenOptions, Console console) throws ProvisioningException { - this.installDir = installDir; + this.installDir = InstallFolderUtils.toRealPath(installDir); this.mavenSessionManager = new MavenSessionManager(mavenOptions); this.console = console; } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/MetadataAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/MetadataAction.java index 058acae8b..950bf4d05 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/MetadataAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/MetadataAction.java @@ -36,6 +36,7 @@ public class MetadataAction implements AutoCloseable { private final InstallationMetadata installationMetadata; public MetadataAction(Path installation) throws MetadataException { + installation = InstallFolderUtils.toRealPath(installation); this.installationMetadata = InstallationMetadata.loadInstallation(installation); } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/ProvisioningAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/ProvisioningAction.java index a1c18d43d..212395fcc 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/ProvisioningAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/ProvisioningAction.java @@ -73,7 +73,7 @@ public class ProvisioningAction { private final MavenOptions mvnOptions; public ProvisioningAction(Path installDir, MavenOptions mvnOptions, Console console) throws ProvisioningException { - this.installDir = installDir; + this.installDir = InstallFolderUtils.toRealPath(installDir); this.console = console; this.mvnOptions = mvnOptions; this.mavenSessionManager = new MavenSessionManager(mvnOptions); 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 73aff4833..7b1e5c4c1 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 @@ -55,8 +55,8 @@ public class UpdateAction implements AutoCloseable { public UpdateAction(Path installDir, MavenOptions mavenOptions, Console console, List overrideRepositories) throws OperationException, ProvisioningException { - this.metadata = InstallationMetadata.loadInstallation(installDir); - this.installDir = installDir; + this.installDir = InstallFolderUtils.toRealPath(installDir); + this.metadata = InstallationMetadata.loadInstallation(this.installDir); this.console = console; this.prosperoConfig = addTemporaryRepositories(overrideRepositories); this.mavenOptions = prosperoConfig.getMavenOptions().merge(mavenOptions); @@ -111,6 +111,8 @@ public boolean buildUpdate(Path targetDir) throws ProvisioningException, Operati InstallFolderUtils.verifyIsWritable(targetDir); } + targetDir = InstallFolderUtils.toRealPath(targetDir); + final UpdateSet updateSet = findUpdates(); if (updateSet.isEmpty()) { ProsperoLogger.ROOT_LOGGER.noUpdatesFound(installDir); diff --git a/prospero-common/src/test/java/org/wildfly/prospero/actions/InstallFolderUtilsTest.java b/prospero-common/src/test/java/org/wildfly/prospero/actions/InstallFolderUtilsTest.java new file mode 100644 index 000000000..d4af5da96 --- /dev/null +++ b/prospero-common/src/test/java/org/wildfly/prospero/actions/InstallFolderUtilsTest.java @@ -0,0 +1,65 @@ +package org.wildfly.prospero.actions; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InstallFolderUtilsTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void testSymlinkIsConvertedToRealPath() throws Exception { + final Path realPath = createFolder(); + final Path link = Files.createSymbolicLink(temp.newFolder().toPath().resolve("link"), realPath); + + assertThat(InstallFolderUtils.toRealPath(link)) + .isEqualTo(realPath); + } + + @Test + public void testSymlinkNonExistingSubfolderIsConvertedToRealPath() throws Exception { + final Path realPath = createFolder(); + final Path link = Files.createSymbolicLink(temp.newFolder().toPath().resolve("link"), realPath); + + assertThat(InstallFolderUtils.toRealPath(link.resolve("test/foo"))) + .isEqualTo(realPath.resolve("test/foo")); + } + + @Test + public void testSymlinkSubfolderIsConvertedToRealPath() throws Exception { + final Path realPath = createFolder(); + Files.createDirectories(realPath.resolve("test/foo")); + final Path link = Files.createSymbolicLink(temp.newFolder().toPath().resolve("link"), realPath); + + assertThat(InstallFolderUtils.toRealPath(link.resolve("test/foo"))) + .isEqualTo(realPath.resolve("test/foo")); + } + + @Test + public void testNonExistingFolderIsNotConverted() throws Exception { + final Path folder = temp.newFolder().toPath().toRealPath().resolve("link"); + + assertThat(InstallFolderUtils.toRealPath(folder)) + .isEqualTo(folder); + } + + @Test + public void testExistingFolderIsNotConverted() throws Exception { + final Path folder = temp.newFolder().toPath().toRealPath(); + + assertThat(InstallFolderUtils.toRealPath(folder)) + .isEqualTo(folder); + } + + private Path createFolder() throws IOException { + return temp.newFolder("real").toPath().toRealPath(); + } +} \ No newline at end of file