Skip to content

Commit 8fbf30a

Browse files
authored
Update Vault config to Retry token validation to mitigate "412 required index state not present" errors validating newly created tokens (#174)
* Retry token validation to mitigate "412 required index state not present" errors validating newly created tokens. The errors seem to result from Vault's eventually consistent approach in a clustered environment which is described at https://developer.hashicorp.com/vault/docs/enterprise/consistency * Avoid an extra retry when attempting to validate the vault token * Reduce validateToken complexity * Improve iterator name * Update CHANGELOG.md * Address PMD violations
1 parent ceb2c10 commit 8fbf30a

File tree

7 files changed

+108
-11
lines changed

7 files changed

+108
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project are documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## 1.7.0
8+
9+
### Updated
10+
11+
- Updates the Vault configuration to retry token validation which is prone to initial fail due to Vaults eventual consistency approach
12+
713
## 1.6.0
814

915
### Added

pom.xml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,15 @@
191191
<scope>test</scope>
192192
</dependency>
193193
<dependency>
194-
<groupId>org.slf4j</groupId>
195-
<artifactId>slf4j-reload4j</artifactId>
196-
<version>${slf4j.version}</version>
194+
<groupId>com.github.valfirst</groupId>
195+
<artifactId>slf4j-test</artifactId>
196+
<version>3.0.1</version>
197+
<scope>test</scope>
198+
</dependency>
199+
<dependency>
200+
<groupId>org.assertj</groupId>
201+
<artifactId>assertj-core</artifactId>
202+
<version>3.24.2</version>
197203
<scope>test</scope>
198204
</dependency>
199205
<dependency>

src/main/java/com/github/sitture/envconfig/EnvConfigProperties.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ EnvConfigVaultProperties getVaultProperties() {
109109
return new EnvConfigVaultProperties(getRequiredConfigKey(EnvConfigUtils.CONFIG_VAULT_ADDRESS_KEY),
110110
getRequiredConfigKey(EnvConfigUtils.CONFIG_VAULT_NAMESPACE_KEY),
111111
getRequiredConfigKey(EnvConfigUtils.CONFIG_VAULT_TOKEN_KEY),
112-
getRequiredConfigKey(EnvConfigUtils.CONFIG_VAULT_SECRET_PATH_KEY));
112+
getRequiredConfigKey(EnvConfigUtils.CONFIG_VAULT_SECRET_PATH_KEY),
113+
Integer.parseInt(getConfigurationProperty(EnvConfigUtils.CONFIG_VAULT_VALIDATE_MAX_RETRIES, "5")));
113114
}
114115
}

src/main/java/com/github/sitture/envconfig/EnvConfigUtils.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class EnvConfigUtils {
2323
public static final String CONFIG_VAULT_NAMESPACE_KEY = CONFIG_PREFIX + "vault.namespace";
2424
public static final String CONFIG_VAULT_SECRET_PATH_KEY = CONFIG_PREFIX + "vault.secret.path";
2525
public static final String CONFIG_VAULT_TOKEN_KEY = CONFIG_PREFIX + "vault.token";
26+
public static final String CONFIG_VAULT_VALIDATE_MAX_RETRIES = CONFIG_PREFIX + "vault.validate.token.max.retries";
2627

2728
private EnvConfigUtils() {
2829
}

src/main/java/com/github/sitture/envconfig/EnvConfigVaultProperties.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ class EnvConfigVaultProperties {
66
private final String namespace;
77
private final String token;
88
private final String secretPath;
9+
private final int validateTokenMaxRetries;
910

10-
EnvConfigVaultProperties(final String address, final String namespace, final String token, final String secretPath) {
11+
EnvConfigVaultProperties(final String address, final String namespace, final String token, final String secretPath, final int validateTokenMaxRetries) {
1112
this.address = address;
1213
this.namespace = namespace;
1314
this.token = token;
1415
this.secretPath = secretPath;
16+
this.validateTokenMaxRetries = validateTokenMaxRetries;
1517
}
1618

1719
String getAddress() {
@@ -30,4 +32,7 @@ String getSecretPath() {
3032
return secretPath;
3133
}
3234

35+
int getValidateTokenMaxRetries() {
36+
return validateTokenMaxRetries;
37+
}
3338
}

src/main/java/com/github/sitture/envconfig/VaultConfiguration.java

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
import org.apache.commons.configuration2.Configuration;
88
import org.apache.commons.configuration2.MapConfiguration;
99
import org.apache.commons.lang3.StringUtils;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import java.util.concurrent.TimeUnit;
1013

1114
class VaultConfiguration {
1215

16+
private static final Logger LOG = LoggerFactory.getLogger(VaultConfiguration.class);
1317
private final Vault vault;
1418
private final EnvConfigVaultProperties vaultProperties;
1519

16-
VaultConfiguration(final EnvConfigVaultProperties vaultProperties) {
20+
VaultConfiguration(final EnvConfigVaultProperties vaultProperties) {
1721
this.vaultProperties = vaultProperties;
1822
try {
1923
final VaultConfig config = new VaultConfig()
@@ -22,10 +26,43 @@ class VaultConfiguration {
2226
.token(vaultProperties.getToken())
2327
.build();
2428
this.vault = Vault.create(config);
25-
// attempt to lookupSelf to validate token
26-
this.vault.auth().lookupSelf();
27-
} catch (VaultException e) {
28-
throw new EnvConfigException("Could not connect to vault", e);
29+
validateToken();
30+
} catch (VaultException vaultException) {
31+
throw new EnvConfigException("Could not connect to vault", vaultException);
32+
}
33+
}
34+
35+
private void validateToken() throws VaultException {
36+
final int validateTokenMaxRetries = this.vaultProperties.getValidateTokenMaxRetries();
37+
for (int i = 0; i < validateTokenMaxRetries; i++) {
38+
try {
39+
this.vault.auth().lookupSelf();
40+
break;
41+
} catch (VaultException vaultException) {
42+
retryUntilMaxMaxRetries(vaultException, i, validateTokenMaxRetries);
43+
}
44+
}
45+
}
46+
47+
private static void retryUntilMaxMaxRetries(final VaultException vaultException, final int attempt, final int validateTokenMaxRetries) {
48+
final long retryInterval = attempt * 2L;
49+
logError(String.format("An exception occurred validating the vault token, will retry in %s seconds", retryInterval), vaultException);
50+
try {
51+
TimeUnit.SECONDS.sleep(retryInterval);
52+
} catch (InterruptedException ex) {
53+
logError("InterruptedException thrown whilst waiting to retry validating the vault token", ex);
54+
}
55+
if (attempt == validateTokenMaxRetries - 1) {
56+
final String message = String.format("Reached CONFIG_VAULT_VALIDATE_MAX_RETRIES limit (%s) attempting to validate token", validateTokenMaxRetries);
57+
logError(message, vaultException);
58+
59+
throw new EnvConfigException(message, vaultException);
60+
}
61+
}
62+
63+
private static void logError(final String message, final Exception exception) {
64+
if (LOG.isErrorEnabled()) {
65+
LOG.error(message, exception);
2966
}
3067
}
3168

src/test/java/com/github/sitture/envconfig/VaultConfigurationTest.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
package com.github.sitture.envconfig;
22

33
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
4+
import com.github.valfirst.slf4jtest.LoggingEvent;
5+
import com.github.valfirst.slf4jtest.TestLogger;
6+
import com.github.valfirst.slf4jtest.TestLoggerFactory;
7+
import io.github.jopenlibs.vault.VaultException;
48
import org.apache.commons.configuration2.Configuration;
59
import org.junit.jupiter.api.Assertions;
610
import org.junit.jupiter.api.Test;
11+
import org.slf4j.event.Level;
712

13+
import java.util.function.Predicate;
14+
15+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
816
import static com.github.tomakehurst.wiremock.client.WireMock.get;
917
import static com.github.tomakehurst.wiremock.client.WireMock.notFound;
1018
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
1119
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
20+
import static com.github.valfirst.slf4jtest.Assertions.assertThat;
21+
1222

23+
@SuppressWarnings("PMD.TooManyStaticImports")
1324
@WireMockTest(httpPort = 8999)
1425
class VaultConfigurationTest {
1526

@@ -35,8 +46,34 @@ void testExceptionWhenSecretNotFound() {
3546
Assertions.assertEquals("Could not find the vault secret: path/to/project/default", exception.getMessage());
3647
}
3748

49+
@Test
50+
void testRetriesWhenSelfLookupFails() {
51+
stubSelfLookupFailure();
52+
final String secretPath = "path/to/project/";
53+
final EnvConfigVaultProperties vaultProperties = getMockVaultProperties(secretPath);
54+
final TestLogger testLogger = TestLoggerFactory.getTestLogger(VaultConfiguration.class);
55+
56+
final EnvConfigException exception = Assertions.assertThrows(
57+
EnvConfigException.class, () -> new VaultConfiguration(vaultProperties).getConfiguration("default"));
58+
59+
Assertions.assertEquals("Reached CONFIG_VAULT_VALIDATE_MAX_RETRIES limit (2) attempting to validate token", exception.getMessage());
60+
Assertions.assertEquals(412, ((VaultException) exception.getCause()).getHttpStatusCode());
61+
Assertions.assertEquals("Vault responded with HTTP status code: 412\nResponse body: ", exception.getCause().getMessage());
62+
63+
final Predicate<LoggingEvent> errorWithVault412Throwable = event -> event.getLevel().equals(Level.ERROR)
64+
&& event.getThrowable().isPresent()
65+
&& "Vault responded with HTTP status code: 412\nResponse body: ".equals(event.getThrowable().get().getMessage());
66+
67+
assertThat(testLogger).hasLogged(errorWithVault412Throwable.and(
68+
event -> "An exception occurred validating the vault token, will retry in 0 seconds".equals(event.getMessage())));
69+
assertThat(testLogger).hasLogged(errorWithVault412Throwable.and(
70+
event -> "An exception occurred validating the vault token, will retry in 2 seconds".equals(event.getMessage())));
71+
assertThat(testLogger).hasLogged(errorWithVault412Throwable.and(
72+
event -> "Reached CONFIG_VAULT_VALIDATE_MAX_RETRIES limit (2) attempting to validate token".equals(event.getMessage())));
73+
}
74+
3875
private EnvConfigVaultProperties getMockVaultProperties(final String secretPath) {
39-
return new EnvConfigVaultProperties("http://localhost:8999", "mock", "mock_token", secretPath);
76+
return new EnvConfigVaultProperties("http://localhost:8999", "mock", "mock_token", secretPath, 2);
4077
}
4178

4279
private void stubGetSecretSuccess() {
@@ -50,6 +87,10 @@ private void stubGetSecretSuccess() {
5087
+ "}\n")));
5188
}
5289

90+
private void stubSelfLookupFailure() {
91+
stubFor(get("/v1/auth/token/lookup-self").willReturn(aResponse().withStatus(412)));
92+
}
93+
5394
private void stubSelfLookupSuccess() {
5495
stubFor(get("/v1/auth/token/lookup-self").willReturn(okJson("{\n"
5596
+ " \"data\": {\n"

0 commit comments

Comments
 (0)