Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@

import org.springframework.http.HttpEntity;
import org.springframework.util.Assert;
import org.springframework.vault.client.VaultClientResponseException;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.support.JacksonCompat;
import org.springframework.vault.support.VaultHealth;

/**
* Default implementation of {@link ReactiveVaultSysOperations}.
*
* @author Mark Paluch
* @author Henk Hofs
*/
public class ReactiveVaultSysTemplate implements ReactiveVaultSysOperations {

Expand Down Expand Up @@ -70,8 +73,24 @@ public Mono<VaultHealth> health() {
.retrieve()
.toEntity(VaultSysTemplate.VaultHealthImpl.class)
.filter(HttpEntity::hasBody)
.map(HttpEntity::getBody);
.map(HttpEntity::getBody)
.onErrorResume(VaultClientResponseException.class,
ReactiveVaultSysTemplate::deserializeHealthResponse)
.cast(VaultHealth.class);
});
}

private static Mono<VaultSysTemplate.VaultHealthImpl> deserializeHealthResponse(
VaultClientResponseException responseError) {

try {
return Mono.just(JacksonCompat.instance()
.getObjectMapperAccessor()
.deserialize(responseError.getResponseBodyAsString(), VaultSysTemplate.VaultHealthImpl.class));
}
catch (Exception jsonError) {
return Mono.error(responseError);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
* Default implementation of {@link VaultSysOperations}.
*
* @author Mark Paluch
* @author Henk Hofs
*/
public class VaultSysTemplate implements VaultSysOperations {

Expand Down Expand Up @@ -454,6 +455,8 @@ static class VaultHealthImpl implements VaultHealth {

private final boolean replicationRecoverySecondary;

private final @Nullable Boolean haConnectionHealthy;

private final int serverTimeUtc;

private final @Nullable String version;
Expand All @@ -463,6 +466,7 @@ static class VaultHealthImpl implements VaultHealth {
@JsonProperty("standby") boolean standby,
@JsonProperty("performance_standby") boolean performanceStandby,
@Nullable @JsonProperty("replication_dr_mode") String replicationRecoverySecondary,
@Nullable @JsonProperty("ha_connection_healthy") Boolean haConnectionHealthy,
@JsonProperty("server_time_utc") int serverTimeUtc, @Nullable @JsonProperty("version") String version) {

this.initialized = initialized;
Expand All @@ -471,6 +475,7 @@ static class VaultHealthImpl implements VaultHealth {
this.performanceStandby = performanceStandby;
this.replicationRecoverySecondary = replicationRecoverySecondary != null
&& !"disabled".equalsIgnoreCase(replicationRecoverySecondary);
this.haConnectionHealthy = haConnectionHealthy;
this.serverTimeUtc = serverTimeUtc;
this.version = version;
}
Expand All @@ -496,6 +501,11 @@ public boolean isRecoveryReplicationSecondary() {
return this.replicationRecoverySecondary;
}

@Override
public @Nullable Boolean getHaConnectionHealthy() {
return this.haConnectionHealthy;
}

public int getServerTimeUtc() {
return this.serverTimeUtc;
}
Expand All @@ -513,13 +523,15 @@ public boolean equals(Object o) {
return this.initialized == that.initialized && this.sealed == that.sealed && this.standby == that.standby
&& this.performanceStandby == that.performanceStandby
&& this.replicationRecoverySecondary == that.replicationRecoverySecondary
&& this.serverTimeUtc == that.serverTimeUtc && Objects.equals(this.version, that.version);
&& this.serverTimeUtc == that.serverTimeUtc
&& Objects.equals(this.haConnectionHealthy, that.haConnectionHealthy)
&& Objects.equals(this.version, that.version);
}

@Override
public int hashCode() {
return Objects.hash(this.initialized, this.sealed, this.standby, this.performanceStandby,
this.replicationRecoverySecondary, this.serverTimeUtc, this.version);
this.replicationRecoverySecondary, this.haConnectionHealthy, this.serverTimeUtc, this.version);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* @author Stuart Ingram
* @author Bill Koch
* @author Mark Paluch
* @author Henk Hofs
*/
public interface VaultHealth {

Expand Down Expand Up @@ -59,6 +60,17 @@ public interface VaultHealth {
*/
boolean isRecoveryReplicationSecondary();

/**
* @return {@literal true} if the standby node is connected to the active node,
* {@literal false} if not, or {@literal null} if Vault did not report the HA
* connection state.
* @since 4.2
*/
@Nullable
default Boolean getHaConnectionHealthy() {
return null;
}

/**
* @return the server time in seconds, UTC.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2026-present the original author or authors.
*
* 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
*
* https://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.springframework.vault.core;

import java.io.IOException;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.vault.client.ReactiveVaultClient;
import org.springframework.vault.client.VaultEndpoint;

import static org.assertj.core.api.Assertions.*;

/**
* Unit tests for {@link ReactiveVaultSysTemplate}.
*
* @author Henk Hofs
*/
class ReactiveVaultSysTemplateUnitTests {

private static final MockWebServer mockWebServer = new MockWebServer();

static VaultEndpoint localhost = new VaultEndpoint();

@BeforeAll
static void beforeAll() throws IOException {
mockWebServer.start();
localhost.setHost("localhost");
localhost.setPort(mockWebServer.getPort());
localhost.setScheme("http");
}

@AfterAll
static void afterAll() throws IOException {
mockWebServer.shutdown();
}

@Test
void shouldDeserializeHealthErrorResponse() throws Exception {

mockWebServer.enqueue(new MockResponse().setResponseCode(429)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("""
{
"initialized": true,
"sealed": false,
"standby": true,
"performance_standby": false,
"ha_connection_healthy": true,
"server_time_utc": 123,
"version": "1.20.0"
}
"""));

ReactiveVaultSysOperations sysOperations = new ReactiveVaultSysTemplate(
new ReactiveVaultTemplate(ReactiveVaultClient.create(localhost)));

sysOperations.health().as(StepVerifier::create).assertNext(health -> {
assertThat(health.isInitialized()).isTrue();
assertThat(health.isSealed()).isFalse();
assertThat(health.isStandby()).isTrue();
assertThat(health.isPerformanceStandby()).isFalse();
assertThat(health.getHaConnectionHealthy()).isTrue();
assertThat(health.getServerTimeUtc()).isEqualTo(123);
assertThat(health.getVersion()).isEqualTo("1.20.0");
}).verifyComplete();

RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.GET.name());
assertThat(recordedRequest.getPath()).isEqualTo("/v1/sys/health");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@ class VaultSysTemplateUnitTests {
void shouldReportRecoveryReplicationMode() {

VaultSysTemplate.VaultHealthImpl disabled = new VaultSysTemplate.VaultHealthImpl(true, true, true, true,
"disabled", 0, null);
"disabled", null, 0, null);

assertThat(disabled.isRecoveryReplicationSecondary()).isFalse();

VaultSysTemplate.VaultHealthImpl enabled = new VaultSysTemplate.VaultHealthImpl(true, true, true, true,
"enabled", 0, null);
"enabled", null, 0, null);

assertThat(enabled.isRecoveryReplicationSecondary()).isTrue();
}

@Test
void shouldReportHaConnectionHealth() {

VaultSysTemplate.VaultHealthImpl health = new VaultSysTemplate.VaultHealthImpl(true, false, true, false,
"disabled", true, 0, null);

assertThat(health.getHaConnectionHealthy()).isTrue();
}

}