Skip to content

Commit 1775074

Browse files
authored
Add KeyPrefix option (#53)
1 parent 7037831 commit 1775074

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

Source/VaultSharp.Extensions.Configuration/VaultConfigurationProvider.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ private async Task<bool> LoadVaultDataAsync(IVaultClient vaultClient)
136136
var len = this.ConfigurationSource.BasePath.TrimStart('/').Length;
137137
key = key.TrimStart('/').Substring(len).TrimStart('/').Replace('/', ':');
138138
key = this.ReplaceTheAdditionalCharactersForConfigurationPath(key);
139+
140+
if (this.ConfigurationSource.Options.KeyPrefix != null)
141+
{
142+
if (string.IsNullOrEmpty(key))
143+
{
144+
key = this.ConfigurationSource.Options.KeyPrefix;
145+
}
146+
else
147+
{
148+
key = this.ConfigurationSource.Options.KeyPrefix + ":" + key;
149+
}
150+
151+
}
139152
var data = secretData.SecretData.Data;
140153

141154
var shouldSetValue = true;

Source/VaultSharp.Extensions.Configuration/VaultOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class VaultOptions
2222
/// <param name="reloadOnChange">Reload secrets if changed in Vault.</param>
2323
/// <param name="reloadCheckIntervalSeconds">Interval in seconds to check Vault for any changes.</param>
2424
/// <param name="omitVaultKeyName">Omit Vault Key Name in Configuration Keys.</param>
25+
/// <param name="keyPrefix">Store all Vault keys under this prefix </param>
2526
/// <param name="additionalCharactersForConfigurationPath">Additional characters for the Configuration path.</param>
2627
/// <param name="namespace">Vault namespace.</param>
2728
/// <param name="alwaysAddTrailingSlashToBasePath">Should a trailing slash be added to the base path. See AlwaysAddTrailingSlashToBasePath property for details </param>
@@ -35,6 +36,7 @@ public VaultOptions(
3536
bool reloadOnChange = false,
3637
int reloadCheckIntervalSeconds = 300,
3738
bool omitVaultKeyName = false,
39+
string? keyPrefix = null,
3840
IEnumerable<char>? additionalCharactersForConfigurationPath = null,
3941
string? @namespace = null,
4042
bool alwaysAddTrailingSlashToBasePath = true,
@@ -48,6 +50,7 @@ public VaultOptions(
4850
this.ReloadOnChange = reloadOnChange;
4951
this.ReloadCheckIntervalSeconds = reloadCheckIntervalSeconds;
5052
this.OmitVaultKeyName = omitVaultKeyName;
53+
this.KeyPrefix = keyPrefix;
5154
this.AdditionalCharactersForConfigurationPath = additionalCharactersForConfigurationPath ?? Array.Empty<char>();
5255
this.Namespace = @namespace;
5356
this.AlwaysAddTrailingSlashToBasePath = alwaysAddTrailingSlashToBasePath;
@@ -133,6 +136,11 @@ public VaultOptions(
133136
/// </summary>
134137
public bool OmitVaultKeyName { get; }
135138

139+
/// <summary>
140+
/// Store all read keys under this Configuration key name prefix.
141+
/// </summary>
142+
public string? KeyPrefix { get; }
143+
136144
/// <summary>
137145
/// Gets an array of characters that will be used as a path to form the Configuration.
138146
/// </summary>

Tests/VaultSharp.Extensions.Configuration.Test/IntegrationTests.cs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,97 @@ public async Task Success_SimpleTest_TokenAuth()
193193
}
194194
}
195195

196+
[Fact]
197+
public async Task Success_SimpleTestWithKeyPrefix_TokenAuth()
198+
{
199+
// arrange
200+
var values =
201+
new Dictionary<string, IEnumerable<KeyValuePair<string, object>>>
202+
{
203+
{
204+
"test", new[]
205+
{
206+
new KeyValuePair<string, object>("option1", "value1"),
207+
new KeyValuePair<string, object>("option3", 5),
208+
new KeyValuePair<string, object>("option4", true),
209+
new KeyValuePair<string, object>("option5", new[] { "v1", "v2", "v3" }),
210+
new KeyValuePair<string, object>(
211+
"option6",
212+
new[]
213+
{
214+
new TestConfigObject() {OptionA = "a1", OptionB = "b1"},
215+
new TestConfigObject() {OptionA = "a2", OptionB = "b2"},
216+
}),
217+
}
218+
},
219+
{
220+
"test/subsection", new[]
221+
{
222+
new KeyValuePair<string, object>("option2", "value2"),
223+
}
224+
},
225+
{
226+
"test/otherSubsection.otherSubsection2/otherSubsection3.otherSubsection4.otherSubsection5", new[]
227+
{
228+
new KeyValuePair<string, object>("option7", "value7"),
229+
}
230+
},
231+
{
232+
"test/subsection/testsection", new[]
233+
{
234+
new KeyValuePair<string, object>("option8", "value8"),
235+
}
236+
},
237+
};
238+
239+
var container = this.PrepareVaultContainer();
240+
try
241+
{
242+
await container.StartAsync().ConfigureAwait(false);
243+
await this.LoadDataAsync("http://localhost:8200", values).ConfigureAwait(false);
244+
245+
// act
246+
var builder = new ConfigurationBuilder();
247+
248+
var keyPrefix = "MyConfig";
249+
builder.AddVaultConfiguration(
250+
() => new VaultOptions("http://localhost:8200", "root", additionalCharactersForConfigurationPath: new[] { '.' }, keyPrefix: keyPrefix),
251+
"test",
252+
"secret",
253+
this.logger);
254+
var configurationRoot = builder.Build();
255+
256+
// assert
257+
configurationRoot.GetValue<string>($"{keyPrefix}:option1").Should().Be("value1");
258+
configurationRoot.GetValue<int>($"{keyPrefix}:option3").Should().Be(5);
259+
configurationRoot.GetValue<bool>($"{keyPrefix}:option4").Should().Be(true);
260+
configurationRoot.GetValue<string>($"{keyPrefix}:option5:0").Should().Be("v1");
261+
configurationRoot.GetValue<string>($"{keyPrefix}:option5:1").Should().Be("v2");
262+
configurationRoot.GetValue<string>($"{keyPrefix}:option5:2").Should().Be("v3");
263+
var t1 = new TestConfigObject();
264+
configurationRoot.Bind($"{keyPrefix}:option6:0", t1);
265+
t1.OptionA.Should().Be("a1");
266+
t1.OptionB.Should().Be("b1");
267+
var t2 = new TestConfigObject();
268+
configurationRoot.Bind($"{keyPrefix}:option6:1", t2);
269+
t2.OptionA.Should().Be("a2");
270+
t2.OptionB.Should().Be("b2");
271+
configurationRoot.GetSection($"{keyPrefix}:subsection").GetValue<string>("option2").Should().Be("value2");
272+
configurationRoot.GetSection($"{keyPrefix}:otherSubsection")
273+
.GetSection($"otherSubsection2")
274+
.GetSection("otherSubsection3")
275+
.GetSection("otherSubsection4")
276+
.GetSection("otherSubsection5")
277+
.GetValue<string>("option7").Should().Be("value7");
278+
configurationRoot.GetSection($"{keyPrefix}:subsection").GetSection("testsection").GetValue<string>("option8").Should().Be("value8");
279+
}
280+
finally
281+
{
282+
await container.DisposeAsync().ConfigureAwait(false);
283+
}
284+
}
285+
286+
196287
[Fact]
197288
public async Task Success_SimpleTestOmitVaultKey_TokenAuth()
198289
{
@@ -296,6 +387,70 @@ public async Task Success_WatcherTest_TokenAuth()
296387
}
297388
}
298389

390+
[Fact]
391+
public async Task Success_WatcherTestWithPrefix_TokenAuth()
392+
{
393+
// arrange
394+
using var cts = new CancellationTokenSource();
395+
396+
var values =
397+
new Dictionary<string, IEnumerable<KeyValuePair<string, object>>>
398+
{
399+
{ "test", new[] { new KeyValuePair<string, object>("option1", "value1") } },
400+
{ "test/subsection", new[] { new KeyValuePair<string, object>("option2", "value2") } },
401+
};
402+
403+
var container = this.PrepareVaultContainer();
404+
try
405+
{
406+
await container.StartAsync().ConfigureAwait(false);
407+
await this.LoadDataAsync("http://localhost:8200", values).ConfigureAwait(false);
408+
409+
var testPrefix = "MyConfig";
410+
411+
// act
412+
var builder = new ConfigurationBuilder();
413+
builder.AddVaultConfiguration(
414+
() => new VaultOptions("http://localhost:8200", "root", reloadOnChange: true, reloadCheckIntervalSeconds: 10, keyPrefix: testPrefix),
415+
"test",
416+
"secret",
417+
this.logger);
418+
var configurationRoot = builder.Build();
419+
var changeWatcher = new VaultChangeWatcher(configurationRoot, this.logger);
420+
await changeWatcher.StartAsync(cts.Token).ConfigureAwait(false);
421+
var reloadToken = configurationRoot.GetReloadToken();
422+
423+
// assert
424+
configurationRoot.GetValue<string>($"{testPrefix}:option1").Should().Be("value1");
425+
configurationRoot.GetSection($"{testPrefix}:subsection").GetValue<string>("option2").Should().Be("value2");
426+
reloadToken.HasChanged.Should().BeFalse();
427+
428+
// load new data and wait for reload
429+
values = new Dictionary<string, IEnumerable<KeyValuePair<string, object>>>
430+
{
431+
{ "test", new[] { new KeyValuePair<string, object>("option1", "value1_new") } },
432+
{ "test/subsection", new[] { new KeyValuePair<string, object>("option2", "value2_new") } },
433+
{ "test/subsection3", new[] { new KeyValuePair<string, object>("option3", "value3_new") } },
434+
{ "test/testsection", new[] { new KeyValuePair<string, object>("option4", "value4_new") } },
435+
};
436+
await this.LoadDataAsync("http://localhost:8200", values).ConfigureAwait(false);
437+
await Task.Delay(TimeSpan.FromSeconds(15), cts.Token).ConfigureAwait(true);
438+
439+
reloadToken.HasChanged.Should().BeTrue();
440+
configurationRoot.GetValue<string>($"{testPrefix}:option1").Should().Be("value1_new");
441+
configurationRoot.GetSection($"{testPrefix}:subsection").GetValue<string>("option2").Should().Be("value2_new");
442+
configurationRoot.GetSection($"{testPrefix}:subsection3").GetValue<string>("option3").Should().Be("value3_new");
443+
configurationRoot.GetSection($"{testPrefix}:testsection").GetValue<string>("option4").Should().Be("value4_new");
444+
445+
changeWatcher.Dispose();
446+
}
447+
finally
448+
{
449+
await cts.CancelAsync();
450+
await container.DisposeAsync().ConfigureAwait(false);
451+
}
452+
}
453+
299454
[Fact]
300455
public async Task Success_WatcherTest_NoChanges()
301456
{

0 commit comments

Comments
 (0)