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"
}
],