Skip to content

Commit 6685d0f

Browse files
authored
Merge pull request #293 from zvigrinberg/feature/rediness-probe-external-services
feat: add readiness health check for external services
2 parents bcc41f6 + ce64267 commit 6685d0f

File tree

10 files changed

+299
-9
lines changed

10 files changed

+299
-9
lines changed

deploy/exhort.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ spec:
6464
httpGet:
6565
path: /q/health/ready
6666
port: 9000
67-
initialDelaySeconds: 5
68-
periodSeconds: 20
67+
initialDelaySeconds: 2
68+
periodSeconds: 15
6969
---
7070
apiVersion: v1
7171
kind: Service

deploy/openshift/template.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ objects:
7777
httpGet:
7878
path: /q/health/ready
7979
port: '${{MANAGEMENT_PORT}}'
80-
initialDelaySeconds: 5
81-
periodSeconds: 10
80+
initialDelaySeconds: 2
81+
periodSeconds: 15
8282
ports:
8383
- name: http
8484
containerPort: '${{SERVICE_PORT}}'

src/main/java/com/redhat/exhort/integration/Constants.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ public final class Constants {
3131

3232
private Constants() {}
3333

34+
public static final String PROVIDER_NAME = "providerName";
35+
36+
public static final String EXCLUDE_FROM_READINESS_CHECK = "exclusionFromReadiness";
3437
public static final String PROVIDERS_PARAM = "providers";
38+
39+
public static final String HEALTH_CHECKS_LIST_HEADER_NAME = "healthChecksRoutesList";
3540
public static final String SBOM_TYPE_PARAM = "sbomType";
3641

3742
public static final String ACCEPT_HEADER = "Accept";
@@ -90,8 +95,11 @@ private Constants() {}
9095
public static final String SNYK_DEP_GRAPH_API_PATH = "/test/dep-graph";
9196
public static final String SNYK_TOKEN_API_PATH = "/user/me";
9297
public static final String OSS_INDEX_AUTH_COMPONENT_API_PATH = "/authorized/component-report";
98+
public static final String OSS_INDEX_VERSION_PATH = "/version";
9399
public static final String OSV_NVD_PURLS_PATH = "/purls";
94100

101+
public static final String OSV_NVD_HEALTH_PATH = "/q/health";
102+
95103
public static final String TRUSTED_CONTENT_PATH = "/recommend";
96104
public static final String DEFAULT_ACCEPT_MEDIA_TYPE = MediaType.APPLICATION_JSON;
97105
public static final boolean DEFAULT_VERBOSE_MODE = false;

src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.redhat.exhort.integration.backend.sbom.SbomParser;
5353
import com.redhat.exhort.integration.backend.sbom.SbomParserFactory;
5454
import com.redhat.exhort.integration.providers.ProviderAggregationStrategy;
55+
import com.redhat.exhort.integration.providers.ProvidersBodyPlusResponseCodeAggregationStrategy;
5556
import com.redhat.exhort.integration.providers.VulnerabilityProvider;
5657
import com.redhat.exhort.integration.trustedcontent.TcResponseAggregation;
5758
import com.redhat.exhort.model.DependencyTree;
@@ -257,7 +258,24 @@ public void configure() {
257258
.setBody().simple("${exception.message}")
258259
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(Status.INTERNAL_SERVER_ERROR.getStatusCode()))
259260
.setHeader(Exchange.CONTENT_TYPE, constant(MediaType.TEXT_PLAIN));
261+
262+
from(direct("exhortHealthCheck"))
263+
.routeId("exhortHealthCheck")
264+
.recipientList(header(Constants.HEALTH_CHECKS_LIST_HEADER_NAME))
265+
.aggregationStrategy(new ProvidersBodyPlusResponseCodeAggregationStrategy());
266+
267+
from(direct("healthCheckProviderDisabled"))
268+
.routeId("healthCheckProviderDisabled")
269+
.setProperty(Constants.EXCLUDE_FROM_READINESS_CHECK, constant(true))
270+
.setBody(constant(String.format("Provider %s is disabled",exchangeProperty(Constants.PROVIDER_NAME))))
271+
272+
.process(exchange -> {
273+
String providerName = exchange.getProperty(Constants.PROVIDER_NAME, String.class);
274+
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_TEXT,String.format("Provider %s is disabled", providerName)); })
275+
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(Response.Status.SERVICE_UNAVAILABLE));
276+
260277
//fmt:on
278+
261279
}
262280

263281
private void processAnalysisRequest(Exchange exchange) {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2024 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
*
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.redhat.exhort.integration.providers;
20+
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Objects;
24+
import java.util.stream.Collectors;
25+
26+
import org.apache.camel.builder.ExchangeBuilder;
27+
import org.apache.camel.health.HealthCheckResultBuilder;
28+
import org.apache.camel.impl.health.AbstractHealthCheck;
29+
30+
import com.redhat.exhort.api.v4.ProviderStatus;
31+
import com.redhat.exhort.integration.Constants;
32+
33+
public class ProviderHealthCheck extends AbstractHealthCheck {
34+
35+
private static final List<String> ALL_PROVIDERS_HEALTH_CHECKS =
36+
List.of("direct:snykHealthCheck", "direct:osvNvdHealthCheck", "direct:ossIndexHealthCheck");
37+
38+
public ProviderHealthCheck() {
39+
super("External Providers Readiness Check");
40+
}
41+
42+
@Override
43+
protected void doCall(HealthCheckResultBuilder builder, Map<String, Object> options) {
44+
var response =
45+
getCamelContext()
46+
.createProducerTemplate()
47+
.send(
48+
"direct:exhortHealthCheck",
49+
ExchangeBuilder.anExchange(getCamelContext())
50+
.withHeader(
51+
Constants.HEALTH_CHECKS_LIST_HEADER_NAME, this.ALL_PROVIDERS_HEALTH_CHECKS)
52+
.build());
53+
54+
List<ProviderStatus> httpResponseBodiesAndStatuses =
55+
(List<ProviderStatus>) response.getMessage().getBody();
56+
Map<String, Object> providers =
57+
httpResponseBodiesAndStatuses.stream()
58+
.collect(
59+
Collectors.toMap(
60+
provider -> provider.getName(),
61+
provider -> formatProviderStatus(provider),
62+
(a, b) -> a));
63+
builder.details(providers);
64+
65+
if (httpResponseBodiesAndStatuses.stream()
66+
.filter(providerStatus -> Objects.nonNull(providerStatus.getCode()))
67+
.anyMatch(providerDetails -> providerDetails.getCode() < 400 && providerDetails.getOk())) {
68+
builder.up();
69+
70+
} else {
71+
builder.down();
72+
}
73+
}
74+
75+
private static String formatProviderStatus(ProviderStatus provider) {
76+
if (Objects.nonNull(provider.getCode())) {
77+
return String.format(
78+
"providerName=%s, isEnabled=%s, statusCode=%s, message=%s",
79+
provider.getName(), provider.getOk(), provider.getCode(), provider.getMessage());
80+
} else {
81+
return String.format(
82+
"providerName=%s, isEnabled=%s, message=%s",
83+
provider.getName(), provider.getOk(), provider.getMessage());
84+
}
85+
}
86+
87+
@Override
88+
public boolean isLiveness() {
89+
return false;
90+
}
91+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2024 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
*
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.redhat.exhort.integration.providers;
20+
21+
import java.util.Objects;
22+
23+
import org.apache.camel.Exchange;
24+
import org.apache.camel.Message;
25+
import org.apache.camel.processor.aggregate.AbstractListAggregationStrategy;
26+
27+
import com.redhat.exhort.api.v4.ProviderStatus;
28+
import com.redhat.exhort.integration.Constants;
29+
30+
import jakarta.ws.rs.core.Response;
31+
32+
public class ProvidersBodyPlusResponseCodeAggregationStrategy
33+
extends AbstractListAggregationStrategy<ProviderStatus> {
34+
@Override
35+
public ProviderStatus getValue(Exchange exchange) {
36+
ProviderStatus providerValues = new ProviderStatus();
37+
providerValues.setMessage(getHttpResponseBodyFromMessage(exchange.getMessage()));
38+
Integer statusCode = Integer.valueOf(getHttpResponseStatusFromMessage(exchange.getMessage()));
39+
if (!serviceExcludedFromReadinessCheck(exchange)) {
40+
providerValues.setCode(statusCode);
41+
}
42+
providerValues.setOk(!serviceExcludedFromReadinessCheck(exchange));
43+
String providerName = exchange.getProperty(Constants.PROVIDER_NAME, String.class);
44+
providerValues.setName(providerName);
45+
46+
return providerValues;
47+
}
48+
49+
private static Boolean serviceExcludedFromReadinessCheck(Exchange exchange) {
50+
return Objects.requireNonNullElse(
51+
exchange.getProperty(Constants.EXCLUDE_FROM_READINESS_CHECK, Boolean.class), false);
52+
}
53+
54+
private static String getHttpResponseStatusFromMessage(Message message) {
55+
if (message.getHeader(Exchange.HTTP_RESPONSE_CODE) instanceof Integer) {
56+
return message.getHeader(Exchange.HTTP_RESPONSE_CODE).toString();
57+
} else {
58+
return String.valueOf(
59+
message.getHeader(Exchange.HTTP_RESPONSE_CODE, Response.Status.class).getStatusCode());
60+
}
61+
}
62+
63+
private static String getHttpResponseBodyFromMessage(Message message) {
64+
if (Objects.nonNull(message.getHeader(Exchange.HTTP_RESPONSE_TEXT, String.class))) {
65+
return message.getHeader(Exchange.HTTP_RESPONSE_TEXT, String.class);
66+
} else {
67+
if (Objects.nonNull(message.getBody(String.class))
68+
&& message.getBody(String.class) instanceof String) {
69+
return message.getBody(String.class);
70+
} else {
71+
return Objects.requireNonNull(
72+
message.getHeader(Exchange.HTTP_RESPONSE_CODE, String.class),
73+
message.getBody(String.class));
74+
}
75+
}
76+
}
77+
}

src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexIntegration.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import jakarta.inject.Inject;
3636
import jakarta.ws.rs.HttpMethod;
3737
import jakarta.ws.rs.core.MediaType;
38+
import jakarta.ws.rs.core.Response;
3839

3940
@ApplicationScoped
4041
public class OssIndexIntegration extends EndpointRouteBuilder {
@@ -82,6 +83,31 @@ public void configure() {
8283
.onFallback()
8384
.process(responseHandler::processResponseError);
8485

86+
from(direct("ossIndexHealthCheck"))
87+
.routeId("ossIndexHealthCheck")
88+
.setProperty(Constants.PROVIDER_NAME, constant(Constants.OSS_INDEX_PROVIDER))
89+
.choice()
90+
.when(method(vulnerabilityProvider, "getEnabled").contains(Constants.OSS_INDEX_PROVIDER))
91+
.to(direct("ossCheckVersionEndpoint"))
92+
.otherwise()
93+
.to(direct("healthCheckProviderDisabled"));
94+
95+
from(direct("ossCheckVersionEndpoint"))
96+
.routeId("ossCheckVersionEndpoint")
97+
.circuitBreaker()
98+
.faultToleranceConfiguration()
99+
.timeoutEnabled(true)
100+
.timeoutDuration(timeout)
101+
.end()
102+
.process(this::processVersionRequest)
103+
.to(vertxHttp("{{api.ossindex.host}}"))
104+
.setBody(constant("Service is up and running"))
105+
.setHeader(Exchange.HTTP_RESPONSE_TEXT,constant("Service is up and running"))
106+
.onFallback()
107+
.setBody(constant(Constants.OSS_INDEX_PROVIDER + "Service is down"))
108+
.setHeader(Exchange.HTTP_RESPONSE_CODE,constant(Response.Status.SERVICE_UNAVAILABLE))
109+
.end();
110+
85111
from(direct("ossValidateCredentials"))
86112
.routeId("ossValidateCredentials")
87113
.circuitBreaker()
@@ -120,4 +146,14 @@ private void processComponentRequest(Exchange exchange) {
120146
exchange.setProperty(
121147
Constants.AUTH_PROVIDER_REQ_PROPERTY_PREFIX + Constants.OSS_INDEX_PROVIDER, Boolean.TRUE);
122148
}
149+
150+
private void processVersionRequest(Exchange exchange) {
151+
var message = exchange.getMessage();
152+
message.removeHeader(Exchange.HTTP_PATH);
153+
message.removeHeader(Exchange.HTTP_QUERY);
154+
message.removeHeader(Exchange.HTTP_URI);
155+
message.removeHeader(Constants.ACCEPT_ENCODING_HEADER);
156+
message.setHeader(Exchange.HTTP_METHOD, HttpMethod.GET);
157+
message.setHeader(Exchange.HTTP_PATH, Constants.OSS_INDEX_VERSION_PATH);
158+
}
123159
}

src/main/java/com/redhat/exhort/integration/providers/osvnvd/OsvNvdIntegration.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,21 @@
2323
import org.eclipse.microprofile.config.inject.ConfigProperty;
2424

2525
import com.redhat.exhort.integration.Constants;
26+
import com.redhat.exhort.integration.providers.VulnerabilityProvider;
2627

2728
import jakarta.enterprise.context.ApplicationScoped;
2829
import jakarta.inject.Inject;
2930
import jakarta.ws.rs.HttpMethod;
3031
import jakarta.ws.rs.core.MediaType;
32+
import jakarta.ws.rs.core.Response;
3133

3234
@ApplicationScoped
3335
public class OsvNvdIntegration extends EndpointRouteBuilder {
3436

3537
@ConfigProperty(name = "api.osvnvd.timeout", defaultValue = "10s")
3638
String timeout;
3739

40+
@Inject VulnerabilityProvider vulnerabilityProvider;
3841
@Inject OsvNvdResponseHandler responseHandler;
3942

4043
@Override
@@ -59,18 +62,51 @@ public void configure() throws Exception {
5962
.process(this::processRequest)
6063
.to(vertxHttp("{{api.osvnvd.host}}"))
6164
.transform().method(responseHandler, "responseToIssues");
65+
66+
from(direct("osvNvdHealthCheck"))
67+
.routeId("osvNvdHealthCheck")
68+
.setProperty(Constants.PROVIDER_NAME, constant(Constants.OSV_NVD_PROVIDER))
69+
.choice()
70+
.when(method(vulnerabilityProvider, "getEnabled").contains(Constants.OSV_NVD_PROVIDER))
71+
.to(direct("osvNvdHealthCheckEndpoint"))
72+
.otherwise()
73+
.to(direct("healthCheckProviderDisabled"));
74+
75+
from(direct("osvNvdHealthCheckEndpoint"))
76+
.routeId("osvNvdHealthCheckEndpoint")
77+
.process(this::processHealthRequest)
78+
.circuitBreaker()
79+
.faultToleranceConfiguration()
80+
.timeoutEnabled(true)
81+
.timeoutDuration(timeout)
82+
.end()
83+
.to(vertxHttp("{{api.osvnvd.management.host}}"))
84+
.setHeader(Exchange.HTTP_RESPONSE_TEXT,constant("Service is up and running"))
85+
.setBody(constant("Service is up and running"))
86+
.onFallback()
87+
.setBody(constant(Constants.OSV_NVD_PROVIDER + "Service is down"))
88+
.setHeader(Exchange.HTTP_RESPONSE_CODE,constant(Response.Status.SERVICE_UNAVAILABLE))
89+
.end();
6290
// fmt:on
6391
}
6492

6593
private void processRequest(Exchange exchange) {
6694
var message = exchange.getMessage();
6795
message.removeHeader(Exchange.HTTP_QUERY);
6896
message.removeHeader(Exchange.HTTP_URI);
69-
message.removeHeader("Accept-Encoding");
97+
message.removeHeader(Constants.ACCEPT_ENCODING_HEADER);
7098
message.setHeader(Exchange.CONTENT_TYPE, MediaType.APPLICATION_JSON);
7199
message.setHeader(Exchange.HTTP_PATH, Constants.OSV_NVD_PURLS_PATH);
72100
message.setHeader(Exchange.HTTP_METHOD, HttpMethod.POST);
73-
exchange.setProperty(
74-
Constants.AUTH_PROVIDER_REQ_PROPERTY_PREFIX + Constants.OSV_NVD_PROVIDER, Boolean.FALSE);
101+
}
102+
103+
private void processHealthRequest(Exchange exchange) {
104+
var message = exchange.getMessage();
105+
message.removeHeader(Exchange.HTTP_QUERY);
106+
message.removeHeader(Exchange.HTTP_URI);
107+
message.removeHeader(Constants.ACCEPT_ENCODING_HEADER);
108+
message.removeHeader(Exchange.CONTENT_TYPE);
109+
message.setHeader(Exchange.HTTP_PATH, Constants.OSV_NVD_HEALTH_PATH);
110+
message.setHeader(Exchange.HTTP_METHOD, HttpMethod.GET);
75111
}
76112
}

0 commit comments

Comments
 (0)