diff --git a/integration-tests/src/test/java/org/wildfly/prospero/it/utils/DirectoryComparator.java b/integration-tests/src/test/java/org/wildfly/prospero/it/utils/DirectoryComparator.java index af1b4858b..eb2ab21d0 100644 --- a/integration-tests/src/test/java/org/wildfly/prospero/it/utils/DirectoryComparator.java +++ b/integration-tests/src/test/java/org/wildfly/prospero/it/utils/DirectoryComparator.java @@ -13,6 +13,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.List; +import java.util.Set; import static org.junit.Assert.fail; @@ -20,23 +21,23 @@ public class DirectoryComparator { private static final BinaryDiff BINARY_DIFF = new BinaryDiff(); private static class FileChange { + final Path expected; final Path actual; - public FileChange(Path expected, Path actual) { this.expected = expected; this.actual = actual; } - } - public static void assertNoChanges(Path originalServer, Path targetDir) throws IOException { + } + public static void assertNoChanges(Path originalServer, Path targetDir, Path... exceptions) throws IOException { final List changes = new ArrayList<>(); // get a list of files present only in the expected server or ones present in both but with different content - Files.walkFileTree(originalServer, listAddedAndModifiedFiles(originalServer, targetDir, changes)); + Files.walkFileTree(originalServer, listAddedAndModifiedFiles(originalServer, targetDir, changes, Set.of(exceptions))); // get a list of files present only in the actual server - Files.walkFileTree(targetDir, listRemovedFiles(originalServer, targetDir, changes)); + Files.walkFileTree(targetDir, listRemovedFiles(originalServer, targetDir, changes, Set.of(exceptions))); if (!changes.isEmpty()) { fail(describeChanges(originalServer, targetDir, changes)); @@ -68,11 +69,14 @@ private static String describeChanges(Path originalServer, Path targetDir, List< return sb.toString(); } - private static SimpleFileVisitor listRemovedFiles(Path originalServer, Path targetDir, List changes) { + private static SimpleFileVisitor listRemovedFiles(Path originalServer, Path targetDir, List changes, Set exceptions) { return new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { final Path relative = targetDir.relativize(file); + if (exceptions.contains(relative)) { + return FileVisitResult.CONTINUE; + } final Path expectedFile = originalServer.resolve(relative); if (!Files.exists(expectedFile)) { changes.add(new FileChange(null, file)); @@ -89,14 +93,27 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } return FileVisitResult.CONTINUE; } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + final Path relative = targetDir.relativize(dir); + if (exceptions.contains(relative)) { + return FileVisitResult.SKIP_SUBTREE; + } else { + return FileVisitResult.CONTINUE; + } + } }; } - private static SimpleFileVisitor listAddedAndModifiedFiles(Path originalServer, Path targetDir, List changes) { + private static SimpleFileVisitor listAddedAndModifiedFiles(Path originalServer, Path targetDir, List changes, Set exceptions) { return new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { final Path relative = originalServer.relativize(file); + if (exceptions.contains(relative)) { + return FileVisitResult.CONTINUE; + } final Path actualFile = targetDir.resolve(relative); if (!Files.exists(actualFile)) { changes.add(new FileChange(file, null)); @@ -115,6 +132,16 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } return FileVisitResult.CONTINUE; } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + final Path relative = originalServer.relativize(dir); + if (exceptions.contains(relative)) { + return FileVisitResult.SKIP_SUBTREE; + } else { + return FileVisitResult.CONTINUE; + } + } }; } } diff --git a/integration-tests/src/test/java/org/wildfly/prospero/test/BuildProperties.java b/integration-tests/src/test/java/org/wildfly/prospero/test/BuildProperties.java new file mode 100644 index 000000000..f259a791d --- /dev/null +++ b/integration-tests/src/test/java/org/wildfly/prospero/test/BuildProperties.java @@ -0,0 +1,42 @@ +/* + * 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.test; + +import java.io.IOException; +import java.util.Properties; + +/** + * Access to the Maven build properties + */ +public class BuildProperties { + + private static final Properties properties; + + static { + properties = new Properties(); + try { + properties.load(BuildProperties.class.getClassLoader().getResourceAsStream("properties-from-pom.properties")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String getProperty(String name) { + return properties.getProperty(name); + } +} diff --git a/integration-tests/src/test/java/org/wildfly/prospero/test/TestInstallation.java b/integration-tests/src/test/java/org/wildfly/prospero/test/TestInstallation.java new file mode 100644 index 000000000..b1f78ad45 --- /dev/null +++ b/integration-tests/src/test/java/org/wildfly/prospero/test/TestInstallation.java @@ -0,0 +1,269 @@ +/* + * 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.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.jboss.galleon.ProvisioningException; +import org.jboss.galleon.api.config.GalleonProvisioningConfig; +import org.jboss.galleon.creator.FeaturePackBuilder; +import org.jboss.galleon.creator.FeaturePackCreator; +import org.jboss.galleon.creator.PackageBuilder; +import org.jboss.galleon.repo.RepositoryArtifactResolver; +import org.jboss.galleon.universe.FeaturePackLocation; +import org.jboss.galleon.universe.maven.repo.SimplisticMavenRepoManager; +import org.wildfly.channel.Channel; +import org.wildfly.channel.Repository; +import org.wildfly.prospero.actions.InstallationHistoryAction; +import org.wildfly.prospero.actions.ProvisioningAction; +import org.wildfly.prospero.actions.UpdateAction; +import org.wildfly.prospero.api.Console; +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; +import org.wildfly.prospero.it.AcceptingConsole; +import org.wildfly.prospero.metadata.ProsperoMetadataUtils; + +/** + * A simple Galleon feature pack creating one or more module(s) with artifact(s) provisioned by Prospero. + */ +public class TestInstallation { + + private final Path serverRoot; + + public TestInstallation(Path serverRoot) { + this.serverRoot = serverRoot; + } + + public static Builder fpBuilder(String name) { + return new Builder(name); + } + + public void verifyModuleJar(String groupId, String artifactId, String version) { + final Path moduleRoot = serverRoot.resolve("modules").resolve(groupId.replace('.', '/') + "/" + artifactId + "/main"); + + assertThat(moduleRoot.resolve("module.xml")) + .exists() + .content().contains(artifactId + "-" + version + ".jar"); + assertThat(moduleRoot.resolve(artifactId + "-" + version + ".jar")) + .exists(); + } + + /** + * verifies that required files in .installation folder exist + */ + public void verifyInstallationMetadataPresent() { + final Path metadataRoot = serverRoot.resolve(ProsperoMetadataUtils.METADATA_DIR); + assertThat(metadataRoot).exists(); + assertThat(metadataRoot.resolve(ProsperoMetadataUtils.INSTALLER_CHANNELS_FILE_NAME)).exists(); + assertThat(metadataRoot.resolve(ProsperoMetadataUtils.MANIFEST_FILE_NAME)).exists(); + } + + /** + * see {@link #install(String, List, Console, List)} + * + * @param fplName + * @param channels + * @throws ProvisioningException + * @throws MalformedURLException + * @throws OperationException + */ + public void install(String fplName, List channels) throws ProvisioningException, MalformedURLException, OperationException { + install(fplName, channels, new AcceptingConsole()); + } + + /** + * see {@link #install(String, List, Console, List)} + * + * @param fplName + * @param channels + * @param console + * @throws ProvisioningException + * @throws MalformedURLException + * @throws OperationException + */ + public void install(String fplName, List channels, Console console) throws ProvisioningException, MalformedURLException, OperationException { + new ProvisioningAction(serverRoot, MavenOptions.OFFLINE_NO_CACHE, console) + .provision(GalleonProvisioningConfig.builder() + .addFeaturePackDep(FeaturePackLocation.fromString(fplName)) + .build(), channels); + } + + /** + * provision a feature pack with name {@name fplName}. + * + * @param fplName + * @param channels + * @param console + * @param overrideRepositoryUrls + * @throws ProvisioningException + * @throws MalformedURLException + * @throws OperationException + */ + public void install(String fplName, List channels, Console console, List overrideRepositoryUrls) throws ProvisioningException, MalformedURLException, OperationException { + final List overrideRepositories = IntStream.range(0, overrideRepositoryUrls.size()) + .mapToObj(i -> new Repository("test-" + i, overrideRepositoryUrls.get(i).toExternalForm())) + .collect(Collectors.toList()); + + new ProvisioningAction(serverRoot, MavenOptions.OFFLINE_NO_CACHE, console) + .provision(GalleonProvisioningConfig.builder() + .addFeaturePackDep(FeaturePackLocation.fromString(fplName)) + .build(), channels, overrideRepositories); + } + + /** + * see {@link #update(Console)} + * @return + * @throws ProvisioningException + * @throws OperationException + */ + public List update() throws ProvisioningException, OperationException { + return update(new AcceptingConsole()); + } + + /** + * Performs update on the test server + * + * @param console + * @return + * @throws ProvisioningException + * @throws OperationException + */ + public List update(Console console) throws ProvisioningException, OperationException { + try (UpdateAction updateAction = new UpdateAction(serverRoot, MavenOptions.OFFLINE_NO_CACHE, console, Collections.emptyList())) { + return updateAction.performUpdate(); + } + } + + /** + * see {@link #revertToOriginalState(Console, List)} (Console)} + * @return + * @throws ProvisioningException + * @throws OperationException + */ + public void revertToOriginalState() throws OperationException, ProvisioningException { + revertToOriginalState(new AcceptingConsole()); + } + + /** + * see {@link #revertToOriginalState(Console, List)} (Console)} + * @return + * @throws ProvisioningException + * @throws OperationException + */ + public void revertToOriginalState(Console console) throws OperationException, ProvisioningException { + revertToOriginalState(console, Collections.emptyList()); + } + + /** + * Reverts installation to the first recorded state + * + * @param console + * @param repositories + * @throws OperationException + * @throws ProvisioningException + */ + public void revertToOriginalState(Console console, List repositories) throws OperationException, ProvisioningException { + InstallationHistoryAction installationHistoryAction = new InstallationHistoryAction(serverRoot, console); + final List revisions = installationHistoryAction.getRevisions(); + final SavedState originalState = revisions.get(revisions.size() - 1); + final ArrayList list = new ArrayList<>(); + for (int i = 0; i < repositories.size(); i++) { + list.add(new Repository("test-" + i, repositories.get(i).toExternalForm())); + } + installationHistoryAction.rollback(originalState, MavenOptions.OFFLINE_NO_CACHE, list); + } + + /** + * Simplified builder for a feature pack. Generates a zip file with the feature pack + */ + public static class Builder { + private List modules = new ArrayList<>(); + private final String name; + + private Builder(String name) { + this.name = name; + } + + private static RepositoryArtifactResolver initRepoManager(Path repoHome) { + return SimplisticMavenRepoManager.getInstance(repoHome); + } + + public TestInstallation.Builder addModule(String groupId, String artifactId, String version) { + modules.add(new DefaultArtifact(groupId, artifactId, "jar", version)); + return this; + } + + public Artifact build() throws IOException, ProvisioningException { + final Path tempRoot = Files.createTempDirectory("fp-builder"); + tempRoot.toFile().deleteOnExit(); + final RepositoryArtifactResolver repo = initRepoManager(tempRoot); + FeaturePackCreator creator = FeaturePackCreator.getInstance().addArtifactResolver(repo); + + final FeaturePackBuilder featurePackBuilder = creator.newFeaturePack(FeaturePackLocation.fromString(name).getFPID()) + .addPlugin("wildfly-galleon-plugins", "org.wildfly.galleon-plugins:wildfly-galleon-plugins:jar:7.1.2.Final"); + + final PackageBuilder packageBuilder = featurePackBuilder + .newPackage("p1", true); + + final StringBuilder versions = new StringBuilder(); + for (Artifact module : modules) { + final String moduleName = module.getGroupId() + "." + module.getArtifactId(); + final String path = moduleName.replace('.', '/'); + packageBuilder + .writeContent("pm/wildfly/module/modules/" + path + "/main/module.xml", + "\n" + + " \n" + + " \n" + + " \n" + + "\n" + + "", false); + + versions.append(String.format("%s:%s=%s:%s:%s::jar%n", module.getGroupId(), module.getArtifactId(), + module.getGroupId(), module.getArtifactId(), module.getVersion())); + } + + + + featurePackBuilder + .writeResources("wildfly/artifact-versions.properties", + versions.toString()) + .writeResources("wildfly/wildfly-channel.properties", "resolution=REQUIRED"); + creator.install(); + + final String[] parts = name.split(":"); + final String fpPath = parts[0].replace('.', '/') + "/" + parts[1] + "/" + parts[2]; + return new DefaultArtifact(parts[0], parts[1], null, "zip", parts[2], + null, tempRoot.resolve(Path.of(fpPath, parts[1] + "-" + parts[2] + ".zip")).toFile()); + } + } +} diff --git a/integration-tests/src/test/java/org/wildfly/prospero/test/TestLocalRepository.java b/integration-tests/src/test/java/org/wildfly/prospero/test/TestLocalRepository.java new file mode 100644 index 000000000..d5feeadd8 --- /dev/null +++ b/integration-tests/src/test/java/org/wildfly/prospero/test/TestLocalRepository.java @@ -0,0 +1,150 @@ +/* + * 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.test; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.deployment.DeploymentException; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.jboss.galleon.ProvisioningException; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestMapper; +import org.wildfly.prospero.wfchannel.MavenSessionManager; + +/** + * Utility class to generate a local Maven repository and deploy artifacts to it + */ +public class TestLocalRepository { + + private final Path root; + private final RepositorySystem system; + private final DefaultRepositorySystemSession session; + private final List upstreamRepositories; + + /** + * + * @param root - path of the repository + * @param upstreamRepos - URLs of Maven repositories used to resolve artifacts to be deployed in this repository + * @throws ProvisioningException + */ + public TestLocalRepository(Path root, List upstreamRepos) throws ProvisioningException { + this.root = root; + final AtomicInteger i = new AtomicInteger(0); + this.upstreamRepositories = upstreamRepos.stream() + .map(r -> new RemoteRepository.Builder("repo-" + i.getAndIncrement(), "default", r.toExternalForm()).build()) + .collect(Collectors.toList()); + + final MavenSessionManager msm = new MavenSessionManager(); + system = msm.newRepositorySystem(); + session = msm.newRepositorySystemSession(system); + } + + /** + * Deploys channel manifest in the repository + * + * @param manifestGav - The Maven GAV that the manifest will be deployed at + * @param manifest - the manifest to be deployed + * @throws IOException + * @throws DeploymentException + */ + public void deploy(Artifact manifestGav, ChannelManifest manifest) throws IOException, DeploymentException { + final Path tempFile = Files.createTempFile("test-manifest", "yaml"); + try { + Files.writeString(tempFile, ChannelManifestMapper.toYaml(manifest)); + + final Artifact artifactWithFile = manifestGav.setFile(tempFile.toFile()); + deploy(artifactWithFile); + } finally { + Files.delete(tempFile); + } + } + + /** + * Deploys an artifact + * + * @param artifact + * @throws DeploymentException + */ + public void deploy(Artifact artifact) throws DeploymentException { + final DeployRequest req = new DeployRequest(); + req.setRepository(new RemoteRepository.Builder("local-repo", "default", root.toUri().toString()).build()); + req.addArtifact(artifact); + + system.deploy(session, req); + } + + /** + * Resolves an artifact in upstream repositories, and if successful, deploys it locally. + * + * @param artifact + * @throws ArtifactResolutionException + * @throws DeploymentException + */ + public void resolveAndDeploy(Artifact artifact) throws ArtifactResolutionException, DeploymentException { + deploy(resolveUpstream(artifact)); + } + + /** + * Mocks an update to the artifact with provided GAV. + * + * Resolves an upstream artifact with {@code groupId:artifactId:version} and deploys it as an update. + * The version of the new artifact is {@code version} + {@code newVersionSuffix} + * + * @param groupId + * @param artifactId + * @param version + * @param newVersionSuffix + * @throws DeploymentException + * @throws ArtifactResolutionException + */ + public void deployMockUpdate(String groupId, String artifactId, String version, String newVersionSuffix) throws DeploymentException, ArtifactResolutionException { + Artifact artifact = resolveUpstream(new DefaultArtifact(groupId, artifactId, null, "jar", version)); + deploy(artifact.setVersion(version + newVersionSuffix)); + } + + /** + * Local URI of this repository + * @return + */ + public URI getUri() { + return root.toUri(); + } + + private Artifact resolveUpstream(Artifact artifact) throws ArtifactResolutionException { + final ArtifactRequest artifactRequest = new ArtifactRequest(); + artifactRequest.setRepositories(upstreamRepositories); + artifactRequest.setArtifact(artifact); + final ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest); + return artifactResult.getArtifact(); + } +}