diff --git a/integration-tests/src/test/java/org/wildfly/prospero/it/cli/CacheManifestTest.java b/integration-tests/src/test/java/org/wildfly/prospero/it/cli/CacheManifestTest.java new file mode 100644 index 000000000..a2039772f --- /dev/null +++ b/integration-tests/src/test/java/org/wildfly/prospero/it/cli/CacheManifestTest.java @@ -0,0 +1,350 @@ +/* + * 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.it.cli; + +import org.apache.commons.io.FileUtils; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.deployment.DeploymentException; +import org.jboss.galleon.ProvisioningException; +import org.jboss.galleon.config.ProvisioningConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.wildfly.channel.Channel; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestCoordinate; +import org.wildfly.channel.ChannelManifestMapper; +import org.wildfly.channel.ChannelMetadataCoordinate; +import org.wildfly.channel.Stream; +import org.wildfly.prospero.actions.InstallationHistoryAction; +import org.wildfly.prospero.actions.MetadataAction; +import org.wildfly.prospero.actions.UpdateAction; +import org.wildfly.prospero.api.ProvisioningDefinition; +import org.wildfly.prospero.api.SavedState; +import org.wildfly.prospero.api.exceptions.MetadataException; +import org.wildfly.prospero.api.exceptions.OperationException; +import org.wildfly.prospero.api.exceptions.UnresolvedChannelMetadataException; +import org.wildfly.prospero.cli.CliConsole; +import org.wildfly.prospero.galleon.ArtifactCache; +import org.wildfly.prospero.it.commonapi.WfCoreTestBase; +import org.wildfly.prospero.it.utils.TestRepositoryUtils; +import org.wildfly.prospero.test.MetadataTestUtils; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CacheManifestTest extends WfCoreTestBase { + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + private ProvisioningConfig baseProvCfg; + private Channel baseChannel; + private File baseManifest; + private TestRepositoryUtils repositoryUtils; + private ChannelManifest secondManifest; + + @Before + public void setUp() throws Exception { + super.setUp(); + + final ProvisioningDefinition.Builder provDef = defaultWfCoreDefinition(); + provDef.setManifest("org.test.channels:wf-core-base"); + baseProvCfg = provDef.build().toProvisioningConfig(); + + // create channel with default repos and manifest GA + final Channel.Builder builder = new Channel.Builder(); + defaultRemoteRepositories().forEach(r->builder.addRepository(r.getId(), r.getUrl())); + builder.setManifestCoordinate(new ChannelManifestCoordinate("org.test.channels", "wf-core-base")); + baseChannel = builder.build(); + baseManifest = Path.of(MetadataTestUtils.class.getClassLoader().getResource("manifests/wfcore-base.yaml").toURI()).toFile(); + + // deploy base manifest + repositoryUtils = new TestRepositoryUtils(new URI(updateRepository.getUrl()).toURL()); + repositoryUtils.deployArtifact(new DefaultArtifact( + "org.test.channels", + "wf-core-base", + ChannelManifest.CLASSIFIER, + ChannelManifest.EXTENSION, + "1.0.0", + null, + baseManifest + )); + + // create and deploy second manifest + secondManifest = new ChannelManifest(null, null, null, null, + List.of(new Stream("org.wildfly.core", "wildfly-controller", UPGRADE_VERSION))); + repositoryUtils.deployArtifact(new DefaultArtifact( + "org.test.channels", + "wf-core-second", + ChannelManifest.CLASSIFIER, + ChannelManifest.EXTENSION, + "1.0.0", + null, + Files.writeString(temp.newFile("second-manifest-1.0.0").toPath(), ChannelManifestMapper.toYaml(secondManifest)).toFile() + )); + + // deploy additional test artifact + final RepositorySystem system = mavenSessionManager.newRepositorySystem(); + deployIfMissing(system, mavenSessionManager.newRepositorySystemSession(system), "org.wildfly.core", "wildfly-controller", null, "jar"); + } + + @After + public void tearDown() throws Exception { + repositoryUtils.removeArtifact("org.test.channels", "wf-core-base"); + repositoryUtils.removeArtifact("org.test.channels", "wf-core-second"); + } + + @Test + public void updateWithTwoManifestAvailable() throws Exception { + // install server + installBaseServer(); + + // update server + addSecondChannel(); + final ChannelManifest updatedManifest = updateBaseChannel(); + performUpdate(); + + // verify both manifests are in the cache + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.1-manifest.yaml")) + .exists() + .hasContent(ChannelManifestMapper.toYaml(updatedManifest)); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-second-1.0.0-manifest.yaml")) + .exists() + .hasContent(ChannelManifestMapper.toYaml(secondManifest)); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("artifacts.txt")) + .content() + .contains("org.test.channels:wf-core-base:yaml:manifest:1.0.1") + .contains("org.test.channels:wf-core-second:yaml:manifest:1.0.0") + .doesNotContain("org.test.channels:wf-core-base:yaml:manifest:1.0.0"); + } + + @Test + public void updateWithOneManifestAvailable() throws Exception { + // install server + installBaseServer(); + + // update server with the base manifest read from cache + addSecondChannel(); + repositoryUtils.removeArtifact("org.test.channels", "wf-core-base"); + performUpdate(); + + // verify both manifests are in the cache + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")) + .exists() + .hasSameTextualContentAs(baseManifest.toPath()); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-second-1.0.0-manifest.yaml")) + .exists() + .hasContent(ChannelManifestMapper.toYaml(secondManifest)); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("artifacts.txt")) + .content() + .contains("org.test.channels:wf-core-base:yaml:manifest:1.0.0") + .contains("org.test.channels:wf-core-second:yaml:manifest:1.0.0"); + } + + @Test + public void revertWithOriginalManifestAvailable() throws Exception { + installBaseServer(); + + // update server with the base manifest read from cache + addSecondChannel(); + final ChannelManifest updatedManifest = updateBaseChannel(); + performUpdate(); + + // verify both manifests are in the cache + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.1-manifest.yaml")) + .exists() + .hasContent(ChannelManifestMapper.toYaml(updatedManifest)); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-second-1.0.0-manifest.yaml")) + .exists() + .hasContent(ChannelManifestMapper.toYaml(secondManifest)); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("artifacts.txt")) + .content() + .contains("org.test.channels:wf-core-base:yaml:manifest:1.0.1") + .contains("org.test.channels:wf-core-second:yaml:manifest:1.0.0") + .doesNotContain("org.test.channels:wf-core-base:yaml:manifest:1.0.0"); + + // revert to base + final InstallationHistoryAction historyAction = new InstallationHistoryAction(outputPath, new CliConsole()); + final List revisions = historyAction.getRevisions(); + final SavedState savedState = revisions.get(revisions.size() - 1); + historyAction.rollback(savedState, mavenOptions, Collections.emptyList()); + + // verify the base manifest is available and at 1.0.0 version + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")) + .exists() + .hasSameTextualContentAs(baseManifest.toPath()); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("artifacts.txt")) + .content() + .contains("org.test.channels:wf-core-base:yaml:manifest:1.0.0"); + } + + @Test + public void revertWithOriginalManifestNotAvailable() throws Exception { + installBaseServer(); + + // update server with the base manifest read from cache + addSecondChannel(); + final ChannelManifest updatedManifest = updateBaseChannel(); + performUpdate(); + + // verify both manifests are in the cache + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.1-manifest.yaml")) + .exists() + .hasContent(ChannelManifestMapper.toYaml(updatedManifest)); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-second-1.0.0-manifest.yaml")) + .exists() + .hasContent(ChannelManifestMapper.toYaml(secondManifest)); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("artifacts.txt")) + .content() + .contains("org.test.channels:wf-core-base:yaml:manifest:1.0.1") + .contains("org.test.channels:wf-core-second:yaml:manifest:1.0.0") + .doesNotContain("org.test.channels:wf-core-base:yaml:manifest:1.0.0"); + + // delete the base manifest + repositoryUtils.removeArtifact("org.test.channels", "wf-core-base"); + + // revert to base + final InstallationHistoryAction historyAction = new InstallationHistoryAction(outputPath, new CliConsole()); + final List revisions = historyAction.getRevisions(); + final SavedState savedState = revisions.get(revisions.size() - 1); + historyAction.rollback(savedState, mavenOptions, Collections.emptyList()); + + // verify the base manifest is available and at 1.0.0 version + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-second-1.0.0-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.1-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.1-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("artifacts.txt")) + .content() + .doesNotContain("org.test.channels:wf-core-second:yaml:manifest:1.0.0") + .doesNotContain("org.test.channels:wf-core-base:yaml:manifest:1.0.0") + .doesNotContain("org.test.channels:wf-core-base:yaml:manifest:1.0.1"); + } + + private void performUpdate() throws OperationException, ProvisioningException { + try (UpdateAction updateAction = new UpdateAction(outputPath, mavenOptions, new CliConsole(), Collections.emptyList())) { + updateAction.performUpdate(); + FileUtils.deleteQuietly(mavenSessionManager.getProvisioningRepo().toFile()); + } + } + + @Test + public void updateWithManifestNotInCacheAndNotAvailableFails() throws Exception { + // install server + installBaseServer(); + + // remove the manifest from repository and cache + repositoryUtils.removeArtifact("org.test.channels", "wf-core-base"); + Files.delete(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")); + + // update server + assertThatThrownBy(()->performUpdate()) + .isInstanceOf(UnresolvedChannelMetadataException.class) + .hasFieldOrPropertyWithValue("missingArtifacts", + Set.of(new ChannelMetadataCoordinate("org.test.channels", "wf-core-base", "", + ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION))); + } + + private void installBaseServer() throws MalformedURLException, ProvisioningException, OperationException { + // install server + installation.provision(baseProvCfg, List.of(baseChannel)); + FileUtils.deleteQuietly(mavenSessionManager.getProvisioningRepo().toFile()); + + // verify the manifest is in cache + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-second-1.0.0-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.1-manifest.yaml")) + .doesNotExist(); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("wf-core-base-1.0.0-manifest.yaml")) + .exists() + .hasSameTextualContentAs(baseManifest.toPath()); + assertThat(outputPath.resolve(ArtifactCache.CACHE_FOLDER).resolve("artifacts.txt")) + .content() + .doesNotContain("org.test.channels:wf-core-second:yaml:manifest:1.0.0") + .contains("org.test.channels:wf-core-base:yaml:manifest:1.0.0"); + } + + private void addSecondChannel() throws MetadataException { + try (MetadataAction metadataAction = new MetadataAction(outputPath)) { + metadataAction + .addChannel(new Channel.Builder() + .setName("second-channel") + .setManifestCoordinate(new ChannelManifestCoordinate("org.test.channels", "wf-core-second")) + .addRepository("test-dev-repo", updateRepository.getUrl()) + .build()); + } + } + + private ChannelManifest updateBaseChannel() throws DeploymentException, IOException { + final ChannelManifest updateManifest = updateWildflyController(); + repositoryUtils.deployArtifact(new DefaultArtifact( + "org.test.channels", + "wf-core-base", + ChannelManifest.CLASSIFIER, + ChannelManifest.EXTENSION, + "1.0.1", + null, + Files.writeString(temp.newFile("base-manifest-1.0.1").toPath(), ChannelManifestMapper.toYaml(updateManifest)).toFile() + )); + return updateManifest; + } + + private ChannelManifest updateWildflyController() throws MalformedURLException { + final ChannelManifest sourceManifest = ChannelManifestMapper.from(baseManifest.toURI().toURL()); + final Collection streams = sourceManifest.getStreams().stream() + .map(s-> { + if (s.getGroupId().equals("org.wildfly.core") && s.getArtifactId().equals("wildfly-cli")) { + return new Stream(s.getGroupId(), s.getArtifactId(), UPGRADE_VERSION); + } else { + return s; + } + }) + .collect(Collectors.toList()); + return new ChannelManifest( + sourceManifest.getSchemaVersion(), + sourceManifest.getId(), + sourceManifest.getDescription(), + sourceManifest.getManifestRequirements(), + streams); + } +} diff --git a/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java b/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java index 76c76abaa..9a565301c 100644 --- a/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java +++ b/integration-tests/src/test/java/org/wildfly/prospero/it/commonapi/WfCoreTestBase.java @@ -110,7 +110,7 @@ public static void deployUpgrade() throws Exception { .setOffline(false) .build()); final RepositorySystem system = msm.newRepositorySystem(); - final DefaultRepositorySystemSession session = msm.newRepositorySystemSession(system, false); + final DefaultRepositorySystemSession session = msm.newRepositorySystemSession(system); /* mock a wildfly-core feature pack that requires a channel resolve * the mocked artifact lives in {@code testRepo} @@ -141,7 +141,7 @@ public void setUp() throws Exception { installation = new ProvisioningAction(outputPath, mavenOptions, new CliConsole()); } - private static Artifact deployIfMissing(RepositorySystem system, DefaultRepositorySystemSession session, String groupId, String artifactId, String classifier, String extension) throws ArtifactResolutionException, DeploymentException { + protected static Artifact deployIfMissing(RepositorySystem system, DefaultRepositorySystemSession session, String groupId, String artifactId, String classifier, String extension) throws ArtifactResolutionException, DeploymentException { final ArtifactRequest artifactRequest = new ArtifactRequest(); Artifact updateCli = new DefaultArtifact(groupId, artifactId, classifier, extension, UPGRADE_VERSION); artifactRequest.setArtifact(updateCli); diff --git a/integration-tests/src/test/java/org/wildfly/prospero/it/utils/TestRepositoryUtils.java b/integration-tests/src/test/java/org/wildfly/prospero/it/utils/TestRepositoryUtils.java new file mode 100644 index 000000000..ef1ca8bbe --- /dev/null +++ b/integration-tests/src/test/java/org/wildfly/prospero/it/utils/TestRepositoryUtils.java @@ -0,0 +1,71 @@ +/* + * 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.it.utils; + +import org.apache.commons.io.FileUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.deployment.DeploymentException; +import org.eclipse.aether.repository.RemoteRepository; +import org.jboss.galleon.ProvisioningException; +import org.wildfly.prospero.api.MavenOptions; +import org.wildfly.prospero.wfchannel.MavenSessionManager; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; + +public class TestRepositoryUtils { + + private final String repositoryUrl; + private final RepositorySystem repoSystem; + private final DefaultRepositorySystemSession repoSession; + + public TestRepositoryUtils(URL repository) throws ProvisioningException { + this.repositoryUrl = repository.toExternalForm(); + final MavenSessionManager msm = new MavenSessionManager(MavenOptions.OFFLINE_NO_CACHE); + repoSystem = msm.newRepositorySystem(); + repoSession = msm.newRepositorySystemSession(repoSystem); + } + + public TestRepositoryUtils(Path repository) throws ProvisioningException, MalformedURLException { + this(repository.toUri().toURL()); + } + + public void deployArtifact(Artifact artifact) throws MalformedURLException, DeploymentException { + final DeployRequest request = new DeployRequest(); + request.addArtifact(artifact); + request.setRepository(new RemoteRepository.Builder("test-repo", "default", repositoryUrl).build()); + repoSystem.deploy(repoSession, request); + } + + public void removeArtifact(String groupId, String artifactId) throws IOException { + try { + FileUtils.deleteDirectory(Path.of(new URI(repositoryUrl)).resolve(groupId.replace('.', File.separatorChar)).resolve(artifactId).toFile()); + } catch (URISyntaxException e) { + // already validated + throw new RuntimeException(e); + } + } +} 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 2cc0efcf1..2473d2c75 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/ProsperoLogger.java @@ -366,4 +366,7 @@ public interface ProsperoLogger extends BasicLogger { @Message(id = 264, value = "Bad artifact record format in the cache descriptor, line %d: '%s'") IOException unableToReadArtifactCache(int row, String line, @Cause Exception e); + + @Message(id = 265, value = "Unable to create temporary file") + ProvisioningException unableToCreateTemporaryFile(@Cause Throwable t); } 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 2b5736c3a..fc0efd09e 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 @@ -32,9 +32,8 @@ import org.wildfly.prospero.api.exceptions.MetadataException; import org.wildfly.prospero.api.SavedState; import org.wildfly.prospero.galleon.GalleonEnvironment; -import org.wildfly.prospero.metadata.ProsperoMetadataUtils; -import org.wildfly.prospero.metadata.ManifestVersionRecord; import org.wildfly.prospero.model.ProsperoConfig; +import org.wildfly.prospero.updates.UpdateSet; import org.wildfly.prospero.wfchannel.MavenSessionManager; import org.jboss.galleon.ProvisioningException; @@ -42,7 +41,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Optional; import static org.wildfly.prospero.galleon.GalleonUtils.MAVEN_REPO_LOCAL; @@ -110,7 +108,7 @@ public void prepareRevert(SavedState savedState, MavenOptions mavenOptions, List try (GalleonEnvironment galleonEnv = GalleonEnvironment .builder(targetDir, prosperoConfig.getChannels(), mavenSessionManager) .setConsole(console) - .setRestoreManifest(revertMetadata.getManifest()) + .setRestoreManifest(revertMetadata.getManifest(), revertMetadata.getManifestVersions().orElse(null)) .setSourceServerPath(installation) .build(); PrepareCandidateAction prepareCandidateAction = new PrepareCandidateAction(installation, @@ -125,11 +123,10 @@ public void prepareRevert(SavedState savedState, MavenOptions mavenOptions, List } prepareCandidateAction.buildCandidate(targetDir, galleonEnv, - ApplyCandidateAction.Type.REVERT, provisioningConfig); + ApplyCandidateAction.Type.REVERT, provisioningConfig, + UpdateSet.EMPTY, (channels) -> revertMetadata.getManifestVersions()); } - revertCurrentVersions(targetDir, revertMetadata); - ProsperoLogger.ROOT_LOGGER.revertCandidateCompleted(targetDir); } finally { System.clearProperty(MAVEN_REPO_LOCAL); @@ -137,24 +134,6 @@ public void prepareRevert(SavedState savedState, MavenOptions mavenOptions, List } } - private static void revertCurrentVersions(Path targetDir, InstallationMetadata revertMetadata) throws MetadataException { - try { - final Optional manifestHistory = revertMetadata.getManifestVersions(); - if (manifestHistory.isPresent()) { - ProsperoMetadataUtils.writeVersionRecord( - targetDir.resolve(ProsperoMetadataUtils.METADATA_DIR).resolve(ProsperoMetadataUtils.CURRENT_VERSION_FILE), - manifestHistory.get()); - } else { - Path versionsFile = targetDir.resolve(ProsperoMetadataUtils.METADATA_DIR).resolve(ProsperoMetadataUtils.CURRENT_VERSION_FILE); - if (Files.exists(versionsFile)) { - Files.delete(versionsFile); - } - } - } catch (IOException e) { - throw ProsperoLogger.ROOT_LOGGER.unableToWriteFile(targetDir.resolve(ProsperoMetadataUtils.METADATA_DIR).resolve(ProsperoMetadataUtils.CURRENT_VERSION_FILE), e); - } - } - private static void verifyStateExists(SavedState savedState, InstallationMetadata metadata) throws MetadataException { if (metadata.getRevisions().stream().noneMatch(s->s.getName().equals(savedState.getName()))) { throw ProsperoLogger.ROOT_LOGGER.savedStateNotFound(savedState.getName()); diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/PrepareCandidateAction.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/PrepareCandidateAction.java index bcbb65565..acfb299d3 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/actions/PrepareCandidateAction.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/PrepareCandidateAction.java @@ -21,6 +21,7 @@ import org.jboss.galleon.ProvisioningException; import org.jboss.galleon.ProvisioningManager; import org.jboss.galleon.config.ProvisioningConfig; +import org.jboss.logging.Logger; import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelManifest; import org.wildfly.channel.UnresolvedMavenArtifactException; @@ -31,11 +32,11 @@ import org.wildfly.prospero.api.exceptions.ArtifactResolutionException; import org.wildfly.prospero.api.exceptions.MetadataException; import org.wildfly.prospero.api.exceptions.OperationException; +import org.wildfly.prospero.galleon.ArtifactCache; import org.wildfly.prospero.galleon.GalleonEnvironment; import org.wildfly.prospero.galleon.GalleonFeaturePackAnalyzer; import org.wildfly.prospero.galleon.GalleonUtils; import org.wildfly.prospero.metadata.ManifestVersionRecord; -import org.wildfly.prospero.metadata.ManifestVersionResolver; import org.wildfly.prospero.metadata.ProsperoMetadataUtils; import org.wildfly.prospero.model.ProsperoConfig; import org.wildfly.prospero.updates.CandidateProperties; @@ -49,10 +50,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.function.Function; -class PrepareCandidateAction implements AutoCloseable{ +class PrepareCandidateAction implements AutoCloseable { + private static final Logger LOG = Logger.getLogger(PrepareCandidateAction.class.getName()); private final InstallationMetadata metadata; private final ProsperoConfig prosperoConfig; private final MavenSessionManager mavenSessionManager; @@ -70,7 +74,8 @@ boolean buildCandidate(Path targetDir, GalleonEnvironment galleonEnv, ApplyCandi } /** - * Builds an update/revert candidate server in {@code targetDir}. + * Builds an update/revert candidate server in {@code targetDir}. Uses the manifests resolved during + * provisioning of the candidate to generate metadata. * * @param targetDir * @param galleonEnv @@ -83,7 +88,29 @@ boolean buildCandidate(Path targetDir, GalleonEnvironment galleonEnv, ApplyCandi */ boolean buildCandidate(Path targetDir, GalleonEnvironment galleonEnv, ApplyCandidateAction.Type operation, ProvisioningConfig config, UpdateSet updateSet) throws ProvisioningException, OperationException { - doBuildUpdate(targetDir, galleonEnv, config); + return this.buildCandidate(targetDir, galleonEnv, operation, config, updateSet, this::getManifestVersionRecord); + } + + /** + * Builds an update/revert candidate server in {@code targetDir}. + * + * @param targetDir + * @param galleonEnv + * @param operation + * @param config + * @param updateSet + * @param manifestVersionRecordSupplier - provides information about manifest versions that should be used to generate + * metadata and cache after provisioning + * @return + * @throws ProvisioningException + * @throws OperationException + */ + boolean buildCandidate(Path targetDir, GalleonEnvironment galleonEnv, ApplyCandidateAction.Type operation, + ProvisioningConfig config, UpdateSet updateSet, + Function, Optional> manifestVersionRecordSupplier) throws ProvisioningException, OperationException { + Objects.requireNonNull(manifestVersionRecordSupplier); + + doBuildUpdate(targetDir, galleonEnv, config, manifestVersionRecordSupplier); try { final SavedState savedState = metadata.getRevisions().get(0); @@ -96,7 +123,8 @@ boolean buildCandidate(Path targetDir, GalleonEnvironment galleonEnv, ApplyCandi return true; } - private void doBuildUpdate(Path targetDir, GalleonEnvironment galleonEnv, ProvisioningConfig provisioningConfig) + private void doBuildUpdate(Path targetDir, GalleonEnvironment galleonEnv, ProvisioningConfig provisioningConfig, + Function, Optional> manifestVersionResolver) throws ProvisioningException, OperationException { final ProvisioningManager provMgr = galleonEnv.getProvisioningManager(); try { @@ -110,33 +138,50 @@ private void doBuildUpdate(Path targetDir, GalleonEnvironment galleonEnv, Provis e.getAttemptedRepositories(), mavenSessionManager.isOffline()); } - try { - final ManifestVersionRecord manifestRecord = - new ManifestVersionResolver(mavenSessionManager.getProvisioningRepo(), mavenSessionManager.newRepositorySystem()) - .getCurrentVersions(galleonEnv.getChannels()); - writeProsperoMetadata(targetDir, galleonEnv.getChannelSession().getRecordedChannel(), prosperoConfig.getChannels(), - manifestRecord); - } catch (IOException ex) { - throw ProsperoLogger.ROOT_LOGGER.unableToDownloadFile(ex); + + final Optional manifestRecord = manifestVersionResolver.apply(galleonEnv.getChannels()); + + if (LOG.isTraceEnabled()) { + LOG.tracef("Recording manifests: %s", manifestRecord.orElse(new ManifestVersionRecord())); } + manifestRecord.ifPresent(rec -> cacheManifests(rec, targetDir)); + writeProsperoMetadata(targetDir, galleonEnv.getChannelSession().getRecordedChannel(), prosperoConfig.getChannels(), + manifestRecord); try { final GalleonFeaturePackAnalyzer galleonFeaturePackAnalyzer = new GalleonFeaturePackAnalyzer(galleonEnv.getChannels(), mavenSessionManager); - galleonFeaturePackAnalyzer.cacheGalleonArtifacts(targetDir, provisioningConfig); } catch (Exception e) { throw new RuntimeException(e); } } + private Optional getManifestVersionRecord(List channels) { + final ProsperoManifestVersionResolver manifestResolver = new ProsperoManifestVersionResolver(mavenSessionManager); + try { + return Optional.of(manifestResolver.getCurrentVersions(channels)); + } catch (IOException e) { + ProsperoLogger.ROOT_LOGGER.debug("Unable to retrieve current manifest versions", e); + return Optional.empty(); + } + } + + private void cacheManifests(ManifestVersionRecord manifestRecord, Path installDir) { + try { + ArtifactCache.getInstance(installDir).cache(manifestRecord, mavenSessionManager.getResolvedArtifactVersions()); + } catch (IOException e) { + ProsperoLogger.ROOT_LOGGER.debug("Unable to record manifests in the internal cache", e); + } + } + @Override public void close() { metadata.close(); } - private void writeProsperoMetadata(Path home, ChannelManifest manifest, List channels, ManifestVersionRecord manifestVersions) throws MetadataException { + private void writeProsperoMetadata(Path home, ChannelManifest manifest, List channels, Optional manifestVersions) throws MetadataException { try (InstallationMetadata installationMetadata = InstallationMetadata.newInstallation(home, manifest, - new ProsperoConfig(channels), Optional.of(manifestVersions))) { + new ProsperoConfig(channels), manifestVersions)) { installationMetadata.recordProvision(true, false); } } @@ -158,6 +203,5 @@ private void writeCandidateProperties(UpdateSet updateSet, Path installationDir) } catch (IOException e) { ProsperoLogger.ROOT_LOGGER.unableToWriteChannelNamesToFile(candidateFile.toFile().getAbsolutePath(),e); } - } } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/actions/ProsperoManifestVersionResolver.java b/prospero-common/src/main/java/org/wildfly/prospero/actions/ProsperoManifestVersionResolver.java new file mode 100644 index 000000000..190d9f8e5 --- /dev/null +++ b/prospero-common/src/main/java/org/wildfly/prospero/actions/ProsperoManifestVersionResolver.java @@ -0,0 +1,112 @@ +/* + * 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.actions; + +import org.jboss.logging.Logger; +import org.wildfly.channel.Channel; +import org.wildfly.channel.ChannelManifestMapper; +import org.wildfly.channel.MavenArtifact; +import org.wildfly.channel.MavenCoordinate; +import org.wildfly.prospero.metadata.ManifestVersionRecord; +import org.wildfly.prospero.metadata.ManifestVersionResolver; +import org.wildfly.prospero.wfchannel.MavenSessionManager; +import org.wildfly.prospero.wfchannel.ResolvedArtifactsStore; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * Identifies manifests used to provision installation. + * It uses {@code ResolvedManifestVersions} to find already resolved manifests and falls back onto + * {@code ManifestVersionResolver} if not possible. + */ +class ProsperoManifestVersionResolver { + + private static final Logger LOG = Logger.getLogger(ProsperoManifestVersionResolver.class.getName()); + + private final ResolvedArtifactsStore manifestVersions; + + private final Supplier manifestVersionResolver; + + ProsperoManifestVersionResolver(MavenSessionManager mavenSessionManager) { + this.manifestVersions = mavenSessionManager.getResolvedArtifactVersions(); + this.manifestVersionResolver = () -> new ManifestVersionResolver( + mavenSessionManager.getProvisioningRepo(), + mavenSessionManager.newRepositorySystem()); + } + + ProsperoManifestVersionResolver(ResolvedArtifactsStore manifestVersions, ManifestVersionResolver manifestVersionResolver) { + this.manifestVersions = manifestVersions; + this.manifestVersionResolver = () -> manifestVersionResolver; + } + + /** + * attempt to resolve the current versions from artifacts recorded during provisioning. + * Fallback on artifacts in the local maven cache if not available. + * + * @param channels + * @return + * @throws IOException + */ + public ManifestVersionRecord getCurrentVersions(List channels) throws IOException { + final ManifestVersionRecord record = new ManifestVersionRecord(); + final ArrayList fallbackChannels = new ArrayList<>(); + for (Channel channel : channels) { + if (channel.getManifestCoordinate().getMaven() != null && channel.getManifestCoordinate().getMaven().getVersion() == null) { + final MavenCoordinate manifestCoord = channel.getManifestCoordinate().getMaven(); + if (LOG.isDebugEnabled()) { + LOG.debugf("Trying to lookup manifest %s", manifestCoord); + } + final MavenArtifact version = manifestVersions.getManifestVersion(manifestCoord.getGroupId(), manifestCoord.getArtifactId()); + if (version == null) { + if (LOG.isDebugEnabled()) { + LOG.debugf("Failed to lookup manifest %s in currently resolved artifacts", manifestCoord); + } + fallbackChannels.add(channel); + } else { + if (LOG.isDebugEnabled()) { + LOG.debugf("Manifest %s resolved in currently resolve artifacts, recording.", manifestCoord); + } + final String description = ChannelManifestMapper.from(version.getFile().toURI().toURL()).getDescription(); + record.addManifest(new ManifestVersionRecord.MavenManifest( + manifestCoord.getGroupId(), + manifestCoord.getArtifactId(), + version.getVersion(), + description)); + } + } else { + if (LOG.isDebugEnabled()) { + LOG.debugf("Manifest for channel %s will be resolved via fallback.", channel.getName()); + } + fallbackChannels.add(channel); + } + } + + if (LOG.isDebugEnabled()) { + LOG.debugf("Resolving channel manifests using fallback mechanisms."); + } + final ManifestVersionRecord currentVersions = manifestVersionResolver.get().getCurrentVersions(fallbackChannels); + currentVersions.getMavenManifests().forEach(record::addManifest); + currentVersions.getOpenManifests().forEach(record::addManifest); + currentVersions.getUrlManifests().forEach(record::addManifest); + + return record; + } +} 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 b7c9dacab..1e76029e2 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 @@ -36,6 +36,7 @@ import org.wildfly.prospero.api.exceptions.MetadataException; import org.wildfly.prospero.api.exceptions.OperationException; import org.wildfly.prospero.api.exceptions.StreamNotFoundException; +import org.wildfly.prospero.galleon.ArtifactCache; import org.wildfly.prospero.galleon.GalleonFeaturePackAnalyzer; import org.wildfly.prospero.galleon.GalleonEnvironment; import org.wildfly.prospero.galleon.GalleonUtils; @@ -146,6 +147,9 @@ public void provision(ProvisioningConfig provisioningConfig, List chann } catch (IOException e) { throw ProsperoLogger.ROOT_LOGGER.unableToDownloadFile(e); } + + cacheManifests(manifestRecord); + if (ProsperoLogger.ROOT_LOGGER.isDebugEnabled()) { ProsperoLogger.ROOT_LOGGER.debug("Recording installed metadata"); } @@ -184,6 +188,14 @@ public void provision(ProvisioningConfig provisioningConfig, List chann ProsperoLogger.ROOT_LOGGER.provisioningComplete(installDir); } + private void cacheManifests(ManifestVersionRecord manifestRecord) { + try { + ArtifactCache.getInstance(installDir).cache(manifestRecord, mavenSessionManager.getResolvedArtifactVersions()); + } catch (IOException e) { + ProsperoLogger.ROOT_LOGGER.debug("Unable to record manifests in the internal cache", e); + } + } + /** * List agreements and licenses that need to be accepted before installing the required Feature Packs. * diff --git a/prospero-common/src/main/java/org/wildfly/prospero/galleon/ArtifactCache.java b/prospero-common/src/main/java/org/wildfly/prospero/galleon/ArtifactCache.java index fafefdb4c..3a274fcbe 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/galleon/ArtifactCache.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/galleon/ArtifactCache.java @@ -21,9 +21,12 @@ import org.jboss.galleon.util.HashUtils; import org.jboss.galleon.util.IoUtils; import org.jboss.logging.Logger; +import org.wildfly.channel.ChannelManifest; import org.wildfly.channel.MavenArtifact; import org.wildfly.prospero.ProsperoLogger; +import org.wildfly.prospero.metadata.ManifestVersionRecord; import org.wildfly.prospero.metadata.ProsperoMetadataUtils; +import org.wildfly.prospero.wfchannel.ResolvedArtifactsStore; import java.io.BufferedWriter; import java.io.File; @@ -36,6 +39,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -175,6 +179,41 @@ public void cache(MavenArtifact artifact) throws IOException { record(artifact, cacheDir.resolve(artifact.getFile().getName())); } + /** + * detects and caches the manifests from {@code manifestRecord} in {@code CACHE_FOLDER}. + * The version and content of the manifest is resolved using {@code resolvedArtifacts}. + * NOTE: only manifests identified by maven coordinates are cached. + * + * @param manifestRecord - record containing all manifest used in installation. + * @param resolvedArtifacts - artifacts resolved during provisioning. + * @throws IOException + */ + public void cache(ManifestVersionRecord manifestRecord, ResolvedArtifactsStore resolvedArtifacts) throws IOException { + Objects.requireNonNull(manifestRecord); + Objects.requireNonNull(resolvedArtifacts); + + for (ManifestVersionRecord.MavenManifest manifest : manifestRecord.getMavenManifests()) { + final MavenArtifact record = resolvedArtifacts.getManifestVersion(manifest.getGroupId(), manifest.getArtifactId()); + if (record != null && record.getVersion().equals(manifest.getVersion())) { + if (LOG.isDebugEnabled()) { + LOG.debugf("Adding manifest %s to the cache", record); + } + final File cachedManifest = record.getFile(); + + if (cachedManifest.exists()) { + cache(new MavenArtifact( + manifest.getGroupId(), + manifest.getArtifactId(), + ChannelManifest.EXTENSION, + ChannelManifest.CLASSIFIER, + manifest.getVersion(), + cachedManifest + )); + } + } + } + } + private static String getCacheFileKey(MavenArtifact artifact) { final org.jboss.galleon.universe.maven.MavenArtifact galleonArtifact = new org.jboss.galleon.universe.maven.MavenArtifact(); galleonArtifact.setGroupId(artifact.getGroupId()); diff --git a/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolver.java b/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolver.java index b30d19048..60ee359e7 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolver.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolver.java @@ -17,6 +17,9 @@ package org.wildfly.prospero.galleon; +import org.eclipse.aether.AbstractRepositoryListener; +import org.eclipse.aether.RepositoryEvent; +import org.eclipse.aether.RepositoryListener; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.DefaultArtifact; @@ -24,13 +27,16 @@ import org.eclipse.aether.installation.InstallationException; import org.jboss.logging.Logger; import org.wildfly.channel.ArtifactCoordinate; +import org.wildfly.channel.ArtifactTransferException; import org.wildfly.channel.ChannelMetadataCoordinate; import org.wildfly.channel.UnresolvedMavenArtifactException; import org.wildfly.channel.spi.MavenVersionsResolver; import java.io.File; +import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Set; @@ -44,18 +50,32 @@ * Installs locally resolved artifacts in LRM to allow galleon to start thin servers. */ public class CachedVersionResolver implements MavenVersionsResolver { + private static final Logger LOG = Logger.getLogger(CachedVersionResolver.class.getName()); + + private static final RepositoryListener NOOP_REPOSITORY_LISTENER = new AbstractRepositoryListener(){}; private final MavenVersionsResolver fallbackResolver; private final RepositorySystem system; private final RepositorySystemSession session; private final ArtifactCache artifactCache; private final Logger log = Logger.getLogger(CachedVersionResolver.class); + private final RepositoryListener listener; + private final Function manifestVersionProvider; + @Deprecated public CachedVersionResolver(MavenVersionsResolver fallbackResolver, ArtifactCache cache, RepositorySystem system, RepositorySystemSession session) { + this(fallbackResolver, cache, system, session, (a)->null); + } + + public CachedVersionResolver(MavenVersionsResolver fallbackResolver, ArtifactCache cache, RepositorySystem system, + RepositorySystemSession session, + Function manifestVersionProvider) { this.fallbackResolver = fallbackResolver; this.system = system; this.session = session; this.artifactCache = cache; + this.manifestVersionProvider = manifestVersionProvider; + this.listener = session.getRepositoryListener() != null ? session.getRepositoryListener() : NOOP_REPOSITORY_LISTENER; } @Override @@ -110,7 +130,78 @@ public List resolveArtifacts(List coordinates) throws @Override public List resolveChannelMetadata(List manifestCoords) throws UnresolvedMavenArtifactException { - return fallbackResolver.resolveChannelMetadata(manifestCoords); + try { + return fallbackResolver.resolveChannelMetadata(manifestCoords); + } catch (ArtifactTransferException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Failed to resolve manifests, attempting to fall back to the cache."); + } + final URL[] cachedMetadata = new URL[manifestCoords.size()]; + for (ArtifactCoordinate a : e.getUnresolvedArtifacts()) { + // get version from manifest_versions to verify this is the latest version + final String version = manifestVersionProvider.apply(a); + if (LOG.isDebugEnabled()) { + LOG.debugf("Last used version for manifest %s is %s.", a, version); + } + + if (version == null) { + // we can't use cache, just throw the resolution exception + throw e; + } + + final Optional artifact = artifactCache.getArtifact( + a.getGroupId(), + a.getArtifactId(), + a.getExtension(), + a.getClassifier(), + version + ); + + + if (artifact.isPresent()) { + if (LOG.isDebugEnabled()) { + LOG.debugf("Found cached manifest for %s.", a); + } + log.warnf("Unable to resolve manifest for channel %s, no updates will be resolved for this channel.", a); + this.listener.artifactResolved(new RepositoryEvent.Builder(session, RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .setArtifact(new DefaultArtifact( + a.getGroupId(), + a.getArtifactId(), + a.getClassifier(), + a.getExtension(), + version, + null, + artifact.get() + )) + .build()); + try { + // maintain order as in manifestCoords + for (int i = 0; i < manifestCoords.size(); i++) { + final ChannelMetadataCoordinate coord = manifestCoords.get(i); + if (coord.getGroupId().equals(a.getGroupId()) && coord.getArtifactId().equals(a.getArtifactId()) && + coord.getClassifier().equals(a.getClassifier()) && coord.getExtension().equals(a.getExtension())) { + cachedMetadata[i] = artifact.get().toURI().toURL(); + break; + } + } + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + } else { + throw e; + } + } + for (int i = 0; i < cachedMetadata.length; i++) { + // retry urls that were before resolved + if (cachedMetadata[i] == null) { + if (LOG.isDebugEnabled()) { + LOG.debugf("Retrying resolution of manifest %s.", cachedMetadata[i]); + } + cachedMetadata[i] = fallbackResolver.resolveChannelMetadata(List.of(manifestCoords.get(i))).get(0); + } + } + return Arrays.asList(cachedMetadata); + } } @Override diff --git a/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolverFactory.java b/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolverFactory.java index fd90b9730..93fb807b6 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolverFactory.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/galleon/CachedVersionResolverFactory.java @@ -19,13 +19,18 @@ import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; +import org.wildfly.channel.ArtifactCoordinate; import org.wildfly.channel.Repository; import org.wildfly.channel.maven.VersionResolverFactory; import org.wildfly.channel.spi.MavenVersionsResolver; +import org.wildfly.prospero.metadata.ManifestVersionRecord; +import org.wildfly.prospero.metadata.ProsperoMetadataUtils; import java.io.IOException; import java.nio.file.Path; import java.util.Collection; +import java.util.List; +import java.util.Optional; public class CachedVersionResolverFactory implements MavenVersionsResolver.Factory { @@ -33,17 +38,40 @@ public class CachedVersionResolverFactory implements MavenVersionsResolver.Facto private final RepositorySystem system; private final DefaultRepositorySystemSession session; private final ArtifactCache artifactCache; + private final Path installDir; public CachedVersionResolverFactory(VersionResolverFactory factory, Path installDir, RepositorySystem system, DefaultRepositorySystemSession session) throws IOException { this.factory = factory; this.system = system; this.session = session; this.artifactCache = ArtifactCache.getInstance(installDir); + this.installDir = installDir; } @Override public MavenVersionsResolver create(Collection repositories) { - return new CachedVersionResolver(factory.create(repositories), artifactCache, system, session); + return new CachedVersionResolver(factory.create(repositories), artifactCache, system, session, + (a)->getCurrentManifestVersion(a, installDir.resolve(ProsperoMetadataUtils.METADATA_DIR).resolve(ProsperoMetadataUtils.CURRENT_VERSION_FILE))); + } + + private static String getCurrentManifestVersion(ArtifactCoordinate a, Path manifestVersionRecord) { + String version = null; + try { + final Optional read = ManifestVersionRecord.read(manifestVersionRecord); + if (read.isPresent()) { + final List manifests = read.get().getMavenManifests(); + for (ManifestVersionRecord.MavenManifest manifest : manifests) { + if (manifest.getGroupId().equals(a.getGroupId()) && manifest.getArtifactId().equals(a.getArtifactId())) { + version = manifest.getVersion(); + break; + } + } + } + } catch (IOException ex) { + // TODO: log and do not use cache + throw new RuntimeException(ex); + } + return version; } } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonEnvironment.java b/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonEnvironment.java index ac36bf51d..8af989efd 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonEnvironment.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonEnvironment.java @@ -17,6 +17,8 @@ package org.wildfly.prospero.galleon; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.jboss.galleon.Constants; @@ -24,8 +26,12 @@ import org.jboss.galleon.ProvisioningManager; import org.jboss.galleon.layout.ProvisioningLayoutFactory; import org.jboss.galleon.universe.maven.repo.MavenRepoManager; +import org.jboss.logging.Logger; +import org.wildfly.channel.ArtifactTransferException; import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestCoordinate; +import org.wildfly.channel.ChannelManifestMapper; import org.wildfly.channel.ChannelMetadataCoordinate; import org.wildfly.channel.ChannelSession; import org.wildfly.channel.InvalidChannelMetadataException; @@ -38,12 +44,14 @@ import org.wildfly.prospero.api.exceptions.MetadataException; import org.wildfly.prospero.api.exceptions.UnresolvedChannelMetadataException; import org.wildfly.prospero.api.exceptions.OperationException; +import org.wildfly.prospero.metadata.ManifestVersionRecord; import org.wildfly.prospero.wfchannel.MavenSessionManager; import java.io.FileNotFoundException; 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; @@ -57,6 +65,7 @@ import java.util.stream.Stream; public class GalleonEnvironment implements AutoCloseable { + private static final Logger LOG = Logger.getLogger(GalleonEnvironment.class.getName()); public static final String TRACK_JBMODULES = "JBMODULES"; public static final String TRACK_JBEXAMPLES = "JBEXTRACONFIGS"; @@ -67,13 +76,21 @@ public class GalleonEnvironment implements AutoCloseable { private final MavenRepoManager repositoryManager; private final ChannelSession channelSession; private final List channels; + private Path restoreManifestPath = null; private boolean resetGalleonLineEndings = true; private GalleonEnvironment(Builder builder) throws ProvisioningException, MetadataException, ChannelDefinitionException, UnresolvedChannelMetadataException { Optional console = Optional.ofNullable(builder.console); Optional restoreManifest = Optional.ofNullable(builder.manifest); - channels = builder.channels; + if (restoreManifest.isPresent()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Replacing channel manifests with restore manifest"); + } + channels = replaceManifestWithRestoreManifests(builder, restoreManifest); + } else { + channels = builder.channels; + } List substitutedChannels = new ArrayList<>(); final ChannelManifestSubstitutor substitutor = new ChannelManifestSubstitutor(Map.of("installation.home", builder.installDir.toString())); // substitute any properties found in URL of ChannelManifestCoordinate. @@ -91,8 +108,17 @@ private GalleonEnvironment(Builder builder) throws ProvisioningException, Metada ProsperoLogger.ROOT_LOGGER.debug("Unable to read artifact cache, falling back to Maven resolver.", e); factory = new VersionResolverFactory(system, session, MavenProxyHandler::addProxySettings); } + channelSession = initChannelSession(session, factory); + if (restoreManifest.isPresent()) { + // try to load the manifests used by the state that's being reverted to + // they have to be in the maven cache for later version resolution + final ManifestVersionRecord manifestVersions = new ManifestVersionRecord("1.0.0", + builder.restoredManifestVersions, Collections.emptyList(), Collections.emptyList()); + populateMavenCacheWithManifests(manifestVersions.getMavenManifests(), channelSession); + } + if (builder.artifactDirectResolve) { repositoryManager = new MavenArtifactDirectResolverRepositoryManager(channelSession); } else { @@ -124,6 +150,33 @@ private GalleonEnvironment(Builder builder) throws ProvisioningException, Metada layoutFactory.setProgressCallback(TRACK_JB_ARTIFACTS_RESOLVE, callback); } + private List replaceManifestWithRestoreManifests(Builder builder, Optional restoreManifest) throws ProvisioningException { + ChannelManifestCoordinate manifestCoord; + try { + restoreManifestPath = Files.createTempFile("prospero-restore-manifest", "yaml"); + restoreManifestPath.toFile().deleteOnExit(); + if (LOG.isDebugEnabled()) { + LOG.debugf("Created temporary restore manifest file at %s", restoreManifestPath); + } + Files.writeString(restoreManifestPath, ChannelManifestMapper.toYaml(restoreManifest.get())); + manifestCoord = new ChannelManifestCoordinate(restoreManifestPath.toUri().toURL()); + } catch (IOException e) { + throw ProsperoLogger.ROOT_LOGGER.unableToCreateTemporaryFile(e); + } + List channels = builder.channels.stream() + .map(c-> new Channel( + c.getSchemaVersion(), + c.getName(), + c.getDescription(), + c.getVendor(), + c.getRepositories(), + manifestCoord, + c.getBlocklistCoordinate(), + c.getNoStreamStrategy())) + .collect(Collectors.toList()); + return channels; + } + private ChannelSession initChannelSession(DefaultRepositorySystemSession session, MavenVersionsResolver.Factory factory) throws UnresolvedChannelMetadataException, ChannelDefinitionException { final ChannelSession channelSession; try { @@ -171,9 +224,44 @@ public void close() { if (resetGalleonLineEndings) { System.clearProperty(Constants.PROP_LINUX_LINE_ENDINGS); } + if (restoreManifestPath != null) { + FileUtils.deleteQuietly(restoreManifestPath.toFile()); + } provisioningManager.close(); } + /* + * Attempts to populate current's {@code channelSession} maven cache with the maven manifest artifacts. + * Used when the provisioning is by-passing the manifest resolution (e.g. during revert when it's using old + * provisioned manifest) + */ + static void populateMavenCacheWithManifests(List mavenManifests, ChannelSession channelSession) { + Objects.requireNonNull(mavenManifests); + Objects.requireNonNull(channelSession); + + if (LOG.isDebugEnabled()) { + LOG.debugf("Resolving manifests %s to prepopulate maven cache.", StringUtils.join(mavenManifests, ",")); + } + + for (ManifestVersionRecord.MavenManifest mavenManifest : mavenManifests) { + try { + if (LOG.isTraceEnabled()) { + LOG.tracef("Trying to resolve %s.", mavenManifest); + } + channelSession.resolveDirectMavenArtifact( + mavenManifest.getGroupId(), + mavenManifest.getArtifactId(), + ChannelManifest.EXTENSION, + ChannelManifest.CLASSIFIER, + mavenManifest.getVersion() + ); + } catch (ArtifactTransferException e) { + ProsperoLogger.ROOT_LOGGER.debugf(e, "Unable to resolve manifest %s:%s:%s for maven cache", + mavenManifest.getGroupId(), mavenManifest.getArtifactId(), mavenManifest.getVersion()); + } + } + } + public static Builder builder(Path installDir, List channels, MavenSessionManager mavenSessionManager) { Objects.requireNonNull(installDir); Objects.requireNonNull(channels); @@ -192,6 +280,7 @@ public static class Builder { private Consumer fpTracker; private Path sourceServerPath; private boolean artifactDirectResolve; + private List restoredManifestVersions; private Builder(Path installDir, List channels, MavenSessionManager mavenSessionManager) { this.installDir = installDir; @@ -209,6 +298,19 @@ public Builder setRestoreManifest(ChannelManifest manifest) { return this; } + /** + * use the {@code manifest} to resolve artifact versions instead of defined channels. If {@code manifestVersions} + * is provided, they will be pre-loaded for maven cache. + * + * @param manifest + * @param manifestVersions + * @return + */ + public Builder setRestoreManifest(ChannelManifest manifest, ManifestVersionRecord manifestVersions) { + this.restoredManifestVersions = manifestVersions == null ? Collections.emptyList() : manifestVersions.getMavenManifests(); + return this.setRestoreManifest(manifest); + } + public Builder setResolvedFpTracker(Consumer fpTracker) { this.fpTracker = fpTracker; return this; diff --git a/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonFeaturePackAnalyzer.java b/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonFeaturePackAnalyzer.java index 6ac529534..4ceb0438a 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonFeaturePackAnalyzer.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/galleon/GalleonFeaturePackAnalyzer.java @@ -73,7 +73,7 @@ public void cacheGalleonArtifacts(Path installedDir, ProvisioningConfig provisio ProvisioningLayoutFactory layoutFactory = null; ProvisioningLayout layout = null; try { - galleonEnv = galleonEnvWithFpMapper(tempInstallationPath, fps); + galleonEnv = galleonEnvWithFpMapper(tempInstallationPath, installedDir, fps); final ProvisioningManager pm = galleonEnv.getProvisioningManager(); @@ -154,7 +154,7 @@ public Set getFeaturePacks(ProvisioningConfig provisioningConfig) throws // no data will be actually written out, but we need a path to init the Galleon final Path tempInstallationPath = Files.createTempDirectory("temp"); final Set fps = new HashSet<>(); - try (GalleonEnvironment galleonEnv = galleonEnvWithFpMapper(tempInstallationPath, fps)) { + try (GalleonEnvironment galleonEnv = galleonEnvWithFpMapper(tempInstallationPath, tempInstallationPath, fps)) { final ProvisioningManager pm = galleonEnv.getProvisioningManager(); final ProvisioningLayoutFactory layoutFactory = pm.getLayoutFactory(); @@ -165,10 +165,11 @@ public Set getFeaturePacks(ProvisioningConfig provisioningConfig) throws } } - private GalleonEnvironment galleonEnvWithFpMapper(Path tempInstallationPath, Set fps) throws ProvisioningException, OperationException { + private GalleonEnvironment galleonEnvWithFpMapper(Path tempInstallationPath, Path sourcePath, Set fps) throws ProvisioningException, OperationException { final GalleonEnvironment galleonEnv = GalleonEnvironment .builder(tempInstallationPath, channels, mavenSessionManager) .setConsole(null) + .setSourceServerPath(sourcePath) .setResolvedFpTracker(fps::add) .build(); return galleonEnv; diff --git a/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java b/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java index d4820d329..ceed66d34 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/updates/UpdateSet.java @@ -19,10 +19,12 @@ import org.wildfly.prospero.api.ArtifactChange; +import java.util.Collections; import java.util.List; public class UpdateSet { + public static final UpdateSet EMPTY = new UpdateSet(Collections.emptyList()); private final List artifactUpdates; public UpdateSet(List updates) { diff --git a/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/MavenSessionManager.java b/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/MavenSessionManager.java index 33b9722ad..76c3ebef0 100644 --- a/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/MavenSessionManager.java +++ b/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/MavenSessionManager.java @@ -51,6 +51,7 @@ public class MavenSessionManager { private static final String AETHER_OFFLINE_PROTOCOLS_PROPERTY = "aether.offline.protocols"; public static final String AETHER_OFFLINE_PROTOCOLS_VALUE = "file"; private final Path provisioningRepo; + private final ProsperoMavenRepositoryListener repositoryListener = new ProsperoMavenRepositoryListener(); private boolean offline; public MavenSessionManager(MavenOptions mavenOptions) throws ProvisioningException { @@ -106,6 +107,7 @@ public DefaultRepositorySystemSession newRepositorySystemSession(RepositorySyste return newRepositorySystemSession(system, false); } + @Deprecated public DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system, boolean resolveLocalCache) { DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); @@ -119,6 +121,8 @@ public DefaultRepositorySystemSession newRepositorySystemSession(RepositorySyste LocalRepository localRepo = new LocalRepository(location.toFile()); if (resolveLocalCache) { copyResolvedArtifactsToProvisiongRepository(session); + } else { + session.setRepositoryListener(repositoryListener); } session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)); session.setOffline(offline); @@ -131,10 +135,12 @@ private void copyResolvedArtifactsToProvisiongRepository(DefaultRepositorySystem // install the artifact into a temporary provisioningRepo. The provisioningRepo then is used // by Galleon to start thin server. final RepositorySystem localCacheBuilder = newRepositorySystem(); - final DefaultRepositorySystemSession localCacheBuilderSession = newRepositorySystemSession(localCacheBuilder, false); + final DefaultRepositorySystemSession localCacheBuilderSession = newRepositorySystemSession(localCacheBuilder); session.setRepositoryListener(new AbstractRepositoryListener() { @Override public void artifactResolved(RepositoryEvent event) { + repositoryListener.artifactResolved(event); + if (event.getFile() == null || event.getRepository() instanceof LocalRepository) { return; } @@ -161,4 +167,13 @@ public void setOffline(boolean offline) { public boolean isOffline() { return offline; } + + /** + * returns a {@code ResolvedArtifactsStore} containing artifacts resolved during that maven session. + * + * @return + */ + public ResolvedArtifactsStore getResolvedArtifactVersions() { + return repositoryListener; + } } diff --git a/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/ProsperoMavenRepositoryListener.java b/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/ProsperoMavenRepositoryListener.java new file mode 100644 index 000000000..2ea5f9900 --- /dev/null +++ b/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/ProsperoMavenRepositoryListener.java @@ -0,0 +1,69 @@ +/* + * 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.wfchannel; + +import org.eclipse.aether.AbstractRepositoryListener; +import org.eclipse.aether.RepositoryEvent; +import org.eclipse.aether.artifact.Artifact; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.MavenArtifact; + +import java.util.HashMap; +import java.util.Map; + +/** + * listener called every time an artifact is resolved by Maven. Keeps track of artifacts resolved by Maven + */ +class ProsperoMavenRepositoryListener extends AbstractRepositoryListener implements ResolvedArtifactsStore { + + private final Map manifestVersions = new HashMap<>(); + + @Override + public MavenArtifact getManifestVersion(String groupId, String artifactId) { + return manifestVersions.get(getKey(groupId, artifactId, ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION)); + } + + @Override + public void artifactResolved(RepositoryEvent event) { + final Artifact a = event.getArtifact(); + + if (a == null || a.getFile() == null) { + return; + } + + if (a.getClassifier() != null && a.getClassifier().equals(ChannelManifest.CLASSIFIER)) { + manifestVersions.put(getKey(a), + new MavenArtifact(a.getGroupId(), a.getArtifactId(), a.getExtension(), a.getClassifier(), a.getVersion(), a.getFile())); + } + } + + private static String getKey(Artifact a) { + return getKey(a.getGroupId(), a.getArtifactId(), a.getClassifier(), a.getExtension()); + } + + private static String getKey(String groupId, String artifactId, String classifier, String extension) { + return String.format("%s:%s:%s:%s", groupId, artifactId, nullable(classifier), nullable(extension)); + } + + private static String nullable(String txt) { + if (txt == null) { + return ""; + } + return txt; + } +} diff --git a/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/ResolvedArtifactsStore.java b/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/ResolvedArtifactsStore.java new file mode 100644 index 000000000..298155705 --- /dev/null +++ b/prospero-common/src/main/java/org/wildfly/prospero/wfchannel/ResolvedArtifactsStore.java @@ -0,0 +1,35 @@ +/* + * 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.wfchannel; + +import org.wildfly.channel.MavenArtifact; + +/** + * a collection of artifacts resolved in the maven session + */ +public interface ResolvedArtifactsStore { + + /** + * queries the store to find a manifest resolved in this maven session matching the {@code GA} + * + * @param groupId - the {@code groupId} of the manifest + * @param artifactId - the {@code artifactId} of the manifest + * @return - {@code MavenArtifact} representing the resolved manifest or {@code null} if the manifest has not been resolved + */ + MavenArtifact getManifestVersion(String groupId, String artifactId); +} diff --git a/prospero-common/src/test/java/org/wildfly/prospero/actions/ProsperoManifestVersionResolverTest.java b/prospero-common/src/test/java/org/wildfly/prospero/actions/ProsperoManifestVersionResolverTest.java new file mode 100644 index 000000000..88000f561 --- /dev/null +++ b/prospero-common/src/test/java/org/wildfly/prospero/actions/ProsperoManifestVersionResolverTest.java @@ -0,0 +1,140 @@ +/* + * 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.actions; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.wildfly.channel.Channel; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestCoordinate; +import org.wildfly.channel.ChannelManifestMapper; +import org.wildfly.channel.MavenArtifact; +import org.wildfly.prospero.metadata.ManifestVersionRecord; +import org.wildfly.prospero.metadata.ManifestVersionResolver; +import org.wildfly.prospero.wfchannel.ResolvedArtifactsStore; + +import java.io.File; +import java.net.URI; +import java.nio.file.Files; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.class) +public class ProsperoManifestVersionResolverTest { + protected static final String A_GROUP_ID = "org.test"; + protected static final String MANIFEST_ONE_ARTIFACT_ID = "manifest-one"; + protected static final String MANIFEST_TWO_ARTIFACT_ID = "manifest-two"; + protected static final String A_VERSION = "1.2.3"; + @Mock + public ResolvedArtifactsStore artifactVersions; + + @Mock + public ManifestVersionResolver manifestVersionResolver; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + private File manifestFile; + private ProsperoManifestVersionResolver resolver; + + @Before + public void setUp() throws Exception { + manifestFile = temp.newFile("test"); + Files.writeString(manifestFile.toPath(), ChannelManifestMapper.toYaml( + new ChannelManifest("test", "test", "desc", null))); + + // when the fallback resolver is called return an empty record to make the test pass + when(manifestVersionResolver.getCurrentVersions(any())).thenReturn(new ManifestVersionRecord()); + + resolver = new ProsperoManifestVersionResolver(artifactVersions, manifestVersionResolver); + } + + @Test + public void versionResolvedDuringProvisioning() throws Exception { + whenManifestOneWasResolveDuringProvisioning(); + + final ManifestVersionRecord currentVersions = resolver.getCurrentVersions(List.of(new Channel.Builder() + .setManifestCoordinate(new ChannelManifestCoordinate(A_GROUP_ID, MANIFEST_ONE_ARTIFACT_ID)) + .build())); + + assertThat(currentVersions.getMavenManifests()) + .map(ManifestVersionRecord.MavenManifest::getVersion) + .containsOnly(A_VERSION); + assertThat(currentVersions.getMavenManifests()) + .map(ManifestVersionRecord.MavenManifest::getDescription) + .containsOnly("desc"); + } + + @Test + public void versionNotResolvedDuringProvisioning_FallsbackToMavenCache() throws Exception { + when(artifactVersions.getManifestVersion(A_GROUP_ID, MANIFEST_ONE_ARTIFACT_ID)).thenReturn(null); + + final List channels = List.of(new Channel.Builder() + .setManifestCoordinate(new ChannelManifestCoordinate(A_GROUP_ID, MANIFEST_ONE_ARTIFACT_ID)) + .build()); + resolver.getCurrentVersions(channels); + + verify(manifestVersionResolver).getCurrentVersions(channels); + } + + @Test + public void alreadyResolvedChannelsAreNotPassedThrough() throws Exception { + whenManifestOneWasResolveDuringProvisioning(); + + final List channels = List.of( + new Channel.Builder() + .setManifestCoordinate(new ChannelManifestCoordinate(A_GROUP_ID, MANIFEST_ONE_ARTIFACT_ID)) + .build(), + new Channel.Builder() + .setManifestCoordinate(new ChannelManifestCoordinate(A_GROUP_ID, MANIFEST_TWO_ARTIFACT_ID)) + .build()); + resolver.getCurrentVersions(channels); + + verify(manifestVersionResolver).getCurrentVersions(List.of(channels.get(1))); + } + + @Test + public void nonOpenMavenChannelsAreResolvedWithFallback() throws Exception { + whenManifestOneWasResolveDuringProvisioning(); + + final List channels = List.of( + new Channel.Builder() + .setManifestCoordinate(new ChannelManifestCoordinate(A_GROUP_ID, MANIFEST_ONE_ARTIFACT_ID, A_VERSION)) + .build(), + new Channel.Builder() + .setManifestUrl(new URI("http://test.te/manifest.yaml").toURL()) + .build()); + resolver.getCurrentVersions(channels); + + verify(manifestVersionResolver).getCurrentVersions(channels); + } + + private void whenManifestOneWasResolveDuringProvisioning() { + when(artifactVersions.getManifestVersion(A_GROUP_ID, MANIFEST_ONE_ARTIFACT_ID)).thenReturn( + new MavenArtifact(A_GROUP_ID, MANIFEST_ONE_ARTIFACT_ID, null, null, A_VERSION, manifestFile)); + } +} \ No newline at end of file diff --git a/prospero-common/src/test/java/org/wildfly/prospero/galleon/ArtifactCacheTest.java b/prospero-common/src/test/java/org/wildfly/prospero/galleon/ArtifactCacheTest.java index 0aaf7384e..ede981c5f 100644 --- a/prospero-common/src/test/java/org/wildfly/prospero/galleon/ArtifactCacheTest.java +++ b/prospero-common/src/test/java/org/wildfly/prospero/galleon/ArtifactCacheTest.java @@ -21,7 +21,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.wildfly.channel.ChannelManifest; import org.wildfly.channel.MavenArtifact; +import org.wildfly.prospero.metadata.ManifestVersionRecord; +import org.wildfly.prospero.wfchannel.MavenSessionManager; +import org.wildfly.prospero.wfchannel.ResolvedArtifactsStore; import java.io.File; import java.io.IOException; @@ -34,6 +38,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ArtifactCacheTest { @@ -151,4 +157,82 @@ public void getArtifactDoesntReturnArtifactIfTheHashIsDifferent() throws Excepti assertEquals(Optional.empty(), cachedArtifact); } + + @Test + public void cacheMavenManifests_ResolvedInList() throws Exception { + final ManifestVersionRecord record = new ManifestVersionRecord(); + record.addManifest(new ManifestVersionRecord.MavenManifest("foo", "bar", "1.2.3", "")); + final MavenSessionManager msm = mock(MavenSessionManager.class); + final ResolvedArtifactsStore manifestVersions = mock(ResolvedArtifactsStore.class); + when(msm.getResolvedArtifactVersions()).thenReturn(manifestVersions); + final File testFile = temp.newFile("test"); + Files.writeString(testFile.toPath(), "test file"); + when(manifestVersions.getManifestVersion("foo", "bar")).thenReturn( + new MavenArtifact("foo", "bar", null, null, "1.2.3", testFile)); + + cache.cache(record, msm.getResolvedArtifactVersions()); + + // verify the "test" file exists in cache + final Optional cachedArtifact = cache.getArtifact("foo", "bar", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3"); + assertThat(cachedArtifact.get()) + .hasSameBinaryContentAs(testFile); + } + + @Test + public void cacheMavenManifests_NotResolvedLocally() throws Exception { + final ManifestVersionRecord record = new ManifestVersionRecord(); + record.addManifest(new ManifestVersionRecord.MavenManifest("foo", "bar", "1.2.3", "")); + final MavenSessionManager msm = mock(MavenSessionManager.class); + final ResolvedArtifactsStore manifestVersions = mock(ResolvedArtifactsStore.class); + when(msm.getResolvedArtifactVersions()).thenReturn(manifestVersions); + // the artifact is not found in resolved list + when(manifestVersions.getManifestVersion("foo", "bar")).thenReturn(null); + + cache.cache(record, msm.getResolvedArtifactVersions()); + + // verify the "test" file exists in cache + final Optional cachedArtifact = cache.getArtifact("foo", "bar", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3"); + assertThat(cachedArtifact) + .isEmpty(); + } + + @Test + public void cacheMavenManifests_ResolvedFileDoesNotExist() throws Exception { + final ManifestVersionRecord record = new ManifestVersionRecord(); + record.addManifest(new ManifestVersionRecord.MavenManifest("foo", "bar", "1.2.3", "")); + final MavenSessionManager msm = mock(MavenSessionManager.class); + final ResolvedArtifactsStore manifestVersions = mock(ResolvedArtifactsStore.class); + when(msm.getResolvedArtifactVersions()).thenReturn(manifestVersions); + // the artifact is not found in resolved list + when(manifestVersions.getManifestVersion("foo", "bar")).thenReturn(null); + + cache.cache(record, msm.getResolvedArtifactVersions()); + + // verify the "test" file exists in cache + final Optional cachedArtifact = cache.getArtifact("foo", "bar", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3"); + assertThat(cachedArtifact) + .isEmpty(); + } + + @Test + public void cacheMavenManifests_ResolvedInListWithDifferentVersion() throws Exception { + final ManifestVersionRecord record = new ManifestVersionRecord(); + record.addManifest(new ManifestVersionRecord.MavenManifest("foo", "bar", "1.2.3", "")); + final MavenSessionManager msm = mock(MavenSessionManager.class); + final ResolvedArtifactsStore manifestVersions = mock(ResolvedArtifactsStore.class); + when(msm.getResolvedArtifactVersions()).thenReturn(manifestVersions); + final File testFile = temp.newFile("test-1.2.3"); + Files.writeString(testFile.toPath(), "test file 1.2.3"); + final File testFile2 = temp.newFile("test-1.2.4"); + Files.writeString(testFile2.toPath(), "test file 1.2.4"); + when(manifestVersions.getManifestVersion("foo", "bar")).thenReturn( + new MavenArtifact("foo", "bar", null, null, "1.2.4", testFile2)); + + cache.cache(record, msm.getResolvedArtifactVersions()); + + // verify the "test" file exists in cache + final Optional cachedArtifact = cache.getArtifact("foo", "bar", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3"); + assertThat(cachedArtifact) + .isEmpty(); + } } \ No newline at end of file diff --git a/prospero-common/src/test/java/org/wildfly/prospero/galleon/CachedVersionResolverTest.java b/prospero-common/src/test/java/org/wildfly/prospero/galleon/CachedVersionResolverTest.java index 69e476c73..ac89290c4 100644 --- a/prospero-common/src/test/java/org/wildfly/prospero/galleon/CachedVersionResolverTest.java +++ b/prospero-common/src/test/java/org/wildfly/prospero/galleon/CachedVersionResolverTest.java @@ -17,6 +17,7 @@ package org.wildfly.prospero.galleon; +import org.eclipse.aether.RepositoryListener; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -34,13 +35,21 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.wildfly.channel.ArtifactCoordinate; +import org.wildfly.channel.ArtifactTransferException; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestCoordinate; +import org.wildfly.channel.ChannelMetadataCoordinate; import org.wildfly.channel.spi.MavenVersionsResolver; import java.io.File; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.function.Function; 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.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -62,6 +71,10 @@ public class CachedVersionResolverTest { private RepositorySystem system; @Mock private ArtifactCache artifactCache; + @Mock + private Function manifestVersionProvider; + @Mock + private RepositoryListener repositoryListener; @Captor private ArgumentCaptor requestCaptor; @Captor @@ -81,7 +94,8 @@ public class CachedVersionResolverTest { @Before public void setUp() throws Exception { - resolver = new CachedVersionResolver(mockResolver, artifactCache, system, session); + when(session.getRepositoryListener()).thenReturn(repositoryListener); + resolver = new CachedVersionResolver(mockResolver, artifactCache, system, session, manifestVersionProvider); } @Test @@ -184,4 +198,97 @@ public void testBulkResolveFailsToInstall() throws Exception { assertEquals("artifact", listCaptor.getValue().get(0).getArtifactId()); assertEquals("artifactTwo", listCaptor.getValue().get(1).getArtifactId()); } + + @Test + public void testResolveChannelMetadata_FallbackFailsIfNoCurrentVersionFound() throws Exception { + final ArtifactTransferException resolutionException = new ArtifactTransferException("", + Set.of(new ArtifactCoordinate("org.test", "manifest-one", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "")), + Collections.emptySet()); + when(mockResolver.resolveChannelMetadata(any())).thenThrow(resolutionException); + when(manifestVersionProvider.apply(any())).thenReturn(null); + assertThatThrownBy(()->resolver.resolveChannelMetadata(List.of(new ChannelMetadataCoordinate("org.test", "manifest-one", ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION)))) + .isEqualTo(resolutionException); + } + + @Test + public void testResolveChannelMetadata_FallbackFailsIfCacheDoesntHaveCurrentVersion() throws Exception { + final ArtifactTransferException resolutionException = new ArtifactTransferException("", + Set.of(new ArtifactCoordinate("org.test", "manifest-one", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "")), + Collections.emptySet()); + when(mockResolver.resolveChannelMetadata(any())).thenThrow(resolutionException); + when(manifestVersionProvider.apply(any())).thenReturn("1.2.3"); + when(artifactCache.getArtifact("org.test", "manifest-one", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3")) + .thenReturn(Optional.empty()); + + assertThatThrownBy(()->resolver.resolveChannelMetadata(List.of(new ChannelMetadataCoordinate("org.test", "manifest-one", ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION)))) + .isEqualTo(resolutionException); + } + + @Test + public void testResolveChannelMetadata_FallbackReturnsCachedFile() throws Exception { + final ArtifactTransferException resolutionException = new ArtifactTransferException("", + Set.of(new ArtifactCoordinate("org.test", "manifest-one", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "")), + Collections.emptySet()); + when(mockResolver.resolveChannelMetadata(any())).thenThrow(resolutionException); + when(manifestVersionProvider.apply(any())).thenReturn("1.2.3"); + final File testFile = new File("test"); + when(artifactCache.getArtifact("org.test", "manifest-one", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3")) + .thenReturn(Optional.of(testFile)); + + assertThat(resolver.resolveChannelMetadata(List.of(new ChannelMetadataCoordinate("org.test", "manifest-one", ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION)))) + .containsExactly(testFile.toURI().toURL()); + verify(repositoryListener).artifactResolved(any()); + } + + @Test + public void testResolveChannelMetadata_FallbackReturnsCachedFile_WithTwoManifests() throws Exception { + final ArtifactTransferException resolutionException = new ArtifactTransferException("", + Set.of( + new ArtifactCoordinate("org.test", "manifest-one", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, ""), + new ArtifactCoordinate("org.test", "manifest-two", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "")), + Collections.emptySet()); + when(mockResolver.resolveChannelMetadata(any())).thenThrow(resolutionException); + when(manifestVersionProvider.apply(any())).thenReturn("1.2.3"); + final File testFileOne = new File("testOne"); + final File testFileTwo = new File("testTwo"); + when(artifactCache.getArtifact("org.test", "manifest-one", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3")) + .thenReturn(Optional.of(testFileOne)); + when(artifactCache.getArtifact("org.test", "manifest-two", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3")) + .thenReturn(Optional.of(testFileTwo)); + + assertThat(resolver.resolveChannelMetadata(List.of( + new ChannelMetadataCoordinate("org.test", "manifest-one", ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION), + new ChannelMetadataCoordinate("org.test", "manifest-two", ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION)))) + .containsExactly(testFileOne.toURI().toURL(), testFileTwo.toURI().toURL()); + verify(repositoryListener, times(2)).artifactResolved(any()); + } + + @Test + public void testResolveChannelMetadata_OneManifestResolvedNormally() throws Exception { + final ArtifactTransferException resolutionException = new ArtifactTransferException("", + Set.of( + new ArtifactCoordinate("org.test", "manifest-two", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "")), + Collections.emptySet()); + final File testFileOne = new File("testOne"); + final File testFileTwo = new File("testTwo"); + // throw resolution exception on first attempt to resolve channel metadata + when(mockResolver.resolveChannelMetadata(List.of( + new ChannelManifestCoordinate("org.test", "manifest-one"), + new ChannelManifestCoordinate("org.test", "manifest-two")))) + .thenThrow(resolutionException); + // resolve the URL of existing artifact + when(mockResolver.resolveChannelMetadata(List.of( + new ChannelManifestCoordinate("org.test", "manifest-one")))) + .thenReturn(List.of(testFileOne.toURI().toURL())); + when(manifestVersionProvider.apply(any())).thenReturn("1.2.3"); + when(artifactCache.getArtifact("org.test", "manifest-two", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.2.3")) + .thenReturn(Optional.of(testFileTwo)); + + assertThat(resolver.resolveChannelMetadata(List.of( + new ChannelManifestCoordinate("org.test", "manifest-one"), + new ChannelManifestCoordinate("org.test", "manifest-two")))) + .containsExactly(testFileOne.toURI().toURL(), testFileTwo.toURI().toURL()); + // only called on the cached artifact + verify(repositoryListener).artifactResolved(any()); + } } \ No newline at end of file diff --git a/prospero-common/src/test/java/org/wildfly/prospero/galleon/GalleonEnvironmentTest.java b/prospero-common/src/test/java/org/wildfly/prospero/galleon/GalleonEnvironmentTest.java index 0ddbb9a14..278447bb8 100644 --- a/prospero-common/src/test/java/org/wildfly/prospero/galleon/GalleonEnvironmentTest.java +++ b/prospero-common/src/test/java/org/wildfly/prospero/galleon/GalleonEnvironmentTest.java @@ -17,21 +17,43 @@ package org.wildfly.prospero.galleon; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.internal.impl.DefaultRepositorySystem; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; import org.wildfly.channel.Channel; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestCoordinate; +import org.wildfly.channel.ChannelManifestMapper; import org.wildfly.prospero.api.exceptions.ChannelDefinitionException; +import org.wildfly.prospero.metadata.ManifestVersionRecord; import org.wildfly.prospero.wfchannel.MavenSessionManager; import java.io.File; +import java.net.URL; import java.nio.file.Files; +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.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class GalleonEnvironmentTest { @@ -42,6 +64,12 @@ public class GalleonEnvironmentTest { @Mock private MavenSessionManager msm; + @Mock + private DefaultRepositorySystemSession session; + + @Mock + private DefaultRepositorySystem system; + @Test public void createEnvWithInvalidManifestThrowsException() throws Exception { final File manifest = temp.newFile(); @@ -49,8 +77,111 @@ public void createEnvWithInvalidManifestThrowsException() throws Exception { final Channel build = new Channel.Builder() .setManifestUrl(manifest.toURI().toURL()) .build(); + when(msm.newRepositorySystemSession(any())).thenReturn(session); + assertThrows(ChannelDefinitionException.class, ()-> GalleonEnvironment.builder(temp.newFolder().toPath(), List.of(build), msm).build()); } + @Test + public void populateMavenCacheWithRevertManifests_EmptyManifests_DoesNothing() throws Exception { + when(msm.newRepositorySystemSession(any())).thenReturn(session); + when(msm.newRepositorySystem()).thenReturn(system); + + final ChannelManifest restoreManifest = new ChannelManifest("", null, null, Collections.emptyList()); + GalleonEnvironment.builder(temp.newFolder().toPath(), List.of(), msm) + .setRestoreManifest(restoreManifest, null) + .build(); + + verifyNoInteractions(system); + } + + @Test + public void populateMavenCacheWithRevertManifests_MavenManifestsWithVersion_CallsResolve() throws Exception { + when(msm.newRepositorySystemSession(any())).thenReturn(session); + when(msm.newRepositorySystem()).thenReturn(system); + final DefaultArtifact manifestArtifact = new DefaultArtifact("group", "artifact", "manifest", "yaml", "version"); + // mock resolving an artifact + final ArtifactResult res = new ArtifactResult(new ArtifactRequest(manifestArtifact, null, null)); + res.setArtifact(manifestArtifact.setFile(new File("test.yaml"))); + when(system.resolveArtifact(any(), any())).thenReturn(res); + + final ChannelManifest restoreManifest = new ChannelManifest("", null, null, Collections.emptyList()); + final ManifestVersionRecord record = new ManifestVersionRecord(); + record.addManifest(new ManifestVersionRecord.MavenManifest(manifestArtifact.getGroupId(), manifestArtifact.getArtifactId(), + manifestArtifact.getVersion(), "desc")); + GalleonEnvironment.builder(temp.newFolder().toPath(), List.of(), msm) + .setRestoreManifest(restoreManifest, record) + .build(); + + verify(system).resolveArtifact(any(), argThat(req -> req.getArtifact().equals(manifestArtifact))); + } + @Test + public void populateMavenCacheWithRevertManifests_MavenManifestsWithVersion_IgnoresErrors() throws Exception { + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ArtifactRequest.class); + when(msm.newRepositorySystemSession(any())).thenReturn(session); + when(msm.newRepositorySystem()).thenReturn(system); + // mock failed resolution of one artifact and correct one of the other + final DefaultArtifact missingArtifact = new DefaultArtifact("idont", "exist", "manifest", "yaml", "version"); + final DefaultArtifact manifestArtifact = new DefaultArtifact("group", "artifact", "manifest", "yaml", "version"); + final ArtifactResult res = new ArtifactResult(new ArtifactRequest(manifestArtifact, null, null)); + res.setArtifact(manifestArtifact.setFile(new File("test.yaml"))); + when(system.resolveArtifact(any(), argumentCaptor.capture())) + .then((Answer) inv -> { + final ArtifactRequest req = inv.getArgument(1, ArtifactRequest.class); + if (req.getArtifact().equals(missingArtifact)) { + throw new ArtifactResolutionException(List.of()); + } else { + return res; + } + }); + + final ChannelManifest restoreManifest = new ChannelManifest("", null, null, Collections.emptyList()); + final ManifestVersionRecord record = new ManifestVersionRecord(); + record.addManifest(new ManifestVersionRecord.MavenManifest(missingArtifact.getGroupId(), missingArtifact.getArtifactId(), + missingArtifact.getVersion(), "desc")); + record.addManifest(new ManifestVersionRecord.MavenManifest(manifestArtifact.getGroupId(), manifestArtifact.getArtifactId(), + manifestArtifact.getVersion(), "desc")); + GalleonEnvironment.builder(temp.newFolder().toPath(), List.of(), msm) + .setRestoreManifest(restoreManifest, record) + .build(); + + verify(system, atLeast(2)).resolveArtifact(any(), any()); + assertThat(argumentCaptor.getAllValues()) + .map(ArtifactRequest::getArtifact) + .containsOnly(missingArtifact, manifestArtifact); + } + + @Test + public void restoreManifestIsUsedInChannels() throws Exception { + when(msm.newRepositorySystemSession(any())).thenReturn(session); + when(msm.newRepositorySystem()).thenReturn(system); + + final Channel c1 = new Channel.Builder() + .setManifestCoordinate("group", "artifactOne", "1.0.0") + .build(); + final Channel c2 = new Channel.Builder() + .setManifestCoordinate("group", "artifactTwo", "1.0.0") + .build(); + final ChannelManifest restoreManifest = new ChannelManifest("restore manifest", null, null, Collections.emptyList()); + + final URL manifestUrl; + try (GalleonEnvironment env = GalleonEnvironment.builder(temp.newFolder().toPath(), List.of(c1, c2), msm) + .setRestoreManifest(restoreManifest) + .build()) { + + manifestUrl = env.getChannels().get(0).getManifestCoordinate().getUrl(); + final ChannelManifest resManifest = ChannelManifestMapper.from(manifestUrl); + + assertThat(resManifest.getName()).isEqualTo("restore manifest"); + + assertThat(env.getChannels()) + .map(Channel::getManifestCoordinate) + .map(ChannelManifestCoordinate::getUrl) + .containsOnly(manifestUrl); + } + assertThat(Path.of(manifestUrl.toURI())) + .doesNotExist(); + } + } \ No newline at end of file diff --git a/prospero-common/src/test/java/org/wildfly/prospero/wfchannel/ProsperoMavenRepositoryListenerTest.java b/prospero-common/src/test/java/org/wildfly/prospero/wfchannel/ProsperoMavenRepositoryListenerTest.java new file mode 100644 index 000000000..dc9f0d56b --- /dev/null +++ b/prospero-common/src/test/java/org/wildfly/prospero/wfchannel/ProsperoMavenRepositoryListenerTest.java @@ -0,0 +1,120 @@ +/* + * 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.wfchannel; + +import org.eclipse.aether.RepositoryEvent; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.MavenArtifact; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class ProsperoMavenRepositoryListenerTest { + + protected static final String A_GROUP = "org.test"; + protected static final String AN_ARTIFACT = "artifact-one"; + protected static final String A_VERSION = "1.2.3"; + @Mock + private RepositorySystemSession session; + private ProsperoMavenRepositoryListener listener; + private File testFile; + + @Before + public void setUp() { + + listener = new ProsperoMavenRepositoryListener(); + testFile = new File("test"); + } + + @Test + public void testRecordArtifact() throws Exception { + listener.artifactResolved(new RepositoryEvent.Builder(session, RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .setArtifact(resolvedArtifact(testFile)) + .build()); + + assertThat(listener.getManifestVersion(A_GROUP, AN_ARTIFACT)) + .isEqualTo(new MavenArtifact(A_GROUP, AN_ARTIFACT, + ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, A_VERSION, testFile)); + + } + + @Test + public void testDontRecordArtifactWithoutFile() throws Exception { + listener.artifactResolved(new RepositoryEvent.Builder(session, RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .setArtifact(resolvedArtifact(null)) + .build()); + + assertThat(listener.getManifestVersion(A_GROUP, AN_ARTIFACT)) + .isNull(); + } + + @Test + public void testDontRecordArtifactWhenEventDoesntHaveArtifact() throws Exception { + listener.artifactResolved(new RepositoryEvent.Builder(session, RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .build()); + + assertThat(listener.getManifestVersion(A_GROUP, AN_ARTIFACT)) + .isNull(); + } + + @Test + public void testReturnNullIfArtifactHasntBeenResolved() throws Exception { + listener.artifactResolved(new RepositoryEvent.Builder(session, RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .setArtifact(resolvedArtifact(testFile)) + .build()); + + assertThat(listener.getManifestVersion(A_GROUP, "idont-exist")) + .isNull(); + } + + @Test + public void resolvingSameArtifactTwiceReplacesArtifact() throws Exception { + final File testFileTwo = new File("test-two"); + + listener.artifactResolved(new RepositoryEvent.Builder(session, RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .setArtifact(resolvedArtifact(testFile)) + .build()); + listener.artifactResolved(new RepositoryEvent.Builder(session, RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .setArtifact(resolvedArtifact(testFileTwo)) + .build()); + + assertThat(listener.getManifestVersion(A_GROUP, AN_ARTIFACT)) + .isEqualTo(new MavenArtifact(A_GROUP, AN_ARTIFACT, + ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, A_VERSION, testFileTwo)); + } + + private DefaultArtifact resolvedArtifact(File testFile) { + if (testFile == null) { + return new DefaultArtifact(A_GROUP, AN_ARTIFACT, + ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION, A_VERSION); + } else { + return new DefaultArtifact(A_GROUP, AN_ARTIFACT, + ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION, A_VERSION, + null, testFile); + } + } +} \ No newline at end of file