From 6a4a2a19e8d71ff8447958504d945b4840f6dc5d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 19 Apr 2023 09:38:27 +0200 Subject: [PATCH] Add reactive health check. Closes gh-783 --- .../vault/core/ReactiveVaultOperations.java | 6 ++ .../core/ReactiveVaultSysOperations.java | 47 ++++++++++++ .../vault/core/ReactiveVaultSysTemplate.java | 75 +++++++++++++++++++ .../vault/core/ReactiveVaultTemplate.java | 5 ++ .../vault/core/VaultSysTemplate.java | 3 +- ...ctiveVaultSysTemplateIntegrationTests.java | 70 +++++++++++++++++ 6 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysOperations.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysTemplate.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultSysTemplateIntegrationTests.java diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java index df0c94254..ffceb3143 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java @@ -64,6 +64,12 @@ public interface ReactiveVaultOperations { */ ReactiveVaultTransitOperations opsForTransit(String path); + /** + * @return the operations interface administrative Vault access. + * @since 3.1 + */ + ReactiveVaultSysOperations opsForSys(); + /** * Read from a Vault path. Reading data using this method is suitable for API * calls/secret backends that do not require a request body. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysOperations.java new file mode 100644 index 000000000..2a6d87fc8 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysOperations.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2023 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 reactor.core.publisher.Mono; + +import org.springframework.vault.VaultException; +import org.springframework.vault.support.VaultHealth; + +/** + * Interface that specifies a basic set of administrative Vault operations using reactive + * infrastructure. + * + * @author Mark Paluch + * @since 3.1 + */ +public interface ReactiveVaultSysOperations { + + /** + * @return {@literal true} if Vault is initialized. + * @see GET + * /sys/init + */ + Mono isInitialized() throws VaultException; + + /** + * Return the health status of Vault. + * @return the {@link VaultHealth}. + * @see GET + * /sys/health + */ + Mono health() throws VaultException; + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysTemplate.java new file mode 100644 index 000000000..67c8761b9 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultSysTemplate.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016-2023 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.util.Map; + +import org.springframework.http.HttpEntity; +import org.springframework.util.Assert; +import org.springframework.vault.client.VaultHttpHeaders; +import org.springframework.vault.support.VaultHealth; + +import reactor.core.publisher.Mono; + +/** + * Default implementation of {@link ReactiveVaultSysOperations}. + * + * @author Mark Paluch + */ +public class ReactiveVaultSysTemplate implements ReactiveVaultSysOperations { + + private final ReactiveVaultOperations vaultOperations; + + /** + * Create a new {@link ReactiveVaultSysTemplate} with the given + * {@link ReactiveVaultOperations}. + * @param vaultOperations must not be {@literal null}. + */ + public ReactiveVaultSysTemplate(ReactiveVaultOperations vaultOperations) { + + Assert.notNull(vaultOperations, "ReactiveVaultOperations must not be null"); + + this.vaultOperations = vaultOperations; + + } + + @Override + public Mono isInitialized() { + + return this.vaultOperations.doWithSession(webClient -> { + return webClient.get() + .uri("sys/init") + .header(VaultHttpHeaders.VAULT_NAMESPACE, "") + .exchangeToMono(clientResponse -> clientResponse.toEntity(Map.class)) + .map(it -> (Boolean) it.getBody().get("initialized")); + }); + } + + @Override + public Mono health() { + + return this.vaultOperations.doWithVault(webClient -> { + + return webClient.get() + .uri("sys/health") + .header(VaultHttpHeaders.VAULT_NAMESPACE, "") + .exchangeToMono(clientResponse -> { + return clientResponse.toEntity(VaultSysTemplate.VaultHealthImpl.class).map(HttpEntity::getBody); + }); + }); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java index aa5dbfbd2..3ad44a11e 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java @@ -227,6 +227,11 @@ private ExchangeFilterFunction getSessionFilter() { })); } + @Override + public ReactiveVaultSysOperations opsForSys() { + return new ReactiveVaultSysTemplate(this); + } + @Override public ReactiveVaultTransitOperations opsForTransit() { return opsForTransit("transit"); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java index a119aad57..841bfba59 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java @@ -398,8 +398,7 @@ public VaultHealth doWithRestOperations(RestOperations restOperations) { catch (RestClientResponseException responseError) { try { - ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(responseError.getResponseBodyAsString(), VaultHealthImpl.class); + return OBJECT_MAPPER.readValue(responseError.getResponseBodyAsString(), VaultHealthImpl.class); } catch (Exception jsonError) { throw responseError; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultSysTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultSysTemplateIntegrationTests.java new file mode 100644 index 000000000..552a515dd --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultSysTemplateIntegrationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2023 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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.RequiresVaultVersion; + +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.*; + +/** + * Integration tests for {@link ReactiveVaultSysTemplate} through + * {@link ReactiveVaultSysOperations}. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class) +class ReactiveVaultSysTemplateIntegrationTests extends IntegrationTestSupport { + + @Autowired + ReactiveVaultOperations vaultOperations; + + ReactiveVaultSysOperations adminOperations; + + @BeforeEach + void before() { + this.adminOperations = this.vaultOperations.opsForSys(); + } + + @Test + @RequiresVaultVersion("0.6.1") + void shouldReportHealth() { + + this.adminOperations.health().as(StepVerifier::create).assertNext(health -> { + assertThat(health.isInitialized()).isTrue(); + assertThat(health.isSealed()).isFalse(); + assertThat(health.isPerformanceStandby()).isFalse(); + assertThat(health.isRecoveryReplicationSecondary()).isFalse(); + assertThat(health.isStandby()).isFalse(); + }).verifyComplete(); + } + + @Test + void isInitializedShouldReturnTrue() { + this.adminOperations.isInitialized().as(StepVerifier::create).expectNext(true).verifyComplete(); + } + +}