diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39ec971f..71fdd2a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: - 2.1.0 - 2.1.1 - 2.1.2 + - 2.2.0 - latest os: - ubuntu-latest diff --git a/README.md b/README.md index 47971243..c0ff308e 100644 --- a/README.md +++ b/README.md @@ -326,16 +326,31 @@ Vault's HTTP API: credentials). * `debug()`: Health check endpoints. -The driver DSL also allows you to specify retry logic, by chaining the `withRetries()` ahead of -accessing the endpoint implementation: +By default, API operations are not retried. To retry every call, set the retries in the +configuration: ``` // Retry up to 5 times if failures occur, waiting 1000 milliseconds in between each retry attempt. -final LogicalResponse response = vault.withRetries(5, 1000) +final VaultConfig config = new VaultConfig() + .address("http://127.0.0.1:8200") + .token("3c9fd6be-7bc2-9d1f-6fb3-cd746c0fc4e8") + .retries(5, 1000) + .build(); +``` + +To retry only some calls, chain `retrying()` ahead of accessing the endpoint implementation. It +returns a new `Vault` sharing the same configuration, and leaves the original `vault` unchanged: + +``` +final LogicalResponse response = vault.retrying(5, 1000) .logical() .read("secret/hello"); ``` +`withRetries()` is deprecated: despite being chained like `retrying()`, it changes the retries in the +configuration, so they apply to every later call, and to every `Vault` sharing that configuration. +Replace it with `retrying()` or `VaultConfig.retries()`. + Only transient failures are retried: I/O errors (e.g. connection refused or timed out) and HTTP 5xx, 408, 412 and 429 responses. Other 4xx responses (e.g. 400, 403, 404) fail immediately, since retrying them would not help. @@ -407,6 +422,17 @@ patches. * Javadoc examples use the `Sys` methods instead of the deprecated ones. The `Wrapping` examples are fixed: they used `new Vault(config)` instead of `Vault.create(config)`, and a misspelled `lookupWarp()` method + * Retries can be limited to some calls [(issue #75)](https://github.com/jopenlibs/vault-java-driver/issues/75). + `Vault.retrying(maxRetries, retryIntervalMilliseconds)` returns a new `Vault` whose operations + use those retries, leaving the original instance and its configuration unchanged. + `VaultConfig.retries(maxRetries, retryIntervalMilliseconds)` sets retries for every call. + Both reject negative values with `IllegalArgumentException`. The operation classes (e.g. + `Logical`, `Sys`, `Pki`) have new constructors taking a `RetryPolicy` + * `Vault.withRetries()` is deprecated, with unchanged behaviour: it writes the retries to the + shared `VaultConfig`, so they apply to every later call and to every `Vault` sharing that + configuration, not only the chained call. Use `retrying()` or `VaultConfig.retries()` instead. + `Vault.retrying()` is a `default` method throwing `UnsupportedOperationException`, so custom + implementations of `Vault` keep compiling * **6.2.3**: This release contains the following updates: * Improved Java 11 compatibility * Fixed integration tests with Vault 2.x diff --git a/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java new file mode 100644 index 00000000..9682bc61 --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java @@ -0,0 +1,73 @@ +package io.github.jopenlibs.vault; + +/** + *

An immutable retry setting for API operations: the maximum number of times a failing + * operation is retried, and the time to wait between attempts.

+ * + *

A policy with maxRetries set to 0 disables retries.

+ * + * @see Vault#retrying(int, int) + * @see VaultConfig#retries(int, int) + */ +public final class RetryPolicy { + + private final int maxRetries; + private final int retryIntervalMilliseconds; + + /** + * Creates a retry policy. + * + * @param maxRetries The number of times that a failing API operation will be retried + * @param retryIntervalMilliseconds The number of milliseconds to wait between retries + * @throws IllegalArgumentException If either value is negative + */ + public RetryPolicy(final int maxRetries, final int retryIntervalMilliseconds) { + if (maxRetries < 0) { + throw new IllegalArgumentException("maxRetries must not be negative: " + maxRetries); + } + if (retryIntervalMilliseconds < 0) { + throw new IllegalArgumentException( + "retryIntervalMilliseconds must not be negative: " + retryIntervalMilliseconds); + } + this.maxRetries = maxRetries; + this.retryIntervalMilliseconds = retryIntervalMilliseconds; + } + + /** + * @return The number of times that a failing API operation will be retried + */ + public int getMaxRetries() { + return maxRetries; + } + + /** + * @return The number of milliseconds to wait between retries + */ + public int getRetryIntervalMilliseconds() { + return retryIntervalMilliseconds; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RetryPolicy)) { + return false; + } + final RetryPolicy other = (RetryPolicy) o; + return maxRetries == other.maxRetries + && retryIntervalMilliseconds == other.retryIntervalMilliseconds; + } + + @Override + public int hashCode() { + return 31 * maxRetries + retryIntervalMilliseconds; + } + + @Override + public String toString() { + return "RetryPolicy{maxRetries=" + maxRetries + + ", retryIntervalMilliseconds=" + retryIntervalMilliseconds + "}"; + } +} diff --git a/src/main/java/io/github/jopenlibs/vault/Vault.java b/src/main/java/io/github/jopenlibs/vault/Vault.java index dffb117f..e696d542 100644 --- a/src/main/java/io/github/jopenlibs/vault/Vault.java +++ b/src/main/java/io/github/jopenlibs/vault/Vault.java @@ -50,17 +50,58 @@ static Vault create(final VaultConfig vaultConfig, final Boolean useSecretsEngin } /** - * This method is chained ahead of endpoints (e.g. logical(), auth(), - * etc... to specify retry rules for any API operations invoked on that endpoint. + *

Sets the retry rules for API operations, by writing them to the configuration of this + * instance.

+ * + *

Despite being chained ahead of endpoints (e.g. logical(), + * auth()), the retries are not limited to that call: they apply to every later + * call on this instance, and on every other Vault sharing the same configuration. + * A Vault returned by {@link #retrying(int, int)} ignores them.

* * @param maxRetries The number of times that API operations will be retried when a failure * occurs * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in * between retries - * @return This object, with maxRetries and retryIntervalMilliseconds populated + * @return This object, with maxRetries and retryIntervalMilliseconds populated in its + * configuration + * @deprecated To retry only some calls, use {@link #retrying(int, int)}, which returns a new + * instance. To retry every call, use {@link VaultConfig#retries(int, int)}. */ + @Deprecated Vault withRetries(final int maxRetries, final int retryIntervalMilliseconds); + /** + *

Returns a new Vault whose API operations are retried with the given rules, + * overriding the retries set in the configuration. This instance, and its configuration, are + * left unchanged:

+ * + *
+ *
{@code
+     * final LogicalResponse response = vault.retrying(5, 1000).logical().read("secret/hello");
+     * }
+ *
+ * + *

The returned instance shares the configuration of this one, so later changes to it (e.g. + * a new token) apply to both.

+ * + *

The default implementation throws UnsupportedOperationException, so that + * existing implementations of this interface keep compiling.

+ * + * @param maxRetries The number of times that API operations will be retried when a failure + * occurs + * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in + * between retries + * @return A new Vault using the given retries + * @throws IllegalArgumentException If either value is negative + * @throws UnsupportedOperationException If this implementation does not support it + * @see RetryPolicy + * @see VaultConfig#retries(int, int) + */ + default Vault retrying(final int maxRetries, final int retryIntervalMilliseconds) { + throw new UnsupportedOperationException( + getClass().getName() + " does not support retrying()"); + } + /** * Returns the implementing class for Vault's core/logical operations (e.g. read, write). * diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index c480c487..190337d3 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -319,12 +319,38 @@ public VaultConfig httpClient(HttpClient httpClient) { return this; } + /** + *

Sets the retry rules for every API operation using this configuration: the maximum number + * of times that a failing operation will be retried, and the time to wait between attempts. + * By default, operations are not retried.

+ * + *

To retry only some calls, use Vault.retrying() instead, which overrides + * these values.

+ * + * @param maxRetries The number of times that API operations will be retried when a failure + * occurs + * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in + * between retries + * @return This object, with maxRetries and retryIntervalMilliseconds populated, ready for + * additional builder-pattern method calls or else finalization with the build() method + * @throws IllegalArgumentException If either value is negative, in which case the previous + * values are kept + * @see Vault#retrying(int, int) + * @see RetryPolicy + */ + public VaultConfig retries(final int maxRetries, final int retryIntervalMilliseconds) { + final var policy = new RetryPolicy(maxRetries, retryIntervalMilliseconds); + this.maxRetries = policy.getMaxRetries(); + this.retryIntervalMilliseconds = policy.getRetryIntervalMilliseconds(); + return this; + } + /** *

Sets the maximum number of times that an API operation will retry upon failure.

* *

This method is not meant to be called from application-level code outside of this package - * (hence the protected access level. It is meant to be invoked via - * Vault.withRetries() in a builder pattern DSL-style.

+ * (hence the package-private access level). It is used by the deprecated + * Vault.withRetries(): use {@link #retries(int, int)} instead.

* * @param maxRetries The number of times that API operations will be retried when a failure * occurs. @@ -338,8 +364,8 @@ void setMaxRetries(final int maxRetries) { * attempts for a failing API operation.

* *

This method is not meant to be called from application-level code outside of this package - * (hence the protected access level. It is meant to be invoked via - * Vault.withRetries() in a builder pattern DSL-style.

+ * (hence the package-private access level). It is used by the deprecated + * Vault.withRetries(): use {@link #retries(int, int)} instead.

* * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in * between retries. diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 04b81768..64bce224 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -43,7 +43,7 @@ * secrets.put("other_value", "You can store multiple name/value pairs under a given key"); * * final LogicalResponse writeResponse = vault - * .withRetries(5, 1000) // optional + * .retrying(5, 1000) // optional * .logical() * .write("secret/hello", secrets); * @@ -58,6 +58,11 @@ public class VaultImpl implements Vault { private final VaultConfig vaultConfig; + /** + * The retries for the operations of this instance, or null to use the retries + * currently set in {@link #vaultConfig}. + */ + private final RetryPolicy retryPolicy; private final Logger logger = Logger.getLogger(VaultImpl.class.getCanonicalName()); /** @@ -69,6 +74,7 @@ public class VaultImpl implements Vault { */ public VaultImpl(final VaultConfig vaultConfig) { this.vaultConfig = vaultConfig; + this.retryPolicy = null; logNameSpace(); if (this.vaultConfig.getSecretsEnginePathMap().isEmpty() && this.vaultConfig.getGlobalEngineVersion() == null) { @@ -95,6 +101,7 @@ public VaultImpl(final VaultConfig vaultConfig, final Integer engineVersion) { } vaultConfig.setEngineVersion(engineVersion); this.vaultConfig = vaultConfig; + this.retryPolicy = null; logNameSpace(); } @@ -115,6 +122,7 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa final Integer globalFallbackVersion) throws VaultException { this.vaultConfig = vaultConfig; + this.retryPolicy = null; logNameSpace(); this.vaultConfig.setEngineVersion(globalFallbackVersion); if (Boolean.TRUE.equals(useSecretsEnginePathMap) && this.vaultConfig.getSecretsEnginePathMap().isEmpty()) { @@ -139,6 +147,15 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa } } + /** + * Used by {@link #retrying(int, int)}: shares the configuration of an existing instance, which + * has already been set up, so the engine version setup is not run again. + */ + private VaultImpl(final VaultConfig vaultConfig, final RetryPolicy retryPolicy) { + this.vaultConfig = vaultConfig; + this.retryPolicy = retryPolicy; + } + private void logNameSpace() { final String nameSpace = this.vaultConfig.getNameSpace(); if (nameSpace != null && !nameSpace.isEmpty()) { @@ -149,22 +166,51 @@ private void logNameSpace() { } /** - * This method is chained ahead of endpoints (e.g. logical(), auth(), - * etc... to specify retry rules for any API operations invoked on that endpoint. + *

Sets the retry rules for API operations, by writing them to the configuration of this + * instance.

+ * + *

Despite being chained ahead of endpoints (e.g. logical(), + * auth()), the retries are not limited to that call: they apply to every later + * call on this instance, and on every other Vault sharing the same configuration. + * A Vault returned by {@link #retrying(int, int)} ignores them.

* * @param maxRetries The number of times that API operations will be retried when a failure * occurs * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in * between retries - * @return This object, with maxRetries and retryIntervalMilliseconds populated + * @return This object, with maxRetries and retryIntervalMilliseconds populated in its + * configuration + * @deprecated To retry only some calls, use {@link #retrying(int, int)}, which returns a new + * instance. To retry every call, use {@link VaultConfig#retries(int, int)}. */ @Override + @Deprecated public VaultImpl withRetries(final int maxRetries, final int retryIntervalMilliseconds) { this.vaultConfig.setMaxRetries(maxRetries); this.vaultConfig.setRetryIntervalMilliseconds(retryIntervalMilliseconds); return this; } + /** + *

Returns a new VaultImpl whose API operations are retried with the given + * rules, overriding the retries set in the configuration. This instance, and its configuration, + * are left unchanged.

+ * + *

The returned instance shares the configuration of this one, so later changes to it (e.g. + * a new token) apply to both.

+ * + * @param maxRetries The number of times that API operations will be retried when a failure + * occurs + * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in + * between retries + * @return A new VaultImpl using the given retries + * @throws IllegalArgumentException If either value is negative + */ + @Override + public VaultImpl retrying(final int maxRetries, final int retryIntervalMilliseconds) { + return new VaultImpl(vaultConfig, new RetryPolicy(maxRetries, retryIntervalMilliseconds)); + } + /** * Returns the implementing class for Vault's core/logical operations (e.g. read, write). * @@ -172,7 +218,7 @@ public VaultImpl withRetries(final int maxRetries, final int retryIntervalMillis */ @Override public Logical logical() { - return new Logical(vaultConfig); + return new Logical(vaultConfig, retryPolicy); } /** @@ -183,7 +229,7 @@ public Logical logical() { */ @Override public Auth auth() { - return new Auth(vaultConfig); + return new Auth(vaultConfig, retryPolicy); } /** @@ -194,7 +240,7 @@ public Auth auth() { */ @Override public Sys sys() { - return new Sys(vaultConfig); + return new Sys(vaultConfig, retryPolicy); } /** @@ -205,7 +251,7 @@ public Sys sys() { */ @Override public Pki pki() { - return new Pki(vaultConfig); + return new Pki(vaultConfig, "pki", retryPolicy); } /** @@ -232,17 +278,17 @@ public Pki pki() { */ @Override public Pki pki(final String mountPath) { - return new Pki(vaultConfig, mountPath); + return new Pki(vaultConfig, mountPath, retryPolicy); } @Override public Database database() { - return new Database(vaultConfig); + return new Database(vaultConfig, "database", retryPolicy); } @Override public Database database(final String mountPath) { - return new Database(vaultConfig, mountPath); + return new Database(vaultConfig, mountPath, retryPolicy); } /** @@ -255,7 +301,7 @@ public Database database(final String mountPath) { @Override @Deprecated public Leases leases() { - return new Leases(vaultConfig); + return new Leases(vaultConfig, retryPolicy); } /** @@ -265,7 +311,7 @@ public Leases leases() { */ @Override public Debug debug() { - return new Debug(vaultConfig); + return new Debug(vaultConfig, retryPolicy); } /** @@ -279,7 +325,7 @@ public Debug debug() { @Override @Deprecated public Mounts mounts() { - return new Mounts(vaultConfig); + return new Mounts(vaultConfig, retryPolicy); } /** @@ -292,7 +338,7 @@ public Mounts mounts() { @Override @Deprecated public Seal seal() { - return new Seal(vaultConfig); + return new Seal(vaultConfig, retryPolicy); } /** diff --git a/src/main/java/io/github/jopenlibs/vault/api/Auth.java b/src/main/java/io/github/jopenlibs/vault/api/Auth.java index 42017d04..55747698 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Auth.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Auth.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.VaultImpl; @@ -254,7 +255,17 @@ public String getEntityAlias() { private String nameSpace; public Auth(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Auth(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } @@ -1153,7 +1164,7 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept */ @Deprecated public LogicalResponse lookupWrap() throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().lookupWrap(config.getToken(), false); } @@ -1163,7 +1174,7 @@ public LogicalResponse lookupWrap() throws VaultException { */ @Deprecated public LogicalResponse lookupWrap(final char[] wrappedToken) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().lookupWrap(wrappedToken, true); } @@ -1174,7 +1185,7 @@ public LogicalResponse lookupWrap(final char[] wrappedToken) throws VaultExcepti @Deprecated public LogicalResponse lookupWrap(final char[] wrappedToken, boolean inBody) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().lookupWrap(wrappedToken, inBody); } @@ -1220,7 +1231,7 @@ public void revokeSelf(final String tokenAuthMount) throws VaultException { */ @Deprecated public UnwrapResponse unwrap() throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().unwrap(config.getToken(), false); } @@ -1230,7 +1241,7 @@ public UnwrapResponse unwrap() throws VaultException { */ @Deprecated public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().unwrap(wrappedToken, true); } @@ -1240,7 +1251,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { */ @Deprecated public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().unwrap(wrappedToken, inBody); } @@ -1250,7 +1261,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws V */ @Deprecated public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().wrap(jsonObject, ttlInSec); } @@ -1260,7 +1271,7 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault */ @Deprecated public WrapResponse rewrap(final char[] wrappedToken) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().rewrap(wrappedToken); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/Debug.java b/src/main/java/io/github/jopenlibs/vault/api/Debug.java index 52832f34..ffe4de32 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Debug.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Debug.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.HealthResponse; @@ -21,7 +22,17 @@ public class Debug extends OperationsBase { private String nameSpace; public Debug(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Debug(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/Logical.java b/src/main/java/io/github/jopenlibs/vault/api/Logical.java index ca7c91e7..9d3fbbcb 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Logical.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Logical.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.json.Json; @@ -37,7 +38,17 @@ public class Logical extends OperationsBase { public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, listV1, listV2, listSubKeys, readV1, readV2, writeV1, writeV2, unDelete, mount} public Logical(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Logical(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } @@ -164,8 +175,8 @@ public LogicalResponse read(final String path, Boolean shouldRetry, final Intege return new LogicalResponse(restResponse, attempt, logicalOperations.readV2); }, - shouldRetry ? config.getMaxRetries() : 1, - config.getRetryIntervalMilliseconds() + shouldRetry ? getMaxRetries() : 1, + getRetryIntervalMilliseconds() ); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java b/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java index 5a0b9f66..7c5940ed 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java +++ b/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.rest.Rest; @@ -14,12 +15,47 @@ public abstract class OperationsBase { protected final VaultConfig config; + /** + * The retries for this instance, or null to use the retries currently set in + * {@link #config}. + */ + protected final RetryPolicy retryPolicy; + protected OperationsBase(VaultConfig config) { + this(config, null); + } + + /** + * @param config The configuration + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration, read on every + * call + */ + protected OperationsBase(VaultConfig config, RetryPolicy retryPolicy) { this.config = config; + this.retryPolicy = retryPolicy; + } + + /** + * @return The maximum number of retries from the retry policy if set, otherwise the one + * currently set in the configuration + */ + protected int getMaxRetries() { + return retryPolicy != null ? retryPolicy.getMaxRetries() : config.getMaxRetries(); + } + + /** + * @return The interval between retries from the retry policy if set, otherwise the one + * currently set in the configuration + */ + protected int getRetryIntervalMilliseconds() { + return retryPolicy != null + ? retryPolicy.getRetryIntervalMilliseconds() + : config.getRetryIntervalMilliseconds(); } protected T retry(final EndpointOperation op) throws VaultException { - return retry(op, config.getMaxRetries(), config.getRetryIntervalMilliseconds()); + return retry(op, getMaxRetries(), getRetryIntervalMilliseconds()); } /** diff --git a/src/main/java/io/github/jopenlibs/vault/api/database/Database.java b/src/main/java/io/github/jopenlibs/vault/api/database/Database.java index b58fee17..4f51a72d 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/database/Database.java +++ b/src/main/java/io/github/jopenlibs/vault/api/database/Database.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.database; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -49,7 +50,23 @@ public Database(final VaultConfig config) { * /v1/ prefix (e.g. "root-ca") */ public Database(final VaultConfig config, final String mountPath) { - super(config); + this(config, mountPath, null); + } + + /** + * Constructor for use when the Database backend is mounted on some non-default custom path, with + * a retry policy. + * + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param mountPath The path on which your Vault Database backend is mounted, without the + * /v1/ prefix (e.g. "db123") + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Database(final VaultConfig config, final String mountPath, + final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.mountPath = mountPath; this.nameSpace = this.config.getNameSpace(); diff --git a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java index 1a3148e8..9fb0463e 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java +++ b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.pki; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -50,7 +51,23 @@ public Pki(final VaultConfig config) { * /v1/ prefix (e.g. "root-ca") */ public Pki(final VaultConfig config, final String mountPath) { - super(config); + this(config, mountPath, null); + } + + /** + * Constructor for use when the PKI backend is mounted on some non-default custom path, with + * a retry policy. + * + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param mountPath The path on which your Vault PKI backend is mounted, without the + * /v1/ prefix (e.g. "root-ca") + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Pki(final VaultConfig config, final String mountPath, + final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.mountPath = mountPath; this.nameSpace = this.config.getNameSpace(); diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java index 10938922..8e06d2dd 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -23,7 +24,17 @@ public class Leases extends OperationsBase { private String nameSpace; public Leases(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Leases(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java index d4bd8e02..976305da 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -23,7 +24,17 @@ public class Seal extends OperationsBase { private String nameSpace; public Seal(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Seal(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java index 98011247..f5d3dc53 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.api.OperationsBase; @@ -19,7 +20,17 @@ public class Sys extends OperationsBase { private String nameSpace; public Sys(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Sys(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } @@ -35,7 +46,7 @@ public Sys withNameSpace(final String nameSpace) { * @return The implementing class for wrapping operations */ public Wrapping wrapping() { - return new Wrapping(this.config); + return new Wrapping(this.config, this.retryPolicy); } /** @@ -44,7 +55,7 @@ public Wrapping wrapping() { * @return The implementing class for Vault's seal operations (e.g. seal, unseal, sealStatus). */ public Seal seal() { - return new Seal(this.config); + return new Seal(this.config, this.retryPolicy); } /** @@ -54,7 +65,7 @@ public Seal seal() { * @return the implementing class for Vault's sys mounts operations */ public Mounts mounts() { - return new Mounts(this.config); + return new Mounts(this.config, this.retryPolicy); } /** @@ -64,6 +75,6 @@ public Mounts mounts() { * @return The implementing class for Vault's lease operations */ public Leases leases() { - return new Leases(this.config); + return new Leases(this.config, this.retryPolicy); } } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java index b8823ff9..bb88489c 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.Logical; @@ -27,7 +28,17 @@ public class Wrapping extends OperationsBase { private String nameSpace; public Wrapping(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Wrapping(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java index 42b655bd..fa2862b6 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys.mounts; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -17,7 +18,17 @@ public class Mounts extends OperationsBase { public Mounts(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Mounts(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); } /** diff --git a/src/test/java/io/github/jopenlibs/vault/RetryPolicyTests.java b/src/test/java/io/github/jopenlibs/vault/RetryPolicyTests.java new file mode 100644 index 00000000..914d5f51 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/RetryPolicyTests.java @@ -0,0 +1,53 @@ +package io.github.jopenlibs.vault; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +public class RetryPolicyTests { + + @Test + public void testValues() { + final var policy = new RetryPolicy(5, 1000); + assertEquals(5, policy.getMaxRetries()); + assertEquals(1000, policy.getRetryIntervalMilliseconds()); + } + + @Test + public void testZeroValuesAllowed() { + final var policy = new RetryPolicy(0, 0); + assertEquals(0, policy.getMaxRetries()); + assertEquals(0, policy.getRetryIntervalMilliseconds()); + } + + @Test + public void testNegativeMaxRetriesRejected() { + assertThrows(IllegalArgumentException.class, () -> new RetryPolicy(-1, 1000)); + } + + @Test + public void testNegativeIntervalRejected() { + assertThrows(IllegalArgumentException.class, () -> new RetryPolicy(5, -1)); + } + + @Test + public void testEqualsAndHashCode() { + final var policy = new RetryPolicy(5, 1000); + final var same = new RetryPolicy(5, 1000); + assertEquals(policy, policy); + assertEquals(policy, same); + assertEquals(policy.hashCode(), same.hashCode()); + assertNotEquals(policy, new RetryPolicy(4, 1000)); + assertNotEquals(policy, new RetryPolicy(5, 999)); + assertNotEquals(policy, null); + assertNotEquals(policy, "RetryPolicy"); + } + + @Test + public void testToString() { + assertEquals("RetryPolicy{maxRetries=5, retryIntervalMilliseconds=1000}", + new RetryPolicy(5, 1000).toString()); + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/RetryTests.java b/src/test/java/io/github/jopenlibs/vault/RetryTests.java index 2b021f9b..cb47620d 100644 --- a/src/test/java/io/github/jopenlibs/vault/RetryTests.java +++ b/src/test/java/io/github/jopenlibs/vault/RetryTests.java @@ -1,11 +1,18 @@ package io.github.jopenlibs.vault; +import io.github.jopenlibs.vault.api.OperationsBase; import io.github.jopenlibs.vault.mock.RetriesMockVault; import io.github.jopenlibs.vault.response.LogicalResponse; +import java.util.List; import java.util.Map; import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; /** *

Unit tests for the Vault driver, having no dependency on an actual Vault server instance @@ -117,4 +124,213 @@ public void testNoRetries_Write_400() throws Exception { VaultTestUtils.shutdownMockVault(server); } + private static final String READ_RESPONSE = + "{\"lease_id\":\"12345\",\"renewable\":false,\"lease_duration\":10000,\"data\":{\"value\":\"mock\"}}"; + + @Test + public void testRetrying_Read() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(5, 200, READ_RESPONSE)); + server.start(); + try { + final var vault = Vault.create(mockConfig()); + final LogicalResponse response = vault.retrying(5, 10).logical().read("secret/hello"); + assertEquals(5, response.getRetries()); + assertEquals("mock", response.getData().get("value")); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_LeavesOriginalUnchanged() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(1, 200, READ_RESPONSE)); + server.start(); + try { + final var config = mockConfig(); + final var vault = Vault.create(config); + assertNotSame(vault, vault.retrying(5, 10)); + assertEquals(0, config.getMaxRetries()); + assertEquals(0, config.getRetryIntervalMilliseconds()); + + final var e = assertThrows(VaultException.class, + () -> vault.logical().read("secret/hello")); + assertEquals(500, e.getHttpStatusCode()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_OverridesConfigRetries() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(3, 200, READ_RESPONSE)); + server.start(); + try { + final var vault = Vault.create(mockConfig()); + vault.withRetries(5, 10); + // 1 retry is fewer than the 3 failures, while the config retries would be enough + final var e = assertThrows(VaultException.class, + () -> vault.retrying(1, 10).logical().read("secret/hello")); + assertEquals(500, e.getHttpStatusCode()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_SysLeasesGetsPolicy() throws Exception { + final var server = VaultTestUtils.initHttpMockVault(new RetriesMockVault(2, 204, null)); + server.start(); + try { + final var vault = Vault.create(mockConfig()); + final var response = vault.retrying(2, 10).sys().leases().revoke("lease-id"); + assertEquals(2, response.getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_SharesConfig() throws Exception { + final var config = mockConfig(); + final var retrying = Vault.create(config).retrying(5, 10); + assertSame(config, baseFieldOf(retrying.auth(), "config")); + config.token("new_token"); + assertArrayEquals("new_token".toCharArray(), + ((VaultConfig) baseFieldOf(retrying.logical(), "config")).getToken()); + } + + @Test + @SuppressWarnings("deprecation") + public void testRetrying_EveryOperationGetsPolicy() throws Exception { + final var vault = Vault.create(mockConfig()); + final var policy = new RetryPolicy(5, 10); + final var retrying = vault.retrying(5, 10); + for (final OperationsBase operations : List.of(retrying.logical(), retrying.auth(), + retrying.sys(), retrying.pki(), retrying.pki("root-ca"), retrying.database(), + retrying.database("db123"), retrying.leases(), retrying.debug(), + retrying.mounts(), retrying.seal())) { + assertEquals(operations.getClass().getName(), policy, retryPolicyOf(operations)); + } + for (final OperationsBase operations : List.of(vault.logical(), vault.pki(), + vault.database())) { + assertNull(operations.getClass().getName(), retryPolicyOf(operations)); + } + } + + @Test + public void testRetrying_PkiAndDatabaseUseDefaultMountPaths() throws Exception { + final var retrying = Vault.create(mockConfig()).retrying(5, 10); + assertEquals("pki", fieldOf(retrying.pki(), "mountPath")); + assertEquals("database", fieldOf(retrying.database(), "mountPath")); + } + + @Test + public void testRetrying_RejectsNegativeValues() throws Exception { + final var vault = Vault.create(mockConfig()); + assertThrows(IllegalArgumentException.class, () -> vault.retrying(-1, 10)); + assertThrows(IllegalArgumentException.class, () -> vault.retrying(5, -1)); + } + + @Test + public void testConfigRetries_SetsValues() throws Exception { + final var config = new VaultConfig(); + assertSame(config, config.retries(5, 1000)); + assertEquals(5, config.getMaxRetries()); + assertEquals(1000, config.getRetryIntervalMilliseconds()); + } + + @Test + public void testConfigRetries_AppliesToEveryCall() throws Exception { + final var vault = Vault.create(new VaultConfig().address("http://127.0.0.1:8999") + .token("mock_token").engineVersion(1).retries(2, 10).build()); + + final var readServer = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(2, 200, READ_RESPONSE)); + readServer.start(); + try { + assertEquals(2, vault.logical().read("secret/hello").getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(readServer); + } + + final var revokeServer = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(2, 204, null)); + revokeServer.start(); + try { + assertEquals(2, vault.sys().leases().revoke("lease-id").getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(revokeServer); + } + } + + @Test + public void testConfigRetries_RejectsNegativeValues() throws Exception { + final var config = new VaultConfig().retries(5, 1000); + assertThrows(IllegalArgumentException.class, () -> config.retries(-1, 10)); + assertThrows(IllegalArgumentException.class, () -> config.retries(3, -1)); + assertEquals(5, config.getMaxRetries()); + assertEquals(1000, config.getRetryIntervalMilliseconds()); + } + + @Test + public void testConfigRetries_OverriddenByRetrying() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(3, 200, READ_RESPONSE)); + server.start(); + try { + final var vault = Vault.create(mockConfig().retries(5, 10)); + final var e = assertThrows(VaultException.class, + () -> vault.retrying(1, 10).logical().read("secret/hello")); + assertEquals(500, e.getHttpStatusCode()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + @SuppressWarnings("deprecation") + public void testWithRetries_Unchanged() throws Exception { + final var config = mockConfig(); + final var vault = Vault.create(config); + final var other = Vault.create(config); + final var retrying = vault.retrying(1, 10); + + assertSame(vault, vault.withRetries(5, 1000)); + assertEquals(5, config.getMaxRetries()); + assertEquals(1000, config.getRetryIntervalMilliseconds()); + // Shared by every Vault using the config, but ignored by one returned by retrying() + assertNull(retryPolicyOf(other.logical())); + assertEquals(new RetryPolicy(1, 10), retryPolicyOf(retrying.logical())); + + // Negative values are still written to the config, not validated + vault.withRetries(-1, -1); + assertEquals(-1, config.getMaxRetries()); + assertEquals(-1, config.getRetryIntervalMilliseconds()); + } + + private static VaultConfig mockConfig() throws VaultException { + return new VaultConfig().address("http://127.0.0.1:8999") + .token("mock_token").engineVersion(1).build(); + } + + private static Object retryPolicyOf(final OperationsBase operations) throws Exception { + return baseFieldOf(operations, "retryPolicy"); + } + + private static Object baseFieldOf(final OperationsBase operations, final String name) + throws Exception { + final var field = OperationsBase.class.getDeclaredField(name); + field.setAccessible(true); + return field.get(operations); + } + + private static Object fieldOf(final Object target, final String name) throws Exception { + final var field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(target); + } + } diff --git a/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java b/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java index 6c5e2388..ee96719a 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java +++ b/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java @@ -20,6 +20,7 @@ import org.junit.rules.ExpectedException; import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; /** @@ -152,12 +153,13 @@ public void testRead() throws VaultException { @Test public void testReadExceptionNotFound() throws VaultException { - expectedEx.expect(VaultException.class); - expectedEx.expectMessage("cannot fetch sysview for path"); - final Vault vault = container.getRootVault(); - vault.sys().mounts().read("pki-non-existing-path"); + // Vault 2.2 and later answer "invalid request" instead of "cannot fetch sysview for path", + // so only the status code is checked + final VaultException e = assertThrows(VaultException.class, + () -> vault.sys().mounts().read("pki-non-existing-path")); + assertEquals(400, e.getHttpStatusCode()); } @Test diff --git a/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java b/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java index e339f8a8..3dbb3a64 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java +++ b/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java @@ -1,5 +1,8 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.rest.Rest; import io.github.jopenlibs.vault.rest.RestException; @@ -10,6 +13,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -107,6 +111,94 @@ public void testStopsRetryingWhenInterrupted() { assertTrue(Thread.currentThread().isInterrupted()); } + @Test + public void testNullPolicyUsesConfigRetries() throws Exception { + final var config = configWithRetries(2, 1); + final var operations = new TestOperations(config); + assertNull(operations.retryPolicy); + assertEquals(2, operations.getMaxRetries()); + assertEquals(1, operations.getRetryIntervalMilliseconds()); + assertEquals(3, operations.countAttempts()); + } + + @Test + public void testNullPolicyReadsConfigOnEveryCall() throws Exception { + final var config = configWithRetries(1, 1); + final var operations = new TestOperations(config, null); + assertEquals(2, operations.countAttempts()); + + Vault.create(config).withRetries(3, 1); + assertEquals(3, operations.getMaxRetries()); + assertEquals(4, operations.countAttempts()); + } + + @Test + public void testNullPolicyKeepsNegativeConfigValues() throws Exception { + final var config = configWithRetries(-1, -5); + final var operations = new TestOperations(config); + assertEquals(-1, operations.getMaxRetries()); + assertEquals(-5, operations.getRetryIntervalMilliseconds()); + assertEquals(1, operations.countAttempts()); + } + + @Test + public void testPolicyOverridesConfigRetries() throws Exception { + final var config = configWithRetries(5, 1000); + final var policy = new RetryPolicy(1, 2); + final var operations = new TestOperations(config, policy); + assertSame(policy, operations.retryPolicy); + assertEquals(1, operations.getMaxRetries()); + assertEquals(2, operations.getRetryIntervalMilliseconds()); + assertEquals(2, operations.countAttempts()); + + Vault.create(config).withRetries(3, 1); + assertEquals(2, operations.countAttempts()); + assertEquals(3, config.getMaxRetries()); + } + + @Test + public void testZeroPolicyDisablesRetries() throws Exception { + final var operations = new TestOperations(configWithRetries(5, 1), new RetryPolicy(0, 0)); + assertEquals(1, operations.countAttempts()); + } + + private static VaultConfig configWithRetries(final int maxRetries, + final int retryIntervalMilliseconds) throws VaultException { + final var config = new VaultConfig().address("http://127.0.0.1:8999") + .token("mock_token").engineVersion(1).build(); + Vault.create(config).withRetries(maxRetries, retryIntervalMilliseconds); + return config; + } + + private static final class TestOperations extends OperationsBase { + + TestOperations(final VaultConfig config) { + super(config); + } + + TestOperations(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); + } + + /** + * @return The number of attempts made for an operation that always fails with a transient + * error + */ + int countAttempts() { + final var calls = new AtomicInteger(); + try { + retry(attempt -> { + calls.incrementAndGet(); + throw new VaultException("unavailable", 503); + }); + fail("Expected VaultException"); + } catch (VaultException e) { + assertEquals(503, e.getHttpStatusCode()); + } + return calls.get(); + } + } + private static RestException connectionRefused() { try { new Rest().url("http://127.0.0.1:1/").connectTimeoutSeconds(1).get(); diff --git a/src/test/java/io/github/jopenlibs/vault/api/RetryPolicyPropagationTests.java b/src/test/java/io/github/jopenlibs/vault/api/RetryPolicyPropagationTests.java new file mode 100644 index 00000000..e621cc03 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/api/RetryPolicyPropagationTests.java @@ -0,0 +1,105 @@ +package io.github.jopenlibs.vault.api; + +import io.github.jopenlibs.vault.RetryPolicy; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.VaultTestUtils; +import io.github.jopenlibs.vault.api.database.Database; +import io.github.jopenlibs.vault.api.pki.Pki; +import io.github.jopenlibs.vault.api.sys.Leases; +import io.github.jopenlibs.vault.api.sys.Seal; +import io.github.jopenlibs.vault.api.sys.Sys; +import io.github.jopenlibs.vault.api.sys.Wrapping; +import io.github.jopenlibs.vault.api.sys.mounts.Mounts; +import io.github.jopenlibs.vault.mock.RetriesMockVault; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +/** + * Tests that the operation classes keep the {@link RetryPolicy} they are given, and pass it to the + * operation classes they create. + */ +public class RetryPolicyPropagationTests { + + private static final RetryPolicy POLICY = new RetryPolicy(3, 10); + + @Test + public void testConstructorsKeepPolicy() throws VaultException { + final var config = config(); + assertSame(POLICY, new Logical(config, POLICY).retryPolicy); + assertSame(POLICY, new Auth(config, POLICY).retryPolicy); + assertSame(POLICY, new Debug(config, POLICY).retryPolicy); + assertSame(POLICY, new Sys(config, POLICY).retryPolicy); + assertSame(POLICY, new Leases(config, POLICY).retryPolicy); + assertSame(POLICY, new Seal(config, POLICY).retryPolicy); + assertSame(POLICY, new Wrapping(config, POLICY).retryPolicy); + assertSame(POLICY, new Mounts(config, POLICY).retryPolicy); + assertSame(POLICY, new Pki(config, "pki", POLICY).retryPolicy); + assertSame(POLICY, new Database(config, "database", POLICY).retryPolicy); + } + + @Test + public void testConfigOnlyConstructorsHaveNoPolicy() throws VaultException { + final var config = config(); + assertNull(new Logical(config).retryPolicy); + assertNull(new Auth(config).retryPolicy); + assertNull(new Debug(config).retryPolicy); + assertNull(new Sys(config).retryPolicy); + assertNull(new Leases(config).retryPolicy); + assertNull(new Seal(config).retryPolicy); + assertNull(new Wrapping(config).retryPolicy); + assertNull(new Mounts(config).retryPolicy); + assertNull(new Pki(config).retryPolicy); + assertNull(new Pki(config, "root-ca").retryPolicy); + assertNull(new Database(config).retryPolicy); + assertNull(new Database(config, "db123").retryPolicy); + } + + @Test + public void testPkiAndDatabaseKeepMountPathConstructorWithNull() throws VaultException { + final var config = config(); + // Must still compile: a (VaultConfig, RetryPolicy) overload would make these ambiguous + assertNull(new Pki(config, null).retryPolicy); + assertNull(new Database(config, null).retryPolicy); + } + + @Test + public void testSysPassesPolicy() throws VaultException { + final var sys = new Sys(config(), POLICY); + assertSame(POLICY, sys.wrapping().retryPolicy); + assertSame(POLICY, sys.seal().retryPolicy); + assertSame(POLICY, sys.mounts().retryPolicy); + assertSame(POLICY, sys.leases().retryPolicy); + } + + @Test + public void testSysWithoutPolicyPassesNull() throws VaultException { + final var sys = new Sys(config()); + assertNull(sys.wrapping().retryPolicy); + assertNull(sys.seal().retryPolicy); + assertNull(sys.mounts().retryPolicy); + assertNull(sys.leases().retryPolicy); + } + + @Test + public void testAuthPassesPolicyToSys() throws Exception { + // The config allows no retries, so the call only succeeds if Auth passes its policy on + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(2, 200, "{\"data\":{\"token\":\"mock\"}}")); + server.start(); + try { + final var response = new Auth(config(), POLICY).lookupWrap(); + assertEquals(2, response.getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + private static VaultConfig config() throws VaultException { + return new VaultConfig().address("http://127.0.0.1:8999").token("mock_token") + .engineVersion(1).build(); + } +}