diff --git a/.tekton/exhort-pull-request.yaml b/.tekton/exhort-pull-request.yaml index 9d5e00ac..366a8659 100644 --- a/.tekton/exhort-pull-request.yaml +++ b/.tekton/exhort-pull-request.yaml @@ -215,6 +215,27 @@ spec: workspace: mvn-settings - name: workspace workspace: workspace + - name: run-tests + runAfter: + - prefetch-dependencies + - copy-settings + taskSpec: + steps: + - image: registry.redhat.io/ubi9/openjdk-21:1.18 + name: builder + workingDir: $(workspaces.source.path)/source + script: mvn -B --settings settings.xml verify -Dquarkus.redis.hosts=redis://localhost/ + computeResources: + limits: + memory: 8Gi + requests: + memory: 6Gi + sidecars: + - image: docker.io/redis/redis-stack:7.2.0-v7 + name: redis-stack + workspaces: + - name: source + workspace: workspace - name: build-container params: - name: IMAGE @@ -232,8 +253,7 @@ spec: - name: COMMIT_SHA value: $(tasks.clone-repository.results.commit) runAfter: - - prefetch-dependencies - - copy-settings + - run-tests taskRef: params: - name: name @@ -395,7 +415,7 @@ spec: - ReadWriteOnce resources: requests: - storage: 1Gi + storage: 4Gi status: {} - name: git-auth secret: diff --git a/.tekton/exhort-push.yaml b/.tekton/exhort-push.yaml index ca4b2882..d7be9ade 100644 --- a/.tekton/exhort-push.yaml +++ b/.tekton/exhort-push.yaml @@ -209,6 +209,27 @@ spec: workspace: mvn-settings - name: workspace workspace: workspace + - name: run-tests + runAfter: + - prefetch-dependencies + - copy-settings + taskSpec: + steps: + - image: registry.redhat.io/ubi9/openjdk-21:1.18 + name: builder + workingDir: $(workspaces.source.path)/source + script: mvn -B --settings settings.xml verify -Dquarkus.redis.hosts=redis://localhost/ + computeResources: + limits: + memory: 8Gi + requests: + memory: 6Gi + sidecars: + - image: docker.io/redis/redis-stack:7.2.0-v7 + name: redis-stack + workspaces: + - name: source + workspace: workspace - name: build-container params: - name: IMAGE @@ -226,8 +247,7 @@ spec: - name: COMMIT_SHA value: $(tasks.clone-repository.results.commit) runAfter: - - prefetch-dependencies - - copy-settings + - run-tests taskRef: params: - name: name @@ -389,7 +409,7 @@ spec: - ReadWriteOnce resources: requests: - storage: 1Gi + storage: 2Gi status: {} - name: git-auth secret: diff --git a/deploy/exhort.yaml b/deploy/exhort.yaml index fd0dd114..fb4d003f 100644 --- a/deploy/exhort.yaml +++ b/deploy/exhort.yaml @@ -52,6 +52,16 @@ spec: secretKeyRef: name: exhort-secret key: telemetry-write-key + - name: DB_REDIS_HOST + valueFrom: + secretKeyRef: + name: exhort-secret + key: db.host + - name: DB_REDIS_PORT + valueFrom: + secretKeyRef: + name: exhort-secret + key: db.port livenessProbe: httpGet: path: /q/health/live diff --git a/deploy/openshift/template.yaml b/deploy/openshift/template.yaml index 68c47654..e7b4f3c2 100644 --- a/deploy/openshift/template.yaml +++ b/deploy/openshift/template.yaml @@ -96,6 +96,16 @@ objects: secretKeyRef: name: exhort-secret key: telemetry-write-key + - name: DB_REDIS_HOST + valueFrom: + secretKeyRef: + name: '${ELASTICACHE_SECRET}' + key: db.endpoint + - name: DB_REDIS_PORT + valueFrom: + secretKeyRef: + name: '${ELASTICACHE_SECRET}' + key: db.port - name: MONITORING_ENABLED value: "true" - name: MONITORING_SENTRY_DSN @@ -157,6 +167,11 @@ parameters: description: Service name value: exhort required: true + - name: ELASTICACHE_SECRET + displayName: Elasticache Secret + description: Name of the secret containing the Elasticache settings + value: exhort-elasticache + required: true - name: SERVICE_PORT displayName: Service port description: Service port diff --git a/pom.xml b/pom.xml index ce440fb8..097e5b53 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,10 @@ io.quarkus quarkus-resteasy-reactive-jackson + + io.quarkus + quarkus-redis-client + org.apache.camel.quarkus camel-quarkus-jackson @@ -209,6 +213,10 @@ camel-quarkus-junit5 test + + io.quarkus + quarkus-junit5-mockito + io.rest-assured rest-assured diff --git a/src/main/docker/Dockerfile.jvm.staged b/src/main/docker/Dockerfile.jvm.staged index 419be16a..d7472e2e 100644 --- a/src/main/docker/Dockerfile.jvm.staged +++ b/src/main/docker/Dockerfile.jvm.staged @@ -31,7 +31,7 @@ RUN mvn -B --settings settings.xml org.apache.maven.plugins:maven-dependency-plu COPY src src -RUN mvn verify -B +RUN mvn package -B -DskipTests=true RUN grep version /build/target/maven-archiver/pom.properties | cut -d '=' -f2 >.env-version RUN grep artifactId /build/target/maven-archiver/pom.properties | cut -d '=' -f2 >.env-id diff --git a/src/main/java/com/redhat/exhort/integration/Constants.java b/src/main/java/com/redhat/exhort/integration/Constants.java index 96339c3d..88e62812 100644 --- a/src/main/java/com/redhat/exhort/integration/Constants.java +++ b/src/main/java/com/redhat/exhort/integration/Constants.java @@ -88,6 +88,7 @@ private Constants() {} public static final String GZIP_RESPONSE_PROPERTY = "gzipResponse"; public static final String SBOM_ID_PROPERTY = "sbomId"; public static final String UNSCANNED_REFS_PROPERTY = "unscannedRefs"; + public static final String CACHED_RECOMMENDATIONS = "missedRecommendations"; public static final String API_VERSION_V4 = "v4"; public static final String API_VERSION_V3 = "v3"; diff --git a/src/main/java/com/redhat/exhort/integration/cache/CacheService.java b/src/main/java/com/redhat/exhort/integration/cache/CacheService.java new file mode 100644 index 00000000..060c2d22 --- /dev/null +++ b/src/main/java/com/redhat/exhort/integration/cache/CacheService.java @@ -0,0 +1,39 @@ +/* + * 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 com.redhat.exhort.integration.cache; + +import java.util.Map; +import java.util.Set; + +import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; + +import com.redhat.exhort.api.PackageRef; +import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.model.trustedcontent.CachedRecommendation; +import com.redhat.exhort.model.trustedcontent.TrustedContentResponse; + +public interface CacheService { + + public void cacheRecommendations( + @Body TrustedContentResponse response, + @ExchangeProperty(Constants.CACHED_RECOMMENDATIONS) Set misses); + + public Map getRecommendations(Set purls); +} diff --git a/src/main/java/com/redhat/exhort/integration/cache/RedisCacheService.java b/src/main/java/com/redhat/exhort/integration/cache/RedisCacheService.java new file mode 100644 index 00000000..60303ecc --- /dev/null +++ b/src/main/java/com/redhat/exhort/integration/cache/RedisCacheService.java @@ -0,0 +1,77 @@ +/* + * 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 com.redhat.exhort.integration.cache; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import com.redhat.exhort.api.PackageRef; +import com.redhat.exhort.model.trustedcontent.CachedRecommendation; +import com.redhat.exhort.model.trustedcontent.TrustedContentResponse; + +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.value.ValueCommands; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class RedisCacheService implements CacheService { + + @ConfigProperty(name = "recommendations.cache.ttl", defaultValue = "1d") + Duration recommendationTtl; + + private final ValueCommands recommendationsCommands; + + public RedisCacheService(RedisDataSource ds) { + this.recommendationsCommands = ds.value(CachedRecommendation.class); + } + + @Override + public void cacheRecommendations(TrustedContentResponse response, Set misses) { + if (response == null || response.status() == null || !response.status().getOk()) { + return; + } + misses.stream() + .forEach( + v -> + recommendationsCommands.psetex( + "recommendations:" + v.ref(), + recommendationTtl.toMillis(), + new CachedRecommendation(v, response.recommendations().get(v)))); + } + + @Override + public Map getRecommendations(Set purls) { + if (purls == null || purls.isEmpty()) { + return Collections.emptyMap(); + } + var result = + recommendationsCommands.mget( + purls.stream().map(p -> "recommendations:" + p.ref()).toArray(String[]::new)); + return result.values().stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(v -> v.ref(), v -> v)); + } +} diff --git a/src/main/java/com/redhat/exhort/integration/providers/ProviderResponseHandler.java b/src/main/java/com/redhat/exhort/integration/providers/ProviderResponseHandler.java index 7cb33b0d..5426304a 100644 --- a/src/main/java/com/redhat/exhort/integration/providers/ProviderResponseHandler.java +++ b/src/main/java/com/redhat/exhort/integration/providers/ProviderResponseHandler.java @@ -285,17 +285,13 @@ public ProviderReport buildReport( } Map reports = new HashMap<>(); sourcesIssues - .keySet() + .entrySet() .forEach( k -> reports.put( - k, + k.getKey(), buildReportForSource( - sourcesIssues.get(k), - tree, - privateProviders, - tcResponse, - response.unscanned()))); + k.getValue(), tree, privateProviders, tcResponse, response.unscanned()))); return new ProviderReport().status(defaultOkStatus(getProviderName())).sources(reports); } diff --git a/src/main/java/com/redhat/exhort/integration/trustedcontent/TcResponseAggregation.java b/src/main/java/com/redhat/exhort/integration/trustedcontent/TcResponseAggregation.java index 528bf210..e224bbe6 100644 --- a/src/main/java/com/redhat/exhort/integration/trustedcontent/TcResponseAggregation.java +++ b/src/main/java/com/redhat/exhort/integration/trustedcontent/TcResponseAggregation.java @@ -18,20 +18,32 @@ package com.redhat.exhort.integration.trustedcontent; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + import org.apache.camel.AggregationStrategy; import org.apache.camel.Exchange; +import org.apache.camel.ExchangeProperty; +import com.redhat.exhort.api.PackageRef; import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.integration.cache.CacheService; +import com.redhat.exhort.model.trustedcontent.IndexedRecommendation; +import com.redhat.exhort.model.trustedcontent.TrustedContentCachedRequest; import com.redhat.exhort.model.trustedcontent.TrustedContentResponse; import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.inject.Inject; import jakarta.inject.Singleton; @Singleton @RegisterForReflection public class TcResponseAggregation implements AggregationStrategy { + @Inject CacheService cacheService; + @Override public Exchange aggregate(Exchange oldExchange, Exchange newExchange) { oldExchange.setProperty( @@ -39,4 +51,17 @@ public Exchange aggregate(Exchange oldExchange, Exchange newExchange) { newExchange.getIn().getBody(TrustedContentResponse.class)); return oldExchange; } + + public TrustedContentResponse aggregateCachedResponse( + @ExchangeProperty(Constants.CACHED_RECOMMENDATIONS) TrustedContentCachedRequest cached, + Exchange exchange) + throws ExecutionException { + var externalResponse = exchange.getIn().getBody(TrustedContentResponse.class); + cacheService.cacheRecommendations(externalResponse, cached.miss()); + Map recommendations = + new HashMap<>(externalResponse.recommendations()); + recommendations.putAll(cached.cached()); + exchange.removeProperty(Constants.CACHED_RECOMMENDATIONS); + return new TrustedContentResponse(recommendations, externalResponse.status()); + } } diff --git a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java index 56dc0a20..8f951096 100644 --- a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java @@ -42,11 +42,19 @@ public class TrustedContentIntegration extends EndpointRouteBuilder { @Inject TrustedContentRequestBuilder requestBuilder; + @Inject TcResponseAggregation aggregation; + @Override public void configure() { // fmt:off from(direct("getTrustedContent")) - .routeId("getTrustedContent") + .routeId("getTrustedContent") + .setBody(method(requestBuilder, "filterCachedRecommendations")) + .to(direct("getRemoteTrustedContent")) + .setBody(method(aggregation, "aggregateCachedResponse")); + + from(direct("getRemoteTrustedContent")) + .routeId("getRemoteTrustedContent") .circuitBreaker() .faultToleranceConfiguration() .timeoutEnabled(true) @@ -59,6 +67,7 @@ public void configure() { .endCircuitBreaker() .onFallback() .process(responseHandler::processResponseError); + // fmt:on } diff --git a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentRequestBuilder.java b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentRequestBuilder.java index 51c257df..93306595 100644 --- a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentRequestBuilder.java +++ b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentRequestBuilder.java @@ -18,13 +18,22 @@ package com.redhat.exhort.integration.trustedcontent; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.camel.Exchange; import org.apache.camel.ExchangeProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.api.PackageRef; import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.integration.cache.CacheService; import com.redhat.exhort.model.DependencyTree; +import com.redhat.exhort.model.trustedcontent.IndexedRecommendation; +import com.redhat.exhort.model.trustedcontent.TrustedContentCachedRequest; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -37,13 +46,35 @@ public class TrustedContentRequestBuilder { @Inject ObjectMapper mapper; - public String buildRequest( - @ExchangeProperty(Constants.DEPENDENCY_TREE_PROPERTY) DependencyTree tree) - throws JsonProcessingException { + @Inject CacheService cacheService; + + public String buildRequest(Set misses) throws JsonProcessingException { - var purls = mapper.createArrayNode(); - tree.getAll().stream().map(PackageRef::toString).forEach(purls::add); - var obj = mapper.createObjectNode().set("purls", purls); + var node = mapper.createArrayNode(); + misses.stream().map(PackageRef::toString).forEach(node::add); + var obj = mapper.createObjectNode().set("purls", node); return mapper.writeValueAsString(obj); } + + public Set filterCachedRecommendations( + @ExchangeProperty(Constants.DEPENDENCY_TREE_PROPERTY) DependencyTree tree, + Exchange exchange) { + Set miss = new HashSet<>(); + var allRefs = tree.getAll(); + var cached = cacheService.getRecommendations(allRefs); + + Map cachedIdxRecommendations = new HashMap<>(); + allRefs.forEach( + p -> { + var cachedReq = cached.get(p); + if (cachedReq == null) { + miss.add(p); + } else if (cachedReq.recommendation() != null) { + cachedIdxRecommendations.put(p, cachedReq.recommendation()); + } + }); + var req = new TrustedContentCachedRequest(cachedIdxRecommendations, miss); + exchange.setProperty(Constants.CACHED_RECOMMENDATIONS, req); + return req.miss(); + } } diff --git a/src/main/java/com/redhat/exhort/model/trustedcontent/CachedRecommendation.java b/src/main/java/com/redhat/exhort/model/trustedcontent/CachedRecommendation.java new file mode 100644 index 00000000..32b3d447 --- /dev/null +++ b/src/main/java/com/redhat/exhort/model/trustedcontent/CachedRecommendation.java @@ -0,0 +1,23 @@ +/* + * 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 com.redhat.exhort.model.trustedcontent; + +import com.redhat.exhort.api.PackageRef; + +public record CachedRecommendation(PackageRef ref, IndexedRecommendation recommendation) {} diff --git a/src/main/java/com/redhat/exhort/model/trustedcontent/TrustedContentCachedRequest.java b/src/main/java/com/redhat/exhort/model/trustedcontent/TrustedContentCachedRequest.java new file mode 100644 index 00000000..e2b04670 --- /dev/null +++ b/src/main/java/com/redhat/exhort/model/trustedcontent/TrustedContentCachedRequest.java @@ -0,0 +1,27 @@ +/* + * 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 com.redhat.exhort.model.trustedcontent; + +import java.util.Map; +import java.util.Set; + +import com.redhat.exhort.api.PackageRef; + +public record TrustedContentCachedRequest( + Map cached, Set miss) {} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 542a132a..98dd7c6a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -45,4 +45,6 @@ trustedcontent.recommendation.ubi.mapping.ubuntu=${trustedcontent.recommendation trustedcontent.recommendation.ubi.mapping.centos=${trustedcontent.recommendation.ubi.purl.ubi9} trustedcontent.recommendation.ubi.mapping.debian=${trustedcontent.recommendation.ubi.purl.ubi9} trustedcontent.recommendation.ubi.mapping.fedora=${trustedcontent.recommendation.ubi.purl.ubi9} -trustedcontent.recommendation.ubi.mapping.amazonlinux=${trustedcontent.recommendation.ubi.purl.ubi9} \ No newline at end of file +trustedcontent.recommendation.ubi.mapping.amazonlinux=${trustedcontent.recommendation.ubi.purl.ubi9} + +%prod.quarkus.redis.hosts=redis://${db.redis.host:localhost}:${db.redis.port:6379}/ \ No newline at end of file diff --git a/src/test/java/com/redhat/exhort/integration/cache/RedisCacheServiceTest.java b/src/test/java/com/redhat/exhort/integration/cache/RedisCacheServiceTest.java new file mode 100644 index 00000000..c11f7490 --- /dev/null +++ b/src/test/java/com/redhat/exhort/integration/cache/RedisCacheServiceTest.java @@ -0,0 +1,89 @@ +/* + * 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 com.redhat.exhort.integration.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import com.redhat.exhort.api.PackageRef; +import com.redhat.exhort.api.v4.ProviderStatus; +import com.redhat.exhort.model.trustedcontent.IndexedRecommendation; +import com.redhat.exhort.model.trustedcontent.TrustedContentResponse; + +import io.quarkus.test.junit.QuarkusTest; + +import jakarta.inject.Inject; + +@QuarkusTest +public class RedisCacheServiceTest { + + private static final PackageRef[] TEST_PURLS = { + new PackageRef("pkg:maven/io.quarkus/quarkus-core@2.13.6.Final"), + new PackageRef("pkg:maven/io.quarkus/quarkus-arc@2.13.6.Final") + }; + + @Inject CacheService cacheService; + + @Test + void testFailedResponse() { + var failedResponse = + new TrustedContentResponse(buildRecommendations(), new ProviderStatus().ok(Boolean.FALSE)); + cacheService.cacheRecommendations(failedResponse, Set.of(TEST_PURLS)); + + assertTrue(cacheService.getRecommendations(Set.of(TEST_PURLS)).isEmpty()); + } + + @Test + void testCacheAllData() { + var failedResponse = + new TrustedContentResponse(buildRecommendations(), new ProviderStatus().ok(Boolean.TRUE)); + cacheService.cacheRecommendations(failedResponse, Set.of(TEST_PURLS)); + + var cachedData = cacheService.getRecommendations(Set.of(TEST_PURLS)); + assertEquals(2, cachedData.size()); + Stream.of(TEST_PURLS) + .forEach( + p -> { + var cachedPurl = cachedData.get(p); + assertNotNull(cachedPurl); + assertEquals(p, cachedPurl.ref()); + assertEquals( + p.ref() + "-redhat-00001", cachedPurl.recommendation().packageName().ref()); + }); + } + + private Map buildRecommendations() { + return Stream.of(TEST_PURLS) + .collect( + Collectors.toMap( + p -> p, + p -> + new IndexedRecommendation( + new PackageRef(p.ref() + "-redhat-00001"), Collections.emptyMap()))); + } +} diff --git a/src/test/java/com/redhat/exhort/integration/trustedcontent/TrustedContentRequestBuilderTest.java b/src/test/java/com/redhat/exhort/integration/trustedcontent/TrustedContentRequestBuilderTest.java new file mode 100644 index 00000000..3e6b09a6 --- /dev/null +++ b/src/test/java/com/redhat/exhort/integration/trustedcontent/TrustedContentRequestBuilderTest.java @@ -0,0 +1,168 @@ +/* + * 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 com.redhat.exhort.integration.trustedcontent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.camel.Exchange; +import org.junit.jupiter.api.Test; + +import com.redhat.exhort.api.PackageRef; +import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.integration.cache.CacheService; +import com.redhat.exhort.model.DependencyTree; +import com.redhat.exhort.model.DirectDependency; +import com.redhat.exhort.model.trustedcontent.CachedRecommendation; +import com.redhat.exhort.model.trustedcontent.IndexedRecommendation; +import com.redhat.exhort.model.trustedcontent.TrustedContentCachedRequest; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; + +import jakarta.inject.Inject; + +@QuarkusTest +public class TrustedContentRequestBuilderTest { + + private static final PackageRef[] TEST_PURLS = { + new PackageRef("pkg:maven/io.quarkus/quarkus-core@2.13.6.Final"), + new PackageRef("pkg:maven/io.quarkus/quarkus-arc@2.13.6.Final"), + new PackageRef("pkg:maven/io.quarkus/quarkus-vertx@2.13.6.Final") + }; + + @Inject TrustedContentRequestBuilder requestBuilder; + + @InjectMock CacheService cacheService; + + @Test + void testFilterCachedRecommendations_noDeps() { + Exchange exchange = mock(); + DependencyTree tree = new DependencyTree(Collections.emptyMap()); + var deps = tree.getAll(); + + when(cacheService.getRecommendations(deps)).thenReturn(Collections.emptyMap()); + + var result = requestBuilder.filterCachedRecommendations(tree, exchange); + + assertTrue(result.isEmpty()); + verify(exchange) + .setProperty( + eq(Constants.CACHED_RECOMMENDATIONS), + argThat( + req -> { + var tcReq = (TrustedContentCachedRequest) req; + return tcReq.cached().isEmpty() && tcReq.miss().isEmpty(); + })); + } + + @Test + void testFilterCachedRecommendations_noCachedData() { + Exchange exchange = mock(); + DependencyTree tree = buildDeps(); + var deps = tree.getAll(); + + when(cacheService.getRecommendations(deps)).thenReturn(Collections.emptyMap()); + + var result = requestBuilder.filterCachedRecommendations(tree, exchange); + + assertEquals(deps.size(), result.size()); + verify(exchange) + .setProperty( + eq(Constants.CACHED_RECOMMENDATIONS), + argThat( + req -> { + var tcReq = (TrustedContentCachedRequest) req; + return tcReq.cached().isEmpty() && tcReq.miss().size() == TEST_PURLS.length; + })); + } + + @Test + void testFilterCachedRecommendations_emptyCachedData() { + Exchange exchange = mock(); + DependencyTree tree = buildDeps(); + var deps = tree.getAll(); + + var cachedData = + Stream.of(TEST_PURLS) + .collect(Collectors.toMap(p -> p, p -> new CachedRecommendation(p, null))); + when(cacheService.getRecommendations(deps)).thenReturn(cachedData); + + var result = requestBuilder.filterCachedRecommendations(tree, exchange); + + assertTrue(result.isEmpty()); + verify(exchange) + .setProperty( + eq(Constants.CACHED_RECOMMENDATIONS), + argThat( + req -> { + var tcReq = (TrustedContentCachedRequest) req; + return tcReq.cached().isEmpty() && tcReq.miss().isEmpty(); + })); + } + + @Test + void testFilterCachedRecommendations_partialCache() { + Exchange exchange = mock(); + DependencyTree tree = buildDeps(); + var deps = tree.getAll(); + + var cachedData = + Map.of( + TEST_PURLS[0], + new CachedRecommendation(TEST_PURLS[0], null), + TEST_PURLS[1], + new CachedRecommendation( + TEST_PURLS[1], + new IndexedRecommendation( + new PackageRef(TEST_PURLS[1].ref() + "-redhat-0001"), null))); + when(cacheService.getRecommendations(deps)).thenReturn(cachedData); + + var result = requestBuilder.filterCachedRecommendations(tree, exchange); + + assertEquals(1, result.size()); + assertTrue(result.contains(TEST_PURLS[2])); + verify(exchange) + .setProperty( + eq(Constants.CACHED_RECOMMENDATIONS), + argThat( + req -> { + var tcReq = (TrustedContentCachedRequest) req; + return tcReq.cached().size() == 1 + && tcReq.cached().containsKey(TEST_PURLS[1]) + && tcReq.miss().contains(TEST_PURLS[2]); + })); + } + + private DependencyTree buildDeps() { + var deps = + Stream.of(TEST_PURLS).collect(Collectors.toMap(d -> d, d -> new DirectDependency(d, null))); + return new DependencyTree(deps); + } +} diff --git a/src/test/resources/__files/reports/batch_report_all_token.json b/src/test/resources/__files/reports/batch_report_all_token.json index 2fcba13e..574e508b 100644 --- a/src/test/resources/__files/reports/batch_report_all_token.json +++ b/src/test/resources/__files/reports/batch_report_all_token.json @@ -31,9 +31,7 @@ "dependencies": [ { "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final?type=jar", - "issues": [ - - ], + "issues": [], "transitive": [ { "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1?type=jar", @@ -176,12 +174,8 @@ }, { "ref": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.5.Final?type=jar", - "issues": [ - - ], - "transitive": [ - - ], + "issues": [], + "transitive": [], "recommendation": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.8.Final-redhat-00006?repository_url=https%3A%2F%2Fmaven.repository.redhat.com%2Fga%2F&type=jar" } ] @@ -195,9 +189,7 @@ "code": 200, "message": "OK" }, - "sources": { - - } + "sources": {} }, "osv-nvd": { "status": { @@ -224,9 +216,7 @@ "dependencies": [ { "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final?type=jar", - "issues": [ - - ], + "issues": [], "transitive": [ { "ref": "pkg:maven/io.quarkus/quarkus-core@2.13.5.Final?type=jar", @@ -454,9 +444,7 @@ }, { "ref": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.5.Final?type=jar", - "issues": [ - - ], + "issues": [], "transitive": [ { "ref": "pkg:maven/org.postgresql/postgresql@42.5.0?type=jar", @@ -585,9 +573,7 @@ "dependencies": [ { "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final?type=jar", - "issues": [ - - ], + "issues": [], "transitive": [ { "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1?type=jar", @@ -644,9 +630,7 @@ }, "cvssScore": 5.9, "severity": "MEDIUM", - "cves": [ - - ], + "cves": [], "unique": true, "remediation": { "fixedIn": [ @@ -758,9 +742,7 @@ }, { "ref": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.5.Final?type=jar", - "issues": [ - - ], + "issues": [], "transitive": [ { "ref": "pkg:maven/org.postgresql/postgresql@42.5.0?type=jar", @@ -880,9 +862,7 @@ "code": 401, "message": "Unauthorized: Verify the provided credentials are valid." }, - "sources": { - - } + "sources": {} }, "trusted-content": { "status": { @@ -891,9 +871,7 @@ "code": 200, "message": "OK" }, - "sources": { - - } + "sources": {} }, "osv-nvd": { "status": { @@ -902,9 +880,7 @@ "code": 200, "message": "OK" }, - "sources": { - - } + "sources": {} }, "snyk": { "status": { @@ -928,9 +904,7 @@ "recommendations": 0, "unscanned": 1 }, - "dependencies": [ - - ], + "dependencies": [], "unscanned": [ { "ref": "pkg:oci/default-app@sha256%3A7c288032ecf3319045d9fa538c3b0cc868a320d01d03bce15b99c2c336319994?repository_url=quay.io%2Fdefault-app&tag=0.0.1", @@ -956,9 +930,7 @@ "code": 401, "message": "Unauthorized: Verify the provided credentials are valid." }, - "sources": { - - } + "sources": {} }, "trusted-content": { "status": { @@ -967,9 +939,7 @@ "code": 200, "message": "OK" }, - "sources": { - - } + "sources": {} }, "osv-nvd": { "status": { @@ -996,12 +966,8 @@ "dependencies": [ { "ref": "pkg:oci/debian@sha256%3A7c288032ecf3319045d9fa538c3b0cc868a320d01d03bce15b99c2c336319994?tag=0.0.1", - "issues": [ - - ], - "transitive": [ - - ], + "issues": [], + "transitive": [], "recommendation": "pkg:oci/ubi@sha256%3Af5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?arch=amd64&repository_url=registry.access.redhat.com%2Fubi9%2Fubi&tag=9.3-1552" } ] @@ -1033,12 +999,8 @@ "dependencies": [ { "ref": "pkg:oci/debian@sha256%3A7c288032ecf3319045d9fa538c3b0cc868a320d01d03bce15b99c2c336319994?tag=0.0.1", - "issues": [ - - ], - "transitive": [ - - ], + "issues": [], + "transitive": [], "recommendation": "pkg:oci/ubi@sha256%3Af5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?arch=amd64&repository_url=registry.access.redhat.com%2Fubi9%2Fubi&tag=9.3-1552" } ],