diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 836f7e0..ce873e0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,6 +36,10 @@ To send us a pull request, please:
5. Send us a pull request, answering any default questions in the pull request interface.
6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
+Please enable the removal of trailing whitespaces in your IDE. In JetBrains IDEs, enable the following settings under `Settings -> Editor -> General`:
+
+
+
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
diff --git a/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs b/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs
index c728808..7fb4cab 100644
--- a/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs
+++ b/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs
@@ -31,14 +31,26 @@ public interface ISecretsManagerCache : IDisposable
SecretCacheItem GetCachedSecret(string secretId);
///
- /// Asynchronously retrieves the specified SecretBinary after calling .
+ /// Asynchronously retrieves the specified SecretBinary after calling .
+ /// If both versionId and versionStage are specified, versionId takes precedence.
///
- Task GetSecretBinary(string secretId, CancellationToken cancellationToken = default);
+ /// The secret identifier. This can be the full ARN or the friendly name for the secret.
+ /// The version identifier.
+ /// The version stage.
+ /// The cancellation token used for the Secrets Manager API call.
+ /// The SecretBinary.
+ Task GetSecretBinary(string secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default);
///
- /// Asynchronously retrieves the specified SecretString after calling .
+ /// Asynchronously retrieves the specified SecretString after calling .
+ /// If both versionId and versionStage are specified, versionId takes precedence.
///
- Task GetSecretString(string secretId, CancellationToken cancellationToken = default);
+ /// The secret identifier. This can be the full ARN or the friendly name for the secret.
+ /// The version identifier.
+ /// The version stage.
+ /// The cancellation token used for the Secrets Manager API call.
+ /// The SecretString.
+ Task GetSecretString(string secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default);
///
/// Requests the secret value from SecretsManager asynchronously and updates the cache entry with any changes.
@@ -47,4 +59,4 @@ public interface ISecretsManagerCache : IDisposable
///
Task RefreshNowAsync(string secretId, CancellationToken cancellationToken = default);
}
-}
\ No newline at end of file
+}
diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs
index 29dbd36..c71c2f6 100755
--- a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs
+++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs
@@ -25,15 +25,15 @@ namespace Amazon.SecretsManager.Extensions.Caching
///
public class SecretCacheItem : SecretCacheObject
{
- /// The cached secret value versions for this cached secret.
+ /// The cached secret value versions for this cached secret.
private readonly MemoryCache versions = new MemoryCache(new MemoryCacheOptions());
private const ushort MAX_VERSIONS_CACHE_SIZE = 10;
-
+
public SecretCacheItem(String secretId, IAmazonSecretsManager client, SecretCacheConfiguration config)
: base(secretId, client, config)
{
}
-
+
///
/// Asynchronously retrieves the most current DescribeSecretResponse from Secrets Manager
/// as part of the Refresh operation.
@@ -46,9 +46,9 @@ protected override async Task ExecuteRefreshAsync(Cancel
///
/// Asynchronously retrieves the GetSecretValueResponse from the proper SecretCacheVersion.
///
- protected override async Task GetSecretValueAsync(DescribeSecretResponse result, CancellationToken cancellationToken = default)
+ protected override async Task GetSecretValueAsync(DescribeSecretResponse result, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default)
{
- SecretCacheVersion version = GetVersion(result);
+ SecretCacheVersion version = GetVersion(result, versionId, versionStage);
if (version == null)
{
return null;
@@ -75,13 +75,19 @@ public override bool Equals(object obj)
/// Retrieves the SecretCacheVersion corresponding to the Version Stage
/// specified by the SecretCacheConfiguration.
///
- private SecretCacheVersion GetVersion(DescribeSecretResponse describeResult)
+ private SecretCacheVersion GetVersion(DescribeSecretResponse describeResult, string versionId = null, string versionStage = null)
{
if (null == describeResult?.VersionIdsToStages) return null;
String currentVersionId = null;
foreach (KeyValuePair> entry in describeResult.VersionIdsToStages)
{
- if (entry.Value.Contains(config.VersionStage))
+ if (versionId != null && entry.Key.Equals(versionId))
+ {
+ currentVersionId = versionId;
+ break;
+ }
+
+ if ((versionStage != null && entry.Value.Contains(versionStage)) || entry.Value.Contains(config.VersionStage))
{
currentVersionId = entry.Key;
break;
@@ -108,4 +114,4 @@ private void TrimCacheToSizeLimit()
versions.Compact((double)(versions.Count - config.MaxCacheSize) / versions.Count);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs
index 776446e..45217de 100755
--- a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs
+++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs
@@ -22,20 +22,20 @@ namespace Amazon.SecretsManager.Extensions.Caching
public abstract class SecretCacheObject
{
- /// The number of milliseconds to wait after an exception.
+ /// The number of milliseconds to wait after an exception.
private const long EXCEPTION_BACKOFF = 1000;
- /// The growth factor of the backoff duration.
+ /// The growth factor of the backoff duration.
private const long EXCEPTION_BACKOFF_GROWTH_FACTOR = 2;
-
+
/// The maximum number of milliseconds to wait before retrying a failed
/// request.
private const long BACKOFF_PLATEAU = 128 * EXCEPTION_BACKOFF;
- private JitteredDelay EXCEPTION_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF),
- TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF),
+ private JitteredDelay EXCEPTION_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF),
+ TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF),
TimeSpan.FromMilliseconds(BACKOFF_PLATEAU));
-
+
/// When forcing a refresh using the refreshNow method, a random sleep
/// will be performed using this value. This helps prevent code from
/// executing a refreshNow in a continuous loop without waiting.
@@ -45,35 +45,35 @@ public abstract class SecretCacheObject
private JitteredDelay FORCE_REFRESH_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(FORCE_REFRESH_JITTER_BASE_INCREMENT),
TimeSpan.FromMilliseconds(FORCE_REFRESH_JITTER_VARIANCE));
- /// The secret identifier for this cached object.
+ /// The secret identifier for this cached object.
protected String secretId;
- /// A private object to synchronize access to certain methods.
+ /// A private object to synchronize access to certain methods.
protected static readonly SemaphoreSlim Lock = new SemaphoreSlim(1,1);
- /// The AWS Secrets Manager client to use for requesting secrets.
+ /// The AWS Secrets Manager client to use for requesting secrets.
protected IAmazonSecretsManager client;
/// The Secret Cache Configuration.
protected SecretCacheConfiguration config;
- /// A flag to indicate a refresh is needed.
+ /// A flag to indicate a refresh is needed.
private bool refreshNeeded = true;
/// The result of the last AWS Secrets Manager request for this item.
private Object data = null;
-
+
/// If the last request to AWS Secrets Manager resulted in an exception,
/// that exception will be thrown back to the caller when requesting
/// secret data.
protected Exception exception = null;
-
+
/// The number of exceptions encountered since the last successfully
/// AWS Secrets Manager request. This is used to calculate an exponential
/// backoff.
private long exceptionCount = 0;
-
+
/// The time to wait before retrying a failed AWS Secrets Manager request.
private long nextRetryTime = 0;
@@ -93,10 +93,10 @@ public SecretCacheObject(String secretId, IAmazonSecretsManager client, SecretCa
this.client = client;
this.config = config;
}
-
+
protected abstract Task ExecuteRefreshAsync(CancellationToken cancellationToken = default);
- protected abstract Task GetSecretValueAsync(T result, CancellationToken cancellationToken = default);
+ protected abstract Task GetSecretValueAsync(T result, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default);
///
/// Return the typed result object.
@@ -209,7 +209,7 @@ public async Task RefreshNowAsync(CancellationToken cancellationToken = de
/// If the secret is due for a refresh, the refresh will occur before the result is returned.
/// If the refresh fails, the cached result is returned, or the cached exception is thrown.
///
- public async Task GetSecretValue(CancellationToken cancellationToken)
+ public async Task GetSecretValue(CancellationToken cancellationToken, string versionId = null, string versionStage = null)
{
bool success = false;
await Lock.WaitAsync(cancellationToken);
@@ -226,7 +226,7 @@ public async Task GetSecretValue(CancellationToken cance
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception).Throw();
}
- return await GetSecretValueAsync(GetResult(), cancellationToken);
+ return await GetSecretValueAsync(GetResult(), versionId, versionStage, cancellationToken);
}
}
}
diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs
index 8725544..0943acc 100755
--- a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs
+++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs
@@ -56,7 +56,7 @@ protected override async Task ExecuteRefreshAsync(Cancel
return await this.client.GetSecretValueAsync(new GetSecretValueRequest { SecretId = this.secretId, VersionId = this.versionId }, cancellationToken);
}
- protected override Task GetSecretValueAsync(GetSecretValueResponse result, CancellationToken cancellationToken = default)
+ protected override Task GetSecretValueAsync(GetSecretValueResponse result, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default)
{
return Task.FromResult(result);
}
diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs
index ce77a6d..c5529ab 100755
--- a/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs
+++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs
@@ -86,24 +86,36 @@ public void Dispose()
}
///
- /// Asynchronously retrieves the specified SecretString after calling .
+ /// Asynchronously retrieves the specified SecretString after calling .
+ /// If both versionId and versionStage are specified, versionId takes precedence.
///
- public async Task GetSecretString(String secretId, CancellationToken cancellationToken = default)
+ /// The secret identifier. This can be the full ARN or the friendly name for the secret.
+ /// The version identifier.
+ /// The version stage.
+ /// The cancellation token used for the Secrets Manager API call.
+ /// The SecretString.
+ public async Task GetSecretString(String secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default)
{
SecretCacheItem secret = GetCachedSecret(secretId);
GetSecretValueResponse response = null;
- response = await secret.GetSecretValue(cancellationToken);
+ response = await secret.GetSecretValue(cancellationToken, versionId, versionStage);
return response?.SecretString;
}
///
- /// Asynchronously retrieves the specified SecretBinary after calling .
+ /// Asynchronously retrieves the specified SecretBinary after calling .
+ /// If both versionId and versionStage are specified, versionId takes precedence.
///
- public async Task GetSecretBinary(String secretId, CancellationToken cancellationToken = default)
+ /// The secret identifier. This can be the full ARN or the friendly name for the secret.
+ /// The version identifier.
+ /// The version stage.
+ /// The cancellation token used for the Secrets Manager API call.
+ /// The SecretBinary.
+ public async Task GetSecretBinary(String secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default)
{
SecretCacheItem secret = GetCachedSecret(secretId);
GetSecretValueResponse response = null;
- response = await secret.GetSecretValue(cancellationToken);
+ response = await secret.GetSecretValue(cancellationToken, versionId, versionStage);
return response?.SecretBinary?.ToArray();
}
@@ -137,4 +149,4 @@ public SecretCacheItem GetCachedSecret(string secretId)
return secret;
}
}
-}
\ No newline at end of file
+}
diff --git a/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs b/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs
index 3f83e64..3f86857 100755
--- a/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs
+++ b/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs
@@ -28,6 +28,7 @@ public class CacheTests
{
private const string AWSCURRENT_VERSIONID_1 = "01234567890123456789012345678901";
private const string AWSCURRENT_VERSIONID_2 = "12345678901234567890123456789012";
+ private const string AWSCURRENT_VERSIONID_3 = "23456789012345678901234567890123";
private readonly GetSecretValueResponse secretStringResponse1 = new GetSecretValueResponse
{
@@ -57,6 +58,13 @@ public class CacheTests
SecretString = "AnotherSecretValue"
};
+ private readonly GetSecretValueResponse secretStringResponse5 = new GetSecretValueResponse
+ {
+ Name = "MySecretString",
+ VersionId = AWSCURRENT_VERSIONID_3,
+ SecretString = "MySecretValue5",
+ };
+
private readonly GetSecretValueResponse binaryResponse1 = new GetSecretValueResponse
{
Name = "MyBinarySecret",
@@ -71,10 +79,18 @@ public class CacheTests
SecretBinary = new MemoryStream(Enumerable.Repeat((byte)0x30, 10).ToArray())
};
+ private readonly GetSecretValueResponse binaryResponse3 = new GetSecretValueResponse
+ {
+ Name = "MyBinarySecret",
+ VersionId = AWSCURRENT_VERSIONID_3,
+ SecretBinary = new MemoryStream(Enumerable.Repeat((byte)0x20, 10).ToArray())
+ };
+
private readonly DescribeSecretResponse describeSecretResponse1 = new DescribeSecretResponse()
{
VersionIdsToStages = new Dictionary> {
- { AWSCURRENT_VERSIONID_1, new List { "AWSCURRENT" } }
+ { AWSCURRENT_VERSIONID_1, new List { "AWSCURRENT" } },
+ { AWSCURRENT_VERSIONID_3, new List { "AWSPREVIOUS" } }
}
};
@@ -112,6 +128,38 @@ public async Task GetSecretStringTest()
Assert.Equal(first, secretStringResponse1.SecretString);
}
+ [Fact]
+ public async Task GetSecretStringVersionIdTest()
+ {
+ Mock secretsManager = new Mock(MockBehavior.Strict);
+ secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken)))
+ .ReturnsAsync(secretStringResponse1)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+ secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken)))
+ .ReturnsAsync(describeSecretResponse1)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+
+ SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object);
+ string first = await cache.GetSecretString(secretStringResponse1.Name, secretStringResponse1.VersionId);
+ Assert.Equal(first, secretStringResponse1.SecretString);
+ }
+
+ [Fact]
+ public async Task GetSecretStringVersionStageTest()
+ {
+ Mock secretsManager = new Mock(MockBehavior.Strict);
+ secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse5.Name), default(CancellationToken)))
+ .ReturnsAsync(secretStringResponse5)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+ secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse5.Name), default(CancellationToken)))
+ .ReturnsAsync(describeSecretResponse1)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+
+ SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object);
+ string first = await cache.GetSecretString(secretStringResponse5.Name, "", "AWSPREVIOUS");
+ Assert.Equal(first, secretStringResponse5.SecretString);
+ }
+
[Fact]
public async Task NoSecretStringPresentTest()
{
@@ -140,11 +188,45 @@ public async Task GetSecretBinaryTest()
.ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object);
-
+
byte[] first = await cache.GetSecretBinary(binaryResponse1.Name);
Assert.Equal(first, binaryResponse1.SecretBinary.ToArray());
}
+ [Fact]
+ public async Task GetSecretBinaryVersionIdTest()
+ {
+ Mock secretsManager = new Mock(MockBehavior.Strict);
+ secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken)))
+ .ReturnsAsync(binaryResponse1)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+ secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken)))
+ .ReturnsAsync(describeSecretResponse1)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+
+ SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object);
+
+ byte[] first = await cache.GetSecretBinary(binaryResponse1.Name, binaryResponse1.VersionId);
+ Assert.Equal(first, binaryResponse1.SecretBinary.ToArray());
+ }
+
+ [Fact]
+ public async Task GetSecretBinaryVersionStageTest()
+ {
+ Mock secretsManager = new Mock(MockBehavior.Strict);
+ secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse3.Name), default(CancellationToken)))
+ .ReturnsAsync(binaryResponse3)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+ secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse3.Name), default(CancellationToken)))
+ .ReturnsAsync(describeSecretResponse1)
+ .ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
+
+ SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object);
+
+ byte[] first = await cache.GetSecretBinary(binaryResponse3.Name, "", "AWSPREVIOUS");
+ Assert.Equal(first, binaryResponse3.SecretBinary.ToArray());
+ }
+
[Fact]
public async Task NoSecretBinaryPresentTest()
{
@@ -174,14 +256,14 @@ public async Task GetSecretBinaryMultipleTest()
.ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object);
-
+
byte[] first = null;
for (int i = 0; i < 10; i++)
{
first = await cache.GetSecretBinary(binaryResponse1.Name);
}
Assert.Equal(first, binaryResponse1.SecretBinary.ToArray());
-
+
}
[Fact]
@@ -196,11 +278,11 @@ public async Task BasicSecretCacheTest()
.ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object);
-
+
String first = await cache.GetSecretString(secretStringResponse1.Name);
String second = await cache.GetSecretString(secretStringResponse1.Name);
Assert.Equal(first, second);
-
+
}
[Fact]
@@ -280,7 +362,7 @@ public async Task BasicSecretCacheTTLRefreshTest()
.ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration { CacheItemTTL = 1000 });
-
+
String first = await cache.GetSecretString(secretStringResponse1.Name);
String second = await cache.GetSecretString(secretStringResponse1.Name);
Assert.Equal(first, second);
@@ -434,7 +516,7 @@ public async Task HookSecretCacheTest()
.ReturnsAsync(describeSecretResponse1)
.ReturnsAsync(describeSecretResponse1)
.ThrowsAsync(new AmazonSecretsManagerException("This should not be called"));
-
+
TestHook testHook = new TestHook();
SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration { CacheHook = testHook });
@@ -451,4 +533,4 @@ public async Task HookSecretCacheTest()
Assert.Equal(4, testHook.GetCount());
}
}
-}
\ No newline at end of file
+}