org.apache.maven.plugins
diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java
index ad79122..4495ec1 100644
--- a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java
+++ b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java
@@ -68,23 +68,49 @@ public SecretCache() {
* using the
* provided builder.
*
+ *
+ * Any UserAgent suffix already configured on the builder is preserved: the
+ * caching client identifier ({@code AwsSecretCache/}) is appended to
+ * it rather than overwriting it.
+ *
* @param builder The builder to use for creating the AWS Secrets Manager
* client.
*/
@SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW",
justification = "Delegates to constructor that validates before field initialization")
public SecretCache(SecretsManagerClientBuilder builder) {
- this(new SecretCacheConfiguration().withClient(builder
- .overrideConfiguration(
- builder.overrideConfiguration().toBuilder()
- .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, VersionInfo.USER_AGENT)
- .build())
- .build()));
+ this(new SecretCacheConfiguration().withClient(buildClientWithUserAgent(builder)));
+ }
+
+ /**
+ * Builds a client from the provided builder, appending the caching client
+ * UserAgent identifier while preserving any suffix the caller already set.
+ *
+ * @param builder The caller-provided builder.
+ * @return The built AWS Secrets Manager client.
+ */
+ private static SecretsManagerClient buildClientWithUserAgent(SecretsManagerClientBuilder builder) {
+ ClientOverrideConfiguration existingOverride = builder.overrideConfiguration();
+ String callerSuffix = existingOverride
+ .advancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX).orElse(null);
+ ClientOverrideConfiguration mergedOverride = existingOverride.toBuilder()
+ .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX,
+ VersionInfo.userAgentSuffix(callerSuffix))
+ .build();
+ return builder.overrideConfiguration(mergedOverride).build();
}
/**
* Constructs a new secret cache using the provided AWS Secrets Manager client.
*
+ *
+ * Note: a pre-built {@link SecretsManagerClient} is immutable, so the caching
+ * client UserAgent identifier ({@code AwsSecretCache/}) cannot be
+ * appended to it here. Callers who want the caching identifier reported in
+ * service logs should instead pass a {@link SecretsManagerClientBuilder} (via
+ * {@link #SecretCache(SecretsManagerClientBuilder)}), which preserves any
+ * existing suffix and appends the caching identifier.
+ *
* @param client The AWS Secrets Manager client to use for requesting secret
* values.
*/
@@ -117,7 +143,8 @@ public SecretCache(SecretCacheConfiguration config) {
this.cache = new LRUCache(config.getMaxCacheSize());
this.config = config;
ClientOverrideConfiguration defaultOverride = ClientOverrideConfiguration.builder()
- .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, VersionInfo.USER_AGENT).build();
+ .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX,
+ VersionInfo.userAgentSuffix(null)).build();
if (config.getClient() != null) {
this.client = config.getClient();
diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/internal/VersionInfo.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/internal/VersionInfo.java
index 116f01b..1312350 100644
--- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/internal/VersionInfo.java
+++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/internal/VersionInfo.java
@@ -13,21 +13,107 @@
package com.amazonaws.secretsmanager.caching.cache.internal;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
/**
* This class specifies the versioning system for the AWS SecretsManager caching
* client.
+ *
+ *
+ * The library version is defined in a single place ({@code pom.xml}) and made
+ * available at runtime through the {@code version.properties} classpath resource,
+ * whose {@code ${project.version}} placeholder is substituted at build time via
+ * Maven resource filtering. This avoids the version drift that occurs when the
+ * version has to be maintained in more than one place.
*/
-public class VersionInfo {
+public final class VersionInfo {
+
+ /** Placeholder returned when the version cannot be resolved at runtime. */
+ public static final String UNKNOWN_VERSION = "unknown";
+
+ /** Prefix identifying this caching client in the SDK UserAgent header. */
+ public static final String USER_AGENT_PREFIX = "AwsSecretCache/";
+
+ /** Name of the filtered classpath resource holding the version. */
+ static final String VERSION_RESOURCE = "version.properties";
+
+ /** Properties key under which the version is stored. */
+ static final String VERSION_KEY = "version";
+
/**
- * Library version number
+ * Library version number, resolved at runtime from {@code pom.xml} via the
+ * filtered {@code version.properties} resource. Falls back to
+ * {@link #UNKNOWN_VERSION} when the resource is unavailable.
*/
- public static final String RELEASE_VERSION = "2.1.0";
+ public static final String RELEASE_VERSION = resolveVersion();
/**
* User agent for AWS Secrets Manager API calls.
*/
- public static final String USER_AGENT = "AwsSecretCache/" + RELEASE_VERSION;
+ public static final String USER_AGENT = USER_AGENT_PREFIX + RELEASE_VERSION;
private VersionInfo() {
}
-}
\ No newline at end of file
+
+ /**
+ * Resolves the library version from the filtered {@code version.properties}
+ * classpath resource. Never throws; returns {@link #UNKNOWN_VERSION} if the
+ * resource is missing or unreadable, so that a failure to resolve the version
+ * can never break secret retrieval.
+ *
+ * @return the resolved version, or {@link #UNKNOWN_VERSION}.
+ */
+ private static String resolveVersion() {
+ return readVersion(VersionInfo.class.getResourceAsStream(VERSION_RESOURCE));
+ }
+
+ /**
+ * Parses the {@code version} property from the given stream, closing the
+ * stream before returning.
+ *
+ * @param in the properties stream (may be {@code null}); this method takes
+ * ownership and closes it.
+ * @return the version value, or {@link #UNKNOWN_VERSION} when the stream is
+ * {@code null}, unreadable, missing the key, blank, or still contains
+ * an unsubstituted Maven placeholder.
+ */
+ public static String readVersion(final InputStream in) {
+ if (in == null) {
+ return UNKNOWN_VERSION;
+ }
+ try (InputStream stream = in) {
+ Properties properties = new Properties();
+ properties.load(stream);
+ String version = properties.getProperty(VERSION_KEY);
+ if (version == null) {
+ return UNKNOWN_VERSION;
+ }
+ version = version.trim();
+ if (version.isEmpty() || version.contains("${")) {
+ return UNKNOWN_VERSION;
+ }
+ return version;
+ } catch (IOException e) {
+ return UNKNOWN_VERSION;
+ }
+ }
+
+ /**
+ * Builds the UserAgent suffix for the caching client, preserving any suffix a
+ * caller has already configured. When a caller suffix is present, the caching
+ * identifier is appended to it ({@code " AwsSecretCache/"});
+ * otherwise the caching identifier is returned on its own.
+ *
+ * @param callerSuffix the caller-provided UserAgent suffix (may be {@code null}
+ * or blank).
+ * @return the combined UserAgent suffix.
+ */
+ public static String userAgentSuffix(final String callerSuffix) {
+ if (callerSuffix == null || callerSuffix.trim().isEmpty()) {
+ return USER_AGENT;
+ }
+ return callerSuffix + " " + USER_AGENT;
+ }
+}
diff --git a/src/main/resources/com/amazonaws/secretsmanager/caching/cache/internal/version.properties b/src/main/resources/com/amazonaws/secretsmanager/caching/cache/internal/version.properties
new file mode 100644
index 0000000..defbd48
--- /dev/null
+++ b/src/main/resources/com/amazonaws/secretsmanager/caching/cache/internal/version.properties
@@ -0,0 +1 @@
+version=${project.version}
diff --git a/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java
index 3e96373..e3ee640 100644
--- a/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java
+++ b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java
@@ -24,6 +24,7 @@
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -32,6 +33,8 @@
import org.testng.annotations.Test;
import software.amazon.awssdk.core.SdkBytes;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;
import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest;
@@ -39,6 +42,8 @@
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
+import com.amazonaws.secretsmanager.caching.cache.internal.VersionInfo;
+
/**
* SecretCacheTest.
*/
@@ -526,4 +531,41 @@ public void testSecretCacheWithPQTLSEnabledNoClient() {
}
}
}
+
+ @Test
+ public void builderPathPreservesAndAppendsUserAgent() {
+ // A caller who already configured their own UserAgent suffix on the builder.
+ SecretsManagerClientBuilder builder = Mockito.mock(SecretsManagerClientBuilder.class);
+ ClientOverrideConfiguration existing = ClientOverrideConfiguration.builder()
+ .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, "MyApp/9.9").build();
+ Mockito.when(builder.overrideConfiguration()).thenReturn(existing);
+ Mockito.when(builder.overrideConfiguration(Mockito.any(ClientOverrideConfiguration.class))).thenReturn(builder);
+ Mockito.when(builder.build()).thenReturn(asm);
+
+ new SecretCache(builder).close();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ClientOverrideConfiguration.class);
+ Mockito.verify(builder).overrideConfiguration(captor.capture());
+ String suffix = captor.getValue().advancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX).orElse(null);
+
+ Assert.assertEquals(suffix, "MyApp/9.9 " + VersionInfo.USER_AGENT);
+ }
+
+ @Test
+ public void builderPathSetsOursWhenNoCallerSuffix() {
+ // A caller who did not configure any UserAgent suffix.
+ SecretsManagerClientBuilder builder = Mockito.mock(SecretsManagerClientBuilder.class);
+ ClientOverrideConfiguration existing = ClientOverrideConfiguration.builder().build();
+ Mockito.when(builder.overrideConfiguration()).thenReturn(existing);
+ Mockito.when(builder.overrideConfiguration(Mockito.any(ClientOverrideConfiguration.class))).thenReturn(builder);
+ Mockito.when(builder.build()).thenReturn(asm);
+
+ new SecretCache(builder).close();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ClientOverrideConfiguration.class);
+ Mockito.verify(builder).overrideConfiguration(captor.capture());
+ String suffix = captor.getValue().advancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX).orElse(null);
+
+ Assert.assertEquals(suffix, VersionInfo.USER_AGENT);
+ }
}
diff --git a/src/test/java/com/amazonaws/secretsmanager/caching/internal/VersionInfoTest.java b/src/test/java/com/amazonaws/secretsmanager/caching/internal/VersionInfoTest.java
index d28b555..73166ce 100644
--- a/src/test/java/com/amazonaws/secretsmanager/caching/internal/VersionInfoTest.java
+++ b/src/test/java/com/amazonaws/secretsmanager/caching/internal/VersionInfoTest.java
@@ -1,5 +1,9 @@
package com.amazonaws.secretsmanager.caching.internal;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
import org.testng.Assert;
@@ -8,6 +12,7 @@
import com.amazonaws.secretsmanager.caching.cache.internal.VersionInfo;
public class VersionInfoTest {
+
@Test
public void versionInfoIsValid() {
String ua = VersionInfo.USER_AGENT;
@@ -15,4 +20,72 @@ public void versionInfoIsValid() {
Assert.assertTrue(p.matcher(ua).matches(), "User agent " + ua + " is not valid");
}
+
+ @Test
+ public void releaseVersionResolvesToSemver() {
+ Pattern p = Pattern.compile("\\d+\\.\\d+\\.\\d+");
+
+ Assert.assertTrue(p.matcher(VersionInfo.RELEASE_VERSION).matches(),
+ "RELEASE_VERSION should resolve to a semver but was " + VersionInfo.RELEASE_VERSION);
+ }
+
+ @Test
+ public void readVersionParsesValidStream() {
+ InputStream in = new ByteArrayInputStream("version=1.2.3".getBytes(StandardCharsets.UTF_8));
+
+ Assert.assertEquals(VersionInfo.readVersion(in), "1.2.3");
+ }
+
+ @Test
+ public void readVersionFallsBackOnNullStream() {
+ Assert.assertEquals(VersionInfo.readVersion(null), VersionInfo.UNKNOWN_VERSION);
+ }
+
+ @Test
+ public void readVersionFallsBackWhenKeyMissing() {
+ InputStream in = new ByteArrayInputStream("foo=bar".getBytes(StandardCharsets.UTF_8));
+
+ Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
+ }
+
+ @Test
+ public void readVersionFallsBackOnUnfilteredPlaceholder() {
+ InputStream in = new ByteArrayInputStream("version=${project.version}".getBytes(StandardCharsets.UTF_8));
+
+ Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
+ }
+
+ @Test
+ public void readVersionFallsBackOnEmptyValue() {
+ InputStream in = new ByteArrayInputStream("version=".getBytes(StandardCharsets.UTF_8));
+
+ Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
+ }
+
+ @Test
+ public void readVersionFallsBackOnIoException() {
+ InputStream in = new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new IOException("simulated read failure");
+ }
+ };
+
+ Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
+ }
+
+ @Test
+ public void userAgentSuffixAppendsCallerSuffix() {
+ Assert.assertEquals(VersionInfo.userAgentSuffix("MyApp/9.9"), "MyApp/9.9 " + VersionInfo.USER_AGENT);
+ }
+
+ @Test
+ public void userAgentSuffixWithNullCaller() {
+ Assert.assertEquals(VersionInfo.userAgentSuffix(null), VersionInfo.USER_AGENT);
+ }
+
+ @Test
+ public void userAgentSuffixWithBlankCaller() {
+ Assert.assertEquals(VersionInfo.userAgentSuffix(" "), VersionInfo.USER_AGENT);
+ }
}