Bypass Vault namespace for healthchecks

Apparently sys/init and sys/health are not available from within a namespace.

Closes gh-386.
This commit is contained in:
Mark Paluch
2020-02-13 09:26:04 +01:00
parent a042bc88f0
commit 8dca66fb47
4 changed files with 187 additions and 42 deletions

View File

@@ -28,6 +28,7 @@ import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.lang.Nullable;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.core.ReactiveVaultOperations;
import org.springframework.vault.support.VaultHealth;
import org.springframework.web.reactive.function.client.WebClientResponseException;
@@ -72,7 +73,8 @@ public class VaultReactiveHealthIndicator extends AbstractReactiveHealthIndicato
protected Mono<Health> doHealthCheck(Builder builder) {
return this.vaultOperations
.doWithSession((it) -> it.get().uri("sys/health").exchange())
.doWithSession((it) -> it.get().uri("sys/health")
.header(VaultHttpHeaders.VAULT_NAMESPACE, "").exchange())
.flatMap((it) -> it.bodyToMono(VaultHealthImpl.class))
.onErrorResume(WebClientResponseException.class,
VaultReactiveHealthIndicator::deserializeError)

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2019-2020 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.cloud.vault.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Assume;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.cloud.vault.util.TestRestTemplateFactory;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.authentication.SimpleSessionManager;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.ClientHttpConnectorFactory;
import org.springframework.vault.client.ClientHttpRequestFactoryFactory;
import org.springframework.vault.client.RestTemplateBuilder;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.client.WebClientBuilder;
import org.springframework.vault.core.ReactiveVaultTemplate;
import org.springframework.vault.core.VaultSysOperations;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.Policy;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.support.VaultTokenRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Vault's namespace feature.
*/
@RunWith(SpringRunner.class)
public class VaultNamespaceTests {
@ClassRule
public static VaultRule vaultRule = new VaultRule();
static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*")
.capabilities(Policy.BuiltinCapabilities.READ,
Policy.BuiltinCapabilities.CREATE, Policy.BuiltinCapabilities.UPDATE)
.build());
RestTemplateBuilder maketingRestTemplate;
WebClientBuilder marketingWebClientBuilder = WebClientBuilder.builder()
.httpConnector(ClientHttpConnectorFactory.create(new ClientOptions(),
Settings.createSslConfiguration()))
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "marketing");
String marketingToken;
@Before
public void before() {
Assume.assumeTrue("Namespaces require enterprise version",
this.vaultRule.prepare().getVersion().isEnterprise());
List<String> namespaces = new ArrayList<>(Arrays.asList("dev/", "marketing/"));
List<String> list = this.vaultRule.prepare().getVaultOperations()
.list("sys/namespaces");
namespaces.removeAll(list);
for (String namespace : namespaces) {
this.vaultRule.prepare().getVaultOperations()
.write("sys/namespaces/" + namespace.replaceAll("/", ""));
}
this.maketingRestTemplate = RestTemplateBuilder.builder()
.requestFactory(ClientHttpRequestFactoryFactory
.create(new ClientOptions(), Settings.createSslConfiguration()))
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "marketing");
VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(Settings.token())));
mountKv(marketing, "marketing-secrets");
marketing.opsForSys().createOrUpdatePolicy("relaxed", POLICY);
this.marketingToken = marketing.opsForToken()
.create(VaultTokenRequest.builder().withPolicy("relaxed").build())
.getToken().getToken();
}
private void mountKv(VaultTemplate template, String path) {
VaultSysOperations vaultSysOperations = template.opsForSys();
Map<String, VaultMount> mounts = vaultSysOperations.getMounts();
if (!mounts.containsKey(path + "/")) {
vaultSysOperations.mount(path, VaultMount.builder().type("kv")
.options(Collections.singletonMap("version", "1")).build());
}
}
@Test
public void shouldReportHealth() {
VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(this.marketingToken)));
Health.Builder builder = Health.unknown();
new VaultHealthIndicator(marketing).doHealthCheck(builder);
assertThat(builder.build().getStatus()).isEqualTo(Status.UP);
}
@Test
public void shouldReportReactiveHealth() {
ReactiveVaultTemplate reactiveMarketing = new ReactiveVaultTemplate(
this.marketingWebClientBuilder,
() -> Mono.just(VaultToken.of(this.marketingToken)));
Health.Builder builder = Health.unknown();
new VaultReactiveHealthIndicator(reactiveMarketing).doHealthCheck(builder)
.as(StepVerifier::create)
.assertNext(actual -> assertThat(actual.getStatus()).isEqualTo(Status.UP))
.verifyComplete();
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.vault.util;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -39,12 +40,14 @@ public final class Version implements Comparable<Version> {
private final int build;
private final boolean enterprise;
/**
* Creates a new {@link Version} from the given integer values. At least one value has
* to be given but a maximum of 4.
* @param parts must not be {@literal null} or empty.
*/
private Version(int... parts) {
private Version(boolean enterprise, int... parts) {
Assert.notNull(parts, "Parts must not be null!");
Assert.isTrue(parts.length > 0 && parts.length < 5,
@@ -54,6 +57,7 @@ public final class Version implements Comparable<Version> {
this.minor = parts.length > 1 ? parts[1] : 0;
this.bugfix = parts.length > 2 ? parts[2] : 0;
this.build = parts.length > 3 ? parts[3] : 0;
this.enterprise = enterprise;
Assert.isTrue(this.major >= 0, "Major version must be greater or equal zero!");
Assert.isTrue(this.minor >= 0, "Minor version must be greater or equal zero!");
@@ -72,6 +76,7 @@ public final class Version implements Comparable<Version> {
String[] parts = version.trim().split("\\.");
int[] intParts = new int[parts.length];
boolean enterprise = version.endsWith("+ent");
for (int i = 0; i < parts.length; i++) {
@@ -89,7 +94,7 @@ public final class Version implements Comparable<Version> {
}
}
return new Version(intParts);
return new Version(enterprise, intParts);
}
/**
@@ -169,42 +174,8 @@ public final class Version implements Comparable<Version> {
return 0;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Version)) {
return false;
}
Version that = (Version) obj;
return this.major == that.major && this.minor == that.minor
&& this.bugfix == that.bugfix && this.build == that.build;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = 17;
result += 31 * this.major;
result += 31 * this.minor;
result += 31 * this.bugfix;
result += 31 * this.build;
return result;
public boolean isEnterprise() {
return this.enterprise;
}
/*
@@ -215,7 +186,7 @@ public final class Version implements Comparable<Version> {
@Override
public String toString() {
List<Integer> digits = new ArrayList<>();
List<Integer> digits = new ArrayList<Integer>();
digits.add(this.major);
digits.add(this.minor);
@@ -227,7 +198,28 @@ public final class Version implements Comparable<Version> {
digits.add(this.build);
}
return StringUtils.collectionToDelimitedString(digits, ".");
return StringUtils.collectionToDelimitedString(digits, ".")
+ (isEnterprise() ? "+ent" : "");
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Version)) {
return false;
}
Version version = (Version) o;
return this.major == version.major && this.minor == version.minor
&& this.bugfix == version.bugfix && this.build == version.build
&& this.enterprise == version.enterprise;
}
@Override
public int hashCode() {
return Objects.hash(this.major, this.minor, this.bugfix, this.build,
this.enterprise);
}
}

View File

@@ -20,7 +20,7 @@
<description>Spring Cloud Vault Dependencies</description>
<properties>
<spring-vault.version>2.2.1.RELEASE</spring-vault.version>
<spring-vault.version>2.2.2.BUILD-SNAPSHOT</spring-vault.version>
</properties>
<dependencyManagement>