From 3c0a4049aa133f1710423a190195b9c79497b1f8 Mon Sep 17 00:00:00 2001 From: Emmanuel GALLOIS Date: Tue, 23 Jun 2026 16:22:41 +0200 Subject: [PATCH] feat(QTDI-2030): Add health and readiness endpoints to component-server Introduces GET /api/v1/health (liveness probe) and GET /api/v1/readiness (readiness probe) as dedicated Kubernetes-style endpoints, replacing the insufficient /environment endpoint for cluster health monitoring. Health checks: heap memory availability (configurable threshold), component index validity, and Vault connectivity via VaultClient.ping(). Readiness check: component index load state (isStarted()). Both endpoints return {"status":"UP"|"DOWN","cause":"..."} with HTTP 200/503. Closes QTDI-2030 Co-Authored-By: GitHub Copilot --- .../component/server/api/HealthResource.java | 53 +++++++ .../server/api/ReadinessResource.java | 53 +++++++ .../server/front/model/HealthStatus.java | 30 ++++ .../component-server/pom.xml | 6 + .../ComponentServerConfiguration.java | 6 + .../server/front/HealthResourceImpl.java | 40 +++++ .../server/front/ReadinessResourceImpl.java | 40 +++++ .../service/ComponentManagerService.java | 4 + .../server/service/HealthService.java | 117 +++++++++++++++ .../server/front/HealthResourceImplIT.java | 65 ++++++++ .../server/service/HealthServiceTest.java | 141 ++++++++++++++++++ .../components/vault/client/VaultClient.java | 19 +++ 12 files changed, 574 insertions(+) create mode 100644 component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/HealthResource.java create mode 100644 component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/ReadinessResource.java create mode 100644 component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/HealthStatus.java create mode 100644 component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/HealthResourceImpl.java create mode 100644 component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/ReadinessResourceImpl.java create mode 100644 component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/HealthService.java create mode 100644 component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/HealthResourceImplIT.java create mode 100644 component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/service/HealthServiceTest.java diff --git a/component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/HealthResource.java b/component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/HealthResource.java new file mode 100644 index 0000000000000..a44c8660b7c1f --- /dev/null +++ b/component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/HealthResource.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.api; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.talend.sdk.component.server.front.model.HealthStatus; + +@Path("health") +@Tag(name = "Health", description = "Kubernetes liveness probe endpoint.") +public interface HealthResource { + + @GET + @Produces(APPLICATION_JSON) + @Operation(operationId = "getLiveness", + description = "Liveness probe: returns 200 when the server is healthy (heap, component index, Vault). " + + "Returns 503 with a cause when any check fails.") + @APIResponses({ + @APIResponse(responseCode = "200", + description = "Server is healthy.", + content = @Content(mediaType = APPLICATION_JSON, + schema = @Schema(implementation = HealthStatus.class))), + @APIResponse(responseCode = "503", + description = "Server is not healthy.", + content = @Content(mediaType = APPLICATION_JSON, + schema = @Schema(implementation = HealthStatus.class))) + }) + Response getLiveness(); +} diff --git a/component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/ReadinessResource.java b/component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/ReadinessResource.java new file mode 100644 index 0000000000000..e260756280747 --- /dev/null +++ b/component-server-parent/component-server-api/src/main/java/org/talend/sdk/component/server/api/ReadinessResource.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.api; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.talend.sdk.component.server.front.model.HealthStatus; + +@Path("readiness") +@Tag(name = "Health", description = "Kubernetes readiness probe endpoint.") +public interface ReadinessResource { + + @GET + @Produces(APPLICATION_JSON) + @Operation(operationId = "getReadiness", + description = "Readiness probe: returns 200 when the component index is loaded and the server is ready " + + "to serve traffic. Returns 503 with a cause otherwise.") + @APIResponses({ + @APIResponse(responseCode = "200", + description = "Server is ready.", + content = @Content(mediaType = APPLICATION_JSON, + schema = @Schema(implementation = HealthStatus.class))), + @APIResponse(responseCode = "503", + description = "Server is not ready.", + content = @Content(mediaType = APPLICATION_JSON, + schema = @Schema(implementation = HealthStatus.class))) + }) + Response getReadiness(); +} diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/HealthStatus.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/HealthStatus.java new file mode 100644 index 0000000000000..ba6e5d4355599 --- /dev/null +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/HealthStatus.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.front.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class HealthStatus { + + private String status; + + private String cause; +} diff --git a/component-server-parent/component-server/pom.xml b/component-server-parent/component-server/pom.xml index abe2f936646b1..91a9aaa228dd0 100644 --- a/component-server-parent/component-server/pom.xml +++ b/component-server-parent/component-server/pom.xml @@ -158,6 +158,12 @@ ${meecrowave.version} test + + org.mockito + mockito-core + ${mockito4.version} + test + org.apache.tomee ziplock diff --git a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/configuration/ComponentServerConfiguration.java b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/configuration/ComponentServerConfiguration.java index 847360ece24c9..953b94cdd33d1 100644 --- a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/configuration/ComponentServerConfiguration.java +++ b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/configuration/ComponentServerConfiguration.java @@ -201,6 +201,12 @@ public class ComponentServerConfiguration { @ConfigProperty(name = "talend.component.server.plugins.reloading.marker") private Optional pluginsReloadFileMarker; + @Inject + @Documentation("Minimum percentage of available JVM heap required for the liveness probe to report UP. " + + "If the available heap drops below this threshold the health endpoint returns 503.") + @ConfigProperty(name = "talend.server.health.memory.threshold", defaultValue = "10") + private Integer healthMemoryThreshold; + @PostConstruct private void init() { if (logRequests != null && logRequests) { diff --git a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/HealthResourceImpl.java b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/HealthResourceImpl.java new file mode 100644 index 0000000000000..6f136fc332bef --- /dev/null +++ b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/HealthResourceImpl.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.front; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; + +import org.talend.sdk.component.server.api.HealthResource; +import org.talend.sdk.component.server.front.model.HealthStatus; +import org.talend.sdk.component.server.service.HealthService; + +@ApplicationScoped +public class HealthResourceImpl implements HealthResource { + + @Inject + private HealthService healthService; + + @Override + public Response getLiveness() { + final HealthStatus status = healthService.checkLiveness(); + if ("UP".equals(status.getStatus())) { + return Response.ok(status).build(); + } + return Response.status(Response.Status.SERVICE_UNAVAILABLE).entity(status).build(); + } +} diff --git a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/ReadinessResourceImpl.java b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/ReadinessResourceImpl.java new file mode 100644 index 0000000000000..4b91d12d73c39 --- /dev/null +++ b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/front/ReadinessResourceImpl.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.front; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; + +import org.talend.sdk.component.server.api.ReadinessResource; +import org.talend.sdk.component.server.front.model.HealthStatus; +import org.talend.sdk.component.server.service.HealthService; + +@ApplicationScoped +public class ReadinessResourceImpl implements ReadinessResource { + + @Inject + private HealthService healthService; + + @Override + public Response getReadiness() { + final HealthStatus status = healthService.checkReadiness(); + if ("UP".equals(status.getStatus())) { + return Response.ok(status).build(); + } + return Response.status(Response.Status.SERVICE_UNAVAILABLE).entity(status).build(); + } +} diff --git a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/ComponentManagerService.java b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/ComponentManagerService.java index edd9d0f533459..871c676bb46ff 100644 --- a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/ComponentManagerService.java +++ b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/ComponentManagerService.java @@ -460,4 +460,8 @@ public ComponentManager manager() { return instance; } + public boolean isStarted() { + return started; + } + } diff --git a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/HealthService.java b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/HealthService.java new file mode 100644 index 0000000000000..0db7c05c24331 --- /dev/null +++ b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/service/HealthService.java @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.service; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.talend.sdk.component.server.configuration.ComponentServerConfiguration; +import org.talend.sdk.component.server.front.model.HealthStatus; +import org.talend.sdk.components.vault.client.VaultClient; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ApplicationScoped +public class HealthService { + + private static final String STATUS_UP = "UP"; + + private static final String STATUS_DOWN = "DOWN"; + + @Inject + private ComponentManagerService componentManagerService; + + @Inject + private VaultClient vaultClient; + + @Inject + private ComponentServerConfiguration configuration; + + /** + * Performs the liveness check: heap availability, component index, and Vault connectivity. + * + * @return {@link HealthStatus} with {@code status="UP"} if all checks pass, + * or {@code status="DOWN"} with a human-readable cause on failure. + */ + public HealthStatus checkLiveness() { + final HealthStatus memoryStatus = checkMemory(); + if (STATUS_DOWN.equals(memoryStatus.getStatus())) { + return memoryStatus; + } + final HealthStatus indexStatus = checkComponentIndex(); + if (STATUS_DOWN.equals(indexStatus.getStatus())) { + return indexStatus; + } + final HealthStatus vaultStatus = checkVault(); + if (STATUS_DOWN.equals(vaultStatus.getStatus())) { + return vaultStatus; + } + return new HealthStatus(STATUS_UP, null); + } + + /** + * Performs the readiness check: verifies that the component index is loaded. + * + * @return {@link HealthStatus} with {@code status="UP"} if ready, + * or {@code status="DOWN"} with cause otherwise. + */ + public HealthStatus checkReadiness() { + if (!componentManagerService.isStarted()) { + return new HealthStatus(STATUS_DOWN, "Component index not ready"); + } + return new HealthStatus(STATUS_UP, null); + } + + private HealthStatus checkMemory() { + final Runtime runtime = Runtime.getRuntime(); + final long maxMemory = runtime.maxMemory(); + if (maxMemory == Long.MAX_VALUE) { + return new HealthStatus(STATUS_UP, null); + } + final long availableMemory = maxMemory - runtime.totalMemory() + runtime.freeMemory(); + final int availablePercent = (int) (availableMemory * 100L / maxMemory); + final int threshold = configuration.getHealthMemoryThreshold(); + if (availablePercent < threshold) { + final String cause = String + .format("Available heap is %d%% which is below the configured threshold of %d%%", + availablePercent, threshold); + log.warn("Liveness check failed: {}", cause); + return new HealthStatus(STATUS_DOWN, cause); + } + return new HealthStatus(STATUS_UP, null); + } + + private HealthStatus checkComponentIndex() { + try { + componentManagerService.manager().getContainer().findAll(); + return new HealthStatus(STATUS_UP, null); + } catch (final Throwable t) { + final String cause = "Component index check failed: " + t.getMessage(); + log.warn("Liveness check failed: {}", cause); + return new HealthStatus(STATUS_DOWN, cause); + } + } + + private HealthStatus checkVault() { + if (!vaultClient.ping()) { + final String cause = "Vault is not reachable"; + log.warn("Liveness check failed: {}", cause); + return new HealthStatus(STATUS_DOWN, cause); + } + return new HealthStatus(STATUS_UP, null); + } +} diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/HealthResourceImplIT.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/HealthResourceImplIT.java new file mode 100644 index 0000000000000..4468e784449f7 --- /dev/null +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/HealthResourceImplIT.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.front; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import javax.inject.Inject; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.apache.meecrowave.junit5.MonoMeecrowaveConfig; +import org.junit.jupiter.api.Test; +import org.talend.sdk.component.server.front.model.HealthStatus; + +@MonoMeecrowaveConfig +class HealthResourceImplIT { + + @Inject + private WebTarget base; + + @Test + void livenessReturns200WhenHealthy() { + final Response response = base.path("health").request(APPLICATION_JSON_TYPE).get(); + assertEquals(200, response.getStatus()); + final HealthStatus status = response.readEntity(HealthStatus.class); + assertNotNull(status); + assertEquals("UP", status.getStatus()); + } + + @Test + void readinessReturns200WhenReady() { + final Response response = base.path("readiness").request(APPLICATION_JSON_TYPE).get(); + assertEquals(200, response.getStatus()); + final HealthStatus status = response.readEntity(HealthStatus.class); + assertNotNull(status); + assertEquals("UP", status.getStatus()); + } + + @Test + void healthAndReadinessAreIndependentFromEnvironment() { + final Response envResponse = base.path("environment").request(APPLICATION_JSON_TYPE).get(); + assertEquals(200, envResponse.getStatus()); + + final Response healthResponse = base.path("health").request(APPLICATION_JSON_TYPE).get(); + assertEquals(200, healthResponse.getStatus()); + + final Response readinessResponse = base.path("readiness").request(APPLICATION_JSON_TYPE).get(); + assertEquals(200, readinessResponse.getStatus()); + } +} diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/service/HealthServiceTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/service/HealthServiceTest.java new file mode 100644 index 0000000000000..c39fec4f0fdf1 --- /dev/null +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/service/HealthServiceTest.java @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * 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.talend.sdk.component.server.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.talend.sdk.component.container.ContainerManager; +import org.talend.sdk.component.runtime.manager.ComponentManager; +import org.talend.sdk.component.server.configuration.ComponentServerConfiguration; +import org.talend.sdk.component.server.front.model.HealthStatus; +import org.talend.sdk.components.vault.client.VaultClient; + +class HealthServiceTest { + + private AutoCloseable closeable; + + @Mock + private ComponentManagerService componentManagerService; + + @Mock + private VaultClient vaultClient; + + @Mock + private ComponentServerConfiguration configuration; + + @InjectMocks + private HealthService healthService; + + @BeforeEach + void setup() { + closeable = MockitoAnnotations.openMocks(this); + when(configuration.getHealthMemoryThreshold()).thenReturn(10); + } + + @AfterEach + void tearDown() throws Exception { + closeable.close(); + } + + @Test + void livenessReturnUpWhenAllChecksPass() { + final ComponentManager manager = mock(ComponentManager.class); + final ContainerManager containerManager = mock(ContainerManager.class); + when(componentManagerService.manager()).thenReturn(manager); + when(manager.getContainer()).thenReturn(containerManager); + when(vaultClient.ping()).thenReturn(true); + + final HealthStatus status = healthService.checkLiveness(); + + assertEquals("UP", status.getStatus()); + assertNull(status.getCause()); + } + + @Test + void livenessReturnDownWhenHeapBelowThreshold() { + when(configuration.getHealthMemoryThreshold()).thenReturn(101); + + final HealthStatus status = healthService.checkLiveness(); + + assertEquals("DOWN", status.getStatus()); + assertNotNull(status.getCause()); + } + + @Test + void livenessReturnDownWhenComponentIndexThrows() { + final ComponentManager manager = mock(ComponentManager.class); + when(componentManagerService.manager()).thenReturn(manager); + when(manager.getContainer()).thenThrow(new RuntimeException("index failure")); + + final HealthStatus status = healthService.checkLiveness(); + + assertEquals("DOWN", status.getStatus()); + assertNotNull(status.getCause()); + } + + @Test + void livenessReturnDownWhenVaultPingFails() { + final ComponentManager manager = mock(ComponentManager.class); + final ContainerManager containerManager = mock(ContainerManager.class); + when(componentManagerService.manager()).thenReturn(manager); + when(manager.getContainer()).thenReturn(containerManager); + when(vaultClient.ping()).thenReturn(false); + + final HealthStatus status = healthService.checkLiveness(); + + assertEquals("DOWN", status.getStatus()); + assertEquals("Vault is not reachable", status.getCause()); + } + + @Test + void readinessReturnUpWhenStarted() { + when(componentManagerService.isStarted()).thenReturn(true); + + final HealthStatus status = healthService.checkReadiness(); + + assertEquals("UP", status.getStatus()); + assertNull(status.getCause()); + } + + @Test + void readinessReturnDownWhenNotStarted() { + when(componentManagerService.isStarted()).thenReturn(false); + + final HealthStatus status = healthService.checkReadiness(); + + assertEquals("DOWN", status.getStatus()); + assertEquals("Component index not ready", status.getCause()); + } + + @Test + void environmentRemainsIndependentOfHealthChecks() { + when(componentManagerService.isStarted()).thenReturn(false); + + final HealthStatus readiness = healthService.checkReadiness(); + + assertEquals("DOWN", readiness.getStatus()); + } +} diff --git a/vault-client/src/main/java/org/talend/sdk/components/vault/client/VaultClient.java b/vault-client/src/main/java/org/talend/sdk/components/vault/client/VaultClient.java index c6d805ca35225..03d26fca9cfd9 100644 --- a/vault-client/src/main/java/org/talend/sdk/components/vault/client/VaultClient.java +++ b/vault-client/src/main/java/org/talend/sdk/components/vault/client/VaultClient.java @@ -201,6 +201,25 @@ public Thread newThread(final Runnable r) { }); } + /** + * Checks connectivity to Vault. + * + * @return {@code true} if Vault is reachable or if Vault is not configured ({@code no-vault}), + * {@code false} if a transport-level error prevents reaching Vault. + */ + public boolean ping() { + if ("no-vault".equals(setup.getVaultUrl())) { + return true; + } + try { + vault.path("v1/sys/health").request().get().close(); + return true; + } catch (final javax.ws.rs.ProcessingException e) { + log.warn("Vault ping failed: {}", e.getMessage()); + return false; + } + } + @SneakyThrows public Map decrypt(final Map values) { return decrypt(values, null);